The section reads the site's own admin API, so it requires a CONNECTION where Media beside it requires only a root. The difference is real rather than copy-paste: Media is a file browser over a folder on this disk, while a gallery is a row in the site's database — opening it without the site answering would be a screen where every action fails. The context action hands its selection over in the URL and sends you to the panel rather than opening a dialog of its own. Naming a gallery, seeing the slug it will get and watching the items land belongs on the screen that owns galleries, not in a modal over a file browser that would then have to explain where the result went. It offers itself only inside originals/, because a gallery item is a path relative to that folder and an entry that appears everywhere while working in one place is worse than one that appears where it works. The test is on the PATH, not on which panel is open — the same browser is mounted in several places and only the path says where it is pointed. A mixed selection is refused. A folder among files is either a mistake or a request to recurse, and neither is something to guess from a right-click. The handoff is consumed once and stripped from the URL. Left there, a refresh would create the gallery again, which somebody discovers as four copies. Nothing is written until Create: the folder's name is a suggestion in the field, not a decision already taken. Co-Authored-By: Claude Opus 5 <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: false,
|
|
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: false,
|
|
requires: 'connection',
|
|
},
|
|
{
|
|
key: 'collabs',
|
|
label: 'Collabs',
|
|
icon: Handshake,
|
|
hint: 'Projects with other people',
|
|
path: '/fofos-admin/collabs',
|
|
built: false,
|
|
requires: 'connection',
|
|
},
|
|
{
|
|
key: 'drops',
|
|
label: 'Drops',
|
|
icon: Sparkles,
|
|
hint: 'Releases',
|
|
path: '/fofos-admin/drops',
|
|
built: false,
|
|
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';
|