import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { authApi } from './api'; import { useAuth } from './auth'; import { THEME_PALETTES, type ThemePalette } from '../types'; /* * Modus (hell/dunkel) und Design. * * Die Wahl liegt doppelt: im localStorage, damit das Inline-Skript in * index.html sie vor dem ersten Frame setzen kann, und am Benutzer in der * Datenbank, damit sie auf jedem Geraet gilt. Ohne Wahl folgt der Modus der * Systemeinstellung. */ export type ThemeName = ThemePalette; export type ThemeMode = 'light' | 'dark'; const MODE_KEY = 'icon_crm_theme_mode'; const THEME_KEY = 'icon_crm_theme'; interface ThemeContextValue { theme: ThemeName; mode: ThemeMode; /** true, solange der Benutzer nichts gewaehlt hat und das System entscheidet. */ followsSystem: boolean; setTheme: (theme: ThemeName) => void; setMode: (mode: ThemeMode) => void; toggleMode: () => void; /** Zurueck auf "folgt dem System". */ resetMode: () => void; } const ThemeContext = createContext(null); function systemPrefersDark(): boolean { return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false; } function readStored(key: string): T | null { try { return (localStorage.getItem(key) as T | null) ?? null; } catch { return null; } } function store(key: string, value: string | null) { try { if (value === null) localStorage.removeItem(key); else localStorage.setItem(key, value); } catch { /* Privater Modus oder gesperrte Website-Daten - dann gilt nur die Sitzung. */ } } export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { user, updateUser } = useAuth(); const [storedMode, setStoredMode] = useState(() => readStored(MODE_KEY)); const [theme, setThemeState] = useState(() => { // Ein Wert aus einer früheren Fassung (etwa ein entferntes Design) darf // nicht dazu führen, dass eine Klasse gesetzt wird, die es nicht gibt. const stored = readStored(THEME_KEY); return stored && THEME_PALETTES.includes(stored) ? stored : 'icon'; }); const [systemDark, setSystemDark] = useState(systemPrefersDark); // Die Wahl des angemeldeten Benutzers gewinnt gegen den lokalen Speicher: // sie gilt auf jedem Geraet. useEffect(() => { if (!user) return; if (user.theme_mode && user.theme_mode !== storedMode) { setStoredMode(user.theme_mode); store(MODE_KEY, user.theme_mode); } /* * Auch der Wert vom Server wird geprüft. Ein Design, das aus der Liste * geflogen ist, steht weiter am Konto - und hat die Anwendung schon einmal * beim Start abstürzen lassen (14.09.2026, "duke"). */ if (user.theme && THEME_PALETTES.includes(user.theme as ThemeName) && user.theme !== theme) { setThemeState(user.theme as ThemeName); store(THEME_KEY, user.theme); } // Absichtlich nur an user gebunden - sonst schriebe jede lokale Aenderung // sich selbst wieder zurueck. // eslint-disable-next-line react-hooks/exhaustive-deps }, [user?.id, user?.theme, user?.theme_mode]); useEffect(() => { const media = window.matchMedia?.('(prefers-color-scheme: dark)'); if (!media) return; const listener = (event: MediaQueryListEvent) => setSystemDark(event.matches); media.addEventListener('change', listener); return () => media.removeEventListener('change', listener); }, []); const mode: ThemeMode = storedMode ?? (systemDark ? 'dark' : 'light'); useEffect(() => { const root = document.documentElement; root.classList.toggle('dark', mode === 'dark'); /* * Jede theme-* Klasse abräumen, nicht nur die bekannten: das Inline-Skript * in index.html setzt sie aus dem localStorage, und dort kann ein längst * entfernter Name stehen. Der bliebe sonst für immer am hängen. */ for (const name of [...root.classList]) { if (name.startsWith('theme-')) root.classList.remove(name); } if (theme !== 'icon') root.classList.add(`theme-${theme}`); }, [mode, theme]); const persist = useCallback( async (body: { theme?: ThemeName; theme_mode?: ThemeMode | null }) => { if (!user) return; try { const { user: updated } = await authApi.updateMe(body); updateUser(updated); } catch { /* Die Darstellung steht lokal schon richtig - ein Fehler beim Sichern darf den Wechsel nicht zurueckdrehen. */ } }, [user, updateUser] ); const setMode = useCallback( (next: ThemeMode) => { setStoredMode(next); store(MODE_KEY, next); void persist({ theme_mode: next }); }, [persist] ); const value = useMemo( () => ({ theme, mode, followsSystem: storedMode === null, setTheme: (next: ThemeName) => { setThemeState(next); store(THEME_KEY, next); void persist({ theme: next }); }, setMode, toggleMode: () => setMode(mode === 'dark' ? 'light' : 'dark'), resetMode: () => { setStoredMode(null); store(MODE_KEY, null); void persist({ theme_mode: null }); }, }), [theme, mode, storedMode, setMode, persist] ); return {children}; }; export function useTheme(): ThemeContextValue { const ctx = useContext(ThemeContext); if (!ctx) throw new Error('useTheme muss innerhalb von verwendet werden.'); return ctx; }