"use client"; import { useState, useCallback } from "react"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { createVersion } from "@/actions/versions"; import { useToast } from "@/components/ui/use-toast"; import { Upload, Film, X, CheckCircle2 } from "lucide-react"; import { formatFileSize } from "@/lib/utils"; import { cn } from "@/lib/utils"; interface VersionUploadProps { taskId: string; projectId: string; currentVersionNumber: number; open: boolean; onClose: () => void; onSuccess?: (versionId: string) => void; } export function VersionUpload({ taskId, projectId, currentVersionNumber, open, onClose, onSuccess, }: VersionUploadProps) { const [file, setFile] = useState(null); const [notes, setNotes] = useState(""); const [fps, setFps] = useState("24"); const [uploadProgress, setUploadProgress] = useState(0); const [uploadState, setUploadState] = useState< "idle" | "uploading" | "processing" | "done" | "error" >("idle"); const [isDragOver, setIsDragOver] = useState(false); const { toast } = useToast(); const router = useRouter(); const nextVersion = currentVersionNumber + 1; const versionLabel = `v${String(nextVersion).padStart(3, "0")}`; const acceptedTypes = "video/mp4,video/quicktime,video/x-msvideo,.mp4,.mov,.avi,.mxf"; const handleFileSelect = (selectedFile: File) => { if (!selectedFile.type.match(/video\//)) { toast({ title: "Please select a video file (.mp4, .mov)", variant: "destructive" }); return; } if (selectedFile.size > 2 * 1024 * 1024 * 1024) { toast({ title: "File too large (max 2GB)", variant: "destructive" }); return; } setFile(selectedFile); }; const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); const dropped = e.dataTransfer.files[0]; if (dropped) handleFileSelect(dropped); }, []); const handleUpload = async () => { if (!file) return; setUploadState("uploading"); setUploadProgress(0); try { // Upload via local API (XHR for progress) or UploadThing const fileUrl = await uploadViaLocal(file, (p) => setUploadProgress(Math.round(p * 0.85)) ); setUploadProgress(90); setUploadState("processing"); const result = await createVersion({ taskId, fileUrl, fileName: file.name, fileSize: file.size, mimeType: file.type, fps: parseFloat(fps) || 24, notes: notes.trim() || undefined, }); setUploadProgress(100); setUploadState("done"); toast({ title: `${versionLabel} uploaded successfully` }); setTimeout(() => { onSuccess?.(result.version.id); router.refresh(); onClose(); resetState(); }, 1000); } catch (err) { setUploadState("error"); toast({ title: "Upload failed", description: (err as Error).message, variant: "destructive", }); } }; const resetState = () => { setFile(null); setNotes(""); setFps("24"); setUploadProgress(0); setUploadState("idle"); }; const handleClose = () => { if (uploadState === "uploading") return; resetState(); onClose(); }; return ( !o && handleClose()}> Upload {versionLabel}
{/* Drop zone */} {!file ? (
{ e.preventDefault(); setIsDragOver(true); }} onDragLeave={() => setIsDragOver(false)} onDrop={handleDrop} className={cn( "relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-10 transition-colors cursor-pointer", isDragOver ? "border-amber-500 bg-amber-500/10" : "border-zinc-700 hover:border-amber-500/50 hover:bg-zinc-800/50" )} onClick={() => document.getElementById("file-input")?.click()} > e.target.files?.[0] && handleFileSelect(e.target.files[0])} />

Drop video here or click to browse

MP4, MOV, AVI up to 2 GB

) : (

{file.name}

{formatFileSize(file.size)}

{uploadState === "idle" && ( )} {uploadState === "done" && ( )}
)} {/* Metadata */} {file && uploadState === "idle" && ( <>
setFps(e.target.value)} placeholder="24" className="h-8 text-sm" />