added episodic grouping on project pages
Deploy / deploy (push) Successful in 2m28s

This commit is contained in:
twotalesanimation
2026-05-20 22:20:20 +02:00
parent e4eb8fb3a9
commit de59bbc0aa
@@ -11,7 +11,7 @@ import { TaskCard } from "@/components/tasks/TaskCard";
import { NewTaskDialog } from "@/components/tasks/NewTaskDialog";
import { KanbanBoard } from "@/components/tasks/KanbanBoard";
import { cn } from "@/lib/utils";
import { Film, Package, ListTodo, LayoutDashboard, Plus, Settings, FileUp } from "lucide-react";
import { Film, Package, ListTodo, LayoutDashboard, Plus, Settings, FileUp, ChevronDown, ChevronRight } from "lucide-react";
import type { ShotWithDetails } from "@/types";
import { ProjectSettingsTab } from "@/components/projects/ProjectSettingsTab";
@@ -80,6 +80,34 @@ export function ProjectTabsClient({
const [showImportShots, setShowImportShots] = useState(false);
const [showNewAsset, setShowNewAsset] = useState(false);
const [showNewTask, setShowNewTask] = useState(false);
const [collapsedEpisodes, setCollapsedEpisodes] = useState<Set<string>>(new Set());
const toggleEpisode = (ep: string) =>
setCollapsedEpisodes((prev) => {
const next = new Set(prev);
next.has(ep) ? next.delete(ep) : next.add(ep);
return next;
});
// For episodic projects: group and sort shots by episode → scene → shotNumber
const episodeGroups: [string, ShotWithDetails[]][] = projectType === "EPISODIC"
? (() => {
const sorted = [...shots].sort((a, b) => {
const ea = a.episode ?? "";
const eb = b.episode ?? "";
if (ea !== eb) return ea.localeCompare(eb, undefined, { numeric: true });
if (a.scene !== b.scene) return a.scene.localeCompare(b.scene, undefined, { numeric: true });
return a.shotNumber - b.shotNumber;
});
const map = new Map<string, ShotWithDetails[]>();
for (const shot of sorted) {
const key = shot.episode ?? "(No Episode)";
if (!map.has(key)) map.set(key, []);
map.get(key)!.push(shot);
}
return Array.from(map.entries());
})()
: [];
const tabs: { id: Tab; label: string; icon: React.ElementType; count: number; managerOnly?: boolean }[] = [
{ id: "shots", label: "Shots", icon: Film, count: shots.length },
@@ -151,6 +179,38 @@ export function ProjectTabsClient({
<div>
{shots.length === 0 ? (
<EmptyState icon={Film} label="No shots yet" />
) : projectType === "EPISODIC" ? (
<div className="space-y-4">
{episodeGroups.map(([episode, episodeShots]) => {
const collapsed = collapsedEpisodes.has(episode);
return (
<div key={episode}>
<button
onClick={() => toggleEpisode(episode)}
className="flex items-center gap-2 w-full mb-3 group text-left"
>
{collapsed
? <ChevronRight className="h-4 w-4 text-muted-foreground shrink-0" />
: <ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />}
<span className="font-semibold text-sm">
Episode {episode}
</span>
<span className="text-xs text-muted-foreground font-normal">
{episodeShots.length} shot{episodeShots.length !== 1 ? "s" : ""}
</span>
<div className="flex-1 h-px bg-border ml-1" />
</button>
{!collapsed && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{episodeShots.map((shot) => (
<ShotCard key={shot.id} shot={shot} projectId={projectId} canManage={canManage} />
))}
</div>
)}
</div>
);
})}
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{shots.map((shot) => (