const { useState, useMemo, useEffect, useRef } = React; const API_BASE = ""; // same-origin: the funnel and /api live under librancy.com /** * LIBRANCY — Conversion Funnel Engine (Phase 1, Sprint 1) * ------------------------------------------------------------------ * A vertical-agnostic, config-driven quiz funnel. The AI-Kids vertical * is loaded as data; Entrepreneurship / Public Speaking slot in the same way. * * What's wired in this build: * - Single source of truth for child name -> name propagates to EVERY screen * (fixes the Ubasinachi/Ogbonna bug from the Horizon prototype) * - Soft price signal on the hero (FUN-01) * - REAL personalization on the offer: age band + interests drive the * recommended path and the reason copy (FUN-09) — not "all 4 to everyone" * - Entry tier + split-payment option (the two pricing fixes) * - NDPA consent checkbox before data capture (FUN-08) * - Trust artifacts (sample lesson, guarantee, social proof, Paystack) * - Mobile-first, single-column, large tap targets * - Lead persistence against the real API contract (see saveLead) * * Production note: in the real app, funnel state mirrors to localStorage for * session recovery and POSTs to /api/leads on the parent-details step. The * artifact sandbox forbids localStorage, so state lives in React only here. */ /* ============================ DESIGN TOKENS ============================ */ const T = { navy: "#001F3F", navy2: "#0A3055", coral: "#FF6B6B", coralDark: "#E24B4A", teal: "#00A88E", tealDeep: "#00715E", tealTint: "#E6F7F3", yellow: "#FFC93C", yellowTint: "#FFF6DC", ink: "#1B2733", slate: "#5C6B7A", mist: "#8597A6", line: "#E4EAF0", paper: "#FFFFFF", cloud: "#F5F8FB", good: "#1B9E6B", }; const FONT = `"Poppins","Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif`; const NGN = (n) => "₦" + n.toLocaleString("en-NG"); /* ============================ VERTICAL CONFIG ============================ */ /* Everything specific to a vertical lives here. The engine never hard-codes "AI". */ const VERTICALS = { ai: { slug: "ai", brandSuffix: "AI", accent: T.teal, hero: { eyebrow: "Librancy AI Academy", headline: "What if your child could build the future before everyone else?", sub: "Discover how AI, creativity and technology can unlock your child’s potential — with courses made for African children, at home and in the diaspora.", priceFrom: 9900, timeMins: 3, }, ageBands: [ { id: "7-9", label: "7–9", tag: "Explorer", icon: "spark" }, { id: "10-12", label: "10–12", tag: "Builder", icon: "rocket" }, { id: "13-15", label: "13–15", tag: "Founder", icon: "bolt" }, ], interests: [ "Drawing & creativity", "Making videos", "Playing games", "Building things", "Solving problems", "Speaking", "Technology", "Storytelling", "Exploring ideas", ], aspirations: [ "Confident leader", "Tech creator", "Entrepreneur", "Smart communicator", "Global problem-solver", "Innovative thinker", "World-class creator", "Financially independent", ], courses: [ { id: "explorer", name: "AI Explorers", ages: "7–9", band: "7-9", price: 37500, blurb: "Their first joyful encounter with AI — create stories, art and characters, and learn to prompt with confidence." }, { id: "builders", name: "AI Builders", ages: "10–13", band: "10-12", price: 37500, blurb: "Build real AI projects and their first AI tools using friendly no-code platforms." }, { id: "prompt", name: "Prompt Mastery", ages: "12–15", band: "13-15", price: 37500, blurb: "Master the language of AI — structured prompting and powerful AI interactions." }, { id: "founders", name: "AI Founder’s Program", ages: "10–15", band: "13-15", price: 37500, blurb: "Build a real AI business end-to-end: problem → idea → validate → build → launch → scale." }, ], bundle: { name: "Complete Bundle", count: 4, price: 87500, anchor: 150000 }, entry: { courseId: "explorer", price: 9900 }, }, }; /* ============================ TINY ICONS ============================ */ const Ico = ({ d, size = 22, stroke = "currentColor", fill = "none", sw = 1.8 }) => ( ); const I = { spark: } />, rocket: } />, bolt: } />, check: } size={18} />, arrow: } size={18} />, back: } size={18} />, shield: } size={18} />, play: } size={18} />, lock: } size={16} />, gift: } size={20} />, star: } size={15} />, }; /* ============================ PRIMITIVES ============================ */ function Shell({ children }) { return (
{children}
); } function Progress({ step, total }) { const pct = Math.round((step / total) * 100); return (
); } function TopBar({ onBack, showBack }) { return (
{showBack ? ( ) : }
); } function Page({ children }) { return
{children}
; } function Eyebrow({ children, color = T.teal }) { return
{children}
; } function H1({ children }) { return

{children}

; } function Sub({ children }) { return

{children}

; } function PrimaryBtn({ children, onClick, disabled }) { return ( ); } const ghostBtn = { background: "none", border: "none", display: "inline-flex", alignItems: "center", fontSize: 14, fontWeight: 500, cursor: "pointer", fontFamily: FONT, padding: 4 }; function Footer({ children }) { return
{children}
; } /* Selectable card (single or multi) */ function Choice({ active, onClick, children, compact }) { return ( ); } function TrustStrip() { const item = (icon, label) => (
{icon}{label}
); return (
{item(I.shield, "Secure Paystack")} {item(I.check, "30-day guarantee")} {item(I.star, "12,000+ families")}
); } /* ============================ SCREENS ============================ */ // 1 — HERO function Hero({ v, next }) { return ( <>
{v.hero.eyebrow}

