diff --git a/frontend/src/App.module.css b/frontend/src/App.module.css index 3b7c6c7..d301e70 100644 --- a/frontend/src/App.module.css +++ b/frontend/src/App.module.css @@ -5,9 +5,9 @@ } .header { - background: rgba(10,14,26,0.92); + background: color-mix(in srgb, var(--bg-deep) 92%, transparent); backdrop-filter: blur(20px); - border-bottom: 1px solid rgba(75,183,248,0.1); + border-bottom: 1px solid rgba(75,183,248,0.12); position: sticky; top: 0; z-index: 100; @@ -66,13 +66,31 @@ color: var(--text-secondary); } -.navLink:hover { color: var(--text-primary); background: var(--surface-mid); } +.navLink:hover { color: var(--text-primary); background: var(--surface-high); } .navLinkActive { color: var(--primary); background: var(--primary-dim); } +.themeToggle { + background: var(--surface-high); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + width: 34px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + cursor: pointer; + transition: background 0.15s, transform 0.1s; + margin-left: 6px; + flex-shrink: 0; +} +.themeToggle:hover { background: var(--primary-dim); } +.themeToggle:active { transform: scale(0.92); } + .main { flex: 1; max-width: 1100px; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9abf884..3b2a2f6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,11 +14,32 @@ let DevPanel: React.ComponentType | 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(getInitialTheme); const [devUser, setDevUser] = useState(1); const [devMatches, setDevMatches] = useState([]); const [refreshKey, setRefreshKey] = useState(0); + // Theme auf 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; @@ -70,6 +91,14 @@ export default function App() { isActive ? styles.navLinkActive : styles.navLink}> Admin + diff --git a/frontend/src/components/MatchCard.module.css b/frontend/src/components/MatchCard.module.css index 6ae05a4..e4aaafb 100644 --- a/frontend/src/components/MatchCard.module.css +++ b/frontend/src/components/MatchCard.module.css @@ -175,7 +175,7 @@ /* Tipp area */ .tipRow { - border-top: 1px solid rgba(255,255,255,0.05); + border-top: 1px solid var(--border-subtle); padding-top: 14px; display: flex; align-items: center; @@ -323,7 +323,7 @@ .editBtn { background: transparent; - border: 1px solid rgba(255,255,255,0.1); + border: 1px solid var(--border-subtle); color: var(--text-muted); padding: 4px 12px; border-radius: 20px; diff --git a/frontend/src/components/TipModal.module.css b/frontend/src/components/TipModal.module.css index a414030..b5caa63 100644 --- a/frontend/src/components/TipModal.module.css +++ b/frontend/src/components/TipModal.module.css @@ -52,7 +52,7 @@ .handle { width: 40px; height: 4px; - background: rgba(255,255,255,0.15); + background: var(--surface-high); border-radius: 2px; margin: 0 auto 20px; } @@ -222,7 +222,7 @@ width: 56px; height: 56px; background: var(--surface-high); - border: 1px solid rgba(255,255,255,0.1); + border: 1px solid var(--border-subtle); border-radius: 16px; color: var(--text-primary); font-size: 24px; @@ -287,7 +287,7 @@ background: var(--surface-high); border-radius: 12px; margin-bottom: 20px; - border: 1px solid rgba(255,255,255,0.08); + border: 1px solid var(--border-subtle); position: relative; overflow: hidden; box-shadow: @@ -511,7 +511,7 @@ flex-direction: column; gap: 3px; padding: 10px 0; - border-bottom: 1px solid rgba(255,255,255,0.05); + border-bottom: 1px solid var(--border-subtle); } .insightLine:last-child { diff --git a/frontend/src/index.css b/frontend/src/index.css index 651e546..e2cc7fb 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -2,26 +2,54 @@ WM 2026 Tippspiel — Stadium Elite Design System ============================================================ */ +/* --- Dark Mode (Standard) --- */ :root { - --bg-deep: #0A0E1A; - --bg-mid: #0F1628; - --surface-low: #111827; - --surface-mid: #151D30; - --surface-high: #1C2640; - --primary: #4BB7F8; - --primary-dim: rgba(75,183,248,0.12); - --gold: #FEAE32; - --gold-glow: rgba(254,174,50,0.4); - --cyan: #69DAFF; - --text-primary: #F0F4FF; + --bg-deep: #0A0E1A; + --bg-mid: #0F1628; + --surface-low: #111827; + --surface-mid: #151D30; + --surface-high: #1C2640; + --border-subtle: rgba(255,255,255,0.07); + --primary: #4BB7F8; + --primary-dim: rgba(75,183,248,0.12); + --gold: #FEAE32; + --gold-glow: rgba(254,174,50,0.4); + --cyan: #69DAFF; + --text-primary: #F0F4FF; --text-secondary: rgba(240,244,255,0.55); - --text-muted: rgba(240,244,255,0.3); - --success: #34D399; - --error: #F87171; - --radius-sm: 8px; - --radius-md: 14px; - --radius-lg: 20px; - --radius-xl: 28px; + --text-muted: rgba(240,244,255,0.3); + --success: #34D399; + --error: #F87171; + --radius-sm: 8px; + --radius-md: 14px; + --radius-lg: 20px; + --radius-xl: 28px; + --shadow-card: 0 10px 25px rgba(0,0,0,0.25); + --card-shine: rgba(255,255,255,0.04); + --scrollbar-bg: var(--surface-high); +} + +/* --- Light Mode --- */ +[data-theme="light"] { + --bg-deep: #F0F4FA; + --bg-mid: #E8EEF7; + --surface-low: #FFFFFF; + --surface-mid: #FFFFFF; + --surface-high: #DDE5F0; + --border-subtle: rgba(0,0,0,0.08); + --primary: #1A8FE3; + --primary-dim: rgba(26,143,227,0.10); + --gold: #D4880A; + --gold-glow: rgba(212,136,10,0.3); + --cyan: #0080C6; + --text-primary: #0D1526; + --text-secondary: rgba(13,21,38,0.6); + --text-muted: rgba(13,21,38,0.35); + --success: #1AAB72; + --error: #D93025; + --shadow-card: 0 4px 16px rgba(0,0,0,0.10); + --card-shine: rgba(255,255,255,0.7); + --scrollbar-bg: var(--surface-high); } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } @@ -38,7 +66,7 @@ html, body, #root { /* Scrollbar */ ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: var(--surface-high); border-radius: 3px; } +::-webkit-scrollbar-thumb { background: var(--scrollbar-bg); border-radius: 3px; } /* Utility */ .font-display { font-family: 'Plus Jakarta Sans', sans-serif; } @@ -50,10 +78,10 @@ html, body, #root { .card { background: var(--surface-mid); border-radius: var(--radius-md); + border: 1px solid var(--border-subtle); box-shadow: - 0 10px 25px rgba(0,0,0,0.25), - inset 0 1px 0 rgba(255,255,255,0.07), - inset 1px 0 0 rgba(255,255,255,0.04); + var(--shadow-card), + inset 0 1px 0 var(--border-subtle); position: relative; overflow: hidden; } @@ -61,7 +89,7 @@ html, body, #root { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 50%; - background: linear-gradient(180deg, rgba(255,255,255,0.04) 0%, transparent 100%); + background: linear-gradient(180deg, var(--card-shine) 0%, transparent 100%); pointer-events: none; } diff --git a/frontend/src/pages/AdminPage.module.css b/frontend/src/pages/AdminPage.module.css index b8b33b1..eafcb10 100644 --- a/frontend/src/pages/AdminPage.module.css +++ b/frontend/src/pages/AdminPage.module.css @@ -171,8 +171,8 @@ .spinner { width: 14px; height: 14px; - border: 2px solid rgba(255,255,255,0.3); - border-top-color: #fff; + border: 2px solid var(--surface-high); + border-top-color: var(--text-primary); border-radius: 50%; animation: spin 0.7s linear infinite; flex-shrink: 0; diff --git a/frontend/src/pages/MatchesPage.module.css b/frontend/src/pages/MatchesPage.module.css index bf91887..6f511a4 100644 --- a/frontend/src/pages/MatchesPage.module.css +++ b/frontend/src/pages/MatchesPage.module.css @@ -40,7 +40,7 @@ font-size: 13px; font-weight: 500; cursor: pointer; - border: 1px solid rgba(255,255,255,0.1); + border: 1px solid var(--border-subtle); background: transparent; color: var(--text-secondary); transition: all 0.15s; @@ -58,7 +58,7 @@ flex-wrap: wrap; gap: 6px; padding: 4px 0; - border-top: 1px solid rgba(255,255,255,0.05); + border-top: 1px solid var(--border-subtle); margin-top: -8px; } @@ -68,7 +68,7 @@ font-size: 12px; font-weight: 600; cursor: pointer; - border: 1px solid rgba(255,255,255,0.08); + border: 1px solid var(--border-subtle); background: transparent; color: var(--text-muted); transition: all 0.15s; diff --git a/frontend/src/pages/ProfilePage.module.css b/frontend/src/pages/ProfilePage.module.css index a077bef..00639b5 100644 --- a/frontend/src/pages/ProfilePage.module.css +++ b/frontend/src/pages/ProfilePage.module.css @@ -1,7 +1,7 @@ .page { display: flex; flex-direction: column; gap: 20px; max-width: 640px; } .loading { display: flex; justify-content: center; padding: 60px; } .spinner { width: 32px; height: 32px; border: 3px solid var(--surface-high); border-top-color: var(--primary); border-radius: 50%; animation: spin 0.8s linear infinite; } -.spinnerSm { width: 14px; height: 14px; border: 2px solid rgba(255,255,255,0.3); border-top-color: #fff; border-radius: 50%; animation: spin 0.7s linear infinite; display: inline-block; } +.spinnerSm { width: 14px; height: 14px; border: 2px solid var(--surface-high); border-top-color: var(--text-primary); border-radius: 50%; animation: spin 0.7s linear infinite; display: inline-block; } @keyframes spin { to { transform: rotate(360deg); } } .empty { color: var(--text-secondary); padding: 40px; text-align: center; }