diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx index d7e261c..8b1ed2b 100644 --- a/src/contexts/ThemeContext.tsx +++ b/src/contexts/ThemeContext.tsx @@ -1,63 +1,4 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; +import { ThemeContextType } from "@/types/theme.interface"; +import { createContext } from "react"; -type ThemeMode = 'light' | 'dark'; -type AccentTheme = 'blue' | 'purple' | 'green'; - -interface ThemeContextType { - mode: ThemeMode; - accent: AccentTheme; - setMode: (mode: ThemeMode) => void; - setAccent: (accent: AccentTheme) => void; - toggleMode: () => void; -} - -const ThemeContext = createContext(undefined); - -export function ThemeProvider({ children }: { children: React.ReactNode }) { - const [mode, setModeState] = useState('dark'); - const [accent, setAccentState] = useState('blue'); - - // Initialize theme from localStorage - useEffect(() => { - const savedMode = localStorage.getItem('techzaa-mode') as ThemeMode; - const savedAccent = localStorage.getItem('techzaa-accent') as AccentTheme; - - if (savedMode) setModeState(savedMode); - if (savedAccent) setAccentState(savedAccent); - }, []); - - // Apply theme classes to document - useEffect(() => { - const root = document.documentElement; - - // Apply dark/light mode - root.classList.remove('light', 'dark'); - root.classList.add(mode); - - // Apply accent theme - root.classList.remove('theme-blue', 'theme-purple', 'theme-green'); - root.classList.add(`theme-${accent}`); - - // Save to localStorage - localStorage.setItem('techzaa-mode', mode); - localStorage.setItem('techzaa-accent', accent); - }, [mode, accent]); - - const setMode = (newMode: ThemeMode) => setModeState(newMode); - const setAccent = (newAccent: AccentTheme) => setAccentState(newAccent); - const toggleMode = () => setModeState(prev => prev === 'dark' ? 'light' : 'dark'); - - return ( - - {children} - - ); -} - -export function useTheme() { - const context = useContext(ThemeContext); - if (context === undefined) { - throw new Error('useTheme must be used within a ThemeProvider'); - } - return context; -} +export const ThemeContext = createContext(null); diff --git a/src/hooks/useTheme.tsx b/src/hooks/useTheme.tsx new file mode 100644 index 0000000..42e3959 --- /dev/null +++ b/src/hooks/useTheme.tsx @@ -0,0 +1,15 @@ +import { ThemeContext } from "@/contexts/ThemeContext"; +import { ThemeContextType } from "@/types/theme.interface"; +import { useContext } from "react"; + +export function useTheme(): ThemeContextType { + const context = useContext(ThemeContext); + + if (!context) { + throw new Error( + "useTheme must be used within a ThemeProvider" + ); + } + + return context; +} \ No newline at end of file diff --git a/src/provider/ThemeProvider.tsx b/src/provider/ThemeProvider.tsx new file mode 100644 index 0000000..f908d40 --- /dev/null +++ b/src/provider/ThemeProvider.tsx @@ -0,0 +1,66 @@ +import { ThemeContext } from "@/contexts/ThemeContext"; +import { ThemeMode, AccentTheme } from "@/types/theme.interface"; +import { useState, useEffect, useCallback, useMemo } from "react"; + +const STORAGE_KEYS = { + mode: "techzaa-mode", + accent: "techzaa-accent", +} as const; + +const isValidMode = (value: unknown): value is ThemeMode => + value === "light" || value === "dark"; + +const isValidAccent = (value: unknown): value is AccentTheme => + value === "blue" || value === "purple" || value === "green"; + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [mode, setMode] = useState(() => { + if (typeof window === "undefined") return "dark"; + + const stored = localStorage.getItem(STORAGE_KEYS.mode); + + return isValidMode(stored) ? stored : "dark"; + }); + + const [accent, setAccent] = useState(() => { + if (typeof window === "undefined") return "blue"; + + const stored = localStorage.getItem(STORAGE_KEYS.accent); + + return isValidAccent(stored) ? stored : "blue"; + }); + + useEffect(() => { + if (typeof document === "undefined") return; + + const root = document.documentElement; + + root.classList.remove("light", "dark"); + root.classList.add(mode); + + root.classList.remove("theme-blue", "theme-purple", "theme-green"); + root.classList.add(`theme-${accent}`); + + localStorage.setItem(STORAGE_KEYS.mode, mode); + localStorage.setItem(STORAGE_KEYS.accent, accent); + }, [mode, accent]); + + const toggleMode = useCallback(() => { + setMode((prev) => (prev === "dark" ? "light" : "dark")); + }, []); + + const value = useMemo( + () => ({ + mode, + accent, + setMode, + setAccent, + toggleMode, + }), + [mode, accent, toggleMode], + ); + + return ( + {children} + ); +} \ No newline at end of file diff --git a/src/types/theme.interface.ts b/src/types/theme.interface.ts new file mode 100644 index 0000000..013c193 --- /dev/null +++ b/src/types/theme.interface.ts @@ -0,0 +1,10 @@ +export type ThemeMode = "light" | "dark"; +export type AccentTheme = "blue" | "purple" | "green"; + +export interface ThemeContextType { + mode: ThemeMode; + accent: AccentTheme; + setMode: React.Dispatch>; + setAccent: React.Dispatch>; + toggleMode: () => void; +} \ No newline at end of file