From 2fce63530b5e1a1ca79b0055287bc7fdcc05d00d Mon Sep 17 00:00:00 2001 From: AyoubBenziza Date: Tue, 9 Jun 2026 10:30:03 +0200 Subject: [PATCH] feat(quiz): show per-player answer status during the round MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - voteAck now carries `voted` (playerIds who validated) — never their answer, so no leak; the count/total stay for the progress text - PlayerCards show a green "Prêt" check for players who answered and a pulsing "en attente" dot for those who haven't, during in_round only - score stays visible on the cards alongside the status - engine test updated for the new voteAck shape Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/server/src/game/engine.test.ts | 3 +- apps/server/src/game/engine.ts | 1 + apps/web/src/components/player-cards.tsx | 37 ++++++++++++++++++++++-- apps/web/src/pages/room.tsx | 10 ++++++- packages/shared/src/events.ts | 3 +- 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/apps/server/src/game/engine.test.ts b/apps/server/src/game/engine.test.ts index a8638d2..7cbf67c 100644 --- a/apps/server/src/game/engine.test.ts +++ b/apps/server/src/game/engine.test.ts @@ -109,8 +109,9 @@ describe("GameEngine", () => { const ack = emits.find((e) => e.event === "round:voteAck")!.payload as { count: number total: number + voted: string[] } - expect(ack).toEqual({ count: 1, total: 2 }) + expect(ack).toEqual({ count: 1, total: 2, voted: [p1] }) engine.handleVote(p2, { choiceIndex: 0 }) await run diff --git a/apps/server/src/game/engine.ts b/apps/server/src/game/engine.ts index f4a8df9..6c56f59 100644 --- a/apps/server/src/game/engine.ts +++ b/apps/server/src/game/engine.ts @@ -73,6 +73,7 @@ export class GameEngine implements RoomGameController { this.io.to(this.room.code).emit("round:voteAck", { count: this.runtime.votes.size, total, + voted: [...this.runtime.votes.keys()], }) // Fin anticipée : tous les éligibles ont voté. if (total > 0 && this.runtime.votes.size >= total) { diff --git a/apps/web/src/components/player-cards.tsx b/apps/web/src/components/player-cards.tsx index 353116f..bab3677 100644 --- a/apps/web/src/components/player-cards.tsx +++ b/apps/web/src/components/player-cards.tsx @@ -1,14 +1,23 @@ import { AnimatePresence, motion } from "framer-motion" -import { Crown } from "lucide-react" +import { Check, Crown } from "lucide-react" import type { RoomSnapshot } from "@nerdware/shared" interface PlayerCardsProps { snapshot: RoomSnapshot playerId: string | null + /** Affiche l'état de vote (manche en cours uniquement). */ + showVoteStatus?: boolean + /** playerIds ayant déjà validé leur réponse. */ + votedIds?: string[] } /** HUD persistant : une card par joueur avec son score, couronne au leader. */ -export function PlayerCards({ snapshot, playerId }: PlayerCardsProps) { +export function PlayerCards({ + snapshot, + playerId, + showVoteStatus = false, + votedIds = [], +}: PlayerCardsProps) { const scoreOf = (id: string) => snapshot.scores.find((s) => s.playerId === id)?.score ?? 0 @@ -16,6 +25,7 @@ export function PlayerCards({ snapshot, playerId }: PlayerCardsProps) { const ranked = [...snapshot.players].sort( (a, b) => scoreOf(b.id) - scoreOf(a.id) ) + const voted = new Set(votedIds) return (
@@ -24,6 +34,7 @@ export function PlayerCards({ snapshot, playerId }: PlayerCardsProps) { const score = scoreOf(p.id) const isLeader = max > 0 && score === max const isMe = p.id === playerId + const hasVoted = voted.has(p.id) return ( (toi)} + {score} + + {showVoteStatus && + (hasVoted ? ( + + Prêt + + ) : ( + + + en attente + + ))} ) })} diff --git a/apps/web/src/pages/room.tsx b/apps/web/src/pages/room.tsx index f0dc0b5..dc109ba 100644 --- a/apps/web/src/pages/room.tsx +++ b/apps/web/src/pages/room.tsx @@ -13,6 +13,7 @@ export function RoomPage({ code }: { code: string }) { const playerId = useRoomStore((s) => s.playerId) const connected = useRoomStore((s) => s.connected) const boomKey = useRoomStore((s) => s.boomKey) + const voteProgress = useRoomStore((s) => s.voteProgress) // Pas de snapshot pour ce code (accès direct / refresh) : retour à l'accueil. if (!snapshot || snapshot.code !== code) { @@ -45,7 +46,14 @@ export function RoomPage({ code }: { code: string }) { /> - {inGame && } + {inGame && ( + + )} {snapshot.status === "lobby" && } {inGame && } diff --git a/packages/shared/src/events.ts b/packages/shared/src/events.ts index cc3a8b6..d8d2ee4 100644 --- a/packages/shared/src/events.ts +++ b/packages/shared/src/events.ts @@ -73,10 +73,11 @@ export interface VotePayload { answer: Answer } -/** Progression anonyme des votes. */ +/** Progression des votes. `voted` = playerIds ayant validé (jamais leur réponse). */ export interface VoteAckPayload { count: number total: number + voted: string[] } export interface RevealPayload {