105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useTranslations } from "next-intl";
|
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|
import { motion } from "framer-motion";
|
|
|
|
interface RegionData {
|
|
name: string;
|
|
count: number;
|
|
color: string;
|
|
}
|
|
|
|
const regionColors: Record<string, string> = {
|
|
Asia: "var(--accent-cyan)",
|
|
Europe: "var(--accent-purple)",
|
|
Americas: "var(--accent-pink)",
|
|
Africa: "var(--accent-orange)",
|
|
Oceania: "var(--accent-green)",
|
|
};
|
|
|
|
const regionNameToKey: Record<string, string> = {
|
|
Asia: "asia",
|
|
Europe: "europe",
|
|
Americas: "americas",
|
|
Africa: "africa",
|
|
Oceania: "oceania",
|
|
};
|
|
|
|
export function RegionRanking() {
|
|
const t = useTranslations("regionRanking");
|
|
const tContinents = useTranslations("continents");
|
|
const [regions, setRegions] = useState<RegionData[]>([]);
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
const res = await fetch("/api/v1/stats");
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
const breakdown = (data.regionBreakdown ?? {}) as Record<string, number>;
|
|
const sorted = Object.entries(breakdown)
|
|
.map(([name, count]) => ({
|
|
name,
|
|
count,
|
|
color: regionColors[name] ?? "var(--accent-cyan)",
|
|
}))
|
|
.sort((a, b) => b.count - a.count);
|
|
setRegions(sorted);
|
|
}
|
|
} catch {
|
|
// retry on interval
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
const interval = setInterval(fetchData, 15000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const maxCount = Math.max(...regions.map((r) => r.count), 1);
|
|
|
|
return (
|
|
<Card className="border-white/5">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle>{t("title")}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3 p-4 pt-0">
|
|
{regions.length === 0 ? (
|
|
<p className="py-4 text-center text-xs text-[var(--text-muted)]">{t("noData")}</p>
|
|
) : (
|
|
regions.map((region, i) => (
|
|
<div key={region.name} className="space-y-1">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-mono text-xs text-[var(--text-muted)]">
|
|
#{i + 1}
|
|
</span>
|
|
<span className="text-sm font-medium" style={{ color: region.color }}>
|
|
{regionNameToKey[region.name]
|
|
? tContinents(regionNameToKey[region.name] as "asia" | "europe" | "americas" | "africa" | "oceania")
|
|
: region.name}
|
|
</span>
|
|
</div>
|
|
<span className="font-mono text-xs text-[var(--text-secondary)]">
|
|
{region.count}
|
|
</span>
|
|
</div>
|
|
<div className="h-1.5 overflow-hidden rounded-full bg-white/5">
|
|
<motion.div
|
|
className="h-full rounded-full"
|
|
style={{ backgroundColor: region.color }}
|
|
initial={{ width: 0 }}
|
|
animate={{ width: `${(region.count / maxCount) * 100}%` }}
|
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|