From c9f319730012a48ebc7a91dde5c27d481d0be0ee Mon Sep 17 00:00:00 2001 From: AyoubBenziza Date: Mon, 8 Jun 2026 16:05:42 +0200 Subject: [PATCH] =?UTF-8?q?feat(web):=20real=20explosion=20at=200=20?= =?UTF-8?q?=E2=80=94=20fullscreen=20flash=20+=20giant=20=F0=9F=92=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - store: boomKey counter + boom() action - Countdown fires boom() once when it reaches 0 (timer expiry only) - Boom component (portal to body) plays a fullscreen white flash + giant 💥 - rendered at RoomPage level, keyed by boomKey so it plays fully (independent of the countdown unmounting on reveal) and replays each round Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/web/src/components/boom.tsx | 35 +++++++++++++++++++++++++++ apps/web/src/components/countdown.tsx | 16 ++++++++++-- apps/web/src/pages/room.tsx | 4 +++ apps/web/src/store/room.ts | 7 ++++++ 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/boom.tsx diff --git a/apps/web/src/components/boom.tsx b/apps/web/src/components/boom.tsx new file mode 100644 index 0000000..6670893 --- /dev/null +++ b/apps/web/src/components/boom.tsx @@ -0,0 +1,35 @@ +import { createPortal } from "react-dom" +import { motion } from "framer-motion" + +/** + * Explosion plein écran : flash blanc + 💥 géant. + * Rendue dans document.body via portal pour échapper aux transforms parents, + * et montée avec une `key` qui change → l'animation rejoue à chaque boom. + */ +export function Boom() { + return createPortal( +
+ + + 💥 + +
, + document.body + ) +} diff --git a/apps/web/src/components/countdown.tsx b/apps/web/src/components/countdown.tsx index 9f2012a..7847867 100644 --- a/apps/web/src/components/countdown.tsx +++ b/apps/web/src/components/countdown.tsx @@ -1,5 +1,6 @@ -import { useEffect, useState } from "react" +import { useEffect, useRef, useState } from "react" import { motion } from "framer-motion" +import { useRoomStore } from "@/store/room" const DANGER_FROM = 10 // secondes : seuil d'entrée en mode "bombe" @@ -22,12 +23,23 @@ function dangerColor(t: number): string { export function Countdown({ endsAt }: { endsAt: number }) { const [left, setLeft] = useState(() => secondsLeft(endsAt)) + const boom = useRoomStore((s) => s.boom) + const boomed = useRef(false) useEffect(() => { const id = setInterval(() => setLeft(secondsLeft(endsAt)), 250) return () => clearInterval(id) }, [endsAt]) + // Déclenche l'explosion une seule fois au passage à 0 (fin au timer). + useEffect(() => { + if (left === 0 && !boomed.current) { + boomed.current = true + boom() + } + }, [left, boom]) + + const exploded = left === 0 const danger = left <= DANGER_FROM && left > 0 // 0 (début du danger) → 1 (explosion imminente) const intensity = danger ? Math.min(1, (DANGER_FROM - left) / DANGER_FROM) : 0 @@ -56,7 +68,7 @@ export function Countdown({ endsAt }: { endsAt: number }) { } > {danger && 💣} - {left} + {exploded ? "💥" : left} ) } diff --git a/apps/web/src/pages/room.tsx b/apps/web/src/pages/room.tsx index bb975dd..f0dc0b5 100644 --- a/apps/web/src/pages/room.tsx +++ b/apps/web/src/pages/room.tsx @@ -6,11 +6,13 @@ import { QuizView } from "@/components/quiz-view" import { GameEndView } from "@/components/game-end-view" import { PlayerCards } from "@/components/player-cards" import { RoomCode } from "@/components/room-code" +import { Boom } from "@/components/boom" export function RoomPage({ code }: { code: string }) { const snapshot = useRoomStore((s) => s.snapshot) const playerId = useRoomStore((s) => s.playerId) const connected = useRoomStore((s) => s.connected) + const boomKey = useRoomStore((s) => s.boomKey) // Pas de snapshot pour ce code (accès direct / refresh) : retour à l'accueil. if (!snapshot || snapshot.code !== code) { @@ -49,6 +51,8 @@ export function RoomPage({ code }: { code: string }) { {inGame && } {snapshot.status === "ended" && } + + {boomKey > 0 && } ) } diff --git a/apps/web/src/store/room.ts b/apps/web/src/store/room.ts index ca91a02..b1ca21e 100644 --- a/apps/web/src/store/room.ts +++ b/apps/web/src/store/room.ts @@ -37,11 +37,14 @@ interface RoomState { reveal: RoundReveal | null myChoiceIndex: number | null finalScores: PlayerScore[] | null + /** Compteur d'explosions : incrémenté à chaque fin de manche au timer (boom à 0). */ + boomKey: number createRoom: (playerName: string) => Promise joinRoom: (roomCode: string, playerName: string) => Promise startGame: (questionCount: number) => Promise vote: (choiceIndex: number) => void + boom: () => void clearError: () => void reset: () => void } @@ -57,6 +60,7 @@ export const useRoomStore = create((set, get) => ({ reveal: null, myChoiceIndex: null, finalScores: null, + boomKey: 0, createRoom: (playerName) => new Promise((resolve, reject) => { @@ -113,6 +117,8 @@ export const useRoomStore = create((set, get) => ({ socket.emit("round:vote", { answer: { choiceIndex } }) }, + boom: () => set((s) => ({ boomKey: s.boomKey + 1 })), + clearError: () => set({ lastError: null }), reset: () => set({ @@ -124,6 +130,7 @@ export const useRoomStore = create((set, get) => ({ reveal: null, myChoiceIndex: null, finalScores: null, + boomKey: 0, }), }))