File size: 9,931 Bytes
65d68f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | "use client";
import { useMemo, useState } from "react";
import Link from "next/link";
import { CheckCircle2, Clock, Flag, Send } from "lucide-react";
import { buildWeekendPyqTest, evaluatePyqAnswers, updatePhase3Activity, type WeekendPyqResult } from "@/lib/phase3-learning";
import { cn } from "@/lib/utils";
import { getDefaultTuitionSupportInput } from "./tuitionSupportData";
export function WeekendPyqTestPage() {
const input = useMemo(() => getDefaultTuitionSupportInput(), []);
const test = useMemo(() => buildWeekendPyqTest(input), [input]);
const [activeQuestionId, setActiveQuestionId] = useState(test.questions[0]?.id ?? "");
const [answers, setAnswers] = useState<Record<string, string>>({});
const [marked, setMarked] = useState<Set<string>>(() => new Set());
const [result, setResult] = useState<WeekendPyqResult | null>(null);
const activeQuestion = test.questions.find((question) => question.id === activeQuestionId) ?? test.questions[0];
function updateAnswer(questionId: string, answer: string) {
setAnswers((current) => ({ ...current, [questionId]: answer }));
}
function toggleMarked(questionId: string) {
setMarked((current) => {
const next = new Set(current);
if (next.has(questionId)) next.delete(questionId);
else next.add(questionId);
return next;
});
}
function submitTest() {
const nextResult = evaluatePyqAnswers(test, answers);
setResult(nextResult);
updatePhase3Activity({
pyqSubmittedAt: new Date().toISOString(),
pyqScore: nextResult.score,
pyqMaxScore: nextResult.maxScore,
weakTopicsAdded: nextResult.weakAreas,
});
}
return (
<section className="mx-auto grid w-full max-w-7xl gap-5 px-4 py-6 text-white sm:px-6 lg:grid-cols-[220px_minmax(0,1fr)_280px] lg:px-8">
<aside className="rounded-xl border border-white/[0.08] bg-[#0B0B0B] p-4 lg:sticky lg:top-5 lg:self-start">
<p className="text-[10px] font-bold uppercase tracking-[0.16em] text-white/34">Navigator</p>
<div className="mt-4 grid grid-cols-4 gap-2 lg:grid-cols-2">
{test.questions.map((question, index) => {
const active = question.id === activeQuestion.id;
const answered = Boolean(answers[question.id]?.trim());
return (
<button
key={question.id}
type="button"
onClick={() => setActiveQuestionId(question.id)}
className={cn(
"relative flex min-h-12 items-center justify-center rounded-lg border text-sm font-bold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30",
active ? "border-white bg-white text-black" : "border-white/[0.08] bg-white/[0.03] text-white/62 hover:text-white",
)}
>
{index + 1}
{answered ? <CheckCircle2 className="absolute right-1.5 top-1.5 size-3" /> : null}
{marked.has(question.id) ? <Flag className="absolute bottom-1.5 right-1.5 size-3" /> : null}
</button>
);
})}
</div>
<Link href="/tuition" className="mt-4 inline-flex min-h-11 w-full items-center justify-center rounded-lg border border-white/[0.1] px-3 text-sm font-semibold text-white/66 hover:border-white/25 hover:text-white">
Back to Today's Tuition
</Link>
</aside>
<main className="min-w-0 space-y-5">
<header className="border-b border-white/[0.1] pb-5">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<p className="text-[10px] font-bold uppercase tracking-[0.16em] text-white/34">{test.weekLabel}</p>
<h1 className="mt-2 text-2xl font-semibold text-white sm:text-3xl">Previous Questions Practice</h1>
<p className="mt-2 text-sm leading-6 text-white/56">
Sample exam practice for {test.subject} - {test.chapter}. More previous-question practice is coming as the beta improves.
</p>
</div>
<span className="inline-flex items-center gap-2 rounded-full border border-white/[0.1] bg-white/[0.04] px-3 py-1.5 text-xs font-semibold text-white/60">
<Clock className="size-4" />
{test.estimatedMinutes} min
</span>
</div>
<p className="mt-4 border-l-2 border-white/15 pl-3 text-xs leading-5 text-white/48">{test.safePatternNote}</p>
</header>
<article className="rounded-xl border border-white/[0.08] bg-[#0B0B0B] p-5 sm:p-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-[11px] font-bold uppercase tracking-[0.16em] text-white/34">{activeQuestion.label}</p>
<h2 className="mt-2 text-lg font-semibold leading-7 text-white">{activeQuestion.question}</h2>
</div>
<span className="rounded-full border border-white/[0.1] px-2.5 py-1 text-xs font-semibold text-white/56">
{activeQuestion.marks} marks
</span>
</div>
<textarea
aria-label="Write your board-style answer"
value={answers[activeQuestion.id] ?? ""}
onChange={(event) => updateAnswer(activeQuestion.id, event.target.value)}
rows={10}
placeholder="Write your board-style answer here. Include exam keywords."
className="mt-5 min-h-56 w-full resize-none rounded-lg border border-white/[0.12] bg-[#050505] p-4 text-base leading-7 text-white placeholder:text-white/34 focus:border-white/30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/30"
/>
<div className="mt-4 flex flex-wrap items-center justify-between gap-3">
<button
type="button"
onClick={() => toggleMarked(activeQuestion.id)}
className="inline-flex min-h-11 items-center gap-2 rounded-lg border border-white/[0.1] px-3 text-sm font-semibold text-white/66 transition hover:border-white/25 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30"
>
<Flag className="size-4" />
{marked.has(activeQuestion.id) ? "Marked for review" : "Mark for review"}
</button>
<button
type="button"
onClick={submitTest}
className="inline-flex min-h-11 items-center gap-2 rounded-lg bg-white px-4 text-sm font-bold text-black transition hover:bg-white/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40 focus-visible:ring-offset-2 focus-visible:ring-offset-black"
>
<Send className="size-4" />
Submit practice
</button>
</div>
</article>
</main>
<aside className="space-y-5 lg:sticky lg:top-5 lg:self-start">
<section className="rounded-xl border border-white/[0.08] bg-[#0B0B0B] p-4">
<p className="text-[10px] font-bold uppercase tracking-[0.16em] text-white/34">Sections</p>
<div className="mt-3 divide-y divide-white/[0.08] border-y border-white/[0.08]">
{["A", "B"].map((section) => (
<div key={section} className="py-3">
<p className="text-sm font-semibold text-white">Section {section}</p>
<p className="mt-1 text-xs text-white/44">
{section === "A" ? "Previous-question style" : "Exam practice"}
</p>
</div>
))}
</div>
</section>
{result ? (
<section className="rounded-xl border border-white/[0.08] bg-white p-4 text-black">
<p className="text-[10px] font-bold uppercase tracking-[0.16em] text-black/45">Answer review</p>
<p className="mt-2 text-3xl font-black">{result.score}/{result.maxScore}</p>
<div className="mt-4 space-y-3 text-sm">
<ReviewBlock title="Board-style answer" items={[result.correctAnswer]} />
<ReviewBlock title="Missing keywords" items={result.missingKeywords.length ? result.missingKeywords : ["No major keyword gap detected."]} />
<ReviewBlock title="Marks lost" items={[`${result.maxScore - result.score} mark${result.maxScore - result.score === 1 ? "" : "s"} lost from missing keywords or unclear points.`]} />
<ReviewBlock title="Topics to revise" items={result.weakAreas.length ? result.weakAreas : ["No weak area added."]} />
<p className="rounded-lg bg-black/[0.06] p-3 text-xs leading-5 text-black/70">{result.suggestedRevision}</p>
</div>
<Link href="/tuition/revision" className="mt-4 inline-flex min-h-11 w-full items-center justify-center rounded-lg bg-black px-3 text-sm font-bold text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black/40">
Add weak topics to revision
</Link>
</section>
) : (
<section className="border-y border-white/[0.08] py-4">
<p className="text-[10px] font-bold uppercase tracking-[0.16em] text-white/34">After submit</p>
<p className="mt-2 text-sm leading-6 text-white/52">
You will see score, board-style answer, missing keywords, marks lost, and revision suggestions.
</p>
</section>
)}
</aside>
</section>
);
}
function ReviewBlock({ title, items }: { title: string; items: string[] }) {
return (
<div>
<p className="text-[10px] font-bold uppercase tracking-[0.14em] text-black/45">{title}</p>
<ul className="mt-1 space-y-1">
{items.map((item) => (
<li key={item} className="text-xs leading-5 text-black/70">{item}</li>
))}
</ul>
</div>
);
}
|