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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user