sahel-api / website /src /components /LocationMapPicker.jsx
Mohamed-20's picture
Deploy: backend, website, dashboard
4d65452
import { useEffect, useRef, useState, useCallback } from 'react';
const DEFAULT_CENTER = { lat: 33.5731, lng: -7.5898 };
const LEAFLET_CSS = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
const LEAFLET_JS = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
function loadLeaflet() {
if (window.L) return Promise.resolve(window.L);
return new Promise((resolve, reject) => {
if (!document.querySelector(`link[href="${LEAFLET_CSS}"]`)) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = LEAFLET_CSS;
document.head.appendChild(link);
}
const existing = document.querySelector(`script[src="${LEAFLET_JS}"]`);
if (existing) {
existing.addEventListener('load', () => resolve(window.L));
existing.addEventListener('error', reject);
return;
}
const script = document.createElement('script');
script.src = LEAFLET_JS;
script.async = true;
script.onload = () => resolve(window.L);
script.onerror = reject;
document.body.appendChild(script);
});
}
async function geocodeQuery(query) {
const q = query.trim();
if (!q) return null;
const url = `https://nominatim.openstreetmap.org/search?format=json&limit=1&q=${encodeURIComponent(q)}`;
const res = await fetch(url, { headers: { 'Accept-Language': 'fr' } });
if (!res.ok) return null;
const data = await res.json();
if (!data?.[0]) return null;
return {
lat: parseFloat(data[0].lat),
lng: parseFloat(data[0].lon),
displayName: data[0].display_name,
};
}
export default function LocationMapPicker({
latitude,
longitude,
searchQuery,
onLocationChange,
className = '',
}) {
const containerRef = useRef(null);
const mapRef = useRef(null);
const markerRef = useRef(null);
const [searching, setSearching] = useState(false);
const [hint, setHint] = useState('Cliquez sur la carte pour placer votre commerce.');
const lat = parseFloat(latitude);
const lng = parseFloat(longitude);
const hasCoords = Number.isFinite(lat) && Number.isFinite(lng);
const setMarker = useCallback(
(L, map, latLng, fly = true) => {
if (markerRef.current) {
markerRef.current.setLatLng(latLng);
} else {
markerRef.current = L.marker(latLng, { draggable: true }).addTo(map);
markerRef.current.on('dragend', () => {
const pos = markerRef.current.getLatLng();
onLocationChange(pos.lat, pos.lng);
setHint(`Position : ${pos.lat.toFixed(5)}, ${pos.lng.toFixed(5)}`);
});
}
if (fly) map.flyTo(latLng, Math.max(map.getZoom(), 15), { duration: 0.6 });
onLocationChange(latLng[0], latLng[1]);
setHint(`Position : ${latLng[0].toFixed(5)}, ${latLng[1].toFixed(5)}`);
},
[onLocationChange],
);
useEffect(() => {
let cancelled = false;
loadLeaflet()
.then((L) => {
if (cancelled || !containerRef.current || mapRef.current) return;
const start = hasCoords ? [lat, lng] : [DEFAULT_CENTER.lat, DEFAULT_CENTER.lng];
const map = L.map(containerRef.current, { scrollWheelZoom: true }).setView(start, hasCoords ? 16 : 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap',
maxZoom: 19,
}).addTo(map);
map.on('click', (e) => {
setMarker(L, map, [e.latlng.lat, e.latlng.lng], false);
});
if (hasCoords) {
setMarker(L, map, [lat, lng], false);
}
mapRef.current = map;
})
.catch(() => {
setHint('Carte indisponible — utilisez la recherche ci-dessous.');
});
return () => {
cancelled = true;
if (mapRef.current) {
mapRef.current.remove();
mapRef.current = null;
markerRef.current = null;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- init once
}, []);
useEffect(() => {
if (!mapRef.current || !window.L || !hasCoords) return;
const L = window.L;
setMarker(L, mapRef.current, [lat, lng], true);
}, [lat, lng, hasCoords, setMarker]);
const runSearch = async (query) => {
const text = (query ?? searchQuery ?? '').trim();
if (!text || !mapRef.current || !window.L) return;
setSearching(true);
try {
const result = await geocodeQuery(text);
if (result) {
setMarker(window.L, mapRef.current, [result.lat, result.lng]);
onLocationChange(result.lat, result.lng, result.displayName);
} else {
setHint('Adresse introuvable. Essayez une recherche plus précise.');
}
} finally {
setSearching(false);
}
};
return (
<div className={`flex flex-col gap-sm ${className}`}>
<div className="flex flex-col sm:flex-row gap-xs">
<input
type="text"
className="flex-grow bg-background border border-hairline-border rounded px-sm py-xs font-body-md focus:ring-1 focus:ring-primary focus:border-primary outline-none"
placeholder="Rechercher une adresse ou un lieu..."
defaultValue={searchQuery}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
runSearch(e.target.value);
}
}}
id="map-search-input"
/>
<button
type="button"
disabled={searching}
className="px-md py-xs bg-primary text-on-primary rounded font-label-md text-label-md border-0 cursor-pointer hover:opacity-90 disabled:opacity-60 whitespace-nowrap"
onClick={() => {
const input = document.getElementById('map-search-input');
runSearch(input?.value);
}}
>
{searching ? 'Recherche...' : 'Explorer'}
</button>
</div>
<p className="font-label-sm text-label-sm text-on-surface-variant m-0">{hint}</p>
<div
ref={containerRef}
className="w-full h-[280px] rounded-lg border border-hairline-border z-0"
aria-label="Carte interactive — cliquez pour choisir l'emplacement"
/>
</div>
);
}