Harden the contact form and email visitors a confirmation

Adds a fill-time trap, link-spam filter and optional Cloudflare
Turnstile on top of the honeypot; bot hits get a silent success.
Once CONTACT_FROM points at a Resend-verified domain, the visitor
receives a branded copy of their message after the response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Veras
2026-10-12 12:09:05 -04:00
parent e21a23a245
commit 5551955aaa
2 changed files with 164 additions and 29 deletions
+33 -3
View File
@@ -1,21 +1,44 @@
"use client";
import { useActionState } from "react";
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">
<span className="card-num">Received</span>
<h3 className="card-title">Thanks — your message is with the team.</h3>
<p className="copy">We’ll review what you shared and get back to you shortly.</p>
<p className="copy">
We’ll review what you shared and get back to you shortly.
{state.copySent && " A confirmation with a copy of your message is on its way to your inbox."}
</p>
</div>
);
}
@@ -44,6 +67,7 @@ export default function ContactForm() {
</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">
@@ -59,13 +83,19 @@ export default function ContactForm() {
<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}.`
: "We reply personally — no automated sequences."}
: "A strategist reads every message and replies personally."}
</p>
</form>
);