The admin was a section each for Home, About Us, Collections, Gallery, Collabs, Drops and Services, because the site's pages were a constant in site-map.ts and a developer was the only one who could add one. A page builder is going to replace that, so the nav stops enumerating pages. What is left is the two things a page is NOT: Layout — the shell every page renders inside — and Pages, listed but not built. Layout is first because it is first in the work: building pages before the frame they sit in means building them twice. The panel is a sketch of the page with the controls under it. Menu across the top, a body, the footer at the bottom, in the chosen colours and typefaces — the same principle the header editor already follows, where the admin renders the structure as the page will show it. It is honest about being a sketch: drawing the logo and the rules would be a second implementation of the layout, and the two would drift the first time either changed. Fonts are a mapping, not free text. styles.css already declares four ROLES — body, sub, title, display — and every template asks for a role rather than a typeface, so choosing a font is choosing which of the four faces the build subsets plays each part. The list travels with the payload rather than being copied here, because a picker offering a face the build never subsets renders as a fallback nobody chose. The per-page panels are NOT deleted. AboutPanel, HomePanel and RecordsPanel are still in the tree and still work; they are unreachable, which is a line in sections.ts rather than a rewrite. The page builder is not finished, and removing the only way to edit the live site's content before its replacement exists would leave a gap with nothing in it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
203 lines
8.5 KiB
TypeScript
203 lines
8.5 KiB
TypeScript
import type { LucideIcon } from 'lucide-react';
|
|
import { Images, Mail, Cog, FolderOpen, PanelsTopLeft, Files } from 'lucide-react';
|
|
|
|
/**
|
|
* The sections of the fofos admin, in nav order.
|
|
*
|
|
* ── There is no longer one section per page ──
|
|
*
|
|
* There was: Home, About Us, Collections, Gallery, Collabs, Drops, Services — a section each, because
|
|
* the site's pages were a constant in `site-map.ts` and a developer was the only one who could add
|
|
* one. Every new page meant a new entry here, a new panel, and a new API route.
|
|
*
|
|
* That model is being replaced by a page builder, so the nav stops enumerating pages. What is left is
|
|
* the two things a page is not:
|
|
*
|
|
* Layout the shell every page renders inside — the menu, the footer, the colours, the typefaces
|
|
* Pages the pages themselves, built rather than hard-coded
|
|
*
|
|
* The per-page panels have NOT been deleted. `AboutPanel`, `HomePanel` and `RecordsPanel` are still in
|
|
* the tree and still work; they are simply not reachable, which is a line in this file rather than a
|
|
* rewrite. That is deliberate — the page builder is not finished, and deleting the only way to edit
|
|
* the live site's content before its replacement exists would be a gap with nothing in it.
|
|
*
|
|
* ── `administration` ──
|
|
*
|
|
* OFFICER's end: where fofos is, how to reach it, and whatever else this plugin needs to do its job.
|
|
* Nothing in it ever appears on the site.
|
|
*
|
|
* ── `built` ──
|
|
*
|
|
* The panels arrive one section at a time, each verified against the live site before the next is
|
|
* started. A section that is not built yet says so, plainly, with what it will do — which is a better
|
|
* answer than a nav that hides most of itself and gives no sense of what is coming.
|
|
*
|
|
* It mirrors `built` in fofos's own `src/apps/web/site-map.ts`, and for the same reason: one list, so
|
|
* the nav and what actually renders cannot disagree.
|
|
*/
|
|
|
|
export type SectionKey = 'administration' | 'galleries' | 'media' | 'messages' | 'layout' | 'pages';
|
|
|
|
export type Section = {
|
|
key: SectionKey;
|
|
label: string;
|
|
icon: LucideIcon;
|
|
/** One line under the label in the nav — what this section is, for somebody who has not opened it. */
|
|
hint: string;
|
|
/** The fofos path this reads, relative to the plugin's mount. Null when nothing is built. */
|
|
path: string | null;
|
|
built: boolean;
|
|
/**
|
|
* What has to be true before this section can be opened.
|
|
*
|
|
* 'connection' the fofos API must be reachable — every section that READS the site
|
|
* 'root' only the root location must be set — the file browser needs nowhere to talk to
|
|
* 'nothing' always open, because it is where you go to satisfy the other two
|
|
*
|
|
* This exists because the sections do not all depend on the same thing, and treating them as if they
|
|
* did was wrong in a way that showed: Media needs a folder on this machine and nothing else, and was
|
|
* locked behind a connection it never uses.
|
|
*/
|
|
requires: 'connection' | 'root' | 'nothing';
|
|
/**
|
|
* Pinned to the FOOT of the nav rather than sitting in the list.
|
|
*
|
|
* A flag rather than the nav hard-coding a key, because "which sections are the site's and which are
|
|
* the plugin's own" is a fact about the section. It is also why this stays in one array: `sectionFor`
|
|
* and `isSectionKey` have to resolve every key, and a second exported list would be a second place to
|
|
* forget one.
|
|
*/
|
|
pinned?: boolean;
|
|
};
|
|
|
|
export const SECTIONS: Section[] = [
|
|
{
|
|
/*
|
|
The shell every page renders inside, and therefore the first thing to decide.
|
|
|
|
First in the nav because it is first in the work: the menu, the footer, the colours and the
|
|
typefaces are the frame every page is built into, and building pages before the frame exists
|
|
means building them twice.
|
|
*/
|
|
key: 'layout',
|
|
label: 'Layout',
|
|
icon: PanelsTopLeft,
|
|
hint: 'The menu, the footer, the colours and the typefaces',
|
|
path: '/fofos-admin/layout',
|
|
built: true,
|
|
requires: 'connection',
|
|
},
|
|
{
|
|
/*
|
|
The pages, built rather than hard-coded. Not started.
|
|
|
|
This is what replaced the seven per-page sections that used to be here. Listed while unbuilt on
|
|
purpose: the nav says what is coming, which is a better answer than a nav that quietly lost seven
|
|
entries and offers no account of where they went.
|
|
*/
|
|
key: 'pages',
|
|
label: 'Pages',
|
|
icon: Files,
|
|
hint: 'Build a page from blocks, and choose where it sits',
|
|
path: null,
|
|
built: false,
|
|
requires: 'connection',
|
|
},
|
|
{
|
|
/*
|
|
Galleries — an entity of their own, not a list hanging off a page.
|
|
|
|
Above Media and pinned with it, because the two are the same kind of thing: the raw material the
|
|
site is built from, held on this machine. Media is the files; a gallery is a set of them somebody
|
|
has chosen and put in an order, which is a decision worth keeping once rather than remaking on
|
|
every page that wants it.
|
|
|
|
`requires: 'connection'`, unlike Media beside it, and the difference is real rather than a
|
|
copy-paste. Media is a file browser over a folder on this disk and needs nothing else. A gallery
|
|
is a ROW in the site's database — reading or writing one goes through the site's admin API, so
|
|
this section is useless without the development server answering, and opening it there would be
|
|
a screen where every action fails.
|
|
*/
|
|
key: 'galleries',
|
|
label: 'Galleries',
|
|
icon: Images,
|
|
hint: 'Named sets of media, reusable across the site',
|
|
// `/fofos-admin/galleries` — it reads the site's own admin API, unlike Media beside it.
|
|
path: '/fofos-admin/galleries',
|
|
built: true,
|
|
requires: 'connection',
|
|
pinned: true,
|
|
},
|
|
{
|
|
/*
|
|
The site's own media, browsed in place.
|
|
|
|
Above Administration and below the pages, because it is neither: the pages are the site's CONTENT
|
|
and Administration is this plugin's machinery, while this is the raw material both draw on. It is
|
|
also the section somebody uses most often once the site exists, which is the other reason it sits
|
|
where a hand falls rather than at the bottom.
|
|
|
|
`path: null` — it reads no fofos API. It embeds the platform's own file browser, confined to the
|
|
originals folder under the root location.
|
|
*/
|
|
key: 'media',
|
|
label: 'Media',
|
|
icon: FolderOpen,
|
|
hint: 'Photos and video the site is built from',
|
|
path: null,
|
|
built: true,
|
|
// A folder on this machine. It never speaks to fofos, so a connection is not its business.
|
|
requires: 'root',
|
|
// Pinned beside Administration: both are about this machine rather than about the site's pages.
|
|
pinned: true,
|
|
},
|
|
{
|
|
/*
|
|
Last, and `path: null` — it reads nothing from fofos. Every other section is a window onto the
|
|
client's site; this one is the window onto the connection itself, which is why it keeps working
|
|
when fofos is unreachable and the others do not.
|
|
*/
|
|
key: 'administration',
|
|
label: 'Administration',
|
|
icon: Cog,
|
|
hint: 'Where fofos is, and the token to reach it',
|
|
path: null,
|
|
built: true,
|
|
// Always reachable. It is where the other two requirements are satisfied, so locking it would be
|
|
// locking somebody out of the only door.
|
|
requires: 'nothing',
|
|
pinned: true,
|
|
},
|
|
];
|
|
|
|
/** Where the detail panel goes when the URL says nothing, or says something it does not recognise. */
|
|
export const DEFAULT_SECTION: SectionKey = 'layout';
|
|
|
|
export const isSectionKey = (value: string | null): value is SectionKey =>
|
|
SECTIONS.some((section) => section.key === value);
|
|
|
|
export const sectionFor = (key: SectionKey): Section =>
|
|
SECTIONS.find((section) => section.key === key) ?? SECTIONS[SECTIONS.length - 1]!;
|
|
|
|
/**
|
|
* Can this section be opened, given what is currently true?
|
|
*
|
|
* One function, used by the nav to decide whether to offer a link and by the detail panel to decide
|
|
* what to render — so the two cannot disagree about a section. That was already the rule when there was
|
|
* one global flag; it matters more now that each section asks its own question.
|
|
*/
|
|
export const sectionOpen = (section: Section, state: { ready: boolean; rootSet: boolean }): boolean => {
|
|
switch (section.requires) {
|
|
case 'nothing':
|
|
return true;
|
|
case 'root':
|
|
return state.rootSet;
|
|
case 'connection':
|
|
return state.ready;
|
|
}
|
|
};
|
|
|
|
/** Why it is locked, in the fewest words that are actually useful. */
|
|
export const sectionBlockedBy = (section: Section): string =>
|
|
section.requires === 'root' ? 'needs a root location' : 'needs a connection to fofos';
|