mirror of
https://github.com/hex248/ob248.com.git
synced 2026-02-07 18:23:04 +00:00
theme setup
This commit is contained in:
16
index.html
16
index.html
@@ -5,6 +5,22 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ob248.com</title>
|
<title>ob248.com</title>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem("theme");
|
||||||
|
const theme =
|
||||||
|
stored === "light" || stored === "dark" ? stored : "system";
|
||||||
|
const isDark =
|
||||||
|
theme === "dark" ||
|
||||||
|
(theme === "system" &&
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||||
|
if (isDark) document.documentElement.classList.add("dark");
|
||||||
|
} catch (_error) {
|
||||||
|
document.documentElement.classList.remove("dark");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
76
src/components/theme-provider.tsx
Normal file
76
src/components/theme-provider.tsx
Normal file
@@ -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<ThemeContextValue | null>(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<Theme>(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 (
|
||||||
|
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useTheme = () => {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useTheme must be used within ThemeProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ThemeProvider, useTheme, type Theme };
|
||||||
19
src/components/theme-toggle.tsx
Normal file
19
src/components/theme-toggle.tsx
Normal file
@@ -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 (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||||
|
>
|
||||||
|
{isDark ? "light" : "dark"}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ThemeToggle };
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import App from "./App.tsx";
|
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");
|
if (!root) throw new Error("Failed to find the root element");
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<ThemeProvider>
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user