@hydralms/components 0.1.3 → 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.
- package/dist/ForumBoard-CHXU3mjC.js +2207 -0
- package/dist/ForumBoard-d1w5-r6n.cjs +1 -0
- package/dist/assessment-toolbar/assessment-toolbar.d.ts +1 -1
- package/dist/assessment-toolbar/index.d.ts +5 -1
- package/dist/assessment-toolbar/question-header-bar.d.ts +2 -0
- package/dist/assessment-toolbar/question-materials-drawer.d.ts +2 -0
- package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
- package/dist/assessment-toolbar/types.d.ts +52 -4
- package/dist/assessment-toolbar/use-countdown.d.ts +43 -0
- package/dist/common/index.d.ts +2 -1
- package/dist/common/stepper.d.ts +6 -0
- package/dist/common/types.d.ts +37 -0
- package/dist/components.css +1 -1
- package/dist/content/attachment-list.d.ts +6 -0
- package/dist/content/content-block.d.ts +1 -1
- package/dist/content/index.d.ts +2 -1
- package/dist/content/types.d.ts +39 -0
- package/dist/curriculum/curriculum-item.d.ts +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +551 -312
- package/dist/modules/AssignmentModule/AssignmentModule.d.ts +8 -0
- package/dist/modules/AssignmentModule/types.d.ts +65 -0
- package/dist/modules/CertificateModule/CertificateModule.d.ts +9 -0
- package/dist/modules/CertificateModule/types.d.ts +49 -0
- package/dist/modules/DiscussionModule/DiscussionModule.d.ts +8 -0
- package/dist/modules/DiscussionModule/types.d.ts +47 -0
- package/dist/modules/ExamModule/ExamModule.d.ts +8 -0
- package/dist/modules/ExamModule/types.d.ts +64 -0
- package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +9 -0
- package/dist/modules/GradeCenterModule/types.d.ts +54 -0
- package/dist/modules/QuizModule/QuizModule.d.ts +1 -1
- package/dist/modules/QuizModule/types.d.ts +6 -1
- package/dist/modules/SurveyModule/SurveyModule.d.ts +7 -0
- package/dist/modules/SurveyModule/types.d.ts +49 -0
- package/dist/modules/index.d.ts +12 -0
- package/dist/modules.cjs +1 -0
- package/dist/modules.js +1422 -0
- package/dist/progress/achievement-badge.d.ts +6 -0
- package/dist/progress/activity-timeline.d.ts +6 -0
- package/dist/progress/index.d.ts +4 -1
- package/dist/progress/stat-card.d.ts +1 -1
- package/dist/progress/streak-badge.d.ts +6 -0
- package/dist/progress/types.d.ts +97 -0
- package/dist/questions/essay.d.ts +1 -1
- package/dist/questions/hotspot.d.ts +21 -0
- package/dist/questions/index.d.ts +9 -1
- package/dist/questions/inline-choice.d.ts +21 -0
- package/dist/questions/matching.d.ts +22 -0
- package/dist/questions/numeric.d.ts +11 -0
- package/dist/questions/ordering.d.ts +12 -0
- package/dist/questions/scenario.d.ts +23 -0
- package/dist/questions/scoring.d.ts +22 -0
- package/dist/questions/spreadsheet.d.ts +29 -0
- package/dist/questions/types.d.ts +106 -1
- package/dist/questions/use-drag-reorder.d.ts +17 -0
- package/dist/sections/CertificateViewer/types.d.ts +7 -5
- package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
- package/dist/sections/ExamSession/types.d.ts +6 -1
- package/dist/sections/ForumBoard/ForumBoard.d.ts +8 -0
- package/dist/sections/ForumBoard/types.d.ts +64 -0
- package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
- package/dist/sections/QuizSession/types.d.ts +6 -1
- package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
- package/dist/sections/RequirementsChecklist/types.d.ts +37 -0
- package/dist/sections/RubricView/RubricView.d.ts +9 -0
- package/dist/sections/RubricView/types.d.ts +50 -0
- package/dist/sections/index.d.ts +7 -1
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +250 -1715
- package/dist/social/post-card.d.ts +1 -1
- package/dist/tabs-DRM2Iq_J.cjs +172 -0
- package/dist/tabs-Wf3h_Cx3.js +21580 -0
- package/dist/ui/alert.d.ts +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/button.d.ts +1 -1
- package/dist/ui/drawer.d.ts +84 -0
- package/dist/ui/index.d.ts +3 -0
- package/dist/ui/progress.d.ts +1 -1
- package/dist/ui/rich-text-editor.d.ts +30 -0
- package/dist/ui/rich-text-toolbar.d.ts +8 -0
- package/dist/utils/array-utils.d.ts +4 -0
- package/dist/utils/flatten-leaves.d.ts +6 -0
- package/dist/utils/format-file-size.d.ts +1 -0
- package/dist/utils/format-timestamp.d.ts +1 -0
- package/dist/utils/is-empty-html.d.ts +5 -0
- package/dist/utils/shuffle.d.ts +1 -0
- package/dist/utils/string-utils.d.ts +12 -0
- package/dist/video/video-bookmark.d.ts +1 -1
- package/dist/video/video-playlist-item.d.ts +1 -1
- package/package.json +92 -3
- package/src/assessment-toolbar/assessment-toolbar.tsx +54 -49
- package/src/assessment-toolbar/index.ts +6 -0
- package/src/assessment-toolbar/question-header-bar.tsx +61 -0
- package/src/assessment-toolbar/question-materials-drawer.tsx +55 -0
- package/src/assessment-toolbar/question-navigator.tsx +3 -31
- package/src/assessment-toolbar/timer-display.tsx +2 -2
- package/src/assessment-toolbar/types.ts +54 -4
- package/src/assessment-toolbar/use-countdown.ts +153 -0
- package/src/common/index.ts +3 -0
- package/src/common/search-input.tsx +7 -6
- package/src/common/stepper.tsx +100 -0
- package/src/common/types.ts +39 -0
- package/src/content/attachment-list.tsx +90 -0
- package/src/content/content-block.tsx +4 -2
- package/src/content/file-upload-zone.tsx +1 -6
- package/src/content/index.ts +3 -0
- package/src/content/types.ts +41 -0
- package/src/curriculum/curriculum-item.tsx +7 -3
- package/src/feedback/feedback-banner.tsx +12 -14
- package/src/flashcards/flashcard-deck.tsx +1 -9
- package/src/flashcards/flashcard.tsx +1 -1
- package/src/modules/AssignmentModule/AssignmentModule.tsx +305 -0
- package/src/modules/AssignmentModule/types.ts +73 -0
- package/src/modules/CertificateModule/CertificateModule.tsx +161 -0
- package/src/modules/CertificateModule/types.ts +47 -0
- package/src/modules/CoursePlayer/CoursePlayer.tsx +44 -48
- package/src/modules/DiscussionModule/DiscussionModule.tsx +110 -0
- package/src/modules/DiscussionModule/types.ts +54 -0
- package/src/modules/ExamModule/ExamModule.tsx +285 -0
- package/src/modules/ExamModule/types.ts +66 -0
- package/src/modules/FlashcardLab/FlashcardLab.tsx +29 -16
- package/src/modules/GradeCenterModule/GradeCenterModule.tsx +169 -0
- package/src/modules/GradeCenterModule/types.ts +63 -0
- package/src/modules/QuizModule/QuizModule.tsx +88 -88
- package/src/modules/QuizModule/types.ts +6 -1
- package/src/modules/SurveyModule/SurveyModule.tsx +180 -0
- package/src/modules/SurveyModule/types.ts +51 -0
- package/src/modules/index.ts +24 -0
- package/src/progress/achievement-badge.tsx +52 -0
- package/src/progress/activity-timeline.tsx +84 -0
- package/src/progress/index.ts +7 -0
- package/src/progress/stat-card.tsx +30 -18
- package/src/progress/streak-badge.tsx +35 -0
- package/src/progress/types.ts +101 -0
- package/src/questions/choice.tsx +7 -9
- package/src/questions/essay.tsx +23 -25
- package/src/questions/fill-in-the-blank.tsx +13 -16
- package/src/questions/hotspot.tsx +154 -0
- package/src/questions/index.ts +16 -0
- package/src/questions/inline-choice.tsx +151 -0
- package/src/questions/matching.tsx +228 -0
- package/src/questions/multiple-choice.tsx +7 -9
- package/src/questions/numeric.tsx +102 -0
- package/src/questions/ordering.tsx +159 -0
- package/src/questions/question-renderer.tsx +21 -0
- package/src/questions/scenario.tsx +140 -0
- package/src/questions/scoring.ts +201 -0
- package/src/questions/spreadsheet.tsx +259 -0
- package/src/questions/true-false.tsx +7 -9
- package/src/questions/types.ts +123 -1
- package/src/questions/use-drag-reorder.ts +80 -0
- package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +2 -15
- package/src/sections/AssessmentReview/AssessmentReview.tsx +13 -2
- package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +7 -5
- package/src/sections/CertificateViewer/CertificateViewer.tsx +409 -56
- package/src/sections/CertificateViewer/types.ts +13 -5
- package/src/sections/CourseOutline/CourseOutline.tsx +4 -14
- package/src/sections/DiscussionThread/DiscussionThread.tsx +13 -10
- package/src/sections/ExamSession/ExamSession.tsx +44 -7
- package/src/sections/ExamSession/types.ts +6 -1
- package/src/sections/ForumBoard/ForumBoard.tsx +284 -0
- package/src/sections/ForumBoard/types.ts +67 -0
- package/src/sections/GradebookTable/GradebookTable.tsx +1 -1
- package/src/sections/LecturePlayer/LecturePlayer.tsx +1 -1
- package/src/sections/LessonPage/LessonPage.tsx +5 -9
- package/src/sections/PracticeQuiz/PracticeQuiz.tsx +15 -26
- package/src/sections/ProgressDashboard/ProgressDashboard.tsx +65 -65
- package/src/sections/QuizSession/QuizSession.tsx +67 -8
- package/src/sections/QuizSession/types.ts +6 -1
- package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +107 -0
- package/src/sections/RequirementsChecklist/types.ts +38 -0
- package/src/sections/ResourceLibrary/ResourceLibrary.tsx +4 -9
- package/src/sections/RubricView/RubricView.tsx +138 -0
- package/src/sections/RubricView/types.ts +52 -0
- package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +23 -9
- package/src/sections/SurveyForm/SurveyForm.tsx +8 -5
- package/src/sections/index.ts +20 -1
- package/src/social/post-card.tsx +8 -19
- package/src/social/user-avatar.tsx +1 -0
- package/src/styles/globals.css +13 -0
- package/src/ui/drawer.tsx +600 -0
- package/src/ui/index.ts +19 -0
- package/src/ui/rich-text-editor.tsx +109 -0
- package/src/ui/rich-text-toolbar.tsx +156 -0
- package/src/utils/array-utils.ts +17 -0
- package/src/utils/flatten-leaves.ts +17 -0
- package/src/utils/format-file-size.ts +5 -0
- package/src/utils/format-timestamp.ts +13 -0
- package/src/utils/is-empty-html.ts +7 -0
- package/src/utils/shuffle.ts +8 -0
- package/src/utils/string-utils.ts +30 -0
- package/src/video/video-bookmark.tsx +4 -3
- package/src/video/video-chapter-list.tsx +9 -4
- package/src/video/video-player.tsx +11 -4
- package/src/video/video-playlist-item.tsx +8 -3
- package/src/video/video-thumbnail-card.tsx +4 -0
- package/src/video/video-transcript.tsx +8 -5
- package/dist/table-BrS5cDQu.js +0 -2510
- 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
|
-
|
|
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-
|
|
127
|
-
<
|
|
128
|
-
<HelpCircle className="size-
|
|
129
|
-
|
|
130
|
-
</
|
|
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
|
-
<
|
|
133
|
-
<Clock className="size-
|
|
134
|
-
|
|
135
|
-
</
|
|
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
|
-
<
|
|
139
|
-
<CheckCircle2 className="size-
|
|
140
|
-
|
|
141
|
-
</
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/modules/index.ts
CHANGED
|
@@ -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
|
+
}
|