@hydralms/components 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/dist/StudentProfile-BVfZMbnV.cjs +1 -0
  2. package/dist/StudentProfile-DeMxdrL3.js +3275 -0
  3. package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
  4. package/dist/assessment-toolbar/timer-display.d.ts +1 -1
  5. package/dist/common/index.d.ts +2 -1
  6. package/dist/common/pagination.d.ts +26 -0
  7. package/dist/common/types.d.ts +1 -0
  8. package/dist/components.css +1 -1
  9. package/dist/content/audio-player.d.ts +22 -0
  10. package/dist/content/code-block.d.ts +30 -0
  11. package/dist/content/embed-block.d.ts +28 -0
  12. package/dist/content/index.d.ts +6 -0
  13. package/dist/content/types.d.ts +24 -0
  14. package/dist/curriculum/course-card.d.ts +51 -0
  15. package/dist/curriculum/index.d.ts +2 -0
  16. package/dist/curriculum/types.d.ts +2 -2
  17. package/dist/index.cjs +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +494 -444
  20. package/dist/license/HydraContext.d.ts +16 -0
  21. package/dist/license/ProBadge.d.ts +6 -0
  22. package/dist/license/index.d.ts +7 -0
  23. package/dist/license/tiers.d.ts +3 -0
  24. package/dist/license/useHydraLicense.d.ts +6 -0
  25. package/dist/license/validate.d.ts +13 -0
  26. package/dist/license/withProGate.d.ts +6 -0
  27. package/dist/modules/AssignmentModule/AssignmentModule.d.ts +4 -7
  28. package/dist/modules/AssignmentModule/types.d.ts +5 -1
  29. package/dist/modules/CertificateModule/CertificateModule.d.ts +4 -8
  30. package/dist/modules/CertificateModule/types.d.ts +6 -4
  31. package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
  32. package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
  33. package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
  34. package/dist/modules/DiscussionModule/DiscussionModule.d.ts +4 -7
  35. package/dist/modules/ExamModule/ExamModule.d.ts +4 -7
  36. package/dist/modules/ExamModule/types.d.ts +5 -14
  37. package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
  38. package/dist/modules/FlashcardLab/types.d.ts +2 -0
  39. package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +4 -8
  40. package/dist/modules/GradeCenterModule/types.d.ts +2 -0
  41. package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
  42. package/dist/modules/QuizModule/types.d.ts +5 -14
  43. package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
  44. package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
  45. package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
  46. package/dist/modules/StudentProfileModule/types.d.ts +43 -0
  47. package/dist/modules/SurveyModule/SurveyModule.d.ts +4 -6
  48. package/dist/modules/SurveyModule/types.d.ts +2 -0
  49. package/dist/modules/_shared/assessment-intro.d.ts +16 -0
  50. package/dist/modules/_shared/assessment-results.d.ts +23 -0
  51. package/dist/modules/_shared/types.d.ts +10 -0
  52. package/dist/modules/_shared/use-timer.d.ts +9 -0
  53. package/dist/modules/index.d.ts +6 -0
  54. package/dist/modules.cjs +1 -1
  55. package/dist/modules.js +1266 -854
  56. package/dist/progress/types.d.ts +2 -0
  57. package/dist/provider/HydraProvider.d.ts +5 -1
  58. package/dist/questions/choice.d.ts +1 -1
  59. package/dist/questions/confidence-indicator.d.ts +37 -0
  60. package/dist/questions/essay.d.ts +1 -1
  61. package/dist/questions/fill-in-the-blank.d.ts +1 -1
  62. package/dist/questions/hotspot.d.ts +1 -1
  63. package/dist/questions/index.d.ts +2 -0
  64. package/dist/questions/inline-choice.d.ts +1 -1
  65. package/dist/questions/matching.d.ts +1 -1
  66. package/dist/questions/multiple-choice.d.ts +1 -1
  67. package/dist/questions/numeric.d.ts +1 -1
  68. package/dist/questions/ordering.d.ts +1 -1
  69. package/dist/questions/question-renderer.d.ts +1 -1
  70. package/dist/questions/scenario.d.ts +1 -1
  71. package/dist/questions/spreadsheet.d.ts +1 -1
  72. package/dist/questions/true-false.d.ts +1 -1
  73. package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
  74. package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
  75. package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
  76. package/dist/sections/AssessmentReview/types.d.ts +6 -0
  77. package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
  78. package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
  79. package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
  80. package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
  81. package/dist/sections/CertificateViewer/types.d.ts +6 -0
  82. package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
  83. package/dist/sections/CourseCatalog/types.d.ts +80 -0
  84. package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
  85. package/dist/sections/CourseOutline/types.d.ts +6 -0
  86. package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
  87. package/dist/sections/DiscussionThread/types.d.ts +6 -0
  88. package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
  89. package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
  90. package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
  91. package/dist/sections/ExamSession/types.d.ts +6 -0
  92. package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
  93. package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
  94. package/dist/sections/ForumBoard/ForumBoard.d.ts +1 -1
  95. package/dist/sections/ForumBoard/types.d.ts +14 -0
  96. package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
  97. package/dist/sections/GradebookTable/types.d.ts +14 -0
  98. package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
  99. package/dist/sections/LecturePlayer/types.d.ts +8 -0
  100. package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
  101. package/dist/sections/LessonPage/types.d.ts +6 -0
  102. package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
  103. package/dist/sections/PracticeQuiz/types.d.ts +6 -0
  104. package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
  105. package/dist/sections/ProgressDashboard/types.d.ts +6 -0
  106. package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
  107. package/dist/sections/QuizSession/types.d.ts +6 -0
  108. package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +1 -1
  109. package/dist/sections/RequirementsChecklist/types.d.ts +6 -0
  110. package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
  111. package/dist/sections/ResourceLibrary/types.d.ts +15 -1
  112. package/dist/sections/RubricView/RubricView.d.ts +1 -1
  113. package/dist/sections/RubricView/types.d.ts +6 -0
  114. package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
  115. package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
  116. package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
  117. package/dist/sections/StudentProfile/types.d.ts +98 -0
  118. package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
  119. package/dist/sections/SurveyForm/types.d.ts +6 -0
  120. package/dist/sections/_shared/merge-answers.d.ts +9 -0
  121. package/dist/sections/_shared/section-shell.d.ts +20 -0
  122. package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
  123. package/dist/sections/index.d.ts +6 -0
  124. package/dist/sections.cjs +1 -1
  125. package/dist/sections.js +268 -307
  126. package/dist/tabs-BsfVo2Bl.cjs +173 -0
  127. package/dist/{tabs-Wf3h_Cx3.js → tabs-BuY1iNJE.js} +7532 -6807
  128. package/dist/ui/badge.d.ts +1 -1
  129. package/dist/ui/index.d.ts +2 -0
  130. package/dist/ui/progress.d.ts +1 -1
  131. package/dist/ui/rich-text-editor.d.ts +3 -1
  132. package/dist/ui/toast.d.ts +43 -0
  133. package/dist/utils/debounce.d.ts +5 -1
  134. package/dist/utils/pick-palette-color.d.ts +19 -0
  135. package/dist/video/types.d.ts +15 -0
  136. package/dist/video/video-player.d.ts +1 -1
  137. package/dist/withProGate-BWqcKdPM.js +137 -0
  138. package/dist/withProGate-DX6XqKLp.cjs +1 -0
  139. package/package.json +34 -220
  140. package/src/assessment-toolbar/question-navigator.tsx +10 -5
  141. package/src/assessment-toolbar/timer-display.tsx +4 -3
  142. package/src/assessment-toolbar/use-countdown.ts +1 -1
  143. package/src/common/empty-state.tsx +1 -0
  144. package/src/common/index.ts +2 -0
  145. package/src/common/pagination.tsx +135 -0
  146. package/src/common/search-input.tsx +2 -1
  147. package/src/common/types.ts +2 -0
  148. package/src/content/attachment-list.tsx +2 -0
  149. package/src/content/audio-player.tsx +196 -0
  150. package/src/content/code-block.tsx +113 -0
  151. package/src/content/content-block.tsx +64 -0
  152. package/src/content/embed-block.tsx +78 -0
  153. package/src/content/file-upload-zone.tsx +10 -0
  154. package/src/content/index.ts +6 -0
  155. package/src/content/types.ts +5 -0
  156. package/src/curriculum/course-card.tsx +199 -0
  157. package/src/curriculum/curriculum-item.tsx +3 -3
  158. package/src/curriculum/curriculum-tree.tsx +20 -13
  159. package/src/curriculum/index.ts +2 -0
  160. package/src/curriculum/types.ts +2 -2
  161. package/src/flashcards/flashcard.tsx +28 -8
  162. package/src/index.ts +3 -0
  163. package/src/license/HydraContext.tsx +62 -0
  164. package/src/license/ProBadge.tsx +43 -0
  165. package/src/license/index.ts +7 -0
  166. package/src/license/tiers.ts +24 -0
  167. package/src/license/useHydraLicense.ts +10 -0
  168. package/src/license/validate.ts +90 -0
  169. package/src/license/withProGate.tsx +21 -0
  170. package/src/modules/AssignmentModule/AssignmentModule.tsx +17 -8
  171. package/src/modules/AssignmentModule/types.ts +5 -1
  172. package/src/modules/CertificateModule/CertificateModule.tsx +21 -9
  173. package/src/modules/CertificateModule/types.ts +6 -4
  174. package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
  175. package/src/modules/CourseCatalogModule/types.ts +47 -0
  176. package/src/modules/CoursePlayer/CoursePlayer.tsx +37 -22
  177. package/src/modules/DiscussionModule/DiscussionModule.tsx +57 -22
  178. package/src/modules/ExamModule/ExamModule.tsx +64 -198
  179. package/src/modules/ExamModule/types.ts +5 -14
  180. package/src/modules/FlashcardLab/FlashcardLab.tsx +10 -5
  181. package/src/modules/FlashcardLab/types.ts +2 -0
  182. package/src/modules/GradeCenterModule/GradeCenterModule.tsx +7 -2
  183. package/src/modules/GradeCenterModule/types.ts +2 -0
  184. package/src/modules/QuizModule/QuizModule.tsx +49 -169
  185. package/src/modules/QuizModule/types.ts +5 -15
  186. package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
  187. package/src/modules/StudentDashboardModule/types.ts +56 -0
  188. package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
  189. package/src/modules/StudentProfileModule/types.ts +45 -0
  190. package/src/modules/SurveyModule/SurveyModule.tsx +9 -4
  191. package/src/modules/SurveyModule/types.ts +2 -0
  192. package/src/modules/_shared/assessment-intro.tsx +75 -0
  193. package/src/modules/_shared/assessment-results.tsx +133 -0
  194. package/src/modules/_shared/types.ts +11 -0
  195. package/src/modules/_shared/use-timer.ts +49 -0
  196. package/src/modules/index.ts +9 -0
  197. package/src/progress/achievement-badge.tsx +3 -3
  198. package/src/progress/grade-indicator.tsx +9 -1
  199. package/src/progress/progress-ring.tsx +2 -1
  200. package/src/progress/stat-card.tsx +8 -1
  201. package/src/progress/types.ts +2 -0
  202. package/src/provider/HydraProvider.tsx +15 -6
  203. package/src/questions/choice.tsx +13 -6
  204. package/src/questions/confidence-indicator.tsx +107 -0
  205. package/src/questions/essay.tsx +6 -4
  206. package/src/questions/fill-in-the-blank.tsx +8 -4
  207. package/src/questions/hotspot.tsx +4 -4
  208. package/src/questions/index.ts +2 -0
  209. package/src/questions/inline-choice.tsx +5 -4
  210. package/src/questions/matching.tsx +5 -4
  211. package/src/questions/multiple-choice.tsx +13 -6
  212. package/src/questions/numeric.tsx +8 -4
  213. package/src/questions/ordering.tsx +12 -4
  214. package/src/questions/question-renderer.tsx +3 -2
  215. package/src/questions/scenario.tsx +4 -4
  216. package/src/questions/spreadsheet.tsx +5 -4
  217. package/src/questions/true-false.tsx +13 -6
  218. package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +64 -8
  219. package/src/sections/AnnouncementFeed/types.ts +15 -1
  220. package/src/sections/AssessmentReview/AssessmentReview.tsx +37 -0
  221. package/src/sections/AssessmentReview/types.ts +6 -0
  222. package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +37 -1
  223. package/src/sections/AssignmentSubmission/types.ts +6 -0
  224. package/src/sections/CertificateViewer/CertificateViewer.tsx +29 -227
  225. package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
  226. package/src/sections/CertificateViewer/types.ts +6 -0
  227. package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
  228. package/src/sections/CourseCatalog/types.ts +76 -0
  229. package/src/sections/CourseOutline/CourseOutline.tsx +41 -0
  230. package/src/sections/CourseOutline/types.ts +6 -0
  231. package/src/sections/DiscussionThread/DiscussionThread.tsx +42 -1
  232. package/src/sections/DiscussionThread/types.ts +6 -0
  233. package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
  234. package/src/sections/EnrollmentWizard/types.ts +65 -0
  235. package/src/sections/ExamSession/ExamSession.tsx +100 -94
  236. package/src/sections/ExamSession/types.ts +6 -0
  237. package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
  238. package/src/sections/FlashcardStudySession/types.ts +6 -0
  239. package/src/sections/ForumBoard/ForumBoard.tsx +59 -1
  240. package/src/sections/ForumBoard/types.ts +14 -0
  241. package/src/sections/GradebookTable/GradebookTable.tsx +54 -1
  242. package/src/sections/GradebookTable/types.ts +14 -0
  243. package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
  244. package/src/sections/LecturePlayer/types.ts +8 -0
  245. package/src/sections/LessonPage/LessonPage.tsx +36 -5
  246. package/src/sections/LessonPage/types.ts +6 -0
  247. package/src/sections/PracticeQuiz/PracticeQuiz.tsx +106 -74
  248. package/src/sections/PracticeQuiz/types.ts +6 -0
  249. package/src/sections/ProgressDashboard/ProgressDashboard.tsx +64 -10
  250. package/src/sections/ProgressDashboard/types.ts +6 -0
  251. package/src/sections/QuizSession/QuizSession.tsx +71 -82
  252. package/src/sections/QuizSession/types.ts +6 -0
  253. package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +41 -1
  254. package/src/sections/RequirementsChecklist/types.ts +6 -0
  255. package/src/sections/ResourceLibrary/ResourceLibrary.tsx +64 -8
  256. package/src/sections/ResourceLibrary/types.ts +15 -1
  257. package/src/sections/RubricView/RubricView.tsx +37 -1
  258. package/src/sections/RubricView/types.ts +6 -0
  259. package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +36 -15
  260. package/src/sections/ScrollableQuiz/types.ts +6 -0
  261. package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
  262. package/src/sections/StudentProfile/types.ts +99 -0
  263. package/src/sections/SurveyForm/SurveyForm.tsx +32 -5
  264. package/src/sections/SurveyForm/types.ts +6 -0
  265. package/src/sections/_shared/merge-answers.ts +22 -0
  266. package/src/sections/_shared/section-shell.tsx +64 -0
  267. package/src/sections/_shared/use-assessment-session.ts +125 -0
  268. package/src/sections/index.ts +22 -0
  269. package/src/social/user-avatar.tsx +9 -5
  270. package/src/styles/globals.css +39 -41
  271. package/src/ui/badge.tsx +8 -0
  272. package/src/ui/index.ts +2 -0
  273. package/src/ui/progress.tsx +4 -0
  274. package/src/ui/rich-text-editor.tsx +10 -0
  275. package/src/ui/rich-text-toolbar.tsx +2 -1
  276. package/src/ui/toast.tsx +170 -0
  277. package/src/utils/debounce.ts +8 -2
  278. package/src/utils/pick-palette-color.ts +33 -0
  279. package/src/video/types.ts +16 -0
  280. package/src/video/video-player.tsx +13 -1
  281. package/dist/ForumBoard-CHXU3mjC.js +0 -2207
  282. package/dist/ForumBoard-d1w5-r6n.cjs +0 -1
  283. package/dist/tabs-DRM2Iq_J.cjs +0 -172
