import { useEffect, useState } from "react"; import { Link, useLocation } from "react-router-dom"; import { videosApi } from "../api/endpoints"; import type { VideoListItem, VideoQuota } from "../api/types"; import { TrashIcon } from "../components/icons/Icons"; import { EmptyState, errorMessage, formatDate, Loading, Modal } from "../components/ui"; import { useUpgrade } from "../hooks/useUpgrade"; import { PostButton } from "../components/launchpad/PostButton"; const JOB_LABELS: Record = { regenerating: "Changing template", voice_regenerating: "Changing voice", language_regenerating: "Translating", script_regenerating: "Refreshing script", }; /** blog2video's status, as a label and a dot color. */ function statusOf(v: VideoListItem): { label: string; tone: "done" | "ready" | "busy" | "review" | "failed" } { const raw = v.b2v_status ?? ""; if (v.status === "failed" || raw === "failed" || raw === "error") return { label: "Failed", tone: "failed" }; // An edit job on a finished video (blog2video's status, kept on the artifact too). Before "Complete": a video // rendered earlier keeps its MP4 link while it is being changed. const job = JOB_LABELS[raw] ?? JOB_LABELS[v.status]; if (job) return { label: job, tone: "busy" }; if (raw === "done" || v.url) return { label: "Complete", tone: "done" }; if (raw === "generated" || v.status === "ready") return { label: "Generated", tone: "ready" }; if (raw === "awaiting_script_review") return { label: "Review script", tone: "review" }; if (raw === "awaiting_stock_footage_review") return { label: "Review footage", tone: "review" }; if (raw === "rendering" || v.status === "rendering") return { label: "Rendering", tone: "busy" }; return { label: "Generating", tone: "busy" }; } function shortUrl(url: string | null | undefined): string | null { if (!url) return null; const s = url.replace(/^https?:\/\//, ""); return s.length > 34 ? `${s.slice(0, 34)}…` : s; } export default function Videos() { const [videos, setVideos] = useState(null); const [quota, setQuota] = useState(null); // The video in the delete modal, and that delete's progress and error. const [deleting, setDeleting] = useState(null); const [busy, setBusy] = useState(false); const [deleteError, setDeleteError] = useState(null); const [error, setError] = useState(null); const { openUpgrade, status } = useUpgrade(); const notice = (useLocation().state as { notice?: string } | null)?.notice; useEffect(() => { videosApi.list().then(setVideos).catch((e) => setError(errorMessage(e))); videosApi.quota().then(setQuota).catch(() => undefined); }, []); function askDelete(v: VideoListItem) { setDeleteError(null); setDeleting(v); } /** Stays open (and loading) until the delete finishes; a failure shows in the modal so it can be retried. */ async function confirmDelete() { if (!deleting) return; const id = deleting.id; setBusy(true); setDeleteError(null); try { await videosApi.remove(id); setVideos((v) => (v ?? []).filter((x) => x.id !== id)); setDeleting(null); } catch (e) { setDeleteError(errorMessage(e)); } finally { setBusy(false); } } const out = !!quota && quota.used >= quota.limit; return ( // The Library's Videos tab: the allowance on the left, New video on the right, then the list.

{quota && <> {quota.used} of {quota.limit} video{quota.limit === 1 ? "" : "s"} used {quota.resets_at ? ` this period, refills ${formatDate(quota.resets_at)}` : " on your plan"} }

{out && status?.can_upgrade ? ( ) : ( New video )}
{notice &&

{notice}

} {error &&

{error}

} {!videos && !error &&
} {videos?.length === 0 && ( Make your first video} /> )} {videos && videos.length > 0 && (
    {videos.map((v) => { const st = statusOf(v); const source = shortUrl(v.source_url); return (
  • {v.title} {source && {source}} {v.scenes ? {v.scenes} scene{v.scenes === 1 ? "" : "s"} : null} {v.created_at && {formatDate(v.created_at)}} {st.tone === "done" && }
  • ); })}
)} {deleting && ( // While the delete runs, Escape, the backdrop and the close button do nothing. undefined : () => setDeleting(null)}>

"{deleting.title}" will be deleted permanently. This can't be undone.

{deleteError &&

{deleteError}

}
)}
); }