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