Files
fofos-admin/web/sections.ts
T
pastilhasandClaude Opus 5 15042ccc76 a gallery is a carousel
It always rendered as one — data-carousel, a snap track, two arrows, driven by
carousel.client.js. The entity was the last thing still calling it something
else, and that collided with the site's own Gallery section: two nav rows
reading Gallery and Galleries, meaning a list of objects and a list of
photograph sets.

GalleriesPanel, GalleryPicker and GalleryItems become Carousels*, the endpoint
becomes /fofos-admin/carousels, the block kind becomes carousel and
subject-gallery becomes subject-carousel. The publish allowlist follows the
renamed tables.

What did NOT move: GALLERY_CONFIG at /fofos-admin/gallery, subjects.ts's
base '/gallery', and home_gallery_media. Those are the Gallery section and the
legacy rows, and both are correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 18:01:15 +01:00

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 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: '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';