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/DashboardPage.tsx
T
Ronny c6c167abb3 style: remove hero border, LED time between flags like Design 2
- Removed visible blue border on hero card (now near-invisible white/6%)
- LED kickoff time moved between the two flags (replacing VS text)
- Layout: flag — LED time — flag centered vertically

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:35:38 +02:00

191 lines
6.7 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 { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api, DashboardData, Match } from '../api/client';
import TipModal from '../components/TipModal';
import styles from './DashboardPage.module.css';
interface Props {
devUser?: number;
}
function formatStreak(streak: number): string {
if (streak >= 20) return `${streak}`;
if (streak >= 10) return `🔥🔥${streak}`;
if (streak >= 3) return `🔥${streak}`;
if (streak > 0) return String(streak);
return '0';
}
function formatKickoff(utcDate: string): string {
return new Date(utcDate).toLocaleString('de-DE', {
hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Berlin',
});
}
function formatCountdown(minutes: number): string {
if (minutes < 60) return `in ${Math.round(minutes)} Min`;
if (minutes < 60 * 24) return `in ${Math.floor(minutes / 60)}h`;
const days = Math.floor(minutes / (60 * 24));
return `in ${days} ${days === 1 ? 'Tag' : 'Tagen'}`;
}
export default function DashboardPage(_props: Props) {
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [tipMatch, setTipMatch] = useState<Match | null>(null);
const navigate = useNavigate();
useEffect(() => {
setLoading(true);
setError(false);
api.getDashboard()
.then(d => { setData(d); setLoading(false); })
.catch(() => { setError(true); setLoading(false); });
}, []);
if (loading) return <div className={styles.loading}>Laden...</div>;
if (error || !data) return <div className={styles.error}>Dashboard konnte nicht geladen werden.</div>;
const { hero, stats, nudges } = data;
return (
<div className={styles.dashboard}>
{/* Hero Card */}
<div className={styles.hero} onClick={() => navigate('/spiele')}>
{/* Radial glow background effect */}
<div className={styles.heroGlow} />
<div className={styles.heroHeader}>
<span className={styles.heroLabel}>Nächstes Spiel</span>
{hero && (
<span className={styles.heroCountdown}>
<span className={styles.countdownDot} />
{formatCountdown(hero.match.minutesUntilKickoff).toUpperCase()}
</span>
)}
</div>
{hero ? (
<>
{/* Teams with LED time in center */}
<div className={styles.heroTeams}>
<div className={styles.heroTeam}>
<div className={styles.heroCrestBox}>
{hero.match.homeTeam.crest ? (
<img src={hero.match.homeTeam.crest} alt={hero.match.homeTeam.name} className={styles.heroCrest} />
) : (
<span className={styles.heroCrestFallback}>🏳</span>
)}
</div>
<span className={styles.heroTeamName}>{hero.match.homeTeam.shortName}</span>
</div>
{/* Center: LED time */}
<div className={styles.heroCenter}>
<span className={styles.heroLED}>{formatKickoff(hero.match.utcDate)}</span>
</div>
<div className={styles.heroTeam}>
<div className={styles.heroCrestBox}>
{hero.match.awayTeam.crest ? (
<img src={hero.match.awayTeam.crest} alt={hero.match.awayTeam.name} className={styles.heroCrest} />
) : (
<span className={styles.heroCrestFallback}>🏳</span>
)}
</div>
<span className={styles.heroTeamName}>{hero.match.awayTeam.shortName}</span>
</div>
</div>
{/* CTA or Tip Display */}
{hero.userTip ? (
<div className={styles.heroTip}>
Dein Tipp: {hero.userTip.home}:{hero.userTip.away}
</div>
) : hero.tippable ? (
<button
className={styles.heroTipBtn}
onClick={e => {
e.stopPropagation();
setTipMatch({
id: hero.match.id,
externalId: 0,
utcDate: hero.match.utcDate,
status: hero.match.status,
stage: '',
group: null,
homeTeam: hero.match.homeTeam,
awayTeam: hero.match.awayTeam,
score: { home: null, away: null },
userTip: null,
minutesUntilKickoff: hero.match.minutesUntilKickoff,
tippable: true,
});
}}
>
Jetzt tippen
</button>
) : null}
</>
) : (
<p className={styles.heroEmpty}>Keine anstehenden Spiele</p>
)}
</div>
{/* Stats Row */}
<div className={styles.statsRow}>
<div className={`card ${styles.statTile}`}>
<span className={styles.statValue}>{stats.rank !== null ? stats.rank : '—'}</span>
<span className={styles.statLabel}>Dein Rang</span>
</div>
<div className={`card ${styles.statTile}`}>
<span className={`${styles.statValue} ${styles.statGold}`}>{stats.totalPoints}</span>
<span className={styles.statLabel}>Punkte</span>
</div>
<div className={`card ${styles.statTile}`}>
<span className={styles.statValue}>{formatStreak(stats.streak)}</span>
<span className={styles.statLabel}>Streak</span>
</div>
</div>
{/* Nudges */}
{nudges.length > 0 && (
<div className={styles.nudges}>
{nudges.map((nudge, i) => (
<div
key={i}
className={`card ${styles.nudge}`}
onClick={() => {
if (nudge.type === 'untipped') navigate('/spiele');
else if (nudge.type === 'leader') navigate('/rangliste');
}}
>
<span className={styles.nudgeIcon}>
{nudge.type === 'untipped' ? '📅' : nudge.type === 'leader' ? '🏆' : '🎯'}
</span>
<span className={styles.nudgeText}>{nudge.text}</span>
</div>
))}
</div>
)}
{tipMatch && (
<TipModal
match={tipMatch}
onClose={() => setTipMatch(null)}
onSaved={(_matchId, tipHome, tipAway) => {
setTipMatch(null);
if (data && data.hero) {
setData({
...data,
hero: { ...data.hero, userTip: { home: tipHome, away: tipAway } },
});
}
}}
/>
)}
</div>
);
}