import { useEffect, useMemo, useState } from 'react' import { Marker, Popup } from 'react-leaflet' import L from 'leaflet' import { pluginsApi, type PluginMapMarker } from '../../api/client' /** * Host-rendered overlay for the `mapMarkerProvider` plugin hook (#587). A plugin * returns bounded marker specs (coordinates + plain text + an allowlisted url); the * server range-checks + normalizes them, and this layer draws them as plain Leaflet * markers. Plugin JS NEVER runs on the map canvas — every value here is host-vetted * data, and the popup renders it as text (the url is already http/https/mailto-only). * * Mounted inside the trip map's ; fail-safe — a fetch error just yields * no extra markers, the core map is untouched. */ const TONE_COLORS: Record = { default: '#4F46E5', success: '#10b981', warn: '#f59e0b', danger: '#ef4444', } function markerIcon(tone: PluginMapMarker['tone']): L.DivIcon { const color = TONE_COLORS[tone] ?? TONE_COLORS.default return L.divIcon({ className: 'plugin-map-marker', html: ``, iconSize: [16, 16], iconAnchor: [8, 8], popupAnchor: [0, -8], }) } export function PluginMapMarkers({ tripId }: { tripId?: number | string }) { const [markers, setMarkers] = useState([]) useEffect(() => { if (tripId == null) { setMarkers([]); return } let alive = true pluginsApi.mapMarkers(tripId) .then(r => { if (alive) setMarkers(r.markers || []) }) .catch(() => { if (alive) setMarkers([]) }) // fail-safe: no extra markers return () => { alive = false } }, [tripId]) const icons = useMemo(() => { const m = new Map() for (const tone of ['default', 'success', 'warn', 'danger'] as const) m.set(tone, markerIcon(tone)) return m }, []) if (markers.length === 0) return null return ( <> {markers.map(mk => ( {(mk.label || mk.popupText || mk.url) && (
{mk.label &&
{mk.label}
} {mk.popupText &&
{mk.popupText}
} {mk.url && ( {mk.url} )}
)}
))} ) }