Change schema
This commit is contained in:
5
bun.lock
5
bun.lock
@ -8,6 +8,7 @@
|
|||||||
"@aws-sdk/s3-request-presigner": "^3.954.0",
|
"@aws-sdk/s3-request-presigner": "^3.954.0",
|
||||||
"@prisma/adapter-pg": "^7.2.0",
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
@ -283,10 +284,14 @@
|
|||||||
|
|
||||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="],
|
||||||
|
|
||||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
||||||
|
|
||||||
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
|
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
|
||||||
|
|
||||||
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
|
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
|
||||||
|
|
||||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
"@aws-sdk/s3-request-presigner": "^3.954.0",
|
"@aws-sdk/s3-request-presigner": "^3.954.0",
|
||||||
"@prisma/adapter-pg": "^7.2.0",
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
|||||||
@ -103,6 +103,7 @@ model ArtTag {
|
|||||||
|
|
||||||
name String @unique
|
name String @unique
|
||||||
slug String @unique
|
slug String @unique
|
||||||
|
isParent Boolean @default(false)
|
||||||
showOnAnimalPage Boolean @default(false)
|
showOnAnimalPage Boolean @default(false)
|
||||||
|
|
||||||
description String?
|
description String?
|
||||||
|
|||||||
293
src/app/(normal)/artworks/animalstudies/index/page.tsx
Normal file
293
src/app/(normal)/artworks/animalstudies/index/page.tsx
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { ArrowLeftIcon } from "lucide-react";
|
||||||
|
|
||||||
|
type SimpleArtwork = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
sortKey: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function sortBySortIndexName<T extends { sortIndex: number; name: string }>(a: T, b: T) {
|
||||||
|
return a.sortIndex - b.sortIndex || a.name.localeCompare(b.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortArtworks(a: SimpleArtwork, b: SimpleArtwork) {
|
||||||
|
const ak = a.sortKey ?? 999999;
|
||||||
|
const bk = b.sortKey ?? 999999;
|
||||||
|
return ak - bk || a.name.localeCompare(b.name) || a.id.localeCompare(b.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function AnimalListPage() {
|
||||||
|
/**
|
||||||
|
* We fetch all "animal page" tags and only artworks that are:
|
||||||
|
* - published
|
||||||
|
* - in "Animal Studies" category
|
||||||
|
*
|
||||||
|
* This makes the list reflect exactly what the user sees on the Animal Studies page.
|
||||||
|
*/
|
||||||
|
const tags = await prisma.artTag.findMany({
|
||||||
|
where: { showOnAnimalPage: true },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
sortIndex: true,
|
||||||
|
parentId: true,
|
||||||
|
|
||||||
|
// artworks tagged with THIS tag
|
||||||
|
artworks: {
|
||||||
|
where: {
|
||||||
|
published: true,
|
||||||
|
categories: { some: { name: "Animal Studies" } },
|
||||||
|
},
|
||||||
|
select: { id: true, name: true, sortKey: true },
|
||||||
|
orderBy: [{ sortKey: "asc" }, { name: "asc" }, { id: "asc" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build maps to render a robust parent/child hierarchy (no reliance on Prisma nested children)
|
||||||
|
const byId = new Map(tags.map((t) => [t.id, t]));
|
||||||
|
const childrenByParentId = new Map<string, typeof tags>();
|
||||||
|
|
||||||
|
for (const t of tags) {
|
||||||
|
if (!t.parentId) continue;
|
||||||
|
const arr = childrenByParentId.get(t.parentId) ?? [];
|
||||||
|
arr.push(t);
|
||||||
|
childrenByParentId.set(t.parentId, arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [pid, arr] of childrenByParentId) {
|
||||||
|
childrenByParentId.set(pid, arr.slice().sort(sortBySortIndexName));
|
||||||
|
}
|
||||||
|
|
||||||
|
const parents = tags
|
||||||
|
.filter((t) => t.parentId === null)
|
||||||
|
.slice()
|
||||||
|
.sort(sortBySortIndexName);
|
||||||
|
|
||||||
|
// Orphans: child references a parentId that isn't present (or isn't showOnAnimalPage)
|
||||||
|
const orphans = tags
|
||||||
|
.filter((t) => t.parentId !== null && !byId.has(t.parentId))
|
||||||
|
.slice()
|
||||||
|
.sort(sortBySortIndexName);
|
||||||
|
|
||||||
|
// Small helper to render artworks list (linked)
|
||||||
|
const ArtworkList = ({ items }: { items: SimpleArtwork[] }) => {
|
||||||
|
const list = items.slice().sort(sortArtworks);
|
||||||
|
if (list.length === 0) {
|
||||||
|
return <p className="text-sm text-muted-foreground italic">No artworks found.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="space-y-1.5">
|
||||||
|
{list.map((a) => (
|
||||||
|
<li key={a.id}>
|
||||||
|
<Link
|
||||||
|
href={`/artworks/single/${a.id}`}
|
||||||
|
className="
|
||||||
|
inline-flex items-center gap-2
|
||||||
|
rounded-md px-2 py-1
|
||||||
|
text-sm font-medium
|
||||||
|
hover:bg-muted
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="
|
||||||
|
text-muted-foreground
|
||||||
|
transition-transform
|
||||||
|
group-hover:translate-x-0.5
|
||||||
|
"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="leading-snug">{a.name}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Count helper for badges
|
||||||
|
const countArtworks = (tagId: string) => {
|
||||||
|
const t = byId.get(tagId);
|
||||||
|
return t?.artworks?.length ?? 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const countArtworksInChildren = (tagId: string) => {
|
||||||
|
const children = childrenByParentId.get(tagId) ?? [];
|
||||||
|
let sum = 0;
|
||||||
|
for (const c of children) sum += c.artworks.length;
|
||||||
|
return sum;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-5xl px-4 py-6">
|
||||||
|
<header className="mb-6 space-y-3">
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="ghost"
|
||||||
|
className="w-fit gap-2 px-0 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<Link href="/artworks/animalstudies">
|
||||||
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
Back to Animal studies
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||||
|
Animal index
|
||||||
|
</h1>
|
||||||
|
{/* <p className="text-sm text-muted-foreground">
|
||||||
|
Click to expand and browse linked artworks.
|
||||||
|
</p> */}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="space-y-1">
|
||||||
|
<CardTitle className="text-base">Grouped animals</CardTitle>
|
||||||
|
{/* <p className="text-sm text-muted-foreground">
|
||||||
|
Parent tags expand into children; standalone tags appear as single entries.
|
||||||
|
</p> */}
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Click to expand and browse linked artworks.
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
{parents.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground italic">No parent tags found.</p>
|
||||||
|
) : (
|
||||||
|
<Accordion type="multiple" className="w-full">
|
||||||
|
{parents.map((p) => {
|
||||||
|
const children = childrenByParentId.get(p.id) ?? [];
|
||||||
|
const parentDirectCount = p.artworks.length;
|
||||||
|
const childrenCount = countArtworksInChildren(p.id);
|
||||||
|
|
||||||
|
// Standalone root tag: no children
|
||||||
|
const isStandalone = children.length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccordionItem key={p.id} value={p.id} className="border-b">
|
||||||
|
<AccordionTrigger className="py-4">
|
||||||
|
<div className="flex w-full items-center justify-between pr-2">
|
||||||
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
|
<span className="truncate font-medium">{p.name}</span>
|
||||||
|
{isStandalone ? (
|
||||||
|
<Badge variant="secondary">single</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="secondary">{children.length} sub</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{parentDirectCount > 0 ? (
|
||||||
|
<Badge variant="outline">{parentDirectCount} direct</Badge>
|
||||||
|
) : null}
|
||||||
|
{!isStandalone ? (
|
||||||
|
<Badge variant="outline">{childrenCount} in sub</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline">{parentDirectCount} artworks</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
|
||||||
|
<AccordionContent className="pb-5">
|
||||||
|
{/* If standalone root: just list its artworks */}
|
||||||
|
{isStandalone ? (
|
||||||
|
<div className="rounded-md border p-4">
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="text-sm font-medium">Artworks</div>
|
||||||
|
<Badge variant="outline">{p.artworks.length}</Badge>
|
||||||
|
</div>
|
||||||
|
<ArtworkList items={p.artworks} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Optional: artworks directly on the parent */}
|
||||||
|
<div className="rounded-md border p-4">
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="text-sm font-medium">Directly tagged</div>
|
||||||
|
<Badge variant="outline">{p.artworks.length}</Badge>
|
||||||
|
</div>
|
||||||
|
<ArtworkList items={p.artworks} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Children blocks */}
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
|
{children.map((c) => (
|
||||||
|
<div key={c.id} className="rounded-md border p-4">
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate text-sm font-medium">{c.name}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Sub-tag of {p.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline">{c.artworks.length}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ArtworkList items={c.artworks} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Accordion>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{orphans.length ? (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="space-y-1">
|
||||||
|
<CardTitle className="text-base">Ungrouped animals</CardTitle>
|
||||||
|
{/* <p className="text-sm text-muted-foreground">
|
||||||
|
Tags whose parent is not visible (or not configured for the animal page).
|
||||||
|
</p> */}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
|
{orphans.map((t) => (
|
||||||
|
<div key={t.id} className="rounded-md border p-4">
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="truncate text-sm font-medium">{t.name}</div>
|
||||||
|
<Badge variant="outline">{t.artworks.length}</Badge>
|
||||||
|
</div>
|
||||||
|
<ArtworkList items={t.artworks} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -96,10 +96,10 @@ export default async function AnimalStudiesPage({ searchParams }: { searchParams
|
|||||||
selectedTagSlugs={selectedTagSlugs}
|
selectedTagSlugs={selectedTagSlugs}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button asChild variant="secondary" className="h-11 gap-2">
|
<Button asChild type="button" variant="secondary" className="h-11 gap-2">
|
||||||
<Link href="/animallist">
|
<Link href="/artworks/animalstudies/index">
|
||||||
<ListIcon className="h-4 w-4" />
|
<ListIcon className="h-4 w-4" />
|
||||||
List all used animals
|
Animal index
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
66
src/components/ui/accordion.tsx
Normal file
66
src/components/ui/accordion.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||||
|
import { ChevronDownIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Accordion({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||||
|
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
data-slot="accordion-item"
|
||||||
|
className={cn("border-b last:border-b-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
className={cn(
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
data-slot="accordion-content"
|
||||||
|
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||||
Reference in New Issue
Block a user