| Server IP : 69.72.244.102 / Your IP : 216.73.216.164 Web Server : LiteSpeed System : Linux s3434.fra1.stableserver.net 4.18.0-513.24.1.lve.2.el8.x86_64 #1 SMP Fri May 24 12:42:50 UTC 2024 x86_64 User : konzalta ( 1271) PHP Version : 7.4.33 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : OFF | Pkexec : OFF Directory : /home/konzalta/exams.konzaltant.com/ |
Upload File : |
import { useState, useEffect, useMemo } from "react";
import { AreaChart, Area, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine } from "recharts";
// ---------- palette: morning light ----------
const C = {
paper: "#FDF7ED", paperDeep: "#F8EFDF", card: "#FFFFFF",
rule: "#EEE3D0", ink: "#33393E",
teal: "#2E8B7A", tealSoft: "#DDF0EB",
apricot: "#E0805C", apricotSoft: "#FBE9E0",
board: "#1C5E54", boardDeep: "#12443D", chalk: "#F3EFE4",
leaf: "#74A65C", leafSoft: "#E9F2E1",
gold: "#D9A441", goldSoft: "#F9EED8",
};
const GRAIN =
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.035 0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E\")";
const GRADE_POINTS = { A: 12, "A-": 11, "B+": 10, B: 9, "B-": 8, "C+": 7, C: 6, "C-": 5, "D+": 4, D: 3, "D-": 2, E: 1 };
const LADDER = ["E","D-","D","D+","C-","C","C+","B-","B","B+","A-","A"];
const pointsToGrade = (p) => LADDER[Math.min(11, Math.max(0, Math.round(p) - 1))];
const masteryToPoints = (avg) => Math.round((1 + (avg / 100) * 11) * 10) / 10;
const MIN_EVIDENCE = 6; // attempts before a sub-skill score is shown
// ---------- curriculum: subject -> topics -> sub-skills (tagged at the finest level) ----------
const SUBJECT_DEFS = [
{ name: "Mathematics", d1: 2, topics: [
{ el: "Algebra", base: 82 },
{ el: "Geometry", base: 54, ex: 12, subs: [
{ name: "Circle theorems", base: 38, att: 9, ex: 6 },
{ name: "Angles & polygons", base: 61, att: 11, ex: 4 },
{ name: "Constructions & loci", base: 70, att: 4, ex: 2 },
]},
{ el: "Trigonometry", base: 61, ex: 9 },
{ el: "Probability & Statistics", base: 74 },
{ el: "Calculus", base: 47, ex: 15, subs: [
{ name: "Differentiation", base: 52, att: 8, ex: 7 },
{ name: "Integration", base: 41, att: 3, ex: 5 },
{ name: "Rates of change", base: 49, att: 7, ex: 3 },
]},
{ el: "Commercial Arithmetic", base: 88 },
]},
{ name: "English", d1: 1, topics: [
{ el: "Comprehension", base: 84 }, { el: "Grammar", base: 79 },
{ el: "Composition", base: 71 }, { el: "Set Books", base: 66, ex: 8 },
]},
{ name: "Kiswahili", d1: 1, topics: [
{ el: "Ufahamu", base: 80 }, { el: "Sarufi", base: 72 },
{ el: "Insha", base: 63, ex: 10 }, { el: "Fasihi", base: 75 },
]},
{ name: "Biology", d1: 1, topics: [
{ el: "Cell Biology", base: 77 },
{ el: "Genetics", base: 52, ex: 14, subs: [
{ name: "Monohybrid crosses", base: 58, att: 9, ex: 6 },
{ name: "Pedigree analysis", base: 44, att: 5, ex: 5 },
{ name: "Variation & mutation", base: 60, att: 7, ex: 3 },
]},
{ el: "Ecology", base: 70 }, { el: "Physiology", base: 58, ex: 11 },
]},
{ name: "Chemistry", d1: 1, topics: [
{ el: "The Mole", base: 58, ex: 13 },
{ el: "Organic Chemistry", base: 41, ex: 18, subs: [
{ name: "Alkanes & alkenes", base: 48, att: 10, ex: 8 },
{ name: "Alcohols & acids", base: 36, att: 7, ex: 7 },
{ name: "Polymers", base: 55, att: 3, ex: 3 },
]},
{ el: "Acids, Bases & Salts", base: 69 }, { el: "Electrochemistry", base: 55, ex: 9 },
]},
{ name: "Physics", d1: 1, topics: [
{ el: "Mechanics", base: 76 }, { el: "Electricity", base: 64, ex: 7 },
{ el: "Waves & Optics", base: 71 }, { el: "Modern Physics", base: 60, ex: 8 },
]},
{ name: "History & Govt", d1: 1, topics: [
{ el: "Pre-Colonial Kenya", base: 90 }, { el: "Colonial Period", base: 85 },
{ el: "Government & Constitution", base: 78 }, { el: "World History", base: 81 },
]},
{ name: "Business Studies", d1: 1, topics: [
{ el: "Trade & Commerce", base: 83 }, { el: "Bookkeeping", base: 68, ex: 6 },
{ el: "Money & Banking", base: 80 }, { el: "Entrepreneurship", base: 86 },
]},
];
// ---------- personas: same product, three students ----------
const PERSONAS = [
{ id: "baraka", name: "Baraka O.", letter: "B", shift: -27, monthMove: "+0.8",
band: "climbing", subtitle: "Every question you get right today is a mark you didn't have yesterday.",
recTitle: "Start here", recNote: "Small wins first — your three most fixable areas.",
practiseNote: "Foundation mode: questions matched to your level, so wins come often." },
{ id: "wanjiru", name: "Wanjiru M.", letter: "W", shift: 0, monthMove: "+0.4",
band: "pushing", subtitle: "Every hour you put in now is a mark in November.",
recTitle: "Start here", recNote: "Your three weakest elements right now.",
practiseNote: "Targeted mode: your weak elements, weighted in." },
{ id: "achieng", name: "Achieng A.", letter: "A", shift: 16, monthMove: "+0.2",
band: "polishing", subtitle: "At your level the marks are in precision — polish beats effort.",
recTitle: "Sharpen these", recNote: "Where careless half-marks hide. Hardest tier only.",
practiseNote: "Polish mode: hardest-tier questions, timed. No easy marks here." },
];
const clamp = (v) => Math.max(8, Math.min(97, Math.round(v)));
const shiftM = (base, shift) => clamp(base + shift + (base % 3) - 1); // deterministic per-element variation
function buildStudent(p) {
const subjects = SUBJECT_DEFS.map((s) => {
const topics = s.topics.map((t) => ({
...t,
m: shiftM(t.base, p.shift),
subs: t.subs?.map((x) => ({ ...x, m: shiftM(x.base, p.shift) })),
}));
const avg = topics.reduce((a, t) => a + t.m, 0) / topics.length;
const pts = masteryToPoints(avg);
return { name: s.name, topics, ready: Math.round(avg), pts, grade: pointsToGrade(pts), t1: Math.max(1, pts - s.d1) };
});
const best7 = subjects.map((s) => s.pts).sort((a, b) => b - a).slice(0, 7);
const meanPts = Math.round((best7.reduce((a, b) => a + b, 0) / 7) * 10) / 10;
const meanGrade = pointsToGrade(meanPts);
const targetPts = Math.min(12, Math.round(meanPts) + 1); // adaptive: current + one rung
return { ...p, subjects, meanPts, meanGrade, target: pointsToGrade(targetPts), targetPts };
}
const TRENDS = {
baraka: [ { wk: "Term 1", pts: 2.1 }, { wk: "Mid-Yr", pts: 2.6 }, { wk: "Pre-Mock", pts: 3.1 }, { wk: "Mock 1", pts: 3.8 }, { wk: "Now", pts: 4.4 } ],
wanjiru: [ { wk: "Term 1", pts: 7.4 }, { wk: "Mid-Yr", pts: 7.7 }, { wk: "Pre-Mock", pts: 8.1 }, { wk: "Mock 1", pts: 8.6 }, { wk: "Now", pts: 9.0 } ],
achieng: [ { wk: "Term 1", pts: 10.2 }, { wk: "Mid-Yr", pts: 10.4 }, { wk: "Pre-Mock", pts: 10.6 }, { wk: "Mock 1", pts: 10.9 }, { wk: "Now", pts: 11.1 } ],
};
const STUDY_WEEKS = [
{ wk: "W1", h: 14 }, { wk: "W2", h: 18 }, { wk: "W3", h: 16 }, { wk: "W4", h: 21 },
{ wk: "W5", h: 19 }, { wk: "W6", h: 23 }, { wk: "W7", h: 20 }, { wk: "W8", h: 24 },
];
const QUESTIONS = [
{ subject: "Mathematics", els: ["Circle theorems"], q: "The gradient of the curve y = 2x³ − 3x² + 5 at x = 2 is:",
opts: ["12", "18", "24", "6"], ans: 0,
why: "dy/dx = 6x² − 6x. At x = 2: 6(4) − 6(2) = 24 − 12 = 12. Differentiate first, then substitute — don't substitute into y." },
{ subject: "Mathematics", els: ["Angles & polygons"], q: "The interior angle of a regular polygon is 150°. How many sides does it have?",
opts: ["10", "12", "15", "9"], ans: 1,
why: "Exterior angle = 180° − 150° = 30°. Sides = 360° ÷ 30° = 12. Working through the exterior angle is always the shortcut." },
{ subject: "Chemistry", els: ["Alkanes & alkenes"], q: "Which reagent distinguishes an alkene from an alkane?",
opts: ["Sodium hydroxide", "Bromine water", "Litmus paper", "Silver nitrate"], ans: 1,
why: "Bromine water decolourises in an alkene (addition across C=C) but stays orange with an alkane. A classic KCSE Paper 1 mark." },
{ subject: "Chemistry", els: ["The Mole"], q: "How many moles are in 8.8 g of CO₂? (C=12, O=16)",
opts: ["0.1", "0.2", "0.4", "0.8"], ans: 1,
why: "Molar mass of CO₂ = 12 + 32 = 44 g/mol. Moles = 8.8 ÷ 44 = 0.2. Always assemble molar mass before dividing." },
{ subject: "Biology", els: ["Monohybrid crosses"], q: "In a monohybrid cross of two heterozygous parents (Tt × Tt), the phenotype ratio is:",
opts: ["1:1", "1:2:1", "3:1", "9:3:3:1"], ans: 2,
why: "Tt × Tt gives TT, Tt, Tt, tt — three showing the dominant trait to one recessive. 1:2:1 is the *genotype* ratio; the question asked phenotype." },
];
const daysToKCSE = () => Math.max(0, Math.ceil((new Date("2026-11-02T08:00:00+03:00") - new Date()) / 86400000));
const NAV = ["Dashboard", "My Subjects", "Practise", "Mock Exams", "Progress"];
const Eyebrow = ({ children, style }) => <div className="eyebrow" style={style}>{children}</div>;
// topic rows with optional sub-skill drill-down (confidence-gated)
function TopicRows({ topics, bar, onPractise }) {
const [openSub, setOpenSub] = useState(null);
return (
<div className="space-y-3">
{topics.map((t) => {
const w = t.m < 65;
const has = !!t.subs;
const isOpen = openSub === t.el;
return (
<div key={t.el}>
<div className="flex items-center gap-3">
{has ? (
<button onClick={() => setOpenSub(isOpen ? null : t.el)} aria-expanded={isOpen}
className={`text-sm flex-1 min-w-0 truncate text-left ${w ? "weakname" : ""}`}
style={{ background: "none", border: "none", cursor: "pointer", color: C.ink, padding: 0, fontWeight: 600 }}>
{t.el} <span className="mono" style={{ fontSize: 10, color: C.teal }}>{isOpen ? "▾" : "▸"} {t.subs.length} skills</span>
</button>
) : (
<span className={`text-sm flex-1 min-w-0 truncate ${w ? "weakname" : ""}`}>{t.el}</span>
)}
<span className="flex-1 max-w-40" style={{ height: 8, background: C.rule, borderRadius: 999 }}>
<span className="block h-full" style={{ ...bar(t.m), background: w ? C.apricot : t.m >= 75 ? C.leaf : C.teal, borderRadius: 999 }} />
</span>
<span className="mono text-xs font-semibold" style={{ width: 34, textAlign: "right" }}>{t.m}%</span>
{w ? (
<button className="practise mono px-3 py-1.5 shrink-0" style={{ fontSize: 11, background: C.apricot, color: "#fff", border: "none", borderRadius: 999, cursor: "pointer" }}
onClick={onPractise}>
Practise · {t.ex}
</button>
) : <span style={{ width: 86 }} aria-hidden="true" />}
</div>
{has && isOpen && (
<div className="expand mt-2 mb-1 ml-4 pl-3 space-y-2.5" style={{ borderLeft: `2px solid ${C.rule}` }}>
{t.subs.map((s) => {
const enough = s.att >= MIN_EVIDENCE;
const sw = s.m < 65;
return (
<div key={s.name} className="flex items-center gap-3">
<span className={`flex-1 min-w-0 truncate ${enough && sw ? "weakname" : ""}`} style={{ fontSize: 13, opacity: enough ? 1 : 0.75 }}>{s.name}</span>
{enough ? (
<>
<span className="flex-1 max-w-32" style={{ height: 6, background: C.rule, borderRadius: 999 }}>
<span className="block h-full" style={{ ...bar(s.m), background: sw ? C.apricot : s.m >= 75 ? C.leaf : C.teal, borderRadius: 999 }} />
</span>
<span className="mono font-semibold" style={{ fontSize: 11, width: 34, textAlign: "right" }}>{s.m}%</span>
{sw ? (
<button className="practise mono px-2.5 py-1 shrink-0" style={{ fontSize: 10, background: C.apricot, color: "#fff", border: "none", borderRadius: 999, cursor: "pointer" }} onClick={onPractise}>
Practise · {s.ex}
</button>
) : <span style={{ width: 74 }} aria-hidden="true" />}
</>
) : (
<span className="mono px-2.5 py-1" style={{ fontSize: 10, borderRadius: 999, background: C.paperDeep, color: "rgba(51,57,62,.6)", border: `1px dashed ${C.rule}` }}>
still building your picture — {MIN_EVIDENCE - s.att} more questions
</span>
)}
</div>
);
})}
</div>
)}
</div>
);
})}
</div>
);
}
function Dashboard({ st, bar, plan, setPlan, recs, go }) {
const [open, setOpen] = useState(0);
const doneCount = plan.filter((p) => p.done).length;
return (
<>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mt-6">
<div className="card p-4">
<Eyebrow>Predicted mean grade</Eyebrow>
<div className="flex items-baseline gap-2 mt-1">
<span className="display" style={{ fontSize: "2.6rem", lineHeight: 1, color: C.teal, textShadow: `2px 2px 0 ${C.tealSoft}` }}>{st.meanGrade}</span>
<span className="mono text-sm" style={{ opacity: 0.6 }}>{st.meanPts.toFixed(1)}/12</span>
</div>
<div className="flex gap-0.5 mt-3" aria-hidden="true">
{LADDER.map((g) => (
<div key={g} className="flex-1" style={{ height: 5, borderRadius: 99, background: GRADE_POINTS[g] <= st.meanPts ? C.teal : C.rule, boxShadow: g === st.target ? `0 0 0 1.5px ${C.gold}` : "none" }} />
))}
</div>
<div className="mono mt-1.5" style={{ fontSize: 10, color: C.leaf }}>▲ {st.monthMove} pts this month</div>
</div>
<div className="card p-4 flex items-center gap-3">
<div className="mono text-center shrink-0" style={{ width: 62, height: 62, borderRadius: "50%", border: `2px solid ${C.gold}`, boxShadow: `inset 0 0 0 2px #fff, inset 0 0 0 3.5px ${C.gold}`, color: C.gold, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", transform: "rotate(-6deg)" }}>
<span style={{ fontSize: 7.5, letterSpacing: "0.15em" }}>NEXT RUNG</span>
<span className="display" style={{ fontSize: 20, lineHeight: 1 }}>{st.target}</span>
</div>
<div>
<Eyebrow>Your next rung</Eyebrow>
<div className="text-sm mt-1" style={{ opacity: 0.75 }}>{Math.max(1, Math.round(st.targetPts - st.meanPts))} point away.<br />Within reach.</div>
</div>
</div>
<div className="card p-4">
<Eyebrow>Days to Paper 1</Eyebrow>
<div className="display mt-1" style={{ fontSize: "2.6rem", lineHeight: 1 }}>{daysToKCSE()}</div>
<div className="mono mt-2" style={{ fontSize: 10, opacity: 0.55 }}>KCSE starts 2 Nov 2026</div>
</div>
<div className="card p-4">
<Eyebrow>Study streak</Eyebrow>
<div className="flex items-baseline gap-1 mt-1">
<span className="display" style={{ fontSize: "2.6rem", lineHeight: 1, color: C.leaf }}>12</span>
<span className="text-sm" style={{ opacity: 0.6 }}>days</span>
</div>
<div className="flex gap-1 mt-2">
{["M","T","W","T","F","S","S"].map((d, i) => (
<span key={i} className="mono text-center" style={{ width: 20, height: 20, lineHeight: "19px", fontSize: 9, borderRadius: "50%", background: i < 4 ? C.leafSoft : "transparent", border: `1px solid ${i < 4 ? C.leaf : C.rule}`, color: i < 4 ? C.leaf : "rgba(51,57,62,.4)" }}>{d}</span>
))}
</div>
</div>
</div>
<div className="grid lg:grid-cols-3 gap-5 mt-5 items-start">
<div className="lg:col-span-2 space-y-5 min-w-0">
<section className="card p-5">
<div className="flex items-baseline justify-between">
<h2 className="display text-base">Your subjects</h2>
<span className="eyebrow">best 7 of 8 count · tap to open</span>
</div>
<div className="mt-3">
{st.subjects.map((sub, i) => {
const weak = sub.topics.filter((t) => t.m < 65);
const isOpen = open === i;
return (
<div key={sub.name} style={{ borderBottom: i < st.subjects.length - 1 ? `1px solid ${C.rule}` : "none" }}>
<button onClick={() => setOpen(isOpen ? -1 : i)} aria-expanded={isOpen}
className="subrow w-full text-left flex items-center gap-3 py-2.5 px-2"
style={{ background: "transparent", border: "none", cursor: "pointer", color: C.ink, borderRadius: 10 }}>
<span className="display text-center shrink-0" style={{ width: 38, height: 28, lineHeight: "26px", fontSize: 13.5, borderRadius: 999, color: isOpen ? "#fff" : C.teal, background: isOpen ? C.teal : C.tealSoft }}>{sub.grade}</span>
<span className="text-sm font-semibold truncate" style={{ minWidth: 104, color: isOpen ? C.teal : C.ink }}>{sub.name}</span>
<span className="flex-1 hidden sm:block" style={{ height: 6, background: C.rule, borderRadius: 999 }}>
<span className="block h-full" style={{ width: `${sub.ready}%`, background: sub.ready >= 75 ? C.leaf : sub.ready >= 60 ? C.teal : C.apricot, borderRadius: 999 }} />
</span>
<span className="mono text-xs font-semibold" style={{ width: 34, textAlign: "right" }}>{sub.ready}%</span>
<span className="mono px-2 py-0.5 shrink-0" style={{ fontSize: 10.5, borderRadius: 999, color: weak.length ? C.apricot : C.leaf, background: weak.length ? C.apricotSoft : C.leafSoft }}>
{weak.length ? `${weak.length} to fix` : "on track"}
</span>
<span aria-hidden="true" className="mono text-xs" style={{ opacity: 0.4, transform: isOpen ? "rotate(90deg)" : "none", transition: "transform 200ms" }}>›</span>
</button>
{isOpen && (
<div className="expand pb-4 px-2">
<TopicRows topics={sub.topics} bar={bar} onPractise={() => go("Practise")} />
</div>
)}
</div>
);
})}
</div>
</section>
</div>
<div className="space-y-5 min-w-0">
<section className="card p-5">
<div className="flex items-baseline justify-between">
<h2 className="display text-base">Today's plan</h2>
<span className="mono text-xs" style={{ color: C.leaf }}>{doneCount}/{plan.length}</span>
</div>
<div className="mt-1" style={{ height: 5, background: C.rule, borderRadius: 999 }}>
<div className="h-full" style={{ ...bar((doneCount / plan.length) * 100), background: C.leaf, borderRadius: 999 }} />
</div>
<div className="mt-3 space-y-1">
{plan.map((p, i) => (
<button key={p.t} onClick={() => setPlan(plan.map((x, j) => j === i ? { ...x, done: !x.done } : x))}
className="w-full flex items-center gap-2.5 py-1.5 text-left" style={{ background: "transparent", border: "none", cursor: "pointer", color: C.ink }}>
<span aria-hidden="true" className="mono text-center shrink-0" style={{ width: 18, height: 18, lineHeight: "16px", fontSize: 11, borderRadius: 6, border: `1.5px solid ${p.done ? C.leaf : C.rule}`, background: p.done ? C.leaf : "transparent", color: "#fff" }}>{p.done ? "✓" : ""}</span>
<span className="text-sm flex-1 min-w-0 truncate" style={{ textDecoration: p.done ? "line-through" : "none", opacity: p.done ? 0.5 : 1 }}>{p.t}</span>
<span className="mono text-xs shrink-0" style={{ opacity: 0.5 }}>{p.h}</span>
</button>
))}
</div>
</section>
<section className="card p-5">
<h2 className="display text-base">{st.recTitle}</h2>
<p className="text-xs mt-1" style={{ opacity: 0.6 }}>{st.recNote}</p>
<div className="mt-3 space-y-2.5">
{recs.map((r) => (
<button key={r.subject + r.name} onClick={() => go("Practise")} className="practise w-full text-left px-3 py-2.5 flex items-center gap-3"
style={{ background: C.apricotSoft, border: `1px solid ${C.apricot}33`, borderRadius: 12, cursor: "pointer", color: C.ink }}>
<span className="mono shrink-0 text-center" style={{ width: 38, fontSize: 12, fontWeight: 600, color: C.apricot }}>{r.m}%</span>
<span className="min-w-0">
<span className="block text-sm font-semibold truncate">{r.name}</span>
<span className="mono block" style={{ fontSize: 10, opacity: 0.6 }}>{r.subject} · {r.ex} exercises</span>
</span>
<span className="mono text-xs ml-auto shrink-0" style={{ color: C.apricot }}>→</span>
</button>
))}
</div>
</section>
</div>
</div>
</>
);
}
function SubjectsPage({ st, bar, go }) {
const [sel, setSel] = useState(null);
const s = sel !== null ? st.subjects[sel] : null;
return (
<>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mt-6">
{st.subjects.map((sub, i) => {
const weak = sub.topics.filter((t) => t.m < 65).length;
const active = sel === i;
return (
<button key={sub.name} onClick={() => setSel(active ? null : i)} aria-pressed={active}
className="card p-4 text-left" style={{ cursor: "pointer", outline: active ? `2px solid ${C.teal}` : "none", color: C.ink }}>
<div className="flex items-center justify-between">
<span className="display" style={{ fontSize: "1.8rem", color: C.teal }}>{sub.grade}</span>
<span className="mono" style={{ fontSize: 10, color: sub.ready >= 75 ? C.leaf : sub.ready >= 60 ? C.teal : C.apricot }}>{sub.ready}%</span>
</div>
<div className="text-sm font-semibold mt-1 truncate">{sub.name}</div>
<div className="mt-2" style={{ height: 5, background: C.rule, borderRadius: 999 }}>
<div className="h-full" style={{ ...bar(sub.ready), background: sub.ready >= 75 ? C.leaf : sub.ready >= 60 ? C.teal : C.apricot, borderRadius: 999 }} />
</div>
<div className="mono mt-2" style={{ fontSize: 10, color: weak ? C.apricot : C.leaf }}>{weak ? `${weak} topics to fix` : "on track"}</div>
</button>
);
})}
</div>
{s && (
<section className="card p-5 mt-5 expand">
<div className="flex items-baseline justify-between flex-wrap gap-2">
<h2 className="display text-base">{s.name}</h2>
<span className="mono text-sm px-2.5 py-1" style={{ color: C.teal, background: C.tealSoft, borderRadius: 999 }}>predicted {s.grade}</span>
</div>
<div className="mt-4">
<TopicRows topics={s.topics} bar={bar} onPractise={() => go("Practise")} />
</div>
<p className="mono text-xs mt-4" style={{ opacity: 0.5 }}>Topics with ▸ open into sub-skills. Scores appear once you've answered enough questions for them to mean something.</p>
</section>
)}
{!s && <p className="mono text-xs mt-5" style={{ opacity: 0.5 }}>Tap a subject to see its topic and sub-skill breakdown.</p>}
</>
);
}
function PractisePage({ st, recs }) {
const [filter, setFilter] = useState(null);
const pool = useMemo(() => filter ? QUESTIONS.filter((q) => q.els.includes(filter)) : QUESTIONS, [filter]);
const [idx, setIdx] = useState(0);
const [picked, setPicked] = useState(null);
const [score, setScore] = useState({ right: 0, total: 0 });
const q = pool[idx % pool.length];
const answered = picked !== null;
const correct = answered && picked === q.ans;
const choose = (i) => {
if (answered) return;
setPicked(i);
setScore((s) => ({ right: s.right + (i === q.ans ? 1 : 0), total: s.total + 1 }));
};
const next = () => { setPicked(null); setIdx((i) => i + 1); };
const pickFilter = (el) => { setFilter(el === filter ? null : el); setIdx(0); setPicked(null); };
return (
<>
<p className="mono text-xs mt-5" style={{ color: C.teal }}>{st.practiseNote}</p>
<div className="flex items-center gap-2 flex-wrap mt-3">
<span className="eyebrow">Focus on:</span>
{recs.map((r) => (
<button key={r.name} onClick={() => pickFilter(r.name)}
className="mono px-3 py-1.5" style={{ fontSize: 11, borderRadius: 999, cursor: "pointer", border: `1.5px solid ${filter === r.name ? C.apricot : C.rule}`, background: filter === r.name ? C.apricot : C.card, color: filter === r.name ? "#fff" : C.ink }}>
{r.name} · {r.m}%
</button>
))}
{filter && <button onClick={() => pickFilter(filter)} className="mono text-xs" style={{ background: "none", border: "none", cursor: "pointer", color: C.teal }}>clear</button>}
<span className="mono text-xs ml-auto" style={{ opacity: 0.6 }}>Session: {score.right}/{score.total}</span>
</div>
<section className="card p-6 mt-4 max-w-2xl">
<div className="flex items-center justify-between">
<Eyebrow>{q.subject} · {q.els.join(", ")}</Eyebrow>
<span className="mono text-xs" style={{ opacity: 0.45 }}>Q{(idx % pool.length) + 1} of {pool.length}</span>
</div>
<h2 className="text-lg font-semibold mt-3" style={{ lineHeight: 1.45 }}>{q.q}</h2>
<div className="mt-4 space-y-2">
{q.opts.map((o, i) => {
const isPick = picked === i, isAns = i === q.ans;
let bg = C.card, bd = C.rule;
if (answered && isAns) { bg = C.leafSoft; bd = C.leaf; }
else if (answered && isPick && !isAns) { bg = C.apricotSoft; bd = C.apricot; }
return (
<button key={o} onClick={() => choose(i)} disabled={answered && !isPick && !isAns}
className="w-full text-left px-4 py-3 flex items-center gap-3"
style={{ background: bg, border: `1.5px solid ${bd}`, borderRadius: 12, cursor: answered ? "default" : "pointer", color: C.ink, transition: "background 150ms, border 150ms" }}>
<span className="mono text-xs" style={{ width: 18, opacity: 0.5 }}>{String.fromCharCode(65 + i)}</span>
<span className="text-sm">{o}</span>
{answered && isAns && <span className="mono text-xs ml-auto" style={{ color: C.leaf }}>✓ correct</span>}
{answered && isPick && !isAns && <span className="mono text-xs ml-auto" style={{ color: C.apricot }}>your pick</span>}
</button>
);
})}
</div>
{answered && (
<div className="expand mt-4 px-4 py-3" style={{ background: correct ? C.leafSoft : C.apricotSoft, borderRadius: 12 }}>
<div className="mono text-xs font-semibold" style={{ color: correct ? C.leaf : C.apricot }}>
{correct ? "Correct — mastery nudged up." : "Not this time — mastery noted."}
</div>
<p className="text-sm mt-1.5" style={{ lineHeight: 1.55 }}>{q.why}</p>
<div className="flex items-center gap-2 mt-3 flex-wrap">
{q.els.map((el) => (
<span key={el} className="mono px-2 py-0.5" style={{ fontSize: 10.5, borderRadius: 999, background: "#fff", border: `1px solid ${C.rule}` }}>
{el} {correct ? "▲ +2" : "▼ −1"}
</span>
))}
<button onClick={next} className="practise mono text-xs px-4 py-2 ml-auto" style={{ background: C.teal, color: "#fff", border: "none", borderRadius: 999, cursor: "pointer" }}>
Next question →
</button>
</div>
</div>
)}
</section>
<p className="mono text-xs mt-4" style={{ opacity: 0.5 }}>Every answer updates your sub-skill mastery — your dashboard reads from the same scores.</p>
</>
);
}
function MocksPage({ st }) {
return (
<div className="grid lg:grid-cols-3 gap-5 mt-6 items-start">
<section className="card p-5" style={{ borderColor: C.gold }}>
<Eyebrow style={{ color: C.gold, opacity: 1 }}>Next up</Eyebrow>
<h2 className="display text-lg mt-1">Mock Exam 2</h2>
<div className="mono text-sm mt-1" style={{ opacity: 0.65 }}>25 – 30 July · school hall</div>
<div className="display mt-3" style={{ fontSize: "2.4rem", color: C.gold }}>3 <span className="text-base" style={{ color: C.ink, opacity: 0.6 }}>days to go</span></div>
<button className="practise mono text-xs px-4 py-2.5 mt-4 w-full" style={{ background: C.teal, color: "#fff", border: "none", borderRadius: 999, cursor: "pointer" }}>
Take a 1-hour practice mock
</button>
<p className="text-xs mt-3" style={{ opacity: 0.6 }}>A timed mini-mock sharpens exam pace — your weakest sub-skills are weighted in.</p>
</section>
<section className="card p-5 lg:col-span-2">
<div className="flex items-baseline justify-between">
<h2 className="display text-base">Mock 1 (July) vs where you are now</h2>
<span className="mono text-xs" style={{ opacity: 0.55 }}>mean now: {st.meanGrade}</span>
</div>
<div className="mt-3">
<div className="grid grid-cols-12 gap-2 py-1.5 mono" style={{ fontSize: 10.5, opacity: 0.55 }}>
<span className="col-span-6">Subject</span><span className="col-span-2 text-center">Mock 1</span>
<span className="col-span-2 text-center">Now</span><span className="col-span-2 text-center">Move</span>
</div>
{st.subjects.map((r) => {
const mock = pointsToGrade(Math.max(1, r.pts - 1));
const d = Math.round(r.pts) - GRADE_POINTS[mock];
return (
<div key={r.name} className="grid grid-cols-12 gap-2 items-center py-2" style={{ borderTop: `1px solid ${C.rule}` }}>
<span className="col-span-6 text-sm font-semibold truncate">{r.name}</span>
<span className="col-span-2 mono text-sm text-center" style={{ opacity: 0.6 }}>{mock}</span>
<span className="col-span-2 mono text-sm text-center font-semibold" style={{ color: C.teal }}>{r.grade}</span>
<span className="col-span-2 mono text-xs text-center" style={{ color: d > 0 ? C.leaf : d < 0 ? C.apricot : "rgba(51,57,62,.4)" }}>
{d > 0 ? `▲ +${d}` : d < 0 ? `▼ ${d}` : "—"}
</span>
</div>
);
})}
</div>
<p className="mono text-xs mt-3" style={{ opacity: 0.5 }}>“Now” is your live predicted grade from sub-skill mastery — mock results recalibrate it.</p>
</section>
</div>
);
}
function ProgressPage({ st, trend }) {
return (
<>
<div className="grid lg:grid-cols-2 gap-5 mt-6 items-start">
<section className="card p-5">
<div className="flex items-baseline justify-between">
<h2 className="display text-base">Your climb</h2>
<span className="eyebrow">mean points per checkpoint</span>
</div>
<div style={{ height: 200 }} className="mt-3">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={trend} margin={{ top: 8, right: 10, bottom: 0, left: -16 }}>
<defs>
<linearGradient id="tealFade" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={C.teal} stopOpacity={0.2} />
<stop offset="100%" stopColor={C.teal} stopOpacity={0} />
</linearGradient>
</defs>
<XAxis dataKey="wk" tick={{ fontFamily: "IBM Plex Mono", fontSize: 11, fill: "rgba(51,57,62,.6)" }} stroke={C.rule} tickLine={false} />
<YAxis domain={[1, 12]} tick={{ fontFamily: "IBM Plex Mono", fontSize: 11, fill: "rgba(51,57,62,.6)" }} stroke="transparent" tickLine={false} />
<Tooltip formatter={(v) => [`${v} pts (${pointsToGrade(v)})`, "Mean"]} contentStyle={{ borderRadius: 10, border: `1px solid ${C.rule}`, fontFamily: "IBM Plex Sans" }} />
<ReferenceLine y={st.targetPts} stroke={C.gold} strokeDasharray="5 4"
label={{ value: `Next rung ${st.target}`, fill: C.gold, fontSize: 11, fontFamily: "IBM Plex Mono", position: "insideTopRight" }} />
<Area type="monotone" dataKey="pts" stroke={C.teal} strokeWidth={2.5} fill="url(#tealFade)" dot={{ r: 3.5, fill: C.teal, strokeWidth: 0 }} activeDot={{ r: 5 }} />
</AreaChart>
</ResponsiveContainer>
</div>
</section>
<section className="card p-5">
<div className="flex items-baseline justify-between">
<h2 className="display text-base">Hours in the books</h2>
<span className="eyebrow">last 8 weeks</span>
</div>
<div style={{ height: 200 }} className="mt-3">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={STUDY_WEEKS} margin={{ top: 8, right: 10, bottom: 0, left: -22 }}>
<XAxis dataKey="wk" tick={{ fontFamily: "IBM Plex Mono", fontSize: 11, fill: "rgba(51,57,62,.6)" }} stroke={C.rule} tickLine={false} />
<YAxis tick={{ fontFamily: "IBM Plex Mono", fontSize: 11, fill: "rgba(51,57,62,.6)" }} stroke="transparent" tickLine={false} />
<Tooltip formatter={(v) => [`${v} h`, "Study time"]} contentStyle={{ borderRadius: 10, border: `1px solid ${C.rule}`, fontFamily: "IBM Plex Sans" }} />
<Bar dataKey="h" fill={C.leaf} radius={[6, 6, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</section>
</div>
<section className="card p-5 mt-5">
<h2 className="display text-base">Grade movement since Term 1</h2>
<div className="mt-3 grid sm:grid-cols-2 gap-x-8">
{st.subjects.map((s) => {
const d = Math.round(s.pts) - s.t1;
return (
<div key={s.name} className="flex items-center gap-3 py-2" style={{ borderBottom: `1px solid ${C.rule}` }}>
<span className="text-sm font-semibold flex-1 min-w-0 truncate">{s.name}</span>
<span className="mono text-sm" style={{ opacity: 0.55 }}>{pointsToGrade(s.t1)}</span>
<span className="mono text-xs" style={{ opacity: 0.4 }}>→</span>
<span className="mono text-sm font-semibold" style={{ color: C.teal }}>{s.grade}</span>
<span className="mono text-xs" style={{ width: 44, textAlign: "right", color: d > 0 ? C.leaf : d < 0 ? C.apricot : "rgba(51,57,62,.4)" }}>
{d > 0 ? `▲ +${d}` : d < 0 ? `▼ ${d}` : "—"}
</span>
</div>
);
})}
</div>
<p className="mono text-xs mt-3" style={{ opacity: 0.5 }}>Now at {st.meanGrade} overall — predictions are estimates, not KNEC results.</p>
</section>
</>
);
}
export default function TayariApp() {
const [page, setPage] = useState("Dashboard");
const [personaId, setPersonaId] = useState("wanjiru");
const [plan, setPlan] = useState([
{ t: "Mathematics — Circle theorems drill", h: "1.5h", done: true },
{ t: "Chemistry — Alcohols & acids", h: "1.5h", done: true },
{ t: "English — Set-book essay", h: "1h", done: false },
{ t: "Kiswahili — Insha practice", h: "1h", done: false },
{ t: "Biology — Pedigree analysis", h: "1h", done: false },
]);
const [mounted, setMounted] = useState(false);
useEffect(() => { const t = setTimeout(() => setMounted(true), 60); return () => clearTimeout(t); }, []);
const bar = (w) => ({ width: mounted ? `${w}%` : "0%", transition: "width 900ms cubic-bezier(.22,1,.36,1)" });
const st = useMemo(() => buildStudent(PERSONAS.find((p) => p.id === personaId)), [personaId]);
const trend = TRENDS[personaId];
// recommendations: weakest evidenced sub-skills first, then weak topics without subs
const recs = useMemo(() => {
const pool = [];
for (const s of st.subjects) for (const t of s.topics) {
if (t.subs) for (const x of t.subs) { if (x.att >= MIN_EVIDENCE && x.m < 65) pool.push({ name: x.name, m: x.m, ex: x.ex, subject: s.name }); }
else if (t.m < 65) pool.push({ name: t.el, m: t.m, ex: t.ex, subject: s.name });
}
return pool.sort((a, b) => a.m - b.m).slice(0, 3);
}, [st]);
const greeting = { Dashboard: `Habari ya asubuhi, ${st.name.split(" ")[0]}`, "My Subjects": "Your eight subjects", Practise: "Practise", "Mock Exams": "Mock exams", Progress: "Your progress" }[page];
const sub = { Dashboard: st.subtitle, "My Subjects": "Tap any subject, then any topic with ▸, to see exactly what to fix.", Practise: "Short, targeted sets beat long, tired nights.", "Mock Exams": "Mocks calibrate your prediction — treat them like the real thing.", Progress: "Look how far you've come since Term 1." }[page];
const PersonaSwitch = ({ compact }) => (
<div className={compact ? "flex items-center gap-1.5" : "flex items-center gap-1.5 mt-3 px-2"}>
{!compact && <span className="mono" style={{ fontSize: 9, letterSpacing: "0.12em", opacity: 0.55, color: C.chalk }}>DEMO — VIEW AS</span>}
{PERSONAS.map((p) => (
<button key={p.id} onClick={() => setPersonaId(p.id)} title={p.name} aria-pressed={personaId === p.id}
className="display" style={{ width: 26, height: 26, borderRadius: "50%", fontSize: 11, cursor: "pointer",
background: personaId === p.id ? C.gold : compact ? C.paperDeep : "rgba(243,239,228,0.15)",
color: personaId === p.id ? C.boardDeep : compact ? C.ink : C.chalk,
border: personaId === p.id ? `2px solid ${C.gold}` : `1.5px solid ${compact ? C.rule : "rgba(243,239,228,0.3)"}` }}>
{p.letter}
</button>
))}
</div>
);
return (
<div style={{ background: `${GRAIN}, linear-gradient(${C.paper}, ${C.paperDeep})`, color: C.ink, minHeight: "100vh", fontFamily: "'IBM Plex Sans', system-ui, sans-serif" }}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
.mono { font-family: 'IBM Plex Mono', ui-monospace, monospace; }
.display { font-family: 'Archivo Black', 'IBM Plex Sans', sans-serif; letter-spacing: -0.01em; }
.eyebrow { font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; letter-spacing: 0.2em; text-transform: uppercase; opacity: 0.55; }
.card { background: ${C.card}; border: 1px solid ${C.rule}; border-radius: 14px; box-shadow: 0 1px 2px rgba(51,57,62,.05), 0 8px 24px rgba(51,57,62,.06); }
.weakname { text-decoration: underline wavy ${C.apricot}; text-decoration-thickness: 1.5px; text-underline-offset: 4px; }
.subrow { transition: background 150ms ease; }
.subrow:hover { background: rgba(46,139,122,0.05); }
.practise { transition: background 140ms ease, transform 140ms ease, filter 140ms ease; }
.practise:hover { filter: brightness(0.92); transform: translateY(-1px); }
.navbtn { transition: background 140ms ease; border-radius: 10px; }
.navbtn:hover { background: rgba(243,239,228,0.12); }
.expand { animation: openUp 320ms ease both; }
@keyframes openUp { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
button:focus-visible { outline: 3px solid ${C.teal}; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .expand { animation: none; } * { transition: none !important; } }
`}</style>
<div className="flex min-h-screen">
<aside className="hidden lg:flex flex-col w-56 shrink-0 px-4 py-6"
style={{ background: `${GRAIN}, linear-gradient(170deg, ${C.board}, ${C.boardDeep})`, color: C.chalk }}>
<div className="px-2">
<div className="display text-lg" style={{ color: "#fff" }}>Tayari</div>
<div className="eyebrow mt-1" style={{ color: C.gold, opacity: 1 }}>Uko tayari?</div>
</div>
<nav className="mt-8 space-y-1" aria-label="Main">
{NAV.map((n) => (
<button key={n} onClick={() => setPage(n)} aria-current={page === n ? "page" : undefined}
className="navbtn w-full text-left px-3 py-2 text-sm"
style={{ background: page === n ? "rgba(217,164,65,0.2)" : "transparent", color: page === n ? "#fff" : "rgba(243,239,228,.75)", border: "none", cursor: "pointer", fontWeight: page === n ? 600 : 400 }}>
{n}
</button>
))}
</nav>
<div className="mt-auto">
<div className="px-3 py-3 text-xs" style={{ background: "rgba(243,239,228,0.08)", borderRadius: 12, color: "rgba(243,239,228,.85)", fontStyle: "italic" }}>
Small daily progress, big December results.
</div>
<div className="flex items-center gap-2 mt-4 px-2">
<div className="display" style={{ width: 34, height: 34, borderRadius: "50%", background: C.gold, color: C.boardDeep, display: "grid", placeItems: "center", fontSize: 13 }}>{st.letter}</div>
<div>
<div className="text-sm font-semibold" style={{ color: "#fff" }}>{st.name}</div>
<div className="mono" style={{ fontSize: 10, opacity: 0.6 }}>Form 4 · 2026</div>
</div>
</div>
<PersonaSwitch />
</div>
</aside>
<div className="flex-1 min-w-0 px-5 lg:px-8 py-6 max-w-6xl mx-auto">
<div className="lg:hidden flex items-center gap-2 overflow-x-auto pb-3 -mx-1 px-1">
{NAV.map((n) => (
<button key={n} onClick={() => setPage(n)}
className="mono shrink-0 px-3 py-1.5" style={{ fontSize: 11, borderRadius: 999, cursor: "pointer", border: `1.5px solid ${page === n ? C.teal : C.rule}`, background: page === n ? C.teal : C.card, color: page === n ? "#fff" : C.ink }}>
{n}
</button>
))}
<span className="ml-auto shrink-0"><PersonaSwitch compact /></span>
</div>
<div className="flex items-end justify-between flex-wrap gap-3">
<div>
<h1 className="display text-2xl">{greeting}</h1>
<p className="text-sm mt-1" style={{ opacity: 0.7 }}>{sub}</p>
</div>
<div className="mono text-xs px-3 py-1.5" style={{ background: C.card, border: `1px solid ${C.rule}`, borderRadius: 999 }}>Wed 22 July 2026 · Term 2</div>
</div>
{page === "Dashboard" && <Dashboard st={st} bar={bar} plan={plan} setPlan={setPlan} recs={recs} go={setPage} />}
{page === "My Subjects" && <SubjectsPage st={st} bar={bar} go={setPage} />}
{page === "Practise" && <PractisePage st={st} recs={recs} />}
{page === "Mock Exams" && <MocksPage st={st} />}
{page === "Progress" && <ProgressPage st={st} trend={trend} />}
</div>
</div>
</div>
);
}