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