From 8572e22c5dd107923506baef68c0ab96b9fea57e Mon Sep 17 00:00:00 2001 From: Citali Date: Tue, 3 Feb 2026 12:17:47 +0100 Subject: [PATCH] Refactor code --- src/actions/artworks/deleteArtwork.ts | 1 + .../artworks/generateGalleryVariant.ts | 1 + src/actions/artworks/getArtworkColors.ts | 3 +- .../artworks/getArtworkFilterOptions.ts | 1 + src/actions/artworks/getArtworks.ts | 13 +- src/actions/artworks/getArtworksTablePage.ts | 19 +- .../artworks/getGalleryVariantStats.ts | 8 +- src/actions/artworks/timelapse.ts | 55 ++--- src/actions/artworks/updateArtwork.ts | 12 +- src/actions/auth/registerFirstUser.ts | 25 +-- src/actions/categories/createCategory.ts | 26 +-- src/actions/categories/deleteCategory.ts | 5 +- src/actions/categories/getCategories.ts | 13 +- src/actions/categories/updateCategory.ts | 25 +-- src/actions/colors/getArtworkColorStats.ts | 11 +- .../colors/processPendingArtworkColors.ts | 10 +- .../commissions/customCards/deleteCard.ts | 1 + src/actions/commissions/customCards/images.ts | 8 +- .../commissions/customCards/newCard.ts | 1 + .../commissions/customCards/updateCard.ts | 1 + .../customCards/updateSortOrder.ts | 1 + src/actions/commissions/examples.ts | 8 +- .../commissions/guidelines/getGuidelines.ts | 3 +- .../commissions/guidelines/saveGuidelines.ts | 3 +- .../requests/deleteCommissionRequest.ts | 3 +- .../requests/getCommissionRequestById.ts | 3 +- .../getCommissionRequestsTablePage.ts | 43 ++-- .../requests/setCommissionRequestStatus.ts | 3 +- .../requests/updateCommissionRequest.ts | 20 +- .../requests/updateCommissionRequestStatus.ts | 25 +-- src/actions/commissions/types/deleteType.ts | 15 +- src/actions/commissions/types/extras.ts | 8 +- src/actions/commissions/types/newType.ts | 15 +- src/actions/commissions/types/options.ts | 13 +- .../types/updateCommissionTypeSortOrder.ts | 9 +- src/actions/commissions/types/updateType.ts | 3 +- src/actions/home/getDashboard.ts | 8 +- src/actions/tags/createTag.ts | 20 +- src/actions/tags/deleteTag.ts | 3 +- src/actions/tags/getTags.ts | 7 +- src/actions/tags/isDescendant.ts | 3 +- src/actions/tags/updateTag.ts | 20 +- src/actions/tos/getTos.ts | 5 +- src/actions/tos/saveTos.ts | 5 +- src/actions/uploads/createBulkImages.ts | 8 +- src/actions/uploads/createImage.ts | 199 +----------------- src/actions/uploads/createImageFromFile.ts | 2 +- src/actions/users/createUser.ts | 18 +- src/actions/users/deleteUser.ts | 8 +- src/actions/users/getUsers.ts | 21 +- src/actions/users/resendVerification.ts | 17 +- src/app/(admin)/artworks/[id]/page.tsx | 15 +- src/app/(admin)/artworks/page.tsx | 1 + src/app/(admin)/categories/[id]/page.tsx | 7 +- src/app/(admin)/categories/new/page.tsx | 3 +- src/app/(admin)/categories/page.tsx | 8 +- .../commissions/custom-cards/[id]/page.tsx | 6 +- .../commissions/custom-cards/new/page.tsx | 7 +- .../(admin)/commissions/custom-cards/page.tsx | 1 + .../(admin)/commissions/guidelines/page.tsx | 1 + src/app/(admin)/commissions/kanban/page.tsx | 1 + .../commissions/requests/[id]/page.tsx | 3 +- src/app/(admin)/commissions/requests/page.tsx | 5 +- .../(admin)/commissions/types/[id]/page.tsx | 10 +- .../(admin)/commissions/types/extras/page.tsx | 3 +- .../(admin)/commissions/types/new/page.tsx | 6 +- .../commissions/types/options/page.tsx | 3 +- src/app/(admin)/commissions/types/page.tsx | 12 +- src/app/(admin)/layout.tsx | 16 +- src/app/(admin)/page.tsx | 132 +----------- src/app/(admin)/tags/[id]/page.tsx | 5 +- src/app/(admin)/tags/new/page.tsx | 1 + src/app/(admin)/tags/page.tsx | 1 + src/app/(admin)/tos/page.tsx | 3 +- src/app/(admin)/uploads/bulk/page.tsx | 7 +- src/app/(admin)/uploads/single/page.tsx | 7 +- src/app/(admin)/users/new/page.tsx | 4 +- src/app/(admin)/users/page.tsx | 4 +- src/app/(auth)/forgot-password/page.tsx | 1 + src/app/(auth)/layout.tsx | 4 +- src/app/(auth)/login/page.tsx | 3 +- src/app/(auth)/register/page.tsx | 1 + src/app/(auth)/reset-password/page.tsx | 3 +- src/app/api/artworks/page/route.ts | 1 + src/app/api/auth/[...all]/route.ts | 3 +- src/app/api/image/[...key]/route.ts | 25 ++- src/app/api/requests/image/route.ts | 34 ++- src/app/api/v1/commissions/route.ts | 33 +-- src/app/error.tsx | 1 + src/app/global-error.tsx | 1 + src/app/layout.tsx | 10 +- src/app/loading.tsx | 1 + src/app/not-found.tsx | 1 + .../artworks/ArtworkColorProcessor.tsx | 11 +- src/components/artworks/ArtworkGallery.tsx | 15 +- .../ArtworkGalleryVariantProcessor.tsx | 14 +- src/components/artworks/ArtworksTable.tsx | 53 +++-- src/components/artworks/FilterBar.tsx | 10 +- src/components/artworks/MultiSelectFilter.tsx | 5 +- .../artworks/single/ArtworkColors.tsx | 7 +- .../artworks/single/ArtworkDetails.tsx | 28 +-- .../artworks/single/ArtworkTimelapse.tsx | 9 +- .../artworks/single/ArtworkVariants.tsx | 1 + .../artworks/single/DeleteArtworkButton.tsx | 5 +- .../artworks/single/EditArtworkForm.tsx | 38 ++-- src/components/auth/ForgotPasswordForm.tsx | 5 +- src/components/auth/LoginForm.tsx | 18 +- src/components/auth/LogoutButton.tsx | 1 + src/components/auth/RegisterForm.tsx | 5 +- src/components/auth/ResetPasswordForm.tsx | 5 +- src/components/categories/CategoryTable.tsx | 3 +- .../categories/EditCategoryForm.tsx | 28 +-- src/components/categories/NewCategoryForm.tsx | 28 +-- .../customCards/CustomCardImagePicker.tsx | 9 +- .../customCards/EditCustomCardForm.tsx | 8 +- .../customCards/ListCustomCards.tsx | 3 +- .../customCards/NewCustomCardForm.tsx | 12 +- .../commissions/extras/ExtraDialog.tsx | 5 +- .../commissions/extras/ExtraListClient.tsx | 5 +- .../commissions/guidelines/Editor.tsx | 49 ++--- .../kanban/CommissionsKanbanClient.tsx | 12 +- .../commissions/options/OptionDialog.tsx | 5 +- .../commissions/options/OptionsListClient.tsx | 1 + .../requests/CommissionRequestEditor.tsx | 84 +++----- .../requests/CommissionRequestsTable.tsx | 51 +++-- .../commissions/requests/RequestsTable.tsx | 21 +- .../commissions/types/EditTypeForm.tsx | 5 +- .../commissions/types/ListTypes.tsx | 82 ++++---- .../commissions/types/NewTypeForm.tsx | 4 +- .../commissions/types/SortableItem.tsx | 21 +- .../commissions/types/SortableItemCard.tsx | 24 ++- .../types/form/ComboboxCreateable.tsx | 59 +++--- .../types/form/CommissionCustomInputField.tsx | 91 ++++---- .../types/form/CommissionExtraField.tsx | 77 +++---- .../types/form/CommissionOptionField.tsx | 73 +++---- .../editor/plugins/basic-blocks-base-kit.tsx | 1 + .../editor/plugins/basic-blocks-kit.tsx | 1 + .../editor/plugins/basic-marks-base-kit.tsx | 1 + .../editor/plugins/basic-marks-kit.tsx | 1 + .../editor/plugins/basic-nodes-kit.tsx | 1 + .../editor/plugins/code-block-base-kit.tsx | 1 + .../editor/plugins/code-block-kit.tsx | 1 + .../editor/plugins/indent-base-kit.tsx | 1 + src/components/editor/plugins/indent-kit.tsx | 1 + .../editor/plugins/list-base-kit.tsx | 1 + src/components/editor/plugins/list-kit.tsx | 1 + .../editor/plugins/markdown-kit.tsx | 1 + src/components/global/Footer.tsx | 1 + src/components/global/Header.tsx | 7 +- src/components/global/MobileSidebar.tsx | 2 +- src/components/global/ModeToggle.tsx | 57 ++--- src/components/global/Sidebar.tsx | 1 + src/components/global/ThemeProvider.tsx | 13 +- src/components/global/TopNav.tsx | 74 +------ src/components/global/nav.ts | 1 + src/components/home/StatCard.tsx | 7 +- src/components/home/StatusPill.tsx | 1 + src/components/lists/ItemList.tsx | 31 ++- src/components/tags/AliasEditor.tsx | 1 + src/components/tags/EditTagForm.tsx | 7 +- src/components/tags/NewTagForm.tsx | 7 +- src/components/tags/TagTableAnimal.tsx | 1 + src/components/tags/TagTableMain.tsx | 1 + src/components/tags/TagTabs.tsx | 7 +- src/components/tos/Editor.tsx | 49 ++--- .../uploads/UploadBulkImageForm.tsx | 30 ++- src/components/uploads/UploadImageForm.tsx | 19 +- src/components/users/CreateUserForm.tsx | 15 +- src/components/users/UsersTable.tsx | 33 +-- src/schemas/artworks/timelapse.ts | 39 ++++ src/schemas/auth.ts | 9 + src/schemas/commissions/publicRequest.ts | 34 +++ src/schemas/commissions/requestsTable.ts | 22 ++ src/schemas/commissions/updateRequest.ts | 14 ++ .../commissions/updateRequestStatus.ts | 12 ++ src/schemas/users.ts | 16 ++ src/types/Artwork.ts | 6 + src/types/auth.ts | 7 + src/types/colors.ts | 16 ++ src/types/commissions.ts | 13 ++ src/types/dashboard.ts | 3 + src/types/pagination.ts | 1 + src/types/s3.ts | 3 + src/types/uploads.ts | 3 + src/types/users.ts | 9 + 185 files changed, 1268 insertions(+), 1458 deletions(-) create mode 100644 src/schemas/artworks/timelapse.ts create mode 100644 src/schemas/auth.ts create mode 100644 src/schemas/commissions/publicRequest.ts create mode 100644 src/schemas/commissions/requestsTable.ts create mode 100644 src/schemas/commissions/updateRequest.ts create mode 100644 src/schemas/commissions/updateRequestStatus.ts create mode 100644 src/schemas/users.ts create mode 100644 src/types/auth.ts create mode 100644 src/types/colors.ts create mode 100644 src/types/commissions.ts create mode 100644 src/types/dashboard.ts create mode 100644 src/types/pagination.ts create mode 100644 src/types/s3.ts create mode 100644 src/types/uploads.ts create mode 100644 src/types/users.ts diff --git a/src/actions/artworks/deleteArtwork.ts b/src/actions/artworks/deleteArtwork.ts index daae5b9..1830a9f 100644 --- a/src/actions/artworks/deleteArtwork.ts +++ b/src/actions/artworks/deleteArtwork.ts @@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma"; import { s3 } from "@/lib/s3"; import { DeleteObjectCommand } from "@aws-sdk/client-s3"; +// Deletes an artwork and all related assets and records. export async function deleteArtwork(artworkId: string) { const artwork = await prisma.artwork.findUnique({ where: { id: artworkId }, diff --git a/src/actions/artworks/generateGalleryVariant.ts b/src/actions/artworks/generateGalleryVariant.ts index 904d51e..a9aa352 100644 --- a/src/actions/artworks/generateGalleryVariant.ts +++ b/src/actions/artworks/generateGalleryVariant.ts @@ -8,6 +8,7 @@ import sharp from "sharp"; const GALLERY_TARGET_SIZE = 300; +// Generates a gallery-sized variant for a single artwork. export async function generateGalleryVariant( artworkId: string, opts?: { force?: boolean }, diff --git a/src/actions/artworks/getArtworkColors.ts b/src/actions/artworks/getArtworkColors.ts index d7d73fa..8bc1878 100644 --- a/src/actions/artworks/getArtworkColors.ts +++ b/src/actions/artworks/getArtworkColors.ts @@ -2,10 +2,11 @@ import { prisma } from "@/lib/prisma"; +// Returns color swatches for a given artwork. export async function getArtworkColors(artworkId: string) { return prisma.artworkColor.findMany({ where: { artworkId }, include: { color: true }, orderBy: [{ type: "asc" }], }); -} \ No newline at end of file +} diff --git a/src/actions/artworks/getArtworkFilterOptions.ts b/src/actions/artworks/getArtworkFilterOptions.ts index 92f1149..ccdd464 100644 --- a/src/actions/artworks/getArtworkFilterOptions.ts +++ b/src/actions/artworks/getArtworkFilterOptions.ts @@ -2,6 +2,7 @@ import { prisma } from "@/lib/prisma"; +// Returns album/category options for artwork filters. export async function getArtworkFilterOptions() { const [albums, categories] = await Promise.all([ prisma.album.findMany({ select: { id: true, name: true }, orderBy: { name: "asc" } }), diff --git a/src/actions/artworks/getArtworks.ts b/src/actions/artworks/getArtworks.ts index cac2a7d..d898f36 100644 --- a/src/actions/artworks/getArtworks.ts +++ b/src/actions/artworks/getArtworks.ts @@ -1,9 +1,10 @@ -"use server" +"use server"; -import { prisma } from "@/lib/prisma" +import { prisma } from "@/lib/prisma"; +// Loads a single artwork with relations for the edit page. export async function getSingleArtwork(id: string) { - return await prisma.artwork.findUnique({ + return prisma.artwork.findUnique({ where: { id }, include: { // album: true, @@ -17,7 +18,7 @@ export async function getSingleArtwork(id: string) { // sortContexts: true, tags: true, variants: true, - timelapse: true - } - }) + timelapse: true, + }, + }); } diff --git a/src/actions/artworks/getArtworksTablePage.ts b/src/actions/artworks/getArtworksTablePage.ts index c7d3099..e06cf2e 100644 --- a/src/actions/artworks/getArtworksTablePage.ts +++ b/src/actions/artworks/getArtworksTablePage.ts @@ -1,15 +1,19 @@ "use server"; +import type { Prisma } from "@/generated/prisma/client"; import { prisma } from "@/lib/prisma"; -import { ArtworkTableInput, artworkTableInputSchema, artworkTableOutputSchema } from "@/schemas/artworks/tableSchema"; +import { type ArtworkTableInput, artworkTableInputSchema, artworkTableOutputSchema } from "@/schemas/artworks/tableSchema"; +// Builds the admin artworks table page with filters, sorting, and pagination. function triToBool(tri: "any" | "true" | "false"): boolean | undefined { if (tri === "any") return undefined; return tri === "true"; } -function mapSortingToOrderBy(sorting: ArtworkTableInput["sorting"]) { - const allowed: Record any> = { +function mapSortingToOrderBy( + sorting: ArtworkTableInput["sorting"] +): Prisma.ArtworkOrderByWithRelationInput[] { + const allowed: Record Prisma.ArtworkOrderByWithRelationInput> = { createdAt: (desc) => ({ createdAt: desc ? "desc" : "asc" }), updatedAt: (desc) => ({ updatedAt: desc ? "desc" : "asc" }), sortIndex: (desc) => ({ sortIndex: desc ? "desc" : "asc" }), @@ -25,9 +29,10 @@ function mapSortingToOrderBy(sorting: ArtworkTableInput["sorting"]) { tagsCount: (desc) => ({ tags: { _count: desc ? "desc" : "asc" } }), }; - const orderBy = sorting - .map((s) => allowed[s.id]?.(s.desc)) - .filter(Boolean); + const orderBy = sorting.flatMap((s) => { + const mapper = allowed[s.id]; + return mapper ? [mapper(s.desc)] : []; + }); orderBy.push({ id: "desc" }); return orderBy; @@ -44,7 +49,7 @@ export async function getArtworksTablePage(input: unknown) { const nsfw = triToBool(filters.nsfw); const needsWork = triToBool(filters.needsWork); - const where: any = { + const where: Prisma.ArtworkWhereInput = { ...(typeof published === "boolean" ? { published } : {}), ...(typeof nsfw === "boolean" ? { nsfw } : {}), ...(typeof needsWork === "boolean" ? { needsWork } : {}), diff --git a/src/actions/artworks/getGalleryVariantStats.ts b/src/actions/artworks/getGalleryVariantStats.ts index 1fe8664..5363801 100644 --- a/src/actions/artworks/getGalleryVariantStats.ts +++ b/src/actions/artworks/getGalleryVariantStats.ts @@ -1,13 +1,9 @@ "use server"; import { prisma } from "@/lib/prisma"; +import type { GalleryVariantStats } from "@/types/Artwork"; -export type GalleryVariantStats = { - total: number; - withGallery: number; - missing: number; -}; - +// Returns counts for gallery variant presence. export async function getGalleryVariantStats(): Promise { const [total, withGallery] = await Promise.all([ prisma.artwork.count(), diff --git a/src/actions/artworks/timelapse.ts b/src/actions/artworks/timelapse.ts index f1205ad..f9f0ffa 100644 --- a/src/actions/artworks/timelapse.ts +++ b/src/actions/artworks/timelapse.ts @@ -2,25 +2,30 @@ import { prisma } from "@/lib/prisma"; import { s3 } from "@/lib/s3"; +import { + confirmArtworkTimelapseUploadSchema, + createArtworkTimelapseUploadSchema, + deleteArtworkTimelapseSchema, + setArtworkTimelapseEnabledSchema, +} from "@/schemas/artworks/timelapse"; +import type { + ConfirmArtworkTimelapseUploadInput, + CreateArtworkTimelapseUploadInput, + DeleteArtworkTimelapseInput, + SetArtworkTimelapseEnabledInput, +} from "@/schemas/artworks/timelapse"; import { PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { revalidatePath } from "next/cache"; -import { z } from "zod/v4"; import { v4 as uuidv4 } from "uuid"; -const createUploadSchema = z.object({ - artworkId: z.string().min(1), - fileName: z.string().min(1), - mimeType: z.string().min(1), - sizeBytes: z.number().int().positive(), -}); - /** * Creates a presigned PUT url so the client can upload large timelapse videos directly to S3 * (avoids Next.js body-size/proxy limits). */ -export async function createArtworkTimelapseUpload(input: z.infer) { - const { artworkId, fileName, mimeType, sizeBytes } = createUploadSchema.parse(input); +export async function createArtworkTimelapseUpload(input: CreateArtworkTimelapseUploadInput) { + const { artworkId, fileName, mimeType, sizeBytes } = + createArtworkTimelapseUploadSchema.parse(input); const ext = fileName.includes(".") ? fileName.split(".").pop() : undefined; const suffix = ext ? `.${ext}` : ""; @@ -41,17 +46,10 @@ export async function createArtworkTimelapseUpload(input: z.infer) { - const { artworkId, s3Key, fileName, mimeType, sizeBytes } = confirmSchema.parse(input); +export async function confirmArtworkTimelapseUpload(input: ConfirmArtworkTimelapseUploadInput) { + const { artworkId, s3Key, fileName, mimeType, sizeBytes } = + confirmArtworkTimelapseUploadSchema.parse(input); // If an old timelapse exists, delete the old object so you don't leak storage. const existing = await prisma.artworkTimelapse.findUnique({ where: { artworkId } }); @@ -91,13 +89,8 @@ export async function confirmArtworkTimelapseUpload(input: z.infer) { - const { artworkId, enabled } = enabledSchema.parse(input); +export async function setArtworkTimelapseEnabled(input: SetArtworkTimelapseEnabledInput) { + const { artworkId, enabled } = setArtworkTimelapseEnabledSchema.parse(input); await prisma.artworkTimelapse.update({ where: { artworkId }, @@ -108,12 +101,8 @@ export async function setArtworkTimelapseEnabled(input: z.infer) { - const { artworkId } = deleteSchema.parse(input); +export async function deleteArtworkTimelapse(input: DeleteArtworkTimelapseInput) { + const { artworkId } = deleteArtworkTimelapseSchema.parse(input); const existing = await prisma.artworkTimelapse.findUnique({ where: { artworkId } }); if (!existing) return { ok: true }; diff --git a/src/actions/artworks/updateArtwork.ts b/src/actions/artworks/updateArtwork.ts index cab1ae4..877aefa 100644 --- a/src/actions/artworks/updateArtwork.ts +++ b/src/actions/artworks/updateArtwork.ts @@ -1,12 +1,13 @@ -"use server" +"use server"; import { prisma } from "@/lib/prisma"; import { artworkSchema } from "@/schemas/artworks/imageSchema"; import { normalizeNames, slugify } from "@/utils/artworkHelpers"; import { z } from "zod/v4"; +// Updates an artwork and its tag/category relationships. export async function updateArtwork( - values: z.infer, + values: z.infer, id: string ) { const validated = artworkSchema.safeParse(values); @@ -36,12 +37,11 @@ export async function updateArtwork( const categoriesToCreate = normalizeNames(newCategoryNames); const updatedArtwork = await prisma.$transaction(async (tx) => { - - if(setAsHeader) { + if (setAsHeader) { await tx.artwork.updateMany({ where: { setAsHeader: true }, data: { setAsHeader: false }, - }) + }); } const tagsRelation = @@ -73,7 +73,7 @@ export async function updateArtwork( : {}; return tx.artwork.update({ - where: { id: id }, + where: { id }, data: { name, slug: slugify(name), diff --git a/src/actions/auth/registerFirstUser.ts b/src/actions/auth/registerFirstUser.ts index d90b3f9..77f6e2d 100644 --- a/src/actions/auth/registerFirstUser.ts +++ b/src/actions/auth/registerFirstUser.ts @@ -2,28 +2,25 @@ import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; -import { z } from "zod/v4"; +import type { SignUpResponse } from "@/types/auth"; +import { registerFirstUserSchema } from "@/schemas/auth"; +import type { RegisterFirstUserInput } from "@/schemas/auth"; -const schema = z.object({ - name: z.string().min(1).max(200), - email: z.string().email().max(320), - password: z.string().min(8).max(128), -}); - -export async function registerFirstUser(input: z.infer) { +// Registers the very first user and upgrades them to admin. +export async function registerFirstUser(input: RegisterFirstUserInput) { const count = await prisma.user.count(); if (count !== 0) throw new Error("Registration is disabled."); - const { name, email, password } = schema.parse(input); + const { name, email, password } = registerFirstUserSchema.parse(input); - const res = await auth.api.signUpEmail({ + const res = (await auth.api.signUpEmail({ body: { name, email, password }, - }); + })) as SignUpResponse; const userId = - (res as any)?.user?.id ?? - (res as any)?.data?.user?.id ?? - (res as any)?.data?.id; + res.user?.id ?? + res.data?.user?.id ?? + res.data?.id; if (!userId) throw new Error("Signup failed: no user id returned."); diff --git a/src/actions/categories/createCategory.ts b/src/actions/categories/createCategory.ts index fe01670..db38199 100644 --- a/src/actions/categories/createCategory.ts +++ b/src/actions/categories/createCategory.ts @@ -1,25 +1,27 @@ -"use server" +"use server"; -import { prisma } from "@/lib/prisma" -import { categorySchema } from "@/schemas/artworks/categorySchema" +import { prisma } from "@/lib/prisma"; +import { categorySchema } from "@/schemas/artworks/categorySchema"; +import type * as z from "zod/v4"; -export async function createCategory(formData: categorySchema) { - const parsed = categorySchema.safeParse(formData) +// Creates a new artwork category. +export async function createCategory(formData: z.infer) { + const parsed = categorySchema.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.artCategory.create({ data: { name: data.name, slug: data.slug, - description: data.description + description: data.description, }, - }) + }); - return created -} \ No newline at end of file + return created; +} diff --git a/src/actions/categories/deleteCategory.ts b/src/actions/categories/deleteCategory.ts index 8ced77a..a4da6f1 100644 --- a/src/actions/categories/deleteCategory.ts +++ b/src/actions/categories/deleteCategory.ts @@ -3,6 +3,7 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; +// Deletes a category if it is not referenced by artworks or tags. export async function deleteCategory(catId: string) { const cat = await prisma.artCategory.findUnique({ where: { id: catId }, @@ -11,7 +12,7 @@ export async function deleteCategory(catId: string) { _count: { select: { tagLinks: true, - artworks: true + artworks: true, }, }, }, @@ -32,6 +33,6 @@ export async function deleteCategory(catId: string) { await prisma.artCategory.delete({ where: { id: catId } }); revalidatePath("/categories"); - + return { success: true }; } diff --git a/src/actions/categories/getCategories.ts b/src/actions/categories/getCategories.ts index 5770b27..87f79d8 100644 --- a/src/actions/categories/getCategories.ts +++ b/src/actions/categories/getCategories.ts @@ -1,19 +1,20 @@ -"use server" +"use server"; -import { prisma } from "@/lib/prisma" +import { prisma } from "@/lib/prisma"; +// Category data fetchers for admin pages. export async function getCategoriesWithTags() { - return await prisma.artCategory.findMany({ + return prisma.artCategory.findMany({ include: { tagLinks: { include: { tag: true } } }, orderBy: { sortIndex: "asc" }, - }) + }); } export async function getCategoriesWithCount() { - return await prisma.artCategory.findMany({ + return prisma.artCategory.findMany({ include: { _count: { select: { artworks: true, tagLinks: true } }, }, orderBy: [{ sortIndex: "asc" }, { name: "asc" }], - }) + }); } diff --git a/src/actions/categories/updateCategory.ts b/src/actions/categories/updateCategory.ts index 465693b..f4917df 100644 --- a/src/actions/categories/updateCategory.ts +++ b/src/actions/categories/updateCategory.ts @@ -1,27 +1,28 @@ -"use server" +"use server"; -import { prisma } from '@/lib/prisma'; -import { categorySchema } from '@/schemas/artworks/categorySchema'; -import { z } from 'zod/v4'; +import { prisma } from "@/lib/prisma"; +import { categorySchema } from "@/schemas/artworks/categorySchema"; +import type * as z from "zod/v4"; +// Updates an artwork category by id. export async function updateCategory(id: string, rawData: z.infer) { - const parsed = categorySchema.safeParse(rawData) + const parsed = categorySchema.safeParse(rawData); 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 updated = await prisma.artCategory.update({ where: { id }, data: { name: data.name, slug: data.slug, - description: data.description + description: data.description, }, - }) + }); - return updated -} \ No newline at end of file + return updated; +} diff --git a/src/actions/colors/getArtworkColorStats.ts b/src/actions/colors/getArtworkColorStats.ts index a188bd3..cc6ec53 100644 --- a/src/actions/colors/getArtworkColorStats.ts +++ b/src/actions/colors/getArtworkColorStats.ts @@ -1,16 +1,9 @@ "use server"; import { prisma } from "@/lib/prisma"; +import type { ArtworkColorStats } from "@/types/colors"; -export type ArtworkColorStats = { - total: number; - ready: number; - pending: number; - processing: number; - failed: number; - missingSortKey: number; -}; - +// Aggregates color-processing status counts for artworks. export async function getArtworkColorStats(): Promise { const [ total, diff --git a/src/actions/colors/processPendingArtworkColors.ts b/src/actions/colors/processPendingArtworkColors.ts index 73db36c..239af53 100644 --- a/src/actions/colors/processPendingArtworkColors.ts +++ b/src/actions/colors/processPendingArtworkColors.ts @@ -3,15 +3,9 @@ import type { Prisma } from "@/generated/prisma/client"; import { prisma } from "@/lib/prisma"; import { generateArtworkColorsForArtwork } from "../artworks/generateArtworkColors"; +import type { ProcessColorsResult } from "@/types/colors"; -export type ProcessColorsResult = { - picked: number; - processed: number; - ok: number; - failed: number; - results: Array<{ artworkId: string; ok: boolean; error?: string }>; -}; - +// Processes pending/failed artwork colors with a configurable batch size. export async function processPendingArtworkColors(args?: { limit?: number; includeFailed?: boolean; diff --git a/src/actions/commissions/customCards/deleteCard.ts b/src/actions/commissions/customCards/deleteCard.ts index 23df7be..9916c48 100644 --- a/src/actions/commissions/customCards/deleteCard.ts +++ b/src/actions/commissions/customCards/deleteCard.ts @@ -2,6 +2,7 @@ import { prisma } from "@/lib/prisma"; +// Deletes a custom commission card by id. export async function deleteCommissionCustomCard(id: string) { await prisma.commissionCustomCard.delete({ where: { id }, diff --git a/src/actions/commissions/customCards/images.ts b/src/actions/commissions/customCards/images.ts index 7e6b1c5..ce1d012 100644 --- a/src/actions/commissions/customCards/images.ts +++ b/src/actions/commissions/customCards/images.ts @@ -1,6 +1,7 @@ "use server"; import { s3 } from "@/lib/s3"; +import type { CommissionCustomCardImageItem } from "@/types/commissions"; import { DeleteObjectCommand, ListObjectsV2Command, @@ -9,13 +10,6 @@ import { const PREFIX = "commissions/custom-cards/"; -export type CommissionCustomCardImageItem = { - key: string; - url: string; - size: number | null; - lastModified: string | null; -}; - function buildImageUrl(key: string) { return `/api/image/${encodeURI(key)}`; } diff --git a/src/actions/commissions/customCards/newCard.ts b/src/actions/commissions/customCards/newCard.ts index 0798fc3..723343e 100644 --- a/src/actions/commissions/customCards/newCard.ts +++ b/src/actions/commissions/customCards/newCard.ts @@ -6,6 +6,7 @@ import { type CommissionCustomCardValues, } from "@/schemas/commissionCustomCard"; +// Creates a new custom commission card with options/extras. export async function createCommissionCustomCard( formData: CommissionCustomCardValues ) { diff --git a/src/actions/commissions/customCards/updateCard.ts b/src/actions/commissions/customCards/updateCard.ts index 9b2a817..539c768 100644 --- a/src/actions/commissions/customCards/updateCard.ts +++ b/src/actions/commissions/customCards/updateCard.ts @@ -6,6 +6,7 @@ import { type CommissionCustomCardValues, } from "@/schemas/commissionCustomCard"; +// Updates a custom commission card and resets related options/extras. export async function updateCommissionCustomCard( id: string, rawData: CommissionCustomCardValues diff --git a/src/actions/commissions/customCards/updateSortOrder.ts b/src/actions/commissions/customCards/updateSortOrder.ts index 6d3e752..cea0e60 100644 --- a/src/actions/commissions/customCards/updateSortOrder.ts +++ b/src/actions/commissions/customCards/updateSortOrder.ts @@ -2,6 +2,7 @@ import { prisma } from "@/lib/prisma"; +// Updates sort order for custom commission cards. export async function updateCommissionCustomCardSortOrder( items: { id: string; sortIndex: number }[] ) { diff --git a/src/actions/commissions/examples.ts b/src/actions/commissions/examples.ts index 4089633..d0fd4f0 100644 --- a/src/actions/commissions/examples.ts +++ b/src/actions/commissions/examples.ts @@ -1,6 +1,7 @@ "use server"; import { s3 } from "@/lib/s3"; +import type { CommissionExampleItem } from "@/types/commissions"; import { DeleteObjectCommand, ListObjectsV2Command, @@ -9,13 +10,6 @@ import { const PREFIX = "commissions/examples/"; -export type CommissionExampleItem = { - key: string; - url: string; - size: number | null; - lastModified: string | null; -}; - function buildImageUrl(key: string) { return `/api/image/${encodeURI(key)}`; } diff --git a/src/actions/commissions/guidelines/getGuidelines.ts b/src/actions/commissions/guidelines/getGuidelines.ts index ad39822..3dc3ce1 100644 --- a/src/actions/commissions/guidelines/getGuidelines.ts +++ b/src/actions/commissions/guidelines/getGuidelines.ts @@ -1,7 +1,8 @@ -'use server'; +"use server"; import { prisma } from "@/lib/prisma"; +// Returns the latest active commission guidelines (markdown + example image). export async function getActiveGuidelines(): Promise<{ markdown: string | null; exampleImageUrl: string | null; diff --git a/src/actions/commissions/guidelines/saveGuidelines.ts b/src/actions/commissions/guidelines/saveGuidelines.ts index d2f63eb..2ac7d50 100644 --- a/src/actions/commissions/guidelines/saveGuidelines.ts +++ b/src/actions/commissions/guidelines/saveGuidelines.ts @@ -1,7 +1,8 @@ -'use server'; +"use server"; import { prisma } from "@/lib/prisma"; +// Deactivates existing guidelines and creates a new active version. export async function saveGuidelines(markdown: string, exampleImageUrl: string | null) { await prisma.commissionGuidelines.updateMany({ where: { isActive: true }, diff --git a/src/actions/commissions/requests/deleteCommissionRequest.ts b/src/actions/commissions/requests/deleteCommissionRequest.ts index 1b605a6..0fe9b5e 100644 --- a/src/actions/commissions/requests/deleteCommissionRequest.ts +++ b/src/actions/commissions/requests/deleteCommissionRequest.ts @@ -1,8 +1,9 @@ "use server"; import { prisma } from "@/lib/prisma"; -import { z } from "zod"; +import { z } from "zod/v4"; +// Deletes a commission request by id. export async function deleteCommissionRequest(id: string) { const parsed = z.string().min(1).parse(id); diff --git a/src/actions/commissions/requests/getCommissionRequestById.ts b/src/actions/commissions/requests/getCommissionRequestById.ts index edd9179..cef9d68 100644 --- a/src/actions/commissions/requests/getCommissionRequestById.ts +++ b/src/actions/commissions/requests/getCommissionRequestById.ts @@ -1,8 +1,9 @@ "use server"; import { prisma } from "@/lib/prisma"; -import { calculatePriceRange, PriceSource } from "@/utils/commissionPricing"; +import { calculatePriceRange, type PriceSource } from "@/utils/commissionPricing"; +// Loads a commission request with related data and computed price estimate. export async function getCommissionRequestById(id: string) { const req = await prisma.commissionRequest.findUnique({ where: { id }, diff --git a/src/actions/commissions/requests/getCommissionRequestsTablePage.ts b/src/actions/commissions/requests/getCommissionRequestsTablePage.ts index 4a35b1e..aabefd5 100644 --- a/src/actions/commissions/requests/getCommissionRequestsTablePage.ts +++ b/src/actions/commissions/requests/getCommissionRequestsTablePage.ts @@ -1,39 +1,24 @@ "use server"; +import type { Prisma } from "@/generated/prisma/client"; import { prisma } from "@/lib/prisma"; -import { - commissionRequestTableRowSchema, - commissionStatusSchema, -} from "@/schemas/commissions/requests"; -import { z } from "zod"; - -export type CursorPagination = { pageIndex: number; pageSize: number }; -export type SortDir = "asc" | "desc"; - -const triStateSchema = z.enum(["any", "true", "false"]); - -const sortingSchema = z.array( - z.object({ - id: z.string(), - desc: z.boolean(), - }) -); - -const filtersSchema = z.object({ - q: z.string().optional(), - email: z.string().optional(), - status: z.union([z.literal("any"), commissionStatusSchema]).default("any"), - hasFiles: triStateSchema.default("any"), -}); +import { commissionRequestTableRowSchema } from "@/schemas/commissions/requests"; +import type { + CommissionRequestsTableFilters, + CommissionRequestsTableSorting, +} from "@/schemas/commissions/requestsTable"; +import type { CursorPagination } from "@/types/pagination"; +import { z } from "zod/v4"; +// Builds a paginated, filtered, and sorted commission-requests table payload for the admin UI. export async function getCommissionRequestsTablePage(input: { pagination: CursorPagination; - sorting: z.infer; - filters: z.infer; + sorting: CommissionRequestsTableSorting; + filters: CommissionRequestsTableFilters; }) { const { pagination, sorting, filters } = input; - const where: any = {}; + const where: Prisma.CommissionRequestWhereInput = {}; if (filters.q) { const q = filters.q.trim(); @@ -60,7 +45,7 @@ export async function getCommissionRequestsTablePage(input: { // sorting const sort = sorting?.[0] ?? { id: "createdAt", desc: true }; - const orderBy: any = + const orderBy: Prisma.CommissionRequestOrderByWithRelationInput = sort.id === "createdAt" ? { createdAt: sort.desc ? "desc" : "asc" } : sort.id === "status" @@ -94,7 +79,7 @@ export async function getCommissionRequestsTablePage(input: { customerName: r.customerName, customerEmail: r.customerEmail, customerSocials: r.customerSocials ?? null, - status: r.status as any, + status: r.status, fileCount: r._count.files, })); diff --git a/src/actions/commissions/requests/setCommissionRequestStatus.ts b/src/actions/commissions/requests/setCommissionRequestStatus.ts index 4e48057..c678808 100644 --- a/src/actions/commissions/requests/setCommissionRequestStatus.ts +++ b/src/actions/commissions/requests/setCommissionRequestStatus.ts @@ -2,8 +2,9 @@ import { prisma } from "@/lib/prisma"; import { commissionStatusSchema } from "@/schemas/commissions/requests"; -import { z } from "zod"; +import { z } from "zod/v4"; +// Sets a commission request status (admin action). export async function setCommissionRequestStatus(input: { id: string; status: z.infer; diff --git a/src/actions/commissions/requests/updateCommissionRequest.ts b/src/actions/commissions/requests/updateCommissionRequest.ts index 54fd3e0..959aa66 100644 --- a/src/actions/commissions/requests/updateCommissionRequest.ts +++ b/src/actions/commissions/requests/updateCommissionRequest.ts @@ -1,20 +1,14 @@ "use server"; import { prisma } from "@/lib/prisma"; -import { commissionStatusSchema } from "@/schemas/commissions/requests"; -import { z } from "zod/v4"; +import { + updateCommissionRequestSchema, +} from "@/schemas/commissions/updateRequest"; +import type { UpdateCommissionRequestInput } from "@/schemas/commissions/updateRequest"; -const updateSchema = z.object({ - id: z.string().min(1), - status: commissionStatusSchema, - customerName: z.string().min(1).max(200), - customerEmail: z.string().email().max(320), - customerSocials: z.string().max(2000).optional().nullable(), - message: z.string().min(1).max(20_000), -}); - -export async function updateCommissionRequest(input: z.infer) { - const data = updateSchema.parse(input); +// Updates editable fields on a commission request. +export async function updateCommissionRequest(input: UpdateCommissionRequestInput) { + const data = updateCommissionRequestSchema.parse(input); await prisma.commissionRequest.update({ where: { id: data.id }, diff --git a/src/actions/commissions/requests/updateCommissionRequestStatus.ts b/src/actions/commissions/requests/updateCommissionRequestStatus.ts index 98d06cc..535cc32 100644 --- a/src/actions/commissions/requests/updateCommissionRequestStatus.ts +++ b/src/actions/commissions/requests/updateCommissionRequestStatus.ts @@ -1,21 +1,18 @@ "use server"; import { revalidatePath } from "next/cache"; -import { z } from "zod"; -import { COMMISSION_STATUSES } from "@/lib/commissions/kanban"; -import { prisma } from "@/lib/prisma"; // adjust to your prisma import -// import { requireAdmin } from "@/lib/auth/requireAdmin"; // recommended if you have it +import { prisma } from "@/lib/prisma"; +import { + updateCommissionRequestStatusSchema, +} from "@/schemas/commissions/updateRequestStatus"; +import type { UpdateCommissionRequestStatusInput } from "@/schemas/commissions/updateRequestStatus"; -const schema = z.object({ - id: z.string().min(1), - status: z.enum(COMMISSION_STATUSES), -}); - -export async function updateCommissionRequestStatus(input: z.infer) { - // await requireAdmin(); // enforce auth/role check here - - const { id, status } = schema.parse(input); +// Updates a commission request status and revalidates the kanban page. +export async function updateCommissionRequestStatus( + input: UpdateCommissionRequestStatusInput +) { + const { id, status } = updateCommissionRequestStatusSchema.parse(input); await prisma.commissionRequest.update({ where: { id }, @@ -23,5 +20,5 @@ export async function updateCommissionRequestStatus(input: z.infer 0) throw new Error("Extra is linked to types."); - console.log("TBD"); - // await prisma.commissionExtra.delete({ where: { id } }); - // revalidatePath(LIST_PATH); + await prisma.commissionExtra.delete({ where: { id } }); + revalidatePath(LIST_PATH); } diff --git a/src/actions/commissions/types/newType.ts b/src/actions/commissions/types/newType.ts index b4d060b..c22a3a9 100644 --- a/src/actions/commissions/types/newType.ts +++ b/src/actions/commissions/types/newType.ts @@ -2,9 +2,11 @@ import { prisma } from "@/lib/prisma"; import { commissionTypeSchema } from "@/schemas/commissionType"; +import type * as z from "zod/v4"; +// Creates a commission option entry. export async function createCommissionOption(data: { name: string }) { - return await prisma.commissionOption.create({ + return prisma.commissionOption.create({ data: { name: data.name, description: "", @@ -12,8 +14,9 @@ export async function createCommissionOption(data: { name: string }) { }); } +// Creates a commission extra entry. export async function createCommissionExtra(data: { name: string }) { - return await prisma.commissionExtra.create({ + return prisma.commissionExtra.create({ data: { name: data.name, description: "", @@ -21,11 +24,12 @@ export async function createCommissionExtra(data: { name: string }) { }); } +// Creates a commission custom input entry. export async function createCommissionCustomInput(data: { name: string; fieldId: string; }) { - return await prisma.commissionCustomInput.create({ + return prisma.commissionCustomInput.create({ data: { name: data.name, fieldId: data.fieldId, @@ -33,7 +37,10 @@ export async function createCommissionCustomInput(data: { }); } -export async function createCommissionType(formData: commissionTypeSchema) { +// Creates a commission type with nested options/extras/custom inputs. +export async function createCommissionType( + formData: z.infer +) { const parsed = commissionTypeSchema.safeParse(formData); if (!parsed.success) { diff --git a/src/actions/commissions/types/options.ts b/src/actions/commissions/types/options.ts index 3577a0b..aec21de 100644 --- a/src/actions/commissions/types/options.ts +++ b/src/actions/commissions/types/options.ts @@ -4,13 +4,9 @@ import { prisma } from "@/lib/prisma"; import { commissionOptionSchema } from "@/schemas/commissionType"; import { revalidatePath } from "next/cache"; -const LIST_PATH = "/commissions/options"; - -function toInt(v: string) { - const n = Number.parseInt(v, 10); - return Number.isFinite(n) ? n : 0; -} +const LIST_PATH = "/commissions/types/options"; +// CRUD helpers for commission options (admin-only pages). export async function createCommissionOption(input: unknown) { const data = commissionOptionSchema.parse(input); const created = await prisma.commissionOption.create({ @@ -37,7 +33,6 @@ export async function updateCommissionOption(id: string, input: unknown) { } export async function deleteCommissionOption(id: string) { - console.log("TBD"); - // await prisma.commissionOption.delete({ where: { id } }); - // revalidatePath(LIST_PATH); + await prisma.commissionOption.delete({ where: { id } }); + revalidatePath(LIST_PATH); } diff --git a/src/actions/commissions/types/updateCommissionTypeSortOrder.ts b/src/actions/commissions/types/updateCommissionTypeSortOrder.ts index bfd4fdd..15001b3 100644 --- a/src/actions/commissions/types/updateCommissionTypeSortOrder.ts +++ b/src/actions/commissions/types/updateCommissionTypeSortOrder.ts @@ -1,7 +1,8 @@ -"use server" +"use server"; import { prisma } from "@/lib/prisma"; +// Updates sort order for commission types. export async function updateCommissionTypeSortOrder( ordered: { id: string; sortIndex: number }[] ) { @@ -10,7 +11,7 @@ export async function updateCommissionTypeSortOrder( where: { id }, data: { sortIndex }, }) - ) + ); - await Promise.all(updates) -} \ No newline at end of file + await Promise.all(updates); +} diff --git a/src/actions/commissions/types/updateType.ts b/src/actions/commissions/types/updateType.ts index 981ca38..0ba4469 100644 --- a/src/actions/commissions/types/updateType.ts +++ b/src/actions/commissions/types/updateType.ts @@ -4,9 +4,10 @@ import { prisma } from "@/lib/prisma"; import { commissionTypeSchema } from "@/schemas/commissionType"; import type * as z from "zod/v4"; +// Updates a commission type and resets related nested records. export async function updateCommissionType( id: string, - rawData: z.infer, + rawData: z.infer ) { const data = commissionTypeSchema.parse(rawData); diff --git a/src/actions/home/getDashboard.ts b/src/actions/home/getDashboard.ts index f025166..b5b4702 100644 --- a/src/actions/home/getDashboard.ts +++ b/src/actions/home/getDashboard.ts @@ -1,12 +1,10 @@ "use server"; import { prisma } from "@/lib/prisma"; +import type { CountRow } from "@/types/dashboard"; -type CountRow = { - [P in K]: string; -} & { _count: { _all: number } }; - -function toCountMapSafe(rows: any[], key: string) { +// Aggregates dashboard stats for admin overview cards and tables. +function toCountMapSafe(rows: Array>, key: K) { const out: Record = {}; for (const r of rows) out[String(r[key])] = Number(r?._count?._all ?? 0); return out; diff --git a/src/actions/tags/createTag.ts b/src/actions/tags/createTag.ts index 6391d21..4953e0a 100644 --- a/src/actions/tags/createTag.ts +++ b/src/actions/tags/createTag.ts @@ -1,18 +1,20 @@ -"use server" +"use server"; -import { prisma } from "@/lib/prisma" -import { TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema" +import { prisma } from "@/lib/prisma"; +import { tagSchema } from "@/schemas/artworks/tagSchema"; +import type { TagFormInput } from "@/schemas/artworks/tagSchema"; +// Creates a tag and related category links/aliases. export async function createTag(formData: TagFormInput) { - const parsed = tagSchema.safeParse(formData) + const parsed = tagSchema.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 parentId = data.parentId ?? null; const tagSlug = data.name.toLowerCase().replace(/\s+/g, "-"); @@ -52,5 +54,5 @@ export async function createTag(formData: TagFormInput) { return tag; }); - return created + return created; } diff --git a/src/actions/tags/deleteTag.ts b/src/actions/tags/deleteTag.ts index 1be1f83..4cc3d28 100644 --- a/src/actions/tags/deleteTag.ts +++ b/src/actions/tags/deleteTag.ts @@ -3,6 +3,7 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; +// Deletes a tag if it has no artwork references or child tags. export async function deleteTag(tagId: string) { const tag = await prisma.tag.findUnique({ where: { id: tagId }, @@ -39,6 +40,6 @@ export async function deleteTag(tagId: string) { }); revalidatePath("/tags"); - + return { success: true }; } diff --git a/src/actions/tags/getTags.ts b/src/actions/tags/getTags.ts index 9ef3316..8dfa10c 100644 --- a/src/actions/tags/getTags.ts +++ b/src/actions/tags/getTags.ts @@ -1,7 +1,8 @@ -"use server" +"use server"; -import { prisma } from "@/lib/prisma" +import { prisma } from "@/lib/prisma"; +// Returns tags ordered by sortIndex. export async function getTags() { - return await prisma.tag.findMany({ orderBy: { sortIndex: "asc" } }) + return prisma.tag.findMany({ orderBy: { sortIndex: "asc" } }); } diff --git a/src/actions/tags/isDescendant.ts b/src/actions/tags/isDescendant.ts index c4d4fa9..9c6f021 100644 --- a/src/actions/tags/isDescendant.ts +++ b/src/actions/tags/isDescendant.ts @@ -1,7 +1,8 @@ -"use server" +"use server"; import { prisma } from "@/lib/prisma"; +// Returns true if possibleAncestorId is a descendant of tagId (cycle check). export async function isDescendant(tagId: string, possibleAncestorId: string): Promise { // Walk upwards across any category hierarchy; if we hit tagId, it's a cycle. const visited = new Set(); diff --git a/src/actions/tags/updateTag.ts b/src/actions/tags/updateTag.ts index 76d7539..fb314f4 100644 --- a/src/actions/tags/updateTag.ts +++ b/src/actions/tags/updateTag.ts @@ -1,18 +1,20 @@ -"use server" +"use server"; -import { prisma } from '@/lib/prisma'; -import { TagFormInput, tagSchema } from '@/schemas/artworks/tagSchema'; -import { isDescendant } from './isDescendant'; +import { prisma } from "@/lib/prisma"; +import { tagSchema } from "@/schemas/artworks/tagSchema"; +import type { TagFormInput } from "@/schemas/artworks/tagSchema"; +import { isDescendant } from "./isDescendant"; +// Updates a tag and its category/alias relationships. export async function updateTag(id: string, rawData: TagFormInput) { - const parsed = tagSchema.safeParse(rawData) + const parsed = tagSchema.safeParse(rawData); 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 parentId = data.parentId ?? null; const tagSlug = data.name.toLowerCase().replace(/\s+/g, "-"); @@ -111,5 +113,5 @@ export async function updateTag(id: string, rawData: TagFormInput) { return tag; }); - return updated + return updated; } diff --git a/src/actions/tos/getTos.ts b/src/actions/tos/getTos.ts index 5c57c4e..2cf88f2 100644 --- a/src/actions/tos/getTos.ts +++ b/src/actions/tos/getTos.ts @@ -1,10 +1,11 @@ -'use server'; +"use server"; import { prisma } from "@/lib/prisma"; +// Returns the most recent Terms of Service markdown. export async function getLatestTos(): Promise { const tos = await prisma.termsOfService.findFirst({ - orderBy: { createdAt: 'desc' }, + orderBy: { createdAt: "desc" }, }); return tos?.markdown ?? null; } diff --git a/src/actions/tos/saveTos.ts b/src/actions/tos/saveTos.ts index 1cb6498..d08d8f0 100644 --- a/src/actions/tos/saveTos.ts +++ b/src/actions/tos/saveTos.ts @@ -1,11 +1,12 @@ -'use server'; +"use server"; import { prisma } from "@/lib/prisma"; +// Saves a new Terms of Service version. export async function saveTosAction(markdown: string) { await prisma.termsOfService.create({ data: { markdown, }, }); -} \ No newline at end of file +} diff --git a/src/actions/uploads/createBulkImages.ts b/src/actions/uploads/createBulkImages.ts index b38e0dd..4cfe345 100644 --- a/src/actions/uploads/createBulkImages.ts +++ b/src/actions/uploads/createBulkImages.ts @@ -1,9 +1,9 @@ +"use server"; + import { createImageFromFile } from "./createImageFromFile"; +import type { BulkResult } from "@/types/uploads"; -type BulkResult = - | { ok: true; artworkId: string; name: string } - | { ok: false; name: string; error: string }; - +// Bulk image upload server action used by the admin UI. export async function createImagesBulk(formData: FormData): Promise { const entries = formData.getAll("file"); const files = entries.filter((x): x is File => x instanceof File); diff --git a/src/actions/uploads/createImage.ts b/src/actions/uploads/createImage.ts index 862c07c..ce24fa6 100644 --- a/src/actions/uploads/createImage.ts +++ b/src/actions/uploads/createImage.ts @@ -1,203 +1,12 @@ -"use server" +"use server"; -import { fileUploadSchema } from "@/schemas/artworks/imageSchema"; +import type { fileUploadSchema } from "@/schemas/artworks/imageSchema"; import "dotenv/config"; -import { z } from "zod/v4"; +import type { z } from "zod/v4"; import { createImageFromFile } from "./createImageFromFile"; +// Creates a single artwork image using the shared upload pipeline. export async function createImage(values: z.infer) { const imageFile = values.file[0]; return createImageFromFile(imageFile, { colorMode: "inline" }); } - -/* -export async function createImage(values: z.infer) { - const imageFile = values.file[0]; - - if (!(imageFile instanceof File)) { - console.log("No image or invalid type"); - return null; - } - - const fileName = imageFile.name; - const fileType = imageFile.type; - const fileSize = imageFile.size; - const lastModified = new Date(imageFile.lastModified); - - const fileKey = uuidv4(); - - const arrayBuffer = await imageFile.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - const realFileType = fileType.split("/")[1]; - const originalKey = `original/${fileKey}.${realFileType}`; - const modifiedKey = `modified/${fileKey}.webp`; - const resizedKey = `resized/${fileKey}.webp`; - const thumbnailKey = `thumbnail/${fileKey}.webp`; - - const sharpData = sharp(buffer); - const metadata = await sharpData.metadata(); - - //--- Original file - await s3.send( - new PutObjectCommand({ - Bucket: `${process.env.BUCKET_NAME}`, - Key: originalKey, - Body: buffer, - ContentType: "image/" + metadata.format, - }) - ); - //--- Modified file - const modifiedBuffer = await sharp(buffer) - .toFormat('webp') - .toBuffer() - const modifiedMetadata = await sharp(modifiedBuffer).metadata(); - await s3.send( - new PutObjectCommand({ - Bucket: `${process.env.BUCKET_NAME}`, - Key: modifiedKey, - Body: modifiedBuffer, - ContentType: "image/" + modifiedMetadata.format, - }) - ); - //--- Resized file - const { width, height } = modifiedMetadata; - const targetSize = 400; - let resizeOptions; - if (width && height) { - if (height < width) { - resizeOptions = { height: targetSize }; - } else { - resizeOptions = { width: targetSize }; - } - } else { - resizeOptions = { height: targetSize }; - } - const resizedBuffer = await sharp(modifiedBuffer) - .resize({ ...resizeOptions, withoutEnlargement: true }) - .toFormat('webp') - .toBuffer(); - const resizedMetadata = await sharp(resizedBuffer).metadata(); - await s3.send( - new PutObjectCommand({ - Bucket: `${process.env.BUCKET_NAME}`, - Key: resizedKey, - Body: resizedBuffer, - ContentType: "image/" + resizedMetadata.format, - }) - ); - //--- Thumbnail file - const thumbnailTargetSize = 160; - let thumbnailOptions; - if (width && height) { - if (height < width) { - thumbnailOptions = { height: thumbnailTargetSize }; - } else { - thumbnailOptions = { width: thumbnailTargetSize }; - } - } else { - thumbnailOptions = { height: thumbnailTargetSize }; - } - const thumbnailBuffer = await sharp(modifiedBuffer) - .resize({ ...thumbnailOptions, withoutEnlargement: true }) - .toFormat('webp') - .toBuffer(); - const thumbnailMetadata = await sharp(thumbnailBuffer).metadata(); - await s3.send( - new PutObjectCommand({ - Bucket: `${process.env.BUCKET_NAME}`, - Key: thumbnailKey, - Body: thumbnailBuffer, - ContentType: "image/" + thumbnailMetadata.format, - }) - ); - - const fileRecord = await prisma.fileData.create({ - data: { - name: fileName, - fileKey, - originalFile: fileName, - uploadDate: lastModified, - fileType: realFileType, - fileSize: fileSize, - }, - }); - - const artworkSlug = fileName.toLowerCase().replace(/\s+/g, "-"); - const artworkRecord = await prisma.artwork.create({ - data: { - name: fileName, - slug: artworkSlug, - creationDate: lastModified, - fileId: fileRecord.id, - }, - }); - - await prisma.artworkMetadata.create({ - data: { - artworkId: artworkRecord.id, - format: metadata.format || "unknown", - width: metadata.width || 0, - height: metadata.height || 0, - space: metadata.space || "unknown", - channels: metadata.channels || 0, - depth: metadata.depth || "unknown", - density: metadata.density ?? undefined, - bitsPerSample: metadata.bitsPerSample ?? undefined, - isProgressive: metadata.isProgressive ?? undefined, - isPalette: metadata.isPalette ?? undefined, - hasProfile: metadata.hasProfile ?? undefined, - hasAlpha: metadata.hasAlpha ?? undefined, - autoOrientW: metadata.autoOrient?.width ?? undefined, - autoOrientH: metadata.autoOrient?.height ?? undefined, - }, - }); - - await prisma.fileVariant.createMany({ - data: [ - { - s3Key: originalKey, - type: "original", - height: metadata.height, - width: metadata.width, - fileExtension: metadata.format, - mimeType: "image/" + metadata.format, - sizeBytes: metadata.size, - artworkId: artworkRecord.id - }, - { - s3Key: modifiedKey, - type: "modified", - height: modifiedMetadata.height, - width: modifiedMetadata.width, - fileExtension: modifiedMetadata.format, - mimeType: "image/" + modifiedMetadata.format, - sizeBytes: modifiedMetadata.size, - artworkId: artworkRecord.id - }, - { - s3Key: resizedKey, - type: "resized", - height: resizedMetadata.height, - width: resizedMetadata.width, - fileExtension: resizedMetadata.format, - mimeType: "image/" + resizedMetadata.format, - sizeBytes: resizedMetadata.size, - artworkId: artworkRecord.id - }, - { - s3Key: thumbnailKey, - type: "thumbnail", - height: thumbnailMetadata.height, - width: thumbnailMetadata.width, - fileExtension: thumbnailMetadata.format, - mimeType: "image/" + thumbnailMetadata.format, - sizeBytes: thumbnailMetadata.size, - artworkId: artworkRecord.id - } - ], - }); - - return artworkRecord -} - */ \ No newline at end of file diff --git a/src/actions/uploads/createImageFromFile.ts b/src/actions/uploads/createImageFromFile.ts index 4843bdb..2408be3 100644 --- a/src/actions/uploads/createImageFromFile.ts +++ b/src/actions/uploads/createImageFromFile.ts @@ -8,12 +8,12 @@ import sharp from "sharp"; import { v4 as uuidv4 } from "uuid"; import { generateArtworkColorsForArtwork } from "../artworks/generateArtworkColors"; +// Upload pipeline that generates variants and metadata, then creates artwork records. export async function createImageFromFile( imageFile: File, opts?: { originalName?: string; colorMode?: "inline" | "defer" | "off" }, ) { if (!(imageFile instanceof File)) { - console.log("No image or invalid type"); return null; } diff --git a/src/actions/users/createUser.ts b/src/actions/users/createUser.ts index af1584a..75c5f77 100644 --- a/src/actions/users/createUser.ts +++ b/src/actions/users/createUser.ts @@ -2,24 +2,20 @@ import { auth } from "@/lib/auth"; import { headers } from "next/headers"; -import { z } from "zod/v4"; +import type { SessionWithRole } from "@/types/auth"; +import { createUserSchema } from "@/schemas/users"; +import type { CreateUserInput } from "@/schemas/users"; -const schema = z.object({ - name: z.string().min(1).max(200), - email: z.string().email().max(320), - password: z.string().min(8).max(128), - role: z.enum(["user", "admin"]).default("user"), -}); - -export async function createUser(input: z.infer) { +// Creates a new user account (admin-only). +export async function createUser(input: CreateUserInput) { const session = await auth.api.getSession({ headers: await headers() }); - const role = (session as any)?.user?.role; + const role = (session as SessionWithRole)?.user?.role; if (!session || role !== "admin") { throw new Error("Forbidden"); } - const data = schema.parse(input); + const data = createUserSchema.parse(input); return auth.api.createUser({ body: { diff --git a/src/actions/users/deleteUser.ts b/src/actions/users/deleteUser.ts index 7d348d1..29111fc 100644 --- a/src/actions/users/deleteUser.ts +++ b/src/actions/users/deleteUser.ts @@ -4,13 +4,15 @@ import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { headers } from "next/headers"; import { z } from "zod/v4"; +import type { SessionWithRole } from "@/types/auth"; +// Deletes a user account with safety checks (admin-only, cannot delete self or last admin). export async function deleteUser(id: string) { const userId = z.string().min(1).parse(id); const session = await auth.api.getSession({ headers: await headers() }); - const role = (session as any)?.user?.role as string | undefined; - const currentUserId = (session as any)?.user?.id as string | undefined; + const role = (session as SessionWithRole)?.user?.role; + const currentUserId = (session as SessionWithRole)?.user?.id; if (!session || role !== "admin") throw new Error("Forbidden"); if (!currentUserId) throw new Error("Session missing user id"); @@ -40,5 +42,5 @@ async function await_attachTarget(userId: string) { select: { id: true, role: true }, }); if (!target) throw new Error("User not found."); - return target as { id: string; role: "admin" | "user" }; + return target; } diff --git a/src/actions/users/getUsers.ts b/src/actions/users/getUsers.ts index b99ac60..c88c29e 100644 --- a/src/actions/users/getUsers.ts +++ b/src/actions/users/getUsers.ts @@ -3,20 +3,13 @@ import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { headers } from "next/headers"; +import type { SessionWithRole } from "@/types/auth"; +import type { UsersListRow } from "@/types/users"; -export type UsersListRow = { - id: string; - name: string | null; - email: string; - role: "admin" | "user"; - emailVerified: boolean; - createdAt: Date; - updatedAt: Date; -}; - +// Returns all users for the admin users table. export async function getUsers(): Promise { const session = await auth.api.getSession({ headers: await headers() }); - const role = (session as any)?.user?.role as string | undefined; + const role = (session as SessionWithRole)?.user?.role; if (!session || role !== "admin") { throw new Error("Forbidden"); @@ -35,5 +28,9 @@ export async function getUsers(): Promise { }, }); - return rows as UsersListRow[]; + return rows.map((r) => ({ + ...r, + createdAt: r.createdAt.toISOString(), + updatedAt: r.updatedAt.toISOString(), + })); } diff --git a/src/actions/users/resendVerification.ts b/src/actions/users/resendVerification.ts index fa5934d..108aea8 100644 --- a/src/actions/users/resendVerification.ts +++ b/src/actions/users/resendVerification.ts @@ -2,21 +2,20 @@ import { auth } from "@/lib/auth"; import { headers } from "next/headers"; -import { z } from "zod/v4"; +import type { SessionWithRole } from "@/types/auth"; +import { resendVerificationSchema } from "@/schemas/users"; +import type { ResendVerificationInput } from "@/schemas/users"; -const schema = z.object({ - email: z.string().email(), -}); - -export async function resendVerification(input: z.infer) { +// Resends a verification email for a user (admin-only). +export async function resendVerification(input: ResendVerificationInput) { const session = await auth.api.getSession({ headers: await headers() }); - const role = (session as any)?.user?.role as string | undefined; + const role = (session as SessionWithRole)?.user?.role; if (!session || role !== "admin") throw new Error("Forbidden"); - const { email } = schema.parse(input); + const { email } = resendVerificationSchema.parse(input); // Uses the public auth route (same origin) - const res = await fetch("http://localhost/api/auth/send-verification-email", { + const res = await fetch(`${process.env.BETTER_AUTH_URL}/api/auth/send-verification-email`, { // NOTE: In production, you should use an absolute URL from env, or use authClient. // This is kept minimal; if you want, I'll refactor to authClient to avoid hostname concerns. method: "POST", diff --git a/src/app/(admin)/artworks/[id]/page.tsx b/src/app/(admin)/artworks/[id]/page.tsx index ccb8f90..c9d5ebd 100644 --- a/src/app/(admin)/artworks/[id]/page.tsx +++ b/src/app/(admin)/artworks/[id]/page.tsx @@ -8,6 +8,7 @@ import ArtworkVariants from "@/components/artworks/single/ArtworkVariants"; import DeleteArtworkButton from "@/components/artworks/single/DeleteArtworkButton"; import EditArtworkForm from "@/components/artworks/single/EditArtworkForm"; +// Single artwork edit page. export default async function ArtworkSinglePage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; @@ -16,30 +17,30 @@ export default async function ArtworkSinglePage({ params }: { params: Promise<{ const categories = await getCategoriesWithTags(); const tags = await getTags(); - if (!item) return
Artwork with this id not found
+ if (!item) return
Artwork with this id not found
; return (

Edit artwork

- {item ? : 'Artwork not found...'} +
- {item && } +
- {item && } +
- {item && } +
- {item && } +
- {item && } +
diff --git a/src/app/(admin)/artworks/page.tsx b/src/app/(admin)/artworks/page.tsx index 4bb7f03..c16b016 100644 --- a/src/app/(admin)/artworks/page.tsx +++ b/src/app/(admin)/artworks/page.tsx @@ -1,6 +1,7 @@ import { ArtworkColorProcessor } from "@/components/artworks/ArtworkColorProcessor"; import { ArtworksTable } from "@/components/artworks/ArtworksTable"; +// Admin artworks list page. export default async function ArtworksPage() { return (
diff --git a/src/app/(admin)/categories/[id]/page.tsx b/src/app/(admin)/categories/[id]/page.tsx index 3146115..d02d7aa 100644 --- a/src/app/(admin)/categories/[id]/page.tsx +++ b/src/app/(admin)/categories/[id]/page.tsx @@ -1,13 +1,14 @@ import EditCategoryForm from "@/components/categories/EditCategoryForm"; import { prisma } from "@/lib/prisma"; +// Edit category page. export default async function PortfolioCategoriesEditPage({ params }: { params: { id: string } }) { const { id } = await params; const category = await prisma.artCategory.findUnique({ where: { id, - } - }) + }, + }); return (
@@ -15,4 +16,4 @@ export default async function PortfolioCategoriesEditPage({ params }: { params: {category && }
); -} \ No newline at end of file +} diff --git a/src/app/(admin)/categories/new/page.tsx b/src/app/(admin)/categories/new/page.tsx index 170e0c7..35a9095 100644 --- a/src/app/(admin)/categories/new/page.tsx +++ b/src/app/(admin)/categories/new/page.tsx @@ -1,5 +1,6 @@ import NewCategoryForm from "@/components/categories/NewCategoryForm"; +// Create a new category page. export default function PortfolioCategoriesNewPage() { return (
@@ -7,4 +8,4 @@ export default function PortfolioCategoriesNewPage() {
); -} \ No newline at end of file +} diff --git a/src/app/(admin)/categories/page.tsx b/src/app/(admin)/categories/page.tsx index d532137..5ca6d1b 100644 --- a/src/app/(admin)/categories/page.tsx +++ b/src/app/(admin)/categories/page.tsx @@ -4,6 +4,7 @@ import { PlusCircleIcon } from "lucide-react"; import Link from "next/link"; import { Suspense } from "react"; +// Admin categories management page. export default async function CategoriesPage() { const items = await getCategoriesWithCount(); @@ -11,7 +12,10 @@ export default async function CategoriesPage() {

