feat(shot-status): shot version beside the name, and a scaled-in-edit flag
Deploy / deploy (push) Successful in 7m28s
Deploy / deploy (push) Successful in 7m28s
Two small board additions. The shot version now shows next to the shot code. It is Shot.shotVersion — the value the AE panel burns into overlays and queueExport renders under, and the one the shot detail page edits — not a count of uploaded review Versions, so the board agrees with what the artist has in the panel. Shot.isScaled records that a shot is scaled in the edit: the plate is not used at its native size, which changes how the shot is comped and reviewed. That makes it a standing property of the shot rather than something buried in the notes, so it is flagged the way isKeyShot is — a cyan pill next to the shot name, and a Scaled toggle column beside Key. toggleScaledShot() (actions/shots.ts) mirrors toggleKeyShot, same roles. KeyShotToggle's switch markup would have been duplicated verbatim for the second flag, so both now sit on a shared FlagToggle taking a colour tone and a setter. The key-shot switch behaves exactly as before, optimistic update and rollback included. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { updateShotNotes, bulkUpdateDueDates, bulkDeleteShots, toggleKeyShot } from "@/actions/shots";
|
||||
import { updateShotNotes, bulkUpdateDueDates, bulkDeleteShots, toggleKeyShot, toggleScaledShot } from "@/actions/shots";
|
||||
import { previewMovReview, buildMovReview } from "@/actions/deliveries";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
Trash2,
|
||||
ChevronsUpDown,
|
||||
Clapperboard,
|
||||
Scaling,
|
||||
} from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { shotThumbnail } from "@/lib/shot-thumbnail";
|
||||
@@ -67,6 +68,8 @@ type ShotRow = {
|
||||
notes: string | null;
|
||||
description: string | null;
|
||||
isKeyShot: boolean;
|
||||
isScaled: boolean;
|
||||
shotVersion: string | null;
|
||||
artist: { id: string; name: string | null; image: string | null; email: string } | null;
|
||||
shotGroup: { id: string; name: string } | null;
|
||||
};
|
||||
@@ -474,10 +477,30 @@ function NotesCell({
|
||||
);
|
||||
}
|
||||
|
||||
// ── KeyShotToggle ─────────────────────────────────────────────────────────────
|
||||
// ── Shot flag toggles ─────────────────────────────────────────────────────────
|
||||
|
||||
function KeyShotToggle({ shot }: { shot: ShotRow }) {
|
||||
const [optimistic, setOptimistic] = useState(shot.isKeyShot);
|
||||
/**
|
||||
* A standing yes/no property of a shot, flipped straight from the board.
|
||||
* Optimistic: the switch moves immediately and rolls back if the save fails,
|
||||
* because a producer ticking twenty shots should not wait on a round trip each
|
||||
* time.
|
||||
*/
|
||||
function FlagToggle({
|
||||
value,
|
||||
onSet,
|
||||
onLabel,
|
||||
offLabel,
|
||||
failureTitle,
|
||||
tone,
|
||||
}: {
|
||||
value: boolean;
|
||||
onSet: (next: boolean) => Promise<unknown>;
|
||||
onLabel: string;
|
||||
offLabel: string;
|
||||
failureTitle: string;
|
||||
tone: { on: string; ring: string };
|
||||
}) {
|
||||
const [optimistic, setOptimistic] = useState(value);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -486,10 +509,10 @@ function KeyShotToggle({ shot }: { shot: ShotRow }) {
|
||||
setOptimistic(next);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await toggleKeyShot(shot.id, next);
|
||||
await onSet(next);
|
||||
} catch {
|
||||
setOptimistic(!next);
|
||||
toast({ title: "Failed to update key shot", variant: "destructive" });
|
||||
toast({ title: failureTitle, variant: "destructive" });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -499,12 +522,13 @@ function KeyShotToggle({ shot }: { shot: ShotRow }) {
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={optimistic}
|
||||
aria-label={optimistic ? "Remove key shot" : "Mark as key shot"}
|
||||
aria-label={optimistic ? offLabel : onLabel}
|
||||
disabled={isPending}
|
||||
onClick={handle}
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-400 disabled:opacity-50",
|
||||
optimistic ? "bg-amber-500" : "bg-zinc-600 hover:bg-zinc-500"
|
||||
"relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none focus-visible:ring-2 disabled:opacity-50",
|
||||
tone.ring,
|
||||
optimistic ? tone.on : "bg-zinc-600 hover:bg-zinc-500"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
@@ -517,6 +541,32 @@ function KeyShotToggle({ shot }: { shot: ShotRow }) {
|
||||
);
|
||||
}
|
||||
|
||||
function KeyShotToggle({ shot }: { shot: ShotRow }) {
|
||||
return (
|
||||
<FlagToggle
|
||||
value={shot.isKeyShot}
|
||||
onSet={(next) => toggleKeyShot(shot.id, next)}
|
||||
onLabel="Mark as key shot"
|
||||
offLabel="Remove key shot"
|
||||
failureTitle="Failed to update key shot"
|
||||
tone={{ on: "bg-amber-500", ring: "focus-visible:ring-amber-400" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ScaledShotToggle({ shot }: { shot: ShotRow }) {
|
||||
return (
|
||||
<FlagToggle
|
||||
value={shot.isScaled}
|
||||
onSet={(next) => toggleScaledShot(shot.id, next)}
|
||||
onLabel="Mark as scaled in the edit"
|
||||
offLabel="Remove scaled flag"
|
||||
failureTitle="Failed to update scaled flag"
|
||||
tone={{ on: "bg-cyan-500", ring: "focus-visible:ring-cyan-400" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ShotTable({
|
||||
shots,
|
||||
canManage,
|
||||
@@ -559,6 +609,12 @@ function ShotTable({
|
||||
Key
|
||||
</span>
|
||||
</th>
|
||||
<th className="py-2 px-3 text-xs font-medium text-zinc-500 w-20 text-center">
|
||||
<span className="flex items-center gap-1 justify-center">
|
||||
<Scaling className="h-3 w-3" />
|
||||
Scaled
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-800/60">
|
||||
@@ -619,6 +675,15 @@ function ShotTable({
|
||||
>
|
||||
{shot.shotCode}
|
||||
</Link>
|
||||
{shot.shotVersion && (
|
||||
<span className="font-mono text-xs text-zinc-400 shrink-0">{shot.shotVersion}</span>
|
||||
)}
|
||||
{shot.isScaled && (
|
||||
<span className="flex items-center gap-1 rounded-full border border-cyan-500/30 bg-cyan-500/10 px-1.5 py-0.5 text-[10px] font-medium text-cyan-300 shrink-0">
|
||||
<Scaling className="h-2.5 w-2.5" />
|
||||
Scaled
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{shot.description && (
|
||||
<p className="text-xs text-zinc-500 mt-0.5 truncate max-w-[200px]">
|
||||
@@ -661,6 +726,11 @@ function ShotTable({
|
||||
<td className="py-2 px-3 text-center">
|
||||
<KeyShotToggle shot={shot} />
|
||||
</td>
|
||||
|
||||
{/* Scaled-in-edit toggle */}
|
||||
<td className="py-2 px-3 text-center">
|
||||
<ScaledShotToggle shot={shot} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -41,6 +41,10 @@ async function getShotsForProject(projectId: string) {
|
||||
notes: true,
|
||||
description: true,
|
||||
isKeyShot: true,
|
||||
isScaled: true,
|
||||
// The shot-level version the panel burns into overlays and queueExport
|
||||
// renders under — not the count of review Versions uploaded.
|
||||
shotVersion: true,
|
||||
artist: { select: { id: true, name: true, image: true, email: true } },
|
||||
shotGroup: { select: { id: true, name: true } },
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user