Files
fofos-admin/web/sections/subjects.ts
T
pastilhasandClaude Opus 5 9ca6077e2c Collabs, as a third configuration of one panel
The same object as a collection and a gallery piece, so it is a config in
subjects.ts and a one-line panel — no new screen.

Two differences, both deliberate. It has no SPECS: a collab is a project rather
than an object, so there is no measurement or raw material and the form is a
name, an address and a description. And its title column is `project_name`,
which is the client's word and stays — the site's API publishes both that and
`title` so this panel reads one field like every other section, and the
translation lives in one router rather than as a branch in every screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 13:42:26 +01:00

135 lines
4.7 KiB
TypeScript

import type { LucideIcon } from 'lucide-react';
import { Boxes, Handshake, Shapes } from 'lucide-react';
/**
* The two sections whose rows are drawn through a template — collections, and gallery pieces.
*
* ── Why one shape ──
*
* They are the same object. A title, an address, some words, a carousel, and a table of properties;
* the sections differ in what they GROUP, not in how one is edited. Copying the panel would be three
* hundred lines duplicated for an icon and a noun, and the copies would drift the first time one of
* them got a fix.
*
* The site draws them through one set of blocks for exactly the same reason — see
* `src/apps/web/components/subject.ts` in the website.
*
* ── What a section still owns ──
*
* Its endpoint, the word it puts in the URL, and its SPEC FIELDS. That last one is the real
* difference: a collection ends with a delivery time; a piece ends with the sitemap's NOTAS*,
* production time and shipping conditions. Both are just rows in a form, and naming them here is what
* keeps the form from having to know which section it is drawing.
*/
/** What every subject row has. Snake case: it is the wire, not our shape. */
export type SubjectRecord = {
id: number;
/** The carousel holding its media, or null while nobody has chosen one. */
carousel_id: number | null;
title: string;
slug: string | null;
description: string | null;
sort_order: number;
photo_count: number;
};
export type Collection = SubjectRecord & {
descriptive_text: string | null;
measurement: string | null;
material: string | null;
colors: string | null;
delivery_time: string | null;
};
export type Collab = SubjectRecord & {
/**
* The client's word for the title, carried alongside `title`.
*
* The column really is `project_name` — see the site's collabs schema — and the wire publishes both
* so this panel can go on reading `title` like every other section. Writes accept either.
*/
project_name: string;
};
export type Piece = SubjectRecord & {
descriptive_text: string | null;
measurement: string | null;
material: string | null;
colors: string | null;
production_notes: string | null;
shipping_notes: string | null;
};
export type SubjectPhoto = { path: string; name: string };
export type SubjectSection<T extends SubjectRecord> = {
/** The query key the selection lives under — `?collection=3`. See docs/navigation-audit.md. */
param: string;
/** Where the rows are, relative to the plugin's mount. */
endpoint: string;
/** The site path they live at, for the address hint under the slug field. */
base: string;
icon: LucideIcon;
/** Lower case, mid-sentence: "Choose a collection, or make one." */
noun: string;
/** Key, label, placeholder — in the order the client's sitemap lists them. */
specs: [Extract<keyof T, string>, string, string][];
/** Which of those are long enough to want a textarea. */
long?: Extract<keyof T, string>[];
};
export const COLLECTIONS: SubjectSection<Collection> = {
param: 'collection',
endpoint: '/fofos-admin/collections',
base: '/collections',
icon: Boxes,
noun: 'collection',
specs: [
['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'],
],
long: ['descriptive_text'],
};
export const COLLABS: SubjectSection<Collab> = {
param: 'collab',
endpoint: '/fofos-admin/collabs',
base: '/collabs',
icon: Handshake,
noun: 'collab',
/*
No specs, and that is the difference.
A collab is a PROJECT rather than an object — there is no measurement, no raw material, no
colours to list. The form is a name, an address and a description, which is all the sitemap gives
it, and the template carries no specs block to draw.
*/
specs: [],
};
export const PIECES: SubjectSection<Piece> = {
param: 'piece',
endpoint: '/fofos-admin/pieces',
base: '/gallery',
icon: Shapes,
noun: 'piece',
/*
The last two are the sitemap's NOTAS* — conditions of buying the object rather than properties of
it. Two more fields here, because that is honestly all the difference amounts to; the template is
where they are drawn apart.
*/
specs: [
['descriptive_text', 'Descriptive text', 'What this piece is'],
['measurement', 'Measurement', '45cm x 45cm'],
['material', 'Raw material', 'Acrylic yarn'],
['colors', 'Colours', ''],
['production_notes', 'Production time', 'Made to order, 4-6 weeks'],
['shipping_notes', 'Shipping', 'Ships worldwide, tracked'],
],
long: ['descriptive_text'],
};