Art Categories

- + Add new category
@@ -24,4 +28,4 @@ export default async function CategoriesPage() {
); -} \ No newline at end of file +} diff --git a/src/app/(admin)/commissions/custom-cards/[id]/page.tsx b/src/app/(admin)/commissions/custom-cards/[id]/page.tsx index f42d37b..e3c5285 100644 --- a/src/app/(admin)/commissions/custom-cards/[id]/page.tsx +++ b/src/app/(admin)/commissions/custom-cards/[id]/page.tsx @@ -1,8 +1,8 @@ -import { listCommissionCustomCardImages } from "@/actions/commissions/customCards/images"; import EditCustomCardForm from "@/components/commissions/customCards/EditCustomCardForm"; import { prisma } from "@/lib/prisma"; import { notFound } from "next/navigation"; +// Edit custom commission card page. export default async function CommissionCustomCardEditPage({ params, }: { @@ -10,7 +10,7 @@ export default async function CommissionCustomCardEditPage({ }) { const { id } = await params; - const [card, options, extras, images, tags] = await Promise.all([ + const [card, options, extras, tags] = await Promise.all([ prisma.commissionCustomCard.findUnique({ where: { id }, include: { @@ -21,7 +21,6 @@ export default async function CommissionCustomCardEditPage({ }), 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" }] }), ]); @@ -38,7 +37,6 @@ export default async function CommissionCustomCardEditPage({ card={card} 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 39ec588..a0b7a8f 100644 --- a/src/app/(admin)/commissions/custom-cards/new/page.tsx +++ b/src/app/(admin)/commissions/custom-cards/new/page.tsx @@ -1,12 +1,11 @@ -import { listCommissionCustomCardImages } from "@/actions/commissions/customCards/images"; import NewCustomCardForm from "@/components/commissions/customCards/NewCustomCardForm"; import { prisma } from "@/lib/prisma"; +// New custom commission card page. export default async function CommissionCustomCardsNewPage() { - const [options, extras, images, tags] = await Promise.all([ + const [options, extras, 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" }] }), ]); @@ -15,7 +14,7 @@ export default async function CommissionCustomCardsNewPage() {

