feature/polish #9
4 changed files with 55 additions and 1 deletions
|
|
@ -18,6 +18,7 @@
|
||||||
"@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",
|
||||||
|
|
@ -29,6 +30,7 @@
|
||||||
"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,4 +1,4 @@
|
||||||
import { useState } from "react"
|
import { useEffect, useRef, 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,6 +7,7 @@ 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"
|
||||||
|
|
||||||
/** 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 {
|
||||||
|
|
@ -125,6 +126,15 @@ 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()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
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) =>
|
||||||
|
|
|
||||||
36
apps/web/src/lib/confetti.ts
Normal file
36
apps/web/src/lib/confetti.ts
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
6
bun.lock
6
bun.lock
|
|
@ -44,6 +44,7 @@
|
||||||
"@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",
|
||||||
|
|
@ -55,6 +56,7 @@
|
||||||
"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",
|
||||||
|
|
@ -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/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=="],
|
||||||
|
|
@ -690,6 +694,8 @@
|
||||||
|
|
||||||
"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