2c2f07c2ec
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>
50 lines
1.7 KiB
TypeScript
50 lines
1.7 KiB
TypeScript
import { useNavigate } from 'react-router-dom';
|
|
import type { PlayerInfo } from '../types';
|
|
import { computeTotal } from '../lib/standings';
|
|
import { leaveGame } from '../lib/leaveGame';
|
|
|
|
interface Props {
|
|
players: PlayerInfo[];
|
|
standings: number[][][];
|
|
}
|
|
|
|
export default function GameOver({ players, standings }: Props) {
|
|
const navigate = useNavigate();
|
|
|
|
const totals = players
|
|
.map((p) => ({ ...p, total: computeTotal(standings, p.order) }))
|
|
.sort((a, b) => b.total - a.total);
|
|
|
|
const handleLeave = () => leaveGame(navigate);
|
|
|
|
const medals = ['🥇', '🥈', '🥉', ''];
|
|
|
|
return (
|
|
<div className="max-w-md mx-auto p-4 pt-12 flex flex-col items-center gap-6 min-h-screen">
|
|
<h1 className="font-serif text-3xl text-gold tracking-wide">Koniec hry</h1>
|
|
<div className="w-full rounded-2xl overflow-hidden bg-header border border-[#142018]">
|
|
{totals.map((p, i) => (
|
|
<div
|
|
key={p.order}
|
|
className="flex items-center justify-between px-5 py-3 border-b border-gold/[.08] last:border-0"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-2xl w-8">{medals[i]}</span>
|
|
<span className="font-serif text-green-score">{p.name}</span>
|
|
</div>
|
|
<span className={`font-serif text-xl ${i === 0 ? 'text-gold-bright' : 'text-gold'}`}>
|
|
{p.total}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<button
|
|
onClick={handleLeave}
|
|
className="w-full py-3 rounded-xl bg-gold text-table font-serif font-semibold text-lg hover:bg-gold-bright transition-colors"
|
|
>
|
|
Domov
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|