release/0.1.0 #18

Merged
ayoub merged 68 commits from release/0.1.0 into main 2026-06-11 18:36:42 +00:00
4 changed files with 60 additions and 2 deletions
Showing only changes of commit c9f3197300 - Show all commits

View file

@ -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(
<div className="pointer-events-none fixed inset-0 z-50 flex items-center justify-center overflow-hidden">
<motion.div
className="absolute inset-0 bg-white"
initial={{ opacity: 0 }}
animate={{ opacity: [0, 0.9, 0] }}
transition={{ duration: 0.5, times: [0, 0.12, 1], ease: "easeOut" }}
/>
<motion.span
className="relative leading-none select-none"
style={{ fontSize: "45vmin" }}
initial={{ scale: 0, rotate: -20, opacity: 0 }}
animate={{
scale: [0, 1.35, 1.1],
rotate: [-20, 12, 0],
opacity: [0, 1, 0],
x: [0, 12, -8, 0],
}}
transition={{ duration: 0.8, times: [0, 0.3, 1], ease: "easeOut" }}
>
💥
</motion.span>
</div>,
document.body
)
}

View file

@ -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 && <span aria-hidden>💣</span>}
{left}
{exploded ? "💥" : left}
</motion.span>
)
}

View file

@ -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 && <QuizView snapshot={snapshot} />}
{snapshot.status === "ended" && <GameEndView snapshot={snapshot} />}
</div>
{boomKey > 0 && <Boom key={boomKey} />}
</div>
)
}

View file

@ -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<RoomCreatedPayload>
joinRoom: (roomCode: string, playerName: string) => Promise<RoomCreatedPayload>
startGame: (questionCount: number) => Promise<void>
vote: (choiceIndex: number) => void
boom: () => void
clearError: () => void
reset: () => void
}
@ -57,6 +60,7 @@ export const useRoomStore = create<RoomState>((set, get) => ({
reveal: null,
myChoiceIndex: null,
finalScores: null,
boomKey: 0,
createRoom: (playerName) =>
new Promise((resolve, reject) => {
@ -113,6 +117,8 @@ export const useRoomStore = create<RoomState>((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<RoomState>((set, get) => ({
reveal: null,
myChoiceIndex: null,
finalScores: null,
boomKey: 0,
}),
}))