This commit is contained in:
@@ -11,7 +11,7 @@ import { TaskCard } from "@/components/tasks/TaskCard";
|
|||||||
import { NewTaskDialog } from "@/components/tasks/NewTaskDialog";
|
import { NewTaskDialog } from "@/components/tasks/NewTaskDialog";
|
||||||
import { KanbanBoard } from "@/components/tasks/KanbanBoard";
|
import { KanbanBoard } from "@/components/tasks/KanbanBoard";
|
||||||
import { cn } from "@/lib/utils";
|
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 type { ShotWithDetails } from "@/types";
|
||||||
import { ProjectSettingsTab } from "@/components/projects/ProjectSettingsTab";
|
import { ProjectSettingsTab } from "@/components/projects/ProjectSettingsTab";
|
||||||
|
|
||||||
@@ -80,6 +80,34 @@ export function ProjectTabsClient({
|
|||||||
const [showImportShots, setShowImportShots] = useState(false);
|
const [showImportShots, setShowImportShots] = useState(false);
|
||||||
const [showNewAsset, setShowNewAsset] = useState(false);
|
const [showNewAsset, setShowNewAsset] = useState(false);
|
||||||
const [showNewTask, setShowNewTask] = 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 }[] = [
|
const tabs: { id: Tab; label: string; icon: React.ElementType; count: number; managerOnly?: boolean }[] = [
|
||||||
{ id: "shots", label: "Shots", icon: Film, count: shots.length },
|
{ id: "shots", label: "Shots", icon: Film, count: shots.length },
|
||||||
@@ -151,6 +179,38 @@ export function ProjectTabsClient({
|
|||||||
<div>
|
<div>
|
||||||
{shots.length === 0 ? (
|
{shots.length === 0 ? (
|
||||||
<EmptyState icon={Film} label="No shots yet" />
|
<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">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||||
{shots.map((shot) => (
|
{shots.map((shot) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user