Files
fofos-admin/web/sections/SubjectFields.tsx
T
pastilhasandClaude Opus 5 5e5c06e76e Pieces: the Gallery, as a section like Collections
The Gallery is a flat list of peças, each with a title, a gallery of mixed
media, and the specs from the client's sitemap — measurement, raw material,
colours, and NOTAS: production time and shipping conditions.

That is a collection one level flatter, so the panel is shared rather than
copied. SubjectPanel and SubjectFields take a section config (subjects.ts);
CollectionsPanel and PiecesPanel are one line each.

The five template blocks are renamed collection-* to subject-*. They never read
anything collection-specific, and copying them per section would have meant
twenty kinds by the time Drops and Collabs land.

Called Pieces in the nav, not Gallery — there is already a Galleries section
meaning a named set of media, and two rows called Gallery and Galleries is a
question somebody has to answer every time they look at it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 17:44:32 +01:00

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 gallery 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>
);
};