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, onDraft, }: { 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; /** * Every change to the draft, not only the saved one. * * The page shows the same photograph at full size, and it has to move when these buttons do — * otherwise the thumbnail and the page disagree for the 600ms until the save lands, which reads as * the control being broken rather than as it being debounced. */ onDraft?: (crop: string | null) => void; }) => { const [draft, setDraft] = useState(crop); useEffect(() => setDraft(crop), [url, aspect]); useAutosave(draft !== crop ? (draft ?? '') : null, false, () => onCrop(draft)); return ( { setDraft(next); onDraft?.(next); }} aspect={aspect} label="Choose a photograph" /> ); };