BillTracker/client/contexts/ThemeContext.jsx

76 lines
1.8 KiB
JavaScript

import { createContext, useContext, useEffect, useState } from 'react';
/**
* Themes: 'light' | 'dark'
* Persisted to localStorage under key 'bt-theme'.
*
* Class mapping on document.documentElement:
* light → (no classes)
* dark → 'dark'
*/
const STORAGE_KEY = 'bt-theme';
const VALID_THEMES = ['light', 'dark'];
const DEFAULT_THEME = 'light';
function applyTheme(theme) {
const root = document.documentElement;
root.classList.remove('dark');
if (theme === 'dark') {
root.classList.add('dark');
}
// 'light' → no classes
}
function loadStoredTheme() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === 'dark-purple') return 'dark';
if (stored && VALID_THEMES.includes(stored)) return stored;
} catch {
// localStorage unavailable
}
return DEFAULT_THEME;
}
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setThemeState] = useState(() => {
const stored = loadStoredTheme();
// Apply immediately (before first paint) to avoid flash
applyTheme(stored);
return stored;
});
const setTheme = (newTheme) => {
if (!VALID_THEMES.includes(newTheme)) return;
applyTheme(newTheme);
setThemeState(newTheme);
try {
localStorage.setItem(STORAGE_KEY, newTheme);
} catch {
// localStorage unavailable
}
};
// Keep DOM in sync if theme state ever changes externally
useEffect(() => {
applyTheme(theme);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return ctx;
}