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:
@@ -0,0 +1,43 @@
|
||||
interface Props {
|
||||
/** Exact number of cards the player still holds. */
|
||||
count: number;
|
||||
/** Row (top player) or column (side players). */
|
||||
direction: 'row' | 'col';
|
||||
desktop?: boolean;
|
||||
}
|
||||
|
||||
/** Overlapping fan of face-down cards next to an opponent's circle — one card
|
||||
* per card still in their hand, so the stack shrinks as they play. */
|
||||
export default function FaceDownCards({ count, direction, desktop = false }: Props) {
|
||||
const n = Math.max(0, Math.min(count, 8));
|
||||
if (n === 0) return null;
|
||||
|
||||
const row = direction === 'row';
|
||||
const w = desktop ? 40 : 25;
|
||||
const h = desktop ? 56 : 36;
|
||||
const overlap = row ? Math.round(w * 0.45) : Math.round(h * 0.5);
|
||||
|
||||
return (
|
||||
<div className="flex" style={{ flexDirection: row ? 'row' : 'column' }}>
|
||||
{Array.from({ length: n }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
width: w,
|
||||
height: h,
|
||||
marginLeft: row && i > 0 ? -overlap : 0,
|
||||
marginTop: !row && i > 0 ? -overlap : 0,
|
||||
zIndex: i,
|
||||
background:
|
||||
i % 2 === 0
|
||||
? 'linear-gradient(150deg,#1d4a28,#0e2818)'
|
||||
: 'linear-gradient(150deg,#1b4424,#0d2616)',
|
||||
borderRadius: 3,
|
||||
border: '1px solid rgba(201,168,76,.18)',
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,.5)',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user