From d48bc2d449a11a7bc7dff926a0f468b6a4465041 Mon Sep 17 00:00:00 2001 From: Ronny Date: Sat, 11 Apr 2026 19:02:53 +0200 Subject: [PATCH] feat: add Dashboard as new startseite Hero card with next match + countdown, stats tiles (rank, points, streak), and contextual nudges. Replaces match list as landing page. --- frontend/src/App.tsx | 3 +- frontend/src/pages/DashboardPage.module.css | 141 ++++++++++++++++++++ frontend/src/pages/DashboardPage.tsx | 137 +++++++++++++++++++ 3 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/DashboardPage.module.css create mode 100644 frontend/src/pages/DashboardPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6d1731d..0b7390c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { Routes, Route, NavLink } from 'react-router-dom'; import { Sun, Moon, Settings } from 'lucide-react'; +import DashboardPage from './pages/DashboardPage'; import MatchesPage from './pages/MatchesPage'; import LeaderboardPage from './pages/LeaderboardPage'; import ProfilePage from './pages/ProfilePage'; @@ -106,7 +107,7 @@ export default function App() {
- } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/DashboardPage.module.css b/frontend/src/pages/DashboardPage.module.css new file mode 100644 index 0000000..8a3d5d5 --- /dev/null +++ b/frontend/src/pages/DashboardPage.module.css @@ -0,0 +1,141 @@ +.dashboard { + padding: 16px; + max-width: 600px; + margin: 0 auto; +} + +.hero { + background: var(--surface-mid); + border-radius: var(--radius-lg); + padding: 20px; + cursor: pointer; + border: 1px solid rgba(75, 183, 248, 0.1); + transition: transform 0.2s; +} + +.hero:hover { + transform: scale(1.01); +} + +.heroLabel { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--text-secondary); + display: flex; + justify-content: space-between; + align-items: center; +} + +.heroCountdown { + color: var(--gold); + font-weight: 700; + font-size: 0.85rem; +} + +.heroTeams { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; + margin: 16px 0; +} + +.heroTeam { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; +} + +.heroCrest { + width: 48px; + height: 48px; + object-fit: contain; +} + +.heroVs { + font-size: 1.2rem; + font-weight: 700; + color: var(--gold); +} + +.heroTip { + background: var(--surface-high); + border-radius: var(--radius-sm); + padding: 8px 16px; + text-align: center; + color: var(--gold); + font-weight: 600; +} + +.heroTipBtn { + display: block; + width: 100%; + padding: 10px; + border: none; + border-radius: var(--radius-sm); + background: var(--primary); + color: white; + font-weight: 600; + cursor: pointer; + font-size: 0.95rem; +} + +.statsRow { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 8px; + margin: 16px 0; +} + +.statTile { + background: var(--surface-mid); + border-radius: var(--radius-md); + padding: 14px 8px; + text-align: center; + border: 1px solid rgba(75, 183, 248, 0.08); +} + +.statValue { + display: block; + font-size: 1.5rem; + font-weight: 700; + color: var(--gold); +} + +.statLabel { + display: block; + font-size: 0.7rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-top: 4px; +} + +.nudges { + display: flex; + flex-direction: column; + gap: 6px; +} + +.nudge { + background: var(--surface-low); + border-radius: var(--radius-sm); + padding: 12px 16px; + color: var(--text-secondary); + font-size: 0.9rem; + cursor: pointer; + transition: background 0.2s; +} + +.nudge:hover { + background: var(--surface-mid); +} + +.loading, +.error { + text-align: center; + padding: 60px 20px; + color: var(--text-muted); +} diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..1089fd4 --- /dev/null +++ b/frontend/src/pages/DashboardPage.tsx @@ -0,0 +1,137 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { api, DashboardData } from '../api/client'; +import styles from './DashboardPage.module.css'; + +interface Props { + devUser?: number; +} + +function formatCountdown(minutes: number): string { + 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: 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
Laden...
; + if (error || !data) return
Dashboard konnte nicht geladen werden.
; + + const { hero, stats, nudges } = data; + + return ( +
+ {/* Hero Card */} +
navigate('/spiele')}> +
+ Nächstes Spiel + {hero && ( + + {formatCountdown(hero.match.minutesUntilKickoff)} + + )} +
+ + {hero ? ( + <> +
+
+ {hero.match.homeTeam.crest ? ( + {hero.match.homeTeam.name} + ) : ( +
+ )} + {hero.match.homeTeam.shortName} +
+ vs +
+ {hero.match.awayTeam.crest ? ( + {hero.match.awayTeam.name} + ) : ( +
+ )} + {hero.match.awayTeam.shortName} +
+
+ + {hero.userTip ? ( +
+ Dein Tipp: {hero.userTip.home}:{hero.userTip.away} ✓ +
+ ) : hero.tippable ? ( + + ) : null} + + ) : ( +

+ Keine anstehenden Spiele +

+ )} +
+ + {/* Stats Row */} +
+
+ + {stats.rank !== null ? stats.rank : '—'} + + Dein Rang +
+
+ {stats.totalPoints} + Punkte +
+
+ + {stats.streak > 0 ? `${stats.streak} 🔥` : stats.streak} + + Streak +
+
+ + {/* Nudges */} + {nudges.length > 0 && ( +
+ {nudges.map((nudge, i) => ( +
{ + if (nudge.type === 'untipped') navigate('/spiele'); + else if (nudge.type === 'leader') navigate('/rangliste'); + }} + > + {nudge.text} +
+ ))} +
+ )} +
+ ); +}