/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react' import { getCookie, setCookie, removeCookie } from '@/lib/cookies' type Theme = 'dark' | 'light' | 'system' type ResolvedTheme = Exclude const DEFAULT_THEME = 'system' const THEME_COOKIE_NAME = 'vite-ui-theme' const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year const THEMES = new Set(['dark', 'light', 'system']) type ThemeProviderProps = { children: React.ReactNode defaultTheme?: Theme storageKey?: string } type ThemeProviderState = { defaultTheme: Theme resolvedTheme: ResolvedTheme theme: Theme setTheme: (theme: Theme) => void resetTheme: () => void } const initialState: ThemeProviderState = { defaultTheme: DEFAULT_THEME, resolvedTheme: 'light', theme: DEFAULT_THEME, setTheme: () => null, resetTheme: () => null, } const ThemeContext = createContext(initialState) function getSystemTheme(): ResolvedTheme { if (typeof window === 'undefined') return 'light' return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' } function resolveTheme(theme: Theme): ResolvedTheme { return theme === 'system' ? getSystemTheme() : theme } function getStoredTheme(storageKey: string, fallback: Theme): Theme { const storedTheme = getCookie(storageKey) as Theme | undefined return storedTheme && THEMES.has(storedTheme) ? storedTheme : fallback } export function ThemeProvider({ children, defaultTheme = DEFAULT_THEME, storageKey = THEME_COOKIE_NAME, ...props }: ThemeProviderProps) { const [theme, _setTheme] = useState(() => getStoredTheme(storageKey, defaultTheme) ) const [resolvedTheme, setResolvedTheme] = useState(() => resolveTheme(getStoredTheme(storageKey, defaultTheme)) ) useEffect(() => { const root = window.document.documentElement const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') const applyTheme = () => { const nextResolvedTheme = theme === 'system' ? getSystemTheme() : theme root.classList.remove('light', 'dark') root.classList.add(nextResolvedTheme) setResolvedTheme(nextResolvedTheme) } applyTheme() mediaQuery.addEventListener('change', applyTheme) return () => mediaQuery.removeEventListener('change', applyTheme) }, [theme]) const setTheme = useCallback( (theme: Theme) => { setCookie(storageKey, theme, THEME_COOKIE_MAX_AGE) _setTheme(theme) }, [storageKey] ) const resetTheme = useCallback(() => { removeCookie(storageKey) _setTheme(defaultTheme) }, [defaultTheme, storageKey]) const contextValue = useMemo( () => ({ defaultTheme, resolvedTheme, resetTheme, theme, setTheme, }), [defaultTheme, resolvedTheme, resetTheme, theme, setTheme] ) return ( {children} ) } // eslint-disable-next-line react-refresh/only-export-components export const useTheme = () => { const context = useContext(ThemeContext) if (!context) throw new Error('useTheme must be used within a ThemeProvider') return context }