added draft for quiz and assessment unfinish
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { ArrowLeft, House, Plus, Save, ClipboardList, ChevronUp, ChevronDown, AlertTriangle } from "lucide-react";
|
import { ArrowLeft, House, Plus, Save, ClipboardList, ChevronUp, ChevronDown, AlertTriangle, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useCourses } from "@/contexts/AdminCoursesContext";
|
import { useCourses } from "@/contexts/AdminCoursesContext";
|
||||||
@@ -34,6 +34,22 @@ function snapAssessment({ title, passingScore, timeLimit, isRequired, maxQuestio
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Server row → form state ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function toFormState(a) {
|
||||||
|
return {
|
||||||
|
title: a?.title ?? "",
|
||||||
|
passingScore: a?.passing_score ?? 70,
|
||||||
|
timeLimit: a?.time_limit_minutes ?? "",
|
||||||
|
isRequired: a?.is_required === true || a?.is_required === 1,
|
||||||
|
maxQuestions: a?.max_questions ?? "",
|
||||||
|
maxAttempts: a?.max_attempts ?? 3,
|
||||||
|
cooldownHours: a?.cooldown_hours ?? 24,
|
||||||
|
shuffleQuestions: a?.shuffle_questions === true || a?.shuffle_questions === 1,
|
||||||
|
questions: (a?.questions ?? []).map((q) => ({ ...q, _tempId: q.question_id, options: q.options ?? [] })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ── Validation ────────────────────────────────────────────────────────────────
|
// ── Validation ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function validate(questions) {
|
function validate(questions) {
|
||||||
@@ -213,6 +229,9 @@ export default function CourseAssessment() {
|
|||||||
const [localAssessment, setLocalAssessment] = useState(null);
|
const [localAssessment, setLocalAssessment] = useState(null);
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const DRAFT_KEY = `draftAssessment_${courseId}`;
|
||||||
|
const [draftInfo, setDraftInfo] = useState(null); // { savedAt } — non-null while an unsaved local draft exists
|
||||||
|
|
||||||
const [initializing, setInitializing] = useState(true);
|
const [initializing, setInitializing] = useState(true);
|
||||||
const [questions, setQuestions] = useState([]);
|
const [questions, setQuestions] = useState([]);
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
@@ -260,21 +279,39 @@ export default function CourseAssessment() {
|
|||||||
// ── Seed ──────────────────────────────────────────────────────────────────
|
// ── Seed ──────────────────────────────────────────────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!localAssessment) return;
|
if (!localAssessment) return;
|
||||||
const t = localAssessment.title ?? "";
|
const s = toFormState(localAssessment);
|
||||||
const ps = localAssessment.passing_score ?? 70;
|
setTitle(s.title); setPassingScore(s.passingScore); setTimeLimit(s.timeLimit); setIsRequired(s.isRequired);
|
||||||
const tl = localAssessment.time_limit_minutes ?? "";
|
setMaxQuestions(s.maxQuestions); setMaxAttempts(s.maxAttempts); setCooldownHours(s.cooldownHours);
|
||||||
const ir = localAssessment.is_required === true || localAssessment.is_required === 1;
|
setShuffleQuestions(s.shuffleQuestions); setQuestions(s.questions);
|
||||||
const mq = localAssessment.max_questions ?? "";
|
initialSnapshot.current = snapAssessment(s);
|
||||||
const ma = localAssessment.max_attempts ?? 3;
|
originalQuestionIdsRef.current = s.questions.map((q) => q.question_id).filter(Boolean);
|
||||||
const ch = localAssessment.cooldown_hours ?? 24;
|
|
||||||
const sq = localAssessment.shuffle_questions === true || localAssessment.shuffle_questions === 1;
|
|
||||||
const qs = (localAssessment.questions ?? []).map((q) => ({ ...q, _tempId: q.question_id, options: q.options ?? [] }));
|
|
||||||
setTitle(t); setPassingScore(ps); setTimeLimit(tl); setIsRequired(ir);
|
|
||||||
setMaxQuestions(mq); setMaxAttempts(ma); setCooldownHours(ch); setShuffleQuestions(sq); setQuestions(qs);
|
|
||||||
initialSnapshot.current = snapAssessment({ title: t, passingScore: ps, timeLimit: tl, isRequired: ir, maxQuestions: mq, maxAttempts: ma, cooldownHours: ch, shuffleQuestions: sq, questions: qs });
|
|
||||||
originalQuestionIdsRef.current = qs.map((q) => q.question_id).filter(Boolean);
|
|
||||||
}, [localAssessment]);
|
}, [localAssessment]);
|
||||||
|
|
||||||
|
// ── Restore an unsaved local draft (survives a reload before Save Assessment) ─
|
||||||
|
useEffect(() => {
|
||||||
|
if (initializing) return;
|
||||||
|
const raw = localStorage.getItem(DRAFT_KEY);
|
||||||
|
if (!raw) return;
|
||||||
|
try {
|
||||||
|
const draft = JSON.parse(raw);
|
||||||
|
if (!draft?.questions) return;
|
||||||
|
setTitle(draft.title ?? "");
|
||||||
|
setPassingScore(draft.passingScore ?? 70);
|
||||||
|
setTimeLimit(draft.timeLimit ?? "");
|
||||||
|
setIsRequired(draft.isRequired ?? false);
|
||||||
|
setMaxQuestions(draft.maxQuestions ?? "");
|
||||||
|
setMaxAttempts(draft.maxAttempts ?? 3);
|
||||||
|
setCooldownHours(draft.cooldownHours ?? 24);
|
||||||
|
setShuffleQuestions(draft.shuffleQuestions ?? false);
|
||||||
|
setQuestions(draft.questions ?? []);
|
||||||
|
setDraftInfo({ savedAt: draft.savedAt });
|
||||||
|
toast("Restored an unsaved draft from this browser.");
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem(DRAFT_KEY);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [initializing]);
|
||||||
|
|
||||||
// ── Measure sticky header → --assessment-h ────────────────────────────────
|
// ── Measure sticky header → --assessment-h ────────────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!headerRef.current) return;
|
if (!headerRef.current) return;
|
||||||
@@ -384,6 +421,38 @@ export default function CourseAssessment() {
|
|||||||
? questions.length > 0
|
? questions.length > 0
|
||||||
: snapAssessment({ title, passingScore, timeLimit, isRequired, maxQuestions, maxAttempts, cooldownHours, shuffleQuestions, questions }) !== initialSnapshot.current;
|
: snapAssessment({ title, passingScore, timeLimit, isRequired, maxQuestions, maxAttempts, cooldownHours, shuffleQuestions, questions }) !== initialSnapshot.current;
|
||||||
|
|
||||||
|
// ── Auto-save unsaved changes to localStorage as a draft ───────────────────
|
||||||
|
useEffect(() => {
|
||||||
|
if (initializing) return;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (isDirty) {
|
||||||
|
const payload = { title, passingScore, timeLimit, isRequired, maxQuestions, maxAttempts, cooldownHours, shuffleQuestions, questions, savedAt: Date.now() };
|
||||||
|
try {
|
||||||
|
localStorage.setItem(DRAFT_KEY, JSON.stringify(payload));
|
||||||
|
setDraftInfo({ savedAt: payload.savedAt });
|
||||||
|
} catch {
|
||||||
|
// localStorage full/unavailable — draft persistence is best-effort only.
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(DRAFT_KEY);
|
||||||
|
setDraftInfo(null);
|
||||||
|
}
|
||||||
|
}, 400);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [initializing, isDirty, title, passingScore, timeLimit, isRequired, maxQuestions, maxAttempts, cooldownHours, shuffleQuestions, questions, DRAFT_KEY]);
|
||||||
|
|
||||||
|
// ── Clear draft — discards the local draft and reverts to the last loaded state ─
|
||||||
|
const handleClearDraft = () => {
|
||||||
|
localStorage.removeItem(DRAFT_KEY);
|
||||||
|
setDraftInfo(null);
|
||||||
|
const s = toFormState(localAssessment);
|
||||||
|
setTitle(s.title); setPassingScore(s.passingScore); setTimeLimit(s.timeLimit); setIsRequired(s.isRequired);
|
||||||
|
setMaxQuestions(s.maxQuestions); setMaxAttempts(s.maxAttempts); setCooldownHours(s.cooldownHours);
|
||||||
|
setShuffleQuestions(s.shuffleQuestions); setQuestions(s.questions);
|
||||||
|
setErrors({});
|
||||||
|
toast("Draft cleared.");
|
||||||
|
};
|
||||||
|
|
||||||
// ── Save ───────────────────────────────────────────────────────────────────
|
// ── Save ───────────────────────────────────────────────────────────────────
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const errs = validate(questions);
|
const errs = validate(questions);
|
||||||
@@ -456,6 +525,8 @@ export default function CourseAssessment() {
|
|||||||
await bulkSyncAssessmentQuestions(courseId, id, questions, user?.user_id, wipesPool);
|
await bulkSyncAssessmentQuestions(courseId, id, questions, user?.user_id, wipesPool);
|
||||||
|
|
||||||
initialSnapshot.current = snapAssessment({ title, passingScore, timeLimit, isRequired, maxQuestions, maxAttempts, cooldownHours, shuffleQuestions, questions });
|
initialSnapshot.current = snapAssessment({ title, passingScore, timeLimit, isRequired, maxQuestions, maxAttempts, cooldownHours, shuffleQuestions, questions });
|
||||||
|
localStorage.removeItem(DRAFT_KEY);
|
||||||
|
setDraftInfo(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmSave = async () => {
|
const handleConfirmSave = async () => {
|
||||||
@@ -478,7 +549,7 @@ export default function CourseAssessment() {
|
|||||||
style={{ top: "var(--navbar-h)" }}
|
style={{ top: "var(--navbar-h)" }}
|
||||||
>
|
>
|
||||||
<div className="lg:container lg:mx-auto lg:px-6 px-4">
|
<div className="lg:container lg:mx-auto lg:px-6 px-4">
|
||||||
<div className="flex items-center gap-3 py-3">
|
<div className="flex items-center gap-2 py-3">
|
||||||
<Button type="button" variant="ghost" size="icon" onClick={() => navigate("/admin/courses")}>
|
<Button type="button" variant="ghost" size="icon" onClick={() => navigate("/admin/courses")}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -491,6 +562,22 @@ export default function CourseAssessment() {
|
|||||||
{questions.length} question{questions.length !== 1 ? "s" : ""} · {totalPoints} total point{totalPoints !== 1 ? "s" : ""}
|
{questions.length} question{questions.length !== 1 ? "s" : ""} · {totalPoints} total point{totalPoints !== 1 ? "s" : ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{draftInfo && (
|
||||||
|
<div className="hidden sm:flex items-center gap-3 text-xs text-muted-foreground">
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-sm">
|
||||||
|
<span className="size-2 rounded-full bg-amber-500" />
|
||||||
|
Draft saved {new Date(draftInfo.savedAt).toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleClearDraft}
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
Clear draft
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Button onClick={handleSave} disabled={loading || confirmLoading || !isDirty}>
|
<Button onClick={handleSave} disabled={loading || confirmLoading || !isDirty}>
|
||||||
{(loading || confirmLoading) ? <Spinner className="h-4 w-4 mr-2" /> : <Save className="h-4 w-4 mr-2" />}
|
{(loading || confirmLoading) ? <Spinner className="h-4 w-4 mr-2" /> : <Save className="h-4 w-4 mr-2" />}
|
||||||
Save Assessment
|
Save Assessment
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { ArrowLeft, House, Plus, Save, HelpCircle, ChevronUp, ChevronDown } from "lucide-react";
|
import { ArrowLeft, House, Plus, Save, HelpCircle, ChevronUp, ChevronDown, Trash2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { useCourses } from "@/contexts/AdminCoursesContext";
|
import { useCourses } from "@/contexts/AdminCoursesContext";
|
||||||
@@ -49,6 +49,19 @@ function snapQuiz({ title, passingScore, isRequired, maxQuestions, shuffleQuesti
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Server row → form state ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function toFormState(q) {
|
||||||
|
return {
|
||||||
|
title: q?.title ?? "",
|
||||||
|
passingScore: q?.passing_score ?? 70,
|
||||||
|
isRequired: q?.is_required === true || q?.is_required === 1,
|
||||||
|
maxQuestions: q?.max_questions ?? "",
|
||||||
|
shuffleQuestions: q?.shuffle_questions === true || q?.shuffle_questions === 1,
|
||||||
|
questions: (q?.questions ?? []).map((qq) => ({ ...qq, _tempId: qq.question_id, options: qq.options ?? [] })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ── Jump to input ─────────────────────────────────────────────────────────────
|
// ── Jump to input ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function JumpToInput({ max, onJump }) {
|
function JumpToInput({ max, onJump }) {
|
||||||
@@ -206,6 +219,9 @@ export default function ModifyQuiz() {
|
|||||||
const [localQuiz, setLocalQuiz] = useState(null);
|
const [localQuiz, setLocalQuiz] = useState(null);
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const DRAFT_KEY = `draftQuiz_${courseId ?? "lib"}_${unitId}`;
|
||||||
|
const [draftInfo, setDraftInfo] = useState(null); // { savedAt } — non-null while an unsaved local draft exists
|
||||||
|
|
||||||
const [initializing, setInitializing] = useState(true);
|
const [initializing, setInitializing] = useState(true);
|
||||||
const [questions, setQuestions] = useState([]);
|
const [questions, setQuestions] = useState([]);
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
@@ -266,17 +282,35 @@ export default function ModifyQuiz() {
|
|||||||
// ── Seed form from fetched quiz ────────────────────────────────────────────
|
// ── Seed form from fetched quiz ────────────────────────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!localQuiz) return;
|
if (!localQuiz) return;
|
||||||
const t = localQuiz.title ?? "";
|
const s = toFormState(localQuiz);
|
||||||
const ps = localQuiz.passing_score ?? 70;
|
setTitle(s.title); setPassingScore(s.passingScore); setIsRequired(s.isRequired);
|
||||||
const ir = localQuiz.is_required === true || localQuiz.is_required === 1;
|
setMaxQuestions(s.maxQuestions); setShuffleQuestions(s.shuffleQuestions); setQuestions(s.questions);
|
||||||
const mq = localQuiz.max_questions ?? "";
|
initialSnapshot.current = snapQuiz(s);
|
||||||
const sq = localQuiz.shuffle_questions === true || localQuiz.shuffle_questions === 1;
|
originalQuestionIdsRef.current = s.questions.map((q) => q.question_id).filter(Boolean);
|
||||||
const qs = (localQuiz.questions ?? []).map((q) => ({ ...q, _tempId: q.question_id, options: q.options ?? [] }));
|
|
||||||
setTitle(t); setPassingScore(ps); setIsRequired(ir); setMaxQuestions(mq); setShuffleQuestions(sq); setQuestions(qs);
|
|
||||||
initialSnapshot.current = snapQuiz({ title: t, passingScore: ps, isRequired: ir, maxQuestions: mq, shuffleQuestions: sq, questions: qs });
|
|
||||||
originalQuestionIdsRef.current = qs.map((q) => q.question_id).filter(Boolean);
|
|
||||||
}, [localQuiz]);
|
}, [localQuiz]);
|
||||||
|
|
||||||
|
// ── Restore an unsaved local draft (survives a reload before Save Quiz) ────
|
||||||
|
useEffect(() => {
|
||||||
|
if (initializing) return;
|
||||||
|
const raw = localStorage.getItem(DRAFT_KEY);
|
||||||
|
if (!raw) return;
|
||||||
|
try {
|
||||||
|
const draft = JSON.parse(raw);
|
||||||
|
if (!draft?.questions) return;
|
||||||
|
setTitle(draft.title ?? "");
|
||||||
|
setPassingScore(draft.passingScore ?? 70);
|
||||||
|
setIsRequired(draft.isRequired ?? false);
|
||||||
|
setMaxQuestions(draft.maxQuestions ?? "");
|
||||||
|
setShuffleQuestions(draft.shuffleQuestions ?? false);
|
||||||
|
setQuestions(draft.questions ?? []);
|
||||||
|
setDraftInfo({ savedAt: draft.savedAt });
|
||||||
|
toast("Restored an unsaved draft from this browser.");
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem(DRAFT_KEY);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [initializing]);
|
||||||
|
|
||||||
// ── Measure sticky header → --quiz-h ──────────────────────────────────────
|
// ── Measure sticky header → --quiz-h ──────────────────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!headerRef.current) return;
|
if (!headerRef.current) return;
|
||||||
@@ -386,6 +420,37 @@ export default function ModifyQuiz() {
|
|||||||
? questions.length > 0 // new quiz — enable once they've added a question
|
? questions.length > 0 // new quiz — enable once they've added a question
|
||||||
: snapQuiz({ title, passingScore, isRequired, maxQuestions, shuffleQuestions, questions }) !== initialSnapshot.current;
|
: snapQuiz({ title, passingScore, isRequired, maxQuestions, shuffleQuestions, questions }) !== initialSnapshot.current;
|
||||||
|
|
||||||
|
// ── Auto-save unsaved changes to localStorage as a draft ───────────────────
|
||||||
|
useEffect(() => {
|
||||||
|
if (initializing) return;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (isDirty) {
|
||||||
|
const payload = { title, passingScore, isRequired, maxQuestions, shuffleQuestions, questions, savedAt: Date.now() };
|
||||||
|
try {
|
||||||
|
localStorage.setItem(DRAFT_KEY, JSON.stringify(payload));
|
||||||
|
setDraftInfo({ savedAt: payload.savedAt });
|
||||||
|
} catch {
|
||||||
|
// localStorage full/unavailable — draft persistence is best-effort only.
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(DRAFT_KEY);
|
||||||
|
setDraftInfo(null);
|
||||||
|
}
|
||||||
|
}, 400);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [initializing, isDirty, title, passingScore, isRequired, maxQuestions, shuffleQuestions, questions, DRAFT_KEY]);
|
||||||
|
|
||||||
|
// ── Clear draft — discards the local draft and reverts to the last loaded state ─
|
||||||
|
const handleClearDraft = () => {
|
||||||
|
localStorage.removeItem(DRAFT_KEY);
|
||||||
|
setDraftInfo(null);
|
||||||
|
const s = toFormState(localQuiz);
|
||||||
|
setTitle(s.title); setPassingScore(s.passingScore); setIsRequired(s.isRequired);
|
||||||
|
setMaxQuestions(s.maxQuestions); setShuffleQuestions(s.shuffleQuestions); setQuestions(s.questions);
|
||||||
|
setErrors({});
|
||||||
|
toast("Draft cleared.");
|
||||||
|
};
|
||||||
|
|
||||||
// ── Save ───────────────────────────────────────────────────────────────────
|
// ── Save ───────────────────────────────────────────────────────────────────
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const errs = validate(questions);
|
const errs = validate(questions);
|
||||||
@@ -432,6 +497,8 @@ export default function ModifyQuiz() {
|
|||||||
await bulkSyncQuizQuestions(courseId, unitId, quizId, questions, user?.user_id, wipesPool);
|
await bulkSyncQuizQuestions(courseId, unitId, quizId, questions, user?.user_id, wipesPool);
|
||||||
|
|
||||||
initialSnapshot.current = snapQuiz({ title, passingScore, isRequired, maxQuestions, shuffleQuestions, questions });
|
initialSnapshot.current = snapQuiz({ title, passingScore, isRequired, maxQuestions, shuffleQuestions, questions });
|
||||||
|
localStorage.removeItem(DRAFT_KEY);
|
||||||
|
setDraftInfo(null);
|
||||||
navigate(`${scopeBase}/view`);
|
navigate(`${scopeBase}/view`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -460,6 +527,24 @@ export default function ModifyQuiz() {
|
|||||||
{questions.length} question{questions.length !== 1 ? "s" : ""} · {totalPoints} total point{totalPoints !== 1 ? "s" : ""}
|
{questions.length} question{questions.length !== 1 ? "s" : ""} · {totalPoints} total point{totalPoints !== 1 ? "s" : ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{draftInfo && (
|
||||||
|
<div className="hidden sm:flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-amber-500" />
|
||||||
|
Draft saved {new Date(draftInfo.savedAt).toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-2 text-xs text-muted-foreground hover:text-destructive"
|
||||||
|
onClick={handleClearDraft}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5 mr-1" />
|
||||||
|
Clear draft
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Button onClick={handleSave} disabled={loading || !isDirty}>
|
<Button onClick={handleSave} disabled={loading || !isDirty}>
|
||||||
{loading ? <Spinner className="h-4 w-4 mr-2" /> : <Save className="h-4 w-4 mr-2" />}
|
{loading ? <Spinner className="h-4 w-4 mr-2" /> : <Save className="h-4 w-4 mr-2" />}
|
||||||
Save Quiz
|
Save Quiz
|
||||||
|
|||||||
Reference in New Issue
Block a user