@hydralms/components 0.1.2 → 0.2.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 (199) hide show
  1. package/dist/ForumBoard-CHXU3mjC.js +2207 -0
  2. package/dist/ForumBoard-d1w5-r6n.cjs +1 -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/types.d.ts +52 -4
  9. package/dist/assessment-toolbar/use-countdown.d.ts +43 -0
  10. package/dist/common/index.d.ts +2 -1
  11. package/dist/common/stepper.d.ts +6 -0
  12. package/dist/common/types.d.ts +37 -0
  13. package/dist/components.css +1 -1
  14. package/dist/content/attachment-list.d.ts +6 -0
  15. package/dist/content/content-block.d.ts +1 -1
  16. package/dist/content/index.d.ts +2 -1
  17. package/dist/content/types.d.ts +39 -0
  18. package/dist/curriculum/curriculum-item.d.ts +1 -1
  19. package/dist/index.cjs +1 -1
  20. package/dist/index.js +551 -312
  21. package/dist/modules/AssignmentModule/AssignmentModule.d.ts +8 -0
  22. package/dist/modules/AssignmentModule/types.d.ts +65 -0
  23. package/dist/modules/CertificateModule/CertificateModule.d.ts +9 -0
  24. package/dist/modules/CertificateModule/types.d.ts +49 -0
  25. package/dist/modules/DiscussionModule/DiscussionModule.d.ts +8 -0
  26. package/dist/modules/DiscussionModule/types.d.ts +47 -0
  27. package/dist/modules/ExamModule/ExamModule.d.ts +8 -0
  28. package/dist/modules/ExamModule/types.d.ts +64 -0
  29. package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +9 -0
  30. package/dist/modules/GradeCenterModule/types.d.ts +54 -0
  31. package/dist/modules/QuizModule/QuizModule.d.ts +1 -1
  32. package/dist/modules/QuizModule/types.d.ts +6 -1
  33. package/dist/modules/SurveyModule/SurveyModule.d.ts +7 -0
  34. package/dist/modules/SurveyModule/types.d.ts +49 -0
  35. package/dist/modules/index.d.ts +12 -0
  36. package/dist/modules.cjs +1 -0
  37. package/dist/modules.js +1422 -0
  38. package/dist/progress/achievement-badge.d.ts +6 -0
  39. package/dist/progress/activity-timeline.d.ts +6 -0
  40. package/dist/progress/index.d.ts +4 -1
  41. package/dist/progress/stat-card.d.ts +1 -1
  42. package/dist/progress/streak-badge.d.ts +6 -0
  43. package/dist/progress/types.d.ts +97 -0
  44. package/dist/questions/essay.d.ts +1 -1
  45. package/dist/questions/hotspot.d.ts +21 -0
  46. package/dist/questions/index.d.ts +9 -1
  47. package/dist/questions/inline-choice.d.ts +21 -0
  48. package/dist/questions/matching.d.ts +22 -0
  49. package/dist/questions/numeric.d.ts +11 -0
  50. package/dist/questions/ordering.d.ts +12 -0
  51. package/dist/questions/scenario.d.ts +23 -0
  52. package/dist/questions/scoring.d.ts +22 -0
  53. package/dist/questions/spreadsheet.d.ts +29 -0
  54. package/dist/questions/types.d.ts +106 -1
  55. package/dist/questions/use-drag-reorder.d.ts +17 -0
  56. package/dist/sections/CertificateViewer/types.d.ts +7 -5
  57. package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
  58. package/dist/sections/ExamSession/types.d.ts +6 -1
  59. package/dist/sections/ForumBoard/ForumBoard.d.ts +8 -0
  60. package/dist/sections/ForumBoard/types.d.ts +64 -0
  61. package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
  62. package/dist/sections/QuizSession/types.d.ts +6 -1
  63. package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
  64. package/dist/sections/RequirementsChecklist/types.d.ts +37 -0
  65. package/dist/sections/RubricView/RubricView.d.ts +9 -0
  66. package/dist/sections/RubricView/types.d.ts +50 -0
  67. package/dist/sections/index.d.ts +7 -1
  68. package/dist/sections.cjs +1 -1
  69. package/dist/sections.js +250 -1715
  70. package/dist/social/post-card.d.ts +1 -1
  71. package/dist/tabs-DRM2Iq_J.cjs +172 -0
  72. package/dist/tabs-Wf3h_Cx3.js +21580 -0
  73. package/dist/ui/alert.d.ts +1 -1
  74. package/dist/ui/badge.d.ts +1 -1
  75. package/dist/ui/button.d.ts +1 -1
  76. package/dist/ui/drawer.d.ts +84 -0
  77. package/dist/ui/index.d.ts +3 -0
  78. package/dist/ui/progress.d.ts +1 -1
  79. package/dist/ui/rich-text-editor.d.ts +30 -0
  80. package/dist/ui/rich-text-toolbar.d.ts +8 -0
  81. package/dist/utils/array-utils.d.ts +4 -0
  82. package/dist/utils/flatten-leaves.d.ts +6 -0
  83. package/dist/utils/format-file-size.d.ts +1 -0
  84. package/dist/utils/format-timestamp.d.ts +1 -0
  85. package/dist/utils/is-empty-html.d.ts +5 -0
  86. package/dist/utils/shuffle.d.ts +1 -0
  87. package/dist/utils/string-utils.d.ts +12 -0
  88. package/dist/video/video-bookmark.d.ts +1 -1
  89. package/dist/video/video-playlist-item.d.ts +1 -1
  90. package/package.json +141 -3
  91. package/src/assessment-toolbar/assessment-toolbar.tsx +54 -49
  92. package/src/assessment-toolbar/index.ts +6 -0
  93. package/src/assessment-toolbar/question-header-bar.tsx +61 -0
  94. package/src/assessment-toolbar/question-materials-drawer.tsx +55 -0
  95. package/src/assessment-toolbar/question-navigator.tsx +3 -31
  96. package/src/assessment-toolbar/timer-display.tsx +2 -2
  97. package/src/assessment-toolbar/types.ts +54 -4
  98. package/src/assessment-toolbar/use-countdown.ts +153 -0
  99. package/src/common/index.ts +3 -0
  100. package/src/common/search-input.tsx +7 -6
  101. package/src/common/stepper.tsx +100 -0
  102. package/src/common/types.ts +39 -0
  103. package/src/content/attachment-list.tsx +90 -0
  104. package/src/content/content-block.tsx +4 -2
  105. package/src/content/file-upload-zone.tsx +1 -6
  106. package/src/content/index.ts +3 -0
  107. package/src/content/types.ts +41 -0
  108. package/src/curriculum/curriculum-item.tsx +7 -3
  109. package/src/feedback/feedback-banner.tsx +12 -14
  110. package/src/flashcards/flashcard-deck.tsx +1 -9
  111. package/src/flashcards/flashcard.tsx +1 -1
  112. package/src/modules/AssignmentModule/AssignmentModule.tsx +305 -0
  113. package/src/modules/AssignmentModule/types.ts +73 -0
  114. package/src/modules/CertificateModule/CertificateModule.tsx +161 -0
  115. package/src/modules/CertificateModule/types.ts +47 -0
  116. package/src/modules/CoursePlayer/CoursePlayer.tsx +44 -48
  117. package/src/modules/DiscussionModule/DiscussionModule.tsx +110 -0
  118. package/src/modules/DiscussionModule/types.ts +54 -0
  119. package/src/modules/ExamModule/ExamModule.tsx +285 -0
  120. package/src/modules/ExamModule/types.ts +66 -0
  121. package/src/modules/FlashcardLab/FlashcardLab.tsx +29 -16
  122. package/src/modules/GradeCenterModule/GradeCenterModule.tsx +169 -0
  123. package/src/modules/GradeCenterModule/types.ts +63 -0
  124. package/src/modules/QuizModule/QuizModule.tsx +88 -88
  125. package/src/modules/QuizModule/types.ts +6 -1
  126. package/src/modules/SurveyModule/SurveyModule.tsx +180 -0
  127. package/src/modules/SurveyModule/types.ts +51 -0
  128. package/src/modules/index.ts +24 -0
  129. package/src/progress/achievement-badge.tsx +52 -0
  130. package/src/progress/activity-timeline.tsx +84 -0
  131. package/src/progress/index.ts +7 -0
  132. package/src/progress/stat-card.tsx +30 -18
  133. package/src/progress/streak-badge.tsx +35 -0
  134. package/src/progress/types.ts +101 -0
  135. package/src/questions/choice.tsx +7 -9
  136. package/src/questions/essay.tsx +23 -25
  137. package/src/questions/fill-in-the-blank.tsx +13 -16
  138. package/src/questions/hotspot.tsx +154 -0
  139. package/src/questions/index.ts +16 -0
  140. package/src/questions/inline-choice.tsx +151 -0
  141. package/src/questions/matching.tsx +228 -0
  142. package/src/questions/multiple-choice.tsx +7 -9
  143. package/src/questions/numeric.tsx +102 -0
  144. package/src/questions/ordering.tsx +159 -0
  145. package/src/questions/question-renderer.tsx +21 -0
  146. package/src/questions/scenario.tsx +140 -0
  147. package/src/questions/scoring.ts +201 -0
  148. package/src/questions/spreadsheet.tsx +259 -0
  149. package/src/questions/true-false.tsx +7 -9
  150. package/src/questions/types.ts +123 -1
  151. package/src/questions/use-drag-reorder.ts +80 -0
  152. package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +2 -15
  153. package/src/sections/AssessmentReview/AssessmentReview.tsx +13 -2
  154. package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +7 -5
  155. package/src/sections/CertificateViewer/CertificateViewer.tsx +409 -56
  156. package/src/sections/CertificateViewer/types.ts +13 -5
  157. package/src/sections/CourseOutline/CourseOutline.tsx +4 -14
  158. package/src/sections/DiscussionThread/DiscussionThread.tsx +13 -10
  159. package/src/sections/ExamSession/ExamSession.tsx +44 -7
  160. package/src/sections/ExamSession/types.ts +6 -1
  161. package/src/sections/ForumBoard/ForumBoard.tsx +284 -0
  162. package/src/sections/ForumBoard/types.ts +67 -0
  163. package/src/sections/GradebookTable/GradebookTable.tsx +1 -1
  164. package/src/sections/LecturePlayer/LecturePlayer.tsx +1 -1
  165. package/src/sections/LessonPage/LessonPage.tsx +5 -9
  166. package/src/sections/PracticeQuiz/PracticeQuiz.tsx +15 -26
  167. package/src/sections/ProgressDashboard/ProgressDashboard.tsx +65 -65
  168. package/src/sections/QuizSession/QuizSession.tsx +67 -8
  169. package/src/sections/QuizSession/types.ts +6 -1
  170. package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +107 -0
  171. package/src/sections/RequirementsChecklist/types.ts +38 -0
  172. package/src/sections/ResourceLibrary/ResourceLibrary.tsx +4 -9
  173. package/src/sections/RubricView/RubricView.tsx +138 -0
  174. package/src/sections/RubricView/types.ts +52 -0
  175. package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +23 -9
  176. package/src/sections/SurveyForm/SurveyForm.tsx +8 -5
  177. package/src/sections/index.ts +20 -1
  178. package/src/social/post-card.tsx +8 -19
  179. package/src/social/user-avatar.tsx +1 -0
  180. package/src/styles/globals.css +13 -0
  181. package/src/ui/drawer.tsx +600 -0
  182. package/src/ui/index.ts +19 -0
  183. package/src/ui/rich-text-editor.tsx +109 -0
  184. package/src/ui/rich-text-toolbar.tsx +156 -0
  185. package/src/utils/array-utils.ts +17 -0
  186. package/src/utils/flatten-leaves.ts +17 -0
  187. package/src/utils/format-file-size.ts +5 -0
  188. package/src/utils/format-timestamp.ts +13 -0
  189. package/src/utils/is-empty-html.ts +7 -0
  190. package/src/utils/shuffle.ts +8 -0
  191. package/src/utils/string-utils.ts +30 -0
  192. package/src/video/video-bookmark.tsx +4 -3
  193. package/src/video/video-chapter-list.tsx +9 -4
  194. package/src/video/video-player.tsx +11 -4
  195. package/src/video/video-playlist-item.tsx +8 -3
  196. package/src/video/video-thumbnail-card.tsx +4 -0
  197. package/src/video/video-transcript.tsx +8 -5
  198. package/dist/table-BrS5cDQu.js +0 -2510
  199. package/dist/table-D6AkBBEo.cjs +0 -1
