@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,63 @@
1
+ import type {
2
+ GradeItem,
3
+ GradeCategory,
4
+ OverallGrade,
5
+ } from "../../sections/GradebookTable/types";
6
+ import type {
7
+ ModuleProgress,
8
+ ActivityItem,
9
+ Achievement,
10
+ } from "../../sections/ProgressDashboard/types";
11
+ import type { AssessmentScore } from "../../sections/AssessmentReview/types";
12
+ import type { QuestionData, SessionAnswer } from "../../questions/types";
13
+
14
+ /**
15
+ * GradeCenterModule — a tabbed grade center with gradebook, progress dashboard,
16
+ * and drill-down into individual assessment reviews.
17
+ *
18
+ * Combines GradebookTable, ProgressDashboard, and AssessmentReview sections
19
+ * in a master-detail layout with tab navigation.
20
+ *
21
+ * @example
22
+ * <GradeCenterModule
23
+ * courseTitle="React Fundamentals"
24
+ * gradeItems={items}
25
+ * overallGrade={overallGrade}
26
+ * progressData={{ overallProgress: 75, totalTimeSpent: 45000, modules: [] }}
27
+ * reviewData={{ quiz1: { questions, sessionAnswers, score } }}
28
+ * />
29
+ */
30
+ export interface GradeCenterModuleProps {
31
+ /** Course title shown in the header */
32
+ courseTitle?: string;
33
+ /** Grade items (assignments, quizzes, etc.) */
34
+ gradeItems: GradeItem[];
35
+ /** Optional category grouping */
36
+ categories?: GradeCategory[];
37
+ /** Overall course grade summary */
38
+ overallGrade?: OverallGrade;
39
+ /** Whether to show the weight column in the gradebook */
40
+ showWeights?: boolean;
41
+ /** Data for the progress tab — tab is hidden if not provided */
42
+ progressData?: {
43
+ overallProgress: number;
44
+ totalTimeSpent: number;
45
+ modules: ModuleProgress[];
46
+ recentActivity?: ActivityItem[];
47
+ streak?: { currentDays: number; longestDays: number };
48
+ achievements?: Achievement[];
49
+ };
50
+ /** Review data keyed by grade item UID — enables drill-down on click */
51
+ reviewData?: Record<
52
+ string,
53
+ {
54
+ questions: QuestionData[];
55
+ sessionAnswers: SessionAnswer[];
56
+ score?: AssessmentScore;
57
+ }
58
+ >;
59
+ /** CSS class name for the root element */
60
+ className?: string;
61
+ /** Inline styles for the root element */
62
+ style?: React.CSSProperties;
63
+ }
@@ -15,9 +15,11 @@ import { StatCard } from "../../progress/stat-card";
15
15
  import { Button } from "../../ui/button";
16
16
  import { Badge } from "../../ui/badge";
17
17
  import { Card, CardContent } from "../../ui/card";
18
+ import { Separator } from "../../ui/separator";
18
19
  import { formatDuration } from "../../utils/format-duration";
19
20
  import { cn } from "../../lib/utils";
20
21
  import type { SessionAnswer } from "../../questions/types";
22
+ import { scoreAssessment } from "../../questions/scoring";
21
23
  import type { QuizModuleProps, QuizModuleResult } from "./types";
22
24
 
23
25
  type InternalStep =
