Study/src/components/quizzes/QuizResults.tsx
Elijah b7ce314f01
Some checks failed
Automated Container Build / build-and-push (push) Failing after 7s
Initial working commit
2026-06-27 19:19:38 -07:00

151 lines
6.1 KiB
TypeScript

"use client";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { CategoryBreakdown } from "./CategoryBreakdown";
import { scoreQuestion } from "@/lib/scoring";
interface QuizResultsProps {
quiz: any; // QuizData
attempt: any; // QuizAttempt
onRetake: (missedIds: string[]) => void;
onClose: () => void;
}
export function QuizResults({ quiz, attempt, onRetake, onClose }: QuizResultsProps) {
let answers: Record<string, string[]> = {};
try {
answers = JSON.parse(attempt.answersJson);
} catch {}
// Determine non-full-credit questions for review list
const reviewItems: any[] = [];
const missedIds: string[] = [];
const answeredIds = Object.keys(answers);
const questionsToReview = quiz.questions.filter((q: any) => answeredIds.includes(q.id));
questionsToReview.forEach((q: any) => {
const formattedQ = {
id: q.id,
type: q.type,
options: q.options,
};
const score = scoreQuestion(formattedQ, answers[q.id] || []);
// If not full credit (1.0), add to review and missed arrays
if (score < 1) {
missedIds.push(q.id);
reviewItems.push({
question: q,
score,
selections: answers[q.id] || []
});
}
});
const percentage = attempt.maxScore > 0 ? (attempt.score / attempt.maxScore) * 100 : 0;
return (
<div className="max-w-3xl mx-auto space-y-8 animate-fade-in pb-12">
{/* Top Banner */}
<div className="bg-bg-surface rounded-2xl border border-border-light shadow-[var(--shadow-card)] p-8 text-center relative overflow-hidden">
{percentage >= 80 && (
<div className="absolute top-0 left-0 w-full h-2 bg-success"></div>
)}
{percentage < 80 && percentage >= 60 && (
<div className="absolute top-0 left-0 w-full h-2 bg-primary"></div>
)}
{percentage < 60 && (
<div className="absolute top-0 left-0 w-full h-2 bg-error"></div>
)}
<h2 className="text-2xl font-bold text-text-heading mb-2">Quiz Complete</h2>
{attempt.isPartialRetake && (
<p className="text-sm text-text-secondary mb-4">(Partial Retake)</p>
)}
<div className="flex justify-center items-end gap-2 mb-6">
<span className="text-5xl font-black text-text-heading">{Math.round(percentage)}%</span>
<span className="text-lg text-text-muted mb-1 font-medium">({attempt.score.toFixed(2)} / {attempt.maxScore})</span>
</div>
<div className="flex justify-center gap-4">
<button
onClick={onClose}
className="px-6 py-2.5 rounded-lg border border-border text-text-heading font-medium hover:bg-bg-surface-alt transition-all duration-200 cursor-pointer"
>
Back to Quizzes
</button>
{missedIds.length > 0 && (
<button
onClick={() => onRetake(missedIds)}
className="px-6 py-2.5 rounded-lg bg-primary text-white font-medium hover:bg-primary-hover transition-all duration-200 cursor-pointer"
>
Retake Missed ({missedIds.length})
</button>
)}
</div>
</div>
{/* Category Breakdown */}
<div className="bg-bg-surface rounded-2xl border border-border-light shadow-[var(--shadow-card)] p-6 md:p-8">
<h3 className="text-lg font-bold text-text-heading mb-6">Category Breakdown</h3>
<CategoryBreakdown quiz={quiz} attempt={attempt} answeredIds={answeredIds} answers={answers} />
</div>
{/* Review List */}
{reviewItems.length > 0 && (
<div className="space-y-6">
<h3 className="text-xl font-bold text-text-heading px-2">Areas for Review</h3>
{reviewItems.map((item, idx) => {
const q = item.question;
const sels = item.selections;
return (
<div key={q.id} className="bg-bg-surface rounded-2xl border border-border-light shadow-sm overflow-hidden">
<div className="bg-error-bg/30 border-b border-error/20 px-6 py-3 flex items-center justify-between">
<span className="text-sm font-medium text-error">Score: {item.score.toFixed(2)} / 1</span>
<span className="text-xs font-bold uppercase tracking-wider text-text-muted">{q.category}</span>
</div>
<div className="p-6">
<div className="markdown-content text-text-heading mb-6 font-medium">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{q.prompt}</ReactMarkdown>
</div>
<div className="space-y-2 mb-6">
{q.options.map((opt: any) => {
const isSelected = sels.includes(opt.id);
let style = "text-text-body";
if (opt.isCorrect) style = "text-success font-medium bg-success-bg/50 border-success/30";
else if (isSelected && !opt.isCorrect) style = "text-error font-medium line-through bg-error-bg/50 border-error/30";
else style = "border-transparent";
return (
<div key={opt.id} className={`flex items-start gap-3 p-3 rounded-lg border ${style}`}>
<div className="pt-0.5">
{opt.isCorrect ? "✓" : (isSelected ? "✗" : "•")}
</div>
<div>{opt.text}</div>
</div>
);
})}
</div>
<div className="bg-bg-callout border border-primary/20 rounded-xl p-5">
<h4 className="text-xs font-bold text-text-heading uppercase tracking-wider mb-2">Rationale</h4>
<div className="markdown-content text-sm text-text-body">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{q.rationale}</ReactMarkdown>
</div>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
);
}