From af5e2dd590b50a68f93ad628ecf26f84a540ab81 Mon Sep 17 00:00:00 2001 From: Citali Date: Thu, 1 Jan 2026 11:53:13 +0100 Subject: [PATCH] Add visible feedback and error handling to commission requests --- .../commissions/submitCommissionRequest.ts | 32 ++- .../commissions/CommissionOrderForm.tsx | 245 +++++++++--------- src/components/commissions/FileDropzone.tsx | 107 +++++--- 3 files changed, 223 insertions(+), 161 deletions(-) diff --git a/src/actions/commissions/submitCommissionRequest.ts b/src/actions/commissions/submitCommissionRequest.ts index 705dfbc..e7c51f5 100644 --- a/src/actions/commissions/submitCommissionRequest.ts +++ b/src/actions/commissions/submitCommissionRequest.ts @@ -67,22 +67,32 @@ export async function submitCommissionRequest(input: { ); if (!res.ok) { - const text = await res.text().catch(() => ""); - let parsed: any = null; + const raw = await res.text().catch(() => ""); + const statusLine = `${res.status} ${res.statusText || ""}`.trim(); + + // Show something useful even if raw is empty + let message = `Admin API error: ${statusLine}`; + + if (raw) { try { - parsed = text ? JSON.parse(text) : null; + const parsed = JSON.parse(raw); + message = + parsed?.error + ? `Admin API error: ${statusLine} — ${parsed.error}` + : parsed?.message + ? `Admin API error: ${statusLine} — ${parsed.message}` + : `Admin API error: ${statusLine} — ${raw}`; } catch { - // ignore + message = `Admin API error: ${statusLine} — ${raw}`; } - - const message = - parsed?.error ?? - parsed?.message ?? - (text ? text.slice(0, 300) : `Request failed (${res.status})`); - - throw new Error(message); } + // Log full body server-side for debugging (safe; this is server-only) + console.error("[submitCommissionRequest] upstream error", { statusLine, raw }); + + throw new Error(message); +} + // Expected response: { id: string; createdAt: string } return (await res.json()) as { id: string; createdAt: string }; } diff --git a/src/components/commissions/CommissionOrderForm.tsx b/src/components/commissions/CommissionOrderForm.tsx index 518d269..7a27b95 100644 --- a/src/components/commissions/CommissionOrderForm.tsx +++ b/src/components/commissions/CommissionOrderForm.tsx @@ -1,7 +1,7 @@ -"use client" +"use client"; -import { submitCommissionRequest } from "@/actions/commissions/submitCommissionRequest" -import { Button } from "@/components/ui/button" +import { submitCommissionRequest } from "@/actions/commissions/submitCommissionRequest"; +import { Button } from "@/components/ui/button"; import { Form, FormControl, @@ -9,29 +9,37 @@ import { FormItem, FormLabel, FormMessage, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { Textarea } from "@/components/ui/textarea" -import { CommissionCustomInput, CommissionExtra, CommissionOption, CommissionType, CommissionTypeCustomInput, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma/client" -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" -import * as z from "zod/v4" -import { FileDropzone } from "./FileDropzone" +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { + CommissionCustomInput, + CommissionExtra, + CommissionOption, + CommissionType, + CommissionTypeCustomInput, + CommissionTypeExtra, + CommissionTypeOption, +} from "@/generated/prisma/client"; +import { commissionOrderSchema } from "@/schemas/commissionOrder"; +import { calculatePriceRange } from "@/utils/calculatePrice"; +import { zodResolver } from "@hookform/resolvers/zod"; +import Link from "next/link"; +import { useMemo, useState } from "react"; +import { useForm, useWatch } from "react-hook-form"; +import { toast } from "sonner"; +import * as z from "zod/v4"; +import { FileDropzone } from "./FileDropzone"; type CommissionTypeWithRelations = CommissionType & { - options: (CommissionTypeOption & { option: CommissionOption })[] - extras: (CommissionTypeExtra & { extra: CommissionExtra })[] - customInputs: (CommissionTypeCustomInput & { customInput: CommissionCustomInput })[] -} + options: (CommissionTypeOption & { option: CommissionOption })[]; + extras: (CommissionTypeExtra & { extra: CommissionExtra })[]; + customInputs: (CommissionTypeCustomInput & { customInput: CommissionCustomInput })[]; +}; type Props = { - types: CommissionTypeWithRelations[] -} + types: CommissionTypeWithRelations[]; +}; export function CommissionOrderForm({ types }: Props) { const form = useForm>({ @@ -45,50 +53,71 @@ export function CommissionOrderForm({ types }: Props) { customerSocials: "", message: "", }, - }) + }); - const [files, setFiles] = useState([]) + const [files, setFiles] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); - const typeId = useWatch({ control: form.control, name: "typeId" }) - const optionId = useWatch({ control: form.control, name: "optionId" }) - const extraIds = useWatch({ control: form.control, name: "extraIds" }) + const typeId = useWatch({ control: form.control, name: "typeId" }); + const optionId = useWatch({ control: form.control, name: "optionId" }); + const extraIds = useWatch({ control: form.control, name: "extraIds" }); - const selectedType = useMemo( - () => types.find((t) => t.id === typeId), - [types, typeId] - ) + const selectedType = useMemo(() => types.find((t) => t.id === typeId), [types, typeId]); const selectedOption = useMemo( () => selectedType?.options.find((o) => o.optionId === optionId), [selectedType, optionId] - ) + ); const selectedExtras = useMemo( () => selectedType?.extras.filter((e) => extraIds?.includes(e.extraId)) ?? [], [selectedType, extraIds] - ) + ); const [minPrice, maxPrice] = useMemo(() => { - return calculatePriceRange(selectedOption, selectedExtras) - }, [selectedOption, selectedExtras]) + return calculatePriceRange(selectedOption, selectedExtras); + }, [selectedOption, selectedExtras]); async function onSubmit(values: z.infer) { - 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 - }; + setIsSubmitting(true); - const res = await submitCommissionRequest({ - payload, - files, - }); + try { + const payload = { + typeId: values.typeId || null, + optionId: values.optionId || null, + extraIds: values.extraIds ?? [], + customerName: values.customerName, + customerEmail: values.customerEmail, + customerSocials: values.customerSocials ?? null, + message: values.message, + }; - console.log("Created request:", res); + await submitCommissionRequest({ payload, files }); + + toast.success("Request submitted", { + description: "Thanks! I’ll get back to you as soon as possible.", + }); + + form.reset({ + typeId: "", + optionId: "", + extraIds: [], + customerName: "", + customerEmail: "", + customerSocials: "", + message: "", + }); + + setFiles([]); + form.clearErrors(); + } catch (err) { + const message = + err instanceof Error ? err.message : "Submission failed. Please try again."; + + toast.error("Submission failed", { description: message }); + } finally { + setIsSubmitting(false); + } } return ( @@ -108,6 +137,7 @@ export function CommissionOrderForm({ types }: Props) { type="button" variant={field.value === type.id ? "default" : "outline"} onClick={() => field.onChange(type.id)} + disabled={isSubmitting} > {type.name} @@ -136,6 +166,7 @@ export function CommissionOrderForm({ types }: Props) { checked={field.value === opt.optionId} value={opt.optionId} onChange={() => field.onChange(opt.optionId)} + disabled={isSubmitting} /> {opt.option.name} @@ -155,21 +186,19 @@ export function CommissionOrderForm({ types }: Props) { Extras
- {selectedType?.extras.map((ext) => ( + {selectedType.extras.map((ext) => (