@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.
Files changed (302) hide show
  1. package/dist/StudentProfile-BPsZBaJj.cjs +1 -0
  2. package/dist/StudentProfile-Cw2p-RZn.js +3273 -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 +495 -439
  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 +6 -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 +1267 -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/AdaptiveLearningPath/AdaptiveLearningPath.d.ts +5 -0
  74. package/dist/sections/AdaptiveLearningPath/path-connector.d.ts +8 -0
  75. package/dist/sections/AdaptiveLearningPath/path-milestone-marker.d.ts +7 -0
  76. package/dist/sections/AdaptiveLearningPath/path-node-card.d.ts +10 -0
  77. package/dist/sections/AdaptiveLearningPath/path-skill-bar.d.ts +8 -0
  78. package/dist/sections/AdaptiveLearningPath/types.d.ts +136 -0
  79. package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
  80. package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
  81. package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
  82. package/dist/sections/AssessmentReview/types.d.ts +6 -0
  83. package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
  84. package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
  85. package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
  86. package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
  87. package/dist/sections/CertificateViewer/types.d.ts +6 -0
  88. package/dist/sections/ContentAuthoringStudio/ContentAuthoringStudio.d.ts +5 -0
  89. package/dist/sections/ContentAuthoringStudio/block-editor-item.d.ts +14 -0
  90. package/dist/sections/ContentAuthoringStudio/block-type-picker.d.ts +12 -0
  91. package/dist/sections/ContentAuthoringStudio/types.d.ts +67 -0
  92. package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
  93. package/dist/sections/CourseCatalog/types.d.ts +80 -0
  94. package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
  95. package/dist/sections/CourseOutline/types.d.ts +6 -0
  96. package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
  97. package/dist/sections/DiscussionThread/types.d.ts +6 -0
  98. package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
  99. package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
  100. package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
  101. package/dist/sections/ExamSession/types.d.ts +6 -0
  102. package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
  103. package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
  104. package/dist/sections/ForumBoard/ForumBoard.d.ts +1 -1
  105. package/dist/sections/ForumBoard/types.d.ts +14 -0
  106. package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
  107. package/dist/sections/GradebookTable/types.d.ts +14 -0
  108. package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
  109. package/dist/sections/LecturePlayer/types.d.ts +8 -0
  110. package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
  111. package/dist/sections/LessonPage/types.d.ts +6 -0
  112. package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
  113. package/dist/sections/PracticeQuiz/types.d.ts +6 -0
  114. package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
  115. package/dist/sections/ProgressDashboard/types.d.ts +6 -0
  116. package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
  117. package/dist/sections/QuizSession/types.d.ts +6 -0
  118. package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +1 -1
  119. package/dist/sections/RequirementsChecklist/types.d.ts +6 -0
  120. package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
  121. package/dist/sections/ResourceLibrary/types.d.ts +15 -1
  122. package/dist/sections/RubricView/RubricView.d.ts +1 -1
  123. package/dist/sections/RubricView/types.d.ts +6 -0
  124. package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
  125. package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
  126. package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
  127. package/dist/sections/StudentProfile/types.d.ts +98 -0
  128. package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
  129. package/dist/sections/SurveyForm/types.d.ts +6 -0
  130. package/dist/sections/_shared/merge-answers.d.ts +9 -0
  131. package/dist/sections/_shared/section-shell.d.ts +20 -0
  132. package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
  133. package/dist/sections/index.d.ts +10 -0
  134. package/dist/sections.cjs +1 -1
  135. package/dist/sections.js +1361 -307
  136. package/dist/ui/badge.d.ts +1 -1
  137. package/dist/ui/index.d.ts +2 -0
  138. package/dist/ui/progress.d.ts +1 -1
  139. package/dist/ui/rich-text-editor.d.ts +3 -1
  140. package/dist/ui/toast.d.ts +43 -0
  141. package/dist/utils/debounce.d.ts +5 -1
  142. package/dist/utils/pick-palette-color.d.ts +19 -0
  143. package/dist/video/types.d.ts +15 -0
  144. package/dist/video/video-player.d.ts +1 -1
  145. package/dist/withProGate-BJdu1T9Y.cjs +2 -0
  146. package/dist/withProGate-BvFc7Jwy.js +4975 -0
  147. package/package.json +57 -226
  148. package/src/assessment-toolbar/question-navigator.tsx +10 -5
  149. package/src/assessment-toolbar/timer-display.tsx +4 -3
  150. package/src/assessment-toolbar/use-countdown.ts +1 -1
  151. package/src/common/empty-state.tsx +1 -0
  152. package/src/common/index.ts +2 -0
  153. package/src/common/pagination.tsx +135 -0
  154. package/src/common/search-input.tsx +2 -1
  155. package/src/common/types.ts +2 -0
  156. package/src/content/attachment-list.tsx +2 -0
  157. package/src/content/audio-player.tsx +196 -0
  158. package/src/content/code-block.tsx +113 -0
  159. package/src/content/content-block.tsx +64 -0
  160. package/src/content/embed-block.tsx +78 -0
  161. package/src/content/file-upload-zone.tsx +10 -0
  162. package/src/content/index.ts +6 -0
  163. package/src/content/types.ts +5 -0
  164. package/src/curriculum/course-card.tsx +199 -0
  165. package/src/curriculum/curriculum-item.tsx +3 -3
  166. package/src/curriculum/curriculum-tree.tsx +20 -13
  167. package/src/curriculum/index.ts +2 -0
  168. package/src/curriculum/types.ts +2 -2
  169. package/src/flashcards/flashcard.tsx +28 -8
  170. package/src/index.ts +3 -0
  171. package/src/license/HydraContext.tsx +62 -0
  172. package/src/license/ProBadge.tsx +43 -0
  173. package/src/license/index.ts +7 -0
  174. package/src/license/tiers.ts +34 -0
  175. package/src/license/useHydraLicense.ts +10 -0
  176. package/src/license/validate.ts +90 -0
  177. package/src/license/withProGate.tsx +21 -0
  178. package/src/modules/AssignmentModule/AssignmentModule.tsx +17 -8
  179. package/src/modules/AssignmentModule/types.ts +5 -1
  180. package/src/modules/CertificateModule/CertificateModule.tsx +21 -9
  181. package/src/modules/CertificateModule/types.ts +6 -4
  182. package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
  183. package/src/modules/CourseCatalogModule/types.ts +47 -0
  184. package/src/modules/CoursePlayer/CoursePlayer.tsx +39 -22
  185. package/src/modules/DiscussionModule/DiscussionModule.tsx +57 -22
  186. package/src/modules/ExamModule/ExamModule.tsx +64 -198
  187. package/src/modules/ExamModule/types.ts +5 -14
  188. package/src/modules/FlashcardLab/FlashcardLab.tsx +10 -5
  189. package/src/modules/FlashcardLab/types.ts +2 -0
  190. package/src/modules/GradeCenterModule/GradeCenterModule.tsx +7 -2
  191. package/src/modules/GradeCenterModule/types.ts +2 -0
  192. package/src/modules/QuizModule/QuizModule.tsx +49 -169
  193. package/src/modules/QuizModule/types.ts +5 -15
  194. package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
  195. package/src/modules/StudentDashboardModule/types.ts +56 -0
  196. package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
  197. package/src/modules/StudentProfileModule/types.ts +45 -0
  198. package/src/modules/SurveyModule/SurveyModule.tsx +9 -4
  199. package/src/modules/SurveyModule/types.ts +2 -0
  200. package/src/modules/_shared/assessment-intro.tsx +75 -0
  201. package/src/modules/_shared/assessment-results.tsx +133 -0
  202. package/src/modules/_shared/types.ts +11 -0
  203. package/src/modules/_shared/use-timer.ts +49 -0
  204. package/src/modules/index.ts +9 -0
  205. package/src/progress/achievement-badge.tsx +3 -3
  206. package/src/progress/grade-indicator.tsx +9 -1
  207. package/src/progress/progress-ring.tsx +2 -1
  208. package/src/progress/stat-card.tsx +14 -2
  209. package/src/progress/types.ts +2 -0
  210. package/src/provider/HydraProvider.tsx +15 -6
  211. package/src/questions/choice.tsx +13 -6
  212. package/src/questions/confidence-indicator.tsx +107 -0
  213. package/src/questions/essay.tsx +6 -4
  214. package/src/questions/fill-in-the-blank.tsx +8 -4
  215. package/src/questions/hotspot.tsx +4 -4
  216. package/src/questions/index.ts +2 -0
  217. package/src/questions/inline-choice.tsx +5 -4
  218. package/src/questions/matching.tsx +5 -4
  219. package/src/questions/multiple-choice.tsx +13 -6
  220. package/src/questions/numeric.tsx +8 -4
  221. package/src/questions/ordering.tsx +12 -4
  222. package/src/questions/question-renderer.tsx +3 -2
  223. package/src/questions/scenario.tsx +4 -4
  224. package/src/questions/spreadsheet.tsx +5 -4
  225. package/src/questions/true-false.tsx +13 -6
  226. package/src/sections/AdaptiveLearningPath/AdaptiveLearningPath.tsx +251 -0
  227. package/src/sections/AdaptiveLearningPath/path-connector.tsx +27 -0
  228. package/src/sections/AdaptiveLearningPath/path-milestone-marker.tsx +50 -0
  229. package/src/sections/AdaptiveLearningPath/path-node-card.tsx +166 -0
  230. package/src/sections/AdaptiveLearningPath/path-skill-bar.tsx +49 -0
  231. package/src/sections/AdaptiveLearningPath/types.ts +159 -0
  232. package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +64 -8
  233. package/src/sections/AnnouncementFeed/types.ts +15 -1
  234. package/src/sections/AssessmentReview/AssessmentReview.tsx +37 -0
  235. package/src/sections/AssessmentReview/types.ts +6 -0
  236. package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +37 -1
  237. package/src/sections/AssignmentSubmission/types.ts +6 -0
  238. package/src/sections/CertificateViewer/CertificateViewer.tsx +29 -227
  239. package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
  240. package/src/sections/CertificateViewer/types.ts +6 -0
  241. package/src/sections/ContentAuthoringStudio/ContentAuthoringStudio.tsx +289 -0
  242. package/src/sections/ContentAuthoringStudio/block-editor-item.tsx +487 -0
  243. package/src/sections/ContentAuthoringStudio/block-type-picker.tsx +123 -0
  244. package/src/sections/ContentAuthoringStudio/types.ts +67 -0
  245. package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
  246. package/src/sections/CourseCatalog/types.ts +76 -0
  247. package/src/sections/CourseOutline/CourseOutline.tsx +41 -0
  248. package/src/sections/CourseOutline/types.ts +6 -0
  249. package/src/sections/DiscussionThread/DiscussionThread.tsx +42 -1
  250. package/src/sections/DiscussionThread/types.ts +6 -0
  251. package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
  252. package/src/sections/EnrollmentWizard/types.ts +65 -0
  253. package/src/sections/ExamSession/ExamSession.tsx +100 -94
  254. package/src/sections/ExamSession/types.ts +6 -0
  255. package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
  256. package/src/sections/FlashcardStudySession/types.ts +6 -0
  257. package/src/sections/ForumBoard/ForumBoard.tsx +67 -7
  258. package/src/sections/ForumBoard/types.ts +14 -0
  259. package/src/sections/GradebookTable/GradebookTable.tsx +54 -1
  260. package/src/sections/GradebookTable/types.ts +14 -0
  261. package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
  262. package/src/sections/LecturePlayer/types.ts +8 -0
  263. package/src/sections/LessonPage/LessonPage.tsx +34 -6
  264. package/src/sections/LessonPage/types.ts +6 -0
  265. package/src/sections/PracticeQuiz/PracticeQuiz.tsx +106 -74
  266. package/src/sections/PracticeQuiz/types.ts +6 -0
  267. package/src/sections/ProgressDashboard/ProgressDashboard.tsx +64 -10
  268. package/src/sections/ProgressDashboard/types.ts +6 -0
  269. package/src/sections/QuizSession/QuizSession.tsx +71 -82
  270. package/src/sections/QuizSession/types.ts +6 -0
  271. package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +41 -1
  272. package/src/sections/RequirementsChecklist/types.ts +6 -0
  273. package/src/sections/ResourceLibrary/ResourceLibrary.tsx +64 -8
  274. package/src/sections/ResourceLibrary/types.ts +15 -1
  275. package/src/sections/RubricView/RubricView.tsx +37 -1
  276. package/src/sections/RubricView/types.ts +6 -0
  277. package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +36 -15
  278. package/src/sections/ScrollableQuiz/types.ts +6 -0
  279. package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
  280. package/src/sections/StudentProfile/types.ts +99 -0
  281. package/src/sections/SurveyForm/SurveyForm.tsx +32 -5
  282. package/src/sections/SurveyForm/types.ts +6 -0
  283. package/src/sections/_shared/merge-answers.ts +22 -0
  284. package/src/sections/_shared/section-shell.tsx +64 -0
  285. package/src/sections/_shared/use-assessment-session.ts +125 -0
  286. package/src/sections/index.ts +40 -0
  287. package/src/social/user-avatar.tsx +9 -5
  288. package/src/styles/globals.css +39 -41
  289. package/src/ui/badge.tsx +8 -0
  290. package/src/ui/index.ts +2 -0
  291. package/src/ui/progress.tsx +4 -0
  292. package/src/ui/rich-text-editor.tsx +10 -0
  293. package/src/ui/rich-text-toolbar.tsx +2 -1
  294. package/src/ui/toast.tsx +170 -0
  295. package/src/utils/debounce.ts +8 -2
  296. package/src/utils/pick-palette-color.ts +33 -0
  297. package/src/video/types.ts +16 -0
  298. package/src/video/video-player.tsx +27 -6
  299. package/dist/ForumBoard-CHXU3mjC.js +0 -2207
  300. package/dist/ForumBoard-d1w5-r6n.cjs +0 -1
  301. package/dist/tabs-DRM2Iq_J.cjs +0 -172
  302. package/dist/tabs-Wf3h_Cx3.js +0 -21580
