ThemeProvider.tsx raw

   1  import { PRIMARY_COLORS, StorageKey, TPrimaryColor } from '@/constants'
   2  import storage, { dispatchSettingsChanged } from '@/services/local-storage.service'
   3  import { TTheme, TThemeSetting } from '@/types'
   4  import { createContext, useContext, useEffect, useState } from 'react'
   5  
   6  type ThemeProviderState = {
   7    theme: TTheme
   8    themeSetting: TThemeSetting
   9    setThemeSetting: (themeSetting: TThemeSetting) => void
  10    primaryColor: TPrimaryColor
  11    setPrimaryColor: (color: TPrimaryColor) => void
  12  }
  13  
  14  const ThemeProviderContext = createContext<ThemeProviderState | undefined>(undefined)
  15  
  16  const updateCSSVariables = (color: TPrimaryColor, currentTheme: TTheme) => {
  17    const root = window.document.documentElement
  18    const colorConfig = PRIMARY_COLORS[color] ?? PRIMARY_COLORS.DEFAULT
  19  
  20    const config = currentTheme === 'light' ? colorConfig.light : colorConfig.dark
  21  
  22    root.style.setProperty('--primary', config.primary)
  23    root.style.setProperty('--primary-hover', config['primary-hover'])
  24    root.style.setProperty('--primary-foreground', config['primary-foreground'])
  25    root.style.setProperty('--ring', config.ring)
  26  }
  27  
  28  function migrateThemeSetting(raw: string | null): TThemeSetting {
  29    if (raw === 'pure-black') return 'dark'
  30    if (raw === 'light' || raw === 'dark' || raw === 'system') return raw
  31    return 'system'
  32  }
  33  
  34  export function ThemeProvider({ children }: { children: React.ReactNode }) {
  35    const [themeSetting, setThemeSetting] = useState<TThemeSetting>(
  36      migrateThemeSetting(localStorage.getItem(StorageKey.THEME_SETTING))
  37    )
  38    const [theme, setTheme] = useState<TTheme>('light')
  39    const [primaryColor, setPrimaryColor] = useState<TPrimaryColor>(
  40      (localStorage.getItem(StorageKey.PRIMARY_COLOR) as TPrimaryColor) ?? 'DEFAULT'
  41    )
  42  
  43    useEffect(() => {
  44      if (themeSetting !== 'system') {
  45        setTheme(themeSetting)
  46        return
  47      }
  48  
  49      const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
  50      const handleChange = (e: MediaQueryListEvent) => {
  51        setTheme(e.matches ? 'dark' : 'light')
  52      }
  53      mediaQuery.addEventListener('change', handleChange)
  54      setTheme(mediaQuery.matches ? 'dark' : 'light')
  55  
  56      return () => {
  57        mediaQuery.removeEventListener('change', handleChange)
  58      }
  59    }, [themeSetting])
  60  
  61    useEffect(() => {
  62      const root = window.document.documentElement
  63      root.classList.remove('light', 'dark')
  64      root.classList.add(theme)
  65  
  66      // Dark mode always uses pure-black styling
  67      if (theme === 'dark') {
  68        root.classList.add('pure-black')
  69      } else {
  70        root.classList.remove('pure-black')
  71      }
  72    }, [theme])
  73  
  74    useEffect(() => {
  75      updateCSSVariables(primaryColor, theme)
  76    }, [theme, primaryColor])
  77  
  78    const updateThemeSetting = (themeSetting: TThemeSetting) => {
  79      storage.setThemeSetting(themeSetting)
  80      setThemeSetting(themeSetting)
  81      dispatchSettingsChanged()
  82    }
  83  
  84    const updatePrimaryColor = (color: TPrimaryColor) => {
  85      storage.setPrimaryColor(color)
  86      setPrimaryColor(color)
  87      dispatchSettingsChanged()
  88    }
  89  
  90    return (
  91      <ThemeProviderContext.Provider
  92        value={{
  93          theme,
  94          themeSetting,
  95          setThemeSetting: updateThemeSetting,
  96          primaryColor,
  97          setPrimaryColor: updatePrimaryColor
  98        }}
  99      >
 100        {children}
 101      </ThemeProviderContext.Provider>
 102    )
 103  }
 104  
 105  export const useTheme = () => {
 106    const context = useContext(ThemeProviderContext)
 107  
 108    if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider')
 109  
 110    return context
 111  }
 112