Skip to main content
Back to Blog
Tutorials
3 min read
November 12, 2024

How to Build a Waitlist Page in Next.js

Create a pre-launch waitlist page with email capture, position tracking, referral bonuses, and a countdown timer.

Ryel Banfield

Founder & Lead Developer

A waitlist page creates anticipation before launch and builds your email list. Here is how to build a compelling one.

Step 1: Database Schema

// db/schema.ts
import { pgTable, text, timestamp, integer, uuid } from "drizzle-orm/pg-core";

export const waitlist = pgTable("waitlist", {
  id: uuid("id").defaultRandom().primaryKey(),
  email: text("email").notNull().unique(),
  name: text("name"),
  referralCode: text("referral_code").notNull().unique(),
  referredBy: text("referred_by"),
  position: integer("position").notNull(),
  referralCount: integer("referral_count").default(0),
  createdAt: timestamp("created_at").defaultNow(),
});

Step 2: API Route

// app/api/waitlist/route.ts
import { NextResponse } from "next/server";
import { db } from "@/db";
import { waitlist } from "@/db/schema";
import { eq, count } from "drizzle-orm";
import { nanoid } from "nanoid";

export async function POST(req: Request) {
  const { email, name, ref } = await req.json();

  if (!email || !email.includes("@")) {
    return NextResponse.json(
      { error: "Valid email is required" },
      { status: 400 }
    );
  }

  // Check if already registered
  const existing = await db
    .select()
    .from(waitlist)
    .where(eq(waitlist.email, email))
    .limit(1);

  if (existing.length > 0) {
    return NextResponse.json({
      position: existing[0].position,
      referralCode: existing[0].referralCode,
      alreadyRegistered: true,
    });
  }

  // Get next position
  const [{ total }] = await db
    .select({ total: count() })
    .from(waitlist);
  const position = total + 1;

  // Generate referral code
  const referralCode = nanoid(8);

  // Insert
  await db.insert(waitlist).values({
    email,
    name: name || null,
    referralCode,
    referredBy: ref || null,
    position,
  });

  // Increment referrer's count
  if (ref) {
    await db
      .update(waitlist)
      .set({
        referralCount: db.raw`referral_count + 1`,
      })
      .where(eq(waitlist.referralCode, ref));
  }

  return NextResponse.json({
    position,
    referralCode,
    alreadyRegistered: false,
  });
}

Step 3: Waitlist Page

// app/page.tsx
import { WaitlistForm } from "@/components/WaitlistForm";
import { CountdownTimer } from "@/components/CountdownTimer";

export default function WaitlistPage() {
  return (
    <main className="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-950 via-blue-950 to-gray-950 text-white">
      <div className="mx-auto max-w-lg px-4 text-center">
        {/* Badge */}
        <div className="mb-6 inline-flex items-center rounded-full border border-blue-500/30 bg-blue-500/10 px-4 py-1.5 text-sm text-blue-300">
          Coming Soon
        </div>

        {/* Headline */}
        <h1 className="text-4xl font-bold tracking-tight sm:text-5xl">
          Something great is
          <span className="bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">
            {" "}
            coming soon
          </span>
        </h1>

        <p className="mt-4 text-lg text-gray-400">
          Be the first to know when we launch. Join the waitlist and get early
          access.
        </p>

        {/* Countdown */}
        <div className="mt-8">
          <CountdownTimer targetDate="2026-07-01T00:00:00Z" />
        </div>

        {/* Form */}
        <div className="mt-8">
          <WaitlistForm />
        </div>

        {/* Social proof */}
        <p className="mt-6 text-sm text-gray-500">
          Join 2,400+ people already on the waitlist
        </p>
      </div>
    </main>
  );
}

Step 4: Waitlist Form Component

"use client";

import { useState, useTransition } from "react";
import { useSearchParams } from "next/navigation";

export function WaitlistForm() {
  const [email, setEmail] = useState("");
  const [result, setResult] = useState<{
    position: number;
    referralCode: string;
  } | null>(null);
  const [isPending, startTransition] = useTransition();
  const searchParams = useSearchParams();
  const ref = searchParams.get("ref");

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();

    startTransition(async () => {
      const res = await fetch("/api/waitlist", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, ref }),
      });
      const data = await res.json();
      if (res.ok) setResult(data);
    });
  }

  if (result) {
    const referralLink = `${window.location.origin}?ref=${result.referralCode}`;

    return (
      <div className="rounded-2xl border border-green-500/30 bg-green-500/10 p-6">
        <p className="text-lg font-semibold text-green-300">
          You are #{result.position} on the waitlist!
        </p>
        <p className="mt-2 text-sm text-gray-400">
          Share your referral link to move up:
        </p>
        <div className="mt-3 flex gap-2">
          <input
            readOnly
            value={referralLink}
            className="flex-1 rounded-lg bg-gray-800 px-3 py-2 text-sm text-gray-300"
          />
          <button
            onClick={() => navigator.clipboard.writeText(referralLink)}
            className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium hover:bg-blue-700"
          >
            Copy
          </button>
        </div>
        <div className="mt-4 flex justify-center gap-3">
          <a
            href={`https://twitter.com/intent/tweet?text=I just joined the waitlist!&url=${encodeURIComponent(referralLink)}`}
            target="_blank"
            rel="noopener noreferrer"
            className="rounded-lg bg-gray-800 px-3 py-1.5 text-sm hover:bg-gray-700"
          >
            Share on X
          </a>
        </div>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} className="flex gap-2">
      <input
        type="email"
        required
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Enter your email"
        className="flex-1 rounded-xl border border-gray-700 bg-gray-900 px-4 py-3 text-white placeholder-gray-500 focus:border-blue-500 focus:outline-none"
      />
      <button
        type="submit"
        disabled={isPending}
        className="rounded-xl bg-blue-600 px-6 py-3 font-medium hover:bg-blue-700 disabled:opacity-50"
      >
        {isPending ? "Joining..." : "Join"}
      </button>
    </form>
  );
}

Step 5: Countdown Timer

"use client";

import { useState, useEffect } from "react";

export function CountdownTimer({ targetDate }: { targetDate: string }) {
  const [timeLeft, setTimeLeft] = useState(calculateTimeLeft());

  function calculateTimeLeft() {
    const diff = new Date(targetDate).getTime() - Date.now();
    if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0 };

    return {
      days: Math.floor(diff / 86400000),
      hours: Math.floor((diff % 86400000) / 3600000),
      minutes: Math.floor((diff % 3600000) / 60000),
      seconds: Math.floor((diff % 60000) / 1000),
    };
  }

  useEffect(() => {
    const timer = setInterval(() => setTimeLeft(calculateTimeLeft()), 1000);
    return () => clearInterval(timer);
  }, []);

  return (
    <div className="flex justify-center gap-4">
      {Object.entries(timeLeft).map(([unit, value]) => (
        <div key={unit} className="text-center">
          <div className="rounded-xl bg-gray-800/50 px-4 py-3 text-3xl font-bold tabular-nums">
            {String(value).padStart(2, "0")}
          </div>
          <p className="mt-1 text-xs uppercase tracking-wider text-gray-500">
            {unit}
          </p>
        </div>
      ))}
    </div>
  );
}

Need a Pre-Launch Landing Page?

We design and build stunning launch pages that capture leads and build anticipation. Contact us to get started.

waitlistlaunch pagelanding pageNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles