From b06b8a4fc5557781a5c897b218eb185e0d9f25ee Mon Sep 17 00:00:00 2001 From: felipevega2x Date: Wed, 4 Mar 2026 21:55:49 -0600 Subject: [PATCH 1/3] feat: connect WaitlistForm to Supabase API - Add POST /api/waitlist with email validation and Supabase insert - Return 409 on duplicate email, 503 on Supabase errors - Update WaitlistForm to submit to API (email, company_name, use_case) - Add duplicate/error states with appropriate messages Made-with: Cursor --- src/app/api/waitlist/route.ts | 56 ++++++++++++++++++++++++++ src/features/waitlist/WaitlistForm.tsx | 46 ++++++++++++++------- 2 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 src/app/api/waitlist/route.ts diff --git a/src/app/api/waitlist/route.ts b/src/app/api/waitlist/route.ts new file mode 100644 index 0000000..e379bf8 --- /dev/null +++ b/src/app/api/waitlist/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from "next/server"; +import { getServiceSupabase } from "@/lib/supabase"; + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +interface WaitlistPayload { + email?: string; + company_name?: string; + use_case?: string; +} + +export async function POST(request: Request) { + try { + const body = (await request.json()) as WaitlistPayload; + + const email = body.email?.trim().toLowerCase(); + if (!email || !EMAIL_REGEX.test(email)) { + return NextResponse.json( + { error: "A valid email address is required." }, + { status: 400 } + ); + } + + const company_name = body.company_name?.trim() || null; + const use_case = body.use_case?.trim() || null; + + const supabase = getServiceSupabase(); + + const { error } = await supabase + .from("waitlist") + .insert({ email, company_name, use_case }); + + if (error) { + if (error.code === "23505") { + return NextResponse.json( + { error: "This email is already on the waitlist." }, + { status: 409 } + ); + } + + console.error("[api/waitlist] Supabase error:", error.message); + return NextResponse.json( + { error: "Waitlist is temporarily unavailable. Please try again later." }, + { status: 503 } + ); + } + + return NextResponse.json({ ok: true }, { status: 201 }); + } catch (err) { + console.error("[api/waitlist] Unexpected error:", err); + return NextResponse.json( + { error: "Something went wrong. Please try again later." }, + { status: 500 } + ); + } +} diff --git a/src/features/waitlist/WaitlistForm.tsx b/src/features/waitlist/WaitlistForm.tsx index 9df9995..e285240 100644 --- a/src/features/waitlist/WaitlistForm.tsx +++ b/src/features/waitlist/WaitlistForm.tsx @@ -13,9 +13,7 @@ import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { ShineBorder } from "@/components/ui/shine-border"; -const FORMSPREE_ENDPOINT = "https://formspree.io/f/xyzndpdo"; - -type Status = "idle" | "ok" | "error"; +type Status = "idle" | "ok" | "duplicate" | "error"; export default function WaitlistForm() { const [email, setEmail] = useState(""); @@ -23,6 +21,7 @@ export default function WaitlistForm() { const [message, setMessage] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [status, setStatus] = useState("idle"); + const [errorMessage, setErrorMessage] = useState(""); const [botField, setBotField] = useState(""); @@ -32,34 +31,45 @@ export default function WaitlistForm() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setStatus("idle"); + setErrorMessage(""); if (botField) return; if (!validateEmail(email)) { setStatus("error"); + setErrorMessage("Please check your email and try again."); return; } setIsSubmitting(true); try { - const res = await fetch(FORMSPREE_ENDPOINT, { + const res = await fetch("/api/waitlist", { method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, - company, - message, - _gotcha: botField, - _subject: "New waitlist signup · Acta", - page: typeof window !== "undefined" ? window.location.href : "", + company_name: company, + use_case: message, }), }); - if (!res.ok) throw new Error("Submission failed"); + if (res.status === 409) { + setStatus("duplicate"); + return; + } + + if (res.status === 400) { + setStatus("error"); + setErrorMessage("Please check your email and try again."); + return; + } + + if (!res.ok) { + setStatus("error"); + setErrorMessage("Something went wrong. Please try again later."); + return; + } setEmail(""); setCompany(""); @@ -67,6 +77,7 @@ export default function WaitlistForm() { setStatus("ok"); } catch { setStatus("error"); + setErrorMessage("Something went wrong. Please try again later."); } finally { setIsSubmitting(false); } @@ -141,9 +152,14 @@ export default function WaitlistForm() { Thank you! We will contact you soon.

)} + {status === "duplicate" && ( +

+ This email is already on the waitlist. +

+ )} {status === "error" && (

- Something went wrong. Please check your email and try again. + {errorMessage}

)} From 417b45a87e7b8e02a66f0f9a5fab3c788ff8aa86 Mon Sep 17 00:00:00 2001 From: felipevega2x Date: Wed, 4 Mar 2026 22:06:30 -0600 Subject: [PATCH 2/3] fix: add Supabase env fallbacks for build/CI Made-with: Cursor --- src/lib/supabase.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index 0f8c760..13922e0 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -1,12 +1,19 @@ import { createClient, type SupabaseClient } from "@supabase/supabase-js"; -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; -const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; -const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY; +// Placeholders for build/CI when env vars are unset; API will return 503 on insert. +// Not real credentials – Supabase calls will fail; avoids build errors when env is empty. +const PLACEHOLDER_URL = "https://placeholder.supabase.co"; +const PLACEHOLDER_KEY = "placeholder-anon-key-not-a-secret"; + +const supabaseUrl = + process.env.NEXT_PUBLIC_SUPABASE_URL?.trim() || PLACEHOLDER_URL; +const supabaseAnonKey = + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY?.trim() || PLACEHOLDER_KEY; +const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY?.trim(); /** * Public (anon) Supabase client – safe to use in both client and server code. - * Requires NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in env. + * Uses placeholders when env vars are unset (build/CI); inserts will fail with 503. */ export const supabase: SupabaseClient = createClient( supabaseUrl, From 9d1fb523fdf205d3f0bb3e4b48525565a5ac6af8 Mon Sep 17 00:00:00 2001 From: felipevega2x Date: Wed, 4 Mar 2026 22:08:10 -0600 Subject: [PATCH 3/3] chore: apply prettier formatting Made-with: Cursor --- src/app/api/waitlist/route.ts | 4 +++- src/features/how-it-works/HowItWorks.tsx | 7 ++----- src/features/waitlist/WaitlistForm.tsx | 4 +--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/app/api/waitlist/route.ts b/src/app/api/waitlist/route.ts index e379bf8..5bf57c5 100644 --- a/src/app/api/waitlist/route.ts +++ b/src/app/api/waitlist/route.ts @@ -40,7 +40,9 @@ export async function POST(request: Request) { console.error("[api/waitlist] Supabase error:", error.message); return NextResponse.json( - { error: "Waitlist is temporarily unavailable. Please try again later." }, + { + error: "Waitlist is temporarily unavailable. Please try again later.", + }, { status: 503 } ); } diff --git a/src/features/how-it-works/HowItWorks.tsx b/src/features/how-it-works/HowItWorks.tsx index f0f39d0..a484832 100644 --- a/src/features/how-it-works/HowItWorks.tsx +++ b/src/features/how-it-works/HowItWorks.tsx @@ -6,7 +6,7 @@ import { Globe, Link2, ShieldCheck, ChevronDown } from "lucide-react"; import { cn } from "@/lib/utils"; const ease = [0.4, 0, 0.2, 1] as const; // smooth ease-out -const OPEN_DELAY = 0.10; // mini delay before opening (seconds) +const OPEN_DELAY = 0.1; // mini delay before opening (seconds) const OPEN_DURATION = 0.4; // slightly longer for smoother feel type Step = { @@ -211,10 +211,7 @@ export default function HowItWorks() { const baseId = useId(); return ( -
+
{/* Section label */}

)} {status === "error" && ( -

- {errorMessage} -

+

{errorMessage}

)}