"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>
);
}