9e1a982d37
Build & Deploy Tippspiel / build (push) Successful in 50s
Extracted trophy graphic from unofficial logo SVG, removed "FIFA WORLD CUP", "2026", and country names text. Works in both dark and light mode (colored on transparent). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
138 lines
4.9 KiB
TypeScript
138 lines
4.9 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Routes, Route, NavLink } from 'react-router-dom';
|
|
import { Sun, Moon } 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 Theme = 'dark' | 'light';
|
|
|
|
function getInitialTheme(): Theme {
|
|
try {
|
|
const stored = localStorage.getItem('theme') as Theme | null;
|
|
if (stored === 'light' || stored === 'dark') return stored;
|
|
} catch {}
|
|
return 'dark';
|
|
}
|
|
|
|
export default function App() {
|
|
const [theme, setTheme] = useState<Theme>(getInitialTheme);
|
|
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', theme); } catch {}
|
|
}, [theme]);
|
|
|
|
function toggleTheme() {
|
|
setTheme(t => t === 'dark' ? 'light' : '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}>
|
|
<div className={styles.logo}>
|
|
<img
|
|
src="/assets/wm2026-trophy.svg"
|
|
alt="FIFA WM 2026"
|
|
className={styles.logoImg}
|
|
/>
|
|
<span className={styles.logoText}>Tippspiel</span>
|
|
{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>
|
|
</nav>
|
|
<div className={styles.headerActions}>
|
|
<button
|
|
className={styles.themeToggle}
|
|
onClick={toggleTheme}
|
|
title={theme === 'dark' ? 'Light Mode aktivieren' : 'Dark Mode aktivieren'}
|
|
aria-label="Theme wechseln"
|
|
>
|
|
{theme === 'dark' ? <Sun 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>
|
|
);
|
|
}
|