@@ -20,6 +20,7 @@ import { Alert, AlertDescription } from "../../ui/alert";
20
20
  import { Separator } from "../../ui/separator";
21
21
  import { cn } from "../../lib/utils";
22
22
  import type { SubmissionData } from "../../sections/AssignmentSubmission/types";
23
+ import { withProGate } from "../../license/withProGate";
23
24
  import type { AssignmentModuleProps } from "./types";
24
25
 
25
26
  type InternalStep =
@@ -39,19 +40,21 @@ const TYPE_LABELS: Record<string, string> = {
39
40
  *
40
41
  * Steps: Instructions → Work (AssignmentSubmission) → Confirmation.
41
42
  */
42
- export function AssignmentModule({
43
+ function AssignmentModuleBase({
43
44
  title,
44
45
  instructions,
45
46
  dueDate,
46
47
  maxScore,
47
- submissionTypes,
48
+ submissionTypes = ["text", "file"],
48
49
  fileConstraints,
49
50
  rubric,
50
51
  existingSubmission,
51
52
  status = "not_started",
52
53
  grade,
53
54
  onSubmit,
55
+ onComplete,
54
56
  onSaveDraft,
57
+ readOnly = false,
55
58
  className,
56
59
  style,
57
60
  }: AssignmentModuleProps) {
@@ -71,7 +74,7 @@ export function AssignmentModule({
71
74
  }, [step.tag]);
72
75
 
73
76
  const rubricMaxScore = useMemo(() => {
74
- if (!rubric) return 0;
77
+ if (!Array.isArray(rubric)) return 0;
75
78
  return rubric.reduce(
76
79
  (sum, c) => sum + Math.max(...c.levels.map((l) => l.points)),
77
80
  0
@@ -80,11 +83,13 @@ export function AssignmentModule({
80
83
 
81
84
  function handleSubmit(submission: SubmissionData) {
82
85
  onSubmit?.(submission);
86
+ onComplete?.({ submission, status: "submitted" });
83
87
  setStep({ tag: "confirmation", submission });
84
88
  }
85
89
 
86
90
  function handleSaveDraft(submission: SubmissionData) {
87
91
  onSaveDraft?.(submission);
92
+ onComplete?.({ submission, status: "draft" });
88
93
  }
89
94
 
90
95
  const canEdit =
@@ -138,7 +143,7 @@ export function AssignmentModule({
138
143
  </div>
139
144
 
140
145
  {/* Rubric preview */}
141
- {rubric && rubric.length > 0 && (
146
+ {Array.isArray(rubric) && rubric.length > 0 && (
142
147
  <>
143
148
  <Separator className="my-6" />
144
149
  <RubricView
@@ -149,11 +154,12 @@ export function AssignmentModule({
149
154
  )}
150
155
 
151
156
  {/* Action */}
152
- {canEdit && (
157
+ {canEdit && !readOnly && (
153
158
  <div className="text-center mt-8">
154
159
  <Button
155
160
  size="lg"
156
161
  onClick={() => setStep({ tag: "work" })}
162
+ disabled={readOnly}
157
163
  >
158
164
  <Play className="size-4 mr-2" />
159
165
  Start Assignment
@@ -196,6 +202,7 @@ export function AssignmentModule({
196
202
  grade={grade}
197
203
  onSubmit={handleSubmit}
198
204
  onSaveDraft={handleSaveDraft}
205
+ readOnly={readOnly}
199
206
  />
200
207
  </div>
201
208
  );
@@ -243,7 +250,7 @@ export function AssignmentModule({
243
250
  <AlertDescription>{grade.feedback}</AlertDescription>
244
251
  </Alert>
245
252
  )}
246
- {rubric && grade.rubricLevels && (
253
+ {Array.isArray(rubric) && grade.rubricLevels && (
247
254
  <>
248
255
  <Separator className="my-6" />
249
256
  <RubricView
@@ -266,7 +273,7 @@ export function AssignmentModule({
266
273
  {submission.textContent && (
267
274
  <div className="flex items-start gap-2">
268
275
  <FileText className="size-4 mt-0.5 shrink-0" />
269
- <span className="line-clamp-2">{submission.textContent}</span>
276
+ <span className="line-clamp-2">{submission.textContent.replace(/<[^>]*>/g, "")}</span>
270
277
  </div>
271
278
  )}
272
279
  {submission.files && submission.files.length > 0 && (
@@ -287,7 +294,7 @@ export function AssignmentModule({
287
294
  </div>
288
295
 
289
296
  {/* Edit button */}
290
- {canEdit && (
297
+ {canEdit && !readOnly && (
291
298
  <div className="text-center mt-8">
292
299
  <Button
293
300
  variant="outline"
@@ -303,3 +310,5 @@ export function AssignmentModule({
303
310
  </div>
304
311
  );
305
312
  }
313
+
314
+ export const AssignmentModule = withProGate(AssignmentModuleBase, "AssignmentModule");
@@ -57,8 +57,12 @@ export interface AssignmentModuleProps {
57
57
  };
58
58
  /** Called on final submission */
59
59
  onSubmit?: (submission: SubmissionData) => void;
60
+ /** Called when the assignment is submitted or draft-saved, with the full result */
61
+ onComplete?: (result: AssignmentModuleResult) => void;
60
62
  /** Called on draft save */
61
63
  onSaveDraft?: (submission: SubmissionData) => void;
64
+ /** When true, disables interactions for preview/demo mode. @default false */
65
+ readOnly?: boolean;
62
66
  /** CSS class name for the root element */
63
67
  className?: string;
64
68
  /** Inline styles for the root element */
@@ -69,5 +73,5 @@ export interface AssignmentModuleResult {
69
73
  /** The submitted data */
70
74
  submission: SubmissionData;
71
75
  /** Status after submission */
72
- status: string;
76
+ status: "not_started" | "draft" | "submitted" | "late" | "graded" | "resubmit";
73
77
  }
@@ -6,6 +6,7 @@ import { ProgressRing } from "../../progress/progress-ring";
6
6
  import { Button } from "../../ui/button";
7
7
  import { Card, CardContent } from "../../ui/card";
8
8
  import { cn } from "../../lib/utils";
9
+ import { withProGate } from "../../license/withProGate";
9
10
  import type { CertificateModuleProps } from "./types";
10
11
 
11
12
  type InternalStep = { tag: "requirements" } | { tag: "certificate" };
@@ -17,7 +18,7 @@ type InternalStep = { tag: "requirements" } | { tag: "certificate" };
17
18
  * Steps: Requirements (checklist + progress) → Certificate (CertificateViewer).
18
19
  * The certificate step is only accessible when all requirements are completed.
19
20
  */
20
- export function CertificateModule({
21
+ function CertificateModuleBase({
21
22
  courseTitle,
22
23
  recipientName,
23
24
  organizationName,
@@ -28,15 +29,20 @@ export function CertificateModule({
28
29
  requirements,
29
30
  overallProgress,
30
31
  onRequirementClick,
31
- onCertificateEarned,
32
+ onComplete,
33
+ readOnly = false,
32
34
  className,
33
35
  style,
34
36
  }: CertificateModuleProps) {
35
- const { allComplete, completedCount } = useMemo(() => {
37
+ const { allComplete, completedCount, derivedProgress } = useMemo(() => {
36
38
  const count = requirements.filter((r) => r.completed).length;
37
- return { allComplete: count === requirements.length, completedCount: count };
39
+ const total = requirements.length;
40
+ const derived = total > 0 ? Math.round((count / total) * 100) : 0;
41
+ return { allComplete: count === total, completedCount: count, derivedProgress: derived };
38
42
  }, [requirements]);
39
43
 
44
+ const displayProgress = overallProgress ?? derivedProgress;
45
+
40
46
  const [step, setStep] = useState<InternalStep>({ tag: "requirements" });
41
47
  const contentRef = useRef<HTMLDivElement>(null);
42
48
  const earnedFiredRef = useRef(false);
@@ -45,13 +51,16 @@ export function CertificateModule({
45
51
  contentRef.current?.focus({ preventScroll: true });
46
52
  }, [step.tag]);
47
53
 
48
- // Fire onCertificateEarned once when certificate step is first shown
54
+ const onCompleteRef = useRef(onComplete);
55
+ onCompleteRef.current = onComplete;
56
+
57
+ // Fire onComplete once when certificate step is first shown
49
58
  useEffect(() => {
50
59
  if (step.tag === "certificate" && !earnedFiredRef.current) {
51
60
  earnedFiredRef.current = true;
52
- onCertificateEarned?.();
61
+ onCompleteRef.current?.();
53
62
  }
54
- }, [step.tag, onCertificateEarned]);
63
+ }, [step.tag]);
55
64
 
56
65
  // ─── Requirements Screen ───
57
66
  if (step.tag === "requirements") {
@@ -67,7 +76,7 @@ export function CertificateModule({
67
76
  {/* Header with progress */}
68
77
  <div className="text-center mb-6">
69
78
  <ProgressRing
70
- value={overallProgress}
79
+ value={displayProgress}
71
80
  size={100}
72
81
  strokeWidth={8}
73
82
  color={
@@ -88,7 +97,7 @@ export function CertificateModule({
88
97
  {/* Requirements checklist */}
89
98
  <RequirementsChecklist
90
99
  requirements={requirements}
91
- onRequirementClick={onRequirementClick}
100
+ onRequirementClick={readOnly ? undefined : onRequirementClick}
92
101
  className="mb-6"
93
102
  />
94
103
 
@@ -98,6 +107,7 @@ export function CertificateModule({
98
107
  <Button
99
108
  size="lg"
100
109
  onClick={() => setStep({ tag: "certificate" })}
110
+ disabled={readOnly}
101
111
  >
102
112
  <Award className="size-4 mr-2" />
103
113
  View Certificate
@@ -159,3 +169,5 @@ export function CertificateModule({
159
169
  </div>
160
170
  );
161
171
  }
172
+
173
+ export const CertificateModule = withProGate(CertificateModuleBase, "CertificateModule");
@@ -34,12 +34,14 @@ export interface CertificateModuleProps {
34
34
  certificateVariant?: CertificateVariant;
35
35
  /** Completion requirements */
36
36
  requirements: Requirement[];
37
- /** Overall course progress (0-100) */
38
- overallProgress: number;
37
+ /** Overall course progress (0-100). When omitted, derived from requirements completion. */
38
+ overallProgress?: number;
39
39
  /** Called when a requirement item is clicked */
40
40
  onRequirementClick?: (uid: string) => void;
41
- /** Called when the certificate screen is first displayed */
42
- onCertificateEarned?: () => void;
41
+ /** Called when the certificate is first displayed. @default undefined */
42
+ onComplete?: () => void;
43
+ /** When true, disables interactions for preview/demo mode. @default false */
44
+ readOnly?: boolean;
43
45
  /** CSS class name for the root element */
44
46
  className?: string;
45
47
  /** Inline styles for the root element */
@@ -0,0 +1,126 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import { ArrowLeft } from "lucide-react";
3
+ import { CourseCatalog } from "../../sections/CourseCatalog/CourseCatalog";
4
+ import { EnrollmentWizard } from "../../sections/EnrollmentWizard/EnrollmentWizard";
5
+ import { Button } from "../../ui/button";
6
+ import { Card, CardContent } from "../../ui/card";
7
+ import { cn } from "../../lib/utils";
8
+ import { withProGate } from "../../license/withProGate";
9
+ import type { CourseCatalogModuleProps } from "./types";
10
+
11
+ type InternalStep =
12
+ | { tag: "browse" }
13
+ | { tag: "enroll"; courseUid: string };
14
+
15
+ /**
16
+ * CourseCatalogModule — a browse-and-enroll course catalog module.
17
+ *
18
+ * Master-detail layout: CourseCatalog (browse) ↔ EnrollmentWizard (enroll).
19
+ * Clicking a course drills into the enrollment flow; a back button returns to browsing.
20
+ */
21
+ function CourseCatalogModuleBase({
22
+ title,
23
+ courses = [],
24
+ categories = [],
25
+ enrollmentData,
26
+ onEnroll,
27
+ onCourseOpen,
28
+ readOnly = false,
29
+ className,
30
+ style,
31
+ }: CourseCatalogModuleProps) {
32
+ const [step, setStep] = useState<InternalStep>({ tag: "browse" });
33
+ const contentRef = useRef<HTMLDivElement>(null);
34
+
35
+ useEffect(() => {
36
+ contentRef.current?.focus({ preventScroll: true });
37
+ }, [step.tag]);
38
+
39
+ function handleCourseClick(courseUid: string) {
40
+ if (enrollmentData?.[courseUid]) {
41
+ setStep({ tag: "enroll", courseUid });
42
+ } else {
43
+ onCourseOpen?.(courseUid);
44
+ }
45
+ }
46
+
47
+ function handleBack() {
48
+ setStep({ tag: "browse" });
49
+ }
50
+
51
+ function handleEnrollConfirm(courseUid: string) {
52
+ onEnroll(courseUid);
53
+ setStep({ tag: "browse" });
54
+ }
55
+
56
+ return (
57
+ <div
58
+ ref={contentRef}
59
+ tabIndex={-1}
60
+ className={cn("outline-none", className)}
61
+ style={style}
62
+ >
63
+ {title && step.tag === "browse" && (
64
+ <h2 className="text-xl font-bold text-foreground mb-4">{title}</h2>
65
+ )}
66
+
67
+ {step.tag === "enroll" ? (
68
+ /* --- Enrollment View --- */
69
+ <div>
70
+ <Button
71
+ variant="ghost"
72
+ size="sm"
73
+ onClick={handleBack}
74
+ className="mb-4"
75
+ >
76
+ <ArrowLeft className="size-4 mr-1.5" />
77
+ Back to Catalog
78
+ </Button>
79
+ {enrollmentData?.[step.courseUid] ? (
80
+ <EnrollmentWizard
81
+ course={enrollmentData[step.courseUid].course}
82
+ prerequisites={enrollmentData[step.courseUid].prerequisites}
83
+ onEnroll={handleEnrollConfirm}
84
+ onCancel={handleBack}
85
+ />
86
+ ) : (
87
+ /* Fallback when enrollment data is not yet available */
88
+ <Card>
89
+ <CardContent className="pt-6">
90
+ <h3 className="text-lg font-semibold text-foreground mb-2">
91
+ {courses.find((c) => c.uid === step.courseUid)?.title ??
92
+ "Course"}
93
+ </h3>
94
+ <p className="text-sm text-muted-foreground mb-4">
95
+ Enrollment details are loading. You can enroll directly below.
96
+ </p>
97
+ <div className="flex gap-2">
98
+ <Button variant="outline" onClick={handleBack}>
99
+ Cancel
100
+ </Button>
101
+ <Button
102
+ onClick={() => handleEnrollConfirm(step.courseUid)}
103
+ disabled={readOnly}
104
+ >
105
+ Enroll Now
106
+ </Button>
107
+ </div>
108
+ </CardContent>
109
+ </Card>
110
+ )}
111
+ </div>
112
+ ) : (
113
+ /* --- Browse View --- */
114
+ <CourseCatalog
115
+ courses={courses}
116
+ categories={categories}
117
+ onCourseClick={(course) => handleCourseClick(course.uid)}
118
+ onEnroll={(course) => handleCourseClick(course.uid)}
119
+ readOnly={readOnly}
120
+ />
121
+ )}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ export const CourseCatalogModule = withProGate(CourseCatalogModuleBase, "CourseCatalogModule");
@@ -0,0 +1,47 @@
1
+ import type { CourseInfo } from "../../sections/CourseCatalog/types";
2
+ import type {
3
+ EnrollmentCourse,
4
+ Prerequisite,
5
+ } from "../../sections/EnrollmentWizard/types";
6
+
7
+ /**
8
+ * CourseCatalogModule — a browse-and-enroll course catalog module.
9
+ *
10
+ * Master-detail layout: CourseCatalog (browse) ↔ EnrollmentWizard (enroll).
11
+ * Clicking a course drills into the enrollment flow; a back button returns to browsing.
12
+ *
13
+ * @example
14
+ * <CourseCatalogModule
15
+ * title="Course Catalog"
16
+ * courses={courses}
17
+ * categories={categories}
18
+ * enrollmentData={enrollmentData}
19
+ * onEnroll={(courseUid) => handleEnroll(courseUid)}
20
+ * />
21
+ */
22
+ export interface CourseCatalogModuleProps {
23
+ /** Catalog title */
24
+ title?: string;
25
+ /** Courses to display */
26
+ courses: CourseInfo[];
27
+ /** Optional categories for filtering */
28
+ categories?: { uid: string; label: string }[];
29
+ /** Enrollment data keyed by course UID — provides course details and prerequisites for the enrollment wizard */
30
+ enrollmentData?: Record<
31
+ string,
32
+ {
33
+ course: EnrollmentCourse;
34
+ prerequisites?: Prerequisite[];
35
+ }
36
+ >;
37
+ /** Called when a user confirms enrollment */
38
+ onEnroll: (courseUid: string) => void;
39
+ /** Called when a course is opened (for lazy-loading enrollment data) */
40
+ onCourseOpen?: (courseUid: string) => void;
41
+ /** When true, disables interactions for preview/demo mode. @default false */
42
+ readOnly?: boolean;
43
+ /** CSS class name for the root element */
44
+ className?: string;
45
+ /** Inline styles for the root element */
46
+ style?: React.CSSProperties;
47
+ }
@@ -1,4 +1,4 @@
1
- import { useState, useMemo, useRef, useEffect } from "react";
1
+ import { useCallback, useState, useMemo, useRef, useEffect } from "react";
2
2
  import {
3
3
  ChevronLeft,
4
4
  ChevronRight,
@@ -23,9 +23,10 @@ import { Button } from "../../ui/button";
23
23
  import { flattenLeaves } from "../../utils/flatten-leaves";
24
24
  import { cn } from "../../lib/utils";
25
25
  import type { CurriculumItem } from "../../curriculum/types";
26
+ import { withProGate } from "../../license/withProGate";
26
27
  import type { CoursePlayerProps, CoursePlayerItem } from "./types";
27
28
 
28
- export function CoursePlayer({
29
+ function CoursePlayerBase({
29
30
  courseTitle,
30
31
  curriculum,
31
32
  progress,
@@ -71,33 +72,49 @@ export function CoursePlayer({
71
72
 
72
73
  const activeItem = itemMap.get(activeUid);
73
74
 
75
+ const videoConfig = useMemo(
76
+ () => activeItem?.type === "video"
77
+ ? { src: activeItem.src, poster: activeItem.poster, title: activeItem.title }
78
+ : null,
79
+ // activeItem is looked up from a stable Map — uid change means new item
80
+ // eslint-disable-next-line react-hooks/exhaustive-deps
81
+ [activeItem?.uid],
82
+ );
83
+
74
84
  useEffect(() => {
75
85
  contentRef.current?.focus({ preventScroll: true });
76
86
  }, [activeUid]);
77
87
 
78
- function navigateTo(uid: string) {
88
+ const onItemChangeRef = useRef(onItemChange);
89
+ onItemChangeRef.current = onItemChange;
90
+ const onItemCompleteRef = useRef(onItemComplete);
91
+ onItemCompleteRef.current = onItemComplete;
92
+
93
+ const navigateTo = useCallback((uid: string) => {
79
94
  setActiveUid(uid);
80
- onItemChange?.(uid);
81
- }
95
+ onItemChangeRef.current?.(uid);
96
+ }, []);
82
97
 
83
- function handleItemClick(item: CurriculumItem) {
98
+ const handleItemClick = useCallback((item: CurriculumItem) => {
84
99
  if (!item.children || item.children.length === 0) {
85
100
  navigateTo(item.uid);
86
101
  }
87
- }
102
+ }, [navigateTo]);
88
103
 
89
- function handleMarkComplete() {
104
+ const handleMarkComplete = useCallback(() => {
90
105
  setCompletedUids((prev) => new Set(prev).add(activeUid));
91
- onItemComplete?.(activeUid);
92
- }
106
+ onItemCompleteRef.current?.(activeUid);
107
+ }, [activeUid]);
93
108
 
94
- function handleNext() {
95
- if (hasNext) navigateTo(leafUids[currentIndex + 1]);
96
- }
109
+ const handleNext = useCallback(() => {
110
+ const idx = leafUids.indexOf(activeUid);
111
+ if (idx < leafUids.length - 1) navigateTo(leafUids[idx + 1]);
112
+ }, [leafUids, activeUid, navigateTo]);
97
113
 
98
- function handlePrevious() {
99
- if (hasPrevious) navigateTo(leafUids[currentIndex - 1]);
100
- }
114
+ const handlePrevious = useCallback(() => {
115
+ const idx = leafUids.indexOf(activeUid);
116
+ if (idx > 0) navigateTo(leafUids[idx - 1]);
117
+ }, [leafUids, activeUid, navigateTo]);
101
118
 
102
119
  // Build combined progress for CourseOutline
103
120
  const combinedProgress = useMemo(() => {
@@ -169,7 +186,7 @@ export function CoursePlayer({
169
186
  {/* Content */}
170
187
  <div ref={contentRef} tabIndex={-1} className="flex-1 overflow-y-auto p-6 outline-none">
171
188
  {activeItem ? (
172
- renderContent(activeItem, readOnly, handleMarkComplete, isCurrentCompleted, handleNext, hasNext, nextItem)
189
+ renderContent(activeItem, readOnly, handleMarkComplete, isCurrentCompleted, handleNext, hasNext, nextItem, videoConfig)
173
190
  ) : (
174
191
  <EmptyState
175
192
  title="No content selected"
@@ -233,6 +250,7 @@ function renderContent(
233
250
  onNext: () => void,
234
251
  hasNext: boolean,
235
252
  nextItem: CoursePlayerItem | null | undefined,
253
+ videoConfig: { src: string; poster?: string; title: string } | null,
236
254
  ) {
237
255
  switch (item.type) {
238
256
  case "lesson":
@@ -250,11 +268,8 @@ function renderContent(
250
268
  case "video":
251
269
  return (
252
270
  <LecturePlayer
253
- video={{
254
- src: item.src,
255
- poster: item.poster,
256
- title: item.title,
257
- }}
271
+ video={videoConfig ?? { src: "", title: item.title }}
272
+ onComplete={isCompleted ? undefined : onMarkComplete}
258
273
  />
259
274
  );
260
275
  case "quiz":
@@ -275,3 +290,5 @@ function renderContent(
275
290
  );
276
291
  }
277
292
  }
293
+
294
+ export const CoursePlayer = withProGate(CoursePlayerBase, "CoursePlayer");
@@ -1,10 +1,12 @@
1
- import { useState, useRef, useEffect } from "react";
1
+ import { useCallback, useState, useRef, useEffect } from "react";
2
2
  import { ArrowLeft } from "lucide-react";
3
3
  import { ForumBoard } from "../../sections/ForumBoard/ForumBoard";
4
4
  import { DiscussionThread } from "../../sections/DiscussionThread/DiscussionThread";
5
+ import { EmptyState } from "../../common";
5
6
  import { Button } from "../../ui/button";
6
7
  import { cn } from "../../lib/utils";
7
8
  import type { ForumSortOrder } from "../../sections/ForumBoard/types";
9
+ import { withProGate } from "../../license/withProGate";
8
10
  import type { DiscussionModuleProps } from "./types";
9
11
 
10
12
  /**
@@ -13,11 +15,11 @@ import type { DiscussionModuleProps } from "./types";
13
15
  * Panel-based layout: ForumBoard (topic listing) ↔ DiscussionThread (thread detail).
14
16
  * Clicking a topic drills into the thread; a back button returns to the board.
15
17
  */
16
- export function DiscussionModule({
18
+ function DiscussionModuleBase({
17
19
  forumTitle,
18
- topics,
20
+ topics = [],
19
21
  currentUser,
20
- threads,
22
+ threads = {},
21
23
  onCreateTopic,
22
24
  onReply,
23
25
  onToggleLike,
@@ -36,20 +38,44 @@ export function DiscussionModule({
36
38
  contentRef.current?.focus({ preventScroll: true });
37
39
  }, [activeTopicUid]);
38
40
 
39
- function handleTopicClick(topicUid: string) {
41
+ const onTopicOpenRef = useRef(onTopicOpen);
42
+ onTopicOpenRef.current = onTopicOpen;
43
+
44
+ const handleTopicClick = useCallback((topicUid: string) => {
40
45
  setActiveTopicUid(topicUid);
41
- onTopicOpen?.(topicUid);
42
- }
46
+ onTopicOpenRef.current?.(topicUid);
47
+ }, []);
43
48
 
44
- function handleBack() {
49
+ const handleBack = useCallback(() => {
45
50
  setActiveTopicUid(null);
46
- }
51
+ }, []);
47
52
 
48
53
  const activeThread = activeTopicUid ? threads[activeTopicUid] : null;
49
54
  const activeTopic = activeTopicUid
50
55
  ? topics.find((t) => t.uid === activeTopicUid)
51
56
  : null;
52
57
 
58
+ const handleReply = useCallback(
59
+ (parentUid: string, content: string) => {
60
+ if (activeTopicUid) onReply?.(activeTopicUid, parentUid, content);
61
+ },
62
+ [activeTopicUid, onReply],
63
+ );
64
+
65
+ const handleToggleLike = useCallback(
66
+ (postUid: string) => {
67
+ if (activeTopicUid) onToggleLike?.(activeTopicUid, postUid);
68
+ },
69
+ [activeTopicUid, onToggleLike],
70
+ );
71
+
72
+ const handleMarkAnswer = useCallback(
73
+ (postUid: string) => {
74
+ if (activeTopicUid) onMarkAnswer?.(activeTopicUid, postUid);
75
+ },
76
+ [activeTopicUid, onMarkAnswer],
77
+ );
78
+
53
79
  return (
54
80
  <div
55
81
  ref={contentRef}
@@ -74,22 +100,29 @@ export function DiscussionModule({
74
100
  rootPost={activeThread.rootPost}
75
101
  replies={activeThread.replies}
76
102
  currentUser={currentUser}
77
- onReply={(parentUid, content) =>
78
- onReply?.(activeTopicUid, parentUid, content)
79
- }
80
- onToggleLike={
81
- onToggleLike
82
- ? (postUid) => onToggleLike(activeTopicUid, postUid)
83
- : undefined
84
- }
85
- onMarkAnswer={
86
- onMarkAnswer
87
- ? (postUid) => onMarkAnswer(activeTopicUid, postUid)
88
- : undefined
89
- }
103
+ onReply={handleReply}
104
+ onToggleLike={onToggleLike ? handleToggleLike : undefined}
105
+ onMarkAnswer={onMarkAnswer ? handleMarkAnswer : undefined}
90
106
  readOnly={readOnly}
91
107
  />
92
108
  </div>
109
+ ) : activeTopicUid && !activeThread ? (
110
+ /* ─── Empty Thread State ─── */
111
+ <div>
112
+ <Button
113
+ variant="ghost"
114
+ size="sm"
115
+ onClick={handleBack}
116
+ className="mb-4"
117
+ >
118
+ <ArrowLeft className="size-4 mr-1.5" />
119
+ Back to Topics
120
+ </Button>
121
+ <EmptyState
122
+ title="No discussion yet"
123
+ description="Be the first to start this conversation."
124
+ />
125
+ </div>
93
126
  ) : (
94
127
  /* ─── Forum Board View ─── */
95
128
  <ForumBoard
@@ -108,3 +141,5 @@ export function DiscussionModule({
108
141
  </div>
109
142
  );
110
143
  }
144
+
145
+ export const DiscussionModule = withProGate(DiscussionModuleBase, "DiscussionModule");