mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -1,16 +1,29 @@
|
||||
// components/QuizBlock.jsx
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
ChevronLeft, ChevronRight,
|
||||
Circle, CheckCircle2,
|
||||
Square, CheckSquare2,
|
||||
Clock, AlertTriangle, Info,
|
||||
Clock, AlertTriangle, Info, ArrowRight,
|
||||
} from "lucide-react";
|
||||
import { useClientNotifications } from "@/contexts/ClientNotificationContext";
|
||||
import { useDateFormat } from "@/hooks/useDateFormat";
|
||||
|
||||
// Zod schema: every question must have a non-empty answer before submitting.
|
||||
function buildAnswerSchema(questions = []) {
|
||||
const shape = {};
|
||||
for (const q of questions) {
|
||||
const key = String(q.question_id);
|
||||
shape[key] = q.type === "multi_select"
|
||||
? z.array(z.unknown()).min(1, "This question requires at least one selection.")
|
||||
: z.union([z.string(), z.number()]).refine((v) => v !== "" && v != null, "This question requires an answer.");
|
||||
}
|
||||
return z.object(shape);
|
||||
}
|
||||
|
||||
function QuizSkeleton() {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto space-y-5">
|
||||
@@ -50,7 +63,7 @@ function isQuestionAnswered(answer) {
|
||||
* onActiveChange — (isActive: boolean) => void — fires when session starts/ends
|
||||
* label — "Quiz" or "Assessment"
|
||||
*/
|
||||
const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession, onSubmit, onRetake, onActiveChange, label = "Quiz" }) => {
|
||||
const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession, onSubmit, onRetake, onActiveChange, onNextContent, nextLabel, label = "Quiz" }) => {
|
||||
const { fmtDateTime } = useDateFormat();
|
||||
const questions = quiz?.questions ?? [];
|
||||
const total = questions.length;
|
||||
@@ -63,6 +76,7 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
const [starting, setStarting] = useState(false);
|
||||
const [result, setResult] = useState(null);
|
||||
const [reviewAttempted, setReviewAttempted] = useState(false);
|
||||
const [submitError, setSubmitError] = useState(null);
|
||||
|
||||
// ── Timer state ───────────────────────────────────────────────────────────
|
||||
const [remainingSeconds, setRemainingSeconds] = useState(null);
|
||||
@@ -216,11 +230,25 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
};
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
// Zod: all questions must be answered before submission is allowed
|
||||
const schema = buildAnswerSchema(questions);
|
||||
const stringifiedAnswers = Object.fromEntries(
|
||||
Object.entries(answers).map(([k, v]) => [String(k), v])
|
||||
);
|
||||
const parsed = schema.safeParse(stringifiedAnswers);
|
||||
if (!parsed.success) {
|
||||
const unanswered = questions.filter(
|
||||
(q) => !isQuestionAnswered(answers[q.question_id])
|
||||
).length;
|
||||
setSubmitError(`Answer all questions before submitting — ${unanswered} still unanswered.`);
|
||||
return;
|
||||
}
|
||||
setSubmitError(null);
|
||||
setSubmitting(true);
|
||||
const res = await onSubmit?.(answers, sessionRef.current.sessionId);
|
||||
setSubmitting(false);
|
||||
if (res) { setResult(res); setStage("result"); }
|
||||
}, [answers, onSubmit]);
|
||||
}, [answers, questions, onSubmit]);
|
||||
|
||||
const handleOptionClick = (optionId) => {
|
||||
const question = questions[currentIndex];
|
||||
@@ -239,8 +267,6 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
|
||||
const handleGoToReview = () => {
|
||||
setReviewAttempted(true);
|
||||
const allAnswered = questions.every(q => isQuestionAnswered(answers[q.question_id]));
|
||||
if (!allAnswered) return;
|
||||
setStage("review");
|
||||
};
|
||||
|
||||
@@ -382,7 +408,7 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
<ul className="space-y-2.5 text-sm text-foreground/80">
|
||||
{hasTimeLimit && (
|
||||
<li className="flex items-start gap-2.5">
|
||||
<span className="mt-2 size-1.5 rounded-full bg-foreground shrink-0" />
|
||||
<span className="mt-2 size-1.5 rounded-full bg-primary shrink-0" />
|
||||
<span>
|
||||
The timer starts the moment you begin and{" "}
|
||||
<strong>cannot be paused</strong>. Your assessment auto-submits when time runs out.
|
||||
@@ -390,17 +416,17 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
</li>
|
||||
)}
|
||||
<li className="flex items-start gap-2.5">
|
||||
<span className="mt-2 size-1.5 rounded-full bg-foreground shrink-0" />
|
||||
<span>Answer each question before moving to the next. You can return to any answered question to change your answer.</span>
|
||||
<span className="mt-2 size-1.5 rounded-full bg-primary shrink-0" />
|
||||
<span>You can skip questions and return to them later. All questions must be answered before you can submit.</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2.5">
|
||||
<span className="mt-2 size-1.5 rounded-full bg-foreground shrink-0" />
|
||||
<span className="mt-2 size-1.5 rounded-full bg-primary shrink-0" />
|
||||
<span>
|
||||
Your progress is <strong>saved automatically</strong> — you can safely resume if you lose connection.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2.5">
|
||||
<span className="mt-2 size-1.5 rounded-full bg-foreground shrink-0" />
|
||||
<span className="mt-2 size-1.5 rounded-full bg-primary shrink-0" />
|
||||
<span>Review all answers on the summary screen before final submission.</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -501,6 +527,20 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
<p className="text-xs text-muted-foreground">Attempt #{result.attempt_number}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{result.passed && onNextContent && nextLabel && (
|
||||
<div
|
||||
onClick={onNextContent}
|
||||
className="flex items-center gap-3 bg-card border rounded-xl px-4 py-3 shadow-sm hover:bg-muted transition-colors text-sm font-medium cursor-pointer select-none"
|
||||
>
|
||||
<div className="flex flex-col items-start flex-1">
|
||||
<span className="text-xs text-muted-foreground font-normal">Up next</span>
|
||||
<span>{nextLabel}</span>
|
||||
</div>
|
||||
<ArrowRight className="size-4 text-muted-foreground shrink-0" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!result.passed && (
|
||||
<div className="flex justify-center">
|
||||
<Button variant="outline" onClick={handleRetake}>Retake {label}</Button>
|
||||
@@ -559,7 +599,7 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
{/* All questions grid */}
|
||||
<div className="rounded-xl border bg-card p-5 space-y-4">
|
||||
<p className="text-sm font-semibold">All questions</p>
|
||||
<div className="grid grid-cols-8 gap-2">
|
||||
<div className="grid xs:grid-cols-8 lg:grid-cols-12 gap-2">
|
||||
{questions.map((q, i) => {
|
||||
const answered = isQuestionAnswered(answers[q.question_id]);
|
||||
return (
|
||||
@@ -569,7 +609,7 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
onClick={() => { setCurrentIndex(i); setStage("taking"); }}
|
||||
className={`aspect-square rounded-lg text-sm font-medium transition-colors
|
||||
${answered
|
||||
? "bg-foreground text-background hover:opacity-80"
|
||||
? "bg-primary text-background hover:opacity-80"
|
||||
: "border-2 border-amber-400 text-amber-600 dark:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10"
|
||||
}`}
|
||||
>
|
||||
@@ -580,7 +620,7 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
</div>
|
||||
<div className="flex gap-4 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-3 rounded-sm bg-foreground inline-block" /> Answered
|
||||
<span className="size-3 rounded-sm bg-primary inline-block" /> Answered
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-3 rounded-sm border-2 border-amber-400 inline-block" /> Unanswered
|
||||
@@ -588,14 +628,21 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="outline" onClick={() => setStage("taking")} disabled={submitting}>
|
||||
<ChevronLeft className="size-4" />
|
||||
Back to assessment
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={submitting}>
|
||||
{submitting ? "Submitting…" : "Submit assessment"}
|
||||
</Button>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="outline" onClick={() => setStage("taking")} disabled={submitting}>
|
||||
<ChevronLeft className="size-4" />
|
||||
Back to assessment
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={submitting}>
|
||||
{submitting ? "Submitting…" : "Submit assessment"}
|
||||
</Button>
|
||||
</div>
|
||||
{submitError && (
|
||||
<p className="text-xs text-red-600 dark:text-red-400 text-right flex items-center justify-end gap-1">
|
||||
<AlertTriangle className="size-3 shrink-0" /> {submitError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -622,7 +669,7 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
onClick={() => { setCurrentIndex(i); setStage("taking"); }}
|
||||
className={`size-9 rounded-full text-sm font-semibold transition-colors
|
||||
${answered
|
||||
? "bg-foreground text-background hover:opacity-80"
|
||||
? "bg-primary text-background hover:opacity-80"
|
||||
: "bg-muted text-muted-foreground border border-border hover:bg-muted/80"
|
||||
}`}
|
||||
>
|
||||
@@ -670,7 +717,7 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
onClick={() => { setCurrentIndex(i); setStage("taking"); }}
|
||||
className="w-full flex items-center gap-4 rounded-xl border bg-card p-4 text-left hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<span className="shrink-0 size-8 rounded-full bg-foreground text-background text-sm font-semibold flex items-center justify-center">
|
||||
<span className="shrink-0 size-8 rounded-full bg-primary text-background text-sm font-semibold flex items-center justify-center">
|
||||
{i + 1}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -691,14 +738,21 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="outline" onClick={() => setStage("taking")} disabled={submitting}>
|
||||
<ChevronLeft className="size-4" />
|
||||
Back to questions
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={submitting}>
|
||||
{submitting ? "Submitting…" : "Submit quiz"}
|
||||
</Button>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="outline" onClick={() => setStage("taking")} disabled={submitting}>
|
||||
<ChevronLeft className="size-4" />
|
||||
Back to questions
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={submitting}>
|
||||
{submitting ? "Submitting…" : "Submit quiz"}
|
||||
</Button>
|
||||
</div>
|
||||
{submitError && (
|
||||
<p className="text-xs text-red-600 dark:text-red-400 text-right flex items-center justify-end gap-1">
|
||||
<AlertTriangle className="size-3 shrink-0" /> {submitError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -776,9 +830,100 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-5 items-start">
|
||||
{/* ── Left: question ── */}
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-4 items-start">
|
||||
{/* ── Right: sidebar — order-first on mobile so timer/progress sit above the question ── */}
|
||||
<div className="space-y-2 order-first lg:order-last">
|
||||
{/* Timer */}
|
||||
{remainingSeconds !== null && (
|
||||
<div className={`rounded-xl flex items-center justify-between gap-3 px-4 py-3 lg:block lg:p-4 ${
|
||||
timeExpired
|
||||
? "bg-red-600 text-white"
|
||||
: remainingSeconds <= 60
|
||||
? "bg-red-700 text-white"
|
||||
: "bg-primary text-background"
|
||||
}`}>
|
||||
<p className="text-xs uppercase tracking-widest opacity-60">Time Remaining</p>
|
||||
<p className="text-2xl lg:text-4xl font-bold font-mono tabular-nums lg:mt-1">
|
||||
{timeExpired ? "00:00" : formatTime(remainingSeconds)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress panel */}
|
||||
<div className="rounded-xl border bg-card p-3 lg:p-4 space-y-2 lg:space-y-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium">Progress</span>
|
||||
<span className="text-muted-foreground">{answeredCount} / {total} answered</span>
|
||||
</div>
|
||||
|
||||
{/* Number grid — scrollable after ~30 questions (3 rows on mobile, 4 on desktop) */}
|
||||
<div className="overflow-y-auto max-h-24 lg:max-h-40">
|
||||
<div className="grid grid-cols-10 lg:grid-cols-8 gap-1 lg:gap-1.5">
|
||||
{questions.map((q, i) => {
|
||||
const ans = answers[q.question_id];
|
||||
const answered = isQuestionAnswered(ans);
|
||||
const isCurrent = i === currentIndex;
|
||||
return (
|
||||
<button
|
||||
key={q.question_id}
|
||||
type="button"
|
||||
onClick={() => setCurrentIndex(i)}
|
||||
disabled={timeExpired && submitting}
|
||||
className={`aspect-square rounded text-xs font-medium transition-colors
|
||||
${isCurrent
|
||||
? "border-2 border-foreground bg-background text-foreground"
|
||||
: answered
|
||||
? "bg-primary text-background hover:opacity-80"
|
||||
: "border border-border bg-background text-muted-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend — desktop only */}
|
||||
<div className="hidden lg:flex flex-col gap-1.5 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-3 rounded-sm bg-primary inline-block shrink-0" />
|
||||
Answered
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-3 rounded-sm border border-border inline-block shrink-0" />
|
||||
Not answered
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-3 rounded-sm border-2 border-foreground inline-block shrink-0" />
|
||||
Current
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Auto-save indicator */}
|
||||
<div className="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400 rounded-lg bg-green-500/5 border border-green-500/20 px-3 py-2">
|
||||
<CheckCircle2 className="size-3.5 shrink-0" />
|
||||
All answers saved automatically
|
||||
</div>
|
||||
|
||||
{/* Review button */}
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={handleGoToReview}
|
||||
disabled={submitting}
|
||||
>
|
||||
Review & submit
|
||||
</Button>
|
||||
{reviewAttempted && unansweredCount > 0 && (
|
||||
<p className="text-xs text-red-600 dark:text-red-400 text-center">
|
||||
Answer all {total} questions first — {unansweredCount} remaining.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Left: question — order-last on mobile so it appears below the compact sidebar ── */}
|
||||
<div className="space-y-4 order-last lg:order-first">
|
||||
{/* Question header */}
|
||||
<div className="flex items-center gap-2.5 text-sm">
|
||||
<span className="text-muted-foreground">Question {currentIndex + 1} of {total}</span>
|
||||
@@ -823,102 +968,12 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
<ChevronLeft className="size-4" />
|
||||
Previous
|
||||
</Button>
|
||||
<Button onClick={handleNext} disabled={submitting || !isCurrentAnswered}>
|
||||
<Button onClick={handleNext} disabled={submitting}>
|
||||
{submitting ? "Submitting…" : isLast ? "Review & submit" : "Next"}
|
||||
{!submitting && <ChevronRight className="size-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Right: sidebar ── */}
|
||||
<div className="space-y-3">
|
||||
{/* Timer */}
|
||||
{remainingSeconds !== null && (
|
||||
<div className={`rounded-xl p-4 ${
|
||||
timeExpired
|
||||
? "bg-red-600 text-white"
|
||||
: remainingSeconds <= 60
|
||||
? "bg-red-700 text-white"
|
||||
: "bg-foreground text-background"
|
||||
}`}>
|
||||
<p className="text-xs uppercase tracking-widest opacity-60">Time Remaining</p>
|
||||
<p className="text-4xl font-bold font-mono mt-1 tabular-nums">
|
||||
{timeExpired ? "00:00" : formatTime(remainingSeconds)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress panel */}
|
||||
<div className="rounded-xl border bg-card p-4 space-y-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium">Progress</span>
|
||||
<span className="text-muted-foreground">{answeredCount} / {total} answered</span>
|
||||
</div>
|
||||
|
||||
{/* Number grid */}
|
||||
<div className="grid grid-cols-8 gap-1.5">
|
||||
{questions.map((q, i) => {
|
||||
const ans = answers[q.question_id];
|
||||
const answered = isQuestionAnswered(ans);
|
||||
const isCurrent = i === currentIndex;
|
||||
const canJump = answered || isCurrent;
|
||||
return (
|
||||
<button
|
||||
key={q.question_id}
|
||||
type="button"
|
||||
onClick={() => canJump && setCurrentIndex(i)}
|
||||
disabled={(timeExpired && submitting)}
|
||||
className={`aspect-square rounded text-xs font-medium transition-colors
|
||||
${isCurrent
|
||||
? "border-2 border-foreground bg-background text-foreground"
|
||||
: answered
|
||||
? "bg-foreground text-background hover:opacity-80"
|
||||
: "border border-border bg-background text-muted-foreground cursor-default"
|
||||
}`}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex flex-col gap-1.5 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-3 rounded-sm bg-foreground inline-block shrink-0" />
|
||||
Answered
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-3 rounded-sm border border-border inline-block shrink-0" />
|
||||
Not answered
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-3 rounded-sm border-2 border-foreground inline-block shrink-0" />
|
||||
Current
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Auto-save indicator */}
|
||||
<div className="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400 rounded-lg bg-green-500/5 border border-green-500/20 px-3 py-2">
|
||||
<CheckCircle2 className="size-3.5 shrink-0" />
|
||||
All answers saved automatically
|
||||
</div>
|
||||
|
||||
{/* Review button */}
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={handleGoToReview}
|
||||
disabled={submitting}
|
||||
>
|
||||
Review & submit
|
||||
</Button>
|
||||
{reviewAttempted && unansweredCount > 0 && (
|
||||
<p className="text-xs text-red-600 dark:text-red-400 text-center">
|
||||
Answer all {total} questions first — {unansweredCount} remaining.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -941,25 +996,24 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Question number pills */}
|
||||
{/* Question number pills — all freely clickable; skipped questions show as muted */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{questions.map((q, i) => {
|
||||
const ans = answers[q.question_id];
|
||||
const answered = isQuestionAnswered(ans);
|
||||
const isCurrent = i === currentIndex;
|
||||
const canJump = answered || isCurrent;
|
||||
const ans = answers[q.question_id];
|
||||
const answered = isQuestionAnswered(ans);
|
||||
const isCurrent = i === currentIndex;
|
||||
return (
|
||||
<button
|
||||
key={q.question_id}
|
||||
type="button"
|
||||
onClick={() => canJump && setCurrentIndex(i)}
|
||||
onClick={() => setCurrentIndex(i)}
|
||||
disabled={timeExpired && submitting}
|
||||
className={`size-9 rounded-full text-sm font-semibold transition-colors
|
||||
${isCurrent
|
||||
? "border-2 border-primary bg-background text-foreground"
|
||||
: answered
|
||||
? "bg-foreground text-background hover:opacity-80"
|
||||
: "bg-muted text-muted-foreground border border-border cursor-default"
|
||||
? "bg-primary text-background hover:opacity-80"
|
||||
: "bg-muted text-muted-foreground border border-border hover:bg-muted/60"
|
||||
}`}
|
||||
>
|
||||
{i + 1}
|
||||
@@ -1025,16 +1079,11 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
|
||||
<ChevronLeft className="size-4" />
|
||||
Previous
|
||||
</Button>
|
||||
<Button onClick={handleNext} disabled={submitting || !isCurrentAnswered}>
|
||||
<Button onClick={handleNext} disabled={submitting}>
|
||||
{submitting ? "Submitting…" : isLast ? "Review answers" : "Next"}
|
||||
{!submitting && <ChevronRight className="size-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
{reviewAttempted && unansweredCount > 0 && (
|
||||
<p className="text-xs text-red-600 dark:text-red-400 text-right">
|
||||
Answer all {total} questions first — {unansweredCount} remaining.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user