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 <noreply@anthropic.com>
This commit is contained in:
Alejandro Veras
2026-10-12 12:06:39 -04:00
parent 39b2c0a857
commit e21a23a245
3 changed files with 129 additions and 32 deletions
+75
View File
@@ -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 <onboarding@resend.dev>";
const clean = (v: FormDataEntryValue | null, max: number) => String(v ?? "").trim().slice(0, max);
const escape = (s: string) =>
s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]!);
export async function sendContact(_prev: ContactState, formData: FormData): Promise<ContactState> {
// 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 = `<table cellpadding="6" style="font-family:Arial,sans-serif;font-size:14px">${rows
.map(([k, v]) => `<tr><td style="color:#666">${k}</td><td><b>${escape(v)}</b></td></tr>`)
.join("")}</table><p style="font-family:Arial,sans-serif;font-size:14px;white-space:pre-wrap">${escape(message)}</p>`;
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" };
}
+28
View File
@@ -2570,3 +2570,31 @@ a {
font-size: 12px; font-size: 12px;
color: var(--piombo); 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;
}
+26 -32
View File
@@ -1,55 +1,49 @@
"use client"; "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"; import { CONTACT_EMAIL, serviceNav } from "@/lib/routes";
const interests = [...serviceNav.map((s) => s.name), "Not sure yet"]; 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() { export default function ContactForm() {
const [sent, setSent] = useState(false); const [state, action, pending] = useActionState(sendContact, initial);
function onSubmit(e: FormEvent<HTMLFormElement>) { if (state.status === "sent") {
e.preventDefault(); return (
const data = new FormData(e.currentTarget); <div className="contact-form form-sent" role="status">
const get = (k: string) => String(data.get(k) ?? "").trim(); <span className="card-num">Received</span>
const subject = `Growth conversation — ${get("company") || get("name")}`; <h3 className="card-title">Thanks — your message is with the team.</h3>
const body = [ <p className="copy">We’ll review what you shared and get back to you shortly.</p>
`Name: ${get("name")}`, </div>
`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);
} }
return ( return (
<form className="contact-form" onSubmit={onSubmit}> <form className="contact-form" action={action}>
<div className="field-row"> <div className="field-row">
<label className="field"> <label className="field">
<span>Name</span> <span>Name</span>
<input name="name" autoComplete="name" required /> <input name="name" autoComplete="name" required maxLength={120} />
</label> </label>
<label className="field"> <label className="field">
<span>Work email</span> <span>Work email</span>
<input name="email" type="email" autoComplete="email" required /> <input name="email" type="email" autoComplete="email" required maxLength={200} />
</label> </label>
</div> </div>
<div className="field-row"> <div className="field-row">
<label className="field"> <label className="field">
<span>Company</span> <span>Company</span>
<input name="company" autoComplete="organization" /> <input name="company" autoComplete="organization" maxLength={160} />
</label> </label>
<label className="field"> <label className="field">
<span>Website</span> <span>Website</span>
<input name="website" type="url" inputMode="url" placeholder="https://" /> <input name="website" type="url" inputMode="url" placeholder="https://" maxLength={300} />
</label> </label>
</div> </div>
{/* Honeypot for bots — hidden from people and assistive tech. */}
<input name="company_url" className="hp" tabIndex={-1} autoComplete="off" aria-hidden="true" />
<fieldset className="field"> <fieldset className="field">
<legend>What are you looking at?</legend> <legend>What are you looking at?</legend>
<div className="chips"> <div className="chips">
@@ -63,15 +57,15 @@ export default function ContactForm() {
</fieldset> </fieldset>
<label className="field"> <label className="field">
<span>What’s happening in the business?</span> <span>What’s happening in the business?</span>
<textarea name="message" rows={5} required /> <textarea name="message" rows={5} required maxLength={5000} />
</label> </label>
<button type="submit" className="btn btn-solid"> <button type="submit" className="btn btn-solid" disabled={pending}>
Send to the Team <span className="arrow">→</span> {pending ? "Sending…" : "Send to the Team"} <span className="arrow">→</span>
</button> </button>
<p className="form-note" role="status"> <p className={`form-note${state.status === "error" ? " is-error" : ""}`} role="status">
{sent {state.status === "error"
? `Your email app should have opened with the details. If it didn’t, write to ${CONTACT_EMAIL}.` ? `${state.message} You can also write to ${CONTACT_EMAIL}.`
: `Opens your email app addressed to ${CONTACT_EMAIL}.`} : "We reply personally — no automated sequences."}
</p> </p>
</form> </form>
); );