feat(web): screen transitions + confetti when the lead changes
- RoomPage animates between coarse phases (lobby ↔ game ↔ end) with a fade/ slide (AnimatePresence, mode wait). Keyed by phase, not status, so the YouTube player survives in_round → reveal → scores. - PlayerCards fires a small confetti burst when a player overtakes to take the lead during the game. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7093ce8cd9
commit
e490f23dec
3 changed files with 63 additions and 22 deletions
|
|
@ -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<string | null>(null)
|
||||
useEffect(() => {
|
||||
if (leaderId && leaderId !== prevLeader.current) {
|
||||
if (prevLeader.current !== null) {
|
||||
celebrateLead()
|
||||
}
|
||||
prevLeader.current = leaderId
|
||||
}
|
||||
}, [leaderId])
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex min-h-svh items-start justify-center p-6 pt-12">
|
||||
|
|
@ -68,23 +72,34 @@ export function RoomPage({ code }: { code: string }) {
|
|||
/>
|
||||
</header>
|
||||
|
||||
{inGame && (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={phase}
|
||||
className="flex flex-col gap-6"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
transition={{ duration: 0.22, ease: "easeOut" }}
|
||||
>
|
||||
{phase === "lobby" && <LobbyView snapshot={snapshot} />}
|
||||
{phase === "game" && (
|
||||
<>
|
||||
<PlayerCards
|
||||
snapshot={snapshot}
|
||||
playerId={playerId}
|
||||
showVoteStatus={snapshot.status === "in_round"}
|
||||
votedIds={voteProgress?.voted}
|
||||
/>
|
||||
)}
|
||||
|
||||
{snapshot.status === "lobby" && <LobbyView snapshot={snapshot} />}
|
||||
{inGame &&
|
||||
(round?.type === "blindtest" ? (
|
||||
{round?.type === "blindtest" ? (
|
||||
<BlindtestView snapshot={snapshot} />
|
||||
) : (
|
||||
<QuizView snapshot={snapshot} />
|
||||
))}
|
||||
{snapshot.status === "ended" && <GameEndView snapshot={snapshot} />}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{phase === "ended" && <GameEndView snapshot={snapshot} />}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{boomKey > 0 && <Boom key={boomKey} />}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue