mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-13 04:55:51 +00:00
Update zola.toml to include site title, description, theme, and additional URLs for navigation
This commit is contained in:
parent
5548bfce90
commit
ab87bc2bd1
59 changed files with 4068 additions and 1 deletions
25
frontend/src/components/marquee.tsx
Normal file
25
frontend/src/components/marquee.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
const MARQUEE_TEXT =
|
||||
"Every human being deserves free high quality learning materials — Man Lung Yeung";
|
||||
|
||||
export function Marquee() {
|
||||
const segment = () => (
|
||||
<span class="marquee-text">
|
||||
{MARQUEE_TEXT}
|
||||
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="marquee-shell" role="region" aria-label="Site quote">
|
||||
<div class="marquee-line" />
|
||||
<div class="marquee" aria-hidden="true">
|
||||
<div class="marquee-track">
|
||||
{segment()}
|
||||
{segment()}
|
||||
{segment()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="marquee-line" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
frontend/src/components/scroll-reveal.tsx
Normal file
35
frontend/src/components/scroll-reveal.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { onCleanup, onMount } from "solid-js";
|
||||
|
||||
export function ScrollRevealBootstrap() {
|
||||
onMount(() => {
|
||||
const cards = document.querySelectorAll<HTMLElement>("[data-scroll-reveal-card]");
|
||||
if (!cards.length) return;
|
||||
|
||||
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
if (reducedMotion) {
|
||||
cards.forEach((card) => card.classList.add("is-inview"));
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries, obs) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("is-inview");
|
||||
obs.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.14 }
|
||||
);
|
||||
|
||||
cards.forEach((card, index) => {
|
||||
card.style.transitionDelay = `${index * 80}ms`;
|
||||
observer.observe(card);
|
||||
});
|
||||
|
||||
onCleanup(() => observer.disconnect());
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
94
frontend/src/components/site-header.tsx
Normal file
94
frontend/src/components/site-header.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { createSignal, onCleanup, onMount, For } from "solid-js";
|
||||
import { ThemeToggle } from "./theme-toggle";
|
||||
|
||||
type NavLink = {
|
||||
label: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
type SiteHeaderProps = {
|
||||
brand: string;
|
||||
homeUrl: string;
|
||||
links: NavLink[];
|
||||
};
|
||||
|
||||
export function SiteHeader(props: SiteHeaderProps) {
|
||||
const [scrolled, setScrolled] = createSignal(false);
|
||||
const [navOpen, setNavOpen] = createSignal(false);
|
||||
|
||||
onMount(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 24);
|
||||
onScroll();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
const onResize = () => {
|
||||
if (window.innerWidth > 467 && navOpen()) {
|
||||
setNavOpen(false);
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
};
|
||||
window.addEventListener("resize", onResize, { passive: true });
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
window.removeEventListener("resize", onResize);
|
||||
document.body.style.overflow = "";
|
||||
});
|
||||
});
|
||||
|
||||
const setOpen = (open: boolean) => {
|
||||
setNavOpen(open);
|
||||
document.body.style.overflow = open ? "hidden" : "";
|
||||
};
|
||||
|
||||
const headerClass = () => {
|
||||
const classes = ["site-header"];
|
||||
if (scrolled()) classes.push("is-scrolled");
|
||||
if (navOpen()) classes.push("nav-is-open");
|
||||
return classes.join(" ");
|
||||
};
|
||||
|
||||
return (
|
||||
<header class={headerClass()}>
|
||||
<div class="container header-row">
|
||||
<div class="brand">
|
||||
<a class="brand-name" href={props.homeUrl}>
|
||||
{props.brand}
|
||||
</a>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<nav class="main-nav" aria-label="Primary navigation">
|
||||
<For each={props.links}>{(link) => <a href={link.href}>{link.label}</a>}</For>
|
||||
</nav>
|
||||
<ThemeToggle />
|
||||
<button
|
||||
type="button"
|
||||
class="nav-burger"
|
||||
aria-controls="mobile-nav"
|
||||
aria-expanded={navOpen() ? "true" : "false"}
|
||||
aria-label={navOpen() ? "Close menu" : "Open menu"}
|
||||
onClick={() => setOpen(!navOpen())}
|
||||
>
|
||||
<span class="nav-burger-line" />
|
||||
<span class="nav-burger-line" />
|
||||
<span class="nav-burger-line" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="nav-backdrop"
|
||||
aria-hidden={navOpen() ? "false" : "true"}
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
<nav id="mobile-nav" class="mobile-nav-panel" aria-label="Mobile navigation">
|
||||
<For each={props.links}>
|
||||
{(link) => (
|
||||
<a href={link.href} onClick={() => setOpen(false)}>
|
||||
{link.label}
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
30
frontend/src/components/theme-toggle.tsx
Normal file
30
frontend/src/components/theme-toggle.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { createSignal, onMount, Show } from "solid-js";
|
||||
import { resolveTheme, setTheme, type ThemeChoice } from "../theme";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const [theme, setThemeState] = createSignal<ThemeChoice>("light");
|
||||
|
||||
onMount(() => {
|
||||
setThemeState(resolveTheme());
|
||||
});
|
||||
|
||||
const toggle = () => {
|
||||
const next: ThemeChoice = theme() === "dark" ? "light" : "dark";
|
||||
setTheme(next);
|
||||
setThemeState(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
onClick={toggle}
|
||||
aria-label={theme() === "dark" ? "Switch to light theme" : "Switch to dark theme"}
|
||||
title={theme() === "dark" ? "Light mode" : "Dark mode"}
|
||||
>
|
||||
<Show when={theme() === "dark"} fallback={<span aria-hidden="true">☾</span>}>
|
||||
<span aria-hidden="true">☀</span>
|
||||
</Show>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
59
frontend/src/main.tsx
Normal file
59
frontend/src/main.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { render } from "solid-js/web";
|
||||
import { SiteHeader } from "./components/site-header";
|
||||
import { Marquee } from "./components/marquee";
|
||||
import { ScrollRevealBootstrap } from "./components/scroll-reveal";
|
||||
import { initTheme } from "./theme";
|
||||
|
||||
function readConfig() {
|
||||
const el = document.getElementById("site-config");
|
||||
if (!el?.textContent) {
|
||||
return {
|
||||
brand: "Open Free Books",
|
||||
homeUrl: "/",
|
||||
browseUrl: "/browse/",
|
||||
aboutUrl: "/about/",
|
||||
githubUrl: "https://github.com/openfreebooks/openfreebooks",
|
||||
};
|
||||
}
|
||||
return JSON.parse(el.textContent) as {
|
||||
brand: string;
|
||||
homeUrl: string;
|
||||
browseUrl: string;
|
||||
aboutUrl: string;
|
||||
githubUrl: string;
|
||||
};
|
||||
}
|
||||
|
||||
function mountFooterYear() {
|
||||
const yearEl = document.getElementById("footer-year");
|
||||
if (yearEl) {
|
||||
yearEl.textContent = String(new Date().getFullYear());
|
||||
}
|
||||
}
|
||||
|
||||
initTheme();
|
||||
|
||||
const config = readConfig();
|
||||
const navLinks = [
|
||||
{ label: "Browse", href: config.browseUrl },
|
||||
{ label: "About", href: config.aboutUrl },
|
||||
{ label: "GitHub", href: config.githubUrl },
|
||||
];
|
||||
|
||||
const headerRoot = document.getElementById("site-header");
|
||||
if (headerRoot) {
|
||||
render(
|
||||
() => (
|
||||
<SiteHeader brand={config.brand} homeUrl={config.homeUrl} links={navLinks} />
|
||||
),
|
||||
headerRoot
|
||||
);
|
||||
}
|
||||
|
||||
const marqueeRoot = document.getElementById("marquee");
|
||||
if (marqueeRoot) {
|
||||
render(() => <Marquee />, marqueeRoot);
|
||||
}
|
||||
|
||||
render(() => <ScrollRevealBootstrap />, document.body);
|
||||
mountFooterYear();
|
||||
32
frontend/src/theme.ts
Normal file
32
frontend/src/theme.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
const STORAGE_KEY = "openfreebooks-theme";
|
||||
|
||||
export type ThemeChoice = "light" | "dark";
|
||||
|
||||
export function getSystemTheme(): ThemeChoice {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
export function getStoredTheme(): ThemeChoice | null {
|
||||
const value = localStorage.getItem(STORAGE_KEY);
|
||||
if (value === "light" || value === "dark") {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveTheme(): ThemeChoice {
|
||||
return getStoredTheme() ?? getSystemTheme();
|
||||
}
|
||||
|
||||
export function applyTheme(theme: ThemeChoice) {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
}
|
||||
|
||||
export function setTheme(theme: ThemeChoice) {
|
||||
localStorage.setItem(STORAGE_KEY, theme);
|
||||
applyTheme(theme);
|
||||
}
|
||||
|
||||
export function initTheme() {
|
||||
applyTheme(resolveTheme());
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue