The nav already said it: Pages is a section, and the pages are rows under it. Two things, two screens. The detail panel was showing one screen with a row lit up, which made the list the answer to both questions. ?section=pages the list — what exists, what it is called, where it lives, in what order ?section=pages&page=4 that page — what is on it Both addressable, which is why the distinction is a query parameter rather than state. Pressing PAGES itself now clears `page`. Without that the section row and the row for whichever page was last open lead to the same screen and there is no way back to the list but the browser's back button. Scoped exactly like `media`, and for the same reason: a parameter belonging to one section should not ride along into the others. The list stops having a selection — selecting a page leaves the screen entirely — so its rows are plain rows again and the panel no longer reads or writes the URL at all. Adding a page deliberately does NOT open it. The next thing anybody does is name it, and the name lives on the list; jumping into the builder would mean coming straight back. SitePreview is now shared, lifted out of LayoutPanel with the dev-port lookup beside it. Layout passes the theme it is editing; a page passes nothing and just gets its own page. The builder shows the real page at its real address today, which is what the blocks will be judged against when they exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
85 lines
3.4 KiB
TypeScript
85 lines
3.4 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { useSearchParams } from 'react-router';
|
|
import { useClient } from 'hooks/useClient';
|
|
import { readableError } from '../errors';
|
|
import { SitePreview, useDevPort } from '../SitePreview';
|
|
import { usePages, type Page } from '../usePages';
|
|
|
|
/**
|
|
* One page, being built.
|
|
*
|
|
* ── Why this is a different panel from Pages ──
|
|
*
|
|
* Pages is the LIST: what exists, what it is called, where it lives, in what order. This is one PAGE:
|
|
* what is on it. They are different jobs and the nav already says so — Pages is a section and the pages
|
|
* are rows underneath it — so the detail panel follows the same shape rather than showing a list with
|
|
* one row highlighted.
|
|
*
|
|
* ── It renders the real page ──
|
|
*
|
|
* The preview is the site's own page at its own address, through the platform's dev proxy. Whatever
|
|
* the blocks end up being, this is what they will be judged against, and it is already true today.
|
|
*/
|
|
export const PageBuilderPanel = () => {
|
|
const client = useClient();
|
|
const clientRef = useRef(client);
|
|
clientRef.current = client;
|
|
|
|
const [params] = useSearchParams();
|
|
const id = Number(params.get('page')) || null;
|
|
|
|
const devPort = useDevPort();
|
|
const { pages, announce } = usePages();
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
/*
|
|
The list, if nobody has announced it yet.
|
|
|
|
Landing here directly — a bookmark, a reload, a URL typed with `?page=4` — means Pages has never
|
|
rendered and the channel is empty. So this fetches once and announces, which also fills the nav.
|
|
Arriving from the nav, `pages` is already there and nothing is requested.
|
|
*/
|
|
useEffect(() => {
|
|
if (pages) return;
|
|
void clientRef.current
|
|
.get<{ data: Page[] }>('/fofos-admin/pages')
|
|
.then((response) => announce(response.data))
|
|
.catch((failure: unknown) => setError(readableError(failure)));
|
|
}, [pages, announce]);
|
|
|
|
const page = pages?.find((row) => row.id === id) ?? null;
|
|
|
|
if (error) return <div className="p-4 text-xs text-destructive">{error}</div>;
|
|
if (!pages) return <div className="p-4 text-xs text-muted-foreground">Loading…</div>;
|
|
|
|
if (!page) {
|
|
// A page that was deleted in another panel, or an id somebody typed. Said plainly rather than
|
|
// redirected: a silent bounce to the list leaves you wondering whether you clicked the wrong thing.
|
|
return <div className="p-4 text-xs text-muted-foreground">That page no longer exists.</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="flex size-full flex-col gap-3 p-4">
|
|
<div className="flex shrink-0 items-baseline gap-2">
|
|
<h3 className="text-sm font-medium">{page.title}</h3>
|
|
<span className="font-mono text-[10px] text-muted-foreground">{page.path}</span>
|
|
</div>
|
|
|
|
{/* The real page, at its own address. `path` has no leading slash — Home's is the empty string,
|
|
which the proxy resolves to the site's root. */}
|
|
<SitePreview port={devPort} path={page.slug} />
|
|
|
|
{/*
|
|
Where the blocks go.
|
|
|
|
Said rather than left blank, because an empty panel under a working preview reads as something
|
|
that failed to load. This is the next thing to build and the panel should say so.
|
|
*/}
|
|
<div className="shrink-0 rounded-lg border border-dashed border-border p-4 text-center">
|
|
<p className="text-xs text-muted-foreground">The blocks that make up this page go here.</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|