Keep last round's summary visible until all bids are placed

The round summary used to disappear after a fixed 5 seconds. It is now
derived from the game state: the previous round's result is shown while
the new round is in bidding and hides once card play starts. This also
survives a page reload.

- Drop the client-side round timer from useWizard
- On narrow screens show the summary in the page flow so it does not
  cover the bid buttons

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 10:40:13 +02:00
co-authored by Claude Opus 5.5
parent 21a2b044d9
commit 87151c2cc9
4 changed files with 17 additions and 29 deletions
+1 -3
View File
@@ -13,8 +13,7 @@ const STATUS_MESSAGES = {
} as const } as const
function App() { function App() {
const { status, playerId, lobby, game, completedTrick, completedRound, toasts, dismissToast, actions } = const { status, playerId, lobby, game, completedTrick, toasts, dismissToast, actions } = useWizard()
useWizard()
const lobbyCode = lobby?.code const lobbyCode = lobby?.code
// Keep the lobby code in the address bar so the URL can be shared as an invite link. // Keep the lobby code in the address bar so the URL can be shared as an invite link.
@@ -38,7 +37,6 @@ function App() {
game={game} game={game}
playerId={playerId} playerId={playerId}
completedTrick={completedTrick} completedTrick={completedTrick}
completedRound={completedRound}
actions={actions} actions={actions}
/> />
) )
+2 -17
View File
@@ -5,7 +5,6 @@ import type {
GameOptions, GameOptions,
GameView, GameView,
Lobby, Lobby,
RoundResult,
ServerMessage, ServerMessage,
TrickResult, TrickResult,
} from './types' } from './types'
@@ -20,7 +19,6 @@ export interface Toast {
// Per tab, so several tabs can act as different players; survives reloads. // Per tab, so several tabs can act as different players; survives reloads.
const TOKEN_KEY = 'wizard.token' const TOKEN_KEY = 'wizard.token'
const TRICK_DISPLAY_MS = 2200 const TRICK_DISPLAY_MS = 2200
const ROUND_DISPLAY_MS = 5000
const TOAST_MS = 4000 const TOAST_MS = 4000
const MAX_RETRY_DELAY_MS = 10000 const MAX_RETRY_DELAY_MS = 10000
// Close code the server uses when the session was opened in another tab. // Close code the server uses when the session was opened in another tab.
@@ -60,8 +58,6 @@ export function useWizard() {
const [game, setGame] = useState<GameView | null>(null) const [game, setGame] = useState<GameView | null>(null)
// The most recently completed trick, shown for a moment before it is cleared. // The most recently completed trick, shown for a moment before it is cleared.
const [completedTrick, setCompletedTrick] = useState<TrickResult | null>(null) const [completedTrick, setCompletedTrick] = useState<TrickResult | null>(null)
// Result of the round that just ended, shown as a short summary.
const [completedRound, setCompletedRound] = useState<RoundResult | null>(null)
const [toasts, setToasts] = useState<Toast[]>([]) const [toasts, setToasts] = useState<Toast[]>([])
const socketRef = useRef<WebSocket | null>(null) const socketRef = useRef<WebSocket | null>(null)
@@ -82,7 +78,6 @@ export function useWizard() {
let attempt = 0 let attempt = 0
let retryTimer: ReturnType<typeof setTimeout> | undefined let retryTimer: ReturnType<typeof setTimeout> | undefined
let trickTimer: ReturnType<typeof setTimeout> | undefined let trickTimer: ReturnType<typeof setTimeout> | undefined
let roundTimer: ReturnType<typeof setTimeout> | undefined
const handleMessage = (message: ServerMessage) => { const handleMessage = (message: ServerMessage) => {
switch (message.type) { switch (message.type) {
@@ -99,16 +94,7 @@ export function useWizard() {
clearTimeout(trickTimer) clearTimeout(trickTimer)
trickTimer = setTimeout(() => setCompletedTrick(null), TRICK_DISPLAY_MS) trickTimer = setTimeout(() => setCompletedTrick(null), TRICK_DISPLAY_MS)
} }
const round = message.events.find((event) => event.type === 'round_completed') if (!message.game) setCompletedTrick(null)
if (round) {
setCompletedRound(round.result)
clearTimeout(roundTimer)
roundTimer = setTimeout(() => setCompletedRound(null), ROUND_DISPLAY_MS)
}
if (!message.game) {
setCompletedTrick(null)
setCompletedRound(null)
}
break break
} }
case 'error': case 'error':
@@ -155,7 +141,6 @@ export function useWizard() {
disposed = true disposed = true
clearTimeout(retryTimer) clearTimeout(retryTimer)
clearTimeout(trickTimer) clearTimeout(trickTimer)
clearTimeout(roundTimer)
socketRef.current?.close() socketRef.current?.close()
socketRef.current = null socketRef.current = null
} }
@@ -188,7 +173,7 @@ export function useWizard() {
[send], [send],
) )
return { status, playerId, lobby, game, completedTrick, completedRound, toasts, dismissToast, actions } return { status, playerId, lobby, game, completedTrick, toasts, dismissToast, actions }
} }
export type WizardActions = ReturnType<typeof useWizard>['actions'] export type WizardActions = ReturnType<typeof useWizard>['actions']
@@ -1,5 +1,5 @@
import { useState } from 'react' import { useState } from 'react'
import type { GameView, Lobby, RoundResult, TrickResult } from '../../api/types' import type { GameView, Lobby, TrickResult } from '../../api/types'
import type { WizardActions } from '../../api/useWizard' import type { WizardActions } from '../../api/useWizard'
import { Button } from '../../components/Button/Button' import { Button } from '../../components/Button/Button'
import { Modal } from '../../components/Modal/Modal' import { Modal } from '../../components/Modal/Modal'
@@ -19,11 +19,10 @@ interface GameScreenProps {
game: GameView game: GameView
playerId: string playerId: string
completedTrick: TrickResult | null completedTrick: TrickResult | null
completedRound: RoundResult | null
actions: WizardActions actions: WizardActions
} }
export function GameScreen({ lobby, game, playerId, completedTrick, completedRound, actions }: GameScreenProps) { export function GameScreen({ lobby, game, playerId, completedTrick, actions }: GameScreenProps) {
const [showScores, setShowScores] = useState(false) const [showScores, setShowScores] = useState(false)
const [confirmLeave, setConfirmLeave] = useState(false) const [confirmLeave, setConfirmLeave] = useState(false)
@@ -62,6 +61,12 @@ export function GameScreen({ lobby, game, playerId, completedTrick, completedRou
else if (trumpCard?.kind === 'wizard') trumpText = 'Wizard turned up — no trump' else if (trumpCard?.kind === 'wizard') trumpText = 'Wizard turned up — no trump'
else if (trumpCard?.kind === 'jester') trumpText = 'Jester turned up — no trump' else if (trumpCard?.kind === 'jester') trumpText = 'Jester turned up — no trump'
// The result of the previous round stays visible until everyone has bid in
// the new round (i.e. until the round moves on to playing cards).
const previousRound = obs.round_history.at(-1)
const showPreviousRound =
obs.phase === 'bidding' && previousRound !== undefined && previousRound.round_number === obs.round_number - 1
return ( return (
<main className={styles.page}> <main className={styles.page}>
<header className={styles.topBar}> <header className={styles.topBar}>
@@ -82,6 +87,8 @@ export function GameScreen({ lobby, game, playerId, completedTrick, completedRou
</div> </div>
</header> </header>
{showPreviousRound && <RoundSummary result={previousRound} players={players} />}
<section className={styles.opponents}> <section className={styles.opponents}>
{opponents.map((seat) => ( {opponents.map((seat) => (
<PlayerSeat key={seat} observation={obs} seat={seat} player={players[seat]} /> <PlayerSeat key={seat} observation={obs} seat={seat} player={players[seat]} />
@@ -112,7 +119,6 @@ export function GameScreen({ lobby, game, playerId, completedTrick, completedRou
</div> </div>
</section> </section>
{completedRound && obs.phase !== 'finished' && <RoundSummary result={completedRound} players={players} />}
{showScores && ( {showScores && (
<Modal title="Scoreboard" wide onClose={() => setShowScores(false)}> <Modal title="Scoreboard" wide onClose={() => setShowScores(false)}>
@@ -66,12 +66,11 @@
} }
} }
/* On small screens it would cover the bid buttons; show it in the page flow instead. */
@media (max-width: 720px) { @media (max-width: 720px) {
.summary { .summary {
top: auto; position: static;
right: 50%; width: 100%;
bottom: 16px; box-shadow: none;
transform: translateX(50%);
animation: none;
} }
} }