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}
)}