Pages, with the pages under it in the nav
The section is real, and the nav draws the site's pages as rows beneath 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. Drawn whether or not Pages is the open section — hiding the list until you click makes the nav a menu of menus. Three kinds of change, saved three different ways, and the split is deliberate. TEXT — a title, a menu label, an address — is drafted and written by the one Save in the bar, because typing is something people do halfway and change their minds about. ADDING, DELETING and REORDERING are immediate: each is a single deliberate gesture with an obvious result, and holding them in a draft would put a list on screen that does not exist yet, then leave Save to decide what order to apply three kinds of change in. Only the rows that actually changed are sent, so a save touches what somebody edited and nothing else. A failed save reloads from the server rather than leaving the typed values on screen, where they would look saved. Home's address and delete button are ABSENT, not disabled. A greyed control invites somebody to work out why they cannot use it; there is nothing to work out, the front page is always `/`. The two panels share the list through a channel rather than both fetching it. They are siblings and neither can pass the other a prop, and two fetches means two lists that agree until somebody renames a page. Selection stays in the URL — `?page=` — because that is addressable and the list is not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+48
-1
@@ -2,6 +2,7 @@ import { Link, useSearchParams } from 'react-router';
|
||||
import { Lock } from 'lucide-react';
|
||||
import { DEFAULT_SECTION, SECTIONS, isSectionKey, sectionOpen, type SectionKey } from './sections';
|
||||
import { useReady } from './useReady';
|
||||
import { usePages } from './usePages';
|
||||
|
||||
/**
|
||||
* The left column: which part of the fofos site the detail panel is showing.
|
||||
@@ -49,6 +50,10 @@ const rowClass = (active: boolean) =>
|
||||
|
||||
export const NavPanel = () => {
|
||||
const [params] = useSearchParams();
|
||||
// Announced by PagesPanel rather than fetched here — see usePages. Null until it has spoken, which
|
||||
// is why nothing is drawn instead of an empty list.
|
||||
const { pages } = usePages();
|
||||
const selectedPage = Number(params.get('page')) || null;
|
||||
const raw = params.get('section');
|
||||
const active: SectionKey = isSectionKey(raw) ? raw : DEFAULT_SECTION;
|
||||
const { ready, rootSet } = useReady();
|
||||
@@ -100,7 +105,7 @@ export const NavPanel = () => {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
const row = (
|
||||
<Link
|
||||
key={section.key}
|
||||
to={{ search: `?${search.toString()}` }}
|
||||
@@ -110,6 +115,48 @@ export const NavPanel = () => {
|
||||
{body}
|
||||
</Link>
|
||||
);
|
||||
|
||||
/*
|
||||
The site's pages, as rows under Pages.
|
||||
|
||||
Indented and quieter than a section, because they are not sections — they are the contents of
|
||||
one. The indent is a left border rather than padding alone, so the relationship survives a row
|
||||
wrapping onto two lines.
|
||||
|
||||
Drawn whether or not Pages is the open section. The list of pages IS the site, and hiding it
|
||||
until you click makes the nav a menu of menus.
|
||||
*/
|
||||
if (section.key !== 'pages' || !pages?.length) return row;
|
||||
|
||||
return (
|
||||
<div key={section.key} className="flex flex-col">
|
||||
{row}
|
||||
{pages.map((page) => {
|
||||
const childSearch = new URLSearchParams(params);
|
||||
childSearch.set('section', 'pages');
|
||||
childSearch.set('page', String(page.id));
|
||||
childSearch.delete('media');
|
||||
|
||||
const current = active === 'pages' && selectedPage === page.id;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={page.id}
|
||||
to={{ search: `?${childSearch.toString()}` }}
|
||||
replace
|
||||
className={`flex items-baseline gap-2 border-b border-border py-1.5 pr-3 pl-9 text-left transition-colors ${
|
||||
current ? 'bg-muted' : 'hover:bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate text-xs">{page.title}</span>
|
||||
{/* The address, because two pages can be called nearly the same thing and this is what
|
||||
tells them apart. Home's is `/`, which is short enough not to need explaining. */}
|
||||
<span className="shrink-0 font-mono text-[10px] text-muted-foreground">{page.path}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user