From e5fc6ca838401b605337da6eae9be654018bcb8e Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sun, 27 Sep 2026 13:32:24 -0400 Subject: [PATCH] map: aircraft details dock at the right edge instead of a popup The popup sat on top of the plane and hid the trail it had just drawn. Details now render in a panel docked top-right (portaled into the Leaflet container, click/scroll propagation disabled), so the map can be panned to follow the path. Clicking empty map or the same plane deselects; hover tooltip unchanged. Verified: tsc --noEmit clean (node:20 on radio-box). Co-Authored-By: Claude Opus 5.5 --- drb-frontend/components/MapView.tsx | 80 +++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/drb-frontend/components/MapView.tsx b/drb-frontend/components/MapView.tsx index 10682fd..4e9e326 100644 --- a/drb-frontend/components/MapView.tsx +++ b/drb-frontend/components/MapView.tsx @@ -1,6 +1,7 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { createPortal } from "react-dom"; import { FeatureGroup, LayersControl, @@ -11,6 +12,7 @@ import { TileLayer, Tooltip, useMap, + useMapEvents, } from "react-leaflet"; import L from "leaflet"; import type { AircraftTrack, CallRecord, IncidentRecord, NodeRecord, NodeStatus } from "@/lib/types"; @@ -158,47 +160,81 @@ function AircraftTrail({ icao, current }: { icao: string; current: AircraftTrack ); } +function Stat({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +// Details dock at the right edge instead of a popup over the plane, so the +// trail stays visible and the map can be panned to follow it. +function AircraftPanel({ a, onClose }: { a: AircraftTrack; onClose: () => void }) { + const ref = useRef(null); + useEffect(() => { + // The panel is portaled into the Leaflet container, whose native listeners + // would otherwise treat clicks/scrolls here as map clicks (deselect) or zoom. + if (!ref.current) return; + L.DomEvent.disableClickPropagation(ref.current); + L.DomEvent.disableScrollPropagation(ref.current); + }, []); + const fmt = (n: number | null, unit: string) => (n == null ? "—" : `${Math.round(n).toLocaleString()} ${unit}`); + return ( +
+
+
+
+ + {a.callsign || a.icao} +
+
ICAO {a.icao}
+
+ +
+
+ + + + +
+
+ ); +} + function AircraftLayer() { + const map = useMap(); const { aircraft } = useAircraft(); const [selected, setSelected] = useState(null); const positioned = aircraft.filter((a) => a.lat != null && a.lon != null); const selectedTrack = positioned.find((a) => a.icao === selected); + // Clicking empty map deselects; marker clicks don't reach the map. + useMapEvents({ click: () => setSelected(null) }); + return ( <> {selectedTrack && } + {selectedTrack && + createPortal( setSelected(null)} />, map.getContainer())} {positioned.map((a) => ( setSelected(a.icao), - popupclose: () => setSelected((cur) => (cur === a.icao ? null : cur)), - }} + eventHandlers={{ click: () => setSelected((cur) => (cur === a.icao ? null : a.icao)) }} > {a.callsign || a.icao} {a.altitude_ft != null && ` · ${Math.round(a.altitude_ft).toLocaleString()} ft`} - -
-
{a.callsign || a.icao}
-
ICAO {a.icao}
- {a.altitude_ft != null && ( -
- - Altitude: {Math.round(a.altitude_ft).toLocaleString()} ft -
- )} - {a.ground_speed_kt != null &&
Speed: {Math.round(a.ground_speed_kt)} kt
} - {a.track_deg != null &&
Heading: {Math.round(a.track_deg)}°
} -
-
))}