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/App.tsx
T
Ronny 5ab0189f04
Build & Deploy Tippspiel / build (push) Successful in 51s
feat: logo links to home/dashboard
2026-04-12 17:34:00 +02:00

163 lines
6.1 KiB
TypeScript

import { useState, useEffect } from 'react';
import { Routes, Route, NavLink } from 'react-router-dom';
import { Sun, Moon, Monitor } from 'lucide-react';
import DashboardPage from './pages/DashboardPage';
import MatchesPage from './pages/MatchesPage';
import LeaderboardPage from './pages/LeaderboardPage';
import ProfilePage from './pages/ProfilePage';
import AdminPage from './pages/AdminPage';
import BottomNav from './components/BottomNav';
import Toast from './components/Toast';
import { useRankChange } from './hooks/useRankChange';
import styles from './App.module.css';
const IS_DEV = import.meta.env.DEV || import.meta.env.VITE_TEST_MODE === 'true';
// Lazy-load DevPanel in Development/Test-Mode
let DevPanel: React.ComponentType<any> | null = null;
// VITE_TEST_MODE wird erst zur Laufzeit geprüft, daher Import immer einbinden
import('./components/DevPanel').then(m => { DevPanel = m.default; }).catch(() => {});
type ThemeSetting = 'dark' | 'light' | 'system';
function getInitialSetting(): ThemeSetting {
try {
const stored = localStorage.getItem('theme') as ThemeSetting | null;
if (stored === 'light' || stored === 'dark' || stored === 'system') return stored;
} catch {}
return 'dark';
}
function resolveTheme(setting: ThemeSetting): 'dark' | 'light' {
if (setting === 'system') {
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
return setting;
}
export default function App() {
const [themeSetting, setThemeSetting] = useState<ThemeSetting>(getInitialSetting);
const theme = resolveTheme(themeSetting);
const { message: rankMsg, dismiss: dismissRank } = useRankChange();
const [devUser, setDevUser] = useState(1);
const [devMatches, setDevMatches] = useState<any[]>([]);
const [refreshKey, setRefreshKey] = useState(0);
// Theme auf <html> setzen und in localStorage speichern
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
try { localStorage.setItem('theme', themeSetting); } catch {}
}, [theme, themeSetting]);
// Listen for OS theme changes when in system mode
useEffect(() => {
if (themeSetting !== 'system') return;
const mq = window.matchMedia('(prefers-color-scheme: light)');
const handler = () => setRefreshKey(k => k + 1); // force re-render to re-resolve
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, [themeSetting]);
function toggleTheme() {
setThemeSetting(t => t === 'dark' ? 'light' : t === 'light' ? 'system' : 'dark');
}
// DevUser als Query-Parameter im API-Fetch setzen
useEffect(() => {
if (!IS_DEV) return;
// Patch fetch für Dev-Mode: devUser Query-Param anhängen
const origFetch = window.fetch;
window._devUser = devUser;
window.fetch = (input, init) => {
if (typeof input === 'string' && input.startsWith('/api')) {
const url = new URL(input, window.location.origin);
url.searchParams.set('devUser', String(window._devUser ?? 1));
return origFetch(url.toString(), init);
}
return origFetch(input, init);
};
return () => { window.fetch = origFetch; };
}, [devUser]);
// Matches für DevPanel laden
useEffect(() => {
if (!IS_DEV) return;
fetch('/api/matches').then(r => r.json()).then(d => setDevMatches(d.matches ?? [])).catch(() => {});
}, [refreshKey, devUser]);
function handleDevRefresh() {
setRefreshKey(k => k + 1);
}
return (
<div className={styles.app}>
<header className={styles.header}>
<div className={styles.headerInner}>
<NavLink to="/" className={styles.logo}>
<img
src="/assets/wm2026-trophy.svg"
alt="FIFA WM 2026"
className={styles.logoImg}
/>
<div className={styles.logoTextBlock}>
<span className={styles.logoText}>Tippspiel</span>
<span className={styles.logoSub}>FIFA World Cup 2026</span>
</div>
</NavLink>
<div className={styles.logoExtra}>
{IS_DEV && (
<span className={styles.devBadge}>DEV · User {devUser}</span>
)}
</div>
<nav className={styles.nav}>
<NavLink to="/spiele" className={({ isActive }) => isActive ? styles.navLinkActive : styles.navLink}>
Spielplan
</NavLink>
<NavLink to="/rangliste" className={({ isActive }) => isActive ? styles.navLinkActive : styles.navLink}>
Rangliste
</NavLink>
<NavLink to="/profil" className={({ isActive }) => isActive ? styles.navLinkActive : styles.navLink}>
Mein Profil
</NavLink>
<NavLink to="/admin" className={({ isActive }) => isActive ? styles.navLinkActive : styles.navLink}>
Admin
</NavLink>
</nav>
<div className={styles.headerActions}>
<button
className={styles.themeToggle}
onClick={toggleTheme}
title={themeSetting === 'dark' ? 'Light Mode' : themeSetting === 'light' ? 'System' : 'Dark Mode'}
aria-label="Theme wechseln"
>
{themeSetting === 'dark' ? <Sun size={16} /> : themeSetting === 'light' ? <Monitor size={16} /> : <Moon size={16} />}
</button>
</div>
</div>
</header>
<main className={styles.main}>
<Routes>
<Route path="/" element={<DashboardPage key={refreshKey} />} />
<Route path="/spiele" element={<MatchesPage key={refreshKey} />} />
<Route path="/rangliste" element={<LeaderboardPage key={refreshKey} />} />
<Route path="/profil" element={<ProfilePage key={refreshKey} />} />
<Route path="/admin" element={<AdminPage />} />
</Routes>
</main>
{rankMsg && <Toast message={rankMsg} onDismiss={dismissRank} />}
<BottomNav />
{IS_DEV && DevPanel && (
<DevPanel
currentUser={devUser}
onUserChange={(u: number) => { setDevUser(u); setRefreshKey(k => k + 1); }}
matches={devMatches}
onRefresh={handleDevRefresh}
/>
)}
</div>
);
}