New Custom Commission Card

- +
); } diff --git a/src/app/(admin)/commissions/custom-cards/page.tsx b/src/app/(admin)/commissions/custom-cards/page.tsx index 85c7160..84a9263 100644 --- a/src/app/(admin)/commissions/custom-cards/page.tsx +++ b/src/app/(admin)/commissions/custom-cards/page.tsx @@ -3,6 +3,7 @@ import { prisma } from "@/lib/prisma"; import { PlusCircleIcon } from "lucide-react"; import Link from "next/link"; +// Custom commission cards list page. export default async function CommissionCustomCardsPage() { const cards = await prisma.commissionCustomCard.findMany({ include: { diff --git a/src/app/(admin)/commissions/guidelines/page.tsx b/src/app/(admin)/commissions/guidelines/page.tsx index 61d7de4..1753e5d 100644 --- a/src/app/(admin)/commissions/guidelines/page.tsx +++ b/src/app/(admin)/commissions/guidelines/page.tsx @@ -2,6 +2,7 @@ import { listCommissionExamples } from "@/actions/commissions/examples"; import { getActiveGuidelines } from "@/actions/commissions/guidelines/getGuidelines"; import GuidelinesEditor from "@/components/commissions/guidelines/Editor"; +// Admin page for editing commission guidelines. export default async function CommissionGuidelinesPage() { const [{ markdown, exampleImageUrl }, examples] = await Promise.all([ getActiveGuidelines(), diff --git a/src/app/(admin)/commissions/kanban/page.tsx b/src/app/(admin)/commissions/kanban/page.tsx index 16fee62..4143732 100644 --- a/src/app/(admin)/commissions/kanban/page.tsx +++ b/src/app/(admin)/commissions/kanban/page.tsx @@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma"; import type { BoardItem, ColumnsState } from "@/types/Board"; +// Admin kanban page for commission requests. export default async function CommissionsBoardPage() { const requests = await prisma.commissionRequest.findMany({ where: { diff --git a/src/app/(admin)/commissions/requests/[id]/page.tsx b/src/app/(admin)/commissions/requests/[id]/page.tsx index 90247b9..02aa87a 100644 --- a/src/app/(admin)/commissions/requests/[id]/page.tsx +++ b/src/app/(admin)/commissions/requests/[id]/page.tsx @@ -2,6 +2,7 @@ import { getCommissionRequestById } from "@/actions/commissions/requests/getComm import { CommissionRequestEditor } from "@/components/commissions/requests/CommissionRequestEditor"; import { notFound } from "next/navigation"; +// Admin page for editing a single commission request. export default async function CommissionRequestPage({ params, }: { @@ -20,7 +21,7 @@ export default async function CommissionRequestPage({ Submitted: {new Date(request.createdAt).toLocaleString()} · ID: {request.id}

- + ); diff --git a/src/app/(admin)/commissions/requests/page.tsx b/src/app/(admin)/commissions/requests/page.tsx index e85441f..15eb697 100644 --- a/src/app/(admin)/commissions/requests/page.tsx +++ b/src/app/(admin)/commissions/requests/page.tsx @@ -1,6 +1,7 @@ import RequestsTable from "@/components/commissions/requests/RequestsTable"; import { prisma } from "@/lib/prisma"; +// Server-rendered commissions list page. export default async function CommissionPage() { const items = await prisma.commissionRequest.findMany({ include: { @@ -15,11 +16,11 @@ export default async function CommissionPage() {

Commission Requests

