From dadd628c3089a03e6307da18dfb1877f3cc8747c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20=C3=81vila=20Rangel?= Date: Fri, 2 Oct 2026 16:41:41 -0400 Subject: [PATCH] feat(website): add Concepta contact page Adds /contact with a name/email/subject/message form, the Orlando address and phone number, and a Contact link in the Concepta header (desktop pill and mobile drawer). Submissions go through a new /api/contact route that sends via the existing Resend integration to guilherme.rosa.c@conceptatech.com and thomas@conceptatech.com with reply-to set to the sender, and optionally saves the sender to RESEND_AUDIENCE_ID_CONTACT. Co-Authored-By: Claude Opus 5.5 (1M context) --- components/FloatingNavbar.tsx | 20 ++ components/contact/ContactPage.tsx | 408 ++++++++++++++++++++++++ site-tests/contact-integration.test.mjs | 29 ++ src/app/api/contact/route.ts | 140 ++++++++ src/app/contact/page.tsx | 27 ++ src/app/layout.jsx | 2 +- 6 files changed, 625 insertions(+), 1 deletion(-) create mode 100644 components/contact/ContactPage.tsx create mode 100644 site-tests/contact-integration.test.mjs create mode 100644 src/app/api/contact/route.ts create mode 100644 src/app/contact/page.tsx diff --git a/components/FloatingNavbar.tsx b/components/FloatingNavbar.tsx index 2106608c..e37b10c0 100644 --- a/components/FloatingNavbar.tsx +++ b/components/FloatingNavbar.tsx @@ -36,6 +36,7 @@ function getActiveProduct(pathname: string | null): ProductId { if (!pathname) return 'concepta' if (pathname === '/') return 'concepta' if (pathname === '/reports' || pathname.startsWith('/reports/')) return 'concepta' + if (pathname === '/contact') return 'concepta' if (pathname === '/fluttercon2026' || pathname.startsWith('/fluttercon2026/')) return 'concepta' if (pathname.startsWith('/documentation/remix')) return 'remix' if (pathname === '/remix' || pathname.startsWith('/remix/')) return 'remix' @@ -590,6 +591,13 @@ function MobileDrawer({ > Reports + + Contact + ) : ( Reports + + Contact + )} diff --git a/components/contact/ContactPage.tsx b/components/contact/ContactPage.tsx new file mode 100644 index 00000000..6afcbcab --- /dev/null +++ b/components/contact/ContactPage.tsx @@ -0,0 +1,408 @@ +"use client"; + +import { useState } from "react"; +import { ArrowUpRight, CheckCircle2, MapPin, Phone } from "lucide-react"; +import { Button } from "../Button"; +import { + CONCEPTA_ADDRESS_DISPLAY, + CONCEPTA_LEGAL_NAME, + CONCEPTA_MAP_URL, + CONCEPTA_PHONE_DISPLAY, + CONCEPTA_PHONE_HREF, +} from "../constants"; +import { HeroBackground } from "../HeroBackground"; + +type Status = "idle" | "loading" | "success" | "error"; + +const EMPTY_FORM = { name: "", email: "", subject: "", message: "", website: "" }; + +export function ContactPage() { + const [form, setForm] = useState(EMPTY_FORM); + const [status, setStatus] = useState("idle"); + const [message, setMessage] = useState(""); + + function update(field: keyof typeof EMPTY_FORM) { + return ( + e: React.ChangeEvent + ) => setForm((prev) => ({ ...prev, [field]: e.target.value })); + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (status === "loading") return; + + setStatus("loading"); + setMessage(""); + + try { + const res = await fetch("/api/contact", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(form), + }); + const data = await res.json(); + + if (res.ok && data.success) { + setStatus("success"); + setMessage(data.message || "Thanks — we'll be in touch soon."); + setForm(EMPTY_FORM); + } else { + setStatus("error"); + setMessage(data.message || "Something went wrong. Please try again."); + } + } catch { + setStatus("error"); + setMessage("Something went wrong. Please try again."); + } + } + + const disabled = status === "loading"; + + return ( + <> + +
+
+ Contact +

Let's talk about what you're shipping.

+

+ Tell us about your project and we'll get back to you shortly. +

+
+ +
+
+

+ Send us a message +

+ {status === "success" ? ( +

+ + {message} +

+ ) : ( +
+
+ + +
+ +