From c915df904dce492c715bc7db7d1d2eee6b1bc521 Mon Sep 17 00:00:00 2001 From: Citali Date: Mon, 2 Feb 2026 17:00:03 +0100 Subject: [PATCH] Add tags to commssion types and custom types. Add button for example images to cards --- .../20260202151753_tags_03/migration.sql | 16 +++ prisma/schema.prisma | 56 ++++----- .../commissions/customCards/newCard.ts | 3 + .../commissions/customCards/updateCard.ts | 6 + src/actions/commissions/types/newType.ts | 78 ++++++------ src/actions/commissions/types/updateType.ts | 37 +++--- src/actions/tags/migrateArtTags.ts | 5 - src/actions/tags/migrateArtworkTagJoin.ts | 75 ------------ .../commissions/custom-cards/[id]/page.tsx | 5 +- .../commissions/custom-cards/new/page.tsx | 5 +- .../(admin)/commissions/types/[id]/page.tsx | 13 +- .../(admin)/commissions/types/new/page.tsx | 12 +- src/app/(admin)/commissions/types/page.tsx | 2 +- src/app/(admin)/tags/page.tsx | 45 ++----- .../customCards/EditCustomCardForm.tsx | 38 +++++- .../customCards/NewCustomCardForm.tsx | 38 +++++- .../commissions/types/EditTypeForm.tsx | 113 ++++++++++++++---- .../commissions/types/ListTypes.tsx | 12 +- .../commissions/types/NewTypeForm.tsx | 102 +++++++++++++--- src/components/tags/EditTagForm.tsx | 91 ++++++++++---- src/components/tags/NewTagForm.tsx | 99 ++++++++++----- src/components/ui/multiselect.tsx | 85 +++++++------ src/components/ui/select.tsx | 46 +++---- src/schemas/commissionCustomCard.ts | 1 + src/schemas/commissionType.ts | 1 + 25 files changed, 617 insertions(+), 367 deletions(-) create mode 100644 prisma/migrations/20260202151753_tags_03/migration.sql delete mode 100644 src/actions/tags/migrateArtTags.ts delete mode 100644 src/actions/tags/migrateArtworkTagJoin.ts diff --git a/prisma/migrations/20260202151753_tags_03/migration.sql b/prisma/migrations/20260202151753_tags_03/migration.sql new file mode 100644 index 0000000..c18c04e --- /dev/null +++ b/prisma/migrations/20260202151753_tags_03/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "_CommissionCustomCardTags" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_CommissionCustomCardTags_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE INDEX "_CommissionCustomCardTags_B_index" ON "_CommissionCustomCardTags"("B"); + +-- AddForeignKey +ALTER TABLE "_CommissionCustomCardTags" ADD CONSTRAINT "_CommissionCustomCardTags_A_fkey" FOREIGN KEY ("A") REFERENCES "CommissionCustomCard"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_CommissionCustomCardTags" ADD CONSTRAINT "_CommissionCustomCardTags_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index eb52aa8..b2036e5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -47,13 +47,13 @@ model Artwork { galleryId String? gallery Gallery? @relation(fields: [galleryId], references: [id]) - metadata ArtworkMetadata? + metadata ArtworkMetadata? timelapse ArtworkTimelapse? albums Album[] categories ArtCategory[] colors ArtworkColor[] - tags Tag[] @relation("ArtworkTags") + tags Tag[] @relation("ArtworkTags") variants FileVariant[] @@index([colorStatus]) @@ -165,12 +165,12 @@ model ArtworkTimelapse { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - artworkId String @unique + artworkId String @unique artwork Artwork @relation(fields: [artworkId], references: [id], onDelete: Cascade) enabled Boolean @default(false) - s3Key String @unique + s3Key String @unique fileName String? mimeType String? sizeBytes Int? @@ -224,12 +224,13 @@ model Tag { description String? - aliases TagAlias[] - categoryLinks TagCategory[] - categoryParents TagCategory[] @relation("TagCategoryParent") - artworks Artwork[] @relation("ArtworkTags") - commissionTypes CommissionType[] @relation("CommissionTypeTags") - miniatures Miniature[] @relation("MiniatureTags") + aliases TagAlias[] + categoryLinks TagCategory[] + categoryParents TagCategory[] @relation("TagCategoryParent") + artworks Artwork[] @relation("ArtworkTags") + commissionTypes CommissionType[] @relation("CommissionTypeTags") + commissionCustomCards CommissionCustomCard[] @relation("CommissionCustomCardTags") + miniatures Miniature[] @relation("MiniatureTags") } model TagAlias { @@ -240,7 +241,7 @@ model TagAlias { alias String @unique tagId String - tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) @@unique([tagId, alias]) @@index([alias]) @@ -310,13 +311,14 @@ model CommissionCustomCard { name String - description String? + description String? referenceImageUrl String? - isVisible Boolean @default(true) - isSpecialOffer Boolean @default(false) + isVisible Boolean @default(true) + isSpecialOffer Boolean @default(false) - options CommissionCustomCardOption[] - extras CommissionCustomCardExtra[] + tags Tag[] @relation("CommissionCustomCardTags") + options CommissionCustomCardOption[] + extras CommissionCustomCardExtra[] requests CommissionRequest[] @@index([isVisible, sortIndex]) @@ -332,9 +334,9 @@ model CommissionOption { description String? - types CommissionTypeOption[] + types CommissionTypeOption[] customCards CommissionCustomCardOption[] - requests CommissionRequest[] + requests CommissionRequest[] } model CommissionTypeOption { @@ -366,8 +368,8 @@ model CommissionExtra { description String? - requests CommissionRequest[] - types CommissionTypeExtra[] + requests CommissionRequest[] + types CommissionTypeExtra[] customCards CommissionCustomCardExtra[] } @@ -475,12 +477,12 @@ model CommissionRequest { userAgent String? customFields Json? - optionId String? - typeId String? + optionId String? + typeId String? customCardId String? - option CommissionOption? @relation(fields: [optionId], references: [id]) - type CommissionType? @relation(fields: [typeId], references: [id]) - customCard CommissionCustomCard? @relation(fields: [customCardId], references: [id]) + option CommissionOption? @relation(fields: [optionId], references: [id]) + type CommissionType? @relation(fields: [typeId], references: [id]) + customCard CommissionCustomCard? @relation(fields: [customCardId], references: [id]) extras CommissionExtra[] files CommissionRequestFile[] @@ -491,9 +493,9 @@ model CommissionGuidelines { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - markdown String + markdown String exampleImageUrl String? - isActive Boolean @default(true) + isActive Boolean @default(true) @@index([isActive]) } diff --git a/src/actions/commissions/customCards/newCard.ts b/src/actions/commissions/customCards/newCard.ts index a9ecf81..0798fc3 100644 --- a/src/actions/commissions/customCards/newCard.ts +++ b/src/actions/commissions/customCards/newCard.ts @@ -25,6 +25,9 @@ export async function createCommissionCustomCard( referenceImageUrl: data.referenceImageUrl ?? null, isVisible: data.isVisible ?? true, isSpecialOffer: data.isSpecialOffer ?? false, + tags: data.tagIds?.length + ? { connect: data.tagIds.map((id) => ({ id })) } + : undefined, options: { create: data.options?.map((opt, index) => ({ diff --git a/src/actions/commissions/customCards/updateCard.ts b/src/actions/commissions/customCards/updateCard.ts index fc3334f..9b2a817 100644 --- a/src/actions/commissions/customCards/updateCard.ts +++ b/src/actions/commissions/customCards/updateCard.ts @@ -20,6 +20,12 @@ export async function updateCommissionCustomCard( referenceImageUrl: data.referenceImageUrl ?? null, isVisible: data.isVisible ?? true, isSpecialOffer: data.isSpecialOffer ?? false, + tags: data.tagIds + ? { + set: [], + connect: data.tagIds.map((id) => ({ id })), + } + : undefined, options: { deleteMany: {}, create: data.options?.map((opt, index) => ({ diff --git a/src/actions/commissions/types/newType.ts b/src/actions/commissions/types/newType.ts index bc98a5a..b4d060b 100644 --- a/src/actions/commissions/types/newType.ts +++ b/src/actions/commissions/types/newType.ts @@ -1,7 +1,7 @@ -"use server" +"use server"; -import { prisma } from "@/lib/prisma" -import { commissionTypeSchema } from "@/schemas/commissionType" +import { prisma } from "@/lib/prisma"; +import { commissionTypeSchema } from "@/schemas/commissionType"; export async function createCommissionOption(data: { name: string }) { return await prisma.commissionOption.create({ @@ -9,7 +9,7 @@ export async function createCommissionOption(data: { name: string }) { name: data.name, description: "", }, - }) + }); } export async function createCommissionExtra(data: { name: string }) { @@ -18,64 +18,70 @@ export async function createCommissionExtra(data: { name: string }) { name: data.name, description: "", }, - }) + }); } export async function createCommissionCustomInput(data: { - name: string - fieldId: string + name: string; + fieldId: string; }) { return await prisma.commissionCustomInput.create({ data: { name: data.name, fieldId: data.fieldId, }, - }) + }); } export async function createCommissionType(formData: commissionTypeSchema) { - const parsed = commissionTypeSchema.safeParse(formData) + const parsed = commissionTypeSchema.safeParse(formData); if (!parsed.success) { - console.error("Validation failed", parsed.error) - throw new Error("Invalid input") + console.error("Validation failed", parsed.error); + throw new Error("Invalid input"); } - const data = parsed.data + const data = parsed.data; const created = await prisma.commissionType.create({ data: { name: data.name, description: data.description, + tags: data.tagIds?.length + ? { connect: data.tagIds.map((id) => ({ id })) } + : undefined, options: { - create: data.options?.map((opt, index) => ({ - option: { connect: { id: opt.optionId } }, - price: opt.price, - pricePercent: opt.pricePercent, - priceRange: opt.priceRange, - sortIndex: index, - })) || [], + create: + data.options?.map((opt, index) => ({ + option: { connect: { id: opt.optionId } }, + price: opt.price, + pricePercent: opt.pricePercent, + priceRange: opt.priceRange, + sortIndex: index, + })) || [], }, extras: { - create: data.extras?.map((ext, index) => ({ - extra: { connect: { id: ext.extraId } }, - price: ext.price, - pricePercent: ext.pricePercent, - priceRange: ext.priceRange, - sortIndex: index, - })) || [], + create: + data.extras?.map((ext, index) => ({ + extra: { connect: { id: ext.extraId } }, + price: ext.price, + pricePercent: ext.pricePercent, + priceRange: ext.priceRange, + sortIndex: index, + })) || [], }, customInputs: { - create: data.customInputs?.map((c, index) => ({ - customInput: { connect: { id: c.customInputId } }, - label: c.label, - inputType: c.inputType, - required: c.required, - sortIndex: index, - })) || [], + create: + data.customInputs?.map((c, index) => ({ + customInput: { connect: { id: c.customInputId } }, + label: c.label, + inputType: c.inputType, + required: c.required, + sortIndex: index, + })) || [], }, }, - }) + }); - return created -} \ No newline at end of file + return created; +} diff --git a/src/actions/commissions/types/updateType.ts b/src/actions/commissions/types/updateType.ts index 99b8942..981ca38 100644 --- a/src/actions/commissions/types/updateType.ts +++ b/src/actions/commissions/types/updateType.ts @@ -1,20 +1,26 @@ -"use server" +"use server"; -import { prisma } from "@/lib/prisma" -import { commissionTypeSchema } from "@/schemas/commissionType" -import * as z from "zod/v4" +import { prisma } from "@/lib/prisma"; +import { commissionTypeSchema } from "@/schemas/commissionType"; +import type * as z from "zod/v4"; export async function updateCommissionType( id: string, - rawData: z.infer + rawData: z.infer, ) { - const data = commissionTypeSchema.parse(rawData) + const data = commissionTypeSchema.parse(rawData); const updated = await prisma.commissionType.update({ where: { id }, data: { name: data.name, description: data.description, + tags: data.tagIds + ? { + set: [], + connect: data.tagIds.map((id) => ({ id })), + } + : undefined, options: { deleteMany: {}, create: data.options?.map((opt, index) => ({ @@ -37,13 +43,14 @@ export async function updateCommissionType( }, customInputs: { deleteMany: {}, - create: data.customInputs?.map((c, index) => ({ - customInput: { connect: { id: c.customInputId } }, - label: c.label, - inputType: c.inputType, - required: c.required, - sortIndex: index, - })) || [], + create: + data.customInputs?.map((c, index) => ({ + customInput: { connect: { id: c.customInputId } }, + label: c.label, + inputType: c.inputType, + required: c.required, + sortIndex: index, + })) || [], }, }, include: { @@ -51,7 +58,7 @@ export async function updateCommissionType( extras: true, customInputs: true, }, - }) + }); - return updated + return updated; } diff --git a/src/actions/tags/migrateArtTags.ts b/src/actions/tags/migrateArtTags.ts deleted file mode 100644 index 22b3a8c..0000000 --- a/src/actions/tags/migrateArtTags.ts +++ /dev/null @@ -1,5 +0,0 @@ -"use server"; - -export async function migrateArtTags() { - throw new Error("Migration disabled: ArtTag models removed."); -} diff --git a/src/actions/tags/migrateArtworkTagJoin.ts b/src/actions/tags/migrateArtworkTagJoin.ts deleted file mode 100644 index 0016244..0000000 --- a/src/actions/tags/migrateArtworkTagJoin.ts +++ /dev/null @@ -1,75 +0,0 @@ -"use server"; - -import { prisma } from "@/lib/prisma"; -import { revalidatePath } from "next/cache"; - -type JoinMigrationResult = { - ok: boolean; - copied: number; - oldExists: boolean; - newExists: boolean; - droppedOld: boolean; - message?: string; -}; - -export async function migrateArtworkTagJoin( - opts: { dropOld?: boolean } = {}, -): Promise { - const dropOld = Boolean(opts.dropOld); - - const [oldRow, newRow] = await Promise.all([ - prisma.$queryRaw<{ name: string | null }[]>` - select to_regclass('_ArtworkTagsV2')::text as name; - `, - prisma.$queryRaw<{ name: string | null }[]>` - select to_regclass('_ArtworkTags')::text as name; - `, - ]); - - const oldExists = Boolean(oldRow?.[0]?.name); - const newExists = Boolean(newRow?.[0]?.name); - - if (!newExists) { - return { - ok: false, - copied: 0, - oldExists, - newExists, - droppedOld: false, - message: "New join table _ArtworkTags does not exist. Run the migration first.", - }; - } - - if (!oldExists) { - return { - ok: true, - copied: 0, - oldExists, - newExists, - droppedOld: false, - message: "Old join table _ArtworkTagsV2 not found. Nothing to copy.", - }; - } - - const copied = await prisma.$executeRawUnsafe(` - INSERT INTO "_ArtworkTags" ("A","B") - SELECT "A","B" FROM "_ArtworkTagsV2" - ON CONFLICT ("A","B") DO NOTHING; - `); - - let droppedOld = false; - if (dropOld) { - await prisma.$executeRawUnsafe(`DROP TABLE "_ArtworkTagsV2";`); - droppedOld = true; - } - - revalidatePath("/tags"); - - return { - ok: true, - copied: Number(copied ?? 0), - oldExists, - newExists, - droppedOld, - }; -} diff --git a/src/app/(admin)/commissions/custom-cards/[id]/page.tsx b/src/app/(admin)/commissions/custom-cards/[id]/page.tsx index e43edb3..f42d37b 100644 --- a/src/app/(admin)/commissions/custom-cards/[id]/page.tsx +++ b/src/app/(admin)/commissions/custom-cards/[id]/page.tsx @@ -10,17 +10,19 @@ export default async function CommissionCustomCardEditPage({ }) { const { id } = await params; - const [card, options, extras, images] = await Promise.all([ + const [card, options, extras, images, tags] = await Promise.all([ prisma.commissionCustomCard.findUnique({ where: { id }, include: { options: { orderBy: { sortIndex: "asc" } }, extras: { orderBy: { sortIndex: "asc" } }, + tags: true, }, }), prisma.commissionOption.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }), prisma.commissionExtra.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }), listCommissionCustomCardImages(), + prisma.tag.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }), ]); if (!card) { @@ -37,6 +39,7 @@ export default async function CommissionCustomCardEditPage({ allOptions={options} allExtras={extras} images={images} + allTags={tags} /> ); diff --git a/src/app/(admin)/commissions/custom-cards/new/page.tsx b/src/app/(admin)/commissions/custom-cards/new/page.tsx index 6d67630..39ec588 100644 --- a/src/app/(admin)/commissions/custom-cards/new/page.tsx +++ b/src/app/(admin)/commissions/custom-cards/new/page.tsx @@ -3,10 +3,11 @@ import NewCustomCardForm from "@/components/commissions/customCards/NewCustomCar import { prisma } from "@/lib/prisma"; export default async function CommissionCustomCardsNewPage() { - const [options, extras, images] = await Promise.all([ + const [options, extras, images, tags] = await Promise.all([ prisma.commissionOption.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }), prisma.commissionExtra.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }), listCommissionCustomCardImages(), + prisma.tag.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }), ]); return ( @@ -14,7 +15,7 @@ export default async function CommissionCustomCardsNewPage() {

New Custom Commission Card

- + ); } diff --git a/src/app/(admin)/commissions/types/[id]/page.tsx b/src/app/(admin)/commissions/types/[id]/page.tsx index ab4d473..b950c15 100644 --- a/src/app/(admin)/commissions/types/[id]/page.tsx +++ b/src/app/(admin)/commissions/types/[id]/page.tsx @@ -11,8 +11,12 @@ export default async function CommissionTypesEditPage({ params }: { params: { id options: { include: { option: true }, orderBy: { sortIndex: "asc" } }, extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } }, customInputs: { include: { customInput: true }, orderBy: { sortIndex: "asc" } }, + tags: true, }, }) + const tags = await prisma.tag.findMany({ + orderBy: [{ sortIndex: "asc" }, { name: "asc" }], + }); const options = await prisma.commissionOption.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }], }); @@ -32,7 +36,12 @@ export default async function CommissionTypesEditPage({ params }: { params: { id

Edit Commission Type

- + ); -} \ No newline at end of file +} diff --git a/src/app/(admin)/commissions/types/new/page.tsx b/src/app/(admin)/commissions/types/new/page.tsx index aa82862..7d8680e 100644 --- a/src/app/(admin)/commissions/types/new/page.tsx +++ b/src/app/(admin)/commissions/types/new/page.tsx @@ -2,6 +2,9 @@ import NewTypeForm from "@/components/commissions/types/NewTypeForm"; import { prisma } from "@/lib/prisma"; export default async function CommissionTypesNewPage() { + const tags = await prisma.tag.findMany({ + orderBy: [{ sortIndex: "asc" }, { name: "asc" }], + }); const options = await prisma.commissionOption.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }], }); @@ -17,8 +20,13 @@ export default async function CommissionTypesNewPage() {

New Commission Type

- + ); -} \ No newline at end of file +} diff --git a/src/app/(admin)/commissions/types/page.tsx b/src/app/(admin)/commissions/types/page.tsx index 250d7f9..a10c968 100644 --- a/src/app/(admin)/commissions/types/page.tsx +++ b/src/app/(admin)/commissions/types/page.tsx @@ -24,4 +24,4 @@ export default async function CommissionTypesPage() { {types && types.length > 0 ? :

No types found.

} ); -} \ No newline at end of file +} diff --git a/src/app/(admin)/tags/page.tsx b/src/app/(admin)/tags/page.tsx index dda3035..57ef92c 100644 --- a/src/app/(admin)/tags/page.tsx +++ b/src/app/(admin)/tags/page.tsx @@ -1,20 +1,9 @@ -import { migrateArtworkTagJoin } from "@/actions/tags/migrateArtworkTagJoin"; 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"; -async function migrateArtworkTagJoinCopy() { - "use server"; - await migrateArtworkTagJoin(); -} - -async function migrateArtworkTagJoinDropOld() { - "use server"; - await migrateArtworkTagJoin({ dropOld: true }); -} - export default async function ArtTagsPage() { const items = await prisma.tag.findMany({ include: { @@ -56,22 +45,10 @@ export default async function ArtTagsPage() {

