observatory

This commit is contained in:
alma 2025-05-04 17:08:40 +02:00
parent 53371f0220
commit 247b327f38

View File

@ -1,21 +1,11 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useRef } from "react";
import L from "leaflet";
import { MapContainer, TileLayer, Marker, Popup, ZoomControl } from "react-leaflet";
// Import leaflet CSS
import "leaflet/dist/leaflet.css";
// Add declaration for Leaflet's Icon.Default
declare module 'leaflet' {
namespace Icon {
interface Default {
_getIconUrl?: string;
}
}
}
interface CountryData {
name: string;
count: number;
@ -28,91 +18,94 @@ interface MapComponentProps {
selectedCountry: string | null;
}
// Fix Leaflet default icon path issues
function fixLeafletIcons() {
// Delete to prevent duplicate icon definitions
delete L.Icon.Default.prototype._getIconUrl;
// Set paths to the images
L.Icon.Default.mergeOptions({
iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png",
iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png",
shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
});
}
export function MapComponent({ countries, onCountrySelect, selectedCountry }: MapComponentProps) {
const [isClient, setIsClient] = useState(false);
// Use a ref to track if map is initialized
const mapRef = useRef<L.Map | null>(null);
const mapContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Fix Leaflet icons on client side
fixLeafletIcons();
setIsClient(true);
// Only initialize map if it doesn't exist and we have a container
if (!mapRef.current && mapContainerRef.current) {
// Create the map instance
mapRef.current = L.map(mapContainerRef.current).setView([20, 0], 2);
// Add tile layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(mapRef.current);
// Add zoom control
L.control.zoom({
position: 'bottomright'
}).addTo(mapRef.current);
}
// Cleanup function to remove the map when component unmounts
return () => {
if (mapRef.current) {
mapRef.current.remove();
mapRef.current = null;
}
};
}, []);
// Get custom icon based on count
const getMarkerIcon = (count: number, isSelected: boolean) => {
const size = Math.min(Math.max(20, count * 5), 40);
// Update markers when countries or selection changes
useEffect(() => {
if (!mapRef.current) return;
return L.divIcon({
html: `<div style="
background-color: ${isSelected ? '#3b82f6' : '#ef4444'};
color: white;
border-radius: 50%;
width: ${size}px;
height: ${size}px;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
font-size: ${size > 30 ? 14 : 12}px;
box-shadow: 0 0 0 2px white;
">${count}</div>`,
className: '',
iconSize: [size, size],
iconAnchor: [size/2, size/2]
// Clear existing markers
mapRef.current.eachLayer((layer) => {
if (layer instanceof L.Marker) {
mapRef.current?.removeLayer(layer);
}
});
};
if (!isClient) {
return (
<div className="w-full h-full bg-gray-100 flex items-center justify-center">
<p className="text-gray-500">Loading map...</p>
</div>
);
}
// Add markers for each country
countries.forEach((country) => {
// Create custom icon
const size = Math.min(Math.max(20, country.count * 5), 40);
const icon = L.divIcon({
html: `<div style="
background-color: ${country.name === selectedCountry ? '#3b82f6' : '#ef4444'};
color: white;
border-radius: 50%;
width: ${size}px;
height: ${size}px;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
font-size: ${size > 30 ? 14 : 12}px;
box-shadow: 0 0 0 2px white;
">${country.count}</div>`,
className: '',
iconSize: [size, size],
iconAnchor: [size/2, size/2]
});
// Create marker
const marker = L.marker(country.position, { icon })
.addTo(mapRef.current!);
// Add popup
marker.bindPopup(`
<div style="text-align: center;">
<strong>${country.name}</strong>
<div>${country.count} news articles</div>
</div>
`);
// Add click handler
marker.on('click', () => {
onCountrySelect(country.name);
});
});
}, [countries, selectedCountry, onCountrySelect]);
return (
<MapContainer
center={[20, 0]} // Center of the world
zoom={2}
style={{ height: '100%', width: '100%' }}
zoomControl={false}
key="world-map" // Static key to help with React identity
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<ZoomControl position="bottomright" />
{countries.map((country) => (
<Marker
key={country.name}
position={country.position}
icon={getMarkerIcon(country.count, country.name === selectedCountry)}
eventHandlers={{
click: () => onCountrySelect(country.name)
}}
>
<Popup>
<div className="text-center">
<strong>{country.name}</strong>
<div>{country.count} news articles</div>
</div>
</Popup>
</Marker>
))}
</MapContainer>
<div
ref={mapContainerRef}
style={{ width: '100%', height: '100%' }}
/>
);
}