import clsx from "clsx"; import { useEffect, useState } from "preact/hooks"; import { useHordeState } from "../../contexts/state"; import { fetchLeaderboard, type LeaderboardEntry } from "../../utils/api"; import { formatNumber, formatTime } from "@common/utils"; import { Modal } from "@common/components/modal/Modal"; import modalStyles from "../../assets/modal.module.css"; import styles from "../../assets/leaderboard-modal.module.css"; interface Props { open: boolean; onClose: () => void; } const PAGE_SIZE = 25; export const LeaderboardModal = ({ open, onClose }: Props) => { const { state } = useHordeState(); const { user, workers } = state; const ownWorkerIds = new Set(user?.worker_ids ?? []); const ownWorkers = workers.filter(w => ownWorkerIds.has(w.id)); const totalUptime = ownWorkers.reduce((s, w) => s + w.uptime, 0); const totalGenerated = ownWorkers.reduce((s, w) => s + (w.kudos_details?.generated ?? 0), 0); const kudosPerHour = totalUptime > 0 ? (totalGenerated / totalUptime) * 3600 : 0; const [rows, setRows] = useState<(LeaderboardEntry & { rank: number; diff?: number })[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!open) return; let cancelled = false; const load = async () => { setLoading(true); setError(null); try { if (!user) { const page = await fetchLeaderboard(1); if (!cancelled) { setRows(page.slice(0, 10).map((e, i) => ({ ...e, rank: i + 1 }))); } } else { let userRank = -1; let page = 1; outer: while (true) { const entries = await fetchLeaderboard(page); if (entries.length === 0) break; for (let i = 0; i < entries.length; i++) { if (entries[i].username === user.username) { userRank = (page - 1) * PAGE_SIZE + i + 1; break outer; } } page++; if (page > 20) break; } if (cancelled) return; const startRank = Math.max(1, userRank - 10); const startPage = Math.ceil(startRank / PAGE_SIZE); const endPage = Math.ceil(userRank / PAGE_SIZE); const collected: LeaderboardEntry[] = []; for (let p = startPage; p <= endPage; p++) { const entries = await fetchLeaderboard(p); collected.push(...entries); } if (cancelled) return; const userKudos = user.kudos; const result = collected .slice(startRank - (startPage - 1) * PAGE_SIZE - 1) .slice(0, userRank - startRank + 1) .map((e, i) => ({ ...e, rank: startRank + i, diff: e.username === user.username ? undefined : e.kudos - userKudos, })); setRows(result); } } catch (e) { if (!cancelled) setError(String(e)); } finally { if (!cancelled) setLoading(false); } }; load(); return () => { cancelled = true; }; }, [open, user?.username]); return (
Leaderboard
{loading &&

Loading…

} {error &&

{error}

} {!loading && !error && rows.length > 0 && ( {user && } {rows.map(r => ( {user && ( )} ))}
# Username KudosDiff
{r.rank} {r.username} {formatNumber(r.kudos)} 0 ? styles.above : styles.below))}> {r.diff !== undefined ? `+${formatNumber(r.diff)}` : '—'}
)}
{(() => { if (!user || rows.length < 2 || kudosPerHour <= 0) return null; const nextRow = rows[rows.length - 2]; if (!nextRow.diff || nextRow.diff <= 0) return null; const secs = Math.ceil((nextRow.diff / kudosPerHour) * 3600); return (
Time to #{nextRow.rank}: {formatTime(secs)} ({formatNumber(Math.round(kudosPerHour))} kudos/h)
); })()}
); };