7355a3613d
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
4.0 KiB
TypeScript
103 lines
4.0 KiB
TypeScript
"use client";
|
||
|
||
import Script from "next/script";
|
||
import { useActionState, useEffect, useRef } from "react";
|
||
import { sendContact, type ContactState } from "@/app/contact/actions";
|
||
import { CONTACT_EMAIL, serviceNav } from "@/lib/routes";
|
||
|
||
const interests = [...serviceNav.map((s) => s.name), "Not sure yet"];
|
||
const initial: ContactState = { status: "idle" };
|
||
// Cloudflare Turnstile renders only once a site key is configured.
|
||
const TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
|
||
|
||
declare global {
|
||
interface Window {
|
||
turnstile?: { reset: (el?: Element | string) => void };
|
||
}
|
||
}
|
||
|
||
export default function ContactForm() {
|
||
const [state, action, pending] = useActionState(sendContact, initial);
|
||
const startedRef = useRef<HTMLInputElement>(null);
|
||
|
||
// When the visitor started filling the form; the server rejects instant (bot) submissions.
|
||
useEffect(() => {
|
||
if (startedRef.current && !startedRef.current.value) startedRef.current.value = String(Date.now());
|
||
}, []);
|
||
|
||
// A Turnstile token is single-use, so get a fresh one after a failed attempt.
|
||
useEffect(() => {
|
||
if (state.status === "error") window.turnstile?.reset();
|
||
}, [state]);
|
||
|
||
if (state.status === "sent") {
|
||
return (
|
||
<div className="contact-form form-sent" role="status">
|
||
<svg className="sent-mark" viewBox="0 0 48 48" aria-hidden="true">
|
||
<circle cx="24" cy="24" r="23" />
|
||
<path d="M14 24.5l7 7 13-14" />
|
||
</svg>
|
||
<h3 className="card-title">Your message was sent successfully.</h3>
|
||
<p className="copy">Thank you for reaching out. Our team will review what you shared and get back to you shortly.</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<form className="contact-form" action={action}>
|
||
<div className="field-row">
|
||
<label className="field">
|
||
<span>Name</span>
|
||
<input name="name" autoComplete="name" required maxLength={120} />
|
||
</label>
|
||
<label className="field">
|
||
<span>Work email</span>
|
||
<input name="email" type="email" autoComplete="email" required maxLength={200} />
|
||
</label>
|
||
</div>
|
||
<div className="field-row">
|
||
<label className="field">
|
||
<span>Company</span>
|
||
<input name="company" autoComplete="organization" maxLength={160} />
|
||
</label>
|
||
<label className="field">
|
||
<span>Website</span>
|
||
<input name="website" type="url" inputMode="url" placeholder="https://" maxLength={300} />
|
||
</label>
|
||
</div>
|
||
{/* Honeypot for bots — hidden from people and assistive tech. */}
|
||
<input name="company_url" className="hp" tabIndex={-1} autoComplete="off" aria-hidden="true" />
|
||
<input ref={startedRef} type="hidden" name="started" />
|
||
<fieldset className="field">
|
||
<legend>What are you looking at?</legend>
|
||
<div className="chips">
|
||
{interests.map((i) => (
|
||
<label key={i} className="chip">
|
||
<input type="checkbox" name="interest" value={i} />
|
||
<span>{i}</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
</fieldset>
|
||
<label className="field">
|
||
<span>What’s happening in the business?</span>
|
||
<textarea name="message" rows={5} required maxLength={5000} />
|
||
</label>
|
||
{TURNSTILE_SITE_KEY && (
|
||
<>
|
||
<Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer />
|
||
<div className="cf-turnstile" data-sitekey={TURNSTILE_SITE_KEY} data-theme="dark" data-size="flexible" />
|
||
</>
|
||
)}
|
||
<button type="submit" className="btn btn-solid" disabled={pending}>
|
||
{pending ? "Sending…" : "Send to the Team"} <span className="arrow">→</span>
|
||
</button>
|
||
<p className={`form-note${state.status === "error" ? " is-error" : ""}`} role="status">
|
||
{state.status === "error"
|
||
? `${state.message} You can also write to ${CONTACT_EMAIL}.`
|
||
: "A strategist reads every message and replies personally."}
|
||
</p>
|
||
</form>
|
||
);
|
||
}
|