diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 560e157..1a9570e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,12 +8,14 @@ "name": "wm2026-tippspiel-frontend", "version": "1.0.0", "dependencies": { + "canvas-confetti": "^1.9.4", "lucide-react": "^1.7.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.24.0" }, "devDependencies": { + "@types/canvas-confetti": "^1.9.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", @@ -1155,6 +1157,13 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/canvas-confetti": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", + "integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1279,6 +1288,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", + "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 521c4ac..9460d50 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,12 +8,14 @@ "preview": "vite preview" }, "dependencies": { + "canvas-confetti": "^1.9.4", "lucide-react": "^1.7.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.24.0" }, "devDependencies": { + "@types/canvas-confetti": "^1.9.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", diff --git a/frontend/src/components/ConfettiReveal.module.css b/frontend/src/components/ConfettiReveal.module.css new file mode 100644 index 0000000..c5902aa --- /dev/null +++ b/frontend/src/components/ConfettiReveal.module.css @@ -0,0 +1,91 @@ +.overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; + animation: fadeIn 0.3s ease; +} + +.card { + background: var(--surface-mid); + border-radius: var(--radius-lg); + padding: 32px 24px; + text-align: center; + max-width: 320px; + width: 90%; + animation: scaleIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +.result { + font-size: 1.3rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 8px; +} + +.tipLine { + font-size: 0.95rem; + color: var(--text-secondary); + margin-bottom: 16px; +} + +.badge { + display: inline-block; + padding: 8px 20px; + border-radius: 20px; + font-size: 1.1rem; + font-weight: 700; + margin-bottom: 8px; +} + +.badgeExact { + background: linear-gradient(135deg, var(--gold), #FFD700); + color: #1a1a1a; + animation: shimmer 2s ease-in-out; +} + +.badgeTendency { + background: var(--success); + color: #1a1a1a; +} + +.badgeWrong { + background: var(--text-muted); + color: var(--bg-deep); +} + +.label { + font-size: 1rem; + color: var(--text-primary); + margin-bottom: 20px; +} + +.dismissBtn { + background: var(--primary); + color: white; + border: none; + border-radius: var(--radius-sm); + padding: 10px 32px; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes scaleIn { + from { transform: scale(0.8); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +@keyframes shimmer { + 0% { box-shadow: 0 0 0 rgba(254, 174, 50, 0); } + 50% { box-shadow: 0 0 20px rgba(254, 174, 50, 0.6); } + 100% { box-shadow: 0 0 0 rgba(254, 174, 50, 0); } +} diff --git a/frontend/src/components/ConfettiReveal.tsx b/frontend/src/components/ConfettiReveal.tsx new file mode 100644 index 0000000..b6f270c --- /dev/null +++ b/frontend/src/components/ConfettiReveal.tsx @@ -0,0 +1,43 @@ +import { useEffect, useRef } from 'react'; +import confetti from 'canvas-confetti'; +import { Match } from '../api/client'; +import styles from './ConfettiReveal.module.css'; + +interface Props { + match: Match; + onDismiss: () => void; +} + +export default function ConfettiReveal({ match, onDismiss }: Props) { + const didFire = useRef(false); + const tip = match.userTip!; + const points = tip.points!; + + useEffect(() => { + if (points === 3 && !didFire.current) { + didFire.current = true; + confetti({ particleCount: 120, spread: 80, origin: { y: 0.6 } }); + } + }, [points]); + + const resultLabel = points === 3 ? 'EXAKT! 🎉' : points === 1 ? 'Richtige Tendenz! 👏' : 'Knapp daneben... 😅'; + const badgeClass = points === 3 ? styles.badgeExact : points === 1 ? styles.badgeTendency : styles.badgeWrong; + + return ( +