@hydralms/components 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/dist/StudentProfile-BVfZMbnV.cjs +1 -0
  2. package/dist/StudentProfile-DeMxdrL3.js +3275 -0
  3. package/dist/assessment-toolbar/assessment-toolbar.d.ts +1 -1
  4. package/dist/assessment-toolbar/index.d.ts +5 -1
  5. package/dist/assessment-toolbar/question-header-bar.d.ts +2 -0
  6. package/dist/assessment-toolbar/question-materials-drawer.d.ts +2 -0
  7. package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
  8. package/dist/assessment-toolbar/timer-display.d.ts +1 -1
  9. package/dist/assessment-toolbar/types.d.ts +52 -4
  10. package/dist/assessment-toolbar/use-countdown.d.ts +43 -0
  11. package/dist/common/index.d.ts +3 -1
  12. package/dist/common/pagination.d.ts +26 -0
  13. package/dist/common/stepper.d.ts +6 -0
  14. package/dist/common/types.d.ts +38 -0
  15. package/dist/components.css +1 -1
  16. package/dist/content/attachment-list.d.ts +6 -0
  17. package/dist/content/audio-player.d.ts +22 -0
  18. package/dist/content/code-block.d.ts +30 -0
  19. package/dist/content/content-block.d.ts +1 -1
  20. package/dist/content/embed-block.d.ts +28 -0
  21. package/dist/content/index.d.ts +8 -1
  22. package/dist/content/types.d.ts +63 -0
  23. package/dist/curriculum/course-card.d.ts +51 -0
  24. package/dist/curriculum/curriculum-item.d.ts +1 -1
  25. package/dist/curriculum/index.d.ts +2 -0
  26. package/dist/curriculum/types.d.ts +2 -2
  27. package/dist/index.cjs +1 -1
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +597 -308
  30. package/dist/license/HydraContext.d.ts +16 -0
  31. package/dist/license/ProBadge.d.ts +6 -0
  32. package/dist/license/index.d.ts +7 -0
  33. package/dist/license/tiers.d.ts +3 -0
  34. package/dist/license/useHydraLicense.d.ts +6 -0
  35. package/dist/license/validate.d.ts +13 -0
  36. package/dist/license/withProGate.d.ts +6 -0
  37. package/dist/modules/AssignmentModule/AssignmentModule.d.ts +5 -0
  38. package/dist/modules/AssignmentModule/types.d.ts +69 -0
  39. package/dist/modules/CertificateModule/CertificateModule.d.ts +5 -0
  40. package/dist/modules/CertificateModule/types.d.ts +51 -0
  41. package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
  42. package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
  43. package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
  44. package/dist/modules/DiscussionModule/DiscussionModule.d.ts +5 -0
  45. package/dist/modules/DiscussionModule/types.d.ts +47 -0
  46. package/dist/modules/ExamModule/ExamModule.d.ts +5 -0
  47. package/dist/modules/ExamModule/types.d.ts +55 -0
  48. package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
  49. package/dist/modules/FlashcardLab/types.d.ts +2 -0
  50. package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +5 -0
  51. package/dist/modules/GradeCenterModule/types.d.ts +56 -0
  52. package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
  53. package/dist/modules/QuizModule/types.d.ts +10 -14
  54. package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
  55. package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
  56. package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
  57. package/dist/modules/StudentProfileModule/types.d.ts +43 -0
  58. package/dist/modules/SurveyModule/SurveyModule.d.ts +5 -0
  59. package/dist/modules/SurveyModule/types.d.ts +51 -0
  60. package/dist/modules/_shared/assessment-intro.d.ts +16 -0
  61. package/dist/modules/_shared/assessment-results.d.ts +23 -0
  62. package/dist/modules/_shared/types.d.ts +10 -0
  63. package/dist/modules/_shared/use-timer.d.ts +9 -0
  64. package/dist/modules/index.d.ts +18 -0
  65. package/dist/modules.cjs +1 -0
  66. package/dist/modules.js +1834 -0
  67. package/dist/progress/achievement-badge.d.ts +6 -0
  68. package/dist/progress/activity-timeline.d.ts +6 -0
  69. package/dist/progress/index.d.ts +4 -1
  70. package/dist/progress/stat-card.d.ts +1 -1
  71. package/dist/progress/streak-badge.d.ts +6 -0
  72. package/dist/progress/types.d.ts +99 -0
  73. package/dist/provider/HydraProvider.d.ts +5 -1
  74. package/dist/questions/choice.d.ts +1 -1
  75. package/dist/questions/confidence-indicator.d.ts +37 -0
  76. package/dist/questions/essay.d.ts +2 -2
  77. package/dist/questions/fill-in-the-blank.d.ts +1 -1
  78. package/dist/questions/hotspot.d.ts +21 -0
  79. package/dist/questions/index.d.ts +11 -1
  80. package/dist/questions/inline-choice.d.ts +21 -0
  81. package/dist/questions/matching.d.ts +22 -0
  82. package/dist/questions/multiple-choice.d.ts +1 -1
  83. package/dist/questions/numeric.d.ts +11 -0
  84. package/dist/questions/ordering.d.ts +12 -0
  85. package/dist/questions/question-renderer.d.ts +1 -1
  86. package/dist/questions/scenario.d.ts +23 -0
  87. package/dist/questions/scoring.d.ts +22 -0
  88. package/dist/questions/spreadsheet.d.ts +29 -0
  89. package/dist/questions/true-false.d.ts +1 -1
  90. package/dist/questions/types.d.ts +106 -1
  91. package/dist/questions/use-drag-reorder.d.ts +17 -0
  92. package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
  93. package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
  94. package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
  95. package/dist/sections/AssessmentReview/types.d.ts +6 -0
  96. package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
  97. package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
  98. package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
  99. package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
  100. package/dist/sections/CertificateViewer/types.d.ts +13 -5
  101. package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
  102. package/dist/sections/CourseCatalog/types.d.ts +80 -0
  103. package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
  104. package/dist/sections/CourseOutline/types.d.ts +6 -0
  105. package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
  106. package/dist/sections/DiscussionThread/types.d.ts +6 -0
  107. package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
  108. package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
  109. package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
  110. package/dist/sections/ExamSession/types.d.ts +12 -1
  111. package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
  112. package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
  113. package/dist/sections/ForumBoard/ForumBoard.d.ts +8 -0
  114. package/dist/sections/ForumBoard/types.d.ts +78 -0
  115. package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
  116. package/dist/sections/GradebookTable/types.d.ts +14 -0
  117. package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
  118. package/dist/sections/LecturePlayer/types.d.ts +8 -0
  119. package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
  120. package/dist/sections/LessonPage/types.d.ts +6 -0
  121. package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
  122. package/dist/sections/PracticeQuiz/types.d.ts +6 -0
  123. package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
  124. package/dist/sections/ProgressDashboard/types.d.ts +6 -0
  125. package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
  126. package/dist/sections/QuizSession/types.d.ts +12 -1
  127. package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
  128. package/dist/sections/RequirementsChecklist/types.d.ts +43 -0
  129. package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
  130. package/dist/sections/ResourceLibrary/types.d.ts +15 -1
  131. package/dist/sections/RubricView/RubricView.d.ts +9 -0
  132. package/dist/sections/RubricView/types.d.ts +56 -0
  133. package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
  134. package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
  135. package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
  136. package/dist/sections/StudentProfile/types.d.ts +98 -0
  137. package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
  138. package/dist/sections/SurveyForm/types.d.ts +6 -0
  139. package/dist/sections/_shared/merge-answers.d.ts +9 -0
  140. package/dist/sections/_shared/section-shell.d.ts +20 -0
  141. package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
  142. package/dist/sections/index.d.ts +13 -1
  143. package/dist/sections.cjs +1 -1
  144. package/dist/sections.js +282 -1786
  145. package/dist/social/post-card.d.ts +1 -1
  146. package/dist/tabs-BsfVo2Bl.cjs +173 -0
  147. package/dist/tabs-BuY1iNJE.js +22305 -0
  148. package/dist/ui/alert.d.ts +1 -1
  149. package/dist/ui/badge.d.ts +1 -1
  150. package/dist/ui/button.d.ts +1 -1
  151. package/dist/ui/drawer.d.ts +84 -0
  152. package/dist/ui/index.d.ts +5 -0
  153. package/dist/ui/progress.d.ts +1 -1
  154. package/dist/ui/rich-text-editor.d.ts +32 -0
  155. package/dist/ui/rich-text-toolbar.d.ts +8 -0
  156. package/dist/ui/toast.d.ts +43 -0
  157. package/dist/utils/array-utils.d.ts +4 -0
  158. package/dist/utils/debounce.d.ts +5 -1
  159. package/dist/utils/flatten-leaves.d.ts +6 -0
  160. package/dist/utils/format-file-size.d.ts +1 -0
  161. package/dist/utils/format-timestamp.d.ts +1 -0
  162. package/dist/utils/is-empty-html.d.ts +5 -0
  163. package/dist/utils/pick-palette-color.d.ts +19 -0
  164. package/dist/utils/shuffle.d.ts +1 -0
  165. package/dist/utils/string-utils.d.ts +12 -0
  166. package/dist/video/types.d.ts +15 -0
  167. package/dist/video/video-bookmark.d.ts +1 -1
  168. package/dist/video/video-player.d.ts +1 -1
  169. package/dist/video/video-playlist-item.d.ts +1 -1
  170. package/dist/withProGate-BWqcKdPM.js +137 -0
  171. package/dist/withProGate-DX6XqKLp.cjs +1 -0
  172. package/package.json +40 -137
  173. package/src/assessment-toolbar/assessment-toolbar.tsx +54 -49
  174. package/src/assessment-toolbar/index.ts +6 -0
  175. package/src/assessment-toolbar/question-header-bar.tsx +61 -0
  176. package/src/assessment-toolbar/question-materials-drawer.tsx +55 -0
  177. package/src/assessment-toolbar/question-navigator.tsx +13 -36
  178. package/src/assessment-toolbar/timer-display.tsx +6 -5
  179. package/src/assessment-toolbar/types.ts +54 -4
  180. package/src/assessment-toolbar/use-countdown.ts +153 -0
  181. package/src/common/empty-state.tsx +1 -0
  182. package/src/common/index.ts +5 -0
  183. package/src/common/pagination.tsx +135 -0
  184. package/src/common/search-input.tsx +8 -6
  185. package/src/common/stepper.tsx +100 -0
  186. package/src/common/types.ts +41 -0
  187. package/src/content/attachment-list.tsx +92 -0
  188. package/src/content/audio-player.tsx +196 -0
  189. package/src/content/code-block.tsx +113 -0
  190. package/src/content/content-block.tsx +68 -2
  191. package/src/content/embed-block.tsx +78 -0
  192. package/src/content/file-upload-zone.tsx +11 -6
  193. package/src/content/index.ts +9 -0
  194. package/src/content/types.ts +46 -0
  195. package/src/curriculum/course-card.tsx +199 -0
  196. package/src/curriculum/curriculum-item.tsx +9 -5
  197. package/src/curriculum/curriculum-tree.tsx +20 -13
  198. package/src/curriculum/index.ts +2 -0
  199. package/src/curriculum/types.ts +2 -2
  200. package/src/feedback/feedback-banner.tsx +12 -14
  201. package/src/flashcards/flashcard-deck.tsx +1 -9
  202. package/src/flashcards/flashcard.tsx +29 -9
  203. package/src/index.ts +3 -0
  204. package/src/license/HydraContext.tsx +62 -0
  205. package/src/license/ProBadge.tsx +43 -0
  206. package/src/license/index.ts +7 -0
  207. package/src/license/tiers.ts +24 -0
  208. package/src/license/useHydraLicense.ts +10 -0
  209. package/src/license/validate.ts +90 -0
  210. package/src/license/withProGate.tsx +21 -0
  211. package/src/modules/AssignmentModule/AssignmentModule.tsx +314 -0
  212. package/src/modules/AssignmentModule/types.ts +77 -0
  213. package/src/modules/CertificateModule/CertificateModule.tsx +173 -0
  214. package/src/modules/CertificateModule/types.ts +49 -0
  215. package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
  216. package/src/modules/CourseCatalogModule/types.ts +47 -0
  217. package/src/modules/CoursePlayer/CoursePlayer.tsx +80 -69
  218. package/src/modules/DiscussionModule/DiscussionModule.tsx +145 -0
  219. package/src/modules/DiscussionModule/types.ts +54 -0
  220. package/src/modules/ExamModule/ExamModule.tsx +151 -0
  221. package/src/modules/ExamModule/types.ts +57 -0
  222. package/src/modules/FlashcardLab/FlashcardLab.tsx +39 -21
  223. package/src/modules/FlashcardLab/types.ts +2 -0
  224. package/src/modules/GradeCenterModule/GradeCenterModule.tsx +174 -0
  225. package/src/modules/GradeCenterModule/types.ts +65 -0
  226. package/src/modules/QuizModule/QuizModule.tsx +58 -178
  227. package/src/modules/QuizModule/types.ts +10 -15
  228. package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
  229. package/src/modules/StudentDashboardModule/types.ts +56 -0
  230. package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
  231. package/src/modules/StudentProfileModule/types.ts +45 -0
  232. package/src/modules/SurveyModule/SurveyModule.tsx +185 -0
  233. package/src/modules/SurveyModule/types.ts +53 -0
  234. package/src/modules/_shared/assessment-intro.tsx +75 -0
  235. package/src/modules/_shared/assessment-results.tsx +133 -0
  236. package/src/modules/_shared/types.ts +11 -0
  237. package/src/modules/_shared/use-timer.ts +49 -0
  238. package/src/modules/index.ts +33 -0
  239. package/src/progress/achievement-badge.tsx +52 -0
  240. package/src/progress/activity-timeline.tsx +84 -0
  241. package/src/progress/grade-indicator.tsx +9 -1
  242. package/src/progress/index.ts +7 -0
  243. package/src/progress/progress-ring.tsx +2 -1
  244. package/src/progress/stat-card.tsx +37 -18
  245. package/src/progress/streak-badge.tsx +35 -0
  246. package/src/progress/types.ts +103 -0
  247. package/src/provider/HydraProvider.tsx +15 -6
  248. package/src/questions/choice.tsx +19 -14
  249. package/src/questions/confidence-indicator.tsx +107 -0
  250. package/src/questions/essay.tsx +28 -28
  251. package/src/questions/fill-in-the-blank.tsx +20 -19
  252. package/src/questions/hotspot.tsx +154 -0
  253. package/src/questions/index.ts +18 -0
  254. package/src/questions/inline-choice.tsx +152 -0
  255. package/src/questions/matching.tsx +229 -0
  256. package/src/questions/multiple-choice.tsx +19 -14
  257. package/src/questions/numeric.tsx +106 -0
  258. package/src/questions/ordering.tsx +167 -0
  259. package/src/questions/question-renderer.tsx +24 -2
  260. package/src/questions/scenario.tsx +140 -0
  261. package/src/questions/scoring.ts +201 -0
  262. package/src/questions/spreadsheet.tsx +260 -0
  263. package/src/questions/true-false.tsx +19 -14
  264. package/src/questions/types.ts +123 -1
  265. package/src/questions/use-drag-reorder.ts +80 -0
  266. package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +66 -23
  267. package/src/sections/AnnouncementFeed/types.ts +15 -1
  268. package/src/sections/AssessmentReview/AssessmentReview.tsx +50 -2
  269. package/src/sections/AssessmentReview/types.ts +6 -0
  270. package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +44 -6
  271. package/src/sections/AssignmentSubmission/types.ts +6 -0
  272. package/src/sections/CertificateViewer/CertificateViewer.tsx +215 -60
  273. package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
  274. package/src/sections/CertificateViewer/types.ts +19 -5
  275. package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
  276. package/src/sections/CourseCatalog/types.ts +76 -0
  277. package/src/sections/CourseOutline/CourseOutline.tsx +45 -14
  278. package/src/sections/CourseOutline/types.ts +6 -0
  279. package/src/sections/DiscussionThread/DiscussionThread.tsx +55 -11
  280. package/src/sections/DiscussionThread/types.ts +6 -0
  281. package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
  282. package/src/sections/EnrollmentWizard/types.ts +65 -0
  283. package/src/sections/ExamSession/ExamSession.tsx +125 -82
  284. package/src/sections/ExamSession/types.ts +12 -1
  285. package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
  286. package/src/sections/FlashcardStudySession/types.ts +6 -0
  287. package/src/sections/ForumBoard/ForumBoard.tsx +342 -0
  288. package/src/sections/ForumBoard/types.ts +81 -0
  289. package/src/sections/GradebookTable/GradebookTable.tsx +55 -2
  290. package/src/sections/GradebookTable/types.ts +14 -0
  291. package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
  292. package/src/sections/LecturePlayer/types.ts +8 -0
  293. package/src/sections/LessonPage/LessonPage.tsx +40 -13
  294. package/src/sections/LessonPage/types.ts +6 -0
  295. package/src/sections/PracticeQuiz/PracticeQuiz.tsx +119 -98
  296. package/src/sections/PracticeQuiz/types.ts +6 -0
  297. package/src/sections/ProgressDashboard/ProgressDashboard.tsx +121 -67
  298. package/src/sections/ProgressDashboard/types.ts +6 -0
  299. package/src/sections/QuizSession/QuizSession.tsx +115 -67
  300. package/src/sections/QuizSession/types.ts +12 -1
  301. package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +147 -0
  302. package/src/sections/RequirementsChecklist/types.ts +44 -0
  303. package/src/sections/ResourceLibrary/ResourceLibrary.tsx +68 -17
  304. package/src/sections/ResourceLibrary/types.ts +15 -1
  305. package/src/sections/RubricView/RubricView.tsx +174 -0
  306. package/src/sections/RubricView/types.ts +58 -0
  307. package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +58 -23
  308. package/src/sections/ScrollableQuiz/types.ts +6 -0
  309. package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
  310. package/src/sections/StudentProfile/types.ts +99 -0
  311. package/src/sections/SurveyForm/SurveyForm.tsx +40 -10
  312. package/src/sections/SurveyForm/types.ts +6 -0
  313. package/src/sections/_shared/merge-answers.ts +22 -0
  314. package/src/sections/_shared/section-shell.tsx +64 -0
  315. package/src/sections/_shared/use-assessment-session.ts +125 -0
  316. package/src/sections/index.ts +42 -1
  317. package/src/social/post-card.tsx +8 -19
  318. package/src/social/user-avatar.tsx +10 -5
  319. package/src/styles/globals.css +52 -41
  320. package/src/ui/badge.tsx +8 -0
  321. package/src/ui/drawer.tsx +600 -0
  322. package/src/ui/index.ts +21 -0
  323. package/src/ui/progress.tsx +4 -0
  324. package/src/ui/rich-text-editor.tsx +119 -0
  325. package/src/ui/rich-text-toolbar.tsx +157 -0
  326. package/src/ui/toast.tsx +170 -0
  327. package/src/utils/array-utils.ts +17 -0
  328. package/src/utils/debounce.ts +8 -2
  329. package/src/utils/flatten-leaves.ts +17 -0
  330. package/src/utils/format-file-size.ts +5 -0
  331. package/src/utils/format-timestamp.ts +13 -0
  332. package/src/utils/is-empty-html.ts +7 -0
  333. package/src/utils/pick-palette-color.ts +33 -0
  334. package/src/utils/shuffle.ts +8 -0
  335. package/src/utils/string-utils.ts +30 -0
  336. package/src/video/types.ts +16 -0
  337. package/src/video/video-bookmark.tsx +4 -3
  338. package/src/video/video-chapter-list.tsx +9 -4
  339. package/src/video/video-player.tsx +24 -5
  340. package/src/video/video-playlist-item.tsx +8 -3
  341. package/src/video/video-thumbnail-card.tsx +4 -0
  342. package/src/video/video-transcript.tsx +8 -5
  343. package/dist/table-BrS5cDQu.js +0 -2510
  344. package/dist/table-D6AkBBEo.cjs +0 -1
