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 { 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 });
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue