@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
@@ -1,26 +1,19 @@
1
+ import { BookOpen, Clock } from "lucide-react";
1
2
  import {
2
- Award,
3
- BookOpen,
4
- CheckCircle,
5
- Clock,
6
- Flame,
7
- Send,
8
- Trophy,
9
- } from "lucide-react";
10
- import { ProgressRing, StatCard } from "../../progress";
3
+ ProgressRing,
4
+ StatCard,
5
+ AchievementBadge,
6
+ StreakBadge,
7
+ ActivityTimeline,
8
+ } from "../../progress";
9
+ import type { TimelineEvent } from "../../progress";
11
10
  import { Progress } from "../../ui/progress";
12
- import { Card } from "../../ui/card";
11
+ import { Card, CardContent } from "../../ui/card";
12
+ import { Separator } from "../../ui/separator";
13
13
  import { formatDuration } from "../../utils/format-duration";
14
14
  import type { ProgressDashboardProps } from "./types";
15
15
  import { cn } from "../../lib/utils";
16
16
 
17
- const ACTIVITY_ICONS = {
18
- lesson_completed: BookOpen,
19
- quiz_passed: CheckCircle,
20
- assignment_submitted: Send,
21
- badge_earned: Award,
22
- };
23
-
24
17
  export function ProgressDashboard({
25
18
  overallProgress,
26
19
  totalTimeSpent,
@@ -36,52 +29,62 @@ export function ProgressDashboard({
36
29
  return (
37
30
  <div className={className} style={style}>
38
31
  {/* Stats row */}
39
- <div className="grid grid-cols-[repeat(auto-fit,minmax(160px,1fr))] gap-2 mb-3">
40
- <Card className="p-2 flex justify-center">
41
- <ProgressRing value={overallProgress} size={100} />
32
+ <div className="grid grid-cols-[repeat(auto-fit,minmax(160px,1fr))] gap-3 mb-4">
33
+ <Card>
34
+ <CardContent className="p-3 flex justify-center">
35
+ <ProgressRing value={overallProgress} size={100} />
36
+ </CardContent>
42
37
  </Card>
43
38
  <StatCard
44
39
  icon={<Clock size={24} />}
45
40
  label="Time Spent"
41
+ description="Total learning time"
46
42
  value={formatDuration(totalTimeSpent)}
47
43
  />
48
44
  {streak && (
49
- <StatCard
50
- icon={<Flame size={24} />}
51
- label="Current Streak"
52
- value={`${streak.currentDays} days`}
53
- subtitle={`Longest: ${streak.longestDays} days`}
54
- />
45
+ <Card>
46
+ <CardContent className="p-4 flex items-center">
47
+ <StreakBadge
48
+ currentStreak={streak.currentDays}
49
+ longestStreak={streak.longestDays}
50
+ showLongest
51
+ />
52
+ </CardContent>
53
+ </Card>
55
54
  )}
56
55
  <StatCard
57
56
  icon={<BookOpen size={24} />}
58
57
  label="Modules"
58
+ description="Course progress"
59
59
  value={`${modules.filter((m) => m.completedItems === m.totalItems).length} / ${modules.length}`}
60
60
  subtitle="completed"
61
61
  />
62
62
  </div>
63
63
 
64
64
  {/* Module progress */}
65
+ <Separator className="mb-3" />
65
66
  <p className="text-lg font-semibold mb-2 text-foreground">Module Progress</p>
66
- <div className="flex flex-col gap-2 mb-3">
67
+ <div className="flex flex-col gap-2 mb-4">
67
68
  {modules.map((mod) => {
68
69
  const pct = mod.totalItems > 0 ? (mod.completedItems / mod.totalItems) * 100 : 0;
69
70
  return (
70
71
  <Card
71
72
  key={mod.uid}
72
73
  className={cn(
73
- "p-2 transition-colors",
74
+ "transition-colors",
74
75
  onModuleClick && "cursor-pointer hover:border-primary",
75
76
  )}
76
77
  onClick={() => onModuleClick?.(mod.uid)}
77
78
  >
78
- <div className="flex justify-between items-center mb-0.5">
79
- <span className="font-semibold text-sm text-foreground">{mod.name}</span>
80
- <span className="text-xs text-muted-foreground">
81
- {mod.completedItems} / {mod.totalItems}
82
- </span>
83
- </div>
84
- <Progress value={pct} size="sm" />
79
+ <CardContent className="p-3">
80
+ <div className="flex justify-between items-center mb-0.5">
81
+ <span className="font-semibold text-sm text-foreground">{mod.name}</span>
82
+ <span className="text-xs text-muted-foreground">
83
+ {mod.completedItems} / {mod.totalItems}
84
+ </span>
85
+ </div>
86
+ <Progress value={pct} size="sm" />
87
+ </CardContent>
85
88
  </Card>
86
89
  );
87
90
  })}
@@ -90,22 +93,18 @@ export function ProgressDashboard({
90
93
  {/* Recent activity */}
91
94
  {recentActivity && recentActivity.length > 0 && (
92
95
  <>
96
+ <Separator className="mb-3" />
93
97
  <p className="text-lg font-semibold mb-2 text-foreground">Recent Activity</p>
94
- <div className="flex flex-col gap-1.5 mb-3">
95
- {recentActivity.slice(0, recentActivityLimit).map((activity) => {
96
- const Icon = ACTIVITY_ICONS[activity.type] ?? CheckCircle;
97
- return (
98
- <div key={activity.uid} className="flex gap-1.5 items-center">
99
- <Icon size={16} />
100
- <span className="flex-1 text-sm text-foreground">
101
- {activity.description}
102
- </span>
103
- <span className="text-xs text-muted-foreground">
104
- {new Date(activity.timestamp).toLocaleDateString()}
105
- </span>
106
- </div>
107
- );
108
- })}
98
+ <div className="mb-4">
99
+ <ActivityTimeline
100
+ events={recentActivity.map<TimelineEvent>((activity) => ({
101
+ uid: activity.uid,
102
+ type: activity.type,
103
+ title: activity.description,
104
+ timestamp: activity.timestamp,
105
+ }))}
106
+ limit={recentActivityLimit}
107
+ />
109
108
  </div>
110
109
  </>
111
110
  )}
@@ -113,24 +112,25 @@ export function ProgressDashboard({
113
112
  {/* Achievements */}
114
113
  {achievements && achievements.length > 0 && (
115
114
  <>
115
+ <Separator className="mb-3" />
116
116
  <p className="text-lg font-semibold mb-2 text-foreground">Achievements</p>
117
117
  <div className="grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-2">
118
118
  {achievements.map((badge) => (
119
- <Card key={badge.uid} className="p-2 text-center">
120
- {badge.iconUrl ? (
121
- <img
122
- src={badge.iconUrl}
123
- alt={badge.name}
124
- className="w-12 h-12 mb-1 mx-auto"
125
- />
126
- ) : (
127
- <Trophy size={32} className="mx-auto mb-2 text-warning" />
128
- )}
129
- <p className="font-semibold text-sm text-foreground">{badge.name}</p>
130
- <p className="text-xs text-muted-foreground">
131
- {badge.description}
132
- </p>
133
- </Card>
119
+ <AchievementBadge
120
+ key={badge.uid}
121
+ title={badge.name}
122
+ description={badge.description}
123
+ icon={
124
+ badge.iconUrl ? (
125
+ <img
126
+ src={badge.iconUrl}
127
+ alt={badge.name}
128
+ className="w-12 h-12"
129
+ />
130
+ ) : undefined
131
+ }
132
+ earnedDate={badge.earnedAt}
133
+ />
134
134
  ))}
135
135
  </div>
136
136
  </>
@@ -1,9 +1,11 @@
1
1
  import { useMemo, useState } from "react";
2
- import { AssessmentToolbar } from "../../assessment-toolbar";
2
+ import { ChevronLeft, ChevronRight, Send } from "lucide-react";
3
+ import { AssessmentToolbar, QuestionHeaderBar, QuestionMaterialsDrawer } from "../../assessment-toolbar";
3
4
  import type { QuestionNavigatorItem } from "../../assessment-toolbar/types";
4
5
  import { QuestionRenderer } from "../../questions";
5
6
  import type { SessionAnswer } from "../../questions/types";
6
- import { Card, CardContent } from "../../ui/card";
7
+ import { Button } from "../../ui/button";
8
+ import { Card, CardHeader, CardContent } from "../../ui/card";
7
9
  import type { QuizSessionProps } from "./types";
8
10
  import { cn } from "../../lib/utils";
9
11
 
@@ -14,6 +16,7 @@ export function QuizSession({
14
16
  onAnswerChange,
15
17
  timeElapsedSeconds,
16
18
  timeLimitSeconds,
19
+ questionMaterials,
17
20
  isSubmitting = false,
18
21
  readOnly = false,
19
22
  className,
@@ -23,14 +26,28 @@ export function QuizSession({
23
26
  const [sessionAnswers, setSessionAnswers] =
24
27
  useState<SessionAnswer[]>(initialAnswers);
25
28
  const [flaggedUids, setFlaggedUids] = useState<Set<string>>(new Set());
29
+ const [materialsOpen, setMaterialsOpen] = useState(false);
26
30
 
27
31
  const currentQuestion = questions[currentIndex];
28
32
 
33
+ const currentQuestionAnswers = useMemo(
34
+ () =>
35
+ currentQuestion
36
+ ? sessionAnswers.filter((a) => a.uid === currentQuestion.uid)
37
+ : [],
38
+ [sessionAnswers, currentQuestion],
39
+ );
40
+
41
+ const currentMaterials = useMemo(
42
+ () => questionMaterials?.filter((m) => m.questionUid === currentQuestion?.uid) ?? [],
43
+ [questionMaterials, currentQuestion],
44
+ );
45
+
29
46
  const navigatorItems = useMemo<QuestionNavigatorItem[]>(
30
47
  () =>
31
48
  questions.map((q, idx) => ({
32
49
  uid: q.uid,
33
- sequence: idx + 1,
50
+ sequence: idx,
34
51
  isFlagged: flaggedUids.has(q.uid),
35
52
  isAnswered: sessionAnswers.some((a) => a.uid === q.uid),
36
53
  isSkipped: false,
@@ -89,25 +106,67 @@ export function QuizSession({
89
106
  timeLimitSeconds={timeLimitSeconds}
90
107
  questions={navigatorItems}
91
108
  onNavigateToQuestion={handleNavigate}
92
- onToggleFlag={handleToggleFlag}
93
109
  currentQuestionUid={currentQuestion?.uid}
94
110
  isSubmitting={isSubmitting}
95
111
  readOnly={readOnly}
96
112
  />
97
113
  {currentQuestion && (
98
114
  <Card className="mt-3">
99
- <CardContent className="pt-6">
115
+ <CardHeader className="pb-0">
116
+ <QuestionHeaderBar
117
+ questionNumber={currentIndex + 1}
118
+ totalQuestions={questions.length}
119
+ isFlagged={flaggedUids.has(currentQuestion.uid)}
120
+ onToggleFlag={() => handleToggleFlag(currentQuestion.uid)}
121
+ hasMaterials={currentMaterials.length > 0}
122
+ onOpenMaterials={() => setMaterialsOpen(true)}
123
+ readOnly={readOnly}
124
+ />
125
+ </CardHeader>
126
+ <CardContent>
100
127
  <QuestionRenderer
101
128
  question={currentQuestion}
102
- sessionAnswers={sessionAnswers.filter(
103
- (a) => a.uid === currentQuestion.uid,
104
- )}
129
+ sessionAnswers={currentQuestionAnswers}
105
130
  onAnswer={handleAnswer}
106
131
  readOnly={readOnly}
107
132
  />
108
133
  </CardContent>
109
134
  </Card>
110
135
  )}
136
+
137
+ {/* Bottom navigation */}
138
+ {!readOnly && (
139
+ <div className="flex items-center justify-between gap-3 mt-3">
140
+ <Button
141
+ variant="outline"
142
+ disabled={currentIndex <= 0}
143
+ onClick={() => setCurrentIndex((i) => Math.max(i - 1, 0))}
144
+ >
145
+ <ChevronLeft className="size-4 mr-1" />
146
+ Previous
147
+ </Button>
148
+ {currentIndex < questions.length - 1 ? (
149
+ <Button
150
+ onClick={() => setCurrentIndex((i) => Math.min(i + 1, questions.length - 1))}
151
+ >
152
+ Next
153
+ <ChevronRight className="size-4 ml-1" />
154
+ </Button>
155
+ ) : (
156
+ <Button onClick={handleSubmit} disabled={isSubmitting}>
157
+ {isSubmitting ? "Submitting..." : "Submit Quiz"}
158
+ {!isSubmitting && <Send className="size-4 ml-1" />}
159
+ </Button>
160
+ )}
161
+ </div>
162
+ )}
163
+
164
+ <QuestionMaterialsDrawer
165
+ open={materialsOpen}
166
+ onOpenChange={setMaterialsOpen}
167
+ materials={currentMaterials}
168
+ questionNumber={currentIndex + 1}
169
+ />
111
170
  </div>
112
171
  );
113
172
  }
@@ -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
  * QuizSession section — a complete assessment session experience.
@@ -38,6 +38,11 @@ export interface QuizSessionProps {
38
38
  timeLimitSeconds?: number;
39
39
  /** Whether the submit action is currently in flight */
40
40
  isSubmitting?: boolean;
41
+ /**
42
+ * Related materials keyed by question UID. When provided, a "Materials"
43
+ * button appears in the question header, opening a drawer with content blocks.
44
+ */
45
+ questionMaterials?: QuestionMaterial[];
41
46
  /** When true, all inputs are disabled (e.g. after submission) */
42
47
  readOnly?: boolean;
43
48
  /** CSS class name for the root element */
@@ -0,0 +1,107 @@
1
+ import { useMemo } from "react";
2
+ import { CheckCircle2, Circle, ChevronRight } from "lucide-react";
3
+ import { Progress } from "../../ui/progress";
4
+ import { cn } from "../../lib/utils";
5
+ import type { RequirementsChecklistProps } from "./types";
6
+
7
+ /**
8
+ * RequirementsChecklist — shows completion requirements with checked/unchecked status.
9
+ *
10
+ * Displays a vertical list of requirements with an overall progress bar.
11
+ * Incomplete items can be clicked to navigate to the relevant content.
12
+ */
13
+ export function RequirementsChecklist({
14
+ title,
15
+ requirements,
16
+ onRequirementClick,
17
+ className,
18
+ style,
19
+ }: RequirementsChecklistProps) {
20
+ const completedCount = useMemo(
21
+ () => requirements.filter((r) => r.completed).length,
22
+ [requirements]
23
+ );
24
+ const progressPercent =
25
+ requirements.length > 0
26
+ ? Math.round((completedCount / requirements.length) * 100)
27
+ : 0;
28
+
29
+ return (
30
+ <div className={cn("flex flex-col gap-4", className)} style={style}>
31
+ {/* Header */}
32
+ {title && (
33
+ <h3 className="text-lg font-semibold text-foreground">{title}</h3>
34
+ )}
35
+
36
+ {/* Progress bar */}
37
+ <div className="flex flex-col gap-1.5">
38
+ <div className="flex items-center justify-between text-sm">
39
+ <span className="text-muted-foreground">
40
+ {completedCount} of {requirements.length} complete
41
+ </span>
42
+ <span className="font-medium text-foreground">{progressPercent}%</span>
43
+ </div>
44
+ <Progress value={progressPercent} />
45
+ </div>
46
+
47
+ {/* Requirement list */}
48
+ <ul className="flex flex-col gap-1">
49
+ {requirements.map((req) => (
50
+ <li key={req.uid}>
51
+ <button
52
+ type="button"
53
+ className={cn(
54
+ "w-full flex items-start gap-3 p-3 rounded-lg text-left transition-colors",
55
+ req.completed
56
+ ? "bg-success/5"
57
+ : onRequirementClick
58
+ ? "hover:bg-muted/50 cursor-pointer"
59
+ : "cursor-default"
60
+ )}
61
+ onClick={() => {
62
+ if (!req.completed && onRequirementClick) {
63
+ onRequirementClick(req.uid);
64
+ }
65
+ }}
66
+ disabled={req.completed}
67
+ >
68
+ {/* Icon */}
69
+ {req.completed ? (
70
+ <CheckCircle2 className="size-5 text-success shrink-0 mt-0.5" />
71
+ ) : (
72
+ <Circle className="size-5 text-muted-foreground shrink-0 mt-0.5" />
73
+ )}
74
+
75
+ {/* Content */}
76
+ <div className="flex-1 min-w-0">
77
+ <div
78
+ className={cn(
79
+ "text-sm font-medium",
80
+ req.completed
81
+ ? "text-muted-foreground line-through"
82
+ : "text-foreground"
83
+ )}
84
+ >
85
+ {req.label}
86
+ </div>
87
+ {req.description && (
88
+ <div className="text-xs text-muted-foreground mt-0.5">
89
+ {req.description}
90
+ </div>
91
+ )}
92
+ </div>
93
+
94
+ {/* Action indicator */}
95
+ {!req.completed && onRequirementClick && (
96
+ <div className="shrink-0 flex items-center gap-1 text-xs text-primary mt-0.5">
97
+ {req.actionLabel && <span>{req.actionLabel}</span>}
98
+ <ChevronRight className="size-4" />
99
+ </div>
100
+ )}
101
+ </button>
102
+ </li>
103
+ ))}
104
+ </ul>
105
+ </div>
106
+ );
107
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * RequirementsChecklist section — shows completion requirements with progress tracking.
3
+ *
4
+ * Displays a list of requirements with checked/unchecked status and an overall
5
+ * progress bar. Incomplete items can be clicked to navigate to the relevant content.
6
+ *
7
+ * @example
8
+ * <RequirementsChecklist
9
+ * title="Certificate Requirements"
10
+ * requirements={requirements}
11
+ * onRequirementClick={(uid) => navigateTo(uid)}
12
+ * />
13
+ */
14
+ export interface RequirementsChecklistProps {
15
+ /** Section title */
16
+ title?: string;
17
+ /** List of requirements to display */
18
+ requirements: Requirement[];
19
+ /** Called when the user clicks an incomplete requirement */
20
+ onRequirementClick?: (uid: string) => void;
21
+ /** CSS class name for the root element */
22
+ className?: string;
23
+ /** Inline styles for the root element */
24
+ style?: React.CSSProperties;
25
+ }
26
+
27
+ export interface Requirement {
28
+ /** Unique identifier */
29
+ uid: string;
30
+ /** Requirement label */
31
+ label: string;
32
+ /** Optional description */
33
+ description?: string;
34
+ /** Whether this requirement has been completed */
35
+ completed: boolean;
36
+ /** Optional action label for clickable items */
37
+ actionLabel?: string;
38
+ }
@@ -8,12 +8,7 @@ import { Tabs, TabsList, TabsTrigger } from "../../ui/tabs";
8
8
  import { Tooltip, TooltipTrigger, TooltipContent } from "../../ui/tooltip";
9
9
  import type { ResourceLibraryProps, Resource } from "./types";
10
10
  import { cn } from "../../lib/utils";
11
-
12
- function formatBytes(bytes: number): string {
13
- if (bytes < 1024) return `${bytes} B`;
14
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
15
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
16
- }
11
+ import { formatFileSize } from "../../utils/format-file-size";
17
12
 
18
13
  const TYPE_TO_ICON: Record<string, string> = {
19
14
  pdf: "document",
@@ -77,10 +72,10 @@ export function ResourceLibrary({
77
72
  <div className="flex-1 min-w-0">
78
73
  <span className="text-sm text-foreground">{resource.name}</span>
79
74
  {(resource.description || resource.fileSize != null) && (
80
- <span className="text-sm text-muted-foreground">
75
+ <span className="block text-xs text-muted-foreground">
81
76
  {[
82
77
  resource.description,
83
- resource.fileSize != null && formatBytes(resource.fileSize),
78
+ resource.fileSize != null && formatFileSize(resource.fileSize),
84
79
  ]
85
80
  .filter(Boolean)
86
81
  .join(" \u00b7 ")}
@@ -205,7 +200,7 @@ export function ResourceLibrary({
205
200
  )}
206
201
  {resource.fileSize != null && (
207
202
  <span className="text-xs text-muted-foreground">
208
- {formatBytes(resource.fileSize)}
203
+ {formatFileSize(resource.fileSize)}
209
204
  </span>
210
205
  )}
211
206
  </CardContent>
@@ -0,0 +1,138 @@
1
+ import { CheckCircle2 } from "lucide-react";
2
+ import { Card, CardContent } from "../../ui/card";
3
+ import {
4
+ Table,
5
+ TableHeader,
6
+ TableBody,
7
+ TableRow,
8
+ TableHead,
9
+ TableCell,
10
+ } from "../../ui/table";
11
+ import { Badge } from "../../ui/badge";
12
+ import { Separator } from "../../ui/separator";
13
+ import { cn } from "../../lib/utils";
14
+ import type { RubricViewProps } from "./types";
15
+
16
+ /**
17
+ * RubricView — displays a grading rubric with criteria rows and proficiency level columns.
18
+ *
19
+ * When `selectedLevels` is provided, highlights the scored level per criterion and
20
+ * shows an overall score. Useful inside AssignmentModule for showing grading criteria
21
+ * and results.
22
+ */
23
+ export function RubricView({
24
+ criteria,
25
+ selectedLevels,
26
+ totalScore,
27
+ maxScore,
28
+ feedback,
29
+ className,
30
+ style,
31
+ }: RubricViewProps) {
32
+ const isScored = selectedLevels && Object.keys(selectedLevels).length > 0;
33
+
34
+ return (
35
+ <div className={cn("flex flex-col gap-4", className)} style={style}>
36
+ {/* Score header */}
37
+ {isScored && totalScore !== undefined && maxScore !== undefined && (
38
+ <div className="flex items-center justify-between">
39
+ <h3 className="text-lg font-semibold text-foreground">Rubric</h3>
40
+ <Badge variant={totalScore >= maxScore * 0.7 ? "success" : "destructive"}>
41
+ {totalScore} / {maxScore} pts
42
+ </Badge>
43
+ </div>
44
+ )}
45
+
46
+ {!isScored && (
47
+ <h3 className="text-lg font-semibold text-foreground">Grading Rubric</h3>
48
+ )}
49
+
50
+ {/* Criteria table */}
51
+ <Card>
52
+ <CardContent className="p-0">
53
+ <div className="overflow-x-auto">
54
+ <Table className="text-sm">
55
+ <TableHeader>
56
+ <TableRow className="bg-muted/50">
57
+ <TableHead className="text-left font-medium text-muted-foreground w-1/5">
58
+ Criterion
59
+ </TableHead>
60
+ {criteria[0]?.levels.map((level) => (
61
+ <TableHead
62
+ key={level.uid}
63
+ className="text-center font-medium text-muted-foreground"
64
+ >
65
+ <div>{level.label}</div>
66
+ <div className="text-xs font-normal mt-0.5">
67
+ {level.points} pts
68
+ </div>
69
+ </TableHead>
70
+ ))}
71
+ </TableRow>
72
+ </TableHeader>
73
+ <TableBody>
74
+ {criteria.map((criterion, i) => {
75
+ const selectedLevelUid = selectedLevels?.[criterion.uid];
76
+ return (
77
+ <TableRow
78
+ key={criterion.uid}
79
+ className={cn(i % 2 === 1 && "bg-muted/20")}
80
+ >
81
+ <TableCell className="align-top">
82
+ <div className="font-medium text-foreground">
83
+ {criterion.name}
84
+ </div>
85
+ {criterion.description && (
86
+ <div className="text-xs text-muted-foreground mt-0.5">
87
+ {criterion.description}
88
+ </div>
89
+ )}
90
+ </TableCell>
91
+ {criterion.levels.map((level) => {
92
+ const isSelected = selectedLevelUid === level.uid;
93
+ return (
94
+ <TableCell
95
+ key={level.uid}
96
+ className={cn(
97
+ "align-top text-center",
98
+ isSelected &&
99
+ "bg-primary/10 ring-2 ring-primary/30 ring-inset rounded-sm"
100
+ )}
101
+ >
102
+ <div className="text-xs text-muted-foreground">
103
+ {level.description}
104
+ </div>
105
+ {isSelected && (
106
+ <div className="mt-1.5 flex justify-center">
107
+ <CheckCircle2 className="size-4 text-primary" />
108
+ </div>
109
+ )}
110
+ </TableCell>
111
+ );
112
+ })}
113
+ </TableRow>
114
+ );
115
+ })}
116
+ </TableBody>
117
+ </Table>
118
+ </div>
119
+ </CardContent>
120
+ </Card>
121
+
122
+ {/* Feedback */}
123
+ {feedback && (
124
+ <>
125
+ <Separator />
126
+ <div>
127
+ <h4 className="text-sm font-medium text-foreground mb-1">
128
+ Instructor Feedback
129
+ </h4>
130
+ <p className="text-sm text-muted-foreground whitespace-pre-wrap">
131
+ {feedback}
132
+ </p>
133
+ </div>
134
+ </>
135
+ )}
136
+ </div>
137
+ );
138
+ }