Attempt 1 to add a darkmode
This commit is contained in:
@@ -1,44 +1,38 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from 'next/link';
|
||||
import "./scss/nbn.scss";
|
||||
import "@/scss/nbn.scss";
|
||||
import NavbarClient from "@/components/Navbar";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Spectrum Next Registers",
|
||||
description: "A platform for exploring the Spectrum Next registers",
|
||||
robots: { index: true, follow: true },
|
||||
formatDetection: { email: false, address: false, telephone: false },
|
||||
title: "Spectrum Next Explorer",
|
||||
description: "A platform for exploring the Spectrum Next hardware",
|
||||
robots: { index: true, follow: true },
|
||||
formatDetection: { email: false, address: false, telephone: false },
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<nav className="navbar navbar-expand-lg navbar-dark bg-dark sticky-top">
|
||||
<div className="container-fluid">
|
||||
<Link className="navbar-brand" href="/">Next Explorer</Link>
|
||||
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div className="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" href="/">Home</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" href="/registers">Registers</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div className="container-fluid py-3">
|
||||
{children}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
const noFlashThemeScript = `
|
||||
(function() {
|
||||
try {
|
||||
var cookieMatch = document.cookie.match(/(?:^|; )theme=([^;]+)/);
|
||||
var stored = cookieMatch ? decodeURIComponent(cookieMatch[1]) : null;
|
||||
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
var effective = (stored === 'light' || stored === 'dark') ? stored : (prefersDark ? 'dark' : 'light');
|
||||
var el = document.documentElement;
|
||||
if (el.getAttribute('data-bs-theme') !== effective) {
|
||||
el.setAttribute('data-bs-theme', effective);
|
||||
}
|
||||
} catch(_) {}
|
||||
})();`;
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: noFlashThemeScript }} />
|
||||
</head>
|
||||
<body>
|
||||
<NavbarClient />
|
||||
<div className="container-fluid py-3">{children}</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import Link from 'next/link';
|
||||
import { Register } from '@/utils/register_parser';
|
||||
import RegisterDetail from '@/app/registers/RegisterDetail';
|
||||
import {Container, Row} from "react-bootstrap";
|
||||
import { getRegisters } from '@/app/services/register.service';
|
||||
import { getRegisters } from '@/services/register.service';
|
||||
|
||||
export default async function RegisterDetailPage({ params }: { params: { hex: string } }) {
|
||||
const registers = await getRegisters();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import RegisterBrowser from '@/app/registers/RegisterBrowser';
|
||||
import { getRegisters } from '@/app/services/register.service';
|
||||
import { getRegisters } from '@/services/register.service';
|
||||
|
||||
export default async function RegistersPage() {
|
||||
const registers = await getRegisters();
|
||||
|
||||
return (
|
||||
<div className="container-fluid py-4">
|
||||
<h1 className="mb-4">Spectrum Next Registers</h1>
|
||||
<h1 className="mb-4">NextReg Explorer</h1>
|
||||
<RegisterBrowser registers={registers} />
|
||||
</div>
|
||||
);
|
||||
|
||||
25
src/components/Navbar.tsx
Normal file
25
src/components/Navbar.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import * as Icon from "react-bootstrap-icons";
|
||||
import { Navbar, Nav, Container, Dropdown } from "react-bootstrap";
|
||||
import ThemeDropdown from "@/components/ThemeDropdown";
|
||||
|
||||
export default function NavbarClient() {
|
||||
return (
|
||||
<Navbar expand="lg" bg="primary" data-bs-theme="dark" sticky="top" className="navbar">
|
||||
<Container fluid>
|
||||
<Link className="navbar-brand" href="/">SpecNext Explorer</Link>
|
||||
<Navbar.Toggle aria-controls="navbarSupportedContent" />
|
||||
<Navbar.Collapse id="navbarSupportedContent">
|
||||
<Nav className="me-auto mb-2 mb-lg-0">
|
||||
<Link className="nav-link" href="/">Home</Link>
|
||||
<Link className="nav-link" href="/registers">Registers</Link>
|
||||
</Nav>
|
||||
|
||||
<ThemeDropdown />
|
||||
</Navbar.Collapse>
|
||||
</Container>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
138
src/components/ThemeDropdown.tsx
Normal file
138
src/components/ThemeDropdown.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState, useCallback } from "react";
|
||||
import * as Icon from "react-bootstrap-icons";
|
||||
import { Nav, Dropdown } from "react-bootstrap";
|
||||
|
||||
type Theme = "light" | "dark" | "auto";
|
||||
const COOKIE = "theme";
|
||||
|
||||
function getCookie(name: string): string | null {
|
||||
if (typeof document === "undefined") return null;
|
||||
const m = document.cookie.match(new RegExp("(^|; )" + name + "=([^;]+)"));
|
||||
return m ? decodeURIComponent(m[2]) : null;
|
||||
}
|
||||
function setCookie(name: string, value: string) {
|
||||
document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=31536000; SameSite=Lax`;
|
||||
}
|
||||
|
||||
// Use a single function to read current system preference (works on iOS)
|
||||
const prefersDark = () =>
|
||||
typeof window !== "undefined" &&
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
||||
export default function ThemeDropdown() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [theme, setThemeState] = useState<Theme>("auto");
|
||||
|
||||
const applyTheme = useCallback((t: Theme) => {
|
||||
const effective = t === "auto" ? (prefersDark() ? "dark" : "light") : t;
|
||||
document.documentElement.setAttribute("data-bs-theme", effective);
|
||||
}, []);
|
||||
|
||||
// Initial mount: read cookie and APPLY immediately (important for iOS)
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const v = getCookie(COOKIE);
|
||||
const initial: Theme = v === "light" || v === "dark" || v === "auto" ? (v as Theme) : "auto";
|
||||
setThemeState(initial);
|
||||
applyTheme(initial); // ensure render matches auto right away
|
||||
}, [applyTheme]);
|
||||
|
||||
// Follow system changes while in auto; include iOS visibility/page-show events
|
||||
useEffect(() => {
|
||||
if (!mounted || theme !== "auto") return;
|
||||
|
||||
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const onChange = () => applyTheme("auto");
|
||||
|
||||
// Safari <14 uses addListener/removeListener
|
||||
if (typeof mql.addEventListener === "function") {
|
||||
mql.addEventListener("change", onChange);
|
||||
} else if (typeof mql.addListener === "function") {
|
||||
mql.addListener(onChange);
|
||||
}
|
||||
|
||||
const onVisibility = () => applyTheme("auto");
|
||||
const onPageShow = () => applyTheme("auto");
|
||||
|
||||
document.addEventListener("visibilitychange", onVisibility);
|
||||
window.addEventListener("pageshow", onPageShow);
|
||||
|
||||
return () => {
|
||||
if (typeof mql.removeEventListener === "function") {
|
||||
mql.removeEventListener("change", onChange);
|
||||
} else if (typeof mql.removeListener === "function") {
|
||||
mql.removeListener(onChange);
|
||||
}
|
||||
document.removeEventListener("visibilitychange", onVisibility);
|
||||
window.removeEventListener("pageshow", onPageShow);
|
||||
};
|
||||
}, [mounted, theme, applyTheme]);
|
||||
|
||||
const handleSetTheme = (t: Theme) => {
|
||||
setCookie(COOKIE, t);
|
||||
setThemeState(t);
|
||||
applyTheme(t);
|
||||
};
|
||||
|
||||
const isActive = (t: Theme) => theme === t;
|
||||
|
||||
const ToggleIcon = !mounted
|
||||
? Icon.CircleHalf
|
||||
: theme === "dark"
|
||||
? Icon.MoonStarsFill
|
||||
: theme === "light"
|
||||
? Icon.SunFill
|
||||
: Icon.CircleHalf;
|
||||
|
||||
return (
|
||||
<Nav className="ms-md-auto">
|
||||
<Dropdown as={Nav.Item} align="end">
|
||||
<Dropdown.Toggle as={Nav.Link} id="bd-theme" className="px-0 px-lg-2 py-2 d-flex align-items-center">
|
||||
<ToggleIcon />
|
||||
<span className="d-lg-none ms-2" id="bd-theme-text">Toggle theme</span>
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu aria-labelledby="bd-theme-text">
|
||||
<Dropdown.Item
|
||||
as="button"
|
||||
className="d-flex align-items-center"
|
||||
aria-pressed={isActive("light")}
|
||||
active={isActive("light")}
|
||||
onClick={() => handleSetTheme("light")}
|
||||
>
|
||||
<Icon.SunFill />
|
||||
<span className="ms-2">Light</span>
|
||||
{isActive("light") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
|
||||
</Dropdown.Item>
|
||||
|
||||
<Dropdown.Item
|
||||
as="button"
|
||||
className="d-flex align-items-center"
|
||||
aria-pressed={isActive("dark")}
|
||||
active={isActive("dark")}
|
||||
onClick={() => handleSetTheme("dark")}
|
||||
>
|
||||
<Icon.MoonStarsFill />
|
||||
<span className="ms-2">Dark</span>
|
||||
{isActive("dark") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
|
||||
</Dropdown.Item>
|
||||
|
||||
<Dropdown.Item
|
||||
as="button"
|
||||
className="d-flex align-items-center"
|
||||
aria-pressed={isActive("auto")}
|
||||
active={isActive("auto")}
|
||||
onClick={() => handleSetTheme("auto")}
|
||||
>
|
||||
<Icon.CircleHalf />
|
||||
<span className="ms-2">Auto</span>
|
||||
{isActive("auto") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</Nav>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
@import "variables";
|
||||
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@import "../../node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
@import "bootswatch";
|
||||
|
||||
Reference in New Issue
Block a user