The shape is one decision for the whole section and stays on the template — every collab is a 21:9 band or none of them is. Which part of THIS photograph survives that shape is a decision about this photograph, and there is one per row. A single rectangle chosen against one lead and applied to all of them is how a portrait ends up cropped through somebody's head, which is what the block-level crop did and why it is gone. So a row's settings now carry Main image: a picture and a cropping frame in the proportions the page asked for. Empty means the first item of its carousel, which is the ordinary state and is not stored — reorder the carousel and the lead follows. Choosing one is the deliberate act of saying "not that one", and it may be a picture the carousel does not contain at all. `cover_shape` rides along with each row so this panel can draw the right frame without knowing how a template is assembled. SHAPES moved out of PageBuilderPanel because two screens offer them now, and two lists is two places to add the next ratio. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
147 lines
5.2 KiB
TypeScript
147 lines
5.2 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;
|
|
/** The row's own lead, or null meaning "the first item of its carousel". */
|
|
cover_url: string | null;
|
|
/** How that picture is framed, `x,y,w,h` in percentages. Null is centred. */
|
|
cover_crop: string | null;
|
|
/**
|
|
* The shape the template frames a lead into, or null for the picture's own.
|
|
*
|
|
* Read off the page, not the row — one decision for the whole section — and sent with the row so
|
|
* this panel can draw a cropping frame of the right proportions without knowing how a template is
|
|
* put together.
|
|
*/
|
|
cover_shape: number | null;
|
|
};
|
|
|
|
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'],
|
|
};
|