From c47d8ac5167c7226186afc06f936f733e06e3dcb Mon Sep 17 00:00:00 2001 From: Oliver Bryan Date: Thu, 5 Feb 2026 12:21:59 +0000 Subject: [PATCH] theme setup --- index.html | 16 +++++++ src/components/theme-provider.tsx | 76 +++++++++++++++++++++++++++++++ src/components/theme-toggle.tsx | 19 ++++++++ src/main.tsx | 5 +- 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/components/theme-provider.tsx create mode 100644 src/components/theme-toggle.tsx diff --git a/index.html b/index.html index dacd81ce..8535acda 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,22 @@ ob248.com +
diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx new file mode 100644 index 00000000..823649ab --- /dev/null +++ b/src/components/theme-provider.tsx @@ -0,0 +1,76 @@ +import { createContext, useContext, useEffect, useMemo, useState } from "react"; + +type Theme = "light" | "dark" | "system"; + +type ThemeContextValue = { + theme: Theme; + resolvedTheme: "light" | "dark"; + setTheme: (theme: Theme) => void; +}; + +const ThemeContext = createContext(null); + +const storageKey = "theme"; + +const getStoredTheme = (): Theme => { + if (typeof window === "undefined") return "system"; + const stored = window.localStorage.getItem(storageKey); + if (stored === "light" || stored === "dark" || stored === "system") { + return stored; + } + return "system"; +}; + +const getSystemTheme = (): "light" | "dark" => { + if (typeof window === "undefined") return "light"; + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; +}; + +function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setTheme] = useState(getStoredTheme); + const resolvedTheme = theme === "system" ? getSystemTheme() : theme; + + useEffect(() => { + if (typeof window === "undefined") return; + window.localStorage.setItem(storageKey, theme); + + const root = document.documentElement; + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + + const applyTheme = (next: "light" | "dark") => { + root.classList.toggle("dark", next === "dark"); + }; + + applyTheme(theme === "system" ? getSystemTheme() : theme); + + const handleChange = () => { + if (theme === "system") { + applyTheme(getSystemTheme()); + } + }; + + mediaQuery.addEventListener("change", handleChange); + return () => mediaQuery.removeEventListener("change", handleChange); + }, [theme]); + + const value = useMemo( + () => ({ theme, resolvedTheme, setTheme }), + [theme, resolvedTheme], + ); + + return ( + {children} + ); +} + +const useTheme = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error("useTheme must be used within ThemeProvider"); + } + return context; +}; + +export { ThemeProvider, useTheme, type Theme }; diff --git a/src/components/theme-toggle.tsx b/src/components/theme-toggle.tsx new file mode 100644 index 00000000..dfa9b784 --- /dev/null +++ b/src/components/theme-toggle.tsx @@ -0,0 +1,19 @@ +import { useTheme } from "@/components/theme-provider"; +import { Button } from "@/components/ui/button"; + +function ThemeToggle() { + const { resolvedTheme, setTheme } = useTheme(); + const isDark = resolvedTheme === "dark"; + + return ( + + ); +} + +export { ThemeToggle }; diff --git a/src/main.tsx b/src/main.tsx index d7c55e3d..d70f50fd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,6 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; +import { ThemeProvider } from "@/components/theme-provider"; import "./index.css"; import App from "./App.tsx"; @@ -7,6 +8,8 @@ const root = document.getElementById("root"); if (!root) throw new Error("Failed to find the root element"); createRoot(root).render( - + + + , );