Fazekit

Code

"use client";
import * as React from "react";
import { MotionConfig, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
import { defaultWaitlistData, type WaitlistData } from "./data";
import { Faq, Footer } from "./sections/faq-footer";
import { Features } from "./sections/features";
import { Founders } from "./sections/founders";
import { Hero } from "./sections/hero";
import { Navbar } from "./sections/navbar";
import { Peek } from "./sections/peek";
import { Proof } from "./sections/proof";
import { scrollToId } from "./sections/ui";
import type { WaitlistEntry } from "./sections/waitlist-form";

export type { WaitlistData, WaitlistEntry };
export { defaultWaitlistData };

export interface StartupWaitlistTemplateProps {
  /** Override any top-level content group (brand, hero, features…). Missing groups use the demo content. */
  data?: Partial<WaitlistData>;
  /**
   * Save the email to your backend. Resolve with the real position / referral URL if you have them;
   * otherwise a deterministic demo position and link are generated.
   */
  onJoin?: (email: string) => Promise<Partial<Pick<WaitlistEntry, "position" | "referralUrl" | "referrals">> | void> | void;
  /** Remember the visitor's spot in localStorage so returning visitors see their position. Default true. */
  persist?: boolean;
  className?: string;
}

const STORE_KEY = "nova-waitlist-entry";

function hash(s: string) {
  let h = 2166136261;
  for (const c of s) h = Math.imul(h ^ c.charCodeAt(0), 16777619);
  return h >>> 0;
}

export function StartupWaitlistTemplate({ data, onJoin, persist = true, className }: StartupWaitlistTemplateProps) {
  const d = React.useMemo(() => ({ ...defaultWaitlistData, ...data }), [data]);
  const reduce = useReducedMotion();
  const [entry, setEntry] = React.useState<WaitlistEntry | null>(null);

  // Restore a previous sign-up (client only).
  React.useEffect(() => {
    if (!persist) return;
    try {
      const raw = window.localStorage.getItem(STORE_KEY);
      if (raw) setEntry(JSON.parse(raw) as WaitlistEntry);
    } catch {
      /* storage unavailable */
    }
  }, [persist]);

  const join = React.useCallback(
    async (email: string) => {
      const [res] = await Promise.all([onJoin?.(email), new Promise((r) => setTimeout(r, 900))]);
      const h = hash(email.toLowerCase());
      const code = h.toString(36).slice(0, 6);
      const next: WaitlistEntry = {
        email,
        position: res?.position ?? d.waitlist.baseCount + 1 + (h % 180),
        code,
        referralUrl: res?.referralUrl ?? `https://${d.brand.domain}/r/${code}`,
        referrals: res?.referrals ?? 0,
      };
      setEntry(next);
      if (persist) {
        try {
          window.localStorage.setItem(STORE_KEY, JSON.stringify(next));
        } catch {
          /* ignore */
        }
      }
    },
    [onJoin, d.waitlist.baseCount, d.brand.domain, persist],
  );

  const toForm = React.useCallback(() => {
    scrollToId("top", reduce);
    window.setTimeout(() => document.getElementById(entry ? "nv-email-ref" : "nv-email")?.focus({ preventScroll: true }), reduce ? 0 : 700);
  }, [reduce, entry]);

  const count = d.waitlist.baseCount + (entry ? 1 : 0);

  return (
    <MotionConfig reducedMotion="user">
      <div className={cn("relative w-full overflow-x-clip bg-background text-foreground antialiased selection:bg-indigo-500/25", className)}>
        <Navbar name={d.brand.name} links={d.nav} joined={!!entry} onJoin={toForm} />
        <main>
          <Hero brand={d.brand} hero={d.hero} waitlist={d.waitlist} proof={d.proof} entry={entry} count={count} onJoin={join} />
          <Features features={d.features} />
          <Peek peek={d.peek} brandName={d.brand.name} />
          <Proof proof={d.proof} count={count} joinedEmail={entry?.email ?? null} />
          <Founders founders={d.founders} />
          <Faq faq={d.faq} />
        </main>
        <Footer brand={d.brand} footer={d.footer} position={entry?.position ?? null} onJoin={toForm} />
      </div>
    </MotionConfig>
  );
}

More in Landing Pages

View all →