/* ============================================================ ZENITH — Authentication surfaces · LoginMain : students & teachers (segmented toggle), root path · LoginAdmin : separate admin console, reached via #admin Pure front-end POC — any credentials sign you in. ============================================================ */ function BrandPanel({ lang }) { const t = (en, si) => tx(lang, en, si); const ticks = [ t("Sit full A/L papers in a real exam-hall environment", "සැබෑ විභාග ශාලා පරිසරයක පත්‍ර ලියන්න"), t("Get a predicted Z-Score with island & district rank", "දිවයින් හා දිස්ත්‍රික් ශ්‍රේණිය සමඟ පුරෝකථන Z අගයක්"), t("Teacher-marked papers, released every Saturday", "ගුරු ලකුණු කළ පත්‍ර, සෑම සෙනසුරාදාම නිකුත් වේ"), ]; return (

{t("A/L mock-exam & Z-Score platform", "උ.පෙ මොක් විභාග හා Z අගය වේදිකාව")}

{t("Sit. Get marked.", "ලියන්න. ලකුණු ලබන්න.")}
{t("Know your Z-Score.", "ඔබේ Z අගය දැනගන්න.")}

{ticks.map((tk, i) => (
{tk}
))}
Open Ai Pvt Ltd · Colombo
); } function LangToggle({ lang, setLang }) { return (
); } /* "or" divider */ function OrDivider({ label }) { return (
{label}
); } /* Google sign-in button (official multi-colour mark) */ function GoogleButton({ label, onClick }) { return ( ); } function AuthField({ label, type = "text", value, onChange, placeholder, icon }) { return ( ); } /* ---------- Student & Teacher login ---------- */ function LoginMain({ lang, setLang, onLogin, onAdmin, onSignup }) { const t = (en, si) => tx(lang, en, si); const [role, setRole] = useState("student"); const [id, setId] = useState("tharindu@email.com"); const [pw, setPw] = useState("demo1234"); const switchRole = r => { setRole(r); setId(r === "student" ? "tharindu@email.com" : "r.wijesinghe@openai.lk"); }; return (

{t("Welcome back", "නැවත සාදරයෙන් පිළිගනිමු")}

{t("Sign in to continue your A/L preparation.", "ඔබේ උ.පෙ සූදානම දිගටම කරගෙන යාමට පිවිසෙන්න.")}

{/* role toggle */}
{ e.preventDefault(); onLogin(role, id); }} style={{ display: "flex", flexDirection: "column", gap: 16, marginTop: 22 }}> setId(e.target.value)} placeholder={role === "student" ? "you@email.com" : "you@institute.lk"} /> setPw(e.target.value)} placeholder="••••••••" /> {t("Sign in", "පිවිසෙන්න")} onLogin(role, id)} />

{t("Demo — any credentials work.", "ආදර්ශනය — ඕනෑම අක්තපත්‍රයක් ක්‍රියා කරයි.")}

