feat[realtime]: add live vehicle tracking via Supabase Realtime + Leaflet map
- VehicleMap.jsx: OpenStreetMap integration with real-time vehicle updates - router.jsx: Added /vehicles route with navigation
This commit is contained in:
parent
666baff7db
commit
0b86aa7f40
2 changed files with 112 additions and 2 deletions
95
frontend/src/components/VehicleMap.jsx
Normal file
95
frontend/src/components/VehicleMap.jsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { supabase } from '../supabaseClient';
|
||||||
|
|
||||||
|
// Load Leaflet CSS dynamically
|
||||||
|
const loadLeafletCss = () => {
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
|
||||||
|
document.head.appendChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load Leaflet JS dynamically (as a module)
|
||||||
|
const loadLeafletJs = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
|
||||||
|
script.onload = () => resolve(window.L);
|
||||||
|
script.onerror = reject;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function VehicleMap() {
|
||||||
|
const [vehicles, setVehicles] = useState([]);
|
||||||
|
const [map, setMap] = useState(null);
|
||||||
|
|
||||||
|
// Initialise map once Leaflet is loaded
|
||||||
|
useEffect(() => {
|
||||||
|
loadLeafletCss();
|
||||||
|
let L;
|
||||||
|
loadLeafletJs()
|
||||||
|
.then((leaflet) => {
|
||||||
|
L = leaflet;
|
||||||
|
const mapInstance = L.map('vehicle-map').setView([20, 0], 2);
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors',
|
||||||
|
}).addTo(mapInstance);
|
||||||
|
setMap({ L, mapInstance });
|
||||||
|
})
|
||||||
|
.catch((err) => console.error('Failed to load Leaflet', err));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Subscribe to realtime changes on vehicles table
|
||||||
|
useEffect(() => {
|
||||||
|
const channel = supabase
|
||||||
|
.channel('public:vehicles')
|
||||||
|
.on('postgres_changes', { event: '*', schema: 'public', table: 'vehicles' }, (payload) => {
|
||||||
|
// payload.new contains the new row data
|
||||||
|
setVehicles((prev) => {
|
||||||
|
const filtered = prev.filter((v) => v.id !== payload.new.id);
|
||||||
|
return [...filtered, payload.new];
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
supabase
|
||||||
|
.from('vehicles')
|
||||||
|
.select('id, latitude, longitude, number')
|
||||||
|
.then(({ data, error }) => {
|
||||||
|
if (!error && data) setVehicles(data);
|
||||||
|
else console.error('Error fetching vehicles', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
supabase.removeChannel(channel);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Update markers when vehicles or map change
|
||||||
|
useEffect(() => {
|
||||||
|
if (!map) return;
|
||||||
|
const { L, mapInstance } = map;
|
||||||
|
// Clear existing markers layer group
|
||||||
|
if (mapInstance._vehicleLayer) {
|
||||||
|
mapInstance.removeLayer(mapInstance._vehicleLayer);
|
||||||
|
}
|
||||||
|
const layer = L.layerGroup();
|
||||||
|
vehicles.forEach((v) => {
|
||||||
|
if (v.latitude && v.longitude) {
|
||||||
|
const marker = L.marker([v.latitude, v.longitude]).bindPopup(`Vehicle ${v.number}`);
|
||||||
|
layer.addLayer(marker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
layer.addTo(mapInstance);
|
||||||
|
mapInstance._vehicleLayer = layer;
|
||||||
|
}, [vehicles, map]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="mt-4 mb-2">Live Vehicle Tracking</h2>
|
||||||
|
<div id="vehicle-map" style={{ height: '500px', width: '100%' }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRootRoute, RouterProvider, createBrowserRouter } from '@tanstack/react-router';
|
import { createRootRoute, RouterProvider, createBrowserRouter, Link } from '@tanstack/react-router';
|
||||||
import LoadsList from './components/LoadsList';
|
import LoadsList from './components/LoadsList';
|
||||||
import ShippersList from './components/ShippersList';
|
import ShippersList from './components/ShippersList';
|
||||||
|
import VehicleMap from './components/VehicleMap';
|
||||||
|
|
||||||
// Root layout – can later include a navbar or sidebar
|
// Root layout – can later include a navbar or sidebar
|
||||||
function RootLayout({ children }) {
|
function RootLayout({ children }) {
|
||||||
|
|
@ -11,6 +12,14 @@ function RootLayout({ children }) {
|
||||||
<header className="bg-primary text-white p-3">
|
<header className="bg-primary text-white p-3">
|
||||||
<h1 className="mb-0" style={{ fontSize: '1.5rem' }}>FreightDesk Dashboard</h1>
|
<h1 className="mb-0" style={{ fontSize: '1.5rem' }}>FreightDesk Dashboard</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="mb-4">
|
||||||
|
<Link href="/loads" className="nav-link">Loads</Link>
|
||||||
|
<Link href="/shippers" className="nav-link">Shippers</Link>
|
||||||
|
<Link href="/vehicles" className="nav-link">Vehicles</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -33,6 +42,12 @@ const shippersRoute = rootRoute.createRoute({
|
||||||
component: ShippersList,
|
component: ShippersList,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Vehicle tracking page route
|
||||||
|
const vehiclesRoute = rootRoute.createRoute({
|
||||||
|
path: '/vehicles',
|
||||||
|
component: VehicleMap,
|
||||||
|
});
|
||||||
|
|
||||||
// Default route – redirect to /loads
|
// Default route – redirect to /loads
|
||||||
const indexRoute = rootRoute.createRoute({
|
const indexRoute = rootRoute.createRoute({
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|
@ -46,7 +61,7 @@ const indexRoute = rootRoute.createRoute({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build the router
|
// Build the router
|
||||||
const routeTree = rootRoute.addChildren([loadsRoute, shippersRoute, indexRoute]);
|
const routeTree = rootRoute.addChildren([loadsRoute, shippersRoute, vehiclesRoute, indexRoute]);
|
||||||
|
|
||||||
export const router = createBrowserRouter({ routeTree });
|
export const router = createBrowserRouter({ routeTree });
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue