258 lines
7.6 KiB
TypeScript
258 lines
7.6 KiB
TypeScript
import { useState, type FormEvent, type ReactNode } from "react";
|
||
import { submitEnquiry, type EnquiryResult } from "@/data/enquiry";
|
||
|
||
/**
|
||
* Editorial form used in two places:
|
||
* variant="contact" — the Contact Us page
|
||
* variant="bespoke" — bulk gifting / bespoke gifting enquiries
|
||
*/
|
||
|
||
const CONTACT_TOPICS = [
|
||
"General enquiry",
|
||
"My order",
|
||
"Feedback",
|
||
"Press & collaborations",
|
||
];
|
||
|
||
const BESPOKE_TYPES = [
|
||
"Corporate & bulk gifting",
|
||
"Weddings & celebrations",
|
||
"Brand collaboration",
|
||
"Events & experiences",
|
||
"Custom packaging",
|
||
];
|
||
|
||
const BUDGETS = [
|
||
"Under ₹25,000",
|
||
"₹25,000 – ₹1,00,000",
|
||
"₹1,00,000 – ₹5,00,000",
|
||
"Above ₹5,00,000",
|
||
];
|
||
|
||
const inputClass =
|
||
"mt-2 w-full border-b border-ink-900/20 bg-transparent pb-3 pt-1 text-[1.05rem] font-light text-ink-900 outline-none transition-colors duration-300 placeholder:text-ink-500/40 focus:border-orange-500";
|
||
|
||
function Field({
|
||
label,
|
||
error,
|
||
children,
|
||
className = "",
|
||
}: {
|
||
label: string;
|
||
error?: string;
|
||
children: ReactNode;
|
||
className?: string;
|
||
}) {
|
||
return (
|
||
<label className={`block ${className}`}>
|
||
<span className="eyebrow text-[0.62rem]! text-ink-500">{label}</span>
|
||
{children}
|
||
{error && <span className="mt-2 block text-[0.8rem] text-orange-600">{error}</span>}
|
||
</label>
|
||
);
|
||
}
|
||
|
||
export default function ContactForm({ variant }: { variant: "contact" | "bespoke" }) {
|
||
const bespoke = variant === "bespoke";
|
||
const [pending, setPending] = useState(false);
|
||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||
const [done, setDone] = useState(false);
|
||
|
||
async function onSubmit(event: FormEvent<HTMLFormElement>) {
|
||
event.preventDefault();
|
||
const form = event.currentTarget;
|
||
const f = new FormData(form);
|
||
const get = (k: string) => String(f.get(k) ?? "").trim();
|
||
|
||
setPending(true);
|
||
setErrors({});
|
||
let result: EnquiryResult;
|
||
try {
|
||
result = await submitEnquiry({
|
||
data: {
|
||
form: variant,
|
||
name: get("name"),
|
||
email: get("email"),
|
||
phone: get("phone"),
|
||
company: get("company"),
|
||
topic: bespoke ? undefined : get("topic"),
|
||
enquiryType: bespoke ? get("enquiryType") : undefined,
|
||
quantity: get("quantity"),
|
||
budget: get("budget"),
|
||
date: get("date"),
|
||
message: get("message"),
|
||
website: get("website"),
|
||
},
|
||
});
|
||
} catch {
|
||
result = { ok: false, errors: { form: "Something went wrong — please try again, or call the atelier." } };
|
||
}
|
||
setPending(false);
|
||
|
||
if (result.ok) {
|
||
form.reset();
|
||
setDone(true);
|
||
} else {
|
||
setErrors(result.errors);
|
||
}
|
||
}
|
||
|
||
if (done) {
|
||
return (
|
||
<div className="border border-ink-900/10 bg-ivory-100 px-7 py-14 text-center sm:px-12">
|
||
<p className="font-script text-[2.6rem] leading-none text-orange-500">Thank you</p>
|
||
<p className="mx-auto mt-6 max-w-sm text-[0.98rem] leading-relaxed text-ink-500">
|
||
Your {bespoke ? "enquiry" : "note"} has reached the atelier — we reply
|
||
within one working day.
|
||
</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => setDone(false)}
|
||
className="link-quiet eyebrow mt-8 text-ink-700 hover:text-orange-500"
|
||
>
|
||
Send another
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<form onSubmit={onSubmit} noValidate={false}>
|
||
{/* honeypot — hidden from real users */}
|
||
<input
|
||
type="text"
|
||
name="website"
|
||
tabIndex={-1}
|
||
autoComplete="off"
|
||
aria-hidden="true"
|
||
className="hidden"
|
||
/>
|
||
|
||
<div className="grid gap-x-8 gap-y-8 sm:grid-cols-2">
|
||
<Field label="Your name *" error={errors.name}>
|
||
<input
|
||
type="text"
|
||
name="name"
|
||
required
|
||
minLength={2}
|
||
maxLength={120}
|
||
autoComplete="name"
|
||
placeholder="Full name"
|
||
className={inputClass}
|
||
/>
|
||
</Field>
|
||
<Field label="Email *" error={errors.email}>
|
||
<input
|
||
type="email"
|
||
name="email"
|
||
required
|
||
maxLength={200}
|
||
autoComplete="email"
|
||
placeholder="you@example.com"
|
||
className={inputClass}
|
||
/>
|
||
</Field>
|
||
<Field label="Phone" error={errors.phone}>
|
||
<input
|
||
type="tel"
|
||
name="phone"
|
||
maxLength={20}
|
||
autoComplete="tel"
|
||
placeholder={bespoke ? "Best number to reach you" : "Optional"}
|
||
className={inputClass}
|
||
/>
|
||
</Field>
|
||
{bespoke ? (
|
||
<Field label="Company / brand">
|
||
<input
|
||
type="text"
|
||
name="company"
|
||
maxLength={160}
|
||
autoComplete="organization"
|
||
placeholder="Optional"
|
||
className={inputClass}
|
||
/>
|
||
</Field>
|
||
) : (
|
||
<Field label="What is this about?">
|
||
<select name="topic" defaultValue={CONTACT_TOPICS[0]} className={`${inputClass} cursor-pointer`}>
|
||
{CONTACT_TOPICS.map((t) => (
|
||
<option key={t} value={t}>
|
||
{t}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</Field>
|
||
)}
|
||
</div>
|
||
|
||
{bespoke && (
|
||
<div className="mt-8 grid gap-x-8 gap-y-8 sm:grid-cols-2">
|
||
<Field label="Enquiry type">
|
||
<select name="enquiryType" defaultValue={BESPOKE_TYPES[0]} className={`${inputClass} cursor-pointer`}>
|
||
{BESPOKE_TYPES.map((t) => (
|
||
<option key={t} value={t}>
|
||
{t}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</Field>
|
||
<Field label="Approximate quantity">
|
||
<input
|
||
type="text"
|
||
name="quantity"
|
||
maxLength={60}
|
||
placeholder="e.g. 150 boxes"
|
||
className={inputClass}
|
||
/>
|
||
</Field>
|
||
<Field label="Budget per gift">
|
||
<select name="budget" defaultValue="" className={`${inputClass} cursor-pointer`}>
|
||
<option value="" disabled>
|
||
Select a range
|
||
</option>
|
||
{BUDGETS.map((b) => (
|
||
<option key={b} value={b}>
|
||
{b}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</Field>
|
||
<Field label="Date needed">
|
||
<input type="date" name="date" className={`${inputClass} cursor-pointer`} />
|
||
</Field>
|
||
</div>
|
||
)}
|
||
|
||
<div className="mt-8">
|
||
<Field label={bespoke ? "Tell us about the occasion *" : "Your message *"} error={errors.message}>
|
||
<textarea
|
||
name="message"
|
||
required
|
||
minLength={10}
|
||
maxLength={4000}
|
||
rows={bespoke ? 5 : 4}
|
||
placeholder={
|
||
bespoke
|
||
? "The occasion, who it's for, and the feeling you want it to leave behind."
|
||
: "How can we help?"
|
||
}
|
||
className={`${inputClass} resize-y`}
|
||
/>
|
||
</Field>
|
||
</div>
|
||
|
||
{errors.form && <p className="mt-6 text-[0.85rem] text-orange-600">{errors.form}</p>}
|
||
|
||
<div className="mt-10 flex flex-wrap items-center gap-6">
|
||
<button type="submit" disabled={pending} className="btn btn-dark disabled:opacity-60">
|
||
{pending ? "Sending…" : bespoke ? "Send enquiry" : "Send message"}
|
||
</button>
|
||
<p className="text-[0.8rem] leading-relaxed text-ink-500">
|
||
We reply within one working day.
|
||
</p>
|
||
</div>
|
||
</form>
|
||
);
|
||
}
|