@ -1,8 +1,10 @@
import { DivIcon , LatLng } from "leaflet" ;
import { MapPinIcon } from "lucide-react" ;
import { useEffect , useRef , useState } from "react" ;
import L , { DivIcon , LatLng } from "leaflet" ;
import { ExternalLinkIcon , MapPinIcon , MinusIcon , PlusIcon } from "lucide-react" ;
import { type ReactNode , useEffect , useRef , useState } from "react" ;
import { createRoot } from "react-dom/client" ;
import ReactDOMServer from "react-dom/server" ;
import { MapContainer , Marker , TileLayer , useMap , useMapEvents } from "react-leaflet" ;
import { cn } from "@/lib/utils" ;
const markerIcon = new DivIcon ( {
className : "relative border-none" ,
@ -54,6 +56,146 @@ const LocationMarker = (props: MarkerProps) => {
return position === undefined ? null : < Marker position = { position } icon = { markerIcon } > < / Marker > ;
} ;
// Reusable glass-style button component
interface GlassButtonProps {
icon : ReactNode ;
onClick : ( ) = > void ;
ariaLabel : string ;
title : string ;
}
const GlassButton = ( { icon , onClick , ariaLabel , title } : GlassButtonProps ) = > {
return (
< button
type = "button"
onClick = { onClick }
aria - label = { ariaLabel }
title = { title }
className = { cn (
"w-8 h-8 flex items-center justify-center rounded-lg" ,
"transition-all duration-200 cursor-pointer" ,
"bg-white/80 backdrop-blur-md border border-white/30 shadow-lg" ,
"hover:bg-white/90 hover:scale-105 active:scale-95" ,
"focus:outline-none focus:ring-2 focus:ring-blue-500" ,
) }
>
{ icon }
< / button >
) ;
} ;
// 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 (
< div className = "flex flex-col gap-1.5" >
{ position && (
< GlassButton
icon = { < ExternalLinkIcon size = { 16 } className = "text-foreground" / > }
onClick = { onOpenGoogleMaps }
ariaLabel = "Open location in Google Maps"
title = "Open in Google Maps"
/ >
) }
< GlassButton icon = { < PlusIcon size = { 16 } className = "text-foreground" / > } onClick = { onZoomIn } ariaLabel = "Zoom in" title = "Zoom in" / >
< GlassButton icon = { < MinusIcon size = { 16 } className = "text-foreground" / > } onClick = { onZoomOut } ariaLabel = "Zoom out" title = "Zoom out" / >
< / div >
) ;
} ;
// Custom Leaflet Control class
class MapControlsContainer extends L . Control {
private container : HTMLDivElement | null = null ;
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 = null ;
}
getContainer() {
return this . container ;
}
}
interface MapControlsProps {
position : LatLng | undefined ;
}
const MapControls = ( { position } : MapControlsProps ) = > {
const map = useMap ( ) ;
const controlRef = useRef < MapControlsContainer | null > ( null ) ;
const rootRef = useRef < ReturnType < typeof createRoot > | 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 (
< ControlButtons position = { position } onZoomIn = { handleZoomIn } onZoomOut = { handleZoomOut } onOpenGoogleMaps = { handleOpenInGoogleMaps } / > ,
) ;
}
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 (
< ControlButtons position = { position } onZoomIn = { handleZoomIn } onZoomOut = { handleZoomOut } onOpenGoogleMaps = { handleOpenInGoogleMaps } / > ,
) ;
}
} , [ position ] ) ;
return null ;
} ;
const MapCleanup = ( ) = > {
const map = useMap ( ) ;
@ -86,9 +228,10 @@ const DEFAULT_CENTER_LAT_LNG = new LatLng(48.8584, 2.2945);
const LeafletMap = ( props : MapProps ) = > {
const position = props . latlng || DEFAULT_CENTER_LAT_LNG ;
return (
< MapContainer className = "w-full h-72" center = { position } zoom = { 13 } scrollWheelZoom = { false } >
< MapContainer className = "w-full h-72" center = { position } zoom = { 13 } scrollWheelZoom = { false } zoomControl = { false } >
< TileLayer url = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" / >
< LocationMarker position = { position } readonly = { props . readonly } onChange = { props . onChange ? props . onChange : ( ) = > { } } / >
< MapControls position = { props . latlng } / >
< MapCleanup / >
< / MapContainer >
) ;