"use client" import { useEffect, useRef } from "react" import type * as Leaflet from "leaflet" import "leaflet/dist/leaflet.css" import "./property-map.css" export type MapMarker = { id: string name: string lat: number lng: number subtitle?: string href?: string } // Indigo pin as an inline SVG divIcon — avoids Leaflet's default marker image // assets (which break under bundlers) and matches the app accent. const PIN_SVG = `` function escapeHtml(s: string): string { return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] as string ) } /** * Leaflet map (OpenStreetMap tiles) rendering property markers. Client-only: * Leaflet touches `window`, so it's dynamically imported inside an effect. */ export function PropertyMap({ markers, className = "h-72 w-full", zoom = 15, }: { markers: MapMarker[] className?: string zoom?: number }) { const containerRef = useRef(null) const mapRef = useRef(null) useEffect(() => { let cancelled = false void (async () => { const mod = await import("leaflet") const L = ((mod as unknown as { default?: typeof Leaflet }).default ?? mod) as typeof Leaflet if (cancelled || !containerRef.current || mapRef.current) return const map = L.map(containerRef.current, { scrollWheelZoom: false, zoomControl: true, }) mapRef.current = map L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© OpenStreetMap contributors', maxZoom: 19, }).addTo(map) const icon = L.divIcon({ html: PIN_SVG, className: "pmn-map-pin", iconSize: [28, 28], iconAnchor: [14, 28], popupAnchor: [0, -26], }) const latlngs: [number, number][] = [] for (const m of markers) { if (!Number.isFinite(m.lat) || !Number.isFinite(m.lng)) continue const marker = L.marker([m.lat, m.lng], { icon }).addTo(map) const link = m.href ? `
View details →
` : "" const sub = m.subtitle ? `
${escapeHtml(m.subtitle)}
` : "" marker.bindPopup(`
${escapeHtml(m.name)}
${sub}${link}`) latlngs.push([m.lat, m.lng]) } if (latlngs.length === 1) { map.setView(latlngs[0], zoom) } else if (latlngs.length > 1) { map.fitBounds(latlngs, { padding: [40, 40] }) } else { map.setView([39.8283, -98.5795], 4) // continental US fallback } // Tiles can render blank if the container sized after init. setTimeout(() => map.invalidateSize(), 0) })() return () => { cancelled = true mapRef.current?.remove() mapRef.current = null } }, [markers, zoom]) return
}