Add functions to commission form
This commit is contained in:
@ -5,7 +5,12 @@ const nextConfig: NextConfig = {
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '50mb',
|
||||
},
|
||||
},
|
||||
output: "standalone",
|
||||
};
|
||||
}
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@ -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())
|
||||
|
||||
88
src/actions/commissions/submitCommissionRequest.ts
Normal file
88
src/actions/commissions/submitCommissionRequest.ts
Normal 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 };
|
||||
}
|
||||
@ -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" }],
|
||||
});
|
||||
|
||||
@ -14,7 +14,7 @@ export default async function CommissionsPage() {
|
||||
})
|
||||
|
||||
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>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 items-start">
|
||||
{commissions.map((commission) => (
|
||||
|
||||
@ -9,7 +9,7 @@ export default async function TosPage() {
|
||||
// console.log(tos?.markdown)
|
||||
|
||||
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">
|
||||
<ReactMarkdown>{tos?.markdown}</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
|
||||
|
||||
@ -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<typeof commissionOrderSchema>) {
|
||||
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 (
|
||||
|
||||
Reference in New Issue
Block a user