From 5e96182489bb5ea2ae6bcbbbc469cbf72008f7ab Mon Sep 17 00:00:00 2001 From: AyoubBenziza Date: Thu, 11 Jun 2026 00:35:32 +0200 Subject: [PATCH] feat(web): dedicated transition for the image mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image_reveal rounds (RoundType "quiz") now get their own "IMAGE" transition (emerald/teal theme, đŸ–Œïž) instead of reusing the quiz one. The store derives a roundKind (quiz | image | blindtest) from the round payload format; mode-change detection and the custom-media folder use it (assets/transitions/image/). Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/web/src/assets/transitions/README.md | 3 +- apps/web/src/components/round-transition.tsx | 39 +++++++++------ apps/web/src/pages/room.tsx | 3 +- apps/web/src/store/room.ts | 50 +++++++++++++------- 4 files changed, 60 insertions(+), 35 deletions(-) diff --git a/apps/web/src/assets/transitions/README.md b/apps/web/src/assets/transitions/README.md index a1675d9..8240354 100644 --- a/apps/web/src/assets/transitions/README.md +++ b/apps/web/src/assets/transitions/README.md @@ -6,8 +6,9 @@ formats supportĂ©s `*.gif`, `*.webp`, `*.apng`, `*.png`, `*.avif`. Rangement par mode (recommandĂ© pour des transitions personnalisĂ©es) : - `quiz/` → jouĂ©es avant une manche de **quiz** +- `image/` → jouĂ©es avant une manche du mode **images** - `blindtest/` → jouĂ©es avant une manche de **blindtest** -- racine (`./`) → **communs**, fallback pour les deux modes +- racine (`./`) → **communs**, fallback pour tous les modes RĂšgles : diff --git a/apps/web/src/components/round-transition.tsx b/apps/web/src/components/round-transition.tsx index 937123c..6969c29 100644 --- a/apps/web/src/components/round-transition.tsx +++ b/apps/web/src/components/round-transition.tsx @@ -1,12 +1,15 @@ import { useEffect, useState } from "react" import { createPortal } from "react-dom" import { AnimatePresence, motion } from "framer-motion" -import type { RoundType } from "@nerdware/shared" + +/** CatĂ©gorie visuelle de la transition (image_reveal est distinct du quiz). */ +export type TransitionKind = "quiz" | "image" | "blindtest" const VISIBLE_MS = 1300 // durĂ©e d'affichage avant le wipe de sortie (≈ leadMs serveur) // MĂ©dias custom dĂ©posĂ©s par l'utilisateur, rangĂ©s par mode : // src/assets/transitions/quiz/* → transitions de quiz +// src/assets/transitions/image/* → transitions du mode images // src/assets/transitions/blindtest/* → transitions de blindtest // src/assets/transitions/* → communs (fallback) // DĂ©tectĂ©s au build ; sinon, animation Framer par dĂ©faut. @@ -15,17 +18,17 @@ const ALL_MEDIA = import.meta.glob( { eager: true, import: "default" } ) as Record -const QUIZ_MEDIA: string[] = [] -const BLINDTEST_MEDIA: string[] = [] +const MEDIA: Record = { quiz: [], image: [], blindtest: [] } const SHARED_MEDIA: string[] = [] for (const [path, url] of Object.entries(ALL_MEDIA)) { - if (path.includes("/transitions/quiz/")) QUIZ_MEDIA.push(url) - else if (path.includes("/transitions/blindtest/")) BLINDTEST_MEDIA.push(url) + if (path.includes("/transitions/quiz/")) MEDIA.quiz.push(url) + else if (path.includes("/transitions/image/")) MEDIA.image.push(url) + else if (path.includes("/transitions/blindtest/")) MEDIA.blindtest.push(url) else SHARED_MEDIA.push(url) } const THEME: Record< - RoundType, + TransitionKind, { gradient: string; accent: string; label: string; kind: string; emoji: string } > = { quiz: { @@ -35,6 +38,13 @@ const THEME: Record< kind: "Question", emoji: "🧠", }, + image: { + gradient: "from-emerald-500 via-teal-600 to-cyan-700", + accent: "text-lime-300", + label: "IMAGE", + kind: "Image", + emoji: "đŸ–Œïž", + }, blindtest: { gradient: "from-fuchsia-500 via-purple-600 to-indigo-700", accent: "text-yellow-300", @@ -44,29 +54,28 @@ const THEME: Record< }, } -function mediaFor(type: RoundType): string[] { - const own = type === "quiz" ? QUIZ_MEDIA : BLINDTEST_MEDIA - return [...own, ...SHARED_MEDIA] +function mediaFor(kind: TransitionKind): string[] { + return [...MEDIA[kind], ...SHARED_MEDIA] } /** - * Transition "WarioWare" entre manches, thĂ©mĂ©e par mode. Annonce le mode quand - * il change (quiz ↔ blindtest), sinon affiche un teaser lĂ©ger de la manche. + * Transition "WarioWare" entre manches, thĂ©mĂ©e par mode (quiz / image / blindtest). + * Annonce le mode quand il change, sinon affiche un teaser lĂ©ger de la manche. * MontĂ©e avec une `key` qui change → rejoue Ă  chaque manche. */ export function RoundTransition({ - type, + kind, index, modeChanged, }: { - type: RoundType + kind: TransitionKind index: number modeChanged: boolean }) { const [show, setShow] = useState(true) - const theme = THEME[type] + const theme = THEME[kind] const [media] = useState(() => { - const pool = mediaFor(type) + const pool = mediaFor(kind) return pool.length ? pool[Math.floor(Math.random() * pool.length)] : null }) diff --git a/apps/web/src/pages/room.tsx b/apps/web/src/pages/room.tsx index 05e0db4..f9edea7 100644 --- a/apps/web/src/pages/room.tsx +++ b/apps/web/src/pages/room.tsx @@ -22,6 +22,7 @@ export function RoomPage({ code }: { code: string }) { const roundKey = useRoomStore((s) => s.roundKey) const voteProgress = useRoomStore((s) => s.voteProgress) const round = useRoomStore((s) => s.round) + const roundKind = useRoomStore((s) => s.roundKind) const roundModeChanged = useRoomStore((s) => s.roundModeChanged) // Pas dans cette room (accĂšs direct sans rejoindre / refresh) : retour accueil. @@ -106,7 +107,7 @@ export function RoomPage({ code }: { code: string }) { {roundKey > 0 && round && ( diff --git a/apps/web/src/store/room.ts b/apps/web/src/store/room.ts index 74cdad0..e978fb7 100644 --- a/apps/web/src/store/room.ts +++ b/apps/web/src/store/room.ts @@ -54,7 +54,9 @@ interface RoomState { mediaSync: MediaSyncPayload | null boomKey: number roundKey: number - /** Le type d'Ă©preuve a-t-il changĂ© par rapport Ă  la manche prĂ©cĂ©dente ? */ + /** CatĂ©gorie visuelle de la manche (image_reveal distinct du quiz). */ + roundKind: "quiz" | "image" | "blindtest" + /** Le mode a-t-il changĂ© par rapport Ă  la manche prĂ©cĂ©dente ? */ roundModeChanged: boolean createRoom: () => Promise @@ -91,6 +93,7 @@ export const useRoomStore = create((set, get) => ({ mediaSync: null, boomKey: 0, roundKey: 0, + roundKind: "quiz", roundModeChanged: false, createRoom: () => @@ -221,6 +224,7 @@ export const useRoomStore = create((set, get) => ({ mediaSync: null, boomKey: 0, roundKey: 0, + roundKind: "quiz", roundModeChanged: false, }), })) @@ -248,23 +252,33 @@ socket.on("room:state", (snapshot) => socket.on("error", (error) => useRoomStore.setState({ lastError: error })) socket.on("round:start", (payload) => - useRoomStore.setState((s) => ({ - round: { - type: payload.type, - djId: payload.djId, - mine: payload.mine, - startsAt: payload.startsAt, - endsAt: payload.endsAt, - payload: payload.payload, - }, - voteProgress: null, - reveal: null, - myChoiceIndex: null, - hasVoted: false, - mediaSync: null, - roundKey: s.roundKey + 1, - roundModeChanged: (s.round?.type ?? null) !== payload.type, - })) + useRoomStore.setState((s) => { + const fmt = (payload.payload as { format?: string } | null)?.format + const kind = + payload.type === "blindtest" + ? "blindtest" + : fmt === "image_reveal" + ? "image" + : "quiz" + return { + round: { + type: payload.type, + djId: payload.djId, + mine: payload.mine, + startsAt: payload.startsAt, + endsAt: payload.endsAt, + payload: payload.payload, + }, + voteProgress: null, + reveal: null, + myChoiceIndex: null, + hasVoted: false, + mediaSync: null, + roundKey: s.roundKey + 1, + roundKind: kind, + roundModeChanged: s.roundKind !== kind, + } + }) ) socket.on("round:voteAck", (voteProgress) => useRoomStore.setState({ voteProgress })