Add tags to commssion types and custom types. Add button for example images to cards
This commit is contained in:
16
prisma/migrations/20260202151753_tags_03/migration.sql
Normal file
16
prisma/migrations/20260202151753_tags_03/migration.sql
Normal file
@ -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;
|
||||||
@ -47,13 +47,13 @@ model Artwork {
|
|||||||
galleryId String?
|
galleryId String?
|
||||||
gallery Gallery? @relation(fields: [galleryId], references: [id])
|
gallery Gallery? @relation(fields: [galleryId], references: [id])
|
||||||
|
|
||||||
metadata ArtworkMetadata?
|
metadata ArtworkMetadata?
|
||||||
timelapse ArtworkTimelapse?
|
timelapse ArtworkTimelapse?
|
||||||
|
|
||||||
albums Album[]
|
albums Album[]
|
||||||
categories ArtCategory[]
|
categories ArtCategory[]
|
||||||
colors ArtworkColor[]
|
colors ArtworkColor[]
|
||||||
tags Tag[] @relation("ArtworkTags")
|
tags Tag[] @relation("ArtworkTags")
|
||||||
variants FileVariant[]
|
variants FileVariant[]
|
||||||
|
|
||||||
@@index([colorStatus])
|
@@index([colorStatus])
|
||||||
@ -165,12 +165,12 @@ model ArtworkTimelapse {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
artworkId String @unique
|
artworkId String @unique
|
||||||
artwork Artwork @relation(fields: [artworkId], references: [id], onDelete: Cascade)
|
artwork Artwork @relation(fields: [artworkId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
enabled Boolean @default(false)
|
enabled Boolean @default(false)
|
||||||
|
|
||||||
s3Key String @unique
|
s3Key String @unique
|
||||||
fileName String?
|
fileName String?
|
||||||
mimeType String?
|
mimeType String?
|
||||||
sizeBytes Int?
|
sizeBytes Int?
|
||||||
@ -224,12 +224,13 @@ model Tag {
|
|||||||
|
|
||||||
description String?
|
description String?
|
||||||
|
|
||||||
aliases TagAlias[]
|
aliases TagAlias[]
|
||||||
categoryLinks TagCategory[]
|
categoryLinks TagCategory[]
|
||||||
categoryParents TagCategory[] @relation("TagCategoryParent")
|
categoryParents TagCategory[] @relation("TagCategoryParent")
|
||||||
artworks Artwork[] @relation("ArtworkTags")
|
artworks Artwork[] @relation("ArtworkTags")
|
||||||
commissionTypes CommissionType[] @relation("CommissionTypeTags")
|
commissionTypes CommissionType[] @relation("CommissionTypeTags")
|
||||||
miniatures Miniature[] @relation("MiniatureTags")
|
commissionCustomCards CommissionCustomCard[] @relation("CommissionCustomCardTags")
|
||||||
|
miniatures Miniature[] @relation("MiniatureTags")
|
||||||
}
|
}
|
||||||
|
|
||||||
model TagAlias {
|
model TagAlias {
|
||||||
@ -240,7 +241,7 @@ model TagAlias {
|
|||||||
alias String @unique
|
alias String @unique
|
||||||
|
|
||||||
tagId String
|
tagId String
|
||||||
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([tagId, alias])
|
@@unique([tagId, alias])
|
||||||
@@index([alias])
|
@@index([alias])
|
||||||
@ -310,13 +311,14 @@ model CommissionCustomCard {
|
|||||||
|
|
||||||
name String
|
name String
|
||||||
|
|
||||||
description String?
|
description String?
|
||||||
referenceImageUrl String?
|
referenceImageUrl String?
|
||||||
isVisible Boolean @default(true)
|
isVisible Boolean @default(true)
|
||||||
isSpecialOffer Boolean @default(false)
|
isSpecialOffer Boolean @default(false)
|
||||||
|
|
||||||
options CommissionCustomCardOption[]
|
tags Tag[] @relation("CommissionCustomCardTags")
|
||||||
extras CommissionCustomCardExtra[]
|
options CommissionCustomCardOption[]
|
||||||
|
extras CommissionCustomCardExtra[]
|
||||||
requests CommissionRequest[]
|
requests CommissionRequest[]
|
||||||
|
|
||||||
@@index([isVisible, sortIndex])
|
@@index([isVisible, sortIndex])
|
||||||
@ -332,9 +334,9 @@ model CommissionOption {
|
|||||||
|
|
||||||
description String?
|
description String?
|
||||||
|
|
||||||
types CommissionTypeOption[]
|
types CommissionTypeOption[]
|
||||||
customCards CommissionCustomCardOption[]
|
customCards CommissionCustomCardOption[]
|
||||||
requests CommissionRequest[]
|
requests CommissionRequest[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model CommissionTypeOption {
|
model CommissionTypeOption {
|
||||||
@ -366,8 +368,8 @@ model CommissionExtra {
|
|||||||
|
|
||||||
description String?
|
description String?
|
||||||
|
|
||||||
requests CommissionRequest[]
|
requests CommissionRequest[]
|
||||||
types CommissionTypeExtra[]
|
types CommissionTypeExtra[]
|
||||||
customCards CommissionCustomCardExtra[]
|
customCards CommissionCustomCardExtra[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,12 +477,12 @@ model CommissionRequest {
|
|||||||
userAgent String?
|
userAgent String?
|
||||||
customFields Json?
|
customFields Json?
|
||||||
|
|
||||||
optionId String?
|
optionId String?
|
||||||
typeId String?
|
typeId String?
|
||||||
customCardId String?
|
customCardId String?
|
||||||
option CommissionOption? @relation(fields: [optionId], references: [id])
|
option CommissionOption? @relation(fields: [optionId], references: [id])
|
||||||
type CommissionType? @relation(fields: [typeId], references: [id])
|
type CommissionType? @relation(fields: [typeId], references: [id])
|
||||||
customCard CommissionCustomCard? @relation(fields: [customCardId], references: [id])
|
customCard CommissionCustomCard? @relation(fields: [customCardId], references: [id])
|
||||||
|
|
||||||
extras CommissionExtra[]
|
extras CommissionExtra[]
|
||||||
files CommissionRequestFile[]
|
files CommissionRequestFile[]
|
||||||
@ -491,9 +493,9 @@ model CommissionGuidelines {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
markdown String
|
markdown String
|
||||||
exampleImageUrl String?
|
exampleImageUrl String?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
@@index([isActive])
|
@@index([isActive])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,9 @@ export async function createCommissionCustomCard(
|
|||||||
referenceImageUrl: data.referenceImageUrl ?? null,
|
referenceImageUrl: data.referenceImageUrl ?? null,
|
||||||
isVisible: data.isVisible ?? true,
|
isVisible: data.isVisible ?? true,
|
||||||
isSpecialOffer: data.isSpecialOffer ?? false,
|
isSpecialOffer: data.isSpecialOffer ?? false,
|
||||||
|
tags: data.tagIds?.length
|
||||||
|
? { connect: data.tagIds.map((id) => ({ id })) }
|
||||||
|
: undefined,
|
||||||
options: {
|
options: {
|
||||||
create:
|
create:
|
||||||
data.options?.map((opt, index) => ({
|
data.options?.map((opt, index) => ({
|
||||||
|
|||||||
@ -20,6 +20,12 @@ export async function updateCommissionCustomCard(
|
|||||||
referenceImageUrl: data.referenceImageUrl ?? null,
|
referenceImageUrl: data.referenceImageUrl ?? null,
|
||||||
isVisible: data.isVisible ?? true,
|
isVisible: data.isVisible ?? true,
|
||||||
isSpecialOffer: data.isSpecialOffer ?? false,
|
isSpecialOffer: data.isSpecialOffer ?? false,
|
||||||
|
tags: data.tagIds
|
||||||
|
? {
|
||||||
|
set: [],
|
||||||
|
connect: data.tagIds.map((id) => ({ id })),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
options: {
|
options: {
|
||||||
deleteMany: {},
|
deleteMany: {},
|
||||||
create: data.options?.map((opt, index) => ({
|
create: data.options?.map((opt, index) => ({
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use server"
|
"use server";
|
||||||
|
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma";
|
||||||
import { commissionTypeSchema } from "@/schemas/commissionType"
|
import { commissionTypeSchema } from "@/schemas/commissionType";
|
||||||
|
|
||||||
export async function createCommissionOption(data: { name: string }) {
|
export async function createCommissionOption(data: { name: string }) {
|
||||||
return await prisma.commissionOption.create({
|
return await prisma.commissionOption.create({
|
||||||
@ -9,7 +9,7 @@ export async function createCommissionOption(data: { name: string }) {
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
description: "",
|
description: "",
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createCommissionExtra(data: { name: string }) {
|
export async function createCommissionExtra(data: { name: string }) {
|
||||||
@ -18,64 +18,70 @@ export async function createCommissionExtra(data: { name: string }) {
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
description: "",
|
description: "",
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createCommissionCustomInput(data: {
|
export async function createCommissionCustomInput(data: {
|
||||||
name: string
|
name: string;
|
||||||
fieldId: string
|
fieldId: string;
|
||||||
}) {
|
}) {
|
||||||
return await prisma.commissionCustomInput.create({
|
return await prisma.commissionCustomInput.create({
|
||||||
data: {
|
data: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
fieldId: data.fieldId,
|
fieldId: data.fieldId,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createCommissionType(formData: commissionTypeSchema) {
|
export async function createCommissionType(formData: commissionTypeSchema) {
|
||||||
const parsed = commissionTypeSchema.safeParse(formData)
|
const parsed = commissionTypeSchema.safeParse(formData);
|
||||||
|
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
console.error("Validation failed", parsed.error)
|
console.error("Validation failed", parsed.error);
|
||||||
throw new Error("Invalid input")
|
throw new Error("Invalid input");
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = parsed.data
|
const data = parsed.data;
|
||||||
|
|
||||||
const created = await prisma.commissionType.create({
|
const created = await prisma.commissionType.create({
|
||||||
data: {
|
data: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
|
tags: data.tagIds?.length
|
||||||
|
? { connect: data.tagIds.map((id) => ({ id })) }
|
||||||
|
: undefined,
|
||||||
options: {
|
options: {
|
||||||
create: data.options?.map((opt, index) => ({
|
create:
|
||||||
option: { connect: { id: opt.optionId } },
|
data.options?.map((opt, index) => ({
|
||||||
price: opt.price,
|
option: { connect: { id: opt.optionId } },
|
||||||
pricePercent: opt.pricePercent,
|
price: opt.price,
|
||||||
priceRange: opt.priceRange,
|
pricePercent: opt.pricePercent,
|
||||||
sortIndex: index,
|
priceRange: opt.priceRange,
|
||||||
})) || [],
|
sortIndex: index,
|
||||||
|
})) || [],
|
||||||
},
|
},
|
||||||
extras: {
|
extras: {
|
||||||
create: data.extras?.map((ext, index) => ({
|
create:
|
||||||
extra: { connect: { id: ext.extraId } },
|
data.extras?.map((ext, index) => ({
|
||||||
price: ext.price,
|
extra: { connect: { id: ext.extraId } },
|
||||||
pricePercent: ext.pricePercent,
|
price: ext.price,
|
||||||
priceRange: ext.priceRange,
|
pricePercent: ext.pricePercent,
|
||||||
sortIndex: index,
|
priceRange: ext.priceRange,
|
||||||
})) || [],
|
sortIndex: index,
|
||||||
|
})) || [],
|
||||||
},
|
},
|
||||||
customInputs: {
|
customInputs: {
|
||||||
create: data.customInputs?.map((c, index) => ({
|
create:
|
||||||
customInput: { connect: { id: c.customInputId } },
|
data.customInputs?.map((c, index) => ({
|
||||||
label: c.label,
|
customInput: { connect: { id: c.customInputId } },
|
||||||
inputType: c.inputType,
|
label: c.label,
|
||||||
required: c.required,
|
inputType: c.inputType,
|
||||||
sortIndex: index,
|
required: c.required,
|
||||||
})) || [],
|
sortIndex: index,
|
||||||
|
})) || [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return created
|
return created;
|
||||||
}
|
}
|
||||||
@ -1,20 +1,26 @@
|
|||||||
"use server"
|
"use server";
|
||||||
|
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma";
|
||||||
import { commissionTypeSchema } from "@/schemas/commissionType"
|
import { commissionTypeSchema } from "@/schemas/commissionType";
|
||||||
import * as z from "zod/v4"
|
import type * as z from "zod/v4";
|
||||||
|
|
||||||
export async function updateCommissionType(
|
export async function updateCommissionType(
|
||||||
id: string,
|
id: string,
|
||||||
rawData: z.infer<typeof commissionTypeSchema>
|
rawData: z.infer<typeof commissionTypeSchema>,
|
||||||
) {
|
) {
|
||||||
const data = commissionTypeSchema.parse(rawData)
|
const data = commissionTypeSchema.parse(rawData);
|
||||||
|
|
||||||
const updated = await prisma.commissionType.update({
|
const updated = await prisma.commissionType.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
|
tags: data.tagIds
|
||||||
|
? {
|
||||||
|
set: [],
|
||||||
|
connect: data.tagIds.map((id) => ({ id })),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
options: {
|
options: {
|
||||||
deleteMany: {},
|
deleteMany: {},
|
||||||
create: data.options?.map((opt, index) => ({
|
create: data.options?.map((opt, index) => ({
|
||||||
@ -37,13 +43,14 @@ export async function updateCommissionType(
|
|||||||
},
|
},
|
||||||
customInputs: {
|
customInputs: {
|
||||||
deleteMany: {},
|
deleteMany: {},
|
||||||
create: data.customInputs?.map((c, index) => ({
|
create:
|
||||||
customInput: { connect: { id: c.customInputId } },
|
data.customInputs?.map((c, index) => ({
|
||||||
label: c.label,
|
customInput: { connect: { id: c.customInputId } },
|
||||||
inputType: c.inputType,
|
label: c.label,
|
||||||
required: c.required,
|
inputType: c.inputType,
|
||||||
sortIndex: index,
|
required: c.required,
|
||||||
})) || [],
|
sortIndex: index,
|
||||||
|
})) || [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
@ -51,7 +58,7 @@ export async function updateCommissionType(
|
|||||||
extras: true,
|
extras: true,
|
||||||
customInputs: true,
|
customInputs: true,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return updated
|
return updated;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
"use server";
|
|
||||||
|
|
||||||
export async function migrateArtTags() {
|
|
||||||
throw new Error("Migration disabled: ArtTag models removed.");
|
|
||||||
}
|
|
||||||
@ -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<JoinMigrationResult> {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -10,17 +10,19 @@ export default async function CommissionCustomCardEditPage({
|
|||||||
}) {
|
}) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
const [card, options, extras, images] = await Promise.all([
|
const [card, options, extras, images, tags] = await Promise.all([
|
||||||
prisma.commissionCustomCard.findUnique({
|
prisma.commissionCustomCard.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
options: { orderBy: { sortIndex: "asc" } },
|
options: { orderBy: { sortIndex: "asc" } },
|
||||||
extras: { orderBy: { sortIndex: "asc" } },
|
extras: { orderBy: { sortIndex: "asc" } },
|
||||||
|
tags: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.commissionOption.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
|
prisma.commissionOption.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
|
||||||
prisma.commissionExtra.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
|
prisma.commissionExtra.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
|
||||||
listCommissionCustomCardImages(),
|
listCommissionCustomCardImages(),
|
||||||
|
prisma.tag.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!card) {
|
if (!card) {
|
||||||
@ -37,6 +39,7 @@ export default async function CommissionCustomCardEditPage({
|
|||||||
allOptions={options}
|
allOptions={options}
|
||||||
allExtras={extras}
|
allExtras={extras}
|
||||||
images={images}
|
images={images}
|
||||||
|
allTags={tags}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,10 +3,11 @@ import NewCustomCardForm from "@/components/commissions/customCards/NewCustomCar
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
export default async function CommissionCustomCardsNewPage() {
|
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.commissionOption.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
|
||||||
prisma.commissionExtra.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
|
prisma.commissionExtra.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
|
||||||
listCommissionCustomCardImages(),
|
listCommissionCustomCardImages(),
|
||||||
|
prisma.tag.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -14,7 +15,7 @@ export default async function CommissionCustomCardsNewPage() {
|
|||||||
<div className="flex gap-4 justify-between pb-8">
|
<div className="flex gap-4 justify-between pb-8">
|
||||||
<h1 className="text-2xl font-bold mb-4">New Custom Commission Card</h1>
|
<h1 className="text-2xl font-bold mb-4">New Custom Commission Card</h1>
|
||||||
</div>
|
</div>
|
||||||
<NewCustomCardForm options={options} extras={extras} images={images} />
|
<NewCustomCardForm options={options} extras={extras} images={images} tags={tags} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,12 @@ export default async function CommissionTypesEditPage({ params }: { params: { id
|
|||||||
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
|
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
|
||||||
extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
|
extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
|
||||||
customInputs: { include: { customInput: 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({
|
const options = await prisma.commissionOption.findMany({
|
||||||
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
});
|
});
|
||||||
@ -32,7 +36,12 @@ export default async function CommissionTypesEditPage({ params }: { params: { id
|
|||||||
<div className="flex gap-4 justify-between pb-8">
|
<div className="flex gap-4 justify-between pb-8">
|
||||||
<h1 className="text-2xl font-bold mb-4">Edit Commission Type</h1>
|
<h1 className="text-2xl font-bold mb-4">Edit Commission Type</h1>
|
||||||
</div>
|
</div>
|
||||||
<EditTypeForm type={commissionType} allOptions={options} allExtras={extras} />
|
<EditTypeForm
|
||||||
|
type={commissionType}
|
||||||
|
allOptions={options}
|
||||||
|
allExtras={extras}
|
||||||
|
allTags={tags}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2,6 +2,9 @@ import NewTypeForm from "@/components/commissions/types/NewTypeForm";
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
export default async function CommissionTypesNewPage() {
|
export default async function CommissionTypesNewPage() {
|
||||||
|
const tags = await prisma.tag.findMany({
|
||||||
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
|
});
|
||||||
const options = await prisma.commissionOption.findMany({
|
const options = await prisma.commissionOption.findMany({
|
||||||
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
});
|
});
|
||||||
@ -17,7 +20,12 @@ export default async function CommissionTypesNewPage() {
|
|||||||
<div className="flex gap-4 justify-between pb-8">
|
<div className="flex gap-4 justify-between pb-8">
|
||||||
<h1 className="text-2xl font-bold mb-4">New Commission Type</h1>
|
<h1 className="text-2xl font-bold mb-4">New Commission Type</h1>
|
||||||
</div>
|
</div>
|
||||||
<NewTypeForm options={options} extras={extras} customInputs={customInputs} />
|
<NewTypeForm
|
||||||
|
options={options}
|
||||||
|
extras={extras}
|
||||||
|
customInputs={customInputs}
|
||||||
|
tags={tags}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,20 +1,9 @@
|
|||||||
import { migrateArtworkTagJoin } from "@/actions/tags/migrateArtworkTagJoin";
|
|
||||||
import TagTabs from "@/components/tags/TagTabs";
|
import TagTabs from "@/components/tags/TagTabs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
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";
|
||||||
|
|
||||||
async function migrateArtworkTagJoinCopy() {
|
|
||||||
"use server";
|
|
||||||
await migrateArtworkTagJoin();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function migrateArtworkTagJoinDropOld() {
|
|
||||||
"use server";
|
|
||||||
await migrateArtworkTagJoin({ dropOld: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function ArtTagsPage() {
|
export default async function ArtTagsPage() {
|
||||||
const items = await prisma.tag.findMany({
|
const items = await prisma.tag.findMany({
|
||||||
include: {
|
include: {
|
||||||
@ -56,22 +45,10 @@ export default async function ArtTagsPage() {
|
|||||||
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">
|
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||||
Tags
|
Tags
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">Manage tags.</p>
|
||||||
Manage tags, aliases, categories, and usage across artworks.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||||
<form action={migrateArtworkTagJoinCopy}>
|
|
||||||
<Button type="submit" variant="secondary" className="h-11">
|
|
||||||
Copy tag relations
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
<form action={migrateArtworkTagJoinDropOld}>
|
|
||||||
<Button type="submit" variant="destructive" className="h-11">
|
|
||||||
Copy + drop old
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
<Button asChild className="h-11 gap-2">
|
<Button asChild className="h-11 gap-2">
|
||||||
<Link href="/tags/new">
|
<Link href="/tags/new">
|
||||||
<PlusCircleIcon className="h-4 w-4" />
|
<PlusCircleIcon className="h-4 w-4" />
|
||||||
@ -79,17 +56,15 @@ export default async function ArtTagsPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header >
|
</header>
|
||||||
|
|
||||||
{
|
{rows.length > 0 ? (
|
||||||
rows.length > 0 ? (
|
<TagTabs tags={rows} />
|
||||||
<TagTabs tags={rows} />
|
) : (
|
||||||
) : (
|
<p className="text-muted-foreground">
|
||||||
<p className="text-muted-foreground">
|
There are no tags yet. Consider adding some!
|
||||||
There are no tags yet. Consider adding some!
|
</p>
|
||||||
</p>
|
)}
|
||||||
)
|
</div>
|
||||||
}
|
|
||||||
</div >
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import type { CommissionExtra, CommissionOption } from "@/generated/prisma/client";
|
import type { CommissionExtra, CommissionOption, Tag } from "@/generated/prisma/client";
|
||||||
import {
|
import {
|
||||||
commissionCustomCardSchema,
|
commissionCustomCardSchema,
|
||||||
type CommissionCustomCardValues,
|
type CommissionCustomCardValues,
|
||||||
@ -26,6 +26,7 @@ import { toast } from "sonner";
|
|||||||
import { CommissionExtraField } from "../types/form/CommissionExtraField";
|
import { CommissionExtraField } from "../types/form/CommissionExtraField";
|
||||||
import { CommissionOptionField } from "../types/form/CommissionOptionField";
|
import { CommissionOptionField } from "../types/form/CommissionOptionField";
|
||||||
import { CustomCardImagePicker } from "./CustomCardImagePicker";
|
import { CustomCardImagePicker } from "./CustomCardImagePicker";
|
||||||
|
import MultipleSelector from "@/components/ui/multiselect";
|
||||||
|
|
||||||
type CustomCardOption = {
|
type CustomCardOption = {
|
||||||
optionId: string;
|
optionId: string;
|
||||||
@ -48,6 +49,7 @@ type CustomCardWithItems = {
|
|||||||
referenceImageUrl: string | null;
|
referenceImageUrl: string | null;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
isSpecialOffer: boolean;
|
isSpecialOffer: boolean;
|
||||||
|
tags: Tag[];
|
||||||
options: CustomCardOption[];
|
options: CustomCardOption[];
|
||||||
extras: CustomCardExtra[];
|
extras: CustomCardExtra[];
|
||||||
};
|
};
|
||||||
@ -57,6 +59,7 @@ type Props = {
|
|||||||
allOptions: CommissionOption[];
|
allOptions: CommissionOption[];
|
||||||
allExtras: CommissionExtra[];
|
allExtras: CommissionExtra[];
|
||||||
images: CommissionCustomCardImageItem[];
|
images: CommissionCustomCardImageItem[];
|
||||||
|
allTags: Tag[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EditCustomCardForm({
|
export default function EditCustomCardForm({
|
||||||
@ -64,6 +67,7 @@ export default function EditCustomCardForm({
|
|||||||
allOptions,
|
allOptions,
|
||||||
allExtras,
|
allExtras,
|
||||||
images,
|
images,
|
||||||
|
allTags,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<CommissionCustomCardValues>({
|
const form = useForm<CommissionCustomCardValues>({
|
||||||
@ -74,6 +78,7 @@ export default function EditCustomCardForm({
|
|||||||
isVisible: card.isVisible,
|
isVisible: card.isVisible,
|
||||||
isSpecialOffer: card.isSpecialOffer,
|
isSpecialOffer: card.isSpecialOffer,
|
||||||
referenceImageUrl: card.referenceImageUrl ?? null,
|
referenceImageUrl: card.referenceImageUrl ?? null,
|
||||||
|
tagIds: card.tags.map((t) => t.id),
|
||||||
options: card.options.map((o) => ({
|
options: card.options.map((o) => ({
|
||||||
optionId: o.optionId,
|
optionId: o.optionId,
|
||||||
price: o.price ?? undefined,
|
price: o.price ?? undefined,
|
||||||
@ -171,6 +176,37 @@ export default function EditCustomCardForm({
|
|||||||
render={() => <CustomCardImagePicker form={form} initialImages={images} />}
|
render={() => <CustomCardImagePicker form={form} initialImages={images} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="tagIds"
|
||||||
|
render={({ field }) => {
|
||||||
|
const selectedIds = field.value ?? [];
|
||||||
|
const selectedOptions = allTags
|
||||||
|
.filter((t) => selectedIds.includes(t.id))
|
||||||
|
.map((t) => ({ label: t.name, value: t.id }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Tags</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<MultipleSelector
|
||||||
|
options={allTags.map((t) => ({ 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))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Used to link this custom card to tagged artworks.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<CommissionOptionField options={allOptions} />
|
<CommissionOptionField options={allOptions} />
|
||||||
<CommissionExtraField extras={allExtras} />
|
<CommissionExtraField extras={allExtras} />
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import type { CommissionExtra, CommissionOption } from "@/generated/prisma/client";
|
import type { CommissionExtra, CommissionOption, Tag } from "@/generated/prisma/client";
|
||||||
import {
|
import {
|
||||||
commissionCustomCardSchema,
|
commissionCustomCardSchema,
|
||||||
type CommissionCustomCardValues,
|
type CommissionCustomCardValues,
|
||||||
@ -26,14 +26,16 @@ import { toast } from "sonner";
|
|||||||
import { CommissionExtraField } from "../types/form/CommissionExtraField";
|
import { CommissionExtraField } from "../types/form/CommissionExtraField";
|
||||||
import { CommissionOptionField } from "../types/form/CommissionOptionField";
|
import { CommissionOptionField } from "../types/form/CommissionOptionField";
|
||||||
import { CustomCardImagePicker } from "./CustomCardImagePicker";
|
import { CustomCardImagePicker } from "./CustomCardImagePicker";
|
||||||
|
import MultipleSelector from "@/components/ui/multiselect";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
options: CommissionOption[];
|
options: CommissionOption[];
|
||||||
extras: CommissionExtra[];
|
extras: CommissionExtra[];
|
||||||
images: CommissionCustomCardImageItem[];
|
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 router = useRouter();
|
||||||
const form = useForm<CommissionCustomCardValues>({
|
const form = useForm<CommissionCustomCardValues>({
|
||||||
resolver: zodResolver(commissionCustomCardSchema),
|
resolver: zodResolver(commissionCustomCardSchema),
|
||||||
@ -43,6 +45,7 @@ export default function NewCustomCardForm({ options, extras, images }: Props) {
|
|||||||
isVisible: true,
|
isVisible: true,
|
||||||
isSpecialOffer: false,
|
isSpecialOffer: false,
|
||||||
referenceImageUrl: null,
|
referenceImageUrl: null,
|
||||||
|
tagIds: [],
|
||||||
options: [],
|
options: [],
|
||||||
extras: [],
|
extras: [],
|
||||||
},
|
},
|
||||||
@ -131,6 +134,37 @@ export default function NewCustomCardForm({ options, extras, images }: Props) {
|
|||||||
render={() => <CustomCardImagePicker form={form} initialImages={images} />}
|
render={() => <CustomCardImagePicker form={form} initialImages={images} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="tagIds"
|
||||||
|
render={({ field }) => {
|
||||||
|
const selectedIds = field.value ?? [];
|
||||||
|
const selectedOptions = tags
|
||||||
|
.filter((t) => selectedIds.includes(t.id))
|
||||||
|
.map((t) => ({ label: t.name, value: t.id }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Tags</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<MultipleSelector
|
||||||
|
options={tags.map((t) => ({ 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))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Used to link this custom card to tagged artworks.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<CommissionOptionField options={options} />
|
<CommissionOptionField options={options} />
|
||||||
<CommissionExtraField extras={extras} />
|
<CommissionExtraField extras={extras} />
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,28 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { updateCommissionType } from "@/actions/commissions/types/updateType";
|
import { updateCommissionType } from "@/actions/commissions/types/updateType";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { 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 { commissionTypeSchema } from "@/schemas/commissionType";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@ -15,25 +33,35 @@ import { CommissionExtraField } from "./form/CommissionExtraField";
|
|||||||
import { CommissionOptionField } from "./form/CommissionOptionField";
|
import { CommissionOptionField } from "./form/CommissionOptionField";
|
||||||
|
|
||||||
type CommissionTypeWithConnections = CommissionType & {
|
type CommissionTypeWithConnections = CommissionType & {
|
||||||
options: (CommissionTypeOption & { option: CommissionOption })[]
|
options: (CommissionTypeOption & { option: CommissionOption })[];
|
||||||
extras: (CommissionTypeExtra & { extra: CommissionExtra })[]
|
extras: (CommissionTypeExtra & { extra: CommissionExtra })[];
|
||||||
customInputs: (CommissionTypeCustomInput & { customInput: CommissionCustomInput })[]
|
customInputs: (CommissionTypeCustomInput & {
|
||||||
}
|
customInput: CommissionCustomInput;
|
||||||
|
})[];
|
||||||
|
tags: Tag[];
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: CommissionTypeWithConnections
|
type: CommissionTypeWithConnections;
|
||||||
allOptions: CommissionOption[],
|
allOptions: CommissionOption[];
|
||||||
allExtras: CommissionExtra[],
|
allExtras: CommissionExtra[];
|
||||||
|
allTags: Tag[];
|
||||||
// allCustomInputs: CommissionCustomInput[]
|
// allCustomInputs: CommissionCustomInput[]
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function EditTypeForm({ type, allOptions, allExtras }: Props) {
|
export default function EditTypeForm({
|
||||||
|
type,
|
||||||
|
allOptions,
|
||||||
|
allExtras,
|
||||||
|
allTags,
|
||||||
|
}: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<z.infer<typeof commissionTypeSchema>>({
|
const form = useForm<z.infer<typeof commissionTypeSchema>>({
|
||||||
resolver: zodResolver(commissionTypeSchema),
|
resolver: zodResolver(commissionTypeSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: type.name,
|
name: type.name,
|
||||||
description: type.description ?? "",
|
description: type.description ?? "",
|
||||||
|
tagIds: type.tags.map((t) => t.id),
|
||||||
options: type.options.map((o) => ({
|
options: type.options.map((o) => ({
|
||||||
optionId: o.optionId,
|
optionId: o.optionId,
|
||||||
price: o.price ?? undefined,
|
price: o.price ?? undefined,
|
||||||
@ -54,16 +82,16 @@ export default function EditTypeForm({ type, allOptions, allExtras }: Props) {
|
|||||||
required: f.required,
|
required: f.required,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof commissionTypeSchema>) {
|
async function onSubmit(values: z.infer<typeof commissionTypeSchema>) {
|
||||||
try {
|
try {
|
||||||
await updateCommissionType(type.id, values)
|
await updateCommissionType(type.id, values);
|
||||||
toast.success("Commission type updated.")
|
toast.success("Commission type updated.");
|
||||||
router.push("/commissions/types")
|
router.push("/commissions/types");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err);
|
||||||
toast("Failed to create commission type.")
|
toast("Failed to create commission type.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +108,9 @@ export default function EditTypeForm({ type, allOptions, allExtras }: Props) {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>The name of the commission type.</FormDescription>
|
<FormDescription>
|
||||||
|
The name of the commission type.
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@ -99,6 +129,41 @@ export default function EditTypeForm({ type, allOptions, allExtras }: Props) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="tagIds"
|
||||||
|
render={({ field }) => {
|
||||||
|
const selectedIds = field.value ?? [];
|
||||||
|
const selectedOptions = allTags
|
||||||
|
.filter((t) => selectedIds.includes(t.id))
|
||||||
|
.map((t) => ({ label: t.name, value: t.id }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Tags</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<MultipleSelector
|
||||||
|
options={allTags.map((t) => ({
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Used to link this commission type to tagged artworks.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<CommissionOptionField options={allOptions} />
|
<CommissionOptionField options={allOptions} />
|
||||||
<CommissionExtraField extras={allExtras} />
|
<CommissionExtraField extras={allExtras} />
|
||||||
@ -106,7 +171,13 @@ export default function EditTypeForm({ type, allOptions, allExtras }: Props) {
|
|||||||
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@ -5,7 +5,15 @@ import { updateCommissionTypeSortOrder } from "@/actions/commissions/types/updat
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
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 {
|
import {
|
||||||
closestCenter,
|
closestCenter,
|
||||||
DndContext,
|
DndContext,
|
||||||
|
|||||||
@ -1,47 +1,68 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { createCommissionType } from "@/actions/commissions/types/newType";
|
import { createCommissionType } from "@/actions/commissions/types/newType";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { 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 { commissionTypeSchema } from "@/schemas/commissionType";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import * as z from "zod/v4";
|
import type * as z from "zod/v4";
|
||||||
import { CommissionCustomInputField } from "./form/CommissionCustomInputField";
|
import { CommissionCustomInputField } from "./form/CommissionCustomInputField";
|
||||||
import { CommissionExtraField } from "./form/CommissionExtraField";
|
import { CommissionExtraField } from "./form/CommissionExtraField";
|
||||||
import { CommissionOptionField } from "./form/CommissionOptionField";
|
import { CommissionOptionField } from "./form/CommissionOptionField";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
options: CommissionOption[],
|
options: CommissionOption[];
|
||||||
extras: CommissionExtra[],
|
extras: CommissionExtra[];
|
||||||
customInputs: CommissionCustomInput[]
|
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 router = useRouter();
|
||||||
const form = useForm<z.infer<typeof commissionTypeSchema>>({
|
const form = useForm<z.infer<typeof commissionTypeSchema>>({
|
||||||
resolver: zodResolver(commissionTypeSchema),
|
resolver: zodResolver(commissionTypeSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
tagIds: [],
|
||||||
options: [],
|
options: [],
|
||||||
extras: [],
|
extras: [],
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof commissionTypeSchema>) {
|
async function onSubmit(values: z.infer<typeof commissionTypeSchema>) {
|
||||||
try {
|
try {
|
||||||
const created = await createCommissionType(values)
|
const created = await createCommissionType(values);
|
||||||
console.log("CommissionType created:", created)
|
console.log("CommissionType created:", created);
|
||||||
toast("Commission type created.")
|
toast("Commission type created.");
|
||||||
router.push("/commissions/types")
|
router.push("/commissions/types");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err);
|
||||||
toast("Failed to create commission type.")
|
toast("Failed to create commission type.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +79,9 @@ export default function NewTypeForm({ options, extras, customInputs }: Props) {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>The name of the commission type.</FormDescription>
|
<FormDescription>
|
||||||
|
The name of the commission type.
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@ -77,6 +100,41 @@ export default function NewTypeForm({ options, extras, customInputs }: Props) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="tagIds"
|
||||||
|
render={({ field }) => {
|
||||||
|
const selectedIds = field.value ?? [];
|
||||||
|
const selectedOptions = tags
|
||||||
|
.filter((t) => selectedIds.includes(t.id))
|
||||||
|
.map((t) => ({ label: t.name, value: t.id }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Tags</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<MultipleSelector
|
||||||
|
options={tags.map((t) => ({
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Used to link this commission type to tagged artworks.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<CommissionOptionField options={options} />
|
<CommissionOptionField options={options} />
|
||||||
<CommissionExtraField extras={extras} />
|
<CommissionExtraField extras={extras} />
|
||||||
@ -84,7 +142,13 @@ export default function NewTypeForm({ options, extras, customInputs }: Props) {
|
|||||||
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@ -1,18 +1,32 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
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, 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 { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { ArtCategory, Tag, TagAlias } from "@/generated/prisma/client";
|
import type { ArtCategory, Tag, TagAlias } from "@/generated/prisma/client";
|
||||||
import { TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema";
|
import { type TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useForm } from "react-hook-form";
|
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 { Switch } from "../ui/switch";
|
||||||
import AliasEditor from "./AliasEditor";
|
import AliasEditor from "./AliasEditor";
|
||||||
|
|
||||||
@ -42,19 +56,19 @@ export default function EditTagForm({
|
|||||||
isParent: tag.isParent ?? false,
|
isParent: tag.isParent ?? false,
|
||||||
showOnAnimalPage: tag.showOnAnimalPage ?? false,
|
showOnAnimalPage: tag.showOnAnimalPage ?? false,
|
||||||
isVisible: tag.isVisible ?? true,
|
isVisible: tag.isVisible ?? true,
|
||||||
aliases: tag.aliases?.map(a => a.alias) ?? []
|
aliases: tag.aliases?.map((a) => a.alias) ?? [],
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
async function onSubmit(values: TagFormInput) {
|
async function onSubmit(values: TagFormInput) {
|
||||||
try {
|
try {
|
||||||
const updated = await updateTag(tag.id, values)
|
const updated = await updateTag(tag.id, values);
|
||||||
console.log("Tag updated:", updated)
|
console.log("Tag updated:", updated);
|
||||||
toast("Tag updated.")
|
toast("Tag updated.");
|
||||||
router.push("/tags")
|
router.push("/tags");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err);
|
||||||
toast("Failed to update tag.")
|
toast("Failed to update tag.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +78,10 @@ export default function EditTagForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Tags can be used across artworks, commission types, and future miniatures. Category links
|
||||||
|
are optional and control category-specific behavior.
|
||||||
|
</p>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
{/* String */}
|
{/* String */}
|
||||||
@ -87,7 +105,10 @@ export default function EditTagForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea {...field} placeholder="A descriptive text (optional)" />
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
placeholder="A descriptive text (optional)"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -98,14 +119,14 @@ export default function EditTagForm({
|
|||||||
name="categoryIds"
|
name="categoryIds"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
const selectedOptions = categories
|
const selectedOptions = categories
|
||||||
.filter(cat => field.value?.includes(cat.id))
|
.filter((cat) => field.value?.includes(cat.id))
|
||||||
.map(cat => ({ label: cat.name, value: cat.id }));
|
.map((cat) => ({ label: cat.name, value: cat.id }));
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Categories</FormLabel>
|
<FormLabel>Categories</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<MultipleSelector
|
<MultipleSelector
|
||||||
defaultOptions={categories.map(cat => ({
|
defaultOptions={categories.map((cat) => ({
|
||||||
label: cat.name,
|
label: cat.name,
|
||||||
value: cat.id,
|
value: cat.id,
|
||||||
}))}
|
}))}
|
||||||
@ -114,14 +135,14 @@ export default function EditTagForm({
|
|||||||
selectFirstItem
|
selectFirstItem
|
||||||
value={selectedOptions}
|
value={selectedOptions}
|
||||||
onChange={(options) => {
|
onChange={(options) => {
|
||||||
const ids = options.map(option => option.value);
|
const ids = options.map((option) => option.value);
|
||||||
field.onChange(ids);
|
field.onChange(ids);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
@ -159,7 +180,10 @@ export default function EditTagForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Aliases</FormLabel>
|
<FormLabel>Aliases</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<AliasEditor value={field.value ?? []} onChange={field.onChange} />
|
<AliasEditor
|
||||||
|
value={field.value ?? []}
|
||||||
|
onChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -176,7 +200,10 @@ export default function EditTagForm({
|
|||||||
<FormDescription></FormDescription>
|
<FormDescription></FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@ -193,7 +220,10 @@ export default function EditTagForm({
|
|||||||
<FormDescription></FormDescription>
|
<FormDescription></FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@ -210,7 +240,10 @@ export default function EditTagForm({
|
|||||||
<FormDescription></FormDescription>
|
<FormDescription></FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@ -218,10 +251,16 @@ export default function EditTagForm({
|
|||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div >
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,42 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
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, 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 { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { ArtCategory, Tag } from "@/generated/prisma/client";
|
import type { ArtCategory, Tag } from "@/generated/prisma/client";
|
||||||
import { TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema";
|
import { type TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useForm } from "react-hook-form";
|
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 { Switch } from "../ui/switch";
|
||||||
import AliasEditor from "./AliasEditor";
|
import AliasEditor from "./AliasEditor";
|
||||||
|
|
||||||
|
export default function NewTagForm({
|
||||||
export default function NewTagForm({ categories, allTags }: { categories: ArtCategory[], allTags: Tag[] }) {
|
categories,
|
||||||
|
allTags,
|
||||||
|
}: {
|
||||||
|
categories: ArtCategory[];
|
||||||
|
allTags: Tag[];
|
||||||
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<TagFormInput>({
|
const form = useForm<TagFormInput>({
|
||||||
resolver: zodResolver(tagSchema),
|
resolver: zodResolver(tagSchema),
|
||||||
@ -30,18 +49,18 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
showOnAnimalPage: false,
|
showOnAnimalPage: false,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
async function onSubmit(values: TagFormInput) {
|
async function onSubmit(values: TagFormInput) {
|
||||||
try {
|
try {
|
||||||
const created = await createTag(values)
|
const created = await createTag(values);
|
||||||
console.log("Tag created:", created)
|
console.log("Tag created:", created);
|
||||||
toast("Tag created.")
|
toast("Tag created.");
|
||||||
router.push("/tags")
|
router.push("/tags");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err);
|
||||||
toast("Failed to create tag.")
|
toast("Failed to create tag.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +70,11 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Tags can be used across artworks, commission types, and future
|
||||||
|
miniatures. Category links are optional and control category-specific
|
||||||
|
behavior.
|
||||||
|
</p>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
{/* String */}
|
{/* String */}
|
||||||
@ -74,7 +98,10 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea {...field} placeholder="A descriptive text (optional)" />
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
placeholder="A descriptive text (optional)"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -85,14 +112,14 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
name="categoryIds"
|
name="categoryIds"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
const selectedOptions = categories
|
const selectedOptions = categories
|
||||||
.filter(cat => field.value?.includes(cat.id))
|
.filter((cat) => field.value?.includes(cat.id))
|
||||||
.map(cat => ({ label: cat.name, value: cat.id }));
|
.map((cat) => ({ label: cat.name, value: cat.id }));
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Categories</FormLabel>
|
<FormLabel>Categories</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<MultipleSelector
|
<MultipleSelector
|
||||||
defaultOptions={categories.map(cat => ({
|
defaultOptions={categories.map((cat) => ({
|
||||||
label: cat.name,
|
label: cat.name,
|
||||||
value: cat.id,
|
value: cat.id,
|
||||||
}))}
|
}))}
|
||||||
@ -101,14 +128,14 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
selectFirstItem
|
selectFirstItem
|
||||||
value={selectedOptions}
|
value={selectedOptions}
|
||||||
onChange={(options) => {
|
onChange={(options) => {
|
||||||
const ids = options.map(option => option.value);
|
const ids = options.map((option) => option.value);
|
||||||
field.onChange(ids);
|
field.onChange(ids);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
@ -146,7 +173,10 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Aliases</FormLabel>
|
<FormLabel>Aliases</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<AliasEditor value={field.value ?? []} onChange={field.onChange} />
|
<AliasEditor
|
||||||
|
value={field.value ?? []}
|
||||||
|
onChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -163,7 +193,10 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
<FormDescription></FormDescription>
|
<FormDescription></FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@ -180,7 +213,10 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
<FormDescription></FormDescription>
|
<FormDescription></FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@ -197,7 +233,10 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
<FormDescription></FormDescription>
|
<FormDescription></FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@ -205,10 +244,16 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div >
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -480,8 +480,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
} // When onSearch is provided, we don't want to filter the options. You can still override it.
|
} // When onSearch is provided, we don't want to filter the options. You can still override it.
|
||||||
filter={commandFilter()}
|
filter={commandFilter()}
|
||||||
>
|
>
|
||||||
<button
|
<div
|
||||||
type="button"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'min-h-10 rounded-md border border-input text-base ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 md:text-sm',
|
'min-h-10 rounded-md border border-input text-base ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 md:text-sm',
|
||||||
{
|
{
|
||||||
@ -490,11 +489,6 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
},
|
},
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
|
||||||
if (disabled) return;
|
|
||||||
inputRef?.current?.focus();
|
|
||||||
}}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
>
|
||||||
<div className="relative flex flex-wrap gap-1">
|
<div className="relative flex flex-wrap gap-1">
|
||||||
{selected.map((option) => {
|
{selected.map((option) => {
|
||||||
@ -581,7 +575,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
<X />
|
<X />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{open && (
|
{open && (
|
||||||
<CommandList
|
<CommandList
|
||||||
@ -597,7 +591,10 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>{loadingIndicator}</>
|
// biome-ignore lint: lint/complexity/noUselessFragments
|
||||||
|
<>
|
||||||
|
{loadingIndicator}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{EmptyItem()}
|
{EmptyItem()}
|
||||||
@ -605,47 +602,45 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
{!selectFirstItem && <CommandItem value="-" className="hidden" />}
|
{!selectFirstItem && <CommandItem value="-" className="hidden" />}
|
||||||
{orderedGroupEntries.map(([key, dropdowns]) => (
|
{orderedGroupEntries.map(([key, dropdowns]) => (
|
||||||
<CommandGroup key={key} heading={key} className="h-full overflow-auto">
|
<CommandGroup key={key} heading={key} className="h-full overflow-auto">
|
||||||
<>
|
{dropdowns.map((option) => {
|
||||||
{dropdowns.map((option) => {
|
const alreadySelected = selected.some((s) => s.value === option.value);
|
||||||
const alreadySelected = selected.some((s) => s.value === option.value);
|
const disabledItem = option.disable || (showSelectedInDropdown && alreadySelected);
|
||||||
const disabledItem = option.disable || (showSelectedInDropdown && alreadySelected);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={`${option.label}::${option.value}`}
|
value={`${option.label}::${option.value}`}
|
||||||
disabled={disabledItem}
|
disabled={disabledItem}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
if (disabledItem) return;
|
if (disabledItem) return;
|
||||||
|
|
||||||
if (selected.length >= maxSelected) {
|
if (selected.length >= maxSelected) {
|
||||||
onMaxSelected?.(selected.length);
|
onMaxSelected?.(selected.length);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
|
|
||||||
// Guard against duplicates (safety)
|
// Guard against duplicates (safety)
|
||||||
if (selected.some((s) => s.value === option.value)) return;
|
if (selected.some((s) => s.value === option.value)) return;
|
||||||
|
|
||||||
const newOptions = [...selected, option];
|
const newOptions = [...selected, option];
|
||||||
setSelected(newOptions);
|
setSelected(newOptions);
|
||||||
onChange?.(newOptions);
|
onChange?.(newOptions);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'cursor-pointer',
|
'cursor-pointer',
|
||||||
disabledItem && 'cursor-default text-muted-foreground',
|
disabledItem && 'cursor-default text-muted-foreground',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,27 +1,27 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||||
import * as React from "react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Select({
|
function Select({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectGroup({
|
function SelectGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectValue({
|
function SelectValue({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectTrigger({
|
function SelectTrigger({
|
||||||
@ -30,7 +30,7 @@ function SelectTrigger({
|
|||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
size?: "sm" | "default"
|
size?: "sm" | "default";
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
@ -38,7 +38,7 @@ function SelectTrigger({
|
|||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"border-input data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@ -47,7 +47,7 @@ function SelectTrigger({
|
|||||||
<ChevronDownIcon className="size-4 opacity-50" />
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectContent({
|
function SelectContent({
|
||||||
@ -65,7 +65,7 @@ function SelectContent({
|
|||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
align={align}
|
align={align}
|
||||||
@ -76,7 +76,7 @@ function SelectContent({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"p-1",
|
"p-1",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1"
|
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -84,7 +84,7 @@ function SelectContent({
|
|||||||
<SelectScrollDownButton />
|
<SelectScrollDownButton />
|
||||||
</SelectPrimitive.Content>
|
</SelectPrimitive.Content>
|
||||||
</SelectPrimitive.Portal>
|
</SelectPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectLabel({
|
function SelectLabel({
|
||||||
@ -97,7 +97,7 @@ function SelectLabel({
|
|||||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectItem({
|
function SelectItem({
|
||||||
@ -110,7 +110,7 @@ function SelectItem({
|
|||||||
data-slot="select-item"
|
data-slot="select-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@ -124,7 +124,7 @@ function SelectItem({
|
|||||||
</span>
|
</span>
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
</SelectPrimitive.Item>
|
</SelectPrimitive.Item>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectSeparator({
|
function SelectSeparator({
|
||||||
@ -137,7 +137,7 @@ function SelectSeparator({
|
|||||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectScrollUpButton({
|
function SelectScrollUpButton({
|
||||||
@ -149,13 +149,13 @@ function SelectScrollUpButton({
|
|||||||
data-slot="select-scroll-up-button"
|
data-slot="select-scroll-up-button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default items-center justify-center py-1",
|
"flex cursor-default items-center justify-center py-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronUpIcon className="size-4" />
|
<ChevronUpIcon className="size-4" />
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectScrollDownButton({
|
function SelectScrollDownButton({
|
||||||
@ -167,13 +167,13 @@ function SelectScrollDownButton({
|
|||||||
data-slot="select-scroll-down-button"
|
data-slot="select-scroll-down-button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default items-center justify-center py-1",
|
"flex cursor-default items-center justify-center py-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronDownIcon className="size-4" />
|
<ChevronDownIcon className="size-4" />
|
||||||
</SelectPrimitive.ScrollDownButton>
|
</SelectPrimitive.ScrollDownButton>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -187,5 +187,5 @@ export {
|
|||||||
SelectSeparator,
|
SelectSeparator,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export const commissionCustomCardSchema = z.object({
|
|||||||
isVisible: z.boolean(),
|
isVisible: z.boolean(),
|
||||||
isSpecialOffer: z.boolean(),
|
isSpecialOffer: z.boolean(),
|
||||||
referenceImageUrl: z.string().nullable().optional(),
|
referenceImageUrl: z.string().nullable().optional(),
|
||||||
|
tagIds: z.array(z.string()).optional(),
|
||||||
options: z.array(optionField).optional(),
|
options: z.array(optionField).optional(),
|
||||||
extras: z.array(extraField).optional(),
|
extras: z.array(extraField).optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -32,6 +32,7 @@ const customInputsField = z.object({
|
|||||||
export const commissionTypeSchema = z.object({
|
export const commissionTypeSchema = z.object({
|
||||||
name: z.string().min(1, "Name is required. Min 1 character."),
|
name: z.string().min(1, "Name is required. Min 1 character."),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
tagIds: z.array(z.string()).optional(),
|
||||||
options: z.array(optionField).optional(),
|
options: z.array(optionField).optional(),
|
||||||
extras: z.array(extraField).optional(),
|
extras: z.array(extraField).optional(),
|
||||||
customInputs: z.array(customInputsField).optional(),
|
customInputs: z.array(customInputsField).optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user