From 3f12a6142203ab1e651c308f6e2ee92d2b7c21f6 Mon Sep 17 00:00:00 2001 From: rgrgogu Date: Mon, 20 Jul 2026 03:56:36 +0800 Subject: [PATCH] added draft for quiz and assessment unfinish --- .../admin/pages/courses/CourseAssessment.jsx | 117 +++++++++++++++--- .../admin/pages/courses/units/ModifyQuiz.jsx | 105 ++++++++++++++-- 2 files changed, 197 insertions(+), 25 deletions(-) diff --git a/src/modules/admin/pages/courses/CourseAssessment.jsx b/src/modules/admin/pages/courses/CourseAssessment.jsx index dfd4b71..af844c5 100644 --- a/src/modules/admin/pages/courses/CourseAssessment.jsx +++ b/src/modules/admin/pages/courses/CourseAssessment.jsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from "react"; 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 { 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 ──────────────────────────────────────────────────────────────── function validate(questions) { @@ -213,6 +229,9 @@ export default function CourseAssessment() { const [localAssessment, setLocalAssessment] = useState(null); 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 [questions, setQuestions] = useState([]); const [errors, setErrors] = useState({}); @@ -260,21 +279,39 @@ export default function CourseAssessment() { // ── Seed ────────────────────────────────────────────────────────────────── useEffect(() => { if (!localAssessment) return; - const t = localAssessment.title ?? ""; - const ps = localAssessment.passing_score ?? 70; - const tl = localAssessment.time_limit_minutes ?? ""; - const ir = localAssessment.is_required === true || localAssessment.is_required === 1; - const mq = localAssessment.max_questions ?? ""; - const ma = localAssessment.max_attempts ?? 3; - 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); + 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); + initialSnapshot.current = snapAssessment(s); + originalQuestionIdsRef.current = s.questions.map((q) => q.question_id).filter(Boolean); }, [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 ──────────────────────────────── useEffect(() => { if (!headerRef.current) return; @@ -384,6 +421,38 @@ export default function CourseAssessment() { ? questions.length > 0 : 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 ─────────────────────────────────────────────────────────────────── const handleSave = async () => { const errs = validate(questions); @@ -456,6 +525,8 @@ export default function CourseAssessment() { await bulkSyncAssessmentQuestions(courseId, id, questions, user?.user_id, wipesPool); initialSnapshot.current = snapAssessment({ title, passingScore, timeLimit, isRequired, maxQuestions, maxAttempts, cooldownHours, shuffleQuestions, questions }); + localStorage.removeItem(DRAFT_KEY); + setDraftInfo(null); }; const handleConfirmSave = async () => { @@ -478,7 +549,7 @@ export default function CourseAssessment() { style={{ top: "var(--navbar-h)" }} >
-
+
@@ -491,6 +562,22 @@ export default function CourseAssessment() { {questions.length} question{questions.length !== 1 ? "s" : ""} · {totalPoints} total point{totalPoints !== 1 ? "s" : ""}

+ {draftInfo && ( +
+ + + Draft saved {new Date(draftInfo.savedAt).toLocaleTimeString()} + + +
+ )}
+ {draftInfo && ( +
+ + + Draft saved {new Date(draftInfo.savedAt).toLocaleTimeString()} + + +
+ )}