Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<string | null>(null);
const [mapCenter, setMapCenter] = useState<[number, number] | null>(null);
const [selectedStopId, setSelectedStopId] = useState<string | null>(null);
Expand Down Expand Up @@ -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 (
<div className="flex h-screen">
{/* Sidebar */}
<Sidebar
routes={routes}
positions={positions}
positions={filteredPositions}
loading={loading}
onSelectRoute={setSelectedRouteId}
selectedRouteId={selectedRouteId}
Expand All @@ -110,6 +120,23 @@ export default function App() {

{/* Main Content */}
<div className="flex-1 relative min-w-0 lg:ml-72">
{/* Transport filter toggle */}
<div className="absolute top-3 left-3 z-20 flex rounded-full bg-white/80 shadow">
{(["all", "bus", "lrt"] as const).map((mode) => (
<button
key={mode}
onClick={() => setTransportFilter(mode)}
className={`px-3 py-1.5 text-xs font-medium rounded-full transition-colors ${
transportFilter === mode
? "bg-blue-600 text-white shadow-sm"
: "text-gray-600 hover:text-gray-900"
}`}
>
{mode === "all" ? "All" : mode === "bus" ? "Buses" : "LRT"}
</button>
))}
</div>

{/* Refresh button */}
<button
onClick={handleRefresh}
Expand Down Expand Up @@ -183,13 +210,13 @@ export default function App() {
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>No buses currently running on the tracked routes.</span>
<span>{noVehiclesMessage}</span>
</p>
</div>
</div>
)}
<MapView
positions={positions}
positions={filteredPositions}
routes={routes}
selectedRouteId={selectedRouteId}
onSelectRoute={setSelectedRouteId}
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/RouteListPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ interface RouteListPanelProps {
routes: Route[];
selectedRouteId: string | null;
onSelectRoute: (routeId: string | null) => void;
vehicleCountsByRoute: Record<string, number>;
}

export default function RouteListPanel({
routes,
selectedRouteId,
onSelectRoute,
vehicleCountsByRoute,
}: RouteListPanelProps) {
const [search, setSearch] = useState("");

Expand Down Expand Up @@ -65,6 +67,9 @@ export default function RouteListPanel({
{route.longName && (
<span className="text-gray-500 truncate">{route.longName}</span>
)}
<span className="ml-auto text-xs text-gray-400 tabular-nums">
{vehicleCountsByRoute[route.id] ?? 0}
</span>
</button>
);
})}
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ export default function Sidebar({

const [activeTab, setActiveTab] = useState<Tab>("stops");

const vehicleCountsByRoute = useMemo(() => {
const counts: Record<string, number> = {};
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);
Expand Down Expand Up @@ -158,6 +168,7 @@ export default function Sidebar({
routes={routes}
selectedRouteId={selectedRouteId}
onSelectRoute={onSelectRoute}
vehicleCountsByRoute={vehicleCountsByRoute}
/>
) : (
<AlertsPanel alerts={activeAlerts} isLoading={alertsLoading} />
Expand Down
Loading