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>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user