55 lines
1.7 KiB
TypeScript
55 lines
1.7 KiB
TypeScript
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<string | null>(crop);
|
|
|
|
useEffect(() => setDraft(crop), [url, aspect]);
|
|
|
|
useAutosave(draft !== crop ? (draft ?? '') : null, false, () => onCrop(draft));
|
|
|
|
return (
|
|
<MediaField
|
|
value={url}
|
|
onChange={onUrl}
|
|
crop={draft}
|
|
onCropChange={setDraft}
|
|
aspect={aspect}
|
|
label="Choose a photograph"
|
|
/>
|
|
);
|
|
};
|