diff --git a/next.config.ts b/next.config.ts index 6a30769..45204c9 100644 --- a/next.config.ts +++ b/next.config.ts @@ -5,7 +5,12 @@ const nextConfig: NextConfig = { }; module.exports = { + experimental: { + serverActions: { + bodySizeLimit: '50mb', + }, + }, output: "standalone", -}; +} export default nextConfig; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f299ce0..24fb1fd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -360,6 +360,12 @@ model CommissionRequest { customerName String customerEmail String message String + status String @default("NEW") // NEW | REVIEWING | ACCEPTED | REJECTED | SPAM + + customerSocials String? + ipAddress String? + userAgent String? + customFields Json? optionId String? typeId String? @@ -367,6 +373,7 @@ model CommissionRequest { type CommissionType? @relation(fields: [typeId], references: [id]) extras CommissionExtra[] + files CommissionRequestFile[] } model CommissionGuidelines { @@ -380,6 +387,21 @@ model CommissionGuidelines { @@index([isActive]) } +model CommissionRequestFile { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + fileKey String @unique + originalFile String + fileType String + fileSize Int + uploadDate DateTime + + requestId String + request CommissionRequest @relation(fields: [requestId], references: [id], onDelete: Cascade) +} + model TermsOfService { id String @id @default(cuid()) createdAt DateTime @default(now()) diff --git a/src/actions/commissions/submitCommissionRequest.ts b/src/actions/commissions/submitCommissionRequest.ts new file mode 100644 index 0000000..705dfbc --- /dev/null +++ b/src/actions/commissions/submitCommissionRequest.ts @@ -0,0 +1,88 @@ +"use server"; + +import { z } from "zod"; + +/** + * Server action + * Forwards a multipart/form-data request (payload + files[]) + * from the public app to the admin app's public commissions endpoint. + * + * Server-only env required: + * ADMIN_URL=https://admin.domain.com + */ + +const submitPayloadSchema = z.object({ + typeId: z.string().optional().nullable(), + optionId: z.string().optional().nullable(), + extraIds: z.array(z.string()).default([]), + + customerName: z.string().min(1), + customerEmail: z.string().email(), + customerSocials: z.string().optional().nullable(), + message: z.string().min(1), +}); + +export type SubmitCommissionPayload = z.infer; + +export async function submitCommissionRequest(input: { + payload: SubmitCommissionPayload; + files: File[]; +}) { + const adminUrl = process.env.ADMIN_URL; + if (!adminUrl) { + throw new Error("ADMIN_URL is not set on the server"); + } + + const payload = submitPayloadSchema.parse(input.payload); + const files = input.files ?? []; + + // Optional safety limits + const MAX_FILES = 10; + const MAX_BYTES_EACH = 10 * 1024 * 1024; // 10MB + + if (files.length > MAX_FILES) { + throw new Error("Too many files"); + } + + for (const f of files) { + if (f.size > MAX_BYTES_EACH) { + throw new Error(`File too large: ${f.name}`); + } + } + + const fd = new FormData(); + fd.set("payload", JSON.stringify(payload)); + + for (const file of files) { + fd.append("files", file, file.name); + } + + const res = await fetch( + `${adminUrl.replace(/\/$/, "")}/api/v1/commissions`, + { + method: "POST", + body: fd, + cache: "no-store", + } + ); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + let parsed: any = null; + try { + parsed = text ? JSON.parse(text) : null; + } catch { + // ignore + } + + const message = + parsed?.error ?? + parsed?.message ?? + (text ? text.slice(0, 300) : `Request failed (${res.status})`); + + throw new Error(message); + } + + // Expected response: { id: string; createdAt: string } + return (await res.json()) as { id: string; createdAt: string }; +} diff --git a/src/app/(normal)/artworks/animalstudies/page.tsx b/src/app/(normal)/artworks/animalstudies/page.tsx index 4b53f76..bd8c365 100644 --- a/src/app/(normal)/artworks/animalstudies/page.tsx +++ b/src/app/(normal)/artworks/animalstudies/page.tsx @@ -70,6 +70,9 @@ export default async function AnimalStudiesPage({ searchParams }: { searchParams metadata: true, tags: true, variants: true, + colors: { + select: { color: { select: { hex: true } } } + } }, orderBy: [{ sortKey: "asc" }, { id: "asc" }], }); diff --git a/src/app/(normal)/commissions/page.tsx b/src/app/(normal)/commissions/page.tsx index 32f60dd..8e60a91 100644 --- a/src/app/(normal)/commissions/page.tsx +++ b/src/app/(normal)/commissions/page.tsx @@ -14,7 +14,7 @@ export default async function CommissionsPage() { }) return ( -
+

Commission Pricing

{commissions.map((commission) => ( diff --git a/src/app/(normal)/tos/page.tsx b/src/app/(normal)/tos/page.tsx index 8d48bf4..e6c9fd2 100644 --- a/src/app/(normal)/tos/page.tsx +++ b/src/app/(normal)/tos/page.tsx @@ -9,7 +9,7 @@ export default async function TosPage() { // console.log(tos?.markdown) return ( -
+
{tos?.markdown}
diff --git a/src/components/artworks/ArtworkThumbGallery.tsx b/src/components/artworks/ArtworkThumbGallery.tsx index eeb7193..439332f 100644 --- a/src/components/artworks/ArtworkThumbGallery.tsx +++ b/src/components/artworks/ArtworkThumbGallery.tsx @@ -12,6 +12,7 @@ type ArtworkGalleryItem = { file: { fileKey: string }; metadata: { width: number; height: number } | null; tags: { id: string; name: string }[]; + colors: { color: { hex: string | null } }[]; }; type FitMode = @@ -87,6 +88,7 @@ export default function ArtworkThumbGallery({ aspectRatio={`${w} / ${h}`} className="h-full w-full rounded-md" imageClassName="object-cover" + style={{ ["--dom" as any]: a.colors[0]?.color?.hex ?? "#999999", }} sizes="(min-width: 1280px) 20vw, (min-width: 1024px) 25vw, (min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw" /> diff --git a/src/components/commissions/CommissionOrderForm.tsx b/src/components/commissions/CommissionOrderForm.tsx index c9bc453..518d269 100644 --- a/src/components/commissions/CommissionOrderForm.tsx +++ b/src/components/commissions/CommissionOrderForm.tsx @@ -1,5 +1,6 @@ "use client" +import { submitCommissionRequest } from "@/actions/commissions/submitCommissionRequest" import { Button } from "@/components/ui/button" import { Form, @@ -15,6 +16,7 @@ import { CommissionCustomInput, CommissionExtra, CommissionOption, CommissionTyp import { commissionOrderSchema } from "@/schemas/commissionOrder" import { calculatePriceRange } from "@/utils/calculatePrice" import { zodResolver } from "@hookform/resolvers/zod" +import "dotenv/config" import Link from "next/link" import { useMemo, useState } from "react" import { useForm, useWatch } from "react-hook-form" @@ -71,8 +73,22 @@ export function CommissionOrderForm({ types }: Props) { }, [selectedOption, selectedExtras]) async function onSubmit(values: z.infer) { - const { customFields, ...rest } = values - console.log("Submit:", { ...rest, customFields, files }) + const payload = { + typeId: values.typeId || null, + optionId: values.optionId || null, + customerName: values.customerName, + customerEmail: values.customerEmail, + customerSocials: values.customerSocials ?? null, + message: values.message, + extraIds: values.extraIds ?? [], // <-- normalize + }; + + const res = await submitCommissionRequest({ + payload, + files, + }); + + console.log("Created request:", res); } return (