// Beta signup — invite request flow with submit/confirmed states.
function Arrow({ size = 14 }) {
return (
);
}
function Glow({ color = "var(--accent-glow)", size = 700, opacity = 1, style = {} }) {
return (
);
}
const ROLES = [
{ value: "smb", label: "Small business owner", hint: "I run a shop, salon, studio, café…" },
{ value: "freelancer", label: "Freelancer / agency", hint: "I build tools for clients" },
{ value: "ideaperson", label: "I just have an idea", hint: "First-time builder, no code" },
];
const SOURCES = ["Reddit", "Twitter / X", "TikTok", "YouTube", "A friend", "Google", "Something else"];
const BENEFITS = [
{
icon: "lightning",
title: "First access",
body: "Skip the queue when public beta opens. You build before everyone else.",
},
{
icon: "gift",
title: "90 days of Pro, free",
body: "Full launch features — hosting, marketing, customer acquisition — on the house.",
},
{
icon: "chat",
title: "Direct line to the team",
body: "Private channel with the people building Vibn. Your feedback ships.",
},
];
function BetaApp() {
const [submitted, setSubmitted] = React.useState(false);
const [submitting, setSubmitting] = React.useState(false);
const [scrolled, setScrolled] = React.useState(false);
const [form, setForm] = React.useState({
email: "",
name: "",
build: "",
role: "smb",
source: "",
});
React.useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const update = (k, v) => setForm((f) => ({ ...f, [k]: v }));
const valid = /\S+@\S+\.\S+/.test(form.email) && form.build.trim().length > 4;
const handleSubmit = (e) => {
e.preventDefault();
if (!valid || submitting) return;
setSubmitting(true);
setTimeout(() => {
setSubmitting(false);
setSubmitted(true);
window.scrollTo({ top: 0, behavior: "smooth" });
}, 700);
};
// Stable "queue position" based on email — feels real, deterministic.
const queuePos = React.useMemo(() => {
let h = 7;
for (const c of form.email) h = (h * 31 + c.charCodeAt(0)) >>> 0;
return 2100 + (h % 900); // 2,100 – 2,999
}, [form.email]);
return (
<>
{submitted ? (
) : (
<>
Closed beta · invite-only
Be one of the first to vibe with Vibn.
We're letting in 50 new builders a week.
Tell us what you want to build — the most exciting ideas get the invite first.
>
)}
{/* What you get — shown on both states */}
What you get on the inside
{BENEFITS.map((b) => (
))}
>
);
}
function Field({ label, title, hint, required, children }) {
return (
);
}
function BenefitIcon({ name }) {
const p = { width: 18, height: 18, viewBox: "0 0 20 20", fill: "none",
stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" };
if (name === "lightning") return ;
if (name === "gift") return ;
if (name === "chat") return ;
return null;
}
// ── Submitted state ─────────────────────────────────────────────────────────
function Confirmed({ form, queuePos }) {
const [copied, setCopied] = React.useState(false);
// Fake-but-stable referral code
const ref = React.useMemo(() => {
const seed = form.email || form.name || "anon";
let h = 5;
for (const c of seed) h = (h * 33 + c.charCodeAt(0)) >>> 0;
return "v-" + h.toString(36).slice(0, 6);
}, [form.email, form.name]);
const link = typeof window !== "undefined" ? `${window.location.origin}/join?ref=${ref}` : `vibn.app/join?ref=${ref}`;
const copyLink = () => {
try { navigator.clipboard.writeText(link); } catch (e) { /* noop */ }
setCopied(true);
setTimeout(() => setCopied(false), 1800);
};
// Compute a queue progress bar percentage — visual feedback only
const pct = Math.max(2, Math.min(98, 100 - (queuePos - 2100) / 9));
return (
You're on the list
{form.name ? <>Welcome, {form.name}.> : <>You're in line.>}
We got your invite request — keep an eye on {form.email}.
your spot in line
#{queuePos.toLocaleString()}
You should hear from us in ~{Math.ceil((queuePos - 50) / 50)} weeks. Don't want to wait?
Skip the line
Send 3 friends — jump to the front.
Each friend who joins via your link bumps you up 500 spots.
vibn.app/join?ref=
{ref}
{form.build && (
What we'll help you build first
"{form.build}"
)}
);
}
function ShareIcon({ name }) {
const p = { width: 14, height: 14, viewBox: "0 0 16 16", fill: "currentColor" };
if (name === "x") return ;
if (name === "reddit") return ;
if (name === "mail") return ;
return null;
}
// ── Styles ──────────────────────────────────────────────────────────────────
function BetaStyle() {
return ;
}
ReactDOM.createRoot(document.getElementById("root")).render();