@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,90 @@
1
+ // ─── Shared License Validation ──────────────────────────────
2
+
3
+ const CACHE_KEY = 'hydra_key_v';
4
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
5
+
6
+ export type KeyResult = 'valid-pro' | 'valid-free' | 'invalid' | 'no-key' | 'skip';
7
+
8
+ export interface ValidationResponse {
9
+ valid: boolean;
10
+ plan: 'pro' | 'enterprise' | null;
11
+ }
12
+
13
+ /** Read a cached validation result from sessionStorage. */
14
+ function readCache(rawKey: string): KeyResult | null {
15
+ try {
16
+ const cached = sessionStorage.getItem(CACHE_KEY);
17
+ if (cached) {
18
+ const { result, ts, key } = JSON.parse(cached) as { result: KeyResult; ts: number; key: string };
19
+ if (key === rawKey && Date.now() - ts < CACHE_TTL) return result;
20
+ }
21
+ } catch { /* sessionStorage unavailable */ }
22
+ return null;
23
+ }
24
+
25
+ /** Write a validation result to sessionStorage cache. */
26
+ function writeCache(rawKey: string, result: KeyResult): void {
27
+ try {
28
+ sessionStorage.setItem(CACHE_KEY, JSON.stringify({ result, ts: Date.now(), key: rawKey }));
29
+ } catch { /* full or unavailable */ }
30
+ }
31
+
32
+ /** On network failure, trust a previously cached valid-pro result. */
33
+ function fallbackFromCache(rawKey: string): KeyResult {
34
+ try {
35
+ const cached = sessionStorage.getItem(CACHE_KEY);
36
+ if (cached) {
37
+ const { result, key } = JSON.parse(cached) as { result: KeyResult; key: string };
38
+ if (key === rawKey && result === 'valid-pro') return 'valid-pro';
39
+ }
40
+ } catch { /* ignore */ }
41
+ return 'invalid';
42
+ }
43
+
44
+ /**
45
+ * Validate a license key against the validation endpoint.
46
+ * @param rawKey - The license key string, or null if not provided.
47
+ * @param validateUrl - The validation endpoint URL. Empty string skips validation (dev mode).
48
+ */
49
+ export async function validateKey(rawKey: string | null, validateUrl: string): Promise<KeyResult> {
50
+ // No validation URL configured (local dev) — skip entirely
51
+ if (!validateUrl) return 'skip';
52
+
53
+ // No key provided
54
+ if (!rawKey) return 'no-key';
55
+
56
+ // Check sessionStorage cache
57
+ const cached = readCache(rawKey);
58
+ if (cached) return cached;
59
+
60
+ // Network validation
61
+ try {
62
+ const res = await fetch(`${validateUrl}?key=${encodeURIComponent(rawKey)}`, {
63
+ method: 'GET',
64
+ signal: AbortSignal.timeout(4000),
65
+ });
66
+
67
+ if (!res.ok) return fallbackFromCache(rawKey);
68
+
69
+ const data = await res.json() as ValidationResponse;
70
+ let result: KeyResult;
71
+
72
+ if (!data.valid) {
73
+ result = 'invalid';
74
+ } else if (data.plan === 'pro' || data.plan === 'enterprise') {
75
+ result = 'valid-pro';
76
+ } else {
77
+ result = 'valid-free';
78
+ }
79
+
80
+ writeCache(rawKey, result);
81
+ return result;
82
+ } catch {
83
+ return fallbackFromCache(rawKey);
84
+ }
85
+ }
86
+
87
+ /** Whether a watermark should be shown for the given key result. */
88
+ export function shouldShowWatermark(result: KeyResult): boolean {
89
+ return result === 'no-key' || result === 'invalid' || result === 'valid-free';
90
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { useHydraLicense } from './useHydraLicense';
3
+ import { ProBadge } from './ProBadge';
4
+
5
+ /** Higher-order component that wraps a pro-tier module with ProBadge when unlicensed. */
6
+ export function withProGate<P extends object>(
7
+ Component: React.ComponentType<P>,
8
+ moduleName: string,
9
+ ) {
10
+ const Gated = (props: P) => {
11
+ const { isPro } = useHydraLicense();
12
+ if (isPro) return <Component {...props} />;
13
+ return (
14
+ <ProBadge feature={moduleName}>
15
+ <Component {...props} />
16
+ </ProBadge>
17
+ );
18
+ };
19
+ Gated.displayName = `ProGated(${moduleName})`;
20
+ return Gated;
21
+ }
@@ -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,47 @@ 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],
80
+ );
81
+
74
82
  useEffect(() => {
75
83
  contentRef.current?.focus({ preventScroll: true });
76
84
  }, [activeUid]);
