feat(shot-status): shot version beside the name, and a scaled-in-edit flag
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:
twotalesanimation
2026-08-28 09:57:05 +02:00
parent 7f066c1372
commit bac525ddd0
5 changed files with 120 additions and 9 deletions
@@ -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>
);
})}
+4
View File
@@ -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 } },
},