"use client"; import { useRef, useState } from "react"; import { Video, Upload, X, Plus, Pencil, Trash2, Film } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { addFootagePlate, removeFootagePlate, renameFootagePlate } from "@/actions/shots"; import { useToast } from "@/components/ui/use-toast"; import { cn } from "@/lib/utils"; interface Plate { id: string; label: string; fileUrl: string; fileKey: string; fileName: string; fileSize: bigint | string | null; sortOrder: number; createdAt: Date; } interface FootageViewerProps { shot: { id: string; shotCode: string; footagePlates: Plate[]; }; canManage: boolean; onSaved?: () => void; } function uploadViaXhr( file: File, onProgress: (fraction: number) => void ): Promise<{ url: string; key: string }> { return new Promise((resolve, reject) => { const formData = new FormData(); formData.append("file", file); const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/upload/local"); xhr.upload.addEventListener("progress", (e) => { if (e.lengthComputable) onProgress(e.loaded / e.total); }); xhr.addEventListener("load", () => { if (xhr.status >= 200 && xhr.status < 300) { try { const json = JSON.parse(xhr.responseText); if (json.url) resolve({ url: json.url, key: json.key ?? "" }); else reject(new Error(json.error ?? "Upload failed")); } catch { reject(new Error("Invalid server response")); } } else { try { const json = JSON.parse(xhr.responseText); reject(new Error(json.error ?? `HTTP ${xhr.status}`)); } catch { reject(new Error(`HTTP ${xhr.status}`)); } } }); xhr.addEventListener("error", () => reject(new Error("Network error"))); xhr.addEventListener("abort", () => reject(new Error("Upload aborted"))); xhr.send(formData); }); } function formatSize(size: bigint | string | null): string { if (size == null) return ""; const n = typeof size === "string" ? parseInt(size, 10) : Number(size); if (isNaN(n)) return ""; if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} KB`; return `${(n / 1024 / 1024).toFixed(1)} MB`; } export function FootageViewer({ shot, canManage, onSaved }: FootageViewerProps) { const { toast } = useToast(); const fileInputRef = useRef(null); const [plates, setPlates] = useState(shot.footagePlates ?? []); const [selectedId, setSelectedId] = useState( (shot.footagePlates ?? [])[0]?.id ?? null ); const [pendingFile, setPendingFile] = useState(null); const [pendingLabel, setPendingLabel] = useState(""); const [uploading, setUploading] = useState(false); const [progress, setProgress] = useState(0); const [editingId, setEditingId] = useState(null); const [editLabel, setEditLabel] = useState(""); const [deletingId, setDeletingId] = useState(null); const selectedPlate = plates.find((p) => p.id === selectedId) ?? null; // ── upload new plate ────────────────────────────────────────────────────── const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setPendingFile(file); setPendingLabel(""); e.target.value = ""; }; const handleUpload = async () => { if (!pendingFile) return; setUploading(true); setProgress(0); try { const { url, key } = await uploadViaXhr(pendingFile, setProgress); const result = await addFootagePlate({ shotId: shot.id, fileUrl: url, fileKey: key, fileName: pendingFile.name, fileSize: pendingFile.size, label: pendingLabel.trim() || `Plate ${plates.length + 1}`, }); const newPlate = result.plate as Plate; const updated = [...plates, newPlate]; setPlates(updated); setSelectedId(newPlate.id); setPendingFile(null); setPendingLabel(""); toast({ title: "Footage plate added" }); onSaved?.(); } catch (e) { toast({ title: "Upload failed", description: e instanceof Error ? e.message : undefined, variant: "destructive", }); } finally { setUploading(false); setProgress(0); } }; const handleCancelPending = () => { setPendingFile(null); setPendingLabel(""); }; // ── remove plate ────────────────────────────────────────────────────────── const handleRemove = async (plateId: string) => { setDeletingId(plateId); try { await removeFootagePlate(plateId); const updated = plates.filter((p) => p.id !== plateId); setPlates(updated); if (selectedId === plateId) setSelectedId(updated[0]?.id ?? null); toast({ title: "Plate removed" }); onSaved?.(); } catch (e) { toast({ title: "Failed to remove plate", description: e instanceof Error ? e.message : undefined, variant: "destructive", }); } finally { setDeletingId(null); } }; // ── rename plate ────────────────────────────────────────────────────────── const startEdit = (plate: Plate) => { setEditingId(plate.id); setEditLabel(plate.label); }; const commitEdit = async (plateId: string) => { try { await renameFootagePlate(plateId, editLabel); setPlates((prev) => prev.map((p) => (p.id === plateId ? { ...p, label: editLabel.trim() } : p)) ); onSaved?.(); } catch (e) { toast({ title: "Failed to rename plate", description: e instanceof Error ? e.message : undefined, variant: "destructive", }); } finally { setEditingId(null); } }; return (
{/* Header */}

{/* Video player */} {selectedPlate ? (
) : ( !pendingFile && (
canManage && fileInputRef.current?.click()} >

No footage plates yet.

{canManage &&

Click to add a video file

}
) )} {/* Plate list */} {plates.length > 0 && (
{plates.map((plate, i) => (
{ if (editingId !== plate.id) setSelectedId(plate.id); }} > {/* Index badge */} {i + 1} {/* Label (edit or display) */} {editingId === plate.id ? ( setEditLabel(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") commitEdit(plate.id); if (e.key === "Escape") setEditingId(null); }} onBlur={() => commitEdit(plate.id)} className="h-6 text-xs py-0 px-2 flex-1" onClick={(e) => e.stopPropagation()} /> ) : ( {plate.label || `Plate ${i + 1}`} )} {/* File info */} {plate.fileName && ( {plate.fileName} )} {plate.fileSize && ( {formatSize(plate.fileSize)} )} {/* Actions */} {canManage && editingId !== plate.id && (
e.stopPropagation()} >
)}
))}
)} {/* Pending file row */} {pendingFile && !uploading && (
)} {/* Upload progress */} {uploading && (
Uploading footage… {Math.round(progress * 100)}%
)} {/* Add plate button */} {canManage && !pendingFile && !uploading && ( )}
); }