403Webshell
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 :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/konzalta/exams.konzaltant.com/kcse_readiness_dashboard.jsx
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>
  );
}

Youez - 2016 - github.com/yon3zu
LinuXploit