"use client"; import { useCallback, useEffect, useState } from "react"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import Link from "next/link"; import { ArrowLeft, CheckCircle2, Download, Link2, Loader2, RefreshCw, } from "lucide-react"; import { apiFetch } from "@/lib/api/fetch"; import { RENDER_EXPORT_PRESETS, type RenderExportPreset } from "@/lib/render-presets"; import type { RenderSettings } from "@/lib/render-schemas"; import { cn } from "@/lib/utils"; type Phase = "config" | "submitting" | "polling" | "completed" | "failed"; interface StatusResponse { status: string; progress: number; outputUrl: string | null; progressMessage?: string | null; errorMessage?: string | null; previewB64?: string | null; } interface ActiveRender { id: string; saved_project_id: string; step: string; render_progress: number; } const RESOLUTIONS: RenderSettings["resolution"][] = ["720p", "1080p", "4K"]; const FPS_OPTIONS: RenderSettings["fps"][] = [24, 30, 60]; export default function RenderPage() { const router = useRouter(); const params = useParams<{ projectId: string }>(); const search = useSearchParams(); const projectId = params.projectId; const presetKey = search.get("preset") as RenderExportPreset | null; const [resolution, setResolution] = useState("1080p"); const [fps, setFps] = useState(30); const [phase, setPhase] = useState("config"); const [jobId, setJobId] = useState(null); const [progress, setProgress] = useState(0); const [progressMessage, setProgressMessage] = useState(""); const [previewB64, setPreviewB64] = useState(null); const [outputUrl, setOutputUrl] = useState(null); const [errorMessage, setErrorMessage] = useState(null); // An active render that belongs to a DIFFERENT project (blocks starting a new one). const [blockingJobId, setBlockingJobId] = useState(null); // Apply preset from the query (?preset=full) useEffect(() => { if (!presetKey || !RENDER_EXPORT_PRESETS[presetKey]) return; const cfg = RENDER_EXPORT_PRESETS[presetKey]; setResolution(cfg.settings.resolution); setFps(cfg.settings.fps); }, [presetKey]); // On mount: resume this project's render if one is in flight, or flag a render // running on another project so we can block starting a new one. useEffect(() => { let cancelled = false; (async () => { try { const res = await apiFetch("/api/render/active"); const data = (await res.json()) as { active?: ActiveRender[] }; if (cancelled) return; const mine = data.active?.find((a) => a.saved_project_id === projectId); if (mine) { setJobId(mine.id); setPhase("polling"); setProgress(mine.render_progress ?? 0); return; } const other = data.active?.[0]; if (other) setBlockingJobId(other.id); } catch { /* ignore — config view will show */ } })(); return () => { cancelled = true; }; }, [projectId]); // Poll status while rendering. useEffect(() => { if (phase !== "polling" || !jobId) return; const poll = async () => { try { const res = await apiFetch(`/api/render/${jobId}/status`); const data = (await res.json()) as StatusResponse; if (!res.ok) { setPhase("failed"); setErrorMessage("Could not fetch render status."); return; } setProgress(data.progress ?? 0); setProgressMessage(data.progressMessage ?? `Rendering… ${data.progress}%`); if (data.previewB64) setPreviewB64(data.previewB64); if (data.status === "completed") { if (data.outputUrl) setOutputUrl(data.outputUrl); setProgress(100); setPhase("completed"); } else if (data.status === "failed") { setPhase("failed"); setErrorMessage(data.errorMessage ?? "Render failed."); } } catch { setPhase("failed"); setErrorMessage("Network error while polling status."); } }; poll(); const id = window.setInterval(poll, 2500); return () => window.clearInterval(id); }, [phase, jobId]); const startRender = useCallback(async () => { setPhase("submitting"); setErrorMessage(null); try { const res = await apiFetch("/api/render", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ projectId, settings: { resolution, format: "mp4" as const, fps }, }), }); const data = (await res.json()) as { jobId?: string; error?: string; code?: string }; if (res.status === 409 || data.code === "active_render_limit") { setPhase("config"); setErrorMessage( data.error ?? "You already have an active render. Wait for it to finish." ); return; } if (!res.ok || !data.jobId) { setPhase("failed"); setErrorMessage(data.error ?? "Failed to start render."); return; } setJobId(data.jobId); setProgress(0); setProgressMessage("Queued for rendering…"); setPhase("polling"); } catch { setPhase("failed"); setErrorMessage("Could not reach the render service."); } }, [projectId, resolution, fps]); const backToStudio = `/studio/video/${projectId}`; const isBusy = phase === "submitting" || phase === "polling"; return (
{/* Top bar */}
بازگشت به استودیو خروجی رندر
{/* Preview / hero */}
{previewB64 ? ( // eslint-disable-next-line @next/next/no-img-element Render preview ) : (
{isBusy ? ( ) : phase === "completed" ? ( ) : ( پیش‌نمایش رندر اینجا نمایش داده می‌شود )}
)} {isBusy && (
{progressMessage || "در حال رندر…"} {progress}%
)}
{/* State-specific panel */} {phase === "completed" ? (

ویدیوی شما آماده است!

{outputUrl ? ( <> دانلود MP4 لینک اشتراک‌گذاری ) : (

رندر در محیط توسعه کامل شد (بدون فایل خروجی واقعی). در محیط تولید، لینک دانلود اینجا نمایش داده می‌شود.

)} بازگشت به استودیو
) : phase === "failed" ? (

{errorMessage ?? "خطایی رخ داد."}

) : isBusy ? (

می‌توانید این صفحه را ببندید؛ رندر در پس‌زمینه ادامه می‌یابد و از هر صفحه‌ای قابل پیگیری است.

) : ( // Config
{errorMessage && (

{errorMessage}

)} {blockingJobId && (
شما یک رندر فعال دارید.{" "}
)}

کیفیت

{RESOLUTIONS.map((item) => ( ))}

نرخ فریم

{FPS_OPTIONS.map((item) => ( ))}
)}
); }