- List of all incomming requests via website. + List of all incoming requests via website.

); -} \ No newline at end of file +} diff --git a/src/app/(admin)/commissions/types/[id]/page.tsx b/src/app/(admin)/commissions/types/[id]/page.tsx index b950c15..30f7340 100644 --- a/src/app/(admin)/commissions/types/[id]/page.tsx +++ b/src/app/(admin)/commissions/types/[id]/page.tsx @@ -1,6 +1,7 @@ import EditTypeForm from "@/components/commissions/types/EditTypeForm"; import { prisma } from "@/lib/prisma"; +// Edit commission type page. export default async function CommissionTypesEditPage({ params }: { params: { id: string } }) { const { id } = await params; const commissionType = await prisma.commissionType.findUnique({ @@ -13,7 +14,7 @@ export default async function CommissionTypesEditPage({ params }: { params: { id customInputs: { include: { customInput: true }, orderBy: { sortIndex: "asc" } }, tags: true, }, - }) + }); const tags = await prisma.tag.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }], }); @@ -22,13 +23,10 @@ export default async function CommissionTypesEditPage({ params }: { params: { id }); const extras = await prisma.commissionExtra.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }], - }) - // const customInputs = await prisma.commissionCustomInput.findMany({ - // orderBy: [{ sortIndex: "asc" }, { name: "asc" }], - // }) + }); if (!commissionType) { - return
Type not found
+ return
Type not found
; } return ( diff --git a/src/app/(admin)/commissions/types/extras/page.tsx b/src/app/(admin)/commissions/types/extras/page.tsx index 96ab852..07c3bc5 100644 --- a/src/app/(admin)/commissions/types/extras/page.tsx +++ b/src/app/(admin)/commissions/types/extras/page.tsx @@ -1,10 +1,11 @@ import { ExtraListClient } from "@/components/commissions/extras/ExtraListClient"; import { prisma } from "@/lib/prisma"; +// Admin page for managing commission extras. export default async function CommissionTypesExtrasPage() { const extras = await prisma.commissionExtra.findMany({ orderBy: [{ createdAt: "asc" }, { name: "asc" }], }); return ; -} \ 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 7d8680e..5fd0120 100644 --- a/src/app/(admin)/commissions/types/new/page.tsx +++ b/src/app/(admin)/commissions/types/new/page.tsx @@ -1,6 +1,7 @@ import NewTypeForm from "@/components/commissions/types/NewTypeForm"; import { prisma } from "@/lib/prisma"; +// Create new commission type page. export default async function CommissionTypesNewPage() { const tags = await prisma.tag.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }], @@ -10,10 +11,10 @@ export default async function CommissionTypesNewPage() { }); const extras = await prisma.commissionExtra.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }], - }) + }); const customInputs = await prisma.commissionCustomInput.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }], - }) + }); return (
@@ -27,6 +28,5 @@ export default async function CommissionTypesNewPage() { tags={tags} />
- ); } diff --git a/src/app/(admin)/commissions/types/options/page.tsx b/src/app/(admin)/commissions/types/options/page.tsx index ec03809..35ce786 100644 --- a/src/app/(admin)/commissions/types/options/page.tsx +++ b/src/app/(admin)/commissions/types/options/page.tsx @@ -1,10 +1,11 @@ import { OptionsListClient } from "@/components/commissions/options/OptionsListClient"; import { prisma } from "@/lib/prisma"; +// Admin page for managing commission options. export default async function CommissionTypesOptionsPage() { const options = await prisma.commissionOption.findMany({ orderBy: [{ createdAt: "asc" }, { name: "asc" }], }); return ; -} \ 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 a10c968..4791a96 100644 --- a/src/app/(admin)/commissions/types/page.tsx +++ b/src/app/(admin)/commissions/types/page.tsx @@ -3,6 +3,7 @@ import { prisma } from "@/lib/prisma"; import { PlusCircleIcon } from "lucide-react"; import Link from "next/link"; +// Commission types list page. export default async function CommissionTypesPage() { const types = await prisma.commissionType.findMany({ include: { @@ -17,11 +18,18 @@ export default async function CommissionTypesPage() {

Commission Types

- + Add new Type
- {types && types.length > 0 ? :

No types found.

} + {types && types.length > 0 ? ( + + ) : ( +

No types found.

+ )}
); } diff --git a/src/app/(admin)/layout.tsx b/src/app/(admin)/layout.tsx index 5ef506e..4c9ca8c 100644 --- a/src/app/(admin)/layout.tsx +++ b/src/app/(admin)/layout.tsx @@ -3,25 +3,15 @@ import Footer from "@/components/global/Footer"; import MobileSidebar from "@/components/global/MobileSidebar"; import ModeToggle from "@/components/global/ModeToggle"; import Sidebar from "@/components/global/Sidebar"; +import type { ReactNode } from "react"; +// Main admin layout with sidebar, header actions, and footer. export default function AdminLayout({ children, }: Readonly<{ - children: React.ReactNode; + children: ReactNode; }>) { return ( - //
- //
- //
- //
- //
- // {children} - //
- //
- //
- //
- // - //
- {/* */} @@ -80,48 +70,6 @@ export default async function HomePage() {
- {/* Artwork status */} - {/* - - Artwork status - - - - - - - - */} - - {/* Color pipeline */} - {/* - - Color pipeline - - - - - - -
- Tip: keep “Failed” near zero—those typically need a re-run or file - fix. -
-
-
*/} - {/* Commissions status */} @@ -153,84 +101,6 @@ export default async function HomePage() {
- - {/* Recent activity */} - {/*
- - - Recent artworks - - - - {data.artworks.recent.length === 0 ? ( -
No artworks yet.
- ) : ( -
    - {data.artworks.recent.map((a) => ( -
  • -
    -
    {a.name}
    -
    - {fmtDate(a.createdAt)} · {a.colorStatus} - {a.published ? " · published" : " · draft"} - {a.needsWork ? " · needs work" : ""} -
    -
    - -
  • - ))} -
- )} -
-
- - - - Recent commission requests - - - - {data.commissions.recent.length === 0 ? ( -
- No commission requests yet. -
- ) : ( -
    - {data.commissions.recent.map((r) => ( -
  • -
    -
    - {r.customerName}{" "} - - ({r.customerEmail}) - -
    -
    - {fmtDate(r.createdAt)} · {r.status} -
    -
    - -
  • - ))} -
- )} -
-
-
*/}
); } diff --git a/src/app/(admin)/tags/[id]/page.tsx b/src/app/(admin)/tags/[id]/page.tsx index 76ed05b..ca3a1fa 100644 --- a/src/app/(admin)/tags/[id]/page.tsx +++ b/src/app/(admin)/tags/[id]/page.tsx @@ -1,6 +1,7 @@ import EditTagForm from "@/components/tags/EditTagForm"; import { prisma } from "@/lib/prisma"; +// Edit tag page. export default async function PortfolioTagsEditPage({ params }: { params: { id: string } }) { const { id } = await params; const tag = await prisma.tag.findUnique({ @@ -15,8 +16,8 @@ export default async function PortfolioTagsEditPage({ params }: { params: { id: }, }, aliases: true - } - }) + }, + }); const categories = await prisma.artCategory.findMany({ include: { tagLinks: true }, diff --git a/src/app/(admin)/tags/new/page.tsx b/src/app/(admin)/tags/new/page.tsx index 3cd69ca..a60dae5 100644 --- a/src/app/(admin)/tags/new/page.tsx +++ b/src/app/(admin)/tags/new/page.tsx @@ -1,6 +1,7 @@ import NewTagForm from "@/components/tags/NewTagForm"; import { prisma } from "@/lib/prisma"; +// Create a new tag page. export default async function PortfolioTagsNewPage() { const categories = await prisma.artCategory.findMany({ include: { tagLinks: true }, diff --git a/src/app/(admin)/tags/page.tsx b/src/app/(admin)/tags/page.tsx index 57ef92c..f1dffaf 100644 --- a/src/app/(admin)/tags/page.tsx +++ b/src/app/(admin)/tags/page.tsx @@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma"; import { PlusCircleIcon } from "lucide-react"; import Link from "next/link"; +// Admin tags management page. export default async function ArtTagsPage() { const items = await prisma.tag.findMany({ include: { diff --git a/src/app/(admin)/tos/page.tsx b/src/app/(admin)/tos/page.tsx index f6249ce..7cdc4b9 100644 --- a/src/app/(admin)/tos/page.tsx +++ b/src/app/(admin)/tos/page.tsx @@ -1,6 +1,7 @@ import { getLatestTos } from "@/actions/tos/getTos"; import TosEditor from "@/components/tos/Editor"; +// Admin page for editing Terms of Service. export default async function TosPage() { const markdown = await getLatestTos(); @@ -14,4 +15,4 @@ export default async function TosPage() {
); -} \ No newline at end of file +} diff --git a/src/app/(admin)/uploads/bulk/page.tsx b/src/app/(admin)/uploads/bulk/page.tsx index ce7c2dc..d0da478 100644 --- a/src/app/(admin)/uploads/bulk/page.tsx +++ b/src/app/(admin)/uploads/bulk/page.tsx @@ -1,7 +1,10 @@ import UploadBulkImageForm from "@/components/uploads/UploadBulkImageForm"; +// Bulk image upload page. export default function UploadsBulkPage() { return ( -
+
+ +
); -} \ No newline at end of file +} diff --git a/src/app/(admin)/uploads/single/page.tsx b/src/app/(admin)/uploads/single/page.tsx index 66cad12..6d34955 100644 --- a/src/app/(admin)/uploads/single/page.tsx +++ b/src/app/(admin)/uploads/single/page.tsx @@ -1,7 +1,10 @@ import UploadImageForm from "@/components/uploads/UploadImageForm"; +// Single image upload page. export default function UploadsSinglePage() { return ( -
+
+ +
); -} \ No newline at end of file +} diff --git a/src/app/(admin)/users/new/page.tsx b/src/app/(admin)/users/new/page.tsx index 3dc05b1..03f270b 100644 --- a/src/app/(admin)/users/new/page.tsx +++ b/src/app/(admin)/users/new/page.tsx @@ -1,11 +1,13 @@ import { CreateUserForm } from "@/components/users/CreateUserForm"; import { auth } from "@/lib/auth"; +import type { SessionWithRole } from "@/types/auth"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; +// Admin-only user creation page. export default async function NewUserPage() { const session = await auth.api.getSession({ headers: await headers() }); - const role = (session as any)?.user?.role; + const role = (session as SessionWithRole)?.user?.role; if (!session) redirect("/login"); if (role !== "admin") redirect("/"); diff --git a/src/app/(admin)/users/page.tsx b/src/app/(admin)/users/page.tsx index bd8ded1..1dafa81 100644 --- a/src/app/(admin)/users/page.tsx +++ b/src/app/(admin)/users/page.tsx @@ -1,11 +1,13 @@ import { UsersTable } from "@/components/users/UsersTable"; import { auth } from "@/lib/auth"; +import type { SessionWithRole } from "@/types/auth"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; +// Admin users list page. export default async function UsersPage() { const session = await auth.api.getSession({ headers: await headers() }); - const role = (session as any)?.user?.role as string | undefined; + const role = (session as SessionWithRole)?.user?.role; if (!session) redirect("/login"); if (role !== "admin") redirect("/"); diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx index 5ec8397..6c24df9 100644 --- a/src/app/(auth)/forgot-password/page.tsx +++ b/src/app/(auth)/forgot-password/page.tsx @@ -1,5 +1,6 @@ import { ForgotPasswordForm } from "@/components/auth/ForgotPasswordForm"; +// Forgot password page. export default function ForgotPasswordPage() { return (
diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index 71d5910..2e54f43 100644 --- a/src/app/(auth)/layout.tsx +++ b/src/app/(auth)/layout.tsx @@ -1,8 +1,10 @@ +import type { ReactNode } from "react"; +// Layout wrapper for auth routes. export default function AuthLayout({ children, }: Readonly<{ - children: React.ReactNode; + children: ReactNode; }>) { return (
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index e82d91e..8ddc477 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -1,6 +1,7 @@ import LoginForm from "@/components/auth/LoginForm"; import { Suspense } from "react"; +// Admin login page. export default function LoginPage() { return (
@@ -20,4 +21,4 @@ export default function LoginPage() {
); -} \ No newline at end of file +} diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx index d088d19..e164ffc 100644 --- a/src/app/(auth)/register/page.tsx +++ b/src/app/(auth)/register/page.tsx @@ -2,6 +2,7 @@ import { RegisterForm } from "@/components/auth/RegisterForm"; import { prisma } from "@/lib/prisma"; import { redirect } from "next/navigation"; +// One-time admin registration page (only when no users exist). export default async function RegisterPage() { const count = await prisma.user.count(); if (count !== 0) redirect("/login"); diff --git a/src/app/(auth)/reset-password/page.tsx b/src/app/(auth)/reset-password/page.tsx index 25cd205..2c20eb6 100644 --- a/src/app/(auth)/reset-password/page.tsx +++ b/src/app/(auth)/reset-password/page.tsx @@ -1,6 +1,7 @@ import { ResetPasswordForm } from "@/components/auth/ResetPasswordForm"; import Link from "next/link"; +// Reset password page, expects a token query param. export default async function ResetPasswordPage({ searchParams, }: { @@ -13,7 +14,7 @@ export default async function ResetPasswordPage({

No valid token, please try again or get back to Home

