diff --git a/frontend/package.json b/frontend/package.json index 7c16f33..b05b867 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,8 @@ }, "dependencies": { "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "canvas-confetti": "^1.9.3" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.1", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ba7ca9f..634bd7b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; +import WinnerCelebration from "./components/WinnerCelebration"; import { api } from "./api/client"; import { cycleTag } from "./utils/cycleTag"; @@ -79,6 +80,14 @@ export default function App() { const lastMemberIdsRef = useRef(new Set()); const membersBaselineRef = useRef(false); + // ===== Winner Celebration ===== + const [celebrateOpen, setCelebrateOpen] = useState(false); + const [celebrateName, setCelebrateName] = useState(""); + + // baseline per game: beim ersten Meta-Load NICHT feiern + const winnerBaselineRef = useRef(false); + const lastWinnerIdRef = useRef(null); + const showSnack = (msg) => { setSnack(msg); if (snackTimerRef.current) clearTimeout(snackTimerRef.current); @@ -185,6 +194,12 @@ export default function App() { membersBaselineRef.current = false; lastMemberIdsRef.current = new Set(); + // reset winner celebration baseline when switching games + winnerBaselineRef.current = false; + lastWinnerIdRef.current = null; + setCelebrateOpen(false); + setCelebrateName(""); + (async () => { if (!gameId) return; try { @@ -225,6 +240,32 @@ export default function App() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [me?.id, gameId]); + // ✅ Winner Celebration Trigger: + // - beim ersten Load eines Spiels NICHT feiern + // - feiern nur, wenn winner_user_id sich danach ändert (Host speichert) + useEffect(() => { + const wid = gameMeta?.winner_user_id ? String(gameMeta.winner_user_id) : ""; + if (!wid) return; + + if (!winnerBaselineRef.current) { + winnerBaselineRef.current = true; + lastWinnerIdRef.current = wid; + return; + } + + if (lastWinnerIdRef.current !== wid) { + lastWinnerIdRef.current = wid; + + const name = + (gameMeta?.winner_display_name || "").trim() || + (gameMeta?.winner_email || "").trim() || + "Jemand"; + + setCelebrateName(name); + setCelebrateOpen(true); + } + }, [gameMeta?.winner_user_id, gameMeta?.winner_display_name, gameMeta?.winner_email]); + // ===== Auth actions ===== const doLogin = async () => { await api("/auth/login", { @@ -243,6 +284,12 @@ export default function App() { setGameMeta(null); setMembers([]); setWinnerUserId(""); + + // reset winner celebration on logout + winnerBaselineRef.current = false; + lastWinnerIdRef.current = null; + setCelebrateOpen(false); + setCelebrateName(""); }; // ===== Password ===== @@ -338,6 +385,12 @@ export default function App() { setChipOpen(false); setChipEntry(null); + // reset winner celebration baseline for the new game + winnerBaselineRef.current = false; + lastWinnerIdRef.current = null; + setCelebrateOpen(false); + setCelebrateName(""); + const g = await api("/games", { method: "POST", body: JSON.stringify({ name: "Spiel " + new Date().toLocaleString() }), @@ -491,6 +544,13 @@ export default function App() { return (