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,62 +0,0 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||
import { useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import styles from './AdminPage.module.css';
|
||||
export default function AdminPage() {
|
||||
const [syncResult, setSyncResult] = useState(null);
|
||||
const [evalResult, setEvalResult] = useState(null);
|
||||
const [refreshResult, setRefreshResult] = useState(null);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [evaluating, setEvaluating] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const handleSync = async () => {
|
||||
setSyncing(true);
|
||||
setSyncResult(null);
|
||||
try {
|
||||
const res = await api.syncMatches();
|
||||
setSyncResult({ success: true, timestamp: new Date(), message: `${res.total} Spiele geladen — ${res.created} neu, ${res.updated} aktualisiert` });
|
||||
}
|
||||
catch (e) {
|
||||
setSyncResult({ success: false, timestamp: new Date(), message: e.message });
|
||||
}
|
||||
finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
const handleEvaluate = async () => {
|
||||
setEvaluating(true);
|
||||
setEvalResult(null);
|
||||
try {
|
||||
const res = await api.evaluateTips();
|
||||
setEvalResult({ success: true, timestamp: new Date(), message: `${res.matchesEvaluated} Spiele ausgewertet — ${res.tipsUpdated} Tipps bewertet` });
|
||||
}
|
||||
catch (e) {
|
||||
setEvalResult({ success: false, timestamp: new Date(), message: e.message });
|
||||
}
|
||||
finally {
|
||||
setEvaluating(false);
|
||||
}
|
||||
};
|
||||
const handleRefreshLeaderboard = async () => {
|
||||
setRefreshing(true);
|
||||
setRefreshResult(null);
|
||||
try {
|
||||
await fetch('/api/admin/refresh-leaderboard', { method: 'POST' });
|
||||
setRefreshResult({ success: true, timestamp: new Date(), message: 'Materialized View aktualisiert' });
|
||||
}
|
||||
catch (e) {
|
||||
setRefreshResult({ success: false, timestamp: new Date(), message: e.message });
|
||||
}
|
||||
finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
function formatTime(d) {
|
||||
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
}
|
||||
return (_jsxs("div", { className: styles.page, children: [_jsxs("div", { className: styles.pageHeader, children: [_jsx("h1", { className: `font-display ${styles.title}`, children: "Administration" }), _jsx("div", { className: styles.roleBadge, children: "Editor" })] }), _jsx("p", { className: styles.hint, children: "Nur f\u00FCr Editoren sichtbar. Nach Staffbase-Freischaltung wird diese Seite durch Rollenpr\u00FCfung gesch\u00FCtzt." }), _jsxs("div", { className: styles.cards, children: [_jsx(ActionCard, { icon: "\u21BB", title: "Spiele synchronisieren", desc: "L\u00E4dt alle WM 2026-Spiele von football-data.org und aktualisiert die Datenbank.", result: syncResult, loading: syncing, loadingLabel: "Wird synchronisiert\u2026", actionLabel: "Jetzt synchronisieren", onAction: handleSync, formatTime: formatTime }), _jsx(ActionCard, { icon: "\u25C8", title: "Tipps auswerten", desc: "Berechnet Punkte f\u00FCr alle abgeschlossenen Spiele und aktualisiert die Rangliste.", result: evalResult, loading: evaluating, loadingLabel: "Wird ausgewertet\u2026", actionLabel: "Tipps auswerten", onAction: handleEvaluate, formatTime: formatTime }), _jsx(ActionCard, { icon: "\u27F3", title: "Rangliste aktualisieren", desc: "Aktualisiert die Materialized View manuell \u2014 normalerweise automatisch nach Auswertung.", result: refreshResult, loading: refreshing, loadingLabel: "Wird aktualisiert\u2026", actionLabel: "Rangliste neu berechnen", onAction: handleRefreshLeaderboard, formatTime: formatTime })] })] }));
|
||||
}
|
||||
/* ── Sub-component ── */
|
||||
function ActionCard({ icon, title, desc, result, loading, loadingLabel, actionLabel, onAction, formatTime, }) {
|
||||
return (_jsxs("div", { className: `card ${styles.actionCard}`, children: [_jsxs("div", { className: styles.cardTop, children: [_jsx("div", { className: styles.cardIcon, children: icon }), _jsxs("div", { children: [_jsx("div", { className: styles.cardTitle, children: title }), _jsx("div", { className: styles.cardDesc, children: desc })] })] }), result && (_jsxs("div", { className: `${styles.resultBar} ${result.success ? styles.resultSuccess : styles.resultError}`, children: [_jsx("span", { className: styles.resultDot }), _jsx("span", { className: styles.resultMsg, children: result.message }), _jsx("span", { className: styles.resultTime, children: formatTime(result.timestamp) })] })), _jsx("button", { className: `${styles.actionBtn} ${loading ? styles.actionBtnLoading : ''}`, onClick: onAction, disabled: loading, children: loading ? (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.spinner }), loadingLabel] })) : actionLabel })] }));
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { api } from '../api/client';
|
||||
import styles from './DashboardPage.module.css';
|
||||
function formatStreak(streak) {
|
||||
if (streak >= 20)
|
||||
return `⚡${streak}`;
|
||||
if (streak >= 10)
|
||||
return `🔥🔥${streak}`;
|
||||
if (streak >= 3)
|
||||
return `🔥${streak}`;
|
||||
if (streak > 0)
|
||||
return String(streak);
|
||||
return '0';
|
||||
}
|
||||
function formatCountdown(minutes) {
|
||||
if (minutes < 60)
|
||||
return `in ${minutes} Min`;
|
||||
if (minutes < 60 * 24)
|
||||
return `in ${Math.floor(minutes / 60)}h`;
|
||||
return `in ${Math.floor(minutes / (60 * 24))} Tagen`;
|
||||
}
|
||||
export default function DashboardPage(_props) {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
api.getDashboard()
|
||||
.then(d => { setData(d); setLoading(false); })
|
||||
.catch(() => { setError(true); setLoading(false); });
|
||||
}, []);
|
||||
if (loading)
|
||||
return _jsx("div", { className: styles.loading, children: "Laden..." });
|
||||
if (error || !data)
|
||||
return _jsx("div", { className: styles.error, children: "Dashboard konnte nicht geladen werden." });
|
||||
const { hero, stats, nudges } = data;
|
||||
return (_jsxs("div", { className: styles.dashboard, children: [_jsxs("div", { className: `card ${styles.hero}`, onClick: () => navigate('/spiele'), children: [_jsxs("div", { className: styles.heroLabel, children: [_jsx("span", { children: "N\u00E4chstes Spiel" }), hero && (_jsx("span", { className: styles.heroCountdown, children: formatCountdown(hero.match.minutesUntilKickoff) }))] }), hero ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.heroTeams, children: [_jsxs("div", { className: styles.heroTeam, children: [hero.match.homeTeam.crest ? (_jsx("img", { src: hero.match.homeTeam.crest, alt: hero.match.homeTeam.name, className: styles.heroCrest })) : (_jsx("div", { className: styles.heroCrest })), _jsx("span", { children: hero.match.homeTeam.shortName })] }), _jsx("span", { className: styles.heroVs, children: "vs" }), _jsxs("div", { className: styles.heroTeam, children: [hero.match.awayTeam.crest ? (_jsx("img", { src: hero.match.awayTeam.crest, alt: hero.match.awayTeam.name, className: styles.heroCrest })) : (_jsx("div", { className: styles.heroCrest })), _jsx("span", { children: hero.match.awayTeam.shortName })] })] }), hero.userTip ? (_jsxs("div", { className: styles.heroTip, children: ["Dein Tipp: ", hero.userTip.home, ":", hero.userTip.away, " \u2713"] })) : hero.tippable ? (_jsx("button", { className: styles.heroTipBtn, onClick: e => { e.stopPropagation(); navigate('/spiele'); }, children: "Jetzt tippen" })) : null] })) : (_jsx("p", { style: { textAlign: 'center', color: 'var(--text-muted)', margin: '16px 0' }, children: "Keine anstehenden Spiele" }))] }), _jsxs("div", { className: styles.statsRow, children: [_jsxs("div", { className: `card ${styles.statTile}`, children: [_jsx("span", { className: styles.statValue, children: stats.rank !== null ? stats.rank : '—' }), _jsx("span", { className: styles.statLabel, children: "Dein Rang" })] }), _jsxs("div", { className: `card ${styles.statTile}`, children: [_jsx("span", { className: styles.statValue, children: stats.totalPoints }), _jsx("span", { className: styles.statLabel, children: "Punkte" })] }), _jsxs("div", { className: `card ${styles.statTile}`, children: [_jsx("span", { className: styles.statValue, children: formatStreak(stats.streak) }), _jsx("span", { className: styles.statLabel, children: "Streak" })] })] }), nudges.length > 0 && (_jsx("div", { className: styles.nudges, children: nudges.map((nudge, i) => (_jsx("div", { className: `card ${styles.nudge}`, onClick: () => {
|
||||
if (nudge.type === 'untipped')
|
||||
navigate('/spiele');
|
||||
else if (nudge.type === 'leader')
|
||||
navigate('/rangliste');
|
||||
}, children: nudge.text }, i))) }))] }));
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { api } from '../api/client';
|
||||
import styles from './LeaderboardPage.module.css';
|
||||
function initials(name) {
|
||||
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
||||
}
|
||||
function TrendIcon({ entry, prev }) {
|
||||
if (!prev)
|
||||
return _jsx("span", { className: styles.trendNeutral, children: "\u2192" });
|
||||
if (entry.total_points > prev.total_points)
|
||||
return _jsx("span", { className: styles.trendUp, children: "\u2197" });
|
||||
if (entry.total_points < prev.total_points)
|
||||
return _jsx("span", { className: styles.trendDown, children: "\u2198" });
|
||||
return _jsx("span", { className: styles.trendNeutral, children: "\u2192" });
|
||||
}
|
||||
export default function LeaderboardPage() {
|
||||
const [data, setData] = useState(null);
|
||||
const [tippableCount, setTippableCount] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
api.getLeaderboard(),
|
||||
api.getMatches(),
|
||||
]).then(([lb, matches]) => {
|
||||
setData(lb);
|
||||
setTippableCount(matches.matches.filter(m => m.tippable && !m.userTip).length);
|
||||
}).finally(() => setLoading(false));
|
||||
}, []);
|
||||
if (loading)
|
||||
return (_jsx("div", { className: styles.loading, children: _jsx("div", { className: styles.spinner }) }));
|
||||
if (!data)
|
||||
return null;
|
||||
const { entries, currentUserId, currentUserRank, totalParticipants } = data;
|
||||
const top3 = entries.slice(0, 3);
|
||||
const rest = entries.slice(3);
|
||||
const podiumSlots = [];
|
||||
if (top3[1])
|
||||
podiumSlots.push({ entry: top3[1], rank: 2, medal: '🥈', colorClass: styles.silver, barHeight: '64px' });
|
||||
if (top3[0])
|
||||
podiumSlots.push({ entry: top3[0], rank: 1, medal: '🥇', colorClass: styles.gold, barHeight: '96px' });
|
||||
if (top3[2])
|
||||
podiumSlots.push({ entry: top3[2], rank: 3, medal: '🥉', colorClass: styles.bronze, barHeight: '48px' });
|
||||
return (_jsxs("div", { className: styles.page, children: [_jsxs("div", { className: styles.pageHeader, children: [_jsx("h1", { className: `font-display ${styles.title}`, children: "Rangliste" }), _jsxs("div", { className: styles.meta, children: [totalParticipants, " Teilnehmer", currentUserRank ? ` · Du: Platz ${currentUserRank}` : ''] })] }), entries.length === 0 ? (_jsx("div", { className: styles.empty, children: "Noch keine Punkte vergeben. Spiele m\u00FCssen erst abgeschlossen sein." })) : (_jsxs(_Fragment, { children: [top3.length > 0 && (_jsx("div", { className: styles.podiumWrap, children: podiumSlots.map(({ entry, rank, medal, colorClass, barHeight }) => {
|
||||
const isMe = entry.user_id === currentUserId;
|
||||
const isFirst = rank === 1;
|
||||
return (_jsxs("div", { className: `${styles.podiumCard} ${isFirst ? styles.podiumFirst : ''}`, children: [_jsx("div", { className: styles.podiumMedal, children: medal }), _jsx("div", { className: `${styles.podiumAvatar} ${colorClass} ${isFirst ? styles.podiumAvatarLarge : ''} ${isMe ? styles.podiumAvatarMe : ''}`, children: initials(entry.full_name) }), _jsxs("div", { className: `${styles.podiumName} ${isMe ? styles.nameMe : ''}`, children: [entry.full_name.split(' ')[0], isMe ? ' (Ich)' : ''] }), _jsxs("div", { className: `${styles.podiumPoints} ${colorClass}`, children: [entry.total_points.toLocaleString('de-DE'), _jsx("span", { className: styles.podiumPtLabel, children: " Pkt" })] }), _jsx("div", { className: `${styles.podiumBar} ${colorClass}Bar`, style: { height: barHeight } })] }, entry.user_id));
|
||||
}) })), rest.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.listHeader, children: [_jsx("span", { children: "POS" }), _jsx("span", { children: "SPIELER" }), _jsx("span", { children: "TREND" }), _jsx("span", { children: "PUNKTE" })] }), _jsx("div", { className: styles.list, children: rest.map((entry, i) => {
|
||||
const isMe = entry.user_id === currentUserId;
|
||||
const prev = rest[i - 1];
|
||||
return (_jsxs("div", { className: `card ${styles.row} ${isMe ? styles.rowMe : ''}`, children: [_jsx("div", { className: styles.rankCol, children: _jsx("span", { className: styles.rankNum, children: entry.rank }) }), _jsx("div", { className: `${styles.avatarSmall} ${isMe ? styles.avatarSmallMe : ''}`, children: initials(entry.full_name) }), _jsxs("div", { className: styles.nameCol, children: [_jsx("div", { className: styles.nameRow, children: _jsxs("span", { className: `${styles.rowName} ${isMe ? styles.nameMe : ''}`, children: [entry.full_name, isMe ? ' (Ich)' : ''] }) }), entry.team && _jsx("div", { className: styles.rowTeam, children: entry.team }), isMe && _jsx("div", { className: styles.aufholjagd, children: "AUFHOLJAGD!" })] }), _jsx("div", { className: styles.trendCol, children: _jsx(TrendIcon, { entry: entry, prev: prev }) }), _jsx("div", { className: styles.pointsCol, children: entry.total_points.toLocaleString('de-DE') })] }, entry.user_id));
|
||||
}) })] })), tippableCount > 0 && (_jsxs("div", { className: `card ${styles.ctaCard}`, children: [_jsxs("div", { className: styles.ctaText, children: [_jsx("div", { className: styles.ctaTitle, children: "Punkte sichern!" }), _jsxs("div", { className: styles.ctaBody, children: [tippableCount, " Spiel", tippableCount !== 1 ? 'e' : '', " noch ohne Tipp \u2014 kletter nach oben."] })] }), _jsx("button", { className: `btn-primary ${styles.ctaBtn}`, onClick: () => navigate('/'), children: "TIPPEN" })] }))] }))] }));
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import MatchCard from '../components/MatchCard';
|
||||
import TipModal from '../components/TipModal';
|
||||
import { useRevealQueue } from '../hooks/useRevealQueue';
|
||||
import ConfettiReveal from '../components/ConfettiReveal';
|
||||
import styles from './MatchesPage.module.css';
|
||||
function groupIntoSections(matches) {
|
||||
const now = new Date();
|
||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const tomorrowStart = new Date(todayStart);
|
||||
tomorrowStart.setDate(todayStart.getDate() + 1);
|
||||
const dayAfterTomorrow = new Date(todayStart);
|
||||
dayAfterTomorrow.setDate(todayStart.getDate() + 2);
|
||||
const weekEnd = new Date(todayStart);
|
||||
weekEnd.setDate(todayStart.getDate() + 7);
|
||||
const sections = [
|
||||
{ key: 'today', label: 'Heute', matches: [], defaultOpen: true, highlight: true },
|
||||
{ key: 'tomorrow', label: 'Morgen', matches: [], defaultOpen: true, highlight: false },
|
||||
{ key: 'week', label: 'Diese Woche', matches: [], defaultOpen: false, highlight: false },
|
||||
{ key: 'later', label: 'Demnächst', matches: [], defaultOpen: false, highlight: false },
|
||||
{ key: 'past', label: 'Vergangene Spiele', matches: [], defaultOpen: false, highlight: false },
|
||||
];
|
||||
for (const match of matches) {
|
||||
const d = new Date(match.utcDate);
|
||||
if (d < todayStart) {
|
||||
sections[4].matches.push(match); // past
|
||||
}
|
||||
else if (d < tomorrowStart) {
|
||||
sections[0].matches.push(match); // today
|
||||
}
|
||||
else if (d < dayAfterTomorrow) {
|
||||
sections[1].matches.push(match); // tomorrow
|
||||
}
|
||||
else if (d < weekEnd) {
|
||||
sections[2].matches.push(match); // this week
|
||||
}
|
||||
else {
|
||||
sections[3].matches.push(match); // later
|
||||
}
|
||||
}
|
||||
// Past matches: most recent first
|
||||
sections[4].matches.reverse();
|
||||
return sections.filter(s => s.matches.length > 0);
|
||||
}
|
||||
export default function MatchesPage() {
|
||||
const [allMatches, setAllMatches] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [stageFilter, setStageFilter] = useState('');
|
||||
const [selectedMatch, setSelectedMatch] = useState(null);
|
||||
const [openSections, setOpenSections] = useState(new Set());
|
||||
const loadMatches = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await api.getMatches();
|
||||
setAllMatches(res.matches);
|
||||
}
|
||||
catch (e) {
|
||||
setError(e.message);
|
||||
}
|
||||
finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => { loadMatches(); }, [loadMatches]);
|
||||
// Initialize open sections after initial load
|
||||
useEffect(() => {
|
||||
if (allMatches.length > 0) {
|
||||
const filteredMatches = allMatches.filter(m => !stageFilter || m.stage === stageFilter);
|
||||
const sections = groupIntoSections(filteredMatches);
|
||||
setOpenSections(new Set(sections.filter(s => s.defaultOpen).map(s => s.key)));
|
||||
}
|
||||
}, [allMatches]); // only on initial load
|
||||
const handleTipSaved = (matchId, tipHome, tipAway) => {
|
||||
setAllMatches(prev => prev.map(m => m.id === matchId
|
||||
? { ...m, userTip: { home: tipHome, away: tipAway, points: null } }
|
||||
: m));
|
||||
setSelectedMatch(null);
|
||||
};
|
||||
function toggleSection(key) {
|
||||
setOpenSections(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key))
|
||||
next.delete(key);
|
||||
else
|
||||
next.add(key);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
const { current: revealMatch, dismissCurrent } = useRevealQueue(allMatches);
|
||||
const filteredMatches = allMatches.filter(m => !stageFilter || m.stage === stageFilter);
|
||||
const sections = groupIntoSections(filteredMatches);
|
||||
// Stats always over all matches (unfiltered)
|
||||
const tipped = allMatches.filter(m => m.userTip).length;
|
||||
const tippable = allMatches.filter(m => m.tippable && !m.userTip).length;
|
||||
return (_jsxs("div", { className: styles.page, children: [revealMatch && (_jsx(ConfettiReveal, { match: revealMatch, onDismiss: dismissCurrent })), _jsxs("div", { className: styles.statsRow, children: [_jsxs("div", { className: `card ${styles.statCard}`, children: [_jsx("span", { className: styles.statValue, children: allMatches.length }), _jsx("span", { className: styles.statLabel, children: "Spiele gesamt" })] }), _jsxs("div", { className: `card ${styles.statCard}`, children: [_jsx("span", { className: `${styles.statValue} text-primary`, children: tipped }), _jsx("span", { className: styles.statLabel, children: "Tipps abgegeben" })] }), _jsxs("div", { className: `card ${styles.statCard}`, children: [_jsx("span", { className: `${styles.statValue} text-gold`, children: tippable }), _jsx("span", { className: styles.statLabel, children: "Noch tippbar" })] })] }), _jsxs("select", { className: styles.stageFilter, value: stageFilter, onChange: e => setStageFilter(e.target.value), children: [_jsx("option", { value: "", children: "Alle Phasen" }), _jsx("option", { value: "GROUP_STAGE", children: "Gruppenphase" }), _jsx("option", { value: "ROUND_OF_32", children: "Runde der 32" }), _jsx("option", { value: "LAST_16", children: "Achtelfinale" }), _jsx("option", { value: "QUARTER_FINALS", children: "Viertelfinale" }), _jsx("option", { value: "SEMI_FINALS", children: "Halbfinale" }), _jsx("option", { value: "THIRD_PLACE", children: "Platz 3" }), _jsx("option", { value: "FINAL", children: "Finale" })] }), loading && (_jsxs("div", { className: styles.loadingState, children: [_jsx("div", { className: styles.spinner }), _jsx("span", { children: "Spiele werden geladen\u2026" })] })), error && (_jsxs("div", { className: styles.errorState, children: [_jsxs("span", { children: ["\u26A0\uFE0F ", error] }), _jsx("button", { className: "btn-ghost", onClick: loadMatches, children: "Erneut versuchen" })] })), !loading && !error && filteredMatches.length === 0 && (_jsxs("div", { className: styles.emptyState, children: [_jsx("span", { className: styles.emptyIcon, children: "\u26BD" }), _jsx("p", { children: "Noch keine Spiele vorhanden." }), _jsx("p", { className: styles.emptyHint, children: "Geh auf die Admin-Seite und klicke \"Spiele synchronisieren\"." })] })), !loading && !error && sections.map(section => (_jsxs("div", { className: `${styles.section} ${section.highlight ? styles.sectionHighlight : ''}`, children: [_jsxs("button", { className: styles.sectionHeader, onClick: () => toggleSection(section.key), children: [_jsx("span", { className: styles.sectionLabel, children: section.label }), _jsxs("span", { className: styles.sectionCount, children: [section.matches.length, " Spiele"] }), _jsx("span", { className: styles.sectionChevron, children: openSections.has(section.key) ? '▾' : '▸' })] }), openSections.has(section.key) && (_jsx("div", { className: styles.sectionContent, children: section.matches.map(match => (_jsx(MatchCard, { match: match, onTip: () => setSelectedMatch(match) }, match.id))) }))] }, section.key))), selectedMatch && (_jsx(TipModal, { match: selectedMatch, onClose: () => setSelectedMatch(null), onSaved: handleTipSaved }))] }));
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||
import { useState, useEffect } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import StatsRing from '../components/StatsRing';
|
||||
import styles from './ProfilePage.module.css';
|
||||
function initials(name) {
|
||||
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
||||
}
|
||||
function mostCommonTip(tips) {
|
||||
const counts = {};
|
||||
for (const t of tips) {
|
||||
const key = `${t.tip_home}:${t.tip_away}`;
|
||||
counts[key] = (counts[key] ?? 0) + 1;
|
||||
}
|
||||
let best = '';
|
||||
let max = 0;
|
||||
for (const [key, count] of Object.entries(counts)) {
|
||||
if (count > max) {
|
||||
max = count;
|
||||
best = key;
|
||||
}
|
||||
}
|
||||
return best ? `${best} (${max}x getippt)` : '—';
|
||||
}
|
||||
function homeWinPct(tips) {
|
||||
if (!tips.length)
|
||||
return 0;
|
||||
const homeWins = tips.filter(t => t.tip_home > t.tip_away).length;
|
||||
return Math.round((homeWins / tips.length) * 100);
|
||||
}
|
||||
export default function ProfilePage() {
|
||||
const [stats, setStats] = useState(null);
|
||||
const [tips, setTips] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [teamEdit, setTeamEdit] = useState(false);
|
||||
const [teamValue, setTeamValue] = useState('');
|
||||
const [teamSaving, setTeamSaving] = useState(false);
|
||||
const [teamMsg, setTeamMsg] = useState(null);
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
api.getMyStats(),
|
||||
api.getMyTips(),
|
||||
]).then(([s, t]) => {
|
||||
setStats(s);
|
||||
setTeamValue(s.team ?? '');
|
||||
setTips(t.tips);
|
||||
}).finally(() => setLoading(false));
|
||||
}, []);
|
||||
const saveTeam = async () => {
|
||||
if (!teamValue.trim())
|
||||
return;
|
||||
setTeamSaving(true);
|
||||
setTeamMsg(null);
|
||||
try {
|
||||
const res = await api.updateTeam(teamValue);
|
||||
setStats(prev => prev ? { ...prev, team: res.team } : prev);
|
||||
setTeamValue(res.team);
|
||||
setTeamEdit(false);
|
||||
setTeamMsg({ ok: true, text: 'Team gespeichert' });
|
||||
}
|
||||
catch (e) {
|
||||
setTeamMsg({ ok: false, text: e.message });
|
||||
}
|
||||
finally {
|
||||
setTeamSaving(false);
|
||||
}
|
||||
};
|
||||
if (loading)
|
||||
return _jsx("div", { className: styles.loading, children: _jsx("div", { className: styles.spinner }) });
|
||||
if (!stats)
|
||||
return _jsx("div", { className: styles.empty, children: "Profil nicht verf\u00FCgbar." });
|
||||
const evaluatedTips = tips.filter(t => t.points !== null);
|
||||
const recentTips = evaluatedTips.slice(0, 10);
|
||||
const favTip = mostCommonTip(tips);
|
||||
const homePct = homeWinPct(tips);
|
||||
function pointBadgeClass(points) {
|
||||
if (points === null)
|
||||
return '';
|
||||
if (points >= 3)
|
||||
return styles.badgeExact;
|
||||
if (points >= 1)
|
||||
return styles.badgeTendency;
|
||||
return styles.badgeWrong;
|
||||
}
|
||||
function pointLabel(points) {
|
||||
if (points === null)
|
||||
return '';
|
||||
if (points >= 3)
|
||||
return `${points} Pkt ✓✓`;
|
||||
if (points >= 1)
|
||||
return `${points} Pkt ✓`;
|
||||
return `${points} Pkt ✗`;
|
||||
}
|
||||
return (_jsxs("div", { className: styles.page, children: [_jsxs("div", { className: `card ${styles.heroCard}`, children: [_jsx("div", { className: styles.avatar, children: initials(stats.fullName) }), _jsxs("div", { className: styles.heroInfo, children: [_jsx("h1", { className: `font-display ${styles.name}`, children: stats.fullName }), stats.rank && _jsxs("div", { className: styles.rankBadge, children: ["\uD83C\uDFC6 Platz ", stats.rank] }), _jsx("div", { className: styles.teamRow, children: teamEdit ? (_jsxs("div", { className: styles.teamEditRow, children: [_jsx("input", { className: styles.teamInput, value: teamValue, onChange: e => setTeamValue(e.target.value), placeholder: "z. B. Vertrieb S\u00FCd", maxLength: 80, autoFocus: true, onKeyDown: e => {
|
||||
if (e.key === 'Enter')
|
||||
saveTeam();
|
||||
if (e.key === 'Escape')
|
||||
setTeamEdit(false);
|
||||
} }), _jsx("button", { className: styles.teamSaveBtn, onClick: saveTeam, disabled: teamSaving, children: teamSaving ? _jsx("span", { className: styles.spinnerSm }) : '✓' }), _jsx("button", { className: styles.teamCancelBtn, onClick: () => { setTeamEdit(false); setTeamValue(stats.team ?? ''); }, children: "\u2715" })] })) : (_jsx("button", { className: styles.teamBtn, onClick: () => setTeamEdit(true), children: stats.team
|
||||
? _jsxs(_Fragment, { children: [_jsx("span", { className: styles.teamName, children: stats.team }), _jsx("span", { className: styles.teamEditHint, children: "bearbeiten" })] })
|
||||
: _jsx("span", { className: styles.teamPlaceholder, children: "+ Team hinzuf\u00FCgen" }) })) }), teamMsg && (_jsx("div", { className: `${styles.teamMsg} ${teamMsg.ok ? styles.teamMsgOk : styles.teamMsgErr}`, children: teamMsg.text }))] })] }), _jsxs("div", { className: `card ${styles.ringCard}`, children: [_jsx("h2", { className: styles.sectionTitle, children: "Tipp-Statistik" }), _jsx(StatsRing, { exact: stats.exactCount, tendency: stats.tendencyCount, wrong: stats.wrongCount, total: stats.totalPoints }), stats.accuracy > 0 && (_jsxs("div", { className: styles.accuracyRow, children: [_jsx("span", { className: styles.accuracyLabel, children: "Trefferquote" }), _jsxs("span", { className: `font-display ${styles.accuracyVal}`, children: [stats.accuracy, "%"] })] }))] }), recentTips.length > 0 && (_jsxs("div", { className: `card ${styles.historyCard}`, children: [_jsx("h2", { className: styles.sectionTitle, children: "Letzte Tipps" }), _jsx("ul", { className: styles.tipList, children: recentTips.map((tip, i) => (_jsxs("li", { className: `${styles.tipRow} ${i % 2 === 1 ? styles.tipRowAlt : ''}`, children: [_jsxs("span", { className: styles.tipMatch, children: [tip.home_team_short, " vs ", tip.away_team_short] }), _jsxs("span", { className: styles.tipScore, children: ["Tipp: ", tip.tip_home, ":", tip.tip_away] }), _jsx("span", { className: `${styles.pointBadge} ${pointBadgeClass(tip.points)}`, children: pointLabel(tip.points) })] }, tip.match_id))) })] })), tips.length > 0 && (_jsxs("div", { className: styles.funStats, children: [_jsx("h2", { className: styles.sectionTitle, children: "Fun Facts" }), _jsxs("div", { className: styles.funGrid, children: [_jsxs("div", { className: `card ${styles.funCard}`, children: [_jsx("span", { className: styles.funIcon, children: "\uD83C\uDFAF" }), _jsx("span", { className: styles.funLabel, children: "Lieblings-Tipp" }), _jsx("span", { className: styles.funValue, children: favTip })] }), _jsxs("div", { className: `card ${styles.funCard}`, children: [_jsx("span", { className: styles.funIcon, children: "\uD83C\uDFE0" }), _jsx("span", { className: styles.funLabel, children: "Heimsiege getippt" }), _jsxs("span", { className: styles.funValue, children: [homePct, "%"] })] }), _jsxs("div", { className: `card ${styles.funCard}`, children: [_jsx("span", { className: styles.funIcon, children: "\uD83D\uDCCA" }), _jsx("span", { className: styles.funLabel, children: "Tipps abgegeben" }), _jsx("span", { className: styles.funValue, children: stats.tipsCount })] })] })] }))] }));
|
||||
}
|
||||
Reference in New Issue
Block a user