"use client"; import { useQuery } from "@tanstack/react-query"; import { useMemo, useState } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { PlayerAvatar } from "@/components/PlayerAvatar"; import { formatHours, timeAgo } from "@/lib/time"; import type { PlayerStats } from "@/lib/player-stats"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; type MetricKey = | "playtimeHours" | "distanceKm" | "mobKills" | "playerKills" | "deaths" | "kdr" | "damageDealt" | "damageTaken" | "blocksMined" | "itemsUsed" | "itemsCrafted" | "itemsPickedUp" | "uniqueItemsPickedUp" | "advancements" | "recipesUnlocked" | "chestOpens" | "netherPortalEnters" | "endPortalEnters" | "villagerTrades" | "fishCaught" | "animalsBred" | "longestLifeHours"; type Metric = { key: MetricKey; label: string; group: string; format: (v: number) => string; suffix?: string; }; const METRICS: Metric[] = [ { key: "playtimeHours", label: "Playtime", group: "Time", format: (v) => formatHours(v) }, { key: "longestLifeHours", label: "Longest life", group: "Time", format: (v) => formatHours(v) }, { key: "mobKills", label: "Mob kills", group: "Combat", format: fmtInt }, { key: "playerKills", label: "Player kills", group: "Combat", format: fmtInt }, { key: "deaths", label: "Deaths", group: "Combat", format: fmtInt }, { key: "kdr", label: "K/D ratio", group: "Combat", format: (v) => v.toFixed(2) }, { key: "damageDealt", label: "Damage dealt", group: "Combat", format: fmtInt, suffix: " HP" }, { key: "damageTaken", label: "Damage taken", group: "Combat", format: fmtInt, suffix: " HP" }, { key: "blocksMined", label: "Blocks mined", group: "World", format: fmtInt }, { key: "itemsUsed", label: "Items used / placed", group: "World", format: fmtInt }, { key: "itemsCrafted", label: "Items crafted", group: "World", format: fmtInt }, { key: "itemsPickedUp", label: "Items picked up", group: "World", format: fmtInt }, { key: "uniqueItemsPickedUp", label: "Unique items seen", group: "World", format: fmtInt }, { key: "distanceKm", label: "Distance traveled", group: "Exploration", format: (v) => v.toFixed(1), suffix: " km" }, { key: "chestOpens", label: "Chests opened", group: "Exploration", format: fmtInt }, { key: "netherPortalEnters", label: "Nether trips", group: "Exploration", format: fmtInt }, { key: "endPortalEnters", label: "End trips", group: "Exploration", format: fmtInt }, { key: "advancements", label: "Advancements", group: "Progression", format: fmtInt }, { key: "recipesUnlocked", label: "Recipes unlocked", group: "Progression", format: fmtInt }, { key: "villagerTrades", label: "Villager trades", group: "Economy", format: fmtInt }, { key: "fishCaught", label: "Fish caught", group: "Economy", format: fmtInt }, { key: "animalsBred", label: "Animals bred", group: "Economy", format: fmtInt }, ]; function fmtInt(v: number): string { if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`; if (v >= 10_000) return `${(v / 1_000).toFixed(1)}k`; return v.toLocaleString(); } const TOP_N = 10; export function Leaderboard() { const [metricKey, setMetricKey] = useState("playtimeHours"); const [showAll, setShowAll] = useState(false); const { data = [], isLoading, isError } = useQuery({ queryKey: ["players-stats"], queryFn: () => fetch("/api/players/stats").then((r) => r.json()), staleTime: 60_000, refetchInterval: 5 * 60_000, }); const metric = METRICS.find((m) => m.key === metricKey)!; const sorted = useMemo(() => { return [...data].sort( (a, b) => (b[metricKey] as number) - (a[metricKey] as number) ); }, [data, metricKey]); const visible = showAll ? sorted : sorted.slice(0, TOP_N); const groups = useMemo(() => { const g = new Map(); for (const m of METRICS) { if (!g.has(m.group)) g.set(m.group, []); g.get(m.group)!.push(m); } return g; }, []); return (
Leaderboard Ranked by{" "} {metric.label}
{isLoading ? (
    {Array.from({ length: 5 }).map((_, i) => (
  • ))}
) : isError ? (

Failed to load stats.

) : sorted.length === 0 ? (

No player stats yet.

) : ( <>
    {visible.map((p, i) => { const raw = p[metricKey] as number; const display = `${metric.format(raw)}${metric.suffix || ""}`; return (
  1. {i + 1}

    {p.name}

    last seen {timeAgo(p.lastPlayedMs)}

    {display}
  2. ); })}
{sorted.length > TOP_N && (
)} )}
); }