@hydralms/components 0.2.0 → 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.
Files changed (283) hide show
  1. package/dist/StudentProfile-BVfZMbnV.cjs +1 -0
  2. package/dist/StudentProfile-DeMxdrL3.js +3275 -0
  3. package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
  4. package/dist/assessment-toolbar/timer-display.d.ts +1 -1
  5. package/dist/common/index.d.ts +2 -1
  6. package/dist/common/pagination.d.ts +26 -0
  7. package/dist/common/types.d.ts +1 -0
  8. package/dist/components.css +1 -1
  9. package/dist/content/audio-player.d.ts +22 -0
  10. package/dist/content/code-block.d.ts +30 -0
  11. package/dist/content/embed-block.d.ts +28 -0
  12. package/dist/content/index.d.ts +6 -0
  13. package/dist/content/types.d.ts +24 -0
  14. package/dist/curriculum/course-card.d.ts +51 -0
  15. package/dist/curriculum/index.d.ts +2 -0
  16. package/dist/curriculum/types.d.ts +2 -2
  17. package/dist/index.cjs +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +494 -444
  20. package/dist/license/HydraContext.d.ts +16 -0
  21. package/dist/license/ProBadge.d.ts +6 -0
  22. package/dist/license/index.d.ts +7 -0
  23. package/dist/license/tiers.d.ts +3 -0
  24. package/dist/license/useHydraLicense.d.ts +6 -0
  25. package/dist/license/validate.d.ts +13 -0
  26. package/dist/license/withProGate.d.ts +6 -0
  27. package/dist/modules/AssignmentModule/AssignmentModule.d.ts +4 -7
  28. package/dist/modules/AssignmentModule/types.d.ts +5 -1
  29. package/dist/modules/CertificateModule/CertificateModule.d.ts +4 -8
  30. package/dist/modules/CertificateModule/types.d.ts +6 -4
  31. package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
  32. package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
  33. package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
  34. package/dist/modules/DiscussionModule/DiscussionModule.d.ts +4 -7
  35. package/dist/modules/ExamModule/ExamModule.d.ts +4 -7
  36. package/dist/modules/ExamModule/types.d.ts +5 -14
  37. package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
  38. package/dist/modules/FlashcardLab/types.d.ts +2 -0
  39. package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +4 -8
  40. package/dist/modules/GradeCenterModule/types.d.ts +2 -0
  41. package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
  42. package/dist/modules/QuizModule/types.d.ts +5 -14
  43. package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
  44. package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
  45. package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
  46. package/dist/modules/StudentProfileModule/types.d.ts +43 -0
  47. package/dist/modules/SurveyModule/SurveyModule.d.ts +4 -6
  48. package/dist/modules/SurveyModule/types.d.ts +2 -0
  49. package/dist/modules/_shared/assessment-intro.d.ts +16 -0
  50. package/dist/modules/_shared/assessment-results.d.ts +23 -0
  51. package/dist/modules/_shared/types.d.ts +10 -0
  52. package/dist/modules/_shared/use-timer.d.ts +9 -0
  53. package/dist/modules/index.d.ts +6 -0
  54. package/dist/modules.cjs +1 -1
  55. package/dist/modules.js +1266 -854
  56. package/dist/progress/types.d.ts +2 -0
  57. package/dist/provider/HydraProvider.d.ts +5 -1
  58. package/dist/questions/choice.d.ts +1 -1
  59. package/dist/questions/confidence-indicator.d.ts +37 -0
  60. package/dist/questions/essay.d.ts +1 -1
  61. package/dist/questions/fill-in-the-blank.d.ts +1 -1
  62. package/dist/questions/hotspot.d.ts +1 -1
  63. package/dist/questions/index.d.ts +2 -0
  64. package/dist/questions/inline-choice.d.ts +1 -1
  65. package/dist/questions/matching.d.ts +1 -1
  66. package/dist/questions/multiple-choice.d.ts +1 -1
  67. package/dist/questions/numeric.d.ts +1 -1
  68. package/dist/questions/ordering.d.ts +1 -1
  69. package/dist/questions/question-renderer.d.ts +1 -1
  70. package/dist/questions/scenario.d.ts +1 -1
  71. package/dist/questions/spreadsheet.d.ts +1 -1
  72. package/dist/questions/true-false.d.ts +1 -1
  73. package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
  74. package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
  75. package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
  76. package/dist/sections/AssessmentReview/types.d.ts +6 -0
  77. package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
  78. package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
  79. package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
  80. package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
  81. package/dist/sections/CertificateViewer/types.d.ts +6 -0
  82. package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
  83. package/dist/sections/CourseCatalog/types.d.ts +80 -0
  84. package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
  85. package/dist/sections/CourseOutline/types.d.ts +6 -0
  86. package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
  87. package/dist/sections/DiscussionThread/types.d.ts +6 -0
  88. package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
  89. package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
  90. package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
  91. package/dist/sections/ExamSession/types.d.ts +6 -0
  92. package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
  93. package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
  94. package/dist/sections/ForumBoard/ForumBoard.d.ts +1 -1
  95. package/dist/sections/ForumBoard/types.d.ts +14 -0
  96. package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
  97. package/dist/sections/GradebookTable/types.d.ts +14 -0
  98. package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
  99. package/dist/sections/LecturePlayer/types.d.ts +8 -0
  100. package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
  101. package/dist/sections/LessonPage/types.d.ts +6 -0
  102. package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
  103. package/dist/sections/PracticeQuiz/types.d.ts +6 -0
  104. package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
  105. package/dist/sections/ProgressDashboard/types.d.ts +6 -0
  106. package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
  107. package/dist/sections/QuizSession/types.d.ts +6 -0
  108. package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +1 -1
  109. package/dist/sections/RequirementsChecklist/types.d.ts +6 -0
  110. package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
  111. package/dist/sections/ResourceLibrary/types.d.ts +15 -1
  112. package/dist/sections/RubricView/RubricView.d.ts +1 -1
  113. package/dist/sections/RubricView/types.d.ts +6 -0
  114. package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
  115. package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
  116. package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
  117. package/dist/sections/StudentProfile/types.d.ts +98 -0
  118. package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
  119. package/dist/sections/SurveyForm/types.d.ts +6 -0
  120. package/dist/sections/_shared/merge-answers.d.ts +9 -0
  121. package/dist/sections/_shared/section-shell.d.ts +20 -0
  122. package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
  123. package/dist/sections/index.d.ts +6 -0
  124. package/dist/sections.cjs +1 -1
  125. package/dist/sections.js +268 -307
  126. package/dist/tabs-BsfVo2Bl.cjs +173 -0
  127. package/dist/{tabs-Wf3h_Cx3.js → tabs-BuY1iNJE.js} +7532 -6807
  128. package/dist/ui/badge.d.ts +1 -1
  129. package/dist/ui/index.d.ts +2 -0
  130. package/dist/ui/progress.d.ts +1 -1
  131. package/dist/ui/rich-text-editor.d.ts +3 -1
  132. package/dist/ui/toast.d.ts +43 -0
  133. package/dist/utils/debounce.d.ts +5 -1
  134. package/dist/utils/pick-palette-color.d.ts +19 -0
  135. package/dist/video/types.d.ts +15 -0
  136. package/dist/video/video-player.d.ts +1 -1
  137. package/dist/withProGate-BWqcKdPM.js +137 -0
  138. package/dist/withProGate-DX6XqKLp.cjs +1 -0
  139. package/package.json +34 -220
  140. package/src/assessment-toolbar/question-navigator.tsx +10 -5
  141. package/src/assessment-toolbar/timer-display.tsx +4 -3
  142. package/src/assessment-toolbar/use-countdown.ts +1 -1
  143. package/src/common/empty-state.tsx +1 -0
  144. package/src/common/index.ts +2 -0
  145. package/src/common/pagination.tsx +135 -0
  146. package/src/common/search-input.tsx +2 -1
  147. package/src/common/types.ts +2 -0
  148. package/src/content/attachment-list.tsx +2 -0
  149. package/src/content/audio-player.tsx +196 -0
  150. package/src/content/code-block.tsx +113 -0
  151. package/src/content/content-block.tsx +64 -0
  152. package/src/content/embed-block.tsx +78 -0
  153. package/src/content/file-upload-zone.tsx +10 -0
  154. package/src/content/index.ts +6 -0
  155. package/src/content/types.ts +5 -0
  156. package/src/curriculum/course-card.tsx +199 -0
  157. package/src/curriculum/curriculum-item.tsx +3 -3
  158. package/src/curriculum/curriculum-tree.tsx +20 -13
  159. package/src/curriculum/index.ts +2 -0
  160. package/src/curriculum/types.ts +2 -2
  161. package/src/flashcards/flashcard.tsx +28 -8
  162. package/src/index.ts +3 -0
  163. package/src/license/HydraContext.tsx +62 -0
  164. package/src/license/ProBadge.tsx +43 -0
  165. package/src/license/index.ts +7 -0
  166. package/src/license/tiers.ts +24 -0
  167. package/src/license/useHydraLicense.ts +10 -0
  168. package/src/license/validate.ts +90 -0
  169. package/src/license/withProGate.tsx +21 -0
  170. package/src/modules/AssignmentModule/AssignmentModule.tsx +17 -8
  171. package/src/modules/AssignmentModule/types.ts +5 -1
  172. package/src/modules/CertificateModule/CertificateModule.tsx +21 -9
  173. package/src/modules/CertificateModule/types.ts +6 -4
  174. package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
  175. package/src/modules/CourseCatalogModule/types.ts +47 -0
  176. package/src/modules/CoursePlayer/CoursePlayer.tsx +37 -22
  177. package/src/modules/DiscussionModule/DiscussionModule.tsx +57 -22
  178. package/src/modules/ExamModule/ExamModule.tsx +64 -198
  179. package/src/modules/ExamModule/types.ts +5 -14
  180. package/src/modules/FlashcardLab/FlashcardLab.tsx +10 -5
  181. package/src/modules/FlashcardLab/types.ts +2 -0
  182. package/src/modules/GradeCenterModule/GradeCenterModule.tsx +7 -2
  183. package/src/modules/GradeCenterModule/types.ts +2 -0
  184. package/src/modules/QuizModule/QuizModule.tsx +49 -169
  185. package/src/modules/QuizModule/types.ts +5 -15
  186. package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
  187. package/src/modules/StudentDashboardModule/types.ts +56 -0
  188. package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
  189. package/src/modules/StudentProfileModule/types.ts +45 -0
  190. package/src/modules/SurveyModule/SurveyModule.tsx +9 -4
  191. package/src/modules/SurveyModule/types.ts +2 -0
  192. package/src/modules/_shared/assessment-intro.tsx +75 -0
  193. package/src/modules/_shared/assessment-results.tsx +133 -0
  194. package/src/modules/_shared/types.ts +11 -0
  195. package/src/modules/_shared/use-timer.ts +49 -0
  196. package/src/modules/index.ts +9 -0
  197. package/src/progress/achievement-badge.tsx +3 -3
  198. package/src/progress/grade-indicator.tsx +9 -1
  199. package/src/progress/progress-ring.tsx +2 -1
  200. package/src/progress/stat-card.tsx +8 -1
  201. package/src/progress/types.ts +2 -0
  202. package/src/provider/HydraProvider.tsx +15 -6
  203. package/src/questions/choice.tsx +13 -6
  204. package/src/questions/confidence-indicator.tsx +107 -0
  205. package/src/questions/essay.tsx +6 -4
  206. package/src/questions/fill-in-the-blank.tsx +8 -4
  207. package/src/questions/hotspot.tsx +4 -4
  208. package/src/questions/index.ts +2 -0
  209. package/src/questions/inline-choice.tsx +5 -4
  210. package/src/questions/matching.tsx +5 -4
  211. package/src/questions/multiple-choice.tsx +13 -6
  212. package/src/questions/numeric.tsx +8 -4
  213. package/src/questions/ordering.tsx +12 -4
  214. package/src/questions/question-renderer.tsx +3 -2
  215. package/src/questions/scenario.tsx +4 -4
  216. package/src/questions/spreadsheet.tsx +5 -4
  217. package/src/questions/true-false.tsx +13 -6
  218. package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +64 -8
  219. package/src/sections/AnnouncementFeed/types.ts +15 -1
  220. package/src/sections/AssessmentReview/AssessmentReview.tsx +37 -0
  221. package/src/sections/AssessmentReview/types.ts +6 -0
  222. package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +37 -1
  223. package/src/sections/AssignmentSubmission/types.ts +6 -0
  224. package/src/sections/CertificateViewer/CertificateViewer.tsx +29 -227
  225. package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
  226. package/src/sections/CertificateViewer/types.ts +6 -0
  227. package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
  228. package/src/sections/CourseCatalog/types.ts +76 -0
  229. package/src/sections/CourseOutline/CourseOutline.tsx +41 -0
  230. package/src/sections/CourseOutline/types.ts +6 -0
  231. package/src/sections/DiscussionThread/DiscussionThread.tsx +42 -1
  232. package/src/sections/DiscussionThread/types.ts +6 -0
  233. package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
  234. package/src/sections/EnrollmentWizard/types.ts +65 -0
  235. package/src/sections/ExamSession/ExamSession.tsx +100 -94
  236. package/src/sections/ExamSession/types.ts +6 -0
  237. package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
  238. package/src/sections/FlashcardStudySession/types.ts +6 -0
  239. package/src/sections/ForumBoard/ForumBoard.tsx +59 -1
  240. package/src/sections/ForumBoard/types.ts +14 -0
  241. package/src/sections/GradebookTable/GradebookTable.tsx +54 -1
  242. package/src/sections/GradebookTable/types.ts +14 -0
  243. package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
  244. package/src/sections/LecturePlayer/types.ts +8 -0
  245. package/src/sections/LessonPage/LessonPage.tsx +36 -5
  246. package/src/sections/LessonPage/types.ts +6 -0
  247. package/src/sections/PracticeQuiz/PracticeQuiz.tsx +106 -74
  248. package/src/sections/PracticeQuiz/types.ts +6 -0
  249. package/src/sections/ProgressDashboard/ProgressDashboard.tsx +64 -10
  250. package/src/sections/ProgressDashboard/types.ts +6 -0
  251. package/src/sections/QuizSession/QuizSession.tsx +71 -82
  252. package/src/sections/QuizSession/types.ts +6 -0
  253. package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +41 -1
  254. package/src/sections/RequirementsChecklist/types.ts +6 -0
  255. package/src/sections/ResourceLibrary/ResourceLibrary.tsx +64 -8
  256. package/src/sections/ResourceLibrary/types.ts +15 -1
  257. package/src/sections/RubricView/RubricView.tsx +37 -1
  258. package/src/sections/RubricView/types.ts +6 -0
  259. package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +36 -15
  260. package/src/sections/ScrollableQuiz/types.ts +6 -0
  261. package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
  262. package/src/sections/StudentProfile/types.ts +99 -0
  263. package/src/sections/SurveyForm/SurveyForm.tsx +32 -5
  264. package/src/sections/SurveyForm/types.ts +6 -0
  265. package/src/sections/_shared/merge-answers.ts +22 -0
  266. package/src/sections/_shared/section-shell.tsx +64 -0
  267. package/src/sections/_shared/use-assessment-session.ts +125 -0
  268. package/src/sections/index.ts +22 -0
  269. package/src/social/user-avatar.tsx +9 -5
  270. package/src/styles/globals.css +39 -41
  271. package/src/ui/badge.tsx +8 -0
  272. package/src/ui/index.ts +2 -0
  273. package/src/ui/progress.tsx +4 -0
  274. package/src/ui/rich-text-editor.tsx +10 -0
  275. package/src/ui/rich-text-toolbar.tsx +2 -1
  276. package/src/ui/toast.tsx +170 -0
  277. package/src/utils/debounce.ts +8 -2
  278. package/src/utils/pick-palette-color.ts +33 -0
  279. package/src/video/types.ts +16 -0
  280. package/src/video/video-player.tsx +13 -1
  281. package/dist/ForumBoard-CHXU3mjC.js +0 -2207
  282. package/dist/ForumBoard-d1w5-r6n.cjs +0 -1
  283. package/dist/tabs-DRM2Iq_J.cjs +0 -172
@@ -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
- export function SurveyModule({
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
+ }
@@ -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";