Skip to content

Commit 6b6c0cf

Browse files
committed
Add in missed file
1 parent 34dcdd4 commit 6b6c0cf

1 file changed

Lines changed: 103 additions & 0 deletions

File tree

src/components/LeafletMap.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { useEffect, useRef } from 'react'
2+
import L from 'leaflet'
3+
import 'leaflet/dist/leaflet.css'
4+
import './GoogleMap.css'
5+
import { PinIcon, EditIcon } from './icons'
6+
import type { MapProps } from './GoogleMap'
7+
8+
/**
9+
* Leaflet + OpenStreetMap alternative to GoogleMap, with the same props so it's
10+
* a drop-in swap. Like GoogleMap it uses a fixed centre pin (the map pans
11+
* beneath it) so no marker is needed. OSM raster tiles carry no clickable
12+
* features, so `onPoiSelect` isn't supported; tap-to-place, drag, zoom and edit
13+
* all work.
14+
*/
15+
export function LeafletMap({
16+
variant,
17+
coordinates,
18+
interactive = false,
19+
onCoordinatesChange,
20+
onEdit,
21+
onMapClick,
22+
}: MapProps) {
23+
const canvasRef = useRef<HTMLDivElement>(null)
24+
const mapRef = useRef<L.Map | null>(null)
25+
26+
// Keep the latest handlers in refs so the once-bound listeners aren't stale.
27+
const onCoordinatesChangeRef = useRef(onCoordinatesChange)
28+
onCoordinatesChangeRef.current = onCoordinatesChange
29+
const onMapClickRef = useRef(onMapClick)
30+
onMapClickRef.current = onMapClick
31+
32+
// Initialise the map once.
33+
useEffect(() => {
34+
if (!canvasRef.current) return
35+
const map = L.map(canvasRef.current, {
36+
center: [coordinates.lat, coordinates.lng],
37+
zoom: 16,
38+
zoomControl: interactive,
39+
dragging: interactive,
40+
scrollWheelZoom: interactive,
41+
doubleClickZoom: interactive,
42+
boxZoom: interactive,
43+
touchZoom: interactive,
44+
keyboard: interactive,
45+
attributionControl: true,
46+
})
47+
mapRef.current = map
48+
49+
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
50+
maxZoom: 19,
51+
attribution: '&copy; OpenStreetMap contributors',
52+
}).addTo(map)
53+
54+
// Leaflet needs a size recalculation once the container has laid out.
55+
setTimeout(() => map.invalidateSize(), 0)
56+
57+
if (interactive) {
58+
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
59+
map.on('moveend', () => {
60+
const c = map.getCenter()
61+
onCoordinatesChangeRef.current?.({ lat: c.lat, lng: c.lng })
62+
})
63+
map.on('click', (e: L.LeafletMouseEvent) => {
64+
if (!onMapClickRef.current) return
65+
map.setView(e.latlng, map.getZoom(), { animate: !reduceMotion })
66+
onMapClickRef.current({ lat: e.latlng.lat, lng: e.latlng.lng })
67+
})
68+
}
69+
70+
return () => {
71+
map.remove()
72+
mapRef.current = null
73+
}
74+
// eslint-disable-next-line react-hooks/exhaustive-deps
75+
}, [])
76+
77+
// Re-centre when the coordinates prop changes from outside (search, etc.),
78+
// skipping when the map is already there so we don't fight a user drag.
79+
useEffect(() => {
80+
const map = mapRef.current
81+
if (!map) return
82+
const c = map.getCenter()
83+
if (Math.abs(c.lat - coordinates.lat) < 1e-6 && Math.abs(c.lng - coordinates.lng) < 1e-6) {
84+
return
85+
}
86+
map.setView([coordinates.lat, coordinates.lng], map.getZoom(), { animate: false })
87+
}, [coordinates])
88+
89+
return (
90+
<div className={`gmap ${variant}`}>
91+
<div ref={canvasRef} className="gmap-canvas" aria-label="Venue location map (OpenStreetMap)" />
92+
<div className="gmap-pin">
93+
<PinIcon size={variant === 'full' ? 42 : 34} />
94+
</div>
95+
{variant === 'preview' && onEdit && (
96+
<button type="button" className="gmap-edit-button" onClick={onEdit}>
97+
<EditIcon />
98+
Edit map location
99+
</button>
100+
)}
101+
</div>
102+
)
103+
}

0 commit comments

Comments
 (0)