diff --git a/apps/web/package.json b/apps/web/package.json index b9a9752..bbb95dc 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,6 +18,7 @@ "@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", @@ -29,6 +30,7 @@ "devDependencies": { "@eslint/js": "^10", "@tailwindcss/vite": "^4", + "@types/canvas-confetti": "^1.9.0", "@types/node": "^24", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/apps/web/src/components/game-end-view.tsx b/apps/web/src/components/game-end-view.tsx index 5708d87..5c52039 100644 --- a/apps/web/src/components/game-end-view.tsx +++ b/apps/web/src/components/game-end-view.tsx @@ -1,4 +1,4 @@ -import { useState } from "react" +import { useEffect, useRef, useState } from "react" import { Link } from "wouter" import { motion } from "framer-motion" import { Check, ClipboardList, Crown, Music } from "lucide-react" @@ -7,6 +7,7 @@ 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" /** Lien de recherche Spotify (le titre vidéo contient en général artiste + chanson). */ function spotifySearch(track: BlindtestTrackInfo): string { @@ -125,6 +126,15 @@ 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() + } + }, []) + const scores: PlayerScore[] = finalScores ?? snapshot.scores const ranked = [...scores].sort((a, b) => b.score - a.score) const nameOf = (id: string) => diff --git a/apps/web/src/lib/confetti.ts b/apps/web/src/lib/confetti.ts new file mode 100644 index 0000000..0d96077 --- /dev/null +++ b/apps/web/src/lib/confetti.ts @@ -0,0 +1,36 @@ +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() +} diff --git a/bun.lock b/bun.lock index 687265d..21cacd6 100644 --- a/bun.lock +++ b/bun.lock @@ -44,6 +44,7 @@ "@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", @@ -55,6 +56,7 @@ "devDependencies": { "@eslint/js": "^10", "@tailwindcss/vite": "^4", + "@types/canvas-confetti": "^1.9.0", "@types/node": "^24", "@types/react": "^19", "@types/react-dom": "^19", @@ -584,6 +586,8 @@ "@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=="], @@ -690,6 +694,8 @@ "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=="],