@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
@@ -18,6 +18,12 @@ export interface RequirementsChecklistProps {
18
18
  requirements: Requirement[];
19
19
  /** Called when the user clicks an incomplete requirement */
20
20
  onRequirementClick?: (uid: string) => void;
21
+ /** Render skeleton placeholders instead of content */
22
+ isLoading?: boolean;
23
+ /** Error message — renders an error state with optional retry */
24
+ error?: string | null;
25
+ /** Called when the user clicks retry in the error state */
26
+ onRetry?: () => void;
21
27
  /** CSS class name for the root element */
22
28
  className?: string;
23
29
  /** Inline styles for the root element */
@@ -1,11 +1,13 @@
1
1
  import { useMemo, useState } from "react";
2
- import { Download, Grid, List as ListIcon } from "lucide-react";
2
+ import { AlertCircle, Download, Grid, List as ListIcon } from "lucide-react";
3
3
  import { LearningObjectIcon } from "../../curriculum";
4
4
  import { SearchInput, EmptyState } from "../../common";
5
+ import { Skeleton } from "../../ui/skeleton";
5
6
  import { Button } from "../../ui/button";
6
7
  import { Card, CardContent } from "../../ui/card";
7
8
  import { Tabs, TabsList, TabsTrigger } from "../../ui/tabs";
8
9
  import { Tooltip, TooltipTrigger, TooltipContent } from "../../ui/tooltip";
10
+ import { Pagination } from "../../common/pagination";
9
11
  import type { ResourceLibraryProps, Resource } from "./types";
10
12
  import { cn } from "../../lib/utils";
11
13
  import { formatFileSize } from "../../utils/format-file-size";
@@ -30,6 +32,13 @@ export function ResourceLibrary({
30
32
  showSearch = true,
31
33
  emptyMessage = "No resources found",
32
34
  readOnly = false,
35
+ isLoading,
36
+ error,
37
+ onRetry,
38
+ pageSize,
39
+ currentPage = 1,
40
+ totalItems,
41
+ onPageChange,
33
42
  className,
34
43
  style,
35
44
  }: ResourceLibraryProps) {
@@ -53,6 +62,38 @@ export function ResourceLibrary({
53
62
  return result;
54
63
  }, [resources, activeCategoryUid, searchQuery]);
55
64
 
65
+ if (isLoading) {
66
+ return (
67
+ <div className={cn("space-y-4", className)} style={style}>
68
+ <Skeleton className="h-9 w-full" />
69
+ <div className="grid grid-cols-3 gap-4">
70
+ {Array.from({ length: 6 }).map((_, i) => (
71
+ <Skeleton key={i} className="h-32 w-full rounded-lg" />
72
+ ))}
73
+ </div>
74
+ </div>
75
+ );
76
+ }
77
+
78
+ if (error) {
79
+ return (
80
+ <div className={cn("py-12", className)} style={style}>
81
+ <EmptyState
82
+ icon={<AlertCircle className="size-10 text-destructive" />}
83
+ title="Something went wrong"
84
+ description={error}
85
+ action={
86
+ onRetry ? (
87
+ <Button variant="outline" onClick={onRetry}>
88
+ Retry
89
+ </Button>
90
+ ) : undefined
91
+ }
92
+ />
93
+ </div>
94
+ );
95
+ }
96
+
56
97
  function renderResource(resource: Resource) {
57
98
  const iconType = TYPE_TO_ICON[resource.type] ?? "document";
58
99
 
@@ -61,10 +102,10 @@ export function ResourceLibrary({
61
102
  key={resource.uid}
62
103
  className={cn(
63
104
  "flex items-center gap-3 px-3 py-2",
64
- !readOnly && "cursor-pointer hover:bg-muted",
65
- readOnly && "opacity-70",
105
+ !readOnly && onResourceClick && "cursor-pointer hover:bg-muted",
106
+ (readOnly || !onResourceClick) && "opacity-70",
66
107
  )}
67
- onClick={() => !readOnly && onResourceClick(resource)}
108
+ onClick={() => !readOnly && onResourceClick?.(resource)}
68
109
  >
69
110
  <div className="min-w-10">
70
111
  <LearningObjectIcon type={iconType} size={20} />
@@ -173,18 +214,24 @@ export function ResourceLibrary({
173
214
  <EmptyState title={emptyMessage} description="Try adjusting your search or filter." />
174
215
  ) : viewMode === "list" ? (
175
216
  <Card>
176
- {filtered.map(renderResource)}
217
+ {(onPageChange && pageSize
218
+ ? filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize)
219
+ : filtered
220
+ ).map(renderResource)}
177
221
  </Card>
178
222
  ) : (
179
223
  <div className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-2">
180
- {filtered.map((resource) => (
224
+ {(onPageChange && pageSize
225
+ ? filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize)
226
+ : filtered
227
+ ).map((resource) => (
181
228
  <Card
182
229
  key={resource.uid}
183
230
  className={cn(
184
231
  "transition-colors",
185
- !readOnly && "cursor-pointer hover:border-primary",
232
+ !readOnly && onResourceClick && "cursor-pointer hover:border-primary",
186
233
  )}
187
- onClick={() => !readOnly && onResourceClick(resource)}
234
+ onClick={() => !readOnly && onResourceClick?.(resource)}
188
235
  >
189
236
  <CardContent className="pt-4 pb-4">
190
237
  <div className="flex gap-1 items-center mb-1">
@@ -208,6 +255,15 @@ export function ResourceLibrary({
208
255
  ))}
209
256
  </div>
210
257
  )}
258
+
259
+ {onPageChange && pageSize && filtered.length > 0 && (
260
+ <Pagination
261
+ currentPage={currentPage}
262
+ totalPages={Math.ceil((totalItems ?? filtered.length) / pageSize)}
263
+ onPageChange={onPageChange}
264
+ className="mt-4"
265
+ />
266
+ )}
211
267
  </div>
212
268
  );
213
269
  }
@@ -18,7 +18,7 @@ export interface ResourceLibraryProps {
18
18
  /** Optional categories for tab filtering */
19
19
  categories?: { uid: string; label: string }[];
20
20
  /** Called when the user clicks a resource */
21
- onResourceClick: (resource: Resource) => void;
21
+ onResourceClick?: (resource: Resource) => void;
22
22
  /** Called when the user downloads a resource */
23
23
  onDownload?: (resource: Resource) => void;
24
24
  /** Layout view mode */
@@ -31,6 +31,20 @@ export interface ResourceLibraryProps {
31
31
  emptyMessage?: string;
32
32
  /** When true, disables interactions */
33
33
  readOnly?: boolean;
34
+ /** Render skeleton placeholders instead of content */
35
+ isLoading?: boolean;
36
+ /** Error message — renders an error state with optional retry */
37
+ error?: string | null;
38
+ /** Called when the user clicks retry in the error state */
39
+ onRetry?: () => void;
40
+ /** Number of items per page (enables pagination when set with onPageChange) */
41
+ pageSize?: number;
42
+ /** Current page (1-indexed) */
43
+ currentPage?: number;
44
+ /** Total number of items (for server-side pagination) */
45
+ totalItems?: number;
46
+ /** Called when the user navigates to a different page */
47
+ onPageChange?: (page: number) => void;
34
48
  /** CSS class name for the root element */
35
49
  className?: string;
36
50
  /** Inline styles for the root element */
@@ -1,4 +1,7 @@
1
- import { CheckCircle2 } from "lucide-react";
1
+ import { AlertCircle, CheckCircle2 } from "lucide-react";
2
+ import { Skeleton } from "../../ui/skeleton";
3
+ import { EmptyState } from "../../common/empty-state";
4
+ import { Button } from "../../ui/button";
2
5
  import { Card, CardContent } from "../../ui/card";
3
6
  import {
4
7
  Table,
@@ -26,11 +29,44 @@ export function RubricView({
26
29
  totalScore,
27
30
  maxScore,
28
31
  feedback,
32
+ isLoading,
33
+ error,
34
+ onRetry,
29
35
  className,
30
36
  style,
31
37
  }: RubricViewProps) {
32
38
  const isScored = selectedLevels && Object.keys(selectedLevels).length > 0;
33
39
 
40
+ if (isLoading) {
41
+ return (
42
+ <div className={cn("space-y-4", className)} style={style}>
43
+ <Skeleton className="h-10 w-full" />
44
+ {Array.from({ length: 3 }).map((_, i) => (
45
+ <Skeleton key={i} className="h-16 w-full" />
46
+ ))}
47
+ </div>
48
+ );
49
+ }
50
+
51
+ if (error) {
52
+ return (
53
+ <div className={cn("py-12", className)} style={style}>
54
+ <EmptyState
55
+ icon={<AlertCircle className="size-10 text-destructive" />}
56
+ title="Something went wrong"
57
+ description={error}
58
+ action={
59
+ onRetry ? (
60
+ <Button variant="outline" onClick={onRetry}>
61
+ Retry
62
+ </Button>
63
+ ) : undefined
64
+ }
65
+ />
66
+ </div>
67
+ );
68
+ }
69
+
34
70
  return (
35
71
  <div className={cn("flex flex-col gap-4", className)} style={style}>
36
72
  {/* Score header */}
@@ -23,6 +23,12 @@ export interface RubricViewProps {
23
23
  maxScore?: number;
24
24
  /** Instructor feedback text */
25
25
  feedback?: string;
26
+ /** Render skeleton placeholders instead of content */
27
+ isLoading?: boolean;
28
+ /** Error message — renders an error state with optional retry */
29
+ error?: string | null;
30
+ /** Called when the user clicks retry in the error state */
31
+ onRetry?: () => void;
26
32
  /** CSS class name for the root element */
27
33
  className?: string;
28
34
  /** Inline styles for the root element */
@@ -1,10 +1,15 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { QuestionRenderer } from "../../questions";
3
3
  import type { SessionAnswer } from "../../questions/types";
4
+
5
+ const EMPTY_ANSWERS: SessionAnswer[] = [];
4
6
  import { Button } from "../../ui/button";
5
7
  import { Card, CardContent } from "../../ui/card";
6
8
  import { Separator } from "../../ui/separator";
9
+ import { Skeleton } from "../../ui/skeleton";
7
10
  import { cn } from "../../lib/utils";
11
+ import { mergeSessionAnswers } from "../_shared/merge-answers";
12
+ import { SectionShell } from "../_shared/section-shell";
8
13
  import type { ScrollableQuizProps } from "./types";
9
14
 
10
15
  export function ScrollableQuiz({
@@ -17,6 +22,9 @@ export function ScrollableQuiz({
17
22
  questionGroups,
18
23
  isSubmitting = false,
19
24
  readOnly = false,
25
+ isLoading,
26
+ error,
27
+ onRetry,
20
28
  className,
21
29
  style,
22
30
  }: ScrollableQuizProps) {
@@ -60,19 +68,17 @@ export function ScrollableQuiz({
60
68
  else questionRefs.current.delete(uid);
61
69
  }, []);
62
70
 
63
- function handleAnswer(questionUid: string, rawAnswers: { uid: string; content?: string }[]) {
64
- const newAnswers: SessionAnswer[] = rawAnswers.map((a) => ({
65
- uid: questionUid,
66
- answerUid: a.uid,
67
- content: a.content,
68
- }));
69
- setSessionAnswers((prev) => {
70
- const filtered = prev.filter((a) => a.uid !== questionUid);
71
- const merged = [...filtered, ...newAnswers];
72
- onAnswerChange?.(merged);
73
- return merged;
74
- });
75
- }
71
+ const onAnswerChangeRef = useRef(onAnswerChange);
72
+ onAnswerChangeRef.current = onAnswerChange;
73
+
74
+ const handleAnswer = useCallback(
75
+ (questionUid: string, rawAnswers: { uid: string; content?: string }[]) => {
76
+ setSessionAnswers((prev) =>
77
+ mergeSessionAnswers(prev, questionUid, rawAnswers, onAnswerChangeRef.current),
78
+ );
79
+ },
80
+ [],
81
+ );
76
82
 
77
83
  function scrollToQuestion(uid: string) {
78
84
  questionRefs.current.get(uid)?.scrollIntoView({ behavior: "smooth", block: "center" });
@@ -97,7 +103,21 @@ export function ScrollableQuiz({
97
103
  let globalIndex = 0;
98
104
 
99
105
  return (
100
- <div className={cn("flex gap-3", className)} style={style}>
106
+ <SectionShell
107
+ isLoading={isLoading}
108
+ error={error}
109
+ onRetry={onRetry}
110
+ className={className}
111
+ style={style}
112
+ skeleton={
113
+ <>
114
+ <Skeleton className="h-32 w-full" />
115
+ <Skeleton className="h-32 w-full" />
116
+ <Skeleton className="h-32 w-full" />
117
+ </>
118
+ }
119
+ >
120
+ <div className="flex gap-3">
101
121
  {/* Main content */}
102
122
  <div className="flex-1 min-w-0">
103
123
  {orderedQuestions.map((group, gi) => (
@@ -124,7 +144,7 @@ export function ScrollableQuiz({
124
144
  )}
125
145
  <QuestionRenderer
126
146
  question={q}
127
- sessionAnswers={answersByQuestion.get(q.uid) ?? []}
147
+ sessionAnswers={answersByQuestion.get(q.uid) ?? EMPTY_ANSWERS}
128
148
  onAnswer={(answers) => handleAnswer(q.uid, answers)}
129
149
  readOnly={readOnly}
130
150
  />
@@ -180,5 +200,6 @@ export function ScrollableQuiz({
180
200
  </Card>
181
201
  )}
182
202
  </div>
203
+ </SectionShell>
183
204
  );
184
205
  }
@@ -33,6 +33,12 @@ export interface ScrollableQuizProps {
33
33
  isSubmitting?: boolean;
34
34
  /** When true, all inputs are disabled */
35
35
  readOnly?: boolean;
36
+ /** Render skeleton placeholders instead of content */
37
+ isLoading?: boolean;
38
+ /** Error message — renders an error state with optional retry */
39
+ error?: string | null;
40
+ /** Called when the user clicks retry in the error state */
41
+ onRetry?: () => void;
36
42
  /** CSS class name for the root element */
37
43
  className?: string;
38
44
  /** Inline styles for the root element */
@@ -0,0 +1,279 @@
1
+ import { Award, BookOpen, GraduationCap, Calendar } from "lucide-react";
2
+ import { UserAvatar } from "../../social/user-avatar";
3
+ import { StatCard } from "../../progress/stat-card";
4
+ import { AchievementBadge } from "../../progress/achievement-badge";
5
+ import { ProgressRing } from "../../progress/progress-ring";
6
+ import { Card, CardContent } from "../../ui/card";
7
+ import { Badge } from "../../ui/badge";
8
+ import { Button } from "../../ui/button";
9
+ import { Separator } from "../../ui/separator";
10
+ import { Skeleton } from "../../ui/skeleton";
11
+ import { EmptyState } from "../../common/empty-state";
12
+ import { SectionShell } from "../_shared/section-shell";
13
+ import { cn } from "../../lib/utils";
14
+ import { formatTimestamp } from "../../utils/format-timestamp";
15
+ import type { StudentProfileProps } from "./types";
16
+
17
+ export function StudentProfile({
18
+ student,
19
+ enrolledCourses = [],
20
+ achievements = [],
21
+ certificates = [],
22
+ stats,
23
+ showCourses = true,
24
+ showAchievements = true,
25
+ onCourseClick,
26
+ onCertificateClick,
27
+ readOnly = false,
28
+ isLoading,
29
+ error,
30
+ onRetry,
31
+ className,
32
+ style,
33
+ }: StudentProfileProps) {
34
+ const completedCourses = enrolledCourses.filter((c) => c.progress >= 100);
35
+
36
+ const defaultStats = [
37
+ {
38
+ label: "Enrolled",
39
+ value: String(enrolledCourses.length),
40
+ icon: <BookOpen size={24} />,
41
+ },
42
+ {
43
+ label: "Completed",
44
+ value: String(completedCourses.length),
45
+ icon: <GraduationCap size={24} />,
46
+ },
47
+ ];
48
+
49
+ const displayStats = stats ?? defaultStats;
50
+
51
+ return (
52
+ <SectionShell
53
+ isLoading={isLoading}
54
+ error={error}
55
+ onRetry={onRetry}
56
+ className={className}
57
+ style={style}
58
+ skeleton={
59
+ <>
60
+ {/* Profile header skeleton */}
61
+ <Card>
62
+ <CardContent className="p-4 flex items-center gap-4">
63
+ <Skeleton className="size-12 rounded-full" />
64
+ <div className="flex-1 space-y-2">
65
+ <Skeleton className="h-5 w-40" />
66
+ <Skeleton className="h-4 w-56" />
67
+ </div>
68
+ </CardContent>
69
+ </Card>
70
+ {/* Stat skeletons */}
71
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
72
+ <Skeleton className="h-24" />
73
+ <Skeleton className="h-24" />
74
+ <Skeleton className="h-24" />
75
+ <Skeleton className="h-24" />
76
+ </div>
77
+ {/* List skeletons */}
78
+ <Skeleton className="h-8 w-full" />
79
+ <Skeleton className="h-8 w-full" />
80
+ <Skeleton className="h-8 w-full" />
81
+ </>
82
+ }
83
+ >
84
+ <div className={cn("space-y-4", className)} style={style}>
85
+ {/* Profile Header */}
86
+ <Card>
87
+ <CardContent className="p-4 flex items-start gap-4">
88
+ <UserAvatar
89
+ displayName={student.displayName}
90
+ avatarUrl={student.avatarUrl}
91
+ size="large"
92
+ />
93
+ <div className="flex-1 min-w-0">
94
+ <div className="flex items-center gap-2 flex-wrap">
95
+ <h2 className="text-xl font-bold text-foreground">
96
+ {student.displayName}
97
+ </h2>
98
+ {student.role && (
99
+ <Badge variant="secondary">{student.role}</Badge>
100
+ )}
101
+ </div>
102
+ {student.email && (
103
+ <p className="text-sm text-muted-foreground">{student.email}</p>
104
+ )}
105
+ {student.bio && (
106
+ <p className="text-sm text-foreground mt-1">{student.bio}</p>
107
+ )}
108
+ {student.joinedAt && (
109
+ <p className="text-xs text-muted-foreground mt-1 flex items-center gap-1">
110
+ <Calendar size={12} />
111
+ Joined {new Date(student.joinedAt).toLocaleDateString()}
112
+ </p>
113
+ )}
114
+ </div>
115
+ </CardContent>
116
+ </Card>
117
+
118
+ {/* Stats Row */}
119
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
120
+ {displayStats.map((stat) => (
121
+ <StatCard
122
+ key={stat.label}
123
+ icon={stat.icon}
124
+ label={stat.label}
125
+ value={stat.value}
126
+ />
127
+ ))}
128
+ </div>
129
+
130
+ {/* Enrolled Courses */}
131
+ {showCourses && (
132
+ <>
133
+ <Separator />
134
+ <div className="flex items-center gap-2 mb-2">
135
+ <p className="text-lg font-semibold text-foreground">Enrolled Courses</p>
136
+ <Badge variant="secondary">{enrolledCourses.length}</Badge>
137
+ </div>
138
+ {enrolledCourses.length > 0 ? (
139
+ <Card>
140
+ <CardContent className="p-0 divide-y divide-border">
141
+ {enrolledCourses.map((course) => (
142
+ <div
143
+ key={course.uid}
144
+ className={cn(
145
+ "flex items-center gap-3 px-4 py-3 transition-colors",
146
+ !readOnly && onCourseClick && "cursor-pointer hover:bg-muted/50",
147
+ )}
148
+ onClick={
149
+ !readOnly && onCourseClick
150
+ ? () => onCourseClick(course.uid)
151
+ : undefined
152
+ }
153
+ >
154
+ <ProgressRing value={course.progress} size={32} strokeWidth={3} />
155
+ <div className="flex-1 min-w-0">
156
+ <p className="text-sm font-medium text-foreground truncate">
157
+ {course.title}
158
+ </p>
159
+ {course.lastAccessedAt && (
160
+ <p className="text-xs text-muted-foreground">
161
+ {formatTimestamp(course.lastAccessedAt)}
162
+ </p>
163
+ )}
164
+ </div>
165
+ <span className="text-xs text-muted-foreground whitespace-nowrap">
166
+ {Math.round(course.progress)}%
167
+ </span>
168
+ </div>
169
+ ))}
170
+ </CardContent>
171
+ </Card>
172
+ ) : (
173
+ <EmptyState
174
+ icon={<BookOpen />}
175
+ title="No courses yet"
176
+ description="This student has not enrolled in any courses."
177
+ />
178
+ )}
179
+ </>
180
+ )}
181
+
182
+ {/* Achievements */}
183
+ {showAchievements && (
184
+ <>
185
+ <Separator />
186
+ <div className="flex items-center gap-2 mb-2">
187
+ <p className="text-lg font-semibold text-foreground">Achievements</p>
188
+ <Badge variant="secondary">{achievements.length}</Badge>
189
+ </div>
190
+ {achievements.length > 0 ? (
191
+ <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
192
+ {achievements.map((achievement) => (
193
+ <AchievementBadge
194
+ key={achievement.uid}
195
+ title={achievement.name}
196
+ description={achievement.description}
197
+ icon={
198
+ achievement.iconUrl ? (
199
+ <img
200
+ src={achievement.iconUrl}
201
+ alt={achievement.name}
202
+ className="w-12 h-12"
203
+ />
204
+ ) : undefined
205
+ }
206
+ earnedDate={achievement.earnedAt}
207
+ />
208
+ ))}
209
+ </div>
210
+ ) : (
211
+ <EmptyState
212
+ icon={<Award />}
213
+ title="No achievements yet"
214
+ description="Achievements will appear here as they are earned."
215
+ />
216
+ )}
217
+ </>
218
+ )}
219
+
220
+ {/* Certificates */}
221
+ <Separator />
222
+ <div className="flex items-center gap-2 mb-2">
223
+ <p className="text-lg font-semibold text-foreground">Certificates</p>
224
+ <Badge variant="secondary">{certificates.length}</Badge>
225
+ </div>
226
+ {certificates.length > 0 ? (
227
+ <Card>
228
+ <CardContent className="p-0 divide-y divide-border">
229
+ {certificates.map((cert) => (
230
+ <div
231
+ key={cert.uid}
232
+ className={cn(
233
+ "flex items-center gap-3 px-4 py-3 transition-colors",
234
+ !readOnly && onCertificateClick && "cursor-pointer hover:bg-muted/50",
235
+ )}
236
+ onClick={
237
+ !readOnly && onCertificateClick
238
+ ? () => onCertificateClick(cert.uid)
239
+ : undefined
240
+ }
241
+ >
242
+ <div className="w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
243
+ <Award size={20} />
244
+ </div>
245
+ <div className="flex-1 min-w-0">
246
+ <p className="text-sm font-medium text-foreground truncate">
247
+ {cert.courseName}
248
+ </p>
249
+ <p className="text-xs text-muted-foreground">
250
+ Issued {new Date(cert.issuedAt).toLocaleDateString()}
251
+ </p>
252
+ </div>
253
+ {cert.certificateUrl && !readOnly && (
254
+ <Button
255
+ variant="outline"
256
+ size="sm"
257
+ onClick={(e) => {
258
+ e.stopPropagation();
259
+ window.open(cert.certificateUrl, "_blank");
260
+ }}
261
+ >
262
+ View
263
+ </Button>
264
+ )}
265
+ </div>
266
+ ))}
267
+ </CardContent>
268
+ </Card>
269
+ ) : (
270
+ <EmptyState
271
+ icon={<GraduationCap />}
272
+ title="No certificates yet"
273
+ description="Certificates will appear here once courses are completed."
274
+ />
275
+ )}
276
+ </div>
277
+ </SectionShell>
278
+ );
279
+ }