| 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))];
// ---------- data (mastery-engine outputs, mocked) ----------
const SUBJECTS = [
{ name: "Mathematics", grade: "B-", t1: 6.5, elements: [
{ el: "Algebra", m: 82 }, { el: "Geometry", m: 54, ex: 12 }, { el: "Trigonometry", m: 61, ex: 9 },
{ el: "Probability & Statistics", m: 74 }, { el: "Calculus", m: 47, ex: 15 }, { el: "Commercial Arithmetic", m: 88 },
], fix: "Geometry and Calculus are holding you at B-. Clearing Calculus alone is worth one grade." },
{ name: "English", grade: "B+", t1: 9, elements: [
{ el: "Comprehension", m: 84 }, { el: "Grammar", m: 79 }, { el: "Composition", m: 71 }, { el: "Set Books", m: 66, ex: 8 },
], fix: "Set-book essays are your weakest paper. Practise two guided essays this week." },
{ name: "Kiswahili", grade: "B", t1: 8, elements: [
{ el: "Ufahamu", m: 80 }, { el: "Sarufi", m: 72 }, { el: "Insha", m: 63, ex: 10 }, { el: "Fasihi", m: 75 },
], fix: "Insha structure loses you marks. Focus on mwanzo–kati–mwisho drills." },
{ name: "Biology", grade: "C+", t1: 6, elements: [
{ el: "Cell Biology", m: 77 }, { el: "Genetics", m: 52, ex: 14 }, { el: "Ecology", m: 70 }, { el: "Physiology", m: 58, ex: 11 },
], fix: "Genetics crosses and physiology diagrams — both fixable with targeted practice." },
{ name: "Chemistry", grade: "C", t1: 6, elements: [
{ el: "The Mole", m: 58, ex: 13 }, { el: "Organic Chemistry", m: 41, ex: 18 }, { el: "Acids, Bases & Salts", m: 69 }, { el: "Electrochemistry", m: 55, ex: 9 },
], fix: "Organic Chemistry is your single weakest element across all subjects. Start here today." },
{ name: "Physics", grade: "B-", t1: 7, elements: [
{ el: "Mechanics", m: 76 }, { el: "Electricity", m: 64, ex: 7 }, { el: "Waves & Optics", m: 71 }, { el: "Modern Physics", m: 60, ex: 8 },
], fix: "Electricity numericals — practise circuit problems under time." },
{ name: "History & Govt", grade: "A-", t1: 10, elements: [
{ el: "Pre-Colonial Kenya", m: 90 }, { el: "Colonial Period", m: 85 }, { el: "Government & Constitution", m: 78 }, { el: "World History", m: 81 },
], fix: "You're on track for A-. Keep essay timing sharp to push for A." },
{ name: "Business Studies", grade: "B+", t1: 9, elements: [
{ el: "Trade & Commerce", m: 83 }, { el: "Bookkeeping", m: 68, ex: 6 }, { el: "Money & Banking", m: 80 }, { el: "Entrepreneurship", m: 86 },
], fix: "Bookkeeping ledger entries — small errors, easy marks to recover." },
];
const TREND = [
{ 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 },
];
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 MOCK1 = [
{ s: "Mathematics", g: "C+", now: "B-" }, { s: "English", g: "B+", now: "B+" }, { s: "Kiswahili", g: "B-", now: "B" },
{ s: "Biology", g: "C", now: "C+" }, { s: "Chemistry", g: "C-", now: "C" }, { s: "Physics", g: "C+", now: "B-" },
{ s: "History & Govt", g: "B+", now: "A-" }, { s: "Business Studies", g: "B", now: "B+" },
];
const QUESTIONS = [
{ subject: "Mathematics", els: ["Calculus"], 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: ["Geometry"], 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: ["Organic Chemistry"], 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: ["Genetics"], 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 readiness = (s) => Math.round(s.elements.reduce((a, e) => a + e.m, 0) / s.elements.length);
const NAV = ["Dashboard", "My Subjects", "Practise", "Mock Exams", "Progress"];
// ---------- shared bits ----------
const Eyebrow = ({ children, style }) => <div className="eyebrow" style={style}>{children}</div>;
function ElementRows({ elements, bar, onPractise }) {
return (
<div className="space-y-3">
{elements.map((e) => {
const w = e.m < 65;
return (
<div key={e.el} className="flex items-center gap-3">
<span className={`text-sm flex-1 min-w-0 truncate ${w ? "weakname" : ""}`}>{e.el}</span>
<span className="flex-1 max-w-40" style={{ height: 8, background: C.rule, borderRadius: 999 }}>
<span className="block h-full" style={{ ...bar(e.m), background: w ? C.apricot : e.m >= 75 ? C.leaf : C.teal, borderRadius: 999 }} />
</span>
<span className="mono text-xs font-semibold" style={{ width: 34, textAlign: "right" }}>{e.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 && onPractise(e.el)}>
Practise · {e.ex}
</button>
) : <span style={{ width: 86 }} aria-hidden="true" />}
</div>
);
})}
</div>
);
}
// ---------- pages ----------
function Dashboard({ bar, meanPts, meanGrade, target, 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}` }}>{meanGrade}</span>
<span className="mono text-sm" style={{ opacity: 0.6 }}>{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] <= meanPts ? C.teal : C.rule, boxShadow: g === target ? `0 0 0 1.5px ${C.gold}` : "none" }} />
))}
</div>
<div className="mono mt-1.5" style={{ fontSize: 10, color: C.leaf }}>▲ +0.4 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" }}>TARGET</span>
<span className="display" style={{ fontSize: 20, lineHeight: 1 }}>{target}</span>
</div>
<div>
<Eyebrow>Your target</Eyebrow>
<div className="text-sm mt-1" style={{ opacity: 0.75 }}>1 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">
{SUBJECTS.map((sub, i) => {
const weak = sub.elements.filter((e) => e.m < 65);
const ready = readiness(sub);
const isOpen = open === i;
return (
<div key={sub.name} style={{ borderBottom: i < 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: `${ready}%`, background: ready >= 75 ? C.leaf : ready >= 60 ? C.teal : C.apricot, borderRadius: 999 }} />
</span>
<span className="mono text-xs font-semibold" style={{ width: 34, textAlign: "right" }}>{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">
<p className="text-sm mt-1 mb-3 pl-3" style={{ borderLeft: `3px solid ${C.apricot}`, opacity: 0.85, fontStyle: "italic" }}>{sub.fix}</p>
<ElementRows elements={sub.elements} 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">Start here</h2>
<p className="text-xs mt-1" style={{ opacity: 0.6 }}>Your three weakest elements right now.</p>
<div className="mt-3 space-y-2.5">
{recs.map((r) => (
<button key={r.subject + r.el} 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.el}</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({ bar, go }) {
const [sel, setSel] = useState(null);
const s = sel !== null ? SUBJECTS[sel] : null;
return (
<>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mt-6">
{SUBJECTS.map((sub, i) => {
const ready = readiness(sub);
const weak = sub.elements.filter((e) => e.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: ready >= 75 ? C.leaf : ready >= 60 ? C.teal : C.apricot }}>{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(ready), background: ready >= 75 ? C.leaf : 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} elements 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>
<p className="text-sm mt-3 pl-3" style={{ borderLeft: `3px solid ${C.apricot}`, opacity: 0.85, fontStyle: "italic" }}>{s.fix}</p>
<div className="mt-4">
<ElementRows elements={s.elements} bar={bar} onPractise={() => go("Practise")} />
</div>
</section>
)}
{!s && <p className="mono text-xs mt-5" style={{ opacity: 0.5 }}>Tap a subject to see its element breakdown.</p>}
</>
);
}
function PractisePage({ 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 (
<>
<div className="flex items-center gap-2 flex-wrap mt-6">
<span className="eyebrow">Focus on:</span>
{recs.map((r) => (
<button key={r.el} onClick={() => pickFilter(r.el)}
className="mono px-3 py-1.5" style={{ fontSize: 11, borderRadius: 999, cursor: "pointer", border: `1.5px solid ${filter === r.el ? C.apricot : C.rule}`, background: filter === r.el ? C.apricot : C.card, color: filter === r.el ? "#fff" : C.ink }}>
{r.el} · {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, col = C.ink;
if (answered && isAns) { bg = C.leafSoft; bd = C.leaf; col = C.ink; }
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: col, 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 element mastery — your dashboard reads from the same scores.</p>
</>
);
}
function MocksPage() {
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 elements 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: C+ → B</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>
{MOCK1.map((r, i) => {
const d = GRADE_POINTS[r.now] - GRADE_POINTS[r.g];
return (
<div key={r.s} 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.s}</span>
<span className="col-span-2 mono text-sm text-center" style={{ opacity: 0.6 }}>{r.g}</span>
<span className="col-span-2 mono text-sm text-center font-semibold" style={{ color: C.teal }}>{r.now}</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 element mastery — mock results recalibrate it.</p>
</section>
</div>
</>
);
}
function ProgressPage({ meanGrade, target }) {
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={[6, 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={GRADE_POINTS[target]} stroke={C.gold} strokeDasharray="5 4"
label={{ value: `Target ${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">
{SUBJECTS.map((s) => {
const d = GRADE_POINTS[s.grade] - 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 {meanGrade} overall — predictions are estimates, not KNEC results.</p>
</section>
</>
);
}
// ---------- app shell ----------
export default function TayariApp() {
const [page, setPage] = useState("Dashboard");
const [plan, setPlan] = useState([
{ t: "Mathematics — Calculus drill", h: "1.5h", done: true },
{ t: "Chemistry — Organic reactions", h: "1.5h", done: true },
{ t: "English — Set-book essay", h: "1h", done: false },
{ t: "Kiswahili — Insha practice", h: "1h", done: false },
{ t: "Biology — Genetics crosses", 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 meanPts = useMemo(() => {
const pts = SUBJECTS.map((x) => GRADE_POINTS[x.grade]).sort((a, b) => b - a).slice(0, 7);
return pts.reduce((a, b) => a + b, 0) / 7;
}, []);
const meanGrade = pointsToGrade(meanPts);
const target = "B+";
const recs = useMemo(() =>
SUBJECTS.flatMap((s) => s.elements.filter((e) => e.m < 65).map((e) => ({ ...e, subject: s.name })))
.sort((a, b) => a.m - b.m).slice(0, 3), []);
const greeting = { Dashboard: "Habari ya asubuhi, Wanjiru", "My Subjects": "Your eight subjects", Practise: "Practise", "Mock Exams": "Mock exams", Progress: "Your progress" }[page];
const sub = { Dashboard: "Every hour you put in now is a mark in November.", "My Subjects": "Tap any subject 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];
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 }}>W</div>
<div>
<div className="text-sm font-semibold" style={{ color: "#fff" }}>Wanjiru M.</div>
<div className="mono" style={{ fontSize: 10, opacity: 0.6 }}>Form 4 · 2026</div>
</div>
</div>
</div>
</aside>
<div className="flex-1 min-w-0 px-5 lg:px-8 py-6 max-w-6xl mx-auto">
{/* mobile nav */}
<div className="lg:hidden flex 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>
))}
</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 bar={bar} meanPts={meanPts} meanGrade={meanGrade} target={target} plan={plan} setPlan={setPlan} recs={recs} go={setPage} />}
{page === "My Subjects" && <SubjectsPage bar={bar} go={setPage} />}
{page === "Practise" && <PractisePage recs={recs} />}
{page === "Mock Exams" && <MocksPage />}
{page === "Progress" && <ProgressPage meanGrade={meanGrade} target={target} />}
</div>
</div>
</div>
);
}