@hydralms/components 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/StudentProfile-BVfZMbnV.cjs +1 -0
- package/dist/StudentProfile-DeMxdrL3.js +3275 -0
- package/dist/assessment-toolbar/assessment-toolbar.d.ts +1 -1
- package/dist/assessment-toolbar/index.d.ts +5 -1
- package/dist/assessment-toolbar/question-header-bar.d.ts +2 -0
- package/dist/assessment-toolbar/question-materials-drawer.d.ts +2 -0
- package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
- package/dist/assessment-toolbar/timer-display.d.ts +1 -1
- package/dist/assessment-toolbar/types.d.ts +52 -4
- package/dist/assessment-toolbar/use-countdown.d.ts +43 -0
- package/dist/common/index.d.ts +3 -1
- package/dist/common/pagination.d.ts +26 -0
- package/dist/common/stepper.d.ts +6 -0
- package/dist/common/types.d.ts +38 -0
- package/dist/components.css +1 -1
- package/dist/content/attachment-list.d.ts +6 -0
- package/dist/content/audio-player.d.ts +22 -0
- package/dist/content/code-block.d.ts +30 -0
- package/dist/content/content-block.d.ts +1 -1
- package/dist/content/embed-block.d.ts +28 -0
- package/dist/content/index.d.ts +8 -1
- package/dist/content/types.d.ts +63 -0
- package/dist/curriculum/course-card.d.ts +51 -0
- package/dist/curriculum/curriculum-item.d.ts +1 -1
- package/dist/curriculum/index.d.ts +2 -0
- package/dist/curriculum/types.d.ts +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +597 -308
- package/dist/license/HydraContext.d.ts +16 -0
- package/dist/license/ProBadge.d.ts +6 -0
- package/dist/license/index.d.ts +7 -0
- package/dist/license/tiers.d.ts +3 -0
- package/dist/license/useHydraLicense.d.ts +6 -0
- package/dist/license/validate.d.ts +13 -0
- package/dist/license/withProGate.d.ts +6 -0
- package/dist/modules/AssignmentModule/AssignmentModule.d.ts +5 -0
- package/dist/modules/AssignmentModule/types.d.ts +69 -0
- package/dist/modules/CertificateModule/CertificateModule.d.ts +5 -0
- package/dist/modules/CertificateModule/types.d.ts +51 -0
- package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
- package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
- package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
- package/dist/modules/DiscussionModule/DiscussionModule.d.ts +5 -0
- package/dist/modules/DiscussionModule/types.d.ts +47 -0
- package/dist/modules/ExamModule/ExamModule.d.ts +5 -0
- package/dist/modules/ExamModule/types.d.ts +55 -0
- package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
- package/dist/modules/FlashcardLab/types.d.ts +2 -0
- package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +5 -0
- package/dist/modules/GradeCenterModule/types.d.ts +56 -0
- package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
- package/dist/modules/QuizModule/types.d.ts +10 -14
- package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
- package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
- package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
- package/dist/modules/StudentProfileModule/types.d.ts +43 -0
- package/dist/modules/SurveyModule/SurveyModule.d.ts +5 -0
- package/dist/modules/SurveyModule/types.d.ts +51 -0
- package/dist/modules/_shared/assessment-intro.d.ts +16 -0
- package/dist/modules/_shared/assessment-results.d.ts +23 -0
- package/dist/modules/_shared/types.d.ts +10 -0
- package/dist/modules/_shared/use-timer.d.ts +9 -0
- package/dist/modules/index.d.ts +18 -0
- package/dist/modules.cjs +1 -0
- package/dist/modules.js +1834 -0
- package/dist/progress/achievement-badge.d.ts +6 -0
- package/dist/progress/activity-timeline.d.ts +6 -0
- package/dist/progress/index.d.ts +4 -1
- package/dist/progress/stat-card.d.ts +1 -1
- package/dist/progress/streak-badge.d.ts +6 -0
- package/dist/progress/types.d.ts +99 -0
- package/dist/provider/HydraProvider.d.ts +5 -1
- package/dist/questions/choice.d.ts +1 -1
- package/dist/questions/confidence-indicator.d.ts +37 -0
- package/dist/questions/essay.d.ts +2 -2
- package/dist/questions/fill-in-the-blank.d.ts +1 -1
- package/dist/questions/hotspot.d.ts +21 -0
- package/dist/questions/index.d.ts +11 -1
- package/dist/questions/inline-choice.d.ts +21 -0
- package/dist/questions/matching.d.ts +22 -0
- package/dist/questions/multiple-choice.d.ts +1 -1
- package/dist/questions/numeric.d.ts +11 -0
- package/dist/questions/ordering.d.ts +12 -0
- package/dist/questions/question-renderer.d.ts +1 -1
- package/dist/questions/scenario.d.ts +23 -0
- package/dist/questions/scoring.d.ts +22 -0
- package/dist/questions/spreadsheet.d.ts +29 -0
- package/dist/questions/true-false.d.ts +1 -1
- package/dist/questions/types.d.ts +106 -1
- package/dist/questions/use-drag-reorder.d.ts +17 -0
- package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
- package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
- package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
- package/dist/sections/AssessmentReview/types.d.ts +6 -0
- package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
- package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
- package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
- package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
- package/dist/sections/CertificateViewer/types.d.ts +13 -5
- package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
- package/dist/sections/CourseCatalog/types.d.ts +80 -0
- package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
- package/dist/sections/CourseOutline/types.d.ts +6 -0
- package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
- package/dist/sections/DiscussionThread/types.d.ts +6 -0
- package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
- package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
- package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
- package/dist/sections/ExamSession/types.d.ts +12 -1
- package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
- package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
- package/dist/sections/ForumBoard/ForumBoard.d.ts +8 -0
- package/dist/sections/ForumBoard/types.d.ts +78 -0
- package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
- package/dist/sections/GradebookTable/types.d.ts +14 -0
- package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
- package/dist/sections/LecturePlayer/types.d.ts +8 -0
- package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
- package/dist/sections/LessonPage/types.d.ts +6 -0
- package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
- package/dist/sections/PracticeQuiz/types.d.ts +6 -0
- package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
- package/dist/sections/ProgressDashboard/types.d.ts +6 -0
- package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
- package/dist/sections/QuizSession/types.d.ts +12 -1
- package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
- package/dist/sections/RequirementsChecklist/types.d.ts +43 -0
- package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
- package/dist/sections/ResourceLibrary/types.d.ts +15 -1
- package/dist/sections/RubricView/RubricView.d.ts +9 -0
- package/dist/sections/RubricView/types.d.ts +56 -0
- package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
- package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
- package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
- package/dist/sections/StudentProfile/types.d.ts +98 -0
- package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
- package/dist/sections/SurveyForm/types.d.ts +6 -0
- package/dist/sections/_shared/merge-answers.d.ts +9 -0
- package/dist/sections/_shared/section-shell.d.ts +20 -0
- package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
- package/dist/sections/index.d.ts +13 -1
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +282 -1786
- package/dist/social/post-card.d.ts +1 -1
- package/dist/tabs-BsfVo2Bl.cjs +173 -0
- package/dist/tabs-BuY1iNJE.js +22305 -0
- package/dist/ui/alert.d.ts +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/button.d.ts +1 -1
- package/dist/ui/drawer.d.ts +84 -0
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/progress.d.ts +1 -1
- package/dist/ui/rich-text-editor.d.ts +32 -0
- package/dist/ui/rich-text-toolbar.d.ts +8 -0
- package/dist/ui/toast.d.ts +43 -0
- package/dist/utils/array-utils.d.ts +4 -0
- package/dist/utils/debounce.d.ts +5 -1
- package/dist/utils/flatten-leaves.d.ts +6 -0
- package/dist/utils/format-file-size.d.ts +1 -0
- package/dist/utils/format-timestamp.d.ts +1 -0
- package/dist/utils/is-empty-html.d.ts +5 -0
- package/dist/utils/pick-palette-color.d.ts +19 -0
- package/dist/utils/shuffle.d.ts +1 -0
- package/dist/utils/string-utils.d.ts +12 -0
- package/dist/video/types.d.ts +15 -0
- package/dist/video/video-bookmark.d.ts +1 -1
- package/dist/video/video-player.d.ts +1 -1
- package/dist/video/video-playlist-item.d.ts +1 -1
- package/dist/withProGate-BWqcKdPM.js +137 -0
- package/dist/withProGate-DX6XqKLp.cjs +1 -0
- package/package.json +40 -137
- package/src/assessment-toolbar/assessment-toolbar.tsx +54 -49
- package/src/assessment-toolbar/index.ts +6 -0
- package/src/assessment-toolbar/question-header-bar.tsx +61 -0
- package/src/assessment-toolbar/question-materials-drawer.tsx +55 -0
- package/src/assessment-toolbar/question-navigator.tsx +13 -36
- package/src/assessment-toolbar/timer-display.tsx +6 -5
- package/src/assessment-toolbar/types.ts +54 -4
- package/src/assessment-toolbar/use-countdown.ts +153 -0
- package/src/common/empty-state.tsx +1 -0
- package/src/common/index.ts +5 -0
- package/src/common/pagination.tsx +135 -0
- package/src/common/search-input.tsx +8 -6
- package/src/common/stepper.tsx +100 -0
- package/src/common/types.ts +41 -0
- package/src/content/attachment-list.tsx +92 -0
- package/src/content/audio-player.tsx +196 -0
- package/src/content/code-block.tsx +113 -0
- package/src/content/content-block.tsx +68 -2
- package/src/content/embed-block.tsx +78 -0
- package/src/content/file-upload-zone.tsx +11 -6
- package/src/content/index.ts +9 -0
- package/src/content/types.ts +46 -0
- package/src/curriculum/course-card.tsx +199 -0
- package/src/curriculum/curriculum-item.tsx +9 -5
- package/src/curriculum/curriculum-tree.tsx +20 -13
- package/src/curriculum/index.ts +2 -0
- package/src/curriculum/types.ts +2 -2
- package/src/feedback/feedback-banner.tsx +12 -14
- package/src/flashcards/flashcard-deck.tsx +1 -9
- package/src/flashcards/flashcard.tsx +29 -9
- package/src/index.ts +3 -0
- package/src/license/HydraContext.tsx +62 -0
- package/src/license/ProBadge.tsx +43 -0
- package/src/license/index.ts +7 -0
- package/src/license/tiers.ts +24 -0
- package/src/license/useHydraLicense.ts +10 -0
- package/src/license/validate.ts +90 -0
- package/src/license/withProGate.tsx +21 -0
- package/src/modules/AssignmentModule/AssignmentModule.tsx +314 -0
- package/src/modules/AssignmentModule/types.ts +77 -0
- package/src/modules/CertificateModule/CertificateModule.tsx +173 -0
- package/src/modules/CertificateModule/types.ts +49 -0
- package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
- package/src/modules/CourseCatalogModule/types.ts +47 -0
- package/src/modules/CoursePlayer/CoursePlayer.tsx +80 -69
- package/src/modules/DiscussionModule/DiscussionModule.tsx +145 -0
- package/src/modules/DiscussionModule/types.ts +54 -0
- package/src/modules/ExamModule/ExamModule.tsx +151 -0
- package/src/modules/ExamModule/types.ts +57 -0
- package/src/modules/FlashcardLab/FlashcardLab.tsx +39 -21
- package/src/modules/FlashcardLab/types.ts +2 -0
- package/src/modules/GradeCenterModule/GradeCenterModule.tsx +174 -0
- package/src/modules/GradeCenterModule/types.ts +65 -0
- package/src/modules/QuizModule/QuizModule.tsx +58 -178
- package/src/modules/QuizModule/types.ts +10 -15
- package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
- package/src/modules/StudentDashboardModule/types.ts +56 -0
- package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
- package/src/modules/StudentProfileModule/types.ts +45 -0
- package/src/modules/SurveyModule/SurveyModule.tsx +185 -0
- package/src/modules/SurveyModule/types.ts +53 -0
- package/src/modules/_shared/assessment-intro.tsx +75 -0
- package/src/modules/_shared/assessment-results.tsx +133 -0
- package/src/modules/_shared/types.ts +11 -0
- package/src/modules/_shared/use-timer.ts +49 -0
- package/src/modules/index.ts +33 -0
- package/src/progress/achievement-badge.tsx +52 -0
- package/src/progress/activity-timeline.tsx +84 -0
- package/src/progress/grade-indicator.tsx +9 -1
- package/src/progress/index.ts +7 -0
- package/src/progress/progress-ring.tsx +2 -1
- package/src/progress/stat-card.tsx +37 -18
- package/src/progress/streak-badge.tsx +35 -0
- package/src/progress/types.ts +103 -0
- package/src/provider/HydraProvider.tsx +15 -6
- package/src/questions/choice.tsx +19 -14
- package/src/questions/confidence-indicator.tsx +107 -0
- package/src/questions/essay.tsx +28 -28
- package/src/questions/fill-in-the-blank.tsx +20 -19
- package/src/questions/hotspot.tsx +154 -0
- package/src/questions/index.ts +18 -0
- package/src/questions/inline-choice.tsx +152 -0
- package/src/questions/matching.tsx +229 -0
- package/src/questions/multiple-choice.tsx +19 -14
- package/src/questions/numeric.tsx +106 -0
- package/src/questions/ordering.tsx +167 -0
- package/src/questions/question-renderer.tsx +24 -2
- package/src/questions/scenario.tsx +140 -0
- package/src/questions/scoring.ts +201 -0
- package/src/questions/spreadsheet.tsx +260 -0
- package/src/questions/true-false.tsx +19 -14
- package/src/questions/types.ts +123 -1
- package/src/questions/use-drag-reorder.ts +80 -0
- package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +66 -23
- package/src/sections/AnnouncementFeed/types.ts +15 -1
- package/src/sections/AssessmentReview/AssessmentReview.tsx +50 -2
- package/src/sections/AssessmentReview/types.ts +6 -0
- package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +44 -6
- package/src/sections/AssignmentSubmission/types.ts +6 -0
- package/src/sections/CertificateViewer/CertificateViewer.tsx +215 -60
- package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
- package/src/sections/CertificateViewer/types.ts +19 -5
- package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
- package/src/sections/CourseCatalog/types.ts +76 -0
- package/src/sections/CourseOutline/CourseOutline.tsx +45 -14
- package/src/sections/CourseOutline/types.ts +6 -0
- package/src/sections/DiscussionThread/DiscussionThread.tsx +55 -11
- package/src/sections/DiscussionThread/types.ts +6 -0
- package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
- package/src/sections/EnrollmentWizard/types.ts +65 -0
- package/src/sections/ExamSession/ExamSession.tsx +125 -82
- package/src/sections/ExamSession/types.ts +12 -1
- package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
- package/src/sections/FlashcardStudySession/types.ts +6 -0
- package/src/sections/ForumBoard/ForumBoard.tsx +342 -0
- package/src/sections/ForumBoard/types.ts +81 -0
- package/src/sections/GradebookTable/GradebookTable.tsx +55 -2
- package/src/sections/GradebookTable/types.ts +14 -0
- package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
- package/src/sections/LecturePlayer/types.ts +8 -0
- package/src/sections/LessonPage/LessonPage.tsx +40 -13
- package/src/sections/LessonPage/types.ts +6 -0
- package/src/sections/PracticeQuiz/PracticeQuiz.tsx +119 -98
- package/src/sections/PracticeQuiz/types.ts +6 -0
- package/src/sections/ProgressDashboard/ProgressDashboard.tsx +121 -67
- package/src/sections/ProgressDashboard/types.ts +6 -0
- package/src/sections/QuizSession/QuizSession.tsx +115 -67
- package/src/sections/QuizSession/types.ts +12 -1
- package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +147 -0
- package/src/sections/RequirementsChecklist/types.ts +44 -0
- package/src/sections/ResourceLibrary/ResourceLibrary.tsx +68 -17
- package/src/sections/ResourceLibrary/types.ts +15 -1
- package/src/sections/RubricView/RubricView.tsx +174 -0
- package/src/sections/RubricView/types.ts +58 -0
- package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +58 -23
- package/src/sections/ScrollableQuiz/types.ts +6 -0
- package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
- package/src/sections/StudentProfile/types.ts +99 -0
- package/src/sections/SurveyForm/SurveyForm.tsx +40 -10
- package/src/sections/SurveyForm/types.ts +6 -0
- package/src/sections/_shared/merge-answers.ts +22 -0
- package/src/sections/_shared/section-shell.tsx +64 -0
- package/src/sections/_shared/use-assessment-session.ts +125 -0
- package/src/sections/index.ts +42 -1
- package/src/social/post-card.tsx +8 -19
- package/src/social/user-avatar.tsx +10 -5
- package/src/styles/globals.css +52 -41
- package/src/ui/badge.tsx +8 -0
- package/src/ui/drawer.tsx +600 -0
- package/src/ui/index.ts +21 -0
- package/src/ui/progress.tsx +4 -0
- package/src/ui/rich-text-editor.tsx +119 -0
- package/src/ui/rich-text-toolbar.tsx +157 -0
- package/src/ui/toast.tsx +170 -0
- package/src/utils/array-utils.ts +17 -0
- package/src/utils/debounce.ts +8 -2
- package/src/utils/flatten-leaves.ts +17 -0
- package/src/utils/format-file-size.ts +5 -0
- package/src/utils/format-timestamp.ts +13 -0
- package/src/utils/is-empty-html.ts +7 -0
- package/src/utils/pick-palette-color.ts +33 -0
- package/src/utils/shuffle.ts +8 -0
- package/src/utils/string-utils.ts +30 -0
- package/src/video/types.ts +16 -0
- package/src/video/video-bookmark.tsx +4 -3
- package/src/video/video-chapter-list.tsx +9 -4
- package/src/video/video-player.tsx +24 -5
- package/src/video/video-playlist-item.tsx +8 -3
- package/src/video/video-thumbnail-card.tsx +4 -0
- package/src/video/video-transcript.tsx +8 -5
- package/dist/table-BrS5cDQu.js +0 -2510
- package/dist/table-D6AkBBEo.cjs +0 -1
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { useState, useMemo } from "react";
|
|
2
|
+
import {
|
|
3
|
+
CheckCircle2,
|
|
4
|
+
XCircle,
|
|
5
|
+
AlertCircle,
|
|
6
|
+
Clock,
|
|
7
|
+
BookOpen,
|
|
8
|
+
Loader2,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import { Stepper } from "../../common/stepper";
|
|
11
|
+
import { Card, CardContent } from "../../ui/card";
|
|
12
|
+
import { Button } from "../../ui/button";
|
|
13
|
+
import { Badge } from "../../ui/badge";
|
|
14
|
+
import { Separator } from "../../ui/separator";
|
|
15
|
+
import { UserAvatar } from "../../social/user-avatar";
|
|
16
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
17
|
+
import { EmptyState } from "../../common/empty-state";
|
|
18
|
+
import { cn } from "../../lib/utils";
|
|
19
|
+
import type { EnrollmentWizardProps } from "./types";
|
|
20
|
+
|
|
21
|
+
type Step = "details" | "prerequisites" | "confirmation";
|
|
22
|
+
|
|
23
|
+
export function EnrollmentWizard({
|
|
24
|
+
course,
|
|
25
|
+
prerequisites,
|
|
26
|
+
onEnroll,
|
|
27
|
+
onCancel,
|
|
28
|
+
enrollLabel = "Enroll Now",
|
|
29
|
+
isEnrolling = false,
|
|
30
|
+
isLoading,
|
|
31
|
+
error,
|
|
32
|
+
onRetry,
|
|
33
|
+
className,
|
|
34
|
+
style,
|
|
35
|
+
}: EnrollmentWizardProps) {
|
|
36
|
+
const hasPrerequisites =
|
|
37
|
+
Array.isArray(prerequisites) && prerequisites.length > 0;
|
|
38
|
+
|
|
39
|
+
const [step, setStep] = useState<Step>("details");
|
|
40
|
+
|
|
41
|
+
/* ------------------------------------------------------------------ */
|
|
42
|
+
/* Stepper setup */
|
|
43
|
+
/* ------------------------------------------------------------------ */
|
|
44
|
+
const allSteps = useMemo<{ key: Step; label: string }[]>(
|
|
45
|
+
() =>
|
|
46
|
+
hasPrerequisites
|
|
47
|
+
? [
|
|
48
|
+
{ key: "details", label: "Course Details" },
|
|
49
|
+
{ key: "prerequisites", label: "Prerequisites" },
|
|
50
|
+
{ key: "confirmation", label: "Confirmation" },
|
|
51
|
+
]
|
|
52
|
+
: [
|
|
53
|
+
{ key: "details", label: "Course Details" },
|
|
54
|
+
{ key: "confirmation", label: "Confirmation" },
|
|
55
|
+
],
|
|
56
|
+
[hasPrerequisites],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
/* ------------------------------------------------------------------ */
|
|
60
|
+
/* Loading early return */
|
|
61
|
+
/* ------------------------------------------------------------------ */
|
|
62
|
+
if (isLoading) {
|
|
63
|
+
return (
|
|
64
|
+
<div className={cn("space-y-4", className)} style={style}>
|
|
65
|
+
<Skeleton className="h-8 w-48" />
|
|
66
|
+
<Skeleton className="h-48 w-full" />
|
|
67
|
+
<Skeleton className="h-10 w-32" />
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ------------------------------------------------------------------ */
|
|
73
|
+
/* Error early return */
|
|
74
|
+
/* ------------------------------------------------------------------ */
|
|
75
|
+
if (error) {
|
|
76
|
+
return (
|
|
77
|
+
<div className={cn("py-12", className)} style={style}>
|
|
78
|
+
<EmptyState
|
|
79
|
+
icon={<AlertCircle className="size-10 text-destructive" />}
|
|
80
|
+
title="Something went wrong"
|
|
81
|
+
description={error}
|
|
82
|
+
action={
|
|
83
|
+
onRetry ? (
|
|
84
|
+
<Button variant="outline" onClick={onRetry}>
|
|
85
|
+
Retry
|
|
86
|
+
</Button>
|
|
87
|
+
) : undefined
|
|
88
|
+
}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const currentStepIndex = allSteps.findIndex((s) => s.key === step);
|
|
95
|
+
|
|
96
|
+
const allPrerequisitesMet =
|
|
97
|
+
!hasPrerequisites || prerequisites!.every((p) => p.isMet);
|
|
98
|
+
|
|
99
|
+
/* ------------------------------------------------------------------ */
|
|
100
|
+
/* Navigation helpers */
|
|
101
|
+
/* ------------------------------------------------------------------ */
|
|
102
|
+
function goNext() {
|
|
103
|
+
const nextIndex = currentStepIndex + 1;
|
|
104
|
+
if (nextIndex < allSteps.length) {
|
|
105
|
+
setStep(allSteps[nextIndex].key);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function goBack() {
|
|
110
|
+
const prevIndex = currentStepIndex - 1;
|
|
111
|
+
if (prevIndex >= 0) {
|
|
112
|
+
setStep(allSteps[prevIndex].key);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ------------------------------------------------------------------ */
|
|
117
|
+
/* Details step */
|
|
118
|
+
/* ------------------------------------------------------------------ */
|
|
119
|
+
function renderDetails() {
|
|
120
|
+
return (
|
|
121
|
+
<Card>
|
|
122
|
+
<CardContent className="pt-6">
|
|
123
|
+
{/* Thumbnail or initial placeholder */}
|
|
124
|
+
{course.thumbnailUrl ? (
|
|
125
|
+
<img
|
|
126
|
+
src={course.thumbnailUrl}
|
|
127
|
+
alt={course.title}
|
|
128
|
+
className="w-full h-48 object-cover rounded-lg mb-4"
|
|
129
|
+
/>
|
|
130
|
+
) : (
|
|
131
|
+
<div className="w-full h-48 rounded-lg mb-4 bg-muted flex items-center justify-center">
|
|
132
|
+
<BookOpen className="size-12 text-muted-foreground" />
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{/* Title */}
|
|
137
|
+
<h2 className="text-xl font-bold text-foreground mb-1">
|
|
138
|
+
{course.title}
|
|
139
|
+
</h2>
|
|
140
|
+
|
|
141
|
+
{/* Description */}
|
|
142
|
+
{course.description && (
|
|
143
|
+
<p className="text-sm text-muted-foreground mb-3">
|
|
144
|
+
{course.description}
|
|
145
|
+
</p>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{/* Instructor + duration row */}
|
|
149
|
+
<div className="flex items-center gap-3 mb-3">
|
|
150
|
+
{course.instructor && (
|
|
151
|
+
<div className="flex items-center gap-2">
|
|
152
|
+
<UserAvatar
|
|
153
|
+
displayName={course.instructor.displayName}
|
|
154
|
+
avatarUrl={course.instructor.avatarUrl}
|
|
155
|
+
size="small"
|
|
156
|
+
/>
|
|
157
|
+
<span className="text-sm text-foreground">
|
|
158
|
+
{course.instructor.displayName}
|
|
159
|
+
</span>
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
{course.duration && (
|
|
163
|
+
<Badge variant="muted">
|
|
164
|
+
<Clock className="size-3 mr-1" />
|
|
165
|
+
{course.duration}
|
|
166
|
+
</Badge>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Syllabus */}
|
|
171
|
+
{course.syllabus && course.syllabus.length > 0 && (
|
|
172
|
+
<>
|
|
173
|
+
<Separator className="my-3" />
|
|
174
|
+
<div className="mb-1 text-sm font-semibold text-foreground">
|
|
175
|
+
Syllabus
|
|
176
|
+
</div>
|
|
177
|
+
<ul className="list-disc list-inside space-y-1">
|
|
178
|
+
{course.syllabus.map((item, i) => (
|
|
179
|
+
<li key={i} className="text-sm text-muted-foreground">
|
|
180
|
+
{item}
|
|
181
|
+
</li>
|
|
182
|
+
))}
|
|
183
|
+
</ul>
|
|
184
|
+
</>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{/* Actions */}
|
|
188
|
+
<Separator className="my-4" />
|
|
189
|
+
<div className="flex justify-end gap-2">
|
|
190
|
+
{onCancel && (
|
|
191
|
+
<Button variant="outline" onClick={onCancel}>
|
|
192
|
+
Cancel
|
|
193
|
+
</Button>
|
|
194
|
+
)}
|
|
195
|
+
<Button onClick={goNext}>Continue</Button>
|
|
196
|
+
</div>
|
|
197
|
+
</CardContent>
|
|
198
|
+
</Card>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* ------------------------------------------------------------------ */
|
|
203
|
+
/* Prerequisites step */
|
|
204
|
+
/* ------------------------------------------------------------------ */
|
|
205
|
+
function renderPrerequisites() {
|
|
206
|
+
return (
|
|
207
|
+
<Card>
|
|
208
|
+
<CardContent className="pt-6">
|
|
209
|
+
<h3 className="text-lg font-semibold text-foreground mb-3">
|
|
210
|
+
Prerequisites
|
|
211
|
+
</h3>
|
|
212
|
+
|
|
213
|
+
<ul className="space-y-3">
|
|
214
|
+
{prerequisites!.map((prereq) => (
|
|
215
|
+
<li key={prereq.uid} className="flex items-start gap-2">
|
|
216
|
+
{prereq.isMet ? (
|
|
217
|
+
<CheckCircle2 className="size-4 text-success shrink-0 mt-0.5" />
|
|
218
|
+
) : (
|
|
219
|
+
<XCircle className="size-4 text-destructive shrink-0 mt-0.5" />
|
|
220
|
+
)}
|
|
221
|
+
<div>
|
|
222
|
+
<span className="text-sm font-medium text-foreground">
|
|
223
|
+
{prereq.label}
|
|
224
|
+
</span>
|
|
225
|
+
{prereq.description && (
|
|
226
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
227
|
+
{prereq.description}
|
|
228
|
+
</p>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
</li>
|
|
232
|
+
))}
|
|
233
|
+
</ul>
|
|
234
|
+
|
|
235
|
+
<Separator className="my-4" />
|
|
236
|
+
<div className="flex justify-end gap-2">
|
|
237
|
+
<Button variant="outline" onClick={goBack}>
|
|
238
|
+
Back
|
|
239
|
+
</Button>
|
|
240
|
+
<Button onClick={goNext} disabled={!allPrerequisitesMet}>
|
|
241
|
+
Continue
|
|
242
|
+
</Button>
|
|
243
|
+
</div>
|
|
244
|
+
</CardContent>
|
|
245
|
+
</Card>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* ------------------------------------------------------------------ */
|
|
250
|
+
/* Confirmation step */
|
|
251
|
+
/* ------------------------------------------------------------------ */
|
|
252
|
+
function renderConfirmation() {
|
|
253
|
+
return (
|
|
254
|
+
<Card>
|
|
255
|
+
<CardContent className="pt-6">
|
|
256
|
+
<h3 className="text-lg font-semibold text-foreground mb-3">
|
|
257
|
+
Confirm Enrollment
|
|
258
|
+
</h3>
|
|
259
|
+
|
|
260
|
+
{/* Summary */}
|
|
261
|
+
<div className="space-y-2 mb-3">
|
|
262
|
+
<div className="flex items-center gap-2">
|
|
263
|
+
<BookOpen className="size-4 text-muted-foreground" />
|
|
264
|
+
<span className="text-sm font-medium text-foreground">
|
|
265
|
+
{course.title}
|
|
266
|
+
</span>
|
|
267
|
+
</div>
|
|
268
|
+
{course.instructor && (
|
|
269
|
+
<div className="flex items-center gap-2">
|
|
270
|
+
<UserAvatar
|
|
271
|
+
displayName={course.instructor.displayName}
|
|
272
|
+
avatarUrl={course.instructor.avatarUrl}
|
|
273
|
+
size="small"
|
|
274
|
+
/>
|
|
275
|
+
<span className="text-sm text-muted-foreground">
|
|
276
|
+
{course.instructor.displayName}
|
|
277
|
+
</span>
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
{course.duration && (
|
|
281
|
+
<div className="flex items-center gap-2">
|
|
282
|
+
<Clock className="size-4 text-muted-foreground" />
|
|
283
|
+
<span className="text-sm text-muted-foreground">
|
|
284
|
+
{course.duration}
|
|
285
|
+
</span>
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{/* Prerequisites badge */}
|
|
291
|
+
{hasPrerequisites && (
|
|
292
|
+
<div className="mb-3">
|
|
293
|
+
{allPrerequisitesMet ? (
|
|
294
|
+
<Badge variant="success">
|
|
295
|
+
<CheckCircle2 className="size-3 mr-1" />
|
|
296
|
+
All prerequisites met
|
|
297
|
+
</Badge>
|
|
298
|
+
) : (
|
|
299
|
+
<Badge variant="warning">
|
|
300
|
+
<XCircle className="size-3 mr-1" />
|
|
301
|
+
Some prerequisites not met
|
|
302
|
+
</Badge>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
<Separator className="my-4" />
|
|
308
|
+
<div className="flex justify-end gap-2">
|
|
309
|
+
<Button variant="outline" onClick={goBack}>
|
|
310
|
+
Back
|
|
311
|
+
</Button>
|
|
312
|
+
<Button
|
|
313
|
+
onClick={() => onEnroll(course.uid)}
|
|
314
|
+
disabled={isEnrolling}
|
|
315
|
+
>
|
|
316
|
+
{isEnrolling && (
|
|
317
|
+
<Loader2 className="size-4 mr-1 animate-spin" />
|
|
318
|
+
)}
|
|
319
|
+
{enrollLabel}
|
|
320
|
+
</Button>
|
|
321
|
+
</div>
|
|
322
|
+
</CardContent>
|
|
323
|
+
</Card>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* ------------------------------------------------------------------ */
|
|
328
|
+
/* Render */
|
|
329
|
+
/* ------------------------------------------------------------------ */
|
|
330
|
+
return (
|
|
331
|
+
<div className={className} style={style}>
|
|
332
|
+
<Stepper
|
|
333
|
+
steps={allSteps.map((s) => ({ label: s.label }))}
|
|
334
|
+
currentStep={currentStepIndex}
|
|
335
|
+
className="mb-6"
|
|
336
|
+
/>
|
|
337
|
+
|
|
338
|
+
{step === "details" && renderDetails()}
|
|
339
|
+
{step === "prerequisites" && renderPrerequisites()}
|
|
340
|
+
{step === "confirmation" && renderConfirmation()}
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EnrollmentWizard section — a multi-step course enrollment flow.
|
|
3
|
+
*
|
|
4
|
+
* Guides users through course details, prerequisite checks, and enrollment
|
|
5
|
+
* confirmation with visual step progress.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <EnrollmentWizard
|
|
9
|
+
* course={{ uid: "c1", title: "React 101", description: "Learn React" }}
|
|
10
|
+
* prerequisites={[{ uid: "p1", label: "JavaScript Basics", isMet: true }]}
|
|
11
|
+
* onEnroll={(courseUid) => enroll(courseUid)}
|
|
12
|
+
* />
|
|
13
|
+
*/
|
|
14
|
+
export interface EnrollmentWizardProps {
|
|
15
|
+
/** Course information to display */
|
|
16
|
+
course: EnrollmentCourse;
|
|
17
|
+
/** Prerequisites for enrollment */
|
|
18
|
+
prerequisites?: Prerequisite[];
|
|
19
|
+
/** Called when the user confirms enrollment */
|
|
20
|
+
onEnroll: (courseUid: string) => void;
|
|
21
|
+
/** Called when the user cancels the wizard */
|
|
22
|
+
onCancel?: () => void;
|
|
23
|
+
/** Label for the enroll button */
|
|
24
|
+
enrollLabel?: string;
|
|
25
|
+
/** Whether enrollment is in progress (shows spinner) */
|
|
26
|
+
isEnrolling?: boolean;
|
|
27
|
+
/** Render skeleton placeholders instead of content */
|
|
28
|
+
isLoading?: boolean;
|
|
29
|
+
/** Error message — renders an error state with optional retry */
|
|
30
|
+
error?: string | null;
|
|
31
|
+
/** Called when the user clicks retry in the error state */
|
|
32
|
+
onRetry?: () => void;
|
|
33
|
+
/** CSS class name for the root element */
|
|
34
|
+
className?: string;
|
|
35
|
+
/** Inline styles for the root element */
|
|
36
|
+
style?: React.CSSProperties;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface EnrollmentCourse {
|
|
40
|
+
/** Course UID */
|
|
41
|
+
uid: string;
|
|
42
|
+
/** Course title */
|
|
43
|
+
title: string;
|
|
44
|
+
/** Course description */
|
|
45
|
+
description?: string;
|
|
46
|
+
/** Thumbnail image URL */
|
|
47
|
+
thumbnailUrl?: string;
|
|
48
|
+
/** Instructor info */
|
|
49
|
+
instructor?: { displayName: string; avatarUrl?: string };
|
|
50
|
+
/** Estimated duration */
|
|
51
|
+
duration?: string;
|
|
52
|
+
/** Syllabus bullet points */
|
|
53
|
+
syllabus?: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface Prerequisite {
|
|
57
|
+
/** Unique identifier */
|
|
58
|
+
uid: string;
|
|
59
|
+
/** Prerequisite label */
|
|
60
|
+
label: string;
|
|
61
|
+
/** Whether this prerequisite has been met */
|
|
62
|
+
isMet: boolean;
|
|
63
|
+
/** Optional description */
|
|
64
|
+
description?: string;
|
|
65
|
+
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { AssessmentToolbar } from "../../assessment-toolbar";
|
|
3
|
-
import type { QuestionNavigatorItem } from "../../assessment-toolbar/types";
|
|
4
|
-
import { QuestionRenderer } from "../../questions";
|
|
5
|
-
import type { SessionAnswer } from "../../questions/types";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { AssessmentToolbar, QuestionHeaderBar, QuestionMaterialsDrawer } from "../../assessment-toolbar";
|
|
6
3
|
import { ConfirmDialog } from "../../common";
|
|
4
|
+
import { QuestionRenderer } from "../../questions";
|
|
7
5
|
import { Alert, AlertDescription } from "../../ui/alert";
|
|
8
|
-
import { Card, CardContent } from "../../ui/card";
|
|
9
|
-
import {
|
|
6
|
+
import { Card, CardHeader, CardContent } from "../../ui/card";
|
|
7
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
8
|
+
|
|
9
|
+
import { useAssessmentSession } from "../_shared/use-assessment-session";
|
|
10
|
+
import { SectionShell } from "../_shared/section-shell";
|
|
10
11
|
import type { ExamSessionProps, ExamSubmitMetadata } from "./types";
|
|
11
12
|
|
|
12
13
|
export function ExamSession({
|
|
13
|
-
questions,
|
|
14
|
+
questions = [],
|
|
14
15
|
initialAnswers = [],
|
|
15
16
|
onSubmit,
|
|
16
17
|
onAnswerChange,
|
|
@@ -22,21 +23,67 @@ export function ExamSession({
|
|
|
22
23
|
confirmBeforeSubmit = true,
|
|
23
24
|
examTitle,
|
|
24
25
|
instructions,
|
|
26
|
+
questionMaterials,
|
|
25
27
|
isSubmitting = false,
|
|
26
28
|
readOnly = false,
|
|
29
|
+
isLoading,
|
|
30
|
+
error,
|
|
31
|
+
onRetry,
|
|
27
32
|
className,
|
|
28
33
|
style,
|
|
29
34
|
}: ExamSessionProps) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
const {
|
|
36
|
+
currentIndex,
|
|
37
|
+
currentQuestion,
|
|
38
|
+
sessionAnswers,
|
|
39
|
+
flaggedUids,
|
|
40
|
+
materialsOpen,
|
|
41
|
+
setMaterialsOpen,
|
|
42
|
+
questionAreaRef,
|
|
43
|
+
currentQuestionAnswers,
|
|
44
|
+
currentMaterials,
|
|
45
|
+
navigatorItems,
|
|
46
|
+
handleAnswer,
|
|
47
|
+
handleNavigate,
|
|
48
|
+
handleToggleFlag,
|
|
49
|
+
goNext,
|
|
50
|
+
goPrevious,
|
|
51
|
+
hasNext,
|
|
52
|
+
} = useAssessmentSession({ questions, initialAnswers, onAnswerChange, questionMaterials });
|
|
53
|
+
|
|
33
54
|
const [showConfirm, setShowConfirm] = useState(false);
|
|
34
55
|
const [showTimeWarning, setShowTimeWarning] = useState(false);
|
|
35
56
|
const hasAutoSubmitted = useRef(false);
|
|
36
57
|
|
|
37
|
-
|
|
58
|
+
// Refs for stable callbacks
|
|
59
|
+
const sessionAnswersRef = useRef(sessionAnswers);
|
|
60
|
+
sessionAnswersRef.current = sessionAnswers;
|
|
61
|
+
const onSubmitRef = useRef(onSubmit);
|
|
62
|
+
onSubmitRef.current = onSubmit;
|
|
63
|
+
const timeElapsedSecondsRef = useRef(timeElapsedSeconds);
|
|
64
|
+
timeElapsedSecondsRef.current = timeElapsedSeconds;
|
|
65
|
+
const currentQuestionUidRef = useRef(currentQuestion?.uid);
|
|
66
|
+
currentQuestionUidRef.current = currentQuestion?.uid;
|
|
67
|
+
|
|
38
68
|
const remainingSeconds = timeLimitSeconds - timeElapsedSeconds;
|
|
39
69
|
|
|
70
|
+
const answeredCount = useMemo(
|
|
71
|
+
() => navigatorItems.filter((q) => q.isAnswered).length,
|
|
72
|
+
[navigatorItems],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const doSubmit = useCallback((wasAutoSubmitted: boolean) => {
|
|
76
|
+
const answers = sessionAnswersRef.current;
|
|
77
|
+
const answeredUids = new Set(answers.map((a) => a.uid));
|
|
78
|
+
const metadata: ExamSubmitMetadata = {
|
|
79
|
+
timeElapsedSeconds: timeElapsedSecondsRef.current,
|
|
80
|
+
wasAutoSubmitted,
|
|
81
|
+
answeredCount: questions.filter((q) => answeredUids.has(q.uid)).length,
|
|
82
|
+
totalQuestions: questions.length,
|
|
83
|
+
};
|
|
84
|
+
onSubmitRef.current(answers, metadata);
|
|
85
|
+
}, [questions]);
|
|
86
|
+
|
|
40
87
|
useEffect(() => {
|
|
41
88
|
if (remainingSeconds <= timeWarningThreshold && remainingSeconds > 0) {
|
|
42
89
|
setShowTimeWarning(true);
|
|
@@ -48,72 +95,50 @@ export function ExamSession({
|
|
|
48
95
|
hasAutoSubmitted.current = true;
|
|
49
96
|
doSubmit(true);
|
|
50
97
|
}
|
|
51
|
-
|
|
52
|
-
}, [remainingSeconds, autoSubmitOnTimeout]);
|
|
53
|
-
|
|
54
|
-
const navigatorItems = useMemo<QuestionNavigatorItem[]>(
|
|
55
|
-
() =>
|
|
56
|
-
questions.map((q, idx) => ({
|
|
57
|
-
uid: q.uid,
|
|
58
|
-
sequence: idx + 1,
|
|
59
|
-
isFlagged: flaggedUids.has(q.uid),
|
|
60
|
-
isAnswered: sessionAnswers.some((a) => a.uid === q.uid),
|
|
61
|
-
isSkipped: false,
|
|
62
|
-
})),
|
|
63
|
-
[questions, sessionAnswers, flaggedUids],
|
|
64
|
-
);
|
|
98
|
+
}, [remainingSeconds, autoSubmitOnTimeout, doSubmit]);
|
|
65
99
|
|
|
66
|
-
|
|
67
|
-
if (!currentQuestion) return;
|
|
68
|
-
const questionUid = currentQuestion.uid;
|
|
69
|
-
const newAnswers: SessionAnswer[] = rawAnswers.map((a) => ({
|
|
70
|
-
uid: questionUid,
|
|
71
|
-
answerUid: a.uid,
|
|
72
|
-
content: a.content,
|
|
73
|
-
}));
|
|
74
|
-
setSessionAnswers((prev) => {
|
|
75
|
-
const filtered = prev.filter((a) => a.uid !== questionUid);
|
|
76
|
-
const merged = [...filtered, ...newAnswers];
|
|
77
|
-
onAnswerChange?.(merged);
|
|
78
|
-
return merged;
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function handleNavigate(uid: string) {
|
|
83
|
-
const idx = questions.findIndex((q) => q.uid === uid);
|
|
84
|
-
if (idx !== -1) setCurrentIndex(idx);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function handleToggleFlag(uid: string) {
|
|
88
|
-
setFlaggedUids((prev) => {
|
|
89
|
-
const next = new Set(prev);
|
|
90
|
-
if (next.has(uid)) next.delete(uid);
|
|
91
|
-
else next.add(uid);
|
|
92
|
-
return next;
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function handleSubmitClick() {
|
|
100
|
+
const handleSubmitClick = useCallback(() => {
|
|
97
101
|
if (confirmBeforeSubmit) {
|
|
98
102
|
setShowConfirm(true);
|
|
99
103
|
} else {
|
|
100
104
|
doSubmit(false);
|
|
101
105
|
}
|
|
102
|
-
}
|
|
106
|
+
}, [confirmBeforeSubmit, doSubmit]);
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
const handlePrevious = useCallback(() => {
|
|
109
|
+
if (allowBackNavigation) goPrevious();
|
|
110
|
+
}, [allowBackNavigation, goPrevious]);
|
|
111
|
+
|
|
112
|
+
const toggleCurrentFlag = useCallback(() => {
|
|
113
|
+
const uid = currentQuestionUidRef.current;
|
|
114
|
+
if (uid) handleToggleFlag(uid);
|
|
115
|
+
}, [handleToggleFlag]);
|
|
116
|
+
|
|
117
|
+
const openMaterials = useCallback(() => setMaterialsOpen(true), [setMaterialsOpen]);
|
|
118
|
+
|
|
119
|
+
const confirmSubmit = useCallback(() => {
|
|
120
|
+
setShowConfirm(false);
|
|
121
|
+
doSubmit(false);
|
|
122
|
+
}, [doSubmit]);
|
|
123
|
+
|
|
124
|
+
const cancelConfirm = useCallback(() => setShowConfirm(false), []);
|
|
114
125
|
|
|
115
126
|
return (
|
|
116
|
-
<
|
|
127
|
+
<SectionShell
|
|
128
|
+
isLoading={isLoading}
|
|
129
|
+
error={error}
|
|
130
|
+
onRetry={onRetry}
|
|
131
|
+
className={className}
|
|
132
|
+
style={style}
|
|
133
|
+
skeleton={
|
|
134
|
+
<>
|
|
135
|
+
<Skeleton className="h-10 w-full" />
|
|
136
|
+
<Skeleton className="h-48 w-full" />
|
|
137
|
+
<Skeleton className="h-12 w-full" />
|
|
138
|
+
</>
|
|
139
|
+
}
|
|
140
|
+
>
|
|
141
|
+
<div>
|
|
117
142
|
{examTitle && (
|
|
118
143
|
<p className="text-xl font-bold mb-2 text-foreground">{examTitle}</p>
|
|
119
144
|
)}
|
|
@@ -129,16 +154,15 @@ export function ExamSession({
|
|
|
129
154
|
<AssessmentToolbar
|
|
130
155
|
currentQuestionIndex={currentIndex}
|
|
131
156
|
totalQuestions={questions.length}
|
|
132
|
-
hasNext={
|
|
157
|
+
hasNext={hasNext}
|
|
133
158
|
hasPrevious={allowBackNavigation && currentIndex > 0}
|
|
134
|
-
onNext={
|
|
135
|
-
onPrevious={
|
|
159
|
+
onNext={goNext}
|
|
160
|
+
onPrevious={handlePrevious}
|
|
136
161
|
onSubmit={handleSubmitClick}
|
|
137
162
|
timeElapsedSeconds={timeElapsedSeconds}
|
|
138
163
|
timeLimitSeconds={timeLimitSeconds}
|
|
139
164
|
questions={navigatorItems}
|
|
140
165
|
onNavigateToQuestion={handleNavigate}
|
|
141
|
-
onToggleFlag={handleToggleFlag}
|
|
142
166
|
currentQuestionUid={currentQuestion?.uid}
|
|
143
167
|
isSubmitting={isSubmitting}
|
|
144
168
|
readOnly={readOnly}
|
|
@@ -150,12 +174,26 @@ export function ExamSession({
|
|
|
150
174
|
</Card>
|
|
151
175
|
)}
|
|
152
176
|
|
|
177
|
+
<span className="sr-only" aria-live="polite">
|
|
178
|
+
Question {currentIndex + 1} of {questions.length}
|
|
179
|
+
</span>
|
|
153
180
|
{currentQuestion && (
|
|
154
|
-
<Card className="mt-3">
|
|
155
|
-
<
|
|
181
|
+
<Card className="mt-3" ref={questionAreaRef} tabIndex={-1}>
|
|
182
|
+
<CardHeader className="pb-0">
|
|
183
|
+
<QuestionHeaderBar
|
|
184
|
+
questionNumber={currentIndex + 1}
|
|
185
|
+
totalQuestions={questions.length}
|
|
186
|
+
isFlagged={flaggedUids.has(currentQuestion.uid)}
|
|
187
|
+
onToggleFlag={toggleCurrentFlag}
|
|
188
|
+
hasMaterials={currentMaterials.length > 0}
|
|
189
|
+
onOpenMaterials={openMaterials}
|
|
190
|
+
readOnly={readOnly}
|
|
191
|
+
/>
|
|
192
|
+
</CardHeader>
|
|
193
|
+
<CardContent>
|
|
156
194
|
<QuestionRenderer
|
|
157
195
|
question={currentQuestion}
|
|
158
|
-
sessionAnswers={
|
|
196
|
+
sessionAnswers={currentQuestionAnswers}
|
|
159
197
|
onAnswer={handleAnswer}
|
|
160
198
|
readOnly={readOnly}
|
|
161
199
|
/>
|
|
@@ -163,20 +201,25 @@ export function ExamSession({
|
|
|
163
201
|
</Card>
|
|
164
202
|
)}
|
|
165
203
|
|
|
204
|
+
<QuestionMaterialsDrawer
|
|
205
|
+
open={materialsOpen}
|
|
206
|
+
onOpenChange={setMaterialsOpen}
|
|
207
|
+
materials={currentMaterials}
|
|
208
|
+
questionNumber={currentIndex + 1}
|
|
209
|
+
/>
|
|
210
|
+
|
|
166
211
|
<ConfirmDialog
|
|
167
212
|
open={showConfirm}
|
|
168
213
|
title="Submit Exam?"
|
|
169
|
-
message={`You have answered ${
|
|
214
|
+
message={`You have answered ${answeredCount} of ${questions.length} questions. Once submitted, you cannot change your answers.`}
|
|
170
215
|
confirmLabel="Submit Exam"
|
|
171
216
|
cancelLabel="Continue Exam"
|
|
172
217
|
confirmColor="primary"
|
|
173
|
-
onConfirm={
|
|
174
|
-
|
|
175
|
-
doSubmit(false);
|
|
176
|
-
}}
|
|
177
|
-
onCancel={() => setShowConfirm(false)}
|
|
218
|
+
onConfirm={confirmSubmit}
|
|
219
|
+
onCancel={cancelConfirm}
|
|
178
220
|
isLoading={isSubmitting}
|
|
179
221
|
/>
|
|
180
222
|
</div>
|
|
223
|
+
</SectionShell>
|
|
181
224
|
);
|
|
182
225
|
}
|