Two of these were live: PageBuilderPanel still compared kind === 'gallery' in two places, so a carousel block's editor and its Choose button were both dead branches. The type checker could not see them — both compare against string. The rest is copy somebody reads: "Change gallery", "Name a new gallery", "Reading the gallery", "Add to this gallery", Home's Gallery heading, and the "Create Gallery" file action, now "Carousel from folder". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
257 lines
10 KiB
TypeScript
257 lines
10 KiB
TypeScript
import type { LucideIcon } from 'lucide-react';
|
|
import { Boxes, Cog, Files, FolderOpen, GalleryHorizontal, Images, Mail, PanelsTopLeft, Shapes } 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'
|
|
| 'collections'
|
|
| 'pieces'
|
|
| 'carousels'
|
|
| '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;
|
|
};
|
|
|
|
/*
|
|
Layout had three rows under it — Top Menu, Content, Footer — and they are gone.
|
|
|
|
They were a hierarchy the screen did not have: all three are edited in the same panel, by the same
|
|
controls, at the same time. Clicking one changed the URL and nothing else, which teaches that the
|
|
nav does not mean anything. Pages keeps its children because a page IS a destination.
|
|
*/
|
|
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 — what replaced the seven per-page sections.
|
|
|
|
The nav draws its rows UNDER this one, so the site's pages are visible without opening it. That is
|
|
the one place this admin has a hierarchy, and it earns it: a page is a thing you go to, and the
|
|
list of them is the site.
|
|
*/
|
|
key: 'pages',
|
|
label: 'Pages',
|
|
icon: Files,
|
|
hint: 'Every page: what it is called, and where it lives',
|
|
path: '/fofos-admin/pages',
|
|
built: true,
|
|
requires: 'connection',
|
|
},
|
|
{
|
|
/*
|
|
Carousels — 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 carousel 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 carousel
|
|
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: 'collections',
|
|
label: 'Collections',
|
|
icon: Boxes,
|
|
hint: 'A folder of photographs, and what is said about it',
|
|
path: '/fofos-admin/collections',
|
|
built: true,
|
|
/*
|
|
Same reason as Carousels: a collection is a ROW read through the site's admin API, so this is
|
|
useless without the development server answering. What is different is that the row is only half
|
|
of it — which collections EXIST is a question about `originals/COLLECTIONS/`, and the folders are
|
|
registered as this list is read.
|
|
*/
|
|
requires: 'connection',
|
|
pinned: true,
|
|
},
|
|
{
|
|
/*
|
|
The Gallery's pieces.
|
|
|
|
Called Pieces, not Gallery. The section on the site is Gallery and its items are peças, and there
|
|
is a Carousels section here meaning something else entirely — a named, ordered set of media. The
|
|
two were called Gallery and Galleries once, meaning a list of objects and a list of photograph
|
|
sets, and that is what the rename to Carousels was for.
|
|
|
|
Below Collections because it is the same object one level flatter, and it shares its panel — see
|
|
sections/subjects.ts.
|
|
*/
|
|
key: 'pieces',
|
|
label: 'Pieces',
|
|
icon: Shapes,
|
|
hint: 'One object each, shown in the Gallery',
|
|
path: '/fofos-admin/pieces',
|
|
built: true,
|
|
// A piece is a ROW read through the site's admin API, so this is useless without the development
|
|
// server answering. Same as Collections above it.
|
|
requires: 'connection',
|
|
pinned: true,
|
|
},
|
|
{
|
|
key: 'carousels',
|
|
label: 'Carousels',
|
|
icon: GalleryHorizontal,
|
|
hint: 'Named sets of media, reusable across the site',
|
|
// `/fofos-admin/carousels` — it reads the site's own admin API, unlike Media beside it.
|
|
path: '/fofos-admin/carousels',
|
|
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';
|