{v.hero.headline}

{v.hero.sub} {/* Signature visual: a child's learning dashboard, illustrated, not stock */}
Takes about {v.hero.timeMins} minutes Plans from {NGN(v.hero.priceFrom)}
Discover their potential
); } // 2 — CHILD BASICS function ChildBasics({ v, data, set, next }) { const ok = data.childName.trim().length >= 2 && data.ageBand; return ( <>

Let’s start with the basics

Tell us a little about your child so we can shape their journey. set({ childName: e.target.value })} placeholder="e.g. Ada" autoFocus style={inputStyle} />
{v.ageBands.map((b) => { const active = data.ageBand === b.id; return ( ); })}
Continue
); } // 3 — INTERESTS function Interests({ v, data, set, next }) { const toggle = (x) => { const has = data.interests.includes(x); set({ interests: has ? data.interests.filter((i) => i !== x) : [...data.interests, x] }); }; return ( <>

What does {nameOf(data)} naturally enjoy?

Pick all that fit — there are no wrong answers.
{v.interests.map((x) => { const active = data.interests.includes(x); return ( ); })}
{data.interests.length ? `Continue · ${data.interests.length} chosen` : "Choose at least one"}
); } // 4 — EXPERIENCE function Experience({ data, set, next }) { const opts = [ { id: "frequent", t: "Yes, often", s: "Ready for intermediate content" }, { id: "little", t: "A little", s: "Ready for beginner content" }, { id: "none", t: "Not yet", s: "Perfect — 80% of our learners start here" }, ]; return ( <>

Has {nameOf(data)} used AI tools before?

This helps us set the right starting point.
{opts.map((o) => ( set({ experience: o.id })}>
{o.t}
{o.s}
))}
{data.experience === "none" && (
Excellent! Most of our learners start exactly where {nameOf(data)} is. We guide them step by step from the very beginning.
)}
Continue
); } // 5 — ASPIRATIONS function Aspirations({ v, data, set, next }) { const toggle = (x) => { const has = data.aspirations.includes(x); set({ aspirations: has ? data.aspirations.filter((i) => i !== x) : [...data.aspirations, x] }); }; return ( <>

What future would you love for {nameOf(data)}?

Choose all that resonate with you.
{v.aspirations.map((x) => { const active = data.aspirations.includes(x); return ( ); })}
Continue
); } // 6 — VALUE REFRAME (the anchor) function ValueReframe({ data, next }) { return ( <> A quick thought

Some gifts last a season. Others last a lifetime.

Children outgrow shoes in months. Future skills shape the rest of their lives.
A new pair of shoes
Outgrown in months
Future-ready skills {I.star}
Last a lifetime

Which investment could shape {nameOf(data)} longer?

I understand
); } // 7 — COMMITMENT SLIDER function Commitment({ data, set, next }) { const val = data.commitment; const labels = ["Not important", "Moderately", "Very important", "Extremely important"]; const idx = Math.min(3, Math.floor(val / 34)); return ( <>

How important is future-ready education for {nameOf(data)}?

Help us understand your commitment.
set({ commitment: Number(e.target.value) })} style={{ width: "100%", accentColor: T.navy, height: 6 }} />
{labels.map((l, i) => ( {l} ))}
{idx >= 2 && (
Perfect — you’re committed to {nameOf(data)}’s future.
)}
Continue
); } // 8 — PARENT DETAILS (+ NDPA consent, writes Lead) function ParentDetails({ data, set, next }) { const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email); const ok = data.parentName.trim().length >= 2 && emailOk && data.consent; return ( <>

