Video, chosen from the media store
The picker offers it now, and choosing it opens the media picker rather than a dialog. A video is a PATH, saved as soon as it is chosen: free text needs a draft because writing is something people do halfway, and picking a file is one act with one outcome. Edit means whatever editing that KIND means. The canvas draws one button; which door it opens is decided in the panel, where the kinds are known — so Edit on a video reopens the picker and Edit on text opens the editor. Image and Gallery stay named and disabled. Both are things the site already has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
* 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 = {
|
type Choice = {
|
||||||
kind: ComponentKind | null;
|
kind: ComponentKind | null;
|
||||||
@@ -42,7 +42,7 @@ type Choice = {
|
|||||||
const CHOICES: Choice[] = [
|
const CHOICES: Choice[] = [
|
||||||
{ kind: 'free-text', label: 'Text', hint: 'Words, headings and links', Icon: Type },
|
{ 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: '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 },
|
{ kind: null, label: 'Gallery', hint: 'A named set of media', Icon: Images },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { paintCanvas, type CanvasComponent, type CanvasSection } from '../canvas
|
|||||||
import { AddSectionDialog, type SectionLayout } from './AddSectionDialog';
|
import { AddSectionDialog, type SectionLayout } from './AddSectionDialog';
|
||||||
import { FreeTextDialog } from './FreeTextDialog';
|
import { FreeTextDialog } from './FreeTextDialog';
|
||||||
import { AddComponentDialog, type ComponentKind } from './AddComponentDialog';
|
import { AddComponentDialog, type ComponentKind } from './AddComponentDialog';
|
||||||
|
import { MediaPicker } from '../MediaPicker';
|
||||||
import { pageFromParam, usePages, type Page } from '../usePages';
|
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>(
|
const [editing, setEditing] = useState<{ id: number | null; sectionId: number; column: number; html: string } | null>(
|
||||||
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.
|
Bumped after a write, and passed to SitePreview to remount the frame.
|
||||||
|
|
||||||
@@ -193,15 +196,48 @@ export const PageBuilderPanel = () => {
|
|||||||
const chooseKind = (kind: ComponentKind) => {
|
const chooseKind = (kind: ComponentKind) => {
|
||||||
if (!choosing) return;
|
if (!choosing) return;
|
||||||
setChoosing(null);
|
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 === '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 editComponent = (component: CanvasComponent) => {
|
||||||
const owner = sections.find((section) => section.components.some((one) => one.id === component.id));
|
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 : '';
|
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) => {
|
const saveText = async (html: string) => {
|
||||||
@@ -276,6 +312,13 @@ export const PageBuilderPanel = () => {
|
|||||||
|
|
||||||
<AddComponentDialog open={choosing !== null} onCancel={() => setChoosing(null)} onChoose={chooseKind} />
|
<AddComponentDialog open={choosing !== null} onCancel={() => setChoosing(null)} onChoose={chooseKind} />
|
||||||
|
|
||||||
|
<MediaPicker
|
||||||
|
open={picking !== null}
|
||||||
|
onClose={() => setPicking(null)}
|
||||||
|
onPick={(path) => void saveVideo(path)}
|
||||||
|
title="Choose a video"
|
||||||
|
/>
|
||||||
|
|
||||||
<FreeTextDialog
|
<FreeTextDialog
|
||||||
open={editing !== null}
|
open={editing !== null}
|
||||||
busy={creating}
|
busy={creating}
|
||||||
|
|||||||
Reference in New Issue
Block a user