Attempt 2 to add a darkmode
This commit is contained in:
@@ -1,84 +1,66 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import * as Icon from "react-bootstrap-icons";
|
import * as Icon from "react-bootstrap-icons";
|
||||||
import { Nav, Dropdown } from "react-bootstrap";
|
import { Nav, Dropdown } from "react-bootstrap";
|
||||||
|
|
||||||
type Theme = "light" | "dark" | "auto";
|
type Theme = "light" | "dark" | "auto";
|
||||||
const COOKIE = "theme";
|
const COOKIE = "NBN-theme";
|
||||||
|
|
||||||
function getCookie(name: string): string | null {
|
const getCookie = (name: string) => {
|
||||||
if (typeof document === "undefined") return null;
|
|
||||||
const m = document.cookie.match(new RegExp("(^|; )" + name + "=([^;]+)"));
|
const m = document.cookie.match(new RegExp("(^|; )" + name + "=([^;]+)"));
|
||||||
return m ? decodeURIComponent(m[2]) : null;
|
return m ? decodeURIComponent(m[2]) : null;
|
||||||
}
|
};
|
||||||
function setCookie(name: string, value: string) {
|
|
||||||
document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=31536000; SameSite=Lax`;
|
const setCookie = (name: string, value: string) => {
|
||||||
}
|
document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=31536000; SameSite=Lax; Domain=specnext.dev`;
|
||||||
|
};
|
||||||
|
|
||||||
// Use a single function to read current system preference (works on iOS)
|
|
||||||
const prefersDark = () =>
|
const prefersDark = () =>
|
||||||
typeof window !== "undefined" &&
|
window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
window.matchMedia &&
|
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
||||||
|
|
||||||
export default function ThemeDropdown() {
|
export default function ThemeDropdown() {
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const [theme, setThemeState] = useState<Theme>("auto");
|
const [theme, setTheme] = useState<Theme>("auto");
|
||||||
|
|
||||||
const applyTheme = useCallback((t: Theme) => {
|
const apply = useCallback((t: Theme) => {
|
||||||
const effective = t === "auto" ? (prefersDark() ? "dark" : "light") : t;
|
const effective = t === "auto" ? (prefersDark() ? "dark" : "light") : t;
|
||||||
document.documentElement.setAttribute("data-bs-theme", effective);
|
document.documentElement.setAttribute("data-bs-theme", effective);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Initial mount: read cookie and APPLY immediately (important for iOS)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
const v = getCookie(COOKIE);
|
const v = getCookie(COOKIE);
|
||||||
|
console.log("Cookie:", v);
|
||||||
const initial: Theme = v === "light" || v === "dark" || v === "auto" ? (v as Theme) : "auto";
|
const initial: Theme = v === "light" || v === "dark" || v === "auto" ? (v as Theme) : "auto";
|
||||||
setThemeState(initial);
|
setTheme(initial);
|
||||||
applyTheme(initial); // ensure render matches auto right away
|
// Important: apply immediately so “auto” reflects system after hydration
|
||||||
}, [applyTheme]);
|
apply(initial);
|
||||||
|
}, [apply]);
|
||||||
|
|
||||||
// Follow system changes while in auto; include iOS visibility/page-show events
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mounted || theme !== "auto") return;
|
if (!mounted || theme !== "auto") return;
|
||||||
|
|
||||||
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
const onChange = () => applyTheme("auto");
|
const onChange = () => apply("auto");
|
||||||
|
|
||||||
// Safari <14 uses addListener/removeListener
|
|
||||||
if (typeof mql.addEventListener === "function") {
|
|
||||||
mql.addEventListener("change", onChange);
|
mql.addEventListener("change", onChange);
|
||||||
} else if (typeof mql.addListener === "function") {
|
|
||||||
mql.addListener(onChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onVisibility = () => applyTheme("auto");
|
const onPageShow = () => apply("auto");
|
||||||
const onPageShow = () => applyTheme("auto");
|
|
||||||
|
|
||||||
document.addEventListener("visibilitychange", onVisibility);
|
|
||||||
window.addEventListener("pageshow", onPageShow);
|
window.addEventListener("pageshow", onPageShow);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (typeof mql.removeEventListener === "function") {
|
|
||||||
mql.removeEventListener("change", onChange);
|
mql.removeEventListener("change", onChange);
|
||||||
} else if (typeof mql.removeListener === "function") {
|
|
||||||
mql.removeListener(onChange);
|
|
||||||
}
|
|
||||||
document.removeEventListener("visibilitychange", onVisibility);
|
|
||||||
window.removeEventListener("pageshow", onPageShow);
|
window.removeEventListener("pageshow", onPageShow);
|
||||||
};
|
};
|
||||||
}, [mounted, theme, applyTheme]);
|
}, [mounted, theme, apply]);
|
||||||
|
|
||||||
const handleSetTheme = (t: Theme) => {
|
const choose = (t: Theme) => {
|
||||||
setCookie(COOKIE, t);
|
setCookie(COOKIE, t);
|
||||||
setThemeState(t);
|
setTheme(t);
|
||||||
applyTheme(t);
|
apply(t);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isActive = (t: Theme) => theme === t;
|
const isActive = (t: Theme) => theme === t;
|
||||||
|
|
||||||
const ToggleIcon = !mounted
|
const ToggleIcon = !mounted
|
||||||
? Icon.CircleHalf
|
? Icon.CircleHalf
|
||||||
: theme === "dark"
|
: theme === "dark"
|
||||||
@@ -94,39 +76,18 @@ export default function ThemeDropdown() {
|
|||||||
<ToggleIcon />
|
<ToggleIcon />
|
||||||
<span className="d-lg-none ms-2" id="bd-theme-text">Toggle theme</span>
|
<span className="d-lg-none ms-2" id="bd-theme-text">Toggle theme</span>
|
||||||
</Dropdown.Toggle>
|
</Dropdown.Toggle>
|
||||||
|
|
||||||
<Dropdown.Menu aria-labelledby="bd-theme-text">
|
<Dropdown.Menu aria-labelledby="bd-theme-text">
|
||||||
<Dropdown.Item
|
<Dropdown.Item as="button" className="d-flex align-items-center" active={isActive("light")} onClick={() => choose("light")}>
|
||||||
as="button"
|
|
||||||
className="d-flex align-items-center"
|
|
||||||
aria-pressed={isActive("light")}
|
|
||||||
active={isActive("light")}
|
|
||||||
onClick={() => handleSetTheme("light")}
|
|
||||||
>
|
|
||||||
<Icon.SunFill />
|
<Icon.SunFill />
|
||||||
<span className="ms-2">Light</span>
|
<span className="ms-2">Light</span>
|
||||||
{isActive("light") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
|
{isActive("light") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item as="button" className="d-flex align-items-center" active={isActive("dark")} onClick={() => choose("dark")}>
|
||||||
<Dropdown.Item
|
|
||||||
as="button"
|
|
||||||
className="d-flex align-items-center"
|
|
||||||
aria-pressed={isActive("dark")}
|
|
||||||
active={isActive("dark")}
|
|
||||||
onClick={() => handleSetTheme("dark")}
|
|
||||||
>
|
|
||||||
<Icon.MoonStarsFill />
|
<Icon.MoonStarsFill />
|
||||||
<span className="ms-2">Dark</span>
|
<span className="ms-2">Dark</span>
|
||||||
{isActive("dark") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
|
{isActive("dark") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item as="button" className="d-flex align-items-center" active={isActive("auto")} onClick={() => choose("auto")}>
|
||||||
<Dropdown.Item
|
|
||||||
as="button"
|
|
||||||
className="d-flex align-items-center"
|
|
||||||
aria-pressed={isActive("auto")}
|
|
||||||
active={isActive("auto")}
|
|
||||||
onClick={() => handleSetTheme("auto")}
|
|
||||||
>
|
|
||||||
<Icon.CircleHalf />
|
<Icon.CircleHalf />
|
||||||
<span className="ms-2">Auto</span>
|
<span className="ms-2">Auto</span>
|
||||||
{isActive("auto") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
|
{isActive("auto") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
|
||||||
|
|||||||
Reference in New Issue
Block a user