Files
MyClub/frontend/src/components/home/ContactMap.tsx
T
Tomáš Dvořák 12cba639b9 upload
2025-10-16 13:32:05 +02:00

329 lines
9.7 KiB
TypeScript

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<ContactMapProps> = ({
latitude,
longitude,
zoom = 15,
address,
clubName,
mapStyle = 'default',
height = 400,
clubPrimaryColor,
clubSecondaryColor,
}) => {
const mapRef = useRef<HTMLDivElement>(null);
const mapInstanceRef = useRef<any>(null);
const [isLoaded, setIsLoaded] = React.useState(false);
const [loadError, setLoadError] = React.useState<string | null>(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;
// Get tile layer URL based on style
let tileUrl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
let attribution = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
// Use predefined styles or custom URL
if (mapStyle && MAP_STYLES[mapStyle as keyof typeof MAP_STYLES]) {
const style = MAP_STYLES[mapStyle as keyof typeof MAP_STYLES];
tileUrl = style.url;
attribution = style.attribution;
} else if (mapStyle && mapStyle.startsWith('http')) {
// Custom tile URL
tileUrl = mapStyle;
}
// Add tile layer
const tileLayer = L.tileLayer(tileUrl, {
attribution: attribution,
maxZoom: 19,
}).addTo(map);
// Apply club color overlay if provided
if (clubPrimaryColor && clubPrimaryColor !== '') {
const colorFilter = createColorFilter(clubPrimaryColor);
if (colorFilter) {
const pane = map.createPane('colorOverlay');
pane.style.zIndex = '400';
pane.style.pointerEvents = 'none';
pane.style.mixBlendMode = 'multiply';
pane.style.backgroundColor = colorFilter;
pane.style.opacity = '0.15';
}
}
// Create custom marker icon with club colors
const markerColor = clubPrimaryColor || '#3388ff';
const customIcon = createCustomMarkerIcon(markerColor, L);
// Add marker
const marker = L.marker([latitude, longitude], { icon: customIcon }).addTo(map);
// Add popup if address is provided
if (clubName || address) {
let popupContent = '';
if (clubName) popupContent += `<b>${clubName}</b><br>`;
if (address) popupContent += address;
marker.bindPopup(popupContent);
}
// Enable scroll zoom on click
map.on('click', () => {
map.scrollWheelZoom.enable();
});
// Disable scroll zoom on mouseout
map.on('mouseout', () => {
map.scrollWheelZoom.disable();
});
} catch (error) {
console.error('Error initializing map:', error);
setLoadError('Failed to initialize map');
}
// Cleanup
return () => {
if (mapInstanceRef.current) {
mapInstanceRef.current.remove();
mapInstanceRef.current = null;
}
};
}, [isLoaded, latitude, longitude, zoom, address, clubName, mapStyle, clubPrimaryColor, clubSecondaryColor]);
// 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 = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 36" width="36" height="54">
<defs>
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="2"/>
<feOffset dx="0" dy="2" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.3"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<path fill="${color}" stroke="#fff" stroke-width="1.5" filter="url(#shadow)"
d="M12 0C7.03 0 3 4.03 3 9c0 7.5 9 18 9 18s9-10.5 9-18c0-4.97-4.03-9-9-9z"/>
<circle cx="12" cy="9" r="3" fill="#fff"/>
</svg>
`;
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 (
<Box
ref={mapRef}
w="100%"
h={`${height}px`}
bg="gray.100"
display="flex"
alignItems="center"
justifyContent="center"
borderRadius="md"
>
{loadError}
</Box>
);
}
return (
<Box
ref={mapRef}
w="100%"
h={`${height}px`}
borderRadius="md"
overflow="hidden"
boxShadow="md"
/>
);
};
export default ContactMap;