@@ -1,8 +1,10 @@
1
1
  import { useState } from "react";
2
2
  import { CheckCircle } from "lucide-react";
3
3
  import { FlashcardDeck } from "../../flashcards";
4
+ import { Skeleton } from "../../ui/skeleton";
4
5
  import { Button } from "../../ui/button";
5
6
  import { Card, CardContent } from "../../ui/card";
7
+ import { SectionShell } from "../_shared/section-shell";
6
8
  import type {
7
9
  FlashcardStudySessionProps,
8
10
  FlashcardSessionStats,
@@ -16,6 +18,9 @@ export function FlashcardStudySession({
16
18
  shuffled = false,
17
19
  onComplete,
18
20
  readOnly = false,
21
+ isLoading,
22
+ error,
23
+ onRetry,
19
24
  className,
20
25
  style,
21
26
  }: FlashcardStudySessionProps) {
@@ -35,42 +40,54 @@ export function FlashcardStudySession({
35
40
  setIsComplete(false);
36
41
  }
37
42
 
38
- if (isComplete) {
39
- return (
40
- <div className={cn("flex flex-col items-center", className)} style={style}>
41
- <Card>
42
- <CardContent className="pt-6 text-center flex flex-col items-center gap-2">
43
- <CheckCircle size={48} className="text-success" />
44
- <span className="text-xl font-bold text-foreground">Deck complete!</span>
45
- {title && (
46
- <span className="text-muted-foreground">
47
- You finished <strong>{title}</strong>
48
- </span>
49
- )}
50
- <span className="text-sm text-muted-foreground">
51
- {stats.totalCards} card{stats.totalCards !== 1 ? "s" : ""} studied
52
- {stats.wasShuffled ? " (shuffled)" : ""}
53
- </span>
54
- <Button className="mt-2" onClick={handleStudyAgain}>
55
- Study Again
56
- </Button>
57
- </CardContent>
58
- </Card>
59
- </div>
60
- );
61
- }
62
-
63
43
  return (
64
- <div className={cn("flex flex-col items-center", className)} style={style}>
65
- <FlashcardDeck
66
- cards={cards}
67
- deckName={title}
68
- deckDescription={description}
69
- shuffled={shuffled}
70
- showProgress
71
- onComplete={handleComplete}
72
- readOnly={readOnly}
73
- />
74
- </div>
44
+ <SectionShell
45
+ isLoading={isLoading}
46
+ error={error}
47
+ onRetry={onRetry}
48
+ className={className}
49
+ style={style}
50
+ skeleton={
51
+ <>
52
+ <Skeleton className="h-6 w-48" />
53
+ <Skeleton className="h-64 w-full rounded-lg" />
54
+ </>
55
+ }
56
+ >
57
+ {isComplete ? (
58
+ <div className={cn("flex flex-col items-center", className)} style={style}>
59
+ <Card>
60
+ <CardContent className="pt-6 text-center flex flex-col items-center gap-2">
61
+ <CheckCircle size={48} className="text-success" />
62
+ <span className="text-xl font-bold text-foreground">Deck complete!</span>
63
+ {title && (
64
+ <span className="text-muted-foreground">
65
+ You finished <strong>{title}</strong>
66
+ </span>
67
+ )}
68
+ <span className="text-sm text-muted-foreground">
69
+ {stats.totalCards} card{stats.totalCards !== 1 ? "s" : ""} studied
70
+ {stats.wasShuffled ? " (shuffled)" : ""}
71
+ </span>
72
+ <Button className="mt-2" onClick={handleStudyAgain}>
73
+ Study Again
74
+ </Button>
75
+ </CardContent>
76
+ </Card>
77
+ </div>
78
+ ) : (
79
+ <div className={cn("flex flex-col items-center", className)} style={style}>
80
+ <FlashcardDeck
81
+ cards={cards}
82
+ deckName={title}
83
+ deckDescription={description}
84
+ shuffled={shuffled}
85
+ showProgress
86
+ onComplete={handleComplete}
87
+ readOnly={readOnly}
88
+ />
89
+ </div>
90
+ )}
91
+ </SectionShell>
75
92
  );
76
93
  }
@@ -28,6 +28,12 @@ export interface FlashcardStudySessionProps {
28
28
  onComplete?: (stats: FlashcardSessionStats) => void;
29
29
  /** When true, disables card flipping */
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 */
@@ -1,5 +1,6 @@
1
1
  import { useState, useMemo } from "react";
2
2
  import {
3
+ AlertCircle,
3
4
  Plus,
4
5
  MessageSquare,
5
6
  Heart,
@@ -7,6 +8,7 @@ import {
7
8
  CheckCircle2,
8
9
  ArrowUpDown,
9
10
  } from "lucide-react";
11
+ import { Skeleton } from "../../ui/skeleton";
10
12
  import { SearchInput } from "../../common/search-input";
11
13
  import { UserAvatar } from "../../social/user-avatar";
12
14
  import { EmptyState } from "../../common/empty-state";
@@ -19,6 +21,7 @@ import { RichTextEditor } from "../../ui/rich-text-editor";
19
21
  import { isEmptyHtml } from "../../utils/is-empty-html";
20
22
  import { cn } from "../../lib/utils";
21
23
  import { formatTimestamp } from "../../utils/format-timestamp";
24
+ import { Pagination } from "../../common/pagination";
22
25
  import type { ForumBoardProps, ForumTopic, ForumSortOrder } from "./types";
23
26
 
24
27
  const SORT_LABELS: Record<ForumSortOrder, string> = {
@@ -44,6 +47,13 @@ export function ForumBoard({
44
47
  searchQuery = "",
45
48
  onSearchChange,
46
49
  readOnly,
50
+ isLoading,
51
+ error,
52
+ onRetry,
53
+ pageSize,
54
+ currentPage = 1,
55
+ totalItems,
56
+ onPageChange,
47
57
  className,
48
58
  style,
49
59
  }: ForumBoardProps) {
@@ -117,6 +127,42 @@ export function ForumBoard({
117
127
  });
118
128
  }, [topics, activeSearch, activeSort]);
119
129
 
130
+ if (isLoading) {
131
+ return (
132
+ <div className={cn("space-y-4", className)} style={style}>
133
+ <Skeleton className="h-9 w-full" />
134
+ {Array.from({ length: 4 }).map((_, i) => (
135
+ <div key={i} className="flex items-center gap-3">
136
+ <Skeleton className="h-8 w-8 rounded-full" />
137
+ <div className="flex-1 space-y-2">
138
+ <Skeleton className="h-5 w-64" />
139
+ <Skeleton className="h-4 w-32" />
140
+ </div>
141
+ </div>
142
+ ))}
143
+ </div>
144
+ );
145
+ }
146
+
147
+ if (error) {
148
+ return (
149
+ <div className={cn("py-12", className)} style={style}>
150
+ <EmptyState
151
+ icon={<AlertCircle className="size-10 text-destructive" />}
152
+ title="Something went wrong"
153
+ description={error}
154
+ action={
155
+ onRetry ? (
156
+ <Button variant="outline" onClick={onRetry}>
157
+ Retry
158
+ </Button>
159
+ ) : undefined
160
+ }
161
+ />
162
+ </div>
163
+ );
164
+ }
165
+
120
166
  return (
121
167
  <div className={cn("flex flex-col gap-4", className)} style={style}>
122
168
  {/* Header */}
@@ -204,7 +250,10 @@ export function ForumBoard({
204
250
  />
205
251
  ) : (
206
252
  <div className="flex flex-col gap-2">
207
- {sorted.map((topic) => (
253
+ {(onPageChange && pageSize
254
+ ? sorted.slice((currentPage - 1) * pageSize, currentPage * pageSize)
255
+ : sorted
256
+ ).map((topic) => (
208
257
  <TopicRow
209
258
  key={topic.uid}
210
259
  topic={topic}
@@ -213,6 +262,15 @@ export function ForumBoard({
213
262
  ))}
214
263
  </div>
215
264
  )}
265
+
266
+ {onPageChange && pageSize && sorted.length > 0 && (
267
+ <Pagination
268
+ currentPage={currentPage}
269
+ totalPages={Math.ceil((totalItems ?? sorted.length) / pageSize)}
270
+ onPageChange={onPageChange}
271
+ className="mt-4"
272
+ />
273
+ )}
216
274
  </div>
217
275
  );
218
276
  }
@@ -37,6 +37,20 @@ export interface ForumBoardProps {
37
37
  onSearchChange?: (query: string) => void;
38
38
  /** When true, disables create and interactions */
39
39
  readOnly?: boolean;
40
+ /** Render skeleton placeholders instead of content */
41
+ isLoading?: boolean;
42
+ /** Error message — renders an error state with optional retry */
43
+ error?: string | null;
44
+ /** Called when the user clicks retry in the error state */
45
+ onRetry?: () => void;
46
+ /** Number of items per page (enables pagination when set with onPageChange) */
47
+ pageSize?: number;
48
+ /** Current page (1-indexed) */
49
+ currentPage?: number;
50
+ /** Total number of items (for server-side pagination) */
51
+ totalItems?: number;
52
+ /** Called when the user navigates to a different page */
53
+ onPageChange?: (page: number) => void;
40
54
  /** CSS class name for the root element */
41
55
  className?: string;
42
56
  /** Inline styles for the root element */
@@ -1,9 +1,13 @@
1
1
  import { Fragment, useMemo, useState } from "react";
2
- import { ArrowUp, ArrowDown } from "lucide-react";
2
+ import { ArrowUp, ArrowDown, AlertCircle } from "lucide-react";
3
3
  import { GradeIndicator } from "../../progress";
4
4
  import { StatusBadge, DueDateDisplay } from "../../common";
5
5
  import { Card, CardContent } from "../../ui/card";
6
6
  import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "../../ui/table";
7
+ import { Skeleton } from "../../ui/skeleton";
8
+ import { Button } from "../../ui/button";
9
+ import { EmptyState } from "../../common/empty-state";
10
+ import { Pagination } from "../../common/pagination";
7
11
  import type { GradebookTableProps, GradeCategory } from "./types";
8
12
  import { cn } from "../../lib/utils";
9
13
 
@@ -53,6 +57,13 @@ export function GradebookTable({
53
57
  showCategoryTotals = true,
54
58
  onItemClick,
55
59
  readOnly = false,
60
+ isLoading,
61
+ error,
62
+ onRetry,
63
+ pageSize,
64
+ currentPage = 1,
65
+ totalItems,
66
+ onPageChange,
56
67
  className,
57
68
  style,
58
69
  }: GradebookTableProps) {
@@ -104,6 +115,39 @@ export function GradebookTable({
104
115
 
105
116
  const colCount = showWeights ? 5 : 4;
106
117
 
118
+ if (isLoading) {
119
+ return (
120
+ <div className={cn("space-y-4", className)} style={style}>
121
+ <Skeleton className="h-20 w-full" />
122
+ <Skeleton className="h-10 w-full" />
123
+ <Skeleton className="h-12 w-full" />
124
+ <Skeleton className="h-12 w-full" />
125
+ <Skeleton className="h-12 w-full" />
126
+ <Skeleton className="h-12 w-full" />
127
+ <Skeleton className="h-12 w-full" />
128
+ </div>
129
+ );
130
+ }
131
+
132
+ if (error) {
133
+ return (
134
+ <div className={cn("py-12", className)} style={style}>
135
+ <EmptyState
136
+ icon={<AlertCircle className="size-10 text-destructive" />}
137
+ title="Something went wrong"
138
+ description={error}
139
+ action={
140
+ onRetry ? (
141
+ <Button variant="outline" onClick={onRetry}>
142
+ Retry
143
+ </Button>
144
+ ) : undefined
145
+ }
146
+ />
147
+ </div>
148
+ );
149
+ }
150
+
107
151
  return (
108
152
  <div className={className} style={style}>
109
153
  {/* Overall grade */}
@@ -224,6 +268,15 @@ export function GradebookTable({
224
268
  </TableBody>
225
269
  </Table>
226
270
  </Card>
271
+
272
+ {onPageChange && pageSize && (
273
+ <Pagination
274
+ currentPage={currentPage}
275
+ totalPages={Math.ceil((totalItems ?? items.length) / pageSize)}
276
+ onPageChange={onPageChange}
277
+ className="mt-4"
278
+ />
279
+ )}
227
280
  </div>
228
281
  );
229
282
  }
@@ -27,6 +27,20 @@ export interface GradebookTableProps {
27
27
  onItemClick?: (item: GradeItem) => void;
28
28
  /** When true, disables interactions */
29
29
  readOnly?: boolean;
30
+ /** Render skeleton placeholders instead of content */
31
+ isLoading?: boolean;
32
+ /** Error message — renders an error state with optional retry */
33
+ error?: string | null;
34
+ /** Called when the user clicks retry in the error state */
35
+ onRetry?: () => void;
36
+ /** Number of items per page (enables pagination when set with onPageChange) */
37
+ pageSize?: number;
38
+ /** Current page (1-indexed) */
39
+ currentPage?: number;
40
+ /** Total number of items (for server-side pagination) */
41
+ totalItems?: number;
42
+ /** Called when the user navigates to a different page */
43
+ onPageChange?: (page: number) => void;
30
44
  /** CSS class name for the root element */
31
45
  className?: string;
32
46
  /** Inline styles for the root element */
@@ -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 gap-4",
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 { Skeleton } from "../../ui/skeleton";
5
+ import { EmptyState } from "../../common/empty-state";
4
6
  import { Button } from "../../ui/button";
5
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,18 +21,40 @@ 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
31
 
32
+ useEffect(() => {
33
+ setCompleted(isCompleted);
34
+ }, [isCompleted]);
35
+
26
36
  function handleMarkComplete() {
27
37
  setCompleted(true);
28
38
  onMarkComplete?.();
29
39
  }
30
40
 
31
41
  return (
32
- <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>
33
58
  {/* Header */}
34
59
  <div className="mb-3">
35
60
  <p className="text-2xl font-bold mb-0.5 text-foreground">{title}</p>
@@ -45,13 +70,18 @@ export function LessonPage({
45
70
 
46
71
  {/* Content blocks */}
47
72
  <div className="flex flex-col gap-3">
48
- {blocks.map((block, i) => (
73
+ {blocks?.length ? blocks.map((block, i) => (
49
74
  <ContentBlock
50
75
  key={i}
51
76
  block={block}
52
77
  readOnly={readOnly}
53
78
  />
54
- ))}
79
+ )) : (
80
+ <EmptyState
81
+ title="No content yet"
82
+ description="This lesson doesn't have any content blocks."
83
+ />
84
+ )}
55
85
  </div>
56
86
 
57
87
  {/* Completion bar */}
@@ -83,5 +113,6 @@ export function LessonPage({
83
113
  </CardContent>
84
114
  </Card>
85
115
  </div>
116
+ </SectionShell>
86
117
  );
87
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 */