feat: Redesign quiz viewer to match academic textbook aesthetic
All checks were successful
Automated Container Build / build-and-push (push) Successful in 51s

This commit is contained in:
Elijah 2026-06-28 08:24:32 -07:00
parent 7c13159855
commit 464b179df9

View file

@ -260,85 +260,99 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
return <div>Question not found.</div>; return <div>Question not found.</div>;
} }
const categories = Array.from(new Set(quiz.questions.map(q => q.category.toUpperCase()))).join(" · ");
return ( return (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
{/* Running Score Header */}
<div className="w-full max-w-2xl mb-6 flex items-center justify-between bg-bg-surface border border-border-light rounded-lg px-5 py-3 shadow-[var(--shadow-card)]"> {/* Title & Meta */}
<div> <div className="w-full max-w-3xl mb-6 text-left">
<span className="text-sm font-medium text-text-secondary">Progress: </span> <h1 className="text-3xl font-bold font-serif text-text-heading mb-2">
<span className="text-sm font-bold text-text-heading">{currentIndex + 1} / {order.length}</span> {quiz.name}
</div> </h1>
<div> <div className="text-sm tracking-widest text-text-muted font-medium">
<span className="text-sm font-medium text-text-secondary">Score so far: </span> {categories}
<span className="text-sm font-bold text-primary">
{parseFloat(runningScore.toFixed(2))} / {maxPossibleSoFar}
</span>
</div> </div>
</div> </div>
{/* Question Card */} {/* Main Unified Card */}
<div className="w-full max-w-2xl bg-bg-surface rounded-2xl border border-border-light shadow-[var(--shadow-card)] overflow-hidden mb-8"> <div className="w-full max-w-3xl bg-bg-surface-alt rounded-2xl border border-border-light shadow-[var(--shadow-card)] overflow-hidden mb-8">
{/* Header (Type & Category) */} {/* Progress Header */}
<div className="bg-bg-surface-alt border-b border-border-light px-6 py-4 flex items-center justify-between"> <div className="px-6 md:px-8 py-5 flex items-center gap-4">
<span className="text-xs font-bold uppercase tracking-wider text-primary"> <div className="text-sm font-mono text-text-secondary whitespace-nowrap tracking-wider">
{currentQ.type === "MULTIPLE_CHOICE" ? "Multiple Choice" : "Select All That Apply"} Q {currentIndex + 1} / {order.length}
</span> </div>
<span className="inline-flex px-2.5 py-1 rounded-full bg-badge-bg text-badge-text text-xs font-medium"> <div className="flex-1 h-2 bg-border-light rounded-full overflow-hidden">
<div
className="h-full bg-border transition-all duration-300"
style={{ width: `${(currentIndex / order.length) * 100}%` }}
/>
</div>
<div className="bg-badge-bg/50 px-3 py-1 rounded-full text-sm font-mono text-primary whitespace-nowrap">
Score: {parseFloat(runningScore.toFixed(2))} / {maxPossibleSoFar}
</div>
</div>
{/* Question Area */}
<div className="px-6 md:px-8 pb-8 pt-2">
{/* Tags */}
<div className="flex items-center justify-between mb-8">
<span className="inline-flex px-3 py-1 rounded-md bg-primary/10 text-primary text-xs font-bold uppercase tracking-wider">
{currentQ.category} {currentQ.category}
</span> </span>
<span className="inline-flex px-3 py-1 rounded-md bg-amber-500/10 text-amber-600 text-xs font-bold uppercase tracking-wider">
{currentQ.type === "MULTIPLE_CHOICE" ? "Multiple Choice" : "Select All That Apply"}
</span>
</div> </div>
<div className="p-6 md:p-8"> {/* Prompt */}
<div className="markdown-content text-lg text-text-heading mb-8 leading-relaxed font-medium"> <div className="markdown-content text-xl text-text-heading mb-8 font-serif leading-relaxed font-semibold">
<ReactMarkdown remarkPlugins={[remarkGfm]}> <ReactMarkdown remarkPlugins={[remarkGfm]}>
{currentQ.prompt} {currentQ.prompt}
</ReactMarkdown> </ReactMarkdown>
</div> </div>
<div className="space-y-3"> {/* Options */}
<div className="space-y-4">
{(shuffledOptions[currentQId] || currentQ.options).map((opt) => { {(shuffledOptions[currentQId] || currentQ.options).map((opt) => {
const isSelected = currentSelections.includes(opt.id); const isSelected = currentSelections.includes(opt.id);
let stateClass = "border-border hover:border-primary/50 cursor-pointer"; let stateClass = "border-border-light hover:border-border cursor-pointer";
let icon = null;
if (isSubmitted) { if (isSubmitted) {
stateClass = "cursor-default opacity-80"; stateClass = "cursor-default opacity-80";
if (opt.isCorrect) { if (opt.isCorrect) {
stateClass = "border-success bg-success-bg/30 text-text-heading"; stateClass = "border-success bg-success-bg text-text-heading";
icon = (
<svg className="w-5 h-5 text-success flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
</svg>
);
} else if (isSelected && !opt.isCorrect) { } else if (isSelected && !opt.isCorrect) {
// Selected incorrectly stateClass = "border-error bg-error-bg text-text-heading";
stateClass = "border-error bg-error-bg/30 text-text-heading";
icon = (
<svg className="w-5 h-5 text-error flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
</svg>
);
} else { } else {
// Not selected, not correct stateClass = "border-border-light";
stateClass = "border-border-light bg-bg-surface-alt/50";
} }
} else if (isSelected) { } else if (isSelected) {
stateClass = "border-primary bg-primary/5 ring-1 ring-primary"; stateClass = "border-primary bg-primary/5";
} }
return ( return (
<div <div
key={opt.id} key={opt.id}
onClick={() => toggleOption(opt.id)} onClick={() => toggleOption(opt.id)}
className={`flex items-start gap-4 p-4 rounded-xl border-2 transition-all duration-200 ${stateClass}`} className={`flex items-center gap-4 px-5 py-4 rounded-xl border transition-all duration-200 ${stateClass}`}
> >
<div className="pt-0.5"> <div className="flex-shrink-0">
<div className={`w-5 h-5 rounded ${currentQ.type === 'SATA' ? 'border' : 'border rounded-full'} flex items-center justify-center transition-colors ${ <div className={`w-5 h-5 rounded ${currentQ.type === 'SATA' ? 'border' : 'border rounded-full'} flex items-center justify-center transition-colors ${
isSelected ? 'border-primary bg-primary text-white' : 'border-border-light bg-bg-surface' isSelected && isSubmitted && opt.isCorrect ? 'border-success bg-success' :
isSelected && isSubmitted && !opt.isCorrect ? 'border-error bg-error' :
isSubmitted && opt.isCorrect ? 'border-success bg-success' :
isSelected ? 'border-primary bg-primary' : 'border-border bg-bg-surface'
}`}> }`}>
{isSelected && ( {isSelected && (
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
{!isSelected && isSubmitted && opt.isCorrect && (
<svg className="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg> </svg>
)} )}
@ -347,11 +361,6 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
<div className="flex-1 text-text-body font-medium leading-snug"> <div className="flex-1 text-text-body font-medium leading-snug">
{opt.text} {opt.text}
</div> </div>
{icon && (
<div className="pt-0.5 pl-2">
{icon}
</div>
)}
</div> </div>
); );
})} })}
@ -393,13 +402,10 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
return ( return (
<div className="mt-8 flex flex-col gap-4 animate-slide-up"> <div className="mt-8 flex flex-col gap-4 animate-slide-up">
{/* Result Banner */}
<div className={`flex items-center gap-3 px-5 py-4 rounded-xl border ${bannerBg} font-bold text-lg tracking-wide shadow-sm`}> <div className={`flex items-center gap-3 px-5 py-4 rounded-xl border ${bannerBg} font-bold text-lg tracking-wide shadow-sm`}>
{icon} {icon}
{badgeText} {badgeText}
</div> </div>
{/* Rationale Box */}
<div className="bg-bg-callout border border-primary/20 rounded-xl p-5 shadow-sm"> <div className="bg-bg-callout border border-primary/20 rounded-xl p-5 shadow-sm">
<h4 className="text-sm font-bold text-text-heading uppercase tracking-wider mb-2">Rationale</h4> <h4 className="text-sm font-bold text-text-heading uppercase tracking-wider mb-2">Rationale</h4>
<div className="markdown-content text-sm text-text-body"> <div className="markdown-content text-sm text-text-body">
@ -411,34 +417,35 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
</div> </div>
); );
})()} })()}
</div>
{/* Action bar */} {/* Action Button */}
<div className="bg-bg-surface-alt px-6 py-5 border-t border-border-light flex justify-end"> <div className="mt-10 flex">
{!isSubmitted ? ( {!isSubmitted ? (
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={currentSelections.length === 0} disabled={currentSelections.length === 0}
className="px-6 py-2.5 rounded-lg bg-primary text-white font-medium hover:bg-primary-hover disabled:opacity-50 transition-all duration-200 cursor-pointer" className="w-full py-3.5 rounded-xl bg-primary/60 hover:bg-primary/80 text-white font-bold text-lg tracking-wide disabled:opacity-50 disabled:hover:bg-primary/60 transition-all duration-200 cursor-pointer shadow-sm"
> >
Submit Answer Submit Answer
</button> </button>
) : currentIndex + 1 < order.length ? ( ) : currentIndex + 1 < order.length ? (
<button <button
onClick={handleNext} onClick={handleNext}
className="px-6 py-2.5 rounded-lg bg-primary text-white font-medium hover:bg-primary-hover transition-all duration-200 cursor-pointer" className="w-full py-3.5 rounded-xl bg-primary hover:bg-primary-hover text-white font-bold text-lg tracking-wide transition-all duration-200 cursor-pointer shadow-sm"
> >
Next Question Next Question
</button> </button>
) : ( ) : (
<button <button
onClick={handleFinish} onClick={handleFinish}
className="px-6 py-2.5 rounded-lg bg-success text-white font-medium hover:bg-success/90 transition-all duration-200 cursor-pointer" className="w-full py-3.5 rounded-xl bg-success hover:bg-success/90 text-white font-bold text-lg tracking-wide transition-all duration-200 cursor-pointer shadow-sm"
> >
Finish Quiz Finish Quiz
</button> </button>
)} )}
</div> </div>
</div>
</div> </div>
</div> </div>
); );