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,
}),
}))