Waitlist Signup
Conversion-optimized signup forms for building your pre-launch audience.
Conversion
- • Email validation
- • Duplicate prevention
- • Social proof display
- • Success confirmation
Technical
- • Server-side validation
- • Real-time feedback
- • Loading states
- • Accessibility support
Signup Form
src/components/WaitlistSignup.tsx
1"use client";23import { useState } from "react";4import { addToWaitlist } from "~/server/actions/waitlist";56export function WaitlistSignup() {7 const [email, setEmail] = useState("");8 const [isLoading, setIsLoading] = useState(false);9 const [message, setMessage] = useState(null);1011 const handleSubmit = async (e) => {12 e.preventDefault();13 setIsLoading(true);1415 const result = await addToWaitlist(email);1617 if (result.success) {18 setMessage({ type: "success", text: "You're on the list!" });19 setEmail("");20 } else {21 setMessage({ type: "error", text: result.error });22 }2324 setIsLoading(false);25 };2627 return (28 <form onSubmit={handleSubmit} className="flex gap-2">29 <input30 type="email"31 placeholder="Enter your email"32 value={email}33 onChange={(e) => setEmail(e.target.value)}34 disabled={isLoading}35 required36 />37 <button type="submit" disabled={isLoading}>38 {isLoading ? "Joining..." : "Join"}39 </button>4041 {message && (42 <p className={message.type === "success" ? "text-green-600" : "text-red-600"}>43 {message.text}44 </p>45 )}46 </form>47 );48}
Server Action
src/server/actions/waitlist.ts
1"use server";23import { db } from "~/server/db";4import { z } from "zod";56const emailSchema = z.string().email("Please enter a valid email");78export async function addToWaitlist(email: string) {9 try {10 const validEmail = emailSchema.parse(email.toLowerCase().trim());1112 const existing = await db.waitlist.findUnique({13 where: { email: validEmail },14 });1516 if (existing) {17 return { success: false, error: "You're already on the list!" };18 }1920 await db.waitlist.create({21 data: { email: validEmail, isActive: true },22 });2324 return { success: true, message: "You're on the list!" };2526 } catch (error) {27 if (error instanceof z.ZodError) {28 return { success: false, error: error.errors[0]?.message };29 }30 return { success: false, error: "Something went wrong" };31 }32}
Social Proof
1// Fetch waitlist stats2export async function getWaitlistStats() {3 const [total, recentSignups] = await Promise.all([4 db.waitlist.count({ where: { isActive: true } }),5 db.waitlist.count({6 where: {7 isActive: true,8 createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },9 },10 }),11 ]);1213 return { total, recentSignups };14}1516// Display17{stats.total > 0 && (18 <div className="text-sm text-muted-foreground">19 <strong>{stats.total.toLocaleString()}</strong> people joined20 </div>21)}
Best Practices
Conversion
- • Clear, benefit-focused headlines
- • Show social proof
- • Minimize form fields
- • Add trust indicators
Technical
- • Validate server-side
- • Handle duplicates gracefully
- • Provide immediate feedback
- • Ensure mobile responsive
Documentation Complete
Return to the documentation overview