diff --git a/frontend/src/__tests__/App.test.tsx b/frontend/src/__tests__/App.test.tsx
index d5169c9..8421d26 100644
--- a/frontend/src/__tests__/App.test.tsx
+++ b/frontend/src/__tests__/App.test.tsx
@@ -13,7 +13,7 @@ vi.mock("react-leaflet", () => ({
),
TileLayer: () =>
,
Polyline: () => ,
- useMap: () => ({}),
+ useMap: () => ({ on: vi.fn(), off: vi.fn() }),
}));
// Also mock the sub-components that depend on Leaflet
diff --git a/frontend/src/components/MapView.tsx b/frontend/src/components/MapView.tsx
index 6b7c3a3..ff1813b 100644
--- a/frontend/src/components/MapView.tsx
+++ b/frontend/src/components/MapView.tsx
@@ -1,7 +1,10 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { MapContainer, Polyline, TileLayer } from "react-leaflet";
+import { MapContainer, Marker, Polyline, Popup, TileLayer, useMap } from "react-leaflet";
import L from "leaflet";
import type { Route, Stop, VehicleArrivalStop, VehiclePosition } from "../api/types";
+import { buildStopMarkerHtml } from "./StopMarker";
+import { useStopDepartures } from "../hooks/useStopDepartures";
+import StopDeparturesPanel from "./StopDeparturesPanel";
import MapBindings from "./MapBindings";
import SelectedVehicleMarker from "./SelectedVehicleMarker";
import VehicleMarker from "./VehicleMarker";
@@ -33,6 +36,49 @@ interface MapViewProps {
const defaultCenter: [number, number] = [43.4516, -80.4925];
const defaultZoom = 12;
+/** Renders upcoming departures inside a Leaflet popup. */
+function DeparturePopupContent({ stopId }: { stopId: string }) {
+ const { data: departures, isLoading } = useStopDepartures(stopId);
+ return (
+
+
+
+ );
+}
+
+/**
+ * Listens for clicks on the map background (not on markers) and dismisses
+ * the currently open stop popup by toggling off the selection.
+ */
+function MapClickHandler({
+ selectedStopId,
+ onSelectStop,
+}: {
+ selectedStopId: string | null;
+ onSelectStop: (stopId: string) => void;
+}) {
+ const map = useMap();
+ const selectedStopIdRef = useRef(selectedStopId);
+ selectedStopIdRef.current = selectedStopId;
+ const onSelectStopRef = useRef(onSelectStop);
+ onSelectStopRef.current = onSelectStop;
+
+ useEffect(() => {
+ const handleClick = () => {
+ const current = selectedStopIdRef.current;
+ if (current) {
+ onSelectStopRef.current(current);
+ }
+ };
+ map.on("click", handleClick);
+ return () => {
+ map.off("click", handleClick);
+ };
+ }, [map]);
+
+ return null;
+}
+
const buildRouteIndex = (routes: Route[]) => {
const map = new Map();
routes.forEach((route) => map.set(route.id, route));
@@ -180,6 +226,17 @@ export default function MapView({
focusedStopIndexRef.current = _focusedStopIndex;
const nearbyStopsCurrent = nearbyStops;
+ const markerRefs = useRef