From 96c33a2434cf7fbfec4f5924adb83617381ad1eb Mon Sep 17 00:00:00 2001 From: Zaid Abdulameer Date: Tue, 16 Jun 2026 22:09:31 -0400 Subject: [PATCH] feat: add transport type filter toggle (All/Buses/LRT) on the map Add a segmented button group at the top-left of the map that filters vehicles by transport type. The filter affects vehicle markers on the map, the sidebar vehicle count, and the route list vehicle counts. When the LRT filter is active but no LRT vehicles exist, a specific message is shown instead of the generic no-vehicles message. Closes #90 --- frontend/src/App.tsx | 39 ++++++++++++++++++---- frontend/src/components/RouteListPanel.tsx | 5 +++ frontend/src/components/Sidebar.tsx | 11 ++++++ 3 files changed, 49 insertions(+), 6 deletions(-) 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} /> ) : (