@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
@@ -1,26 +1,24 @@
1
+ import { useMemo } from "react";
2
+ import { BookOpen, Clock, AlertCircle } from "lucide-react";
1
3
  import {
2
- Award,
3
- BookOpen,
4
- CheckCircle,
5
- Clock,
6
- Flame,
7
- Send,
8
- Trophy,
9
- } from "lucide-react";
10
- import { ProgressRing, StatCard } from "../../progress";
4
+ ProgressRing,
5
+ StatCard,
6
+ AchievementBadge,
7
+ StreakBadge,
8
+ ActivityTimeline,
9
+ } from "../../progress";
10
+ import type { TimelineEvent } from "../../progress";
11
11
  import { Progress } from "../../ui/progress";
12
- import { Card } from "../../ui/card";
12
+ import { Card, CardContent } from "../../ui/card";
13
+ import { Separator } from "../../ui/separator";
14
+ import { Skeleton } from "../../ui/skeleton";
15
+ import { Button } from "../../ui/button";
16
+ import { EmptyState } from "../../common/empty-state";
13
17
  import { formatDuration } from "../../utils/format-duration";
18
+ import { pickPaletteVariant } from "../../utils/pick-palette-color";
14
19
  import type { ProgressDashboardProps } from "./types";
15
20
  import { cn } from "../../lib/utils";
16
21
 