77
85
 
78
- function navigateTo(uid: string) {
86
+ const onItemChangeRef = useRef(onItemChange);
87
+ onItemChangeRef.current = onItemChange;
88
+ const onItemCompleteRef = useRef(onItemComplete);
89
+ onItemCompleteRef.current = onItemComplete;
90
+
91
+ const navigateTo = useCallback((uid: string) => {
79
92
  setActiveUid(uid);
80
- onItemChange?.(uid);
81
- }
93
+ onItemChangeRef.current?.(uid);
94
+ }, []);
82
95
 
83
- function handleItemClick(item: CurriculumItem) {
96
+ const handleItemClick = useCallback((item: CurriculumItem) => {
84
97
  if (!item.children || item.children.length === 0) {
85
98
  navigateTo(item.uid);
86
99
  }
87
- }
100
+ }, [navigateTo]);
88
101
 
89
- function handleMarkComplete() {
102
+ const handleMarkComplete = useCallback(() => {
90
103
  setCompletedUids((prev) => new Set(prev).add(activeUid));
91
- onItemComplete?.(activeUid);
92
- }
104
+ onItemCompleteRef.current?.(activeUid);
105
+ }, [activeUid]);
93
106
 
94
- function handleNext() {
95
- if (hasNext) navigateTo(leafUids[currentIndex + 1]);
96
- }
107
+ const handleNext = useCallback(() => {
108
+ const idx = leafUids.indexOf(activeUid);
109
+ if (idx < leafUids.length - 1) navigateTo(leafUids[idx + 1]);
110
+ }, [leafUids, activeUid, navigateTo]);
97
111
 
98
- function handlePrevious() {
99
- if (hasPrevious) navigateTo(leafUids[currentIndex - 1]);
100
- }
112
+ const handlePrevious = useCallback(() => {
113
+ const idx = leafUids.indexOf(activeUid);
114
+ if (idx > 0) navigateTo(leafUids[idx - 1]);
115
+ }, [leafUids, activeUid, navigateTo]);
101
116
 
102
117
  // Build combined progress for CourseOutline
103
118
  const combinedProgress = useMemo(() => {
@@ -169,7 +184,7 @@ export function CoursePlayer({
169
184
  {/* Content */}
170
185
  <div ref={contentRef} tabIndex={-1} className="flex-1 overflow-y-auto p-6 outline-none">
171
186
  {activeItem ? (
172
- renderContent(activeItem, readOnly, handleMarkComplete, isCurrentCompleted, handleNext, hasNext, nextItem)
187
+ renderContent(activeItem, readOnly, handleMarkComplete, isCurrentCompleted, handleNext, hasNext, nextItem, videoConfig)
173
188
  ) : (
174
189
  <EmptyState
175
190
  title="No content selected"
@@ -233,6 +248,7 @@ function renderContent(
233
248
  onNext: () => void,
234
249
  hasNext: boolean,
235
250
  nextItem: CoursePlayerItem | null | undefined,
251
+ videoConfig: { src: string; poster?: string; title: string } | null,
236
252
  ) {
237
253
  switch (item.type) {
238
254
  case "lesson":
@@ -250,11 +266,8 @@ function renderContent(
250
266
  case "video":
251
267
  return (
252
268
  <LecturePlayer
253
- video={{
254
- src: item.src,
255
- poster: item.poster,
256
- title: item.title,
257
- }}
269
+ video={videoConfig ?? { src: "", title: item.title }}
270
+ onComplete={isCompleted ? undefined : onMarkComplete}
258
271
  />
259
272
  );
260
273
  case "quiz":
@@ -275,3 +288,5 @@ function renderContent(
275
288
  );
276
289
  }
277
290
  }
291
+
292
+ export const CoursePlayer = withProGate(CoursePlayerBase, "CoursePlayer");