/** * Oveyo PWA — Page Carte (Leaflet réelle + filtres produits + liste boutiques) * V3.2 : boutiques cliquables (liste + markers) → ouvre ShopDetailSheet */ const { useState, useEffect, useRef } = React; const Lu2 = window.LucideReact || {}; const { Card: CardC, IconBubble: IB, Spinner: Sp, EmptyState: ES } = window.OveyoUI; const { useShops: uShops, useCatalog: uCatalog } = window.OveyoState; const MapPage = ({ state, setActiveTab, onOpenSheet }) => { const { Store, Check, ShieldCheck, Clock, MapPin, Filter, ChevronRight } = Lu2; const { products } = uCatalog(); const [selectedProduct, setSelectedProduct] = useState('sucre'); const [neighborhoodFilter, setNeighborhoodFilter] = useState(state.user.neighborhood_slug || ''); const { shops, loading } = uShops(selectedProduct, neighborhoodFilter); const mapRef = useRef(null); const mapInstance = useRef(null); const markersGroup = useRef(null); // ref vers la dernière callback onOpenSheet — pour qu'elle soit accessible // depuis l'event handler des markers Leaflet sans recréer toute la carte const onOpenSheetRef = useRef(onOpenSheet); useEffect(() => { onOpenSheetRef.current = onOpenSheet; }, [onOpenSheet]); // Init Leaflet map (une seule fois) useEffect(() => { if (!mapRef.current || mapInstance.current) return; if (typeof L === 'undefined') return; const map = L.map(mapRef.current, { center: [14.7359, -17.4631], zoom: 13, zoomControl: false, attributionControl: false, }); L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors, © CARTO', }).addTo(map); L.control.zoom({ position: 'topright' }).addTo(map); L.control.attribution({ position: 'bottomleft', prefix: false }).addTo(map); mapInstance.current = map; markersGroup.current = L.layerGroup().addTo(map); // Géolocaliser l'utilisateur if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (pos) => { map.setView([pos.coords.latitude, pos.coords.longitude], 14); L.circleMarker([pos.coords.latitude, pos.coords.longitude], { radius: 8, fillColor: '#F4632B', color: 'white', weight: 3, fillOpacity: 1, }).addTo(map).bindPopup('Tu es ici'); }, () => {}, { timeout: 5000 } ); } return () => { map.remove(); mapInstance.current = null; }; }, []); // Mettre à jour les marqueurs quand les boutiques changent useEffect(() => { if (!mapInstance.current || !markersGroup.current) return; markersGroup.current.clearLayers(); shops.forEach((s) => { if (!s.lat || !s.lng) return; const color = s.is_abuse ? '#F4632B' : s.stock === 'rupture' ? '#6B7891' : '#102A43'; const label = s.price ? `${s.price} F` : '—'; const icon = L.divIcon({ className: 'price-marker', html: `