import React, { useEffect, useRef } from 'react'; import { Box } from '@chakra-ui/react'; // Dynamically load Leaflet let L: any = null; interface ContactMapProps { latitude: number; longitude: number; zoom?: number; address?: string; clubName?: string; mapStyle?: string; height?: number; clubPrimaryColor?: string; clubSecondaryColor?: string; } // Available map styles export const MAP_STYLES = { // Clean & Minimal 'positron': { name: 'Positron (Light)', url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', attribution: '© OpenStreetMap © CartoDB', description: 'Clean light map, perfect for overlays' }, 'positron-no-labels': { name: 'Positron No Labels', url: 'https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', attribution: '© OpenStreetMap © CartoDB', description: 'Minimal light map without labels' }, // Dark Themes 'dark': { name: 'Dark Matter', url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', attribution: '© OpenStreetMap © CartoDB', description: 'Dark theme, great for night mode' }, 'dark-no-labels': { name: 'Dark No Labels', url: 'https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', attribution: '© OpenStreetMap © CartoDB', description: 'Dark map without labels' }, // Black & White 'toner': { name: 'Toner (B&W)', url: 'https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}{r}.png', attribution: '© Stamen Design © OpenStreetMap', description: 'High contrast black and white' }, 'toner-lite': { name: 'Toner Lite (B&W)', url: 'https://tiles.stadiamaps.com/tiles/stamen_toner_lite/{z}/{x}/{y}{r}.png', attribution: '© Stamen Design © OpenStreetMap', description: 'Subtle black and white' }, // Colorful Options 'voyager': { name: 'Voyager', url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', attribution: '© OpenStreetMap © CartoDB', description: 'Balanced colors, good readability' }, 'terrain': { name: 'Terrain', url: 'https://tiles.stadiamaps.com/tiles/stamen_terrain/{z}/{x}/{y}{r}.jpg', attribution: '© Stamen Design © OpenStreetMap', description: 'Natural terrain visualization' }, 'watercolor': { name: 'Watercolor', url: 'https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg', attribution: '© Stamen Design © OpenStreetMap', description: 'Artistic watercolor style' }, // Default 'default': { name: 'OpenStreetMap', url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap contributors', description: 'Standard OpenStreetMap' }, // Satellite 'satellite': { name: 'Satellite', url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', description: 'Satellite imagery' }, }; const ContactMap: React.FC = ({ latitude, longitude, zoom = 15, address, clubName, mapStyle = 'default', height = 400, clubPrimaryColor, clubSecondaryColor, }) => { const mapRef = useRef(null); const mapInstanceRef = useRef(null); const tileLayerRef = useRef(null); const markerRef = useRef(null); const [isLoaded, setIsLoaded] = React.useState(false); const [loadError, setLoadError] = React.useState(null); useEffect(() => { // Load Leaflet CSS and JS dynamically const loadLeaflet = async () => { try { // Check if already loaded if ((window as any).L) { L = (window as any).L; setIsLoaded(true); return; } // Load CSS if (!document.getElementById('leaflet-css')) { const link = document.createElement('link'); link.id = 'leaflet-css'; link.rel = 'stylesheet'; link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; link.integrity = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY='; link.crossOrigin = ''; document.head.appendChild(link); } // Load JS if (!document.getElementById('leaflet-js')) { const script = document.createElement('script'); script.id = 'leaflet-js'; script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'; script.integrity = 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo='; script.crossOrigin = ''; script.onload = () => { L = (window as any).L; setIsLoaded(true); }; script.onerror = () => { setLoadError('Failed to load map library'); }; document.head.appendChild(script); } } catch (error) { setLoadError('Error loading map'); } }; loadLeaflet(); }, []); useEffect(() => { if (!isLoaded || !L || !mapRef.current || mapInstanceRef.current) return; try { // Initialize map const map = L.map(mapRef.current, { center: [latitude, longitude], zoom: zoom, scrollWheelZoom: false, // Disable scroll zoom for better UX }); mapInstanceRef.current = map; // Initial tile layer const { tileUrl, attribution } = (() => { let url = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; let attr = '© OpenStreetMap contributors'; if (mapStyle && MAP_STYLES[mapStyle as keyof typeof MAP_STYLES]) { const style = MAP_STYLES[mapStyle as keyof typeof MAP_STYLES]; url = style.url; attr = style.attribution; } else if (mapStyle && mapStyle.startsWith('http')) { url = mapStyle; } return { tileUrl: url, attribution: attr }; })(); tileLayerRef.current = L.tileLayer(tileUrl, { attribution, maxZoom: 19, }).addTo(map); // Create custom marker icon with club colors const markerColor = clubPrimaryColor || '#3388ff'; const customIcon = createCustomMarkerIcon(markerColor, L); // Add marker markerRef.current = L.marker([latitude, longitude], { icon: customIcon }).addTo(map); // Add popup if address is provided if (clubName || address) { let popupContent = ''; if (clubName) popupContent += `${clubName}
`; if (address) popupContent += address; markerRef.current.bindPopup(popupContent); } // Enable scroll zoom on click map.on('click', () => { map.scrollWheelZoom.enable(); }); // Disable scroll zoom on mouseout map.on('mouseout', () => { map.scrollWheelZoom.disable(); }); // Invalidate size after initial mount to ensure tiles render fully // (use a short delay to let layout settle) try { setTimeout(() => { try { map.invalidateSize(); } catch {} }, 150); } catch {} } catch (error) { console.error('Error initializing map:', error); setLoadError('Failed to initialize map'); } // Cleanup on unmount return () => { try { if (mapInstanceRef.current) { mapInstanceRef.current.remove(); } } finally { mapInstanceRef.current = null; tileLayerRef.current = null; markerRef.current = null; } }; }, [isLoaded]); // Observe container visibility to invalidate size when it becomes visible (e.g., tabs, accordions) useEffect(() => { if (!mapRef.current) return; const el = mapRef.current; const observer = new IntersectionObserver((entries) => { const e = entries[0]; if (e && e.isIntersecting && mapInstanceRef.current) { try { // Next frame ensures correct layout measurement requestAnimationFrame(() => { try { mapInstanceRef.current!.invalidateSize(); } catch {} }); } catch {} } }, { root: null, threshold: 0.1 }); observer.observe(el); return () => observer.disconnect(); }, [isLoaded]); // Observe container resize to keep Leaflet in sync with layout changes useEffect(() => { if (!mapRef.current || !('ResizeObserver' in window)) return; const el = mapRef.current; const ro = new ResizeObserver(() => { if (!mapInstanceRef.current) return; try { mapInstanceRef.current.invalidateSize(); } catch {} }); ro.observe(el); return () => ro.disconnect(); }, [isLoaded]); // Update center/zoom and marker when coords/zoom change useEffect(() => { if (!mapInstanceRef.current || !L) return; try { const map = mapInstanceRef.current; if (typeof latitude === 'number' && typeof longitude === 'number') { map.setView([latitude, longitude], typeof zoom === 'number' ? zoom : map.getZoom()); if (markerRef.current) { markerRef.current.setLatLng([latitude, longitude]); } } } catch {} }, [latitude, longitude, zoom]); // Update map style (tile layer) when mapStyle changes useEffect(() => { if (!mapInstanceRef.current || !L) return; try { const map = mapInstanceRef.current; // Compute URL and attribution let url = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; let attr = '© OpenStreetMap contributors'; if (mapStyle && MAP_STYLES[mapStyle as keyof typeof MAP_STYLES]) { const style = MAP_STYLES[mapStyle as keyof typeof MAP_STYLES]; url = style.url; attr = style.attribution; } else if (mapStyle && mapStyle.startsWith('http')) { url = mapStyle; } if (tileLayerRef.current) { map.removeLayer(tileLayerRef.current); } tileLayerRef.current = L.tileLayer(url, { attribution: attr, maxZoom: 19 }).addTo(map); } catch {} }, [mapStyle]); // Update popup content when clubName/address change useEffect(() => { try { if (markerRef.current) { if (clubName || address) { let popupContent = ''; if (clubName) popupContent += `${clubName}
`; if (address) popupContent += address; markerRef.current.bindPopup(popupContent); } else { markerRef.current.unbindPopup(); } } } catch {} }, [clubName, address]); // Helper function to create color filter function createColorFilter(color: string): string | null { try { // Validate and normalize color const tempDiv = document.createElement('div'); tempDiv.style.color = color; document.body.appendChild(tempDiv); const computedColor = window.getComputedStyle(tempDiv).color; document.body.removeChild(tempDiv); return computedColor; } catch { return null; } } // Helper function to create custom marker with club colors function createCustomMarkerIcon(color: string, leaflet: any) { // Create SVG marker with custom color const svgIcon = ` `; const iconUrl = 'data:image/svg+xml;base64,' + btoa(svgIcon); return leaflet.icon({ iconUrl: iconUrl, iconSize: [36, 54], iconAnchor: [18, 54], popupAnchor: [0, -54], }); } if (loadError) { return ( {loadError} ); } return ( ); }; export default ContactMap;