- ) + ); } return ( diff --git a/src/app/api/artworks/page/route.ts b/src/app/api/artworks/page/route.ts index 1742463..b5e0b19 100644 --- a/src/app/api/artworks/page/route.ts +++ b/src/app/api/artworks/page/route.ts @@ -1,6 +1,7 @@ import { getArtworksPage } from "@/lib/queryArtworks"; import { NextResponse, type NextRequest } from "next/server"; +// Public API for paginated artworks listing. export async function GET(req: NextRequest) { const publishedParam = req.nextUrl.searchParams.get("published") ?? "all"; diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts index 370bead..09bf19e 100644 --- a/src/app/api/auth/[...all]/route.ts +++ b/src/app/api/auth/[...all]/route.ts @@ -1,4 +1,5 @@ import { auth } from "@/lib/auth"; import { toNextJsHandler } from "better-auth/next-js"; -export const { POST, GET } = toNextJsHandler(auth); \ No newline at end of file +// Better Auth route handlers. +export const { POST, GET } = toNextJsHandler(auth); diff --git a/src/app/api/image/[...key]/route.ts b/src/app/api/image/[...key]/route.ts index f4d4d68..3c29f32 100644 --- a/src/app/api/image/[...key]/route.ts +++ b/src/app/api/image/[...key]/route.ts @@ -1,10 +1,27 @@ import { s3 } from "@/lib/s3"; +import type { S3Body } from "@/types/s3"; import { GetObjectCommand } from "@aws-sdk/client-s3"; import type { NextRequest } from "next/server"; +import { Readable } from "stream"; +function isWebReadableStream(value: unknown): value is ReadableStream { + return !!value && typeof (value as ReadableStream).getReader === "function"; +} + +function toBodyInit(body: S3Body): BodyInit { + if (body instanceof Readable) { + return Readable.toWeb(body) as ReadableStream; + } + if (isWebReadableStream(body)) { + return body; + } + return body as BodyInit; +} + +// Streams images from S3 for the admin app. export async function GET(_req: NextRequest, context: { params: Promise<{ key: string[] }> }) { const { key } = await context.params; - const s3Key = key.join("/"); + const s3Key = key.join("/"); try { const command = new GetObjectCommand({ @@ -20,7 +37,7 @@ export async function GET(_req: NextRequest, context: { params: Promise<{ key: s const contentType = response.ContentType ?? "application/octet-stream"; - return new Response(response.Body as ReadableStream, { + return new Response(toBodyInit(response.Body as S3Body), { headers: { "Content-Type": contentType, "Cache-Control": "public, max-age=3600", @@ -28,7 +45,7 @@ export async function GET(_req: NextRequest, context: { params: Promise<{ key: s }, }); } catch (err) { - console.log(err) + console.error(err); return new Response("Image not found", { status: 404 }); } -} \ No newline at end of file +} diff --git a/src/app/api/requests/image/route.ts b/src/app/api/requests/image/route.ts index 9a3d387..92db10f 100644 --- a/src/app/api/requests/image/route.ts +++ b/src/app/api/requests/image/route.ts @@ -1,9 +1,12 @@ import { prisma } from "@/lib/prisma"; import { s3 } from "@/lib/s3"; +import type { S3Body } from "@/types/s3"; import { GetObjectCommand } from "@aws-sdk/client-s3"; import archiver from "archiver"; -import { NextRequest } from "next/server"; +import type { NextRequest } from "next/server"; +import { Readable } from "stream"; +// Streams commission request files (single or zip) from S3. type Mode = "display" | "download" | "bulk"; function contentDisposition(filename: string, mode: Mode) { @@ -17,6 +20,20 @@ function sanitizeZipEntryName(name: string) { return name.replace(/[^\w.\- ()\[\]]+/g, "_").slice(0, 180); } +function isWebReadableStream(value: unknown): value is ReadableStream { + return !!value && typeof (value as ReadableStream).getReader === "function"; +} + +function toBodyInit(body: S3Body): BodyInit { + if (body instanceof Readable) { + return Readable.toWeb(body) as ReadableStream; + } + if (isWebReadableStream(body)) { + return body; + } + return body as BodyInit; +} + export async function GET(req: NextRequest) { try { const bucket = process.env.BUCKET_NAME; @@ -52,7 +69,7 @@ export async function GET(req: NextRequest) { const contentType = file.fileType || s3Res.ContentType || "application/octet-stream"; - return new Response(s3Res.Body as ReadableStream, { + return new Response(toBodyInit(s3Res.Body as S3Body), { headers: { "Content-Type": contentType, // You can tune caching; admin-only content usually should be private. @@ -117,8 +134,17 @@ export async function GET(req: NextRequest) { f.originalFile || f.fileKey.split("/").pop() || "file" ); - // obj.Body is a Node stream in Node runtime; works with archiver - archive.append(obj.Body as any, { name: entryName }); + // obj.Body can be a Node Readable, web ReadableStream, or Buffer. + const body = obj.Body; + if (!body) continue; + + if (body instanceof Readable) { + archive.append(body, { name: entryName }); + } else if (isWebReadableStream(body)) { + archive.append(Readable.from(body as AsyncIterable), { name: entryName }); + } else { + archive.append(body as Buffer, { name: entryName }); + } } await archive.finalize(); diff --git a/src/app/api/v1/commissions/route.ts b/src/app/api/v1/commissions/route.ts index a2bb19d..443eafe 100644 --- a/src/app/api/v1/commissions/route.ts +++ b/src/app/api/v1/commissions/route.ts @@ -1,38 +1,11 @@ import { prisma } from "@/lib/prisma"; import { s3 } from "@/lib/s3"; +import { publicCommissionRequestSchema } from "@/schemas/commissions/publicRequest"; import { DeleteObjectsCommand, PutObjectCommand } from "@aws-sdk/client-s3"; import { NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; -import { z } from "zod/v4"; -const payloadSchema = z.object({ - typeId: z.string().min(1).optional().nullable(), - customCardId: z.string().min(1).optional().nullable(), - optionId: z.string().min(1).optional().nullable(), - extraIds: z.array(z.string().min(1)).default([]), - - customerName: z.string().min(1).max(200), - customerEmail: z.string().email().max(320), - customerSocials: z.string().max(2000).optional().nullable(), - message: z.string().min(1).max(20_000), -}).superRefine((data, ctx) => { - const hasType = Boolean(data.typeId); - const hasCustom = Boolean(data.customCardId); - if (!hasType && !hasCustom) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["typeId"], - message: "Missing commission type or custom card", - }); - } - if (hasType && hasCustom) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["typeId"], - message: "Only one of typeId or customCardId is allowed", - }); - } -}); +// Public API endpoint for commission submissions (multipart form). function safeJsonParse(input: string) { try { @@ -64,7 +37,7 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Invalid payload JSON" }, { status: 400 }); } - const payload = payloadSchema.safeParse(parsedJson); + const payload = publicCommissionRequestSchema.safeParse(parsedJson); if (!payload.success) { return NextResponse.json( { error: "Validation error", issues: payload.error.issues }, diff --git a/src/app/error.tsx b/src/app/error.tsx index 63efb97..0ca1e96 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -2,6 +2,7 @@ import { useEffect } from "react"; +// Global error UI for the app router segment. export default function Error({ error, reset, diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index e974c78..1333659 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -1,5 +1,6 @@ "use client"; +// Root-level error boundary UI. export default function GlobalError({ error, reset, diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 358ae45..6714971 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,11 +1,13 @@ -export const dynamic = "force-dynamic"; -export const revalidate = 0; - import { ThemeProvider } from "@/components/global/ThemeProvider"; import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import type { ReactNode } from "react"; import "./globals.css"; +// Root layout and metadata for the admin app. +export const dynamic = "force-dynamic"; +export const revalidate = 0; + const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], @@ -24,7 +26,7 @@ export const metadata: Metadata = { export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode; + children: ReactNode; }>) { return ( diff --git a/src/app/loading.tsx b/src/app/loading.tsx index b37fd52..3c39e3a 100644 --- a/src/app/loading.tsx +++ b/src/app/loading.tsx @@ -1,3 +1,4 @@ +// Global loading state UI. export default function Loading() { return (
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index d8050c1..7911aa9 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; +// 404 page for missing routes. export default function NotFound() { return (
diff --git a/src/components/artworks/ArtworkColorProcessor.tsx b/src/components/artworks/ArtworkColorProcessor.tsx index 494a284..a974ae9 100644 --- a/src/components/artworks/ArtworkColorProcessor.tsx +++ b/src/components/artworks/ArtworkColorProcessor.tsx @@ -3,19 +3,20 @@ import { getArtworkColorStats } from "@/actions/colors/getArtworkColorStats"; import { processPendingArtworkColors } from "@/actions/colors/processPendingArtworkColors"; import { Button } from "@/components/ui/button"; -import * as React from "react"; +import { useEffect, useState } from "react"; +// Admin tool for processing pending artwork color extraction. export function ArtworkColorProcessor() { - const [stats, setStats] = React.useState> | null>(null); - const [loading, setLoading] = React.useState(false); - const [msg, setMsg] = React.useState(null); + const [stats, setStats] = useState> | null>(null); + const [loading, setLoading] = useState(false); + const [msg, setMsg] = useState(null); const refreshStats = async () => { const s = await getArtworkColorStats(); setStats(s); }; - React.useEffect(() => { + useEffect(() => { void refreshStats(); }, []); diff --git a/src/components/artworks/ArtworkGallery.tsx b/src/components/artworks/ArtworkGallery.tsx index 0e3b81c..6023d7f 100644 --- a/src/components/artworks/ArtworkGallery.tsx +++ b/src/components/artworks/ArtworkGallery.tsx @@ -1,14 +1,17 @@ -"use client" +"use client"; // import { PortfolioImage } from "@/generated/prisma"; +// Possibly unused: no references found in `src/app` or `src/components`. import { cn } from "@/lib/utils"; -import { ArtworkWithRelations } from "@/types/Artwork"; +import type { ArtworkWithRelations } from "@/types/Artwork"; import Image from "next/image"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; +import type { CSSProperties } from "react"; import { useState } from "react"; import { Button } from "../ui/button"; +// Client-side artwork gallery with incremental pagination. export default function ArtworkGallery({ initialArtworks, initialCursor, @@ -52,9 +55,7 @@ export default function ArtworkGallery({ return (
-
+
{artworks.map((artwork) => (
@@ -66,7 +67,7 @@ export default function ArtworkGallery({ )} style={{ '--tw-border-opacity': 1, - } as React.CSSProperties} + } as CSSProperties} >
); -} \ No newline at end of file +} diff --git a/src/components/artworks/ArtworkGalleryVariantProcessor.tsx b/src/components/artworks/ArtworkGalleryVariantProcessor.tsx index c4797e8..c2e91d7 100644 --- a/src/components/artworks/ArtworkGalleryVariantProcessor.tsx +++ b/src/components/artworks/ArtworkGalleryVariantProcessor.tsx @@ -1,23 +1,25 @@ "use client"; +// Possibly unused: no references found in `src/app` or `src/components`. import { generateGalleryVariantsMissing } from "@/actions/artworks/generateGalleryVariant"; import { getGalleryVariantStats } from "@/actions/artworks/getGalleryVariantStats"; import { Button } from "@/components/ui/button"; -import * as React from "react"; +import { useCallback, useEffect, useState } from "react"; +// Admin tool for generating missing gallery variants. export function ArtworkGalleryVariantProcessor() { - const [stats, setStats] = React.useState > | null>(null); - const [loading, setLoading] = React.useState(false); - const [msg, setMsg] = React.useState(null); + const [loading, setLoading] = useState(false); + const [msg, setMsg] = useState(null); - const refreshStats = React.useCallback(async () => { + const refreshStats = useCallback(async () => { const s = await getGalleryVariantStats(); setStats(s); }, []); - React.useEffect(() => { + useEffect(() => { void refreshStats(); }, [refreshStats]); diff --git a/src/components/artworks/ArtworksTable.tsx b/src/components/artworks/ArtworksTable.tsx index 91f08e3..83d1245 100644 --- a/src/components/artworks/ArtworksTable.tsx +++ b/src/components/artworks/ArtworksTable.tsx @@ -1,6 +1,7 @@ "use client"; import { + type Column, type ColumnDef, flexRender, getCoreRowModel, @@ -17,14 +18,11 @@ import { } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; -import * as React from "react"; +import { useEffect, useMemo, useState, useTransition } from "react"; import { deleteArtwork } from "@/actions/artworks/deleteArtwork"; import { getArtworkFilterOptions } from "@/actions/artworks/getArtworkFilterOptions"; import { getArtworksTablePage } from "@/actions/artworks/getArtworksTablePage"; -// import type { ArtworkTableRow } from "@/lib/artworks/artworkTableSchema"; - -// import { MultiSelectFilter } from "@/components/admin/MultiSelectFilter"; import { AlertDialog, AlertDialogAction, @@ -68,11 +66,12 @@ import { import type { ArtworkTableRow } from "@/schemas/artworks/tableSchema"; import { MultiSelectFilter } from "./MultiSelectFilter"; +// Client-side table for filtering, sorting, and managing artworks. type TriState = "any" | "true" | "false"; function useDebouncedValue(value: T, delayMs: number) { - const [debounced, setDebounced] = React.useState(value); - React.useEffect(() => { + const [debounced, setDebounced] = useState(value); + useEffect(() => { const t = setTimeout(() => setDebounced(value), delayMs); return () => clearTimeout(t); }, [value, delayMs]); @@ -81,7 +80,7 @@ function useDebouncedValue(value: T, delayMs: number) { function SortHeader(props: { title: string; - column: any; // TanStack Column + column: Column; }) { const sorted = props.column.getIsSorted() as false | "asc" | "desc"; @@ -173,13 +172,13 @@ type Filters = { }; export function ArtworksTable() { - const [sorting, setSorting] = React.useState([ + const [sorting, setSorting] = useState([ { id: "updatedAt", desc: true }, ]); - const [pageIndex, setPageIndex] = React.useState(0); - const [pageSize, setPageSize] = React.useState(25); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(25); - const [filters, setFilters] = React.useState({ + const [filters, setFilters] = useState({ name: "", slug: "", published: "any", @@ -192,27 +191,27 @@ export function ArtworksTable() { const debouncedName = useDebouncedValue(filters.name, 300); const debouncedSlug = useDebouncedValue(filters.slug, 300); - const [rows, setRows] = React.useState([]); - const [total, setTotal] = React.useState(0); + const [rows, setRows] = useState([]); + const [total, setTotal] = useState(0); - const [albumOptions, setAlbumOptions] = React.useState< + const [albumOptions, setAlbumOptions] = useState< { id: string; name: string }[] >([]); - const [categoryOptions, setCategoryOptions] = React.useState< + const [categoryOptions, setCategoryOptions] = useState< { id: string; name: string }[] >([]); - const [isPending, startTransition] = React.useTransition(); + const [isPending, startTransition] = useTransition(); - const [deleteOpen, setDeleteOpen] = React.useState(false); - const [deleteTarget, setDeleteTarget] = React.useState<{ + const [deleteOpen, setDeleteOpen] = useState(false); + const [deleteTarget, setDeleteTarget] = useState<{ id: string; name: string; } | null>(null); const pageCount = Math.max(1, Math.ceil(total / pageSize)); - React.useEffect(() => { + useEffect(() => { startTransition(async () => { const res = await getArtworkFilterOptions(); setAlbumOptions(res.albums); @@ -220,7 +219,7 @@ export function ArtworksTable() { }); }, []); - const columns = React.useMemo[]>( + const columns = useMemo[]>( () => [ { id: "preview", @@ -242,9 +241,9 @@ export function ArtworksTable() { />
- +
-
+
{row.original.name} , cell: ({ row }) => ( -
+
{ + useEffect(() => { startTransition(async () => { const res = await getArtworksTablePage({ pagination: { pageIndex, pageSize }, @@ -483,7 +482,7 @@ export function ArtworksTable() {
-
+
{/* main header */} @@ -636,7 +635,7 @@ export function ArtworksTable() { setPageIndex(0); }} > - + @@ -665,7 +664,7 @@ export function ArtworksTable() { Prev -
+
Page {pageIndex + 1} / {pageCount}
diff --git a/src/components/artworks/FilterBar.tsx b/src/components/artworks/FilterBar.tsx index 38caa6c..d10e51b 100644 --- a/src/components/artworks/FilterBar.tsx +++ b/src/components/artworks/FilterBar.tsx @@ -1,5 +1,6 @@ "use client"; +// Possibly unused: no references found in `src/app` or `src/components`. // import { PortfolioAlbum, PortfolioType } from "@/generated/prisma"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; // import BackfillButton from "./BackfillButton"; @@ -14,6 +15,7 @@ type FilterBarProps = { // groupId: string; }; +// Filter bar for the artwork gallery (published status only). export default function FilterBar({ // types, // albums, @@ -44,13 +46,6 @@ export default function FilterBar({ router.push(`${pathname}?${params.toString()}`); }; - const sortHref = `${pathname}/sort?${params.toString()}`; - - const sortByColor = () => { - params.set("sortBy", "color"); - router.push(`${pathname}?${params.toString()}`); - }; - return (
@@ -148,7 +143,6 @@ export default function FilterBar({
*/}
- ); } diff --git a/src/components/artworks/MultiSelectFilter.tsx b/src/components/artworks/MultiSelectFilter.tsx index e426b17..941a686 100644 --- a/src/components/artworks/MultiSelectFilter.tsx +++ b/src/components/artworks/MultiSelectFilter.tsx @@ -1,7 +1,7 @@ "use client"; import { Check, ChevronsUpDown } from "lucide-react"; -import * as React from "react"; +import { useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command"; @@ -9,13 +9,14 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover type Option = { id: string; name: string }; +// Simple multi-select filter control for artwork filters. export function MultiSelectFilter(props: { placeholder: string; options: Option[]; value: string[]; // selected ids onChange: (next: string[]) => void; }) { - const selected = React.useMemo(() => new Set(props.value), [props.value]); + const selected = useMemo(() => new Set(props.value), [props.value]); return ( diff --git a/src/components/artworks/single/ArtworkColors.tsx b/src/components/artworks/single/ArtworkColors.tsx index 17c7761..5a492cc 100644 --- a/src/components/artworks/single/ArtworkColors.tsx +++ b/src/components/artworks/single/ArtworkColors.tsx @@ -6,11 +6,12 @@ import { getArtworkColors } from "@/actions/artworks/getArtworkColors"; // import { getArtworkColors } from "@/actions/colors/getArtworkColors"; import { Button } from "@/components/ui/button"; import type { ArtworkColor, Color } from "@/generated/prisma/client"; -import * as React from "react"; +import { useState, useTransition } from "react"; // import { toast } from "sonner"; // if you use it type ColorWithItems = ArtworkColor & { color: Color }; +// Displays and regenerates extracted artwork colors. export default function ArtworkColors({ colors: initialColors, artworkId, @@ -18,8 +19,8 @@ export default function ArtworkColors({ colors: ColorWithItems[]; artworkId: string; }) { - const [colors, setColors] = React.useState(initialColors); - const [isPending, startTransition] = React.useTransition(); + const [colors, setColors] = useState(initialColors); + const [isPending, startTransition] = useTransition(); const handleGenerate = () => { startTransition(async () => { diff --git a/src/components/artworks/single/ArtworkDetails.tsx b/src/components/artworks/single/ArtworkDetails.tsx index 7835c06..ca8d055 100644 --- a/src/components/artworks/single/ArtworkDetails.tsx +++ b/src/components/artworks/single/ArtworkDetails.tsx @@ -1,12 +1,14 @@ -import Link from "next/link"; -import * as React from "react"; - import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; import { cn } from "@/lib/utils"; import type { ArtworkWithRelations } from "@/types/Artwork"; +import Link from "next/link"; +import type { ReactNode } from "react"; + +// Read-only details panel for a single artwork, including metadata and file info. +type ArtworkVariant = ArtworkWithRelations["variants"][number]; function fmtDate(value?: Date | string | null) { if (!value) return "—"; @@ -47,7 +49,7 @@ function fmtBytes(bytes?: number | null) { return `${fmtNum(v, i === 0 ? 0 : 2)} ${units[i]}`; } -function KVTable({ rows }: { rows: Array<{ k: string; v: React.ReactNode }> }) { +function KVTable({ rows }: { rows: Array<{ k: string; v: ReactNode }> }) { return (
@@ -89,7 +91,7 @@ export default function ArtworkDetails({ // Your schema: Artwork has `fileId` + relation `file: FileData` // but depending on your `ArtworkWithRelations` type, `file` may be optional. - const file = (artwork as any).file ?? null; + const file = artwork.file ?? null; const flags = [ artwork.published ? : , @@ -125,9 +127,9 @@ export default function ArtworkDetails({ { k: "Slug", v: {artwork.slug} }, { k: "Sort index", v: fmtNum(artwork.sortIndex ?? 0) }, { k: "Sort key", v: artwork.sortKey != null ? fmtNum(artwork.sortKey) : "—" }, - { k: "Created", v: fmtDate(artwork.createdAt as any) }, - { k: "Updated", v: fmtDate(artwork.updatedAt as any) }, - { k: "Creation date", v: fmtDate(artwork.creationDate as any) }, + { k: "Created", v: fmtDate(artwork.createdAt) }, + { k: "Updated", v: fmtDate(artwork.updatedAt) }, + { k: "Creation date", v: fmtDate(artwork.creationDate) }, { k: "Creation (month/year)", v: @@ -143,7 +145,7 @@ export default function ArtworkDetails({ {artwork.colorStatus ?? "—"} {artwork.colorsGeneratedAt ? ( - generated {fmtDate(artwork.colorsGeneratedAt as any)} + generated {fmtDate(artwork.colorsGeneratedAt)} ) : null} @@ -243,7 +245,7 @@ export default function ArtworkDetails({ { k: "Stored name", v: file.name ?? "—" }, { k: "MIME type", v: file.fileType ?? "—" }, { k: "Size", v: fmtBytes(file.fileSize) }, - { k: "Uploaded", v: fmtDate(file.uploadDate as any) }, + { k: "Uploaded", v: fmtDate(file.uploadDate) }, ]} /> ) : ( @@ -262,8 +264,10 @@ export default function ArtworkDetails({
{artwork.variants .slice() - .sort((a: any, b: any) => (a.type ?? "").localeCompare(b.type ?? "")) - .map((v: any) => ( + .sort((a: ArtworkVariant, b: ArtworkVariant) => + (a.type ?? "").localeCompare(b.type ?? "") + ) + .map((v: ArtworkVariant) => (
= 1024) return `${(mb / 1024).toFixed(2)} GB`; @@ -34,9 +35,9 @@ export default function ArtworkTimelapse({ artworkId: string; timelapse: Timelapse | null; }) { - const [isBusy, startTransition] = React.useTransition(); + const [isBusy, startTransition] = useTransition(); - async function onPickFile(e: React.ChangeEvent) { + async function onPickFile(e: ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; diff --git a/src/components/artworks/single/ArtworkVariants.tsx b/src/components/artworks/single/ArtworkVariants.tsx index 9467d4a..75da31a 100644 --- a/src/components/artworks/single/ArtworkVariants.tsx +++ b/src/components/artworks/single/ArtworkVariants.tsx @@ -23,6 +23,7 @@ function byVariantOrder(a: FileVariant, b: FileVariant) { return a.type.localeCompare(b.type); } +// Displays artwork file variants and allows generating gallery size. export default function ArtworkVariants({ artworkId, variants, diff --git a/src/components/artworks/single/DeleteArtworkButton.tsx b/src/components/artworks/single/DeleteArtworkButton.tsx index 08cfaf0..66e1e68 100644 --- a/src/components/artworks/single/DeleteArtworkButton.tsx +++ b/src/components/artworks/single/DeleteArtworkButton.tsx @@ -1,9 +1,10 @@ -"use client" +"use client"; import { deleteArtwork } from "@/actions/artworks/deleteArtwork"; import { Button } from "@/components/ui/button"; import { useRouter } from "next/navigation"; +// Delete button with confirmation for an artwork. export default function DeleteArtworkButton({ artworkId }: { artworkId: string }) { const router = useRouter(); @@ -23,4 +24,4 @@ export default function DeleteArtworkButton({ artworkId }: { artworkId: string } Delete Artwork ); -} \ No newline at end of file +} diff --git a/src/components/artworks/single/EditArtworkForm.tsx b/src/components/artworks/single/EditArtworkForm.tsx index 6d85764..71519fe 100644 --- a/src/components/artworks/single/EditArtworkForm.tsx +++ b/src/components/artworks/single/EditArtworkForm.tsx @@ -1,4 +1,4 @@ -"use client" +"use client"; import { updateArtwork } from "@/actions/artworks/updateArtwork"; import { Button } from "@/components/ui/button"; @@ -16,12 +16,16 @@ import { useForm } from "react-hook-form"; import { toast } from "sonner"; import type { z } from "zod/v4"; -export default function EditArtworkForm({ artwork, categories, tags }: - { - artwork: ArtworkWithRelations, - categories: CategoryWithTags[] - tags: Tag[] - }) { +// Form for editing an artwork and its metadata. +export default function EditArtworkForm({ + artwork, + categories, + tags, +}: { + artwork: ArtworkWithRelations; + categories: CategoryWithTags[]; + tags: Tag[]; +}) { const router = useRouter(); const form = useForm>({ resolver: zodResolver(artworkSchema), @@ -41,15 +45,15 @@ export default function EditArtworkForm({ artwork, categories, tags }: categoryIds: artwork.categories?.map(cat => cat.id) ?? [], tagIds: artwork.tags?.map(tag => tag.id) ?? [], newCategoryNames: [], - newTagNames: [] - } - }) + newTagNames: [], + }, + }); async function onSubmit(values: z.infer) { - const updatedArtwork = await updateArtwork(values, artwork.id) + const updatedArtwork = await updateArtwork(values, artwork.id); if (updatedArtwork) { - toast.success("Artwork updated") - router.push(`/artworks`) + toast.success("Artwork updated"); + router.push("/artworks"); } } @@ -121,9 +125,9 @@ export default function EditArtworkForm({ artwork, categories, tags }: - field.onChange(e.target.value === '' ? undefined : +e.target.value) + field.onChange(e.target.value === "" ? undefined : +e.target.value) } /> @@ -141,9 +145,9 @@ export default function EditArtworkForm({ artwork, categories, tags }: - field.onChange(e.target.value === '' ? undefined : +e.target.value) + field.onChange(e.target.value === "" ? undefined : +e.target.value) } /> diff --git a/src/components/auth/ForgotPasswordForm.tsx b/src/components/auth/ForgotPasswordForm.tsx index 0954150..e7a9137 100644 --- a/src/components/auth/ForgotPasswordForm.tsx +++ b/src/components/auth/ForgotPasswordForm.tsx @@ -1,14 +1,15 @@ "use client"; import { authClient } from "@/lib/auth-client"; -import * as React from "react"; +import { useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +// Request password reset form (sends email via Better Auth). export function ForgotPasswordForm() { - const [isSubmitting, setIsSubmitting] = React.useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); async function onSubmit(formData: FormData) { setIsSubmitting(true); diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx index e1f4592..4bcda95 100644 --- a/src/components/auth/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -2,13 +2,15 @@ import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; -import * as React from "react"; +import type { FormEvent } from "react"; +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { toast } from "sonner"; +// Email/password login form with verification resend flow. type ApiErrorShape = | { message?: string; error?: string; status?: string; code?: string } | null; @@ -30,13 +32,13 @@ export default function LoginForm() { const searchParams = useSearchParams(); const next = searchParams.get("next") ?? "/"; - const [email, setEmail] = React.useState(""); - const [password, setPassword] = React.useState(""); - const [pending, setPending] = React.useState(false); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [pending, setPending] = useState(false); - const [error, setError] = React.useState(null); - const [needsVerification, setNeedsVerification] = React.useState(false); - const [resendPending, setResendPending] = React.useState(false); + const [error, setError] = useState(null); + const [needsVerification, setNeedsVerification] = useState(false); + const [resendPending, setResendPending] = useState(false); async function resendVerification() { setResendPending(true); @@ -66,7 +68,7 @@ export default function LoginForm() { } } - async function onSubmit(e: React.FormEvent) { + async function onSubmit(e: FormEvent) { e.preventDefault(); setPending(true); setError(null); diff --git a/src/components/auth/LogoutButton.tsx b/src/components/auth/LogoutButton.tsx index 5f9eb57..d4d1ec9 100644 --- a/src/components/auth/LogoutButton.tsx +++ b/src/components/auth/LogoutButton.tsx @@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button"; import { authClient } from "@/lib/auth-client"; import { useRouter } from "next/navigation"; +// Sign-out button for the admin header. export default function LogoutButton() { const router = useRouter(); diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx index adc429e..cf594aa 100644 --- a/src/components/auth/RegisterForm.tsx +++ b/src/components/auth/RegisterForm.tsx @@ -1,16 +1,17 @@ "use client"; import { useRouter } from "next/navigation"; -import * as React from "react"; +import { useState } from "react"; import { toast } from "sonner"; import { registerFirstUser } from "@/actions/auth/registerFirstUser"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +// Registration form for the first admin user. export function RegisterForm() { const router = useRouter(); - const [isSubmitting, setIsSubmitting] = React.useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); async function onSubmit(formData: FormData) { setIsSubmitting(true); diff --git a/src/components/auth/ResetPasswordForm.tsx b/src/components/auth/ResetPasswordForm.tsx index e5e9310..e09abc3 100644 --- a/src/components/auth/ResetPasswordForm.tsx +++ b/src/components/auth/ResetPasswordForm.tsx @@ -2,15 +2,16 @@ import { authClient } from "@/lib/auth-client"; import { useRouter } from "next/navigation"; -import * as React from "react"; +import { useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +// Reset password form for token-based password reset flow. export function ResetPasswordForm({ token }: { token: string }) { const router = useRouter(); - const [isSubmitting, setIsSubmitting] = React.useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); async function onSubmit(formData: FormData) { setIsSubmitting(true); diff --git a/src/components/categories/CategoryTable.tsx b/src/components/categories/CategoryTable.tsx index 1c234f0..3842052 100644 --- a/src/components/categories/CategoryTable.tsx +++ b/src/components/categories/CategoryTable.tsx @@ -17,9 +17,10 @@ type CatRow = { id: string; name: string; slug: string; - _count: { artworks: number, tagLinks: number }; + _count: { artworks: number; tagLinks: number }; }; +// Categories table with edit/delete actions. export default function CategoryTable({ categories }: { categories: CatRow[] }) { const handleDelete = (id: string) => { deleteCategory(id); diff --git a/src/components/categories/EditCategoryForm.tsx b/src/components/categories/EditCategoryForm.tsx index b32f9c0..aa8a9a1 100644 --- a/src/components/categories/EditCategoryForm.tsx +++ b/src/components/categories/EditCategoryForm.tsx @@ -1,11 +1,11 @@ -"use client" +"use client"; import { updateCategory } from "@/actions/categories/updateCategory"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; -import { ArtCategory } from "@/generated/prisma/client"; +import type { ArtCategory } from "@/generated/prisma/client"; import { categorySchema } from "@/schemas/artworks/categorySchema"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; @@ -13,6 +13,7 @@ import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod/v4"; +// Form for editing an existing artwork category. export default function EditCategoryForm({ category }: { category: ArtCategory }) { const router = useRouter(); const form = useForm>({ @@ -21,18 +22,17 @@ export default function EditCategoryForm({ category }: { category: ArtCategory } name: category.name, slug: category.slug, description: category.description || "", - } - }) + }, + }); async function onSubmit(values: z.infer) { try { - const updated = await updateCategory(category.id, values) - console.log("Art category updated:", updated) - toast("Art category updated.") - router.push("/portfolio/categories") + await updateCategory(category.id, values); + toast("Art category updated."); + router.push("/categories"); } catch (err) { - console.error(err) - toast("Failed to update art category.") + console.error(err); + toast("Failed to update art category."); } } @@ -82,10 +82,12 @@ export default function EditCategoryForm({ category }: { category: ArtCategory } />
- +
-
+
); -} \ No newline at end of file +} diff --git a/src/components/categories/NewCategoryForm.tsx b/src/components/categories/NewCategoryForm.tsx index 5cda1b9..b99ddfb 100644 --- a/src/components/categories/NewCategoryForm.tsx +++ b/src/components/categories/NewCategoryForm.tsx @@ -1,7 +1,6 @@ -"use client" +"use client"; import { createCategory } from "@/actions/categories/createCategory"; -// import { createCategory } from "@/actions/portfolio/categories/createCategory"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; @@ -13,7 +12,7 @@ import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod/v4"; - +// Form for creating a new artwork category. export default function NewCategoryForm() { const router = useRouter(); const form = useForm>({ @@ -22,18 +21,17 @@ export default function NewCategoryForm() { name: "", slug: "", description: "", - } - }) + }, + }); async function onSubmit(values: z.infer) { try { - const created = await createCategory(values) - console.log("Art category created:", created) - toast("Art category created.") - router.push("/categories") + await createCategory(values); + toast("Art category created."); + router.push("/categories"); } catch (err) { - console.error(err) - toast("Failed to create art category.") + console.error(err); + toast("Failed to create art category."); } } @@ -83,10 +81,12 @@ export default function NewCategoryForm() { />
- +
- + ); -} \ No newline at end of file +} diff --git a/src/components/commissions/customCards/CustomCardImagePicker.tsx b/src/components/commissions/customCards/CustomCardImagePicker.tsx index 04697dd..f0c66de 100644 --- a/src/components/commissions/customCards/CustomCardImagePicker.tsx +++ b/src/components/commissions/customCards/CustomCardImagePicker.tsx @@ -13,15 +13,15 @@ import { } from "@/components/ui/form"; import type { CommissionCustomCardValues } from "@/schemas/commissionCustomCard"; import Image from "next/image"; -import { useMemo, useTransition } from "react"; +import { useTransition } from "react"; import type { UseFormReturn } from "react-hook-form"; import { useWatch } from "react-hook-form"; type Props = { form: UseFormReturn; - initialImages: { key: string; url: string }[]; }; +// Upload/preview control for a custom card reference image. export function CustomCardImagePicker({ form }: Props) { const [isPending, startTransition] = useTransition(); const referenceImageUrl = useWatch({ @@ -29,10 +29,7 @@ export function CustomCardImagePicker({ form }: Props) { name: "referenceImageUrl", }); - const previewUrl = useMemo(() => { - if (!referenceImageUrl) return ""; - return referenceImageUrl; - }, [referenceImageUrl]); + const previewUrl = referenceImageUrl ?? ""; const handleUpload = (file: File) => { const fd = new FormData(); diff --git a/src/components/commissions/customCards/EditCustomCardForm.tsx b/src/components/commissions/customCards/EditCustomCardForm.tsx index 790500e..b53a148 100644 --- a/src/components/commissions/customCards/EditCustomCardForm.tsx +++ b/src/components/commissions/customCards/EditCustomCardForm.tsx @@ -1,7 +1,6 @@ "use client"; import { updateCommissionCustomCard } from "@/actions/commissions/customCards/updateCard"; -import type { CommissionCustomCardImageItem } from "@/actions/commissions/customCards/images"; import { Button } from "@/components/ui/button"; import { Form, @@ -19,6 +18,7 @@ import { commissionCustomCardSchema, type CommissionCustomCardValues, } from "@/schemas/commissionCustomCard"; +import MultipleSelector from "@/components/ui/multiselect"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; @@ -26,7 +26,6 @@ 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; @@ -58,15 +57,14 @@ type Props = { card: CustomCardWithItems; allOptions: CommissionOption[]; allExtras: CommissionExtra[]; - images: CommissionCustomCardImageItem[]; allTags: Tag[]; }; +// Form for editing an existing custom commission card. export default function EditCustomCardForm({ card, allOptions, allExtras, - images, allTags, }: Props) { const router = useRouter(); @@ -173,7 +171,7 @@ export default function EditCustomCardForm({ } + render={() => } /> ({ resolver: zodResolver(commissionCustomCardSchema), @@ -53,8 +52,7 @@ export default function NewCustomCardForm({ options, extras, images, tags }: Pro async function onSubmit(values: CommissionCustomCardValues) { try { - const created = await createCommissionCustomCard(values); - console.log("Commission custom card created:", created); + await createCommissionCustomCard(values); toast("Custom commission card created."); router.push("/commissions/custom-cards"); } catch (err) { @@ -131,7 +129,7 @@ export default function NewCustomCardForm({ options, extras, images, tags }: Pro } + render={() => } /> (null); + const [busyId, setBusyId] = useState(null); async function onDelete(id: string) { try { diff --git a/src/components/commissions/guidelines/Editor.tsx b/src/components/commissions/guidelines/Editor.tsx index 000329f..0fc6ec9 100644 --- a/src/components/commissions/guidelines/Editor.tsx +++ b/src/components/commissions/guidelines/Editor.tsx @@ -1,22 +1,20 @@ -"use client" - -import type { Value } from 'platejs'; +"use client"; import { deleteCommissionExample, uploadCommissionExample } from "@/actions/commissions/examples"; -import { saveGuidelines } from '@/actions/commissions/guidelines/saveGuidelines'; -import { BasicBlocksKit } from '@/components/editor/plugins/basic-blocks-kit'; -import { BasicMarksKit } from '@/components/editor/plugins/basic-marks-kit'; -import { CodeBlockKit } from '@/components/editor/plugins/code-block-kit'; -import { ListKit } from '@/components/editor/plugins/list-kit'; -import { MarkdownKit } from '@/components/editor/plugins/markdown-kit'; +import { saveGuidelines } from "@/actions/commissions/guidelines/saveGuidelines"; +import { BasicBlocksKit } from "@/components/editor/plugins/basic-blocks-kit"; +import { BasicMarksKit } from "@/components/editor/plugins/basic-marks-kit"; +import { CodeBlockKit } from "@/components/editor/plugins/code-block-kit"; +import { ListKit } from "@/components/editor/plugins/list-kit"; +import { MarkdownKit } from "@/components/editor/plugins/markdown-kit"; import { Button } from "@/components/ui/button"; -import { Editor, EditorContainer } from '@/components/ui/editor'; -import { FixedToolbar } from '@/components/ui/fixed-toolbar'; -import { Label } from '@/components/ui/label'; -import { BulletedListToolbarButton, NumberedListToolbarButton } from '@/components/ui/list-toolbar-button'; -import { MarkToolbarButton } from '@/components/ui/mark-toolbar-button'; +import { Editor, EditorContainer } from "@/components/ui/editor"; +import { FixedToolbar } from "@/components/ui/fixed-toolbar"; +import { Label } from "@/components/ui/label"; +import { BulletedListToolbarButton, NumberedListToolbarButton } from "@/components/ui/list-toolbar-button"; +import { MarkToolbarButton } from "@/components/ui/mark-toolbar-button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { ToolbarButton } from '@/components/ui/toolbar'; +import { ToolbarButton } from "@/components/ui/toolbar"; import { Bold, Braces, @@ -28,15 +26,15 @@ import { Quote, Save, Strikethrough, - Underline + Underline, } from "lucide-react"; -import { Plate, usePlateEditor } from 'platejs/react'; -import { useEffect, useMemo, useState, useTransition } from 'react'; - -const initialValue: Value = [ -]; +import type { Value } from "platejs"; +import { Plate, usePlateEditor } from "platejs/react"; +import { useEffect, useMemo, useState, useTransition } from "react"; +const initialValue: Value = []; +// Rich text editor for commission guidelines with example image selection. export default function GuidelinesEditor({ markdown, exampleImageUrl, @@ -46,7 +44,6 @@ export default function GuidelinesEditor({ exampleImageUrl: string | null; examples: { key: string; url: string; size: number | null; lastModified: string | null }[]; }) { - // const [isSaving, setIsSaving] = useState(false); const [exampleItems, setExampleItems] = useState(examples); const [selectedKey, setSelectedKey] = useState(null); const [isPending, startTransition] = useTransition(); @@ -64,7 +61,6 @@ export default function GuidelinesEditor({ useEffect(() => { if (markdown && editor.api.markdown.deserialize) { const markdownValue = editor.api.markdown.deserialize(markdown); - // console.log(markdownValue); editor.children = markdownValue; } }, [editor, markdown]); @@ -80,12 +76,9 @@ export default function GuidelinesEditor({ }, [exampleItems, selectedKey]); const handleSave = async () => { - // console.log(editor); if (!editor.api.markdown.serialize) return; - // setIsSaving(true); const markdown = editor.api.markdown.serialize(); await saveGuidelines(markdown, selectedUrl || null); - // setIsSaving(false); }; const handleUpload = (file: File) => { @@ -111,7 +104,7 @@ export default function GuidelinesEditor({ }; return ( - {/* Provides editor context */} +
@@ -207,7 +200,7 @@ export default function GuidelinesEditor({ - {/* Styles the editor area */} + diff --git a/src/components/commissions/kanban/CommissionsKanbanClient.tsx b/src/components/commissions/kanban/CommissionsKanbanClient.tsx index 076e613..f3406c3 100644 --- a/src/components/commissions/kanban/CommissionsKanbanClient.tsx +++ b/src/components/commissions/kanban/CommissionsKanbanClient.tsx @@ -9,8 +9,9 @@ import { type BoardColumnId, canonicalStatusForColumn, } from "@/lib/commissions/kanban"; +import type { UniqueIdentifier } from "@dnd-kit/core"; import Link from "next/link"; -import * as React from "react"; +import { useEffect, useRef, useState } from "react"; type BoardItem = { id: string; @@ -27,10 +28,9 @@ type BoardItem = { type ColumnsState = Record; -import type { UniqueIdentifier } from "@dnd-kit/core"; - type KanbanValue = Record; +// Drag-and-drop kanban board for commission requests. function isColumnsState(v: KanbanValue): v is ColumnsState { return ( Array.isArray(v.intake) && @@ -82,10 +82,10 @@ export default function CommissionsKanbanClient({ }: { initialColumns: ColumnsState; }) { - const [columns, setColumns] = React.useState(initialColumns); - const prevRef = React.useRef(columns); + const [columns, setColumns] = useState(initialColumns); + const prevRef = useRef(columns); - React.useEffect(() => { + useEffect(() => { prevRef.current = columns; }, [columns]); diff --git a/src/components/commissions/options/OptionDialog.tsx b/src/components/commissions/options/OptionDialog.tsx index f952c5d..8a04548 100644 --- a/src/components/commissions/options/OptionDialog.tsx +++ b/src/components/commissions/options/OptionDialog.tsx @@ -22,7 +22,7 @@ import { import { Input } from "@/components/ui/input"; import { type CommissionOptionValues, commissionOptionSchema } from "@/schemas/commissionType"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useState } from "react"; +import { type ReactNode, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -33,11 +33,12 @@ type Initial = { }; type Props = { - trigger: React.ReactNode; + trigger: ReactNode; mode: "create" | "edit"; initial?: Initial; }; +// Dialog for creating or editing a commission option. export function OptionDialog({ trigger, mode, initial }: Props) { const [open, setOpen] = useState(false); diff --git a/src/components/commissions/options/OptionsListClient.tsx b/src/components/commissions/options/OptionsListClient.tsx index 6b35d1a..dc16183 100644 --- a/src/components/commissions/options/OptionsListClient.tsx +++ b/src/components/commissions/options/OptionsListClient.tsx @@ -14,6 +14,7 @@ type Item = { description: string | null; }; +// Client-side list for managing commission options. export function OptionsListClient({ options }: { options: Item[] }) { const [busyId, setBusyId] = useState(null); diff --git a/src/components/commissions/requests/CommissionRequestEditor.tsx b/src/components/commissions/requests/CommissionRequestEditor.tsx index 1e76b76..6a0e6eb 100644 --- a/src/components/commissions/requests/CommissionRequestEditor.tsx +++ b/src/components/commissions/requests/CommissionRequestEditor.tsx @@ -1,6 +1,7 @@ "use client"; import { deleteCommissionRequest } from "@/actions/commissions/requests/deleteCommissionRequest"; +import type { getCommissionRequestById } from "@/actions/commissions/requests/getCommissionRequestById"; import { updateCommissionRequest } from "@/actions/commissions/requests/updateCommissionRequest"; import { AlertDialog, @@ -27,35 +28,15 @@ import type { CommissionStatus } from "@/schemas/commissions/requests"; import { Download, ExternalLink } from "lucide-react"; import Image from "next/image"; import { useRouter } from "next/navigation"; -import * as React from "react"; +import { useState, useTransition } from "react"; import { toast } from "sonner"; -type RequestFile = { - id: string; - createdAt: Date; - originalFile: string; - fileType: string; - fileSize: number; -}; - -type RequestShape = { - id: string; - createdAt: Date; - updatedAt: Date; - status: CommissionStatus; - - customerName: string; - customerEmail: string; - customerSocials: string | null; - message: string; - - type: { id: string; name: string } | null; - option: { id: string; name: string } | null; - extras: { id: string; name: string }[]; - - priceEstimate?: { min: number; max: number }; - - files: RequestFile[]; +// Admin editor for a single commission request. +type RequestShape = NonNullable>>; +type RequestShapeSerializable = Omit & { + createdAt: string | Date; + updatedAt: string | Date; + files: Array & { createdAt: string | Date }>; }; const STATUS_OPTIONS: CommissionStatus[] = [ @@ -67,6 +48,8 @@ const STATUS_OPTIONS: CommissionStatus[] = [ "COMPLETED", "SPAM", ]; +const isCommissionStatus = (value: string): value is CommissionStatus => + STATUS_OPTIONS.includes(value as CommissionStatus); function isImage(mime: string) { return !!mime && mime.startsWith("image/"); @@ -93,17 +76,17 @@ function bulkUrl(requestId: string) { return `/api/requests/image?mode=bulk&requestId=${encodeURIComponent(requestId)}`; } -export function CommissionRequestEditor({ request }: { request: RequestShape }) { +export function CommissionRequestEditor({ request }: { request: RequestShapeSerializable }) { const router = useRouter(); - const [status, setStatus] = React.useState(request.status); - const [customerName, setCustomerName] = React.useState(request.customerName); - const [customerEmail, setCustomerEmail] = React.useState(request.customerEmail); - const [customerSocials, setCustomerSocials] = React.useState(request.customerSocials ?? ""); - const [message, setMessage] = React.useState(request.message); + const [status, setStatus] = useState(request.status); + const [customerName, setCustomerName] = useState(request.customerName); + const [customerEmail, setCustomerEmail] = useState(request.customerEmail); + const [customerSocials, setCustomerSocials] = useState(request.customerSocials ?? ""); + const [message, setMessage] = useState(request.message); - const [isSaving, startSaving] = React.useTransition(); - const [deleteOpen, setDeleteOpen] = React.useState(false); + const [isSaving, startSaving] = useTransition(); + const [deleteOpen, setDeleteOpen] = useState(false); const dirty = status !== request.status || @@ -117,27 +100,12 @@ export function CommissionRequestEditor({ request }: { request: RequestShape }) {/* Top bar */}
- {/* - {request.files.length} file{request.files.length === 1 ? "" : "s"} - */} Status: {request.status} - {/* - Updated: {new Date(request.updatedAt).toLocaleString()} - */}
- {/* {request.files.length > 1 ? ( - - ) : null} */} - - - {/* */}
@@ -189,7 +148,12 @@ export function CommissionRequestEditor({ request }: { request: RequestShape })
Status
- { + if (isCommissionStatus(v)) setStatus(v); + }} + > diff --git a/src/components/commissions/requests/CommissionRequestsTable.tsx b/src/components/commissions/requests/CommissionRequestsTable.tsx index da22bf1..e1e5999 100644 --- a/src/components/commissions/requests/CommissionRequestsTable.tsx +++ b/src/components/commissions/requests/CommissionRequestsTable.tsx @@ -38,10 +38,11 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { CommissionRequestTableRow, CommissionStatus } from "@/schemas/commissions/requests"; +import type { CommissionRequestTableRow, CommissionStatus } from "@/schemas/commissions/requests"; import { - ColumnDef, - SortingState, + type Column, + type ColumnDef, + type SortingState, flexRender, getCoreRowModel, useReactTable, @@ -56,11 +57,12 @@ import { Trash2, } from "lucide-react"; import Link from "next/link"; -import * as React from "react"; +import { useCallback, useEffect, useMemo, useState, useTransition } from "react"; +// Client-side table for browsing and managing commission requests. type TriState = "any" | "true" | "false"; -function SortHeader(props: { title: string; column: any }) { +function SortHeader(props: { title: string; column: Column }) { const sorted = props.column.getIsSorted() as false | "asc" | "desc"; return (
@@ -59,15 +55,6 @@ export default function RequestsTable({ requests }: { requests: CommissionReques - - {/* */} @@ -75,4 +62,4 @@ export default function RequestsTable({ requests }: { requests: CommissionReques
); -} \ No newline at end of file +} diff --git a/src/components/commissions/types/EditTypeForm.tsx b/src/components/commissions/types/EditTypeForm.tsx index 10be463..78b27ed 100644 --- a/src/components/commissions/types/EditTypeForm.tsx +++ b/src/components/commissions/types/EditTypeForm.tsx @@ -46,9 +46,9 @@ type Props = { allOptions: CommissionOption[]; allExtras: CommissionExtra[]; allTags: Tag[]; - // allCustomInputs: CommissionCustomInput[] }; +// Form for editing an existing commission type. export default function EditTypeForm({ type, allOptions, @@ -91,7 +91,7 @@ export default function EditTypeForm({ router.push("/commissions/types"); } catch (err) { console.error(err); - toast("Failed to create commission type."); + toast("Failed to update commission type."); } } @@ -167,7 +167,6 @@ export default function EditTypeForm({ - {/* */}
diff --git a/src/components/commissions/types/ListTypes.tsx b/src/components/commissions/types/ListTypes.tsx index 56c3f71..67b5494 100644 --- a/src/components/commissions/types/ListTypes.tsx +++ b/src/components/commissions/types/ListTypes.tsx @@ -1,11 +1,11 @@ -"use client" +"use client"; import { deleteCommissionType } from "@/actions/commissions/types/deleteType"; import { updateCommissionTypeSortOrder } from "@/actions/commissions/types/updateCommissionTypeSortOrder"; 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 { +import type { CommissionCustomInput, CommissionExtra, CommissionOption, @@ -17,15 +17,15 @@ import { import { closestCenter, DndContext, - DragEndEvent, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core"; +import type { DragEndEvent } from "@dnd-kit/core"; import { arrayMove, rectSortingStrategy, - SortableContext + SortableContext, } from "@dnd-kit/sortable"; import { PencilIcon, TrashIcon } from "lucide-react"; import Link from "next/link"; @@ -34,69 +34,69 @@ import SortableItemCard from "./SortableItemCard"; type CommissionTypeWithItems = CommissionType & { options: (CommissionTypeOption & { - option: CommissionOption | null - })[] + option: CommissionOption | null; + })[]; extras: (CommissionTypeExtra & { - extra: CommissionExtra | null - })[], + extra: CommissionExtra | null; + })[]; customInputs: (CommissionTypeCustomInput & { - customInput: CommissionCustomInput - })[] -} + customInput: CommissionCustomInput; + })[]; +}; +// Sortable list of commission types with delete flow. export default function ListTypes({ types }: { types: CommissionTypeWithItems[] }) { - const [items, setItems] = useState(types) - const [isMounted, setIsMounted] = useState(false) - const [dialogOpen, setDialogOpen] = useState(false) - const [deleteTargetId, setDeleteTargetId] = useState(null) - const [isPending, startTransition] = useTransition() + const [items, setItems] = useState(types); + const [isMounted, setIsMounted] = useState(false); + const [dialogOpen, setDialogOpen] = useState(false); + const [deleteTargetId, setDeleteTargetId] = useState(null); + const [isPending, startTransition] = useTransition(); useEffect(() => { - setIsMounted(true) - }, []) + setIsMounted(true); + }, []); - const sensors = useSensors(useSensor(PointerSensor)) + const sensors = useSensors(useSensor(PointerSensor)); const handleDragEnd = async (event: DragEndEvent) => { - const { active, over } = event + const { active, over } = event; - if (!over || active.id === over.id) return + if (!over || active.id === over.id) return; - const oldIndex = items.findIndex((i) => i.id === active.id) - const newIndex = items.findIndex((i) => i.id === over.id) + const oldIndex = items.findIndex((i) => i.id === active.id); + const newIndex = items.findIndex((i) => i.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { - const newItems = arrayMove(items, oldIndex, newIndex) - setItems(newItems) + const newItems = arrayMove(items, oldIndex, newIndex); + setItems(newItems); - await updateCommissionTypeSortOrder(newItems.map((item, i) => ({ id: item.id, sortIndex: i }))) + await updateCommissionTypeSortOrder(newItems.map((item, i) => ({ id: item.id, sortIndex: i }))); } - } + }; const confirmDelete = () => { - if (!deleteTargetId) return + if (!deleteTargetId) return; startTransition(async () => { - await deleteCommissionType(deleteTargetId) - setItems((prev) => prev.filter((i) => i.id !== deleteTargetId)) - setDialogOpen(false) - setDeleteTargetId(null) - }) - } + await deleteCommissionType(deleteTargetId); + setItems((prev) => prev.filter((i) => i.id !== deleteTargetId)); + setDialogOpen(false); + setDeleteTargetId(null); + }); + }; - if (!isMounted) return null + if (!isMounted) return null; return ( <>
i.id)} strategy={rectSortingStrategy}> - {items.map(type => ( + {items.map((type) => ( {type.name} {type.description} -
@@ -158,8 +158,8 @@ export default function ListTypes({ types }: { types: CommissionTypeWithItems[] variant="destructive" className="w-full flex items-center gap-2" onClick={() => { - setDeleteTargetId(type.id) - setDialogOpen(true) + setDeleteTargetId(type.id); + setDialogOpen(true); }} > @@ -167,11 +167,11 @@ export default function ListTypes({ types }: { types: CommissionTypeWithItems[] - + ))} -
+
diff --git a/src/components/commissions/types/NewTypeForm.tsx b/src/components/commissions/types/NewTypeForm.tsx index c0507d8..1e6cedc 100644 --- a/src/components/commissions/types/NewTypeForm.tsx +++ b/src/components/commissions/types/NewTypeForm.tsx @@ -36,6 +36,7 @@ type Props = { tags: Tag[]; }; +// Form for creating a new commission type. export default function NewTypeForm({ options, extras, @@ -56,8 +57,7 @@ export default function NewTypeForm({ async function onSubmit(values: z.infer) { try { - const created = await createCommissionType(values); - console.log("CommissionType created:", created); + await createCommissionType(values); toast("Commission type created."); router.push("/commissions/types"); } catch (err) { diff --git a/src/components/commissions/types/SortableItem.tsx b/src/components/commissions/types/SortableItem.tsx index c504e8d..db96995 100644 --- a/src/components/commissions/types/SortableItem.tsx +++ b/src/components/commissions/types/SortableItem.tsx @@ -1,14 +1,14 @@ -import { - useSortable -} from "@dnd-kit/sortable" -import { CSS } from "@dnd-kit/utilities" +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import type { ReactNode } from "react"; +// Wrapper that makes its child draggable within a sortable list. export default function SortableItem({ id, children, }: { - id: string - children: React.ReactNode + id: string; + children: ReactNode; }) { const { attributes, @@ -17,21 +17,20 @@ export default function SortableItem({ transform, transition, isDragging, - } = useSortable({ id }) + } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), transition, zIndex: isDragging ? 50 : "auto", opacity: isDragging ? 0.7 : 1, - } + }; return (
{children}
- ) + ); } diff --git a/src/components/commissions/types/SortableItemCard.tsx b/src/components/commissions/types/SortableItemCard.tsx index 92dbca5..0b86752 100644 --- a/src/components/commissions/types/SortableItemCard.tsx +++ b/src/components/commissions/types/SortableItemCard.tsx @@ -1,14 +1,15 @@ -"use client" +"use client"; -import { useSortable } from "@dnd-kit/sortable" -import { CSS } from "@dnd-kit/utilities" -import { ReactNode } from "react" +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import type { ReactNode } from "react"; type Props = { - id: string - children: ReactNode -} + id: string; + children: ReactNode; +}; +// Minimal draggable wrapper for card items. export default function SortableItemCard({ id, children }: Props) { const { attributes, @@ -17,19 +18,20 @@ export default function SortableItemCard({ id, children }: Props) { transform, transition, isDragging, - } = useSortable({ id }) + } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), transition, zIndex: isDragging ? 40 : "auto", opacity: isDragging ? 0.7 : 1, - } + }; return (
+ style={style} + >
{children}
- ) + ); } diff --git a/src/components/commissions/types/form/ComboboxCreateable.tsx b/src/components/commissions/types/form/ComboboxCreateable.tsx index ec1f4d8..285db29 100644 --- a/src/components/commissions/types/form/ComboboxCreateable.tsx +++ b/src/components/commissions/types/form/ComboboxCreateable.tsx @@ -1,36 +1,37 @@ -"use client" +"use client"; -import { Button } from "@/components/ui/button" +import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, -} from "@/components/ui/command" +} from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger, -} from "@/components/ui/popover" -import { cn } from "@/lib/utils" -import { Check, ChevronsUpDown, PlusCircle } from "lucide-react" -import { useState } from "react" +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; +import { Check, ChevronsUpDown, PlusCircle } from "lucide-react"; +import { useState } from "react"; type Option = { - label: string - value: string -} + label: string; + value: string; +}; type Props = { - options: Option[] - selected: string | undefined - onSelect: (value: string) => void - onCreateNew: (name: string) => void | Promise - placeholder?: string - disabled?: boolean -} + options: Option[]; + selected: string | undefined; + onSelect: (value: string) => void; + onCreateNew: (name: string) => void | Promise; + placeholder?: string; + disabled?: boolean; +}; +// Combobox with inline "create new" support. export function ComboboxCreateable({ options, selected, @@ -39,18 +40,18 @@ export function ComboboxCreateable({ placeholder = "Select or create…", disabled = false, }: Props) { - const [open, setOpen] = useState(false) - const [input, setInput] = useState("") + const [open, setOpen] = useState(false); + const [input, setInput] = useState(""); - const selectedOption = options.find((o) => o.value === selected) + const selectedOption = options.find((o) => o.value === selected); const filteredOptions = input ? options.filter((opt) => - opt.label.toLowerCase().includes(input.toLowerCase()) - ) - : options + opt.label.toLowerCase().includes(input.toLowerCase()) + ) + : options; - const showCreate = input && !options.some((o) => o.label.toLowerCase() === input.toLowerCase()) + const showCreate = input && !options.some((o) => o.label.toLowerCase() === input.toLowerCase()); return ( @@ -78,8 +79,8 @@ export function ComboboxCreateable({ { - onSelect(opt.value) - setOpen(false) + onSelect(opt.value); + setOpen(false); }} > { - await onCreateNew(input) - setOpen(false) + await onCreateNew(input); + setOpen(false); }} className="text-primary" > @@ -107,5 +108,5 @@ export function ComboboxCreateable({ - ) + ); } diff --git a/src/components/commissions/types/form/CommissionCustomInputField.tsx b/src/components/commissions/types/form/CommissionCustomInputField.tsx index 7c8cb99..c4075ab 100644 --- a/src/components/commissions/types/form/CommissionCustomInputField.tsx +++ b/src/components/commissions/types/form/CommissionCustomInputField.tsx @@ -1,70 +1,71 @@ -"use client" +"use client"; -import { createCommissionCustomInput } from "@/actions/commissions/types/newType" -import { Button } from "@/components/ui/button" +import { createCommissionCustomInput } from "@/actions/commissions/types/newType"; +import { Button } from "@/components/ui/button"; import { FormControl, FormField, FormItem, FormLabel, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select" -import { Switch } from "@/components/ui/switch" -import { CommissionCustomInput } from "@/generated/prisma/client" +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import type { CommissionCustomInput } from "@/generated/prisma/client"; import { closestCenter, DndContext, - DragEndEvent, PointerSensor, useSensor, useSensors, -} from "@dnd-kit/core" +} from "@dnd-kit/core"; +import type { DragEndEvent } from "@dnd-kit/core"; import { SortableContext, verticalListSortingStrategy, -} from "@dnd-kit/sortable" -import { useEffect, useState } from "react" -import { useFieldArray, useFormContext } from "react-hook-form" -import SortableItem from "../SortableItem" -import { ComboboxCreateable } from "./ComboboxCreateable" +} from "@dnd-kit/sortable"; +import { useEffect, useState } from "react"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import SortableItem from "../SortableItem"; +import { ComboboxCreateable } from "./ComboboxCreateable"; type Props = { - customInputs: CommissionCustomInput[] -} + customInputs: CommissionCustomInput[]; +}; +// Form field array for custom inputs with drag-and-drop ordering. export function CommissionCustomInputField({ customInputs: initialInputs }: Props) { - const [mounted, setMounted] = useState(false) - const { control, setValue } = useFormContext() + const [mounted, setMounted] = useState(false); + const { control, setValue } = useFormContext(); const { fields, append, remove, move } = useFieldArray({ control, name: "customInputs", - }) + }); - const [customInputs, setCustomInputs] = useState(initialInputs) - const sensors = useSensors(useSensor(PointerSensor)) + const [customInputs, setCustomInputs] = useState(initialInputs); + const sensors = useSensors(useSensor(PointerSensor)); useEffect(() => { - setMounted(true) - }, []) + setMounted(true); + }, []); const handleDragEnd = (event: DragEndEvent) => { - const { active, over } = event - if (!over || active.id === over.id) return - const oldIndex = fields.findIndex((f) => f.id === active.id) - const newIndex = fields.findIndex((f) => f.id === over.id) + const { active, over } = event; + if (!over || active.id === over.id) return; + const oldIndex = fields.findIndex((f) => f.id === active.id); + const newIndex = fields.findIndex((f) => f.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { - move(oldIndex, newIndex) + move(oldIndex, newIndex); } - } + }; - if (!mounted) return null + if (!mounted) return null; return (
@@ -92,26 +93,26 @@ export function CommissionCustomInputField({ customInputs: initialInputs }: Prop }))} selected={inputField.value} onSelect={(val) => { - const selected = customInputs.find((ci) => ci.id === val) - inputField.onChange(val) + const selected = customInputs.find((ci) => ci.id === val); + inputField.onChange(val); if (selected) { - setValue(`customInputs.${index}.label`, selected.name) - setValue(`customInputs.${index}.inputType`, "text") - setValue(`customInputs.${index}.required`, false) + setValue(`customInputs.${index}.label`, selected.name); + setValue(`customInputs.${index}.inputType`, "text"); + setValue(`customInputs.${index}.required`, false); } }} onCreateNew={async (name) => { - const slug = name.toLowerCase().replace(/\s+/g, "-") + const slug = name.toLowerCase().replace(/\s+/g, "-"); const newInput = await createCommissionCustomInput({ name, fieldId: slug, - }) - setCustomInputs((prev) => [...prev, newInput]) - inputField.onChange(newInput.id) + }); + setCustomInputs((prev) => [...prev, newInput]); + inputField.onChange(newInput.id); - setValue(`customInputs.${index}.label`, newInput.name) - setValue(`customInputs.${index}.inputType`, "text") - setValue(`customInputs.${index}.required`, false) + setValue(`customInputs.${index}.label`, newInput.name); + setValue(`customInputs.${index}.inputType`, "text"); + setValue(`customInputs.${index}.required`, false); }} /> @@ -182,7 +183,7 @@ export function CommissionCustomInputField({ customInputs: initialInputs }: Prop
- ) + ); })} @@ -201,5 +202,5 @@ export function CommissionCustomInputField({ customInputs: initialInputs }: Prop Add Input
- ) + ); } diff --git a/src/components/commissions/types/form/CommissionExtraField.tsx b/src/components/commissions/types/form/CommissionExtraField.tsx index 9b53279..4c45ca2 100644 --- a/src/components/commissions/types/form/CommissionExtraField.tsx +++ b/src/components/commissions/types/form/CommissionExtraField.tsx @@ -1,69 +1,70 @@ -"use client" +"use client"; -import { createCommissionExtra } from "@/actions/commissions/types/newType" -import { Button } from "@/components/ui/button" -import { DualRangeSlider } from "@/components/ui/dual-range" +import { createCommissionExtra } from "@/actions/commissions/types/newType"; +import { Button } from "@/components/ui/button"; +import { DualRangeSlider } from "@/components/ui/dual-range"; import { FormControl, FormField, FormItem, FormLabel, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { CommissionExtra } from "@/generated/prisma/client" +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import type { CommissionExtra } from "@/generated/prisma/client"; import { closestCenter, DndContext, - DragEndEvent, PointerSensor, useSensor, useSensors, -} from "@dnd-kit/core" +} from "@dnd-kit/core"; +import type { DragEndEvent } from "@dnd-kit/core"; import { SortableContext, - verticalListSortingStrategy -} from "@dnd-kit/sortable" -import { useEffect, useState } from "react" -import { useFieldArray, useFormContext } from "react-hook-form" -import SortableItem from "../SortableItem" -import { ComboboxCreateable } from "./ComboboxCreateable" + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { useEffect, useState } from "react"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import SortableItem from "../SortableItem"; +import { ComboboxCreateable } from "./ComboboxCreateable"; type Props = { - extras: CommissionExtra[] -} + extras: CommissionExtra[]; +}; +// Form field array for commission extras with drag-and-drop ordering. export function CommissionExtraField({ extras: initialExtras }: Props) { - const [mounted, setMounted] = useState(false) - const { control } = useFormContext() + const [mounted, setMounted] = useState(false); + const { control } = useFormContext(); const { fields, append, remove, move } = useFieldArray({ control, name: "extras", - }) + }); useEffect(() => { - setMounted(true) - }, []) + setMounted(true); + }, []); - const [extras, setExtras] = useState(initialExtras) + const [extras, setExtras] = useState(initialExtras); - const sensors = useSensors(useSensor(PointerSensor)) + const sensors = useSensors(useSensor(PointerSensor)); const handleDragEnd = (event: DragEndEvent) => { - const { active, over } = event + const { active, over } = event; if (!over || active.id === over.id) { - return + return; } - const oldIndex = fields.findIndex((f) => f.id === active.id) - const newIndex = fields.findIndex((f) => f.id === over.id) + const oldIndex = fields.findIndex((f) => f.id === active.id); + const newIndex = fields.findIndex((f) => f.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { - move(oldIndex, newIndex) + move(oldIndex, newIndex); } - } + }; - if (!mounted) return null + if (!mounted) return null; return (
@@ -90,9 +91,9 @@ export function CommissionExtraField({ extras: initialExtras }: Props) { selected={extraField.value} onSelect={extraField.onChange} onCreateNew={async (name) => { - const newExtra = await createCommissionExtra({ name }) - setExtras((prev) => [...prev, newExtra]) - extraField.onChange(newExtra.id) + const newExtra = await createCommissionExtra({ name }); + setExtras((prev) => [...prev, newExtra]); + extraField.onChange(newExtra.id); }} /> @@ -170,7 +171,7 @@ export function CommissionExtraField({ extras: initialExtras }: Props) { /> - ) + ); }} /> @@ -186,7 +187,7 @@ export function CommissionExtraField({ extras: initialExtras }: Props) { ))} - + -
- ) +
+ ); } diff --git a/src/components/commissions/types/form/CommissionOptionField.tsx b/src/components/commissions/types/form/CommissionOptionField.tsx index 979b99d..8f2aa9b 100644 --- a/src/components/commissions/types/form/CommissionOptionField.tsx +++ b/src/components/commissions/types/form/CommissionOptionField.tsx @@ -1,69 +1,70 @@ -"use client" +"use client"; -import { createCommissionOption } from "@/actions/commissions/types/newType" -import { Button } from "@/components/ui/button" -import { DualRangeSlider } from "@/components/ui/dual-range" +import { createCommissionOption } from "@/actions/commissions/types/newType"; +import { Button } from "@/components/ui/button"; +import { DualRangeSlider } from "@/components/ui/dual-range"; import { FormControl, FormField, FormItem, FormLabel, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { CommissionOption } from "@/generated/prisma/client" +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import type { CommissionOption } from "@/generated/prisma/client"; import { closestCenter, DndContext, - DragEndEvent, PointerSensor, useSensor, useSensors, -} from "@dnd-kit/core" +} from "@dnd-kit/core"; +import type { DragEndEvent } from "@dnd-kit/core"; import { SortableContext, - verticalListSortingStrategy -} from "@dnd-kit/sortable" -import { useEffect, useState } from "react" -import { useFieldArray, useFormContext } from "react-hook-form" -import SortableItem from "../SortableItem" -import { ComboboxCreateable } from "./ComboboxCreateable" + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { useEffect, useState } from "react"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import SortableItem from "../SortableItem"; +import { ComboboxCreateable } from "./ComboboxCreateable"; type Props = { - options: CommissionOption[] -} + options: CommissionOption[]; +}; +// Form field array for commission options with drag-and-drop ordering. export function CommissionOptionField({ options: initialOptions }: Props) { - const [mounted, setMounted] = useState(false) - const { control } = useFormContext() + const [mounted, setMounted] = useState(false); + const { control } = useFormContext(); const { fields, append, remove, move } = useFieldArray({ control, name: "options", - }) + }); useEffect(() => { - setMounted(true) - }, []) + setMounted(true); + }, []); - const [options, setOptions] = useState(initialOptions) + const [options, setOptions] = useState(initialOptions); - const sensors = useSensors(useSensor(PointerSensor)) + const sensors = useSensors(useSensor(PointerSensor)); const handleDragEnd = (event: DragEndEvent) => { - const { active, over } = event + const { active, over } = event; if (!over || active.id === over.id) { - return + return; } - const oldIndex = fields.findIndex((f) => f.id === active.id) - const newIndex = fields.findIndex((f) => f.id === over.id) + const oldIndex = fields.findIndex((f) => f.id === active.id); + const newIndex = fields.findIndex((f) => f.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { - move(oldIndex, newIndex) + move(oldIndex, newIndex); } - } + }; - if (!mounted) return null + if (!mounted) return null; return (
@@ -91,9 +92,9 @@ export function CommissionOptionField({ options: initialOptions }: Props) { selected={optionField.value} onSelect={optionField.onChange} onCreateNew={async (name) => { - const newOption = await createCommissionOption({ name }) - setOptions((prev) => [...prev, newOption]) - optionField.onChange(newOption.id) + const newOption = await createCommissionOption({ name }); + setOptions((prev) => [...prev, newOption]); + optionField.onChange(newOption.id); }} /> @@ -171,7 +172,7 @@ export function CommissionOptionField({ options: initialOptions }: Props) { /> - ) + ); }} /> @@ -204,5 +205,5 @@ export function CommissionOptionField({ options: initialOptions }: Props) { Add Option
- ) + ); } diff --git a/src/components/editor/plugins/basic-blocks-base-kit.tsx b/src/components/editor/plugins/basic-blocks-base-kit.tsx index ce533e8..55aff05 100644 --- a/src/components/editor/plugins/basic-blocks-base-kit.tsx +++ b/src/components/editor/plugins/basic-blocks-base-kit.tsx @@ -22,6 +22,7 @@ import { import { HrElementStatic } from '@/components/ui/hr-node-static'; import { ParagraphElementStatic } from '@/components/ui/paragraph-node-static'; +// Base (static) block plugin bundle for server rendering. export const BaseBasicBlocksKit = [ BaseParagraphPlugin.withComponent(ParagraphElementStatic), BaseH1Plugin.withComponent(H1ElementStatic), diff --git a/src/components/editor/plugins/basic-blocks-kit.tsx b/src/components/editor/plugins/basic-blocks-kit.tsx index 67135cf..3a8319d 100644 --- a/src/components/editor/plugins/basic-blocks-kit.tsx +++ b/src/components/editor/plugins/basic-blocks-kit.tsx @@ -24,6 +24,7 @@ import { import { HrElement } from '@/components/ui/hr-node'; import { ParagraphElement } from '@/components/ui/paragraph-node'; +// Bundle of basic block-level plugins for the editor. export const BasicBlocksKit = [ ParagraphPlugin.withComponent(ParagraphElement), H1Plugin.configure({ diff --git a/src/components/editor/plugins/basic-marks-base-kit.tsx b/src/components/editor/plugins/basic-marks-base-kit.tsx index 7463d1e..f71966d 100644 --- a/src/components/editor/plugins/basic-marks-base-kit.tsx +++ b/src/components/editor/plugins/basic-marks-base-kit.tsx @@ -14,6 +14,7 @@ import { CodeLeafStatic } from '@/components/ui/code-node-static'; import { HighlightLeafStatic } from '@/components/ui/highlight-node-static'; import { KbdLeafStatic } from '@/components/ui/kbd-node-static'; +// Base (static) mark plugin bundle for server rendering. export const BaseBasicMarksKit = [ BaseBoldPlugin, BaseItalicPlugin, diff --git a/src/components/editor/plugins/basic-marks-kit.tsx b/src/components/editor/plugins/basic-marks-kit.tsx index d2fe628..6bce609 100644 --- a/src/components/editor/plugins/basic-marks-kit.tsx +++ b/src/components/editor/plugins/basic-marks-kit.tsx @@ -16,6 +16,7 @@ import { CodeLeaf } from '@/components/ui/code-node'; import { HighlightLeaf } from '@/components/ui/highlight-node'; import { KbdLeaf } from '@/components/ui/kbd-node'; +// Bundle of basic mark-level plugins for the editor. export const BasicMarksKit = [ BoldPlugin, ItalicPlugin, diff --git a/src/components/editor/plugins/basic-nodes-kit.tsx b/src/components/editor/plugins/basic-nodes-kit.tsx index 6f83416..33ea100 100644 --- a/src/components/editor/plugins/basic-nodes-kit.tsx +++ b/src/components/editor/plugins/basic-nodes-kit.tsx @@ -3,4 +3,5 @@ import { BasicBlocksKit } from './basic-blocks-kit'; import { BasicMarksKit } from './basic-marks-kit'; +// Combined basic nodes bundle (blocks + marks). export const BasicNodesKit = [...BasicBlocksKit, ...BasicMarksKit]; diff --git a/src/components/editor/plugins/code-block-base-kit.tsx b/src/components/editor/plugins/code-block-base-kit.tsx index 9a5a69d..a9b693c 100644 --- a/src/components/editor/plugins/code-block-base-kit.tsx +++ b/src/components/editor/plugins/code-block-base-kit.tsx @@ -13,6 +13,7 @@ import { const lowlight = createLowlight(all); +// Base (static) code block plugin bundle. export const BaseCodeBlockKit = [ BaseCodeBlockPlugin.configure({ node: { component: CodeBlockElementStatic }, diff --git a/src/components/editor/plugins/code-block-kit.tsx b/src/components/editor/plugins/code-block-kit.tsx index 74cb748..26cb915 100644 --- a/src/components/editor/plugins/code-block-kit.tsx +++ b/src/components/editor/plugins/code-block-kit.tsx @@ -15,6 +15,7 @@ import { const lowlight = createLowlight(all); +// Code block plugin bundle with syntax highlighting. export const CodeBlockKit = [ CodeBlockPlugin.configure({ node: { component: CodeBlockElement }, diff --git a/src/components/editor/plugins/indent-base-kit.tsx b/src/components/editor/plugins/indent-base-kit.tsx index 8c899a1..769383d 100644 --- a/src/components/editor/plugins/indent-base-kit.tsx +++ b/src/components/editor/plugins/indent-base-kit.tsx @@ -1,6 +1,7 @@ import { BaseIndentPlugin } from '@platejs/indent'; import { KEYS } from 'platejs'; +// Base indent plugin bundle for static rendering. export const BaseIndentKit = [ BaseIndentPlugin.configure({ inject: { diff --git a/src/components/editor/plugins/indent-kit.tsx b/src/components/editor/plugins/indent-kit.tsx index bd9d56f..6a14d73 100644 --- a/src/components/editor/plugins/indent-kit.tsx +++ b/src/components/editor/plugins/indent-kit.tsx @@ -3,6 +3,7 @@ import { IndentPlugin } from '@platejs/indent/react'; import { KEYS } from 'platejs'; +// Indent plugin bundle for interactive editor. export const IndentKit = [ IndentPlugin.configure({ inject: { diff --git a/src/components/editor/plugins/list-base-kit.tsx b/src/components/editor/plugins/list-base-kit.tsx index cdaec2f..af9dca5 100644 --- a/src/components/editor/plugins/list-base-kit.tsx +++ b/src/components/editor/plugins/list-base-kit.tsx @@ -4,6 +4,7 @@ import { KEYS } from 'platejs'; import { BaseIndentKit } from '@/components/editor/plugins/indent-base-kit'; import { BlockListStatic } from '@/components/ui/block-list-static'; +// Base list plugin bundle for static rendering. export const BaseListKit = [ ...BaseIndentKit, BaseListPlugin.configure({ diff --git a/src/components/editor/plugins/list-kit.tsx b/src/components/editor/plugins/list-kit.tsx index 6187179..4708cda 100644 --- a/src/components/editor/plugins/list-kit.tsx +++ b/src/components/editor/plugins/list-kit.tsx @@ -6,6 +6,7 @@ import { KEYS } from 'platejs'; import { IndentKit } from '@/components/editor/plugins/indent-kit'; import { BlockList } from '@/components/ui/block-list'; +// List plugin bundle with indent support. export const ListKit = [ ...IndentKit, ListPlugin.configure({ diff --git a/src/components/editor/plugins/markdown-kit.tsx b/src/components/editor/plugins/markdown-kit.tsx index 45e34d1..72fbb77 100644 --- a/src/components/editor/plugins/markdown-kit.tsx +++ b/src/components/editor/plugins/markdown-kit.tsx @@ -3,6 +3,7 @@ import { KEYS } from 'platejs'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; +// Markdown serialization/deserialization plugin bundle. export const MarkdownKit = [ MarkdownPlugin.configure({ options: { diff --git a/src/components/global/Footer.tsx b/src/components/global/Footer.tsx index 019ad4d..9fc6d5b 100644 --- a/src/components/global/Footer.tsx +++ b/src/components/global/Footer.tsx @@ -1,5 +1,6 @@ import pkg from "../../../package.json"; +// Footer with build/version metadata. const appVersion = process.env.NEXT_PUBLIC_APP_VERSION ?? pkg.version; diff --git a/src/components/global/Header.tsx b/src/components/global/Header.tsx index 72d98f2..07038d6 100644 --- a/src/components/global/Header.tsx +++ b/src/components/global/Header.tsx @@ -1,9 +1,10 @@ - import LogoutButton from "../auth/LogoutButton"; import ModeToggle from "./ModeToggle"; import TopNav from "./TopNav"; -export default async function Header() { +// Possibly unused: no references found in `src/app` or `src/components`. +// Header layout with top navigation and quick actions. +export default function Header() { return (
@@ -13,4 +14,4 @@ export default async function Header() {
); -} \ No newline at end of file +} diff --git a/src/components/global/MobileSidebar.tsx b/src/components/global/MobileSidebar.tsx index 946ad81..8b6d71c 100644 --- a/src/components/global/MobileSidebar.tsx +++ b/src/components/global/MobileSidebar.tsx @@ -13,6 +13,7 @@ import { import Sidebar from "./Sidebar"; +// Drawer-based sidebar for mobile screens. export default function MobileSidebar() { return ( @@ -21,7 +22,6 @@ export default function MobileSidebar() { - Navigation diff --git a/src/components/global/ModeToggle.tsx b/src/components/global/ModeToggle.tsx index 1f6ba76..2a1351c 100644 --- a/src/components/global/ModeToggle.tsx +++ b/src/components/global/ModeToggle.tsx @@ -1,39 +1,40 @@ -"use client" +"use client"; -import { Moon, Sun } from "lucide-react" -import { useTheme } from "next-themes" +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { cn } from "@/lib/utils" -import { useEffect, useState } from "react" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { cn } from "@/lib/utils"; +import { useEffect, useState } from "react"; -const modes = ["light", "dark"] as const -const accents = ["zinc", "red", "rose", "orange", "green", "blue", "yellow", "violet"] as const +const modes = ["light", "dark"] as const; +const accents = ["zinc", "red", "rose", "orange", "green", "blue", "yellow", "violet"] as const; const modeIcons = { light: , dark: , -} +}; +// Theme mode/accent selector for the admin header. export default function ModeToggle() { - const { setTheme, theme } = useTheme() - const [mode, setMode] = useState("dark") - const [accent, setAccent] = useState("violet") + const { setTheme, theme } = useTheme(); + const [mode, setMode] = useState<(typeof modes)[number]>("dark"); + const [accent, setAccent] = useState<(typeof accents)[number]>("violet"); useEffect(() => { - const parts = theme?.split("-") + const parts = theme?.split("-"); if (parts?.length === 2) { - setMode(parts[0]) - setAccent(parts[1]) + setMode(parts[0] as (typeof modes)[number]); + setAccent(parts[1] as (typeof accents)[number]); } - }, [theme]) + }, [theme]); - function updateTheme(newMode: string, newAccent: string) { - const fullTheme = `${newMode}-${newAccent}` - setTheme(fullTheme) + function updateTheme(newMode: (typeof modes)[number], newAccent: (typeof accents)[number]) { + const fullTheme = `${newMode}-${newAccent}`; + setTheme(fullTheme); } - const accentColorMap: Record = { + const accentColorMap: Record<(typeof accents)[number], string> = { zinc: "text-zinc-600", red: "text-red-600", rose: "text-rose-600", @@ -42,15 +43,17 @@ export default function ModeToggle() { blue: "text-blue-600", yellow: "text-yellow-600", violet: "text-violet-600", - } + }; return (
{ - setAccent(value) - updateTheme(mode, value) + if (accents.includes(value as (typeof accents)[number])) { + setAccent(value as (typeof accents)[number]); + updateTheme(mode, value as (typeof accents)[number]); + } }} > @@ -89,5 +94,5 @@ export default function ModeToggle() {
- ) + ); } diff --git a/src/components/global/Sidebar.tsx b/src/components/global/Sidebar.tsx index 48210bd..9268f31 100644 --- a/src/components/global/Sidebar.tsx +++ b/src/components/global/Sidebar.tsx @@ -15,6 +15,7 @@ import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; import { adminNav } from "./nav"; +// Sidebar navigation for admin routes. function isActive(pathname: string, href: string) { if (href === "/") return pathname === "/"; return pathname === href || pathname.startsWith(`${href}/`); diff --git a/src/components/global/ThemeProvider.tsx b/src/components/global/ThemeProvider.tsx index 120faea..ca27992 100644 --- a/src/components/global/ThemeProvider.tsx +++ b/src/components/global/ThemeProvider.tsx @@ -1,11 +1,12 @@ -"use client" +"use client"; -import { ThemeProvider as NextThemesProvider } from "next-themes" -import * as React from "react" +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import type { ComponentProps } from "react"; +// Thin wrapper for next-themes provider. export function ThemeProvider({ children, ...props -}: React.ComponentProps) { - return {children} -} \ No newline at end of file +}: ComponentProps) { + return {children}; +} diff --git a/src/components/global/TopNav.tsx b/src/components/global/TopNav.tsx index d5e4979..2c972b3 100644 --- a/src/components/global/TopNav.tsx +++ b/src/components/global/TopNav.tsx @@ -1,8 +1,10 @@ -"use client" +"use client"; import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu"; import Link from "next/link"; +// Possibly unused: only referenced by `src/components/global/Header.tsx` (which is unused). +// Top navigation menu for larger screens. const uploadItems = [ { title: "Single Image", @@ -12,21 +14,21 @@ const uploadItems = [ title: "Multiple Images", href: "/uploads/bulk", }, -] +]; const artworkItems = [ { title: "Categories", href: "/categories", }, -] +]; const topicItems = [ { title: "Tags", href: "/tags", } -] +]; const commissionItems = [ { @@ -45,7 +47,7 @@ const commissionItems = [ title: "Guidelines", href: "/commissions/guidelines", } -] +]; const usersItems = [ { @@ -56,7 +58,7 @@ const usersItems = [ title: "New User", href: "/users/new", } -] +]; export default function TopNav() { return ( @@ -78,8 +80,6 @@ export default function TopNav() {
{item.title}
-

