244 lines
6.6 KiB
TypeScript
244 lines
6.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import dynamic from "next/dynamic";
|
|
|
|
interface CountryData {
|
|
name: string;
|
|
count: number;
|
|
position?: [number, number]; // latitude, longitude
|
|
}
|
|
|
|
interface ObservatoryMapProps {
|
|
countries: CountryData[];
|
|
onCountrySelect: (country: string) => void;
|
|
selectedCountry: string | null;
|
|
}
|
|
|
|
// Add type declaration for leaflet
|
|
declare global {
|
|
interface Window {
|
|
_leaflet_L: any;
|
|
}
|
|
}
|
|
|
|
// Mapping of countries to geographic coordinates [latitude, longitude]
|
|
const COUNTRY_COORDINATES: Record<string, [number, number]> = {
|
|
// Africa
|
|
'Sudan': [15.5, 30.5],
|
|
'Egypt': [26.8, 30.8],
|
|
'South Africa': [-30.6, 22.9],
|
|
'Nigeria': [9.1, 8.7],
|
|
'Kenya': [0.0, 38.0],
|
|
'Ethiopia': [9.1, 40.5],
|
|
'Morocco': [31.8, -7.1],
|
|
'Algeria': [28.0, 1.7],
|
|
'Tunisia': [34.0, 9.0],
|
|
|
|
// Americas
|
|
'USA': [37.1, -95.7],
|
|
'New York': [40.7, -74.0],
|
|
'Canada': [56.1, -106.3],
|
|
'Mexico': [23.6, -102.5],
|
|
'Brazil': [-14.2, -51.9],
|
|
'Argentina': [-38.4, -63.6],
|
|
|
|
// Europe
|
|
'UK': [55.4, -3.4],
|
|
'France': [46.2, 2.2],
|
|
'Germany': [51.2, 10.4],
|
|
'Italy': [41.9, 12.6],
|
|
'Spain': [40.5, -3.7],
|
|
'Ukraine': [48.4, 31.2],
|
|
'Russia': [61.5, 105.3],
|
|
'Poland': [51.9, 19.1],
|
|
'Sweden': [60.1, 18.6],
|
|
'Norway': [60.5, 8.5],
|
|
'Finland': [61.9, 25.7],
|
|
'Greece': [39.1, 21.8],
|
|
'Netherlands': [52.1, 5.3],
|
|
'Belgium': [50.5, 4.5],
|
|
'Portugal': [39.4, -8.2],
|
|
'Switzerland': [46.8, 8.2],
|
|
'Austria': [47.5, 14.5],
|
|
|
|
// Asia
|
|
'China': [35.9, 104.2],
|
|
'India': [20.6, 79.0],
|
|
'Japan': [36.2, 138.3],
|
|
'South Korea': [35.9, 127.8],
|
|
'Indonesia': [-0.8, 113.9],
|
|
'Thailand': [15.9, 101.0],
|
|
'Vietnam': [14.1, 108.3],
|
|
'Philippines': [12.9, 121.8],
|
|
'Malaysia': [4.2, 101.9],
|
|
'Singapore': [1.3, 103.8],
|
|
'Pakistan': [30.4, 69.3],
|
|
'Iran': [32.4, 53.7],
|
|
'Iraq': [33.2, 43.7],
|
|
'Saudi Arabia': [23.9, 45.1],
|
|
'Turkey': [38.9, 35.2],
|
|
'Israel': [31.0, 34.9],
|
|
'Palestine': [31.9, 35.2],
|
|
'Syria': [34.8, 39.0],
|
|
'Afghanistan': [33.9, 67.7],
|
|
|
|
// Oceania
|
|
'Australia': [-25.3, 133.8],
|
|
|
|
// Other
|
|
'United Nations': [40.7, -74.0], // UN HQ in New York
|
|
};
|
|
|
|
// Dynamic imports for Leaflet components to avoid SSR issues
|
|
// We use "any" type here to avoid TypeScript errors with dynamic imports
|
|
const MapContainer: any = dynamic(
|
|
() => import('react-leaflet').then(mod => mod.MapContainer),
|
|
{ ssr: false }
|
|
);
|
|
const TileLayer: any = dynamic(
|
|
() => import('react-leaflet').then(mod => mod.TileLayer),
|
|
{ ssr: false }
|
|
);
|
|
const Marker: any = dynamic(
|
|
() => import('react-leaflet').then(mod => mod.Marker),
|
|
{ ssr: false }
|
|
);
|
|
const Popup: any = dynamic(
|
|
() => import('react-leaflet').then(mod => mod.Popup),
|
|
{ ssr: false }
|
|
);
|
|
const ZoomControl: any = dynamic(
|
|
() => import('react-leaflet').then(mod => mod.ZoomControl),
|
|
{ ssr: false }
|
|
);
|
|
|
|
export function ObservatoryMap({
|
|
countries,
|
|
onCountrySelect,
|
|
selectedCountry
|
|
}: ObservatoryMapProps) {
|
|
const [isMounted, setIsMounted] = useState(false);
|
|
|
|
// We'll need the Leaflet CSS
|
|
useEffect(() => {
|
|
// Import Leaflet CSS only on the client
|
|
const loadLeafletStyles = async () => {
|
|
try {
|
|
await import('leaflet/dist/leaflet.css');
|
|
} catch (e) {
|
|
console.error('Failed to load Leaflet CSS', e);
|
|
}
|
|
};
|
|
|
|
loadLeafletStyles();
|
|
|
|
// Add marker icons to prevent missing icons issue
|
|
if (typeof window !== 'undefined') {
|
|
// Fix Leaflet's icon paths
|
|
delete window._leaflet_L;
|
|
|
|
try {
|
|
const L = require('leaflet');
|
|
|
|
// Set default icon paths
|
|
delete L.Icon.Default.prototype._getIconUrl;
|
|
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',
|
|
});
|
|
} catch (e) {
|
|
console.error('Failed to load Leaflet', e);
|
|
}
|
|
}
|
|
|
|
setIsMounted(true);
|
|
}, []);
|
|
|
|
// Prepare countries with coordinates
|
|
const countriesWithCoordinates = countries.filter(country => {
|
|
return COUNTRY_COORDINATES[country.name] !== undefined;
|
|
}).map(country => ({
|
|
...country,
|
|
position: COUNTRY_COORDINATES[country.name]
|
|
}));
|
|
|
|
// Sort countries by count (higher count = shows on top)
|
|
const sortedCountries = [...countriesWithCoordinates].sort((a, b) => b.count - a.count);
|
|
|
|
// Get custom icon based on count
|
|
const getMarkerIcon = (count: number, isSelected: boolean) => {
|
|
if (typeof window === 'undefined') return null;
|
|
|
|
try {
|
|
const L = require('leaflet');
|
|
const size = Math.min(Math.max(20, count * 5), 40);
|
|
|
|
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]
|
|
});
|
|
} catch (e) {
|
|
console.error('Failed to create icon', e);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
if (!isMounted) {
|
|
return (
|
|
<div className="w-full h-full bg-gray-100 flex items-center justify-center">
|
|
<p className="text-gray-500">Loading map...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="w-full h-full">
|
|
<MapContainer
|
|
center={[20, 0]} // Center of the world
|
|
zoom={2}
|
|
style={{ height: '100%', width: '100%' }}
|
|
zoomControl={false}
|
|
>
|
|
<TileLayer
|
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
/>
|
|
<ZoomControl position="bottomright" />
|
|
|
|
{sortedCountries.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>
|
|
);
|
|
}
|