feature/blindtest #6

Merged
ayoub merged 9 commits from feature/blindtest into dev 2026-06-10 14:25:59 +00:00
2 changed files with 90 additions and 17 deletions
Showing only changes of commit ff84dc4f6b - Show all commits

View file

@ -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.

View file

@ -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<BlindtestMode, string> = {
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 (
<div className="flex items-center gap-1">
<Button
size="icon-sm"
variant="secondary"
disabled={value <= min}
onClick={() => onChange(clamp(value - 1))}
>
<Minus />
</Button>
<input
type="number"
min={min}
max={max}
value={value}
onChange={(e) => {
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"
/>
<Button
size="icon-sm"
variant="secondary"
disabled={value >= max}
onClick={() => onChange(clamp(value + 1))}
>
<Plus />
</Button>
</div>
)
}
function shuffle<T>(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 }) {
<span className="flex items-center gap-1.5 text-sm font-medium">
<ListMusic className="size-4" /> Titres par joueur
</span>
<div className="flex gap-1">
{TRACKS_OPTIONS.map((n) => (
<Button
key={n}
size="sm"
variant={tracksPerPlayer === n ? "default" : "secondary"}
onClick={() => updateSettings({ tracksPerPlayer: n })}
>
{n}
</Button>
))}
</div>
<Stepper
value={tracksPerPlayer}
onChange={(v) => updateSettings({ tracksPerPlayer: v })}
/>
</div>
</div>
)}
@ -226,9 +272,17 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
)}
{isHost ? (
<Button disabled={busy || !canStart} onClick={start}>
<Play /> {busy ? "Lancement…" : `Lancer (${rounds.length} manches)`}
</Button>
<div className="flex flex-col gap-1">
<Button disabled={busy || !canStart} onClick={start}>
<Play /> {busy ? "Lancement…" : `Lancer (${rounds.length} manches)`}
</Button>
{showBlindtest && !allSubmitted && (
<p className="text-muted-foreground text-center text-xs">
En attente que tout le monde ait soumis ses {tracksPerPlayer}{" "}
titre(s).
</p>
)}
</div>
) : (
<p className="text-muted-foreground text-center text-xs">
En attente du lancement par l'hôte