From 331e615c7c10608919865d111079cfddfd62da6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 4 Sep 2026 14:53:48 +0100 Subject: [PATCH] blocks that draw whatever the page is about MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `/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) --- web/sections/AddComponentDialog.tsx | 53 ++++++++++++++++++++++++++--- web/sections/PageBuilderPanel.tsx | 8 ++++- web/usePages.ts | 8 +++++ 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/web/sections/AddComponentDialog.tsx b/web/sections/AddComponentDialog.tsx index 5e21398..e5cb9cd 100644 --- a/web/sections/AddComponentDialog.tsx +++ b/web/sections/AddComponentDialog.tsx @@ -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'; /** @@ -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. */ -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 = { 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: 'partners', label: 'Partners', hint: 'The partner logos, from Site', Icon: Handshake }, { 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 = ({ open, onCancel, onChoose, + template, }: { open: boolean; onCancel: () => 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; }) => ( !next && onCancel()}> @@ -84,7 +127,8 @@ export const AddComponentDialog = ({
- {CHOICES.map(({ kind, label, hint, Icon }) => ( + {CHOICES.filter((choice) => template || !SUBJECT_KINDS.includes(choice.kind)).map( + ({ kind, label, hint, Icon }) => ( - ))} + ), + )}
diff --git a/web/sections/PageBuilderPanel.tsx b/web/sections/PageBuilderPanel.tsx index 6d556d5..11c5faf 100644 --- a/web/sections/PageBuilderPanel.tsx +++ b/web/sections/PageBuilderPanel.tsx @@ -1011,7 +1011,13 @@ export const PageBuilderPanel = () => { onCreate={(values) => void createSection(values)} /> - setChoosing(null)} onChoose={chooseKind} /> + setChoosing(null)} + onChoose={chooseKind} + // Which decides whether the subject blocks are offered — they render nothing anywhere else. + template={page?.is_template} + /> ` 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; }; /**