17
- const ACTIVITY_ICONS = {
18
- lesson_completed: BookOpen,
19
- quiz_passed: CheckCircle,
20
- assignment_submitted: Send,
21
- badge_earned: Award,
22
- };
23
-
24
22
  export function ProgressDashboard({
25
23
  overallProgress,
26
24
  totalTimeSpent,
@@ -30,58 +28,122 @@ export function ProgressDashboard({
30
28
  achievements,
31
29
  recentActivityLimit = 5,
32
30
  onModuleClick,
31
+ isLoading,
32
+ error,
33
+ onRetry,
33
34
  className,
34
35
  style,
35
36
  }: ProgressDashboardProps) {
37
+ const completedModuleCount = useMemo(
38
+ () => modules.filter((m) => m.completedItems === m.totalItems).length,
39
+ [modules],
40
+ );
41
+
42
+ const timelineEvents = useMemo<TimelineEvent[]>(
43
+ () =>
44
+ recentActivity?.map((activity) => ({
45
+ uid: activity.uid,
46
+ type: activity.type,
47
+ title: activity.description,
48
+ timestamp: activity.timestamp,
49
+ })) ?? [],
50
+ [recentActivity],
51
+ );
52
+
53
+ if (isLoading) {
54
+ return (
55
+ <div className={cn("space-y-4", className)} style={style}>
56
+ <div className="grid grid-cols-3 gap-3">
57
+ <Skeleton className="h-24" />
58
+ <Skeleton className="h-24" />
59
+ <Skeleton className="h-24" />
60
+ </div>
61
+ <Skeleton className="h-32 w-32 rounded-full mx-auto" />
62
+ <Skeleton className="h-8 w-full" />
63
+ <Skeleton className="h-8 w-full" />
64
+ <Skeleton className="h-8 w-full" />
65
+ </div>
66
+ );
67
+ }
68
+
69
+ if (error) {
70
+ return (
71
+ <div className={cn("py-12", className)} style={style}>
72
+ <EmptyState
73
+ icon={<AlertCircle className="size-10 text-destructive" />}
74
+ title="Something went wrong"
75
+ description={error}
76
+ action={
77
+ onRetry ? (
78
+ <Button variant="outline" onClick={onRetry}>
79
+ Retry
80
+ </Button>
81
+ ) : undefined
82
+ }
83
+ />
84
+ </div>
85
+ );
86
+ }
87
+
36
88
  return (
37
89
  <div className={className} style={style}>
38
90
  {/* Stats row */}
39
- <div className="grid grid-cols-[repeat(auto-fit,minmax(160px,1fr))] gap-2 mb-3">
40
- <Card className="p-2 flex justify-center">
41
- <ProgressRing value={overallProgress} size={100} />
91
+ <div className="grid grid-cols-[repeat(auto-fit,minmax(160px,1fr))] gap-3 mb-4">
92
+ <Card>
93
+ <CardContent className="p-3 flex justify-center">
94
+ <ProgressRing value={overallProgress} size={100} />
95
+ </CardContent>
42
96
  </Card>
43
97
  <StatCard
44
98
  icon={<Clock size={24} />}
45
99
  label="Time Spent"
100
+ description="Total learning time"
46
101
  value={formatDuration(totalTimeSpent)}
47
102
  />
48
103
  {streak && (
49
- <StatCard
50
- icon={<Flame size={24} />}
51
- label="Current Streak"
52
- value={`${streak.currentDays} days`}
53
- subtitle={`Longest: ${streak.longestDays} days`}
54
- />
104
+ <Card>
105
+ <CardContent className="p-4 flex items-center">
106
+ <StreakBadge
107
+ currentStreak={streak.currentDays}
108
+ longestStreak={streak.longestDays}
109
+ showLongest
110
+ />
111
+ </CardContent>
112
+ </Card>
55
113
  )}
56
114
  <StatCard
57
115
  icon={<BookOpen size={24} />}
58
116
  label="Modules"
59
- value={`${modules.filter((m) => m.completedItems === m.totalItems).length} / ${modules.length}`}
117
+ description="Course progress"
118
+ value={`${completedModuleCount} / ${modules.length}`}
60
119
  subtitle="completed"
61
120
  />
62
121
  </div>
63
122
 
64
123
  {/* Module progress */}
124
+ <Separator className="mb-3" />
65
125
  <p className="text-lg font-semibold mb-2 text-foreground">Module Progress</p>
66
- <div className="flex flex-col gap-2 mb-3">
67
- {modules.map((mod) => {
126
+ <div className="flex flex-col gap-2 mb-4">
127
+ {modules.map((mod, index) => {
68
128
  const pct = mod.totalItems > 0 ? (mod.completedItems / mod.totalItems) * 100 : 0;
69
129
  return (
70
130
  <Card
71
131
  key={mod.uid}
72
132
  className={cn(
73
- "p-2 transition-colors",
133
+ "transition-colors",
74
134
  onModuleClick && "cursor-pointer hover:border-primary",
75
135
  )}
76
136
  onClick={() => onModuleClick?.(mod.uid)}
77
137
  >
78
- <div className="flex justify-between items-center mb-0.5">
79
- <span className="font-semibold text-sm text-foreground">{mod.name}</span>
80
- <span className="text-xs text-muted-foreground">
81
- {mod.completedItems} / {mod.totalItems}
82
- </span>
83
- </div>
84
- <Progress value={pct} size="sm" />
138
+ <CardContent className="p-3">
139
+ <div className="flex justify-between items-center mb-0.5">
140
+ <span className="font-semibold text-sm text-foreground">{mod.name}</span>
141
+ <span className="text-xs text-muted-foreground">
142
+ {mod.completedItems} / {mod.totalItems}
143
+ </span>
144
+ </div>
145
+ <Progress value={pct} size="sm" variant={pct >= 100 ? "success" : pickPaletteVariant(index)} />
146
+ </CardContent>
85
147
  </Card>
86
148
  );
87
149
  })}
@@ -90,22 +152,13 @@ export function ProgressDashboard({
90
152
  {/* Recent activity */}
91
153
  {recentActivity && recentActivity.length > 0 && (
92
154
  <>
155
+ <Separator className="mb-3" />
93
156
  <p className="text-lg font-semibold mb-2 text-foreground">Recent Activity</p>
94
- <div className="flex flex-col gap-1.5 mb-3">
95
- {recentActivity.slice(0, recentActivityLimit).map((activity) => {
96
- const Icon = ACTIVITY_ICONS[activity.type] ?? CheckCircle;
97
- return (
98
- <div key={activity.uid} className="flex gap-1.5 items-center">
99
- <Icon size={16} />
100
- <span className="flex-1 text-sm text-foreground">
101
- {activity.description}
102
- </span>
103
- <span className="text-xs text-muted-foreground">
104
- {new Date(activity.timestamp).toLocaleDateString()}
105
- </span>
106
- </div>
107
- );
108
- })}
157
+ <div className="mb-4">
158
+ <ActivityTimeline
159
+ events={timelineEvents}
160
+ limit={recentActivityLimit}
161
+ />
109
162
  </div>
110
163
  </>
111
164
  )}
@@ -113,24 +166,25 @@ export function ProgressDashboard({
113
166
  {/* Achievements */}
114
167
  {achievements && achievements.length > 0 && (
115
168
  <>
169
+ <Separator className="mb-3" />
116
170
  <p className="text-lg font-semibold mb-2 text-foreground">Achievements</p>
117
171
  <div className="grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-2">
118
172
  {achievements.map((badge) => (
119
- <Card key={badge.uid} className="p-2 text-center">
120
- {badge.iconUrl ? (
121
- <img
122
- src={badge.iconUrl}
123
- alt={badge.name}
124
- className="w-12 h-12 mb-1 mx-auto"
125
- />
126
- ) : (
127
- <Trophy size={32} className="mx-auto mb-2 text-warning" />
128
- )}
129
- <p className="font-semibold text-sm text-foreground">{badge.name}</p>
130
- <p className="text-xs text-muted-foreground">
131
- {badge.description}
132
- </p>
133
- </Card>
173
+ <AchievementBadge
174
+ key={badge.uid}
175
+ title={badge.name}
176
+ description={badge.description}
177
+ icon={
178
+ badge.iconUrl ? (
179
+ <img
180
+ src={badge.iconUrl}
181
+ alt={badge.name}
182
+ className="w-12 h-12"
183
+ />
184
+ ) : undefined
185
+ }
186
+ earnedDate={badge.earnedAt}
187
+ />
134
188
  ))}
135
189
  </div>
136
190
  </>
@@ -30,6 +30,12 @@ export interface ProgressDashboardProps {
30
30
  recentActivityLimit?: number;
31
31
  /** Called when the user clicks a module */
32
32
  onModuleClick?: (moduleUid: string) => void;
33
+ /** Render skeleton placeholders instead of content */
34
+ isLoading?: boolean;
35
+ /** Error message — renders an error state with optional retry */
36
+ error?: string | null;
37
+ /** Called when the user clicks retry in the error state */
38
+ onRetry?: () => void;
33
39
  /** CSS class name for the root element */
34
40
  className?: string;
35
41
  /** Inline styles for the root element */
@@ -1,11 +1,14 @@
1
- import { useMemo, useState } from "react";
2
- import { AssessmentToolbar } from "../../assessment-toolbar";
3
- import type { QuestionNavigatorItem } from "../../assessment-toolbar/types";
1
+ import { useCallback, useRef } from "react";
2
+ import { ChevronLeft, ChevronRight, Send } from "lucide-react";
3
+ import { AssessmentToolbar, QuestionHeaderBar, QuestionMaterialsDrawer } from "../../assessment-toolbar";
4
4
  import { QuestionRenderer } from "../../questions";
5
- import type { SessionAnswer } from "../../questions/types";
6
- import { Card, CardContent } from "../../ui/card";
5
+ import { Button } from "../../ui/button";
6
+ import { Card, CardHeader, CardContent } from "../../ui/card";
7
+ import { Skeleton } from "../../ui/skeleton";
8
+
9
+ import { useAssessmentSession } from "../_shared/use-assessment-session";
10
+ import { SectionShell } from "../_shared/section-shell";
7
11
  import type { QuizSessionProps } from "./types";
8
- import { cn } from "../../lib/utils";
9
12
 
10
13
  export function QuizSession({
11
14
  questions,
@@ -14,100 +17,145 @@ export function QuizSession({
14
17
  onAnswerChange,
15
18
  timeElapsedSeconds,
16
19
  timeLimitSeconds,
20
+ questionMaterials,
17
21
  isSubmitting = false,
18
22
  readOnly = false,
23
+ isLoading,
24
+ error,
25
+ onRetry,
19
26
  className,
20
27
  style,
21
28
  }: QuizSessionProps) {
22
- const [currentIndex, setCurrentIndex] = useState(0);
23
- const [sessionAnswers, setSessionAnswers] =
24
- useState<SessionAnswer[]>(initialAnswers);
25
- const [flaggedUids, setFlaggedUids] = useState<Set<string>>(new Set());
26
-
27
- const currentQuestion = questions[currentIndex];
29
+ const {
30
+ currentIndex,
31
+ currentQuestion,
32
+ sessionAnswers,
33
+ flaggedUids,
34
+ materialsOpen,
35
+ setMaterialsOpen,
36
+ questionAreaRef,
37
+ currentQuestionAnswers,
38
+ currentMaterials,
39
+ navigatorItems,
40
+ handleAnswer,
41
+ handleNavigate,
42
+ handleToggleFlag,
43
+ goNext,
44
+ goPrevious,
45
+ hasNext,
46
+ hasPrevious,
47
+ } = useAssessmentSession({ questions, initialAnswers, onAnswerChange, questionMaterials });
28
48
 
29
- const navigatorItems = useMemo<QuestionNavigatorItem[]>(
30
- () =>
31
- questions.map((q, idx) => ({
32
- uid: q.uid,
33
- sequence: idx + 1,
34
- isFlagged: flaggedUids.has(q.uid),
35
- isAnswered: sessionAnswers.some((a) => a.uid === q.uid),
36
- isSkipped: false,
37
- })),
38
- [questions, sessionAnswers, flaggedUids],
39
- );
49
+ const sessionAnswersRef = useRef(sessionAnswers);
50
+ sessionAnswersRef.current = sessionAnswers;
51
+ const onSubmitRef = useRef(onSubmit);
52
+ onSubmitRef.current = onSubmit;
40
53
 
41
- function handleAnswer(rawAnswers: { uid: string; content?: string }[]) {
42
- if (!currentQuestion) return;
43
- const questionUid = currentQuestion.uid;
44
- const newAnswers: SessionAnswer[] = rawAnswers.map((a) => ({
45
- uid: questionUid,
46
- answerUid: a.uid,
47
- content: a.content,
48
- }));
49
- setSessionAnswers((prev) => {
50
- const filtered = prev.filter((a) => a.uid !== questionUid);
51
- const merged = [...filtered, ...newAnswers];
52
- onAnswerChange?.(merged);
53
- return merged;
54
- });
55
- }
54
+ const handleSubmit = useCallback(() => {
55
+ onSubmitRef.current(sessionAnswersRef.current);
56
+ }, []);
56
57
 
57
- function handleNavigate(uid: string) {
58
- const idx = questions.findIndex((q) => q.uid === uid);
59
- if (idx !== -1) setCurrentIndex(idx);
60
- }
58
+ const currentQuestionUidRef = useRef(currentQuestion?.uid);
59
+ currentQuestionUidRef.current = currentQuestion?.uid;
61
60
 
62
- function handleToggleFlag(uid: string) {
63
- setFlaggedUids((prev) => {
64
- const next = new Set(prev);
65
- if (next.has(uid)) {
66
- next.delete(uid);
67
- } else {
68
- next.add(uid);
69
- }
70
- return next;
71
- });
72
- }
61
+ const toggleCurrentFlag = useCallback(() => {
62
+ const uid = currentQuestionUidRef.current;
63
+ if (uid) handleToggleFlag(uid);
64
+ }, [handleToggleFlag]);
73
65
 
74
- function handleSubmit() {
75
- onSubmit(sessionAnswers);
76
- }
66
+ const openMaterials = useCallback(() => setMaterialsOpen(true), [setMaterialsOpen]);
77
67
 
78
68
  return (
79
- <div className={cn(className)} style={style}>
69
+ <SectionShell
70
+ isLoading={isLoading}
71
+ error={error}
72
+ onRetry={onRetry}
73
+ className={className}
74
+ style={style}
75
+ skeleton={
76
+ <>
77
+ <Skeleton className="h-10 w-full" />
78
+ <Skeleton className="h-48 w-full" />
79
+ <Skeleton className="h-12 w-full" />
80
+ </>
81
+ }
82
+ >
83
+ <div>
80
84
  <AssessmentToolbar
81
85
  currentQuestionIndex={currentIndex}
82
86
  totalQuestions={questions.length}
83
- hasNext={currentIndex < questions.length - 1}
84
- hasPrevious={currentIndex > 0}
85
- onNext={() => setCurrentIndex((i) => Math.min(i + 1, questions.length - 1))}
86
- onPrevious={() => setCurrentIndex((i) => Math.max(i - 1, 0))}
87
+ hasNext={hasNext}
88
+ hasPrevious={hasPrevious}
89
+ onNext={goNext}
90
+ onPrevious={goPrevious}
87
91
  onSubmit={handleSubmit}
88
92
  timeElapsedSeconds={timeElapsedSeconds}
89
93
  timeLimitSeconds={timeLimitSeconds}
90
94
  questions={navigatorItems}
91
95
  onNavigateToQuestion={handleNavigate}
92
- onToggleFlag={handleToggleFlag}
93
96
  currentQuestionUid={currentQuestion?.uid}
94
97
  isSubmitting={isSubmitting}
95
98
  readOnly={readOnly}
96
99
  />
100
+ <span className="sr-only" aria-live="polite">
101
+ Question {currentIndex + 1} of {questions.length}
102
+ </span>
97
103
  {currentQuestion && (
98
- <Card className="mt-3">
99
- <CardContent className="pt-6">
104
+ <Card className="mt-3" ref={questionAreaRef} tabIndex={-1}>
105
+ <CardHeader className="pb-0">
106
+ <QuestionHeaderBar
107
+ questionNumber={currentIndex + 1}
108
+ totalQuestions={questions.length}
109
+ isFlagged={flaggedUids.has(currentQuestion.uid)}
110
+ onToggleFlag={toggleCurrentFlag}
111
+ hasMaterials={currentMaterials.length > 0}
112
+ onOpenMaterials={openMaterials}
113
+ readOnly={readOnly}
114
+ />
115
+ </CardHeader>
116
+ <CardContent>
100
117
  <QuestionRenderer
101
118
  question={currentQuestion}
102
- sessionAnswers={sessionAnswers.filter(
103
- (a) => a.uid === currentQuestion.uid,
104
- )}
119
+ sessionAnswers={currentQuestionAnswers}
105
120
  onAnswer={handleAnswer}
106
121
  readOnly={readOnly}
107
122
  />
108
123
  </CardContent>
109
124
  </Card>
110
125
  )}
126
+
127
+ {/* Bottom navigation */}
128
+ {!readOnly && (
129
+ <div className="flex items-center justify-between gap-3 mt-3">
130
+ <Button
131
+ variant="outline"
132
+ disabled={!hasPrevious}
133
+ onClick={goPrevious}
134
+ >
135
+ <ChevronLeft className="size-4 mr-1" />
136
+ Previous
137
+ </Button>
138
+ {hasNext ? (
139
+ <Button onClick={goNext}>
140
+ Next
141
+ <ChevronRight className="size-4 ml-1" />
142
+ </Button>
143
+ ) : (
144
+ <Button onClick={handleSubmit} disabled={isSubmitting}>
145
+ {isSubmitting ? "Submitting..." : "Submit Quiz"}
146
+ {!isSubmitting && <Send className="size-4 ml-1" />}
147
+ </Button>
148
+ )}
149
+ </div>
150
+ )}
151
+
152
+ <QuestionMaterialsDrawer
153
+ open={materialsOpen}
154
+ onOpenChange={setMaterialsOpen}
155
+ materials={currentMaterials}
156
+ questionNumber={currentIndex + 1}
157
+ />
111
158
  </div>
159
+ </SectionShell>
112
160
  );
113
161
  }
@@ -1,4 +1,4 @@
1
- import type { QuestionData, SessionAnswer } from "../../questions/types";
1
+ import type { QuestionData, QuestionMaterial, SessionAnswer } from "../../questions/types";
2
2
 
3
3
  /**
4
4
  * QuizSession section — a complete assessment session experience.
@@ -38,8 +38,19 @@ export interface QuizSessionProps {
38
38
  timeLimitSeconds?: number;
39
39
  /** Whether the submit action is currently in flight */
40
40
  isSubmitting?: boolean;
41
+ /**
42
+ * Related materials keyed by question UID. When provided, a "Materials"
43
+ * button appears in the question header, opening a drawer with content blocks.
44
+ */
45
+ questionMaterials?: QuestionMaterial[];
41
46
  /** When true, all inputs are disabled (e.g. after submission) */
42
47
  readOnly?: boolean;
48
+ /** Render skeleton placeholders instead of content */
49
+ isLoading?: boolean;
50
+ /** Error message — renders an error state with optional retry */
51
+ error?: string | null;
52
+ /** Called when the user clicks retry in the error state */
53
+ onRetry?: () => void;
43
54
  /** CSS class name for the root element */
44
55
  className?: string;
45
56
  /** Inline styles for the root element */
@@ -0,0 +1,147 @@
1
+ import { useMemo } from "react";
2
+ import { AlertCircle, CheckCircle2, Circle, ChevronRight } from "lucide-react";
3
+ import { Skeleton } from "../../ui/skeleton";
4
+ import { EmptyState } from "../../common/empty-state";
5
+ import { Button } from "../../ui/button";
6
+ import { Progress } from "../../ui/progress";
7
+ import { cn } from "../../lib/utils";
8
+ import type { RequirementsChecklistProps } from "./types";
9
+
10
+ /**
11
+ * RequirementsChecklist — shows completion requirements with checked/unchecked status.
12
+ *
13
+ * Displays a vertical list of requirements with an overall progress bar.
14
+ * Incomplete items can be clicked to navigate to the relevant content.
15
+ */
16
+ export function RequirementsChecklist({
17
+ title,
18
+ requirements,
19
+ onRequirementClick,
20
+ isLoading,
21
+ error,
22
+ onRetry,
23
+ className,
24
+ style,
25
+ }: RequirementsChecklistProps) {
26
+ const completedCount = useMemo(
27
+ () => requirements.filter((r) => r.completed).length,
28
+ [requirements]
29
+ );
30
+ const progressPercent =
31
+ requirements.length > 0
32
+ ? Math.round((completedCount / requirements.length) * 100)
33
+ : 0;
34
+
35
+ if (isLoading) {
36
+ return (
37
+ <div className={cn("space-y-4", className)} style={style}>
38
+ <Skeleton className="h-6 w-48" />
39
+ <Skeleton className="h-2 w-full" />
40
+ {Array.from({ length: 4 }).map((_, i) => (
41
+ <div key={i} className="flex items-center gap-3">
42
+ <Skeleton className="h-5 w-5 rounded-full" />
43
+ <Skeleton className="h-4 w-3/4" />
44
+ </div>
45
+ ))}
46
+ </div>
47
+ );
48
+ }
49
+
50
+ if (error) {
51
+ return (
52
+ <div className={cn("py-12", className)} style={style}>
53
+ <EmptyState
54
+ icon={<AlertCircle className="size-10 text-destructive" />}
55
+ title="Something went wrong"
56
+ description={error}
57
+ action={
58
+ onRetry ? (
59
+ <Button variant="outline" onClick={onRetry}>
60
+ Retry
61
+ </Button>
62
+ ) : undefined
63
+ }
64
+ />
65
+ </div>
66
+ );
67
+ }
68
+
69
+ return (
70
+ <div className={cn("flex flex-col gap-4", className)} style={style}>
71
+ {/* Header */}
72
+ {title && (
73
+ <h3 className="text-lg font-semibold text-foreground">{title}</h3>
74
+ )}
75
+
76
+ {/* Progress bar */}
77
+ <div className="flex flex-col gap-1.5">
78
+ <div className="flex items-center justify-between text-sm">
79
+ <span className="text-muted-foreground">
80
+ {completedCount} of {requirements.length} complete
81
+ </span>
82
+ <span className="font-medium text-foreground">{progressPercent}%</span>
83
+ </div>
84
+ <Progress value={progressPercent} />
85
+ </div>
86
+
87
+ {/* Requirement list */}
88
+ <ul className="flex flex-col gap-1">
89
+ {requirements.map((req) => (
90
+ <li key={req.uid}>
91
+ <button
92
+ type="button"
93
+ className={cn(
94
+ "w-full flex items-start gap-3 p-3 rounded-lg text-left transition-colors",
95
+ req.completed
96
+ ? "bg-success/5"
97
+ : onRequirementClick
98
+ ? "hover:bg-muted/50 cursor-pointer"
99
+ : "cursor-default"
100
+ )}
101
+ onClick={() => {
102
+ if (!req.completed && onRequirementClick) {
103
+ onRequirementClick(req.uid);
104
+ }
105
+ }}
106
+ disabled={req.completed}
107
+ >
108
+ {/* Icon */}
109
+ {req.completed ? (
110
+ <CheckCircle2 className="size-5 text-success shrink-0 mt-0.5" />
111
+ ) : (
112
+ <Circle className="size-5 text-muted-foreground shrink-0 mt-0.5" />
113
+ )}
114
+
115
+ {/* Content */}
116
+ <div className="flex-1 min-w-0">
117
+ <div
118
+ className={cn(
119
+ "text-sm font-medium",
120
+ req.completed
121
+ ? "text-muted-foreground line-through"
122
+ : "text-foreground"
123
+ )}
124
+ >
125
+ {req.label}
126
+ </div>
127
+ {req.description && (
128
+ <div className="text-xs text-muted-foreground mt-0.5">
129
+ {req.description}
130
+ </div>
131
+ )}
132
+ </div>
133
+
134
+ {/* Action indicator */}
135
+ {!req.completed && onRequirementClick && (
136
+ <div className="shrink-0 flex items-center gap-1 text-xs text-primary mt-0.5">
137
+ {req.actionLabel && <span>{req.actionLabel}</span>}
138
+ <ChevronRight className="size-4" />
139
+ </div>
140
+ )}
141
+ </button>
142
+ </li>
143
+ ))}
144
+ </ul>
145
+ </div>
146
+ );
147
+ }