From e21a23a245083b0b2782c7d9c439988549549b69 Mon Sep 17 00:00:00 2001 From: Alejandro Veras Date: Mon, 12 Oct 2026 12:06:39 -0400 Subject: [PATCH] Send contact form submissions by email through Resend Server Action validates the fields, drops honeypot hits and emails the enquiry to rey@aigrowth360.io with the visitor as reply-to. Needs RESEND_API_KEY in the environment. Co-Authored-By: Claude Opus 5.5 --- src/app/contact/actions.ts | 75 +++++++++++++++++++++++++ src/app/globals.css | 28 +++++++++ src/components/content/contact-form.tsx | 58 +++++++++---------- 3 files changed, 129 insertions(+), 32 deletions(-) create mode 100644 src/app/contact/actions.ts diff --git a/src/app/contact/actions.ts b/src/app/contact/actions.ts new file mode 100644 index 0000000..02c11ca --- /dev/null +++ b/src/app/contact/actions.ts @@ -0,0 +1,75 @@ +"use server"; + +export type ContactState = { status: "idle" | "sent" | "error"; message?: string }; + +// Where enquiries land. Override with CONTACT_TO in the environment if it ever changes. +const TO = process.env.CONTACT_TO || "rey@aigrowth360.io"; +// Resend's shared sender works without verifying a domain, but only delivers to the address that +// owns the Resend account. Set CONTACT_FROM once aigrowth360.io (or another domain) is verified. +const FROM = process.env.CONTACT_FROM || "AI Growth Website "; + +const clean = (v: FormDataEntryValue | null, max: number) => String(v ?? "").trim().slice(0, max); +const escape = (s: string) => + s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]!); + +export async function sendContact(_prev: ContactState, formData: FormData): Promise { + // Honeypot: real visitors never see or fill this field. + if (clean(formData.get("company_url"), 200)) return { status: "sent" }; + + const name = clean(formData.get("name"), 120); + const email = clean(formData.get("email"), 200); + const company = clean(formData.get("company"), 160); + const website = clean(formData.get("website"), 300); + const interests = formData + .getAll("interest") + .map((i) => clean(i, 60)) + .filter(Boolean) + .slice(0, 12); + const message = clean(formData.get("message"), 5000); + + if (!name || !message || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + return { status: "error", message: "Please add your name, a valid email, and a short message." }; + } + + const apiKey = process.env.RESEND_API_KEY; + if (!apiKey) { + console.error("Contact form: RESEND_API_KEY is not set"); + return { status: "error", message: "The form isn’t available right now." }; + } + + const rows: [string, string][] = [ + ["Name", name], + ["Email", email], + ["Company", company || "—"], + ["Website", website || "—"], + ["Interested in", interests.join(", ") || "—"], + ]; + const text = `${rows.map(([k, v]) => `${k}: ${v}`).join("\n")}\n\n${message}`; + const html = `${rows + .map(([k, v]) => ``) + .join("")}
${k}${escape(v)}

${escape(message)}

`; + + try { + const res = await fetch("https://api.resend.com/emails", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + from: FROM, + to: [TO], + reply_to: email, + subject: `New enquiry — ${company || name}`, + text, + html, + }), + }); + if (!res.ok) { + console.error("Contact form: Resend responded", res.status, await res.text()); + return { status: "error", message: "We couldn’t send your message." }; + } + } catch (err) { + console.error("Contact form: request failed", err); + return { status: "error", message: "We couldn’t send your message." }; + } + + return { status: "sent" }; +} diff --git a/src/app/globals.css b/src/app/globals.css index cb83f8f..d39f7ac 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2570,3 +2570,31 @@ a { font-size: 12px; color: var(--piombo); } + +.hp { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + opacity: 0; +} + +.contact-form .btn:disabled { + opacity: 0.6; + cursor: progress; +} + +.form-note.is-error { + color: var(--rosso); +} + +.form-sent { + min-height: 320px; + justify-content: center; +} + +.form-sent .card-title { + margin: 8px 0 0; + font-size: clamp(22px, 2.2vw, 30px); + font-weight: 300; +} diff --git a/src/components/content/contact-form.tsx b/src/components/content/contact-form.tsx index 9e59df4..ec9678e 100644 --- a/src/components/content/contact-form.tsx +++ b/src/components/content/contact-form.tsx @@ -1,55 +1,49 @@ "use client"; -import { useState, type FormEvent } from "react"; +import { useActionState } 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" }; -// No form backend is wired up yet, so submitting composes an email to the team -// in the visitor's mail client. Swap `onSubmit` for a real endpoint when one exists. export default function ContactForm() { - const [sent, setSent] = useState(false); + const [state, action, pending] = useActionState(sendContact, initial); - function onSubmit(e: FormEvent) { - e.preventDefault(); - const data = new FormData(e.currentTarget); - const get = (k: string) => String(data.get(k) ?? "").trim(); - const subject = `Growth conversation — ${get("company") || get("name")}`; - const body = [ - `Name: ${get("name")}`, - `Email: ${get("email")}`, - `Company: ${get("company")}`, - `Website: ${get("website")}`, - `Interested in: ${data.getAll("interest").join(", ") || "—"}`, - "", - get("message"), - ].join("\n"); - window.location.href = `mailto:${CONTACT_EMAIL}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; - setSent(true); + if (state.status === "sent") { + return ( +
+ Received +

Thanks — your message is with the team.

+

We’ll review what you shared and get back to you shortly.

+
+ ); } return ( -
+
+ {/* Honeypot for bots — hidden from people and assistive tech. */} +
What are you looking at?
@@ -63,15 +57,15 @@ export default function ContactForm() {