Change tabs list page
This commit is contained in:
3
bun.lock
3
bun.lock
@ -17,6 +17,7 @@
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@ -365,6 +366,8 @@
|
||||
|
||||
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "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-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-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
|
||||
|
||||
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@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-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
|
||||
|
||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||
|
||||
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import TagTable from "@/components/tags/TagTable";
|
||||
import TagTabs from "@/components/tags/TagTabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { PlusCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@ -15,17 +16,31 @@ export default async function ArtTagsPage() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between pb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Art Tags</h1>
|
||||
<Link href="/tags/new" className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded">
|
||||
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Add new tag
|
||||
</Link>
|
||||
<div className="mx-auto w-full max-w-7xl px-4 py-6">
|
||||
<header className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||
Art Tags
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Manage tags, aliases, categories, and usage across artworks.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button asChild className="h-11 gap-2">
|
||||
<Link href="/tags/new">
|
||||
<PlusCircleIcon className="h-4 w-4" />
|
||||
Add new tag
|
||||
</Link>
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
{items.length > 0 ? (
|
||||
<TagTable tags={items} />
|
||||
<TagTabs tags={items} />
|
||||
) : (
|
||||
<p>There are no tags yet. Consider adding some!</p>
|
||||
<p className="text-muted-foreground">
|
||||
There are no tags yet. Consider adding some!
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -18,6 +18,7 @@ type TagRow = {
|
||||
name: string;
|
||||
slug: string;
|
||||
parent: { id: string; name: string } | null;
|
||||
isParent: boolean;
|
||||
showOnAnimalPage: boolean;
|
||||
aliases: { alias: string }[];
|
||||
categories: { id: string; name: string }[];
|
||||
@ -59,7 +60,7 @@ function Chips({
|
||||
);
|
||||
}
|
||||
|
||||
export default function TagTable({ tags }: { tags: TagRow[] }) {
|
||||
export default function TagTableAnimal({ tags }: { tags: TagRow[] }) {
|
||||
const handleDelete = (id: string) => {
|
||||
deleteTag(id);
|
||||
};
|
||||
@ -103,7 +104,7 @@ export default function TagTable({ tags }: { tags: TagRow[] }) {
|
||||
{t.parent.name}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-muted-foreground">—</span>
|
||||
<span className="text-muted-foreground">{t.isParent ? "Parent" : "—"}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
100
src/components/tags/TagTableMain.tsx
Normal file
100
src/components/tags/TagTableMain.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import type { TagRow } from "@/components/tags/TagTabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { PencilIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
function Chips({
|
||||
values,
|
||||
empty = "—",
|
||||
max = 4,
|
||||
mono = false,
|
||||
}: {
|
||||
values: string[];
|
||||
empty?: string;
|
||||
max?: number;
|
||||
mono?: boolean;
|
||||
}) {
|
||||
if (values.length === 0) return <span className="text-muted-foreground">{empty}</span>;
|
||||
|
||||
const shown = values.slice(0, max);
|
||||
const extra = values.length - shown.length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{shown.map((v) => (
|
||||
<span
|
||||
key={v}
|
||||
className={[
|
||||
"rounded bg-muted px-2 py-1 text-xs",
|
||||
mono ? "font-mono" : "",
|
||||
].join(" ")}
|
||||
title={v}
|
||||
>
|
||||
{v}
|
||||
</span>
|
||||
))}
|
||||
{extra > 0 ? <span className="text-xs text-muted-foreground">+{extra} more</span> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TagTableMain({ tags }: { tags: TagRow[] }) {
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[22%]">Name</TableHead>
|
||||
<TableHead className="w-[16%]">Slug</TableHead>
|
||||
<TableHead className="w-[26%]">Aliases</TableHead>
|
||||
<TableHead className="w-[26%]">Categories</TableHead>
|
||||
<TableHead className="w-[10%] text-right">Artworks</TableHead>
|
||||
<TableHead className="w-[8%] text-right" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{tags.map((t) => (
|
||||
<TableRow key={t.id}>
|
||||
<TableCell className="font-medium">{t.name}</TableCell>
|
||||
|
||||
<TableCell className="font-mono text-sm text-muted-foreground">
|
||||
#{t.slug}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Chips values={t.aliases.map((a) => a.alias)} mono max={5} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Chips values={t.categories.map((c) => c.name)} max={4} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{t._count.artworks}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right">
|
||||
<Link href={`/tags/${t.id}`} aria-label={`Edit ${t.name}`}>
|
||||
<Button size="icon" variant="secondary">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
src/components/tags/TagTabs.tsx
Normal file
66
src/components/tags/TagTabs.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import * as React from "react";
|
||||
// import TagTableMain from "@/components/tags/TagTableMain";
|
||||
// import TagTableAnimal from "@/components/tags/TagTableAnimal";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import TagTableAnimal from "./TagTableAnimal";
|
||||
import TagTableMain from "./TagTableMain";
|
||||
|
||||
export type TagRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
parent: { id: string; name: string } | null;
|
||||
isParent: boolean;
|
||||
showOnAnimalPage: boolean;
|
||||
aliases: { alias: string }[];
|
||||
categories: { id: string; name: string }[];
|
||||
_count: { artworks: number };
|
||||
};
|
||||
|
||||
function isAnimalStudiesTag(t: TagRow) {
|
||||
// Recommended: primarily category-based; keep showOnAnimalPage as fallback.
|
||||
const inCategory = t.categories.some((c) => c.name === "Animal Studies");
|
||||
// return inCategory || t.showOnAnimalPage;
|
||||
return inCategory;
|
||||
}
|
||||
|
||||
export default function TagTabs({ tags }: { tags: TagRow[] }) {
|
||||
const animal = React.useMemo(() => tags.filter(isAnimalStudiesTag), [tags]);
|
||||
const normal = React.useMemo(
|
||||
() => tags.filter((t) => !isAnimalStudiesTag(t)),
|
||||
[tags, animal.length]
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="all" className="w-full">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="all" className="gap-2">
|
||||
All tags <Badge variant="secondary">{tags.length}</Badge>
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger value="animal" className="gap-2">
|
||||
Animal Studies <Badge variant="secondary">{animal.length}</Badge>
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger value="normal" className="gap-2">
|
||||
Other <Badge variant="secondary">{normal.length}</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="all">
|
||||
<TagTableMain tags={tags} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="animal">
|
||||
<TagTableAnimal tags={animal} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="normal">
|
||||
<TagTableMain tags={normal} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
66
src/components/ui/tabs.tsx
Normal file
66
src/components/ui/tabs.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
Reference in New Issue
Block a user