"use client"; import { useEffect, useRef, useState, useCallback, useMemo } from "react"; import dynamic from "next/dynamic"; import { useTranslations, useLocale } from "next-intl"; import { useHeatmapData, type HeatmapPoint } from "@/hooks/use-heatmap-data"; import { useCountryData, type LabelData } from "./use-country-data"; import { GlobeControls } from "./globe-controls"; function GlobeLoading() { const t = useTranslations("globe"); return (
{t("loading")}
); } const Globe = dynamic(() => import("react-globe.gl"), { ssr: false, loading: () => , }); interface ArcData { startLat: number; startLng: number; endLat: number; endLng: number; color: string; } export function GlobeView() { const t = useTranslations("globe"); const tPopup = useTranslations("clawPopup"); const locale = useLocale(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const globeRef = useRef(undefined); const containerRef = useRef(null); const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const [hoveredPoint, setHoveredPoint] = useState(null); const [selectedPoint, setSelectedPoint] = useState(null); const { points } = useHeatmapData(30000); const { countries, labels } = useCountryData(locale); // Generate arcs only between online points const arcs = useMemo((): ArcData[] => { const onlinePoints = points.filter((p) => p.onlineCount > 0); if (onlinePoints.length < 2) return []; const result: ArcData[] = []; const maxArcs = Math.min(onlinePoints.length - 1, 8); for (let i = 0; i < maxArcs; i++) { const from = onlinePoints[i]; const to = onlinePoints[(i + 1) % onlinePoints.length]; result.push({ startLat: from.lat, startLng: from.lng, endLat: to.lat, endLng: to.lng, color: "rgba(0, 240, 255, 0.4)", }); } return result; }, [points]); useEffect(() => { const updateDimensions = () => { if (containerRef.current) { setDimensions({ width: containerRef.current.clientWidth, height: containerRef.current.clientHeight, }); } }; updateDimensions(); const observer = new ResizeObserver(updateDimensions); if (containerRef.current) observer.observe(containerRef.current); return () => observer.disconnect(); }, []); useEffect(() => { if (globeRef.current) { globeRef.current.pointOfView({ lat: 20, lng: 100, altitude: 2.5 }); } }, [dimensions]); const handleResetView = useCallback(() => { if (globeRef.current) { globeRef.current.pointOfView({ lat: 20, lng: 100, altitude: 2.5 }); } }, []); const handleZoomIn = useCallback(() => { if (globeRef.current) { globeRef.current.pointOfView({ lat: 20, lng: 100, altitude: 1.5 }); } }, []); const handleZoomOut = useCallback(() => { if (globeRef.current) { globeRef.current.pointOfView({ lat: 20, lng: 100, altitude: 3.5 }); } }, []); return (
{dimensions.width > 0 && ( "rgba(26, 31, 46, 0.6)"} polygonSideColor={() => "rgba(0, 240, 255, 0.05)"} polygonStrokeColor={() => "rgba(0, 240, 255, 0.15)"} polygonAltitude={0.005} // Country name labels (use HTML elements for CJK support) labelsData={labels} labelLat={(d: object) => (d as LabelData).lat} labelLng={(d: object) => (d as LabelData).lng} labelAltitude={0.01} labelSize={1.5} labelDotRadius={0} labelText={(d: object) => (d as LabelData).name} labelColor={() => "rgba(0, 240, 255, 0.5)"} labelResolution={2} // Graticules showGraticules={true} // Points with claw icons (using htmlElements for custom icons) htmlElementsData={points} htmlLat={(d: object) => (d as HeatmapPoint).lat} htmlLng={(d: object) => (d as HeatmapPoint).lng} htmlAltitude={(d: object) => { const p = d as HeatmapPoint; const base = Math.min(p.weight * 0.02, 0.15); return p.onlineCount > 0 ? base : base * 0.5; }} htmlElement={(d: object) => { const p = d as HeatmapPoint; const el = document.createElement("div"); const size = p.onlineCount > 0 ? Math.max(p.weight * 0.8, 16) : Math.max(p.weight * 0.5, 12); el.style.width = `${size}px`; el.style.height = `${size}px`; el.style.backgroundImage = "url(/claw-icon.svg)"; el.style.backgroundSize = "contain"; el.style.backgroundRepeat = "no-repeat"; el.style.backgroundPosition = "center"; el.style.opacity = p.onlineCount > 0 ? "1" : "0.4"; el.style.filter = p.onlineCount > 0 ? "drop-shadow(0 0 4px rgba(0, 240, 255, 0.6))" : "none"; el.style.cursor = "pointer"; el.style.transition = "transform 0.2s ease"; el.dataset.pointId = `${p.city}-${p.country}`; // Add click handler directly to the element el.addEventListener("click", () => { setSelectedPoint(p); }); el.addEventListener("mouseenter", () => { setHoveredPoint(p); }); el.addEventListener("mouseleave", () => { setHoveredPoint(null); }); return el; }} // Arcs arcsData={arcs} arcStartLat={(d: object) => (d as ArcData).startLat} arcStartLng={(d: object) => (d as ArcData).startLng} arcEndLat={(d: object) => (d as ArcData).endLat} arcEndLng={(d: object) => (d as ArcData).endLng} arcColor={(d: object) => (d as ArcData).color} arcDashLength={0.5} arcDashGap={0.2} arcDashAnimateTime={2000} arcStroke={0.3} // Auto-rotate animateIn={true} enablePointerInteraction={true} /> )} {/* Hover Tooltip */} {hoveredPoint && !selectedPoint && (
🦞

{hoveredPoint.city}

{hoveredPoint.country}

{t("totalClaws", { total: hoveredPoint.clawCount, online: hoveredPoint.onlineCount })}

)} {/* Click Popup */} {selectedPoint && (

{selectedPoint.city}

{selectedPoint.country}

{tPopup("total", { count: selectedPoint.clawCount })} {tPopup("online", { count: selectedPoint.onlineCount })}
{selectedPoint.claws.length > 0 && (
{selectedPoint.claws.map((claw) => (
{claw.name}
))}
)}
)}
); }