"use client"; import { useState, useRef, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; import Image from "next/image"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Separator } from "@/components/ui/separator"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { updateShot, deleteShot } from "@/actions/shots"; import { useToast } from "@/components/ui/use-toast"; import { Upload, X, Film, ImageIcon, Trash2, BookImage, Loader2 } from "lucide-react"; // ─── Shot References ────────────────────────────────────────────────────────── interface ShotRef { id: string; fileUrl: string; fileName: string; label: string | null; sortOrder: number; } function ShotReferencesSection({ shotId }: { shotId: string }) { const { toast } = useToast(); const [refs, setRefs] = useState([]); const [loading, setLoading] = useState(true); const [uploading, setUploading] = useState(false); const fileInputRef = useRef(null); const fetchRefs = useCallback(async () => { try { const res = await fetch(`/api/shots/${shotId}/references`); if (res.ok) { const data = await res.json(); setRefs(data.references); } } finally { setLoading(false); } }, [shotId]); useEffect(() => { fetchRefs(); }, [fetchRefs]); const handleUpload = async (e: React.ChangeEvent) => { const files = Array.from(e.target.files ?? []); if (!files.length) return; setUploading(true); try { for (const file of files) { const fd = new FormData(); fd.append("file", file); const res = await fetch(`/api/shots/${shotId}/references`, { method: "POST", body: fd }); if (!res.ok) throw new Error(await res.text()); const { reference } = await res.json(); setRefs((prev) => [...prev, reference]); } } catch (err) { toast({ title: "Upload failed", description: err instanceof Error ? err.message : undefined, variant: "destructive" }); } finally { setUploading(false); if (fileInputRef.current) fileInputRef.current.value = ""; } }; const handleDelete = async (refId: string) => { try { const res = await fetch(`/api/shots/${shotId}/references?refId=${refId}`, { method: "DELETE" }); if (!res.ok) throw new Error("Delete failed"); setRefs((prev) => prev.filter((r) => r.id !== refId)); } catch { toast({ title: "Failed to delete reference", variant: "destructive" }); } }; return (
References
{loading ? (
Loading…
) : ( <> {refs.length > 0 && (
{refs.map((ref) => (
{ref.label {ref.label && (
{ref.label}
)}
))}
)}
)}
); } // ─── Settings form ──────────────────────────────────────────────────────────── const settingsSchema = z.object({ shotCode: z.string().min(1, "Required").max(120).regex(/^[A-Z0-9_]+$/i, "Alphanumeric and underscores only"), description: z.string().optional(), status: z.enum(["WAITING", "IN_PROGRESS", "INTERNAL_REVIEW", "READY_FOR_CLIENT", "CLIENT_REVIEW", "REVISIONS", "COMPLETE"]), priority: z.enum(["LOW", "NORMAL", "HIGH", "URGENT"]), fps: z.coerce.number().min(1).max(240), frameStart: z.coerce.number().int().optional().or(z.literal("")), frameEnd: z.coerce.number().int().optional().or(z.literal("")), dueDate: z.string().optional(), artistId: z.string().optional(), }); type SettingsFormValues = z.infer; interface Artist { id: string; name: string | null; email: string; } interface ShotSettingsTabProps { shot: { id: string; shotCode: string; description: string | null; status: string; priority: string; fps: number; frameStart?: number | null; frameEnd?: number | null; dueDate: Date | string | null; artistId: string | null; thumbnailUrl: string | null; projectId: string; }; artists: Artist[]; onSaved?: () => void; } export function ShotSettingsTab({ shot, artists, onSaved }: ShotSettingsTabProps) { const router = useRouter(); const { toast } = useToast(); const [isSaving, setIsSaving] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); const [thumbnailFile, setThumbnailFile] = useState(null); const [thumbnailPreview, setThumbnailPreview] = useState(shot.thumbnailUrl ?? null); const [clearThumbnail, setClearThumbnail] = useState(false); const fileInputRef = useRef(null); const formatDate = (d: Date | string | null) => { if (!d) return ""; return new Date(d).toISOString().split("T")[0]; }; const { register, handleSubmit, setValue, formState: { errors }, } = useForm({ resolver: zodResolver(settingsSchema), defaultValues: { shotCode: shot.shotCode, description: shot.description ?? "", status: shot.status as SettingsFormValues["status"], priority: shot.priority as SettingsFormValues["priority"], fps: shot.fps, frameStart: shot.frameStart ?? "", frameEnd: shot.frameEnd ?? "", dueDate: formatDate(shot.dueDate), artistId: shot.artistId ?? "__none__", }, }); const handleThumbnailChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setThumbnailFile(file); setClearThumbnail(false); const reader = new FileReader(); reader.onload = (ev) => setThumbnailPreview(ev.target?.result as string); reader.readAsDataURL(file); }; const onSubmit = async (values: SettingsFormValues) => { setIsSaving(true); try { let thumbnailUrl: string | null | undefined = undefined; if (clearThumbnail) { thumbnailUrl = null; } else if (thumbnailFile) { const fd = new FormData(); fd.append("file", thumbnailFile); fd.append("type", "image"); const res = await fetch("/api/upload", { method: "POST", body: fd }); if (!res.ok) throw new Error("Thumbnail upload failed"); const data = await res.json(); thumbnailUrl = data.url; } await updateShot({ shotId: shot.id, shotCode: values.shotCode, description: values.description || undefined, status: values.status, priority: values.priority, fps: values.fps, frameStart: values.frameStart !== "" && values.frameStart != null ? Number(values.frameStart) : null, frameEnd: values.frameEnd !== "" && values.frameEnd != null ? Number(values.frameEnd) : null, dueDate: values.dueDate || null, artistId: values.artistId === "__none__" ? null : values.artistId, thumbnailUrl, }); toast({ title: "Shot updated" }); onSaved?.(); } catch (e) { toast({ title: "Failed to save", description: e instanceof Error ? e.message : undefined, variant: "destructive" }); } finally { setIsSaving(false); } }; const handleDelete = async () => { setIsDeleting(true); try { await deleteShot(shot.id); toast({ title: "Shot deleted" }); router.push(`/projects/${shot.projectId}`); } catch (e) { toast({ title: "Failed to delete shot", description: e instanceof Error ? e.message : undefined, variant: "destructive" }); setIsDeleting(false); setConfirmDelete(false); } }; return (
{/* Details */}
Details
{errors.shotCode &&

{errors.shotCode.message}

}