Add section asks what shape, and creates one
Pressing the button drawn inside the page opens a dialog: a name, and a choice between one column and two, then Create section. The row lands in the database and the frame re-renders to show it. The layouts are DRAWN rather than named. "One column" is words for a shape, and a shape is faster to recognise than to read; the words stay underneath for anybody the picture does not reach. The name is asked for and not generated. It never appears on the site — it exists so a page of eight bands is navigable, and "Section 4" tells nobody anything, which is exactly the state a generated name would leave every page in. Asked at the only moment somebody knows what they are about to build. The frame is remounted after a write rather than the admin drawing the new band itself. The site renders from the database; the admin does not know how to render a page, and if it learnt it would be a second implementation of the very thing this preview exists to avoid. The canvas now finds the empty-page placeholder by its `data-page-empty` marker instead of taking `firstElementChild`. That was right for exactly as long as an empty page was the only kind — the moment a page has a section, the first child IS that section, and it would have hidden the thing being edited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Columns2, Loader2, Square } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
|
||||
export type SectionLayout = 'one' | 'two';
|
||||
|
||||
/**
|
||||
* The layouts, drawn rather than named.
|
||||
*
|
||||
* "One column" and "Two columns" are words for a shape, and a shape is faster to recognise than to
|
||||
* read. The words stay underneath for anybody the picture does not reach.
|
||||
*
|
||||
* This list and the site's own are two copies of the same fact — the database has it as a check
|
||||
* constraint and PageSection.tsx has it as a branch. A layout offered here that the site cannot draw
|
||||
* would render as nothing, so adding one means all three.
|
||||
*/
|
||||
const LAYOUTS: { value: SectionLayout; label: string; Icon: typeof Square; columns: number }[] = [
|
||||
{ value: 'one', label: 'One column', Icon: Square, columns: 1 },
|
||||
{ value: 'two', label: 'Two columns', Icon: Columns2, columns: 2 },
|
||||
];
|
||||
|
||||
/**
|
||||
* Naming a section and choosing its shape, before it exists.
|
||||
*
|
||||
* ── Why a name is asked for and not generated ──
|
||||
*
|
||||
* The name is never rendered on the site. It exists so a page of eight bands is navigable, and
|
||||
* "Section 4" tells nobody anything — which is precisely the state a generated name would leave every
|
||||
* page in. Asked once, at the only moment somebody knows what they are about to build.
|
||||
*/
|
||||
export const AddSectionDialog = ({
|
||||
open,
|
||||
busy,
|
||||
onCancel,
|
||||
onCreate,
|
||||
}: {
|
||||
open: boolean;
|
||||
busy: boolean;
|
||||
onCancel: () => void;
|
||||
onCreate: (values: { name: string; layout: SectionLayout }) => void;
|
||||
}) => {
|
||||
const [name, setName] = useState('');
|
||||
const [layout, setLayout] = useState<SectionLayout>('one');
|
||||
|
||||
// Fresh every time it opens. A dialog that remembers the last thing somebody typed and abandoned is
|
||||
// a dialog that quietly suggests they meant it.
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setName('');
|
||||
setLayout('one');
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const create = () => {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed || busy) return;
|
||||
onCreate({ name: trimmed, layout });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(next) => !next && onCancel()}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add a section</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs text-muted-foreground">Section name</label>
|
||||
<Input
|
||||
autoFocus
|
||||
value={name}
|
||||
placeholder="Intro"
|
||||
onChange={(event) => setName(event.currentTarget.value)}
|
||||
// Enter creates, because a two-field form is one somebody expects to finish from the
|
||||
// keyboard. Guarded by the same check the button uses.
|
||||
onKeyDown={(event) => event.key === 'Enter' && create()}
|
||||
/>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
For you, not for the site. It never appears on the page.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs text-muted-foreground">Layout</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{LAYOUTS.map(({ value, label, Icon, columns }) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => setLayout(value)}
|
||||
className={`flex flex-col items-center gap-2 rounded-lg border p-4 transition-colors ${
|
||||
layout === value ? 'border-primary bg-muted' : 'border-border hover:bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
<Icon className="size-6 text-muted-foreground" />
|
||||
<span className="text-xs">{label}</span>
|
||||
<span className="sr-only">{columns} column preview</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={onCancel} disabled={busy}>
|
||||
Cancel
|
||||
</Button>
|
||||
{/* Dead without a name. The server refuses an empty one, and a button that fires a request
|
||||
only to show an error is a slower way of saying what the button could have said. */}
|
||||
<Button onClick={create} disabled={!name.trim() || busy}>
|
||||
{busy && <Loader2 className="size-3.5 animate-spin" />}
|
||||
Create section
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user