275 lines
8.5 KiB
TypeScript
275 lines
8.5 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { z } from "zod";
|
|
import { useTranslations } from "next-intl";
|
|
import { Link } from "@/i18n/navigation";
|
|
import { useCartStore } from "@/store/cart";
|
|
|
|
const formSchema = z.object({
|
|
name: z.string().min(2).max(120),
|
|
phone: z.string().min(6).max(40),
|
|
email: z.string().email().max(120),
|
|
company: z.string().max(160).optional().or(z.literal("")),
|
|
message: z.string().max(2000).optional().or(z.literal("")),
|
|
});
|
|
|
|
type FormValues = z.infer<typeof formSchema>;
|
|
|
|
const inputClass =
|
|
"mt-1.5 w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm font-normal text-neutral-800 outline-none transition duration-150 placeholder:text-neutral-400 focus:border-brand-navy600 focus:ring-2 focus:ring-brand-navy/8";
|
|
|
|
export function CheckoutForm() {
|
|
const t = useTranslations("checkout");
|
|
const items = useCartStore((s) => s.items);
|
|
const clear = useCartStore((s) => s.clear);
|
|
const refInput = useRef<HTMLInputElement>(null);
|
|
const proformaInput = useRef<HTMLInputElement>(null);
|
|
const [status, setStatus] = useState<
|
|
"idle" | "sending" | "success" | "error"
|
|
>("idle");
|
|
const [requestId, setRequestId] = useState<string | null>(null);
|
|
const [emailWarn, setEmailWarn] = useState(false);
|
|
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
|
|
const {
|
|
register,
|
|
trigger,
|
|
getValues,
|
|
formState: { errors },
|
|
} = useForm<FormValues>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues: { company: "", message: "" },
|
|
});
|
|
|
|
if (items.length === 0 && status !== "success") {
|
|
return (
|
|
<div className="rounded-lg border border-neutral-200 bg-neutral-50 p-10 text-center">
|
|
<p className="text-sm font-normal text-neutral-600">{t("cartEmpty")}</p>
|
|
<Link
|
|
href="/cart"
|
|
className="mt-6 inline-flex rounded-lg bg-brand-navy px-8 py-3 text-sm font-semibold text-white transition hover:bg-brand-navy800"
|
|
>
|
|
{t("goCart")}
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (status === "success" && requestId) {
|
|
return (
|
|
<div className="rounded-lg border border-neutral-200 bg-neutral-50 p-8">
|
|
<h2 className="text-2xl font-semibold text-brand-navy">{t("successTitle")}</h2>
|
|
<p className="mt-3 text-sm font-normal text-neutral-700">
|
|
{t("successBody")}{" "}
|
|
<span className="font-mono font-semibold text-brand-navy">
|
|
{requestId}
|
|
</span>
|
|
</p>
|
|
{emailWarn ? (
|
|
<p className="mt-4 rounded-lg border border-brand-gold/20 bg-brand-gold/5 p-4 text-sm font-normal text-neutral-800">
|
|
{t("emailWarn")}
|
|
</p>
|
|
) : null}
|
|
<Link
|
|
href="/catalog"
|
|
className="mt-8 inline-flex rounded-lg bg-brand-navy px-8 py-3.5 text-sm font-semibold text-white transition hover:bg-brand-navy800"
|
|
>
|
|
{t("successCta")}
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
async function onFormSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
e.preventDefault();
|
|
const ok = await trigger();
|
|
if (!ok) return;
|
|
|
|
const values = getValues();
|
|
setStatus("sending");
|
|
setErrorMsg(null);
|
|
try {
|
|
const fd = new FormData();
|
|
fd.set("name", values.name);
|
|
fd.set("phone", values.phone);
|
|
fd.set("email", values.email);
|
|
fd.set("company", values.company ?? "");
|
|
fd.set("message", values.message ?? "");
|
|
fd.set("website", "");
|
|
fd.set("cart", JSON.stringify(items));
|
|
|
|
const refs = refInput.current?.files;
|
|
if (refs) {
|
|
for (let i = 0; i < refs.length; i += 1) {
|
|
fd.append("referenceImages", refs[i]);
|
|
}
|
|
}
|
|
const pro = proformaInput.current?.files?.[0];
|
|
if (pro && pro.size > 0) {
|
|
fd.set("proforma", pro);
|
|
}
|
|
|
|
const res = await fetch("/api/quote-request", {
|
|
method: "POST",
|
|
body: fd,
|
|
});
|
|
const data = (await res.json()) as {
|
|
ok?: boolean;
|
|
requestId?: string;
|
|
emailSent?: boolean;
|
|
error?: string;
|
|
};
|
|
|
|
if (!res.ok) {
|
|
setStatus("error");
|
|
setErrorMsg(data.error ?? "Request failed");
|
|
return;
|
|
}
|
|
|
|
if (data.requestId) {
|
|
setRequestId(data.requestId);
|
|
setEmailWarn(data.emailSent === false);
|
|
clear();
|
|
setStatus("success");
|
|
} else {
|
|
setStatus("error");
|
|
setErrorMsg("Invalid response");
|
|
}
|
|
} catch {
|
|
setStatus("error");
|
|
setErrorMsg("Network error");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form
|
|
onSubmit={onFormSubmit}
|
|
className="space-y-8 rounded-lg border border-neutral-200 bg-neutral-50 p-6 sm:p-10"
|
|
>
|
|
<input
|
|
type="text"
|
|
name="website"
|
|
tabIndex={-1}
|
|
autoComplete="off"
|
|
className="absolute -left-[9999px] h-0 w-0 opacity-0"
|
|
aria-hidden
|
|
/>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label className="text-sm font-semibold text-neutral-700" htmlFor="name">
|
|
{t("name")}
|
|
</label>
|
|
<input
|
|
id="name"
|
|
className={inputClass}
|
|
{...register("name")}
|
|
/>
|
|
{errors.name ? (
|
|
<p className="mt-1 text-xs text-red-600">{errors.name.message}</p>
|
|
) : null}
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-semibold text-neutral-700" htmlFor="phone">
|
|
{t("phone")}
|
|
</label>
|
|
<input
|
|
id="phone"
|
|
className={inputClass}
|
|
{...register("phone")}
|
|
/>
|
|
{errors.phone ? (
|
|
<p className="mt-1 text-xs text-red-600">{errors.phone.message}</p>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-sm font-semibold text-neutral-700" htmlFor="email">
|
|
{t("email")}
|
|
</label>
|
|
<input
|
|
id="email"
|
|
type="email"
|
|
className={inputClass}
|
|
{...register("email")}
|
|
/>
|
|
{errors.email ? (
|
|
<p className="mt-1 text-xs text-red-600">{errors.email.message}</p>
|
|
) : null}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-sm font-semibold text-neutral-700" htmlFor="company">
|
|
{t("company")}
|
|
</label>
|
|
<input
|
|
id="company"
|
|
className={inputClass}
|
|
{...register("company")}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-sm font-semibold text-neutral-700" htmlFor="message">
|
|
{t("message")}
|
|
</label>
|
|
<textarea
|
|
id="message"
|
|
rows={4}
|
|
className={inputClass}
|
|
{...register("message")}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-sm font-semibold text-neutral-700" htmlFor="refs">
|
|
{t("referenceImages")}
|
|
</label>
|
|
<input
|
|
id="refs"
|
|
ref={refInput}
|
|
type="file"
|
|
name="referenceImages"
|
|
accept="image/jpeg,image/png,image/webp"
|
|
multiple
|
|
className="mt-1 block w-full text-sm text-neutral-600 file:mr-3 file:rounded-lg file:border-0 file:bg-neutral-100 file:px-3 file:py-2 file:text-sm file:font-semibold file:text-neutral-700 hover:file:bg-neutral-200"
|
|
/>
|
|
<p className="mt-1 text-xs text-neutral-500">{t("referenceHelp")}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-sm font-semibold text-neutral-700" htmlFor="proforma">
|
|
{t("proforma")}
|
|
</label>
|
|
<input
|
|
id="proforma"
|
|
ref={proformaInput}
|
|
type="file"
|
|
name="proforma"
|
|
accept="application/pdf,image/jpeg,image/png,image/webp"
|
|
className="mt-1 block w-full text-sm text-neutral-600 file:mr-3 file:rounded-lg file:border-0 file:bg-neutral-100 file:px-3 file:py-2 file:text-sm file:font-semibold file:text-neutral-700 hover:file:bg-neutral-200"
|
|
/>
|
|
<p className="mt-1 text-xs text-neutral-500">{t("proformaHelp")}</p>
|
|
</div>
|
|
|
|
{errorMsg ? (
|
|
<p className="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800">
|
|
{errorMsg}
|
|
</p>
|
|
) : null}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={status === "sending"}
|
|
className="w-full rounded-lg bg-brand-navy py-4 text-sm font-semibold text-white transition duration-150 hover:bg-brand-navy800 disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto sm:px-12"
|
|
>
|
|
{status === "sending" ? t("sending") : t("submit")}
|
|
</button>
|
|
</form>
|
|
);
|
|
}
|