Collections, above Galleries and Media, where the other things made of media already are. There is no Create button and no Delete, and that is the design rather than an omission. Which collections exist is a question about `originals/COLLECTIONS/`, so making one means making a folder — in Media or in Finder, and both work. A Delete here would either drop the row and leave the photographs, so the collection reappears on the next read, or delete somebody's pictures. Neither is what Delete should mean in an admin. The grid is for choosing the MAIN photo and nothing else. The photographs are already in filename order and the gallery is the folder, so there is nothing to reorder and nothing to add — the one decision is which picture leads. Pressing the chosen one again clears it, which is how "no choice, use the first" stays reachable without a second control that only does that. The badge says Main rather than Cover because it has to be true of the first photograph, which leads by default, as well as of one somebody picked. The fields are the other half — everything a directory cannot tell you: the name it goes by, the address, and the specs from the client's sitemap. `folder` is shown and not editable, and the API refuses to write it: it is an address, and renaming one moves files. Shown anyway, because it is the only thing that says WHICH folder a collection is. One draft, one save. Per-field writes would be eight requests for somebody filling in a form, and the last to land would win in an order nobody controls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
140 lines
6.5 KiB
TypeScript
140 lines
6.5 KiB
TypeScript
import { Navigate } from 'react-router';
|
|
import { href, useNav } from './routes';
|
|
import { pageParam, usePages } from './usePages';
|
|
import { DEFAULT_SECTION } from './sections';
|
|
import { sectionFor, sectionOpen, type SectionKey } from './sections';
|
|
import { AdministrationPanel } from './AdministrationPanel';
|
|
import { MediaPanel } from './MediaPanel';
|
|
import { CollectionsPanel } from './sections/CollectionsPanel';
|
|
import { GalleriesPanel } from './sections/GalleriesPanel';
|
|
import { LayoutPanel } from './sections/LayoutPanel';
|
|
import { PageBuilderPanel } from './sections/PageBuilderPanel';
|
|
import { ViewModeProvider } from './ViewMode';
|
|
import { MessagesPanel } from './MessagesPanel';
|
|
import { PlannedSection } from './PlannedSection';
|
|
import { useReady } from './useReady';
|
|
|
|
/**
|
|
* The right column: whichever section the nav has selected.
|
|
*
|
|
* It reads `?section=` from the URL rather than being handed it — the two panels are siblings in a
|
|
* workspace and neither owns the other.
|
|
*
|
|
* ── There used to be a `?config=1` ──
|
|
*
|
|
* The connection form was an overlay reached from a link in this header, because it was not one of the
|
|
* sections. Now it IS one — Administration — so the overlay is gone and so is the link. A second route
|
|
* to the same screen is a second thing to keep correct, and the two disagreed the moment a section was
|
|
* selected underneath a form that covered it.
|
|
*
|
|
* ── Not ready means Administration, whatever the URL says ──
|
|
*
|
|
* The gate is `useReady`, shared with the nav so the two cannot disagree. Enforced HERE and not only
|
|
* there, because the nav can only decline to offer a link — `?section=gallery` typed into the address
|
|
* bar, or a bookmark from before the connection was forgotten, arrives without going through it. A
|
|
* nav that hides a door is a courtesy; this is the lock.
|
|
*/
|
|
|
|
/**
|
|
* Which component renders which section.
|
|
*
|
|
* A map rather than a conditional on `built`. The version of this that read
|
|
* `built ? <MessagesPanel/> : <Planned/>` was correct only while exactly one section existed — the
|
|
* second one to be finished would have rendered Messages under its own name, which is the kind of bug
|
|
* that looks like a data problem for an hour. A key with no entry here falls through to Planned, so
|
|
* marking a section `built` in sections.ts without writing its panel fails visibly rather than
|
|
* silently showing somebody else's.
|
|
*/
|
|
const PANELS: Partial<Record<SectionKey, () => React.JSX.Element>> = {
|
|
layout: LayoutPanel,
|
|
collections: CollectionsPanel,
|
|
galleries: GalleriesPanel,
|
|
messages: MessagesPanel,
|
|
administration: AdministrationPanel,
|
|
media: MediaPanel,
|
|
};
|
|
|
|
export const DetailPanel = () => {
|
|
/*
|
|
A page selected inside Pages opens the BUILDER, not the list with a row lit up. By SLUG, so the URL
|
|
says which page rather than which row — see pageParam in usePages.
|
|
|
|
The nav says the same thing — Pages is a section and the pages are rows under it — so the detail
|
|
panel follows it. The second path segment is what distinguishes the two, which keeps both
|
|
addressable: `/fofos-admin/pages` is the list, `/fofos-admin/pages/home` is that page.
|
|
*/
|
|
const { section: selected, item: selectedPage, atIndex } = useNav();
|
|
const { pages } = usePages();
|
|
|
|
const { ready, rootSet, loading } = useReady();
|
|
|
|
/*
|
|
The bare `/fofos-admin` redirected to its first section, once, replacing.
|
|
|
|
The screen has to render SOMETHING there, and rendering Layout while the address bar says nothing
|
|
means the URL stops describing what is on screen — which is the whole reason the sections moved out
|
|
of the query string. `replace`, so Back leaves the admin rather than bouncing off the redirect.
|
|
*/
|
|
if (atIndex) return <Navigate to={href(DEFAULT_SECTION)} replace />;
|
|
|
|
if (loading) {
|
|
return <div className="flex size-full items-center justify-center p-6 text-sm text-muted-foreground">Loading…</div>;
|
|
}
|
|
|
|
/*
|
|
Not ready, so Administration regardless of what the URL asked for. The header then NAMES it, which
|
|
is the part the old overlay could not do: the form used to appear under whichever row happened to
|
|
be highlighted, so it read as that section being broken rather than as one step not done yet.
|
|
*/
|
|
/*
|
|
A section that cannot be opened sends you to Administration, because that is where every requirement
|
|
is satisfied. Asked per section rather than by one flag: Media is reachable the moment a root
|
|
location exists, long before fofos is connected.
|
|
*/
|
|
const wanted = sectionFor(selected);
|
|
const current = sectionOpen(wanted, { ready, rootSet }) ? wanted : sectionFor('administration');
|
|
|
|
/*
|
|
Pages has no panel of its own any more.
|
|
|
|
It was a list of cards — a title, a slug, a menu label, per page — beside a nav that already lists
|
|
the pages. Two lists of the same thing, and the one you were looking at was never the one you had
|
|
clicked. The nav IS the list now, ordered by dragging it, so the section itself is only a way in.
|
|
|
|
`/fofos-admin/pages` therefore means "the first one", which is what somebody pressing Pages wants
|
|
and what the nav's own row already implied.
|
|
*/
|
|
if (current.key === 'pages' && !selectedPage) {
|
|
const first = pages?.[0];
|
|
if (first) return <Navigate to={href('pages', pageParam(first))} replace />;
|
|
return (
|
|
<div className="flex size-full items-center justify-center p-6 text-xs text-muted-foreground">
|
|
{pages ? 'This site has no pages yet.' : 'Loading…'}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const Panel =
|
|
current.key === 'pages' && selectedPage ? PageBuilderPanel : current.built ? PANELS[current.key] : undefined;
|
|
|
|
return (
|
|
<div className="flex size-full flex-col overflow-hidden">
|
|
{/*
|
|
No bar.
|
|
|
|
It held the device toggle and Sync / Build / Publish across the top of this panel. The toggle
|
|
moved to the top of the EDITOR, which is where the settings it scopes already are; the three
|
|
actions are out until they have a place that is theirs rather than a corner of the panel that
|
|
happens to be pinned. `SyncButton` and `PublishButton` still exist and are still mounted
|
|
nowhere — see git history for what the bar looked like.
|
|
|
|
`ViewModeProvider` stays. It is not the bar's: the preview and the two panels with one read
|
|
`device` from it, and `usePreviewable` is how a section says it has a page to show.
|
|
*/}
|
|
<ViewModeProvider>
|
|
<div className="min-h-0 flex-1">{Panel ? <Panel /> : <PlannedSection section={current} />}</div>
|
|
</ViewModeProvider>
|
|
</div>
|
|
);
|
|
};
|