Skore: scrollovatelny zoznam s collapse serii, zrusenie preciarkavania

Standings.tsx rozdeleny na fixnu hlavicku, scrollovatelny zoznam kol
(overflow-y-auto, na mobile max-h-45vh) a fixne sucty, takze pri viacerych
odohratych kolach je vidno cely priebeh. Dokoncena seria (riadok Sigma) je
teraz klikatelna a zbaluje/rozbaluje svoje kola.

Zaroven zruseny line-through pri neuspesnom tipe v hernom pohlade aj v
detaile hry (History.tsx) -- neuspesny tip sa teraz len zobrazuje tlmenou farbou.
This commit is contained in:
tim
2026-07-07 00:21:34 +02:00
parent 9ade92d7fe
commit 80929e7fed
2 changed files with 86 additions and 63 deletions
+61 -38
View File
@@ -14,6 +14,15 @@ interface Props {
export default function Standings({ standings, guesses = [], players, myOrder, desktop = false }: Props) { export default function Standings({ standings, guesses = [], players, myOrder, desktop = false }: Props) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
// Finished series (8/8 rounds) collapsed to just their Σ row; toggled per series.
const [collapsedSeries, setCollapsedSeries] = useState<Set<number>>(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. // Player columns in seat order; the local player's column is highlighted.
const cols = [...players].sort((a, b) => a.order - b.order); const cols = [...players].sort((a, b) => a.order - b.order);
@@ -39,13 +48,11 @@ export default function Standings({ standings, guesses = [], players, myOrder, d
total: desktop ? 20 : 18, total: desktop ? 20 : 18,
}; };
const table = ( const gridCols = { gridTemplateColumns: `28px repeat(${cols.length}, 1fr)` };
<div className="flex-1 flex flex-col px-3 pt-3 pb-4">
{/* Column headers */} const columnHeader = (
<div <div className="px-3 pt-3 flex-shrink-0">
className="grid items-end mb-1" <div className="grid items-end mb-1" style={gridCols}>
style={{ gridTemplateColumns: `28px repeat(${cols.length}, 1fr)` }}
>
<div /> <div />
{cols.map((p) => ( {cols.map((p) => (
<div <div
@@ -59,28 +66,31 @@ export default function Standings({ standings, guesses = [], players, myOrder, d
</div> </div>
))} ))}
</div> </div>
<div className="h-px bg-gold/10 mb-1" /> <div className="h-px bg-gold/10" />
</div>
);
{/* Completed rounds, grouped by series with a per-series summary row */} // Completed rounds, grouped by series with a per-series summary row. Finished
{standings.flatMap((seriesRounds, si) => { // series can be collapsed to just their Σ row so long games stay scannable.
const rows = standings.flatMap((seriesRounds, si) => {
const priorRounds = seriesRoundOffsets[si]; const priorRounds = seriesRoundOffsets[si];
const elems = seriesRounds.map((scores, lri) => ( const isFinished = seriesRounds.length === ROUNDS_PER_SERIES;
<div const isCollapsed = isFinished && collapsedSeries.has(si);
key={`r-${si}-${lri}`} const elems = isCollapsed
className="grid items-center py-1 border-b border-gold/[.05]" ? []
style={{ gridTemplateColumns: `28px repeat(${cols.length}, 1fr)` }} : seriesRounds.map((scores, lri) => (
> <div key={`r-${si}-${lri}`} className="grid items-center py-1 border-b border-gold/[.05]" style={gridCols}>
<div className="text-center text-[#7a7252]" style={{ fontSize: fz.idx }}> <div className="text-center text-[#7a7252]" style={{ fontSize: fz.idx }}>
{priorRounds + lri + 1} {priorRounds + lri + 1}
</div> </div>
{cols.map((p) => { {cols.map((p) => {
const points = scores[p.order] ?? 0; const points = scores[p.order] ?? 0;
// Failed tip (0 points) → show the struck-through tip instead of 0. // Failed tip → show the tip in the dimmer "0 points" color, no strikethrough.
if (points === 0) { if (points === 0) {
return ( return (
<div <div
key={p.order} key={p.order}
className="text-center font-serif leading-none line-through" className="text-center font-serif leading-none"
style={{ fontSize: fz.cell, color: '#7a6e4a' }} style={{ fontSize: fz.cell, color: '#7a6e4a' }}
> >
{guesses[si]?.[lri]?.[p.order] ?? 0} {guesses[si]?.[lri]?.[p.order] ?? 0}
@@ -100,15 +110,21 @@ export default function Standings({ standings, guesses = [], players, myOrder, d
</div> </div>
)); ));
// After a finished series, sum its points per player. // After a finished series, sum its points per player. Clicking toggles
if (seriesRounds.length === ROUNDS_PER_SERIES) { // whether that series' individual rounds are shown.
if (isFinished) {
elems.push( elems.push(
<div <div
key={`s-${si}`} key={`s-${si}`}
className="grid items-center py-1 my-0.5 rounded bg-gold/[.07]" role="button"
style={{ gridTemplateColumns: `28px repeat(${cols.length}, 1fr)` }} tabIndex={0}
onClick={() => 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}
> >
<div className="text-center font-serif text-gold" style={{ fontSize: fz.sigma }}> <div className="text-center font-serif text-gold flex items-center justify-center gap-[2px]" style={{ fontSize: fz.sigma }}>
<span className="text-[8px] text-green-dim">{isCollapsed ? '▸' : '▾'}</span>
Σ{si + 1} Σ{si + 1}
</div> </div>
{cols.map((p) => { {cols.map((p) => {
@@ -129,27 +145,26 @@ export default function Standings({ standings, guesses = [], players, myOrder, d
); );
} }
return elems; return elems;
})} });
const scrollableRounds = (
<div className={`flex-1 min-h-0 overflow-y-auto px-3 ${desktop ? '' : 'max-h-[45vh]'}`}>
{rows}
{/* Active round placeholder */} {/* Active round placeholder */}
<div <div className="grid items-center py-1 rounded mt-0.5 bg-gold/[.04]" style={gridCols}>
className="grid items-center py-1 rounded mt-0.5 bg-gold/[.04]"
style={{ gridTemplateColumns: `28px repeat(${cols.length}, 1fr)` }}
>
<div className="text-center font-medium text-gold" style={{ fontSize: fz.idx }}>{completedRounds + 1}</div> <div className="text-center font-medium text-gold" style={{ fontSize: fz.idx }}>{completedRounds + 1}</div>
{cols.map((p) => ( {cols.map((p) => (
<div key={p.order} className="text-center text-[#7a7252]" style={{ fontSize: fz.dot }}>·</div> <div key={p.order} className="text-center text-[#7a7252]" style={{ fontSize: fz.dot }}>·</div>
))} ))}
</div> </div>
</div>
);
<div className="flex-1 min-h-2" /> const totalsBlock = (
<div className="px-3 pb-4 pt-2 flex-shrink-0">
<div className="h-px bg-gold/20 mb-2" /> <div className="h-px bg-gold/20 mb-2" />
<div className="grid items-center py-0.5" style={gridCols}>
{/* Totals */}
<div
className="grid items-center py-0.5"
style={{ gridTemplateColumns: `28px repeat(${cols.length}, 1fr)` }}
>
<div className="text-center uppercase tracking-[.08em] text-green-dim" style={{ fontSize: fz.sigma }}> <div className="text-center uppercase tracking-[.08em] text-green-dim" style={{ fontSize: fz.sigma }}>
Σ Σ
</div> </div>
@@ -168,13 +183,21 @@ export default function Standings({ standings, guesses = [], players, myOrder, d
</div> </div>
); );
const content = (
<div className="flex-1 min-h-0 flex flex-col">
{columnHeader}
{scrollableRounds}
{totalsBlock}
</div>
);
if (desktop) { if (desktop) {
return ( return (
<aside className="w-[268px] flex-shrink-0 bg-header border-l border-[#142018] flex flex-col"> <aside className="w-[268px] flex-shrink-0 bg-header border-l border-[#142018] flex flex-col">
<div className="h-[58px] flex items-center gap-2 px-5 border-b border-[#14221a]"> <div className="h-[58px] flex items-center gap-2 px-5 border-b border-[#14221a] flex-shrink-0">
<span className="font-serif uppercase tracking-[.12em] text-[13px] text-gold">Skóre</span> <span className="font-serif uppercase tracking-[.12em] text-[13px] text-gold">Skóre</span>
</div> </div>
{table} {content}
</aside> </aside>
); );
} }
@@ -189,7 +212,7 @@ export default function Standings({ standings, guesses = [], players, myOrder, d
<span>Skóre</span> <span>Skóre</span>
<span className="text-green-dim">{open ? '▲' : '▼'}</span> <span className="text-green-dim">{open ? '▲' : '▼'}</span>
</button> </button>
{open && table} {open && content}
</div> </div>
); );
} }
+1 -1
View File
@@ -128,7 +128,7 @@ function GameDetailView({ detail, onBack }: { detail: GameDetail; onBack: () =>
return r.won ? ( return r.won ? (
<span className="font-serif" style={{ fontSize: 14, color: '#c8bb95' }}>{r.points}</span> <span className="font-serif" style={{ fontSize: 14, color: '#c8bb95' }}>{r.points}</span>
) : ( ) : (
<span className="font-serif line-through" style={{ fontSize: 14, color: '#7a6e4a' }}>{r.guess}</span> <span className="font-serif" style={{ fontSize: 14, color: '#7a6e4a' }}>{r.guess}</span>
); );
}; };
const seriesTotal = (s: number | undefined, seat: number) => const seriesTotal = (s: number | undefined, seat: number) =>