import { useEffect, useRef } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Navigate, NavLink, useParams } from 'react-router'; import { toast } from 'sonner'; import { useClient } from 'hooks/useClient'; import { LEVELS, isLevel, type Level } from './levels'; /** * The difficulty list — and the URL is what holds the choice. * * `/minesweeper/expert` rather than a channel or panel state, because it is exactly the kind of thing * `docs/navigation-audit.md` calls addressable: it survives a refresh, it can be linked, and the replay * panel reads it from the URL instead of having it passed across. Rows are real `NavLink`s, so cmd-click * and middle-click behave and the active style comes from the router rather than a hand-rolled compare. * * The SIDE EFFECT hangs off the URL too. An effect on the selected level posts the difficulty, so * arriving at `/minesweeper/expert` by any route — click, deep link, back button — leaves the game on * expert. `POST /difficulty` verifies and is idempotent, so a repeat costs a round trip and changes * nothing. */ export const LevelsPanel = () => { const { section } = useParams(); const client = useClient(); // useClient() returns fresh verbs every render; a ref keeps the effect below from re-running forever. const clientRef = useRef(client); clientRef.current = client; const selected = isLevel(section) ? section : null; const { data: status } = useQuery({ queryKey: ['minesweeper', 'status'], queryFn: () => client.get<{ difficulty: Level | null; running: boolean }>('/minesweeper/status'), }); useEffect(() => { if (!selected) return; clientRef.current .post('/minesweeper/difficulty', { level: selected }) .catch(() => toast.error(`Could not switch the game to ${selected}.`)); }, [selected]); // After the hooks, never before: one guard canonicalising both the bare route and a bogus section. // It waits for status so the URL lands on what the game IS rather than on a guess. if (!selected) { if (!status) return ; return ; } return ( {LEVELS.map((level) => ( { if (level.key === selected) void clientRef.current.post('/minesweeper/difficulty', { level: level.key }); }} className={({ isActive }) => `rounded-md px-3 py-2 text-sm transition-colors ${ isActive ? 'bg-accent font-medium text-accent-foreground' : 'text-muted-foreground hover:bg-accent/50' }` } > {level.label} ))} ); }; const Shell = ({ children }: { children?: React.ReactNode }) => (
{children}
);