@@ -0,0 +1,161 @@
1
+ import { useState, useRef, useEffect, useMemo } from "react";
2
+ import { Award, ArrowLeft, PartyPopper } from "lucide-react";
3
+ import { RequirementsChecklist } from "../../sections/RequirementsChecklist/RequirementsChecklist";
4
+ import { CertificateViewer } from "../../sections/CertificateViewer/CertificateViewer";
5
+ import { ProgressRing } from "../../progress/progress-ring";
6
+ import { Button } from "../../ui/button";
7
+ import { Card, CardContent } from "../../ui/card";
8
+ import { cn } from "../../lib/utils";
9
+ import type { CertificateModuleProps } from "./types";
10
+
11
+ type InternalStep = { tag: "requirements" } | { tag: "certificate" };
12
+
13
+ /**
14
+ * CertificateModule — a certificate-earning flow with requirements tracking
15
+ * and certificate display.
16
+ *
17
+ * Steps: Requirements (checklist + progress) → Certificate (CertificateViewer).
18
+ * The certificate step is only accessible when all requirements are completed.
19
+ */
20
+ export function CertificateModule({
21
+ courseTitle,
22
+ recipientName,
23
+ organizationName,
24
+ organizationLogo,
25
+ signatory,
26
+ completionDate,
27
+ certificateVariant = "modern",
28
+ requirements,
29
+ overallProgress,
30
+ onRequirementClick,
31
+ onCertificateEarned,
32
+ className,
33
+ style,
34
+ }: CertificateModuleProps) {
35
+ const { allComplete, completedCount } = useMemo(() => {
36
+ const count = requirements.filter((r) => r.completed).length;
37
+ return { allComplete: count === requirements.length, completedCount: count };
38
+ }, [requirements]);
39
+
40
+ const [step, setStep] = useState<InternalStep>({ tag: "requirements" });
41
+ const contentRef = useRef<HTMLDivElement>(null);
42
+ const earnedFiredRef = useRef(false);
43
+
44
+ useEffect(() => {
45
+ contentRef.current?.focus({ preventScroll: true });
46
+ }, [step.tag]);
47
+
48
+ // Fire onCertificateEarned once when certificate step is first shown
49
+ useEffect(() => {
50
+ if (step.tag === "certificate" && !earnedFiredRef.current) {
51
+ earnedFiredRef.current = true;
52
+ onCertificateEarned?.();
53
+ }
54
+ }, [step.tag, onCertificateEarned]);
55
+
56
+ // ─── Requirements Screen ───
57
+ if (step.tag === "requirements") {
58
+ return (
59
+ <div
60
+ ref={contentRef}
61
+ tabIndex={-1}
62
+ className={cn("max-w-2xl mx-auto outline-none", className)}
63
+ style={style}
64
+ >
65
+ <Card>
66
+ <CardContent className="pt-8 pb-8">
67
+ {/* Header with progress */}
68
+ <div className="text-center mb-6">
69
+ <ProgressRing
70
+ value={overallProgress}
71
+ size={100}
72
+ strokeWidth={8}
73
+ color={
74
+ allComplete ? "var(--success)" : "var(--primary)"
75
+ }
76
+ className="mx-auto mb-4 text-foreground"
77
+ />
78
+ <h2 className="text-xl font-bold text-foreground mb-2">
79
+ {courseTitle}
80
+ </h2>
81
+ <p className="text-sm text-muted-foreground">
82
+ {allComplete
83
+ ? "All requirements met — your certificate is ready!"
84
+ : `${completedCount} of ${requirements.length} requirements complete`}
85
+ </p>
86
+ </div>
87
+
88
+ {/* Requirements checklist */}
89
+ <RequirementsChecklist
90
+ requirements={requirements}
91
+ onRequirementClick={onRequirementClick}
92
+ className="mb-6"
93
+ />
94
+
95
+ {/* View Certificate button */}
96
+ {allComplete && (
97
+ <div className="text-center mt-6">
98
+ <Button
99
+ size="lg"
100
+ onClick={() => setStep({ tag: "certificate" })}
101
+ >
102
+ <Award className="size-4 mr-2" />
103
+ View Certificate
104
+ </Button>
105
+ </div>
106
+ )}
107
+ </CardContent>
108
+ </Card>
109
+ </div>
110
+ );
111
+ }
112
+
113
+ // ─── Certificate Screen ───
114
+ return (
115
+ <div
116
+ ref={contentRef}
117
+ tabIndex={-1}
118
+ className={cn("outline-none", className)}
119
+ style={style}
120
+ >
121
+ {/* Back link */}
122
+ <Button
123
+ variant="ghost"
124
+ size="sm"
125
+ onClick={() => setStep({ tag: "requirements" })}
126
+ className="mb-4"
127
+ >
128
+ <ArrowLeft className="size-4 mr-1.5" />
129
+ Back to Requirements
130
+ </Button>
131
+
132
+ {/* Congratulations header */}
133
+ <div className="text-center mb-8">
134
+ <div className="mx-auto mb-4 w-14 h-14 rounded-full bg-success/10 flex items-center justify-center">
135
+ <PartyPopper className="size-7 text-success" />
136
+ </div>
137
+ <h2 className="text-2xl font-bold text-foreground mb-2">
138
+ Congratulations!
139
+ </h2>
140
+ <p className="text-muted-foreground">
141
+ You&apos;ve completed all requirements for{" "}
142
+ <span className="font-medium text-foreground">{courseTitle}</span>
143
+ </p>
144
+ </div>
145
+
146
+ {/* Certificate */}
147
+ <CertificateViewer
148
+ recipientName={recipientName}
149
+ courseTitle={courseTitle}
150
+ completionDate={
151
+ completionDate ?? new Date().toISOString().split("T")[0]
152
+ }
153
+ organizationName={organizationName}
154
+ organizationLogo={organizationLogo}
155
+ signatory={signatory}
156
+ variant={certificateVariant}
157
+ showActions
158
+ />
159
+ </div>
160
+ );
161
+ }
@@ -0,0 +1,47 @@
1
+ import type { Requirement } from "../../sections/RequirementsChecklist/types";
2
+ import type { CertificateVariant } from "../../sections/CertificateViewer/types";
3
+
4
+ /**
5
+ * CertificateModule — a certificate-earning flow with requirements tracking
6
+ * and certificate display.
7
+ *
8
+ * Steps: Requirements (checklist + progress) → Certificate (CertificateViewer).
9
+ * The certificate step is only accessible when all requirements are met.
10
+ *
11
+ * @example
12
+ * <CertificateModule
13
+ * courseTitle="React Fundamentals"
14
+ * recipientName="Jane Smith"
15
+ * organizationName="HydraLMS Academy"
16
+ * requirements={requirements}
17
+ * overallProgress={100}
18
+ * />
19
+ */
20
+ export interface CertificateModuleProps {
21
+ /** Course or program title */
22
+ courseTitle: string;
23
+ /** Recipient's full name */
24
+ recipientName: string;
25
+ /** Issuing organization name */
26
+ organizationName: string;
27
+ /** Organization logo URL */
28
+ organizationLogo?: string;
29
+ /** Signatory information */
30
+ signatory?: { name: string; title: string };
31
+ /** Completion date (used on the certificate) */
32
+ completionDate?: string;
33
+ /** Certificate visual variant. @default "modern" */
34
+ certificateVariant?: CertificateVariant;
35
+ /** Completion requirements */
36
+ requirements: Requirement[];
37
+ /** Overall course progress (0-100) */
38
+ overallProgress: number;
39
+ /** Called when a requirement item is clicked */
40
+ onRequirementClick?: (uid: string) => void;
41
+ /** Called when the certificate screen is first displayed */
42
+ onCertificateEarned?: () => void;
43
+ /** CSS class name for the root element */
44
+ className?: string;
45
+ /** Inline styles for the root element */
46
+ style?: React.CSSProperties;
47
+ }
@@ -1,11 +1,18 @@
1
- import { useState, useMemo } from "react";
1
+ import { useState, useMemo, useRef, useEffect } from "react";
2
2
  import {
3
3
  ChevronLeft,
4
4
  ChevronRight,
5
5
  Check,
6
- PanelLeftClose,
7
6
  PanelLeft,
8
7
  } from "lucide-react";
