diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 30092d2..1b276c0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { apiPost } from "./api/client"; import MapView from "./components/MapView"; @@ -30,6 +30,7 @@ export default function App() { const positionsQuery = useVehiclePositions(); const [sidebarOpen, setSidebarOpen] = useState(true); + const [transportFilter, setTransportFilter] = useState<"all" | "bus" | "lrt">("all"); const [selectedRouteId, setSelectedRouteId] = useState(null); const [mapCenter, setMapCenter] = useState<[number, number] | null>(null); const [selectedStopId, setSelectedStopId] = useState(null); @@ -73,22 +74,31 @@ export default function App() { }, [queryClient]); const routes = routesQuery.data ?? []; - const positions = positionsQuery.data?.positions ?? []; const cacheStatus: CacheStatus | undefined = positionsQuery.data?.cacheStatus; + const filteredPositions = useMemo(() => { + const allPositions = positionsQuery.data?.positions ?? []; + if (transportFilter === "all") return allPositions; + return allPositions.filter((p) => p.transportType === transportFilter); + }, [positionsQuery.data, transportFilter]); + const loading = routesQuery.isLoading || positionsQuery.isLoading; const error = routesQuery.error ?? positionsQuery.error; const stale = !loading && cacheStatus?.stale; - const hasVehicles = positions.length > 0; + const hasVehicles = filteredPositions.length > 0; const hasRoutes = routes.length > 0; const showNoVehiclesMessage = !loading && !error && hasRoutes && !hasVehicles && !stale; + const noVehiclesMessage = + transportFilter === "lrt" + ? "No LRT vehicles currently active." + : "No buses currently running on the tracked routes."; return (
{/* Sidebar */} + {/* Transport filter toggle */} +
+ {(["all", "bus", "lrt"] as const).map((mode) => ( + + ))} +
+ {/* Refresh button */}
)} void; + vehicleCountsByRoute: Record; } export default function RouteListPanel({ routes, selectedRouteId, onSelectRoute, + vehicleCountsByRoute, }: RouteListPanelProps) { const [search, setSearch] = useState(""); @@ -65,6 +67,9 @@ export default function RouteListPanel({ {route.longName && ( {route.longName} )} + + {vehicleCountsByRoute[route.id] ?? 0} + ); })} diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 7b6504d..2cf5363 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -39,6 +39,16 @@ export default function Sidebar({ const [activeTab, setActiveTab] = useState("stops"); + const vehicleCountsByRoute = useMemo(() => { + const counts: Record = {}; + positions.forEach((p) => { + if (p.routeId) { + counts[p.routeId] = (counts[p.routeId] ?? 0) + 1; + } + }); + return counts; + }, [positions]); + const selectedVehicleRoute = useMemo(() => { if (!selectedVehicleId) return undefined; const vehicle = positions.find((p) => p.id === selectedVehicleId); @@ -158,6 +168,7 @@ export default function Sidebar({ routes={routes} selectedRouteId={selectedRouteId} onSelectRoute={onSelectRoute} + vehicleCountsByRoute={vehicleCountsByRoute} /> ) : (