scrape / app /export /page.tsx
kjbytes's picture
push code
ef4c36f
Raw
History Blame Contribute Delete
4.94 kB
"use client";
import { Suspense, useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { ArrowLeft, Loader2 } from "lucide-react";
import type { Project, RenderProgress } from "@/types";
import { ExportProgress } from "@/components/export-progress";
import { VideoPreview } from "@/components/video-preview";
import {
DEFAULT_ANIMATION,
DEFAULT_LAYOUT,
DEFAULT_STYLE,
} from "@/lib/defaults";
function ExportContent() {
const searchParams = useSearchParams();
const router = useRouter();
const id = searchParams.get("id");
const [project, setProject] = useState<Project | null>(null);
const [loading, setLoading] = useState(true);
const [rendering, setRendering] = useState(false);
const [progress, setProgress] = useState<RenderProgress | null>(null);
const [downloadUrl, setDownloadUrl] = useState<string>();
const [filename, setFilename] = useState<string>();
useEffect(() => {
if (!id) {
router.push("/");
return;
}
void (async () => {
const res = await fetch(`/api/project?id=${id}`);
const data = await res.json();
if (!res.ok) {
router.push("/");
return;
}
setProject(data);
setLoading(false);
})();
}, [id, router]);
const pollProgress = useCallback(async () => {
if (!id) return;
const res = await fetch(`/api/progress?id=${id}`);
const data = (await res.json()) as RenderProgress;
setProgress(data);
if (data.stage === "done" || data.stage === "error") {
setRendering(false);
}
}, [id]);
const startRender = useCallback(async () => {
if (!project || !id) return;
setRendering(true);
setProgress({ percent: 0, stage: "extracting", message: "Starting..." });
const pollInterval = setInterval(() => void pollProgress(), 1000);
try {
const res = await fetch("/api/render", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id,
captions: project.captions,
style: project.style,
layout: project.layout,
animation: project.animation,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
await new Promise<void>((resolve, reject) => {
const waitForDone = setInterval(async () => {
const progressRes = await fetch(`/api/progress?id=${id}`);
const progressData = (await progressRes.json()) as RenderProgress;
setProgress(progressData);
if (progressData.stage === "done") {
clearInterval(waitForDone);
setDownloadUrl(`/api/download/${id}`);
setFilename(`captioned_${id}.mp4`);
resolve();
} else if (progressData.stage === "error") {
clearInterval(waitForDone);
reject(new Error(progressData.message));
}
}, 1000);
});
} catch (error) {
setProgress({
percent: 0,
stage: "error",
message: error instanceof Error ? error.message : "Render failed",
});
} finally {
clearInterval(pollInterval);
setRendering(false);
}
}, [project, id, pollProgress]);
if (loading || !project || !id) {
return (
<div className="flex min-h-screen items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-zinc-400" />
</div>
);
}
return (
<div className="min-h-screen bg-zinc-50">
<header className="border-b border-zinc-200 bg-white">
<div className="mx-auto flex max-w-3xl items-center gap-4 px-6 py-4">
<Link href={`/editor?id=${id}`} className="flex items-center gap-2 text-sm text-zinc-600 hover:text-zinc-900">
<ArrowLeft className="h-4 w-4" /> Edit captions
</Link>
<h1 className="font-bold">Export Video</h1>
</div>
</header>
<main className="mx-auto max-w-3xl space-y-8 px-6 py-10">
<VideoPreview
videoUrl={`/api/video/${id}`}
audioUrl={project.audioPath ? `/api/audio/${id}` : undefined}
captions={project.captions}
style={project.style ?? DEFAULT_STYLE}
layout={project.layout ?? DEFAULT_LAYOUT}
animation={project.animation ?? DEFAULT_ANIMATION}
videoMeta={project.videoMeta}
/>
<ExportProgress
progress={progress}
downloadUrl={downloadUrl}
filename={filename}
rendering={rendering}
onStartRender={() => void startRender()}
/>
</main>
</div>
);
}
export default function ExportPage() {
return (
<Suspense fallback={<div className="flex min-h-screen items-center justify-center"><Loader2 className="h-8 w-8 animate-spin" /></div>}>
<ExportContent />
</Suspense>
);
}