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 9e1a982d37
Build & Deploy Tippspiel / build (push) Successful in 50s
feat: WM 2026 trophy SVG without text in header
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>
2026-04-12 15:39:33 +02:00

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>
);
}