import { useState } from 'react'; import type { PlayerInfo } from '../types'; import { computeTotal } from '../lib/standings'; import { displayName } from '../lib/names'; import { STANDINGS_FONT } from '../lib/boardTheme'; interface Props { standings: number[][][]; /** Tips per series/round/seat, same shape as standings. */ guesses?: number[][][]; players: PlayerInfo[]; myOrder: number; /** Desktop renders an always-open sidebar; mobile a collapsible panel. */ desktop?: boolean; } export default function Standings({ standings, guesses = [], players, myOrder, desktop = false }: Props) { const [open, setOpen] = useState(false); // Finished series (8/8 rounds) collapsed to just their Σ row; toggled per series. const [collapsedSeries, setCollapsedSeries] = useState>(new Set()); const toggleSeries = (si: number) => setCollapsedSeries((prev) => { const next = new Set(prev); if (next.has(si)) next.delete(si); else next.add(si); return next; }); // Player columns in seat order; the local player's column is highlighted. const cols = [...players].sort((a, b) => a.order - b.order); // Completed-round count before each series (running total), so each series' // round index can be offset in a single pass instead of re-summing per row. const seriesRoundOffsets: number[] = []; let completedRounds = 0; for (const s of standings) { seriesRoundOffsets.push(completedRounds); completedRounds += s.length; } // Engine: every series is exactly 8 rounds → a series with 8 entries is done, // and gets a per-series summary row after its last round. const ROUNDS_PER_SERIES = 8; // Bigger, more legible type on the wide desktop sidebar; compact on mobile. // Sizes live in one place (boardTheme.ts) alongside the rest of the board. const fz = desktop ? STANDINGS_FONT.desktop : STANDINGS_FONT.mobile; const gridCols = { gridTemplateColumns: `28px repeat(${cols.length}, 1fr)` }; const columnHeader = (
{cols.map((p) => (
{displayName(p.name)}
))}
); // Completed rounds, grouped by series with a per-series summary row. Finished // series can be collapsed to just their Σ row so long games stay scannable. const rows = standings.flatMap((seriesRounds, si) => { const priorRounds = seriesRoundOffsets[si]; const isFinished = seriesRounds.length === ROUNDS_PER_SERIES; const isCollapsed = isFinished && collapsedSeries.has(si); const elems = isCollapsed ? [] : seriesRounds.map((scores, lri) => (
{priorRounds + lri + 1}
{cols.map((p) => { const points = scores[p.order] ?? 0; // Failed tip → show the tip in the dimmer "0 points" color, no strikethrough. if (points === 0) { return (
{guesses[si]?.[lri]?.[p.order] ?? 0}
); } return (
{points}
); })}
)); // After a finished series, sum its points per player. Clicking toggles // whether that series' individual rounds are shown. if (isFinished) { elems.push(
toggleSeries(si)} onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && toggleSeries(si)} className="grid items-center py-1 my-0.5 rounded bg-gold/[.07] cursor-pointer select-none" style={gridCols} >
{isCollapsed ? '▸' : '▾'} Σ{si + 1}
{cols.map((p) => { const sum = seriesRounds.reduce((a, r) => a + (r[p.order] ?? 0), 0); return (
{sum}
); })}
, ); } return elems; }); const scrollableRounds = (
{rows} {/* Active round placeholder */}
{completedRounds + 1}
{cols.map((p) => (
·
))}
); const totalsBlock = (
Σ
{cols.map((p) => (
{computeTotal(standings, p.order)}
))}
); const content = (
{columnHeader} {scrollableRounds} {totalsBlock}
); if (desktop) { return ( ); } // Mobile: collapsible panel return (
{open && content}
); }