import { useState, useEffect, useRef } from "react"; import L from "leaflet"; import { useTranslation } from "react-i18next"; import { useMap } from "react-leaflet"; import { poiCategories } from "~/lib/poi-categories"; import type { PoiState } from "~/lib/use-pois"; import { Z_POI_MARKER } from "~/lib/z-index"; interface PoiPanelProps { poiState: PoiState; onAddWaypoint?: (lat: number, lon: number, name?: string) => void; } export function PoiPanel({ poiState }: PoiPanelProps) { const { t } = useTranslation("planner"); const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { if (ref.current) L.DomEvent.disableClickPropagation(ref.current); }, []); const countByCategory = new Map(); for (const poi of poiState.pois) { countByCategory.set(poi.category, (countByCategory.get(poi.category) ?? 0) + 1); } return (
{open && (

{t("poi.title")}

{poiState.status === "zoom_too_low" && (

{t("poi.zoomIn")}

)} {poiState.status === "rate_limited" && (

{t("poi.rateLimited")}

)} {poiState.status === "error" && (

{t("poi.error")}

)} {poiState.status === "loading" && (

{t("poi.loading")}

)}
{poiCategories.map((cat) => { const count = countByCategory.get(cat.id) ?? 0; const enabled = poiState.enabledCategories.includes(cat.id); return ( ); })}
)}
); } export function PoiMarkers({ poiState, onAddWaypoint }: PoiPanelProps) { const map = useMap(); const layerRef = useRef(L.layerGroup()); useEffect(() => { layerRef.current.addTo(map); return () => { layerRef.current.remove(); }; }, [map]); // Event delegation for "Add as waypoint" buttons in popups useEffect(() => { const container = map.getContainer(); const handler = (e: MouseEvent) => { const btn = (e.target as HTMLElement).closest(".poi-add-wp") as HTMLElement | null; if (!btn || !onAddWaypoint) return; const lat = parseFloat(btn.dataset.lat!); const lon = parseFloat(btn.dataset.lon!); const name = btn.dataset.name || undefined; onAddWaypoint(lat, lon, name); map.closePopup(); }; container.addEventListener("click", handler); return () => container.removeEventListener("click", handler); }, [map, onAddWaypoint]); useEffect(() => { const group = layerRef.current; group.clearLayers(); const catMap = new Map(poiCategories.map((c) => [c.id, c])); for (const poi of poiState.pois) { const cat = catMap.get(poi.category); if (!cat) continue; const marker = L.marker([poi.lat, poi.lon], { icon: L.divIcon({ className: "", html: `
${cat.icon}
`, iconSize: [0, 0], }), zIndexOffset: Z_POI_MARKER, }); const popupLines = [`${poi.name ?? cat.icon + " " + poi.category}`]; popupLines.push(`${cat.icon} ${cat.id.replace(/_/g, " ")}`); if (poi.tags.description) popupLines.push(`${poi.tags.description}`); const addr = [poi.tags["addr:street"], poi.tags["addr:housenumber"]].filter(Boolean).join(" "); const addrCity = [addr, poi.tags["addr:postcode"], poi.tags["addr:city"]].filter(Boolean).join(", "); if (addrCity) popupLines.push(`📍 ${addrCity}`); const phone = poi.tags.phone ?? poi.tags["contact:phone"]; if (phone) popupLines.push(`📞 ${phone}`); if (poi.tags.opening_hours) popupLines.push(`🕐 ${poi.tags.opening_hours}`); if (poi.tags.website) popupLines.push(`Website`); popupLines.push(`OSM`); if (onAddWaypoint) { popupLines.push(``); } marker.bindPopup(popupLines.join("
"), { maxWidth: 200 }); group.addLayer(marker); } }, [poiState.pois]); return null; }