@hydralms/components 0.2.0 → 0.3.1
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-BPsZBaJj.cjs +1 -0
- package/dist/StudentProfile-Cw2p-RZn.js +3273 -0
- package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
- package/dist/assessment-toolbar/timer-display.d.ts +1 -1
- package/dist/common/index.d.ts +2 -1
- package/dist/common/pagination.d.ts +26 -0
- package/dist/common/types.d.ts +1 -0
- package/dist/components.css +1 -1
- package/dist/content/audio-player.d.ts +22 -0
- package/dist/content/code-block.d.ts +30 -0
- package/dist/content/embed-block.d.ts +28 -0
- package/dist/content/index.d.ts +6 -0
- package/dist/content/types.d.ts +24 -0
- package/dist/curriculum/course-card.d.ts +51 -0
- 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 +495 -439
- 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 +6 -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 +4 -7
- package/dist/modules/AssignmentModule/types.d.ts +5 -1
- package/dist/modules/CertificateModule/CertificateModule.d.ts +4 -8
- package/dist/modules/CertificateModule/types.d.ts +6 -4
- 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 +4 -7
- package/dist/modules/ExamModule/ExamModule.d.ts +4 -7
- package/dist/modules/ExamModule/types.d.ts +5 -14
- 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 +4 -8
- package/dist/modules/GradeCenterModule/types.d.ts +2 -0
- package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
- package/dist/modules/QuizModule/types.d.ts +5 -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 +4 -6
- package/dist/modules/SurveyModule/types.d.ts +2 -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 +6 -0
- package/dist/modules.cjs +1 -1
- package/dist/modules.js +1267 -854
- package/dist/progress/types.d.ts +2 -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 +1 -1
- package/dist/questions/fill-in-the-blank.d.ts +1 -1
- package/dist/questions/hotspot.d.ts +1 -1
- package/dist/questions/index.d.ts +2 -0
- package/dist/questions/inline-choice.d.ts +1 -1
- package/dist/questions/matching.d.ts +1 -1
- package/dist/questions/multiple-choice.d.ts +1 -1
- package/dist/questions/numeric.d.ts +1 -1
- package/dist/questions/ordering.d.ts +1 -1
- package/dist/questions/question-renderer.d.ts +1 -1
- package/dist/questions/scenario.d.ts +1 -1
- package/dist/questions/spreadsheet.d.ts +1 -1
- package/dist/questions/true-false.d.ts +1 -1
- package/dist/sections/AdaptiveLearningPath/AdaptiveLearningPath.d.ts +5 -0
- package/dist/sections/AdaptiveLearningPath/path-connector.d.ts +8 -0
- package/dist/sections/AdaptiveLearningPath/path-milestone-marker.d.ts +7 -0
- package/dist/sections/AdaptiveLearningPath/path-node-card.d.ts +10 -0
- package/dist/sections/AdaptiveLearningPath/path-skill-bar.d.ts +8 -0
- package/dist/sections/AdaptiveLearningPath/types.d.ts +136 -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 +6 -0
- package/dist/sections/ContentAuthoringStudio/ContentAuthoringStudio.d.ts +5 -0
- package/dist/sections/ContentAuthoringStudio/block-editor-item.d.ts +14 -0
- package/dist/sections/ContentAuthoringStudio/block-type-picker.d.ts +12 -0
- package/dist/sections/ContentAuthoringStudio/types.d.ts +67 -0
- 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 +6 -0
- 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 +1 -1
- package/dist/sections/ForumBoard/types.d.ts +14 -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 +6 -0
- package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +1 -1
- package/dist/sections/RequirementsChecklist/types.d.ts +6 -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 +1 -1
- package/dist/sections/RubricView/types.d.ts +6 -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 +10 -0
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +1361 -307
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/progress.d.ts +1 -1
- package/dist/ui/rich-text-editor.d.ts +3 -1
- package/dist/ui/toast.d.ts +43 -0
- package/dist/utils/debounce.d.ts +5 -1
- package/dist/utils/pick-palette-color.d.ts +19 -0
- package/dist/video/types.d.ts +15 -0
- package/dist/video/video-player.d.ts +1 -1
- package/dist/withProGate-BJdu1T9Y.cjs +2 -0
- package/dist/withProGate-BvFc7Jwy.js +4975 -0
- package/package.json +57 -226
- package/src/assessment-toolbar/question-navigator.tsx +10 -5
- package/src/assessment-toolbar/timer-display.tsx +4 -3
- package/src/assessment-toolbar/use-countdown.ts +1 -1
- package/src/common/empty-state.tsx +1 -0
- package/src/common/index.ts +2 -0
- package/src/common/pagination.tsx +135 -0
- package/src/common/search-input.tsx +2 -1
- package/src/common/types.ts +2 -0
- package/src/content/attachment-list.tsx +2 -0
- package/src/content/audio-player.tsx +196 -0
- package/src/content/code-block.tsx +113 -0
- package/src/content/content-block.tsx +64 -0
- package/src/content/embed-block.tsx +78 -0
- package/src/content/file-upload-zone.tsx +10 -0
- package/src/content/index.ts +6 -0
- package/src/content/types.ts +5 -0
- package/src/curriculum/course-card.tsx +199 -0
- package/src/curriculum/curriculum-item.tsx +3 -3
- package/src/curriculum/curriculum-tree.tsx +20 -13
- package/src/curriculum/index.ts +2 -0
- package/src/curriculum/types.ts +2 -2
- package/src/flashcards/flashcard.tsx +28 -8
- 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 +34 -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 +17 -8
- package/src/modules/AssignmentModule/types.ts +5 -1
- package/src/modules/CertificateModule/CertificateModule.tsx +21 -9
- package/src/modules/CertificateModule/types.ts +6 -4
- package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
- package/src/modules/CourseCatalogModule/types.ts +47 -0
- package/src/modules/CoursePlayer/CoursePlayer.tsx +39 -22
- package/src/modules/DiscussionModule/DiscussionModule.tsx +57 -22
- package/src/modules/ExamModule/ExamModule.tsx +64 -198
- package/src/modules/ExamModule/types.ts +5 -14
- package/src/modules/FlashcardLab/FlashcardLab.tsx +10 -5
- package/src/modules/FlashcardLab/types.ts +2 -0
- package/src/modules/GradeCenterModule/GradeCenterModule.tsx +7 -2
- package/src/modules/GradeCenterModule/types.ts +2 -0
- package/src/modules/QuizModule/QuizModule.tsx +49 -169
- package/src/modules/QuizModule/types.ts +5 -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 +9 -4
- package/src/modules/SurveyModule/types.ts +2 -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 +9 -0
- package/src/progress/achievement-badge.tsx +3 -3
- package/src/progress/grade-indicator.tsx +9 -1
- package/src/progress/progress-ring.tsx +2 -1
- package/src/progress/stat-card.tsx +14 -2
- package/src/progress/types.ts +2 -0
- package/src/provider/HydraProvider.tsx +15 -6
- package/src/questions/choice.tsx +13 -6
- package/src/questions/confidence-indicator.tsx +107 -0
- package/src/questions/essay.tsx +6 -4
- package/src/questions/fill-in-the-blank.tsx +8 -4
- package/src/questions/hotspot.tsx +4 -4
- package/src/questions/index.ts +2 -0
- package/src/questions/inline-choice.tsx +5 -4
- package/src/questions/matching.tsx +5 -4
- package/src/questions/multiple-choice.tsx +13 -6
- package/src/questions/numeric.tsx +8 -4
- package/src/questions/ordering.tsx +12 -4
- package/src/questions/question-renderer.tsx +3 -2
- package/src/questions/scenario.tsx +4 -4
- package/src/questions/spreadsheet.tsx +5 -4
- package/src/questions/true-false.tsx +13 -6
- package/src/sections/AdaptiveLearningPath/AdaptiveLearningPath.tsx +251 -0
- package/src/sections/AdaptiveLearningPath/path-connector.tsx +27 -0
- package/src/sections/AdaptiveLearningPath/path-milestone-marker.tsx +50 -0
- package/src/sections/AdaptiveLearningPath/path-node-card.tsx +166 -0
- package/src/sections/AdaptiveLearningPath/path-skill-bar.tsx +49 -0
- package/src/sections/AdaptiveLearningPath/types.ts +159 -0
- package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +64 -8
- package/src/sections/AnnouncementFeed/types.ts +15 -1
- package/src/sections/AssessmentReview/AssessmentReview.tsx +37 -0
- package/src/sections/AssessmentReview/types.ts +6 -0
- package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +37 -1
- package/src/sections/AssignmentSubmission/types.ts +6 -0
- package/src/sections/CertificateViewer/CertificateViewer.tsx +29 -227
- package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
- package/src/sections/CertificateViewer/types.ts +6 -0
- package/src/sections/ContentAuthoringStudio/ContentAuthoringStudio.tsx +289 -0
- package/src/sections/ContentAuthoringStudio/block-editor-item.tsx +487 -0
- package/src/sections/ContentAuthoringStudio/block-type-picker.tsx +123 -0
- package/src/sections/ContentAuthoringStudio/types.ts +67 -0
- package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
- package/src/sections/CourseCatalog/types.ts +76 -0
- package/src/sections/CourseOutline/CourseOutline.tsx +41 -0
- package/src/sections/CourseOutline/types.ts +6 -0
- package/src/sections/DiscussionThread/DiscussionThread.tsx +42 -1
- 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 +100 -94
- package/src/sections/ExamSession/types.ts +6 -0
- package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
- package/src/sections/FlashcardStudySession/types.ts +6 -0
- package/src/sections/ForumBoard/ForumBoard.tsx +67 -7
- package/src/sections/ForumBoard/types.ts +14 -0
- package/src/sections/GradebookTable/GradebookTable.tsx +54 -1
- 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 +34 -6
- package/src/sections/LessonPage/types.ts +6 -0
- package/src/sections/PracticeQuiz/PracticeQuiz.tsx +106 -74
- package/src/sections/PracticeQuiz/types.ts +6 -0
- package/src/sections/ProgressDashboard/ProgressDashboard.tsx +64 -10
- package/src/sections/ProgressDashboard/types.ts +6 -0
- package/src/sections/QuizSession/QuizSession.tsx +71 -82
- package/src/sections/QuizSession/types.ts +6 -0
- package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +41 -1
- package/src/sections/RequirementsChecklist/types.ts +6 -0
- package/src/sections/ResourceLibrary/ResourceLibrary.tsx +64 -8
- package/src/sections/ResourceLibrary/types.ts +15 -1
- package/src/sections/RubricView/RubricView.tsx +37 -1
- package/src/sections/RubricView/types.ts +6 -0
- package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +36 -15
- 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 +32 -5
- 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 +40 -0
- package/src/social/user-avatar.tsx +9 -5
- package/src/styles/globals.css +39 -41
- package/src/ui/badge.tsx +8 -0
- package/src/ui/index.ts +2 -0
- package/src/ui/progress.tsx +4 -0
- package/src/ui/rich-text-editor.tsx +10 -0
- package/src/ui/rich-text-toolbar.tsx +2 -1
- package/src/ui/toast.tsx +170 -0
- package/src/utils/debounce.ts +8 -2
- package/src/utils/pick-palette-color.ts +33 -0
- package/src/video/types.ts +16 -0
- package/src/video/video-player.tsx +27 -6
- package/dist/ForumBoard-CHXU3mjC.js +0 -2207
- package/dist/ForumBoard-d1w5-r6n.cjs +0 -1
- package/dist/tabs-DRM2Iq_J.cjs +0 -172
- package/dist/tabs-Wf3h_Cx3.js +0 -21580
|
@@ -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";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { AssessmentToolbar, QuestionHeaderBar, QuestionMaterialsDrawer } from "../../assessment-toolbar";
|
|
3
|
-
import type { QuestionNavigatorItem } from "../../assessment-toolbar/types";
|
|
4
|
-
import { QuestionRenderer } from "../../questions";
|
|
5
|
-
import type { SessionAnswer } from "../../questions/types";
|
|
6
3
|
import { ConfirmDialog } from "../../common";
|
|
4
|
+
import { QuestionRenderer } from "../../questions";
|
|
7
5
|
import { Alert, AlertDescription } from "../../ui/alert";
|
|
8
6
|
import { Card, CardHeader, CardContent } from "../../ui/card";
|
|
9
|
-
import {
|
|
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,
|
|
@@ -25,32 +26,63 @@ export function ExamSession({
|
|
|
25
26
|
questionMaterials,
|
|
26
27
|
isSubmitting = false,
|
|
27
28
|
readOnly = false,
|
|
29
|
+
isLoading,
|
|
30
|
+
error,
|
|
31
|
+
onRetry,
|
|
28
32
|
className,
|
|
29
33
|
style,
|
|
30
34
|
}: ExamSessionProps) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
|
|
34
54
|
const [showConfirm, setShowConfirm] = useState(false);
|
|
35
55
|
const [showTimeWarning, setShowTimeWarning] = useState(false);
|
|
36
|
-
const [materialsOpen, setMaterialsOpen] = useState(false);
|
|
37
56
|
const hasAutoSubmitted = useRef(false);
|
|
38
57
|
|
|
39
|
-
|
|
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
|
+
|
|
40
68
|
const remainingSeconds = timeLimitSeconds - timeElapsedSeconds;
|
|
41
69
|
|
|
42
|
-
const
|
|
43
|
-
() =>
|
|
44
|
-
|
|
45
|
-
? sessionAnswers.filter((a) => a.uid === currentQuestion.uid)
|
|
46
|
-
: [],
|
|
47
|
-
[sessionAnswers, currentQuestion],
|
|
70
|
+
const answeredCount = useMemo(
|
|
71
|
+
() => navigatorItems.filter((q) => q.isAnswered).length,
|
|
72
|
+
[navigatorItems],
|
|
48
73
|
);
|
|
49
74
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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]);
|
|
54
86
|
|
|
55
87
|
useEffect(() => {
|
|
56
88
|
if (remainingSeconds <= timeWarningThreshold && remainingSeconds > 0) {
|
|
@@ -63,77 +95,50 @@ export function ExamSession({
|
|
|
63
95
|
hasAutoSubmitted.current = true;
|
|
64
96
|
doSubmit(true);
|
|
65
97
|
}
|
|
66
|
-
|
|
67
|
-
}, [remainingSeconds, autoSubmitOnTimeout]);
|
|
68
|
-
|
|
69
|
-
const navigatorItems = useMemo<QuestionNavigatorItem[]>(
|
|
70
|
-
() =>
|
|
71
|
-
questions.map((q, idx) => ({
|
|
72
|
-
uid: q.uid,
|
|
73
|
-
sequence: idx,
|
|
74
|
-
isFlagged: flaggedUids.has(q.uid),
|
|
75
|
-
isAnswered: sessionAnswers.some((a) => a.uid === q.uid),
|
|
76
|
-
isSkipped: false,
|
|
77
|
-
})),
|
|
78
|
-
[questions, sessionAnswers, flaggedUids],
|
|
79
|
-
);
|
|
98
|
+
}, [remainingSeconds, autoSubmitOnTimeout, doSubmit]);
|
|
80
99
|
|
|
81
|
-
const
|
|
82
|
-
() => navigatorItems.filter((q) => q.isAnswered).length,
|
|
83
|
-
[navigatorItems],
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
function handleAnswer(rawAnswers: { uid: string; content?: string }[]) {
|
|
87
|
-
if (!currentQuestion) return;
|
|
88
|
-
const questionUid = currentQuestion.uid;
|
|
89
|
-
const newAnswers: SessionAnswer[] = rawAnswers.map((a) => ({
|
|
90
|
-
uid: questionUid,
|
|
91
|
-
answerUid: a.uid,
|
|
92
|
-
content: a.content,
|
|
93
|
-
}));
|
|
94
|
-
setSessionAnswers((prev) => {
|
|
95
|
-
const filtered = prev.filter((a) => a.uid !== questionUid);
|
|
96
|
-
const merged = [...filtered, ...newAnswers];
|
|
97
|
-
onAnswerChange?.(merged);
|
|
98
|
-
return merged;
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function handleNavigate(uid: string) {
|
|
103
|
-
const idx = questions.findIndex((q) => q.uid === uid);
|
|
104
|
-
if (idx !== -1) setCurrentIndex(idx);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function handleToggleFlag(uid: string) {
|
|
108
|
-
setFlaggedUids((prev) => {
|
|
109
|
-
const next = new Set(prev);
|
|
110
|
-
if (next.has(uid)) next.delete(uid);
|
|
111
|
-
else next.add(uid);
|
|
112
|
-
return next;
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function handleSubmitClick() {
|
|
100
|
+
const handleSubmitClick = useCallback(() => {
|
|
117
101
|
if (confirmBeforeSubmit) {
|
|
118
102
|
setShowConfirm(true);
|
|
119
103
|
} else {
|
|
120
104
|
doSubmit(false);
|
|
121
105
|
}
|
|
122
|
-
}
|
|
106
|
+
}, [confirmBeforeSubmit, doSubmit]);
|
|
123
107
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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), []);
|
|
134
125
|
|
|
135
126
|
return (
|
|
136
|
-
<
|
|
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>
|
|
137
142
|
{examTitle && (
|
|
138
143
|
<p className="text-xl font-bold mb-2 text-foreground">{examTitle}</p>
|
|
139
144
|
)}
|
|
@@ -149,10 +154,10 @@ export function ExamSession({
|
|
|
149
154
|
<AssessmentToolbar
|
|
150
155
|
currentQuestionIndex={currentIndex}
|
|
151
156
|
totalQuestions={questions.length}
|
|
152
|
-
hasNext={
|
|
157
|
+
hasNext={hasNext}
|
|
153
158
|
hasPrevious={allowBackNavigation && currentIndex > 0}
|
|
154
|
-
onNext={
|
|
155
|
-
onPrevious={
|
|
159
|
+
onNext={goNext}
|
|
160
|
+
onPrevious={handlePrevious}
|
|
156
161
|
onSubmit={handleSubmitClick}
|
|
157
162
|
timeElapsedSeconds={timeElapsedSeconds}
|
|
158
163
|
timeLimitSeconds={timeLimitSeconds}
|
|
@@ -169,16 +174,19 @@ export function ExamSession({
|
|
|
169
174
|
</Card>
|
|
170
175
|
)}
|
|
171
176
|
|
|
177
|
+
<span className="sr-only" aria-live="polite">
|
|
178
|
+
Question {currentIndex + 1} of {questions.length}
|
|
179
|
+
</span>
|
|
172
180
|
{currentQuestion && (
|
|
173
|
-
<Card className="mt-3">
|
|
181
|
+
<Card className="mt-3" ref={questionAreaRef} tabIndex={-1}>
|
|
174
182
|
<CardHeader className="pb-0">
|
|
175
183
|
<QuestionHeaderBar
|
|
176
184
|
questionNumber={currentIndex + 1}
|
|
177
185
|
totalQuestions={questions.length}
|
|
178
186
|
isFlagged={flaggedUids.has(currentQuestion.uid)}
|
|
179
|
-
onToggleFlag={
|
|
187
|
+
onToggleFlag={toggleCurrentFlag}
|
|
180
188
|
hasMaterials={currentMaterials.length > 0}
|
|
181
|
-
onOpenMaterials={
|
|
189
|
+
onOpenMaterials={openMaterials}
|
|
182
190
|
readOnly={readOnly}
|
|
183
191
|
/>
|
|
184
192
|
</CardHeader>
|
|
@@ -207,13 +215,11 @@ export function ExamSession({
|
|
|
207
215
|
confirmLabel="Submit Exam"
|
|
208
216
|
cancelLabel="Continue Exam"
|
|
209
217
|
confirmColor="primary"
|
|
210
|
-
onConfirm={
|
|
211
|
-
|
|
212
|
-
doSubmit(false);
|
|
213
|
-
}}
|
|
214
|
-
onCancel={() => setShowConfirm(false)}
|
|
218
|
+
onConfirm={confirmSubmit}
|
|
219
|
+
onCancel={cancelConfirm}
|
|
215
220
|
isLoading={isSubmitting}
|
|
216
221
|
/>
|
|
217
222
|
</div>
|
|
223
|
+
</SectionShell>
|
|
218
224
|
);
|
|
219
225
|
}
|
|
@@ -51,6 +51,12 @@ export interface ExamSessionProps {
|
|
|
51
51
|
questionMaterials?: QuestionMaterial[];
|
|
52
52
|
/** When true, all inputs are disabled */
|
|
53
53
|
readOnly?: boolean;
|
|
54
|
+
/** Render skeleton placeholders instead of content */
|
|
55
|
+
isLoading?: boolean;
|
|
56
|
+
/** Error message — renders an error state with optional retry */
|
|
57
|
+
error?: string | null;
|
|
58
|
+
/** Called when the user clicks retry in the error state */
|
|
59
|
+
onRetry?: () => void;
|
|
54
60
|
/** CSS class name for the root element */
|
|
55
61
|
className?: string;
|
|
56
62
|
/** Inline styles for the root element */
|