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:*",
|
"@nerdware/shared": "workspace:*",
|
||||||
"@tanstack/react-query": "^5.101.0",
|
"@tanstack/react-query": "^5.101.0",
|
||||||
"@workspace/ui": "workspace:*",
|
"@workspace/ui": "workspace:*",
|
||||||
"canvas-confetti": "^1.9.4",
|
|
||||||
"framer-motion": "^11",
|
"framer-motion": "^11",
|
||||||
"lucide-react": "^1.17.0",
|
"lucide-react": "^1.17.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
|
|
@ -30,7 +29,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10",
|
"@eslint/js": "^10",
|
||||||
"@tailwindcss/vite": "^4",
|
"@tailwindcss/vite": "^4",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
|
||||||
"@types/node": "^24",
|
"@types/node": "^24",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^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 { Link } from "wouter"
|
||||||
import { motion } from "framer-motion"
|
import { motion } from "framer-motion"
|
||||||
import { Check, ClipboardList, Crown, Music } from "lucide-react"
|
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 { Button } from "@workspace/ui/components/button"
|
||||||
import { useRoomStore } from "@/store/room"
|
import { useRoomStore } from "@/store/room"
|
||||||
import { Avatar } from "@/components/avatar"
|
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). */
|
/** Lien de recherche Spotify (le titre vidéo contient en général artiste + chanson). */
|
||||||
function spotifySearch(track: BlindtestTrackInfo): string {
|
function spotifySearch(track: BlindtestTrackInfo): string {
|
||||||
|
|
@ -127,16 +125,6 @@ export function GameEndView({ snapshot }: { snapshot: RoomSnapshot }) {
|
||||||
const returnToLobby = useRoomStore((s) => s.returnToLobby)
|
const returnToLobby = useRoomStore((s) => s.returnToLobby)
|
||||||
const isHost = snapshot.hostId === playerId
|
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 scores: PlayerScore[] = finalScores ?? snapshot.scores
|
||||||
const ranked = [...scores].sort((a, b) => b.score - a.score)
|
const ranked = [...scores].sort((a, b) => b.score - a.score)
|
||||||
const nameOf = (id: string) =>
|
const nameOf = (id: string) =>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { useEffect, useRef } from "react"
|
|
||||||
import { AnimatePresence, motion } from "framer-motion"
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
import { Check, Crown } from "lucide-react"
|
import { Check, Crown } from "lucide-react"
|
||||||
import type { RoomSnapshot } from "@nerdware/shared"
|
import type { RoomSnapshot } from "@nerdware/shared"
|
||||||
import { Avatar } from "@/components/avatar"
|
import { Avatar } from "@/components/avatar"
|
||||||
import { celebrateLead } from "@/lib/confetti"
|
|
||||||
|
|
||||||
interface PlayerCardsProps {
|
interface PlayerCardsProps {
|
||||||
snapshot: RoomSnapshot
|
snapshot: RoomSnapshot
|
||||||
|
|
@ -29,18 +27,6 @@ export function PlayerCards({
|
||||||
(a, b) => scoreOf(b.id) - scoreOf(a.id)
|
(a, b) => scoreOf(b.id) - scoreOf(a.id)
|
||||||
)
|
)
|
||||||
const voted = new Set(votedIds)
|
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 (
|
return (
|
||||||
<div className="flex flex-wrap justify-center gap-2">
|
<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 { Link } from "wouter"
|
||||||
import { AnimatePresence, motion } from "framer-motion"
|
|
||||||
import { Button } from "@workspace/ui/components/button"
|
import { Button } from "@workspace/ui/components/button"
|
||||||
import { useRoomStore } from "@/store/room"
|
import { useRoomStore } from "@/store/room"
|
||||||
import { LobbyView } from "@/components/lobby-view"
|
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
|
// Cards de scores visibles en permanence pendant le jeu (suivi continu).
|
||||||
// préserver le lecteur YouTube entre in_round / reveal / scores.
|
const inGame =
|
||||||
const phase =
|
snapshot.status === "in_round" ||
|
||||||
snapshot.status === "lobby"
|
snapshot.status === "reveal" ||
|
||||||
? "lobby"
|
snapshot.status === "scores"
|
||||||
: snapshot.status === "ended"
|
|
||||||
? "ended"
|
|
||||||
: "game"
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-svh items-start justify-center p-6 pt-12">
|
<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>
|
</header>
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
{inGame && (
|
||||||
<motion.div
|
<PlayerCards
|
||||||
key={phase}
|
snapshot={snapshot}
|
||||||
className="flex flex-col gap-6"
|
playerId={playerId}
|
||||||
initial={{ opacity: 0, y: 12 }}
|
showVoteStatus={snapshot.status === "in_round"}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
votedIds={voteProgress?.voted}
|
||||||
exit={{ opacity: 0, y: -12 }}
|
/>
|
||||||
transition={{ duration: 0.22, ease: "easeOut" }}
|
)}
|
||||||
>
|
|
||||||
{phase === "lobby" && <LobbyView snapshot={snapshot} />}
|
{snapshot.status === "lobby" && <LobbyView snapshot={snapshot} />}
|
||||||
{phase === "game" && (
|
{inGame &&
|
||||||
<>
|
(round?.type === "blindtest" ? (
|
||||||
<PlayerCards
|
<BlindtestView snapshot={snapshot} />
|
||||||
snapshot={snapshot}
|
) : (
|
||||||
playerId={playerId}
|
<QuizView snapshot={snapshot} />
|
||||||
showVoteStatus={snapshot.status === "in_round"}
|
))}
|
||||||
votedIds={voteProgress?.voted}
|
{snapshot.status === "ended" && <GameEndView snapshot={snapshot} />}
|
||||||
/>
|
|
||||||
{round?.type === "blindtest" ? (
|
|
||||||
<BlindtestView snapshot={snapshot} />
|
|
||||||
) : (
|
|
||||||
<QuizView snapshot={snapshot} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{phase === "ended" && <GameEndView snapshot={snapshot} />}
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{boomKey > 0 && <Boom key={boomKey} />}
|
{boomKey > 0 && <Boom key={boomKey} />}
|
||||||
|
|
|
||||||
6
bun.lock
6
bun.lock
|
|
@ -44,7 +44,6 @@
|
||||||
"@nerdware/shared": "workspace:*",
|
"@nerdware/shared": "workspace:*",
|
||||||
"@tanstack/react-query": "^5.101.0",
|
"@tanstack/react-query": "^5.101.0",
|
||||||
"@workspace/ui": "workspace:*",
|
"@workspace/ui": "workspace:*",
|
||||||
"canvas-confetti": "^1.9.4",
|
|
||||||
"framer-motion": "^11",
|
"framer-motion": "^11",
|
||||||
"lucide-react": "^1.17.0",
|
"lucide-react": "^1.17.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
|
|
@ -56,7 +55,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10",
|
"@eslint/js": "^10",
|
||||||
"@tailwindcss/vite": "^4",
|
"@tailwindcss/vite": "^4",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
|
||||||
"@types/node": "^24",
|
"@types/node": "^24",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^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/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/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=="],
|
"@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=="],
|
"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=="],
|
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
|
|
||||||
"chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="],
|
"chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="],
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue