Four bands in the order the page shows them, which is the sitemap's shape exactly: a HEADER of a photograph and a phrase, then ABOUT US, then PRODUTO with a video beside its text, then MANIFESTO. No schema change and no API change — `about_content` already carries every one of those columns and `aboutRouter` already reads and writes them. This is the screen that was missing, not the data. One save per band, the rule Home follows and for the same reason: `take()` treats an absent key as "leave it alone", so a single Save carrying every field would let a half-finished manifesto ride along with a photograph somebody meant to change. Both media slots go through MediaPicker rather than a text field. A path somebody types is a path that is wrong by one character and renders as a broken image. One picker serves both, with `picking` naming which slot asked — two would be two copies of a modal that already knows how to browse originals/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
255 lines
8.8 KiB
TypeScript
255 lines
8.8 KiB
TypeScript
import type { LucideIcon } from 'lucide-react';
|
|
import { Home, Info, Layers, Images, Handshake, Sparkles, Wrench, Settings, Mail, Cog, FolderOpen } from 'lucide-react';
|
|
|
|
/**
|
|
* The sections of the fofos admin, in nav order.
|
|
*
|
|
* ── The order is the client's site, not the database's ──
|
|
*
|
|
* It follows the sitemap: the pages someone edits most, first. Site settings, Messages and
|
|
* Administration sit at the bottom because they are not pages — the site's settings, an inbox, and the
|
|
* plugin's own machinery.
|
|
*
|
|
* ── `site` and `administration` are different things ──
|
|
*
|
|
* "Site settings" is the CLIENT's — partners, social links, contact details, all of it public copy that
|
|
* whoever edits the site edits. "Administration" is OFFICER's end: where fofos is, how to reach it, and
|
|
* whatever else this plugin needs to know to do its job. One is content, the other is plumbing, and the
|
|
* people who touch them are not the same people. Nothing in Administration 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'
|
|
| 'site'
|
|
| 'about'
|
|
| 'home'
|
|
| 'gallery'
|
|
| 'drops'
|
|
| 'collabs'
|
|
| 'collections'
|
|
| 'services';
|
|
|
|
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[] = [
|
|
{
|
|
key: 'home',
|
|
label: 'Home',
|
|
icon: Home,
|
|
hint: 'The hero, the intro, and the gallery',
|
|
path: '/fofos-admin/home',
|
|
built: true,
|
|
requires: 'connection',
|
|
},
|
|
{
|
|
key: 'about',
|
|
label: 'About Us',
|
|
icon: Info,
|
|
hint: 'The header photograph and the copy',
|
|
path: '/fofos-admin/about',
|
|
built: true,
|
|
requires: 'connection',
|
|
},
|
|
{
|
|
key: 'collections',
|
|
label: 'Collections',
|
|
icon: Layers,
|
|
hint: 'Collections, and the items in them',
|
|
path: '/fofos-admin/collections',
|
|
built: false,
|
|
requires: 'connection',
|
|
},
|
|
{
|
|
key: 'gallery',
|
|
label: 'Gallery',
|
|
icon: Images,
|
|
hint: 'Standalone pieces',
|
|
path: '/fofos-admin/gallery',
|
|
built: true,
|
|
requires: 'connection',
|
|
},
|
|
{
|
|
key: 'collabs',
|
|
label: 'Collabs',
|
|
icon: Handshake,
|
|
hint: 'Projects with other people',
|
|
path: '/fofos-admin/collabs',
|
|
built: true,
|
|
requires: 'connection',
|
|
},
|
|
{
|
|
key: 'drops',
|
|
label: 'Drops',
|
|
icon: Sparkles,
|
|
hint: 'Releases',
|
|
path: '/fofos-admin/drops',
|
|
built: true,
|
|
requires: 'connection',
|
|
},
|
|
{
|
|
key: 'services',
|
|
label: 'Services',
|
|
icon: Wrench,
|
|
hint: 'A fixed tree — the words, not the shape',
|
|
path: '/fofos-admin/services',
|
|
built: false,
|
|
requires: 'connection',
|
|
},
|
|
{
|
|
key: 'site',
|
|
label: 'Site settings',
|
|
icon: Settings,
|
|
hint: 'Partners, social links, contact details',
|
|
path: '/fofos-admin/site',
|
|
built: false,
|
|
requires: 'connection',
|
|
},
|
|
{
|
|
key: 'messages',
|
|
label: 'Messages',
|
|
icon: Mail,
|
|
hint: 'What people sent through the contact form',
|
|
path: '/fofos-admin/messages',
|
|
built: true,
|
|
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 = 'messages';
|
|
|
|
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';
|