Compare commits
No commits in common. "8b03879ee06f570fa757edd38036798220b6590e" and "604c36818481b6f57ef8550587be7038d0fbbc05" have entirely different histories.
8b03879ee0
...
604c368184
9 changed files with 23 additions and 154 deletions
|
|
@ -18,7 +18,6 @@
|
|||
"@nerdware/shared": "workspace:*",
|
||||
"@tanstack/react-query": "^5.101.0",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"framer-motion": "^11",
|
||||
"lucide-react": "^1.17.0",
|
||||
"react": "^19.2.6",
|
||||
|
|
@ -30,7 +29,6 @@
|
|||
"devDependencies": {
|
||||
"@eslint/js": "^10",
|
||||
"@tailwindcss/vite": "^4",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/node": "^24",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
# Sons du jeu
|
||||
|
||||
Dépose ici les effets sonores (détectés automatiquement au build).
|
||||
|
||||
- **`victory.mp3`** → jingle joué sur l'écran de fin de partie.
|
||||
(formats acceptés : `.mp3`, `.ogg`, `.wav` — nomme le fichier `victory.*`)
|
||||
|
||||
Si aucun fichier n'est présent, le son est simplement ignoré (pas d'erreur).
|
||||
|
||||
> ⚠️ N'utilise que des sons dont tu as les droits (libres / CC0).
|
||||
Binary file not shown.
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useRef, useState } from "react"
|
||||
import { useState } from "react"
|
||||
import { Link } from "wouter"
|
||||
import { motion } from "framer-motion"
|
||||
import { Check, ClipboardList, Crown, Music } from "lucide-react"
|
||||
|
|
@ -7,8 +7,6 @@ import type { BlindtestTrackInfo, PlayerScore, RoomSnapshot } from "@nerdware/sh
|
|||
import { Button } from "@workspace/ui/components/button"
|
||||
import { useRoomStore } from "@/store/room"
|
||||
import { Avatar } from "@/components/avatar"
|
||||
import { celebrate } from "@/lib/confetti"
|
||||
import { playVictory } from "@/lib/sound"
|
||||
|
||||
/** Lien de recherche Spotify (le titre vidéo contient en général artiste + chanson). */
|
||||
function spotifySearch(track: BlindtestTrackInfo): string {
|
||||
|
|
@ -127,16 +125,6 @@ export function GameEndView({ snapshot }: { snapshot: RoomSnapshot }) {
|
|||
const returnToLobby = useRoomStore((s) => s.returnToLobby)
|
||||
const isHost = snapshot.hostId === playerId
|
||||
|
||||
// Confettis de victoire (une fois à l'arrivée sur l'écran de fin).
|
||||
const celebrated = useRef(false)
|
||||
useEffect(() => {
|
||||
if (!celebrated.current) {
|
||||
celebrated.current = true
|
||||
celebrate()
|
||||
playVictory()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const scores: PlayerScore[] = finalScores ?? snapshot.scores
|
||||
const ranked = [...scores].sort((a, b) => b.score - a.score)
|
||||
const nameOf = (id: string) =>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
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
|
||||
|
|
@ -29,18 +27,6 @@ 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">
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import confetti from "canvas-confetti"
|
||||
|
||||
const COLORS = ["#a855f7", "#ec4899", "#22d3ee", "#fbbf24", "#f97316"]
|
||||
|
||||
/** Salve de confettis de victoire : gros pop central + jets latéraux ~1s. */
|
||||
export function celebrate(): void {
|
||||
confetti({
|
||||
particleCount: 140,
|
||||
spread: 100,
|
||||
startVelocity: 45,
|
||||
origin: { y: 0.6 },
|
||||
colors: COLORS,
|
||||
})
|
||||
|
||||
const end = Date.now() + 900
|
||||
const frame = () => {
|
||||
confetti({
|
||||
particleCount: 5,
|
||||
angle: 60,
|
||||
spread: 55,
|
||||
origin: { x: 0 },
|
||||
colors: COLORS,
|
||||
})
|
||||
confetti({
|
||||
particleCount: 5,
|
||||
angle: 120,
|
||||
spread: 55,
|
||||
origin: { x: 1 },
|
||||
colors: COLORS,
|
||||
})
|
||||
if (Date.now() < end) {
|
||||
requestAnimationFrame(frame)
|
||||
}
|
||||
}
|
||||
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,24 +0,0 @@
|
|||
// Sons du jeu. Dépose les fichiers dans src/assets/sounds/ (voir le README).
|
||||
// Tout est optionnel : si le fichier est absent, la lecture est ignorée.
|
||||
|
||||
const victorySources = Object.values(
|
||||
import.meta.glob("../assets/sounds/victory.{mp3,ogg,wav}", {
|
||||
eager: true,
|
||||
import: "default",
|
||||
})
|
||||
) as string[]
|
||||
|
||||
const victoryUrl: string | null = victorySources[0] ?? null
|
||||
let victoryAudio: HTMLAudioElement | null = null
|
||||
|
||||
/** Joue le jingle de victoire (si présent). Nécessite une interaction préalable. */
|
||||
export function playVictory(volume = 0.6): void {
|
||||
if (!victoryUrl) {
|
||||
return
|
||||
}
|
||||
victoryAudio ??= new Audio(victoryUrl)
|
||||
victoryAudio.volume = volume
|
||||
victoryAudio.currentTime = 0
|
||||
// L'autoplay peut être bloqué tant qu'aucun geste utilisateur n'a eu lieu.
|
||||
void victoryAudio.play().catch(() => {})
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
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"
|
||||
|
|
@ -52,14 +51,11 @@ export function RoomPage({ code }: { code: string }) {
|
|||
)
|
||||
}
|
||||
|
||||
// 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"
|
||||
// Cards de scores visibles en permanence pendant le jeu (suivi continu).
|
||||
const inGame =
|
||||
snapshot.status === "in_round" ||
|
||||
snapshot.status === "reveal" ||
|
||||
snapshot.status === "scores"
|
||||
|
||||
return (
|
||||
<div className="flex min-h-svh items-start justify-center p-6 pt-12">
|
||||
|
|
@ -72,34 +68,23 @@ export function RoomPage({ code }: { code: string }) {
|
|||
/>
|
||||
</header>
|
||||
|
||||
<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}
|
||||
/>
|
||||
{round?.type === "blindtest" ? (
|
||||
<BlindtestView snapshot={snapshot} />
|
||||
) : (
|
||||
<QuizView snapshot={snapshot} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{phase === "ended" && <GameEndView snapshot={snapshot} />}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
{inGame && (
|
||||
<PlayerCards
|
||||
snapshot={snapshot}
|
||||
playerId={playerId}
|
||||
showVoteStatus={snapshot.status === "in_round"}
|
||||
votedIds={voteProgress?.voted}
|
||||
/>
|
||||
)}
|
||||
|
||||
{snapshot.status === "lobby" && <LobbyView snapshot={snapshot} />}
|
||||
{inGame &&
|
||||
(round?.type === "blindtest" ? (
|
||||
<BlindtestView snapshot={snapshot} />
|
||||
) : (
|
||||
<QuizView snapshot={snapshot} />
|
||||
))}
|
||||
{snapshot.status === "ended" && <GameEndView snapshot={snapshot} />}
|
||||
</div>
|
||||
|
||||
{boomKey > 0 && <Boom key={boomKey} />}
|
||||
|
|
|
|||
6
bun.lock
6
bun.lock
|
|
@ -44,7 +44,6 @@
|
|||
"@nerdware/shared": "workspace:*",
|
||||
"@tanstack/react-query": "^5.101.0",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"framer-motion": "^11",
|
||||
"lucide-react": "^1.17.0",
|
||||
"react": "^19.2.6",
|
||||
|
|
@ -56,7 +55,6 @@
|
|||
"devDependencies": {
|
||||
"@eslint/js": "^10",
|
||||
"@tailwindcss/vite": "^4",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/node": "^24",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
|
|
@ -586,8 +584,6 @@
|
|||
|
||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||
|
||||
"@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="],
|
||||
|
||||
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||
|
||||
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||
|
|
@ -694,8 +690,6 @@
|
|||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001797", "", {}, "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w=="],
|
||||
|
||||
"canvas-confetti": ["canvas-confetti@1.9.4", "", {}, "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw=="],
|
||||
|
||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue