import L, { LatLng } from "leaflet"; import { ExternalLinkIcon, MinusIcon, PlusIcon } from "lucide-react"; import { type ReactNode, useEffect, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import { MapContainer, Marker, useMap, useMapEvents } from "react-leaflet"; import { cn } from "@/lib/utils"; import { defaultMarkerIcon, ThemedTileLayer } from "./map-utils"; interface MarkerProps { position: LatLng | undefined; onChange: (position: LatLng) => void; readonly?: boolean; } const LocationMarker = (props: MarkerProps) => { const [position, setPosition] = useState(props.position); const initializedRef = useRef(false); const map = useMapEvents({ click(e) { if (props.readonly) { return; } setPosition(e.latlng); map.locate(); // Call the parent onChange function. props.onChange(e.latlng); }, locationfound() { }, }); useEffect(() => { if (!initializedRef.current) { map.locate(); initializedRef.current = true; } }, [map]); // Keep marker and map in sync with external position updates useEffect(() => { if (props.position) { setPosition(props.position); map.setView(props.position); } else { setPosition(undefined); } }, [props.position, map]); return position === undefined ? null : ; }; // Reusable glass-style button component interface GlassButtonProps { icon: ReactNode; onClick: () => void; ariaLabel: string; title: string; } const GlassButton = ({ icon, onClick, ariaLabel, title }: GlassButtonProps) => { return ( ); }; // Container for all map control buttons interface ControlButtonsProps { position: LatLng | undefined; onZoomIn: () => void; onZoomOut: () => void; onOpenGoogleMaps: () => void; } const ControlButtons = ({ position, onZoomIn, onZoomOut, onOpenGoogleMaps }: ControlButtonsProps) => { return (
{position && ( } onClick={onOpenGoogleMaps} ariaLabel="Open location in Google Maps" title="Open in Google Maps" /> )} } onClick={onZoomIn} ariaLabel="Zoom in" title="Zoom in" /> } onClick={onZoomOut} ariaLabel="Zoom out" title="Zoom out" />
); }; // Custom Leaflet Control class class MapControlsContainer extends L.Control { private container: HTMLDivElement | undefined = undefined; onAdd() { this.container = L.DomUtil.create("div", ""); this.container.style.pointerEvents = "auto"; // Prevent map interactions when clicking controls L.DomEvent.disableClickPropagation(this.container); L.DomEvent.disableScrollPropagation(this.container); return this.container; } onRemove() { this.container = undefined; } getContainer() { return this.container; } } interface MapControlsProps { position: LatLng | undefined; } const MapControls = ({ position }: MapControlsProps) => { const map = useMap(); const controlRef = useRef(null); const rootRef = useRef | null>(null); const handleOpenInGoogleMaps = () => { if (!position) return; const url = `https://www.google.com/maps?q=${position.lat},${position.lng}`; window.open(url, "_blank", "noopener,noreferrer"); }; const handleZoomIn = () => { map.zoomIn(); }; const handleZoomOut = () => { map.zoomOut(); }; useEffect(() => { // Create custom Leaflet control const control = new MapControlsContainer({ position: "topright" }); controlRef.current = control; control.addTo(map); // Get container and render React component into it const container = control.getContainer(); if (container) { rootRef.current = createRoot(container); rootRef.current.render( , ); } return () => { // Cleanup: unmount React component and remove control if (rootRef.current) { rootRef.current.unmount(); rootRef.current = null; } if (controlRef.current) { controlRef.current.remove(); controlRef.current = null; } }; }, [map]); // Update rendered content when position changes useEffect(() => { if (rootRef.current) { rootRef.current.render( , ); } }, [position]); return null; }; const MapCleanup = () => { const map = useMap(); useEffect(() => { return () => { // Cleanup map instance when component unmounts setTimeout(() => { if (map) { try { map.remove(); } catch { // Ignore errors during cleanup } } }, 0); }; }, [map]); return null; }; interface MapProps { readonly?: boolean; latlng?: LatLng; onChange?: (position: LatLng) => void; } const DEFAULT_CENTER_LAT_LNG = new LatLng(48.8584, 2.2945); const LeafletMap = (props: MapProps) => { const position = props.latlng || DEFAULT_CENTER_LAT_LNG; return ( { }} /> ); }; export default LeafletMap;