Where should we send {nameOf(data)}’s learning plan?

We’ll use this to create the account and send {nameOf(data)}’s curriculum. set({ parentName: e.target.value })} placeholder="Your name" style={inputStyle} /> set({ email: e.target.value })} placeholder="you@email.com" type="email" style={inputStyle} />
We’ll send account login details here.
set({ phone: e.target.value })} placeholder="+234…" style={inputStyle} />
Continue to courses
); } // 9 — OFFER (real personalization + entry tier + split pay) function Offer({ v, data, set, next }) { const rec = useMemo(() => recommend(v, data), [v, data]); const sel = data.selection; const setSel = (s) => set({ selection: s }); const bundleSelected = sel?.type === "bundle"; const total = sel?.type === "bundle" ? v.bundle.price : sel?.type === "course" ? v.courses.find((c) => c.id === sel.id)?.price ?? 0 : sel?.type === "entry" ? v.entry.price : 0; return ( <>

Choose {nameOf(data)}’s learning path

{rec.reason} {/* Recommended bundle (anchor + default) */}
★ Recommended for {nameOf(data)}
{/* Entry tier — the easy first yes */}
{rec.ordered.map((c) => { const active = sel?.type === "course" && sel.id === c.id; return ( ); })}
{/* Split-pay option appears when a paid path is selected */} {total >= 37500 && ( )}
{selLabel(v, sel) || "Select a path"} {total ? NGN(total) : "—"}
Proceed to checkout
); } // 10 — CHECKOUT function Checkout({ v, data, onPay }) { const sel = data.selection; const total = sel?.type === "bundle" ? v.bundle.price : sel?.type === "course" ? v.courses.find((c) => c.id === sel.id)?.price ?? 0 : sel?.type === "entry" ? v.entry.price : 0; const perInstallment = Math.ceil(total / 3 / 100) * 100; return ( <>

Ready to give {nameOf(data)} an unfair advantage?

Review the plan below to finish enrolment.
{I.shield} Your selection
Total {data.splitPay ? "(today)" : "investment"} {NGN(data.splitPay ? perInstallment : total)}
{data.splitPay && (
then {NGN(perInstallment)} × 2 — one course unlocks per payment
)}
{I.shield} 30-day satisfaction guarantee
If you’re not thrilled with {nameOf(data)}’s progress, we’ll refund you in full. No questions asked.
Complete payment · {NGN(data.splitPay ? perInstallment : total)}
); } // 11 — HANDOFF (Sprint 2 seam, shown honestly) function Handoff({ data }) { return (
} size={34} stroke={T.tealDeep} />

Lead captured — Paystack opens here

In production this hands off to Paystack to take payment, then creates {nameOf(data)}’s account and grants the course entitlement. That wiring is Sprint 2 of Phase 1 — the funnel engine you just clicked through is Sprint 1.

