added custom grouping on project pages
Deploy / deploy (push) Successful in 2m26s

This commit is contained in:
twotalesanimation
2026-05-20 22:43:29 +02:00
parent de59bbc0aa
commit d39b968dcb
7 changed files with 139 additions and 5 deletions
@@ -60,6 +60,7 @@ interface ProjectTabsClientProps {
assets: any[];
tasks: any[];
artists: Artist[];
shotGroups: { id: string; name: string }[];
canManage: boolean;
}
@@ -73,6 +74,7 @@ export function ProjectTabsClient({
assets,
tasks,
artists,
shotGroups,
canManage,
}: ProjectTabsClientProps) {
const [activeTab, setActiveTab] = useState<Tab>("shots");
@@ -109,6 +111,32 @@ export function ProjectTabsClient({
})()
: [];
// For standard projects: group by custom shot group when any exist
const standardGroups: [string, ShotWithDetails[]][] = projectType === "STANDARD" && shots.some((s) => s.shotGroup)
? (() => {
const sorted = [...shots].sort((a, b) => {
const ga = a.shotGroup?.name ?? "\uFFFF";
const gb = b.shotGroup?.name ?? "\uFFFF";
if (ga !== gb) return ga.localeCompare(gb, 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.shotGroup?.name ?? "(Ungrouped)";
if (!map.has(key)) map.set(key, []);
map.get(key)!.push(shot);
}
// Move ungrouped to the end
const ungrouped = map.get("(Ungrouped)");
if (ungrouped) {
map.delete("(Ungrouped)");
map.set("(Ungrouped)", ungrouped);
}
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 },
{ id: "assets", label: "Assets", icon: Package, count: assets.length },
@@ -211,6 +239,36 @@ export function ProjectTabsClient({
);
})}
</div>
) : standardGroups.length > 0 ? (
<div className="space-y-4">
{standardGroups.map(([groupName, groupShots]) => {
const collapsed = collapsedEpisodes.has(groupName);
return (
<div key={groupName}>
<button
onClick={() => toggleEpisode(groupName)}
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">{groupName}</span>
<span className="text-xs text-muted-foreground font-normal">
{groupShots.length} shot{groupShots.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">
{groupShots.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) => (
@@ -267,6 +325,7 @@ export function ProjectTabsClient({
<NewShotDialog
projectId={projectId}
projectType={projectType}
shotGroups={shotGroups}
open={showNewShot}
onClose={() => setShowNewShot(false)}
onSuccess={() => setShowNewShot(false)}
+6
View File
@@ -28,6 +28,7 @@ async function getProject(id: string) {
orderBy: { createdAt: "asc" },
include: {
artist: { select: { id: true, name: true, image: true, email: true } },
shotGroup: { select: { id: true, name: true } },
versions: {
take: 1,
orderBy: { versionNumber: "desc" },
@@ -70,6 +71,10 @@ async function getProject(id: string) {
},
},
},
shotGroups: {
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
select: { id: true, name: true },
},
},
});
}
@@ -202,6 +207,7 @@ export default async function ProjectPage({ params }: { params: Promise<{ id: st
assets={project.assets as any}
tasks={project.tasks as any}
artists={artists}
shotGroups={project.shotGroups}
canManage={!!canManage}
/>
</div>