Files
fofos-admin/web/sections/AddComponentDialog.tsx
T
pastilhasandClaude Opus 5 10e5a625ff Media replaces Video, and the greyed-out Image
Two rows, one of them marked `soon`, for a distinction that was never the editor's to make. The path
names a real file and what that file IS is a fact about it — the site reads it off the extension the
way a gallery item always has. So the picker asks for a file and stops asking what kind.

`video` is gone from what an editor can create and still legal in the database. Every row was migrated;
one written by something older should still be editable rather than open nothing, which is why the
edit path answers to both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 15:38:20 +01:00

110 lines
4.8 KiB
TypeScript

import { Handshake, Image, Images, Mail, Type, type LucideIcon } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
/**
* Choosing what to put in a column.
*
* ── One option is still a choice worth showing ──
*
* I had argued the opposite: Free Text is the only kind that works, so opening the editor straight away
* saves a click that asks to be got past. That was wrong in the way a shortcut usually is — it optimised
* the state of the app today and hid the shape of it. Somebody adding a component cannot see what a
* component IS, or that there will be others, and the day the second kind lands the interaction changes
* underneath them.
*
* It is also the pattern this admin already uses. The nav lists unbuilt sections as `soon` rather than
* hiding them, on the reasoning that a nav showing only what is finished gives no sense of what the
* thing is for. The same argument applies here.
*
* ── The list is in the plugin, not the API ──
*
* The API decides what may be STORED — `free-text`, today. This decides what may be OFFERED, and it has
* to live here because every kind needs a label, an icon and an editor that only exists in this plugin.
* 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';
type Choice = {
kind: ComponentKind | null;
label: string;
hint: string;
Icon: LucideIcon;
};
/**
* `kind: null` means not built.
*
* Named rather than omitted, and the ones named are things the site already HAS — media, video and
* galleries all exist as concepts here. That is the difference between saying what is coming and
* inventing a roadmap. Gallery stopped being one of them on 2026-08-31: the set, its ordering and the
* carousel that draws it were all already built, so the block is a reference to one.
*/
const CHOICES: Choice[] = [
{ kind: 'free-text', label: 'Text', hint: 'Words, headings and links', Icon: Type },
/*
One entry, either kind, because the block does not store which.
Image and Video were two rows — one of them greyed out as `soon` — and the distinction was never
the editor's to make: the path names a real file and what that file IS is a fact about it. So the
picker asks for a file and the site works out the rest, which is also what a gallery item has
always done.
*/
{ kind: 'media', label: 'Media', hint: 'A photograph or a video, from the media store', Icon: Image },
{ kind: 'gallery', label: 'Gallery', hint: 'A named set of media', Icon: Images },
/*
Two that place something the site already draws.
Nothing to choose and nothing to type, so adding one is a single click and there is no editor
behind it. They are here rather than hidden because a page is built by picking blocks, and a block
somebody cannot find is a block that does not exist.
*/
{ 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 },
];
export const AddComponentDialog = ({
open,
onCancel,
onChoose,
}: {
open: boolean;
onCancel: () => void;
onChoose: (kind: ComponentKind) => void;
}) => (
<Dialog open={open} onOpenChange={(next) => !next && onCancel()}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Add a component</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-2">
{CHOICES.map(({ kind, label, hint, Icon }) => (
<button
key={label}
type="button"
disabled={kind === null}
onClick={() => kind && onChoose(kind)}
className={`flex flex-col items-start gap-1 rounded-lg border p-3 text-left transition-colors ${
kind === null
? 'cursor-not-allowed border-border opacity-45'
: 'border-border hover:border-primary hover:bg-muted/50'
}`}
>
<span className="flex items-baseline gap-2">
<Icon className="size-4 shrink-0 self-center text-muted-foreground" />
<span className="text-sm">{label}</span>
{/* Dimmed and labelled rather than hidden, the same way the nav marks a section that is
not built. A list of only what is finished gives no sense of what this is for. */}
{kind === null && (
<span className="text-[10px] tracking-wide text-muted-foreground/70 uppercase">soon</span>
)}
</span>
<span className="text-[11px] text-muted-foreground">{hint}</span>
</button>
))}
</div>
</DialogContent>
</Dialog>
);