/* LIBRANCY — Parent Dashboard v2 (premium brand redesign) * Navy brand header with the real logo · rank-coloured child identity · * certificate showcase (the aspiration artifact) · gradient Beacons economy · * styled leaderboards · the Celebrate loop. Same API, same routes. */ const { useState, useEffect, useCallback } = React; const API = ""; const T = { navy:"#101450", navy2:"#1B2270", navy3:"#26307E", coral:"#FF6B6B", teal:"#02B5AA", tealDeep:"#00715E", tealTint:"#E6F7F3", yellow:"#FEC323", yellowTint:"#FFF6DC", gold:"#B8860B", purple:"#6C5CE7", purpleDeep:"#4C3FD1", purpleTint:"#EEEBFD", pink:"#FF6B9D", pinkTint:"#FFEDF3", blue:"#1C6BF6", blueTint:"#E8F0FF", ink:"#1B2733", slate:"#5C6B7A", mist:"#8597A6", line:"#E4EAF0", paper:"#FFFFFF", cloud:"#F5F8FB", }; const FONT = `"Poppins","Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif`; const NGN = (k) => "₦" + Math.round(k/100).toLocaleString("en-NG"); const REGIONS = ["Lagos","Abuja (FCT)","Rivers","Oyo","Kano","Enugu","Anambra","Delta","Kaduna", "Other Nigeria","Diaspora — UK","Diaspora — US/Canada","Diaspora — Other"]; const RANKS = { Explorer:{ icon:"✦" }, Builder:{ icon:"🚀" }, Creator:{ icon:"★" }, Founder:{ icon:"👑" }, }; const AVATARS = ["🦁","🐘","🦋","🚀","🌟","🎨"]; const store = { get:(k)=>{ try{ return JSON.parse(localStorage.getItem("lbrp_"+k)); }catch{ return null; } }, set:(k,v)=>localStorage.setItem("lbrp_"+k, JSON.stringify(v)), del:(k)=>localStorage.removeItem("lbrp_"+k), }; async function api(path, opts={}) { const token = store.get("token"); const res = await fetch(API+"/api"+path, { method: opts.method||"GET", headers:{ "Content-Type":"application/json", ...(token?{Authorization:"Bearer "+token}:{}) }, body: opts.body ? JSON.stringify(opts.body) : undefined, }); const j = await res.json().catch(()=>({})); if (!res.ok) throw new Error(j.error||"Something went wrong"); return j; } const btnS=(bg,extra={})=>({ border:"none", borderRadius:14, padding:"14px 18px", width:"100%", cursor:"pointer", background:bg, color:"#fff", fontSize:15, fontWeight:600, fontFamily:FONT, ...extra }); const chip=(bg,c)=>({ display:"inline-flex", alignItems:"center", gap:5, background:bg, color:c, fontSize:11.5, fontWeight:700, padding:"4px 11px", borderRadius:20 }); const inputS={ width:"100%", padding:"14px 15px", borderRadius:13, border:`2px solid ${T.line}`, fontSize:15.5, fontFamily:FONT, boxSizing:"border-box", background:"#fff" }; function Card({children,style}){ return
{children}
; } /* ── Auth ── */ function Login({ onDone }) { const params = new URLSearchParams(location.search); const resetToken = params.get("reset"); const [mode,setMode] = useState(resetToken ? "reset" : "login"); const [email,setEmail]=useState(""); const [pw,setPw]=useState(""); const [msg,setMsg]=useState(""); const [busy,setBusy]=useState(false); const run = async () => { setBusy(true); setMsg(""); try { if (mode==="login") { const r = await api("/auth/login",{method:"POST",body:{email,password:pw}}); store.set("token", r.token); onDone(); } else if (mode==="forgot") { await api("/auth/forgot",{method:"POST",body:{email}}); setMsg("If that email has an account, a reset link is on its way. Check your inbox (and spam)."); } else { const r = await api("/auth/reset",{method:"POST",body:{token:resetToken,password:pw}}); store.set("token", r.token); history.replaceState(null,"","/parent/"); onDone(); } } catch(e){ setMsg(e.message); } finally { setBusy(false); } }; return (
Librancy AI
The Future is Built by Curious Minds

{mode==="login"?"Parent dashboard":mode==="forgot"?"Reset your password":"Choose a new password"}

{mode==="login"?"See your child’s progress, celebrate their wins, and grow your Beacons." :mode==="forgot"?"Enter your account email and we’ll send a reset link." :"Enter a new password for your account."}

{mode!=="reset" && setEmail(e.target.value)} type="email" placeholder="Parent email" style={{...inputS, marginBottom:12}} />} {mode!=="forgot" && setPw(e.target.value)} type="password" placeholder={mode==="reset"?"New password (8+ characters)":"Password"} onKeyDown={e=>e.key==="Enter"&&run()} style={{...inputS, marginBottom:12}} />} {msg &&
{msg}
} {mode==="login" && } {mode==="forgot" && }
); } /* ── Child card ── */ function ChildCard({ c, i, onCelebrate, onOpen, refLink }) { const [sent,setSent]=useState(false); const [msg,setMsg]=useState(""); const R = RANKS[c.rank] || RANKS.Explorer; const shareAchievement = async (child) => { // Use the parent's REAL affiliate link (loaded by Dash). Falls back to the // plain site if it hasn't loaded yet — never shares a broken empty ?ref=. const url = refLink || "https://librancy.com"; const text = `${child.firstName} just scored an A on Librancy AI! 🚀 My child is learning to build with AI — yours can too. Join with my link: ${url}`; try { if (navigator.share) { await navigator.share({ title: "Librancy AI", text, url }); return; } } catch (e) { /* fell through to WhatsApp */ } window.open("https://wa.me/?text=" + encodeURIComponent(text), "_blank"); }; const celebrate = async () => { await onCelebrate(c.id, msg || undefined); setSent(true); setMsg(""); setTimeout(()=>setSent(false), 3500); }; const earned = c.courses.filter(co=>co.percent>=100).length; return ( {/* identity band */}
{c.avatar || AVATARS[i % AVATARS.length]}
{c.firstName}
{R.icon} {c.rank} ⚡ {c.sparks.toLocaleString()} 🔥 {c.streak}-day
{/* stat tiles */}
{[["✅", c.lessonsThisWeek, "lessons this week", T.tealTint, T.tealDeep], ["🅰️", c.aGrades, "A-grades", T.yellowTint, T.gold], ["🎨", c.creations, "creations", T.pinkTint, "#C2185B"]].map(([ic,v,k,bg,cc])=>(
{ic}
{v}
{k}
))}
{c.latestCreation && (
LATEST CREATION · {c.latestCreation.lesson.toUpperCase()}
“{c.latestCreation.text}”
)} {/* course → certificate progress */}
{c.courses.map(co=>{ const full = co.percent>=100; return (
{co.title} {full ? "🏆 Certificate earned" : co.percent+"% to certificate"}
{!full && (
)}
); })}
{earned>0 && (
Librancy certificate
{c.firstName} has earned {earned} certificate{earned>1?"s":""} 🎉
A frame-worthy award — share the pride from {c.firstName}’s A-card in the learning space.
)} {/* Parent shares the child's achievement — carries the parent's referral link. */} {c.aGrades > 0 && ( )} {/* celebrate */}
setMsg(e.target.value)} maxLength={140} placeholder={`Say something to ${c.firstName}…`} style={{...inputS, padding:"12px 13px", fontSize:13.5, background:T.cloud, border:`1.5px solid ${T.line}`}} />
They’ll see it next time they open Librancy.
); } /* ── Leaderboards ── */ function Boards() { const [scope,setScope]=useState("family"); const [period,setPeriod]=useState("month"); const [data,setData]=useState(null); useEffect(()=>{ setData(null); api(`/leaderboard?scope=${scope}&period=${period}`).then(setData).catch(()=>setData({rows:[]})); },[scope,period]); return (
🏆 Leaderboards
{[["family","Families"],["region","Regions"]].map(([k,l])=>( ))} {[["month","Monthly race"],["all","All-time"]].map(([k,l])=>( ))}
{!data ?
Loading…
: data.rows.length===0 ?
No activity yet this period — be the first family on the board!
: data.rows.map((r,i)=>(
{r.rank===1?"🥇":r.rank===2?"🥈":r.rank===3?"🥉":(r.rank??"—")} {r.name}{r.me?" · you":""} {Number(r.pts).toLocaleString()} ⚡
))}
); } /* ── Read-only deep view of one child + Reset PIN (parent-facing) ── */ function ChildDetail({ childId, onBack }) { const [d,setD] = useState(null); const [err,setErr] = useState(""); const [pinOpen,setPinOpen] = useState(false); const [pin,setPin] = useState(""); const [pin2,setPin2] = useState(""); const [pinMsg,setPinMsg] = useState(""); const [busy,setBusy] = useState(false); const [copied,setCopied] = useState(false); const [loginCode,setLoginCode] = useState(null); useEffect(()=>{ api(`/parent/child/${childId}/code`).then(r=>setLoginCode(r.loginCode)).catch(()=>{}); },[childId]); const load = useCallback(()=>api(`/parent/child/${childId}`).then(setD).catch(e=>setErr(e.message)),[childId]); useEffect(()=>{ load(); },[load]); const savePin = async () => { setPinMsg(""); if (!/^\d{4}$/.test(pin)) { setPinMsg("PIN must be exactly 4 digits."); return; } if (pin !== pin2) { setPinMsg("The two PINs don't match."); return; } setBusy(true); try { await api(`/parent/child/${childId}/pin`, { method:"POST", body:{ pin } }); setPinMsg("✓ New PIN saved. Your child can sign in with it now."); setPin(""); setPin2(""); setTimeout(()=>{ setPinOpen(false); setPinMsg(""); load(); }, 1400); } catch(e){ setPinMsg(e.message); } finally { setBusy(false); } }; if (err) return
{err}
; if (!d) return
Loading…
; const c = d.child; const R = RANKS[c.rank] || RANKS.Explorer; return (
{/* header */}
{c.avatar || "🌟"}
{c.firstName}
{R.icon} {c.rank} ⚡ {c.sparks.toLocaleString()} 🔥 {c.streak}-day
{/* Reset PIN card */}
🔐 Login PIN
{c.hasPin ? "A PIN is set. Forgot it? Set a new one — the old one can't be shown for safety." : "No PIN set yet. Create one so only your child opens their space."}
{!pinOpen && }
{pinOpen && (
setPin(e.target.value.replace(/\D/g,"").slice(0,4))} placeholder="New 4-digit PIN" inputMode="numeric" type="password" style={{ ...inputS, marginBottom:8, letterSpacing:6, textAlign:"center" }} /> setPin2(e.target.value.replace(/\D/g,"").slice(0,4))} placeholder="Confirm PIN" inputMode="numeric" type="password" style={{ ...inputS, letterSpacing:6, textAlign:"center" }} /> {pinMsg &&
{pinMsg}
}
)}
{/* Login code — child signs in from any device with code + PIN */}
🎟️ Login code
{c.firstName} can sign in at librancy.com/learn from any device with this code and their PIN — no email needed. Perfect if they’re on another device or in another city.
{loginCode ? ( <>
{loginCode}
🔒 Send the code and PIN separately for safety. The code alone can’t sign in.
) : (
No code yet — run the code migration once and it appears here.
)}
{/* Course-by-course lesson breakdown */} {d.courses.map((co,ci)=>(
{co.title}
{co.done}/{co.total} · {co.percent}%
{co.lessons.map((l,li)=>(
{l.completed ? "✅" : l.videoDone ? "▶️" : "⬜"} {l.title} {l.grade && {l.grade}}
))} ))} {/* All creations, full text */}
🎨 All creations ({d.creations.length})
{d.creations.length===0 ?
No creations yet — they'll appear here as {c.firstName} builds.
: d.creations.map((cr,i)=>(
{(cr.lesson||"").toUpperCase()}{cr.source==="ai_build"?" · ✨ AI BUILD":""}
"{cr.text}"
))}
); } /* ── Dashboard ── */ /* ── Partner earnings: parent's affiliate link + balance, in-dashboard ── */ function PartnerEarnings() { const [a,setA] = useState(null); const [err,setErr] = useState(""); const [detail,setDetail] = useState(""); const [msg,setMsg] = useState(""); const [busy,setBusy] = useState(false); const load = useCallback(()=>api("/parent/affiliate").then(setA).catch(e=>setErr(e.message)),[]); useEffect(()=>{ load(); },[load]); if (err) return null; // fail quietly — never break the dashboard if (!a) return null; // still loading const s = a.stats, cfg = a.config, link = a.affiliate.link; const canWithdraw = s.payableMinor >= cfg.minPayoutMinor && a.affiliate.payoutSet; const copy = (e)=>{ navigator.clipboard.writeText(link); const b=e.target, t=b.textContent; b.textContent="Copied!"; setTimeout(()=>b.textContent=t,1200); }; const shareWA = ()=>{ const m="I found Librancy — it teaches kids to build with AI. Join with my link: "+link; window.open("https://wa.me/?text="+encodeURIComponent(m),"_blank"); }; const savePayout = async ()=>{ setMsg(""); if(detail.trim().length<6){ setMsg("Enter your bank name + account number"); return; } setBusy(true); try{ await api("/affiliate/payout-detail",{method:"POST",body:{detail}}); setMsg("✓ Payout details saved"); setDetail(""); load(); } catch(e){ setMsg(e.message); } finally{ setBusy(false); } }; const withdraw = async ()=>{ setMsg(""); setBusy(true); try{ const r=await api("/affiliate/request-payout",{method:"POST"}); setMsg("✓ Payout requested: "+NGN(r.amountMinor)); load(); } catch(e){ setMsg(e.message); } finally{ setBusy(false); } }; return (
💚 Earn cash as a partner
{cfg.ratePct}% per sale
Share Librancy with other parents and teachers. Earn {cfg.ratePct}% of every course they buy — paid out {cfg.matureDays} days after each sale.
{NGN(s.payableMinor)}
READY TO WITHDRAW
{NGN(s.pendingMinor)}
MATURING
{s.conversions}
SALES
{!a.affiliate.payoutSet && (
setDetail(e.target.value)} placeholder="Bank name + account number (to get paid)" style={{ width:"100%", boxSizing:"border-box", padding:"11px 12px", borderRadius:10, border:"none", fontSize:13, fontFamily:FONT }} />
)} {a.affiliate.payoutSet && ( )} {msg &&
{msg}
}
); } function Dash({ onLogout }) { const [d,setD]=useState(null); const [err,setErr]=useState(""); const [showAccount,setShowAccount]=useState(false); const [openChildId,setOpenChildId]=useState(null); const [refLink,setRefLink]=useState(""); // parent's real affiliate link (single source of truth) const load = useCallback(()=>api("/parent/overview").then(setD).catch(e=>setErr(e.message)),[]); useEffect(()=>{ load(); },[load]); useEffect(()=>{ api("/parent/affiliate").then(a=>setRefLink(a.affiliate.link)).catch(()=>{}); },[]); if (err) return
{err}
; if (!d) return
Loading your family…
; const celebrate = (childId, message)=>api("/parent/celebrate",{method:"POST",body:{childId,message}}); const family = (d.parent.name||"").split(" ")[0]; if (openChildId) return { setOpenChildId(null); load(); }} />; return (
{/* Brand header */}
Librancy AI 🔥 {d.beacons.balance} Beacons
PARENT DASHBOARD
The {family} family
{d.children.map((c,i)=>setOpenChildId(c.id)} refLink={refLink} />)}
{/* Beacons economy — gradient card */}
Your Beacons
{d.beacons.balance} = {NGN(d.beacons.creditMinor)} credit
Earned from {d.beacons.referrals} famil{d.beacons.referrals===1?"y":"ies"} who joined through your child’s shared wins ({d.beacons.shares} shares). Credit auto-applies up to {d.beacons.capPct}% at your next checkout — perfect for the next course or a sibling.
{/* Region */}
📍 Your region (for the leaderboard)
{/* Account */} {showAccount && }
); } function AccountPanel({ email, onLogout }) { const [cur,setCur]=useState(""); const [nw,setNw]=useState(""); const [msg,setMsg]=useState(""); const change = async ()=>{ setMsg(""); try { await api("/auth/change-password",{method:"POST",body:{current:cur,new:nw}}); setMsg("Password changed ✓"); setCur(""); setNw(""); } catch(e){ setMsg(e.message); } }; return (
Signed in as {email}
setCur(e.target.value)} type="password" placeholder="Current password" style={{...inputS, marginBottom:8, padding:"11px 13px", fontSize:13.5}} /> setNw(e.target.value)} type="password" placeholder="New password (8+)" style={{...inputS, marginBottom:8, padding:"11px 13px", fontSize:13.5}} /> {msg &&
{msg}
}
); } /* ── App ── */ function App(){ const [authed,setAuthed]=useState(!!store.get("token")); return (
{authed ? {store.del("token");setAuthed(false);}} /> : setAuthed(true)} />}
); } ReactDOM.createRoot(document.getElementById("root")).render();