Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
31 lines
3.8 KiB
TypeScript
31 lines
3.8 KiB
TypeScript
"use client";
|
|
import { useState } from "react";
|
|
import { Button } from "../ui/primitives";
|
|
import type { Overlay } from "./types";
|
|
export interface RenderInput { templateId: "lower_third" | "chyron" | "logo"; fields: { title?: string; subtitle?: string; style?: "dark" | "red"; svg?: string } }
|
|
function Editor({ overlay, disabled, onRender }: { overlay: Overlay; disabled: boolean; onRender: (id: string, input: RenderInput) => Promise<void> }): React.ReactElement {
|
|
const [title, setTitle] = useState(""); const [subtitle, setSubtitle] = useState(""); const [style, setStyle] = useState<"dark" | "red">("dark"); const [svg, setSvg] = useState("");
|
|
const [busy, setBusy] = useState(false); const [message, setMessage] = useState("");
|
|
if (overlay.kind === "clock") return <p className="text-xs text-text-faint">Clock overlay is server-generated.</p>;
|
|
const templateId = overlay.kind;
|
|
return <form className="min-w-0 space-y-3" onSubmit={event => {
|
|
event.preventDefault(); setBusy(true); setMessage("");
|
|
const input: RenderInput = { templateId, fields: templateId === "logo" ? { svg } : { title, subtitle, style } };
|
|
void onRender(overlay.id, input).then(() => setMessage("Render accepted. Waiting for the current revision in status.")).catch(error => setMessage(error instanceof Error ? error.message : "Render failed")).finally(() => setBusy(false));
|
|
}}>
|
|
<p className="text-xs text-text-faint">Template · {templateId}</p>
|
|
{templateId === "logo" ? <label className="block text-xs">Logo SVG<textarea className="cr-field mt-1 min-h-28" required maxLength={64000} value={svg} onChange={event => setSvg(event.target.value)} placeholder="<svg>…</svg>" /><span className="mt-1 block text-text-faint">Geometric SVG only; no scripts, external assets, or text.</span></label> : <>
|
|
<label className="block text-xs">{templateId === "chyron" ? "Chyron text" : "Title"} · {title.length}/80<input className="cr-field mt-1" maxLength={80} value={title} onChange={event => setTitle(event.target.value)} /></label>
|
|
<label className="block text-xs">Subtitle · {subtitle.length}/120<input className="cr-field mt-1" maxLength={120} value={subtitle} onChange={event => setSubtitle(event.target.value)} /></label>
|
|
<label className="block text-xs">Style<select className="cr-field mt-1" value={style} onChange={event => setStyle(event.target.value as "dark" | "red")}><option value="dark">Dark</option><option value="red">Red</option></select></label>
|
|
</>}
|
|
<Button className="min-h-11" variant="outline" disabled={disabled || busy} type="submit">{busy ? "Rendering…" : "Render overlay"}</Button>
|
|
<p className="break-all text-xs text-text-faint">Current revision: {overlay.revisionId ?? "None"}</p>
|
|
{message && <p role="status" className="text-xs">{message}</p>}
|
|
</form>;
|
|
}
|
|
export function OverlayEditor({ overlays, disabled, onRender }: { overlays: Overlay[]; disabled: boolean; onRender: (id: string, input: RenderInput) => Promise<void> }): React.ReactElement {
|
|
const [selected, setSelected] = useState("");
|
|
const overlay = overlays.find(item => item.id === selected) ?? overlays[0];
|
|
return <section aria-label="Overlay editor" className="min-w-0 rounded-lg border border-border-subtle bg-surface p-3"><h2 className="mb-3 text-sm font-semibold">Overlay editor</h2>{overlay ? <><label className="mb-3 block text-xs">Overlay slot<select className="cr-field mt-1" value={overlay.id} onChange={event => setSelected(event.target.value)}>{overlays.map((item, index) => <option key={item.id} value={item.id}>{item.kind} · slot {index + 1}</option>)}</select></label><Editor key={overlay.id} overlay={overlay} disabled={disabled} onRender={onRender} /></> : <p className="text-xs text-text-faint">Configure an overlay slot on a production scene to render a lower third, chyron, or logo.</p>}</section>;
|
|
}
|