@hydralms/components 0.2.0 → 0.3.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/StudentProfile-BVfZMbnV.cjs +1 -0
- package/dist/StudentProfile-DeMxdrL3.js +3275 -0
- package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
- package/dist/assessment-toolbar/timer-display.d.ts +1 -1
- package/dist/common/index.d.ts +2 -1
- package/dist/common/pagination.d.ts +26 -0
- package/dist/common/types.d.ts +1 -0
- package/dist/components.css +1 -1
- package/dist/content/audio-player.d.ts +22 -0
- package/dist/content/code-block.d.ts +30 -0
- package/dist/content/embed-block.d.ts +28 -0
- package/dist/content/index.d.ts +6 -0
- package/dist/content/types.d.ts +24 -0
- package/dist/curriculum/course-card.d.ts +51 -0
- package/dist/curriculum/index.d.ts +2 -0
- package/dist/curriculum/types.d.ts +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +494 -444
- package/dist/license/HydraContext.d.ts +16 -0
- package/dist/license/ProBadge.d.ts +6 -0
- package/dist/license/index.d.ts +7 -0
- package/dist/license/tiers.d.ts +3 -0
- package/dist/license/useHydraLicense.d.ts +6 -0
- package/dist/license/validate.d.ts +13 -0
- package/dist/license/withProGate.d.ts +6 -0
- package/dist/modules/AssignmentModule/AssignmentModule.d.ts +4 -7
- package/dist/modules/AssignmentModule/types.d.ts +5 -1
- package/dist/modules/CertificateModule/CertificateModule.d.ts +4 -8
- package/dist/modules/CertificateModule/types.d.ts +6 -4
- package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
- package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
- package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
- package/dist/modules/DiscussionModule/DiscussionModule.d.ts +4 -7
- package/dist/modules/ExamModule/ExamModule.d.ts +4 -7
- package/dist/modules/ExamModule/types.d.ts +5 -14
- package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
- package/dist/modules/FlashcardLab/types.d.ts +2 -0
- package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +4 -8
- package/dist/modules/GradeCenterModule/types.d.ts +2 -0
- package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
- package/dist/modules/QuizModule/types.d.ts +5 -14
- package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
- package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
- package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
- package/dist/modules/StudentProfileModule/types.d.ts +43 -0
- package/dist/modules/SurveyModule/SurveyModule.d.ts +4 -6
- package/dist/modules/SurveyModule/types.d.ts +2 -0
- package/dist/modules/_shared/assessment-intro.d.ts +16 -0
- package/dist/modules/_shared/assessment-results.d.ts +23 -0
- package/dist/modules/_shared/types.d.ts +10 -0
- package/dist/modules/_shared/use-timer.d.ts +9 -0
- package/dist/modules/index.d.ts +6 -0
- package/dist/modules.cjs +1 -1
- package/dist/modules.js +1266 -854
- package/dist/progress/types.d.ts +2 -0
- package/dist/provider/HydraProvider.d.ts +5 -1
- package/dist/questions/choice.d.ts +1 -1
- package/dist/questions/confidence-indicator.d.ts +37 -0
- package/dist/questions/essay.d.ts +1 -1
- package/dist/questions/fill-in-the-blank.d.ts +1 -1
- package/dist/questions/hotspot.d.ts +1 -1
- package/dist/questions/index.d.ts +2 -0
- package/dist/questions/inline-choice.d.ts +1 -1
- package/dist/questions/matching.d.ts +1 -1
- package/dist/questions/multiple-choice.d.ts +1 -1
- package/dist/questions/numeric.d.ts +1 -1
- package/dist/questions/ordering.d.ts +1 -1
- package/dist/questions/question-renderer.d.ts +1 -1
- package/dist/questions/scenario.d.ts +1 -1
- package/dist/questions/spreadsheet.d.ts +1 -1
- package/dist/questions/true-false.d.ts +1 -1
- package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
- package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
- package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
- package/dist/sections/AssessmentReview/types.d.ts +6 -0
- package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
- package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
- package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
- package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
- package/dist/sections/CertificateViewer/types.d.ts +6 -0
- package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
- package/dist/sections/CourseCatalog/types.d.ts +80 -0
- package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
- package/dist/sections/CourseOutline/types.d.ts +6 -0
- package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
- package/dist/sections/DiscussionThread/types.d.ts +6 -0
- package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
- package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
- package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
- package/dist/sections/ExamSession/types.d.ts +6 -0
- package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
- package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
- package/dist/sections/ForumBoard/ForumBoard.d.ts +1 -1
- package/dist/sections/ForumBoard/types.d.ts +14 -0
- package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
- package/dist/sections/GradebookTable/types.d.ts +14 -0
- package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
- package/dist/sections/LecturePlayer/types.d.ts +8 -0
- package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
- package/dist/sections/LessonPage/types.d.ts +6 -0
- package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
- package/dist/sections/PracticeQuiz/types.d.ts +6 -0
- package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
- package/dist/sections/ProgressDashboard/types.d.ts +6 -0
- package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
- package/dist/sections/QuizSession/types.d.ts +6 -0
- package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +1 -1
- package/dist/sections/RequirementsChecklist/types.d.ts +6 -0
- package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
- package/dist/sections/ResourceLibrary/types.d.ts +15 -1
- package/dist/sections/RubricView/RubricView.d.ts +1 -1
- package/dist/sections/RubricView/types.d.ts +6 -0
- package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
- package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
- package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
- package/dist/sections/StudentProfile/types.d.ts +98 -0
- package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
- package/dist/sections/SurveyForm/types.d.ts +6 -0
- package/dist/sections/_shared/merge-answers.d.ts +9 -0
- package/dist/sections/_shared/section-shell.d.ts +20 -0
- package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
- package/dist/sections/index.d.ts +6 -0
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +268 -307
- package/dist/tabs-BsfVo2Bl.cjs +173 -0
- package/dist/{tabs-Wf3h_Cx3.js → tabs-BuY1iNJE.js} +7532 -6807
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/progress.d.ts +1 -1
- package/dist/ui/rich-text-editor.d.ts +3 -1
- package/dist/ui/toast.d.ts +43 -0
- package/dist/utils/debounce.d.ts +5 -1
- package/dist/utils/pick-palette-color.d.ts +19 -0
- package/dist/video/types.d.ts +15 -0
- package/dist/video/video-player.d.ts +1 -1
- package/dist/withProGate-BWqcKdPM.js +137 -0
- package/dist/withProGate-DX6XqKLp.cjs +1 -0
- package/package.json +34 -220
- package/src/assessment-toolbar/question-navigator.tsx +10 -5
- package/src/assessment-toolbar/timer-display.tsx +4 -3
- package/src/assessment-toolbar/use-countdown.ts +1 -1
- package/src/common/empty-state.tsx +1 -0
- package/src/common/index.ts +2 -0
- package/src/common/pagination.tsx +135 -0
- package/src/common/search-input.tsx +2 -1
- package/src/common/types.ts +2 -0
- package/src/content/attachment-list.tsx +2 -0
- package/src/content/audio-player.tsx +196 -0
- package/src/content/code-block.tsx +113 -0
- package/src/content/content-block.tsx +64 -0
- package/src/content/embed-block.tsx +78 -0
- package/src/content/file-upload-zone.tsx +10 -0
- package/src/content/index.ts +6 -0
- package/src/content/types.ts +5 -0
- package/src/curriculum/course-card.tsx +199 -0
- package/src/curriculum/curriculum-item.tsx +3 -3
- package/src/curriculum/curriculum-tree.tsx +20 -13
- package/src/curriculum/index.ts +2 -0
- package/src/curriculum/types.ts +2 -2
- package/src/flashcards/flashcard.tsx +28 -8
- package/src/index.ts +3 -0
- package/src/license/HydraContext.tsx +62 -0
- package/src/license/ProBadge.tsx +43 -0
- package/src/license/index.ts +7 -0
- package/src/license/tiers.ts +24 -0
- package/src/license/useHydraLicense.ts +10 -0
- package/src/license/validate.ts +90 -0
- package/src/license/withProGate.tsx +21 -0
- package/src/modules/AssignmentModule/AssignmentModule.tsx +17 -8
- package/src/modules/AssignmentModule/types.ts +5 -1
- package/src/modules/CertificateModule/CertificateModule.tsx +21 -9
- package/src/modules/CertificateModule/types.ts +6 -4
- package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
- package/src/modules/CourseCatalogModule/types.ts +47 -0
- package/src/modules/CoursePlayer/CoursePlayer.tsx +37 -22
- package/src/modules/DiscussionModule/DiscussionModule.tsx +57 -22
- package/src/modules/ExamModule/ExamModule.tsx +64 -198
- package/src/modules/ExamModule/types.ts +5 -14
- package/src/modules/FlashcardLab/FlashcardLab.tsx +10 -5
- package/src/modules/FlashcardLab/types.ts +2 -0
- package/src/modules/GradeCenterModule/GradeCenterModule.tsx +7 -2
- package/src/modules/GradeCenterModule/types.ts +2 -0
- package/src/modules/QuizModule/QuizModule.tsx +49 -169
- package/src/modules/QuizModule/types.ts +5 -15
- package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
- package/src/modules/StudentDashboardModule/types.ts +56 -0
- package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
- package/src/modules/StudentProfileModule/types.ts +45 -0
- package/src/modules/SurveyModule/SurveyModule.tsx +9 -4
- package/src/modules/SurveyModule/types.ts +2 -0
- package/src/modules/_shared/assessment-intro.tsx +75 -0
- package/src/modules/_shared/assessment-results.tsx +133 -0
- package/src/modules/_shared/types.ts +11 -0
- package/src/modules/_shared/use-timer.ts +49 -0
- package/src/modules/index.ts +9 -0
- package/src/progress/achievement-badge.tsx +3 -3
- package/src/progress/grade-indicator.tsx +9 -1
- package/src/progress/progress-ring.tsx +2 -1
- package/src/progress/stat-card.tsx +8 -1
- package/src/progress/types.ts +2 -0
- package/src/provider/HydraProvider.tsx +15 -6
- package/src/questions/choice.tsx +13 -6
- package/src/questions/confidence-indicator.tsx +107 -0
- package/src/questions/essay.tsx +6 -4
- package/src/questions/fill-in-the-blank.tsx +8 -4
- package/src/questions/hotspot.tsx +4 -4
- package/src/questions/index.ts +2 -0
- package/src/questions/inline-choice.tsx +5 -4
- package/src/questions/matching.tsx +5 -4
- package/src/questions/multiple-choice.tsx +13 -6
- package/src/questions/numeric.tsx +8 -4
- package/src/questions/ordering.tsx +12 -4
- package/src/questions/question-renderer.tsx +3 -2
- package/src/questions/scenario.tsx +4 -4
- package/src/questions/spreadsheet.tsx +5 -4
- package/src/questions/true-false.tsx +13 -6
- package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +64 -8
- package/src/sections/AnnouncementFeed/types.ts +15 -1
- package/src/sections/AssessmentReview/AssessmentReview.tsx +37 -0
- package/src/sections/AssessmentReview/types.ts +6 -0
- package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +37 -1
- package/src/sections/AssignmentSubmission/types.ts +6 -0
- package/src/sections/CertificateViewer/CertificateViewer.tsx +29 -227
- package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
- package/src/sections/CertificateViewer/types.ts +6 -0
- package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
- package/src/sections/CourseCatalog/types.ts +76 -0
- package/src/sections/CourseOutline/CourseOutline.tsx +41 -0
- package/src/sections/CourseOutline/types.ts +6 -0
- package/src/sections/DiscussionThread/DiscussionThread.tsx +42 -1
- package/src/sections/DiscussionThread/types.ts +6 -0
- package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
- package/src/sections/EnrollmentWizard/types.ts +65 -0
- package/src/sections/ExamSession/ExamSession.tsx +100 -94
- package/src/sections/ExamSession/types.ts +6 -0
- package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
- package/src/sections/FlashcardStudySession/types.ts +6 -0
- package/src/sections/ForumBoard/ForumBoard.tsx +59 -1
- package/src/sections/ForumBoard/types.ts +14 -0
- package/src/sections/GradebookTable/GradebookTable.tsx +54 -1
- package/src/sections/GradebookTable/types.ts +14 -0
- package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
- package/src/sections/LecturePlayer/types.ts +8 -0
- package/src/sections/LessonPage/LessonPage.tsx +36 -5
- package/src/sections/LessonPage/types.ts +6 -0
- package/src/sections/PracticeQuiz/PracticeQuiz.tsx +106 -74
- package/src/sections/PracticeQuiz/types.ts +6 -0
- package/src/sections/ProgressDashboard/ProgressDashboard.tsx +64 -10
- package/src/sections/ProgressDashboard/types.ts +6 -0
- package/src/sections/QuizSession/QuizSession.tsx +71 -82
- package/src/sections/QuizSession/types.ts +6 -0
- package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +41 -1
- package/src/sections/RequirementsChecklist/types.ts +6 -0
- package/src/sections/ResourceLibrary/ResourceLibrary.tsx +64 -8
- package/src/sections/ResourceLibrary/types.ts +15 -1
- package/src/sections/RubricView/RubricView.tsx +37 -1
- package/src/sections/RubricView/types.ts +6 -0
- package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +36 -15
- package/src/sections/ScrollableQuiz/types.ts +6 -0
- package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
- package/src/sections/StudentProfile/types.ts +99 -0
- package/src/sections/SurveyForm/SurveyForm.tsx +32 -5
- package/src/sections/SurveyForm/types.ts +6 -0
- package/src/sections/_shared/merge-answers.ts +22 -0
- package/src/sections/_shared/section-shell.tsx +64 -0
- package/src/sections/_shared/use-assessment-session.ts +125 -0
- package/src/sections/index.ts +22 -0
- package/src/social/user-avatar.tsx +9 -5
- package/src/styles/globals.css +39 -41
- package/src/ui/badge.tsx +8 -0
- package/src/ui/index.ts +2 -0
- package/src/ui/progress.tsx +4 -0
- package/src/ui/rich-text-editor.tsx +10 -0
- package/src/ui/rich-text-toolbar.tsx +2 -1
- package/src/ui/toast.tsx +170 -0
- package/src/utils/debounce.ts +8 -2
- package/src/utils/pick-palette-color.ts +33 -0
- package/src/video/types.ts +16 -0
- package/src/video/video-player.tsx +13 -1
- package/dist/ForumBoard-CHXU3mjC.js +0 -2207
- package/dist/ForumBoard-d1w5-r6n.cjs +0 -1
- package/dist/tabs-DRM2Iq_J.cjs +0 -172
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { useMemo, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { CheckCircle } from "lucide-react";
|
|
3
3
|
import { QuestionRenderer, scoreQuestion } from "../../questions";
|
|
4
4
|
import { FeedbackBanner } from "../../feedback";
|
|
5
5
|
import { Button } from "../../ui/button";
|
|
6
6
|
import { Card, CardContent } from "../../ui/card";
|
|
7
7
|
import { Progress } from "../../ui/progress";
|
|
8
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
9
|
+
import { SectionShell } from "../_shared/section-shell";
|
|
8
10
|
import type { PracticeQuizProps, PracticeQuizStats } from "./types";
|
|
9
11
|
import { shuffle } from "../../utils/shuffle";
|
|
10
12
|
|
|
@@ -15,11 +17,17 @@ export function PracticeQuiz({
|
|
|
15
17
|
onComplete,
|
|
16
18
|
shuffled = false,
|
|
17
19
|
readOnly = false,
|
|
20
|
+
isLoading,
|
|
21
|
+
error,
|
|
22
|
+
onRetry,
|
|
18
23
|
className,
|
|
19
24
|
style,
|
|
20
25
|
}: PracticeQuizProps) {
|
|
21
26
|
const questions = useMemo(
|
|
22
|
-
() =>
|
|
27
|
+
() => {
|
|
28
|
+
const qs = questionsProp ?? [];
|
|
29
|
+
return shuffled ? shuffle(qs) : qs;
|
|
30
|
+
},
|
|
23
31
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
24
32
|
[questionsProp, shuffled],
|
|
25
33
|
);
|
|
@@ -30,6 +38,16 @@ export function PracticeQuiz({
|
|
|
30
38
|
const [firstAttemptCorrect, setFirstAttemptCorrect] = useState<Set<string>>(new Set());
|
|
31
39
|
const [currentAnswer, setCurrentAnswer] = useState<{ uid: string; content?: string }[] | null>(null);
|
|
32
40
|
const [isComplete, setIsComplete] = useState(false);
|
|
41
|
+
const questionAreaRef = useRef<HTMLDivElement>(null);
|
|
42
|
+
const isFirstRender = useRef(true);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (isFirstRender.current) {
|
|
46
|
+
isFirstRender.current = false;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
questionAreaRef.current?.focus();
|
|
50
|
+
}, [currentIndex]);
|
|
33
51
|
|
|
34
52
|
const currentQuestion = questions[currentIndex];
|
|
35
53
|
const isRevealed = currentQuestion ? revealedUids.has(currentQuestion.uid) : false;
|
|
@@ -78,27 +96,45 @@ export function PracticeQuiz({
|
|
|
78
96
|
}
|
|
79
97
|
}
|
|
80
98
|
|
|
81
|
-
const
|
|
82
|
-
if (!currentQuestion || !currentAnswer) return
|
|
83
|
-
|
|
99
|
+
const currentSessionAnswers = useMemo(() => {
|
|
100
|
+
if (!currentQuestion || !currentAnswer) return [];
|
|
101
|
+
return currentAnswer.map((a) => ({
|
|
84
102
|
uid: currentQuestion.uid,
|
|
85
103
|
answerUid: a.uid,
|
|
86
104
|
content: a.content,
|
|
87
105
|
}));
|
|
88
|
-
return scoreQuestion(currentQuestion, sessionAnswers) === true;
|
|
89
106
|
}, [currentQuestion, currentAnswer]);
|
|
90
107
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
const isCurrentCorrect = useMemo(
|
|
109
|
+
() => currentQuestion ? scoreQuestion(currentQuestion, currentSessionAnswers) === true : false,
|
|
110
|
+
[currentQuestion, currentSessionAnswers],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const completionPercentage = questions.length > 0
|
|
114
|
+
? Math.round((firstAttemptCorrect.size / questions.length) * 100)
|
|
115
|
+
: 0;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<SectionShell
|
|
119
|
+
isLoading={isLoading}
|
|
120
|
+
error={error}
|
|
121
|
+
onRetry={onRetry}
|
|
122
|
+
className={className}
|
|
123
|
+
style={style}
|
|
124
|
+
skeleton={
|
|
125
|
+
<>
|
|
126
|
+
<Skeleton className="h-2 w-full" />
|
|
127
|
+
<Skeleton className="h-48 w-full" />
|
|
128
|
+
</>
|
|
129
|
+
}
|
|
130
|
+
>
|
|
131
|
+
{isComplete ? (
|
|
96
132
|
<Card className={className} style={style}>
|
|
97
133
|
<CardContent className="pt-6 text-center">
|
|
98
134
|
<CheckCircle size={48} className="text-success mx-auto mb-4" />
|
|
99
135
|
<p className="text-xl font-bold mb-1 text-foreground">Practice Complete!</p>
|
|
100
136
|
<p className="text-muted-foreground mb-2">
|
|
101
|
-
{firstAttemptCorrect.size} of {questions.length} correct on first attempt ({
|
|
137
|
+
{firstAttemptCorrect.size} of {questions.length} correct on first attempt ({completionPercentage}%)
|
|
102
138
|
</p>
|
|
103
139
|
<div className="flex justify-center">
|
|
104
140
|
<Button
|
|
@@ -117,72 +153,68 @@ export function PracticeQuiz({
|
|
|
117
153
|
</div>
|
|
118
154
|
</CardContent>
|
|
119
155
|
</Card>
|
|
120
|
-
)
|
|
121
|
-
|
|
156
|
+
) : (
|
|
157
|
+
<div className={className} style={style}>
|
|
158
|
+
<div className="flex justify-between items-center mb-2">
|
|
159
|
+
<span className="font-semibold text-sm text-foreground">
|
|
160
|
+
Question {currentIndex + 1} of {questions.length}
|
|
161
|
+
</span>
|
|
162
|
+
<span className="text-xs text-muted-foreground">
|
|
163
|
+
{firstAttemptCorrect.size} correct on first try
|
|
164
|
+
</span>
|
|
165
|
+
</div>
|
|
166
|
+
<Progress
|
|
167
|
+
value={currentIndex + (isRevealed ? 1 : 0)}
|
|
168
|
+
max={questions.length}
|
|
169
|
+
size="sm"
|
|
170
|
+
className="mb-3"
|
|
171
|
+
/>
|
|
122
172
|
|
|
123
|
-
|
|
124
|
-
<div className={className} style={style}>
|
|
125
|
-
<div className="flex justify-between items-center mb-2">
|
|
126
|
-
<span className="font-semibold text-sm text-foreground">
|
|
173
|
+
<span className="sr-only" aria-live="polite">
|
|
127
174
|
Question {currentIndex + 1} of {questions.length}
|
|
128
175
|
</span>
|
|
129
|
-
|
|
130
|
-
{
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
/>
|
|
139
|
-
|
|
140
|
-
{currentQuestion && (
|
|
141
|
-
<Card>
|
|
142
|
-
<CardContent className="pt-6">
|
|
143
|
-
<QuestionRenderer
|
|
144
|
-
question={currentQuestion}
|
|
145
|
-
sessionAnswers={
|
|
146
|
-
currentAnswer?.map((a) => ({
|
|
147
|
-
uid: currentQuestion.uid,
|
|
148
|
-
answerUid: a.uid,
|
|
149
|
-
content: a.content,
|
|
150
|
-
})) ?? []
|
|
151
|
-
}
|
|
152
|
-
onAnswer={(answers) => setCurrentAnswer(answers)}
|
|
153
|
-
readOnly={readOnly || isRevealed}
|
|
154
|
-
showCorrectAnswers={isRevealed}
|
|
155
|
-
/>
|
|
156
|
-
|
|
157
|
-
{instantFeedback && isRevealed && (
|
|
158
|
-
<FeedbackBanner
|
|
159
|
-
isCorrect={isCurrentCorrect}
|
|
160
|
-
explanation={currentQuestion.explanation}
|
|
161
|
-
onRetry={allowRetry && !isCurrentCorrect ? handleRetry : undefined}
|
|
176
|
+
{currentQuestion && (
|
|
177
|
+
<Card ref={questionAreaRef} tabIndex={-1}>
|
|
178
|
+
<CardContent className="pt-6">
|
|
179
|
+
<QuestionRenderer
|
|
180
|
+
question={currentQuestion}
|
|
181
|
+
sessionAnswers={currentSessionAnswers}
|
|
182
|
+
onAnswer={setCurrentAnswer}
|
|
183
|
+
readOnly={readOnly || isRevealed}
|
|
184
|
+
showCorrectAnswers={isRevealed}
|
|
162
185
|
/>
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
>
|
|
171
|
-
Check Answer
|
|
172
|
-
</Button>
|
|
173
|
-
)}
|
|
174
|
-
{(!instantFeedback || isRevealed) && (
|
|
175
|
-
<Button
|
|
176
|
-
onClick={handleNext}
|
|
177
|
-
disabled={readOnly}
|
|
178
|
-
>
|
|
179
|
-
{currentIndex < questions.length - 1 ? "Next Question" : "Finish"}
|
|
180
|
-
</Button>
|
|
186
|
+
|
|
187
|
+
{instantFeedback && isRevealed && (
|
|
188
|
+
<FeedbackBanner
|
|
189
|
+
isCorrect={isCurrentCorrect}
|
|
190
|
+
explanation={currentQuestion.explanation}
|
|
191
|
+
onRetry={allowRetry && !isCurrentCorrect ? handleRetry : undefined}
|
|
192
|
+
/>
|
|
181
193
|
)}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
194
|
+
|
|
195
|
+
<div className="flex justify-end gap-2 mt-2">
|
|
196
|
+
{!isRevealed && instantFeedback && (
|
|
197
|
+
<Button
|
|
198
|
+
onClick={checkAnswer}
|
|
199
|
+
disabled={!currentAnswer || currentAnswer.length === 0 || readOnly}
|
|
200
|
+
>
|
|
201
|
+
Check Answer
|
|
202
|
+
</Button>
|
|
203
|
+
)}
|
|
204
|
+
{(!instantFeedback || isRevealed) && (
|
|
205
|
+
<Button
|
|
206
|
+
onClick={handleNext}
|
|
207
|
+
disabled={readOnly}
|
|
208
|
+
>
|
|
209
|
+
{currentIndex < questions.length - 1 ? "Next Question" : "Finish"}
|
|
210
|
+
</Button>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
</CardContent>
|
|
214
|
+
</Card>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
</SectionShell>
|
|
187
219
|
);
|
|
188
220
|
}
|
|
@@ -28,6 +28,12 @@ export interface PracticeQuizProps {
|
|
|
28
28
|
shuffled?: boolean;
|
|
29
29
|
/** When true, all inputs are disabled */
|
|
30
30
|
readOnly?: boolean;
|
|
31
|
+
/** Render skeleton placeholders instead of content */
|
|
32
|
+
isLoading?: boolean;
|
|
33
|
+
/** Error message — renders an error state with optional retry */
|
|
34
|
+
error?: string | null;
|
|
35
|
+
/** Called when the user clicks retry in the error state */
|
|
36
|
+
onRetry?: () => void;
|
|
31
37
|
/** CSS class name for the root element */
|
|
32
38
|
className?: string;
|
|
33
39
|
/** Inline styles for the root element */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { BookOpen, Clock, AlertCircle } from "lucide-react";
|
|
2
3
|
import {
|
|
3
4
|
ProgressRing,
|
|
4
5
|
StatCard,
|
|
@@ -10,7 +11,11 @@ import type { TimelineEvent } from "../../progress";
|
|
|
10
11
|
import { Progress } from "../../ui/progress";
|
|
11
12
|
import { Card, CardContent } from "../../ui/card";
|
|
12
13
|
import { Separator } from "../../ui/separator";
|
|
14
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
15
|
+
import { Button } from "../../ui/button";
|
|
16
|
+
import { EmptyState } from "../../common/empty-state";
|
|
13
17
|
import { formatDuration } from "../../utils/format-duration";
|
|
18
|
+
import { pickPaletteVariant } from "../../utils/pick-palette-color";
|
|
14
19
|
import type { ProgressDashboardProps } from "./types";
|
|
15
20
|
import { cn } from "../../lib/utils";
|
|
16
21
|
|
|
@@ -23,9 +28,63 @@ export function ProgressDashboard({
|
|
|
23
28
|
achievements,
|
|
24
29
|
recentActivityLimit = 5,
|
|
25
30
|
onModuleClick,
|
|
31
|
+
isLoading,
|
|
32
|
+
error,
|
|
33
|
+
onRetry,
|
|
26
34
|
className,
|
|
27
35
|
style,
|
|
28
36
|
}: ProgressDashboardProps) {
|
|
37
|
+
const completedModuleCount = useMemo(
|
|
38
|
+
() => modules.filter((m) => m.completedItems === m.totalItems).length,
|
|
39
|
+
[modules],
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const timelineEvents = useMemo<TimelineEvent[]>(
|
|
43
|
+
() =>
|
|
44
|
+
recentActivity?.map((activity) => ({
|
|
45
|
+
uid: activity.uid,
|
|
46
|
+
type: activity.type,
|
|
47
|
+
title: activity.description,
|
|
48
|
+
timestamp: activity.timestamp,
|
|
49
|
+
})) ?? [],
|
|
50
|
+
[recentActivity],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (isLoading) {
|
|
54
|
+
return (
|
|
55
|
+
<div className={cn("space-y-4", className)} style={style}>
|
|
56
|
+
<div className="grid grid-cols-3 gap-3">
|
|
57
|
+
<Skeleton className="h-24" />
|
|
58
|
+
<Skeleton className="h-24" />
|
|
59
|
+
<Skeleton className="h-24" />
|
|
60
|
+
</div>
|
|
61
|
+
<Skeleton className="h-32 w-32 rounded-full mx-auto" />
|
|
62
|
+
<Skeleton className="h-8 w-full" />
|
|
63
|
+
<Skeleton className="h-8 w-full" />
|
|
64
|
+
<Skeleton className="h-8 w-full" />
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (error) {
|
|
70
|
+
return (
|
|
71
|
+
<div className={cn("py-12", className)} style={style}>
|
|
72
|
+
<EmptyState
|
|
73
|
+
icon={<AlertCircle className="size-10 text-destructive" />}
|
|
74
|
+
title="Something went wrong"
|
|
75
|
+
description={error}
|
|
76
|
+
action={
|
|
77
|
+
onRetry ? (
|
|
78
|
+
<Button variant="outline" onClick={onRetry}>
|
|
79
|
+
Retry
|
|
80
|
+
</Button>
|
|
81
|
+
) : undefined
|
|
82
|
+
}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
29
88
|
return (
|
|
30
89
|
<div className={className} style={style}>
|
|
31
90
|
{/* Stats row */}
|
|
@@ -56,7 +115,7 @@ export function ProgressDashboard({
|
|
|
56
115
|
icon={<BookOpen size={24} />}
|
|
57
116
|
label="Modules"
|
|
58
117
|
description="Course progress"
|
|
59
|
-
value={`${
|
|
118
|
+
value={`${completedModuleCount} / ${modules.length}`}
|
|
60
119
|
subtitle="completed"
|
|
61
120
|
/>
|
|
62
121
|
</div>
|
|
@@ -65,7 +124,7 @@ export function ProgressDashboard({
|
|
|
65
124
|
<Separator className="mb-3" />
|
|
66
125
|
<p className="text-lg font-semibold mb-2 text-foreground">Module Progress</p>
|
|
67
126
|
<div className="flex flex-col gap-2 mb-4">
|
|
68
|
-
{modules.map((mod) => {
|
|
127
|
+
{modules.map((mod, index) => {
|
|
69
128
|
const pct = mod.totalItems > 0 ? (mod.completedItems / mod.totalItems) * 100 : 0;
|
|
70
129
|
return (
|
|
71
130
|
<Card
|
|
@@ -83,7 +142,7 @@ export function ProgressDashboard({
|
|
|
83
142
|
{mod.completedItems} / {mod.totalItems}
|
|
84
143
|
</span>
|
|
85
144
|
</div>
|
|
86
|
-
<Progress value={pct} size="sm" />
|
|
145
|
+
<Progress value={pct} size="sm" variant={pct >= 100 ? "success" : pickPaletteVariant(index)} />
|
|
87
146
|
</CardContent>
|
|
88
147
|
</Card>
|
|
89
148
|
);
|
|
@@ -97,12 +156,7 @@ export function ProgressDashboard({
|
|
|
97
156
|
<p className="text-lg font-semibold mb-2 text-foreground">Recent Activity</p>
|
|
98
157
|
<div className="mb-4">
|
|
99
158
|
<ActivityTimeline
|
|
100
|
-
events={
|
|
101
|
-
uid: activity.uid,
|
|
102
|
-
type: activity.type,
|
|
103
|
-
title: activity.description,
|
|
104
|
-
timestamp: activity.timestamp,
|
|
105
|
-
}))}
|
|
159
|
+
events={timelineEvents}
|
|
106
160
|
limit={recentActivityLimit}
|
|
107
161
|
/>
|
|
108
162
|
</div>
|
|
@@ -30,6 +30,12 @@ export interface ProgressDashboardProps {
|
|
|
30
30
|
recentActivityLimit?: number;
|
|
31
31
|
/** Called when the user clicks a module */
|
|
32
32
|
onModuleClick?: (moduleUid: string) => void;
|
|
33
|
+
/** Render skeleton placeholders instead of content */
|
|
34
|
+
isLoading?: boolean;
|
|
35
|
+
/** Error message — renders an error state with optional retry */
|
|
36
|
+
error?: string | null;
|
|
37
|
+
/** Called when the user clicks retry in the error state */
|
|
38
|
+
onRetry?: () => void;
|
|
33
39
|
/** CSS class name for the root element */
|
|
34
40
|
className?: string;
|
|
35
41
|
/** Inline styles for the root element */
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
2
|
import { ChevronLeft, ChevronRight, Send } from "lucide-react";
|
|
3
3
|
import { AssessmentToolbar, QuestionHeaderBar, QuestionMaterialsDrawer } from "../../assessment-toolbar";
|
|
4
|
-
import type { QuestionNavigatorItem } from "../../assessment-toolbar/types";
|
|
5
4
|
import { QuestionRenderer } from "../../questions";
|
|
6
|
-
import type { SessionAnswer } from "../../questions/types";
|
|
7
5
|
import { Button } from "../../ui/button";
|
|
8
6
|
import { Card, CardHeader, CardContent } from "../../ui/card";
|
|
7
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
8
|
+
|
|
9
|
+
import { useAssessmentSession } from "../_shared/use-assessment-session";
|
|
10
|
+
import { SectionShell } from "../_shared/section-shell";
|
|
9
11
|
import type { QuizSessionProps } from "./types";
|
|
10
|
-
import { cn } from "../../lib/utils";
|
|
11
12
|
|
|
12
13
|
export function QuizSession({
|
|
13
14
|
questions,
|
|
@@ -19,88 +20,74 @@ export function QuizSession({
|
|
|
19
20
|
questionMaterials,
|
|
20
21
|
isSubmitting = false,
|
|
21
22
|
readOnly = false,
|
|
23
|
+
isLoading,
|
|
24
|
+
error,
|
|
25
|
+
onRetry,
|
|
22
26
|
className,
|
|
23
27
|
style,
|
|
24
28
|
}: QuizSessionProps) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
const {
|
|
30
|
+
currentIndex,
|
|
31
|
+
currentQuestion,
|
|
32
|
+
sessionAnswers,
|
|
33
|
+
flaggedUids,
|
|
34
|
+
materialsOpen,
|
|
35
|
+
setMaterialsOpen,
|
|
36
|
+
questionAreaRef,
|
|
37
|
+
currentQuestionAnswers,
|
|
38
|
+
currentMaterials,
|
|
39
|
+
navigatorItems,
|
|
40
|
+
handleAnswer,
|
|
41
|
+
handleNavigate,
|
|
42
|
+
handleToggleFlag,
|
|
43
|
+
goNext,
|
|
44
|
+
goPrevious,
|
|
45
|
+
hasNext,
|
|
46
|
+
hasPrevious,
|
|
47
|
+
} = useAssessmentSession({ questions, initialAnswers, onAnswerChange, questionMaterials });
|
|
30
48
|
|
|
31
|
-
const
|
|
49
|
+
const sessionAnswersRef = useRef(sessionAnswers);
|
|
50
|
+
sessionAnswersRef.current = sessionAnswers;
|
|
51
|
+
const onSubmitRef = useRef(onSubmit);
|
|
52
|
+
onSubmitRef.current = onSubmit;
|
|
32
53
|
|
|
33
|
-
const
|
|
34
|
-
()
|
|
35
|
-
|
|
36
|
-
? sessionAnswers.filter((a) => a.uid === currentQuestion.uid)
|
|
37
|
-
: [],
|
|
38
|
-
[sessionAnswers, currentQuestion],
|
|
39
|
-
);
|
|
54
|
+
const handleSubmit = useCallback(() => {
|
|
55
|
+
onSubmitRef.current(sessionAnswersRef.current);
|
|
56
|
+
}, []);
|
|
40
57
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
[questionMaterials, currentQuestion],
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const navigatorItems = useMemo<QuestionNavigatorItem[]>(
|
|
47
|
-
() =>
|
|
48
|
-
questions.map((q, idx) => ({
|
|
49
|
-
uid: q.uid,
|
|
50
|
-
sequence: idx,
|
|
51
|
-
isFlagged: flaggedUids.has(q.uid),
|
|
52
|
-
isAnswered: sessionAnswers.some((a) => a.uid === q.uid),
|
|
53
|
-
isSkipped: false,
|
|
54
|
-
})),
|
|
55
|
-
[questions, sessionAnswers, flaggedUids],
|
|
56
|
-
);
|
|
58
|
+
const currentQuestionUidRef = useRef(currentQuestion?.uid);
|
|
59
|
+
currentQuestionUidRef.current = currentQuestion?.uid;
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
uid: questionUid,
|
|
63
|
-
answerUid: a.uid,
|
|
64
|
-
content: a.content,
|
|
65
|
-
}));
|
|
66
|
-
setSessionAnswers((prev) => {
|
|
67
|
-
const filtered = prev.filter((a) => a.uid !== questionUid);
|
|
68
|
-
const merged = [...filtered, ...newAnswers];
|
|
69
|
-
onAnswerChange?.(merged);
|
|
70
|
-
return merged;
|
|
71
|
-
});
|
|
72
|
-
}
|
|
61
|
+
const toggleCurrentFlag = useCallback(() => {
|
|
62
|
+
const uid = currentQuestionUidRef.current;
|
|
63
|
+
if (uid) handleToggleFlag(uid);
|
|
64
|
+
}, [handleToggleFlag]);
|
|
73
65
|
|
|
74
|
-
|
|
75
|
-
const idx = questions.findIndex((q) => q.uid === uid);
|
|
76
|
-
if (idx !== -1) setCurrentIndex(idx);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function handleToggleFlag(uid: string) {
|
|
80
|
-
setFlaggedUids((prev) => {
|
|
81
|
-
const next = new Set(prev);
|
|
82
|
-
if (next.has(uid)) {
|
|
83
|
-
next.delete(uid);
|
|
84
|
-
} else {
|
|
85
|
-
next.add(uid);
|
|
86
|
-
}
|
|
87
|
-
return next;
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function handleSubmit() {
|
|
92
|
-
onSubmit(sessionAnswers);
|
|
93
|
-
}
|
|
66
|
+
const openMaterials = useCallback(() => setMaterialsOpen(true), [setMaterialsOpen]);
|
|
94
67
|
|
|
95
68
|
return (
|
|
96
|
-
<
|
|
69
|
+
<SectionShell
|
|
70
|
+
isLoading={isLoading}
|
|
71
|
+
error={error}
|
|
72
|
+
onRetry={onRetry}
|
|
73
|
+
className={className}
|
|
74
|
+
style={style}
|
|
75
|
+
skeleton={
|
|
76
|
+
<>
|
|
77
|
+
<Skeleton className="h-10 w-full" />
|
|
78
|
+
<Skeleton className="h-48 w-full" />
|
|
79
|
+
<Skeleton className="h-12 w-full" />
|
|
80
|
+
</>
|
|
81
|
+
}
|
|
82
|
+
>
|
|
83
|
+
<div>
|
|
97
84
|
<AssessmentToolbar
|
|
98
85
|
currentQuestionIndex={currentIndex}
|
|
99
86
|
totalQuestions={questions.length}
|
|
100
|
-
hasNext={
|
|
101
|
-
hasPrevious={
|
|
102
|
-
onNext={
|
|
103
|
-
onPrevious={
|
|
87
|
+
hasNext={hasNext}
|
|
88
|
+
hasPrevious={hasPrevious}
|
|
89
|
+
onNext={goNext}
|
|
90
|
+
onPrevious={goPrevious}
|
|
104
91
|
onSubmit={handleSubmit}
|
|
105
92
|
timeElapsedSeconds={timeElapsedSeconds}
|
|
106
93
|
timeLimitSeconds={timeLimitSeconds}
|
|
@@ -110,16 +97,19 @@ export function QuizSession({
|
|
|
110
97
|
isSubmitting={isSubmitting}
|
|
111
98
|
readOnly={readOnly}
|
|
112
99
|
/>
|
|
100
|
+
<span className="sr-only" aria-live="polite">
|
|
101
|
+
Question {currentIndex + 1} of {questions.length}
|
|
102
|
+
</span>
|
|
113
103
|
{currentQuestion && (
|
|
114
|
-
<Card className="mt-3">
|
|
104
|
+
<Card className="mt-3" ref={questionAreaRef} tabIndex={-1}>
|
|
115
105
|
<CardHeader className="pb-0">
|
|
116
106
|
<QuestionHeaderBar
|
|
117
107
|
questionNumber={currentIndex + 1}
|
|
118
108
|
totalQuestions={questions.length}
|
|
119
109
|
isFlagged={flaggedUids.has(currentQuestion.uid)}
|
|
120
|
-
onToggleFlag={
|
|
110
|
+
onToggleFlag={toggleCurrentFlag}
|
|
121
111
|
hasMaterials={currentMaterials.length > 0}
|
|
122
|
-
onOpenMaterials={
|
|
112
|
+
onOpenMaterials={openMaterials}
|
|
123
113
|
readOnly={readOnly}
|
|
124
114
|
/>
|
|
125
115
|
</CardHeader>
|
|
@@ -139,16 +129,14 @@ export function QuizSession({
|
|
|
139
129
|
<div className="flex items-center justify-between gap-3 mt-3">
|
|
140
130
|
<Button
|
|
141
131
|
variant="outline"
|
|
142
|
-
disabled={
|
|
143
|
-
onClick={
|
|
132
|
+
disabled={!hasPrevious}
|
|
133
|
+
onClick={goPrevious}
|
|
144
134
|
>
|
|
145
135
|
<ChevronLeft className="size-4 mr-1" />
|
|
146
136
|
Previous
|
|
147
137
|
</Button>
|
|
148
|
-
{
|
|
149
|
-
<Button
|
|
150
|
-
onClick={() => setCurrentIndex((i) => Math.min(i + 1, questions.length - 1))}
|
|
151
|
-
>
|
|
138
|
+
{hasNext ? (
|
|
139
|
+
<Button onClick={goNext}>
|
|
152
140
|
Next
|
|
153
141
|
<ChevronRight className="size-4 ml-1" />
|
|
154
142
|
</Button>
|
|
@@ -168,5 +156,6 @@ export function QuizSession({
|
|
|
168
156
|
questionNumber={currentIndex + 1}
|
|
169
157
|
/>
|
|
170
158
|
</div>
|
|
159
|
+
</SectionShell>
|
|
171
160
|
);
|
|
172
161
|
}
|
|
@@ -45,6 +45,12 @@ export interface QuizSessionProps {
|
|
|
45
45
|
questionMaterials?: QuestionMaterial[];
|
|
46
46
|
/** When true, all inputs are disabled (e.g. after submission) */
|
|
47
47
|
readOnly?: boolean;
|
|
48
|
+
/** Render skeleton placeholders instead of content */
|
|
49
|
+
isLoading?: boolean;
|
|
50
|
+
/** Error message — renders an error state with optional retry */
|
|
51
|
+
error?: string | null;
|
|
52
|
+
/** Called when the user clicks retry in the error state */
|
|
53
|
+
onRetry?: () => void;
|
|
48
54
|
/** CSS class name for the root element */
|
|
49
55
|
className?: string;
|
|
50
56
|
/** Inline styles for the root element */
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import { CheckCircle2, Circle, ChevronRight } from "lucide-react";
|
|
2
|
+
import { AlertCircle, CheckCircle2, Circle, ChevronRight } from "lucide-react";
|
|
3
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
4
|
+
import { EmptyState } from "../../common/empty-state";
|
|
5
|
+
import { Button } from "../../ui/button";
|
|
3
6
|
import { Progress } from "../../ui/progress";
|
|
4
7
|
import { cn } from "../../lib/utils";
|
|
5
8
|
import type { RequirementsChecklistProps } from "./types";
|
|
@@ -14,6 +17,9 @@ export function RequirementsChecklist({
|
|
|
14
17
|
title,
|
|
15
18
|
requirements,
|
|
16
19
|
onRequirementClick,
|
|
20
|
+
isLoading,
|
|
21
|
+
error,
|
|
22
|
+
onRetry,
|
|
17
23
|
className,
|
|
18
24
|
style,
|
|
19
25
|
}: RequirementsChecklistProps) {
|
|
@@ -26,6 +32,40 @@ export function RequirementsChecklist({
|
|
|
26
32
|
? Math.round((completedCount / requirements.length) * 100)
|
|
27
33
|
: 0;
|
|
28
34
|
|
|
35
|
+
if (isLoading) {
|
|
36
|
+
return (
|
|
37
|
+
<div className={cn("space-y-4", className)} style={style}>
|
|
38
|
+
<Skeleton className="h-6 w-48" />
|
|
39
|
+
<Skeleton className="h-2 w-full" />
|
|
40
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
41
|
+
<div key={i} className="flex items-center gap-3">
|
|
42
|
+
<Skeleton className="h-5 w-5 rounded-full" />
|
|
43
|
+
<Skeleton className="h-4 w-3/4" />
|
|
44
|
+
</div>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (error) {
|
|
51
|
+
return (
|
|
52
|
+
<div className={cn("py-12", className)} style={style}>
|
|
53
|
+
<EmptyState
|
|
54
|
+
icon={<AlertCircle className="size-10 text-destructive" />}
|
|
55
|
+
title="Something went wrong"
|
|
56
|
+
description={error}
|
|
57
|
+
action={
|
|
58
|
+
onRetry ? (
|
|
59
|
+
<Button variant="outline" onClick={onRetry}>
|
|
60
|
+
Retry
|
|
61
|
+
</Button>
|
|
62
|
+
) : undefined
|
|
63
|
+
}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
29
69
|
return (
|
|
30
70
|
<div className={cn("flex flex-col gap-4", className)} style={style}>
|
|
31
71
|
{/* Header */}
|