"use client"; import { useState, useMemo, useCallback } from "react"; import { useLocale } from "next-intl"; import Map, { Source, Layer, Popup } from "react-map-gl/maplibre"; import type { MapLayerMouseEvent, LngLatLike, MapRef } from "react-map-gl/maplibre"; import type { LayerSpecification } from "maplibre-gl"; import "maplibre-gl/dist/maplibre-gl.css"; import { useHeatmapData, type HeatmapPoint } from "@/hooks/use-heatmap-data"; import { MapPopup } from "./map-popup"; const CARTO_STYLE = "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"; interface ContinentViewport { longitude: number; latitude: number; zoom: number; } const continentConfigs: Record = { asia: { longitude: 100, latitude: 35, zoom: 2.5 }, europe: { longitude: 15, latitude: 52, zoom: 3.5 }, americas: { longitude: -80, latitude: 15, zoom: 2.0 }, africa: { longitude: 20, latitude: 5, zoom: 2.5 }, oceania: { longitude: 145, latitude: -25, zoom: 3.0 }, }; const heatmapLayer: LayerSpecification = { id: "claw-heat", type: "heatmap", source: "claws", paint: { "heatmap-weight": ["get", "weight"], "heatmap-intensity": 1, "heatmap-radius": 30, "heatmap-opacity": 0.6, "heatmap-color": [ "interpolate", ["linear"], ["heatmap-density"], 0, "rgba(0,0,0,0)", 0.2, "rgba(0,240,255,0.15)", 0.4, "rgba(0,240,255,0.3)", 0.6, "rgba(0,200,220,0.5)", 1, "rgba(0,240,255,0.8)", ], }, }; const circleLayer: LayerSpecification = { id: "claw-circles", type: "circle", source: "claws", paint: { "circle-radius": [ "interpolate", ["linear"], ["get", "weight"], 1, 5, 5, 10, 10, 16, ], "circle-color": [ "case", [">", ["get", "onlineCount"], 0], "rgba(0, 240, 255, 0.7)", "rgba(0, 240, 255, 0.2)", ], "circle-stroke-color": [ "case", [">", ["get", "onlineCount"], 0], "rgba(0, 240, 255, 0.9)", "rgba(0, 240, 255, 0.3)", ], "circle-stroke-width": 1, "circle-blur": 0.1, }, }; interface ContinentMapProps { slug: string; } export function ContinentMap({ slug }: ContinentMapProps) { const locale = useLocale(); const config = continentConfigs[slug] ?? continentConfigs.asia; const { points } = useHeatmapData(30000); const [popupPoint, setPopupPoint] = useState(null); const [popupLngLat, setPopupLngLat] = useState(null); // Build a MapLibre expression: coalesce(get("name:zh"), get("name_en"), get("name")) // For English, just use name_en with name fallback. const localizedTextField = locale === "en" ? ["coalesce", ["get", "name_en"], ["get", "name"]] : ["coalesce", ["get", `name:${locale}`], ["get", "name_en"], ["get", "name"]]; const handleLoad = useCallback( (e: { target: MapRef["getMap"] extends () => infer M ? M : never }) => { const map = e.target; for (const layer of map.getStyle().layers ?? []) { if (layer.type === "symbol") { const tf = map.getLayoutProperty(layer.id, "text-field"); if (tf != null) { map.setLayoutProperty(layer.id, "text-field", localizedTextField); } } } }, [localizedTextField] ); const geojson = useMemo(() => { const features = points.map((p) => ({ type: "Feature" as const, geometry: { type: "Point" as const, coordinates: [p.lng, p.lat], }, properties: { weight: p.weight, clawCount: p.clawCount, onlineCount: p.onlineCount, city: p.city, country: p.country, claws: JSON.stringify(p.claws), }, })); return { type: "FeatureCollection" as const, features, }; }, [points]); const handleClick = useCallback( (e: MapLayerMouseEvent) => { const feature = e.features?.[0]; if (!feature || feature.geometry.type !== "Point") { setPopupPoint(null); return; } const props = feature.properties; const [lng, lat] = feature.geometry.coordinates; const matched = points.find( (p) => p.city === props.city && p.country === props.country ); if (matched) { setPopupPoint(matched); setPopupLngLat([lng, lat]); } }, [points] ); return (
{popupPoint && popupLngLat && ( setPopupPoint(null)} closeButton={false} className="maplibre-dark-popup" maxWidth="280px" > setPopupPoint(null)} /> )}
); }