Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | 4x 4x 4x 2x 2x 24x 25x 25x 10x 15x 25x 25x 9x 38x 38x 38x 20x 18x 25x 25x 19x 25x 25x 25x 24x 2x 2x 1x 1x 2x 2x 25x 5x 5x 5x 5x 5x 5x 5x 5x 1x 25x 28x 28x 2x 25x | import {
createContext,
useContext,
useState,
useEffect,
useCallback,
type ReactNode,
} from "react";
import { authApi } from "./api";
export type Theme = "light" | "dark" | "system";
type ResolvedTheme = "light" | "dark";
type ThemeContextType = {
theme: Theme;
resolvedTheme: ResolvedTheme;
setTheme: (theme: Theme) => void;
};
const ThemeContext = createContext<ThemeContextType | null>(null);
const STORAGE_KEY = "decor-rocket-theme";
const VALID_THEMES: Theme[] = ["light", "dark", "system"];
function getSystemTheme(): ResolvedTheme {
Iif (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 applyTheme(resolved: ResolvedTheme) {
const root = document.documentElement;
if (resolved === "dark") {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
// Update theme-color meta tag
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) {
meta.setAttribute("content", resolved === "dark" ? "#1C1917" : "#E86F4A");
}
}
function readStoredTheme(): Theme {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored && VALID_THEMES.includes(stored as Theme)) {
return stored as Theme;
}
} catch {
// localStorage unavailable
}
return "light";
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(readStoredTheme);
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() =>
resolveTheme(readStoredTheme()),
);
// Apply theme to DOM whenever resolvedTheme changes
useEffect(() => {
applyTheme(resolvedTheme);
}, [resolvedTheme]);
// Listen for system theme changes when in "system" mode
useEffect(() => {
if (theme !== "system") return;
const mq = window.matchMedia("(prefers-color-scheme: dark)");
const handler = (e: MediaQueryListEvent) => {
const newResolved = e.matches ? "dark" : "light";
setResolvedTheme(newResolved);
};
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, [theme]);
const setTheme = useCallback((newTheme: Theme) => {
// Enable smooth transition
document.documentElement.classList.add("theme-transitioning");
setThemeState(newTheme);
const resolved = resolveTheme(newTheme);
setResolvedTheme(resolved);
// Persist to localStorage (instant)
try {
localStorage.setItem(STORAGE_KEY, newTheme);
} catch {
// Ignore
}
// Sync to API (fire-and-forget)
authApi
.updatePreferences({ themePreference: newTheme })
.catch(() => {});
// Remove transition class after animation completes
setTimeout(() => {
document.documentElement.classList.remove("theme-transitioning");
}, 300);
}, []);
return (
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}
|