@@ -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,22 +1,15 @@
1
1
  import { useMemo } from "react";
2
+ import { AlertCircle } from "lucide-react";
2
3
  import { CurriculumTree } from "../../curriculum";
3
- import type { CurriculumItem } from "../../curriculum/types";
4
4
  import { Progress } from "../../ui/progress";
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";
9
+ import { flattenLeaves } from "../../utils/flatten-leaves";
5
10
  import type { CourseOutlineProps } from "./types";
6
11
  import { cn } from "../../lib/utils";
7
12
 
8
- function flattenLeaves(items: CurriculumItem[]): string[] {
9
- const leaves: string[] = [];
10
- for (const item of items) {
11
- if (!item.children || item.children.length === 0) {
12
- leaves.push(item.uid);
13
- } else {
14
- leaves.push(...flattenLeaves(item.children));
15
- }
16
- }
17
- return leaves;
18
- }
19
-
20
13
  export function CourseOutline({
21
14
  items,
22
15
  progress,
@@ -27,6 +20,9 @@ export function CourseOutline({
27
20
  showDuration = true,
28
21
  showIcons = true,
29
22
  readOnly = false,
23
+ isLoading,
24
+ error,
25
+ onRetry,
30
26
  className,
31
27
  style,
32
28
  }: CourseOutlineProps) {
@@ -45,10 +41,44 @@ export function CourseOutline({
45
41
  };
46
42
  }, [items, progress]);
47
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
+
48
78
  return (
49
79
  <div className={cn(className)} style={style}>
50
80
  {(courseTitle || showOverallProgress) && (
51
- <div className="px-2 pt-2 pb-1">
81
+ <div className="px-2 pt-2 pb-2">
52
82
  {courseTitle && (
53
83
  <p className={cn("font-semibold text-sm text-foreground", showOverallProgress && "mb-1")}>
54
84
  {courseTitle}
@@ -64,6 +94,7 @@ export function CourseOutline({
64
94
  )}
65
95
  </div>
66
96
  )}
97
+ {(courseTitle || showOverallProgress) && <Separator />}
67
98
  <CurriculumTree
68
99
  items={items}
69
100
  progress={progress}
@@ -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,8 +1,12 @@
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
- import { Textarea } from "../../ui/textarea";
7
+ import { RichTextEditor } from "../../ui/rich-text-editor";
8
+ import { isEmptyHtml } from "../../utils/is-empty-html";
9
+ import { Badge } from "../../ui/badge";
6
10
  import { Separator } from "../../ui/separator";
7
11
  import { Card, CardContent } from "../../ui/card";
8
12
  import { Tooltip, TooltipTrigger, TooltipContent } from "../../ui/tooltip";
@@ -21,6 +25,9 @@ export function DiscussionThread({
21
25
  allowReplies = true,
22
26
  sortOrder = "oldest",
23
27
  readOnly = false,
28
+ isLoading,
29
+ error,
30
+ onRetry,
24
31
  className,
25
32
  style,
26
33
  }: DiscussionThreadProps) {
@@ -47,9 +54,45 @@ export function DiscussionThread({
47
54
  return childrenMap;
48
55
  }, [replies, rootPost.uid, sortOrder]);
49
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
+
50
93
  function handleSubmitReply(parentUid: string) {
51
- if (!replyContent.trim()) return;
52
- onReply(parentUid, replyContent.trim());
94
+ if (isEmptyHtml(replyContent)) return;
95
+ onReply(parentUid, replyContent);
53
96
  setReplyContent("");
54
97
  setReplyingToUid(null);
55
98
  }
@@ -132,18 +175,19 @@ export function DiscussionThread({
132
175
  className="mb-2"
133
176
  style={{ marginLeft: `${(effectiveDepth + 1) * 16}px` }}
134
177
  >
135
- <CardContent className="pt-4 pb-4">
136
- <Textarea
178
+ <CardContent className="py-4">
179
+ <RichTextEditor
137
180
  className="min-h-15 mb-2"
138
181
  placeholder="Write a reply..."
139
182
  value={replyContent}
140
- onChange={(e) => setReplyContent(e.target.value)}
183
+ onChange={(html) => setReplyContent(html)}
184
+ variant="minimal"
141
185
  />
142
186
  <div className="flex items-center gap-2">
143
187
  <Button
144
188
  size="sm"
145
189
  onClick={() => handleSubmitReply(post.uid)}
146
- disabled={!replyContent.trim()}
190
+ disabled={isEmptyHtml(replyContent)}
147
191
  >
148
192
  Post Reply
149
193
  </Button>
@@ -170,12 +214,12 @@ export function DiscussionThread({
170
214
 
171
215
  return (
172
216
  <div className={className} style={style}>
173
- <div className="flex items-center gap-1 mb-2">
217
+ <div className="flex items-center gap-2 mb-2">
174
218
  <MessageSquare size={20} className="text-foreground shrink-0" />
175
219
  <span className="text-lg font-semibold text-foreground">{title}</span>
176
- <span className="text-sm text-muted-foreground">
220
+ <Badge variant="muted" className="text-xs">
177
221
  {replies.length} {replies.length === 1 ? "reply" : "replies"}
178
- </span>
222
+ </Badge>
179
223
  </div>
180
224
 
181
225
  <Separator className="mb-2" />
@@ -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 */