-

@@ -103,8 +103,6 @@ export default function TopNav() {
{item.title}
-

-

@@ -122,8 +120,6 @@ export default function TopNav() {
{item.title}
-

-

@@ -141,8 +137,6 @@ export default function TopNav() {
{item.title}
-

-

@@ -166,8 +160,6 @@ export default function TopNav() {
{item.title}
-

-

@@ -176,56 +168,6 @@ export default function TopNav() { - {/* - Portfolio - -
    - {portfolioItems.map((item) => ( -
  • - - -
    {item.title}
    -

    -

    - -
    -
  • - ))} -
-
-
- - - Commissions - -
    -
  • - - -
    Types
    -

    -

    - -
    -
  • -
  • - - -
    Board
    -

    -

    - -
    -
  • -
-
-
- - - - ToS - - */} ); diff --git a/src/components/global/nav.ts b/src/components/global/nav.ts index 17b536b..9acc3a6 100644 --- a/src/components/global/nav.ts +++ b/src/components/global/nav.ts @@ -1,3 +1,4 @@ +// Sidebar navigation config for the admin app. export type AdminNavItem = { title: string; href: string; diff --git a/src/components/home/StatCard.tsx b/src/components/home/StatCard.tsx index 9861239..3f273d5 100644 --- a/src/components/home/StatCard.tsx +++ b/src/components/home/StatCard.tsx @@ -1,12 +1,13 @@ import Link from "next/link"; -import * as React from "react"; +import type { ReactNode } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +// Simple stat tile used on the admin dashboard. export function StatCard(props: { title: string; - value: React.ReactNode; - hint?: React.ReactNode; + value: ReactNode; + hint?: ReactNode; href?: string; }) { const inner = ( diff --git a/src/components/home/StatusPill.tsx b/src/components/home/StatusPill.tsx index 72f187a..726bd1a 100644 --- a/src/components/home/StatusPill.tsx +++ b/src/components/home/StatusPill.tsx @@ -1,3 +1,4 @@ +// Small status row for dashboard summary lists. export function StatusPill(props: { label: string; value: number }) { return (
diff --git a/src/components/lists/ItemList.tsx b/src/components/lists/ItemList.tsx index 57cdf6c..9417b86 100644 --- a/src/components/lists/ItemList.tsx +++ b/src/components/lists/ItemList.tsx @@ -1,41 +1,34 @@ "use client"; -// import { deleteItems } from "@/actions/deleteItem"; +// Possibly unused: no references found in `src/app` or `src/components`. +// Generic list card grid (currently with stubbed delete). import { PencilIcon } from "lucide-react"; import Link from "next/link"; import { Button } from "../ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../ui/card"; type ItemProps = { - id: string - name: string - slug: string - description: string | null - sortIndex: number -} - -export default function ItemList({ items, type }: { items: ItemProps[], type: string }) { - // const [isMounted, setIsMounted] = useState(false); - - // useEffect(() => { - // setIsMounted(true); - // }, []); + id: string; + name: string; + slug: string; + description: string | null; + sortIndex: number; +}; +export default function ItemList({ items, type }: { items: ItemProps[]; type: string }) { const handleDelete = (id: string) => { // deleteItems(id, type); }; - // if (!isMounted) return null; - return (
- {items.map(item => ( + {items.map((item) => (
{item.name} - + {/* {item.type === 'image' && ( @@ -72,4 +65,4 @@ export default function ItemList({ items, type }: { items: ItemProps[], type: st
); -} \ No newline at end of file +} diff --git a/src/components/tags/AliasEditor.tsx b/src/components/tags/AliasEditor.tsx index 359d0c2..8c4babe 100644 --- a/src/components/tags/AliasEditor.tsx +++ b/src/components/tags/AliasEditor.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; +// Small editor for managing tag aliases. export default function AliasEditor({ value, onChange, diff --git a/src/components/tags/EditTagForm.tsx b/src/components/tags/EditTagForm.tsx index 26116df..b447054 100644 --- a/src/components/tags/EditTagForm.tsx +++ b/src/components/tags/EditTagForm.tsx @@ -14,7 +14,8 @@ import { import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import type { ArtCategory, Tag, TagAlias } from "@/generated/prisma/client"; -import { type TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema"; +import { tagSchema } from "@/schemas/artworks/tagSchema"; +import type { TagFormInput } from "@/schemas/artworks/tagSchema"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; @@ -30,6 +31,7 @@ import { import { Switch } from "../ui/switch"; import AliasEditor from "./AliasEditor"; +// Form for editing an existing tag. export default function EditTagForm({ tag, categories, @@ -62,8 +64,7 @@ export default function EditTagForm({ async function onSubmit(values: TagFormInput) { try { - const updated = await updateTag(tag.id, values); - console.log("Tag updated:", updated); + await updateTag(tag.id, values); toast("Tag updated."); router.push("/tags"); } catch (err) { diff --git a/src/components/tags/NewTagForm.tsx b/src/components/tags/NewTagForm.tsx index 8d67c06..e10fddb 100644 --- a/src/components/tags/NewTagForm.tsx +++ b/src/components/tags/NewTagForm.tsx @@ -14,7 +14,8 @@ import { import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import type { ArtCategory, Tag } from "@/generated/prisma/client"; -import { type TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema"; +import { tagSchema } from "@/schemas/artworks/tagSchema"; +import type { TagFormInput } from "@/schemas/artworks/tagSchema"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; @@ -30,6 +31,7 @@ import { import { Switch } from "../ui/switch"; import AliasEditor from "./AliasEditor"; +// Form for creating a new tag. export default function NewTagForm({ categories, allTags, @@ -54,8 +56,7 @@ export default function NewTagForm({ async function onSubmit(values: TagFormInput) { try { - const created = await createTag(values); - console.log("Tag created:", created); + await createTag(values); toast("Tag created."); router.push("/tags"); } catch (err) { diff --git a/src/components/tags/TagTableAnimal.tsx b/src/components/tags/TagTableAnimal.tsx index ee7b2fc..cd76817 100644 --- a/src/components/tags/TagTableAnimal.tsx +++ b/src/components/tags/TagTableAnimal.tsx @@ -13,6 +13,7 @@ import { import { PencilIcon, Trash2Icon } from "lucide-react"; import Link from "next/link"; +// Animal Studies tags table with parent/visibility controls. type TagRow = { id: string; name: string; diff --git a/src/components/tags/TagTableMain.tsx b/src/components/tags/TagTableMain.tsx index 4e9ab3d..8a7e44b 100644 --- a/src/components/tags/TagTableMain.tsx +++ b/src/components/tags/TagTableMain.tsx @@ -13,6 +13,7 @@ import { import { PencilIcon } from "lucide-react"; import Link from "next/link"; +// Main tags table for general tags. function Chips({ values, empty = "—", diff --git a/src/components/tags/TagTabs.tsx b/src/components/tags/TagTabs.tsx index cea8e45..a350336 100644 --- a/src/components/tags/TagTabs.tsx +++ b/src/components/tags/TagTabs.tsx @@ -1,7 +1,7 @@ "use client"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import * as React from "react"; +import { useMemo } from "react"; // import TagTableMain from "@/components/tags/TagTableMain"; // import TagTableAnimal from "@/components/tags/TagTableAnimal"; import { Badge } from "@/components/ui/badge"; @@ -20,6 +20,7 @@ export type TagRow = { _count: { artworks: number }; }; +// Tabs for tag management tables. function isAnimalStudiesTag(t: TagRow) { // Recommended: primarily category-based; keep showOnAnimalPage as fallback. const inCategory = t.categories.some((c) => c.name === "Animal Studies"); @@ -28,8 +29,8 @@ function isAnimalStudiesTag(t: TagRow) { } export default function TagTabs({ tags }: { tags: TagRow[] }) { - const animal = React.useMemo(() => tags.filter(isAnimalStudiesTag), [tags]); - const normal = React.useMemo( + const animal = useMemo(() => tags.filter(isAnimalStudiesTag), [tags]); + const normal = useMemo( () => tags.filter((t) => !isAnimalStudiesTag(t)), [tags, animal.length] ); diff --git a/src/components/tos/Editor.tsx b/src/components/tos/Editor.tsx index ffdfb15..ba09916 100644 --- a/src/components/tos/Editor.tsx +++ b/src/components/tos/Editor.tsx @@ -1,18 +1,16 @@ -"use client" +"use client"; -import type { Value } from 'platejs'; - -import { saveTosAction } from '@/actions/tos/saveTos'; -import { BasicBlocksKit } from '@/components/editor/plugins/basic-blocks-kit'; -import { BasicMarksKit } from '@/components/editor/plugins/basic-marks-kit'; -import { CodeBlockKit } from '@/components/editor/plugins/code-block-kit'; -import { ListKit } from '@/components/editor/plugins/list-kit'; -import { MarkdownKit } from '@/components/editor/plugins/markdown-kit'; -import { Editor, EditorContainer } from '@/components/ui/editor'; -import { FixedToolbar } from '@/components/ui/fixed-toolbar'; -import { BulletedListToolbarButton, NumberedListToolbarButton } from '@/components/ui/list-toolbar-button'; -import { MarkToolbarButton } from '@/components/ui/mark-toolbar-button'; -import { ToolbarButton } from '@/components/ui/toolbar'; +import { saveTosAction } from "@/actions/tos/saveTos"; +import { BasicBlocksKit } from "@/components/editor/plugins/basic-blocks-kit"; +import { BasicMarksKit } from "@/components/editor/plugins/basic-marks-kit"; +import { CodeBlockKit } from "@/components/editor/plugins/code-block-kit"; +import { ListKit } from "@/components/editor/plugins/list-kit"; +import { MarkdownKit } from "@/components/editor/plugins/markdown-kit"; +import { Editor, EditorContainer } from "@/components/ui/editor"; +import { FixedToolbar } from "@/components/ui/fixed-toolbar"; +import { BulletedListToolbarButton, NumberedListToolbarButton } from "@/components/ui/list-toolbar-button"; +import { MarkToolbarButton } from "@/components/ui/mark-toolbar-button"; +import { ToolbarButton } from "@/components/ui/toolbar"; import { Bold, Braces, @@ -24,17 +22,16 @@ import { Quote, Save, Strikethrough, - Underline + Underline, } from "lucide-react"; -import { Plate, usePlateEditor } from 'platejs/react'; -import { useEffect } from 'react'; - -const initialValue: Value = [ -]; +import type { Value } from "platejs"; +import { Plate, usePlateEditor } from "platejs/react"; +import { useEffect } from "react"; +const initialValue: Value = []; +// Rich text editor for Terms of Service content. export default function TosEditor({ markdown }: { markdown: string | null }) { - // const [isSaving, setIsSaving] = useState(false); const editor = usePlateEditor({ plugins: [ ...BasicBlocksKit, @@ -49,22 +46,18 @@ export default function TosEditor({ markdown }: { markdown: string | null }) { useEffect(() => { if (markdown && editor.api.markdown.deserialize) { const markdownValue = editor.api.markdown.deserialize(markdown); - console.log(markdownValue); editor.children = markdownValue; } }, [editor, markdown]); const handleSave = async () => { - console.log(editor); if (!editor.api.markdown.serialize) return; - // setIsSaving(true); const markdown = editor.api.markdown.serialize(); await saveTosAction(markdown); - // setIsSaving(false); }; return ( - {/* Provides editor context */} + {/* Blocks */} editor.tf.h1.toggle()} tooltip="Heading 1"> @@ -105,9 +98,9 @@ export default function TosEditor({ markdown }: { markdown: string | null }) { - {/* Styles the editor area */} + ); -} \ No newline at end of file +} diff --git a/src/components/uploads/UploadBulkImageForm.tsx b/src/components/uploads/UploadBulkImageForm.tsx index 2677f03..f3da556 100644 --- a/src/components/uploads/UploadBulkImageForm.tsx +++ b/src/components/uploads/UploadBulkImageForm.tsx @@ -8,11 +8,13 @@ import { fileUploadSchema } from "@/schemas/artworks/imageSchema"; // keep your import { zodResolver } from "@hookform/resolvers/zod"; import Image from "next/image"; import { useRouter } from "next/navigation"; -import React, { useEffect, useMemo, useRef, useState } from "react"; +import type { ChangeEvent } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; -import * as z from "zod/v4"; +import type * as z from "zod/v4"; +// Bulk upload UI with simulated progress and mapping server results back to each file. type UploadStatus = "queued" | "uploading" | "done" | "error"; type UploadItem = { @@ -29,18 +31,6 @@ type BulkResult = | { ok: true; artworkId: string; name: string } | { ok: false; name: string; error: string }; -function extFromName(name: string) { - const idx = name.lastIndexOf("."); - if (idx === -1) return ""; - return name.slice(idx).toLowerCase(); // includes dot -} - -function stripExt(name: string) { - const idx = name.lastIndexOf("."); - if (idx === -1) return name; - return name.slice(0, idx); -} - export default function UploadImagesBulkForm() { const router = useRouter(); @@ -51,18 +41,22 @@ export default function UploadImagesBulkForm() { const [items, setItems] = useState([]); const intervalsRef = useRef>(new Map()); + const itemsRef = useRef([]); const isBusy = useMemo(() => items.some((i) => i.status === "uploading"), [items]); + useEffect(() => { + itemsRef.current = items; + }, [items]); + useEffect(() => { return () => { - // cleanup object urls - for (const it of items) URL.revokeObjectURL(it.previewUrl); + // cleanup object urls created for previews + for (const it of itemsRef.current) URL.revokeObjectURL(it.previewUrl); // cleanup intervals for (const id of intervalsRef.current.values()) window.clearInterval(id); intervalsRef.current.clear(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const startSimProgress = (id: string) => { @@ -97,7 +91,7 @@ export default function UploadImagesBulkForm() { form.reset(); }; - const onFileChange = (e: React.ChangeEvent) => { + const onFileChange = (e: ChangeEvent) => { const files = e.target.files; if (!files || files.length === 0) return; diff --git a/src/components/uploads/UploadImageForm.tsx b/src/components/uploads/UploadImageForm.tsx index f4b7aef..00d940c 100644 --- a/src/components/uploads/UploadImageForm.tsx +++ b/src/components/uploads/UploadImageForm.tsx @@ -8,13 +8,14 @@ import { fileUploadSchema } from "@/schemas/artworks/imageSchema"; import { zodResolver } from "@hookform/resolvers/zod"; import Image from "next/image"; import { useRouter } from "next/navigation"; -import * as React from "react"; +import { type ChangeEvent, useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import * as z from "zod/v4"; type UploadStatus = "empty" | "queued" | "uploading" | "done" | "error"; +// Single image upload form with simulated progress and preview. export default function UploadImageForm() { const router = useRouter(); @@ -23,16 +24,16 @@ export default function UploadImageForm() { defaultValues: { file: undefined }, }); - const [status, setStatus] = React.useState("empty"); - const [progress, setProgress] = React.useState(0); - const [error, setError] = React.useState(null); + const [status, setStatus] = useState("empty"); + const [progress, setProgress] = useState(0); + const [error, setError] = useState(null); - const [file, setFile] = React.useState(null); - const [previewUrl, setPreviewUrl] = React.useState(null); + const [file, setFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); - const intervalRef = React.useRef(null); + const intervalRef = useRef(null); - React.useEffect(() => { + useEffect(() => { return () => { if (previewUrl) URL.revokeObjectURL(previewUrl); if (intervalRef.current) window.clearInterval(intervalRef.current); @@ -64,7 +65,7 @@ export default function UploadImageForm() { form.reset(); }; - const onFileChange = (e: React.ChangeEvent) => { + const onFileChange = (e: ChangeEvent) => { const files = e.target.files; if (!files || files.length === 0) return; diff --git a/src/components/users/CreateUserForm.tsx b/src/components/users/CreateUserForm.tsx index b1b9ae8..50dd752 100644 --- a/src/components/users/CreateUserForm.tsx +++ b/src/components/users/CreateUserForm.tsx @@ -1,8 +1,5 @@ "use client"; -import * as React from "react"; -import { toast } from "sonner"; - import { createUser } from "@/actions/users/createUser"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -13,10 +10,16 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { useState } from "react"; +import { toast } from "sonner"; +// Simple admin form for creating users. export function CreateUserForm() { - const [isSubmitting, setIsSubmitting] = React.useState(false); - const [role, setRole] = React.useState<"user" | "admin">("user"); + const [isSubmitting, setIsSubmitting] = useState(false); + const [role, setRole] = useState<"user" | "admin">("user"); + const setRoleSafe = (value: string) => { + if (value === "user" || value === "admin") setRole(value); + }; async function onSubmit(formData: FormData) { setIsSubmitting(true); @@ -42,7 +45,7 @@ export function CreateUserForm() { - diff --git a/src/components/users/UsersTable.tsx b/src/components/users/UsersTable.tsx index 0873c7e..a5ee08c 100644 --- a/src/components/users/UsersTable.tsx +++ b/src/components/users/UsersTable.tsx @@ -2,7 +2,7 @@ import { MoreHorizontal, Trash2, UserPlus } from "lucide-react"; import Link from "next/link"; -import * as React from "react"; +import { useCallback, useEffect, useState, useTransition } from "react"; import { toast } from "sonner"; import { deleteUser } from "@/actions/users/deleteUser"; @@ -29,6 +29,7 @@ import { } from "@/components/ui/dropdown-menu"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +// Admin users table with delete flow. function RoleBadge({ role }: { role: UsersListRow["role"] }) { return ( @@ -46,20 +47,20 @@ function VerifiedBadge({ value }: { value: boolean }) { } export function UsersTable() { - const [rows, setRows] = React.useState([]); - const [isPending, startTransition] = React.useTransition(); + const [rows, setRows] = useState([]); + const [isPending, startTransition] = useTransition(); - const [deleteOpen, setDeleteOpen] = React.useState(false); - const [deleteTarget, setDeleteTarget] = React.useState<{ id: string; label: string } | null>(null); + const [deleteOpen, setDeleteOpen] = useState(false); + const [deleteTarget, setDeleteTarget] = useState<{ id: string; label: string } | null>(null); - const refresh = React.useCallback(() => { + const refresh = useCallback(() => { startTransition(async () => { const data = await getUsers(); setRows(data); }); }, []); - React.useEffect(() => { + useEffect(() => { refresh(); }, [refresh]); @@ -152,24 +153,6 @@ export function UsersTable() { - {/* Optional resend verification */} - {/* {!u.emailVerified ? ( - { - e.preventDefault(); - startTransition(async () => { - await resendVerification({ email: u.email }); - toast.success("Verification email resent"); - }); - }} - > - Resend verification email - - ) : null} - - {!u.emailVerified ? : null} */} - { diff --git a/src/schemas/artworks/timelapse.ts b/src/schemas/artworks/timelapse.ts new file mode 100644 index 0000000..a5bb39c --- /dev/null +++ b/src/schemas/artworks/timelapse.ts @@ -0,0 +1,39 @@ +import { z } from "zod/v4"; + +export const createArtworkTimelapseUploadSchema = z.object({ + artworkId: z.string().min(1), + fileName: z.string().min(1), + mimeType: z.string().min(1), + sizeBytes: z.number().int().positive(), +}); + +export type CreateArtworkTimelapseUploadInput = z.infer< + typeof createArtworkTimelapseUploadSchema +>; + +export const confirmArtworkTimelapseUploadSchema = z.object({ + artworkId: z.string().min(1), + s3Key: z.string().min(1), + fileName: z.string().min(1), + mimeType: z.string().min(1), + sizeBytes: z.number().int().positive(), +}); + +export type ConfirmArtworkTimelapseUploadInput = z.infer< + typeof confirmArtworkTimelapseUploadSchema +>; + +export const setArtworkTimelapseEnabledSchema = z.object({ + artworkId: z.string().min(1), + enabled: z.boolean(), +}); + +export type SetArtworkTimelapseEnabledInput = z.infer< + typeof setArtworkTimelapseEnabledSchema +>; + +export const deleteArtworkTimelapseSchema = z.object({ + artworkId: z.string().min(1), +}); + +export type DeleteArtworkTimelapseInput = z.infer; diff --git a/src/schemas/auth.ts b/src/schemas/auth.ts new file mode 100644 index 0000000..c0d6b3e --- /dev/null +++ b/src/schemas/auth.ts @@ -0,0 +1,9 @@ +import { z } from "zod/v4"; + +export const registerFirstUserSchema = z.object({ + name: z.string().min(1).max(200), + email: z.string().email().max(320), + password: z.string().min(8).max(128), +}); + +export type RegisterFirstUserInput = z.infer; diff --git a/src/schemas/commissions/publicRequest.ts b/src/schemas/commissions/publicRequest.ts new file mode 100644 index 0000000..a909caa --- /dev/null +++ b/src/schemas/commissions/publicRequest.ts @@ -0,0 +1,34 @@ +import { z } from "zod/v4"; + +export const publicCommissionRequestSchema = z + .object({ + typeId: z.string().min(1).optional().nullable(), + customCardId: z.string().min(1).optional().nullable(), + optionId: z.string().min(1).optional().nullable(), + extraIds: z.array(z.string().min(1)).default([]), + + customerName: z.string().min(1).max(200), + customerEmail: z.string().email().max(320), + customerSocials: z.string().max(2000).optional().nullable(), + message: z.string().min(1).max(20_000), + }) + .superRefine((data, ctx) => { + const hasType = Boolean(data.typeId); + const hasCustom = Boolean(data.customCardId); + if (!hasType && !hasCustom) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["typeId"], + message: "Missing commission type or custom card", + }); + } + if (hasType && hasCustom) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["typeId"], + message: "Only one of typeId or customCardId is allowed", + }); + } + }); + +export type PublicCommissionRequestInput = z.infer; diff --git a/src/schemas/commissions/requestsTable.ts b/src/schemas/commissions/requestsTable.ts new file mode 100644 index 0000000..d0f1533 --- /dev/null +++ b/src/schemas/commissions/requestsTable.ts @@ -0,0 +1,22 @@ +import { z } from "zod/v4"; + +import { commissionStatusSchema } from "./requests"; + +export const triStateSchema = z.enum(["any", "true", "false"]); + +export const sortingSchema = z.array( + z.object({ + id: z.string(), + desc: z.boolean(), + }) +); + +export const filtersSchema = z.object({ + q: z.string().optional(), + email: z.string().optional(), + status: z.union([z.literal("any"), commissionStatusSchema]).default("any"), + hasFiles: triStateSchema.default("any"), +}); + +export type CommissionRequestsTableSorting = z.infer; +export type CommissionRequestsTableFilters = z.infer; diff --git a/src/schemas/commissions/updateRequest.ts b/src/schemas/commissions/updateRequest.ts new file mode 100644 index 0000000..dd5658b --- /dev/null +++ b/src/schemas/commissions/updateRequest.ts @@ -0,0 +1,14 @@ +import { z } from "zod/v4"; + +import { commissionStatusSchema } from "./requests"; + +export const updateCommissionRequestSchema = z.object({ + id: z.string().min(1), + status: commissionStatusSchema, + customerName: z.string().min(1).max(200), + customerEmail: z.string().email().max(320), + customerSocials: z.string().max(2000).optional().nullable(), + message: z.string().min(1).max(20_000), +}); + +export type UpdateCommissionRequestInput = z.infer; diff --git a/src/schemas/commissions/updateRequestStatus.ts b/src/schemas/commissions/updateRequestStatus.ts new file mode 100644 index 0000000..fe85657 --- /dev/null +++ b/src/schemas/commissions/updateRequestStatus.ts @@ -0,0 +1,12 @@ +import { z } from "zod/v4"; + +import { COMMISSION_STATUSES } from "@/lib/commissions/kanban"; + +export const updateCommissionRequestStatusSchema = z.object({ + id: z.string().min(1), + status: z.enum(COMMISSION_STATUSES), +}); + +export type UpdateCommissionRequestStatusInput = z.infer< + typeof updateCommissionRequestStatusSchema +>; diff --git a/src/schemas/users.ts b/src/schemas/users.ts new file mode 100644 index 0000000..c068f43 --- /dev/null +++ b/src/schemas/users.ts @@ -0,0 +1,16 @@ +import { z } from "zod/v4"; + +export const createUserSchema = z.object({ + name: z.string().min(1).max(200), + email: z.string().email().max(320), + password: z.string().min(8).max(128), + role: z.enum(["user", "admin"]).default("user"), +}); + +export type CreateUserInput = z.infer; + +export const resendVerificationSchema = z.object({ + email: z.string().email(), +}); + +export type ResendVerificationInput = z.infer; diff --git a/src/types/Artwork.ts b/src/types/Artwork.ts index d30d36d..d2b68ef 100644 --- a/src/types/Artwork.ts +++ b/src/types/Artwork.ts @@ -17,3 +17,9 @@ export type ArtworkWithRelations = Prisma.ArtworkGetPayload<{ export type CategoryWithTags = Prisma.ArtCategoryGetPayload<{ include: { tagLinks: { include: { tag: true } } }; }>; + +export type GalleryVariantStats = { + total: number; + withGallery: number; + missing: number; +}; diff --git a/src/types/auth.ts b/src/types/auth.ts new file mode 100644 index 0000000..311c98a --- /dev/null +++ b/src/types/auth.ts @@ -0,0 +1,7 @@ +export type SessionWithRole = + | { user?: { role?: "admin" | "user"; id?: string } } + | null; + +export type SignUpResponse = + | { user?: { id?: string } } + | { data?: { user?: { id?: string }; id?: string } }; diff --git a/src/types/colors.ts b/src/types/colors.ts new file mode 100644 index 0000000..5e4dc01 --- /dev/null +++ b/src/types/colors.ts @@ -0,0 +1,16 @@ +export type ArtworkColorStats = { + total: number; + ready: number; + pending: number; + processing: number; + failed: number; + missingSortKey: number; +}; + +export type ProcessColorsResult = { + picked: number; + processed: number; + ok: number; + failed: number; + results: Array<{ artworkId: string; ok: boolean; error?: string }>; +}; diff --git a/src/types/commissions.ts b/src/types/commissions.ts new file mode 100644 index 0000000..339c889 --- /dev/null +++ b/src/types/commissions.ts @@ -0,0 +1,13 @@ +export type CommissionCustomCardImageItem = { + key: string; + url: string; + size: number | null; + lastModified: string | null; +}; + +export type CommissionExampleItem = { + key: string; + url: string; + size: number | null; + lastModified: string | null; +}; diff --git a/src/types/dashboard.ts b/src/types/dashboard.ts new file mode 100644 index 0000000..601698d --- /dev/null +++ b/src/types/dashboard.ts @@ -0,0 +1,3 @@ +export type CountRow = { + [P in K]: string; +} & { _count: { _all: number } }; diff --git a/src/types/pagination.ts b/src/types/pagination.ts new file mode 100644 index 0000000..4a3117c --- /dev/null +++ b/src/types/pagination.ts @@ -0,0 +1 @@ +export type CursorPagination = { pageIndex: number; pageSize: number }; diff --git a/src/types/s3.ts b/src/types/s3.ts new file mode 100644 index 0000000..93554b0 --- /dev/null +++ b/src/types/s3.ts @@ -0,0 +1,3 @@ +import type { Readable } from "stream"; + +export type S3Body = Readable | ReadableStream | Blob | Uint8Array; diff --git a/src/types/uploads.ts b/src/types/uploads.ts new file mode 100644 index 0000000..23eeb0d --- /dev/null +++ b/src/types/uploads.ts @@ -0,0 +1,3 @@ +export type BulkResult = + | { ok: true; artworkId: string; name: string } + | { ok: false; name: string; error: string }; diff --git a/src/types/users.ts b/src/types/users.ts new file mode 100644 index 0000000..9790f46 --- /dev/null +++ b/src/types/users.ts @@ -0,0 +1,9 @@ +export type UsersListRow = { + id: string; + name: string | null; + email: string; + role: "admin" | "user"; + emailVerified: boolean; + createdAt: string; + updatedAt: string; +};