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 && }
+
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}
- //
- //
- //
- //
diff --git a/src/app/(admin)/page.tsx b/src/app/(admin)/page.tsx
index fe06424..7ca13fc 100644
--- a/src/app/(admin)/page.tsx
+++ b/src/app/(admin)/page.tsx
@@ -6,14 +6,7 @@ import { StatusPill } from "@/components/home/StatusPill";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-function fmtDate(d: Date) {
- return new Intl.DateTimeFormat("de-DE", {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- }).format(d);
-}
-
+// Admin dashboard summary page.
export default async function HomePage() {
const data = await getAdminDashboard();
@@ -28,9 +21,6 @@ export default async function HomePage() {
- {/*
- Add artwork
- */}
Review requests
@@ -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
-
- Open
-
-
-
- {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" : ""}
-
-
-
- Open
-
-
- ))}
-
- )}
-
-
-
-
-
- Recent commission requests
-
- Open
-
-
-
- {data.commissions.recent.length === 0 ? (
-
- No commission requests yet.
-
- ) : (
-
- {data.commissions.recent.map((r) => (
-
-
-
- {r.customerName}{" "}
-
- ({r.customerEmail})
-
-
-
- {fmtDate(r.createdAt)} · {r.status}
-
-
-
- Open
-
-
- ))}
-
- )}
-
-
- */}
);
}
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() {
/>
-
+
-
+
,
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 }
/>
Submit
- router.back()}>Cancel
+ router.back()}>
+ Cancel
+
-
+
);
-}
\ 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() {
/>
Submit
- router.back()}>Cancel
+ router.back()}>
+ Cancel
+
-
+
);
-}
\ 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 */}
+
Example image
@@ -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 ? (
-
-
-
- Download all (ZIP)
-
-
- ) : null} */}
-
{isSaving ? "Saving…" : "Save changes"}
-
- {/*
setDeleteOpen(true)}
- >
- Delete
- */}
@@ -189,7 +148,12 @@ export function CommissionRequestEditor({ request }: { request: RequestShape })
Status
-
setStatus(v as CommissionStatus)}>
+ {
+ 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 (
+
{status}
);
}
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]);
@@ -128,6 +130,8 @@ const STATUS_OPTIONS: CommissionStatus[] = [
"COMPLETED",
"SPAM",
];
+const isCommissionStatus = (value: string): value is CommissionStatus =>
+ STATUS_OPTIONS.includes(value as CommissionStatus);
type Filters = {
q: string;
@@ -137,13 +141,13 @@ type Filters = {
};
export function CommissionRequestsTable() {
- const [sorting, setSorting] = React.useState([
+ const [sorting, setSorting] = useState([
{ id: "createdAt", 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({
q: "",
email: "",
status: "any",
@@ -153,21 +157,21 @@ export function CommissionRequestsTable() {
const debouncedQ = useDebouncedValue(filters.q, 300);
const debouncedEmail = useDebouncedValue(filters.email, 300);
- const [rows, setRows] = React.useState([]);
- const [total, setTotal] = React.useState(0);
+ const [rows, setRows] = useState([]);
+ const [total, setTotal] = useState(0);
- const [isPending, startTransition] = React.useTransition();
+ const [isPending, startTransition] = useTransition();
// Delete dialog
- const [deleteOpen, setDeleteOpen] = React.useState(false);
- const [deleteTarget, setDeleteTarget] = React.useState<{
+ const [deleteOpen, setDeleteOpen] = useState(false);
+ const [deleteTarget, setDeleteTarget] = useState<{
id: string;
label: string;
} | null>(null);
const pageCount = Math.max(1, Math.ceil(total / pageSize));
- const refresh = React.useCallback(() => {
+ const refresh = useCallback(() => {
startTransition(async () => {
const res = await getCommissionRequestsTablePage({
pagination: { pageIndex, pageSize },
@@ -193,11 +197,11 @@ export function CommissionRequestsTable() {
filters.hasFiles,
]);
- React.useEffect(() => {
+ useEffect(() => {
refresh();
}, [refresh]);
- const columns = React.useMemo[]>(() => [
+ const columns = useMemo[]>(() => [
{
accessorKey: "index",
header: "Index",
@@ -425,7 +429,10 @@ export function CommissionRequestsTable() {
{
- setFilters((f) => ({ ...f, status: v as any }));
+ setFilters((f) => ({
+ ...f,
+ status: v === "any" ? "any" : isCommissionStatus(v) ? v : f.status,
+ }));
setPageIndex(0);
}}
>
diff --git a/src/components/commissions/requests/RequestsTable.tsx b/src/components/commissions/requests/RequestsTable.tsx
index eeb1db3..57041b1 100644
--- a/src/components/commissions/requests/RequestsTable.tsx
+++ b/src/components/commissions/requests/RequestsTable.tsx
@@ -1,9 +1,8 @@
-"use client"
+"use client";
-import { deleteCommissionRequest } from "@/actions/commissions/requests/deleteCommissionRequest";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
-import { CommissionRequest } from "@/generated/prisma/client";
+import type { CommissionRequest } from "@/generated/prisma/client";
import { PencilIcon } from "lucide-react";
import Link from "next/link";
@@ -13,11 +12,8 @@ type CommissionRequestWithCount = CommissionRequest & {
};
};
+// Legacy requests table (used by the server-rendered page).
export default function RequestsTable({ requests }: { requests: CommissionRequestWithCount[] }) {
- const handleDelete = (id: string) => {
- deleteCommissionRequest(id);
- };
-
return (
@@ -59,15 +55,6 @@ export default function RequestsTable({ requests }: { requests: CommissionReques
-
- {/* handleDelete(r.id)}
- >
-
- */}
@@ -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({
- {/* */}
Submit
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 (
- )
+ );
}
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) {
))}
-
+
Add Extra
-
- )
+
+ );
}
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 (
{
- setMode(value)
- updateTheme(value, accent)
+ if (value === "light" || value === "dark") {
+ setMode(value);
+ updateTheme(value, accent);
+ }
}}
>
@@ -73,8 +76,10 @@ export default function ModeToggle() {
{
- 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() {
- setRole(v as any)}>
+
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;
+};