{`POST /api/leads        → lead saved (done at step 8)
POST /api/checkout     → Paystack init  (Sprint 2)
POST /webhooks/paystack→ verify + grant (Sprint 2)
   → entitlement(child → course) created
   → account email sent`}
        
); } /* ============================ SUB-COMPONENTS ============================ */ function Logo() { return (
{["#FF6B6B", "#00D4AA", "#FFC93C", "#0066FF", "#FF6B9D"].map((c, i) => { const a = (i / 5) * Math.PI * 2; return ; })}
Librancy AI
); } function DashboardArt({ accent }) { return (
Welcome back, Champion 👋
Your AI journey70%
{["Intro to AI", "Prompt basics", "Build a tool"].map((t, i) => (
{t}
))}
); } const ShoeArt = () => ( ); const FutureArt = ({ accent }) => ( ); function Label({ children, style }) { return
{children}
; } const inputStyle = { width: "100%", padding: "15px 16px", borderRadius: 14, border: `2px solid ${T.line}`, fontSize: 16, fontFamily: FONT, color: T.ink, outline: "none", boxSizing: "border-box", background: T.paper, }; function Divider({ label }) { return (
{label}
); } function Row({ k, val }) { return (
{k} {val}
); } function Perk({ text }) { return (
{I.check}{text}
); } /* ============================ LOGIC ============================ */ function nameOf(d) { const n = (d.childName || "").trim(); return n.length ? n : "your child"; } // Real personalization: age band picks the lead course; interests reorder; reason uses actual inputs. function recommend(v, data) { const band = data.ageBand; const lead = v.courses.find((c) => c.band === band) || v.courses[0]; const ordered = [lead, ...v.courses.filter((c) => c.id !== lead.id)]; const picks = data.interests.slice(0, 3).map((s) => s.toLowerCase()); const interestBit = picks.length ? `${nameOf(data)}’s love of ${listify(data.interests.slice(0, 3))}` : `${nameOf(data)}’s profile`; const reason = `Based on ${interestBit} and age (${data.ageBand}), we’d start with ${lead.name} — and the bundle covers the full path as ${nameOf(data)} grows.`; return { lead, ordered, reason }; } function listify(arr) { const a = arr.map((s) => s.toLowerCase()); if (a.length === 1) return a[0]; if (a.length === 2) return `${a[0]} and ${a[1]}`; return `${a.slice(0, -1).join(", ")} and ${a[a.length - 1]}`; } function selLabel(v, sel) { if (!sel) return ""; if (sel.type === "bundle") return `${v.bundle.name} (${v.bundle.count} courses)`; if (sel.type === "entry") return `${v.courses.find((c) => c.id === v.entry.courseId)?.name} (starter)`; if (sel.type === "course") return v.courses.find((c) => c.id === sel.id)?.name || ""; return ""; } // Maps the funnel's local course ids to the backend course slugs. const SLUG_FOR = { explorer: "explorers", builders: "builders", prompt: "prompt-mastery", founders: "ai-founders" }; // Starts a real Paystack checkout: server prices the SKU, returns a redirect URL. async function startCheckout(data) { const sku = data.selection.type === "bundle" ? "bundle" : data.selection.type === "entry" ? "entry" : "course:" + (SLUG_FOR[data.selection.id] || data.selection.id); const payload = { email: data.email, parentName: data.parentName, phone: data.phone, childName: data.childName, ageBand: data.ageBand, interests: data.interests, sku, plan: data.splitPay ? "split" : "full", }; try { const res = await fetch(API_BASE + "/api/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const j = await res.json(); if (j.authorization_url) { window.location.href = j.authorization_url; return; } alert(j.error || "Could not start checkout. Please try again."); } catch (e) { alert("Network error starting checkout. Please try again."); } } // Posts the lead to the live PHP API (api/index.php → /leads). function saveLead(data) { const lead = { childName: data.childName, ageBand: data.ageBand, interests: data.interests, experience: data.experience, aspirations: data.aspirations, commitment: data.commitment, parentName: data.parentName, email: data.email, phone: data.phone, consentAt: new Date().toISOString(), vertical: "ai", }; fetch(API_BASE + "/api/leads", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(lead), }).catch((e) => console.warn("lead save failed (non-blocking):", e)); return lead; } /* ============================ ENGINE ============================ */ const STEPS = ["hero", "child", "interests", "experience", "aspirations", "reframe", "commitment", "parent", "offer", "checkout", "done"]; function LibrancyFunnel() { const v = VERTICALS.ai; const [step, setStep] = useState(0); const [data, setData] = useState({ childName: "", ageBand: "", interests: [], experience: "", aspirations: [], commitment: 50, parentName: "", email: "", phone: "", consent: false, selection: { type: "bundle" }, splitPay: false, }); const set = (patch) => setData((d) => ({ ...d, ...patch })); const topRef = useRef(null); // Pull live catalog config from the API; fall back to the embedded config offline. useEffect(() => { fetch(API_BASE + "/api/verticals/ai") .then((r) => (r.ok ? r.json() : null)) .then((cfg) => { if (cfg && cfg.config && cfg.config.hero) { Object.assign(VERTICALS.ai.hero, cfg.config.hero); } }) .catch(() => {}); }, []); const go = (dir) => { setStep((s) => { const nextStep = Math.max(0, Math.min(STEPS.length - 1, s + dir)); if (STEPS[s] === "parent" && dir === 1) saveLead(data); // Lead persists here return nextStep; }); }; useEffect(() => { topRef.current && topRef.current.scrollTo(0, 0); }, [step]); const screen = STEPS[step]; const showBack = step > 0 && screen !== "done"; return (
{screen !== "hero" && go(-1)} showBack={showBack} />} {screen === "hero" && go(1)} />} {screen === "child" && go(1)} />} {screen === "interests" && go(1)} />} {screen === "experience" && go(1)} />} {screen === "aspirations" && go(1)} />} {screen === "reframe" && go(1)} />} {screen === "commitment" && go(1)} />} {screen === "parent" && go(1)} />} {screen === "offer" && go(1)} />} {screen === "checkout" && startCheckout(data)} />} {screen === "done" && }
); } // Mount (no build step — React + Babel load from CDN in index.html). ReactDOM.createRoot(document.getElementById("root")).render();