feat: Redesign quiz viewer to match academic textbook aesthetic
All checks were successful
Automated Container Build / build-and-push (push) Successful in 51s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 51s
This commit is contained in:
parent
7c13159855
commit
464b179df9
1 changed files with 88 additions and 81 deletions
|
|
@ -260,85 +260,99 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
|
|||
return <div>Question not found.</div>;
|
||||
}
|
||||
|
||||
const categories = Array.from(new Set(quiz.questions.map(q => q.category.toUpperCase()))).join(" · ");
|
||||
|
||||
return (
|
||||
<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)]">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-text-secondary">Progress: </span>
|
||||
<span className="text-sm font-bold text-text-heading">{currentIndex + 1} / {order.length}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-text-secondary">Score so far: </span>
|
||||
<span className="text-sm font-bold text-primary">
|
||||
{parseFloat(runningScore.toFixed(2))} / {maxPossibleSoFar}
|
||||
</span>
|
||||
|
||||
{/* Title & Meta */}
|
||||
<div className="w-full max-w-3xl mb-6 text-left">
|
||||
<h1 className="text-3xl font-bold font-serif text-text-heading mb-2">
|
||||
{quiz.name}
|
||||
</h1>
|
||||
<div className="text-sm tracking-widest text-text-muted font-medium">
|
||||
{categories}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Question 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">
|
||||
{/* Main Unified Card */}
|
||||
<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) */}
|
||||
<div className="bg-bg-surface-alt border-b border-border-light px-6 py-4 flex items-center justify-between">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-primary">
|
||||
{currentQ.type === "MULTIPLE_CHOICE" ? "Multiple Choice" : "Select All That Apply"}
|
||||
</span>
|
||||
<span className="inline-flex px-2.5 py-1 rounded-full bg-badge-bg text-badge-text text-xs font-medium">
|
||||
{currentQ.category}
|
||||
</span>
|
||||
{/* Progress Header */}
|
||||
<div className="px-6 md:px-8 py-5 flex items-center gap-4">
|
||||
<div className="text-sm font-mono text-text-secondary whitespace-nowrap tracking-wider">
|
||||
Q {currentIndex + 1} / {order.length}
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div className="p-6 md:p-8">
|
||||
<div className="markdown-content text-lg text-text-heading mb-8 leading-relaxed font-medium">
|
||||
{/* 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}
|
||||
</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>
|
||||
|
||||
{/* Prompt */}
|
||||
<div className="markdown-content text-xl text-text-heading mb-8 font-serif leading-relaxed font-semibold">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{currentQ.prompt}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Options */}
|
||||
<div className="space-y-4">
|
||||
{(shuffledOptions[currentQId] || currentQ.options).map((opt) => {
|
||||
const isSelected = currentSelections.includes(opt.id);
|
||||
let stateClass = "border-border hover:border-primary/50 cursor-pointer";
|
||||
let icon = null;
|
||||
|
||||
let stateClass = "border-border-light hover:border-border cursor-pointer";
|
||||
|
||||
if (isSubmitted) {
|
||||
stateClass = "cursor-default opacity-80";
|
||||
if (opt.isCorrect) {
|
||||
stateClass = "border-success bg-success-bg/30 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>
|
||||
);
|
||||
stateClass = "border-success bg-success-bg text-text-heading";
|
||||
} else if (isSelected && !opt.isCorrect) {
|
||||
// Selected incorrectly
|
||||
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>
|
||||
);
|
||||
stateClass = "border-error bg-error-bg text-text-heading";
|
||||
} else {
|
||||
// Not selected, not correct
|
||||
stateClass = "border-border-light bg-bg-surface-alt/50";
|
||||
stateClass = "border-border-light";
|
||||
}
|
||||
} else if (isSelected) {
|
||||
stateClass = "border-primary bg-primary/5 ring-1 ring-primary";
|
||||
stateClass = "border-primary bg-primary/5";
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={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 ${
|
||||
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 && (
|
||||
<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" />
|
||||
</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">
|
||||
{opt.text}
|
||||
</div>
|
||||
{icon && (
|
||||
<div className="pt-0.5 pl-2">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
@ -393,13 +402,10 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
|
|||
|
||||
return (
|
||||
<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`}>
|
||||
{icon}
|
||||
{badgeText}
|
||||
</div>
|
||||
|
||||
{/* Rationale Box */}
|
||||
<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>
|
||||
<div className="markdown-content text-sm text-text-body">
|
||||
|
|
@ -411,33 +417,34 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
|
|||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Action bar */}
|
||||
<div className="bg-bg-surface-alt px-6 py-5 border-t border-border-light flex justify-end">
|
||||
{!isSubmitted ? (
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
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"
|
||||
>
|
||||
Submit Answer
|
||||
</button>
|
||||
) : currentIndex + 1 < order.length ? (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Next Question
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Finish Quiz
|
||||
</button>
|
||||
)}
|
||||
{/* Action Button */}
|
||||
<div className="mt-10 flex">
|
||||
{!isSubmitted ? (
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={currentSelections.length === 0}
|
||||
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
|
||||
</button>
|
||||
) : currentIndex + 1 < order.length ? (
|
||||
<button
|
||||
onClick={handleNext}
|
||||
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
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleFinish}
|
||||
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
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue