227 lines
7.8 KiB
TypeScript
227 lines
7.8 KiB
TypeScript
"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,
|
||
} from "lucide-react";
|
||
import type { ShotWithDetails } from "@/types";
|
||
import { ShotSettingsTab } from "@/components/shots/ShotSettingsTab";
|
||
|
||
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" | "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>
|
||
{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 === "settings" && canManage && (
|
||
<ShotSettingsTab shot={shot} artists={artists} onSaved={fetchShot} />
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|