78 lines
2.7 KiB
TypeScript
78 lines
2.7 KiB
TypeScript
import { useState } from "react";
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
|
SECTION MANIFEST
|
|
|
|
A page is a list of sections, and a section is a view over data
|
|
the database already holds, narrowed by a filter. The map of
|
|
chapters and the vertical list of regions are the same rows seen
|
|
two ways; the three bands of retreats are one view seen three
|
|
times with a different filter each.
|
|
|
|
So a page declares what it wants and this turns it into what
|
|
PageShell takes. Every entry looks like:
|
|
|
|
{
|
|
id, title, blurb, accent, background, // the heading
|
|
Component, // the view
|
|
props: { ... }, // the filter
|
|
views: { // optional toggle
|
|
options: ["carousel", "grid"],
|
|
default: "carousel",
|
|
Toggle: EventCardsToggle,
|
|
},
|
|
}
|
|
|
|
Sections fetch their own data. The page never does, which is why
|
|
there's no loading or error state here — each view handles its
|
|
own, and one section failing doesn't blank the page.
|
|
|
|
Every Component receives `accent` so its empty and error notices
|
|
match the heading above them, and `view` when the entry declares
|
|
a toggle. Both are ignored harmlessly by a section that doesn't
|
|
want them.
|
|
═══════════════════════════════════════════════════════════════ */
|
|
|
|
export function useSectionManifest(manifest) {
|
|
// One entry per section that has a toggle, seeded from its
|
|
// declared default so the control is right before anything loads.
|
|
const [views, setViews] = useState(() =>
|
|
Object.fromEntries(
|
|
manifest
|
|
.filter(entry => entry.views)
|
|
.map(entry => [
|
|
entry.id,
|
|
entry.views.default ?? entry.views.options?.[0] ?? null,
|
|
]),
|
|
),
|
|
);
|
|
|
|
const setView = (id, value) => setViews(prev => ({ ...prev, [id]: value }));
|
|
|
|
return manifest.map(entry => {
|
|
const { Component, props, views: spec, ...heading } = entry;
|
|
const view = views[entry.id];
|
|
const Toggle = spec?.Toggle;
|
|
|
|
return {
|
|
...heading,
|
|
|
|
actions: Toggle ? (
|
|
<Toggle
|
|
view={view}
|
|
setView={value => setView(entry.id, value)}
|
|
accent={entry.accent}
|
|
options={spec.options}
|
|
/>
|
|
) : undefined,
|
|
|
|
content: (
|
|
<Component
|
|
{...(props ?? {})}
|
|
accent={entry.accent}
|
|
{...(spec ? { view } : {})}
|
|
/>
|
|
),
|
|
};
|
|
});
|
|
}
|