it/web/app/contact/page.tsx
2025-10-25 20:37:00 +02:00

151 lines
5.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.

"use client";
import { useEffect, useMemo, useState } from "react";
import { site } from "@/lib/site";
type Status = { state: "idle" | "submitting" | "success" | "error"; message?: string };
export default function ContactPage() {
const [startedAt, setStartedAt] = useState<string>("");
const [status, setStatus] = useState<Status>({ state: "idle" });
useEffect(() => {
setStartedAt(String(Date.now()));
}, []);
const disabledEarly = useMemo(() => {
if (!startedAt) return true;
const min = Number(process.env.NEXT_PUBLIC_CONTACT_MIN_SUBMIT_SECONDS ?? 3) || 3;
return Date.now() - Number(startedAt) < min * 1000;
}, [startedAt]);
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus({ state: "submitting" });
const form = e.currentTarget;
const formData = new FormData(form);
const payload = Object.fromEntries(formData.entries());
try {
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const json = (await res.json()) as { ok: boolean; error?: string };
if (!res.ok || !json.ok) throw new Error(json.error || "Failed");
setStatus({ state: "success", message: "Thanks! Well get back to you shortly." });
form.reset();
setStartedAt(String(Date.now())); // reset time trap
} catch {
setStatus({ state: "error", message: "Could not send. Please try again." });
}
}
return (
<section className="container py-12">
<h1 className="text-3xl font-semibold tracking-tight">Contact</h1>
<p className="mt-2 text-muted-foreground">
Tell us what you want fixed or managed. Well confirm scope and start fast.
</p>
<form onSubmit={onSubmit} className="mt-8 space-y-5">
{/* Honeypot (hidden from users & screen readers) */}
<div aria-hidden className="hidden">
<label htmlFor="website">Website</label>
<input id="website" name="website" type="text" tabIndex={-1} autoComplete="off" />
</div>
{/* Time trap */}
<input type="hidden" name="startedAt" value={startedAt} readOnly />
<div className="grid gap-4 sm:grid-cols-2">
<div className="flex flex-col gap-2">
<label htmlFor="name" className="text-sm font-medium">
Name<span className="text-red-600"> *</span>
</label>
<input
id="name"
name="name"
required
className="rounded-xl border bg-white px-3 py-2 outline-none ring-2 ring-transparent focus:ring-primary"
placeholder="Jane Doe"
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="email" className="text-sm font-medium">
Email<span className="text-red-600"> *</span>
</label>
<input
id="email"
name="email"
type="email"
required
className="rounded-xl border bg-white px-3 py-2 outline-none ring-2 ring-transparent focus:ring-primary"
placeholder="jane@company.com"
inputMode="email"
autoComplete="email"
/>
</div>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="company" className="text-sm font-medium">
Company
</label>
<input
id="company"
name="company"
className="rounded-xl border bg-white px-3 py-2 outline-none ring-2 ring-transparent focus:ring-primary"
placeholder="Company B.V."
autoComplete="organization"
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="message" className="text-sm font-medium">
What do you need?<span className="text-red-600"> *</span>
</label>
<textarea
id="message"
name="message"
required
rows={6}
className="rounded-xl border bg-white px-3 py-2 outline-none ring-2 ring-transparent focus:ring-primary"
placeholder="Briefly describe the problem or goal. Links are optional."
/>
</div>
<div className="flex items-start justify-between gap-4">
<p className="text-xs text-muted-foreground">
By submitting, you agree we can contact you about your request. We dont sell data.
</p>
<button
type="submit"
disabled={status.state === "submitting" || disabledEarly}
className="inline-flex min-w-[9rem] items-center justify-center rounded-xl bg-primary px-4 py-2 text-primary-foreground transition-opacity disabled:cursor-not-allowed disabled:opacity-60"
>
{status.state === "submitting" ? "Sending..." : "Send"}
</button>
</div>
{status.state === "success" && (
<div role="status" className="rounded-xl border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700">
{status.message}
</div>
)}
{status.state === "error" && (
<div role="alert" className="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
{status.message}
</div>
)}
</form>
<div className="mt-10 text-sm text-muted-foreground">
Prefer email? Reach us at <a className="underline" href={`mailto:${site.contact.email}`}>{site.contact.email}</a>.
</div>
</section>
);
}