@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,5 +1,8 @@
1
+ import { useCallback } from "react";
1
2
  import { VideoPlayer } from "../../video";
2
3
  import { Card, CardHeader, CardTitle, CardContent } from "../../ui/card";
4
+ import { Skeleton } from "../../ui/skeleton";
5
+ import { SectionShell } from "../_shared/section-shell";
3
6
  import type { LecturePlayerProps } from "./types";
4
7
  import { cn } from "../../lib/utils";
5
8
 
@@ -9,52 +12,75 @@ export function LecturePlayer({
9
12
  layout = "horizontal",
10
13
  notesPanelWidth = "340px",
11
14
  notesPanelHeight = "240px",
15
+ isLoading,
16
+ error,
17
+ onRetry,
18
+ onComplete,
12
19
  className,
13
20
  style,
14
21
  }: LecturePlayerProps) {
15
22
  const isHorizontal = layout === "horizontal";
16
23
 
17
- if (!notes) {
18
- return (
19
- <div className={className} style={style}>
20
- <VideoPlayer {...video} />
21
- </div>
22
- );
23
- }
24
+ const handleEnded = useCallback(() => {
25
+ video.onEnded?.();
26
+ onComplete?.();
27
+ }, [video, onComplete]);
24
28
 
25
29
  return (
26
- <div
27
- className={cn(
28
- "flex overflow-hidden",
29
- isHorizontal ? "flex-row" : "flex-col",
30
- className,
31
- )}
30
+ <SectionShell
31
+ isLoading={isLoading}
32
+ error={error}
33
+ onRetry={onRetry}
34
+ className={className}
32
35
  style={style}
36
+ skeleton={
37
+ <>
38
+ <Skeleton className="aspect-video w-full rounded-lg" />
39
+ {notes !== undefined && (
40
+ <Skeleton className="h-32 w-full rounded-lg" />
41
+ )}
42
+ </>
43
+ }
33
44
  >
34
- <div className="flex-1 min-w-0 min-h-0">
35
- <VideoPlayer {...video} />
36
- </div>
37
- <Card
38
- className={cn(
39
- "overflow-auto shrink-0 rounded-none border-0",
40
- isHorizontal ? "border-l border-border" : "border-t border-border w-full",
41
- )}
42
- style={{
43
- width: isHorizontal ? notesPanelWidth : undefined,
44
- height: isHorizontal ? undefined : notesPanelHeight,
45
- }}
46
- >
47
- <CardHeader>
48
- <CardTitle>Notes</CardTitle>
49
- </CardHeader>
50
- <CardContent>
51
- {typeof notes === "string" ? (
52
- <span className="text-sm text-foreground">{notes}</span>
53
- ) : (
54
- notes
45
+ {!notes ? (
46
+ <div className={className} style={style}>
47
+ <VideoPlayer {...video} onEnded={handleEnded} />
48
+ </div>
49
+ ) : (
50
+ <div
51
+ className={cn(
52
+ "flex overflow-hidden gap-4",
53
+ isHorizontal ? "flex-row" : "flex-col",
54
+ className,
55
55
  )}
56
- </CardContent>
57
- </Card>
58
- </div>
56
+ style={style}
57
+ >
58
+ <div className="flex-1 min-w-0 min-h-0">
59
+ <VideoPlayer {...video} onEnded={handleEnded} />
60
+ </div>
61
+ <Card
62
+ className={cn(
63
+ "overflow-auto shrink-0 rounded-none border-0",
64
+ isHorizontal ? "border-l border-border" : "border-t border-border w-full",
65
+ )}
66
+ style={{
67
+ width: isHorizontal ? notesPanelWidth : undefined,
68
+ height: isHorizontal ? undefined : notesPanelHeight,
69
+ }}
70
+ >
71
+ <CardHeader>
72
+ <CardTitle>Notes</CardTitle>
73
+ </CardHeader>
74
+ <CardContent>
75
+ {typeof notes === "string" ? (
76
+ <span className="text-sm text-foreground">{notes}</span>
77
+ ) : (
78
+ notes
79
+ )}
80
+ </CardContent>
81
+ </Card>
82
+ </div>
83
+ )}
84
+ </SectionShell>
59
85
  );
60
86
  }
@@ -41,6 +41,14 @@ export interface LecturePlayerProps {
41
41
  * @default "240px"
42
42
  */
43
43
  notesPanelHeight?: string | number;
44
+ /** Render skeleton placeholders instead of content */
45
+ isLoading?: boolean;
46
+ /** Error message — renders an error state with optional retry */
47
+ error?: string | null;
48
+ /** Called when the user clicks retry in the error state */
49
+ onRetry?: () => void;
50
+ /** Called when the video playback ends. */
51
+ onComplete?: () => void;
44
52
  /** CSS class name for the root element */
45
53
  className?: string;
46
54
  /** Inline styles for the root element */
@@ -1,12 +1,15 @@
1
- import { useState } from "react";
1
+ import { useState, useEffect } from "react";
2
2
  import { Check, ChevronRight, Clock } from "lucide-react";
3
3
  import { ContentBlock } from "../../content";
4
- import type { SessionAnswer } from "../../questions/types";
4
+ import { Skeleton } from "../../ui/skeleton";
5
+ import { EmptyState } from "../../common/empty-state";
5
6
  import { Button } from "../../ui/button";
7
+ import { Card, CardContent } from "../../ui/card";
6
8
  import { Separator } from "../../ui/separator";
9
+ import { SectionShell } from "../_shared/section-shell";
7
10
  import { formatDuration } from "../../utils/format-duration";
8
11
  import type { LessonPageProps } from "./types";
9
- import { cn } from "../../lib/utils";
12
+
10
13
 
11
14
  export function LessonPage({
12
15
  title,
@@ -18,15 +21,17 @@ export function LessonPage({
18
21
  estimatedDuration,
19
22
  showDuration = true,
20
23
  readOnly = false,
24
+ isLoading,
25
+ error,
26
+ onRetry,
21
27
  className,
22
28
  style,
23
29
  }: LessonPageProps) {
24
30
  const [completed, setCompleted] = useState(isCompleted);
25
- const [, setQuestionAnswers] = useState<Map<string, SessionAnswer[]>>(new Map());
26
31
 
27
- function handleQuestionAnswer(questionUid: string, answers: SessionAnswer[]) {
28
- setQuestionAnswers((prev) => new Map(prev).set(questionUid, answers));
29
- }
32
+ useEffect(() => {
33
+ setCompleted(isCompleted);
34
+ }, [isCompleted]);
30
35
 
31
36
  function handleMarkComplete() {
32
37
  setCompleted(true);
@@ -34,7 +39,22 @@ export function LessonPage({
34
39
  }
35
40
 
36
41
  return (
37
- <div className={cn(className)} style={style}>
42
+ <SectionShell
43
+ isLoading={isLoading}
44
+ error={error}
45
+ onRetry={onRetry}
46
+ className={className}
47
+ style={style}
48
+ skeleton={
49
+ <>
50
+ <Skeleton className="h-8 w-64" />
51
+ <Skeleton className="h-24 w-full" />
52
+ <Skeleton className="h-16 w-full" />
53
+ <Skeleton className="h-32 w-full" />
54
+ </>
55
+ }
56
+ >
57
+ <div>
38
58
  {/* Header */}
39
59
  <div className="mb-3">
40
60
  <p className="text-2xl font-bold mb-0.5 text-foreground">{title}</p>
@@ -50,18 +70,23 @@ export function LessonPage({
50
70
 
51
71
  {/* Content blocks */}
52
72
  <div className="flex flex-col gap-3">
53
- {blocks.map((block, i) => (
73
+ {blocks?.length ? blocks.map((block, i) => (
54
74
  <ContentBlock
55
75
  key={i}
56
76
  block={block}
57
- onQuestionAnswer={handleQuestionAnswer}
58
77
  readOnly={readOnly}
59
78
  />
60
- ))}
79
+ )) : (
80
+ <EmptyState
81
+ title="No content yet"
82
+ description="This lesson doesn't have any content blocks."
83
+ />
84
+ )}
61
85
  </div>
62
86
 
63
87
  {/* Completion bar */}
64
- <div className="border border-border rounded-md px-4 py-3 mt-4 sticky bottom-0 bg-background z-10">
88
+ <Card className="mt-4 sticky bottom-0 z-10">
89
+ <CardContent className="px-4 py-3">
65
90
  <div className="flex justify-between items-center">
66
91
  {completed ? (
67
92
  <div className="flex items-center gap-1 text-success">
@@ -85,7 +110,9 @@ export function LessonPage({
85
110
  </Button>
86
111
  )}
87
112
  </div>
88
- </div>
113
+ </CardContent>
114
+ </Card>
89
115
  </div>
116
+ </SectionShell>
90
117
  );
91
118
  }
@@ -34,6 +34,12 @@ export interface LessonPageProps {
34
34
  showDuration?: boolean;
35
35
  /** When true, disables interactive elements */
36
36
  readOnly?: boolean;
37
+ /** Render skeleton placeholders instead of content */
38
+ isLoading?: boolean;
39
+ /** Error message — renders an error state with optional retry */
40
+ error?: string | null;
41
+ /** Called when the user clicks retry in the error state */
42
+ onRetry?: () => void;
37
43
  /** CSS class name for the root element */
38
44
  className?: string;
39
45
  /** Inline styles for the root element */
@@ -1,20 +1,14 @@
1
- import { useMemo, useState } from "react";
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { CheckCircle } from "lucide-react";
3
- import { QuestionRenderer } from "../../questions";
3
+ import { QuestionRenderer, scoreQuestion } from "../../questions";
4
4
  import { FeedbackBanner } from "../../feedback";
5
5
  import { Button } from "../../ui/button";
6
6
  import { Card, CardContent } from "../../ui/card";
7
7
  import { Progress } from "../../ui/progress";
8
+ import { Skeleton } from "../../ui/skeleton";
9
+ import { SectionShell } from "../_shared/section-shell";
8
10
  import type { PracticeQuizProps, PracticeQuizStats } from "./types";
9
-
10
- function shuffleArray<T>(arr: T[]): T[] {
11
- const copy = [...arr];
12
- for (let i = copy.length - 1; i > 0; i--) {
13
- const j = Math.floor(Math.random() * (i + 1));
14
- [copy[i], copy[j]] = [copy[j], copy[i]];
15
- }
16
- return copy;
17
- }
11
+ import { shuffle } from "../../utils/shuffle";
18
12
 
19
13
  export function PracticeQuiz({
20
14
  questions: questionsProp,
@@ -23,11 +17,17 @@ export function PracticeQuiz({
23
17
  onComplete,
24
18
  shuffled = false,
25
19
  readOnly = false,
20
+ isLoading,
21
+ error,
22
+ onRetry,
26
23
  className,
27
24
  style,
28
25
  }: PracticeQuizProps) {
29
26
  const questions = useMemo(
30
- () => (shuffled ? shuffleArray(questionsProp) : questionsProp),
27
+ () => {
28
+ const qs = questionsProp ?? [];
29
+ return shuffled ? shuffle(qs) : qs;
30
+ },
31
31
  // eslint-disable-next-line react-hooks/exhaustive-deps
32
32
  [questionsProp, shuffled],
33
33
  );
@@ -38,6 +38,16 @@ export function PracticeQuiz({
38
38
  const [firstAttemptCorrect, setFirstAttemptCorrect] = useState<Set<string>>(new Set());
39
39
  const [currentAnswer, setCurrentAnswer] = useState<{ uid: string; content?: string }[] | null>(null);
40
40
  const [isComplete, setIsComplete] = useState(false);
41
+ const questionAreaRef = useRef<HTMLDivElement>(null);
42
+ const isFirstRender = useRef(true);
43
+
44
+ useEffect(() => {
45
+ if (isFirstRender.current) {
46
+ isFirstRender.current = false;
47
+ return;
48
+ }
49
+ questionAreaRef.current?.focus();
50
+ }, [currentIndex]);
41
51
 
42
52
  const currentQuestion = questions[currentIndex];
43
53
  const isRevealed = currentQuestion ? revealedUids.has(currentQuestion.uid) : false;
@@ -49,13 +59,12 @@ export function PracticeQuiz({
49
59
  setAttemptCounts((prev) => new Map(prev).set(currentQuestion.uid, attempts));
50
60
  setRevealedUids((prev) => new Set(prev).add(currentQuestion.uid));
51
61
 
52
- const correctUids = new Set(
53
- currentQuestion.answers?.filter((a) => a.isCorrect).map((a) => a.uid) ?? [],
54
- );
55
- const selectedUids = new Set(currentAnswer.map((a) => a.uid));
56
- const isCorrect =
57
- correctUids.size === selectedUids.size &&
58
- [...correctUids].every((uid) => selectedUids.has(uid));
62
+ const sessionAnswers = currentAnswer.map((a) => ({
63
+ uid: currentQuestion.uid,
64
+ answerUid: a.uid,
65
+ content: a.content,
66
+ }));
67
+ const isCorrect = scoreQuestion(currentQuestion, sessionAnswers) === true;
59
68
 
60
69
  if (isCorrect && attempts === 1) {
61
70
  setFirstAttemptCorrect((prev) => new Set(prev).add(currentQuestion.uid));
@@ -87,29 +96,45 @@ export function PracticeQuiz({
87
96
  }
88
97
  }
89
98
 
90
- const isCurrentCorrect = useMemo(() => {
91
- if (!currentQuestion || !currentAnswer) return false;
92
- const correctUids = new Set(
93
- currentQuestion.answers?.filter((a) => a.isCorrect).map((a) => a.uid) ?? [],
94
- );
95
- const selectedUids = new Set(currentAnswer.map((a) => a.uid));
96
- return (
97
- correctUids.size === selectedUids.size &&
98
- [...correctUids].every((uid) => selectedUids.has(uid))
99
- );
99
+ const currentSessionAnswers = useMemo(() => {
100
+ if (!currentQuestion || !currentAnswer) return [];
101
+ return currentAnswer.map((a) => ({
102
+ uid: currentQuestion.uid,
103
+ answerUid: a.uid,
104
+ content: a.content,
105
+ }));
100
106
  }, [currentQuestion, currentAnswer]);
101
107
 
102
- if (isComplete) {
103
- const percentage = questions.length > 0
104
- ? Math.round((firstAttemptCorrect.size / questions.length) * 100)
105
- : 0;
106
- return (
108
+ const isCurrentCorrect = useMemo(
109
+ () => currentQuestion ? scoreQuestion(currentQuestion, currentSessionAnswers) === true : false,
110
+ [currentQuestion, currentSessionAnswers],
111
+ );
112
+
113
+ const completionPercentage = questions.length > 0
114
+ ? Math.round((firstAttemptCorrect.size / questions.length) * 100)
115
+ : 0;
116
+
117
+ return (
118
+ <SectionShell
119
+ isLoading={isLoading}
120
+ error={error}
121
+ onRetry={onRetry}
122
+ className={className}
123
+ style={style}
124
+ skeleton={
125
+ <>
126
+ <Skeleton className="h-2 w-full" />
127
+ <Skeleton className="h-48 w-full" />
128
+ </>
129
+ }
130
+ >
131
+ {isComplete ? (
107
132
  <Card className={className} style={style}>
108
133
  <CardContent className="pt-6 text-center">
109
134
  <CheckCircle size={48} className="text-success mx-auto mb-4" />
110
135
  <p className="text-xl font-bold mb-1 text-foreground">Practice Complete!</p>
111
136
  <p className="text-muted-foreground mb-2">
112
- {firstAttemptCorrect.size} of {questions.length} correct on first attempt ({percentage}%)
137
+ {firstAttemptCorrect.size} of {questions.length} correct on first attempt ({completionPercentage}%)
113
138
  </p>
114
139
  <div className="flex justify-center">
115
140
  <Button
@@ -128,72 +153,68 @@ export function PracticeQuiz({
128
153
  </div>
129
154
  </CardContent>
130
155
  </Card>
131
- );
132
- }
133
-
134
- return (
135
- <div className={className} style={style}>
136
- <div className="flex justify-between items-center mb-2">
137
- <span className="font-semibold text-sm text-foreground">
156
+ ) : (
157
+ <div className={className} style={style}>
158
+ <div className="flex justify-between items-center mb-2">
159
+ <span className="font-semibold text-sm text-foreground">
160
+ Question {currentIndex + 1} of {questions.length}
161
+ </span>
162
+ <span className="text-xs text-muted-foreground">
163
+ {firstAttemptCorrect.size} correct on first try
164
+ </span>
165
+ </div>
166
+ <Progress
167
+ value={currentIndex + (isRevealed ? 1 : 0)}
168
+ max={questions.length}
169
+ size="sm"
170
+ className="mb-3"
171
+ />
172
+
173
+ <span className="sr-only" aria-live="polite">
138
174
  Question {currentIndex + 1} of {questions.length}
139
175
  </span>
140
- <span className="text-xs text-muted-foreground">
141
- {firstAttemptCorrect.size} correct on first try
142
- </span>
143
- </div>
144
- <Progress
145
- value={currentIndex + (isRevealed ? 1 : 0)}
146
- max={questions.length}
147
- size="sm"
148
- className="mb-3"
149
- />
150
-
151
- {currentQuestion && (
152
- <Card>
153
- <CardContent className="pt-6">
154
- <QuestionRenderer
155
- question={currentQuestion}
156
- sessionAnswers={
157
- currentAnswer?.map((a) => ({
158
- uid: currentQuestion.uid,
159
- answerUid: a.uid,
160
- content: a.content,
161
- })) ?? []
162
- }
163
- onAnswer={(answers) => setCurrentAnswer(answers)}
164
- readOnly={readOnly || isRevealed}
165
- showCorrectAnswers={isRevealed}
166
- />
167
-
168
- {instantFeedback && isRevealed && (
169
- <FeedbackBanner
170
- isCorrect={isCurrentCorrect}
171
- explanation={currentQuestion.explanation}
172
- onRetry={allowRetry && !isCurrentCorrect ? handleRetry : undefined}
176
+ {currentQuestion && (
177
+ <Card ref={questionAreaRef} tabIndex={-1}>
178
+ <CardContent className="pt-6">
179
+ <QuestionRenderer
180
+ question={currentQuestion}
181
+ sessionAnswers={currentSessionAnswers}
182
+ onAnswer={setCurrentAnswer}
183
+ readOnly={readOnly || isRevealed}
184
+ showCorrectAnswers={isRevealed}
173
185
  />
174
- )}
175
-
176
- <div className="flex justify-end gap-2 mt-2">
177
- {!isRevealed && instantFeedback && (
178
- <Button
179
- onClick={checkAnswer}
180
- disabled={!currentAnswer || currentAnswer.length === 0 || readOnly}
181
- >
182
- Check Answer
183
- </Button>
184
- )}
185
- {(!instantFeedback || isRevealed) && (
186
- <Button
187
- onClick={handleNext}
188
- disabled={readOnly}
189
- >
190
- {currentIndex < questions.length - 1 ? "Next Question" : "Finish"}
191
- </Button>
186
+
187
+ {instantFeedback && isRevealed && (
188
+ <FeedbackBanner
189
+ isCorrect={isCurrentCorrect}
190
+ explanation={currentQuestion.explanation}
191
+ onRetry={allowRetry && !isCurrentCorrect ? handleRetry : undefined}
192
+ />
192
193
  )}
193
- </div>
194
- </CardContent>
195
- </Card>
196
- )}
197
- </div>
194
+
195
+ <div className="flex justify-end gap-2 mt-2">
196
+ {!isRevealed && instantFeedback && (
197
+ <Button
198
+ onClick={checkAnswer}
199
+ disabled={!currentAnswer || currentAnswer.length === 0 || readOnly}
200
+ >
201
+ Check Answer
202
+ </Button>
203
+ )}
204
+ {(!instantFeedback || isRevealed) && (
205
+ <Button
206
+ onClick={handleNext}
207
+ disabled={readOnly}
208
+ >
209
+ {currentIndex < questions.length - 1 ? "Next Question" : "Finish"}
210
+ </Button>
211
+ )}
212
+ </div>
213
+ </CardContent>
214
+ </Card>
215
+ )}
216
+ </div>
217
+ )}
218
+ </SectionShell>
198
219
  );
199
220
  }
@@ -28,6 +28,12 @@ export interface PracticeQuizProps {
28
28
  shuffled?: boolean;
29
29
  /** When true, all inputs are disabled */
30
30
  readOnly?: boolean;
31
+ /** Render skeleton placeholders instead of content */
32
+ isLoading?: boolean;
33
+ /** Error message — renders an error state with optional retry */
34
+ error?: string | null;
35
+ /** Called when the user clicks retry in the error state */
36
+ onRetry?: () => void;
31
37
  /** CSS class name for the root element */
32
38
  className?: string;
33
39
  /** Inline styles for the root element */