Tags

-

- Manage tags, aliases, categories, and usage across artworks. -

+

Manage tags.

-
- -
-
- -
- + - { - rows.length > 0 ? ( - - ) : ( -

- There are no tags yet. Consider adding some! -

- ) - } - + {rows.length > 0 ? ( + + ) : ( +

+ There are no tags yet. Consider adding some! +

+ )} + ); } diff --git a/src/components/commissions/customCards/EditCustomCardForm.tsx b/src/components/commissions/customCards/EditCustomCardForm.tsx index 59aae8a..790500e 100644 --- a/src/components/commissions/customCards/EditCustomCardForm.tsx +++ b/src/components/commissions/customCards/EditCustomCardForm.tsx @@ -14,7 +14,7 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; -import type { CommissionExtra, CommissionOption } from "@/generated/prisma/client"; +import type { CommissionExtra, CommissionOption, Tag } from "@/generated/prisma/client"; import { commissionCustomCardSchema, type CommissionCustomCardValues, @@ -26,6 +26,7 @@ import { toast } from "sonner"; import { CommissionExtraField } from "../types/form/CommissionExtraField"; import { CommissionOptionField } from "../types/form/CommissionOptionField"; import { CustomCardImagePicker } from "./CustomCardImagePicker"; +import MultipleSelector from "@/components/ui/multiselect"; type CustomCardOption = { optionId: string; @@ -48,6 +49,7 @@ type CustomCardWithItems = { referenceImageUrl: string | null; isVisible: boolean; isSpecialOffer: boolean; + tags: Tag[]; options: CustomCardOption[]; extras: CustomCardExtra[]; }; @@ -57,6 +59,7 @@ type Props = { allOptions: CommissionOption[]; allExtras: CommissionExtra[]; images: CommissionCustomCardImageItem[]; + allTags: Tag[]; }; export default function EditCustomCardForm({ @@ -64,6 +67,7 @@ export default function EditCustomCardForm({ allOptions, allExtras, images, + allTags, }: Props) { const router = useRouter(); const form = useForm({ @@ -74,6 +78,7 @@ export default function EditCustomCardForm({ isVisible: card.isVisible, isSpecialOffer: card.isSpecialOffer, referenceImageUrl: card.referenceImageUrl ?? null, + tagIds: card.tags.map((t) => t.id), options: card.options.map((o) => ({ optionId: o.optionId, price: o.price ?? undefined, @@ -171,6 +176,37 @@ export default function EditCustomCardForm({ render={() => } /> + { + const selectedIds = field.value ?? []; + const selectedOptions = allTags + .filter((t) => selectedIds.includes(t.id)) + .map((t) => ({ label: t.name, value: t.id })); + + return ( + + Tags + + ({ label: t.name, value: t.id }))} + placeholder="Select tags for this custom card" + hidePlaceholderWhenSelected + selectFirstItem + value={selectedOptions} + onChange={(options) => field.onChange(options.map((o) => o.value))} + /> + + + Used to link this custom card to tagged artworks. + + + + ); + }} + /> + diff --git a/src/components/commissions/customCards/NewCustomCardForm.tsx b/src/components/commissions/customCards/NewCustomCardForm.tsx index 2e81b4f..81ea233 100644 --- a/src/components/commissions/customCards/NewCustomCardForm.tsx +++ b/src/components/commissions/customCards/NewCustomCardForm.tsx @@ -14,7 +14,7 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; -import type { CommissionExtra, CommissionOption } from "@/generated/prisma/client"; +import type { CommissionExtra, CommissionOption, Tag } from "@/generated/prisma/client"; import { commissionCustomCardSchema, type CommissionCustomCardValues, @@ -26,14 +26,16 @@ import { toast } from "sonner"; import { CommissionExtraField } from "../types/form/CommissionExtraField"; import { CommissionOptionField } from "../types/form/CommissionOptionField"; import { CustomCardImagePicker } from "./CustomCardImagePicker"; +import MultipleSelector from "@/components/ui/multiselect"; type Props = { options: CommissionOption[]; extras: CommissionExtra[]; images: CommissionCustomCardImageItem[]; + tags: Tag[]; }; -export default function NewCustomCardForm({ options, extras, images }: Props) { +export default function NewCustomCardForm({ options, extras, images, tags }: Props) { const router = useRouter(); const form = useForm({ resolver: zodResolver(commissionCustomCardSchema), @@ -43,6 +45,7 @@ export default function NewCustomCardForm({ options, extras, images }: Props) { isVisible: true, isSpecialOffer: false, referenceImageUrl: null, + tagIds: [], options: [], extras: [], }, @@ -131,6 +134,37 @@ export default function NewCustomCardForm({ options, extras, images }: Props) { render={() => } /> + { + const selectedIds = field.value ?? []; + const selectedOptions = tags + .filter((t) => selectedIds.includes(t.id)) + .map((t) => ({ label: t.name, value: t.id })); + + return ( + + Tags + + ({ label: t.name, value: t.id }))} + placeholder="Select tags for this custom card" + hidePlaceholderWhenSelected + selectFirstItem + value={selectedOptions} + onChange={(options) => field.onChange(options.map((o) => o.value))} + /> + + + Used to link this custom card to tagged artworks. + + + + ); + }} + /> + diff --git a/src/components/commissions/types/EditTypeForm.tsx b/src/components/commissions/types/EditTypeForm.tsx index 4e8e03c..10be463 100644 --- a/src/components/commissions/types/EditTypeForm.tsx +++ b/src/components/commissions/types/EditTypeForm.tsx @@ -1,10 +1,28 @@ -"use client" +"use client"; import { updateCommissionType } from "@/actions/commissions/types/updateType"; import { Button } from "@/components/ui/button"; -import { Form, FormControl, FormDescription, 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 type { CommissionCustomInput, CommissionExtra, CommissionOption, CommissionType, CommissionTypeCustomInput, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma/client"; +import MultipleSelector from "@/components/ui/multiselect"; +import type { + CommissionCustomInput, + CommissionExtra, + CommissionOption, + CommissionType, + CommissionTypeCustomInput, + CommissionTypeExtra, + CommissionTypeOption, + Tag, +} from "@/generated/prisma/client"; import { commissionTypeSchema } from "@/schemas/commissionType"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; @@ -15,25 +33,35 @@ import { CommissionExtraField } from "./form/CommissionExtraField"; import { CommissionOptionField } from "./form/CommissionOptionField"; type CommissionTypeWithConnections = CommissionType & { - options: (CommissionTypeOption & { option: CommissionOption })[] - extras: (CommissionTypeExtra & { extra: CommissionExtra })[] - customInputs: (CommissionTypeCustomInput & { customInput: CommissionCustomInput })[] -} + options: (CommissionTypeOption & { option: CommissionOption })[]; + extras: (CommissionTypeExtra & { extra: CommissionExtra })[]; + customInputs: (CommissionTypeCustomInput & { + customInput: CommissionCustomInput; + })[]; + tags: Tag[]; +}; type Props = { - type: CommissionTypeWithConnections - allOptions: CommissionOption[], - allExtras: CommissionExtra[], + type: CommissionTypeWithConnections; + allOptions: CommissionOption[]; + allExtras: CommissionExtra[]; + allTags: Tag[]; // allCustomInputs: CommissionCustomInput[] -} +}; -export default function EditTypeForm({ type, allOptions, allExtras }: Props) { +export default function EditTypeForm({ + type, + allOptions, + allExtras, + allTags, +}: Props) { const router = useRouter(); const form = useForm>({ resolver: zodResolver(commissionTypeSchema), defaultValues: { name: type.name, description: type.description ?? "", + tagIds: type.tags.map((t) => t.id), options: type.options.map((o) => ({ optionId: o.optionId, price: o.price ?? undefined, @@ -54,16 +82,16 @@ export default function EditTypeForm({ type, allOptions, allExtras }: Props) { required: f.required, })), }, - }) + }); async function onSubmit(values: z.infer) { try { - await updateCommissionType(type.id, values) - toast.success("Commission type updated.") - router.push("/commissions/types") + await updateCommissionType(type.id, values); + toast.success("Commission type updated."); + router.push("/commissions/types"); } catch (err) { - console.error(err) - toast("Failed to create commission type.") + console.error(err); + toast("Failed to create commission type."); } } @@ -80,7 +108,9 @@ export default function EditTypeForm({ type, allOptions, allExtras }: Props) { - The name of the commission type. + + The name of the commission type. + )} @@ -99,6 +129,41 @@ export default function EditTypeForm({ type, allOptions, allExtras }: Props) { )} /> + { + const selectedIds = field.value ?? []; + const selectedOptions = allTags + .filter((t) => selectedIds.includes(t.id)) + .map((t) => ({ label: t.name, value: t.id })); + + return ( + + Tags + + ({ + label: t.name, + value: t.id, + }))} + placeholder="Select tags for this commission type" + hidePlaceholderWhenSelected + selectFirstItem + value={selectedOptions} + onChange={(options) => + field.onChange(options.map((o) => o.value)) + } + /> + + + Used to link this commission type to tagged artworks. + + + + ); + }} + /> @@ -106,10 +171,16 @@ export default function EditTypeForm({ type, allOptions, allExtras }: Props) {
- +
); -} \ No newline at end of file +} diff --git a/src/components/commissions/types/ListTypes.tsx b/src/components/commissions/types/ListTypes.tsx index 2a48b18..56c3f71 100644 --- a/src/components/commissions/types/ListTypes.tsx +++ b/src/components/commissions/types/ListTypes.tsx @@ -5,7 +5,15 @@ import { updateCommissionTypeSortOrder } from "@/actions/commissions/types/updat import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { CommissionCustomInput, CommissionExtra, CommissionOption, CommissionType, CommissionTypeCustomInput, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma/client"; +import { + CommissionCustomInput, + CommissionExtra, + CommissionOption, + CommissionType, + CommissionTypeCustomInput, + CommissionTypeExtra, + CommissionTypeOption, +} from "@/generated/prisma/client"; import { closestCenter, DndContext, @@ -187,4 +195,4 @@ export default function ListTypes({ types }: { types: CommissionTypeWithItems[] ); -} \ No newline at end of file +} diff --git a/src/components/commissions/types/NewTypeForm.tsx b/src/components/commissions/types/NewTypeForm.tsx index 04867b8..c0507d8 100644 --- a/src/components/commissions/types/NewTypeForm.tsx +++ b/src/components/commissions/types/NewTypeForm.tsx @@ -1,47 +1,68 @@ -"use client" +"use client"; import { createCommissionType } from "@/actions/commissions/types/newType"; import { Button } from "@/components/ui/button"; -import { Form, FormControl, FormDescription, 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 { CommissionCustomInput, CommissionExtra, CommissionOption } from "@/generated/prisma/client"; +import MultipleSelector from "@/components/ui/multiselect"; +import type { + CommissionCustomInput, + CommissionExtra, + CommissionOption, + Tag, +} from "@/generated/prisma/client"; import { commissionTypeSchema } from "@/schemas/commissionType"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; -import * as z from "zod/v4"; +import type * as z from "zod/v4"; import { CommissionCustomInputField } from "./form/CommissionCustomInputField"; import { CommissionExtraField } from "./form/CommissionExtraField"; import { CommissionOptionField } from "./form/CommissionOptionField"; type Props = { - options: CommissionOption[], - extras: CommissionExtra[], - customInputs: CommissionCustomInput[] -} + options: CommissionOption[]; + extras: CommissionExtra[]; + customInputs: CommissionCustomInput[]; + tags: Tag[]; +}; -export default function NewTypeForm({ options, extras, customInputs }: Props) { +export default function NewTypeForm({ + options, + extras, + customInputs, + tags, +}: Props) { const router = useRouter(); const form = useForm>({ resolver: zodResolver(commissionTypeSchema), defaultValues: { name: "", description: "", + tagIds: [], options: [], extras: [], }, - }) + }); async function onSubmit(values: z.infer) { try { - const created = await createCommissionType(values) - console.log("CommissionType created:", created) - toast("Commission type created.") - router.push("/commissions/types") + const created = await createCommissionType(values); + console.log("CommissionType created:", created); + toast("Commission type created."); + router.push("/commissions/types"); } catch (err) { - console.error(err) - toast("Failed to create commission type.") + console.error(err); + toast("Failed to create commission type."); } } @@ -58,7 +79,9 @@ export default function NewTypeForm({ options, extras, customInputs }: Props) { - The name of the commission type. + + The name of the commission type. + )} @@ -77,6 +100,41 @@ export default function NewTypeForm({ options, extras, customInputs }: Props) { )} /> + { + const selectedIds = field.value ?? []; + const selectedOptions = tags + .filter((t) => selectedIds.includes(t.id)) + .map((t) => ({ label: t.name, value: t.id })); + + return ( + + Tags + + ({ + label: t.name, + value: t.id, + }))} + placeholder="Select tags for this commission type" + hidePlaceholderWhenSelected + selectFirstItem + value={selectedOptions} + onChange={(options) => + field.onChange(options.map((o) => o.value)) + } + /> + + + Used to link this commission type to tagged artworks. + + + + ); + }} + /> @@ -84,10 +142,16 @@ export default function NewTypeForm({ options, extras, customInputs }: Props) {
- +
); -} \ No newline at end of file +} diff --git a/src/components/tags/EditTagForm.tsx b/src/components/tags/EditTagForm.tsx index 06eb048..26116df 100644 --- a/src/components/tags/EditTagForm.tsx +++ b/src/components/tags/EditTagForm.tsx @@ -1,18 +1,32 @@ -"use client" +"use client"; import { updateTag } from "@/actions/tags/updateTag"; import { Button } from "@/components/ui/button"; -import { Form, FormControl, FormDescription, 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 { Textarea } from "@/components/ui/textarea"; -import { ArtCategory, Tag, TagAlias } from "@/generated/prisma/client"; -import { TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema"; +import type { ArtCategory, Tag, TagAlias } from "@/generated/prisma/client"; +import { type TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; 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"; @@ -42,19 +56,19 @@ export default function EditTagForm({ isParent: tag.isParent ?? false, showOnAnimalPage: tag.showOnAnimalPage ?? false, isVisible: tag.isVisible ?? true, - aliases: tag.aliases?.map(a => a.alias) ?? [] - } - }) + aliases: tag.aliases?.map((a) => a.alias) ?? [], + }, + }); async function onSubmit(values: TagFormInput) { try { - const updated = await updateTag(tag.id, values) - console.log("Tag updated:", updated) - toast("Tag updated.") - router.push("/tags") + const updated = await updateTag(tag.id, values); + console.log("Tag updated:", updated); + toast("Tag updated."); + router.push("/tags"); } catch (err) { - console.error(err) - toast("Failed to update tag.") + console.error(err); + toast("Failed to update tag."); } } @@ -64,6 +78,10 @@ export default function EditTagForm({ return (
+

+ Tags can be used across artworks, commission types, and future miniatures. Category links + are optional and control category-specific behavior. +

{/* String */} @@ -87,7 +105,10 @@ export default function EditTagForm({ Description -