import { memo, useCallback, useState } from "react";
import { VideoFrameThumbnail } from "../ui/VideoFrameThumbnail";
import { Button } from "../ui/Button";
import type { RenderJob } from "./useRenderQueue";
interface RenderQueueItemProps {
job: RenderJob;
projectId: string;
onDelete: () => void;
onCancel: () => void;
}
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`;
const s = Math.round(ms / 1000);
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
}
function formatTimeAgo(timestamp: number): string {
const diff = Date.now() - timestamp;
if (diff < 60000) return "just now";
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
return `${Math.floor(diff / 3600000)}h ago`;
}
/** Static frame extracted once via hidden video + canvas. */
export const RenderQueueItem = memo(function RenderQueueItem({
job,
projectId,
onDelete,
onCancel,
}: RenderQueueItemProps) {
const [hovered, setHovered] = useState(false);
const [videoReady, setVideoReady] = useState(false);
const [confirmingDelete, setConfirmingDelete] = useState(false);
// Direct file URL — serves from disk, survives server restarts
const fileSrc = `/api/projects/${projectId}/renders/file/${job.filename}`;
const handleOpen = useCallback(() => {
window.open(fileSrc, "_blank");
}, [fileSrc]);
const handleDownload = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
const a = document.createElement("a");
a.href = fileSrc;
a.download = job.filename;
a.click();
},
[fileSrc, job.filename],
);
const viewSrc = fileSrc;
const isComplete = job.status === "complete";
const isRendering = job.status === "rendering";
return (
setHovered(true)}
onPointerLeave={() => {
setHovered(false);
setVideoReady(false);
setConfirmingDelete(false);
}}
className="px-3 py-2.5 border-b border-panel-border last:border-0 transition-colors duration-150 hover:bg-panel-hover/30"
>
{/* Thumbnail — static frame; swaps to live video on hover.
A real button so keyboard users can open the render too. */}
{/* Info */}
{job.filename}
{job.durationMs && (
{formatDuration(job.durationMs)}
)}
{isRendering && (
{job.stage || "Rendering"}
{job.progress}%
)}
{job.status === "failed" && job.error && (
{job.error}
)}
{job.status === "cancelled" && (
Cancelled
)}
{!isRendering && (
{formatTimeAgo(job.createdAt)}
)}
{/* Actions — always visible to prevent layout shifts */}
{isRendering ? (
) : confirmingDelete ? (
<>
>
) : (
<>
>
)}
);
});