From 93c3c688caf8d8658db37f5b7fd31a3c685f0cf2 Mon Sep 17 00:00:00 2001 From: Kenneth Obsequio Date: Wed, 24 Jun 2026 12:49:20 +0800 Subject: [PATCH] perform test #1 test to courses Signed-off-by: Kenneth Obsequio --- src/contexts/AdminCoursesContext.jsx | 45 ++ src/contexts/ClientCoursesContext.jsx | 30 +- src/contexts/ClientNotificationContext.jsx | 28 +- .../components/courses/QuestionEditor.jsx | 19 +- .../admin/pages/courses/CourseAssessment.jsx | 188 ++++++-- .../admin/pages/courses/EditCourse.jsx | 20 +- .../admin/pages/courses/ViewAssessment.jsx | 327 ++++++++++++-- .../admin/pages/courses/units/UnitQuiz.jsx | 54 ++- .../pages/courses/units/ViewUnitQuiz.jsx | 230 ++++++++-- .../admin/pages/task_list/ViewTaskList.jsx | 2 +- .../admin/pages/task_list/task/CreateTask.jsx | 2 +- .../admin/pages/task_list/task/ViewTask.jsx | 2 +- src/modules/auth/pages/Intro.jsx | 268 ++++++++++++ .../client/components/blocks/QuizBlock.jsx | 409 ++++++++++++++---- .../client/components/blocks/ReadCourse.jsx | 189 +++++--- .../client/components/blocks/ReadLesson.jsx | 161 ++++++- .../client/components/blocks/ReadUnit.jsx | 173 ++++++-- .../client/components/blocks/VisitLink.jsx | 9 +- src/modules/client/pages/CourseDetails.jsx | 180 ++++++-- src/modules/client/pages/CourseList.jsx | 23 +- src/modules/client/pages/MyCertificates.jsx | 16 +- src/modules/client/pages/Profile.jsx | 16 +- src/modules/client/pages/UnitList.jsx | 36 +- src/modules/client/pages/ViewRequirement.jsx | 137 ++++-- src/modules/client/routes/ClientRoutes.jsx | 25 +- src/routes/PublicRoute.jsx | 4 +- 26 files changed, 2141 insertions(+), 452 deletions(-) create mode 100644 src/modules/auth/pages/Intro.jsx diff --git a/src/contexts/AdminCoursesContext.jsx b/src/contexts/AdminCoursesContext.jsx index 589536d..5acb00d 100644 --- a/src/contexts/AdminCoursesContext.jsx +++ b/src/contexts/AdminCoursesContext.jsx @@ -39,6 +39,8 @@ export function CoursesProvider({ children }) { const [quiz, setQuiz] = useState(null); const [questions, setQuestions] = useState([]); const [assessment, setAssessment] = useState(null); + const [completions, setCompletions] = useState(null); // { summary, completions[] } + const [sessions, setSessions] = useState(null); // { summary, sessions[] } const [attributes, setAttributes] = useState([]); const [pagination, setPagination] = useState(PAGINATION_INIT); const [loading, setLoading] = useState(false); @@ -847,6 +849,43 @@ export function CoursesProvider({ children }) { [request], ); + // ========================================================================= + // COMPLETIONS & SESSIONS + // ========================================================================= + + const fetchQuizCompletions = useCallback( + (courseId, unitId, quizId) => + request(async () => { + setCompletions(null); + const { data } = await api.get(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/completions`); + setCompletions(data?.data ?? null); + return data; + }), + [request], + ); + + const fetchAssessmentCompletions = useCallback( + (courseId, assessmentId) => + request(async () => { + setCompletions(null); + const { data } = await api.get(`${BASE}/${courseId}/assessment/${assessmentId}/completions`); + setCompletions(data?.data ?? null); + return data; + }), + [request], + ); + + const fetchAssessmentSessions = useCallback( + (courseId, assessmentId) => + request(async () => { + setSessions(null); + const { data } = await api.get(`${BASE}/${courseId}/assessment/${assessmentId}/sessions`); + setSessions(data?.data ?? null); + return data; + }), + [request], + ); + // ========================================================================= // COURSE PRODUCT & CATEGORIES // ========================================================================= @@ -1032,6 +1071,12 @@ export function CoursesProvider({ children }) { fetchLessonPage, saveLessonPage, + // ── completions & sessions ───────────────────────────────────────────── + completions, sessions, + fetchQuizCompletions, + fetchAssessmentCompletions, + fetchAssessmentSessions, + // ── assessment ───────────────────────────────────────────────────────── fetchAssessment, createAssessment, diff --git a/src/contexts/ClientCoursesContext.jsx b/src/contexts/ClientCoursesContext.jsx index 7f8bc67..04ab443 100644 --- a/src/contexts/ClientCoursesContext.jsx +++ b/src/contexts/ClientCoursesContext.jsx @@ -130,11 +130,34 @@ export function ClientCoursesProvider({ children }) { } }, []); - const submitCourseAssessment = useCallback(async (courseId, assessmentId, answers) => { + const startCourseAssessment = useCallback(async (courseId, assessmentId) => { + try { + const { data } = await api.post(`/client/courses/${courseId}/assessment/${assessmentId}/start`); + return data.data ?? null; + } catch (err) { + toast.error(err?.response?.data?.message ?? "Could not start assessment."); + return null; + } + }, []); + + const saveDraft = useCallback(async (courseId, assessmentId, answers) => { + try { + await api.patch(`/client/courses/${courseId}/assessment/${assessmentId}/draft`, { answers }); + } catch { /* silent — draft saves are best-effort */ } + }, []); + + const refreshAssessmentSession = useCallback(async (courseId, assessmentId) => { + try { + const { data } = await api.get(`/client/courses/${courseId}/assessment/${assessmentId}/session`); + return data.data ?? null; + } catch { return null; } + }, []); + + const submitCourseAssessment = useCallback(async (courseId, assessmentId, answers, sessionId) => { try { const { data } = await api.post( `/client/courses/${courseId}/assessment/${assessmentId}/submit`, - { answers } + { answers, ...(sessionId ? { session_id: sessionId } : {}) } ); return data.data ?? null; } catch (err) { @@ -214,6 +237,9 @@ export function ClientCoursesProvider({ children }) { getLesson, getUnitQuiz, getCourseAssessment, + startCourseAssessment, + saveDraft, + refreshAssessmentSession, submitUnitQuiz, submitCourseAssessment, diff --git a/src/contexts/ClientNotificationContext.jsx b/src/contexts/ClientNotificationContext.jsx index f3b2ce7..e7ae9bd 100644 --- a/src/contexts/ClientNotificationContext.jsx +++ b/src/contexts/ClientNotificationContext.jsx @@ -3,7 +3,8 @@ import api from "@/utils/api.util"; const ClientNotificationContext = createContext(null); -const POLL_INTERVAL = 60_000; +const POLL_INTERVAL_NORMAL = 60_000; +const POLL_INTERVAL_FAST = 10_000; export function useClientNotifications() { const ctx = useContext(ClientNotificationContext); @@ -15,7 +16,8 @@ export function ClientNotificationProvider({ children }) { const [notifications, setNotifications] = useState([]); const [unseenCount, setUnseenCount] = useState(0); const [loading, setLoading] = useState(false); - const intervalRef = useRef(null); + const intervalRef = useRef(null); + const pollSpeedRef = useRef(POLL_INTERVAL_NORMAL); const fetchUnseen = useCallback(async () => { try { @@ -62,9 +64,27 @@ export function ClientNotificationProvider({ children }) { } }, []); + const restartPoll = useCallback((interval) => { + clearInterval(intervalRef.current); + intervalRef.current = setInterval(fetchUnseen, interval); + }, [fetchUnseen]); + + // Speed up polling while a student is mid-assessment; restore when done + const accelerate = useCallback(() => { + if (pollSpeedRef.current === POLL_INTERVAL_FAST) return; + pollSpeedRef.current = POLL_INTERVAL_FAST; + restartPoll(POLL_INTERVAL_FAST); + }, [restartPoll]); + + const decelerate = useCallback(() => { + if (pollSpeedRef.current === POLL_INTERVAL_NORMAL) return; + pollSpeedRef.current = POLL_INTERVAL_NORMAL; + restartPoll(POLL_INTERVAL_NORMAL); + }, [restartPoll]); + useEffect(() => { fetchUnseen(); - intervalRef.current = setInterval(fetchUnseen, POLL_INTERVAL); + intervalRef.current = setInterval(fetchUnseen, POLL_INTERVAL_NORMAL); return () => clearInterval(intervalRef.current); }, [fetchUnseen]); @@ -76,6 +96,8 @@ export function ClientNotificationProvider({ children }) { fetchNotifications, markSeen, markAllSeen, + accelerate, + decelerate, }}> {children} diff --git a/src/modules/admin/components/courses/QuestionEditor.jsx b/src/modules/admin/components/courses/QuestionEditor.jsx index b39fc3f..c8dca7b 100644 --- a/src/modules/admin/components/courses/QuestionEditor.jsx +++ b/src/modules/admin/components/courses/QuestionEditor.jsx @@ -178,12 +178,19 @@ export function QuestionCard({ question, index, onChange, onRemove, error }) { {/* Options */}
- +
+ + {question.type === "multi_select" && correctCount > 0 && ( +

+ Students must select {correctCount} answer{correctCount !== 1 ? "s" : ""} +

+ )} +
{question.type !== "true_false" && (
-
@@ -491,6 +555,28 @@ export default function CourseAssessment() { placeholder={`All (${questions.length})`} /> +
+ + setMaxAttempts(e.target.value)} + /> +
+
+ + setCooldownHours(e.target.value)} + /> +
@@ -569,6 +655,58 @@ export default function CourseAssessment() { )}
+ + {/* ── Update confirmation dialog ── */} + {confirmOpen && ( +
+
+
+
+ +
+
+

Save Assessment Changes?

+

+ Changes will take effect immediately for all students. +

+
+
+ + {inProgressCount > 0 && ( +
+ +

+ {inProgressCount} student{inProgressCount !== 1 ? "s are" : " is"} currently taking this assessment. + Saving now will affect their ongoing session. +

+
+ )} + +
    +
  • • New passing score applies to future submissions only.
  • +
  • • Changing the time limit does not affect already-started sessions.
  • +
  • • Adding or removing questions affects any student not yet on that question.
  • +
+ +
+ + +
+
+
+ )} ); } \ No newline at end of file diff --git a/src/modules/admin/pages/courses/EditCourse.jsx b/src/modules/admin/pages/courses/EditCourse.jsx index 6f798bf..39a10bb 100644 --- a/src/modules/admin/pages/courses/EditCourse.jsx +++ b/src/modules/admin/pages/courses/EditCourse.jsx @@ -56,21 +56,17 @@ const schema = z.object({ // ─── Helpers ────────────────────────────────────────────────────────────────── const CertBadgeIcon = ({ className }) => ( - - - - {/* short top bar */} - - {/* longer middle bar */} - - {/* gold bottom bar */} - + - - - + + + + + + + ); diff --git a/src/modules/admin/pages/courses/ViewAssessment.jsx b/src/modules/admin/pages/courses/ViewAssessment.jsx index 072a54a..bee4e63 100644 --- a/src/modules/admin/pages/courses/ViewAssessment.jsx +++ b/src/modules/admin/pages/courses/ViewAssessment.jsx @@ -1,6 +1,10 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { ArrowLeft, ClipboardList, NotebookPen, CheckCircle2, Circle } from "lucide-react"; +import { + ArrowLeft, ClipboardList, NotebookPen, + CheckCircle2, Circle, Users, Activity, + ChevronDown, ChevronUp, +} from "lucide-react"; import { useCourses } from "@/contexts/AdminCoursesContext"; import { PageMeta } from "@/contexts/MetadataContext"; @@ -8,7 +12,6 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; -import { Spinner } from "@/components/ui/spinner"; // ─── Helpers ────────────────────────────────────────────────────────────────── @@ -24,21 +27,32 @@ function InfoRow({ label, children }) { function SectionCard({ title, children }) { return (
- {title && ( - <> -

{title}

- - - )} + {title && (<>

{title}

)} {children}
); } +function StatCard({ label, value, color = "default" }) { + const colors = { + default: "bg-card border", + green: "bg-green-500/5 border-green-500/20", + red: "bg-red-500/5 border-red-500/20", + blue: "bg-blue-500/5 border-blue-500/20", + amber: "bg-amber-500/5 border-amber-500/20", + }; + return ( +
+

{value ?? 0}

+

{label}

+
+ ); +} + const TYPE_LABELS = { multiple_choice: "Multiple Choice", - multi_select: "Multi Select", - true_false: "True / False", + multi_select: "Multi Select", + true_false: "True / False", }; function QuestionView({ question, index }) { @@ -54,15 +68,20 @@ function QuestionView({ question, index }) {

- - {TYPE_LABELS[question.type] ?? question.type} - + {TYPE_LABELS[question.type] ?? question.type} {question.points ?? 1} pt{(question.points ?? 1) !== 1 ? "s" : ""}
- + {question.type === "multi_select" && (() => { + const cnt = (question.options ?? []).filter((o) => o.is_correct).length; + return cnt > 0 ? ( +

+ Students select {cnt} answer{cnt !== 1 ? "s" : ""} +

+ ) : null; + })()}