diff --git a/web/sections/AddComponentDialog.tsx b/web/sections/AddComponentDialog.tsx index f90470e..c8f4e49 100644 --- a/web/sections/AddComponentDialog.tsx +++ b/web/sections/AddComponentDialog.tsx @@ -23,7 +23,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u * A kind the server would accept and this has never heard of is not creatable anyway. */ -export type ComponentKind = 'free-text'; +export type ComponentKind = 'free-text' | 'video'; type Choice = { kind: ComponentKind | null; @@ -42,7 +42,7 @@ type Choice = { const CHOICES: Choice[] = [ { kind: 'free-text', label: 'Text', hint: 'Words, headings and links', Icon: Type }, { kind: null, label: 'Image', hint: 'One picture from the media store', Icon: Images }, - { kind: null, label: 'Video', hint: 'A video, with a poster', Icon: Video }, + { kind: 'video', label: 'Video', hint: 'No controls, a poster, autoplay and loop', Icon: Video }, { kind: null, label: 'Gallery', hint: 'A named set of media', Icon: Images }, ]; diff --git a/web/sections/PageBuilderPanel.tsx b/web/sections/PageBuilderPanel.tsx index 1061299..0207bd9 100644 --- a/web/sections/PageBuilderPanel.tsx +++ b/web/sections/PageBuilderPanel.tsx @@ -8,6 +8,7 @@ import { paintCanvas, type CanvasComponent, type CanvasSection } from '../canvas import { AddSectionDialog, type SectionLayout } from './AddSectionDialog'; import { FreeTextDialog } from './FreeTextDialog'; import { AddComponentDialog, type ComponentKind } from './AddComponentDialog'; +import { MediaPicker } from '../MediaPicker'; import { pageFromParam, usePages, type Page } from '../usePages'; /** @@ -67,6 +68,8 @@ export const PageBuilderPanel = () => { const [editing, setEditing] = useState<{ id: number | null; sectionId: number; column: number; html: string } | null>( null, ); + /** Choosing the file for a video — new when `id` is null, otherwise replacing an existing one. */ + const [picking, setPicking] = useState<{ id: number | null; sectionId: number; column: number } | null>(null); /* Bumped after a write, and passed to SitePreview to remount the frame. @@ -193,15 +196,48 @@ export const PageBuilderPanel = () => { const chooseKind = (kind: ComponentKind) => { if (!choosing) return; setChoosing(null); - // Only one kind reaches here today. When there are more, this is where each one's editor opens. + // Each kind opens its own editor. A kind with no case here cannot be created, which is the point. if (kind === 'free-text') setEditing({ id: null, ...choosing, html: '' }); + if (kind === 'video') setPicking({ id: null, ...choosing }); + }; + + /* + A video is a PATH, saved as soon as it is chosen. + + Free text needs a dialog because writing is a thing people do halfway; picking a file is one act + with one outcome, so there is nothing to draft and nothing to cancel out of afterwards. + */ + const saveVideo = async (url: string) => { + if (!page || !picking) return; + const base = `/fofos-admin/pages/${page.id}/sections/${picking.sectionId}/components`; + setPicking(null); + setError(null); + try { + if (picking.id === null) { + await clientRef.current.post(base, { kind: 'video', column: picking.column, content: { url } }); + } else { + await clientRef.current.patch(`${base}/${picking.id}`, { content: { url } }); + } + 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)); - const stored = owner?.components.find((one) => one.id === component.id); + if (!owner) return; + + // Edit means whatever editing THIS kind means. The canvas draws one button; which door it opens is + // decided here, where the kinds are known. + if (component.kind === 'video') { + setPicking({ id: component.id, sectionId: owner.id, column: component.column }); + return; + } + + const stored = owner.components.find((one) => one.id === component.id); const html = typeof stored?.content?.html === 'string' ? stored.content.html : ''; - setEditing({ id: component.id, sectionId: owner?.id ?? 0, column: component.column, html }); + setEditing({ id: component.id, sectionId: owner.id, column: component.column, html }); }; const saveText = async (html: string) => { @@ -276,6 +312,13 @@ export const PageBuilderPanel = () => { setChoosing(null)} onChoose={chooseKind} /> + setPicking(null)} + onPick={(path) => void saveVideo(path)} + title="Choose a video" + /> +