blocks that draw whatever the page is about
`/collections/amadoras-mythology` and `/collections/70s` are the same page with a different subject. Building each would be absurd; hard-coding the arrangement would put it out of reach of the admin. So the arrangement is built once — a template page, composed exactly like any other — and every collection is drawn through it. Four blocks read the subject: its name, its description, its specs, its photographs. They hold nothing at all, not even a reference. `collections` holds no content because the list is a query; these hold none because the subject is the URL, and the same block draws a different collection on the next request. Offered only on a template, and HIDDEN rather than disabled elsewhere. A greyed-out row invites "why not here", and the answer — this page has no subject — is not something a tooltip fixes. Available and silently drawing nothing is the worse failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { Handshake, Image, Images, LayoutGrid, Mail, Type, type LucideIcon } from 'lucide-react';
|
import { Handshake, Heading, Image, Images, LayoutGrid, Mail, Ruler, Type, type LucideIcon } from 'lucide-react';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,7 +23,30 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
|
|||||||
* A kind the server would accept and this has never heard of is not creatable anyway.
|
* A kind the server would accept and this has never heard of is not creatable anyway.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ComponentKind = 'free-text' | 'media' | 'gallery' | 'contact-form' | 'partners' | 'collections';
|
export type ComponentKind =
|
||||||
|
| 'free-text'
|
||||||
|
| 'media'
|
||||||
|
| 'gallery'
|
||||||
|
| 'contact-form'
|
||||||
|
| 'partners'
|
||||||
|
| 'collections'
|
||||||
|
| 'collection-title'
|
||||||
|
| 'collection-description'
|
||||||
|
| 'collection-specs'
|
||||||
|
| 'collection-gallery';
|
||||||
|
|
||||||
|
/*
|
||||||
|
The blocks that draw whatever the page is ABOUT.
|
||||||
|
|
||||||
|
Only offered on a template. On an ordinary page they would render nothing, which is the worst kind of
|
||||||
|
block: one that is available, adds cleanly, and silently does nothing.
|
||||||
|
*/
|
||||||
|
export const SUBJECT_KINDS: ComponentKind[] = [
|
||||||
|
'collection-title',
|
||||||
|
'collection-description',
|
||||||
|
'collection-specs',
|
||||||
|
'collection-gallery',
|
||||||
|
];
|
||||||
|
|
||||||
type Choice = {
|
type Choice = {
|
||||||
kind: ComponentKind | null;
|
kind: ComponentKind | null;
|
||||||
@@ -66,16 +89,36 @@ export const CHOICES: Choice[] = [
|
|||||||
{ kind: 'contact-form', label: 'Contact form', hint: 'The site\'s form, as it is', Icon: Mail },
|
{ kind: 'contact-form', label: 'Contact form', hint: 'The site\'s form, as it is', Icon: Mail },
|
||||||
{ kind: 'partners', label: 'Partners', hint: 'The partner logos, from Site', Icon: Handshake },
|
{ kind: 'partners', label: 'Partners', hint: 'The partner logos, from Site', Icon: Handshake },
|
||||||
{ kind: 'collections', label: 'Collections', hint: 'Every collection, as a grid', Icon: LayoutGrid },
|
{ kind: 'collections', label: 'Collections', hint: 'Every collection, as a grid', Icon: LayoutGrid },
|
||||||
|
|
||||||
|
/*
|
||||||
|
The four that draw THIS collection, on the template.
|
||||||
|
|
||||||
|
They hold nothing — not even a reference. The subject is the URL, so one block draws a different
|
||||||
|
collection on every request, and that is what lets one arrangement serve all of them.
|
||||||
|
*/
|
||||||
|
{ kind: 'collection-title', label: 'Its name', hint: 'The collection this page is about', Icon: Heading },
|
||||||
|
{ kind: 'collection-description', label: 'Its description', hint: 'What was written about it', Icon: Type },
|
||||||
|
{ kind: 'collection-specs', label: 'Its specs', hint: 'Measurement, material, colours, delivery', Icon: Ruler },
|
||||||
|
{ kind: 'collection-gallery', label: 'Its photographs', hint: 'The gallery it points at', Icon: Images },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const AddComponentDialog = ({
|
export const AddComponentDialog = ({
|
||||||
open,
|
open,
|
||||||
onCancel,
|
onCancel,
|
||||||
onChoose,
|
onChoose,
|
||||||
|
template,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onChoose: (kind: ComponentKind) => void;
|
onChoose: (kind: ComponentKind) => void;
|
||||||
|
/**
|
||||||
|
* Whether this page is a TEMPLATE, which decides whether the subject blocks are offered.
|
||||||
|
*
|
||||||
|
* Hidden rather than disabled elsewhere. A greyed-out row invites the question "why not here", and
|
||||||
|
* the answer — this page has no subject — is not something a tooltip fixes. On a page that has one
|
||||||
|
* they are the point; on any other they would render nothing at all.
|
||||||
|
*/
|
||||||
|
template?: boolean;
|
||||||
}) => (
|
}) => (
|
||||||
<Dialog open={open} onOpenChange={(next) => !next && onCancel()}>
|
<Dialog open={open} onOpenChange={(next) => !next && onCancel()}>
|
||||||
<DialogContent className="sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
@@ -84,7 +127,8 @@ export const AddComponentDialog = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{CHOICES.map(({ kind, label, hint, Icon }) => (
|
{CHOICES.filter((choice) => template || !SUBJECT_KINDS.includes(choice.kind)).map(
|
||||||
|
({ kind, label, hint, Icon }) => (
|
||||||
<button
|
<button
|
||||||
key={label}
|
key={label}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -107,7 +151,8 @@ export const AddComponentDialog = ({
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-[11px] text-muted-foreground">{hint}</span>
|
<span className="text-[11px] text-muted-foreground">{hint}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1011,7 +1011,13 @@ export const PageBuilderPanel = () => {
|
|||||||
onCreate={(values) => void createSection(values)}
|
onCreate={(values) => void createSection(values)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddComponentDialog open={choosing !== null} onCancel={() => setChoosing(null)} onChoose={chooseKind} />
|
<AddComponentDialog
|
||||||
|
open={choosing !== null}
|
||||||
|
onCancel={() => setChoosing(null)}
|
||||||
|
onChoose={chooseKind}
|
||||||
|
// Which decides whether the subject blocks are offered — they render nothing anywhere else.
|
||||||
|
template={page?.is_template}
|
||||||
|
/>
|
||||||
|
|
||||||
<MediaPicker
|
<MediaPicker
|
||||||
open={picking !== null}
|
open={picking !== null}
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ export type Page = {
|
|||||||
menu_label: string | null;
|
menu_label: string | null;
|
||||||
position: number;
|
position: number;
|
||||||
is_home: boolean;
|
is_home: boolean;
|
||||||
|
/**
|
||||||
|
* The arrangement a whole CLASS of pages is drawn with, rather than a page anybody visits.
|
||||||
|
*
|
||||||
|
* `collection` is the one today: every `/collections/<slug>` is drawn through it. Listed beside the
|
||||||
|
* pages because it is built exactly like one — a separate section for a single row would be a second
|
||||||
|
* place to learn — and marked so nothing offers to rename it, reorder it or put it in a menu.
|
||||||
|
*/
|
||||||
|
is_template?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user