a media slot is the picture, at the shape the page will show it
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>
This commit is contained in:
+14
-48
@@ -1,12 +1,12 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { FolderOpen, Loader2 } from 'lucide-react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { readableError } from '../errors';
|
||||
import { useFlash } from '../useFlash';
|
||||
import { RichText } from '../RichText';
|
||||
import { MediaPicker } from '../MediaPicker';
|
||||
import { MediaField } from '../MediaField';
|
||||
import { useSaveTarget } from '../SaveTarget';
|
||||
|
||||
/**
|
||||
@@ -47,9 +47,6 @@ type About = {
|
||||
manifesto_text: string | null;
|
||||
};
|
||||
|
||||
/** Which media slot the picker is open for, or null. The two cannot be open at once. */
|
||||
type Picking = 'header' | 'product' | null;
|
||||
|
||||
export const AboutPanel = () => {
|
||||
const client = useClient();
|
||||
const clientRef = useRef(client);
|
||||
@@ -65,7 +62,6 @@ export const AboutPanel = () => {
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [saved, flashSaved] = useFlash();
|
||||
const [picking, setPicking] = useState<Picking>(null);
|
||||
|
||||
/** Every write answers with the whole payload, so this is the only place state is set. */
|
||||
const apply = useCallback((data: About) => {
|
||||
@@ -154,20 +150,10 @@ export const AboutPanel = () => {
|
||||
<p className="text-xs text-muted-foreground">The photograph at the top, and the line over it.</p>
|
||||
</div>
|
||||
|
||||
{/* Chosen from originals/, never typed. The same picker Home's hero uses — a path somebody
|
||||
types is a path that is wrong by one character and renders as a broken image. */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={headerUrl}
|
||||
readOnly
|
||||
placeholder="No photograph chosen"
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<Button variant="outline" size="sm" className="shrink-0" onClick={() => setPicking('header')}>
|
||||
<FolderOpen className="size-3.5" />
|
||||
Browse
|
||||
</Button>
|
||||
</div>
|
||||
{/* 21/9 because that is what the page uses — `aspect-[21/9]` in about/partials.tsx. The two
|
||||
have to agree or the admin shows a shape the site does not have, which is worse than showing
|
||||
no shape at all: it looks decided. */}
|
||||
<MediaField value={headerUrl || null} onChange={(path) => setHeaderUrl(path ?? '')} aspect="21/9" label="header photograph" />
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-muted-foreground">Phrase</label>
|
||||
@@ -191,18 +177,14 @@ export const AboutPanel = () => {
|
||||
<p className="text-xs text-muted-foreground">What you make, and a video of it being made.</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={productVideo}
|
||||
readOnly
|
||||
placeholder="No video chosen"
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<Button variant="outline" size="sm" className="shrink-0" onClick={() => setPicking('product')}>
|
||||
<FolderOpen className="size-3.5" />
|
||||
Browse
|
||||
</Button>
|
||||
</div>
|
||||
{/* 16/9, matching the `aspect-video` the product block draws. */}
|
||||
<MediaField
|
||||
value={productVideo || null}
|
||||
onChange={(path) => setProductVideo(path ?? '')}
|
||||
aspect="16/9"
|
||||
kind="video"
|
||||
label="product video"
|
||||
/>
|
||||
|
||||
<RichText value={productText} onChange={setProductText} placeholder="What the product is." />
|
||||
</section>
|
||||
@@ -218,22 +200,6 @@ export const AboutPanel = () => {
|
||||
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
|
||||
{/*
|
||||
One picker, two slots.
|
||||
|
||||
`picking` names which one asked, so the chosen path lands in the right field. Two pickers would
|
||||
be two copies of a modal that already knows how to browse originals/.
|
||||
*/}
|
||||
<MediaPicker
|
||||
open={picking !== null}
|
||||
onClose={() => setPicking(null)}
|
||||
title={picking === 'product' ? 'Choose the product video' : 'Choose the header photograph'}
|
||||
onPick={(path) => {
|
||||
if (picking === 'product') setProductVideo(path);
|
||||
else setHeaderUrl(path);
|
||||
setPicking(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user