@hydralms/components 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/dist/StudentProfile-BVfZMbnV.cjs +1 -0
  2. package/dist/StudentProfile-DeMxdrL3.js +3275 -0
  3. package/dist/assessment-toolbar/assessment-toolbar.d.ts +1 -1
  4. package/dist/assessment-toolbar/index.d.ts +5 -1
  5. package/dist/assessment-toolbar/question-header-bar.d.ts +2 -0
  6. package/dist/assessment-toolbar/question-materials-drawer.d.ts +2 -0
  7. package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
  8. package/dist/assessment-toolbar/timer-display.d.ts +1 -1
  9. package/dist/assessment-toolbar/types.d.ts +52 -4
  10. package/dist/assessment-toolbar/use-countdown.d.ts +43 -0
  11. package/dist/common/index.d.ts +3 -1
  12. package/dist/common/pagination.d.ts +26 -0
  13. package/dist/common/stepper.d.ts +6 -0
  14. package/dist/common/types.d.ts +38 -0
  15. package/dist/components.css +1 -1
  16. package/dist/content/attachment-list.d.ts +6 -0
  17. package/dist/content/audio-player.d.ts +22 -0
  18. package/dist/content/code-block.d.ts +30 -0
  19. package/dist/content/content-block.d.ts +1 -1
  20. package/dist/content/embed-block.d.ts +28 -0
  21. package/dist/content/index.d.ts +8 -1
  22. package/dist/content/types.d.ts +63 -0
  23. package/dist/curriculum/course-card.d.ts +51 -0
  24. package/dist/curriculum/curriculum-item.d.ts +1 -1
  25. package/dist/curriculum/index.d.ts +2 -0
  26. package/dist/curriculum/types.d.ts +2 -2
  27. package/dist/index.cjs +1 -1
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +597 -308
  30. package/dist/license/HydraContext.d.ts +16 -0
  31. package/dist/license/ProBadge.d.ts +6 -0
  32. package/dist/license/index.d.ts +7 -0
  33. package/dist/license/tiers.d.ts +3 -0
  34. package/dist/license/useHydraLicense.d.ts +6 -0
  35. package/dist/license/validate.d.ts +13 -0
  36. package/dist/license/withProGate.d.ts +6 -0
  37. package/dist/modules/AssignmentModule/AssignmentModule.d.ts +5 -0
  38. package/dist/modules/AssignmentModule/types.d.ts +69 -0
  39. package/dist/modules/CertificateModule/CertificateModule.d.ts +5 -0
  40. package/dist/modules/CertificateModule/types.d.ts +51 -0
  41. package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
  42. package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
  43. package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
  44. package/dist/modules/DiscussionModule/DiscussionModule.d.ts +5 -0
  45. package/dist/modules/DiscussionModule/types.d.ts +47 -0
  46. package/dist/modules/ExamModule/ExamModule.d.ts +5 -0
  47. package/dist/modules/ExamModule/types.d.ts +55 -0
  48. package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
  49. package/dist/modules/FlashcardLab/types.d.ts +2 -0
  50. package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +5 -0
  51. package/dist/modules/GradeCenterModule/types.d.ts +56 -0
  52. package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
  53. package/dist/modules/QuizModule/types.d.ts +10 -14
  54. package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
  55. package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
  56. package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
  57. package/dist/modules/StudentProfileModule/types.d.ts +43 -0
  58. package/dist/modules/SurveyModule/SurveyModule.d.ts +5 -0
  59. package/dist/modules/SurveyModule/types.d.ts +51 -0
  60. package/dist/modules/_shared/assessment-intro.d.ts +16 -0
  61. package/dist/modules/_shared/assessment-results.d.ts +23 -0
  62. package/dist/modules/_shared/types.d.ts +10 -0
  63. package/dist/modules/_shared/use-timer.d.ts +9 -0
  64. package/dist/modules/index.d.ts +18 -0
  65. package/dist/modules.cjs +1 -0
  66. package/dist/modules.js +1834 -0
  67. package/dist/progress/achievement-badge.d.ts +6 -0
  68. package/dist/progress/activity-timeline.d.ts +6 -0
  69. package/dist/progress/index.d.ts +4 -1
  70. package/dist/progress/stat-card.d.ts +1 -1
  71. package/dist/progress/streak-badge.d.ts +6 -0
  72. package/dist/progress/types.d.ts +99 -0
  73. package/dist/provider/HydraProvider.d.ts +5 -1
  74. package/dist/questions/choice.d.ts +1 -1
  75. package/dist/questions/confidence-indicator.d.ts +37 -0
  76. package/dist/questions/essay.d.ts +2 -2
  77. package/dist/questions/fill-in-the-blank.d.ts +1 -1
  78. package/dist/questions/hotspot.d.ts +21 -0
  79. package/dist/questions/index.d.ts +11 -1
  80. package/dist/questions/inline-choice.d.ts +21 -0
  81. package/dist/questions/matching.d.ts +22 -0
  82. package/dist/questions/multiple-choice.d.ts +1 -1
  83. package/dist/questions/numeric.d.ts +11 -0
  84. package/dist/questions/ordering.d.ts +12 -0
  85. package/dist/questions/question-renderer.d.ts +1 -1
  86. package/dist/questions/scenario.d.ts +23 -0
  87. package/dist/questions/scoring.d.ts +22 -0
  88. package/dist/questions/spreadsheet.d.ts +29 -0
  89. package/dist/questions/true-false.d.ts +1 -1
  90. package/dist/questions/types.d.ts +106 -1
  91. package/dist/questions/use-drag-reorder.d.ts +17 -0
  92. package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
  93. package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
  94. package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
  95. package/dist/sections/AssessmentReview/types.d.ts +6 -0
  96. package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
  97. package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
  98. package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
  99. package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
  100. package/dist/sections/CertificateViewer/types.d.ts +13 -5
  101. package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
  102. package/dist/sections/CourseCatalog/types.d.ts +80 -0
  103. package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
  104. package/dist/sections/CourseOutline/types.d.ts +6 -0
  105. package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
  106. package/dist/sections/DiscussionThread/types.d.ts +6 -0
  107. package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
  108. package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
  109. package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
  110. package/dist/sections/ExamSession/types.d.ts +12 -1
  111. package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
  112. package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
  113. package/dist/sections/ForumBoard/ForumBoard.d.ts +8 -0
  114. package/dist/sections/ForumBoard/types.d.ts +78 -0
  115. package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
  116. package/dist/sections/GradebookTable/types.d.ts +14 -0
  117. package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
  118. package/dist/sections/LecturePlayer/types.d.ts +8 -0
  119. package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
  120. package/dist/sections/LessonPage/types.d.ts +6 -0
  121. package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
  122. package/dist/sections/PracticeQuiz/types.d.ts +6 -0
  123. package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
  124. package/dist/sections/ProgressDashboard/types.d.ts +6 -0
  125. package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
  126. package/dist/sections/QuizSession/types.d.ts +12 -1
  127. package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
  128. package/dist/sections/RequirementsChecklist/types.d.ts +43 -0
  129. package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
  130. package/dist/sections/ResourceLibrary/types.d.ts +15 -1
  131. package/dist/sections/RubricView/RubricView.d.ts +9 -0
  132. package/dist/sections/RubricView/types.d.ts +56 -0
  133. package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
  134. package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
  135. package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
  136. package/dist/sections/StudentProfile/types.d.ts +98 -0
  137. package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
  138. package/dist/sections/SurveyForm/types.d.ts +6 -0
  139. package/dist/sections/_shared/merge-answers.d.ts +9 -0
  140. package/dist/sections/_shared/section-shell.d.ts +20 -0
  141. package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
  142. package/dist/sections/index.d.ts +13 -1
  143. package/dist/sections.cjs +1 -1
  144. package/dist/sections.js +282 -1786
  145. package/dist/social/post-card.d.ts +1 -1
  146. package/dist/tabs-BsfVo2Bl.cjs +173 -0
  147. package/dist/tabs-BuY1iNJE.js +22305 -0
  148. package/dist/ui/alert.d.ts +1 -1
  149. package/dist/ui/badge.d.ts +1 -1
  150. package/dist/ui/button.d.ts +1 -1
  151. package/dist/ui/drawer.d.ts +84 -0
  152. package/dist/ui/index.d.ts +5 -0
  153. package/dist/ui/progress.d.ts +1 -1
  154. package/dist/ui/rich-text-editor.d.ts +32 -0
  155. package/dist/ui/rich-text-toolbar.d.ts +8 -0
  156. package/dist/ui/toast.d.ts +43 -0
  157. package/dist/utils/array-utils.d.ts +4 -0
  158. package/dist/utils/debounce.d.ts +5 -1
  159. package/dist/utils/flatten-leaves.d.ts +6 -0
  160. package/dist/utils/format-file-size.d.ts +1 -0
  161. package/dist/utils/format-timestamp.d.ts +1 -0
  162. package/dist/utils/is-empty-html.d.ts +5 -0
  163. package/dist/utils/pick-palette-color.d.ts +19 -0
  164. package/dist/utils/shuffle.d.ts +1 -0
  165. package/dist/utils/string-utils.d.ts +12 -0
  166. package/dist/video/types.d.ts +15 -0
  167. package/dist/video/video-bookmark.d.ts +1 -1
  168. package/dist/video/video-player.d.ts +1 -1
  169. package/dist/video/video-playlist-item.d.ts +1 -1
  170. package/dist/withProGate-BWqcKdPM.js +137 -0
  171. package/dist/withProGate-DX6XqKLp.cjs +1 -0
  172. package/package.json +40 -137
  173. package/src/assessment-toolbar/assessment-toolbar.tsx +54 -49
  174. package/src/assessment-toolbar/index.ts +6 -0
  175. package/src/assessment-toolbar/question-header-bar.tsx +61 -0
  176. package/src/assessment-toolbar/question-materials-drawer.tsx +55 -0
  177. package/src/assessment-toolbar/question-navigator.tsx +13 -36
  178. package/src/assessment-toolbar/timer-display.tsx +6 -5
  179. package/src/assessment-toolbar/types.ts +54 -4
  180. package/src/assessment-toolbar/use-countdown.ts +153 -0
  181. package/src/common/empty-state.tsx +1 -0
  182. package/src/common/index.ts +5 -0
  183. package/src/common/pagination.tsx +135 -0
  184. package/src/common/search-input.tsx +8 -6
  185. package/src/common/stepper.tsx +100 -0
  186. package/src/common/types.ts +41 -0
  187. package/src/content/attachment-list.tsx +92 -0
  188. package/src/content/audio-player.tsx +196 -0
  189. package/src/content/code-block.tsx +113 -0
  190. package/src/content/content-block.tsx +68 -2
  191. package/src/content/embed-block.tsx +78 -0
  192. package/src/content/file-upload-zone.tsx +11 -6
  193. package/src/content/index.ts +9 -0
  194. package/src/content/types.ts +46 -0
  195. package/src/curriculum/course-card.tsx +199 -0
  196. package/src/curriculum/curriculum-item.tsx +9 -5
  197. package/src/curriculum/curriculum-tree.tsx +20 -13
  198. package/src/curriculum/index.ts +2 -0
  199. package/src/curriculum/types.ts +2 -2
  200. package/src/feedback/feedback-banner.tsx +12 -14
  201. package/src/flashcards/flashcard-deck.tsx +1 -9
  202. package/src/flashcards/flashcard.tsx +29 -9
  203. package/src/index.ts +3 -0
  204. package/src/license/HydraContext.tsx +62 -0
  205. package/src/license/ProBadge.tsx +43 -0
  206. package/src/license/index.ts +7 -0
  207. package/src/license/tiers.ts +24 -0
  208. package/src/license/useHydraLicense.ts +10 -0
  209. package/src/license/validate.ts +90 -0
  210. package/src/license/withProGate.tsx +21 -0
  211. package/src/modules/AssignmentModule/AssignmentModule.tsx +314 -0
  212. package/src/modules/AssignmentModule/types.ts +77 -0
  213. package/src/modules/CertificateModule/CertificateModule.tsx +173 -0
  214. package/src/modules/CertificateModule/types.ts +49 -0
  215. package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
  216. package/src/modules/CourseCatalogModule/types.ts +47 -0
  217. package/src/modules/CoursePlayer/CoursePlayer.tsx +80 -69
  218. package/src/modules/DiscussionModule/DiscussionModule.tsx +145 -0
  219. package/src/modules/DiscussionModule/types.ts +54 -0
  220. package/src/modules/ExamModule/ExamModule.tsx +151 -0
  221. package/src/modules/ExamModule/types.ts +57 -0
  222. package/src/modules/FlashcardLab/FlashcardLab.tsx +39 -21
  223. package/src/modules/FlashcardLab/types.ts +2 -0
  224. package/src/modules/GradeCenterModule/GradeCenterModule.tsx +174 -0
  225. package/src/modules/GradeCenterModule/types.ts +65 -0
  226. package/src/modules/QuizModule/QuizModule.tsx +58 -178
  227. package/src/modules/QuizModule/types.ts +10 -15
  228. package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
  229. package/src/modules/StudentDashboardModule/types.ts +56 -0
  230. package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
  231. package/src/modules/StudentProfileModule/types.ts +45 -0
  232. package/src/modules/SurveyModule/SurveyModule.tsx +185 -0
  233. package/src/modules/SurveyModule/types.ts +53 -0
  234. package/src/modules/_shared/assessment-intro.tsx +75 -0
  235. package/src/modules/_shared/assessment-results.tsx +133 -0
  236. package/src/modules/_shared/types.ts +11 -0
  237. package/src/modules/_shared/use-timer.ts +49 -0
  238. package/src/modules/index.ts +33 -0
  239. package/src/progress/achievement-badge.tsx +52 -0
  240. package/src/progress/activity-timeline.tsx +84 -0
  241. package/src/progress/grade-indicator.tsx +9 -1
  242. package/src/progress/index.ts +7 -0
  243. package/src/progress/progress-ring.tsx +2 -1
  244. package/src/progress/stat-card.tsx +37 -18
  245. package/src/progress/streak-badge.tsx +35 -0
  246. package/src/progress/types.ts +103 -0
  247. package/src/provider/HydraProvider.tsx +15 -6
  248. package/src/questions/choice.tsx +19 -14
  249. package/src/questions/confidence-indicator.tsx +107 -0
  250. package/src/questions/essay.tsx +28 -28
  251. package/src/questions/fill-in-the-blank.tsx +20 -19
  252. package/src/questions/hotspot.tsx +154 -0
  253. package/src/questions/index.ts +18 -0
  254. package/src/questions/inline-choice.tsx +152 -0
  255. package/src/questions/matching.tsx +229 -0
  256. package/src/questions/multiple-choice.tsx +19 -14
  257. package/src/questions/numeric.tsx +106 -0
  258. package/src/questions/ordering.tsx +167 -0
  259. package/src/questions/question-renderer.tsx +24 -2
  260. package/src/questions/scenario.tsx +140 -0
  261. package/src/questions/scoring.ts +201 -0
  262. package/src/questions/spreadsheet.tsx +260 -0
  263. package/src/questions/true-false.tsx +19 -14
  264. package/src/questions/types.ts +123 -1
  265. package/src/questions/use-drag-reorder.ts +80 -0
  266. package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +66 -23
  267. package/src/sections/AnnouncementFeed/types.ts +15 -1
  268. package/src/sections/AssessmentReview/AssessmentReview.tsx +50 -2
  269. package/src/sections/AssessmentReview/types.ts +6 -0
  270. package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +44 -6
  271. package/src/sections/AssignmentSubmission/types.ts +6 -0
  272. package/src/sections/CertificateViewer/CertificateViewer.tsx +215 -60
  273. package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
  274. package/src/sections/CertificateViewer/types.ts +19 -5
  275. package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
  276. package/src/sections/CourseCatalog/types.ts +76 -0
  277. package/src/sections/CourseOutline/CourseOutline.tsx +45 -14
  278. package/src/sections/CourseOutline/types.ts +6 -0
  279. package/src/sections/DiscussionThread/DiscussionThread.tsx +55 -11
  280. package/src/sections/DiscussionThread/types.ts +6 -0
  281. package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
  282. package/src/sections/EnrollmentWizard/types.ts +65 -0
  283. package/src/sections/ExamSession/ExamSession.tsx +125 -82
  284. package/src/sections/ExamSession/types.ts +12 -1
  285. package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
  286. package/src/sections/FlashcardStudySession/types.ts +6 -0
  287. package/src/sections/ForumBoard/ForumBoard.tsx +342 -0
  288. package/src/sections/ForumBoard/types.ts +81 -0
  289. package/src/sections/GradebookTable/GradebookTable.tsx +55 -2
  290. package/src/sections/GradebookTable/types.ts +14 -0
  291. package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
  292. package/src/sections/LecturePlayer/types.ts +8 -0
  293. package/src/sections/LessonPage/LessonPage.tsx +40 -13
  294. package/src/sections/LessonPage/types.ts +6 -0
  295. package/src/sections/PracticeQuiz/PracticeQuiz.tsx +119 -98
  296. package/src/sections/PracticeQuiz/types.ts +6 -0
  297. package/src/sections/ProgressDashboard/ProgressDashboard.tsx +121 -67
  298. package/src/sections/ProgressDashboard/types.ts +6 -0
  299. package/src/sections/QuizSession/QuizSession.tsx +115 -67
  300. package/src/sections/QuizSession/types.ts +12 -1
  301. package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +147 -0
  302. package/src/sections/RequirementsChecklist/types.ts +44 -0
  303. package/src/sections/ResourceLibrary/ResourceLibrary.tsx +68 -17
  304. package/src/sections/ResourceLibrary/types.ts +15 -1
  305. package/src/sections/RubricView/RubricView.tsx +174 -0
  306. package/src/sections/RubricView/types.ts +58 -0
  307. package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +58 -23
  308. package/src/sections/ScrollableQuiz/types.ts +6 -0
  309. package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
  310. package/src/sections/StudentProfile/types.ts +99 -0
  311. package/src/sections/SurveyForm/SurveyForm.tsx +40 -10
  312. package/src/sections/SurveyForm/types.ts +6 -0
  313. package/src/sections/_shared/merge-answers.ts +22 -0
  314. package/src/sections/_shared/section-shell.tsx +64 -0
  315. package/src/sections/_shared/use-assessment-session.ts +125 -0
  316. package/src/sections/index.ts +42 -1
  317. package/src/social/post-card.tsx +8 -19
  318. package/src/social/user-avatar.tsx +10 -5
  319. package/src/styles/globals.css +52 -41
  320. package/src/ui/badge.tsx +8 -0
  321. package/src/ui/drawer.tsx +600 -0
  322. package/src/ui/index.ts +21 -0
  323. package/src/ui/progress.tsx +4 -0
  324. package/src/ui/rich-text-editor.tsx +119 -0
  325. package/src/ui/rich-text-toolbar.tsx +157 -0
  326. package/src/ui/toast.tsx +170 -0
  327. package/src/utils/array-utils.ts +17 -0
  328. package/src/utils/debounce.ts +8 -2
  329. package/src/utils/flatten-leaves.ts +17 -0
  330. package/src/utils/format-file-size.ts +5 -0
  331. package/src/utils/format-timestamp.ts +13 -0
  332. package/src/utils/is-empty-html.ts +7 -0
  333. package/src/utils/pick-palette-color.ts +33 -0
  334. package/src/utils/shuffle.ts +8 -0
  335. package/src/utils/string-utils.ts +30 -0
  336. package/src/video/types.ts +16 -0
  337. package/src/video/video-bookmark.tsx +4 -3
  338. package/src/video/video-chapter-list.tsx +9 -4
  339. package/src/video/video-player.tsx +24 -5
  340. package/src/video/video-playlist-item.tsx +8 -3
  341. package/src/video/video-thumbnail-card.tsx +4 -0
  342. package/src/video/video-transcript.tsx +8 -5
  343. package/dist/table-BrS5cDQu.js +0 -2510
  344. package/dist/table-D6AkBBEo.cjs +0 -1
