mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-13 13:05: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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue