diff --git a/web/sections/BlockMedia.tsx b/web/sections/BlockMedia.tsx new file mode 100644 index 0000000..ccd1318 --- /dev/null +++ b/web/sections/BlockMedia.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from 'react'; +import { MediaField } from '../MediaField'; +import { useAutosave } from '../useAutosave'; + +/** + * A photograph block's picture and its framing. + * + * ── Why the crop is held here ── + * + * `MediaField` is controlled: it reports every pointer move and draws whatever it is handed back. Wired + * straight to the block, that meant a PATCH and a full reload of the preview PER MOVE — the page went + * white while somebody dragged, which is what this exists to stop. + * + * So the drag runs against local state, which is instant, and the write is debounced by `useAutosave` + * — one request when the hand stops, not sixty on the way there. The same trade every other control in + * this admin makes, and the same hook. + * + * ── Reset by IDENTITY, not by value ── + * + * The effect keys on the url and the shape rather than on the crop. Keyed on the crop it would fight + * the drag: the parent's value is the last SAVED one, so every move would be pulled back to it until + * the save landed. + */ +export const BlockMedia = ({ + url, + crop, + aspect, + onUrl, + onCrop, +}: { + url: string | null; + crop: string | null; + /** Tailwind's ratio, as the page spells it — `21/9`, `1/1`. */ + aspect: string; + onUrl: (url: string | null) => void; + onCrop: (crop: string | null) => void; +}) => { + const [draft, setDraft] = useState(crop); + + useEffect(() => setDraft(crop), [url, aspect]); + + useAutosave(draft !== crop ? (draft ?? '') : null, false, () => onCrop(draft)); + + return ( + + ); +}; diff --git a/web/sections/PageBuilderPanel.tsx b/web/sections/PageBuilderPanel.tsx index 950df3c..41bfe22 100644 --- a/web/sections/PageBuilderPanel.tsx +++ b/web/sections/PageBuilderPanel.tsx @@ -38,7 +38,7 @@ import { BandSettings } from './BandSettings'; import { BlockSettings } from './BlockSettings'; import { GalleryItems } from './GalleryItems'; import { PartnersEditor } from './PartnersEditor'; -import { MediaField } from '../MediaField'; +import { BlockMedia } from './BlockMedia'; /** * The shapes a photograph block may take. @@ -807,15 +807,14 @@ export const PageBuilderPanel = () => { )} {shapeOf(block) ? ( - void handlersRef.current.setContent(block, { ...block.content, url: url ?? '' })} + - void handlersRef.current.setContent(block, { ...block.content, crop: crop ?? undefined }) - } aspect={shapeOf(block)!} - label="Choose a photograph" + onUrl={(url) => void handlersRef.current.setContent(block, { ...block.content, url: url ?? '' })} + onCrop={(crop) => + void handlersRef.current.setContent(block, { ...block.content, crop: crop || undefined }) + } /> ) : ( <>