mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 18:52:56 +00:00
upload
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user