Changed some stuf on artworks table
This commit is contained in:
@ -1,94 +1,12 @@
|
||||
import { ArtworkColorProcessor } from "@/components/artworks/ArtworkColorProcessor";
|
||||
import { ArtworkGalleryVariantProcessor } from "@/components/artworks/ArtworkGalleryVariantProcessor";
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold">Artworks</h1>
|
||||
{/* <ProcessArtworkColorsButton /> */}
|
||||
<ArtworkColorProcessor />
|
||||
<ArtworkGalleryVariantProcessor />
|
||||
<ArtworksTable />
|
||||
</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 >
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
SortingState,
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
@ -65,7 +65,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ArtworkTableRow } from "@/schemas/artworks/tableSchema";
|
||||
import type { ArtworkTableRow } from "@/schemas/artworks/tableSchema";
|
||||
import { MultiSelectFilter } from "./MultiSelectFilter";
|
||||
|
||||
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 (
|
||||
<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"}
|
||||
</Badge>
|
||||
);
|
||||
@ -140,7 +146,10 @@ function TriSelectInline(props: {
|
||||
onChange: (v: TriState) => void;
|
||||
}) {
|
||||
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">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@ -165,7 +174,7 @@ type Filters = {
|
||||
|
||||
export function ArtworksTable() {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([
|
||||
{ id: "createdAt", desc: true },
|
||||
{ id: "updatedAt", desc: true },
|
||||
]);
|
||||
const [pageIndex, setPageIndex] = React.useState(0);
|
||||
const [pageSize, setPageSize] = React.useState(25);
|
||||
@ -186,14 +195,20 @@ export function ArtworksTable() {
|
||||
const [rows, setRows] = React.useState<ArtworkTableRow[]>([]);
|
||||
const [total, setTotal] = React.useState(0);
|
||||
|
||||
const [albumOptions, setAlbumOptions] = React.useState<{ id: string; name: string }[]>([]);
|
||||
const [categoryOptions, setCategoryOptions] = React.useState<{ id: string; name: string }[]>([]);
|
||||
const [albumOptions, setAlbumOptions] = React.useState<
|
||||
{ id: string; name: string }[]
|
||||
>([]);
|
||||
const [categoryOptions, setCategoryOptions] = React.useState<
|
||||
{ id: string; name: string }[]
|
||||
>([]);
|
||||
|
||||
const [isPending, startTransition] = React.useTransition();
|
||||
|
||||
// Delete dialog
|
||||
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));
|
||||
|
||||
@ -203,7 +218,6 @@ export function ArtworksTable() {
|
||||
setAlbumOptions(res.albums);
|
||||
setCategoryOptions(res.categories);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const columns = React.useMemo<ColumnDef<ArtworkTableRow>[]>(
|
||||
@ -240,21 +254,33 @@ export function ArtworksTable() {
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-semibold">{row.original.name}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">{row.original.slug}</div>
|
||||
<div className="truncate text-sm font-semibold">
|
||||
{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">
|
||||
<YesNoBadge value={row.original.published} />
|
||||
<YesNoBadge value={row.original.nsfw} variant="secondary" />
|
||||
<YesNoBadge value={row.original.needsWork} variant="secondary" />
|
||||
<YesNoBadge
|
||||
value={row.original.nsfw}
|
||||
variant="secondary"
|
||||
/>
|
||||
<YesNoBadge
|
||||
value={row.original.needsWork}
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 space-y-2 text-xs">
|
||||
<div className="text-muted-foreground">
|
||||
Created: {new Date(row.original.createdAt).toLocaleString()}
|
||||
Created:{" "}
|
||||
{new Date(row.original.createdAt).toLocaleString()}
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
Updated: {new Date(row.original.updatedAt).toLocaleString()}
|
||||
Updated:{" "}
|
||||
{new Date(row.original.updatedAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -299,18 +325,28 @@ export function ArtworksTable() {
|
||||
accessorKey: "albumsCount",
|
||||
cell: ({ row }) => (
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium tabular-nums">{row.original.albumsCount}</div>
|
||||
{row.original.albums.length ? <Chips items={row.original.albums} /> : <span className="text-xs text-muted-foreground">—</span>}
|
||||
<div className="text-sm font-medium tabular-nums">
|
||||
{row.original.albumsCount}
|
||||
</div>
|
||||
{row.original.albums.length ? (
|
||||
<Chips items={row.original.albums} />
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "categories",
|
||||
header: ({ column }) => <SortHeader title="Categories #" column={column} />,
|
||||
header: ({ column }) => (
|
||||
<SortHeader title="Categories #" column={column} />
|
||||
),
|
||||
accessorKey: "categoriesCount",
|
||||
cell: ({ row }) => (
|
||||
<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 ? (
|
||||
<Chips items={row.original.categories} />
|
||||
) : (
|
||||
@ -321,25 +357,26 @@ export function ArtworksTable() {
|
||||
},
|
||||
{
|
||||
accessorKey: "published",
|
||||
header: ({ column }) => <SortHeader title="Published" column={column} />,
|
||||
header: ({ column }) => (
|
||||
<SortHeader title="Published" column={column} />
|
||||
),
|
||||
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",
|
||||
header: ({ column }) => <SortHeader title="Needs Work" column={column} />,
|
||||
cell: ({ row }) => <YesNoBadge value={row.original.needsWork} variant="secondary" />,
|
||||
header: ({ column }) => (
|
||||
<SortHeader title="Needs Work" column={column} />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<YesNoBadge value={row.original.needsWork} variant="secondary" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: ({ column }) => <SortHeader title="Created" column={column} />,
|
||||
accessorKey: "updatedAt",
|
||||
header: ({ column }) => <SortHeader title="Updated" column={column} />,
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-foreground/80">
|
||||
{new Date(row.original.createdAt).toLocaleDateString()}
|
||||
{new Date(row.original.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
@ -362,7 +399,10 @@ export function ArtworksTable() {
|
||||
|
||||
<DropdownMenuContent align="end" className="w-44">
|
||||
<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" />
|
||||
Edit
|
||||
</Link>
|
||||
@ -397,7 +437,9 @@ export function ArtworksTable() {
|
||||
manualSorting: true,
|
||||
pageCount,
|
||||
onSortingChange: (updater) => {
|
||||
setSorting((prev) => (typeof updater === "function" ? updater(prev) : updater));
|
||||
setSorting((prev) =>
|
||||
typeof updater === "function" ? updater(prev) : updater,
|
||||
);
|
||||
setPageIndex(0);
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
@ -412,10 +454,11 @@ export function ArtworksTable() {
|
||||
name: debouncedName || undefined,
|
||||
slug: debouncedSlug || undefined,
|
||||
published: filters.published,
|
||||
nsfw: filters.nsfw,
|
||||
needsWork: filters.needsWork,
|
||||
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,
|
||||
debouncedSlug,
|
||||
filters.published,
|
||||
filters.nsfw,
|
||||
filters.needsWork,
|
||||
filters.albumIds,
|
||||
filters.categoryIds,
|
||||
@ -453,7 +495,10 @@ export function ArtworksTable() {
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
@ -464,7 +509,10 @@ export function ArtworksTable() {
|
||||
const colId = header.column.id;
|
||||
|
||||
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" ? (
|
||||
<Input
|
||||
className="h-9"
|
||||
@ -493,14 +541,6 @@ export function ArtworksTable() {
|
||||
setPageIndex(0);
|
||||
}}
|
||||
/>
|
||||
) : colId === "nsfw" ? (
|
||||
<TriSelectInline
|
||||
value={filters.nsfw}
|
||||
onChange={(v) => {
|
||||
setFilters((f) => ({ ...f, nsfw: v }));
|
||||
setPageIndex(0);
|
||||
}}
|
||||
/>
|
||||
) : colId === "needsWork" ? (
|
||||
<TriSelectInline
|
||||
value={filters.needsWork}
|
||||
@ -541,7 +581,10 @@ export function ArtworksTable() {
|
||||
<TableBody>
|
||||
{rows.length === 0 ? (
|
||||
<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">
|
||||
{isPending ? "Loading…" : "No results."}
|
||||
</div>
|
||||
@ -561,8 +604,14 @@ export function ArtworksTable() {
|
||||
].join(" ")}
|
||||
>
|
||||
{r.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="py-3 align-top border-b border-border/40">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className="py-3 align-top border-b border-border/40"
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
@ -645,7 +694,9 @@ export function ArtworksTable() {
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete artwork?</AlertDialogTitle>
|
||||
<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>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
@ -670,10 +721,13 @@ export function ArtworksTable() {
|
||||
name: debouncedName || undefined,
|
||||
slug: debouncedSlug || undefined,
|
||||
published: filters.published,
|
||||
nsfw: filters.nsfw,
|
||||
needsWork: filters.needsWork,
|
||||
albumIds: filters.albumIds.length ? filters.albumIds : undefined,
|
||||
categoryIds: filters.categoryIds.length ? filters.categoryIds : undefined,
|
||||
albumIds: filters.albumIds.length
|
||||
? filters.albumIds
|
||||
: undefined,
|
||||
categoryIds: filters.categoryIds.length
|
||||
? filters.categoryIds
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
setRows(res.rows);
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@ -43,7 +44,12 @@ export default function AdminSidebar() {
|
||||
key={entry.href}
|
||||
asChild
|
||||
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>
|
||||
</Button>
|
||||
@ -65,11 +71,14 @@ export default function AdminSidebar() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"justify-start",
|
||||
anyChildActive && "font-medium"
|
||||
"group w-full justify-between border border-transparent",
|
||||
"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>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
@ -83,7 +92,12 @@ export default function AdminSidebar() {
|
||||
asChild
|
||||
variant={active ? "secondary" : "ghost"}
|
||||
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>
|
||||
</Button>
|
||||
|
||||
@ -17,54 +17,41 @@ export type AdminNavGroup =
|
||||
|
||||
export const adminNav: AdminNavGroup[] = [
|
||||
{ type: "link", title: "Home", href: "/" },
|
||||
|
||||
{
|
||||
type: "group",
|
||||
title: "Upload",
|
||||
title: "Uploads",
|
||||
items: [
|
||||
{ title: "Single Image", href: "/uploads/single" },
|
||||
{ title: "Multiple Images", href: "/uploads/bulk" },
|
||||
],
|
||||
},
|
||||
|
||||
{ type: "link", title: "Artworks", href: "/artworks" },
|
||||
|
||||
{
|
||||
type: "group",
|
||||
title: "Artwork Management",
|
||||
title: "Artworks",
|
||||
items: [
|
||||
{ title: "Artwork List", href: "/artworks" },
|
||||
{ title: "Categories", href: "/categories" },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
type: "group",
|
||||
title: "Topics",
|
||||
title: "General",
|
||||
items: [{ title: "Tags", href: "/tags" }],
|
||||
},
|
||||
|
||||
{
|
||||
type: "group",
|
||||
title: "Commissions",
|
||||
items: [
|
||||
{ title: "Requests", href: "/commissions/requests" },
|
||||
{ title: "Board", href: "/commissions/kanban" },
|
||||
|
||||
{ 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: "TypeExtras", href: "/commissions/types/extras" },
|
||||
{ title: "Guidelines", href: "/commissions/guidelines" },
|
||||
],
|
||||
},
|
||||
|
||||
{ type: "link", title: "Terms of Service", href: "/tos" },
|
||||
|
||||
{
|
||||
type: "group",
|
||||
title: "Users",
|
||||
items: [
|
||||
{ title: "Users", href: "/users" },
|
||||
{ title: "New User", href: "/users/new" },
|
||||
],
|
||||
},
|
||||
{ type: "link", title: "Users", href: "/users" },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user