import { useEffect, useMemo, useState } from "react"; import { ChevronRight, ChevronDown } from "lucide-react"; import { useGameAPI } from "@/shared/api/GameAPIContext"; import type { LeaderboardSnapshot, UnsubscribeFn } from "@/shared/api/types"; // Smart precision algorithm for percentage display function calculatePrecision(percentages: number[]): number { if (percentages.length === 0) return 0; // Find the minimum non-zero difference between consecutive percentages const sorted = [...percentages].sort((a, b) => b - a); let minDiff = Infinity; for (let i = 0; i < sorted.length - 1; i++) { const diff = sorted[i] - sorted[i + 1]; if (diff > 0) { minDiff = Math.min(minDiff, diff); } } // If all percentages are the same, use 0 decimal places if (minDiff === Infinity) return 0; // Determine precision based on the minimum difference if (minDiff >= 0.1) return 0; // 0.1% or more difference -> 0 decimals if (minDiff >= 0.01) return 1; // 0.01% or more difference -> 1 decimal return 2; // 0.001% or more difference -> 2 decimals (max precision) } export function Leaderboard({ topN = 8, initialSnapshot, highlightedNation, onNationHover, }: { topN?: number; initialSnapshot?: LeaderboardSnapshot | null; highlightedNation: number | null; onNationHover: (nationId: number | null) => void; }) { const gameAPI = useGameAPI(); const [collapsed, setCollapsed] = useState(false); const [snapshot, setSnapshot] = useState(initialSnapshot || null); const [status, setStatus] = useState<"loading" | "waiting" | "ready" | "error">(initialSnapshot ? "ready" : "waiting"); useEffect(() => { let unsubscribe: UnsubscribeFn = () => {}; // Subscribe to leaderboard snapshots try { unsubscribe = gameAPI.onLeaderboardSnapshot((snapshotData) => { setSnapshot(snapshotData); setStatus("ready"); }); } catch (error) { console.warn("Failed to subscribe to leaderboard snapshots:", error); setStatus("error"); } return () => { unsubscribe(); }; }, [gameAPI]); const rows = useMemo(() => { if (!snapshot) return []; const topEntries = snapshot.entries.slice(0, topN); // Check if local player is in top N const playerEntry = snapshot.entries.find(e => e.id === snapshot.client_player_id); const playerInTopN = playerEntry && topEntries.some(e => e.id === playerEntry.id); // If player exists but not in top N, add them at the end if (playerEntry && !playerInTopN) { const playerPosition = snapshot.entries.findIndex(e => e.id === playerEntry.id); return [...topEntries, { ...playerEntry, position: playerPosition + 1 }]; } return topEntries; }, [snapshot, topN]); const precision = useMemo(() => { if (!snapshot || rows.length === 0) return 0; const percentages = rows.map((r) => r.territory_percent); return calculatePrecision(percentages); }, [snapshot, rows]); return (
onNationHover(null)}>
setCollapsed((c) => !c)}> Leaderboard {collapsed ? : }
{!collapsed && (
{status === "ready" && snapshot ? ( {rows.map((r) => { const position = "position" in r ? r.position : snapshot.entries.findIndex(e => e.id === r.id) + 1; const isPlayer = r.id === snapshot.client_player_id; const isEliminated = r.territory_percent === 0; const isHighlighted = highlightedNation === r.id; return ( !isEliminated && console.log("select", r.id)} onMouseEnter={() => !isEliminated && onNationHover(r.id)} onMouseLeave={() => !isEliminated && onNationHover(null)} > ); })}
{isEliminated ? '' : position}
{r.name}
{isEliminated ? '—' : `${(r.territory_percent * 100).toFixed(precision)}%`} {isEliminated ? '—' : r.troops.toLocaleString()}
) : (
{status === "loading" && "Loading leaderboard…"} {status === "waiting" && "Waiting for updates…"} {status === "error" && "Error loading leaderboard"}
)}
)}
); }