Files
gooseek/backend/webui/src/lib/contexts/ThemeContext.tsx
home ab48a0632b
Some checks failed
Build and Deploy GooSeek / build-backend (push) Failing after 1m4s
Build and Deploy GooSeek / build-webui (push) Failing after 1m2s
Build and Deploy GooSeek / deploy (push) Has been skipped
feat: CI/CD pipeline + Learning/Medicine/Travel services
- Add Gitea Actions workflow for automated build & deploy
- Add K8s manifests: webui, travel-svc, medicine-svc, sandbox-svc
- Update kustomization for localhost:5000 registry
- Add ingress for gooseek.ru and api.gooseek.ru
- Learning cabinet with onboarding, courses, sandbox integration
- Medicine service with symptom analysis and doctor matching
- Travel service with itinerary planning
- Server setup scripts (NVIDIA/CUDA, K3s, Gitea runner)

Made-with: Cursor
2026-03-02 20:25:44 +03:00

95 lines
2.6 KiB
TypeScript

'use client';
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
export type ThemeMode = 'light' | 'dim';
const THEME_STORAGE_KEY = 'gooseek_theme';
function readStoredTheme(): ThemeMode | null {
try {
const v = localStorage.getItem(THEME_STORAGE_KEY);
if (v === 'light' || v === 'dim') return v;
return null;
} catch {
return null;
}
}
function prefersDark(): boolean {
if (typeof window === 'undefined') return false;
return !!window.matchMedia?.('(prefers-color-scheme: dark)')?.matches;
}
function applyTheme(theme: ThemeMode): void {
if (typeof document === 'undefined') return;
const root = document.documentElement;
if (theme === 'dim') root.classList.add('theme-dim');
else root.classList.remove('theme-dim');
try {
localStorage.setItem(THEME_STORAGE_KEY, theme);
} catch {
// ignore storage failures (private mode, quota, etc.)
}
}
function resolveInitialTheme(): ThemeMode {
if (typeof document !== 'undefined' && document.documentElement.classList.contains('theme-dim')) {
return 'dim';
}
const stored = typeof window !== 'undefined' ? readStoredTheme() : null;
if (stored) return stored;
return prefersDark() ? 'dim' : 'light';
}
type ThemeContextValue = {
theme: ThemeMode;
setTheme: (theme: ThemeMode) => void;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<ThemeMode>('light');
useEffect(() => {
const initial = resolveInitialTheme();
setThemeState(initial);
applyTheme(initial);
}, []);
const setTheme = useCallback((next: ThemeMode) => {
setThemeState(next);
applyTheme(next);
}, []);
const toggleTheme = useCallback(() => {
setTheme(theme === 'light' ? 'dim' : 'light');
}, [theme, setTheme]);
useEffect(() => {
const onStorage = (e: StorageEvent) => {
if (e.key !== THEME_STORAGE_KEY) return;
const v = e.newValue;
if (v !== 'light' && v !== 'dim') return;
setThemeState(v);
applyTheme(v);
};
window.addEventListener('storage', onStorage);
return () => window.removeEventListener('storage', onStorage);
}, []);
const value = useMemo(() => ({ theme, setTheme, toggleTheme }), [theme, setTheme, toggleTheme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
export function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error('useTheme must be used within ThemeProvider');
}
return ctx;
}