theme-provider.tsx raw

   1  import { createContext, useContext, useEffect, useState } from "react";
   2  
   3  export type DarkMode = "system" | "light" | "dark";
   4  // eslint-disable-next-line react-refresh/only-export-components
   5  export const Themes = [
   6    "alby",
   7    "classic",
   8    "bitcoin",
   9    "nostr",
  10    "matrix",
  11    "ghibli",
  12    "claymorphism",
  13  ] as const;
  14  export type Theme = (typeof Themes)[number];
  15  
  16  type ThemeProviderProps = {
  17    children: React.ReactNode;
  18    defaultTheme?: Theme;
  19    defaultDarkMode?: DarkMode;
  20    storageKey?: string;
  21  };
  22  
  23  type ThemeProviderState = {
  24    theme: string;
  25    darkMode: string;
  26    setTheme: (theme: Theme) => void;
  27    setDarkMode: (mode: DarkMode) => void;
  28    isDarkMode: boolean;
  29  };
  30  
  31  const initialState: ThemeProviderState = {
  32    theme: "alby",
  33    setTheme: () => null,
  34    darkMode: "system",
  35    setDarkMode: () => null,
  36    isDarkMode: false,
  37  };
  38  
  39  const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
  40  
  41  export function ThemeProvider({
  42    children,
  43    defaultTheme = "alby",
  44    defaultDarkMode = "system",
  45    storageKey = "vite-ui-theme",
  46    ...props
  47  }: ThemeProviderProps) {
  48    const [theme, setTheme] = useState<Theme>(() => {
  49      const themeFromStorage = localStorage.getItem(storageKey) as Theme;
  50      return Themes.includes(themeFromStorage) ? themeFromStorage : defaultTheme;
  51    });
  52  
  53    const [darkMode, setDarkMode] = useState<DarkMode>(() => {
  54      return (
  55        (localStorage.getItem(storageKey + "-darkmode") as DarkMode) ||
  56        defaultDarkMode
  57      );
  58    });
  59  
  60    const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
  61  
  62    useEffect(() => {
  63      const root = window.document.documentElement;
  64  
  65      // Find and remove classes that start with 'theme-'
  66      const classList = root.classList;
  67      classList.forEach((className) => {
  68        if (className.startsWith("theme-")) {
  69          classList.remove(className);
  70        }
  71      });
  72  
  73      classList.add(`theme-${theme}`);
  74  
  75      let prefersDark;
  76      if (darkMode == "system") {
  77        prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
  78      } else {
  79        prefersDark = darkMode === "dark";
  80      }
  81  
  82      setIsDarkMode(prefersDark);
  83  
  84      if (prefersDark) {
  85        classList.add("dark");
  86      } else {
  87        classList.remove("dark");
  88      }
  89    }, [theme, darkMode]);
  90  
  91    const value = {
  92      theme,
  93      setTheme: (theme: Theme) => {
  94        localStorage.setItem(storageKey, theme);
  95        setTheme(theme);
  96      },
  97      darkMode,
  98      setDarkMode: (darkMode: DarkMode) => {
  99        localStorage.setItem(storageKey + "-darkmode", darkMode);
 100        setDarkMode(darkMode);
 101      },
 102      isDarkMode,
 103    };
 104  
 105    return (
 106      <ThemeProviderContext.Provider {...props} value={value}>
 107        {children}
 108      </ThemeProviderContext.Provider>
 109    );
 110  }
 111  
 112  // eslint-disable-next-line react-refresh/only-export-components
 113  export const useTheme = () => {
 114    const context = useContext(ThemeProviderContext);
 115  
 116    if (context === undefined) {
 117      throw new Error("useTheme must be used within a ThemeProvider");
 118    }
 119  
 120    return context;
 121  };
 122