diff --git a/web/canvas.ts b/web/canvas.ts index 6ab8f1d..f5ec30d 100644 --- a/web/canvas.ts +++ b/web/canvas.ts @@ -62,7 +62,7 @@ export type CanvasComponent = { would then need a cast, and the point of naming them is that the compiler knows `free-text` holds `html` and `gallery` holds `galleryId`. */ - content?: { html?: string; galleryId?: number | null; url?: string }; + content?: { html?: string; galleryId?: number | null; url?: string; ratio?: number; crop?: string }; padding?: Record; }; export type CanvasSection = { diff --git a/web/sections/PageBuilderPanel.tsx b/web/sections/PageBuilderPanel.tsx index 595e848..5db8a44 100644 --- a/web/sections/PageBuilderPanel.tsx +++ b/web/sections/PageBuilderPanel.tsx @@ -94,6 +94,7 @@ export const PageBuilderPanel = () => { remove: (id: number) => void; addComponent: (section: CanvasSection, column: number) => void; editComponent: (component: CanvasComponent) => void; + setContent: (component: CanvasComponent, content: Record) => Promise; removeComponent: (component: CanvasComponent) => void; spaceSection: (section: CanvasSection) => void; spaceComponent: (component: CanvasComponent) => void; @@ -102,6 +103,7 @@ export const PageBuilderPanel = () => { remove: () => {}, addComponent: () => {}, editComponent: () => {}, + setContent: async () => {}, removeComponent: () => {}, spaceSection: () => {}, spaceComponent: () => {}, @@ -471,6 +473,28 @@ export const PageBuilderPanel = () => { /* Held so the effect that reads the URL can call it without depending on its identity. */ const editComponentRef = useRef<(component: CanvasComponent) => void>(() => {}); + /** + * Write a block's whole `content`. + * + * Whole, because the API replaces it rather than merging — so every caller sends what is already + * there alongside what it is changing. That is the contract for a jsonb column somebody can put + * anything in: a merge would need the server to know which keys are meaningful, and it does not. + */ + const setContent = async (component: CanvasComponent, content: Record) => { + const owner = sections.find((section) => section.components.some((one) => one.id === component.id)); + if (!page || !owner) return; + setError(null); + try { + await clientRef.current.patch( + `/fofos-admin/pages/${page.id}/sections/${owner.id}/components/${component.id}`, + { content }, + ); + setRendered((n) => n + 1); + } catch (failure) { + setError(readableError(failure)); + } + }; + const editComponent = (component: CanvasComponent) => { const owner = sections.find((section) => section.components.some((one) => one.id === component.id)); if (!owner) return; @@ -728,6 +752,44 @@ export const PageBuilderPanel = () => { + {/* + The shape of the slot, for a PHOTOGRAPH. + + It is how you make a band shorter than the picture in it: the ratio becomes a slot + shape, and the site asks av-club for a rendition cut to that — so the bytes served + match the space rather than being scaled down by the browser. + + Not offered for a video. There is nothing to crop and a ratio would letterbox it. + + `content` is replaced wholesale by the API, so this sends what is already there + alongside the new value — a PATCH with `{ ratio }` alone would drop the url. + */} + {block.content?.url && !/\.(mp4|mov|webm|m4v)$/i.test(block.content.url) && ( +
+ + +
+ )} + {/* What is actually in the block, at the width of the column. @@ -805,6 +867,7 @@ export const PageBuilderPanel = () => { remove: (sectionId: number) => void deleteSection(sectionId), addComponent, editComponent, + setContent, removeComponent: (component) => void deleteComponent(component), spaceSection, spaceComponent,