From 3f0cab8f5fa69ca018885321056dc19457249674 Mon Sep 17 00:00:00 2001 From: AyoubBenziza Date: Wed, 10 Jun 2026 16:04:14 +0200 Subject: [PATCH] feat: end-of-game track recap, no self-vote in who_added, festive home + icons - game:end now carries the played blindtest tracks (title/artist/youtubeId/ url/submittedByName); the end screen lists them as cards with a YouTube open link so players can recover/export the songs - who_added vote no longer lets you pick yourself (filtered out) - home page: animated halo, floating geek emojis, springy gradient title, hover/tap on buttons (Framer Motion) + Lucide icons - lobby settings get Lucide icons (game type, quiz, blindtest mode, tracks, start) Verified e2e: game:end includes the track list with links. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/server/src/game/engine.ts | 13 +++- apps/web/src/components/blindtest-view.tsx | 33 +++++---- apps/web/src/components/game-end-view.tsx | 43 +++++++++++- apps/web/src/components/lobby-view.tsx | 45 ++++++++---- apps/web/src/pages/home.tsx | 81 ++++++++++++++++++---- apps/web/src/store/room.ts | 9 ++- packages/shared/src/blindtest.ts | 9 +++ packages/shared/src/events.ts | 3 + 8 files changed, 187 insertions(+), 49 deletions(-) diff --git a/apps/server/src/game/engine.ts b/apps/server/src/game/engine.ts index 9b11409..8fe2b74 100644 --- a/apps/server/src/game/engine.ts +++ b/apps/server/src/game/engine.ts @@ -72,7 +72,18 @@ export class GameEngine implements RoomGameController { } this.room.status = "ended" this.broadcastState() - this.io.to(this.room.code).emit("game:end", { finalScores: this.scoreboard() }) + // Récap des titres blindtest joués (pour récupérer les liens YouTube). + const tracks = this.room.blindtestTracks.map((t) => ({ + title: t.title, + artist: t.artist, + youtubeId: t.youtubeId, + url: t.url, + submittedByName: this.room.players.get(t.submittedBy)?.name ?? "?", + })) + this.io.to(this.room.code).emit("game:end", { + finalScores: this.scoreboard(), + tracks: tracks.length > 0 ? tracks : undefined, + }) } /** Vote d'un joueur pendant une manche. Délègue à l'épreuve, puis check fin anticipée. */ diff --git a/apps/web/src/components/blindtest-view.tsx b/apps/web/src/components/blindtest-view.tsx index 739ea5f..a96ea40 100644 --- a/apps/web/src/components/blindtest-view.tsx +++ b/apps/web/src/components/blindtest-view.tsx @@ -295,23 +295,22 @@ function VoteForm({ )} {needsWho && (
- {snapshot.players.map((p) => ( - - ))} + {snapshot.players + .filter((p) => p.id !== playerId) + .map((p) => ( + + ))}
)} + + + ))} + + + )} +
{isHost ? ( ) })} @@ -154,7 +163,9 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) { {isHost && showQuiz && (
- Questions de quiz + + Questions de quiz +
{QUESTION_OPTIONS.map((n) => (
- Titres par joueur + + Titres par joueur +
{TRACKS_OPTIONS.map((n) => ( ) : (

diff --git a/apps/web/src/pages/home.tsx b/apps/web/src/pages/home.tsx index 6aebc56..c124839 100644 --- a/apps/web/src/pages/home.tsx +++ b/apps/web/src/pages/home.tsx @@ -1,11 +1,23 @@ import { useState } from "react" import { useLocation } from "wouter" +import { motion } from "framer-motion" +import { LogIn, Sparkles } from "lucide-react" import { Button } from "@workspace/ui/components/button" import { useRoomStore } from "@/store/room" const inputClass = "border-input bg-background ring-offset-background focus-visible:ring-ring h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50" +// Emojis geek qui flottent en fond. +const FLOATERS = [ + { emoji: "🎮", x: "8%", y: "18%", delay: 0 }, + { emoji: "🎧", x: "82%", y: "22%", delay: 0.6 }, + { emoji: "🧠", x: "15%", y: "72%", delay: 1.2 }, + { emoji: "👾", x: "78%", y: "70%", delay: 0.3 }, + { emoji: "🕹️", x: "45%", y: "12%", delay: 0.9 }, + { emoji: "🎬", x: "60%", y: "82%", delay: 1.5 }, +] + export function HomePage() { const [, navigate] = useLocation() const createRoom = useRoomStore((s) => s.createRoom) @@ -43,20 +55,63 @@ export function HomePage() { } return ( -

-
+
+ {/* Halo animé */} + + {/* Emojis flottants */} + {FLOATERS.map((f) => ( + + {f.emoji} + + ))} + +
-

+ NerdWare -

-

- Party game culture geek + +

+ Party game culture geek

- + + +
@@ -66,8 +121,8 @@ export function HomePage() {
setCode(e.target.value)} @@ -78,7 +133,7 @@ export function HomePage() { disabled={!connected || busy || code.trim().length === 0} onClick={handleJoin} > - Rejoindre + Rejoindre
@@ -90,7 +145,7 @@ export function HomePage() { {error && (

{error}

)} -
+
) } diff --git a/apps/web/src/store/room.ts b/apps/web/src/store/room.ts index 94f018c..b134866 100644 --- a/apps/web/src/store/room.ts +++ b/apps/web/src/store/room.ts @@ -2,6 +2,7 @@ import { create } from "zustand" import { DEFAULT_ROOM_SETTINGS, type BlindtestAnswer, + type BlindtestTrackInfo, type ErrorPayload, type MediaControlPayload, type MediaSyncPayload, @@ -49,6 +50,7 @@ interface RoomState { myChoiceIndex: number | null hasVoted: boolean finalScores: PlayerScore[] | null + gameTracks: BlindtestTrackInfo[] | null mediaSync: MediaSyncPayload | null boomKey: number roundKey: number @@ -85,6 +87,7 @@ export const useRoomStore = create((set, get) => ({ myChoiceIndex: null, hasVoted: false, finalScores: null, + gameTracks: null, mediaSync: null, boomKey: 0, roundKey: 0, @@ -213,6 +216,7 @@ export const useRoomStore = create((set, get) => ({ myChoiceIndex: null, hasVoted: false, finalScores: null, + gameTracks: null, mediaSync: null, boomKey: 0, roundKey: 0, @@ -234,6 +238,7 @@ socket.on("room:state", (snapshot) => myChoiceIndex: null, hasVoted: false, finalScores: null, + gameTracks: null, mediaSync: null, } : { snapshot } @@ -265,6 +270,6 @@ socket.on("round:voteAck", (voteProgress) => ) socket.on("round:reveal", (reveal) => useRoomStore.setState({ reveal })) socket.on("media:sync", (mediaSync) => useRoomStore.setState({ mediaSync })) -socket.on("game:end", ({ finalScores }) => - useRoomStore.setState({ finalScores }) +socket.on("game:end", ({ finalScores, tracks }) => + useRoomStore.setState({ finalScores, gameTracks: tracks ?? null }) ) diff --git a/packages/shared/src/blindtest.ts b/packages/shared/src/blindtest.ts index 9c37acf..5382d15 100644 --- a/packages/shared/src/blindtest.ts +++ b/packages/shared/src/blindtest.ts @@ -26,3 +26,12 @@ export interface BlindtestPlayerResult { /** round:reveal → perPlayerResult : playerId → résultat. */ export type BlindtestPerPlayerResult = Record + +/** Titre récapitulé en fin de partie (export / récupération des liens). */ +export interface BlindtestTrackInfo { + title: string + artist: string + youtubeId: string + url: string + submittedByName: string +} diff --git a/packages/shared/src/events.ts b/packages/shared/src/events.ts index 747f6ff..2c1f023 100644 --- a/packages/shared/src/events.ts +++ b/packages/shared/src/events.ts @@ -10,6 +10,7 @@ import type { RoundConfig, RoundType, } from "./domain" +import type { BlindtestTrackInfo } from "./blindtest" // --- Payloads ------------------------------------------------------------ @@ -113,6 +114,8 @@ export interface ScoreUpdatePayload { export interface GameEndPayload { finalScores: PlayerScore[] + /** Titres joués (si la partie comportait du blindtest) — pour récupérer les liens. */ + tracks?: BlindtestTrackInfo[] spotifyExport?: unknown }