@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.
- 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 +141 -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
|
@@ -1,26 +1,19 @@
|
|
|
1
|
+
import { BookOpen, Clock } from "lucide-react";
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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-
|
|
40
|
-
<Card
|
|
41
|
-
<
|
|
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
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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-
|
|
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
|
-
"
|
|
74
|
+
"transition-colors",
|
|
74
75
|
onModuleClick && "cursor-pointer hover:border-primary",
|
|
75
76
|
)}
|
|
76
77
|
onClick={() => onModuleClick?.(mod.uid)}
|
|
77
78
|
>
|
|
78
|
-
<
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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="
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
<
|
|
120
|
-
{badge.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
<
|
|
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={
|
|
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-
|
|
75
|
+
<span className="block text-xs text-muted-foreground">
|
|
81
76
|
{[
|
|
82
77
|
resource.description,
|
|
83
|
-
resource.fileSize != null &&
|
|
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
|
-
{
|
|
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
|
+
}
|