@@ -34,6 +36,7 @@ export function QuizModule({
34
36
  allowRetake = true,
35
37
  onComplete,
36
38
  showReview = true,
39
+ questionMaterials,
37
40
  className,
38
41
  style,
39
42
  }: QuizModuleProps) {
@@ -41,6 +44,11 @@ export function QuizModule({
41
44
  const [timeElapsed, setTimeElapsed] = useState(0);
42
45
  const startTimeRef = useRef<number | null>(null);
43
46
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
47
+ const contentRef = useRef<HTMLDivElement>(null);
48
+
49
+ useEffect(() => {
50
+ contentRef.current?.focus({ preventScroll: true });
51
+ }, [step.tag]);
44
52
 
45
53
  // Timer for quiz step
46
54
  useEffect(() => {
@@ -63,25 +71,7 @@ export function QuizModule({
63
71
  }, [step.tag]);
64
72
 
65
73
  function scoreAnswers(answers: SessionAnswer[]): QuizModuleResult {
66
- let correct = 0;
67
- for (const q of questions) {
68
- const userAnswers = answers.filter((a) => a.uid === q.uid);
69
- const correctUids = new Set(
70
- (q.answers ?? []).filter((a) => a.isCorrect).map((a) => a.uid),
71
- );
72
- const userUids = new Set(userAnswers.map((a) => a.answerUid));
73
-
74
- if (
75
- correctUids.size > 0 &&
76
- correctUids.size === userUids.size &&
77
- [...correctUids].every((uid) => userUids.has(uid))
78
- ) {
79
- correct++;
80
- }
81
- }
82
-
83
- const total = questions.length;
84
- const percentage = total > 0 ? Math.round((correct / total) * 100) : 0;
74
+ const { correct, total, percentage } = scoreAssessment(questions, answers);
85
75
  const elapsed = startTimeRef.current
86
76
  ? Math.floor((Date.now() - startTimeRef.current) / 1000)
87
77
  : timeElapsed;
@@ -111,7 +101,7 @@ export function QuizModule({
111
101
  // ─── Intro Screen ───
112
102
  if (step.tag === "intro") {
113
103
  return (
114
- <div className={cn("max-w-2xl mx-auto", className)} style={style}>
104
+ <div ref={contentRef} tabIndex={-1} className={cn("max-w-2xl mx-auto outline-none", className)} style={style}>
115
105
  <Card>
116
106
  <CardContent className="pt-8 pb-8 text-center">
117
107
  <div className="mx-auto mb-4 w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
@@ -123,22 +113,22 @@ export function QuizModule({
123
113
  {description}
124
114
  </p>
125
115
  )}
126
- <div className="flex flex-wrap justify-center gap-4 mb-8">
127
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
128
- <HelpCircle className="size-4" />
129
- <span>{questions.length} questions</span>
130
- </div>
116
+ <div className="flex flex-wrap justify-center gap-2 mb-8">
117
+ <Badge variant="outline" className="gap-1.5">
118
+ <HelpCircle className="size-3.5" />
119
+ {questions.length} questions
120
+ </Badge>
131
121
  {timeLimitSeconds && (
132
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
133
- <Clock className="size-4" />
134
- <span>{formatDuration(timeLimitSeconds)} time limit</span>
135
- </div>
122
+ <Badge variant="outline" className="gap-1.5">
123
+ <Clock className="size-3.5" />
124
+ {formatDuration(timeLimitSeconds)} time limit
125
+ </Badge>
136
126
  )}
137
127
  {passingScore !== undefined && (
138
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
139
- <CheckCircle2 className="size-4" />
140
- <span>{passingScore}% to pass</span>
141
- </div>
128
+ <Badge variant="outline" className="gap-1.5">
129
+ <CheckCircle2 className="size-3.5" />
130
+ {passingScore}% to pass
131
+ </Badge>
142
132
  )}
143
133
  </div>
144
134
  <Button size="lg" onClick={() => setStep({ tag: "quiz" })}>
@@ -154,12 +144,13 @@ export function QuizModule({
154
144
  // ─── Quiz Screen ───
155
145
  if (step.tag === "quiz") {
156
146
  return (
157
- <div className={cn(className)} style={style}>
147
+ <div ref={contentRef} tabIndex={-1} className={cn("outline-none", className)} style={style}>
158
148
  <QuizSession
159
149
  questions={questions}
160
150
  onSubmit={handleSubmit}
161
151
  timeElapsedSeconds={timeElapsed}
162
152
  timeLimitSeconds={timeLimitSeconds}
153
+ questionMaterials={questionMaterials}
163
154
  />
164
155
  </div>
165
156
  );
@@ -170,62 +161,71 @@ export function QuizModule({
170
161
  const passed = result.passed;
171
162
 
172
163
  return (
173
- <div className={cn(className)} style={style}>
174
- {/* Score summary */}
175
- <div className="text-center mb-8">
176
- <ProgressRing
177
- value={result.percentage}
178
- size={140}
179
- strokeWidth={10}
180
- color={passed ? "var(--success)" : "var(--destructive)"}
181
- className="mx-auto mb-4 text-foreground"
182
- />
183
- <Badge
184
- variant={passed ? "success" : "destructive"}
185
- className="text-sm px-3 py-1 mb-2"
186
- >
187
- {passed ? "Passed" : "Failed"}
188
- </Badge>
189
- <h2 className="text-xl font-bold text-foreground">{title}</h2>
190
- </div>
191
-
192
- {/* Stats grid */}
193
- <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
194
- <StatCard
195
- icon={<CheckCircle2 />}
196
- label="Correct"
197
- value={`${result.correct}/${result.total}`}
198
- />
199
- <StatCard
200
- icon={<XCircle />}
201
- label="Incorrect"
202
- value={`${result.total - result.correct}/${result.total}`}
203
- />
204
- <StatCard
205
- icon={<Trophy />}
206
- label="Score"
207
- value={`${result.percentage}%`}
208
- />
209
- <StatCard
210
- icon={<Clock />}
211
- label="Time"
212
- value={formatDuration(result.timeElapsedSeconds)}
213
- />
214
- </div>
215
-
216
- {/* Actions */}
217
- {allowRetake && (
218
- <div className="flex justify-center mb-8">
219
- <Button variant="outline" onClick={handleRetake}>
220
- <RotateCcw className="size-4 mr-2" />
221
- Retake Quiz
222
- </Button>
223
- </div>
224
- )}
164
+ <div ref={contentRef} tabIndex={-1} className={cn("max-w-2xl mx-auto outline-none", className)} style={style}>
165
+ <Card>
166
+ <CardContent className="pt-8 pb-8">
167
+ {/* Score summary */}
168
+ <div className="text-center mb-8">
169
+ <ProgressRing
170
+ value={result.percentage}
171
+ size={140}
172
+ strokeWidth={10}
173
+ color={passed ? "var(--success)" : "var(--destructive)"}
174
+ className="mx-auto mb-4 text-foreground"
175
+ />
176
+ <Badge
177
+ variant={passed ? "success" : "destructive"}
178
+ className="text-sm px-3 py-1 mb-2"
179
+ >
180
+ {passed ? "Passed" : "Failed"}
181
+ </Badge>
182
+ <h2 className="text-xl font-bold text-foreground">{title}</h2>
183
+ </div>
184
+
185
+ {/* Stats grid */}
186
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
187
+ <StatCard
188
+ icon={<CheckCircle2 />}
189
+ label="Correct"
190
+ description="Questions answered right"
191
+ value={`${result.correct}/${result.total}`}
192
+ />
193
+ <StatCard
194
+ icon={<XCircle />}
195
+ label="Incorrect"
196
+ description="Questions to review"
197
+ value={`${result.total - result.correct}/${result.total}`}
198
+ />
199
+ <StatCard
200
+ icon={<Trophy />}
201
+ label="Score"
202
+ description="Overall percentage"
203
+ value={`${result.percentage}%`}
204
+ />
205
+ <StatCard
206
+ icon={<Clock />}
207
+ label="Time"
208
+ description="Total elapsed"
209
+ value={formatDuration(result.timeElapsedSeconds)}
210
+ />
211
+ </div>
212
+
213
+ {/* Actions */}
214
+ {allowRetake && (
215
+ <div className="flex justify-center mb-8">
216
+ <Button variant="outline" onClick={handleRetake}>
217
+ <RotateCcw className="size-4 mr-2" />
218
+ Retake Quiz
219
+ </Button>
220
+ </div>
221
+ )}
222
+ </CardContent>
223
+ </Card>
225
224
 
226
225
  {/* Per-question review */}
227
226
  {showReview && (
228
- <div>
227
+ <>
228
+ <Separator className="my-6" />
229
229
  <h3 className="text-lg font-semibold text-foreground mb-4">
230
230
  Question Review
231
231
  </h3>
@@ -234,7 +234,7 @@ export function QuizModule({
234
234
  sessionAnswers={result.answers}
235
235
  showCorrectAnswers
236
236
  />
237
- </div>
237
+ </>
238
238
  )}
239
239
  </div>
240
240
  );
@@ -1,4 +1,4 @@
1
- import type { QuestionData, SessionAnswer } from "../../questions/types";
1
+ import type { QuestionData, QuestionMaterial, SessionAnswer } from "../../questions/types";
2
2
 
3
3
  /**
4
4
  * QuizModule — a complete multi-step assessment experience.
@@ -34,6 +34,11 @@ export interface QuizModuleProps {
34
34
  onComplete?: (result: QuizModuleResult) => void;
35
35
  /** Whether to show correct/incorrect answer highlighting in the review. @default true */
36
36
  showReview?: boolean;
37
+ /**
38
+ * Related materials keyed by question UID. When provided, a "Materials"
39
+ * button appears in the question header, opening a drawer with content blocks.
40
+ */
41
+ questionMaterials?: QuestionMaterial[];
37
42
  /** CSS class name for the root element */
38
43
  className?: string;
39
44
  /** Inline styles for the root element */
@@ -0,0 +1,180 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import {
3
+ ClipboardList,
4
+ CheckCircle2,
5
+ HelpCircle,
6
+ RotateCcw,
7
+ Play,
8
+ Clock,
9
+ } from "lucide-react";
10
+ import { SurveyForm } from "../../sections/SurveyForm/SurveyForm";
11
+ import { StatCard } from "../../progress/stat-card";
12
+ import { Button } from "../../ui/button";
13
+ import { Badge } from "../../ui/badge";
14
+ import { Card, CardContent } from "../../ui/card";
15
+ import { formatDuration } from "../../utils/format-duration";
16
+ import { cn } from "../../lib/utils";
17
+ import type { SurveyAnswer } from "../../sections/SurveyForm/types";
18
+ import type { SurveyModuleProps, SurveyModuleResult } from "./types";
19
+
20
+ type InternalStep =
21
+ | { tag: "intro" }
22
+ | { tag: "survey" }
23
+ | { tag: "thankYou"; result: SurveyModuleResult };
24
+
25
+ /**
26
+ * SurveyModule — a complete survey experience with intro, form, and thank-you steps.
27
+ *
28
+ * Steps: Intro → SurveyForm → Thank You with response stats.
29
+ */
30
+ export function SurveyModule({
31
+ title,
32
+ description,
33
+ questions,
34
+ requireAll = false,
35
+ showProgress = true,
36
+ thankYouTitle = "Thank You!",
37
+ thankYouMessage,
38
+ onComplete,
39
+ allowRestart = false,
40
+ className,
41
+ style,
42
+ }: SurveyModuleProps) {
43
+ const [step, setStep] = useState<InternalStep>({ tag: "intro" });
44
+ const startTimeRef = useRef<number | null>(null);
45
+ const contentRef = useRef<HTMLDivElement>(null);
46
+
47
+ useEffect(() => {
48
+ contentRef.current?.focus({ preventScroll: true });
49
+ }, [step.tag]);
50
+
51
+ function handleSubmit(answers: SurveyAnswer[]) {
52
+ const elapsed = startTimeRef.current
53
+ ? Math.floor((Date.now() - startTimeRef.current) / 1000)
54
+ : 0;
55
+ const result: SurveyModuleResult = {
56
+ answers,
57
+ totalQuestions: questions.length,
58
+ answeredCount: answers.length,
59
+ timeElapsedSeconds: elapsed,
60
+ };
61
+ setStep({ tag: "thankYou", result });
62
+ onComplete?.(result);
63
+ }
64
+
65
+ function handleRestart() {
66
+ startTimeRef.current = null;
67
+ setStep({ tag: "intro" });
68
+ }
69
+
70
+ function handleStart() {
71
+ startTimeRef.current = Date.now();
72
+ setStep({ tag: "survey" });
73
+ }
74
+
75
+ // ─── Intro Screen ───
76
+ if (step.tag === "intro") {
77
+ return (
78
+ <div
79
+ ref={contentRef}
80
+ tabIndex={-1}
81
+ className={cn("max-w-2xl mx-auto outline-none", className)}
82
+ style={style}
83
+ >
84
+ <Card>
85
+ <CardContent className="pt-8 pb-8 text-center">
86
+ <div className="mx-auto mb-4 w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
87
+ <ClipboardList className="size-7 text-primary" />
88
+ </div>
89
+ <h2 className="text-2xl font-bold text-foreground mb-2">{title}</h2>
90
+ {description && (
91
+ <p className="text-muted-foreground mb-6 max-w-md mx-auto">
92
+ {description}
93
+ </p>
94
+ )}
95
+ <div className="flex flex-wrap justify-center gap-2 mb-8">
96
+ <Badge variant="outline" className="gap-1.5">
97
+ <HelpCircle className="size-3.5" />
98
+ {questions.length} questions
99
+ </Badge>
100
+ </div>
101
+ <Button size="lg" onClick={handleStart}>
102
+ <Play className="size-4 mr-2" />
103
+ Begin Survey
104
+ </Button>
105
+ </CardContent>
106
+ </Card>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ // ─── Survey Screen ───
112
+ if (step.tag === "survey") {
113
+ return (
114
+ <div
115
+ ref={contentRef}
116
+ tabIndex={-1}
117
+ className={cn("outline-none", className)}
118
+ style={style}
119
+ >
120
+ <SurveyForm
121
+ title={title}
122
+ questions={questions}
123
+ requireAll={requireAll}
124
+ showProgress={showProgress}
125
+ onSubmit={handleSubmit}
126
+ />
127
+ </div>
128
+ );
129
+ }
130
+
131
+ // ─── Thank You Screen ───
132
+ const { result } = step;
133
+
134
+ return (
135
+ <div
136
+ ref={contentRef}
137
+ tabIndex={-1}
138
+ className={cn("max-w-2xl mx-auto outline-none", className)}
139
+ style={style}
140
+ >
141
+ <Card>
142
+ <CardContent className="pt-8 pb-8 text-center">
143
+ <div className="mx-auto mb-4 w-14 h-14 rounded-full bg-success/10 flex items-center justify-center">
144
+ <CheckCircle2 className="size-7 text-success" />
145
+ </div>
146
+ <h2 className="text-2xl font-bold text-foreground mb-2">
147
+ {thankYouTitle}
148
+ </h2>
149
+ <p className="text-muted-foreground mb-6 max-w-md mx-auto">
150
+ {thankYouMessage ?? "Your responses have been recorded. Thank you for your feedback!"}
151
+ </p>
152
+
153
+ {/* Stats */}
154
+ <div className="grid grid-cols-2 gap-3 max-w-sm mx-auto mb-6">
155
+ <StatCard
156
+ icon={<HelpCircle />}
157
+ label="Answered"
158
+ description="Questions completed"
159
+ value={`${result.answeredCount}/${result.totalQuestions}`}
160
+ />
161
+ <StatCard
162
+ icon={<Clock />}
163
+ label="Time"
164
+ description="Total elapsed"
165
+ value={formatDuration(result.timeElapsedSeconds)}
166
+ />
167
+ </div>
168
+
169
+ {/* Actions */}
170
+ {allowRestart && (
171
+ <Button variant="outline" onClick={handleRestart}>
172
+ <RotateCcw className="size-4 mr-2" />
173
+ Take Again
174
+ </Button>
175
+ )}
176
+ </CardContent>
177
+ </Card>
178
+ </div>
179
+ );
180
+ }
@@ -0,0 +1,51 @@
1
+ import type { SurveyQuestion, SurveyAnswer } from "../../sections/SurveyForm/types";
2
+
3
+ /**
4
+ * SurveyModule — a complete survey experience with intro, form, and thank-you steps.
5
+ *
6
+ * Wraps SurveyForm with a welcoming intro screen and a thank-you completion
7
+ * screen showing response stats.
8
+ *
9
+ * @example
10
+ * <SurveyModule
11
+ * title="Course Evaluation"
12
+ * description="Help us improve this course."
13
+ * questions={surveyQuestions}
14
+ * onComplete={(result) => submitSurvey(result)}
15
+ * />
16
+ */
17
+ export interface SurveyModuleProps {
18
+ /** Survey title displayed on the intro and form screens */
19
+ title: string;
20
+ /** Survey description displayed on the intro screen */
21
+ description?: string;
22
+ /** Survey questions */
23
+ questions: SurveyQuestion[];
24
+ /** Whether all questions must be answered before submit. @default false */
25
+ requireAll?: boolean;
26
+ /** Whether to show a progress indicator in the survey form. @default true */
27
+ showProgress?: boolean;
28
+ /** Custom title for the thank-you screen. @default "Thank You!" */
29
+ thankYouTitle?: string;
30
+ /** Custom message for the thank-you screen */
31
+ thankYouMessage?: string;
32
+ /** Called when the survey is completed */
33
+ onComplete?: (result: SurveyModuleResult) => void;
34
+ /** Allow restarting the survey from the thank-you screen. @default false */
35
+ allowRestart?: boolean;
36
+ /** CSS class name for the root element */
37
+ className?: string;
38
+ /** Inline styles for the root element */
39
+ style?: React.CSSProperties;
40
+ }
41
+
42
+ export interface SurveyModuleResult {
43
+ /** The user's submitted answers */
44
+ answers: SurveyAnswer[];
45
+ /** Total number of questions */
46
+ totalQuestions: number;
47
+ /** Number of questions answered */
48
+ answeredCount: number;
49
+ /** Total time taken in seconds */
50
+ timeElapsedSeconds: number;
51
+ }
@@ -10,3 +10,27 @@ export type {
10
10
 
11
11
  export { CoursePlayer } from "./CoursePlayer/CoursePlayer";
12
12
  export type { CoursePlayerProps, CoursePlayerItem } from "./CoursePlayer/types";
13
+
14
+ export { ExamModule } from "./ExamModule/ExamModule";
15
+ export type { ExamModuleProps, ExamModuleResult } from "./ExamModule/types";
16
+
17
+ export { SurveyModule } from "./SurveyModule/SurveyModule";
18
+ export type {
19
+ SurveyModuleProps,
20
+ SurveyModuleResult,
21
+ } from "./SurveyModule/types";
22
+
23
+ export { GradeCenterModule } from "./GradeCenterModule/GradeCenterModule";
24
+ export type { GradeCenterModuleProps } from "./GradeCenterModule/types";
25
+
26
+ export { AssignmentModule } from "./AssignmentModule/AssignmentModule";
27
+ export type {
28
+ AssignmentModuleProps,
29
+ AssignmentModuleResult,
30
+ } from "./AssignmentModule/types";
31
+
32
+ export { CertificateModule } from "./CertificateModule/CertificateModule";
33
+ export type { CertificateModuleProps } from "./CertificateModule/types";
34
+
35
+ export { DiscussionModule } from "./DiscussionModule/DiscussionModule";
36
+ export type { DiscussionModuleProps } from "./DiscussionModule/types";
@@ -0,0 +1,52 @@
1
+ import { Trophy, Lock } from "lucide-react";
2
+ import { Card } from "../ui/card";
3
+ import { cn } from "../lib/utils";
4
+ import type { AchievementBadgeProps } from "./types";
5
+
6
+ const VARIANT_STYLES = {
7
+ default: "text-primary",
8
+ gold: "text-yellow-500",
9
+ silver: "text-gray-400",
10
+ bronze: "text-amber-700",
11
+ } as const;
12
+
13
+ /**
14
+ * AchievementBadge displays a single achievement or badge earned by a learner,
15
+ * with support for locked/earned states and metal-tier variants.
16
+ */
17
+ export function AchievementBadge({
18
+ title,
19
+ description,
20
+ icon,
21
+ earnedDate,
22
+ locked = false,
23
+ variant = "default",
24
+ className,
25
+ style,
26
+ }: AchievementBadgeProps) {
27
+ return (
28
+ <Card
29
+ className={cn("p-2 text-center", locked && "opacity-60", className)}
30
+ style={style}
31
+ >
32
+ <div className="mx-auto mb-1 w-12 h-12 flex items-center justify-center">
33
+ {locked ? (
34
+ <Lock size={32} className="text-muted-foreground" />
35
+ ) : (
36
+ icon ?? (
37
+ <Trophy size={32} className={VARIANT_STYLES[variant]} />
38
+ )
39
+ )}
40
+ </div>
41
+ <p className="font-semibold text-sm text-foreground">{title}</p>
42
+ {description && (
43
+ <p className="text-xs text-muted-foreground">{description}</p>
44
+ )}
45
+ {earnedDate && !locked && (
46
+ <p className="text-xs text-muted-foreground mt-0.5">
47
+ {new Date(earnedDate).toLocaleDateString()}
48
+ </p>
49
+ )}
50
+ </Card>
51
+ );
52
+ }