@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
@@ -0,0 +1,220 @@
1
+ import { useMemo, useState } from "react";
2
+ import { AlertCircle, Grid, List as ListIcon, GraduationCap } from "lucide-react";
3
+ import { CourseCard } from "../../curriculum/course-card";
4
+ import { SearchInput, EmptyState } from "../../common";
5
+ import { Skeleton } from "../../ui/skeleton";
6
+ import { Button } from "../../ui/button";
7
+ import { Tabs, TabsList, TabsTrigger } from "../../ui/tabs";
8
+ import { Tooltip, TooltipTrigger, TooltipContent } from "../../ui/tooltip";
9
+ import { Pagination } from "../../common/pagination";
10
+ import type { CourseCatalogProps } from "./types";
11
+ import { cn } from "../../lib/utils";
12
+
13
+ export function CourseCatalog({
14
+ courses = [],
15
+ categories,
16
+ onCourseClick,
17
+ onEnroll,
18
+ viewMode: initialViewMode = "grid",
19
+ allowViewToggle = true,
20
+ showSearch = true,
21
+ emptyMessage = "No courses found",
22
+ readOnly = false,
23
+ isLoading,
24
+ error,
25
+ onRetry,
26
+ pageSize,
27
+ currentPage = 1,
28
+ totalItems,
29
+ onPageChange,
30
+ className,
31
+ style,
32
+ }: CourseCatalogProps) {
33
+ const [searchQuery, setSearchQuery] = useState("");
34
+ const [activeCategoryUid, setActiveCategoryUid] = useState<string | null>(null);
35
+ const [viewMode, setViewMode] = useState(initialViewMode);
36
+
37
+ const filtered = useMemo(() => {
38
+ let result = courses;
39
+ if (activeCategoryUid) {
40
+ result = result.filter((c) => c.categoryUid === activeCategoryUid);
41
+ }
42
+ if (searchQuery.trim()) {
43
+ const q = searchQuery.toLowerCase();
44
+ result = result.filter(
45
+ (c) =>
46
+ c.title.toLowerCase().includes(q) ||
47
+ c.description?.toLowerCase().includes(q) ||
48
+ c.instructor?.displayName.toLowerCase().includes(q),
49
+ );
50
+ }
51
+ return result;
52
+ }, [courses, activeCategoryUid, searchQuery]);
53
+
54
+ if (isLoading) {
55
+ return (
56
+ <div className={cn("space-y-4", className)} style={style}>
57
+ <Skeleton className="h-9 w-full" />
58
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
59
+ {Array.from({ length: 6 }).map((_, i) => (
60
+ <Skeleton key={i} className="h-64 w-full rounded-lg" />
61
+ ))}
62
+ </div>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ if (error) {
68
+ return (
69
+ <div className={cn("py-12", className)} style={style}>
70
+ <EmptyState
71
+ icon={<AlertCircle className="size-10 text-destructive" />}
72
+ title="Something went wrong"
73
+ description={error}
74
+ action={
75
+ onRetry ? (
76
+ <Button variant="outline" onClick={onRetry}>
77
+ Retry
78
+ </Button>
79
+ ) : undefined
80
+ }
81
+ />
82
+ </div>
83
+ );
84
+ }
85
+
86
+ const paginatedItems =
87
+ onPageChange && pageSize
88
+ ? filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize)
89
+ : filtered;
90
+
91
+ return (
92
+ <div className={className} style={style}>
93
+ {/* Toolbar */}
94
+ <div className="flex gap-2 items-center mb-2">
95
+ {showSearch && (
96
+ <div className="flex-1 max-w-80">
97
+ <SearchInput
98
+ value={searchQuery}
99
+ onChange={setSearchQuery}
100
+ placeholder="Search courses..."
101
+ size="small"
102
+ />
103
+ </div>
104
+ )}
105
+ {allowViewToggle && (
106
+ <div className="flex gap-0.5">
107
+ <Tooltip>
108
+ <TooltipTrigger>
109
+ <Button
110
+ variant="ghost"
111
+ size="icon-xs"
112
+ aria-label="Grid view"
113
+ className={cn(viewMode === "grid" && "text-primary")}
114
+ onClick={() => setViewMode("grid")}
115
+ >
116
+ <Grid size={18} />
117
+ </Button>
118
+ </TooltipTrigger>
119
+ <TooltipContent>Grid view</TooltipContent>
120
+ </Tooltip>
121
+ <Tooltip>
122
+ <TooltipTrigger>
123
+ <Button
124
+ variant="ghost"
125
+ size="icon-xs"
126
+ aria-label="List view"
127
+ className={cn(viewMode === "list" && "text-primary")}
128
+ onClick={() => setViewMode("list")}
129
+ >
130
+ <ListIcon size={18} />
131
+ </Button>
132
+ </TooltipTrigger>
133
+ <TooltipContent>List view</TooltipContent>
134
+ </Tooltip>
135
+ </div>
136
+ )}
137
+ </div>
138
+
139
+ {/* Category tabs */}
140
+ {categories && categories.length > 0 && (
141
+ <Tabs
142
+ value={activeCategoryUid ?? "all"}
143
+ onValueChange={(v) => setActiveCategoryUid(v === "all" ? null : v)}
144
+ className="mb-2"
145
+ >
146
+ <TabsList>
147
+ <TabsTrigger value="all">All</TabsTrigger>
148
+ {categories.map((cat) => (
149
+ <TabsTrigger key={cat.uid} value={cat.uid}>{cat.label}</TabsTrigger>
150
+ ))}
151
+ </TabsList>
152
+ </Tabs>
153
+ )}
154
+
155
+ {/* Course grid/list */}
156
+ {filtered.length === 0 ? (
157
+ <EmptyState
158
+ icon={<GraduationCap />}
159
+ title={emptyMessage}
160
+ description="Try adjusting your search or filter."
161
+ />
162
+ ) : viewMode === "grid" ? (
163
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
164
+ {paginatedItems.map((course) => (
165
+ <CourseCard
166
+ key={course.uid}
167
+ uid={course.uid}
168
+ title={course.title}
169
+ description={course.description}
170
+ thumbnailUrl={course.thumbnailUrl}
171
+ instructor={course.instructor}
172
+ progress={course.progress}
173
+ enrollmentStatus={course.enrollmentStatus}
174
+ studentCount={course.studentCount}
175
+ duration={course.duration}
176
+ layout="vertical"
177
+ onClick={readOnly ? undefined : () => onCourseClick(course)}
178
+ onEnroll={
179
+ onEnroll && !readOnly ? () => onEnroll(course) : undefined
180
+ }
181
+ className={cn(readOnly && "opacity-70")}
182
+ />
183
+ ))}
184
+ </div>
185
+ ) : (
186
+ <div className="flex flex-col gap-2">
187
+ {paginatedItems.map((course) => (
188
+ <CourseCard
189
+ key={course.uid}
190
+ uid={course.uid}
191
+ title={course.title}
192
+ description={course.description}
193
+ thumbnailUrl={course.thumbnailUrl}
194
+ instructor={course.instructor}
195
+ progress={course.progress}
196
+ enrollmentStatus={course.enrollmentStatus}
197
+ studentCount={course.studentCount}
198
+ duration={course.duration}
199
+ layout="horizontal"
200
+ onClick={readOnly ? undefined : () => onCourseClick(course)}
201
+ onEnroll={
202
+ onEnroll && !readOnly ? () => onEnroll(course) : undefined
203
+ }
204
+ className={cn(readOnly && "opacity-70")}
205
+ />
206
+ ))}
207
+ </div>
208
+ )}
209
+
210
+ {onPageChange && pageSize && filtered.length > 0 && (
211
+ <Pagination
212
+ currentPage={currentPage}
213
+ totalPages={Math.ceil((totalItems ?? filtered.length) / pageSize)}
214
+ onPageChange={onPageChange}
215
+ className="mt-4"
216
+ />
217
+ )}
218
+ </div>
219
+ );
220
+ }
@@ -0,0 +1,76 @@
1
+
2
+ /**
3
+ * CourseCatalog section — a searchable, filterable course catalog.
4
+ *
5
+ * Displays courses in a grid or list view with search, category tabs,
6
+ * view mode toggling, and optional pagination.
7
+ *
8
+ * @example
9
+ * <CourseCatalog
10
+ * courses={courses}
11
+ * categories={categories}
12
+ * onCourseClick={(c) => navigate(`/courses/${c.uid}`)}
13
+ * onEnroll={(c) => enroll(c.uid)}
14
+ * />
15
+ */
16
+ export interface CourseCatalogProps {
17
+ /** Courses to display */
18
+ courses: CourseInfo[];
19
+ /** Optional categories for tab filtering */
20
+ categories?: { uid: string; label: string }[];
21
+ /** Called when the user clicks a course */
22
+ onCourseClick: (course: CourseInfo) => void;
23
+ /** Called when the user clicks enroll on a course */
24
+ onEnroll?: (course: CourseInfo) => void;
25
+ /** Layout view mode */
26
+ viewMode?: "grid" | "list";
27
+ /** Whether the user can toggle between grid and list */
28
+ allowViewToggle?: boolean;
29
+ /** Whether to show search */
30
+ showSearch?: boolean;
31
+ /** Empty state message */
32
+ emptyMessage?: string;
33
+ /** When true, disables interactions */
34
+ readOnly?: boolean;
35
+ /** Render skeleton placeholders instead of content */
36
+ isLoading?: boolean;
37
+ /** Error message — renders an error state with optional retry */
38
+ error?: string | null;
39
+ /** Called when the user clicks retry in the error state */
40
+ onRetry?: () => void;
41
+ /** Number of items per page (enables pagination when set with onPageChange) */
42
+ pageSize?: number;
43
+ /** Current page (1-indexed) */
44
+ currentPage?: number;
45
+ /** Total number of items (for server-side pagination) */
46
+ totalItems?: number;
47
+ /** Called when the user navigates to a different page */
48
+ onPageChange?: (page: number) => void;
49
+ /** CSS class name for the root element */
50
+ className?: string;
51
+ /** Inline styles for the root element */
52
+ style?: React.CSSProperties;
53
+ }
54
+
55
+ export interface CourseInfo {
56
+ /** Unique identifier */
57
+ uid: string;
58
+ /** Course title */
59
+ title: string;
60
+ /** Course description */
61
+ description?: string;
62
+ /** Thumbnail image URL */
63
+ thumbnailUrl?: string;
64
+ /** Instructor info */
65
+ instructor?: { displayName: string; avatarUrl?: string };
66
+ /** Category UID for filtering */
67
+ categoryUid?: string;
68
+ /** Progress percentage (0-100) */
69
+ progress?: number;
70
+ /** Enrollment status */
71
+ enrollmentStatus?: "enrolled" | "completed" | "available" | "locked";
72
+ /** Number of enrolled students */
73
+ studentCount?: number;
74
+ /** Estimated duration (e.g. "12 hours") */
75
+ duration?: string;
76
+ }
@@ -1,7 +1,11 @@
1
1
  import { useMemo } from "react";
2
+ import { AlertCircle } from "lucide-react";
2
3
  import { CurriculumTree } from "../../curriculum";
3
4
  import { Progress } from "../../ui/progress";
4
5
  import { Separator } from "../../ui/separator";
6
+ import { Skeleton } from "../../ui/skeleton";
7
+ import { Button } from "../../ui/button";
8
+ import { EmptyState } from "../../common/empty-state";
5
9
  import { flattenLeaves } from "../../utils/flatten-leaves";
6
10
  import type { CourseOutlineProps } from "./types";
7
11
  import { cn } from "../../lib/utils";
@@ -16,6 +20,9 @@ export function CourseOutline({
16
20
  showDuration = true,
17
21
  showIcons = true,
18
22
  readOnly = false,
23
+ isLoading,
24
+ error,
25
+ onRetry,
19
26
  className,
20
27
  style,
21
28
  }: CourseOutlineProps) {
@@ -34,6 +41,40 @@ export function CourseOutline({
34
41
  };
35
42
  }, [items, progress]);
36
43
 
44
+ if (isLoading) {
45
+ return (
46
+ <div className={cn("space-y-4", className)} style={style}>
47
+ <Skeleton className="h-6 w-48" />
48
+ <Skeleton className="h-2 w-full" />
49
+ <Skeleton className="h-8 w-full" />
50
+ <Skeleton className="h-8 w-full ml-6" />
51
+ <Skeleton className="h-8 w-full ml-6" />
52
+ <Skeleton className="h-8 w-full" />
53
+ <Skeleton className="h-8 w-full ml-6" />
54
+ <Skeleton className="h-8 w-full" />
55
+ </div>
56
+ );
57
+ }
58
+
59
+ if (error) {
60
+ return (
61
+ <div className={cn("py-12", className)} style={style}>
62
+ <EmptyState
63
+ icon={<AlertCircle className="size-10 text-destructive" />}
64
+ title="Something went wrong"
65
+ description={error}
66
+ action={
67
+ onRetry ? (
68
+ <Button variant="outline" onClick={onRetry}>
69
+ Retry
70
+ </Button>
71
+ ) : undefined
72
+ }
73
+ />
74
+ </div>
75
+ );
76
+ }
77
+
37
78
  return (
38
79
  <div className={cn(className)} style={style}>
39
80
  {(courseTitle || showOverallProgress) && (
@@ -46,6 +46,12 @@ export interface CourseOutlineProps {
46
46
  showIcons?: boolean;
47
47
  /** When true, disables all click interactions */
48
48
  readOnly?: boolean;
49
+ /** Render skeleton placeholders instead of content */
50
+ isLoading?: boolean;
51
+ /** Error message — renders an error state with optional retry */
52
+ error?: string | null;
53
+ /** Called when the user clicks retry in the error state */
54
+ onRetry?: () => void;
49
55
  /** CSS class name for the root element */
50
56
  className?: string;
51
57
  /** Inline styles for the root element */
@@ -1,5 +1,7 @@
1
1
  import { useMemo, useState } from "react";
2
- import { CheckCircle, Heart, MessageSquare, Reply } from "lucide-react";
2
+ import { AlertCircle, CheckCircle, Heart, MessageSquare, Reply } from "lucide-react";
3
+ import { Skeleton } from "../../ui/skeleton";
4
+ import { EmptyState } from "../../common/empty-state";
3
5
  import { PostCard } from "../../social";
4
6
  import { Button } from "../../ui/button";
5
7
  import { RichTextEditor } from "../../ui/rich-text-editor";
@@ -23,6 +25,9 @@ export function DiscussionThread({
23
25
  allowReplies = true,
24
26
  sortOrder = "oldest",
25
27
  readOnly = false,
28
+ isLoading,
29
+ error,
30
+ onRetry,
26
31
  className,
27
32
  style,
28
33
  }: DiscussionThreadProps) {
@@ -49,6 +54,42 @@ export function DiscussionThread({
49
54
  return childrenMap;
50
55
  }, [replies, rootPost.uid, sortOrder]);
51
56
 
57
+ if (isLoading) {
58
+ return (
59
+ <div className={cn("space-y-4", className)} style={style}>
60
+ <Skeleton className="h-7 w-64" />
61
+ <div className="flex items-start gap-3">
62
+ <Skeleton className="h-8 w-8 rounded-full shrink-0" />
63
+ <Skeleton className="h-32 w-full" />
64
+ </div>
65
+ {Array.from({ length: 2 }).map((_, i) => (
66
+ <div key={i} className="ml-8">
67
+ <Skeleton className="h-20 w-full" />
68
+ </div>
69
+ ))}
70
+ </div>
71
+ );
72
+ }
73
+
74
+ if (error) {
75
+ return (
76
+ <div className={cn("py-12", className)} style={style}>
77
+ <EmptyState
78
+ icon={<AlertCircle className="size-10 text-destructive" />}
79
+ title="Something went wrong"
80
+ description={error}
81
+ action={
82
+ onRetry ? (
83
+ <Button variant="outline" onClick={onRetry}>
84
+ Retry
85
+ </Button>
86
+ ) : undefined
87
+ }
88
+ />
89
+ </div>
90
+ );
91
+ }
92
+
52
93
  function handleSubmitReply(parentUid: string) {
53
94
  if (isEmptyHtml(replyContent)) return;
54
95
  onReply(parentUid, replyContent);
@@ -38,6 +38,12 @@ export interface DiscussionThreadProps {
38
38
  sortOrder?: "newest" | "oldest" | "most_liked";
39
39
  /** When true, disables interactions */
40
40
  readOnly?: boolean;
41
+ /** Render skeleton placeholders instead of content */
42
+ isLoading?: boolean;
43
+ /** Error message — renders an error state with optional retry */
44
+ error?: string | null;
45
+ /** Called when the user clicks retry in the error state */
46
+ onRetry?: () => void;
41
47
  /** CSS class name for the root element */
42
48
  className?: string;
43
49
  /** Inline styles for the root element */