diff --git a/frontend/src/components/TipModal.module.css b/frontend/src/components/TipModal.module.css index fdd1e91..19a8480 100644 --- a/frontend/src/components/TipModal.module.css +++ b/frontend/src/components/TipModal.module.css @@ -77,12 +77,9 @@ .flagLarge { width: 72px; height: 72px; - border-radius: 18px; + border-radius: 16px; background: var(--surface-high); - box-shadow: - var(--shadow-card), - inset 0 1px 0 rgba(255,255,255,0.55), - inset 0 -1px 0 rgba(0,0,0,0.06); + box-shadow: 0 2px 8px rgba(0,0,0,0.12); border: 1px solid var(--border-subtle); display: flex; align-items: center; @@ -95,8 +92,8 @@ content: ''; position: absolute; top: 0; left: 0; right: 0; - height: 55%; - background: linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0.0) 100%); + height: 40%; + background: linear-gradient(180deg, rgba(255,255,255,0.2) 0%, transparent 100%); pointer-events: none; z-index: 1; } diff --git a/frontend/src/pages/MatchesPage.module.css b/frontend/src/pages/MatchesPage.module.css index fbb5551..1622676 100644 --- a/frontend/src/pages/MatchesPage.module.css +++ b/frontend/src/pages/MatchesPage.module.css @@ -1,47 +1,33 @@ -.page { display: flex; flex-direction: column; gap: 24px; } +.page { display: flex; flex-direction: column; gap: 16px; } -.statsRow { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 16px; -} - -.statCard { - padding: 20px; +/* Compact stats line */ +.statsLine { display: flex; - flex-direction: column; align-items: center; - gap: 4px; -} - -.statValue { - font-family: 'Plus Jakarta Sans', sans-serif; - font-size: 32px; - font-weight: 800; - line-height: 1; -} - -.statLabel { - font-size: 12px; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.stageFilter { + justify-content: space-between; + padding: 10px 16px; background: var(--surface-mid); - color: var(--text-primary); - border: 1px solid rgba(75, 183, 248, 0.15); border-radius: var(--radius-sm); - padding: 8px 12px; - font-size: 0.85rem; - width: 100%; - max-width: 200px; - margin-bottom: 12px; + border: 1px solid var(--border-subtle); } +.statsText { + font-size: 13px; + color: var(--text-secondary); +} + +.statsText strong { + color: var(--text-primary); +} + +.statsReminder { + font-size: 12px; + color: var(--gold); + font-weight: 600; +} + +/* Sections */ .section { - margin-bottom: 8px; border-radius: var(--radius-md); overflow: hidden; } @@ -54,20 +40,22 @@ display: flex; align-items: center; width: 100%; - padding: 12px 16px; + padding: 14px 16px; background: var(--surface-mid); border: none; + border-bottom: 1px solid var(--border-subtle); color: var(--text-primary); cursor: pointer; font-size: 0.95rem; gap: 8px; + transition: background 0.15s; } .sectionHeader:hover { background: var(--surface-high); } .sectionLabel { - font-weight: 600; + font-weight: 700; flex: 1; text-align: left; } @@ -75,6 +63,7 @@ .sectionCount { font-size: 0.8rem; color: var(--text-muted); + font-weight: 500; } .sectionChevron { @@ -85,8 +74,8 @@ .sectionContent { display: flex; flex-direction: column; - gap: 8px; - padding: 8px 0; + gap: 10px; + padding: 10px 0; } /* States */ @@ -110,6 +99,4 @@ @keyframes spin { to { transform: rotate(360deg); } } .emptyIcon { font-size: 48px; } -.emptyHint { font-size: 13px; color: var(--text-muted); } - .errorState { color: var(--error); } diff --git a/frontend/src/pages/MatchesPage.tsx b/frontend/src/pages/MatchesPage.tsx index f302a9d..c39b19f 100644 --- a/frontend/src/pages/MatchesPage.tsx +++ b/frontend/src/pages/MatchesPage.tsx @@ -14,50 +14,84 @@ type Section = { highlight: boolean; }; -function groupIntoSections(matches: Match[]): Section[] { +function formatDateLabel(dateStr: string): string { + const d = new Date(dateStr); 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 today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const tomorrow = new Date(today); + tomorrow.setDate(today.getDate() + 1); + const matchDay = new Date(d.getFullYear(), d.getMonth(), d.getDate()); - 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 }, - ]; + if (matchDay.getTime() === today.getTime()) return 'Heute'; + if (matchDay.getTime() === tomorrow.getTime()) return 'Morgen'; - 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 + return d.toLocaleDateString('de-DE', { + weekday: 'short', + day: 'numeric', + month: 'long', + }); +} + +function groupByDate(matches: Match[]): Section[] { + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + + // Separate past and upcoming + const past: Match[] = []; + const upcoming: Match[] = []; + + for (const m of matches) { + const d = new Date(m.utcDate); + if (d < today && m.status === 'FINISHED') { + past.push(m); } else { - sections[3].matches.push(match); // later + upcoming.push(m); } } - // Past matches: most recent first - sections[4].matches.reverse(); + // Group upcoming by date + const dateGroups = new Map(); + for (const m of upcoming) { + const dateKey = new Date(m.utcDate).toISOString().split('T')[0]; + if (!dateGroups.has(dateKey)) dateGroups.set(dateKey, []); + dateGroups.get(dateKey)!.push(m); + } - return sections.filter(s => s.matches.length > 0); + const sections: Section[] = []; + + // Add upcoming date sections (first 3 open, rest collapsed) + let idx = 0; + for (const [dateKey, dateMatches] of dateGroups) { + const label = formatDateLabel(dateMatches[0].utcDate); + sections.push({ + key: dateKey, + label, + matches: dateMatches, + defaultOpen: idx < 3, + highlight: label === 'Heute', + }); + idx++; + } + + // Add past section if any + if (past.length > 0) { + past.reverse(); // newest first + sections.push({ + key: 'past', + label: 'Vergangene Spiele', + matches: past, + defaultOpen: false, + highlight: false, + }); + } + + return sections; } 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()); @@ -76,19 +110,17 @@ export default function MatchesPage() { useEffect(() => { loadMatches(); }, [loadMatches]); - // Initialize open sections after initial load + // Initialize open sections 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) { + const sections = groupByDate(allMatches); + if (sections.length <= 3) { 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 + }, [allMatches]); const handleTipSaved = (matchId: number, tipHome: number, tipAway: number) => { setAllMatches(prev => prev.map(m => @@ -109,13 +141,8 @@ export default function MatchesPage() { } 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; + const sections = groupByDate(allMatches); + const tipped = allMatches.filter(m => m.userTip).length; return (
@@ -123,37 +150,19 @@ export default function MatchesPage() { )} - {/* Header Stats */} -
-
- {allMatches.length} - Spiele gesamt + {/* Compact stats line */} + {allMatches.length > 0 && ( +
+ + {tipped} von {allMatches.length} Spielen getippt + + {tipped < allMatches.length && ( + + Noch {allMatches.length - tipped} offen + + )}
-
- {tipped} - Tipps abgegeben -
-
- {tippable} - Noch tippbar -
-
- - {/* Stage Filter Dropdown */} - + )} {/* Content */} {loading && ( @@ -170,13 +179,10 @@ export default function MatchesPage() {
)} - {!loading && !error && filteredMatches.length === 0 && ( + {!loading && !error && allMatches.length === 0 && (

Noch keine Spiele vorhanden.

-

- Geh auf die Admin-Seite und klicke "Spiele synchronisieren". -

)} @@ -184,7 +190,9 @@ export default function MatchesPage() {
{openSections.has(section.key) && (