From ff84dc4f6b3a12d334608d643ef7d538af297aae Mon Sep 17 00:00:00 2001 From: AyoubBenziza Date: Wed, 10 Jun 2026 16:19:31 +0200 Subject: [PATCH] feat(lobby): block start until everyone submitted; tracks count stepper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - blindtest/mixed: the host can't start until every connected player has submitted their track quota — client disables the button with a hint, and game:start rejects with TRACKS_PENDING (authoritative) - tracks-per-player is now a number stepper (− / + buttons + free typing, clamped 1..10) instead of fixed 1/2/3 buttons Verified e2e: start rejected while a player hasn't submitted, accepted once all did. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/server/src/handlers.ts | 19 ++++++ apps/web/src/components/lobby-view.tsx | 88 +++++++++++++++++++++----- 2 files changed, 90 insertions(+), 17 deletions(-) diff --git a/apps/server/src/handlers.ts b/apps/server/src/handlers.ts index 2c5bf82..4b15c83 100644 --- a/apps/server/src/handlers.ts +++ b/apps/server/src/handlers.ts @@ -151,6 +151,25 @@ export function registerRoomHandlers( }) return } + if (hasBlindtest) { + const tpp = room.settings.tracksPerPlayer + const pending = [...room.players.values()].some( + (p) => + p.connected && + p.name !== "" && + room.blindtestTracks.filter((t) => t.submittedBy === p.id).length < tpp + ) + if (pending) { + ack({ + ok: false, + error: { + code: "TRACKS_PENDING", + message: "Tous les joueurs n'ont pas encore soumis leurs titres.", + }, + }) + return + } + } ack({ ok: true, data: null }) // Précharge le contenu (pool quiz depuis la DB), puis lance la boucle. // Fire-and-forget : la boucle de jeu broadcast son propre état. diff --git a/apps/web/src/components/lobby-view.tsx b/apps/web/src/components/lobby-view.tsx index cd449b3..a34afb0 100644 --- a/apps/web/src/components/lobby-view.tsx +++ b/apps/web/src/components/lobby-view.tsx @@ -3,8 +3,10 @@ import { Brain, Headphones, ListMusic, + Minus, Music, Play, + Plus, Shuffle, Trash2, type LucideIcon, @@ -20,7 +22,7 @@ import { useRoomStore } from "@/store/room" import { Avatar } from "@/components/avatar" const QUESTION_OPTIONS = [3, 5, 10] -const TRACKS_OPTIONS = [1, 2, 3] +const MAX_TRACKS = 10 const GAME_TYPES: { value: GameType; label: string; Icon: LucideIcon }[] = [ { value: "mixed", label: "Mixte", Icon: Shuffle }, { value: "quiz", label: "Quiz", Icon: Brain }, @@ -35,6 +37,53 @@ const MODE_LABELS: Record = { const inputClass = "border-input bg-background h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50" +function Stepper({ + value, + min = 1, + max = MAX_TRACKS, + onChange, +}: { + value: number + min?: number + max?: number + onChange: (v: number) => void +}) { + const clamp = (v: number) => Math.max(min, Math.min(max, v)) + return ( +
+ + { + const n = parseInt(e.target.value, 10) + if (!Number.isNaN(n)) { + onChange(clamp(n)) + } + }} + className="border-input bg-background h-8 w-14 rounded-md border text-center text-sm [appearance:textfield] focus-visible:outline-none [&::-webkit-inner-spin-button]:appearance-none" + /> + +
+ ) +} + function shuffle(items: T[]): T[] { const arr = [...items] for (let i = arr.length - 1; i > 0; i--) { @@ -90,7 +139,12 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) { effectiveType === "blindtest" || effectiveType === "mixed" const rounds = buildRounds(effectiveType, showQuiz ? count : 0, totalTracks) - const canStart = rounds.length > 0 + // Blindtest : tout le monde doit avoir soumis son quota de titres. + const allSubmitted = + !showBlindtest || + (snapshot.submissions.length > 0 && + snapshot.submissions.every((s) => s.count >= tracksPerPlayer)) + const canStart = rounds.length > 0 && allSubmitted async function start() { setError(null) @@ -205,18 +259,10 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) { Titres par joueur -
- {TRACKS_OPTIONS.map((n) => ( - - ))} -
+ updateSettings({ tracksPerPlayer: v })} + /> )} @@ -226,9 +272,17 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) { )} {isHost ? ( - +
+ + {showBlindtest && !allSubmitted && ( +

+ En attente que tout le monde ait soumis ses {tracksPerPlayer}{" "} + titre(s). +

+ )} +
) : (

En attente du lancement par l'hôte…