From d39ec7a5793503174850188d2e6f3874d873ec11 Mon Sep 17 00:00:00 2001 From: Ronny Date: Sat, 11 Apr 2026 22:27:58 +0200 Subject: [PATCH] =?UTF-8?q?style:=20Spielplan=20=E2=80=94=20glassmorphism?= =?UTF-8?q?=20stats=20card,=20timeline=20date=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Stats: glass card with 2/104 progress + Punkte/Exakt/Offen details - Date sections: timeline divider with centered label + lines instead of accordion (no more broken border-radius) - Past matches: simple toggle button, separate from timeline - Match list: clean vertical flow without section containers Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/pages/MatchesPage.module.css | 160 +++++++++++++++------- frontend/src/pages/MatchesPage.tsx | 156 ++++++++++----------- 2 files changed, 186 insertions(+), 130 deletions(-) diff --git a/frontend/src/pages/MatchesPage.module.css b/frontend/src/pages/MatchesPage.module.css index 1622676..5de745a 100644 --- a/frontend/src/pages/MatchesPage.module.css +++ b/frontend/src/pages/MatchesPage.module.css @@ -1,84 +1,148 @@ .page { display: flex; flex-direction: column; gap: 16px; } -/* Compact stats line */ -.statsLine { +/* ── Stats Card (glassmorphism) ── */ +.statsCard { + padding: 18px 20px; display: flex; align-items: center; - justify-content: space-between; - padding: 10px 16px; - background: var(--surface-mid); - border-radius: var(--radius-sm); - border: 1px solid var(--border-subtle); + gap: 16px; } -.statsText { - font-size: 13px; - color: var(--text-secondary); +.statsMain { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; } -.statsText strong { - color: var(--text-primary); +.statsProgress { + display: flex; + align-items: baseline; + gap: 2px; } -.statsReminder { - font-size: 12px; - color: var(--gold); +.statsNum { + font-size: 28px; + font-weight: 800; + color: var(--primary); + line-height: 1; +} + +.statsSlash { + font-size: 18px; + color: var(--text-muted); + font-weight: 300; + margin: 0 1px; +} + +.statsTotal { + font-size: 16px; font-weight: 600; + color: var(--text-secondary); + line-height: 1; } -/* Sections */ -.section { - border-radius: var(--radius-md); - overflow: hidden; +.statsLabel { + font-size: 11px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; } -.sectionHighlight { - border-left: 3px solid var(--gold); +.statsDivider { + width: 1px; + height: 40px; + background: var(--border-subtle); + flex-shrink: 0; } -.sectionHeader { +.statsDetails { + display: flex; + gap: 16px; + flex: 1; + justify-content: space-around; +} + +.statsDetail { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.statsDetailValue { + font-size: 18px; + font-weight: 700; + color: var(--text-primary); + line-height: 1; +} + +.statsDetailLabel { + font-size: 10px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +/* ── Date Headers (timeline dividers) ── */ +.dateHeader { display: flex; align-items: center; - width: 100%; - 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); + gap: 12px; + padding: 4px 0; } -.sectionLabel { - font-weight: 700; +.dateLine { flex: 1; - text-align: left; + height: 1px; + background: var(--border-subtle); } -.sectionCount { - font-size: 0.8rem; - color: var(--text-muted); +.dateLabel { + font-size: 14px; + font-weight: 700; + color: var(--text-primary); + white-space: nowrap; + display: flex; + align-items: center; + gap: 8px; +} + +.dateCount { + font-size: 11px; font-weight: 500; -} - -.sectionChevron { - font-size: 0.85rem; color: var(--text-muted); } -.sectionContent { +/* ── Match List ── */ +.matchList { display: flex; flex-direction: column; gap: 10px; - padding: 10px 0; + margin-bottom: 8px; } -/* States */ +/* ── Past matches toggle ── */ +.pastToggle { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 12px 16px; + background: var(--surface-low); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + color: var(--text-secondary); + cursor: pointer; + font-size: 13px; + font-weight: 600; + transition: background 0.15s; +} +.pastToggle:hover { + background: var(--surface-mid); +} + +/* ── States ── */ .loadingState, .errorState, .emptyState { display: flex; flex-direction: column; diff --git a/frontend/src/pages/MatchesPage.tsx b/frontend/src/pages/MatchesPage.tsx index c39b19f..cb7e6d9 100644 --- a/frontend/src/pages/MatchesPage.tsx +++ b/frontend/src/pages/MatchesPage.tsx @@ -6,14 +6,6 @@ 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 formatDateLabel(dateStr: string): string { const d = new Date(dateStr); const now = new Date(); @@ -32,11 +24,12 @@ function formatDateLabel(dateStr: string): string { }); } -function groupByDate(matches: Match[]): Section[] { +type DateGroup = { dateKey: string; label: string; matches: Match[] }; + +function groupByDate(matches: Match[]): { upcoming: DateGroup[]; past: Match[] } { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - // Separate past and upcoming const past: Match[] = []; const upcoming: Match[] = []; @@ -50,42 +43,25 @@ function groupByDate(matches: Match[]): Section[] { } // Group upcoming by date - const dateGroups = new Map(); + const dateGroups: DateGroup[] = []; + const groupMap = 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); + if (!groupMap.has(dateKey)) groupMap.set(dateKey, []); + groupMap.get(dateKey)!.push(m); } - 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, + for (const [dateKey, dateMatches] of groupMap) { + dateGroups.push({ + dateKey, + label: formatDateLabel(dateMatches[0].utcDate), 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; + past.reverse(); // newest first + return { upcoming: dateGroups, past }; } export default function MatchesPage() { @@ -93,7 +69,7 @@ export default function MatchesPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedMatch, setSelectedMatch] = useState(null); - const [openSections, setOpenSections] = useState>(new Set()); + const [showPast, setShowPast] = useState(false); const loadMatches = useCallback(async () => { setLoading(true); @@ -110,18 +86,6 @@ export default function MatchesPage() { useEffect(() => { loadMatches(); }, [loadMatches]); - // Initialize open sections - useEffect(() => { - if (allMatches.length > 0) { - 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]); - const handleTipSaved = (matchId: number, tipHome: number, tipAway: number) => { setAllMatches(prev => prev.map(m => m.id === matchId @@ -131,18 +95,12 @@ export default function MatchesPage() { 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 sections = groupByDate(allMatches); + const { upcoming, past } = groupByDate(allMatches); + const tipped = allMatches.filter(m => m.userTip).length; + const totalPoints = allMatches.reduce((sum, m) => sum + (m.userTip?.points ?? 0), 0); + const exactCount = allMatches.filter(m => m.userTip?.points === 3).length; return (
@@ -150,17 +108,32 @@ export default function MatchesPage() { )} - {/* Compact stats line */} + {/* Stats card */} {allMatches.length > 0 && ( -
- - {tipped} von {allMatches.length} Spielen getippt - - {tipped < allMatches.length && ( - - Noch {allMatches.length - tipped} offen - - )} +
+
+
+ {tipped} + / + {allMatches.length} +
+ Tipps abgegeben +
+
+
+
+ {totalPoints} + Punkte +
+
+ {exactCount} + Exakt +
+
+ {allMatches.length - tipped} + Offen +
+
)} @@ -186,24 +159,43 @@ export default function MatchesPage() {
)} - {!loading && !error && sections.map(section => ( -
- - {openSections.has(section.key) && ( -
- {section.matches.map(match => ( + {showPast && ( +
+ {past.map(match => ( setSelectedMatch(match)} /> ))}
)}
- ))} + )} {selectedMatch && (