import { useEffect, useRef, useState, useMemo } from "preact/hooks"; import type { StandaloneMap, MapPlace } from "../lib/types"; declare const L: any; function haversineKm(lat1: number, lng1: number, lat2: number, lng2: number): number { const R = 6371; const dLat = (lat2 - lat1) * Math.PI / 180; const dLng = (lng2 - lng1) * Math.PI / 180; const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*Math.sin(dLng/2)**2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); } function fmtDist(km: number): string { return km < 1 ? `${Math.round(km*1000)}m` : `${km.toFixed(1)}km`; } const LEAFLET_CSS = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"; const LEAFLET_JS = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"; function loadLeaflet(): Promise { if (typeof L !== "undefined") return Promise.resolve(); return new Promise((resolve, reject) => { if (!document.querySelector(`link[href="${LEAFLET_CSS}"]`)) { const link = document.createElement("link"); link.rel = "stylesheet"; link.href = LEAFLET_CSS; document.head.appendChild(link); } if (document.querySelector(`script[src="${LEAFLET_JS}"]`)) { const check = setInterval(() => { if (typeof L !== "undefined") { clearInterval(check); resolve(); } }, 50); return; } const script = document.createElement("script"); script.src = LEAFLET_JS; script.onload = () => resolve(); script.onerror = () => reject(new Error("Failed to load Leaflet")); document.head.appendChild(script); }); } function PlaceCard({ place, index, highlighted, onSelect, distKm, }: { place: MapPlace; index: number; highlighted: boolean; onSelect: () => void; distKm?: number; }) { const ref = useRef(null); useEffect(() => { if (highlighted && ref.current) { ref.current.scrollIntoView({ behavior: "smooth", block: "nearest" }); } }, [highlighted]); return (
{place.name}
{distKm !== undefined && ( {fmtDist(distKm)} )} {place.ebt && ( {place.ebt} )}
{place.city && (
{place.city}
)}
{place.address && (
Address {place.address}
)} {place.hours && (
Hours {place.hours}
)} {place.phone && ( )} {place.website && ( )}
); } export function MapView({ standaloneMap, onBack, }: { standaloneMap: StandaloneMap; onBack: () => void; }) { const mapRef = useRef(null); const mapInstance = useRef(null); const markersRef = useRef([]); const userMarker = useRef(null); const [ready, setReady] = useState(false); const [mapError, setMapError] = useState(null); const [query, setQuery] = useState(""); const [highlightedIdx, setHighlightedIdx] = useState(null); const [userLocation, setUserLocation] = useState<{ lat: number; lng: number } | null>(null); const places = standaloneMap.places; const placesWithDist = useMemo(() => { return places.map(p => ({ place: p, distKm: userLocation ? haversineKm(userLocation.lat, userLocation.lng, p.lat, p.lng) : undefined, })); }, [places, userLocation]); const sorted = useMemo(() => { if (!userLocation) return placesWithDist; return [...placesWithDist].sort((a, b) => (a.distKm ?? 0) - (b.distKm ?? 0)); }, [placesWithDist, userLocation]); const filtered = query.trim() ? sorted.filter(({ place }) => { const q = query.toLowerCase(); return ( place.name.toLowerCase().includes(q) || (place.city && place.city.toLowerCase().includes(q)) ); }) : sorted; useEffect(() => { loadLeaflet() .then(() => setReady(true)) .catch(e => setMapError(e.message)); }, []); // Auto-acquire location on mount useEffect(() => { if (!navigator.geolocation) return; navigator.geolocation.getCurrentPosition(pos => { setUserLocation({ lat: pos.coords.latitude, lng: pos.coords.longitude }); }, () => {}); }, []); useEffect(() => { if (!ready || !mapRef.current || mapInstance.current) return; const map = L.map(mapRef.current, { zoomControl: true, scrollWheelZoom: true, }); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© OpenStreetMap', maxZoom: 18, }).addTo(map); const bounds: [number, number][] = []; const markers: any[] = []; places.forEach((place, i) => { const marker = L.circleMarker([place.lat, place.lng], { radius: 7, fillColor: "#3b82f6", color: "#fff", weight: 2, opacity: 1, fillOpacity: 0.85, }).addTo(map); const popupLines = [ `${place.name}`, place.address ? `${place.address}` : "", place.hours ? `${place.hours}` : "", place.phone ? `${place.phone}` : "", place.ebt ? `${place.ebt} EBT` : "", ].filter(Boolean).join("
"); marker.bindPopup(popupLines, { maxWidth: 220, className: "map-popup" }); marker.on("click", () => { setHighlightedIdx(i); }); markers.push(marker); bounds.push([place.lat, place.lng]); }); markersRef.current = markers; if (bounds.length > 0) { map.fitBounds(L.latLngBounds(bounds), { padding: [30, 30] }); } else { map.setView([36.7, -119.7], 6); } mapInstance.current = map; return () => { map.remove(); mapInstance.current = null; markersRef.current = []; }; }, [ready]); const selectPlace = (globalIdx: number) => { setHighlightedIdx(globalIdx); const marker = markersRef.current[globalIdx]; const place = places[globalIdx]; if (mapInstance.current && marker) { mapInstance.current.setView([place.lat, place.lng], Math.max(mapInstance.current.getZoom(), 13)); marker.openPopup(); } }; const centerOnMe = () => { if (!navigator.geolocation) return; navigator.geolocation.getCurrentPosition(pos => { const lat = pos.coords.latitude; const lng = pos.coords.longitude; setUserLocation({ lat, lng }); if (mapInstance.current) { mapInstance.current.setView([lat, lng], 12); } }); }; // Sync user marker whenever location updates useEffect(() => { if (!userLocation || !mapInstance.current) return; const { lat, lng } = userLocation; if (!userMarker.current) { userMarker.current = L.circleMarker([lat, lng], { radius: 8, fillColor: "#3b82f6", color: "#fff", weight: 3, opacity: 1, fillOpacity: 1, }).addTo(mapInstance.current); } else { userMarker.current.setLatLng([lat, lng]); } }, [userLocation, ready]); return (
{/* Header */}

{standaloneMap.title}

{standaloneMap.subtitle && (
{standaloneMap.subtitle}
)}
{/* Map */}
{!ready && !mapError &&
Loading map...
} {mapError &&
Failed to load map: {mapError}
}
{/* Map controls */}
{places.length > 0 && ( )}
{/* Search */} {places.length > 0 && ( )} {/* Place list */} {places.length === 0 ? (
No places in this map yet.
) : (
{filtered.length === 0 && query && (
No places match "{query}".
)} {filtered.map(({ place, distKm }) => { const globalIdx = places.indexOf(place); return ( selectPlace(globalIdx)} distKm={distKm} /> ); })}
)}
); }