This repository has been archived on 2026-05-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tippspiel/frontend/src/pages/MatchesPage.tsx
T
Ronny 676ed9c1b3 fix: improve tipping user journey
- Dashboard "Jetzt tippen" opens TipModal directly instead of
  navigating to /spiele (no more dead-end spielplan)
- After tipping, dashboard updates to show "Dein Tipp: X:Y ✓"
- Spielplan auto-opens all sections when only 1-2 exist
  (no more collapsed "Demnächst" as only section)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:51:36 +02:00

210 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback } from 'react';
import { api, Match } 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';
type Section = {
key: string;
label: string;
matches: Match[];
defaultOpen: boolean;
highlight: boolean;
};
function groupIntoSections(matches: Match[]): Section[] {
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: Section[] = [
{ 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<Match[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [stageFilter, setStageFilter] = useState('');
const [selectedMatch, setSelectedMatch] = useState<Match | null>(null);
const [openSections, setOpenSections] = useState<Set<string>>(new Set());
const loadMatches = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await api.getMatches();
setAllMatches(res.matches);
} catch (e) {
setError((e as Error).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);
// If only 1-2 sections exist, open all of them
if (sections.length <= 2) {
setOpenSections(new Set(sections.map(s => s.key)));
} else {
setOpenSections(new Set(sections.filter(s => s.defaultOpen).map(s => s.key)));
}
}
}, [allMatches]); // only on initial load
const handleTipSaved = (matchId: number, tipHome: number, tipAway: number) => {
setAllMatches(prev => prev.map(m =>
m.id === matchId
? { ...m, userTip: { home: tipHome, away: tipAway, points: null } }
: m
));
setSelectedMatch(null);
};
function toggleSection(key: string) {
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 (
<div className={styles.page}>
{revealMatch && (
<ConfettiReveal match={revealMatch} onDismiss={dismissCurrent} />
)}
{/* Header Stats */}
<div className={styles.statsRow}>
<div className={`card ${styles.statCard}`}>
<span className={styles.statValue}>{allMatches.length}</span>
<span className={styles.statLabel}>Spiele gesamt</span>
</div>
<div className={`card ${styles.statCard}`}>
<span className={`${styles.statValue} text-primary`}>{tipped}</span>
<span className={styles.statLabel}>Tipps abgegeben</span>
</div>
<div className={`card ${styles.statCard}`}>
<span className={`${styles.statValue} text-gold`}>{tippable}</span>
<span className={styles.statLabel}>Noch tippbar</span>
</div>
</div>
{/* Stage Filter Dropdown */}
<select
className={styles.stageFilter}
value={stageFilter}
onChange={e => setStageFilter(e.target.value)}
>
<option value="">Alle Phasen</option>
<option value="GROUP_STAGE">Gruppenphase</option>
<option value="ROUND_OF_32">Runde der 32</option>
<option value="LAST_16">Achtelfinale</option>
<option value="QUARTER_FINALS">Viertelfinale</option>
<option value="SEMI_FINALS">Halbfinale</option>
<option value="THIRD_PLACE">Platz 3</option>
<option value="FINAL">Finale</option>
</select>
{/* Content */}
{loading && (
<div className={styles.loadingState}>
<div className={styles.spinner} />
<span>Spiele werden geladen</span>
</div>
)}
{error && (
<div className={styles.errorState}>
<span> {error}</span>
<button className="btn-ghost" onClick={loadMatches}>Erneut versuchen</button>
</div>
)}
{!loading && !error && filteredMatches.length === 0 && (
<div className={styles.emptyState}>
<span className={styles.emptyIcon}></span>
<p>Noch keine Spiele vorhanden.</p>
<p className={styles.emptyHint}>
Geh auf die Admin-Seite und klicke "Spiele synchronisieren".
</p>
</div>
)}
{!loading && !error && sections.map(section => (
<div key={section.key} className={`${styles.section} ${section.highlight ? styles.sectionHighlight : ''}`}>
<button className={styles.sectionHeader} onClick={() => toggleSection(section.key)}>
<span className={styles.sectionLabel}>{section.label}</span>
<span className={styles.sectionCount}>{section.matches.length} Spiele</span>
<span className={styles.sectionChevron}>{openSections.has(section.key) ? '▾' : '▸'}</span>
</button>
{openSections.has(section.key) && (
<div className={styles.sectionContent}>
{section.matches.map(match => (
<MatchCard key={match.id} match={match} onTip={() => setSelectedMatch(match)} />
))}
</div>
)}
</div>
))}
{selectedMatch && (
<TipModal
match={selectedMatch}
onClose={() => setSelectedMatch(null)}
onSaved={handleTipSaved}
/>
)}
</div>
);
}