{t("New student?", "නව සිසුවෙක්ද?")}
); } /* ---------- Admin console login (separate path) ---------- */ function LoginAdmin({ lang, setLang, onLogin, onBack }) { const t = (en, si) => tx(lang, en, si); const [email, setEmail] = useState("admin@openai.lk"); const [pw, setPw] = useState("demo1234"); const [code, setCode] = useState(""); return (

{t("Administrator Console", "පරිපාලක කොන්සෝලය")}

{t("Restricted sign-in", "සීමිත පිවිසුම")}

{t("Open Ai Pvt Ltd · institute administrators only", "Open Ai Pvt Ltd · ආයතන පරිපාලකයින්ට පමණි")}

{ e.preventDefault(); onLogin(); }} style={{ display: "flex", flexDirection: "column", gap: 15 }}> setEmail(e.target.value)} /> setPw(e.target.value)} /> setCode(e.target.value)} placeholder="123 456" />
{t("Restricted access. All activity is logged.", "සීමිත ප්‍රවේශය. සියලු ක්‍රියාකාරකම් සටහන් වේ.")}
); } function AdminField({ label, type = "text", value, onChange, placeholder, icon }) { return ( ); } /* ---------- Student signup ---------- Catalog-driven: streams/subjects/provinces/schools come from GET /api/catalog (admin-enabled entries only). Subjects unlock after a stream is picked and exactly 3 must be chosen. Creates a REAL student via POST /api/signup. */ function SignupStudent({ lang, setLang, onCreate, onBack, onboarding }) { const t = (en, si) => tx(lang, en, si); const [cat, setCat] = useState(null); // catalog payload const [catErr, setCatErr] = useState(false); const [busy, setBusy] = useState(false); const [err, setErr] = useState(null); // bilingual submit error text const [f, setF] = useState({ name: "", email: "", school: "", schoolOther: "", province: "western", stream: "", subjects: [], medium: "English", pw: "", confirm: "", terms: true, }); const set = (k, v) => setF(prev => ({ ...prev, [k]: v })); const loadCatalog = () => { setCatErr(false); window.ZAPI.getCatalog() .then(c => setCat(c)) .catch(e => { console.error("[islandrank] catalog load failed", e); setCatErr(true); }); }; useEffect(loadCatalog, []); const streamObj = cat && cat.streams.find(s => s.code === f.stream); const toggleSubject = code => { setErr(null); setF(prev => { const has = prev.subjects.includes(code); if (!has && prev.subjects.length >= 3) return prev; // hard cap at 3 return { ...prev, subjects: has ? prev.subjects.filter(c => c !== code) : [...prev.subjects, code] }; }); }; const DETAIL_MSG = { email_exists: ["This email is already registered.", "මෙම විද්‍යුත් තැපෑල දැනටමත් ලියාපදිංචියි."], valid_email_required: ["Enter a valid email address.", "වලංගු විද්‍යුත් තැපෑලක් ඇතුළත් කරන්න."], full_name_required: ["Enter your full name.", "ඔබේ සම්පූර්ණ නම ඇතුළත් කරන්න."], school_required: ["Enter your school name.", "ඔබේ පාසලේ නම ඇතුළත් කරන්න."], province_unknown: ["Select your province.", "ඔබේ පළාත තෝරන්න."], stream_disabled_or_unknown: ["This stream is not available.", "මෙම විෂය ධාරාව ලබාගත නොහැක."], exactly_3_subjects_required: ["Pick exactly 3 subjects.", "විෂයයන් 3ක්ම තෝරන්න."], subjects_not_available_in_stream: ["One of the subjects is not available in this stream.", "තෝරාගත් විෂයයක් මෙම ධාරාවේ නොමැත."], }; const submit = async e => { e.preventDefault(); setErr(null); if (f.subjects.length !== 3) { setErr(t("Pick exactly 3 subjects.", "විෂයයන් 3ක්ම තෝරන්න.")); return; } if (!onboarding) { if (f.pw !== f.confirm) { setErr(t("Passwords do not match.", "මුරපද නොගැලපේ.")); return; } if (!f.terms) { setErr(t("Please agree to the terms to continue.", "ඉදිරියට යාමට නියමයන්ට එකඟ වන්න.")); return; } } setBusy(true); try { const profile = { school: f.school, schoolOther: f.schoolOther || null, province: f.province, stream: f.stream, subjects: f.subjects, medium: f.medium }; if (onboarding) { await window.ZAPI.completeProfile(profile); // account already exists (via Google) onCreate(); } else { const res = await window.ZAPI.signup({ fullName: f.name, email: f.email, password: f.pw, ...profile }); onCreate(res.systemId); } } catch (ex) { const m = DETAIL_MSG[ex.detail]; setErr(m ? t(m[0], m[1]) : t("Registration failed — please try again.", "ලියාපදිංචිය අසාර්ථකයි — නැවත උත්සාහ කරන්න.")); setBusy(false); } }; const canSubmit = cat && f.school && (f.school !== "Other" || f.schoolOther.trim()) && f.stream && f.subjects.length === 3 && !busy && (onboarding || (f.name.trim() && f.email.trim() && f.pw && f.pw === f.confirm && f.terms)); return (

{onboarding ? t("Complete your profile", "ඔබේ පැතිකඩ සම්පූර්ණ කරන්න") : t("Create your account", "ඔබේ ගිණුම සාදන්න")}

{onboarding ? t("Pick your stream and subjects to see the right papers.", "නිවැරදි පත්‍ර දැකීමට ඔබේ විෂය ධාරාව හා විෂයයන් තෝරන්න.") : t("Register as an A/L student to start sitting papers.", "පත්‍ර ලිවීම ඇරඹීමට උ.පෙ සිසුවෙකු ලෙස ලියාපදිංචි වන්න.")}

{!cat && !catErr && (

{t("Loading registration options…", "ලියාපදිංචි විකල්ප පූරණය වෙමින්…")}

)} {catErr && (
{t("Could not load registration options — is the server up?", "ලියාපදිංචි විකල්ප පූරණය අසාර්ථකයි — සේවාදායකය ක්‍රියාත්මකද?")} {" "}
)} {cat && (
{!onboarding && set("name", e.target.value)} placeholder={t("e.g. Tharindu Perera", "උදා. තරිඳු පෙරේරා")} />} {!onboarding && set("email", e.target.value)} placeholder="you@email.com" />} set("school", e.target.value)} placeholder={t("Search your school…", "ඔබේ පාසල සොයන්න…")} options={[...cat.schools.map(s => ({ v: s, label: s })), { v: "Other", label: t("Other (type below)", "වෙනත් (පහත ලියන්න)") }]} /> {f.school === "Other" && ( set("schoolOther", e.target.value)} placeholder={t("Type your school name", "ඔබේ පාසලේ නම ලියන්න")} /> )}
set("province", e.target.value)} options={cat.provinces.map(p => ({ v: p.code, label: tx(lang, p.name, p.nameSi || p.name) }))} /> set("medium", e.target.value)} options={cat.mediums.map(m => ({ v: m, label: m }))} />
{ setErr(null); setF(prev => ({ ...prev, stream: e.target.value, subjects: [] })); }} placeholder={t("Select your stream…", "ඔබේ විෂය ධාරාව තෝරන්න…")} options={cat.streams.map(s => ({ v: s.code, label: tx(lang, s.name, s.nameSi || s.name) }))} /> {/* subjects unlock only after a stream is picked; exactly 3 required */}
{t("Your 3 subjects", "ඔබේ විෂයයන් 3")} {streamObj && {f.subjects.length} / 3} {!streamObj ? (
{t("Select a stream first to choose subjects.", "විෂයයන් තේරීමට පළමුව විෂය ධාරාවක් තෝරන්න.")}
) : (
{streamObj.subjects.map(sub => { const on = f.subjects.includes(sub.code); const full = !on && f.subjects.length >= 3; return ( ); })}
)}
{!onboarding && (
set("pw", e.target.value)} placeholder="••••••••" /> set("confirm", e.target.value)} placeholder="••••••••" />
)} {/* the student number is issued instantly on registration */} {!onboarding && (
{t("Your student number (SX-…) is issued as soon as you register.", "ලියාපදිංචි වූ විගස ඔබේ සිසු අංකය (SX-…) නිකුත් වේ.")}
)} {err && (
{err}
)} {!onboarding && ( )} {onboarding ? (busy ? t("Saving…", "සුරකිමින්…") : t("Continue", "ඉදිරියට")) : (busy ? t("Creating account…", "ගිණුම සාදමින්…") : t("Create account", "ගිණුම සාදන්න"))} )} {!onboarding && (
{t("Already have an account?", "දැනටමත් ගිණුමක් තිබේද?")}
)}
); } /* select with distinct value/label pairs (streams/provinces carry codes) */ function AuthSelectKV({ label, value, onChange, options, icon, placeholder }) { return ( ); } /* searchable combobox (long option lists, e.g. schools) — type to filter, click to pick. Same {v,label} option shape + onChange({target:{value}}) contract as AuthSelectKV. */ function SearchableSelect({ label, value, onChange, options, icon, placeholder, lang }) { const t = (en, si) => tx(lang, en, si); // lang via prop — login screens render outside AppCtx const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const wrapRef = useRef(null); useEffect(() => { const onDoc = e => { if (wrapRef.current && !wrapRef.current.contains(e.target)) { setOpen(false); setQuery(""); } }; document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, []); const selected = options.find(o => o.v === value); const q = query.trim().toLowerCase(); const filtered = q ? options.filter(o => o.label.toLowerCase().includes(q)) : options; const pick = o => { onChange({ target: { value: o.v } }); setOpen(false); setQuery(""); }; return (
{label}
{ setQuery(e.target.value); setOpen(true); }} onFocus={() => { setOpen(true); setQuery(""); }} placeholder={selected ? selected.label : placeholder} style={{ width: "100%", border: "1.5px solid var(--border)", borderRadius: "var(--r-sm)", padding: "13px 40px 13px 42px", fontSize: 15, outline: "none", fontFamily: "var(--font)", background: "#fff" }} /> {open && (
{filtered.length === 0 ? (
{t("No matching school", "ගැලපෙන පාසලක් නැත")}
) : filtered.map(o => (
pick(o)} style={{ padding: "10px 14px", fontSize: 14.5, cursor: "pointer", background: o.v === value ? "var(--blue-soft)" : "#fff", color: o.v === value ? "var(--blue-dark)" : "var(--fg)", fontWeight: o.v === value ? 600 : 400 }} onMouseEnter={e => { if (o.v !== value) e.currentTarget.style.background = "var(--grey-5)"; }} onMouseLeave={e => { if (o.v !== value) e.currentTarget.style.background = "#fff"; }}> {o.label}
))}
)}
); } /* select styled like AuthField */ function AuthSelect({ label, value, onChange, options, icon }) { return ( ); } /* Google Identity Services button — loads the GIS script, renders Google's official button, and hands the returned ID-token credential up to the parent. */ function GoogleSignIn({ clientId, onCredential }) { const ref = useRef(null); useEffect(() => { let cancelled = false; const render = () => { if (cancelled || !window.google || !window.google.accounts || !ref.current) return; window.google.accounts.id.initialize({ client_id: clientId, callback: r => onCredential(r.credential) }); ref.current.innerHTML = ""; window.google.accounts.id.renderButton(ref.current, { theme: "outline", size: "large", text: "continue_with", shape: "pill", width: 300 }); }; if (window.google && window.google.accounts) { render(); return () => { cancelled = true; }; } let s = document.getElementById("gis-script"); if (!s) { s = document.createElement("script"); s.src = "https://accounts.google.com/gsi/client"; s.async = true; s.defer = true; s.id = "gis-script"; document.head.appendChild(s); } s.addEventListener("load", render); return () => { cancelled = true; s && s.removeEventListener("load", render); }; }, [clientId]); return
; } /* Real (Google-only) login surface — used when the backend auth mode is "google". */ function LoginGoogle({ lang, setLang, clientId, onCredential, error, busy }) { const t = (en, si) => tx(lang, en, si); return (

{t("Welcome", "සාදරයෙන් පිළිගනිමු")}

{t("Sign in with your Google account to continue.", "ඉදිරියට යාමට ඔබේ Google ගිණුමෙන් පිවිසෙන්න.")}

{busy ?

{t("Signing in…", "පිවිසෙමින්…")}

: } {error && (
{error}
)} {/* first-time students are onboarded (stream + subjects) right after this button */}
{t("New student? Just continue with Google — you'll choose your stream and 3 subjects right after. Already registered? You'll go straight in.", "නව සිසුවෙක්ද? Google සමඟ ඉදිරියට යන්න — ඉන්පසු ඔබේ විෂය ධාරාව හා විෂයයන් 3 තෝරන්න. දැනටමත් ලියාපදිංචිද? ඔබ කෙලින්ම ඇතුළු වේ.")}

{t("Teacher & admin access is granted by the institute.", "ගුරු හා පරිපාලක ප්‍රවේශය ආයතනය විසින් ලබා දේ.")}

); } Object.assign(window, { LoginMain, LoginAdmin, SignupStudent, AuthSelectKV, SearchableSelect, GoogleSignIn, LoginGoogle });