Files
vfxreview/app/(dashboard)/projects/[id]/shots/[shotId]/page.tsx
T
twotalesanimation bcea123112
Deploy / deploy (push) Has been cancelled
Add original footage to shots function
2026-05-20 11:02:33 +02:00

249 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
import { useParams, useRouter } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { TaskList } from "@/components/tasks/TaskList";
import { Separator } from "@/components/ui/separator";
import { getInitials } from "@/lib/utils";
import { cn } from "@/lib/utils";
import {
Film,
ArrowLeft,
Clock,
AlertCircle,
CheckCircle2,
Settings,
ListTodo,
Video,
} from "lucide-react";
import type { ShotWithDetails } from "@/types";
import { ShotSettingsTab } from "@/components/shots/ShotSettingsTab";
import { FootageViewer } from "@/components/shots/FootageViewer";
const STATUS_CONFIG: Record<
string,
{ label: string; className: string; Icon: React.ElementType }
> = {
WAITING: { label: "Waiting", className: "bg-zinc-500/10 text-zinc-400 border-zinc-500/20", Icon: Clock },
IN_PROGRESS: { label: "In Progress", className: "bg-blue-500/10 text-blue-400 border-blue-500/20", Icon: Film },
IN_REVIEW: { label: "In Review", className: "bg-amber-500/10 text-amber-400 border-amber-500/20", Icon: AlertCircle },
REVISIONS: { label: "Revisions", className: "bg-orange-500/10 text-orange-400 border-orange-500/20", Icon: AlertCircle },
COMPLETE: { label: "Complete", className: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", Icon: CheckCircle2 },
};
const PRIORITY_CONFIG: Record<string, { label: string; dot: string }> = {
LOW: { label: "Low", dot: "bg-zinc-400" },
NORMAL: { label: "Normal", dot: "bg-blue-400" },
HIGH: { label: "High", dot: "bg-amber-400" },
CRITICAL: { label: "Critical", dot: "bg-red-500" },
};
export default function ShotDetailPage() {
const params = useParams<{ id: string; shotId: string }>();
const router = useRouter();
const [shot, setShot] = useState<ShotWithDetails | null>(null);
const [projectName, setProjectName] = useState<string>("");
const [loading, setLoading] = useState(true);
const [canApprove, setCanApprove] = useState(false);
const [tasks, setTasks] = useState<any[]>([]);
const [artists, setArtists] = useState<any[]>([]);
const [canManage, setCanManage] = useState(false);
const [activeTab, setActiveTab] = useState<"tasks" | "footage" | "settings">("tasks");
const fetchShot = async () => {
try {
const res = await fetch(`/api/shots/${params.shotId}?projectId=${params.id}`);
if (res.status === 404) {
router.push("/projects");
return;
}
const data = await res.json();
setShot(data.shot);
setProjectName(data.projectName ?? "");
setCanApprove(data.canApprove ?? false);
setTasks(data.tasks ?? []);
setArtists(data.artists ?? []);
setCanManage(data.canApprove ?? false);
} catch {
router.push("/projects");
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchShot();
}, [params.shotId]);
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<Film className="h-6 w-6 animate-pulse text-muted-foreground" />
</div>
);
}
if (!shot) return null;
const statusCfg = STATUS_CONFIG[shot.status] ?? STATUS_CONFIG.WAITING;
const priorityCfg = PRIORITY_CONFIG[shot.priority] ?? PRIORITY_CONFIG.NORMAL;
const { Icon: StatusIcon } = statusCfg;
return (
<div className="p-6 space-y-6 max-w-[1400px] mx-auto">
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Link href="/projects" className="hover:text-foreground transition-colors">
Projects
</Link>
<span>/</span>
<Link href={`/projects/${params.id}`} className="hover:text-foreground transition-colors">
{projectName}
</Link>
<span>/</span>
<span className="text-foreground font-mono">{shot.shotCode}</span>
</div>
{/* Header */}
<div className="flex items-start gap-6">
{/* Thumbnail cinema scope 2.39:1 */}
<Button variant="ghost" size="icon" asChild className="-ml-2 h-8 w-8">
<Link href={`/projects/${params.id}`}>
<ArrowLeft className="h-4 w-4" />
</Link>
</Button>
{shot.thumbnailUrl && (
<div className="relative flex-shrink-0 w-72 aspect-[2.39] rounded-lg overflow-hidden border border-border">
<Image
src={shot.thumbnailUrl}
alt={shot.shotCode}
fill
className="object-cover"
/>
</div>
)}
<div className="space-y-2">
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold font-mono">{shot.shotCode}</h1>
{shot.sequence && (
<span className="text-sm text-muted-foreground">Seq: {shot.sequence}</span>
)}
</div>
{shot.description && (
<p className="text-muted-foreground ">{shot.description}</p>
)}
<div className="flex items-center gap-3 flex-wrap">
<Badge className={statusCfg.className} variant="outline">
<StatusIcon className="h-3 w-3 mr-1" />
{statusCfg.label}
</Badge>
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
<span
className={`h-2 w-2 rounded-full ${priorityCfg.dot}`}
/>
{priorityCfg.label} Priority
</div>
<span className="text-sm text-muted-foreground">{shot.fps} fps</span>
{shot.artist && (
<div className="flex items-center gap-1.5">
<Avatar className="h-5 w-5">
<AvatarImage src={shot.artist.image ?? undefined} />
<AvatarFallback className="text-[10px] bg-primary/10 text-primary">
{getInitials(shot.artist.name ?? shot.artist.email)}
</AvatarFallback>
</Avatar>
<span className="text-sm text-muted-foreground">
{shot.artist.name ?? shot.artist.email}
</span>
</div>
)}
</div>
</div>
</div>
<Separator />
{/* Tabs */}
<div>
<div className="flex border-b border-border mb-5">
<button
onClick={() => setActiveTab("tasks")}
className={cn(
"flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px",
activeTab === "tasks"
? "border-amber-500 text-amber-400"
: "border-transparent text-zinc-500 hover:text-zinc-300"
)}
>
<ListTodo className="h-4 w-4" />
Tasks
</button>
<button
onClick={() => setActiveTab("footage")}
className={cn(
"flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px",
activeTab === "footage"
? "border-amber-500 text-amber-400"
: "border-transparent text-zinc-500 hover:text-zinc-300"
)}
>
<Video className="h-4 w-4" />
Footage
</button>
{canManage && (
<button
onClick={() => setActiveTab("settings")}
className={cn(
"flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px",
activeTab === "settings"
? "border-amber-500 text-amber-400"
: "border-transparent text-zinc-500 hover:text-zinc-300"
)}
>
<Settings className="h-4 w-4" />
Settings
</button>
)}
</div>
{activeTab === "tasks" && (
<TaskList
tasks={tasks}
projectId={params.id}
shotId={shot.id}
artists={artists}
canManage={canManage}
onTaskCreated={fetchShot}
/>
)}
{activeTab === "footage" && (
<FootageViewer
shot={shot}
canManage={canManage}
onSaved={fetchShot}
/>
)}
{activeTab === "settings" && canManage && (
<ShotSettingsTab shot={shot} artists={artists} onSaved={fetchShot} />
)}
</div>
</div>
);
}