"use client" import { useCallback, useMemo, useRef, useState } from "react" import { cn } from "@/lib/utils" // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface NodeLocation { id: string name: string chain: string network: string region: string status: "running" | "syncing" | "provisioning" | "stopped" | "error" latitude?: number longitude?: number nodeType: string peerCount?: number syncProgress?: number } export interface NodeGlobeProps { nodes: NodeLocation[] onNodeClick?: (nodeId: string) => void selectedNetwork?: string className?: string } // --------------------------------------------------------------------------- // Region coordinate mapping // --------------------------------------------------------------------------- const REGION_COORDS: Record = { // DigitalOcean regions "nyc1": { lat: 40.7128, lng: -74.0060, label: "New York 1" }, "nyc3": { lat: 40.7128, lng: -74.0060, label: "New York 3" }, "sfo1": { lat: 37.7749, lng: -122.4194, label: "San Francisco 1" }, "sfo2": { lat: 37.7749, lng: -122.4194, label: "San Francisco 2" }, "sfo3": { lat: 37.7749, lng: -122.4194, label: "San Francisco 3" }, "ams3": { lat: 52.3676, lng: 4.9041, label: "Amsterdam" }, "sgp1": { lat: 1.3521, lng: 103.8198, label: "Singapore" }, "lon1": { lat: 51.5074, lng: -0.1278, label: "London" }, "fra1": { lat: 50.1109, lng: 8.6821, label: "Frankfurt" }, "tor1": { lat: 43.6532, lng: -79.3832, label: "Toronto" }, "blr1": { lat: 12.9716, lng: 77.5946, label: "Bangalore" }, "syd1": { lat: -33.8688, lng: 151.2093, label: "Sydney" }, // AWS-style "us-east-1": { lat: 39.0438, lng: -77.4874, label: "N. Virginia" }, "us-west-2": { lat: 45.5152, lng: -122.6784, label: "Oregon" }, "eu-west-1": { lat: 53.3498, lng: -6.2603, label: "Ireland" }, "ap-southeast-1": { lat: 1.3521, lng: 103.8198, label: "Singapore" }, "ap-northeast-1": { lat: 35.6762, lng: 139.6503, label: "Tokyo" }, } // --------------------------------------------------------------------------- // Status colour palette // --------------------------------------------------------------------------- const STATUS_COLORS: Record = { running: "#22c55e", syncing: "#eab308", provisioning: "#f97316", stopped: "#6b7280", error: "#ef4444", } const STATUS_LABELS: Record = { running: "Running", syncing: "Syncing", provisioning: "Provisioning", stopped: "Stopped", error: "Error", } // --------------------------------------------------------------------------- // Mercator projection helper // --------------------------------------------------------------------------- /** Convert lat/lng to x/y in 0..1 normalised Mercator space. */ function toMercator(lat: number, lng: number): { x: number; y: number } { const x = (lng + 180) / 360 const latRad = (lat * Math.PI) / 180 const mercN = Math.log(Math.tan(Math.PI / 4 + latRad / 2)) const y = 0.5 - mercN / (2 * Math.PI) return { x, y } } // --------------------------------------------------------------------------- // Simplified world-outline SVG path data // // Hand-reduced continent outlines in Mercator projection coordinates, fitting // a viewBox of 0 0 1000 500. Dark-themed: dark ocean, lighter landmasses. // --------------------------------------------------------------------------- const WORLD_PATH = [ // --- North America --- "M 130,105 L 122,100 118,92 120,84 126,78 135,74 148,72 162,70", "L 175,74 185,72 195,76 205,82 215,90 222,98 228,106 232,115", "L 230,124 224,132 218,140 212,148 205,156 198,162 192,168", "L 186,174 180,178 174,180 168,182 163,186 158,190 154,186", "L 150,180 146,175 142,168 138,160 135,152 132,144 130,136", "L 128,128 126,120 128,112 Z", // --- Mexico / Central America --- "M 154,186 L 158,192 162,198 165,205 168,212 172,218 175,224", "L 178,228 176,232 172,230 168,226 164,222 160,218 156,214", "L 153,208 152,200 152,194 Z", // --- South America --- "M 178,228 L 186,236 194,245 202,255 208,265 214,278 218,292", "L 222,306 225,320 226,335 224,348 220,360 215,370 208,378", "L 200,384 192,386 186,380 182,372 180,360 178,346 176,332", "L 175,318 176,304 178,290 180,276 182,262 183,248 180,238 Z", // --- Europe --- "M 448,82 L 455,76 465,72 476,70 486,72 495,76 502,82 506,90", "L 504,96 498,102 490,106 482,108 474,106 467,102 460,96", "L 454,90 450,86 Z", // --- British Isles --- "M 432,82 L 437,76 443,78 444,84 440,88 434,86 Z", // --- Scandinavia --- "M 476,50 L 484,44 492,42 498,46 502,54 500,62 496,68 490,66", "L 484,60 480,54 Z", // --- Africa --- "M 448,148 L 458,142 470,138 484,136 496,140 508,146 516,156", "L 522,168 526,182 528,196 526,212 522,228 518,244 512,258", "L 504,268 496,274 486,276 478,272 470,264 464,254 458,242", "L 454,228 452,214 450,200 448,186 446,172 446,158 Z", // --- Arabian Peninsula --- "M 530,142 L 540,138 548,142 552,150 548,158 540,162 534,158", "L 530,150 Z", // --- India subcontinent --- "M 582,132 L 594,128 604,132 612,142 616,156 612,170 606,180", "L 598,186 590,184 584,176 580,164 578,152 580,140 Z", // --- Southeast Asia peninsula --- "M 622,156 L 630,150 638,154 640,162 636,170 628,172 624,164 Z", // --- East Asia / China --- "M 618,80 L 630,74 644,70 658,68 672,72 682,80 690,90 692,102", "L 688,114 680,122 670,128 660,130 650,128 640,122 632,114", "L 626,104 622,94 620,86 Z", // --- Japan --- "M 698,86 L 704,80 710,84 712,92 710,102 704,108 698,104 696,96 Z", // --- Maritime Southeast Asia --- "M 632,212 L 644,206 658,204 672,208 684,214 694,220 690,228", "L 678,230 666,226 654,222 642,218 Z", // --- Australia --- "M 678,288 L 694,278 712,274 730,276 746,282 756,292 762,304", "L 760,318 752,330 740,336 726,338 712,334 700,326 690,316", "L 684,304 680,296 Z", // --- New Zealand --- "M 786,332 L 792,326 798,330 800,338 796,344 790,342 788,336 Z", // --- Northern Asia / Russia --- "M 508,52 L 530,46 558,42 588,38 620,36 654,34 686,38 712,44", "L 732,52 742,60 736,66 722,64 706,60 686,56 664,54 642,52", "L 620,50 598,48 576,48 556,50 536,52 520,54 512,54 Z", ].join(" ") // --------------------------------------------------------------------------- // Resolved node with pixel coordinates // --------------------------------------------------------------------------- type ResolvedNode = NodeLocation & { px: number py: number regionLabel: string } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export function NodeGlobe({ nodes, onNodeClick, selectedNetwork, className, }: NodeGlobeProps) { const containerRef = useRef(null) const [hoveredNode, setHoveredNode] = useState(null) const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 }) const MAP_W = 1000 const MAP_H = 500 // ------- derived data ------- const filteredNodes = useMemo(() => { if (!selectedNetwork || selectedNetwork === "all") return nodes return nodes.filter((n) => n.network === selectedNetwork) }, [nodes, selectedNetwork]) const resolvedNodes = useMemo(() => { const out: ResolvedNode[] = [] for (const node of filteredNodes) { let lat = node.latitude let lng = node.longitude let regionLabel = node.region if ((lat == null || lng == null) && REGION_COORDS[node.region]) { const rc = REGION_COORDS[node.region] lat = rc.lat lng = rc.lng regionLabel = rc.label } if (lat == null || lng == null) continue const { x, y } = toMercator(lat, lng) out.push({ ...node, px: x * MAP_W, py: y * MAP_H, regionLabel }) } return out }, [filteredNodes]) const networks = useMemo(() => { const s = new Set(nodes.map((n) => n.network)) return Array.from(s).sort() }, [nodes]) // ------- event handlers ------- const handleMouseEnter = useCallback( (node: ResolvedNode, e: React.MouseEvent) => { setHoveredNode(node) const rect = containerRef.current?.getBoundingClientRect() if (rect) { setTooltipPos({ x: e.clientX - rect.left + 14, y: e.clientY - rect.top - 10, }) } }, [], ) const handleMouseMove = useCallback( (e: React.MouseEvent) => { if (!hoveredNode) return const rect = containerRef.current?.getBoundingClientRect() if (rect) { setTooltipPos({ x: e.clientX - rect.left + 14, y: e.clientY - rect.top - 10, }) } }, [hoveredNode], ) const handleMouseLeave = useCallback(() => { setHoveredNode(null) }, []) // ------- render ------- return (
{/* Pulse keyframes injected once */} {/* SVG flat map */} {/* Ocean background */} {/* Graticule grid */} {Array.from({ length: 17 }, (_, i) => { const x = (i + 1) * (MAP_W / 18) return })} {Array.from({ length: 8 }, (_, i) => { const y = (i + 1) * (MAP_H / 9) return })} {/* Landmasses */} {/* Node markers */} {resolvedNodes.map((node) => { const color = STATUS_COLORS[node.status] const hovered = hoveredNode?.id === node.id // Stagger animation starts to avoid synchronised pulses const delay = `${(node.id.charCodeAt(0) % 7) * 0.35}s` return ( handleMouseEnter(node, e)} onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave} onClick={() => onNodeClick?.(node.id)} role="button" tabIndex={0} aria-label={`${node.name} - ${STATUS_LABELS[node.status]}`} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") onNodeClick?.(node.id) }} > {/* Expanding ring for active statuses */} {(node.status === "running" || node.status === "syncing") && ( )} {/* Ambient glow */} {/* Primary dot */} {/* Centre highlight */} ) })} {/* Tooltip */} {hoveredNode && (
(containerRef.current?.clientWidth ?? 800) * 0.7 ? "translateX(-110%)" : undefined, }} >
{hoveredNode.name}
{hoveredNode.chain} / {hoveredNode.network}
{STATUS_LABELS[hoveredNode.status]}
{hoveredNode.regionLabel}
Type: {hoveredNode.nodeType}
{hoveredNode.peerCount != null && (
Peers: {hoveredNode.peerCount}
)} {hoveredNode.syncProgress != null && hoveredNode.status === "syncing" && (
Sync: {(hoveredNode.syncProgress * 100).toFixed(1)}%
)}
)} {/* Legend */}
{(Object.keys(STATUS_COLORS) as NodeLocation["status"][]).map((status) => (
{STATUS_LABELS[status]}
))} {networks.length > 0 && (
{resolvedNodes.length} node{resolvedNodes.length !== 1 ? "s" : ""} {selectedNetwork && selectedNetwork !== "all" ? ` on ${selectedNetwork}` : ""}
)}
) }