Files
fofos-admin/web/sections/SubjectFields.tsx
T
pastilhasandClaude Opus 5 d6fdf936cf the description is rich text
It is the one piece of writing about a collection, a piece or a collab — the
paragraph the template draws under the name — so it wants the headings, emphasis,
links and alignment the page's own text blocks have, not a plain textarea.

Prose on the site already renders HTML when it finds any and falls back to
paragraphs when it does not, so everything stored as plain text goes on working
untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 14:59:09 +01:00

112 lines
4.1 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useClient } from 'hooks/useClient';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { RichText } from '../RichText';
import { useAutosave } from '../useAutosave';
import type { SubjectRecord, SubjectSection } from './subjects';
/**
* What somebody says about a collection or a piece, in the editor column.
*
* ── One form, two sections ──
*
* The fields below the description come from the section — see subjects.ts. Everything above them is
* shared because it is the same in both: what it is called, and where it lives.
*
* The carousel is not here. It is chosen beside the photographs it puts on screen, in the panel, where
* the consequence of choosing it is visible — a dropdown in a column of text fields would be the one
* control whose effect you cannot see.
*
* ── One draft, one save ──
*
* Every field writes into one draft object and `useAutosave` sends it whole. Per-field saves would be
* eight requests for somebody filling in a form, and the last one to land would win in an order nobody
* controls.
*/
export const SubjectFields = <T extends SubjectRecord>({
section,
record,
onSaved,
}: {
section: SubjectSection<T>;
record: T;
onSaved: () => void;
}) => {
const client = useClient();
const [draft, setDraft] = useState<Partial<Record<string, string>>>({});
// Keyed on the ID, not the values: keyed on the values it would fight the typing, pulling every
// keystroke back to the last saved one until the save landed.
useEffect(() => setDraft({}), [record.id]);
const value = (key: string): string =>
draft[key] ?? ((record as Record<string, unknown>)[key] as string | null) ?? '';
const dirty = Object.keys(draft).length > 0;
useAutosave(dirty ? JSON.stringify(draft) : null, false, async () => {
await client.patch(`${section.endpoint}/${record.id}`, draft);
setDraft({});
onSaved();
});
const set = (key: string) => (next: string) => setDraft((was) => ({ ...was, [key]: next }));
return (
<div className="space-y-4">
<div className="space-y-1.5">
<label className="text-xs text-muted-foreground">Name</label>
<Input value={value('title')} onChange={(event) => set('title')(event.target.value)} />
</div>
<div className="space-y-1.5">
<label className="text-xs text-muted-foreground">Address</label>
<Input value={value('slug')} onChange={(event) => set('slug')(event.target.value)} />
<p className="font-mono text-[11px] text-muted-foreground/70">
{section.base}/{value('slug')}
</p>
</div>
{/*
Rich text, not a textarea.
It is the paragraph the template draws under the name — the one piece of writing about this
thing — so it wants the same headings, emphasis, links and alignment the page's own text
blocks have. `Prose` on the site already renders HTML when it finds any and falls back to
paragraphs when it does not, so what is already stored as plain text goes on working.
Sanitised on the way in by the API, against the same allowlist the page components use.
*/}
<div className="space-y-1.5">
<label className="text-xs text-muted-foreground">Description</label>
<RichText
value={value('description')}
onChange={(html) => set('description')(html)}
placeholder="What this is."
/>
</div>
{section.specs.map(([key, label, placeholder]) => (
<div key={key} className="space-y-1.5">
<label className="text-xs text-muted-foreground">{label}</label>
{section.long?.includes(key) ? (
<Textarea
rows={3}
placeholder={placeholder}
value={value(key)}
onChange={(event) => set(key)(event.target.value)}
/>
) : (
<Input
placeholder={placeholder}
value={value(key)}
onChange={(event) => set(key)(event.target.value)}
/>
)}
</div>
))}
</div>
);
};