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:
@@ -1,26 +1,66 @@
|
||||
import type { StashData } from '../types';
|
||||
import type { PlayerInfo, StashData } from '../types';
|
||||
import CardView from './CardView';
|
||||
|
||||
interface Props {
|
||||
stash: StashData | null;
|
||||
players: PlayerInfo[];
|
||||
myOrder: number;
|
||||
}
|
||||
|
||||
export default function Trick({ stash }: Props) {
|
||||
const cards = stash
|
||||
? [0, 1, 2, 3]
|
||||
.map((i) => (stash.first_player + i) % 4)
|
||||
.map((order) => stash.cards[String(order)])
|
||||
.filter(Boolean)
|
||||
const ROTATIONS = [-3, 2, -1, 1];
|
||||
// Entry direction by seat offset from me: 0=me(bottom) 1=left 2=top 3=right.
|
||||
const FLY_BY_OFFSET = ['fly-bottom', 'fly-left', 'fly-top', 'fly-right'];
|
||||
|
||||
export default function Trick({ stash, players, myOrder }: Props) {
|
||||
// Seat order, starting from whoever led the trick.
|
||||
const playOrder = stash
|
||||
? [0, 1, 2, 3].map((i) => (stash.first_player + i) % 4)
|
||||
: [];
|
||||
|
||||
const nameFor = (order: number) =>
|
||||
players.find((p) => p.order === order)?.name ?? '';
|
||||
|
||||
const overlap = -16;
|
||||
const slotH = 80;
|
||||
|
||||
if (!stash) {
|
||||
return <div className="flex items-center justify-center" style={{ minHeight: slotH + 14 }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-green-900/60 rounded-xl p-4">
|
||||
<p className="text-xs text-green-300 mb-3 text-center">Aktualny stich</p>
|
||||
<div className="flex gap-3 justify-center min-h-28">
|
||||
{cards.map((card, i) => (
|
||||
<CardView key={i} card={card} size="lg" />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
{playOrder.map((order, i) => {
|
||||
const card = stash.cards[String(order)];
|
||||
// Only render cards that have actually been played — no placeholder slot
|
||||
// for players still to play this trick.
|
||||
if (!card) return null;
|
||||
const offset = (order - myOrder + 4) % 4;
|
||||
return (
|
||||
<div
|
||||
key={order}
|
||||
className="relative flex flex-col items-center"
|
||||
style={{ marginLeft: i === 0 ? 0 : overlap, zIndex: i + 1 }}
|
||||
>
|
||||
<span
|
||||
className="uppercase text-center"
|
||||
style={{
|
||||
fontSize: 8,
|
||||
letterSpacing: '.05em',
|
||||
marginBottom: 3,
|
||||
color: 'rgba(216,203,166,.72)',
|
||||
}}
|
||||
>
|
||||
{nameFor(order)}
|
||||
</span>
|
||||
{/* Outer: flies in from the player's direction. Inner: static rotation. */}
|
||||
<div style={{ animation: `${FLY_BY_OFFSET[offset]} .42s cubic-bezier(.2,.7,.3,1) both` }}>
|
||||
<div style={{ transform: `rotate(${ROTATIONS[i]}deg)` }}>
|
||||
<CardView card={card} size="md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user