191 lines
7.9 KiB
TypeScript
191 lines
7.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useTransition } from "react";
|
|
import Image from "next/image";
|
|
import { Film, Save } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { useToast } from "@/components/ui/use-toast";
|
|
import { updateShotSlate } from "@/actions/shots";
|
|
import type { ShotWithDetails } from "@/types";
|
|
|
|
interface Props {
|
|
shot: ShotWithDetails;
|
|
projectName: string;
|
|
onSaved: () => void;
|
|
}
|
|
|
|
/** Left-side slate row: label + editable/static value */
|
|
function SlateRow({ label, children }: { label: string; children: React.ReactNode }) {
|
|
return (
|
|
<div className="flex gap-0 border-b border-zinc-800/60 last:border-0">
|
|
<div className="w-36 shrink-0 py-3 px-4 text-right text-[11px] font-semibold text-zinc-500 uppercase tracking-widest self-start pt-3.5">
|
|
{label}
|
|
</div>
|
|
<div className="flex-1 py-2.5 px-3 text-sm text-zinc-100">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Right-side metadata row */
|
|
function MetaRow({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div className="flex items-center justify-between border-b border-zinc-800/50 last:border-0 py-1.5 px-3">
|
|
<span className="text-[11px] text-zinc-500">{label}</span>
|
|
<span className="text-[11px] text-zinc-300 text-right max-w-[55%] truncate">{value || "—"}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ShotSlateTab({ shot, projectName, onSaved }: Props) {
|
|
const { toast } = useToast();
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
const [slateType, setSlateType] = useState(shot.slateType ?? "");
|
|
const [slateDescription, setSlateDescription] = useState(shot.slateDescription ?? shot.description ?? "");
|
|
const [slateVfxScope, setSlateVfxScope] = useState(shot.slateVfxScope ?? "");
|
|
const [slateSubmissionNote, setSlateSubmissionNote] = useState(shot.slateSubmissionNote ?? "");
|
|
|
|
const today = new Date();
|
|
const dateStr = `${today.getFullYear()}/${String(today.getMonth() + 1).padStart(2, "0")}/${String(today.getDate()).padStart(2, "0")}`;
|
|
const versionName = `${shot.shotCode}_cmp_TT_${shot.shotVersion ?? "v001"}`;
|
|
const frames =
|
|
shot.frameStart != null && shot.frameEnd != null
|
|
? String(shot.frameEnd - shot.frameStart + 1)
|
|
: "—";
|
|
|
|
const handleSave = () => {
|
|
startTransition(async () => {
|
|
try {
|
|
await updateShotSlate(shot.id, {
|
|
slateType: slateType.trim() || null,
|
|
slateDescription: slateDescription.trim() || null,
|
|
slateVfxScope: slateVfxScope.trim() || null,
|
|
slateSubmissionNote: slateSubmissionNote.trim() || null,
|
|
});
|
|
toast({ title: "Slate saved" });
|
|
onSaved();
|
|
} catch (e) {
|
|
toast({ title: "Failed to save", description: e instanceof Error ? e.message : undefined, variant: "destructive" });
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Slate document */}
|
|
<div className="rounded-xl overflow-hidden border border-zinc-800 bg-zinc-950 max-w-4xl">
|
|
{/* Top accent bar */}
|
|
<div className="h-1 bg-red-600" />
|
|
|
|
<div className="flex">
|
|
{/* ── Left column: editorial data ─────────────────────────────── */}
|
|
<div className="flex-1 border-r border-zinc-800">
|
|
{/* Header row */}
|
|
<div className="flex border-b border-zinc-800">
|
|
<div className="flex-1 px-4 py-3 border-r border-zinc-800">
|
|
<p className="text-[10px] text-zinc-500 uppercase tracking-widest mb-0.5">Show</p>
|
|
<p className="text-base font-bold text-white tracking-wide">{projectName.toUpperCase()}</p>
|
|
</div>
|
|
<div className="px-4 py-3 flex items-center gap-2">
|
|
<p className="text-[10px] text-zinc-500 uppercase tracking-widest">Submitting For</p>
|
|
<span className="ml-1 px-2 py-0.5 rounded bg-red-600/20 border border-red-600/40 text-red-400 text-xs font-bold tracking-widest">REVIEW</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Auto-filled rows */}
|
|
<SlateRow label="Version Name">
|
|
<span className="font-mono text-amber-400">{versionName}</span>
|
|
</SlateRow>
|
|
<SlateRow label="Date">
|
|
<span className="text-zinc-300">{dateStr}</span>
|
|
</SlateRow>
|
|
|
|
{/* Editable rows */}
|
|
<SlateRow label="Shot Type">
|
|
<Input
|
|
value={slateType}
|
|
onChange={(e) => setSlateType(e.target.value)}
|
|
placeholder="e.g. 2D COMP"
|
|
className="h-7 bg-zinc-900 border-zinc-700 text-sm font-medium text-zinc-100 placeholder:text-zinc-600"
|
|
/>
|
|
</SlateRow>
|
|
|
|
<SlateRow label="Shot Description">
|
|
<Textarea
|
|
value={slateDescription}
|
|
onChange={(e) => setSlateDescription(e.target.value)}
|
|
placeholder={shot.description ?? "Shot description for slate…"}
|
|
rows={2}
|
|
className="bg-zinc-900 border-zinc-700 text-sm text-zinc-100 placeholder:text-zinc-600 resize-none min-h-[56px]"
|
|
/>
|
|
</SlateRow>
|
|
|
|
<SlateRow label="VFX Scope">
|
|
<Textarea
|
|
value={slateVfxScope}
|
|
onChange={(e) => setSlateVfxScope(e.target.value)}
|
|
placeholder="VFX scope of work…"
|
|
rows={3}
|
|
className="bg-zinc-900 border-zinc-700 text-sm text-zinc-100 placeholder:text-zinc-600 resize-none min-h-[72px]"
|
|
/>
|
|
</SlateRow>
|
|
|
|
<SlateRow label="Submission Note">
|
|
<Input
|
|
value={slateSubmissionNote}
|
|
onChange={(e) => setSlateSubmissionNote(e.target.value)}
|
|
placeholder="e.g. Done"
|
|
className="h-7 bg-zinc-900 border-zinc-700 text-sm text-zinc-100 placeholder:text-zinc-600"
|
|
/>
|
|
</SlateRow>
|
|
</div>
|
|
|
|
{/* ── Right column: thumbnail + metadata ──────────────────────── */}
|
|
<div className="w-56 shrink-0 flex flex-col">
|
|
{/* Thumbnail */}
|
|
<div className="aspect-video bg-zinc-900 border-b border-zinc-800 overflow-hidden flex items-center justify-center">
|
|
{shot.thumbnailUrl ? (
|
|
<Image
|
|
src={shot.thumbnailUrl}
|
|
alt={shot.shotCode}
|
|
width={224}
|
|
height={126}
|
|
className="object-cover w-full h-full"
|
|
/>
|
|
) : (
|
|
<Film className="h-8 w-8 text-zinc-700" />
|
|
)}
|
|
</div>
|
|
|
|
{/* Metadata */}
|
|
<div className="flex-1 text-right">
|
|
<div className="px-3 py-2 border-b border-zinc-800">
|
|
<p className="text-[10px] text-zinc-500 mb-0.5">Vendor</p>
|
|
<p className="text-[11px] text-zinc-200 font-medium leading-tight">TWO TALES ANIMATION (PTY) LTD</p>
|
|
</div>
|
|
<MetaRow label="Shot Name" value={shot.shotCode} />
|
|
<MetaRow label="Episode" value={shot.episode ?? "—"} />
|
|
<MetaRow label="Seq Name" value={shot.sequence ?? "—"} />
|
|
<MetaRow label="Scene" value={shot.scene} />
|
|
<MetaRow label="Frames" value={frames} />
|
|
<MetaRow label="Media Color" value="w/SHOW LUT" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Save button */}
|
|
<div className="flex justify-end max-w-4xl">
|
|
<Button onClick={handleSave} disabled={isPending} className="gap-2">
|
|
<Save className="h-4 w-4" />
|
|
{isPending ? "Saving…" : "Save Slate"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|