testspace / frontend /src /components /Leaderboard.tsx
Mayug Maniparambil
Add admin leaderboard, LLM comparison, random puzzles, clearer rules
c52f47a
Raw
History Blame Contribute Delete
3.99 kB
import { useEffect, useState } from "react";
import { useAdmin } from "../lib/admin";
import { fetchLeaderboard, type Leaderboard as LeaderboardData } from "../lib/api";
function formatElapsed(ms: number | null) {
if (ms === null) {
return "—";
}
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60)
.toString()
.padStart(2, "0");
const seconds = (totalSeconds % 60).toString().padStart(2, "0");
const centiseconds = Math.floor((ms % 1000) / 10)
.toString()
.padStart(2, "0");
return `${minutes}:${seconds}.${centiseconds}`;
}
type Props = {
puzzleId: string;
};
export function Leaderboard({ puzzleId }: Props) {
const { enabled, token } = useAdmin();
const [data, setData] = useState<LeaderboardData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [includeTest, setIncludeTest] = useState(false);
useEffect(() => {
if (!enabled || !puzzleId) {
setData(null);
return;
}
let cancelled = false;
setLoading(true);
setError(null);
fetchLeaderboard(puzzleId, { includeTest })
.then((result) => {
if (!cancelled) {
setData(result);
}
})
.catch((caught) => {
if (!cancelled) {
setError(caught instanceof Error ? caught.message : "Could not load leaderboard.");
}
})
.finally(() => {
if (!cancelled) {
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [enabled, puzzleId, includeTest, token]);
if (!enabled) {
return null;
}
const entries = data?.entries ?? [];
const solvedCount = entries.filter((entry) => entry.solved).length;
return (
<section className="panel leaderboard-panel">
<div className="leaderboard-header">
<div>
<div className="eyebrow">Leaderboard · admin</div>
<div className="leaderboard-summary">
{loading
? "Loading…"
: `${entries.length} session${entries.length === 1 ? "" : "s"} · ${solvedCount} solved`}
</div>
</div>
<label className="leaderboard-toggle">
<input
type="checkbox"
checked={includeTest}
onChange={(event) => setIncludeTest(event.target.checked)}
/>
include <code>test</code>
</label>
</div>
{error ? <div className="status-box error">{error}</div> : null}
{!loading && entries.length === 0 ? (
<div className="leaderboard-empty">No sessions for this puzzle yet.</div>
) : null}
{entries.length > 0 ? (
<div className="leaderboard-table-wrap">
<table className="leaderboard-table">
<thead>
<tr>
<th>#</th>
<th>Player</th>
<th>Time</th>
<th>Attempts</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{entries.map((entry, index) => (
<tr
key={`${entry.player_name}-${entry.started_at}-${index}`}
className={entry.solved ? "row-solved" : "row-unsolved"}
>
<td className="leaderboard-rank">{entry.solved ? index + 1 : "—"}</td>
<td className="leaderboard-player">{entry.player_name}</td>
<td className="leaderboard-time">
{entry.solved ? formatElapsed(entry.elapsed_ms) : "—"}
</td>
<td className="leaderboard-attempts">{entry.submission_count}</td>
<td className="leaderboard-status">
{entry.solved ? "solved" : "attempted"}
</td>
</tr>
))}
</tbody>
</table>
</div>
) : null}
</section>
);
}