'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) => (
))}
)}
);
}