Files
mozimo.in/src/components/ContactForm.tsx
T
2026-09-04 11:38:24 +00:00

258 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}