Changed some stuf on artworks table

This commit is contained in:
2026-02-02 19:19:52 +01:00
parent c915df904d
commit 0c72a756c5
4 changed files with 136 additions and 163 deletions

View File

@ -1,94 +1,12 @@
import { ArtworkColorProcessor } from "@/components/artworks/ArtworkColorProcessor"; import { ArtworkColorProcessor } from "@/components/artworks/ArtworkColorProcessor";
import { ArtworkGalleryVariantProcessor } from "@/components/artworks/ArtworkGalleryVariantProcessor";
import { ArtworksTable } from "@/components/artworks/ArtworksTable"; import { ArtworksTable } from "@/components/artworks/ArtworksTable";
import { getArtworksPage } from "@/lib/queryArtworks";
export default async function ArtworksPage({
searchParams
}: {
searchParams?: {
// type?: string;
published?: string;
// groupBy?: string;
// year?: string;
// album?: string;
cursor?: string;
}
}) {
const {
// type = "all",
published = "all",
// groupBy = "year",
// year,
// album,
cursor = undefined
} = await searchParams ?? {};
// const groupMode = groupBy === "album" ? "album" : "year";
// const groupId = groupMode === "album" ? album ?? "all" : year ?? "all";
// Filter by type
// if (type !== "all") {
// where.typeId = type === "none" ? null : type;
// }
// Filter by published status
// if (published === "published") {
// where.published = true;
// } else if (published === "unpublished") {
// where.published = false;
// } else if (published === "needsWork") {
// where.needsWork = true;
// }
// Filter by group (year or album)
// if (groupMode === "year" && groupId !== "all") {
// where.year = parseInt(groupId);
// } else if (groupMode === "album" && groupId !== "all") {
// where.albumId = groupId;
// }
const { items, nextCursor } = await getArtworksPage(
{
published,
cursor,
take: 48
}
)
export default async function ArtworksPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<h1 className="text-2xl font-bold">Artworks</h1> <h1 className="text-2xl font-bold">Artworks</h1>
{/* <ProcessArtworkColorsButton /> */}
<ArtworkColorProcessor /> <ArtworkColorProcessor />
<ArtworkGalleryVariantProcessor />
<ArtworksTable /> <ArtworksTable />
</div> </div>
// <div>
// <div className="flex justify-between pb-4 items-end">
// <h1 className="text-2xl font-bold mb-4">Artworks</h1>
// <Link href="/uploads/single" 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" /> Upload new artwork
// </Link>
// <Link href="/uploads/bulk" 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" /> Upload many artwork
// </Link>
// </div>
// <FilterBar
// // types={types}
// // albums={albums}
// // years={years}
// // currentType={type}
// currentPublished={published}
// // groupBy={groupMode}
// // groupId={groupId}
// />
// <div className="mt-6">
// {items.length > 0 ? (
// <ArtworkGallery initialArtworks={items} initialCursor={nextCursor} />
// ) : (
// <p className="text-muted-foreground italic">No artworks found.</p>
// )}
// </div>
// </div >
); );
} }

View File

