import { Fragment, useEffect, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useGameStore } from '../store/gameStore'; import { emit, socket } from '../lib/socket'; import { useIsDesktop } from '../lib/useIsDesktop'; import type { GameDetail, GameDetailRound } from '../types'; function fmtDate(iso: string | null): string { if (!iso) return '—'; const d = new Date(iso); return isNaN(d.getTime()) ? '—' : d.toLocaleString('sk-SK'); } export default function History() { const navigate = useNavigate(); const history = useGameStore((s) => s.history); const detail = useGameStore((s) => s.gameDetail); const setGameDetail = useGameStore((s) => s.setGameDetail); useEffect(() => { emit.getPlayerHistory(); return () => setGameDetail(null); }, [setGameDetail]); // After a prematurely-ended game is reopened, the server confirms with // `game_restored`; jump to the lobby where it now shows as resumable. useEffect(() => { const onRestored = () => navigate('/'); socket.on('game_restored', onRestored); return () => { socket.off('game_restored', onRestored); }; }, [navigate]); // --- detail view --- if (detail) { return setGameDetail(null)} />; } // --- list view --- return (

Moja história

{history.length === 0 && (

Zatiaľ žiadne odohrané hry.

)}
{history.map((g) => (
{g.my_points} b. {!g.completed && ( )}
))}
); } const SEP = '1px solid rgba(201,168,76,.16)'; // vertical divider between players const DESKTOP_COLS = '28px repeat(8,1fr)'; // index + 4 players × 2 series columns const MOBILE_COLS = '28px repeat(4,1fr)'; // index + 4 players (one series stacked) /** Scoreboard-style detail. Desktop: 4 player columns in the header (total * beside the name), and under each a pair of series side by side — series 1 & * 2 on top, 3 & 4 below, separated by a blank row. Mobile: the same 4 player * columns, but the series stack one below another. Per round a cell shows the * points (hit bid) or the struck-through bid (missed → 0); each series ends * with a Σ total. */ function GameDetailView({ detail, onBack }: { detail: GameDetail; onBack: () => void }) { const desktop = useIsDesktop(); const seats = detail.players; // seat order 0..3 const seatIds = seats.map((p) => p.player_id); const totals = seatIds.map((pid) => detail.rounds.reduce((a, r) => (r.player_id === pid ? a + r.points : a), 0), ); // series -> round -> playerId -> round entry const bySeries = useMemo(() => { const m = new Map>>(); for (const r of detail.rounds) { let rounds = m.get(r.series_number); if (!rounds) m.set(r.series_number, (rounds = new Map())); let byPlayer = rounds.get(r.round_number); if (!byPlayer) rounds.set(r.round_number, (byPlayer = new Map())); byPlayer.set(r.player_id, r); } return m; }, [detail.rounds]); const seriesNums = [...bySeries.keys()].sort((a, b) => a - b); const roundsOf = (s: number | undefined) => s === undefined ? [] : [...(bySeries.get(s)?.keys() ?? [])]; const cellNode = (s: number | undefined, rn: number, seat: number) => { const r = s === undefined ? undefined : bySeries.get(s)?.get(rn)?.get(seatIds[seat]); if (!r) return null; return r.won ? ( {r.points} ) : ( {r.guess} ); }; const seriesTotal = (s: number | undefined, seat: number) => s === undefined ? '' : [...(bySeries.get(s)?.values() ?? [])].reduce( (a, byP) => a + (byP.get(seatIds[seat])?.points ?? 0), 0, ); // Header: player names with their grand total beside the name (both layouts). const header = (
{seats.map((p, c) => (
0 ? SEP : undefined, }} > {p.username} {totals[c]}
))}
); // A round row: index column + one cell per (player × series) in `cols`. const roundRow = (rn: number, cols: (number | undefined)[]) => (
{rn + 1}
{seats.map((_, c) => cols.map((s, si) => (
0 && si === 0 ? SEP : undefined }}> {cellNode(s, rn, c)}
)), )}
); // Σ row: per-series totals for each player. const sigmaRow = (cols: (number | undefined)[]) => (
Σ
{seats.map((_, c) => cols.map((s, si) => (
0 && si === 0 ? SEP : undefined }} > {seriesTotal(s, c)}
)), )}
); // Tiny row telling which series each sub-column is (desktop only — always a // fixed pair, sB may be absent for a trailing odd series). const seriesTags = (sA: number, sB: number | undefined) => (
{seats.map((_, c) => (
0 ? SEP : undefined }}> {sA + 1}
{sB !== undefined ? sB + 1 : ''}
))}
); // Desktop: pair series side by side (S1|S2, then S3|S4) with a blank row between. const desktopBody = (() => { const blocks: number[][] = []; for (let i = 0; i < seriesNums.length; i += 2) blocks.push(seriesNums.slice(i, i + 2)); return blocks.map((block, bi) => { const [sA, sB] = [block[0], block[1]]; const roundNums = [...new Set([...roundsOf(sA), ...roundsOf(sB)])].sort((a, b) => a - b); return ( {seriesTags(sA, sB)} {roundNums.map((rn) => roundRow(rn, [sA, sB]))} {sigmaRow([sA, sB])} {bi < blocks.length - 1 &&
} ); }); })(); // Mobile: one series per block, stacked vertically, 4 player columns each. const mobileBody = seriesNums.map((s, si) => { const roundNums = roundsOf(s).sort((a, b) => a - b); return (
Séria {s + 1}
{roundNums.map((rn) => roundRow(rn, [s]))} {sigmaRow([s])} {si < seriesNums.length - 1 &&
} ); }); return (

{detail.name || 'Detail hry'}

{fmtDate(detail.created_at)}

{/* Desktop packs 8 columns → allow horizontal scroll on narrow widths; mobile uses only 4 columns and fits the phone, so no scroll. */}
{header} {desktop ? desktopBody : mobileBody}
); }