import { useEffect, useRef } from 'react'; import L from 'leaflet'; import { createCachedTileLayer } from '@/lib/cachedTileLayer'; import { cn } from '@/lib/utils'; export interface LatLng { lat: number; lng: number; } interface SpotLocationPickerProps { value: LatLng | null; onChange: (coords: LatLng) => void; className?: string; } /** Fallback center before the user has a location (NYC, same as the map default). */ const DEFAULT_CENTER: [number, number] = [40.7128, -74.006]; /** * A small, tappable Leaflet map for choosing a spot's location. * * Tap anywhere to drop the pin, or drag the pin to fine-tune. This replaces the * raw latitude/longitude text boxes as the PRIMARY way to set a location — those * move behind an "enter coordinates manually" toggle for power users. Uses the * same cached tile layer as the main map, so the basemap still draws offline. */ export function SpotLocationPicker({ value, onChange, className }: SpotLocationPickerProps) { const containerRef = useRef(null); const mapRef = useRef(null); const markerRef = useRef(null); const onChangeRef = useRef(onChange); // Captured once so the init effect doesn't depend on `value` (which changes // constantly) — the marker-sync effect below handles later updates. const initialValueRef = useRef(value); useEffect(() => { onChangeRef.current = onChange; }, [onChange]); // Create the map once. useEffect(() => { const el = containerRef.current; if (!el || mapRef.current) return; const initial = initialValueRef.current; const center: [number, number] = initial ? [initial.lat, initial.lng] : DEFAULT_CENTER; const map = L.map(el, { center, zoom: initial ? 15 : 12, zoomControl: true, attributionControl: true, }); createCachedTileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '', subdomains: 'abc', minZoom: 2, maxZoom: 19, noWrap: true, className: 'skatespots-dark-tiles', }).addTo(map); map.on('click', (e: L.LeafletMouseEvent) => { onChangeRef.current({ lat: e.latlng.lat, lng: e.latlng.lng }); }); mapRef.current = map; const ro = new ResizeObserver(() => map.invalidateSize()); ro.observe(el); // Leaflet can mis-measure inside a freshly-laid-out page — nudge it once. const t = window.setTimeout(() => map.invalidateSize(), 0); return () => { window.clearTimeout(t); ro.disconnect(); map.remove(); mapRef.current = null; markerRef.current = null; }; }, []); // Draw / move the draggable pin to match `value`. useEffect(() => { const map = mapRef.current; if (!map) return; if (!value) { if (markerRef.current) { markerRef.current.remove(); markerRef.current = null; } return; } const latlng: [number, number] = [value.lat, value.lng]; if (markerRef.current) { markerRef.current.setLatLng(latlng); return; } const marker = L.marker(latlng, { draggable: true, icon: L.divIcon({ className: '', html: '
' + '
' + '
' + '
', iconSize: [22, 34], iconAnchor: [11, 34], }), }).addTo(map); marker.on('dragend', () => { const p = marker.getLatLng(); onChangeRef.current({ lat: p.lat, lng: p.lng }); }); markerRef.current = marker; map.setView(latlng, Math.max(map.getZoom(), 15), { animate: true }); }, [value]); return (
); }