diff --git a/src/actions/categories/createCategory.ts b/src/actions/categories/createCategory.ts new file mode 100644 index 0000000..fe01670 --- /dev/null +++ b/src/actions/categories/createCategory.ts @@ -0,0 +1,25 @@ +"use server" + +import { prisma } from "@/lib/prisma" +import { categorySchema } from "@/schemas/artworks/categorySchema" + +export async function createCategory(formData: categorySchema) { + const parsed = categorySchema.safeParse(formData) + + if (!parsed.success) { + console.error("Validation failed", parsed.error) + throw new Error("Invalid input") + } + + const data = parsed.data + + const created = await prisma.artCategory.create({ + data: { + name: data.name, + slug: data.slug, + description: data.description + }, + }) + + return created +} \ No newline at end of file diff --git a/src/actions/categories/updateCategory.ts b/src/actions/categories/updateCategory.ts new file mode 100644 index 0000000..465693b --- /dev/null +++ b/src/actions/categories/updateCategory.ts @@ -0,0 +1,27 @@ +"use server" + +import { prisma } from '@/lib/prisma'; +import { categorySchema } from '@/schemas/artworks/categorySchema'; +import { z } from 'zod/v4'; + +export async function updateCategory(id: string, rawData: z.infer) { + const parsed = categorySchema.safeParse(rawData) + + if (!parsed.success) { + console.error("Validation failed", parsed.error) + throw new Error("Invalid input") + } + + const data = parsed.data + + const updated = await prisma.artCategory.update({ + where: { id }, + data: { + name: data.name, + slug: data.slug, + description: data.description + }, + }) + + return updated +} \ No newline at end of file diff --git a/src/actions/deleteItem.ts b/src/actions/deleteItem.ts new file mode 100644 index 0000000..a55f239 --- /dev/null +++ b/src/actions/deleteItem.ts @@ -0,0 +1,23 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; + +export async function deleteItems(itemId: string, type: string) { + + switch (type) { + case "categories": + await prisma.artCategory.delete({ where: { id: itemId } }); + break; + case "tags": + await prisma.artTag.delete({ where: { id: itemId } }); + break; + // case "types": + // await prisma.portfolioType.delete({ where: { id: itemId } }); + // break; + // case "albums": + // await prisma.portfolioAlbum.delete({ where: { id: itemId } }); + // break; + } + + return { success: true }; +} \ No newline at end of file diff --git a/src/actions/tags/createTag.ts b/src/actions/tags/createTag.ts new file mode 100644 index 0000000..cae71a8 --- /dev/null +++ b/src/actions/tags/createTag.ts @@ -0,0 +1,36 @@ +"use server" + +import { prisma } from "@/lib/prisma" +import { tagSchema } from "@/schemas/artworks/tagSchema" + +export async function createTag(formData: tagSchema) { + const parsed = tagSchema.safeParse(formData) + + if (!parsed.success) { + console.error("Validation failed", parsed.error) + throw new Error("Invalid input") + } + + const data = parsed.data + + const created = await prisma.artTag.create({ + data: { + name: data.name, + slug: data.slug, + description: data.description + }, + }) + + if (data.categoryIds) { + await prisma.artTag.update({ + where: { id: created.id }, + data: { + categories: { + set: data.categoryIds.map(id => ({ id })) + } + } + }); + } + + return created +} \ No newline at end of file diff --git a/src/actions/tags/updateTag.ts b/src/actions/tags/updateTag.ts new file mode 100644 index 0000000..80e5409 --- /dev/null +++ b/src/actions/tags/updateTag.ts @@ -0,0 +1,38 @@ +"use server" + +import { prisma } from '@/lib/prisma'; +import { tagSchema } from '@/schemas/artworks/tagSchema'; +import { z } from 'zod/v4'; + +export async function updateTag(id: string, rawData: z.infer) { + const parsed = tagSchema.safeParse(rawData) + + if (!parsed.success) { + console.error("Validation failed", parsed.error) + throw new Error("Invalid input") + } + + const data = parsed.data + + const updated = await prisma.artTag.update({ + where: { id }, + data: { + name: data.name, + slug: data.slug, + description: data.description + }, + }) + + if (data.categoryIds) { + await prisma.artTag.update({ + where: { id: id }, + data: { + categories: { + set: data.categoryIds.map(id => ({ id })) + } + } + }); + } + + return updated +} \ No newline at end of file diff --git a/src/app/artworks/[id]/page.tsx b/src/app/artworks/[id]/page.tsx index 66ec041..d87d725 100644 --- a/src/app/artworks/[id]/page.tsx +++ b/src/app/artworks/[id]/page.tsx @@ -24,7 +24,7 @@ export default async function ArtworkSinglePage({ params }: { params: { id: stri } }) - const categories = await prisma.artCategory.findMany({ orderBy: { sortIndex: "asc" } }); + const categories = await prisma.artCategory.findMany({ include: { tags: true }, orderBy: { sortIndex: "asc" } }); const tags = await prisma.artTag.findMany({ orderBy: { sortIndex: "asc" } }); if (!item) return
Artwork with this id not found
diff --git a/src/app/categories/[id]/page.tsx b/src/app/categories/[id]/page.tsx new file mode 100644 index 0000000..3146115 --- /dev/null +++ b/src/app/categories/[id]/page.tsx @@ -0,0 +1,18 @@ +import EditCategoryForm from "@/components/categories/EditCategoryForm"; +import { prisma } from "@/lib/prisma"; + +export default async function PortfolioCategoriesEditPage({ params }: { params: { id: string } }) { + const { id } = await params; + const category = await prisma.artCategory.findUnique({ + where: { + id, + } + }) + + return ( +
+

Edit Category

+ {category && } +
+ ); +} \ No newline at end of file diff --git a/src/app/categories/new/page.tsx b/src/app/categories/new/page.tsx new file mode 100644 index 0000000..170e0c7 --- /dev/null +++ b/src/app/categories/new/page.tsx @@ -0,0 +1,10 @@ +import NewCategoryForm from "@/components/categories/NewCategoryForm"; + +export default function PortfolioCategoriesNewPage() { + return ( +
+

New Category

+ +
+ ); +} \ No newline at end of file diff --git a/src/app/categories/page.tsx b/src/app/categories/page.tsx new file mode 100644 index 0000000..6aea465 --- /dev/null +++ b/src/app/categories/page.tsx @@ -0,0 +1,20 @@ +import ItemList from "@/components/lists/ItemList"; +import { prisma } from "@/lib/prisma"; +import { PlusCircleIcon } from "lucide-react"; +import Link from "next/link"; + +export default async function CategoriesPage() { + const items = await prisma.artCategory.findMany({}) + + return ( +
+
+

Art Categories

+ + Add new category + +
+ {items && items.length > 0 ? :

There are no categories yet. Consider adding some!

} +
+ ); +} \ No newline at end of file diff --git a/src/app/tags/[id]/page.tsx b/src/app/tags/[id]/page.tsx new file mode 100644 index 0000000..34dbd02 --- /dev/null +++ b/src/app/tags/[id]/page.tsx @@ -0,0 +1,23 @@ +import EditTagForm from "@/components/tags/EditTagForm"; +import { prisma } from "@/lib/prisma"; + +export default async function PortfolioTagsEditPage({ params }: { params: { id: string } }) { + const { id } = await params; + const tag = await prisma.artTag.findUnique({ + where: { + id, + }, + include: { + categories: true + } + }) + + const categories = await prisma.artCategory.findMany({ include: { tags: true }, orderBy: { sortIndex: "asc" } }); + + return ( +
+

Edit Tag

+ {tag && } +
+ ); +} \ No newline at end of file diff --git a/src/app/tags/new/page.tsx b/src/app/tags/new/page.tsx new file mode 100644 index 0000000..2ed755a --- /dev/null +++ b/src/app/tags/new/page.tsx @@ -0,0 +1,13 @@ +import NewTagForm from "@/components/tags/NewTagForm"; +import { prisma } from "@/lib/prisma"; + +export default async function PortfolioTagsNewPage() { + const categories = await prisma.artCategory.findMany({ include: { tags: true }, orderBy: { sortIndex: "asc" } }); + + return ( +
+

New Tag

+ +
+ ); +} \ No newline at end of file diff --git a/src/app/tags/page.tsx b/src/app/tags/page.tsx new file mode 100644 index 0000000..82755e7 --- /dev/null +++ b/src/app/tags/page.tsx @@ -0,0 +1,20 @@ +import ItemList from "@/components/lists/ItemList"; +import { prisma } from "@/lib/prisma"; +import { PlusCircleIcon } from "lucide-react"; +import Link from "next/link"; + +export default async function PortfolioTagsPage() { + const items = await prisma.artTag.findMany({}) + + return ( +
+
+

Art Tags

+ + Add new tag + +
+ {items && items.length > 0 ? :

There are no tags yet. Consider adding some!

} +
+ ); +} \ No newline at end of file diff --git a/src/components/artworks/single/EditArtworkForm.tsx b/src/components/artworks/single/EditArtworkForm.tsx index bd160d6..ac046a2 100644 --- a/src/components/artworks/single/EditArtworkForm.tsx +++ b/src/components/artworks/single/EditArtworkForm.tsx @@ -10,12 +10,12 @@ import MultipleSelector from "@/components/ui/multiselect"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; -import { ArtCategory, ArtTag } from "@/generated/prisma/client"; +import { ArtTag } from "@/generated/prisma/client"; // import { Color, ImageColor, ImageMetadata, ImageVariant, PortfolioAlbum, PortfolioCategory, PortfolioImage, PortfolioSortContext, PortfolioTag, PortfolioType } from "@/generated/prisma"; import { cn } from "@/lib/utils"; import { artworkSchema } from "@/schemas/artworks/imageSchema"; // import { imageSchema } from "@/schemas/portfolio/imageSchema"; -import { ArtworkWithRelations } from "@/types/Artwork"; +import { ArtworkWithRelations, CategoryWithTags } from "@/types/Artwork"; import { zodResolver } from "@hookform/resolvers/zod"; import { format } from "date-fns"; import { useRouter } from "next/navigation"; @@ -26,7 +26,7 @@ import { z } from "zod/v4"; export default function EditArtworkForm({ artwork, categories, tags }: { artwork: ArtworkWithRelations, - categories: ArtCategory[] + categories: CategoryWithTags[] tags: ArtTag[] }) { const router = useRouter(); @@ -246,37 +246,6 @@ export default function EditArtworkForm({ artwork, categories, tags }: )} /> */} - { - const selectedOptions = tags - .filter(tag => field.value?.includes(tag.id)) - .map(tag => ({ label: tag.name, value: tag.id })); - return ( - - Tags - - ({ - label: tag.name, - value: tag.id, - }))} - placeholder="Select tags" - hidePlaceholderWhenSelected - selectFirstItem - value={selectedOptions} - onChange={(options) => { - const ids = options.map(option => option.value); - field.onChange(ids); - }} - /> - - - - ) - }} - /> + + { + const selectedTagIds = field.value ?? []; + const selectedCategoryIds = form.watch("categoryIds") ?? []; + + // Tag IDs connected to selected categories + const preferredTagIds = new Set(); + for (const cat of categories) { + if (!selectedCategoryIds.includes(cat.id)) continue; + for (const t of cat.tags) preferredTagIds.add(t.id); + } + + // Build grouped options: Selected -> Category -> Other + const tagOptions = tags + .map((t) => { + let group = "Other tags"; + if (selectedTagIds.includes(t.id)) group = "Selected"; + else if (preferredTagIds.has(t.id)) group = "From selected categories"; + + return { + label: t.name, + value: t.id, + group, // IMPORTANT: groupBy will use this + }; + }) + // Optional: stable ordering within each group + .sort((a, b) => a.label.localeCompare(b.label)); + + // Selected value objects + const selectedOptions = tags + .filter((t) => selectedTagIds.includes(t.id)) + .map((t) => ({ label: t.name, value: t.id })); + + return ( + + Tags + + field.onChange(options.map((o) => o.value))} + /> + + + + ); + }} + /> + {/* Boolean */} >({ + resolver: zodResolver(categorySchema), + defaultValues: { + name: category.name, + slug: category.slug, + description: category.description || "", + } + }) + + async function onSubmit(values: z.infer) { + try { + const updated = await updateCategory(category.id, values) + console.log("Art category updated:", updated) + toast("Art category updated.") + router.push("/portfolio/categories") + } catch (err) { + console.error(err) + toast("Failed to update art category.") + } + } + + return ( +
+
+ + {/* String */} + ( + + Name + + + + + + )} + /> + ( + + Slug + + + + + + )} + /> + ( + + Description + +