@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,289 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { ArrowLeft, User, BookOpen, Trophy, Award } from "lucide-react";
|
|
3
|
+
import { StudentProfile } from "../../sections/StudentProfile/StudentProfile";
|
|
4
|
+
import { CertificateViewer } from "../../sections/CertificateViewer/CertificateViewer";
|
|
5
|
+
import { AchievementBadge } from "../../progress/achievement-badge";
|
|
6
|
+
import { ProgressRing } from "../../progress/progress-ring";
|
|
7
|
+
import { UserAvatar } from "../../social/user-avatar";
|
|
8
|
+
import { Button } from "../../ui/button";
|
|
9
|
+
import { Card, CardContent } from "../../ui/card";
|
|
10
|
+
import { Badge } from "../../ui/badge";
|
|
11
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../../ui/tabs";
|
|
12
|
+
import { Separator } from "../../ui/separator";
|
|
13
|
+
import { cn } from "../../lib/utils";
|
|
14
|
+
import { formatTimestamp } from "../../utils/format-timestamp";
|
|
15
|
+
import { withProGate } from "../../license/withProGate";
|
|
16
|
+
import type { StudentProfileModuleProps } from "./types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* StudentProfileModule — a tabbed student profile page with certificate drill-down.
|
|
20
|
+
*
|
|
21
|
+
* Uses a tabbed layout with Profile, Courses, Achievements, and Certificates tabs.
|
|
22
|
+
* Clicking a certificate (when certificateData is provided) drills down into a
|
|
23
|
+
* full CertificateViewer with a back button to return to the tabbed view.
|
|
24
|
+
*/
|
|
25
|
+
function StudentProfileModuleBase({
|
|
26
|
+
student,
|
|
27
|
+
enrolledCourses = [],
|
|
28
|
+
achievements = [],
|
|
29
|
+
certificates = [],
|
|
30
|
+
stats,
|
|
31
|
+
onCourseClick,
|
|
32
|
+
certificateData,
|
|
33
|
+
readOnly = false,
|
|
34
|
+
className,
|
|
35
|
+
style,
|
|
36
|
+
}: StudentProfileModuleProps) {
|
|
37
|
+
const [drillDownCertUid, setDrillDownCertUid] = useState<string | null>(null);
|
|
38
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
42
|
+
}, [drillDownCertUid]);
|
|
43
|
+
|
|
44
|
+
function handleCertificateClick(certUid: string) {
|
|
45
|
+
if (readOnly) return;
|
|
46
|
+
if (certificateData?.[certUid]) {
|
|
47
|
+
setDrillDownCertUid(certUid);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleBack() {
|
|
52
|
+
setDrillDownCertUid(null);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const drillDownData = drillDownCertUid
|
|
56
|
+
? certificateData?.[drillDownCertUid]
|
|
57
|
+
: null;
|
|
58
|
+
const drillDownCert = drillDownCertUid
|
|
59
|
+
? certificates.find((c) => c.uid === drillDownCertUid)
|
|
60
|
+
: null;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
ref={contentRef}
|
|
65
|
+
tabIndex={-1}
|
|
66
|
+
className={cn("outline-none", className)}
|
|
67
|
+
style={style}
|
|
68
|
+
>
|
|
69
|
+
{/* Header */}
|
|
70
|
+
<div className="flex items-center gap-4 mb-6">
|
|
71
|
+
<UserAvatar
|
|
72
|
+
displayName={student.displayName}
|
|
73
|
+
avatarUrl={student.avatarUrl}
|
|
74
|
+
size="medium"
|
|
75
|
+
/>
|
|
76
|
+
<div className="flex-1 min-w-0">
|
|
77
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
78
|
+
<h2 className="text-xl font-bold text-foreground">
|
|
79
|
+
{student.displayName}
|
|
80
|
+
</h2>
|
|
81
|
+
{student.role && (
|
|
82
|
+
<Badge variant="secondary">{student.role}</Badge>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
{student.email && (
|
|
86
|
+
<p className="text-sm text-muted-foreground">{student.email}</p>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<Separator className="mb-6" />
|
|
92
|
+
|
|
93
|
+
{/* Drill-down view */}
|
|
94
|
+
{drillDownCert && drillDownData ? (
|
|
95
|
+
<div>
|
|
96
|
+
<Button
|
|
97
|
+
variant="ghost"
|
|
98
|
+
size="sm"
|
|
99
|
+
onClick={handleBack}
|
|
100
|
+
className="mb-4"
|
|
101
|
+
>
|
|
102
|
+
<ArrowLeft className="size-4 mr-1.5" />
|
|
103
|
+
Back to Profile
|
|
104
|
+
</Button>
|
|
105
|
+
<CertificateViewer {...drillDownData} showActions />
|
|
106
|
+
</div>
|
|
107
|
+
) : (
|
|
108
|
+
<Tabs defaultValue="profile">
|
|
109
|
+
<TabsList className="mb-6">
|
|
110
|
+
<TabsTrigger value="profile">
|
|
111
|
+
<User className="size-4 mr-1.5" />
|
|
112
|
+
Profile
|
|
113
|
+
</TabsTrigger>
|
|
114
|
+
<TabsTrigger value="courses">
|
|
115
|
+
<BookOpen className="size-4 mr-1.5" />
|
|
116
|
+
Courses
|
|
117
|
+
</TabsTrigger>
|
|
118
|
+
<TabsTrigger value="achievements">
|
|
119
|
+
<Trophy className="size-4 mr-1.5" />
|
|
120
|
+
Achievements
|
|
121
|
+
</TabsTrigger>
|
|
122
|
+
<TabsTrigger value="certificates">
|
|
123
|
+
<Award className="size-4 mr-1.5" />
|
|
124
|
+
Certificates
|
|
125
|
+
</TabsTrigger>
|
|
126
|
+
</TabsList>
|
|
127
|
+
|
|
128
|
+
{/* Profile tab */}
|
|
129
|
+
<TabsContent value="profile">
|
|
130
|
+
<StudentProfile
|
|
131
|
+
student={student}
|
|
132
|
+
enrolledCourses={enrolledCourses}
|
|
133
|
+
achievements={achievements}
|
|
134
|
+
stats={stats}
|
|
135
|
+
onCourseClick={onCourseClick}
|
|
136
|
+
showCourses={false}
|
|
137
|
+
showAchievements={false}
|
|
138
|
+
readOnly
|
|
139
|
+
/>
|
|
140
|
+
</TabsContent>
|
|
141
|
+
|
|
142
|
+
{/* Courses tab */}
|
|
143
|
+
<TabsContent value="courses">
|
|
144
|
+
{enrolledCourses.length > 0 ? (
|
|
145
|
+
<Card>
|
|
146
|
+
<CardContent className="p-0 divide-y divide-border">
|
|
147
|
+
{enrolledCourses.map((course) => (
|
|
148
|
+
<div
|
|
149
|
+
key={course.uid}
|
|
150
|
+
className={cn(
|
|
151
|
+
"flex items-center gap-3 px-4 py-3 transition-colors",
|
|
152
|
+
onCourseClick && !readOnly && "cursor-pointer hover:bg-muted/50",
|
|
153
|
+
)}
|
|
154
|
+
onClick={
|
|
155
|
+
onCourseClick && !readOnly
|
|
156
|
+
? () => onCourseClick(course.uid)
|
|
157
|
+
: undefined
|
|
158
|
+
}
|
|
159
|
+
>
|
|
160
|
+
<ProgressRing
|
|
161
|
+
value={course.progress}
|
|
162
|
+
size={32}
|
|
163
|
+
strokeWidth={3}
|
|
164
|
+
/>
|
|
165
|
+
<div className="flex-1 min-w-0">
|
|
166
|
+
<p className="text-sm font-medium text-foreground truncate">
|
|
167
|
+
{course.title}
|
|
168
|
+
</p>
|
|
169
|
+
{course.lastAccessedAt && (
|
|
170
|
+
<p className="text-xs text-muted-foreground">
|
|
171
|
+
{formatTimestamp(course.lastAccessedAt)}
|
|
172
|
+
</p>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
|
176
|
+
{Math.round(course.progress)}%
|
|
177
|
+
</span>
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</CardContent>
|
|
181
|
+
</Card>
|
|
182
|
+
) : (
|
|
183
|
+
<div className="py-12 text-center">
|
|
184
|
+
<BookOpen className="size-10 text-muted-foreground mx-auto mb-3" />
|
|
185
|
+
<p className="text-sm font-medium text-foreground">
|
|
186
|
+
No courses yet
|
|
187
|
+
</p>
|
|
188
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
189
|
+
Enrolled courses will appear here.
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</TabsContent>
|
|
194
|
+
|
|
195
|
+
{/* Achievements tab */}
|
|
196
|
+
<TabsContent value="achievements">
|
|
197
|
+
{achievements.length > 0 ? (
|
|
198
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
|
|
199
|
+
{achievements.map((achievement) => (
|
|
200
|
+
<AchievementBadge
|
|
201
|
+
key={achievement.uid}
|
|
202
|
+
title={achievement.name}
|
|
203
|
+
description={achievement.description}
|
|
204
|
+
icon={
|
|
205
|
+
achievement.iconUrl ? (
|
|
206
|
+
<img
|
|
207
|
+
src={achievement.iconUrl}
|
|
208
|
+
alt={achievement.name}
|
|
209
|
+
className="w-12 h-12"
|
|
210
|
+
/>
|
|
211
|
+
) : undefined
|
|
212
|
+
}
|
|
213
|
+
earnedDate={achievement.earnedAt}
|
|
214
|
+
/>
|
|
215
|
+
))}
|
|
216
|
+
</div>
|
|
217
|
+
) : (
|
|
218
|
+
<div className="py-12 text-center">
|
|
219
|
+
<Trophy className="size-10 text-muted-foreground mx-auto mb-3" />
|
|
220
|
+
<p className="text-sm font-medium text-foreground">
|
|
221
|
+
No achievements yet
|
|
222
|
+
</p>
|
|
223
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
224
|
+
Achievements will appear here as they are earned.
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
</TabsContent>
|
|
229
|
+
|
|
230
|
+
{/* Certificates tab */}
|
|
231
|
+
<TabsContent value="certificates">
|
|
232
|
+
{certificates.length > 0 ? (
|
|
233
|
+
<Card>
|
|
234
|
+
<CardContent className="p-0 divide-y divide-border">
|
|
235
|
+
{certificates.map((cert) => (
|
|
236
|
+
<div
|
|
237
|
+
key={cert.uid}
|
|
238
|
+
className={cn(
|
|
239
|
+
"flex items-center gap-3 px-4 py-3 transition-colors",
|
|
240
|
+
certificateData?.[cert.uid] &&
|
|
241
|
+
!readOnly &&
|
|
242
|
+
"cursor-pointer hover:bg-muted/50",
|
|
243
|
+
)}
|
|
244
|
+
onClick={
|
|
245
|
+
certificateData?.[cert.uid] && !readOnly
|
|
246
|
+
? () => handleCertificateClick(cert.uid)
|
|
247
|
+
: undefined
|
|
248
|
+
}
|
|
249
|
+
>
|
|
250
|
+
<div className="w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
|
|
251
|
+
<Award size={20} />
|
|
252
|
+
</div>
|
|
253
|
+
<div className="flex-1 min-w-0">
|
|
254
|
+
<p className="text-sm font-medium text-foreground truncate">
|
|
255
|
+
{cert.courseName}
|
|
256
|
+
</p>
|
|
257
|
+
<p className="text-xs text-muted-foreground">
|
|
258
|
+
Issued{" "}
|
|
259
|
+
{new Date(cert.issuedAt).toLocaleDateString()}
|
|
260
|
+
</p>
|
|
261
|
+
</div>
|
|
262
|
+
{certificateData?.[cert.uid] && (
|
|
263
|
+
<Badge variant="secondary" className="text-xs">
|
|
264
|
+
View
|
|
265
|
+
</Badge>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
))}
|
|
269
|
+
</CardContent>
|
|
270
|
+
</Card>
|
|
271
|
+
) : (
|
|
272
|
+
<div className="py-12 text-center">
|
|
273
|
+
<Award className="size-10 text-muted-foreground mx-auto mb-3" />
|
|
274
|
+
<p className="text-sm font-medium text-foreground">
|
|
275
|
+
No certificates yet
|
|
276
|
+
</p>
|
|
277
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
278
|
+
Certificates will appear here once courses are completed.
|
|
279
|
+
</p>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
</TabsContent>
|
|
283
|
+
</Tabs>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export const StudentProfileModule = withProGate(StudentProfileModuleBase, "StudentProfileModule");
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StudentInfo,
|
|
3
|
+
EnrolledCourse,
|
|
4
|
+
ProfileAchievement,
|
|
5
|
+
ProfileCertificate,
|
|
6
|
+
} from "../../sections/StudentProfile/types";
|
|
7
|
+
import type { CertificateViewerProps } from "../../sections/CertificateViewer/types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* StudentProfileModule — a tabbed student profile page with certificate drill-down.
|
|
11
|
+
*
|
|
12
|
+
* Combines StudentProfile, course list, achievements, and CertificateViewer
|
|
13
|
+
* in a tabbed layout with drill-down into individual certificates.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* <StudentProfileModule
|
|
17
|
+
* student={{ uid: "s1", displayName: "Jane Doe" }}
|
|
18
|
+
* enrolledCourses={courses}
|
|
19
|
+
* achievements={badges}
|
|
20
|
+
* certificates={certs}
|
|
21
|
+
* certificateData={{ cert1: { recipientName: "Jane", courseTitle: "React 101", completionDate: "2025-01-01", organizationName: "Hydra Academy" } }}
|
|
22
|
+
* />
|
|
23
|
+
*/
|
|
24
|
+
export interface StudentProfileModuleProps {
|
|
25
|
+
/** Student information */
|
|
26
|
+
student: StudentInfo;
|
|
27
|
+
/** Enrolled courses */
|
|
28
|
+
enrolledCourses?: EnrolledCourse[];
|
|
29
|
+
/** Earned achievements */
|
|
30
|
+
achievements?: ProfileAchievement[];
|
|
31
|
+
/** Earned certificates */
|
|
32
|
+
certificates?: ProfileCertificate[];
|
|
33
|
+
/** Custom stat cards */
|
|
34
|
+
stats?: { label: string; value: string; icon?: React.ReactNode }[];
|
|
35
|
+
/** Called when a course is clicked */
|
|
36
|
+
onCourseClick?: (courseUid: string) => void;
|
|
37
|
+
/** Certificate data keyed by certificate UID — enables drill-down on click */
|
|
38
|
+
certificateData?: Record<string, CertificateViewerProps>;
|
|
39
|
+
/** When true, disables interactions for preview/demo mode. @default false */
|
|
40
|
+
readOnly?: boolean;
|
|
41
|
+
/** CSS class name for the root element */
|
|
42
|
+
className?: string;
|
|
43
|
+
/** Inline styles for the root element */
|
|
44
|
+
style?: React.CSSProperties;
|
|
45
|
+
}
|
|
@@ -15,6 +15,7 @@ import { Card, CardContent } from "../../ui/card";
|
|
|
15
15
|
import { formatDuration } from "../../utils/format-duration";
|
|
16
16
|
import { cn } from "../../lib/utils";
|
|
17
17
|
import type { SurveyAnswer } from "../../sections/SurveyForm/types";
|
|
18
|
+
import { withProGate } from "../../license/withProGate";
|
|
18
19
|
import type { SurveyModuleProps, SurveyModuleResult } from "./types";
|
|
19
20
|
|
|
20
21
|
type InternalStep =
|
|
@@ -27,16 +28,17 @@ type InternalStep =
|
|
|
27
28
|
*
|
|
28
29
|
* Steps: Intro → SurveyForm → Thank You with response stats.
|
|
29
30
|
*/
|
|
30
|
-
|
|
31
|
+
function SurveyModuleBase({
|
|
31
32
|
title,
|
|
32
33
|
description,
|
|
33
|
-
questions,
|
|
34
|
+
questions = [],
|
|
34
35
|
requireAll = false,
|
|
35
36
|
showProgress = true,
|
|
36
37
|
thankYouTitle = "Thank You!",
|
|
37
38
|
thankYouMessage,
|
|
38
39
|
onComplete,
|
|
39
40
|
allowRestart = false,
|
|
41
|
+
readOnly = false,
|
|
40
42
|
className,
|
|
41
43
|
style,
|
|
42
44
|
}: SurveyModuleProps) {
|
|
@@ -98,7 +100,7 @@ export function SurveyModule({
|
|
|
98
100
|
{questions.length} questions
|
|
99
101
|
</Badge>
|
|
100
102
|
</div>
|
|
101
|
-
<Button size="lg" onClick={handleStart}>
|
|
103
|
+
<Button size="lg" onClick={handleStart} disabled={readOnly}>
|
|
102
104
|
<Play className="size-4 mr-2" />
|
|
103
105
|
Begin Survey
|
|
104
106
|
</Button>
|
|
@@ -123,6 +125,7 @@ export function SurveyModule({
|
|
|
123
125
|
requireAll={requireAll}
|
|
124
126
|
showProgress={showProgress}
|
|
125
127
|
onSubmit={handleSubmit}
|
|
128
|
+
readOnly={readOnly}
|
|
126
129
|
/>
|
|
127
130
|
</div>
|
|
128
131
|
);
|
|
@@ -168,7 +171,7 @@ export function SurveyModule({
|
|
|
168
171
|
|
|
169
172
|
{/* Actions */}
|
|
170
173
|
{allowRestart && (
|
|
171
|
-
<Button variant="outline" onClick={handleRestart}>
|
|
174
|
+
<Button variant="outline" onClick={handleRestart} disabled={readOnly}>
|
|
172
175
|
<RotateCcw className="size-4 mr-2" />
|
|
173
176
|
Take Again
|
|
174
177
|
</Button>
|
|
@@ -178,3 +181,5 @@ export function SurveyModule({
|
|
|
178
181
|
</div>
|
|
179
182
|
);
|
|
180
183
|
}
|
|
184
|
+
|
|
185
|
+
export const SurveyModule = withProGate(SurveyModuleBase, "SurveyModule");
|
|
@@ -33,6 +33,8 @@ export interface SurveyModuleProps {
|
|
|
33
33
|
onComplete?: (result: SurveyModuleResult) => void;
|
|
34
34
|
/** Allow restarting the survey from the thank-you screen. @default false */
|
|
35
35
|
allowRestart?: boolean;
|
|
36
|
+
/** When true, disables interactions for preview/demo mode. @default false */
|
|
37
|
+
readOnly?: boolean;
|
|
36
38
|
/** CSS class name for the root element */
|
|
37
39
|
className?: string;
|
|
38
40
|
/** Inline styles for the root element */
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { HelpCircle, Clock, CheckCircle2, Play } from "lucide-react";
|
|
3
|
+
import { Button } from "../../ui/button";
|
|
4
|
+
import { Badge } from "../../ui/badge";
|
|
5
|
+
import { Card, CardContent } from "../../ui/card";
|
|
6
|
+
import { formatDuration } from "../../utils/format-duration";
|
|
7
|
+
|
|
8
|
+
export interface AssessmentIntroProps {
|
|
9
|
+
icon: ReactNode;
|
|
10
|
+
title: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
questionCount: number;
|
|
13
|
+
timeLimitSeconds?: number;
|
|
14
|
+
passingScore?: number;
|
|
15
|
+
startLabel: string;
|
|
16
|
+
onStart: () => void;
|
|
17
|
+
/** Extra content rendered between the description and metadata badges. */
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
/** When true, disables interactions for preview/demo mode. @default false */
|
|
20
|
+
readOnly?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function AssessmentIntro({
|
|
24
|
+
icon,
|
|
25
|
+
title,
|
|
26
|
+
description,
|
|
27
|
+
questionCount,
|
|
28
|
+
timeLimitSeconds,
|
|
29
|
+
passingScore,
|
|
30
|
+
startLabel,
|
|
31
|
+
onStart,
|
|
32
|
+
children,
|
|
33
|
+
readOnly = false,
|
|
34
|
+
}: AssessmentIntroProps) {
|
|
35
|
+
return (
|
|
36
|
+
<Card>
|
|
37
|
+
<CardContent className="pt-8 pb-8 text-center">
|
|
38
|
+
<div className="mx-auto mb-4 w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
|
|
39
|
+
{icon}
|
|
40
|
+
</div>
|
|
41
|
+
<h2 className="text-2xl font-bold text-foreground mb-2">{title}</h2>
|
|
42
|
+
{description && (
|
|
43
|
+
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
|
|
44
|
+
{description}
|
|
45
|
+
</p>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
{children}
|
|
49
|
+
|
|
50
|
+
<div className="flex flex-wrap justify-center gap-2 mb-8">
|
|
51
|
+
<Badge variant="outline" className="gap-1.5">
|
|
52
|
+
<HelpCircle className="size-3.5" />
|
|
53
|
+
{questionCount} questions
|
|
54
|
+
</Badge>
|
|
55
|
+
{timeLimitSeconds != null && (
|
|
56
|
+
<Badge variant="outline" className="gap-1.5">
|
|
57
|
+
<Clock className="size-3.5" />
|
|
58
|
+
{formatDuration(timeLimitSeconds)} time limit
|
|
59
|
+
</Badge>
|
|
60
|
+
)}
|
|
61
|
+
{passingScore !== undefined && (
|
|
62
|
+
<Badge variant="outline" className="gap-1.5">
|
|
63
|
+
<CheckCircle2 className="size-3.5" />
|
|
64
|
+
{passingScore}% to pass
|
|
65
|
+
</Badge>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
<Button size="lg" onClick={onStart} disabled={readOnly}>
|
|
69
|
+
<Play className="size-4 mr-2" />
|
|
70
|
+
{startLabel}
|
|
71
|
+
</Button>
|
|
72
|
+
</CardContent>
|
|
73
|
+
</Card>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { CheckCircle2, XCircle, Trophy, Clock, RotateCcw } from "lucide-react";
|
|
3
|
+
import { ProgressRing } from "../../progress/progress-ring";
|
|
4
|
+
import { StatCard } from "../../progress/stat-card";
|
|
5
|
+
import { Button } from "../../ui/button";
|
|
6
|
+
import { Badge } from "../../ui/badge";
|
|
7
|
+
import { Card, CardContent } from "../../ui/card";
|
|
8
|
+
import { Separator } from "../../ui/separator";
|
|
9
|
+
import { formatDuration } from "../../utils/format-duration";
|
|
10
|
+
import { AssessmentReview } from "../../sections/AssessmentReview/AssessmentReview";
|
|
11
|
+
import type { QuestionData, SessionAnswer } from "../../questions/types";
|
|
12
|
+
|
|
13
|
+
export interface AssessmentResultsProps {
|
|
14
|
+
title: string;
|
|
15
|
+
percentage: number;
|
|
16
|
+
passed: boolean;
|
|
17
|
+
correct: number;
|
|
18
|
+
total: number;
|
|
19
|
+
timeElapsedSeconds: number;
|
|
20
|
+
answers: SessionAnswer[];
|
|
21
|
+
questions: QuestionData[];
|
|
22
|
+
/** Allow retaking the assessment. */
|
|
23
|
+
allowRetake?: boolean;
|
|
24
|
+
onRetake?: () => void;
|
|
25
|
+
retakeLabel?: string;
|
|
26
|
+
/** Show per-question review below the summary. */
|
|
27
|
+
showReview?: boolean;
|
|
28
|
+
/** Extra badges shown next to the pass/fail badge (e.g., "Auto-submitted"). */
|
|
29
|
+
extraBadges?: ReactNode;
|
|
30
|
+
/** When true, disables interactions for preview/demo mode. @default false */
|
|
31
|
+
readOnly?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function AssessmentResults({
|
|
35
|
+
title,
|
|
36
|
+
percentage,
|
|
37
|
+
passed,
|
|
38
|
+
correct,
|
|
39
|
+
total,
|
|
40
|
+
timeElapsedSeconds,
|
|
41
|
+
answers,
|
|
42
|
+
questions,
|
|
43
|
+
allowRetake,
|
|
44
|
+
onRetake,
|
|
45
|
+
retakeLabel = "Retake",
|
|
46
|
+
showReview = true,
|
|
47
|
+
extraBadges,
|
|
48
|
+
readOnly = false,
|
|
49
|
+
}: AssessmentResultsProps) {
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<Card>
|
|
53
|
+
<CardContent className="pt-8 pb-8">
|
|
54
|
+
{/* Score summary */}
|
|
55
|
+
<div className="text-center mb-8">
|
|
56
|
+
<ProgressRing
|
|
57
|
+
value={percentage}
|
|
58
|
+
size={140}
|
|
59
|
+
strokeWidth={10}
|
|
60
|
+
color={passed ? "var(--success)" : "var(--destructive)"}
|
|
61
|
+
className="mx-auto mb-4 text-foreground"
|
|
62
|
+
/>
|
|
63
|
+
<Badge
|
|
64
|
+
variant={passed ? "success" : "destructive"}
|
|
65
|
+
className="text-sm px-3 py-1 mb-2"
|
|
66
|
+
>
|
|
67
|
+
{passed ? "Passed" : "Failed"}
|
|
68
|
+
</Badge>
|
|
69
|
+
{extraBadges}
|
|
70
|
+
<h2 className="text-xl font-bold text-foreground">{title}</h2>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Stats grid */}
|
|
74
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
|
|
75
|
+
<StatCard
|
|
76
|
+
icon={<CheckCircle2 />}
|
|
77
|
+
label="Correct"
|
|
78
|
+
description="Questions answered right"
|
|
79
|
+
value={`${correct}/${total}`}
|
|
80
|
+
accent="var(--success)"
|
|
81
|
+
/>
|
|
82
|
+
<StatCard
|
|
83
|
+
icon={<XCircle />}
|
|
84
|
+
label="Incorrect"
|
|
85
|
+
description="Questions to review"
|
|
86
|
+
value={`${total - correct}/${total}`}
|
|
87
|
+
accent="var(--destructive)"
|
|
88
|
+
/>
|
|
89
|
+
<StatCard
|
|
90
|
+
icon={<Trophy />}
|
|
91
|
+
label="Score"
|
|
92
|
+
description="Overall percentage"
|
|
93
|
+
value={`${percentage}%`}
|
|
94
|
+
accent="var(--palette-3)"
|
|
95
|
+
/>
|
|
96
|
+
<StatCard
|
|
97
|
+
icon={<Clock />}
|
|
98
|
+
label="Time"
|
|
99
|
+
description="Total elapsed"
|
|
100
|
+
value={formatDuration(timeElapsedSeconds)}
|
|
101
|
+
accent="var(--palette-1)"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Actions */}
|
|
106
|
+
{allowRetake && onRetake && !readOnly && (
|
|
107
|
+
<div className="flex justify-center mb-8">
|
|
108
|
+
<Button variant="outline" onClick={onRetake}>
|
|
109
|
+
<RotateCcw className="size-4 mr-2" />
|
|
110
|
+
{retakeLabel}
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</CardContent>
|
|
115
|
+
</Card>
|
|
116
|
+
|
|
117
|
+
{/* Per-question review */}
|
|
118
|
+
{showReview && (
|
|
119
|
+
<>
|
|
120
|
+
<Separator className="my-6" />
|
|
121
|
+
<h3 className="text-lg font-semibold text-foreground mb-4">
|
|
122
|
+
Question Review
|
|
123
|
+
</h3>
|
|
124
|
+
<AssessmentReview
|
|
125
|
+
questions={questions}
|
|
126
|
+
sessionAnswers={answers}
|
|
127
|
+
showCorrectAnswers
|
|
128
|
+
/>
|
|
129
|
+
</>
|
|
130
|
+
)}
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SessionAnswer } from "../../questions/types";
|
|
2
|
+
|
|
3
|
+
/** Shared result shape for scored assessment modules. */
|
|
4
|
+
export interface AssessmentResult {
|
|
5
|
+
answers: SessionAnswer[];
|
|
6
|
+
correct: number;
|
|
7
|
+
total: number;
|
|
8
|
+
percentage: number;
|
|
9
|
+
passed: boolean;
|
|
10
|
+
timeElapsedSeconds: number;
|
|
11
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages an interval-based timer that runs while `active` is true.
|
|
5
|
+
* Returns elapsed seconds and a reset function.
|
|
6
|
+
*/
|
|
7
|
+
export function useTimer(active: boolean) {
|
|
8
|
+
const [timeElapsed, setTimeElapsed] = useState(0);
|
|
9
|
+
const startTimeRef = useRef<number | null>(null);
|
|
10
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
11
|
+
const timeElapsedRef = useRef(0);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (active) {
|
|
15
|
+
startTimeRef.current = Date.now();
|
|
16
|
+
intervalRef.current = setInterval(() => {
|
|
17
|
+
if (startTimeRef.current) {
|
|
18
|
+
const next = Math.floor((Date.now() - startTimeRef.current) / 1000);
|
|
19
|
+
if (next !== timeElapsedRef.current) {
|
|
20
|
+
timeElapsedRef.current = next;
|
|
21
|
+
setTimeElapsed(next);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}, 1000);
|
|
25
|
+
} else {
|
|
26
|
+
if (intervalRef.current) {
|
|
27
|
+
clearInterval(intervalRef.current);
|
|
28
|
+
intervalRef.current = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return () => {
|
|
32
|
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
33
|
+
};
|
|
34
|
+
}, [active]);
|
|
35
|
+
|
|
36
|
+
const getFinalElapsed = useCallback(() => {
|
|
37
|
+
return startTimeRef.current
|
|
38
|
+
? Math.floor((Date.now() - startTimeRef.current) / 1000)
|
|
39
|
+
: timeElapsedRef.current;
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const reset = useCallback(() => {
|
|
43
|
+
timeElapsedRef.current = 0;
|
|
44
|
+
setTimeElapsed(0);
|
|
45
|
+
startTimeRef.current = null;
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
return { timeElapsed, getFinalElapsed, reset };
|
|
49
|
+
}
|
package/src/modules/index.ts
CHANGED
|
@@ -34,3 +34,12 @@ export type { CertificateModuleProps } from "./CertificateModule/types";
|
|
|
34
34
|
|
|
35
35
|
export { DiscussionModule } from "./DiscussionModule/DiscussionModule";
|
|
36
36
|
export type { DiscussionModuleProps } from "./DiscussionModule/types";
|
|
37
|
+
|
|
38
|
+
export { StudentDashboardModule } from "./StudentDashboardModule/StudentDashboardModule";
|
|
39
|
+
export type { StudentDashboardModuleProps } from "./StudentDashboardModule/types";
|
|
40
|
+
|
|
41
|
+
export { CourseCatalogModule } from "./CourseCatalogModule/CourseCatalogModule";
|
|
42
|
+
export type { CourseCatalogModuleProps } from "./CourseCatalogModule/types";
|
|
43
|
+
|
|
44
|
+
export { StudentProfileModule } from "./StudentProfileModule/StudentProfileModule";
|
|
45
|
+
export type { StudentProfileModuleProps } from "./StudentProfileModule/types";
|