It was a read-only text box holding a path and a Browse button beside it. A path is not what anybody is choosing — somebody picking the header photograph is choosing a PHOTOGRAPH, and reading `ABOUT US/HEADER.jpg` to find out whether you picked the right one is doing the computer's job for it. MediaField is the slot: empty, it is a dashed box at the page's own aspect ratio that you click or drop onto; filled, it is the picture, with Change and Remove appearing over it on hover. The panel also stops changing height as things are chosen, so a page with three slots starts to look like the page rather than like a form. ── The aspect ratio is a prop and has to be ── The header is 21:9, the product video 16:9, a gallery slot square. The component cannot know that and must not guess: the SITE decides what shape a slot is, and a component that guessed would be a second opinion to keep in step with the stylesheet. Both callers name the ratio their partial draws. ── Dropping ── It accepts a drag from Officer's own file browser, which carries paths under `application/x-officer-files` — the same payload that moves files between windows. So dragging a photograph out of Media onto a slot is one gesture and no upload. It does not accept a file from the desktop. That is an upload into originals/, a different operation with its own failure modes, and the Media section already does it properly. ── Also ── `thumbUrl` was copied into GalleriesPanel and RecordsPanel and was about to be copied a third time. It is a module now: three copies of a URL shape is three places to be wrong about the rung when the ladder changes, which it did four times in a day. A file that has gone since it was chosen falls back to the empty state rather than a browser's broken-image glyph, and says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
35 lines
1.6 KiB
TypeScript
35 lines
1.6 KiB
TypeScript
import { getHeaders } from 'hooks/useClient';
|
|
|
|
/**
|
|
* A thumbnail URL for something under `originals/`, authenticated by `?token=`.
|
|
*
|
|
* ── Why the token is in the query ──
|
|
*
|
|
* An `<img>` cannot send an Authorization header. `user-middleware` and the permission gate both accept
|
|
* the token in the query string for exactly this, and the platform's own file browser does the same.
|
|
* Built without it first, and every picture 401s while the layout looks perfectly correct.
|
|
*
|
|
* ── Why this is a module ──
|
|
*
|
|
* It was copied into GalleriesPanel and again into RecordsPanel, and a third copy was about to go into
|
|
* MediaField. Three copies of a URL shape is three places to be wrong about the rung when the ladder
|
|
* changes — which it did, four times in a day.
|
|
*/
|
|
export const thumbUrl = (path: string, width: 256 | 320 | 480 | 640 = 320): string => {
|
|
const token = getHeaders()['Authorization']?.replace('Bearer ', '') ?? '';
|
|
const encoded = path.split('/').map(encodeURIComponent).join('/');
|
|
return `/api/fofos-admin/derived/${encoded}/${width}.webp?token=${encodeURIComponent(token)}`;
|
|
};
|
|
|
|
/**
|
|
* A video's poster, for the same reason.
|
|
*
|
|
* A video has no width rungs — `poster-448.webp` and friends are cut from its still — so a slot showing
|
|
* one asks for the poster rather than a width that does not exist.
|
|
*/
|
|
export const posterUrl = (path: string): string => {
|
|
const token = getHeaders()['Authorization']?.replace('Bearer ', '') ?? '';
|
|
const encoded = path.split('/').map(encodeURIComponent).join('/');
|
|
return `/api/fofos-admin/derived/${encoded}/poster-512.webp?token=${encodeURIComponent(token)}`;
|
|
};
|