Add functions to commission form

This commit is contained in:
2026-01-01 09:46:43 +01:00
parent 21faef78ee
commit 84470aa2e2
8 changed files with 141 additions and 5 deletions

View File

@ -5,7 +5,12 @@ const nextConfig: NextConfig = {
}; };
module.exports = { module.exports = {
experimental: {
serverActions: {
bodySizeLimit: '50mb',
},
},
output: "standalone", output: "standalone",
}; }
export default nextConfig; export default nextConfig;

View File

@ -360,6 +360,12 @@ model CommissionRequest {
customerName String customerName String
customerEmail String customerEmail String
message String message String
status String @default("NEW") // NEW | REVIEWING | ACCEPTED | REJECTED | SPAM
customerSocials String?
ipAddress String?
userAgent String?
customFields Json?
optionId String? optionId String?
typeId String? typeId String?
@ -367,6 +373,7 @@ model CommissionRequest {
type CommissionType? @relation(fields: [typeId], references: [id]) type CommissionType? @relation(fields: [typeId], references: [id])
extras CommissionExtra[] extras CommissionExtra[]
files CommissionRequestFile[]
} }
model CommissionGuidelines { model CommissionGuidelines {
@ -380,6 +387,21 @@ model CommissionGuidelines {
@@index([isActive]) @@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 { model TermsOfService {
id String @id @default(cuid()) id String @id @default(cuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())

View File

@ -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<typeof submitPayloadSchema>;
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 };
}

View File

@ -70,6 +70,9 @@ export default async function AnimalStudiesPage({ searchParams }: { searchParams
metadata: true, metadata: true,
tags: true, tags: true,
variants: true, variants: true,
colors: {
select: { color: { select: { hex: true } } }
}
}, },
orderBy: [{ sortKey: "asc" }, { id: "asc" }], orderBy: [{ sortKey: "asc" }, { id: "asc" }],
}); });

View File

@ -14,7 +14,7 @@ export default async function CommissionsPage() {
}) })
return ( return (
<div className="container py-10 space-y-10"> <div className="mx-auto w-full max-w-6xl px-4 py-8 flex flex-col gap-8">
<h1 className="text-3xl font-bold">Commission Pricing</h1> <h1 className="text-3xl font-bold">Commission Pricing</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 items-start"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 items-start">
{commissions.map((commission) => ( {commissions.map((commission) => (

View File

@ -9,7 +9,7 @@ export default async function TosPage() {
// console.log(tos?.markdown) // console.log(tos?.markdown)
return ( return (
<div className="container py-10 space-y-10"> <div className="mx-auto w-full max-w-6xl px-4 py-8">
<div className="markdown"> <div className="markdown">
<ReactMarkdown>{tos?.markdown}</ReactMarkdown> <ReactMarkdown>{tos?.markdown}</ReactMarkdown>
</div> </div>

View File

@ -12,6 +12,7 @@ type ArtworkGalleryItem = {
file: { fileKey: string }; file: { fileKey: string };
metadata: { width: number; height: number } | null; metadata: { width: number; height: number } | null;
tags: { id: string; name: string }[]; tags: { id: string; name: string }[];
colors: { color: { hex: string | null } }[];
}; };
type FitMode = type FitMode =
@ -87,6 +88,7 @@ export default function ArtworkThumbGallery({
aspectRatio={`${w} / ${h}`} aspectRatio={`${w} / ${h}`}
className="h-full w-full rounded-md" className="h-full w-full rounded-md"
imageClassName="object-cover" 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" sizes="(min-width: 1280px) 20vw, (min-width: 1024px) 25vw, (min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw"
/> />

View File

@ -1,5 +1,6 @@
"use client" "use client"
import { submitCommissionRequest } from "@/actions/commissions/submitCommissionRequest"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Form, Form,
@ -15,6 +16,7 @@ import { CommissionCustomInput, CommissionExtra, CommissionOption, CommissionTyp
import { commissionOrderSchema } from "@/schemas/commissionOrder" import { commissionOrderSchema } from "@/schemas/commissionOrder"
import { calculatePriceRange } from "@/utils/calculatePrice" import { calculatePriceRange } from "@/utils/calculatePrice"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import "dotenv/config"
import Link from "next/link" import Link from "next/link"
import { useMemo, useState } from "react" import { useMemo, useState } from "react"
import { useForm, useWatch } from "react-hook-form" import { useForm, useWatch } from "react-hook-form"
@ -71,8 +73,22 @@ export function CommissionOrderForm({ types }: Props) {
}, [selectedOption, selectedExtras]) }, [selectedOption, selectedExtras])
async function onSubmit(values: z.infer<typeof commissionOrderSchema>) { async function onSubmit(values: z.infer<typeof commissionOrderSchema>) {
const { customFields, ...rest } = values const payload = {
console.log("Submit:", { ...rest, customFields, files }) 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 ( return (