Server: - quiz mode (GameRound): in-memory FR question bank (mcq + truefalse), per-room anti-repeat, first-vote lock, score = base + speed bonus ∝ time left; registered via registerRound, loaded at startup - shared: typed quiz payloads (QuizQuestionPayload / reveal truth / result) - tests: quiz scoring, vote lock, bounds, reveal (bun test) Client: - room store handles round:start / voteAck / reveal / game:end - RoomPage dispatches by status: lobby (host start controls) → quiz view (question, countdown, vote, reveal + scoreboard) → game-end view - replaces standalone lobby page Roadmap V1 step 4. Verified end-to-end over the wire (start→vote→reveal→ score→next round→game:end, no answer leaked, anti-repeat works). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
37 lines
1.1 KiB
TypeScript
37 lines
1.1 KiB
TypeScript
import type { PlayerScore, RoomSnapshot } from "@nerdware/shared"
|
|
|
|
interface ScoreboardProps {
|
|
scores: PlayerScore[]
|
|
snapshot: RoomSnapshot
|
|
playerId: string | null
|
|
}
|
|
|
|
export function Scoreboard({ scores, snapshot, playerId }: ScoreboardProps) {
|
|
const nameOf = (id: string) =>
|
|
snapshot.players.find((p) => p.id === id)?.name ?? "?"
|
|
const ranked = [...scores].sort((a, b) => b.score - a.score)
|
|
|
|
return (
|
|
<ol className="flex flex-col gap-1">
|
|
{ranked.map((s, i) => (
|
|
<li
|
|
key={s.playerId}
|
|
className="bg-muted/40 flex items-center justify-between rounded-md px-3 py-2 text-sm"
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
<span className="text-muted-foreground w-5 text-right tabular-nums">
|
|
{i + 1}
|
|
</span>
|
|
<span>
|
|
{nameOf(s.playerId)}
|
|
{s.playerId === playerId && (
|
|
<span className="text-muted-foreground"> (toi)</span>
|
|
)}
|
|
</span>
|
|
</span>
|
|
<span className="font-heading font-bold tabular-nums">{s.score}</span>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
)
|
|
}
|