'use client'; import createGlobe from 'cobe'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Globe } from 'lucide-react'; interface CountryData { code: string; count: number; regions: { region: string; count: number; cities: { city: string; count: number }[]; }[]; } function countryCodeToFlag(code: string): string { return [...code.toUpperCase()].map(c => String.fromCodePoint(0x1f1e6 + c.charCodeAt(0) - 65)).join(''); } function CountryChip({ country }: { country: CountryData }) { const [expanded, setExpanded] = useState(false); return (
{expanded && (
{country.regions.map((r) => (
{r.region} {r.count}
{r.cities.map((c) => (
{c.city} {c.count}
))}
))}
)}
); } export function GlobeCard() { const canvasRef = useRef(null); const pointerInteracting = useRef(null); const pointerInteractionMovement = useRef(0); const phiRef = useRef(0); const [visitorStats, setVisitorStats] = useState<{ total: number; active24h: number; active7d: number; locations: any[]; countries: CountryData[]; }>({ total: 0, active24h: 0, active7d: 0, locations: [], countries: [], }); const [grabbing, setGrabbing] = useState(false); useEffect(() => { async function fetchVisitors() { try { const res = await fetch('/api/visitors'); const data = await res.json(); setVisitorStats({ total: data.totalVisitors || 0, active24h: data.active24h || 0, active7d: data.active7d || 0, locations: data.locations || [], countries: data.countries || [], }); } catch (e) { console.error('Failed to load visitors', e); } } fetchVisitors(); const interval = setInterval(fetchVisitors, 30000); return () => clearInterval(interval); }, []); useEffect(() => { if (!canvasRef.current) return; const globe = createGlobe(canvasRef.current, { devicePixelRatio: 2, width: 600 * 2, height: 600 * 2, phi: 0, theta: 0, dark: 1, diffuse: 1.2, mapSamples: 16000, mapBrightness: 6, baseColor: [0.3, 0.3, 0.3], markerColor: [0.1, 0.8, 1], glowColor: [0.1, 0.1, 0.2], markers: visitorStats.locations, onRender: (state) => { if (pointerInteracting.current === null) { phiRef.current += 0.005; } state.phi = phiRef.current + pointerInteractionMovement.current; }, }); return () => { globe.destroy(); }; }, [visitorStats.locations]); const handlePointerDown = useCallback((e: React.PointerEvent) => { pointerInteracting.current = e.clientX; setGrabbing(true); }, []); const handlePointerMove = useCallback((e: React.PointerEvent) => { if (pointerInteracting.current !== null) { const delta = e.clientX - pointerInteracting.current; pointerInteractionMovement.current = delta / 200; } }, []); const handlePointerUp = useCallback(() => { if (pointerInteracting.current !== null) { phiRef.current += pointerInteractionMovement.current; pointerInteractionMovement.current = 0; pointerInteracting.current = null; } setGrabbing(false); }, []); return (
Visitor Map
{visitorStats.active24h} LAST 24H
{visitorStats.active7d} LAST 7D
{visitorStats.total} ALL TIME
{visitorStats.countries.length > 0 && (
{visitorStats.countries.map((country) => ( ))}
)}
); }