Apply velvet-table redesign, fix game lifecycle and history bugs

Frontend:
- Dark green/gold "velvet table" visual redesign across the whole app
  (Auth, Lobby, GameList, GameTable, History, GameOver, modals), with
  Playfair Display/DM Sans typography and a centralized Tailwind palette.
- Desktop game table fit-scales to fill the window; mobile gets
  overlapping hand/trick layouts and larger touch-friendly cards.
- Standings sidebar now groups completed rounds by series with a
  per-series subtotal row, struck-through tips on missed bids.
- History page rewritten into a scoreboard-style detail view (player
  totals beside names, series grouped 2-up on desktop / stacked on
  mobile) and gained game names, completed/abandoned status, and a
  button to reopen a prematurely-ended game back into the lobby.

Backend:
- Fix started games being deleted from memory (and vanishing from
  everyone's lobby) when all players disconnect; only `end_game` tears
  down a started game now.
- Fix a crash writing a timezone-aware datetime into the naive
  `ended_at` Postgres column.
- Add `reopen_game`/`restore_game` to un-end a prematurely-ended game
  from history and resume it from the lobby.
- Let any seated player end an abandoned game once the host is
  offline, not just the host, so the game isn't stuck forever.
- Expose SERIES_PER_GAME/ROUNDS_PER_SERIES as named constants on the
  engine so the persistence layer derives game-completion rules from
  bridzik.py instead of re-encoding them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tim
2026-07-01 00:11:42 +02:00
parent 30c32b7714
commit 2c2f07c2ec
28 changed files with 1472 additions and 395 deletions
+16 -16
View File
@@ -25,37 +25,37 @@ export default function Lobby() {
};
return (
<div className="max-w-md mx-auto p-4 pt-8">
<div className="max-w-md mx-auto p-4 pt-8 min-h-screen">
<div className="flex items-center justify-between mb-6">
<h1 className="text-xl font-bold">{game?.name ?? 'Hra'}</h1>
<button onClick={handleLeave} className="text-sm text-gray-400 hover:text-white">
Odist
<h1 className="font-serif text-2xl text-gold">{game?.name ?? 'Hra'}</h1>
<button onClick={handleLeave} className="text-sm text-green-dim hover:text-gold">
Odísť
</button>
</div>
<div className="bg-slate-800 rounded-xl p-4 mb-4 flex items-center justify-between">
<div className="bg-header border border-[#142018] rounded-xl p-4 mb-4 flex items-center justify-between">
<div>
<p className="text-xs text-gray-400 mb-1">Kod hry</p>
<p className="font-mono text-sm text-gray-200 break-all">{gid}</p>
<p className="text-xs uppercase tracking-[.1em] text-green-dim mb-1">Kód hry</p>
<p className="font-mono text-sm text-green-score break-all">{gid}</p>
</div>
<button
onClick={handleCopyCode}
className="ml-3 px-3 py-1 rounded-lg text-sm bg-slate-700 hover:bg-slate-600"
className="ml-3 px-3 py-1 rounded-lg text-sm border border-gold/30 text-gold hover:bg-gold hover:text-table transition-colors"
>
Kopirovat
Kopírovať
</button>
</div>
<div className="bg-slate-800 rounded-xl p-4 mb-6 flex flex-col gap-3">
<div className="bg-header border border-[#142018] rounded-xl p-4 mb-6 flex flex-col gap-3">
{[0, 1, 2, 3].map((order) => {
const p = players.find((pl) => pl.order === order);
return (
<div key={order} className="flex items-center gap-3">
<span className={`text-lg ${p ? 'text-green-400' : 'text-gray-600'}`}>
{p ? '' : '○'}
<span className={`text-lg ${p ? 'text-gold' : 'text-[#7a7058]'}`}>
{p ? '' : '○'}
</span>
<span className={p ? 'text-white' : 'text-gray-500 italic'}>
{p ? `${p.name}${myPlayer?.order === p.order ? ' (ty)' : ''}` : 'Caka sa...'}
<span className={p ? 'font-serif text-green-score' : 'text-green-dim italic'}>
{p ? `${p.name}${myPlayer?.order === p.order ? ' (ty)' : ''}` : 'Čaká sa'}
</span>
</div>
);
@@ -65,9 +65,9 @@ export default function Lobby() {
<button
disabled={!canStart}
onClick={() => gid && emit.startGame(gid)}
className="w-full py-3 rounded-xl bg-green-700 hover:bg-green-600 font-bold text-lg disabled:opacity-40 disabled:cursor-default"
className="w-full py-3 rounded-xl bg-gold text-table font-serif font-semibold text-lg disabled:opacity-40 disabled:cursor-default hover:bg-gold-bright transition-colors"
>
{isHost ? (canStart ? 'Zacat hru' : `Caka sa na hracov (${players.length}/4)`) : 'Caka sa na hosta...'}
{isHost ? (canStart ? 'Začať hru' : `Čaká sa na hráčov (${players.length}/4)`) : 'Čaká sa na hostiteľa…'}
</button>
</div>
);