diff --git a/app/(landing)/landing.tsx b/app/(landing)/landing.tsx index 9e4c50f656..14ee9d4429 100644 --- a/app/(landing)/landing.tsx +++ b/app/(landing)/landing.tsx @@ -1,7 +1,6 @@ import Image from 'next/image' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' import HeroWorkflowProvider from './hero-workflow' +import WaitlistForm from './waitlist-form' export default function Landing() { return ( @@ -49,16 +48,7 @@ export default function Landing() { user-friendly environment for devs and agents

-
- - -
+
diff --git a/app/(landing)/waitlist-form.tsx b/app/(landing)/waitlist-form.tsx new file mode 100644 index 0000000000..ba830277c4 --- /dev/null +++ b/app/(landing)/waitlist-form.tsx @@ -0,0 +1,81 @@ +'use client' + +import { useState } from 'react' +import { z } from 'zod' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' + +const emailSchema = z.string().email('Please enter a valid email') + +export default function WaitlistForm() { + const [email, setEmail] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + const [message, setMessage] = useState('') + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setMessage('') + + try { + // Validate email + emailSchema.parse(email) + + setIsSubmitting(true) + const response = await fetch('/api/waitlist', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email }), + }) + + const data = await response.json() + + if (!response.ok) { + setMessage(data.message || 'Failed to join waitlist') + return + } + + setMessage('Thanks for joining our waitlist!') + setEmail('') + } catch (error) { + if (error instanceof z.ZodError) { + setMessage('Please enter a valid email') + } else { + setMessage(error instanceof Error ? error.message : 'Something went wrong') + } + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+ setEmail(e.target.value)} + disabled={isSubmitting} + /> + +
+ {message && ( +

+ {message} +

+ )} +
+ ) +} diff --git a/app/api/waitlist/route.ts b/app/api/waitlist/route.ts new file mode 100644 index 0000000000..445b7779c2 --- /dev/null +++ b/app/api/waitlist/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from 'next/server' +import { eq } from 'drizzle-orm' +import { nanoid } from 'nanoid' +import { z } from 'zod' +import { db } from '@/db' +import { waitlist } from '@/db/schema' + +const waitlistSchema = z.object({ + email: z.string().email(), +}) + +export async function POST(request: Request) { + try { + const body = await request.json() + const { email } = waitlistSchema.parse(body) + + // Check if email already exists + const existingEntry = await db + .select() + .from(waitlist) + .where(eq(waitlist.email, email)) + .execute() + + if (existingEntry.length > 0) { + return NextResponse.json({ message: 'Email already registered' }, { status: 400 }) + } + + // Add to waitlist + await db.insert(waitlist).values({ + id: nanoid(), + email, + createdAt: new Date(), + updatedAt: new Date(), + }) + + return NextResponse.json({ message: 'Successfully joined waitlist' }, { status: 200 }) + } catch (error) { + console.error('Waitlist error:', error) + return NextResponse.json({ message: 'Failed to join waitlist' }, { status: 500 }) + } +} diff --git a/db/migrations/0002_previous_xavin.sql b/db/migrations/0002_previous_xavin.sql new file mode 100644 index 0000000000..e9c81a1163 --- /dev/null +++ b/db/migrations/0002_previous_xavin.sql @@ -0,0 +1,8 @@ +CREATE TABLE "waitlist" ( + "id" text PRIMARY KEY NOT NULL, + "email" text NOT NULL, + "status" text DEFAULT 'pending' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "waitlist_email_unique" UNIQUE("email") +); diff --git a/db/migrations/meta/0002_snapshot.json b/db/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000000..1ab9154c16 --- /dev/null +++ b/db/migrations/meta/0002_snapshot.json @@ -0,0 +1,430 @@ +{ + "id": "80542c3c-48a3-41e5-b911-fe51152efccc", + "prevId": "a17085d8-94da-40fe-b71c-e143059e3f80", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json index 1971faacb9..e3bfcbdf53 100644 --- a/db/migrations/meta/_journal.json +++ b/db/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1739773751302, "tag": "0001_foamy_dakota_north", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1739776634326, + "tag": "0002_previous_xavin", + "breakpoints": true } ] } diff --git a/db/schema.ts b/db/schema.ts index 731d9e4895..7b147bb51b 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -62,3 +62,11 @@ export const workflow = pgTable('workflow', { createdAt: timestamp('created_at').notNull(), updatedAt: timestamp('updated_at').notNull(), }) + +export const waitlist = pgTable('waitlist', { + id: text('id').primaryKey(), + email: text('email').notNull().unique(), + status: text('status').notNull().default('pending'), // pending, approved, rejected + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), +})