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:
Hermes Agent 2026-06-08 01:17:36 +00:00
parent 666baff7db
commit 0b86aa7f40
2 changed files with 112 additions and 2 deletions

View 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>
);
}

View file

@ -1,7 +1,8 @@
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 ShippersList from './components/ShippersList';
import VehicleMap from './components/VehicleMap';
// Root layout can later include a navbar or sidebar
function RootLayout({ children }) {
@ -11,6 +12,14 @@ function RootLayout({ children }) {
<header className="bg-primary text-white p-3">
<h1 className="mb-0" style={{ fontSize: '1.5rem' }}>FreightDesk Dashboard</h1>
</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>
</div>
);
@ -33,6 +42,12 @@ const shippersRoute = rootRoute.createRoute({
component: ShippersList,
});
// Vehicle tracking page route
const vehiclesRoute = rootRoute.createRoute({
path: '/vehicles',
component: VehicleMap,
});
// Default route redirect to /loads
const indexRoute = rootRoute.createRoute({
path: '/',
@ -46,7 +61,7 @@ const indexRoute = rootRoute.createRoute({
});
// Build the router
const routeTree = rootRoute.addChildren([loadsRoute, shippersRoute, indexRoute]);
const routeTree = rootRoute.addChildren([loadsRoute, shippersRoute, vehiclesRoute, indexRoute]);
export const router = createBrowserRouter({ routeTree });