summaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/app/api/waitlist/route.ts67
-rw-r--r--apps/web/src/components/landing/hero.tsx61
-rw-r--r--apps/web/src/lib/db/schema.ts8
-rw-r--r--apps/web/src/lib/rate-limit.ts14
4 files changed, 147 insertions, 3 deletions
diff --git a/apps/web/src/app/api/waitlist/route.ts b/apps/web/src/app/api/waitlist/route.ts
new file mode 100644
index 00000000..e949ea65
--- /dev/null
+++ b/apps/web/src/app/api/waitlist/route.ts
@@ -0,0 +1,67 @@
+import { NextRequest, NextResponse } from "next/server";
+import { db } from "@/lib/db";
+import { waitlist } from "@/lib/db/schema";
+import { eq } from "drizzle-orm";
+import { nanoid } from "nanoid";
+import { waitlistRateLimit } from "@/lib/rate-limit";
+
+export async function POST(request: NextRequest) {
+ // Rate limit check
+ const identifier = request.headers.get("x-forwarded-for") ?? "127.0.0.1";
+ const { success } = await waitlistRateLimit.limit(identifier);
+
+ if (!success) {
+ return NextResponse.json(
+ { error: "Too many requests. Please try again later." },
+ { status: 429 }
+ );
+ }
+
+ try {
+ const { email } = await request.json();
+
+ if (!email || typeof email !== "string") {
+ return NextResponse.json({ error: "Email is required" }, { status: 400 });
+ }
+
+ // Validate email format
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ return NextResponse.json(
+ { error: "Invalid email format" },
+ { status: 400 }
+ );
+ }
+
+ // Check if email already exists
+ const existingEmail = await db
+ .select()
+ .from(waitlist)
+ .where(eq(waitlist.email, email.toLowerCase()))
+ .limit(1);
+
+ if (existingEmail.length > 0) {
+ return NextResponse.json(
+ { error: "Email already registered" },
+ { status: 409 }
+ );
+ }
+
+ // Add to waitlist
+ await db.insert(waitlist).values({
+ id: nanoid(),
+ email: email.toLowerCase(),
+ });
+
+ return NextResponse.json(
+ { message: "Successfully joined waitlist!" },
+ { status: 201 }
+ );
+ } catch (error) {
+ console.error("Waitlist signup error:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx
index e5f0b867..e5eca8b6 100644
--- a/apps/web/src/components/landing/hero.tsx
+++ b/apps/web/src/components/landing/hero.tsx
@@ -5,8 +5,53 @@ import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { ArrowRight } from "lucide-react";
import Link from "next/link";
+import { useState } from "react";
+import { toast } from "sonner";
export function Hero() {
+ const [email, setEmail] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!email.trim()) {
+ toast.error("Email required", {
+ description: "Please enter your email address.",
+ });
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ const response = await fetch("/api/waitlist", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ email: email.trim() }),
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ toast.success("Welcome to the waitlist! 🎉");
+ setEmail("");
+ } else {
+ toast.error("Oops!", {
+ description: data.error || "Something went wrong. Please try again.",
+ });
+ }
+ } catch (error) {
+ toast.error("Network error", {
+ description: "Please check your connection and try again.",
+ });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
return (
<div className="relative min-h-screen flex flex-col items-center justify-center text-center px-4">
<motion.div
@@ -45,15 +90,25 @@ export function Hero() {
animate={{ opacity: 1 }}
transition={{ delay: 0.6, duration: 0.8 }}
>
- <form className="flex gap-3 w-full max-w-lg">
+ <form onSubmit={handleSubmit} className="flex gap-3 w-full max-w-lg">
<Input
type="email"
placeholder="Enter your email"
className="h-11 text-base flex-1"
+ value={email}
+ onChange={(e) => setEmail(e.target.value)}
+ disabled={isSubmitting}
required
/>
- <Button type="submit" size="lg" className="px-6 h-11 text-base">
- <span className="relative z-10">Join waitlist</span>
+ <Button
+ type="submit"
+ size="lg"
+ className="px-6 h-11 text-base"
+ disabled={isSubmitting}
+ >
+ <span className="relative z-10">
+ {isSubmitting ? "Joining..." : "Join waitlist"}
+ </span>
<ArrowRight className="relative z-10 ml-0.5 h-4 w-4 inline-block" />
</Button>
</form>
diff --git a/apps/web/src/lib/db/schema.ts b/apps/web/src/lib/db/schema.ts
index 02df5d4a..7c38724a 100644
--- a/apps/web/src/lib/db/schema.ts
+++ b/apps/web/src/lib/db/schema.ts
@@ -59,3 +59,11 @@ export const verifications = pgTable("verifications", {
() => /* @__PURE__ */ new Date()
),
}).enableRLS();
+
+export const waitlist = pgTable("waitlist", {
+ id: text("id").primaryKey(),
+ email: text("email").notNull().unique(),
+ createdAt: timestamp("created_at")
+ .$defaultFn(() => /* @__PURE__ */ new Date())
+ .notNull(),
+}).enableRLS();
diff --git a/apps/web/src/lib/rate-limit.ts b/apps/web/src/lib/rate-limit.ts
new file mode 100644
index 00000000..bf1cce02
--- /dev/null
+++ b/apps/web/src/lib/rate-limit.ts
@@ -0,0 +1,14 @@
+// lib/rate-limit.ts
+import { Ratelimit } from "@upstash/ratelimit";
+import { Redis } from "@upstash/redis";
+
+const redis = new Redis({
+ url: process.env.UPSTASH_REDIS_REST_URL!,
+ token: process.env.UPSTASH_REDIS_REST_TOKEN!,
+});
+
+export const waitlistRateLimit = new Ratelimit({
+ redis,
+ limiter: Ratelimit.slidingWindow(5, "1 m"), // 5 requests per minute
+ analytics: true,
+});