TrustWin-Landing/src/components/CheckoutForm.tsx

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>
);
}