61 lines
1.4 KiB
TypeScript
61 lines
1.4 KiB
TypeScript
"use client";
|
||
|
||
import {
|
||
createContext,
|
||
useCallback,
|
||
useContext,
|
||
useEffect,
|
||
useState,
|
||
type ReactNode,
|
||
} from "react";
|
||
import {
|
||
DEFAULT_THEME,
|
||
type ThemeMode,
|
||
persistTheme,
|
||
readStoredTheme,
|
||
} from "@/lib/theme";
|
||
|
||
interface ThemeContextValue {
|
||
theme: ThemeMode;
|
||
setTheme: (mode: ThemeMode) => void;
|
||
toggleTheme: () => void;
|
||
}
|
||
|
||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||
|
||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||
// 首屏必須與 SSR 一致(DEFAULT_THEME);localStorage 在 useEffect 讀取,避免 hydration mismatch。
|
||
const [theme, setThemeState] = useState<ThemeMode>(DEFAULT_THEME);
|
||
|
||
useEffect(() => {
|
||
const stored = readStoredTheme();
|
||
if (stored) setThemeState(stored);
|
||
}, []);
|
||
|
||
const setTheme = useCallback((mode: ThemeMode) => {
|
||
setThemeState(mode);
|
||
persistTheme(mode);
|
||
}, []);
|
||
|
||
const toggleTheme = useCallback(() => {
|
||
setThemeState((prev) => {
|
||
const next = prev === "dark" ? "light" : "dark";
|
||
persistTheme(next);
|
||
return next;
|
||
});
|
||
}, []);
|
||
|
||
return (
|
||
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
|
||
{children}
|
||
</ThemeContext.Provider>
|
||
);
|
||
}
|
||
|
||
export function useTheme() {
|
||
const ctx = useContext(ThemeContext);
|
||
if (!ctx) {
|
||
throw new Error("useTheme must be used within ThemeProvider");
|
||
}
|
||
return ctx;
|
||
} |