Two of these were live: PageBuilderPanel still compared kind === 'gallery' in two places, so a carousel block's editor and its Choose button were both dead branches. The type checker could not see them — both compare against string. The rest is copy somebody reads: "Change gallery", "Name a new gallery", "Reading the gallery", "Add to this gallery", Home's Gallery heading, and the "Create Gallery" file action, now "Carousel from folder". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
101 lines
3.5 KiB
TypeScript
101 lines
3.5 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 { 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>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs text-muted-foreground">Description</label>
|
|
<Textarea
|
|
rows={4}
|
|
value={value('description')}
|
|
onChange={(event) => set('description')(event.target.value)}
|
|
/>
|
|
</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>
|
|
);
|
|
};
|