chore: cleanup — remove compiled .js files and .superpowers artifacts
Added frontend/src/**/*.js and .superpowers/ to .gitignore. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { Home, Trophy, User } from 'lucide-react';
|
||||
import styles from './BottomNav.module.css';
|
||||
export default function BottomNav() {
|
||||
const linkClass = ({ isActive }) => isActive ? styles.tabActive : styles.tab;
|
||||
return (_jsxs("nav", { className: styles.bottomNav, children: [_jsxs(NavLink, { to: "/", end: true, className: linkClass, children: [_jsx(Home, { size: 20 }), _jsx("span", { children: "Home" })] }), _jsxs(NavLink, { to: "/spiele", className: linkClass, children: [_jsx("span", { className: styles.emojiIcon, children: "\u26BD" }), _jsx("span", { children: "Spiele" })] }), _jsxs(NavLink, { to: "/rangliste", className: linkClass, children: [_jsx(Trophy, { size: 20 }), _jsx("span", { children: "Rangliste" })] }), _jsxs(NavLink, { to: "/profil", className: linkClass, children: [_jsx(User, { size: 20 }), _jsx("span", { children: "Profil" })] })] }));
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
||||
import { useEffect, useRef } from 'react';
|
||||
import confetti from 'canvas-confetti';
|
||||
import styles from './ConfettiReveal.module.css';
|
||||
export default function ConfettiReveal({ match, onDismiss }) {
|
||||
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 (_jsx("div", { className: styles.overlay, onClick: onDismiss, children: _jsxs("div", { className: `card ${styles.card}`, onClick: e => e.stopPropagation(), children: [_jsxs("div", { className: styles.result, children: [match.homeTeam.shortName, " ", match.score.home, ":", match.score.away, " ", match.awayTeam.shortName] }), _jsxs("div", { className: styles.tipLine, children: ["Dein Tipp: ", tip.home, ":", tip.away] }), _jsxs("div", { className: `${styles.badge} ${badgeClass}`, children: [points, " ", points === 1 ? 'Punkt' : 'Punkte'] }), _jsx("div", { className: styles.label, children: resultLabel }), _jsx("button", { className: styles.dismissBtn, onClick: onDismiss, children: "Weiter" })] }) }));
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useState } from 'react';
|
||||
import styles from './DevPanel.module.css';
|
||||
const DEV_USERS = [
|
||||
{ id: 1, name: 'Ronny M.', role: 'Editor' },
|
||||
{ id: 2, name: 'Max M.', role: 'Viewer' },
|
||||
{ id: 3, name: 'Anna S.', role: 'Viewer' },
|
||||
];
|
||||
const TIME_PRESETS = [
|
||||
{ label: 'In 2 Std.', minutes: 120 },
|
||||
{ label: 'In 10 Min.', minutes: 10 },
|
||||
{ label: 'Jetzt +1 Min.', minutes: 1 },
|
||||
{ label: 'Läuft (−30)', minutes: -30 },
|
||||
{ label: 'Beendet (−120)', minutes: -120 },
|
||||
];
|
||||
const STATUS_PRESETS = [
|
||||
{ label: 'TIMED', status: 'TIMED', scoreHome: null, scoreAway: null },
|
||||
{ label: 'LIVE', status: 'IN_PLAY', scoreHome: 0, scoreAway: 0 },
|
||||
{ label: 'Pause', status: 'PAUSED', scoreHome: 1, scoreAway: 0 },
|
||||
{ label: '2:1 Fertig', status: 'FINISHED', scoreHome: 2, scoreAway: 1 },
|
||||
{ label: '0:0 Fertig', status: 'FINISHED', scoreHome: 0, scoreAway: 0 },
|
||||
];
|
||||
export default function DevPanel({ currentUser, onUserChange, matches, onRefresh }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedMatch, setSelectedMatch] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [log, setLog] = useState([]);
|
||||
function addLog(msg) {
|
||||
setLog(prev => [`${new Date().toLocaleTimeString('de-DE')} ${msg}`, ...prev].slice(0, 8));
|
||||
}
|
||||
async function applyTime(minutes) {
|
||||
if (!selectedMatch)
|
||||
return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await fetch(`/api/dev/match/${selectedMatch}/set-time`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ minutesFromNow: minutes }),
|
||||
});
|
||||
addLog(`✓ Spiel #${selectedMatch}: Zeit → ${minutes > 0 ? `+${minutes}` : minutes} Min.`);
|
||||
onRefresh();
|
||||
}
|
||||
catch (e) {
|
||||
addLog(`✗ Fehler: ${e.message}`);
|
||||
}
|
||||
finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
async function applyStatus(status, scoreHome, scoreAway) {
|
||||
if (!selectedMatch)
|
||||
return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await fetch(`/api/dev/match/${selectedMatch}/set-status`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status, scoreHome, scoreAway }),
|
||||
});
|
||||
addLog(`✓ Spiel #${selectedMatch}: Status → ${status}`);
|
||||
onRefresh();
|
||||
}
|
||||
catch (e) {
|
||||
addLog(`✗ Fehler: ${e.message}`);
|
||||
}
|
||||
finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
async function resetTips() {
|
||||
setBusy(true);
|
||||
try {
|
||||
await fetch('/api/dev/reset-tips', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userId: `dev-user-00${currentUser}` }),
|
||||
});
|
||||
addLog(`✓ Tipps von User ${currentUser} gelöscht`);
|
||||
onRefresh();
|
||||
}
|
||||
catch (e) {
|
||||
addLog(`✗ Fehler: ${e.message}`);
|
||||
}
|
||||
finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
async function resetMatch(all) {
|
||||
setBusy(true);
|
||||
try {
|
||||
const body = all ? {} : { matchId: selectedMatch };
|
||||
const res = await fetch('/api/dev/reset-match', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (all) {
|
||||
addLog(`✓ ${data.count ?? 0} Spiele zurückgesetzt (TIMED)`);
|
||||
}
|
||||
else {
|
||||
addLog(`✓ Spiel #${selectedMatch} zurückgesetzt (TIMED)`);
|
||||
}
|
||||
onRefresh();
|
||||
}
|
||||
catch (e) {
|
||||
addLog(`✗ Fehler: ${e.message}`);
|
||||
}
|
||||
finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
// Nur erste 20 Spiele zur Auswahl anbieten
|
||||
const selectableMatches = matches.slice(0, 20);
|
||||
return (_jsxs("div", { className: styles.wrap, children: [_jsxs("button", { className: styles.toggleBtn, onClick: () => setOpen(o => !o), children: [open ? '✕' : '🧪', " ", !open && _jsx("span", { className: styles.toggleLabel, children: "Dev" })] }), open && (_jsxs("div", { className: styles.panel, children: [_jsx("div", { className: styles.panelHeader, children: _jsx("span", { className: styles.panelTitle, children: "\uD83E\uDDEA Simulations-Modus" }) }), _jsxs("section", { className: styles.section, children: [_jsx("div", { className: styles.sectionLabel, children: "Aktiver User" }), _jsx("div", { className: styles.userButtons, children: DEV_USERS.map(u => (_jsxs("button", { className: `${styles.userBtn} ${currentUser === u.id ? styles.userBtnActive : ''}`, onClick: () => {
|
||||
onUserChange(u.id);
|
||||
addLog(`→ Wechsel zu User ${u.id}: ${u.name}`);
|
||||
}, children: [_jsx("span", { className: styles.userInitial, children: u.name.charAt(0) }), _jsx("span", { className: styles.userName, children: u.name }), _jsx("span", { className: styles.userRole, children: u.role })] }, u.id))) })] }), _jsxs("section", { className: styles.section, children: [_jsx("div", { className: styles.sectionLabel, children: "Spiel ausw\u00E4hlen" }), _jsxs("select", { className: styles.select, value: selectedMatch, onChange: e => setSelectedMatch(e.target.value ? parseInt(e.target.value) : ''), children: [_jsx("option", { value: "", children: "\u2014 Spiel w\u00E4hlen \u2014" }), selectableMatches.map(m => (_jsxs("option", { value: m.id, children: ["#", m.id, " ", m.homeTeam.shortName, " vs ", m.awayTeam.shortName] }, m.id)))] })] }), _jsxs("section", { className: `${styles.section} ${!selectedMatch ? styles.sectionDisabled : ''}`, children: [_jsx("div", { className: styles.sectionLabel, children: "Ansto\u00DFzeit setzen" }), _jsx("div", { className: styles.presetGrid, children: TIME_PRESETS.map(p => (_jsx("button", { className: styles.presetBtn, onClick: () => applyTime(p.minutes), disabled: !selectedMatch || busy, children: p.label }, p.label))) })] }), _jsxs("section", { className: `${styles.section} ${!selectedMatch ? styles.sectionDisabled : ''}`, children: [_jsx("div", { className: styles.sectionLabel, children: "Status setzen" }), _jsx("div", { className: styles.presetGrid, children: STATUS_PRESETS.map(p => (_jsx("button", { className: `${styles.presetBtn} ${p.status === 'FINISHED' ? styles.presetBtnDanger : p.status === 'IN_PLAY' ? styles.presetBtnLive : ''}`, onClick: () => applyStatus(p.status, p.scoreHome, p.scoreAway), disabled: !selectedMatch || busy, children: p.label }, p.label))) })] }), _jsxs("section", { className: styles.section, children: [_jsx("div", { className: styles.sectionLabel, children: "Zur\u00FCcksetzen" }), _jsxs("div", { className: styles.resetGrid, children: [_jsx("button", { className: styles.resetBtn, onClick: () => resetMatch(false), disabled: !selectedMatch || busy, title: "Ausgew\u00E4hltes Spiel auf TIMED zur\u00FCcksetzen", children: "\u21BA Spiel zur\u00FCcksetzen" }), _jsx("button", { className: `${styles.resetBtn} ${styles.resetBtnAll}`, onClick: () => resetMatch(true), disabled: busy, title: "Alle laufenden/beendeten Spiele zur\u00FCcksetzen", children: "\u21BA Alle Spiele" }), _jsx("button", { className: `${styles.resetBtn} ${styles.resetBtnTips}`, onClick: resetTips, disabled: busy, children: "\uD83D\uDDD1 Tipps l\u00F6schen" })] })] }), log.length > 0 && (_jsx("div", { className: styles.log, children: log.map((l, i) => _jsx("div", { className: styles.logLine, children: l }, i)) }))] }))] }));
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
||||
import { Check, TrendingUp, X } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import styles from './MatchCard.module.css';
|
||||
function getCardState(match) {
|
||||
if (match.status === 'IN_PLAY' || match.status === 'PAUSED')
|
||||
return 'live';
|
||||
if (match.status === 'FINISHED') {
|
||||
return match.userTip ? 'finished' : 'missed';
|
||||
}
|
||||
// SCHEDULED or TIMED
|
||||
return match.userTip ? 'tipped' : 'open';
|
||||
}
|
||||
function useCountdown(minutesUntilKickoff) {
|
||||
const [remaining, setRemaining] = useState(minutesUntilKickoff);
|
||||
useEffect(() => {
|
||||
if (minutesUntilKickoff > 60)
|
||||
return; // only active for <1h
|
||||
setRemaining(minutesUntilKickoff);
|
||||
const interval = setInterval(() => {
|
||||
setRemaining(r => Math.max(0, r - 1 / 60));
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [minutesUntilKickoff]);
|
||||
return remaining;
|
||||
}
|
||||
const STATUS_LABELS = {
|
||||
SCHEDULED: 'Geplant',
|
||||
TIMED: 'Terminiert',
|
||||
IN_PLAY: 'Live',
|
||||
PAUSED: 'Pause',
|
||||
FINISHED: 'Beendet',
|
||||
POSTPONED: 'Verschoben',
|
||||
CANCELLED: 'Abgesagt',
|
||||
};
|
||||
function formatKickoff(utcDate) {
|
||||
return new Date(utcDate).toLocaleString('de-DE', {
|
||||
hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Berlin',
|
||||
}) + ' Uhr';
|
||||
}
|
||||
function CountdownBadge({ minutes }) {
|
||||
if (minutes <= 0)
|
||||
return null;
|
||||
if (minutes < 60)
|
||||
return _jsxs("span", { className: styles.badgeUrgent, children: ["\u26A1 in ", minutes, " Min."] });
|
||||
const h = Math.floor(minutes / 60);
|
||||
const m = minutes % 60;
|
||||
if (h < 24)
|
||||
return _jsxs("span", { className: styles.badge, children: ["in ", h, "h", m > 0 ? ` ${m}m` : ''] });
|
||||
const d = Math.floor(h / 24);
|
||||
return _jsxs("span", { className: styles.badge, children: ["in ", d, " Tag", d > 1 ? 'en' : ''] });
|
||||
}
|
||||
function FlagBox({ crest, name }) {
|
||||
return (_jsx("div", { className: styles.flagBox, children: crest
|
||||
? _jsx("img", { className: styles.crest, src: crest, alt: name })
|
||||
: _jsx("span", { style: { fontSize: 18 }, children: "\uD83C\uDFF3\uFE0F" }) }));
|
||||
}
|
||||
export default function MatchCard({ match, onTip }) {
|
||||
const state = getCardState(match);
|
||||
const remaining = useCountdown(match.minutesUntilKickoff);
|
||||
const remainingMins = Math.ceil(remaining);
|
||||
const isFinished = state === 'finished' || state === 'missed';
|
||||
const isLive = state === 'live';
|
||||
const hasTip = !!match.userTip;
|
||||
const points = match.userTip?.points ?? null;
|
||||
const resultClass = points === 3 ? styles.exact :
|
||||
points === 1 ? styles.tendency :
|
||||
(points === 0 && isFinished) ? styles.wrong : '';
|
||||
const glowClass = isFinished && points === 3 ? styles.glowExact :
|
||||
isFinished && points === 1 ? styles.glowTendency :
|
||||
isFinished && points === 0 ? styles.glowWrong : '';
|
||||
return (_jsxs("div", { className: `card ${styles.card} ${styles[`card_${state}`]} ${isLive ? styles.live : ''} ${glowClass}`, children: [_jsxs("div", { className: styles.topRow, children: [_jsxs("span", { className: `${styles.status} ${isLive ? styles.statusLive : ''}`, children: [isLive && _jsx("span", { className: styles.liveDot }), STATUS_LABELS[match.status] ?? match.status] }), match.group && (_jsx("span", { className: styles.group, children: match.group.replace('GROUP_', 'Gruppe ') })), (state === 'open' || state === 'tipped') && match.tippable && (match.minutesUntilKickoff < 60 ? (_jsxs("span", { className: `${styles.countdown} ${remainingMins < 5 ? styles.countdownUrgent : ''}`, children: ["Noch ", remainingMins, " Min!"] })) : (_jsx(CountdownBadge, { minutes: match.minutesUntilKickoff })))] }), _jsxs("div", { className: styles.matchRow, children: [_jsxs("div", { className: styles.teamHome, children: [_jsx("span", { className: styles.teamName, children: match.homeTeam.name }), _jsx(FlagBox, { crest: match.homeTeam.crest, name: match.homeTeam.name })] }), _jsx("div", { className: styles.scoreBox, children: isFinished || isLive ? (_jsxs("div", { className: styles.scoreStack, children: [_jsxs("span", { className: styles.score, children: [match.score.home ?? '–', "\u00A0:\u00A0", match.score.away ?? '–'] }), isLive && hasTip && (_jsxs("span", { className: styles.liveTipCompare, children: ["Tipp: ", match.userTip.home, ":", match.userTip.away] }))] })) : (_jsx("div", { className: styles.kickoffCenter, children: _jsx("span", { className: styles.kickoffCenterTime, children: formatKickoff(match.utcDate) }) })) }), _jsxs("div", { className: styles.teamAway, children: [_jsx(FlagBox, { crest: match.awayTeam.crest, name: match.awayTeam.name }), _jsx("span", { className: styles.teamName, children: match.awayTeam.name })] })] }), _jsx("div", { className: `${styles.tipRow} ${hasTip && points !== null ? `${styles.resultBanner} ${resultClass}` : ''}`, children: state === 'missed' ? (
|
||||
/* ── Missed: no tip, match finished ── */
|
||||
_jsx("span", { className: styles.missedLabel, children: "Nicht getippt" })) : state === 'live' ? (
|
||||
/* ── Live: no tip input, locked ── */
|
||||
hasTip ? (_jsxs("div", { className: styles.tipDisplay, children: [_jsx("div", { className: styles.tipLeft }), _jsxs("div", { className: styles.tipCenter, children: [_jsx("span", { className: styles.tipLabel, children: "DEIN TIPP" }), _jsxs("span", { className: styles.tipScore, children: [match.userTip.home, " : ", match.userTip.away] })] }), _jsx("div", { className: styles.tipRight })] })) : (_jsx("span", { className: styles.noTip, children: "Kein Tipp abgegeben" }))) : hasTip ? (points !== null ? (
|
||||
/* ── Auswertungs-Banner ── */
|
||||
_jsxs("div", { className: styles.tipDisplay, children: [_jsxs("div", { className: `${styles.tipLeft} ${styles.bannerLeft}`, children: [_jsx("span", { className: styles.resultIcon, children: points === 3 ? _jsx(Check, { size: 14, strokeWidth: 3 }) :
|
||||
points === 1 ? _jsx(TrendingUp, { size: 14, strokeWidth: 2.5 }) :
|
||||
_jsx(X, { size: 14, strokeWidth: 3 }) }), _jsx("span", { className: styles.resultLabel, children: points === 3 ? 'Exakt' : points === 1 ? 'Tendenz' : 'Falsch' })] }), _jsx("div", { className: styles.tipCenter, children: _jsxs("span", { className: styles.tipScoreBanner, children: [match.userTip.home, " : ", match.userTip.away] }) }), _jsx("div", { className: styles.tipRight, children: _jsx("span", { className: `${styles.pointsBadge} ${points === 3 ? styles.pointsBadge_exact :
|
||||
points === 1 ? styles.pointsBadge_tendency :
|
||||
styles.pointsBadge_wrong}`, children: points === 0 ? '0 Pkt.' : `+${points} Pkt.` }) })] })) : (
|
||||
/* ── Tipp vorhanden, noch nicht ausgewertet (tipped state) ── */
|
||||
_jsxs("div", { className: styles.tipDisplay, children: [_jsx("div", { className: styles.tipLeft, children: match.tippable && (_jsx("button", { className: styles.changeBtn, onClick: onTip, children: "\u00C4ndern" })) }), _jsxs("div", { className: styles.tipCenter, children: [!match.tippable && _jsx("span", { className: styles.tipLabel, children: "DEIN TIPP" }), _jsxs("span", { className: styles.tipDisplay_score, children: [_jsx(Check, { size: 13, strokeWidth: 3, style: { color: 'var(--success)', flexShrink: 0 } }), _jsxs("span", { className: styles.tipScore, children: [match.userTip.home, " : ", match.userTip.away] })] })] }), _jsx("div", { className: styles.tipRight })] }))) : match.tippable ? (_jsx("button", { className: `btn-primary ${styles.tipBtn}`, onClick: onTip, children: "Tipp abgeben" })) : (_jsx("span", { className: styles.noTip, children: "Kein Tipp abgegeben" })) })] }));
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import styles from './StatsRing.module.css';
|
||||
export default function StatsRing({ exact, tendency, wrong, total }) {
|
||||
const radius = 55;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const all = exact + tendency + wrong;
|
||||
const hasData = all > 0;
|
||||
const segments = hasData ? [
|
||||
{ value: exact / all, color: 'var(--gold)', label: 'Exakt', count: exact },
|
||||
{ value: tendency / all, color: 'var(--success)', label: 'Tendenz', count: tendency },
|
||||
{ value: wrong / all, color: 'var(--error)', label: 'Falsch', count: wrong },
|
||||
] : [];
|
||||
let offset = 0;
|
||||
return (_jsxs("div", { className: styles.ring, children: [_jsxs("svg", { viewBox: "0 0 140 140", className: styles.svg, children: [_jsx("circle", { cx: "70", cy: "70", r: radius, fill: "none", stroke: "var(--surface-high)", strokeWidth: "12" }), segments.map((seg, i) => {
|
||||
if (seg.value === 0)
|
||||
return null;
|
||||
const dashArray = `${seg.value * circumference} ${circumference}`;
|
||||
const rotation = offset * 360 - 90;
|
||||
offset += seg.value;
|
||||
return (_jsx("circle", { cx: "70", cy: "70", r: radius, fill: "none", stroke: seg.color, strokeWidth: "12", strokeDasharray: dashArray, transform: `rotate(${rotation} 70 70)`, strokeLinecap: "round" }, i));
|
||||
}), _jsx("text", { x: "70", y: "65", textAnchor: "middle", dominantBaseline: "central", fill: "var(--text-primary)", fontSize: "28", fontWeight: "700", children: total }), _jsx("text", { x: "70", y: "85", textAnchor: "middle", fill: "var(--text-secondary)", fontSize: "11", children: hasData ? 'Punkte' : 'Keine Tipps' })] }), hasData && (_jsx("div", { className: styles.legend, children: segments.map((seg, i) => (_jsxs("span", { className: styles.legendItem, children: [_jsx("span", { className: styles.dot, style: { background: seg.color } }), seg.label, ": ", seg.count] }, i))) }))] }));
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import styles from './TipModal.module.css';
|
||||
function getTendency(home, away) {
|
||||
if (home > away)
|
||||
return 'home';
|
||||
if (away > home)
|
||||
return 'away';
|
||||
return 'draw';
|
||||
}
|
||||
export default function TipModal({ match, onClose, onSaved }) {
|
||||
const existing = match.userTip;
|
||||
const [home, setHome] = useState(existing?.home ?? 0);
|
||||
const [away, setAway] = useState(existing?.away ?? 0);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const tendency = getTendency(home, away);
|
||||
const tendencyLabel = tendency === 'home' ? match.homeTeam.shortName || match.homeTeam.name :
|
||||
tendency === 'away' ? match.awayTeam.shortName || match.awayTeam.name :
|
||||
'Unentschieden';
|
||||
const tendencyColor = tendency === 'home' ? 'var(--primary)' :
|
||||
tendency === 'away' ? 'var(--cyan)' :
|
||||
'var(--gold)';
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
await api.submitTip(match.id, home, away);
|
||||
setShowSuccess(true);
|
||||
if (navigator.vibrate)
|
||||
navigator.vibrate(50); // haptic feedback
|
||||
setTimeout(() => {
|
||||
setShowSuccess(false);
|
||||
onSaved(match.id, home, away);
|
||||
onClose();
|
||||
}, 1200);
|
||||
}
|
||||
catch (e) {
|
||||
setError(e.message);
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
return (_jsx("div", { className: styles.overlay, onClick: onClose, children: _jsxs("div", { className: styles.sheet, onClick: e => e.stopPropagation(), children: [_jsx("div", { className: styles.handle }), _jsxs("div", { className: styles.teamsRow, children: [_jsxs("div", { className: styles.teamBlock, children: [_jsx("div", { className: styles.flagLarge, children: match.homeTeam.crest
|
||||
? _jsx("img", { src: match.homeTeam.crest, alt: match.homeTeam.name, className: styles.flagImg })
|
||||
: _jsx("span", { className: styles.flagEmoji, children: "\uD83C\uDFF3\uFE0F" }) }), _jsx("span", { className: styles.teamName, children: match.homeTeam.name })] }), _jsx("div", { className: styles.vsBlock }), _jsxs("div", { className: styles.teamBlock, children: [_jsx("div", { className: styles.flagLarge, children: match.awayTeam.crest
|
||||
? _jsx("img", { src: match.awayTeam.crest, alt: match.awayTeam.name, className: styles.flagImg })
|
||||
: _jsx("span", { className: styles.flagEmoji, children: "\uD83C\uDFF3\uFE0F" }) }), _jsx("span", { className: styles.teamName, children: match.awayTeam.name })] })] }), _jsxs("div", { className: styles.pickerSection, children: [_jsx("p", { className: styles.pickerLabel, children: "Dein Tipp" }), _jsxs("div", { className: styles.pickerRow, children: [_jsx(ScorePicker, { value: home, onChange: setHome }), _jsx("div", { className: styles.pickerColon, children: ":" }), _jsx(ScorePicker, { value: away, onChange: setAway })] })] }), _jsxs("div", { className: styles.tendencyBar, style: { '--tendency-color': tendencyColor }, children: [_jsx("span", { className: styles.tendencyIcon, children: tendency === 'draw' ? '🤝' : tendency === 'home' ? '🏠' : '✈️' }), _jsxs("span", { className: styles.tendencyText, children: ["Tendenz: ", _jsx("strong", { children: tendencyLabel })] })] }), error && _jsx("div", { className: styles.error, children: error }), showSuccess && (_jsxs("div", { className: styles.successOverlay, children: [_jsx("div", { className: styles.successCheck, children: "\u2713" }), _jsx("div", { className: styles.successText, children: "Dein Tipp ist drin! \uD83C\uDFAF" })] })), _jsx("button", { className: `btn-primary ${styles.saveBtn}`, onClick: handleSave, disabled: saving, children: saving ? '⏳ Wird gespeichert…' : '✓ Tipp bestätigen' }), _jsx("button", { className: styles.cancelBtn, onClick: onClose, children: "Abbrechen" })] }) }));
|
||||
}
|
||||
function ScorePicker({ value, onChange }) {
|
||||
return (_jsxs("div", { className: styles.picker, children: [_jsx("button", { className: styles.pickerBtn, onClick: () => onChange(Math.min(20, value + 1)), "aria-label": "Erh\u00F6hen", children: "+" }), _jsx("span", { className: styles.pickerValue, children: value }), _jsx("button", { className: styles.pickerBtn, onClick: () => onChange(Math.max(0, value - 1)), "aria-label": "Verringern", children: "\u2212" })] }));
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { jsx as _jsx } from "react/jsx-runtime";
|
||||
import { useEffect, useRef } from 'react';
|
||||
import styles from './Toast.module.css';
|
||||
export default function Toast({ message, onDismiss, duration = 5000 }) {
|
||||
const onDismissRef = useRef(onDismiss);
|
||||
onDismissRef.current = onDismiss;
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => onDismissRef.current(), duration);
|
||||
return () => clearTimeout(timer);
|
||||
}, [duration]);
|
||||
return (_jsx("div", { className: `card ${styles.toast}`, onClick: onDismiss, children: message }));
|
||||
}
|
||||
Reference in New Issue
Block a user