@ -1,10 +1,10 @@
"use client"; "use client";
import { import {
ColumnDef, type ColumnDef,
SortingState,
flexRender, flexRender,
getCoreRowModel, getCoreRowModel,
type SortingState,
useReactTable, useReactTable,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { import {
@ -65,7 +65,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { ArtworkTableRow } from "@/schemas/artworks/tableSchema"; import type { ArtworkTableRow } from "@/schemas/artworks/tableSchema";
import { MultiSelectFilter } from "./MultiSelectFilter"; import { MultiSelectFilter } from "./MultiSelectFilter";
type TriState = "any" | "true" | "false"; type TriState = "any" | "true" | "false";
@ -103,9 +103,15 @@ function SortHeader(props: {
); );
} }
function YesNoBadge(props: { value: boolean; variant?: "default" | "secondary" }) { function YesNoBadge(props: {
value: boolean;
variant?: "default" | "secondary";
}) {
return ( return (
<Badge variant={props.value ? "default" : "secondary"} className="px-2 py-0.5"> <Badge
variant={props.value ? "default" : "secondary"}
className="px-2 py-0.5"
>
{props.value ? "Yes" : "No"} {props.value ? "Yes" : "No"}
</Badge> </Badge>
); );
@ -140,7 +146,10 @@ function TriSelectInline(props: {
onChange: (v: TriState) => void; onChange: (v: TriState) => void;
}) { }) {
return ( return (
<Select value={props.value} onValueChange={(v) => props.onChange(v as TriState)}> <Select
value={props.value}
onValueChange={(v) => props.onChange(v as TriState)}
>
<SelectTrigger className="h-9"> <SelectTrigger className="h-9">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
@ -165,7 +174,7 @@ type Filters = {
export function ArtworksTable() { export function ArtworksTable() {
const [sorting, setSorting] = React.useState<SortingState>([ const [sorting, setSorting] = React.useState<SortingState>([
{ id: "createdAt", desc: true }, { id: "updatedAt", desc: true },
]); ]);
const [pageIndex, setPageIndex] = React.useState(0); const [pageIndex, setPageIndex] = React.useState(0);
const [pageSize, setPageSize] = React.useState(25); const [pageSize, setPageSize] = React.useState(25);
@ -186,14 +195,20 @@ export function ArtworksTable() {
const [rows, setRows] = React.useState<ArtworkTableRow[]>([]); const [rows, setRows] = React.useState<ArtworkTableRow[]>([]);
const [total, setTotal] = React.useState(0); const [total, setTotal] = React.useState(0);
const [albumOptions, setAlbumOptions] = React.useState<{ id: string; name: string }[]>([]); const [albumOptions, setAlbumOptions] = React.useState<
const [categoryOptions, setCategoryOptions] = React.useState<{ id: string; name: string }[]>([]); { id: string; name: string }[]
>([]);
const [categoryOptions, setCategoryOptions] = React.useState<
{ id: string; name: string }[]
>([]);
const [isPending, startTransition] = React.useTransition(); const [isPending, startTransition] = React.useTransition();
// Delete dialog
const [deleteOpen, setDeleteOpen] = React.useState(false); const [deleteOpen, setDeleteOpen] = React.useState(false);
const [deleteTarget, setDeleteTarget] = React.useState<{ id: string; name: string } | null>(null); const [deleteTarget, setDeleteTarget] = React.useState<{
id: string;
name: string;
} | null>(null);
const pageCount = Math.max(1, Math.ceil(total / pageSize)); const pageCount = Math.max(1, Math.ceil(total / pageSize));
@ -203,7 +218,6 @@ export function ArtworksTable() {
setAlbumOptions(res.albums); setAlbumOptions(res.albums);
setCategoryOptions(res.categories); setCategoryOptions(res.categories);
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const columns = React.useMemo<ColumnDef<ArtworkTableRow>[]>( const columns = React.useMemo<ColumnDef<ArtworkTableRow>[]>(
@ -240,21 +254,33 @@ export function ArtworksTable() {
/> />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold">{row.original.name}</div> <div className="truncate text-sm font-semibold">
<div className="truncate text-xs text-muted-foreground">{row.original.slug}</div> {row.original.name}
</div>
<div className="truncate text-xs text-muted-foreground">
{row.original.slug}
</div>
<div className="mt-3 flex flex-wrap gap-2"> <div className="mt-3 flex flex-wrap gap-2">
<YesNoBadge value={row.original.published} /> <YesNoBadge value={row.original.published} />
<YesNoBadge value={row.original.nsfw} variant="secondary" /> <YesNoBadge
<YesNoBadge value={row.original.needsWork} variant="secondary" /> value={row.original.nsfw}
variant="secondary"
/>
<YesNoBadge
value={row.original.needsWork}
variant="secondary"
/>
</div> </div>
<div className="mt-3 space-y-2 text-xs"> <div className="mt-3 space-y-2 text-xs">
<div className="text-muted-foreground"> <div className="text-muted-foreground">
Created: {new Date(row.original.createdAt).toLocaleString()} Created:{" "}
{new Date(row.original.createdAt).toLocaleString()}
</div> </div>
<div className="text-muted-foreground"> <div className="text-muted-foreground">
Updated: {new Date(row.original.updatedAt).toLocaleString()} Updated:{" "}
{new Date(row.original.updatedAt).toLocaleString()}
</div> </div>
</div> </div>
</div> </div>
@ -299,18 +325,28 @@ export function ArtworksTable() {
accessorKey: "albumsCount", accessorKey: "albumsCount",
cell: ({ row }) => ( cell: ({ row }) => (
<div className="space-y-1"> <div className="space-y-1">
<div className="text-sm font-medium tabular-nums">{row.original.albumsCount}</div> <div className="text-sm font-medium tabular-nums">
{row.original.albums.length ? <Chips items={row.original.albums} /> : <span className="text-xs text-muted-foreground"></span>} {row.original.albumsCount}
</div>
{row.original.albums.length ? (
<Chips items={row.original.albums} />
) : (
<span className="text-xs text-muted-foreground"></span>
)}
</div> </div>
), ),
}, },
{ {
id: "categories", id: "categories",
header: ({ column }) => <SortHeader title="Categories #" column={column} />, header: ({ column }) => (
<SortHeader title="Categories #" column={column} />
),
accessorKey: "categoriesCount", accessorKey: "categoriesCount",
cell: ({ row }) => ( cell: ({ row }) => (
<div className="space-y-1"> <div className="space-y-1">
<div className="text-sm font-medium tabular-nums">{row.original.categoriesCount}</div> <div className="text-sm font-medium tabular-nums">
{row.original.categoriesCount}
</div>
{row.original.categories.length ? ( {row.original.categories.length ? (
<Chips items={row.original.categories} /> <Chips items={row.original.categories} />
) : ( ) : (
@ -321,25 +357,26 @@ export function ArtworksTable() {
}, },
{ {
accessorKey: "published", accessorKey: "published",
header: ({ column }) => <SortHeader title="Published" column={column} />, header: ({ column }) => (
<SortHeader title="Published" column={column} />
),
cell: ({ row }) => <YesNoBadge value={row.original.published} />, cell: ({ row }) => <YesNoBadge value={row.original.published} />,
}, },
{
accessorKey: "nsfw",
header: ({ column }) => <SortHeader title="NSFW" column={column} />,
cell: ({ row }) => <YesNoBadge value={row.original.nsfw} variant="secondary" />,
},
{ {
accessorKey: "needsWork", accessorKey: "needsWork",
header: ({ column }) => <SortHeader title="Needs Work" column={column} />, header: ({ column }) => (
cell: ({ row }) => <YesNoBadge value={row.original.needsWork} variant="secondary" />, <SortHeader title="Needs Work" column={column} />
),
cell: ({ row }) => (
<YesNoBadge value={row.original.needsWork} variant="secondary" />
),
}, },
{ {
accessorKey: "createdAt", accessorKey: "updatedAt",
header: ({ column }) => <SortHeader title="Created" column={column} />, header: ({ column }) => <SortHeader title="Updated" column={column} />,
cell: ({ row }) => ( cell: ({ row }) => (
<span className="text-sm text-foreground/80"> <span className="text-sm text-foreground/80">
{new Date(row.original.createdAt).toLocaleDateString()} {new Date(row.original.updatedAt).toLocaleDateString()}
</span> </span>
), ),
}, },
@ -362,7 +399,10 @@ export function ArtworksTable() {
<DropdownMenuContent align="end" className="w-44"> <DropdownMenuContent align="end" className="w-44">
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link href={`/artworks/${item.id}`} className="cursor-pointer"> <Link
href={`/artworks/${item.id}`}
className="cursor-pointer"
>
<Pencil className="mr-2 h-4 w-4" /> <Pencil className="mr-2 h-4 w-4" />
Edit Edit
</Link> </Link>
@ -397,7 +437,9 @@ export function ArtworksTable() {
manualSorting: true, manualSorting: true,
pageCount, pageCount,
onSortingChange: (updater) => { onSortingChange: (updater) => {
setSorting((prev) => (typeof updater === "function" ? updater(prev) : updater)); setSorting((prev) =>
typeof updater === "function" ? updater(prev) : updater,
);
setPageIndex(0); setPageIndex(0);
}, },
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
@ -412,10 +454,11 @@ export function ArtworksTable() {
name: debouncedName || undefined, name: debouncedName || undefined,
slug: debouncedSlug || undefined, slug: debouncedSlug || undefined,
published: filters.published, published: filters.published,
nsfw: filters.nsfw,
needsWork: filters.needsWork, needsWork: filters.needsWork,
albumIds: filters.albumIds.length ? filters.albumIds : undefined, albumIds: filters.albumIds.length ? filters.albumIds : undefined,
categoryIds: filters.categoryIds.length ? filters.categoryIds : undefined, categoryIds: filters.categoryIds.length
? filters.categoryIds
: undefined,
}, },
}); });
@ -429,7 +472,6 @@ export function ArtworksTable() {
debouncedName, debouncedName,
debouncedSlug, debouncedSlug,
filters.published, filters.published,
filters.nsfw,
filters.needsWork, filters.needsWork,
filters.albumIds, filters.albumIds,
filters.categoryIds, filters.categoryIds,
@ -453,7 +495,10 @@ export function ArtworksTable() {
> >
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender(header.column.columnDef.header, header.getContext())} : flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead> </TableHead>
))} ))}
</TableRow> </TableRow>
@ -464,7 +509,10 @@ export function ArtworksTable() {
const colId = header.column.id; const colId = header.column.id;
return ( return (
<TableHead key={header.id} className="border-b border-border/70 bg-muted/30 py-2"> <TableHead
key={header.id}
className="border-b border-border/70 bg-muted/30 py-2"
>
{colId === "name" ? ( {colId === "name" ? (
<Input <Input
className="h-9" className="h-9"
@ -493,14 +541,6 @@ export function ArtworksTable() {
setPageIndex(0); setPageIndex(0);
}} }}
/> />
) : colId === "nsfw" ? (
<TriSelectInline
value={filters.nsfw}
onChange={(v) => {
setFilters((f) => ({ ...f, nsfw: v }));
setPageIndex(0);
}}
/>
) : colId === "needsWork" ? ( ) : colId === "needsWork" ? (
<TriSelectInline <TriSelectInline
value={filters.needsWork} value={filters.needsWork}
@ -541,7 +581,10 @@ export function ArtworksTable() {
<TableBody> <TableBody>
{rows.length === 0 ? ( {rows.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={columns.length} className="py-14 text-center"> <TableCell
colSpan={columns.length}
className="py-14 text-center"
>
<div className="text-sm font-medium"> <div className="text-sm font-medium">
{isPending ? "Loading…" : "No results."} {isPending ? "Loading…" : "No results."}
</div> </div>
@ -561,8 +604,14 @@ export function ArtworksTable() {
].join(" ")} ].join(" ")}
> >
{r.getVisibleCells().map((cell) => ( {r.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="py-3 align-top border-b border-border/40"> <TableCell
{flexRender(cell.column.columnDef.cell, cell.getContext())} key={cell.id}
className="py-3 align-top border-b border-border/40"
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell> </TableCell>
))} ))}
</TableRow> </TableRow>
@ -645,7 +694,9 @@ export function ArtworksTable() {
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete artwork?</AlertDialogTitle> <AlertDialogTitle>Delete artwork?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This will delete <span className="font-medium">{deleteTarget?.name}</span>. This action cannot be undone. This will delete{" "}
<span className="font-medium">{deleteTarget?.name}</span>. This
action cannot be undone.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
@ -670,10 +721,13 @@ export function ArtworksTable() {
name: debouncedName || undefined, name: debouncedName || undefined,
slug: debouncedSlug || undefined, slug: debouncedSlug || undefined,
published: filters.published, published: filters.published,
nsfw: filters.nsfw,
needsWork: filters.needsWork, needsWork: filters.needsWork,
albumIds: filters.albumIds.length ? filters.albumIds : undefined, albumIds: filters.albumIds.length
categoryIds: filters.categoryIds.length ? filters.categoryIds : undefined, ? filters.albumIds
: undefined,
categoryIds: filters.categoryIds.length
? filters.categoryIds
: undefined,
}, },
}); });
setRows(res.rows); setRows(res.rows);

View File

@ -2,6 +2,7 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { ChevronDown } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -43,7 +44,12 @@ export default function AdminSidebar() {
key={entry.href} key={entry.href}
asChild asChild
variant={active ? "secondary" : "ghost"} variant={active ? "secondary" : "ghost"}
className={cn("justify-start")} className={cn(
"justify-start border border-transparent",
"hover:bg-muted/60 dark:hover:bg-muted/40",
active &&
"bg-primary/10 text-primary border-primary/30 dark:bg-primary/20"
)}
> >
<Link href={entry.href}>{entry.title}</Link> <Link href={entry.href}>{entry.title}</Link>
</Button> </Button>
@ -65,11 +71,14 @@ export default function AdminSidebar() {
<Button <Button
variant="ghost" variant="ghost"
className={cn( className={cn(
"justify-start", "group w-full justify-between border border-transparent",
anyChildActive && "font-medium" "hover:bg-muted/60 dark:hover:bg-muted/40",
"data-[state=open]:bg-muted/50 dark:data-[state=open]:bg-muted/30",
anyChildActive && "font-medium text-foreground"
)} )}
> >
{entry.title} <span>{entry.title}</span>
<ChevronDown className="h-4 w-4 transition-transform group-data-[state=open]:rotate-180" />
</Button> </Button>
</CollapsibleTrigger> </CollapsibleTrigger>
@ -83,7 +92,12 @@ export default function AdminSidebar() {
asChild asChild
variant={active ? "secondary" : "ghost"} variant={active ? "secondary" : "ghost"}
size="sm" size="sm"
className="justify-start" className={cn(
"justify-start border border-transparent",
"hover:bg-muted/60 dark:hover:bg-muted/40",
active &&
"bg-primary/10 text-primary border-primary/30 dark:bg-primary/20"
)}
> >
<Link href={item.href}>{item.title}</Link> <Link href={item.href}>{item.title}</Link>
</Button> </Button>

View File

@ -17,54 +17,41 @@ export type AdminNavGroup =
export const adminNav: AdminNavGroup[] = [ export const adminNav: AdminNavGroup[] = [
{ type: "link", title: "Home", href: "/" }, { type: "link", title: "Home", href: "/" },
{ {
type: "group", type: "group",
title: "Upload", title: "Uploads",
items: [ items: [
{ title: "Single Image", href: "/uploads/single" }, { title: "Single Image", href: "/uploads/single" },
{ title: "Multiple Images", href: "/uploads/bulk" }, { title: "Multiple Images", href: "/uploads/bulk" },
], ],
}, },
{ type: "link", title: "Artworks", href: "/artworks" },
{ {
type: "group", type: "group",
title: "Artwork Management", title: "Artworks",
items: [ items: [
{ title: "Artwork List", href: "/artworks" },
{ title: "Categories", href: "/categories" }, { title: "Categories", href: "/categories" },
], ],
}, },
{ {
type: "group", type: "group",
title: "Topics", title: "General",
items: [{ title: "Tags", href: "/tags" }], items: [{ title: "Tags", href: "/tags" }],
}, },
{ {
type: "group", type: "group",
title: "Commissions", title: "Commissions",
items: [ items: [
{ title: "Requests", href: "/commissions/requests" }, { title: "Requests", href: "/commissions/requests" },
{ title: "Board", href: "/commissions/kanban" }, { title: "Board", href: "/commissions/kanban" },
{ title: "Types", href: "/commissions/types" }, { title: "Types", href: "/commissions/types" },
{ title: "Custom Cards", href: "/commissions/custom-cards" }, { title: "Custom (YCH)", href: "/commissions/custom-cards" },
{ title: "TypeOptions", href: "/commissions/types/options" }, { title: "TypeOptions", href: "/commissions/types/options" },
{ title: "TypeExtras", href: "/commissions/types/extras" }, { title: "TypeExtras", href: "/commissions/types/extras" },
{ title: "Guidelines", href: "/commissions/guidelines" }, { title: "Guidelines", href: "/commissions/guidelines" },
], ],
}, },
{ type: "link", title: "Terms of Service", href: "/tos" }, { type: "link", title: "Terms of Service", href: "/tos" },
{ type: "link", title: "Users", href: "/users" },
{
type: "group",
title: "Users",
items: [
{ title: "Users", href: "/users" },
{ title: "New User", href: "/users/new" },
],
},
]; ];