mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-13 13:05:51 +00:00
Refactor CatalogApp component to integrate CatalogFiltersMenu for improved filtering functionality, update layout structure in catalog-canvas-view for better readability, and enhance SASS styles for catalog header and filter menu. Remove unused functions and streamline canvas rendering logic in catalog-to-canvas.
This commit is contained in:
parent
81b316e08d
commit
9ddcce79c6
15 changed files with 2351 additions and 294 deletions
|
|
@ -2,6 +2,7 @@ import { createMemo, createSignal, For, lazy, onCleanup, onMount, Show, Suspense
|
|||
import { subjectToCanvas } from "../lib/catalog-to-canvas";
|
||||
import { watchSiteTheme } from "../lib/catalog-canvas-theme";
|
||||
import { CatalogChapterCard } from "./catalog-chapter-card";
|
||||
import { CatalogFiltersMenu } from "./catalog-filters-menu";
|
||||
import type {
|
||||
CatalogChapter,
|
||||
CatalogData,
|
||||
|
|
@ -151,7 +152,10 @@ export function CatalogApp() {
|
|||
});
|
||||
|
||||
return (
|
||||
<div class="catalog-shell">
|
||||
<div
|
||||
class="catalog-shell"
|
||||
classList={{ "catalog-shell--tree": viewMode() === "tree" }}
|
||||
>
|
||||
<aside class="catalog-sidebar" aria-label="Subjects">
|
||||
<div class="catalog-sidebar__header">
|
||||
<p class="catalog-sidebar__kicker">{data.title}</p>
|
||||
|
|
@ -175,11 +179,11 @@ export function CatalogApp() {
|
|||
|
||||
<div class="catalog-main">
|
||||
<header class="catalog-header">
|
||||
<div class="catalog-header__intro">
|
||||
<h1 class="catalog-header__title">{currentSubject().name}</h1>
|
||||
<p class="catalog-header__stats">{statsLabel()}</p>
|
||||
</div>
|
||||
<div class="catalog-header__controls">
|
||||
<div class="catalog-header__head">
|
||||
<div class="catalog-header__intro">
|
||||
<h1 class="catalog-header__title">{currentSubject().name}</h1>
|
||||
<p class="catalog-header__stats">{statsLabel()}</p>
|
||||
</div>
|
||||
<div class="catalog-view-toggle" role="tablist" aria-label="Chapter layout">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -200,31 +204,13 @@ export function CatalogApp() {
|
|||
Map
|
||||
</button>
|
||||
</div>
|
||||
<div class="catalog-filters-wrap">
|
||||
<p class="catalog-filters__label">Filter by curriculum</p>
|
||||
<div class="catalog-filters" role="group" aria-label="Curriculum filters">
|
||||
<button
|
||||
type="button"
|
||||
class="catalog-filter-btn"
|
||||
classList={{ "is-active": activeFilters().size === 0 }}
|
||||
onClick={() => toggleFilter("ALL")}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<For each={data.curriculums}>
|
||||
{(curriculum) => (
|
||||
<button
|
||||
type="button"
|
||||
class="catalog-filter-btn"
|
||||
classList={{ "is-active": activeFilters().has(curriculum) }}
|
||||
onClick={() => toggleFilter(curriculum)}
|
||||
>
|
||||
{curriculum}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="catalog-header__filters">
|
||||
<CatalogFiltersMenu
|
||||
curriculums={data.curriculums}
|
||||
activeFilters={activeFilters}
|
||||
onToggle={toggleFilter}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ import {
|
|||
} from "json-canvas-viewer";
|
||||
import type { CatalogChapter } from "../../../data/catalog.types";
|
||||
import { renderCatalogChapterCardElement } from "./catalog-chapter-card";
|
||||
import { chapterSlugFromNodeId, relayoutCanvasChapterHeights } from "../lib/catalog-to-canvas";
|
||||
import { measureRenderedMapChapterCards } from "../lib/catalog-chapter-card-layout";
|
||||
import { chapterSlugFromNodeId } from "../lib/catalog-to-canvas";
|
||||
import {
|
||||
syncViewerTheme,
|
||||
VIEWER_THEME_COLORS,
|
||||
siteCanvasTheme,
|
||||
watchSiteTheme,
|
||||
} from "../lib/catalog-canvas-theme";
|
||||
import { focusReadableCatalogView } from "../lib/catalog-canvas-viewport";
|
||||
|
||||
type CatalogCanvasViewProps = {
|
||||
canvas: JSONCanvas;
|
||||
|
|
@ -54,21 +54,6 @@ function bindPageScrollWheel(container: HTMLElement): () => void {
|
|||
return () => container.removeEventListener("wheel", onWheel, { capture: true });
|
||||
}
|
||||
|
||||
function waitForFrames(count = 2): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
let remaining = count;
|
||||
const step = () => {
|
||||
remaining -= 1;
|
||||
if (remaining <= 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(step);
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
});
|
||||
}
|
||||
|
||||
function buildNodeComponents(
|
||||
subjectId: string,
|
||||
chapters: Record<string, CatalogChapter>
|
||||
|
|
@ -115,20 +100,7 @@ export function CatalogCanvasView(props: CatalogCanvasViewProps) {
|
|||
lastSerialized = cacheKey;
|
||||
viewer.load({ canvas });
|
||||
syncViewerTheme(viewer);
|
||||
|
||||
await waitForFrames();
|
||||
if (generation !== loadGeneration || !viewer) return;
|
||||
|
||||
const measured = measureRenderedMapChapterCards(container);
|
||||
const fitted = relayoutCanvasChapterHeights(canvas, measured);
|
||||
if (!fitted) return;
|
||||
|
||||
const fittedKey = `${serializeCanvas(fitted)}|${chaptersKey(chapters)}`;
|
||||
if (fittedKey === lastSerialized) return;
|
||||
|
||||
lastSerialized = fittedKey;
|
||||
viewer.load({ canvas: fitted });
|
||||
syncViewerTheme(viewer);
|
||||
focusReadableCatalogView(viewer, container);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
|
|
|
|||
94
frontend/src/components/catalog-filters-menu.tsx
Normal file
94
frontend/src/components/catalog-filters-menu.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js";
|
||||
|
||||
type CatalogFiltersMenuProps = {
|
||||
curriculums: string[];
|
||||
activeFilters: () => Set<string>;
|
||||
onToggle: (curriculum: string) => void;
|
||||
};
|
||||
|
||||
function filtersSummary(active: Set<string>): string {
|
||||
if (active.size === 0) return "All";
|
||||
return Array.from(active).join(", ");
|
||||
}
|
||||
|
||||
export function CatalogFiltersMenu(props: CatalogFiltersMenuProps) {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
let root!: HTMLDivElement;
|
||||
|
||||
const summary = createMemo(() => filtersSummary(props.activeFilters()));
|
||||
|
||||
const close = () => setOpen(false);
|
||||
|
||||
onMount(() => {
|
||||
const onDocumentClick = (event: MouseEvent) => {
|
||||
if (!open() || !root.contains(event.target as Node)) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", onDocumentClick);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("click", onDocumentClick);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="catalog-filter-menu" ref={root}>
|
||||
<button
|
||||
type="button"
|
||||
class="catalog-filter-menu__trigger"
|
||||
aria-expanded={open() ? "true" : "false"}
|
||||
aria-haspopup="true"
|
||||
aria-controls="catalog-filter-menu-panel"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setOpen((isOpen) => !isOpen);
|
||||
}}
|
||||
>
|
||||
<span class="catalog-filter-menu__trigger-kicker">Filter by curriculum</span>
|
||||
<span class="catalog-filter-menu__trigger-value">{summary()}</span>
|
||||
<span class="catalog-filter-menu__chevron" aria-hidden="true" />
|
||||
</button>
|
||||
<Show when={open()}>
|
||||
<div
|
||||
id="catalog-filter-menu-panel"
|
||||
class="catalog-filter-menu__panel"
|
||||
role="group"
|
||||
aria-label="Curriculum filters"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<label class="catalog-filter-menu__option">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="catalog-filter-menu__checkbox"
|
||||
checked={props.activeFilters().size === 0}
|
||||
onChange={() => props.onToggle("ALL")}
|
||||
/>
|
||||
<span class="catalog-filter-menu__option-label">All</span>
|
||||
</label>
|
||||
<For each={props.curriculums}>
|
||||
{(curriculum) => (
|
||||
<label class="catalog-filter-menu__option">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="catalog-filter-menu__checkbox"
|
||||
checked={props.activeFilters().has(curriculum)}
|
||||
onChange={() => props.onToggle(curriculum)}
|
||||
/>
|
||||
<span class="catalog-filter-menu__option-label">{curriculum}</span>
|
||||
</label>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue