diff --git a/apps/web/src/components/player-cards.tsx b/apps/web/src/components/player-cards.tsx index e079e68..a77c388 100644 --- a/apps/web/src/components/player-cards.tsx +++ b/apps/web/src/components/player-cards.tsx @@ -1,7 +1,9 @@ +import { useEffect, useRef } from "react" import { AnimatePresence, motion } from "framer-motion" import { Check, Crown } from "lucide-react" import type { RoomSnapshot } from "@nerdware/shared" import { Avatar } from "@/components/avatar" +import { celebrateLead } from "@/lib/confetti" interface PlayerCardsProps { snapshot: RoomSnapshot @@ -27,6 +29,18 @@ export function PlayerCards({ (a, b) => scoreOf(b.id) - scoreOf(a.id) ) const voted = new Set(votedIds) + const leaderId = max > 0 ? ranked[0].id : null + + // Confettis quand un joueur DÉPASSE pour prendre la tête (pas à la 1re prise). + const prevLeader = useRef(null) + useEffect(() => { + if (leaderId && leaderId !== prevLeader.current) { + if (prevLeader.current !== null) { + celebrateLead() + } + prevLeader.current = leaderId + } + }, [leaderId]) return (
diff --git a/apps/web/src/lib/confetti.ts b/apps/web/src/lib/confetti.ts index 0d96077..7f20fcb 100644 --- a/apps/web/src/lib/confetti.ts +++ b/apps/web/src/lib/confetti.ts @@ -34,3 +34,15 @@ export function celebrate(): void { } frame() } + +/** Petite salve quand un joueur prend la tête en cours de partie. */ +export function celebrateLead(): void { + confetti({ + particleCount: 45, + spread: 70, + startVelocity: 30, + scalar: 0.8, + origin: { y: 0.25 }, + colors: COLORS, + }) +} diff --git a/apps/web/src/pages/room.tsx b/apps/web/src/pages/room.tsx index 5c00307..05e0db4 100644 --- a/apps/web/src/pages/room.tsx +++ b/apps/web/src/pages/room.tsx @@ -1,4 +1,5 @@ import { Link } from "wouter" +import { AnimatePresence, motion } from "framer-motion" import { Button } from "@workspace/ui/components/button" import { useRoomStore } from "@/store/room" import { LobbyView } from "@/components/lobby-view" @@ -51,11 +52,14 @@ export function RoomPage({ code }: { code: string }) { ) } - // Cards de scores visibles en permanence pendant le jeu (suivi continu). - const inGame = - snapshot.status === "in_round" || - snapshot.status === "reveal" || - snapshot.status === "scores" + // Phase grossière : on n'anime qu'aux frontières lobby ↔ jeu ↔ fin pour + // préserver le lecteur YouTube entre in_round / reveal / scores. + const phase = + snapshot.status === "lobby" + ? "lobby" + : snapshot.status === "ended" + ? "ended" + : "game" return (
@@ -68,23 +72,34 @@ export function RoomPage({ code }: { code: string }) { /> - {inGame && ( - - )} - - {snapshot.status === "lobby" && } - {inGame && - (round?.type === "blindtest" ? ( - - ) : ( - - ))} - {snapshot.status === "ended" && } + + + {phase === "lobby" && } + {phase === "game" && ( + <> + + {round?.type === "blindtest" ? ( + + ) : ( + + )} + + )} + {phase === "ended" && } + +
{boomKey > 0 && }