The version before this made a collection out of every folder under originals/COLLECTIONS/. It kept the disk and the database in step for free, and it also let whoever last tidied a directory decide the website's structure. A collection is editorial: it exists because somebody made it, named it and put photographs in it. Its photographs are a GALLERY — the feature that already means "a named, ordered set of media", with a picker and an order somebody dragged into place. A collection points at one rather than growing a private copy, so ordering stays maintained in the one screen that owns ordering, and the same set can appear in a collection and on a page without being chosen twice. That is why the grid here is not draggable and has no Add: both belong to Galleries. What is decided HERE is which photograph leads, because that is a fact about the collection rather than about the set. Creating is a name and a button under the list it adds to, and it selects what it made — a dialog for one field is a dialog nobody thanks you for. Deleting removes the collection and never the photographs, which the button says out loud rather than leaving somebody to find out. Changing the gallery clears the cover. The new set is different photographs, so a cover chosen from the old one is a path that may not be there, and falling back silently would be a choice quietly reverting itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
109 lines
3.9 KiB
TypeScript
109 lines
3.9 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 { Collection } from './collections';
|
|
|
|
/**
|
|
* What somebody says about a collection, in the editor column.
|
|
*
|
|
* ── Everything here is what a FOLDER cannot tell you ──
|
|
*
|
|
* Which photographs are in a collection is a question about `originals/COLLECTIONS/<name>/`, answered
|
|
* by reading it. This is the other half: the name it goes by, the address it lives at, and the specs
|
|
* from the client's sitemap. None of it can be derived, so all of it is typed.
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
/** The fields, in the order the client's sitemap lists them. */
|
|
const SPECS: [keyof Collection, string, string][] = [
|
|
['descriptive_text', 'Descriptive text', 'What this collection is'],
|
|
['measurement', 'Measurement', '45cm x 45cm'],
|
|
['material', 'Raw material', 'Acrylic yarn'],
|
|
['colors', 'Colours', ''],
|
|
['delivery_time', 'Delivery time', '4-6 weeks'],
|
|
];
|
|
|
|
export const CollectionFields = ({
|
|
collection,
|
|
onSaved,
|
|
}: {
|
|
collection: Collection;
|
|
onSaved: () => void;
|
|
}) => {
|
|
const client = useClient();
|
|
const [draft, setDraft] = useState<Partial<Collection>>({});
|
|
|
|
// 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({}), [collection.id]);
|
|
|
|
const value = (key: keyof Collection): string =>
|
|
(draft[key] as string | null | undefined) ?? (collection[key] as string | null) ?? '';
|
|
|
|
const dirty = Object.keys(draft).length > 0;
|
|
|
|
useAutosave(dirty ? JSON.stringify(draft) : null, false, async () => {
|
|
await client.patch(`/fofos-admin/collections/${collection.id}`, draft);
|
|
setDraft({});
|
|
onSaved();
|
|
});
|
|
|
|
const set = (key: keyof Collection) => (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">/collections/{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>
|
|
|
|
{SPECS.map(([key, label, placeholder]) => (
|
|
<div key={key} className="space-y-1.5">
|
|
<label className="text-xs text-muted-foreground">{label}</label>
|
|
{key === 'descriptive_text' ? (
|
|
<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>
|
|
);
|
|
};
|