import { useMutation, useQuery } from '@tanstack/react-query'; import { Loader2, Play, Trophy } from 'lucide-react'; import { useParams } from 'react-router'; import { toast } from 'sonner'; import { useClient } from 'hooks/useClient'; import { isLevel, type Replay } from './levels'; /** * The saved games for whichever difficulty the URL names, best first. * * Rows are BUTTONS, not links, and that is a rule rather than a preference: a row here does not navigate * anywhere, it makes the game do something. `docs/navigation-audit.md` draws exactly that line — a control * that mutates stays a button, and only one that opens an addressable thing is an anchor. Which difficulty * is showing IS addressable, and that is the panel next door. * * Ordering comes from the server, which is where it belongs: a loss is never "best" however short it was, * so wins rank by time and losses fall to the end. Re-sorting here would be a second place to disagree. */ export const ReplaysPanel = () => { const { section } = useParams(); const client = useClient(); const level = isLevel(section) ? section : null; const { data, isLoading } = useQuery({ queryKey: ['minesweeper', 'replays', level], queryFn: () => client.get<{ replays: Replay[] }>(`/minesweeper/replays?level=${level}`), enabled: !!level, }); const play = useMutation({ mutationFn: (file: string) => client.post('/minesweeper/replay', { file }), onError: () => toast.error('The game would not open that recording.'), }); const replays = data?.replays ?? []; return (
{level ? `${replays.length} saved ${replays.length === 1 ? "game" : "games"}` : "Replays"}
{isLoading ? (

Loading…

) : replays.length === 0 ? (

No recordings for this level yet.

) : ( replays.map((replay, index) => { const pending = play.isPending && play.variables === replay.file; return ( ); }) )}
); };