From 05f1dfc6896d3affa7c494a1b955f230d836a4b7 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Fri, 27 Oct 2023 18:07:45 +0200
Subject: feat: replace next-themes with a custom ThemeProvider

To be honest, next-themes was working fine. However since I use a theme
provider for Prism code blocks, some code is duplicated between this
app and the library. So I prefer to use a custom Provider without the
options I don't need.
---
 .../providers/theme-provider/theme-provider.tsx    | 88 ++++++++++++++++++++++
 1 file changed, 88 insertions(+)
 create mode 100644 src/utils/providers/theme-provider/theme-provider.tsx

(limited to 'src/utils/providers/theme-provider/theme-provider.tsx')

diff --git a/src/utils/providers/theme-provider/theme-provider.tsx b/src/utils/providers/theme-provider/theme-provider.tsx
new file mode 100644
index 0000000..b743a7f
--- /dev/null
+++ b/src/utils/providers/theme-provider/theme-provider.tsx
@@ -0,0 +1,88 @@
+import {
+  type Dispatch,
+  type FC,
+  type ReactNode,
+  type SetStateAction,
+  createContext,
+  useMemo,
+  useEffect,
+} from 'react';
+import { getDataAttributeFrom, getThemeFromSystem } from '../../helpers';
+import { useLocalStorage, useSystemColorScheme } from '../../hooks';
+
+const validThemes = ['dark', 'light', 'system'] as const;
+
+type Theme = (typeof validThemes)[number];
+
+type ThemeContextProps = {
+  resolvedTheme: Exclude<Theme, 'system'>;
+  setTheme: Dispatch<SetStateAction<Theme>>;
+  theme: Theme;
+};
+
+export const ThemeContext = createContext<ThemeContextProps>({
+  resolvedTheme: getThemeFromSystem(),
+  setTheme: (value) => value,
+  theme: 'system',
+});
+
+const validator = (value: unknown): value is Theme =>
+  typeof value === 'string' &&
+  (validThemes as readonly string[]).includes(value);
+
+export type ThemeProviderProps = {
+  /**
+   * The attribute name to append to document root.
+   */
+  attribute: string;
+  /**
+   * The provider children.
+   */
+  children?: ReactNode;
+  /**
+   * The default theme.
+   *
+   * @default 'system'
+   */
+  defaultTheme?: Theme;
+  /**
+   * The key to use in local storage.
+   */
+  storageKey: string;
+};
+
+export const ThemeProvider: FC<ThemeProviderProps> = ({
+  attribute,
+  children,
+  defaultTheme = 'system',
+  storageKey,
+}) => {
+  const [theme, setTheme] = useLocalStorage(
+    storageKey,
+    defaultTheme,
+    validator
+  );
+  const userColorScheme = useSystemColorScheme();
+  const resolvedTheme: Exclude<Theme, 'system'> =
+    theme === 'system' ? userColorScheme : theme;
+  const dataAttribute = getDataAttributeFrom(attribute);
+
+  useEffect(() => {
+    if (typeof window !== 'undefined') {
+      document.documentElement.setAttribute(dataAttribute, `${resolvedTheme}`);
+      document.documentElement.style.colorScheme = resolvedTheme;
+    }
+
+    return () => {
+      document.documentElement.removeAttribute(dataAttribute);
+    };
+  }, [dataAttribute, resolvedTheme]);
+
+  const value = useMemo(() => {
+    return { resolvedTheme, setTheme, theme };
+  }, [resolvedTheme, setTheme, theme]);
+
+  return (
+    <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
+  );
+};
-- 
cgit v1.2.3

