2026-02-08 02:32:45 -05:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import createGlobe from 'cobe';
|
2026-02-08 03:03:53 -05:00
|
|
|
import { useEffect, useRef, useState } from 'react';
|
2026-02-08 02:32:45 -05:00
|
|
|
import { Globe } from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
export function GlobeCard() {
|
|
|
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
2026-02-08 03:03:53 -05:00
|
|
|
const [visitorStats, setVisitorStats] = useState({ total: 0, active24h: 0, locations: [] });
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
locations: data.locations || []
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Failed to load visitors', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fetchVisitors();
|
|
|
|
|
const interval = setInterval(fetchVisitors, 30000); // 30s poll
|
|
|
|
|
return () => clearInterval(interval);
|
|
|
|
|
}, []);
|
2026-02-08 02:32:45 -05:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let phi = 0;
|
|
|
|
|
|
|
|
|
|
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],
|
2026-02-08 03:03:53 -05:00
|
|
|
markers: visitorStats.locations,
|
2026-02-08 02:32:45 -05:00
|
|
|
onRender: (state) => {
|
|
|
|
|
state.phi = phi;
|
|
|
|
|
phi += 0.01;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
globe.destroy();
|
|
|
|
|
};
|
2026-02-08 03:03:53 -05:00
|
|
|
}, [visitorStats.locations]);
|
2026-02-08 02:32:45 -05:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="col-span-1 md:col-span-2 lg:col-span-2 row-span-2 bg-neutral-900 border border-neutral-800 rounded-xl relative overflow-hidden group hover:border-neutral-700 transition-colors">
|
|
|
|
|
<div className="absolute top-6 left-6 z-10 pointer-events-none">
|
|
|
|
|
<div className="flex items-center gap-2 text-neutral-400">
|
|
|
|
|
<Globe size={18} />
|
2026-02-08 03:03:53 -05:00
|
|
|
<span className="text-sm font-medium">Visitor Map</span>
|
2026-02-08 02:32:45 -05:00
|
|
|
</div>
|
2026-02-08 03:03:53 -05:00
|
|
|
<div className="mt-4 space-y-2">
|
|
|
|
|
<div>
|
|
|
|
|
<span className="text-2xl font-bold text-white tracking-tight block">{visitorStats.active24h}</span>
|
|
|
|
|
<span className="text-xs text-neutral-500 font-mono">LAST 24H</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="absolute top-6 right-6 z-10 text-right pointer-events-none">
|
|
|
|
|
<div>
|
|
|
|
|
<span className="text-xl font-bold text-neutral-300 block">{visitorStats.total}</span>
|
|
|
|
|
<span className="text-xs text-neutral-500 font-mono">TOTAL</span>
|
2026-02-08 02:32:45 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="absolute inset-0 flex items-center justify-center opacity-80 mt-10">
|
|
|
|
|
<canvas
|
|
|
|
|
ref={canvasRef}
|
|
|
|
|
style={{ width: 600, height: 600, maxWidth: '100%', aspectRatio: 1 }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|