@@ -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
+ }
@@ -0,0 +1,314 @@
1
+ import { useState, useRef, useEffect, useMemo } from "react";
2
+ import {
3
+ FileEdit,
4
+ CheckCircle2,
5
+ Play,
6
+ ArrowLeft,
7
+ FileText,
8
+ Link as LinkIcon,
9
+ Paperclip,
10
+ } from "lucide-react";
11
+ import { AssignmentSubmission } from "../../sections/AssignmentSubmission/AssignmentSubmission";
12
+ import { RubricView } from "../../sections/RubricView/RubricView";
13
+ import { GradeIndicator } from "../../progress/grade-indicator";
14
+ import { StatusBadge } from "../../common/status-badge";
15
+ import { DueDateDisplay } from "../../common/due-date-display";
16
+ import { Button } from "../../ui/button";
17
+ import { Badge } from "../../ui/badge";
18
+ import { Card, CardContent } from "../../ui/card";
19
+ import { Alert, AlertDescription } from "../../ui/alert";
20
+ import { Separator } from "../../ui/separator";
21
+ import { cn } from "../../lib/utils";
22
+ import type { SubmissionData } from "../../sections/AssignmentSubmission/types";
23
+ import { withProGate } from "../../license/withProGate";
24
+ import type { AssignmentModuleProps } from "./types";
25
+
26
+ type InternalStep =
27
+ | { tag: "instructions" }
28
+ | { tag: "work" }
29
+ | { tag: "confirmation"; submission: SubmissionData };
30
+
31
+ const TYPE_LABELS: Record<string, string> = {
32
+ text: "Text Entry",
33
+ file: "File Upload",
34
+ url: "URL Submission",
35
+ };
36
+
37
+ /**
38
+ * AssignmentModule — a complete assignment experience with instructions,
39
+ * submission work area, and confirmation/grade review.
40
+ *
41
+ * Steps: Instructions → Work (AssignmentSubmission) → Confirmation.
42
+ */
43
+ function AssignmentModuleBase({
44
+ title,
45
+ instructions,
46
+ dueDate,
47
+ maxScore,
48
+ submissionTypes = ["text", "file"],
49
+ fileConstraints,
50
+ rubric,
51
+ existingSubmission,
52
+ status = "not_started",
53
+ grade,
54
+ onSubmit,
55
+ onComplete,
56
+ onSaveDraft,
57
+ readOnly = false,
58
+ className,
59
+ style,
60
+ }: AssignmentModuleProps) {
61
+ const initialStep: InternalStep =
62
+ status === "submitted" || status === "graded"
63
+ ? {
64
+ tag: "confirmation",
65
+ submission: existingSubmission ?? {},
66
+ }
67
+ : { tag: "instructions" };
68
+
69
+ const [step, setStep] = useState<InternalStep>(initialStep);
70
+ const contentRef = useRef<HTMLDivElement>(null);
71
+
72
+ useEffect(() => {
73
+ contentRef.current?.focus({ preventScroll: true });
74
+ }, [step.tag]);
75
+
76
+ const rubricMaxScore = useMemo(() => {
77
+ if (!Array.isArray(rubric)) return 0;
78
+ return rubric.reduce(
79
+ (sum, c) => sum + Math.max(...c.levels.map((l) => l.points)),
80
+ 0
81
+ );
82
+ }, [rubric]);
83
+
84
+ function handleSubmit(submission: SubmissionData) {
85
+ onSubmit?.(submission);
86
+ onComplete?.({ submission, status: "submitted" });
87
+ setStep({ tag: "confirmation", submission });
88
+ }
89
+
90
+ function handleSaveDraft(submission: SubmissionData) {
91
+ onSaveDraft?.(submission);
92
+ onComplete?.({ submission, status: "draft" });
93
+ }
94
+
95
+ const canEdit =
96
+ status === "not_started" ||
97
+ status === "draft" ||
98
+ status === "resubmit";
99
+
100
+ // ─── Instructions Screen ───
101
+ if (step.tag === "instructions") {
102
+ return (
103
+ <div
104
+ ref={contentRef}
105
+ tabIndex={-1}
106
+ className={cn("max-w-2xl mx-auto outline-none", className)}
107
+ style={style}
108
+ >
109
+ <Card>
110
+ <CardContent className="pt-8 pb-8">
111
+ <div className="text-center mb-6">
112
+ <div className="mx-auto mb-4 w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
113
+ <FileEdit className="size-7 text-primary" />
114
+ </div>
115
+ <h2 className="text-2xl font-bold text-foreground mb-2">
116
+ {title}
117
+ </h2>
118
+ </div>
119
+
120
+ {/* Metadata chips */}
121
+ <div className="flex flex-wrap justify-center gap-2 mb-6">
122
+ {dueDate && (
123
+ <DueDateDisplay dueDate={dueDate} size="small" />
124
+ )}
125
+ {maxScore !== undefined && (
126
+ <Badge variant="outline" className="gap-1.5">
127
+ <CheckCircle2 className="size-3.5" />
128
+ {maxScore} points
129
+ </Badge>
130
+ )}
131
+ <Badge variant="outline" className="gap-1.5">
132
+ <Paperclip className="size-3.5" />
133
+ {submissionTypes.map((t) => TYPE_LABELS[t]).join(", ")}
134
+ </Badge>
135
+ <StatusBadge status={status} />
136
+ </div>
137
+
138
+ <Separator className="my-6" />
139
+
140
+ {/* Instructions content */}
141
+ <div className="prose prose-sm text-foreground mb-6">
142
+ {instructions}
143
+ </div>
144
+
145
+ {/* Rubric preview */}
146
+ {Array.isArray(rubric) && rubric.length > 0 && (
147
+ <>
148
+ <Separator className="my-6" />
149
+ <RubricView
150
+ criteria={rubric}
151
+ maxScore={rubricMaxScore}
152
+ />
153
+ </>
154
+ )}
155
+
156
+ {/* Action */}
157
+ {canEdit && !readOnly && (
158
+ <div className="text-center mt-8">
159
+ <Button
160
+ size="lg"
161
+ onClick={() => setStep({ tag: "work" })}
162
+ disabled={readOnly}
163
+ >
164
+ <Play className="size-4 mr-2" />
165
+ Start Assignment
166
+ </Button>
167
+ </div>
168
+ )}
169
+ </CardContent>
170
+ </Card>
171
+ </div>
172
+ );
173
+ }
174
+
175
+ // ─── Work Screen ───
176
+ if (step.tag === "work") {
177
+ return (
178
+ <div
179
+ ref={contentRef}
180
+ tabIndex={-1}
181
+ className={cn("outline-none", className)}
182
+ style={style}
183
+ >
184
+ <Button
185
+ variant="ghost"
186
+ size="sm"
187
+ onClick={() => setStep({ tag: "instructions" })}
188
+ className="mb-4"
189
+ >
190
+ <ArrowLeft className="size-4 mr-1.5" />
191
+ Back to Instructions
192
+ </Button>
193
+ <AssignmentSubmission
194
+ title={title}
195
+ instructions={instructions}
196
+ dueDate={dueDate}
197
+ maxScore={maxScore}
198
+ status={status}
199
+ submissionTypes={submissionTypes}
200
+ existingSubmission={existingSubmission}
201
+ fileConstraints={fileConstraints}
202
+ grade={grade}
203
+ onSubmit={handleSubmit}
204
+ onSaveDraft={handleSaveDraft}
205
+ readOnly={readOnly}
206
+ />
207
+ </div>
208
+ );
209
+ }
210
+
211
+ // ─── Confirmation Screen ───
212
+ const { submission } = step;
213
+
214
+ return (
215
+ <div
216
+ ref={contentRef}
217
+ tabIndex={-1}
218
+ className={cn("max-w-2xl mx-auto outline-none", className)}
219
+ style={style}
220
+ >
221
+ <Card>
222
+ <CardContent className="pt-8 pb-8">
223
+ <div className="text-center mb-6">
224
+ <div className="mx-auto mb-4 w-14 h-14 rounded-full bg-success/10 flex items-center justify-center">
225
+ <CheckCircle2 className="size-7 text-success" />
226
+ </div>
227
+ <h2 className="text-xl font-bold text-foreground mb-2">
228
+ {status === "graded" ? "Assignment Graded" : "Submission Received"}
229
+ </h2>
230
+ <div className="flex justify-center gap-2 mb-4">
231
+ <StatusBadge status={status} />
232
+ {dueDate && <DueDateDisplay dueDate={dueDate} size="small" />}
233
+ </div>
234
+ </div>
235
+
236
+ {/* Grade display */}
237
+ {status === "graded" && grade && (
238
+ <div className="text-center mb-6">
239
+ <GradeIndicator
240
+ percentage={
241
+ maxScore ? Math.round((grade.score / maxScore) * 100) : 0
242
+ }
243
+ size="large"
244
+ />
245
+ <p className="text-sm text-muted-foreground mt-2">
246
+ {grade.score}/{maxScore} points
247
+ </p>
248
+ {grade.feedback && (
249
+ <Alert className="text-left mt-4">
250
+ <AlertDescription>{grade.feedback}</AlertDescription>
251
+ </Alert>
252
+ )}
253
+ {Array.isArray(rubric) && grade.rubricLevels && (
254
+ <>
255
+ <Separator className="my-6" />
256
+ <RubricView
257
+ criteria={rubric}
258
+ selectedLevels={grade.rubricLevels}
259
+ totalScore={grade.score}
260
+ maxScore={rubricMaxScore}
261
+ />
262
+ </>
263
+ )}
264
+ </div>
265
+ )}
266
+
267
+ {/* Submission summary */}
268
+ <Separator className="my-6" />
269
+ <h3 className="text-sm font-semibold text-foreground mb-3">
270
+ What You Submitted
271
+ </h3>
272
+ <div className="space-y-2 text-sm text-muted-foreground">
273
+ {submission.textContent && (
274
+ <div className="flex items-start gap-2">
275
+ <FileText className="size-4 mt-0.5 shrink-0" />
276
+ <span className="line-clamp-2">{submission.textContent.replace(/<[^>]*>/g, "")}</span>
277
+ </div>
278
+ )}
279
+ {submission.files && submission.files.length > 0 && (
280
+ <div className="flex items-start gap-2">
281
+ <Paperclip className="size-4 mt-0.5 shrink-0" />
282
+ <span>
283
+ {submission.files.length} file
284
+ {submission.files.length !== 1 ? "s" : ""} uploaded
285
+ </span>
286
+ </div>
287
+ )}
288
+ {submission.url && (
289
+ <div className="flex items-start gap-2">
290
+ <LinkIcon className="size-4 mt-0.5 shrink-0" />
291
+ <span className="truncate">{submission.url}</span>
292
+ </div>
293
+ )}
294
+ </div>
295
+
296
+ {/* Edit button */}
297
+ {canEdit && !readOnly && (
298
+ <div className="text-center mt-8">
299
+ <Button
300
+ variant="outline"
301
+ onClick={() => setStep({ tag: "work" })}
302
+ >
303
+ <FileEdit className="size-4 mr-2" />
304
+ Edit Submission
305
+ </Button>
306
+ </div>
307
+ )}
308
+ </CardContent>
309
+ </Card>
310
+ </div>
311
+ );
312
+ }
313
+
314
+ export const AssignmentModule = withProGate(AssignmentModuleBase, "AssignmentModule");
@@ -0,0 +1,77 @@
1
+ import type { ReactNode } from "react";
2
+ import type { SubmissionData } from "../../sections/AssignmentSubmission/types";
3
+ import type { RubricCriterion } from "../../sections/RubricView/types";
4
+
5
+ /**
6
+ * AssignmentModule — a complete assignment experience with instructions,
7
+ * submission, and confirmation/grade review.
8
+ *
9
+ * Steps: Instructions → Work (AssignmentSubmission) → Confirmation.
10
+ * Optionally shows a grading rubric in both the instructions and
11
+ * graded confirmation views.
12
+ *
13
+ * @example
14
+ * <AssignmentModule
15
+ * title="Week 3 Essay"
16
+ * instructions={<p>Write a 500-word essay on React hooks.</p>}
17
+ * submissionTypes={["text", "file"]}
18
+ * rubric={rubricCriteria}
19
+ * onSubmit={(submission) => submitAssignment(submission)}
20
+ * />
21
+ */
22
+ export interface AssignmentModuleProps {
23
+ /** Assignment title */
24
+ title: string;
25
+ /** Assignment instructions (rich content) */
26
+ instructions: ReactNode;
27
+ /** Due date as ISO string */
28
+ dueDate?: string;
29
+ /** Maximum score points */
30
+ maxScore?: number;
31
+ /** Allowed submission types */
32
+ submissionTypes: ("text" | "file" | "url")[];
33
+ /** File upload constraints */
34
+ fileConstraints?: {
35
+ maxFiles?: number;
36
+ maxSizeMB?: number;
37
+ acceptedTypes?: string;
38
+ };
39
+ /** Rubric criteria for grading (shown in instructions and graded confirmation) */
40
+ rubric?: RubricCriterion[];
41
+ /** Existing submission for editing/viewing */
42
+ existingSubmission?: SubmissionData;
43
+ /** Current submission status. @default "not_started" */
44
+ status?:
45
+ | "not_started"
46
+ | "draft"
47
+ | "submitted"
48
+ | "late"
49
+ | "graded"
50
+ | "resubmit";
51
+ /** Grade data when graded */
52
+ grade?: {
53
+ score: number;
54
+ feedback?: ReactNode;
55
+ /** Selected rubric level UIDs per criterion UID */
56
+ rubricLevels?: Record<string, string>;
57
+ };
58
+ /** Called on final submission */
59
+ onSubmit?: (submission: SubmissionData) => void;
60
+ /** Called when the assignment is submitted or draft-saved, with the full result */
61
+ onComplete?: (result: AssignmentModuleResult) => void;
62
+ /** Called on draft save */
63
+ onSaveDraft?: (submission: SubmissionData) => void;
64
+ /** When true, disables interactions for preview/demo mode. @default false */
65
+ readOnly?: boolean;
66
+ /** CSS class name for the root element */
67
+ className?: string;
68
+ /** Inline styles for the root element */
69
+ style?: React.CSSProperties;
70
+ }
71
+
72
+ export interface AssignmentModuleResult {
73
+ /** The submitted data */
74
+ submission: SubmissionData;
75
+ /** Status after submission */
76
+ status: "not_started" | "draft" | "submitted" | "late" | "graded" | "resubmit";
77
+ }
@@ -0,0 +1,173 @@
1
+ import { useState, useRef, useEffect, useMemo } from "react";
2
+ import { Award, ArrowLeft, PartyPopper } from "lucide-react";
3
+ import { RequirementsChecklist } from "../../sections/RequirementsChecklist/RequirementsChecklist";
4
+ import { CertificateViewer } from "../../sections/CertificateViewer/CertificateViewer";
5
+ import { ProgressRing } from "../../progress/progress-ring";
6
+ import { Button } from "../../ui/button";
7
+ import { Card, CardContent } from "../../ui/card";
8
+ import { cn } from "../../lib/utils";
9
+ import { withProGate } from "../../license/withProGate";
10
+ import type { CertificateModuleProps } from "./types";
11
+
12
+ type InternalStep = { tag: "requirements" } | { tag: "certificate" };
13
+
14
+ /**
15
+ * CertificateModule — a certificate-earning flow with requirements tracking
16
+ * and certificate display.
17
+ *
18
+ * Steps: Requirements (checklist + progress) → Certificate (CertificateViewer).
19
+ * The certificate step is only accessible when all requirements are completed.
20
+ */
21
+ function CertificateModuleBase({
22
+ courseTitle,
23
+ recipientName,
24
+ organizationName,
25
+ organizationLogo,
26
+ signatory,
27
+ completionDate,
28
+ certificateVariant = "modern",
29
+ requirements,
30
+ overallProgress,
31
+ onRequirementClick,
32
+ onComplete,
33
+ readOnly = false,
34
+ className,
35
+ style,
36
+ }: CertificateModuleProps) {
37
+ const { allComplete, completedCount, derivedProgress } = useMemo(() => {
38
+ const count = requirements.filter((r) => r.completed).length;
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 };
42
+ }, [requirements]);
43
+
44
+ const displayProgress = overallProgress ?? derivedProgress;
45
+
46
+ const [step, setStep] = useState<InternalStep>({ tag: "requirements" });
47
+ const contentRef = useRef<HTMLDivElement>(null);
48
+ const earnedFiredRef = useRef(false);
49
+
50
+ useEffect(() => {
51
+ contentRef.current?.focus({ preventScroll: true });
52
+ }, [step.tag]);
53
+
54
+ const onCompleteRef = useRef(onComplete);
55
+ onCompleteRef.current = onComplete;
56
+
57
+ // Fire onComplete once when certificate step is first shown
58
+ useEffect(() => {
59
+ if (step.tag === "certificate" && !earnedFiredRef.current) {
60
+ earnedFiredRef.current = true;
61
+ onCompleteRef.current?.();
62
+ }
63
+ }, [step.tag]);
64
+
65
+ // ─── Requirements Screen ───
66
+ if (step.tag === "requirements") {
67
+ return (
68
+ <div
69
+ ref={contentRef}
70
+ tabIndex={-1}
71
+ className={cn("max-w-2xl mx-auto outline-none", className)}
72
+ style={style}
73
+ >
74
+ <Card>
75
+ <CardContent className="pt-8 pb-8">
76
+ {/* Header with progress */}
77
+ <div className="text-center mb-6">
78
+ <ProgressRing
79
+ value={displayProgress}
80
+ size={100}
81
+ strokeWidth={8}
82
+ color={
83
+ allComplete ? "var(--success)" : "var(--primary)"
84
+ }
85
+ className="mx-auto mb-4 text-foreground"
86
+ />
87
+ <h2 className="text-xl font-bold text-foreground mb-2">
88
+ {courseTitle}
89
+ </h2>
90
+ <p className="text-sm text-muted-foreground">
91
+ {allComplete
92
+ ? "All requirements met — your certificate is ready!"
93
+ : `${completedCount} of ${requirements.length} requirements complete`}
94
+ </p>
95
+ </div>
96
+
97
+ {/* Requirements checklist */}
98
+ <RequirementsChecklist
99
+ requirements={requirements}
100
+ onRequirementClick={readOnly ? undefined : onRequirementClick}
101
+ className="mb-6"
102
+ />
103
+
104
+ {/* View Certificate button */}
105
+ {allComplete && (
106
+ <div className="text-center mt-6">
107
+ <Button
108
+ size="lg"
109
+ onClick={() => setStep({ tag: "certificate" })}
110
+ disabled={readOnly}
111
+ >
112
+ <Award className="size-4 mr-2" />
113
+ View Certificate
114
+ </Button>
115
+ </div>
116
+ )}
117
+ </CardContent>
118
+ </Card>
119
+ </div>
120
+ );
121
+ }
122
+
123
+ // ─── Certificate Screen ───
124
+ return (
125
+ <div
126
+ ref={contentRef}
127
+ tabIndex={-1}
128
+ className={cn("outline-none", className)}
129
+ style={style}
130
+ >
131
+ {/* Back link */}
132
+ <Button
133
+ variant="ghost"
134
+ size="sm"
135
+ onClick={() => setStep({ tag: "requirements" })}
136
+ className="mb-4"
137
+ >
138
+ <ArrowLeft className="size-4 mr-1.5" />
139
+ Back to Requirements
140
+ </Button>
141
+
142
+ {/* Congratulations header */}
143
+ <div className="text-center mb-8">
144
+ <div className="mx-auto mb-4 w-14 h-14 rounded-full bg-success/10 flex items-center justify-center">
145
+ <PartyPopper className="size-7 text-success" />
146
+ </div>
147
+ <h2 className="text-2xl font-bold text-foreground mb-2">
148
+ Congratulations!
149
+ </h2>
150
+ <p className="text-muted-foreground">
151
+ You&apos;ve completed all requirements for{" "}
152
+ <span className="font-medium text-foreground">{courseTitle}</span>
153
+ </p>
154
+ </div>
155
+
156
+ {/* Certificate */}
157
+ <CertificateViewer
158
+ recipientName={recipientName}
159
+ courseTitle={courseTitle}
160
+ completionDate={
161
+ completionDate ?? new Date().toISOString().split("T")[0]
162
+ }
163
+ organizationName={organizationName}
164
+ organizationLogo={organizationLogo}
165
+ signatory={signatory}
166
+ variant={certificateVariant}
167
+ showActions
168
+ />
169
+ </div>
170
+ );
171
+ }
172
+
173
+ export const CertificateModule = withProGate(CertificateModuleBase, "CertificateModule");