8
+ import {
9
+ Drawer,
10
+ DrawerContent,
11
+ DrawerHeader,
12
+ DrawerBody,
13
+ DrawerTitle,
14
+ DrawerClose,
15
+ } from "../../ui/drawer";
9
16
  import { CourseOutline } from "../../sections/CourseOutline/CourseOutline";
10
17
  import { LessonPage } from "../../sections/LessonPage/LessonPage";
11
18
  import { LecturePlayer } from "../../sections/LecturePlayer/LecturePlayer";
@@ -13,22 +20,11 @@ import { PracticeQuiz } from "../../sections/PracticeQuiz/PracticeQuiz";
13
20
  import { EmptyState } from "../../common";
14
21
  import { Progress } from "../../ui/progress";
15
22
  import { Button } from "../../ui/button";
23
+ import { flattenLeaves } from "../../utils/flatten-leaves";
16
24
  import { cn } from "../../lib/utils";
17
25
  import type { CurriculumItem } from "../../curriculum/types";
18
26
  import type { CoursePlayerProps, CoursePlayerItem } from "./types";
19
27
 
20
- function flattenLeaves(items: CurriculumItem[]): string[] {
21
- const leaves: string[] = [];
22
- for (const item of items) {
23
- if (!item.children || item.children.length === 0) {
24
- leaves.push(item.uid);
25
- } else {
26
- leaves.push(...flattenLeaves(item.children));
27
- }
28
- }
29
- return leaves;
30
- }
31
-
32
28
  export function CoursePlayer({
33
29
  courseTitle,
34
30
  curriculum,
@@ -42,6 +38,7 @@ export function CoursePlayer({
42
38
  className,
43
39
  style,
44
40
  }: CoursePlayerProps) {
41
+ const contentRef = useRef<HTMLDivElement>(null);
45
42
  const leafUids = useMemo(() => flattenLeaves(curriculum), [curriculum]);
46
43
  const itemMap = useMemo(
47
44
  () => new Map(items.map((item) => [item.uid, item])),
@@ -74,6 +71,10 @@ export function CoursePlayer({
74
71
 
75
72
  const activeItem = itemMap.get(activeUid);
76
73
 
74
+ useEffect(() => {
75
+ contentRef.current?.focus({ preventScroll: true });
76
+ }, [activeUid]);
77
+
77
78
  function navigateTo(uid: string) {
78
79
  setActiveUid(uid);
79
80
  onItemChange?.(uid);
@@ -121,45 +122,40 @@ export function CoursePlayer({
121
122
  className={cn("flex h-full overflow-hidden", className)}
122
123
  style={style}
123
124
  >
124
- {/* Sidebar */}
125
- {sidebarOpen && (
126
- <aside className="w-80 shrink-0 border-r border-border overflow-y-auto bg-background">
127
- <div className="flex items-center justify-between px-3 pt-3 pb-1">
128
- <span className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
125
+ {/* Sidebar Drawer */}
126
+ <Drawer open={sidebarOpen} onOpenChange={setSidebarOpen} side="left">
127
+ <DrawerContent size="sm" scrollLock={false}>
128
+ <DrawerHeader className="flex-row items-center justify-between">
129
+ <DrawerTitle className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
129
130
  Course
130
- </span>
131
- <button
132
- type="button"
133
- className="p-1 rounded hover:bg-muted text-muted-foreground"
134
- onClick={() => setSidebarOpen(false)}
135
- >
136
- <PanelLeftClose className="size-4" />
137
- </button>
138
- </div>
139
- <CourseOutline
140
- items={curriculum}
141
- progress={combinedProgress}
142
- courseTitle={courseTitle}
143
- activeItemUid={activeUid}
144
- onItemClick={handleItemClick}
145
- readOnly={readOnly}
146
- />
147
- </aside>
148
- )}
131
+ </DrawerTitle>
132
+ <DrawerClose />
133
+ </DrawerHeader>
134
+ <DrawerBody className="px-2 pb-2">
135
+ <CourseOutline
136
+ items={curriculum}
137
+ progress={combinedProgress}
138
+ courseTitle={courseTitle}
139
+ activeItemUid={activeUid}
140
+ onItemClick={handleItemClick}
141
+ readOnly={readOnly}
142
+ />
143
+ </DrawerBody>
144
+ </DrawerContent>
145
+ </Drawer>
149
146
 
150
147
  {/* Main content area */}
151
148
  <div className="flex-1 flex flex-col min-w-0 overflow-hidden">
152
149
  {/* Toolbar */}
153
150
  <div className="flex items-center gap-2 px-4 py-2 border-b border-border bg-background shrink-0">
154
- {!sidebarOpen && (
155
- <button
156
- type="button"
157
- className="p-1 rounded hover:bg-muted text-muted-foreground mr-1"
158
- onClick={() => setSidebarOpen(true)}
159
- >
160
- <PanelLeft className="size-4" />
161
- </button>
162
- )}
151
+ <Button
152
+ variant="ghost"
153
+ size="sm"
154
+ className="size-7 p-0 mr-1"
155
+ onClick={() => setSidebarOpen(true)}
156
+ >
157
+ <PanelLeft className="size-4" />
158
+ </Button>
163
159
  <div className="flex-1 min-w-0">
164
160
  <span className="text-sm font-semibold text-foreground truncate block">
165
161
  {activeItem?.title ?? "Select an item"}
@@ -171,7 +167,7 @@ export function CoursePlayer({
171
167
  </div>
172
168
 
173
169
  {/* Content */}
174
- <div className="flex-1 overflow-y-auto p-6">
170
+ <div ref={contentRef} tabIndex={-1} className="flex-1 overflow-y-auto p-6 outline-none">
175
171
  {activeItem ? (
176
172
  renderContent(activeItem, readOnly, handleMarkComplete, isCurrentCompleted, handleNext, hasNext, nextItem)
177
173
  ) : (
@@ -0,0 +1,110 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import { ArrowLeft } from "lucide-react";
3
+ import { ForumBoard } from "../../sections/ForumBoard/ForumBoard";
4
+ import { DiscussionThread } from "../../sections/DiscussionThread/DiscussionThread";
5
+ import { Button } from "../../ui/button";
6
+ import { cn } from "../../lib/utils";
7
+ import type { ForumSortOrder } from "../../sections/ForumBoard/types";
8
+ import type { DiscussionModuleProps } from "./types";
9
+
10
+ /**
11
+ * DiscussionModule — a master-detail discussion forum with topic list and thread view.
12
+ *
13
+ * Panel-based layout: ForumBoard (topic listing) ↔ DiscussionThread (thread detail).
14
+ * Clicking a topic drills into the thread; a back button returns to the board.
15
+ */
16
+ export function DiscussionModule({
17
+ forumTitle,
18
+ topics,
19
+ currentUser,
20
+ threads,
21
+ onCreateTopic,
22
+ onReply,
23
+ onToggleLike,
24
+ onMarkAnswer,
25
+ onTopicOpen,
26
+ readOnly,
27
+ className,
28
+ style,
29
+ }: DiscussionModuleProps) {
30
+ const [activeTopicUid, setActiveTopicUid] = useState<string | null>(null);
31
+ const [searchQuery, setSearchQuery] = useState("");
32
+ const [sortOrder, setSortOrder] = useState<ForumSortOrder>("newest");
33
+ const contentRef = useRef<HTMLDivElement>(null);
34
+
35
+ useEffect(() => {
36
+ contentRef.current?.focus({ preventScroll: true });
37
+ }, [activeTopicUid]);
38
+
39
+ function handleTopicClick(topicUid: string) {
40
+ setActiveTopicUid(topicUid);
41
+ onTopicOpen?.(topicUid);
42
+ }
43
+
44
+ function handleBack() {
45
+ setActiveTopicUid(null);
46
+ }
47
+
48
+ const activeThread = activeTopicUid ? threads[activeTopicUid] : null;
49
+ const activeTopic = activeTopicUid
50
+ ? topics.find((t) => t.uid === activeTopicUid)
51
+ : null;
52
+
53
+ return (
54
+ <div
55
+ ref={contentRef}
56
+ tabIndex={-1}
57
+ className={cn("outline-none", className)}
58
+ style={style}
59
+ >
60
+ {activeTopicUid && activeThread ? (
61
+ /* ─── Thread View ─── */
62
+ <div>
63
+ <Button
64
+ variant="ghost"
65
+ size="sm"
66
+ onClick={handleBack}
67
+ className="mb-4"
68
+ >
69
+ <ArrowLeft className="size-4 mr-1.5" />
70
+ Back to Topics
71
+ </Button>
72
+ <DiscussionThread
73
+ title={activeTopic?.title ?? ""}
74
+ rootPost={activeThread.rootPost}
75
+ replies={activeThread.replies}
76
+ currentUser={currentUser}
77
+ onReply={(parentUid, content) =>
78
+ onReply?.(activeTopicUid, parentUid, content)
79
+ }
80
+ onToggleLike={
81
+ onToggleLike
82
+ ? (postUid) => onToggleLike(activeTopicUid, postUid)
83
+ : undefined
84
+ }
85
+ onMarkAnswer={
86
+ onMarkAnswer
87
+ ? (postUid) => onMarkAnswer(activeTopicUid, postUid)
88
+ : undefined
89
+ }
90
+ readOnly={readOnly}
91
+ />
92
+ </div>
93
+ ) : (
94
+ /* ─── Forum Board View ─── */
95
+ <ForumBoard
96
+ title={forumTitle}
97
+ topics={topics}
98
+ currentUser={currentUser}
99
+ onTopicClick={handleTopicClick}
100
+ onCreateTopic={onCreateTopic}
101
+ sortOrder={sortOrder}
102
+ onSortChange={setSortOrder}
103
+ searchQuery={searchQuery}
104
+ onSearchChange={setSearchQuery}
105
+ readOnly={readOnly}
106
+ />
107
+ )}
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,54 @@
1
+ import type {
2
+ DiscussionUser,
3
+ DiscussionPost,
4
+ } from "../../sections/DiscussionThread/types";
5
+ import type { ForumTopic } from "../../sections/ForumBoard/types";
6
+
7
+ /**
8
+ * DiscussionModule — a master-detail discussion forum with topic list and thread view.
9
+ *
10
+ * Panel-based layout: ForumBoard (topic list) ↔ DiscussionThread (thread detail).
11
+ * Clicking a topic drills into the thread; a back button returns to the board.
12
+ *
13
+ * @example
14
+ * <DiscussionModule
15
+ * forumTitle="Class Discussion"
16
+ * topics={topics}
17
+ * threads={threadData}
18
+ * currentUser={user}
19
+ * onCreateTopic={(title, content) => createTopic(title, content)}
20
+ * onReply={(topicUid, parentUid, content) => postReply(topicUid, parentUid, content)}
21
+ * />
22
+ */
23
+ export interface DiscussionModuleProps {
24
+ /** Forum title */
25
+ forumTitle?: string;
26
+ /** List of discussion topics */
27
+ topics: ForumTopic[];
28
+ /** The currently authenticated user */
29
+ currentUser: DiscussionUser;
30
+ /** Thread data keyed by topic UID */
31
+ threads: Record<
32
+ string,
33
+ {
34
+ rootPost: DiscussionPost;
35
+ replies: DiscussionPost[];
36
+ }
37
+ >;
38
+ /** Called when a new topic is created */
39
+ onCreateTopic?: (title: string, content: string) => void;
40
+ /** Called when a reply is posted */
41
+ onReply?: (topicUid: string, parentUid: string, content: string) => void;
42
+ /** Called when a like is toggled */
43
+ onToggleLike?: (topicUid: string, postUid: string) => void;
44
+ /** Called when a post is marked as the answer */
45
+ onMarkAnswer?: (topicUid: string, postUid: string) => void;
46
+ /** Called when a topic is opened (for lazy loading thread data) */
47
+ onTopicOpen?: (topicUid: string) => void;
48
+ /** When true, disables all interactions */
49
+ readOnly?: boolean;
50
+ /** CSS class name for the root element */
51
+ className?: string;
52
+ /** Inline styles for the root element */
53
+ style?: React.CSSProperties;
54
+ }