diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..18594f9 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# Amazon SES SMTP credentials (NOT IAM access keys — use dedicated SMTP credentials) +SES_SMTP_USER= +SES_SMTP_PASS= + +# Verified sender email in Amazon SES (must be a verified identity) +SES_FROM_EMAIL=support@bayarea-cc.com diff --git a/app/api/assessment/route.ts b/app/api/assessment/route.ts new file mode 100644 index 0000000..14b1832 --- /dev/null +++ b/app/api/assessment/route.ts @@ -0,0 +1,92 @@ +import { NextResponse } from "next/server"; +import nodemailer from "nodemailer"; + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export async function POST(request: Request) { + let body: Record; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Invalid request body." }, { status: 400 }); + } + + const name = String(body.name ?? "").trim(); + const email = String(body.email ?? "").trim(); + const mailboxes = String(body.mailboxes ?? "").trim(); + const provider = String(body.provider ?? "").trim(); + const message = String(body.message ?? "").trim(); + + const errors: Record = {}; + if (!name) { + errors.name = "Please enter your name."; + } + if (!email) { + errors.email = "Please enter your business email."; + } else if (!EMAIL_REGEX.test(email)) { + errors.email = "Email address needs to include an @ symbol."; + } + + if (Object.keys(errors).length > 0) { + return NextResponse.json({ error: "Validation failed.", fields: errors }, { status: 400 }); + } + + const sesUser = process.env.SES_SMTP_USER; + const sesPass = process.env.SES_SMTP_PASS; + const fromEmail = process.env.SES_FROM_EMAIL; + + if (!sesUser || !sesPass || !fromEmail) { + return NextResponse.json({ error: "Server configuration error." }, { status: 500 }); + } + + const transporter = nodemailer.createTransport({ + host: "email-smtp.us-east-2.amazonaws.com", + port: 587, + secure: false, + auth: { + user: sesUser, + pass: sesPass, + }, + }); + + const textBody = [ + `Name: ${name}`, + `Business Email: ${email}`, + `Mailboxes: ${mailboxes || "(not provided)"}`, + `Current Provider: ${provider || "(not selected)"}`, + `Message: ${message || "(none)"}`, + ].join("\n"); + + const htmlBody = [ + "", + ``, + ``, + ``, + ``, + ``, + "
Name${escapeHtml(name)}
Business Email${escapeHtml(email)}
Mailboxes${escapeHtml(mailboxes || "(not provided)")}
Current Provider${escapeHtml(provider || "(not selected)")}
Message${escapeHtml(message || "(none)")}
", + ].join("\n"); + + try { + await transporter.sendMail({ + from: fromEmail, + to: "support@bayarea-cc.com", + replyTo: email, + subject: `New email assessment request — ${name}`, + text: textBody, + html: htmlBody, + }); + } catch { + return NextResponse.json({ error: "Unable to send your request. Please try again or call us." }, { status: 500 }); + } + + return NextResponse.json({ ok: true }); +} + +function escapeHtml(text: string): string { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} diff --git a/app/globals.css b/app/globals.css index f63056f..80be431 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1478,6 +1478,7 @@ h3 { display: grid; gap: var(--space-3); max-width: 56rem; + margin-inline: auto; margin-bottom: var(--space-6); } @@ -1499,6 +1500,8 @@ h3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-4); + max-width: 68rem; + margin-inline: auto; } @media (min-width: 760px) { @@ -2635,7 +2638,7 @@ h3 { .pricing-builder { display: grid; gap: var(--space-4); - max-width: var(--page-max); + max-width: 68rem; margin-inline: auto; padding: 0; } diff --git a/app/page.tsx b/app/page.tsx index 1458c55..858efa7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -18,6 +18,7 @@ export default function Page() { const [mailboxes, setMailboxes] = useState(10); const [formErrors, setFormErrors] = useState({ name: "", email: "" }); const [formStatus, setFormStatus] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { const storedTheme = window.localStorage.getItem("bes-theme"); @@ -112,7 +113,7 @@ export default function Page() { } }; - const handleAssessmentSubmit = (event: FormEvent) => { + const handleAssessmentSubmit = async (event: FormEvent) => { event.preventDefault(); const form = event.currentTarget; const formData = new FormData(form); @@ -139,8 +140,40 @@ export default function Page() { return; } - setFormStatus("Thanks. We'll review your mailbox count and current provider before we reply."); - form.reset(); + setIsSubmitting(true); + + try { + const response = await fetch("/api/assessment", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name, + email, + mailboxes: formData.get("mailboxes") ?? "", + provider: formData.get("provider") ?? "", + message: formData.get("message") ?? "", + }), + }); + + if (response.ok) { + setFormStatus("Thanks. We'll review your mailbox count and current provider before we reply."); + form.reset(); + } else { + const data = await response.json().catch(() => null); + if (data?.fields) { + setFormErrors({ + name: data.fields.name ?? "", + email: data.fields.email ?? "", + }); + } else { + setFormStatus(data?.error ?? "Something went wrong. Please try again or call us."); + } + } + } catch { + setFormStatus("Network error. Please check your connection and try again."); + } finally { + setIsSubmitting(false); + } }; return ( @@ -167,6 +200,7 @@ export default function Page() { diff --git a/components/AssessmentSection.tsx b/components/AssessmentSection.tsx index 9a1f3cf..48a4ed8 100644 --- a/components/AssessmentSection.tsx +++ b/components/AssessmentSection.tsx @@ -3,10 +3,11 @@ import type { FormEventHandler } from "react"; type AssessmentSectionProps = { formErrors: { name: string; email: string }; formStatus: string; + isSubmitting: boolean; onAssessmentSubmit: FormEventHandler; }; -export default function AssessmentSection({ formErrors, formStatus, onAssessmentSubmit }: AssessmentSectionProps) { +export default function AssessmentSection({ formErrors, formStatus, isSubmitting, onAssessmentSubmit }: AssessmentSectionProps) { return (
@@ -56,9 +57,14 @@ export default function AssessmentSection({ formErrors, formStatus, onAssessment -

We reply within one business day. We don’t share your details.

{formStatus}

@@ -67,7 +73,7 @@ export default function AssessmentSection({ formErrors, formStatus, onAssessment

Start here

-

Let’s review your email setup.

+

Let’s review your email setup.

Tell us about your domain, mailbox count, current provider, and support needs. We will reply with clear next steps for hosting, DNS, migration, and device setup.

diff --git a/components/ProblemSection.tsx b/components/ProblemSection.tsx index e6043fe..22969bb 100644 --- a/components/ProblemSection.tsx +++ b/components/ProblemSection.tsx @@ -1,6 +1,6 @@ export default function ProblemSection() { return ( -
+

Why it matters

Email problems rarely look technical at first.

diff --git a/components/SiteFooter.tsx b/components/SiteFooter.tsx index c46c235..7118046 100644 --- a/components/SiteFooter.tsx +++ b/components/SiteFooter.tsx @@ -26,11 +26,10 @@ export default function SiteFooter() { @@ -47,7 +46,6 @@ export default function SiteFooter() {

Bay Area Email Services, part of Bay Area IT. All rights reserved.

- Support Back to top
diff --git a/package-lock.json b/package-lock.json index 05291a8..9c0af2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,13 @@ "version": "1.0.0", "dependencies": { "next": "^15.3.3", + "nodemailer": "^6.9.16", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@types/node": "^22.15.24", + "@types/nodemailer": "^6.4.17", "@types/react": "^19.0.12", "@types/react-dom": "^19.0.4", "typescript": "^5.8.3" @@ -648,6 +650,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.24", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.24.tgz", + "integrity": "sha512-Ww4u0rT9wQNXh4JiQaIwx3QWdcOFXzOjQA2zc+jtFYNmQiT4mIUqcDin51bDFdkzKubFnQCZNK7FIHlPKQ/q9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.2.15", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", @@ -781,6 +793,15 @@ } } }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", diff --git a/package.json b/package.json index 663340d..b7c20b7 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,13 @@ }, "dependencies": { "next": "^15.3.3", + "nodemailer": "^6.9.16", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@types/node": "^22.15.24", + "@types/nodemailer": "^6.4.17", "@types/react": "^19.0.12", "@types/react-dom": "^19.0.4", "typescript": "^5.8.3"