Add functions to commission form
This commit is contained in:
@ -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;
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
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,
|
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" }],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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) => (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user