import { useEffect, useRef, useState } from "preact/hooks"; import type { Trip } from "../../lib/types"; import type { UserLocation } from "../../lib/use-location"; import { getCategory, CATEGORIES } from "../../lib/categories"; declare const L: any; function cssVar(name: string): string { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); } 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); }); } export function MapTab({ trip, location }: { trip: Trip; location: UserLocation | null }) { const mapRef = useRef(null); const mapInstance = useRef(null); const userMarker = useRef(null); const accuracyCircle = useRef(null); const [ready, setReady] = useState(false); const [error, setError] = useState(null); const [following, setFollowing] = useState(false); const followingRef = useRef(false); useEffect(() => { loadLeaflet() .then(() => setReady(true)) .catch(e => setError(e.message)); }, []); useEffect(() => { if (!ready || !mapRef.current || mapInstance.current) return; const map = L.map(mapRef.current, { zoomControl: true, scrollWheelZoom: true, }); map.on("dragstart", () => { followingRef.current = false; setFollowing(false); }); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© OpenStreetMap', maxZoom: 18, }).addTo(map); const bounds: [number, number][] = []; trip.places.forEach(place => { const cat = getCategory(place.category); const marker = L.circleMarker([place.lat, place.lng], { radius: 7, fillColor: cat.color, color: "#fff", weight: 2, opacity: 1, fillOpacity: 0.85, }).addTo(map); const popupContent = [ `${place.name}`, `${cat.icon} ${cat.label}`, place.description ? `

${place.description}

` : "", place.phone ? `

${place.phone}

` : "", ].filter(Boolean).join("
"); marker.bindPopup(popupContent, { maxWidth: 220, className: "map-popup", }); bounds.push([place.lat, place.lng]); }); trip.days.forEach(day => { day.drives.forEach(drive => { if (drive.route && drive.route.length >= 2) { L.polyline(drive.route, { color: cssVar("--gold"), weight: 3, opacity: 0.6, dashArray: "8, 6", }).addTo(map); drive.route.forEach(pt => bounds.push(pt)); } }); }); if (bounds.length > 0) { map.fitBounds(L.latLngBounds(bounds), { padding: [30, 30] }); } else { map.setView([35.5, -120.5], 9); } mapInstance.current = map; return () => { map.remove(); mapInstance.current = null; }; }, [ready, trip]); useEffect(() => { if (!mapInstance.current || !location) return; const map = mapInstance.current; const latlng = [location.lat, location.lng] as [number, number]; if (!userMarker.current) { const infoColor = cssVar("--info"); userMarker.current = L.circleMarker(latlng, { radius: 8, fillColor: infoColor, color: "#fff", weight: 3, opacity: 1, fillOpacity: 1, className: "user-location-marker", }).addTo(map); accuracyCircle.current = L.circle(latlng, { radius: location.accuracy, fillColor: infoColor, fillOpacity: 0.1, color: infoColor, weight: 1, opacity: 0.3, }).addTo(map); } else { userMarker.current.setLatLng(latlng); accuracyCircle.current.setLatLng(latlng); accuracyCircle.current.setRadius(location.accuracy); } if (followingRef.current) { map.setView(latlng, Math.max(map.getZoom(), 12)); } }, [location]); const recenter = () => { if (!mapInstance.current || !location) return; followingRef.current = true; setFollowing(true); mapInstance.current.setView([location.lat, location.lng], 14); }; const fitAll = () => { if (!mapInstance.current) return; setFollowing(false); const bounds: [number, number][] = []; trip.places.forEach(p => bounds.push([p.lat, p.lng])); if (location) bounds.push([location.lat, location.lng]); if (bounds.length > 0) { mapInstance.current.fitBounds(L.latLngBounds(bounds), { padding: [30, 30] }); } }; if (error) return
Failed to load map: {error}
; return (
{!ready &&
Loading map...
}
{location && ( )}
{location && (
You
)} {Object.values(CATEGORIES).map(cat => (
{cat.icon} {cat.label}
))}
); }