@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,151 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from "react";
|
|
2
|
+
import { ShieldCheck } from "lucide-react";
|
|
3
|
+
import { ExamSession } from "../../sections/ExamSession/ExamSession";
|
|
4
|
+
import { Badge } from "../../ui/badge";
|
|
5
|
+
import { Alert, AlertDescription } from "../../ui/alert";
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
import type { SessionAnswer } from "../../questions/types";
|
|
8
|
+
import type { ExamSubmitMetadata } from "../../sections/ExamSession/types";
|
|
9
|
+
import { scoreAssessment } from "../../questions/scoring";
|
|
10
|
+
import { useTimer } from "../_shared/use-timer";
|
|
11
|
+
import { AssessmentIntro } from "../_shared/assessment-intro";
|
|
12
|
+
import { AssessmentResults } from "../_shared/assessment-results";
|
|
13
|
+
import { withProGate } from "../../license/withProGate";
|
|
14
|
+
import type { ExamModuleProps, ExamModuleResult } from "./types";
|
|
15
|
+
|
|
16
|
+
type InternalStep =
|
|
17
|
+
| { tag: "intro" }
|
|
18
|
+
| { tag: "exam" }
|
|
19
|
+
| { tag: "results"; result: ExamModuleResult };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* ExamModule — a complete formal exam experience.
|
|
23
|
+
*
|
|
24
|
+
* Steps: Intro (rules/instructions) → Exam (timed ExamSession) → Results (score + review).
|
|
25
|
+
* Manages an external timer that feeds elapsed time to ExamSession.
|
|
26
|
+
*/
|
|
27
|
+
function ExamModuleBase({
|
|
28
|
+
title,
|
|
29
|
+
description,
|
|
30
|
+
instructions,
|
|
31
|
+
questions = [],
|
|
32
|
+
timeLimitSeconds,
|
|
33
|
+
passingScore,
|
|
34
|
+
allowBackNavigation = true,
|
|
35
|
+
autoSubmitOnTimeout = true,
|
|
36
|
+
timeWarningThreshold,
|
|
37
|
+
allowRetake = false,
|
|
38
|
+
showReview = true,
|
|
39
|
+
onComplete,
|
|
40
|
+
readOnly = false,
|
|
41
|
+
className,
|
|
42
|
+
style,
|
|
43
|
+
}: ExamModuleProps) {
|
|
44
|
+
const [step, setStep] = useState<InternalStep>({ tag: "intro" });
|
|
45
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
const { timeElapsed, reset: resetTimer } = useTimer(step.tag === "exam");
|
|
47
|
+
|
|
48
|
+
const onCompleteRef = useRef(onComplete);
|
|
49
|
+
onCompleteRef.current = onComplete;
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
53
|
+
}, [step.tag]);
|
|
54
|
+
|
|
55
|
+
const handleSubmit = useCallback((answers: SessionAnswer[], metadata: ExamSubmitMetadata) => {
|
|
56
|
+
const { correct, total, percentage } = scoreAssessment(questions, answers);
|
|
57
|
+
const result: ExamModuleResult = {
|
|
58
|
+
answers,
|
|
59
|
+
correct,
|
|
60
|
+
total,
|
|
61
|
+
percentage,
|
|
62
|
+
passed: passingScore !== undefined ? percentage >= passingScore : true,
|
|
63
|
+
timeElapsedSeconds: metadata.timeElapsedSeconds,
|
|
64
|
+
wasAutoSubmitted: metadata.wasAutoSubmitted,
|
|
65
|
+
};
|
|
66
|
+
setStep({ tag: "results", result });
|
|
67
|
+
onCompleteRef.current?.(result);
|
|
68
|
+
}, [questions, passingScore]);
|
|
69
|
+
|
|
70
|
+
const handleRetake = useCallback(() => {
|
|
71
|
+
resetTimer();
|
|
72
|
+
setStep({ tag: "intro" });
|
|
73
|
+
}, [resetTimer]);
|
|
74
|
+
|
|
75
|
+
// ─── Intro Screen ───
|
|
76
|
+
if (step.tag === "intro") {
|
|
77
|
+
return (
|
|
78
|
+
<div ref={contentRef} tabIndex={-1} className={cn("max-w-2xl mx-auto outline-none", className)} style={style}>
|
|
79
|
+
<AssessmentIntro
|
|
80
|
+
icon={<ShieldCheck className="size-7 text-primary" />}
|
|
81
|
+
title={title}
|
|
82
|
+
description={description}
|
|
83
|
+
questionCount={questions.length}
|
|
84
|
+
timeLimitSeconds={timeLimitSeconds}
|
|
85
|
+
passingScore={passingScore}
|
|
86
|
+
startLabel="Begin Exam"
|
|
87
|
+
onStart={() => setStep({ tag: "exam" })}
|
|
88
|
+
readOnly={readOnly}
|
|
89
|
+
>
|
|
90
|
+
{instructions && (
|
|
91
|
+
<Alert className="text-left mb-6">
|
|
92
|
+
<AlertDescription>{instructions}</AlertDescription>
|
|
93
|
+
</Alert>
|
|
94
|
+
)}
|
|
95
|
+
</AssessmentIntro>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Exam Screen ───
|
|
101
|
+
if (step.tag === "exam") {
|
|
102
|
+
return (
|
|
103
|
+
<div ref={contentRef} tabIndex={-1} className={cn("outline-none", className)} style={style}>
|
|
104
|
+
<ExamSession
|
|
105
|
+
questions={questions}
|
|
106
|
+
timeLimitSeconds={timeLimitSeconds}
|
|
107
|
+
timeElapsedSeconds={timeElapsed}
|
|
108
|
+
autoSubmitOnTimeout={autoSubmitOnTimeout}
|
|
109
|
+
timeWarningThreshold={timeWarningThreshold}
|
|
110
|
+
allowBackNavigation={allowBackNavigation}
|
|
111
|
+
confirmBeforeSubmit
|
|
112
|
+
examTitle={title}
|
|
113
|
+
onSubmit={handleSubmit}
|
|
114
|
+
readOnly={readOnly}
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Results Screen ───
|
|
121
|
+
const { result } = step;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div ref={contentRef} tabIndex={-1} className={cn("max-w-2xl mx-auto outline-none", className)} style={style}>
|
|
125
|
+
<AssessmentResults
|
|
126
|
+
title={title}
|
|
127
|
+
percentage={result.percentage}
|
|
128
|
+
passed={result.passed}
|
|
129
|
+
correct={result.correct}
|
|
130
|
+
total={result.total}
|
|
131
|
+
timeElapsedSeconds={result.timeElapsedSeconds}
|
|
132
|
+
answers={result.answers}
|
|
133
|
+
questions={questions}
|
|
134
|
+
allowRetake={allowRetake}
|
|
135
|
+
onRetake={handleRetake}
|
|
136
|
+
retakeLabel="Retake Exam"
|
|
137
|
+
showReview={showReview}
|
|
138
|
+
readOnly={readOnly}
|
|
139
|
+
extraBadges={
|
|
140
|
+
result.wasAutoSubmitted ? (
|
|
141
|
+
<Badge variant="outline" className="text-sm px-3 py-1 mb-2 ml-2">
|
|
142
|
+
Auto-submitted
|
|
143
|
+
</Badge>
|
|
144
|
+
) : null
|
|
145
|
+
}
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const ExamModule = withProGate(ExamModuleBase, "ExamModule");
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { QuestionData } from "../../questions/types";
|
|
3
|
+
import type { AssessmentResult } from "../_shared/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ExamModule — a formal timed exam experience with intro, exam, and results steps.
|
|
7
|
+
*
|
|
8
|
+
* Wraps ExamSession with an intro screen showing rules/instructions and a
|
|
9
|
+
* results screen with scoring, stats, and optional answer review.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <ExamModule
|
|
13
|
+
* title="Midterm Exam"
|
|
14
|
+
* instructions={<p>You have 60 minutes. No notes allowed.</p>}
|
|
15
|
+
* questions={questions}
|
|
16
|
+
* timeLimitSeconds={3600}
|
|
17
|
+
* passingScore={70}
|
|
18
|
+
* onComplete={(result) => saveResult(result)}
|
|
19
|
+
* />
|
|
20
|
+
*/
|
|
21
|
+
export interface ExamModuleProps {
|
|
22
|
+
/** Exam title displayed on the intro screen */
|
|
23
|
+
title: string;
|
|
24
|
+
/** Exam description displayed on the intro screen */
|
|
25
|
+
description?: string;
|
|
26
|
+
/** Exam rules/instructions rendered on the intro screen */
|
|
27
|
+
instructions?: ReactNode;
|
|
28
|
+
/** Ordered list of questions */
|
|
29
|
+
questions: QuestionData[];
|
|
30
|
+
/** Time limit in seconds (required for exams) */
|
|
31
|
+
timeLimitSeconds: number;
|
|
32
|
+
/** Passing threshold as a percentage (e.g. 70 means 70%) */
|
|
33
|
+
passingScore?: number;
|
|
34
|
+
/** Whether the user can go back to previous questions. @default true */
|
|
35
|
+
allowBackNavigation?: boolean;
|
|
36
|
+
/** Auto-submit when time runs out. @default true */
|
|
37
|
+
autoSubmitOnTimeout?: boolean;
|
|
38
|
+
/** Seconds remaining at which to show a time warning */
|
|
39
|
+
timeWarningThreshold?: number;
|
|
40
|
+
/** Whether to allow retaking the exam from the results screen. @default false */
|
|
41
|
+
allowRetake?: boolean;
|
|
42
|
+
/** Whether to show correct/incorrect answer highlighting in the review. @default true */
|
|
43
|
+
showReview?: boolean;
|
|
44
|
+
/** Called when the exam is completed (submitted) */
|
|
45
|
+
onComplete?: (result: ExamModuleResult) => void;
|
|
46
|
+
/** When true, disables interactions for preview/demo mode. @default false */
|
|
47
|
+
readOnly?: boolean;
|
|
48
|
+
/** CSS class name for the root element */
|
|
49
|
+
className?: string;
|
|
50
|
+
/** Inline styles for the root element */
|
|
51
|
+
style?: React.CSSProperties;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ExamModuleResult extends AssessmentResult {
|
|
55
|
+
/** Whether the submission was triggered by timeout */
|
|
56
|
+
wasAutoSubmitted: boolean;
|
|
57
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useRef } from "react";
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
3
|
BookOpen,
|
|
4
4
|
Shuffle,
|
|
@@ -17,6 +17,7 @@ import { Card, CardContent } from "../../ui/card";
|
|
|
17
17
|
import { formatDuration } from "../../utils/format-duration";
|
|
18
18
|
import { cn } from "../../lib/utils";
|
|
19
19
|
import type { FlashcardData } from "../../flashcards/types";
|
|
20
|
+
import { withProGate } from "../../license/withProGate";
|
|
20
21
|
import type {
|
|
21
22
|
FlashcardLabProps,
|
|
22
23
|
FlashcardLabResult,
|
|
@@ -27,12 +28,13 @@ type InternalStep =
|
|
|
27
28
|
| { tag: "study"; cards: FlashcardData[]; deckUids: string[]; shuffled: boolean }
|
|
28
29
|
| { tag: "completion"; result: FlashcardLabResult };
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
decks,
|
|
31
|
+
function FlashcardLabBase({
|
|
32
|
+
decks = [],
|
|
32
33
|
showShuffleToggle = true,
|
|
33
34
|
defaultShuffled = false,
|
|
34
35
|
allowMultiSelect = true,
|
|
35
36
|
onComplete,
|
|
37
|
+
readOnly = false,
|
|
36
38
|
className,
|
|
37
39
|
style,
|
|
38
40
|
}: FlashcardLabProps) {
|
|
@@ -40,6 +42,11 @@ export function FlashcardLab({
|
|
|
40
42
|
const [selectedUids, setSelectedUids] = useState<Set<string>>(new Set());
|
|
41
43
|
const [shuffled, setShuffled] = useState(defaultShuffled);
|
|
42
44
|
const startTimeRef = useRef<number | null>(null);
|
|
45
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
49
|
+
}, [step.tag]);
|
|
43
50
|
|
|
44
51
|
function toggleDeck(uid: string) {
|
|
45
52
|
setSelectedUids((prev) => {
|
|
@@ -105,12 +112,14 @@ export function FlashcardLab({
|
|
|
105
112
|
// ─── Setup Screen ───
|
|
106
113
|
if (step.tag === "setup") {
|
|
107
114
|
return (
|
|
108
|
-
<div className={cn(className)} style={style}>
|
|
115
|
+
<div ref={contentRef} tabIndex={-1} className={cn("max-w-2xl mx-auto outline-none", className)} style={style}>
|
|
116
|
+
<Card>
|
|
117
|
+
<CardContent className="pt-8 pb-8">
|
|
109
118
|
<div className="text-center mb-6">
|
|
110
|
-
<div className="mx-auto mb-
|
|
119
|
+
<div className="mx-auto mb-4 w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
|
|
111
120
|
<BookOpen className="size-7 text-primary" />
|
|
112
121
|
</div>
|
|
113
|
-
<h2 className="text-2xl font-bold text-foreground mb-
|
|
122
|
+
<h2 className="text-2xl font-bold text-foreground mb-2">
|
|
114
123
|
Choose Your Decks
|
|
115
124
|
</h2>
|
|
116
125
|
<p className="text-muted-foreground text-sm">
|
|
@@ -159,19 +168,15 @@ export function FlashcardLab({
|
|
|
159
168
|
<div className="flex items-center justify-between">
|
|
160
169
|
<div className="flex items-center gap-3">
|
|
161
170
|
{showShuffleToggle && (
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
shuffled
|
|
167
|
-
? "border-primary bg-primary/10 text-primary"
|
|
168
|
-
: "border-border text-muted-foreground hover:text-foreground",
|
|
169
|
-
)}
|
|
171
|
+
<Button
|
|
172
|
+
variant={shuffled ? "secondary" : "outline"}
|
|
173
|
+
size="sm"
|
|
174
|
+
className="gap-1.5"
|
|
170
175
|
onClick={() => setShuffled((s) => !s)}
|
|
171
176
|
>
|
|
172
177
|
<Shuffle className="size-3.5" />
|
|
173
178
|
Shuffle
|
|
174
|
-
</
|
|
179
|
+
</Button>
|
|
175
180
|
)}
|
|
176
181
|
{selectedUids.size > 0 && (
|
|
177
182
|
<span className="text-xs text-muted-foreground">
|
|
@@ -184,11 +189,13 @@ export function FlashcardLab({
|
|
|
184
189
|
</div>
|
|
185
190
|
<Button
|
|
186
191
|
onClick={startStudying}
|
|
187
|
-
disabled={selectedUids.size === 0}
|
|
192
|
+
disabled={selectedUids.size === 0 || readOnly}
|
|
188
193
|
>
|
|
189
194
|
Start Studying
|
|
190
195
|
</Button>
|
|
191
196
|
</div>
|
|
197
|
+
</CardContent>
|
|
198
|
+
</Card>
|
|
192
199
|
</div>
|
|
193
200
|
);
|
|
194
201
|
}
|
|
@@ -201,12 +208,13 @@ export function FlashcardLab({
|
|
|
201
208
|
.join(", ");
|
|
202
209
|
|
|
203
210
|
return (
|
|
204
|
-
<div className={cn(className)} style={style}>
|
|
211
|
+
<div ref={contentRef} tabIndex={-1} className={cn("outline-none", className)} style={style}>
|
|
205
212
|
<FlashcardStudySession
|
|
206
213
|
cards={step.cards}
|
|
207
214
|
title={deckNames}
|
|
208
215
|
shuffled={step.shuffled}
|
|
209
216
|
onComplete={handleStudyComplete}
|
|
217
|
+
readOnly={readOnly}
|
|
210
218
|
/>
|
|
211
219
|
</div>
|
|
212
220
|
);
|
|
@@ -216,7 +224,9 @@ export function FlashcardLab({
|
|
|
216
224
|
const { result } = step;
|
|
217
225
|
|
|
218
226
|
return (
|
|
219
|
-
<div className={cn(className)} style={style}>
|
|
227
|
+
<div ref={contentRef} tabIndex={-1} className={cn("max-w-2xl mx-auto outline-none", className)} style={style}>
|
|
228
|
+
<Card>
|
|
229
|
+
<CardContent className="pt-8 pb-8">
|
|
220
230
|
<div className="text-center mb-8">
|
|
221
231
|
<div className="relative mx-auto mb-4">
|
|
222
232
|
<ProgressRing
|
|
@@ -229,7 +239,7 @@ export function FlashcardLab({
|
|
|
229
239
|
/>
|
|
230
240
|
<CheckCircle2 className="size-8 text-success absolute inset-0 m-auto" />
|
|
231
241
|
</div>
|
|
232
|
-
<h2 className="text-xl font-bold text-foreground mb-
|
|
242
|
+
<h2 className="text-xl font-bold text-foreground mb-2">
|
|
233
243
|
Study Session Complete
|
|
234
244
|
</h2>
|
|
235
245
|
<p className="text-muted-foreground text-sm">
|
|
@@ -241,35 +251,43 @@ export function FlashcardLab({
|
|
|
241
251
|
<StatCard
|
|
242
252
|
icon={<Layers />}
|
|
243
253
|
label="Cards Studied"
|
|
254
|
+
description="Total cards reviewed"
|
|
244
255
|
value={String(result.totalCards)}
|
|
245
256
|
/>
|
|
246
257
|
<StatCard
|
|
247
258
|
icon={<BookOpen />}
|
|
248
259
|
label="Decks"
|
|
260
|
+
description="Decks completed"
|
|
249
261
|
value={String(result.decksStudied)}
|
|
250
262
|
/>
|
|
251
263
|
<StatCard
|
|
252
264
|
icon={<Clock />}
|
|
253
265
|
label="Time Spent"
|
|
266
|
+
description="Session duration"
|
|
254
267
|
value={formatDuration(result.timeElapsedSeconds)}
|
|
255
268
|
/>
|
|
256
269
|
<StatCard
|
|
257
270
|
icon={<Shuffle />}
|
|
258
271
|
label="Shuffled"
|
|
272
|
+
description="Card order randomized"
|
|
259
273
|
value={result.wasShuffled ? "Yes" : "No"}
|
|
260
274
|
/>
|
|
261
275
|
</div>
|
|
262
276
|
|
|
263
277
|
<div className="flex justify-center gap-3">
|
|
264
|
-
<Button variant="outline" onClick={handlePickNewDeck}>
|
|
278
|
+
<Button variant="outline" onClick={handlePickNewDeck} disabled={readOnly}>
|
|
265
279
|
<ArrowLeft className="size-4 mr-2" />
|
|
266
280
|
Pick New Deck
|
|
267
281
|
</Button>
|
|
268
|
-
<Button onClick={handleStudyAgain}>
|
|
282
|
+
<Button onClick={handleStudyAgain} disabled={readOnly}>
|
|
269
283
|
<RotateCcw className="size-4 mr-2" />
|
|
270
284
|
Study Again
|
|
271
285
|
</Button>
|
|
272
286
|
</div>
|
|
287
|
+
</CardContent>
|
|
288
|
+
</Card>
|
|
273
289
|
</div>
|
|
274
290
|
);
|
|
275
291
|
}
|
|
292
|
+
|
|
293
|
+
export const FlashcardLab = withProGate(FlashcardLabBase, "FlashcardLab");
|
|
@@ -27,6 +27,8 @@ export interface FlashcardLabProps {
|
|
|
27
27
|
allowMultiSelect?: boolean;
|
|
28
28
|
/** Called when the user completes a study session */
|
|
29
29
|
onComplete?: (result: FlashcardLabResult) => void;
|
|
30
|
+
/** When true, disables interactions for preview/demo mode. @default false */
|
|
31
|
+
readOnly?: boolean;
|
|
30
32
|
/** CSS class name for the root element */
|
|
31
33
|
className?: string;
|
|
32
34
|
/** Inline styles for the root element */
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { ArrowLeft, BookOpen, BarChart3 } from "lucide-react";
|
|
3
|
+
import { GradebookTable } from "../../sections/GradebookTable/GradebookTable";
|
|
4
|
+
import { ProgressDashboard } from "../../sections/ProgressDashboard/ProgressDashboard";
|
|
5
|
+
import { AssessmentReview } from "../../sections/AssessmentReview/AssessmentReview";
|
|
6
|
+
import { GradeIndicator } from "../../progress/grade-indicator";
|
|
7
|
+
import { Button } from "../../ui/button";
|
|
8
|
+
import { Card, CardContent } from "../../ui/card";
|
|
9
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../../ui/tabs";
|
|
10
|
+
import { Separator } from "../../ui/separator";
|
|
11
|
+
import { cn } from "../../lib/utils";
|
|
12
|
+
import type { GradeItem } from "../../sections/GradebookTable/types";
|
|
13
|
+
import { withProGate } from "../../license/withProGate";
|
|
14
|
+
import type { GradeCenterModuleProps } from "./types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* GradeCenterModule — a tabbed grade center with gradebook, progress dashboard,
|
|
18
|
+
* and drill-down into individual assessment reviews.
|
|
19
|
+
*
|
|
20
|
+
* Uses a panel-based layout (like CoursePlayer) with tabs for Grades and Progress,
|
|
21
|
+
* and a slide-in detail panel for reviewing individual assessments.
|
|
22
|
+
*/
|
|
23
|
+
function GradeCenterModuleBase({
|
|
24
|
+
courseTitle,
|
|
25
|
+
gradeItems = [],
|
|
26
|
+
categories,
|
|
27
|
+
overallGrade,
|
|
28
|
+
showWeights,
|
|
29
|
+
progressData,
|
|
30
|
+
reviewData,
|
|
31
|
+
readOnly = false,
|
|
32
|
+
className,
|
|
33
|
+
style,
|
|
34
|
+
}: GradeCenterModuleProps) {
|
|
35
|
+
const [drillDownUid, setDrillDownUid] = useState<string | null>(null);
|
|
36
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
40
|
+
}, [drillDownUid]);
|
|
41
|
+
|
|
42
|
+
function handleItemClick(item: GradeItem) {
|
|
43
|
+
if (reviewData?.[item.uid]) {
|
|
44
|
+
setDrillDownUid(item.uid);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function handleBack() {
|
|
49
|
+
setDrillDownUid(null);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const drillDownItem = drillDownUid
|
|
53
|
+
? gradeItems.find((i) => i.uid === drillDownUid)
|
|
54
|
+
: null;
|
|
55
|
+
const drillDownData = drillDownUid ? reviewData?.[drillDownUid] : null;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
ref={contentRef}
|
|
60
|
+
tabIndex={-1}
|
|
61
|
+
className={cn("outline-none", className)}
|
|
62
|
+
style={style}
|
|
63
|
+
>
|
|
64
|
+
{/* Header */}
|
|
65
|
+
<div className="flex items-center justify-between mb-6">
|
|
66
|
+
<div>
|
|
67
|
+
{courseTitle && (
|
|
68
|
+
<h2 className="text-xl font-bold text-foreground">{courseTitle}</h2>
|
|
69
|
+
)}
|
|
70
|
+
<p className="text-sm text-muted-foreground">Grade Center</p>
|
|
71
|
+
</div>
|
|
72
|
+
{overallGrade && (
|
|
73
|
+
<div className="flex items-center gap-3">
|
|
74
|
+
<GradeIndicator
|
|
75
|
+
percentage={overallGrade.percentage}
|
|
76
|
+
letterGrade={overallGrade.letterGrade}
|
|
77
|
+
size="medium"
|
|
78
|
+
/>
|
|
79
|
+
<div className="text-right">
|
|
80
|
+
<div className="text-sm font-medium text-foreground">
|
|
81
|
+
{overallGrade.pointsEarned}/{overallGrade.pointsPossible} pts
|
|
82
|
+
</div>
|
|
83
|
+
{overallGrade.letterGrade && (
|
|
84
|
+
<div className="text-xs text-muted-foreground">
|
|
85
|
+
{overallGrade.letterGrade}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<Separator className="mb-6" />
|
|
94
|
+
|
|
95
|
+
{/* Drill-down view */}
|
|
96
|
+
{drillDownItem && drillDownData ? (
|
|
97
|
+
<div>
|
|
98
|
+
<Button
|
|
99
|
+
variant="ghost"
|
|
100
|
+
size="sm"
|
|
101
|
+
onClick={handleBack}
|
|
102
|
+
className="mb-4"
|
|
103
|
+
>
|
|
104
|
+
<ArrowLeft className="size-4 mr-1.5" />
|
|
105
|
+
Back to Grades
|
|
106
|
+
</Button>
|
|
107
|
+
<Card className="mb-4">
|
|
108
|
+
<CardContent className="py-3 px-4">
|
|
109
|
+
<h3 className="font-semibold text-foreground">
|
|
110
|
+
{drillDownItem.name}
|
|
111
|
+
</h3>
|
|
112
|
+
{drillDownItem.score !== null && (
|
|
113
|
+
<p className="text-sm text-muted-foreground">
|
|
114
|
+
Score: {drillDownItem.score}/{drillDownItem.maxScore} (
|
|
115
|
+
{Math.round(
|
|
116
|
+
(drillDownItem.score / drillDownItem.maxScore) * 100
|
|
117
|
+
)}
|
|
118
|
+
%)
|
|
119
|
+
</p>
|
|
120
|
+
)}
|
|
121
|
+
</CardContent>
|
|
122
|
+
</Card>
|
|
123
|
+
<AssessmentReview
|
|
124
|
+
questions={drillDownData.questions}
|
|
125
|
+
sessionAnswers={drillDownData.sessionAnswers}
|
|
126
|
+
score={drillDownData.score}
|
|
127
|
+
showCorrectAnswers
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
) : (
|
|
131
|
+
<Tabs defaultValue="grades">
|
|
132
|
+
{progressData && (
|
|
133
|
+
<TabsList className="mb-6">
|
|
134
|
+
<TabsTrigger value="grades">
|
|
135
|
+
<BookOpen className="size-4 mr-1.5" />
|
|
136
|
+
Grades
|
|
137
|
+
</TabsTrigger>
|
|
138
|
+
<TabsTrigger value="progress">
|
|
139
|
+
<BarChart3 className="size-4 mr-1.5" />
|
|
140
|
+
Progress
|
|
141
|
+
</TabsTrigger>
|
|
142
|
+
</TabsList>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
<TabsContent value="grades">
|
|
146
|
+
<GradebookTable
|
|
147
|
+
items={gradeItems}
|
|
148
|
+
categories={categories}
|
|
149
|
+
overallGrade={overallGrade}
|
|
150
|
+
showWeights={showWeights}
|
|
151
|
+
onItemClick={handleItemClick}
|
|
152
|
+
readOnly={readOnly}
|
|
153
|
+
/>
|
|
154
|
+
</TabsContent>
|
|
155
|
+
|
|
156
|
+
{progressData && (
|
|
157
|
+
<TabsContent value="progress">
|
|
158
|
+
<ProgressDashboard
|
|
159
|
+
overallProgress={progressData.overallProgress}
|
|
160
|
+
totalTimeSpent={progressData.totalTimeSpent}
|
|
161
|
+
modules={progressData.modules}
|
|
162
|
+
recentActivity={progressData.recentActivity}
|
|
163
|
+
streak={progressData.streak}
|
|
164
|
+
achievements={progressData.achievements}
|
|
165
|
+
/>
|
|
166
|
+
</TabsContent>
|
|
167
|
+
)}
|
|
168
|
+
</Tabs>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const GradeCenterModule = withProGate(GradeCenterModuleBase, "GradeCenterModule");
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GradeItem,
|
|
3
|
+
GradeCategory,
|
|
4
|
+
OverallGrade,
|
|
5
|
+
} from "../../sections/GradebookTable/types";
|
|
6
|
+
import type {
|
|
7
|
+
ModuleProgress,
|
|
8
|
+
ActivityItem,
|
|
9
|
+
Achievement,
|
|
10
|
+
} from "../../sections/ProgressDashboard/types";
|
|
11
|
+
import type { AssessmentScore } from "../../sections/AssessmentReview/types";
|
|
12
|
+
import type { QuestionData, SessionAnswer } from "../../questions/types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* GradeCenterModule — a tabbed grade center with gradebook, progress dashboard,
|
|
16
|
+
* and drill-down into individual assessment reviews.
|
|
17
|
+
*
|
|
18
|
+
* Combines GradebookTable, ProgressDashboard, and AssessmentReview sections
|
|
19
|
+
* in a master-detail layout with tab navigation.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* <GradeCenterModule
|
|
23
|
+
* courseTitle="React Fundamentals"
|
|
24
|
+
* gradeItems={items}
|
|
25
|
+
* overallGrade={overallGrade}
|
|
26
|
+
* progressData={{ overallProgress: 75, totalTimeSpent: 45000, modules: [] }}
|
|
27
|
+
* reviewData={{ quiz1: { questions, sessionAnswers, score } }}
|
|
28
|
+
* />
|
|
29
|
+
*/
|
|
30
|
+
export interface GradeCenterModuleProps {
|
|
31
|
+
/** Course title shown in the header */
|
|
32
|
+
courseTitle?: string;
|
|
33
|
+
/** Grade items (assignments, quizzes, etc.) */
|
|
34
|
+
gradeItems: GradeItem[];
|
|
35
|
+
/** Optional category grouping */
|
|
36
|
+
categories?: GradeCategory[];
|
|
37
|
+
/** Overall course grade summary */
|
|
38
|
+
overallGrade?: OverallGrade;
|
|
39
|
+
/** Whether to show the weight column in the gradebook */
|
|
40
|
+
showWeights?: boolean;
|
|
41
|
+
/** Data for the progress tab — tab is hidden if not provided */
|
|
42
|
+
progressData?: {
|
|
43
|
+
overallProgress: number;
|
|
44
|
+
totalTimeSpent: number;
|
|
45
|
+
modules: ModuleProgress[];
|
|
46
|
+
recentActivity?: ActivityItem[];
|
|
47
|
+
streak?: { currentDays: number; longestDays: number };
|
|
48
|
+
achievements?: Achievement[];
|
|
49
|
+
};
|
|
50
|
+
/** Review data keyed by grade item UID — enables drill-down on click */
|
|
51
|
+
reviewData?: Record<
|
|
52
|
+
string,
|
|
53
|
+
{
|
|
54
|
+
questions: QuestionData[];
|
|
55
|
+
sessionAnswers: SessionAnswer[];
|
|
56
|
+
score?: AssessmentScore;
|
|
57
|
+
}
|
|
58
|
+
>;
|
|
59
|
+
/** When true, disables interactions for preview/demo mode. @default false */
|
|
60
|
+
readOnly?: boolean;
|
|
61
|
+
/** CSS class name for the root element */
|
|
62
|
+
className?: string;
|
|
63
|
+
/** Inline styles for the root element */
|
|
64
|
+
style?: React.CSSProperties;
|
|
65
|
+
}
|