'use client' import { useEffect, useMemo, useRef } from 'react' import createGlobe from 'cobe' import { useTheme } from '@ciphera-net/ui' import { countryCentroids } from '@/lib/country-centroids' interface GlobeProps { data: Array<{ country: string; pageviews: number }> className?: string } export default function Globe({ data, className }: GlobeProps) { const canvasRef = useRef(null) const phiRef = useRef(0) const dragRef = useRef(0) const pointerRef = useRef(null) const { resolvedTheme } = useTheme() const isDarkRef = useRef(resolvedTheme === 'dark') const markersRef = useRef>([]) // Update refs without causing effect re-runs isDarkRef.current = resolvedTheme === 'dark' // Compute markers into ref (memoized to avoid recalculating on every render) const markers = useMemo(() => { const max = data.length ? Math.max(...data.map((d) => d.pageviews)) : 0 return max > 0 ? data .filter((d) => d.country && d.country !== 'Unknown' && countryCentroids[d.country]) .map((d) => ({ location: [countryCentroids[d.country].lat, countryCentroids[d.country].lng] as [number, number], size: 0.03 + (d.pageviews / max) * 0.12, })) : [] }, [data]) markersRef.current = markers useEffect(() => { if (!canvasRef.current) return const size = canvasRef.current.offsetWidth const pixelRatio = Math.min(window.devicePixelRatio, 2) const isDark = isDarkRef.current const globe = createGlobe(canvasRef.current, { width: size * pixelRatio, height: size * pixelRatio, devicePixelRatio: pixelRatio, phi: phiRef.current, theta: 0.3, dark: isDark ? 1 : 0, diffuse: isDark ? 2 : 0.4, mapSamples: 16000, mapBrightness: isDark ? 2 : 1.2, baseColor: isDark ? [0.5, 0.5, 0.5] : [1, 1, 1], markerColor: [253 / 255, 94 / 255, 15 / 255], glowColor: isDark ? [0.15, 0.15, 0.15] : [1, 1, 1], markers: markersRef.current, onRender: (state) => { if (!pointerRef.current) phiRef.current += 0.002 state.phi = phiRef.current + dragRef.current }, }) setTimeout(() => { if (canvasRef.current) canvasRef.current.style.opacity = '1' }, 0) return () => { globe.destroy() } // Only recreate on theme change // eslint-disable-next-line react-hooks/exhaustive-deps }, [resolvedTheme]) return (
{ pointerRef.current = e.clientX canvasRef.current!.style.cursor = 'grabbing' }} onPointerUp={() => { pointerRef.current = null canvasRef.current!.style.cursor = 'grab' }} onPointerOut={() => { pointerRef.current = null if (canvasRef.current) canvasRef.current.style.cursor = 'grab' }} onMouseMove={(e) => { if (pointerRef.current !== null) { const delta = e.clientX - pointerRef.current dragRef.current += delta / 800 pointerRef.current = e.clientX } }} onTouchMove={(e) => { if (pointerRef.current !== null && e.touches[0]) { const delta = e.touches[0].clientX - pointerRef.current dragRef.current += delta / 800 pointerRef.current = e.touches[0].clientX } }} />
) }