Add animal bool to tags
This commit is contained in:
2
prisma/migrations/20251221153124_artwork_6/migration.sql
Normal file
2
prisma/migrations/20251221153124_artwork_6/migration.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ArtTag" ADD COLUMN "showOnAnimalPage" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@ -101,8 +101,9 @@ model ArtTag {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
sortIndex Int @default(0)
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
name String @unique
|
name String @unique
|
||||||
slug String @unique
|
slug String @unique
|
||||||
|
showOnAnimalPage Boolean @default(false)
|
||||||
|
|
||||||
description String?
|
description String?
|
||||||
|
|
||||||
|
|||||||
37
src/actions/categories/deleteCategory.ts
Normal file
37
src/actions/categories/deleteCategory.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
|
export async function deleteCategory(catId: string) {
|
||||||
|
const cat = await prisma.artCategory.findUnique({
|
||||||
|
where: { id: catId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
tags: true,
|
||||||
|
artworks: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cat) {
|
||||||
|
throw new Error("Category not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cat._count.artworks > 0) {
|
||||||
|
throw new Error("Cannot delete category: it is used by artworks.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cat._count.tags > 0) {
|
||||||
|
throw new Error("Cannot delete category: it is used by tags.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.artCategory.delete({ where: { id: catId } });
|
||||||
|
|
||||||
|
revalidatePath("/categories");
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@ export async function createTag(formData: TagFormInput) {
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
slug: tagSlug,
|
slug: tagSlug,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
|
showOnAnimalPage: data.showOnAnimalPage,
|
||||||
parentId
|
parentId
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -32,7 +32,8 @@ export async function updateTag(id: string, rawData: TagFormInput) {
|
|||||||
data: {
|
data: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
slug: tagSlug,
|
slug: tagSlug,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
|
showOnAnimalPage: data.showOnAnimalPage,
|
||||||
parentId,
|
parentId,
|
||||||
categories: data.categoryIds
|
categories: data.categoryIds
|
||||||
? { set: data.categoryIds.map((cid) => ({ id: cid })) }
|
? { set: data.categoryIds.map((cid) => ({ id: cid })) }
|
||||||
|
|||||||
@ -1,10 +1,15 @@
|
|||||||
import ItemList from "@/components/lists/ItemList";
|
import CategoryTable from "@/components/categories/CategoryTable";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { PlusCircleIcon } from "lucide-react";
|
import { PlusCircleIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default async function CategoriesPage() {
|
export default async function CategoriesPage() {
|
||||||
const items = await prisma.artCategory.findMany({})
|
const items = await prisma.artCategory.findMany({
|
||||||
|
include: {
|
||||||
|
_count: { select: { artworks: true, tags: true } },
|
||||||
|
},
|
||||||
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -14,7 +19,11 @@ export default async function CategoriesPage() {
|
|||||||
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Add new category
|
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Add new category
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{items && items.length > 0 ? <ItemList items={items} type="categories" /> : <p>There are no categories yet. Consider adding some!</p>}
|
{items.length > 0 ? (
|
||||||
|
<CategoryTable categories={items} />
|
||||||
|
) : (
|
||||||
|
<p>There are no categories yet. Consider adding some!</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
82
src/components/categories/CategoryTable.tsx
Normal file
82
src/components/categories/CategoryTable.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { deleteCategory } from "@/actions/categories/deleteCategory";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { PencilIcon, Trash2Icon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
type CatRow = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
_count: { artworks: number, tags: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CategoryTable({ categories }: { categories: CatRow[] }) {
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
deleteCategory(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[16%]">Name</TableHead>
|
||||||
|
<TableHead className="w-[12%]">Slug</TableHead>
|
||||||
|
<TableHead className="w-[8%] text-right">Tags</TableHead>
|
||||||
|
<TableHead className="w-[8%] text-right">Artworks</TableHead>
|
||||||
|
<TableHead className="w-[10%] text-right" />
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{categories.map((c) => (
|
||||||
|
<TableRow key={c.id}>
|
||||||
|
<TableCell className="font-medium">{c.name}</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="font-mono text-sm text-muted-foreground">
|
||||||
|
{c.slug}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="text-right tabular-nums">
|
||||||
|
{c._count.tags}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="text-right tabular-nums">
|
||||||
|
{c._count.artworks}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Link href={`/categories/${c.id}`} aria-label={`Edit ${c.name}`}>
|
||||||
|
<Button size="icon" variant="secondary">
|
||||||
|
<PencilIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="destructive"
|
||||||
|
aria-label={`Delete ${c.name}`}
|
||||||
|
onClick={() => handleDelete(c.id)}
|
||||||
|
>
|
||||||
|
<Trash2Icon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -23,6 +23,10 @@ const artworkItems = [
|
|||||||
title: "Tags",
|
title: "Tags",
|
||||||
href: "/tags",
|
href: "/tags",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Animals",
|
||||||
|
href: "/animals",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// const portfolioItems = [
|
// const portfolioItems = [
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { updateTag } from "@/actions/tags/updateTag";
|
import { updateTag } from "@/actions/tags/updateTag";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { ArtCategory, ArtTag, ArtTagAlias } from "@/generated/prisma/client";
|
import { ArtCategory, ArtTag, ArtTagAlias } from "@/generated/prisma/client";
|
||||||
@ -13,6 +13,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import MultipleSelector from "../ui/multiselect";
|
import MultipleSelector from "../ui/multiselect";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
|
||||||
|
import { Switch } from "../ui/switch";
|
||||||
import AliasEditor from "./AliasEditor";
|
import AliasEditor from "./AliasEditor";
|
||||||
|
|
||||||
export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag & { categories: ArtCategory[], aliases: ArtTagAlias[] }, categories: ArtCategory[], allTags: ArtTag[] }) {
|
export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag & { categories: ArtCategory[], aliases: ArtTagAlias[] }, categories: ArtCategory[], allTags: ArtTag[] }) {
|
||||||
@ -24,6 +25,7 @@ export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag
|
|||||||
description: tag.description || "",
|
description: tag.description || "",
|
||||||
categoryIds: tag.categories?.map(cat => cat.id) ?? [],
|
categoryIds: tag.categories?.map(cat => cat.id) ?? [],
|
||||||
parentId: (tag as any).parentId ?? null,
|
parentId: (tag as any).parentId ?? null,
|
||||||
|
showOnAnimalPage: tag.showOnAnimalPage ?? false,
|
||||||
aliases: tag.aliases?.map(a => a.alias) ?? []
|
aliases: tag.aliases?.map(a => a.alias) ?? []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -147,6 +149,23 @@ export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<div className="flex">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="showOnAnimalPage"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Show on animal page</FormLabel>
|
||||||
|
<FormDescription></FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Button type="submit">Submit</Button>
|
<Button type="submit">Submit</Button>
|
||||||
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { createTag } from "@/actions/tags/createTag";
|
import { createTag } from "@/actions/tags/createTag";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { ArtCategory, ArtTag } from "@/generated/prisma/client";
|
import { ArtCategory, ArtTag } from "@/generated/prisma/client";
|
||||||
@ -13,6 +13,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import MultipleSelector from "../ui/multiselect";
|
import MultipleSelector from "../ui/multiselect";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
|
||||||
|
import { Switch } from "../ui/switch";
|
||||||
import AliasEditor from "./AliasEditor";
|
import AliasEditor from "./AliasEditor";
|
||||||
|
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
description: "",
|
description: "",
|
||||||
categoryIds: [],
|
categoryIds: [],
|
||||||
parentId: null,
|
parentId: null,
|
||||||
|
showOnAnimalPage: false,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -148,6 +150,23 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<div className="flex">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="showOnAnimalPage"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Show on animal page</FormLabel>
|
||||||
|
<FormDescription></FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Button type="submit">Submit</Button>
|
<Button type="submit">Submit</Button>
|
||||||
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
||||||
|
|||||||
@ -18,6 +18,7 @@ type TagRow = {
|
|||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
parent: { id: string; name: string } | null;
|
parent: { id: string; name: string } | null;
|
||||||
|
showOnAnimalPage: boolean;
|
||||||
aliases: { alias: string }[];
|
aliases: { alias: string }[];
|
||||||
categories: { id: string; name: string }[];
|
categories: { id: string; name: string }[];
|
||||||
_count: { artworks: number };
|
_count: { artworks: number };
|
||||||
@ -73,6 +74,7 @@ export default function TagTable({ tags }: { tags: TagRow[] }) {
|
|||||||
<TableHead className="w-[18%]">Aliases</TableHead>
|
<TableHead className="w-[18%]">Aliases</TableHead>
|
||||||
<TableHead className="w-[22%]">Categories</TableHead>
|
<TableHead className="w-[22%]">Categories</TableHead>
|
||||||
<TableHead className="w-[14%]">Parent</TableHead>
|
<TableHead className="w-[14%]">Parent</TableHead>
|
||||||
|
<TableHead className="w-[8%]">ShowAnimal</TableHead>
|
||||||
<TableHead className="w-[8%] text-right">Artworks</TableHead>
|
<TableHead className="w-[8%] text-right">Artworks</TableHead>
|
||||||
<TableHead className="w-[10%] text-right" />
|
<TableHead className="w-[10%] text-right" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -105,6 +107,10 @@ export default function TagTable({ tags }: { tags: TagRow[] }) {
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="text-right tabular-nums">
|
||||||
|
{t.showOnAnimalPage ? "yes" : "no"}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="text-right tabular-nums">
|
<TableCell className="text-right tabular-nums">
|
||||||
{t._count.artworks}
|
{t._count.artworks}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export const tagSchema = z.object({
|
|||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
categoryIds: z.array(z.string()).optional(),
|
categoryIds: z.array(z.string()).optional(),
|
||||||
parentId: z.string().nullable().optional(),
|
parentId: z.string().nullable().optional(),
|
||||||
|
showOnAnimalPage: z.boolean(),
|
||||||
|
|
||||||
aliases: z
|
aliases: z
|
||||||
.array(z.string().trim().min(1))
|
.array(z.string().trim().min(1))
|
||||||
|
|||||||
Reference in New Issue
Block a user