@hydralms/components 0.1.3 → 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/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/timer-display.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 +3 -1
- package/dist/common/pagination.d.ts +26 -0
- package/dist/common/stepper.d.ts +6 -0
- package/dist/common/types.d.ts +38 -0
- package/dist/components.css +1 -1
- package/dist/content/attachment-list.d.ts +6 -0
- package/dist/content/audio-player.d.ts +22 -0
- package/dist/content/code-block.d.ts +30 -0
- package/dist/content/content-block.d.ts +1 -1
- package/dist/content/embed-block.d.ts +28 -0
- package/dist/content/index.d.ts +8 -1
- package/dist/content/types.d.ts +63 -0
- package/dist/curriculum/course-card.d.ts +51 -0
- package/dist/curriculum/curriculum-item.d.ts +1 -1
- 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 +597 -308
- 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 +5 -0
- package/dist/modules/AssignmentModule/types.d.ts +69 -0
- package/dist/modules/CertificateModule/CertificateModule.d.ts +5 -0
- package/dist/modules/CertificateModule/types.d.ts +51 -0
- 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 +5 -0
- package/dist/modules/DiscussionModule/types.d.ts +47 -0
- package/dist/modules/ExamModule/ExamModule.d.ts +5 -0
- package/dist/modules/ExamModule/types.d.ts +55 -0
- 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 +5 -0
- package/dist/modules/GradeCenterModule/types.d.ts +56 -0
- package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
- package/dist/modules/QuizModule/types.d.ts +10 -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 +5 -0
- package/dist/modules/SurveyModule/types.d.ts +51 -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 +18 -0
- package/dist/modules.cjs +1 -0
- package/dist/modules.js +1834 -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 +99 -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 +2 -2
- package/dist/questions/fill-in-the-blank.d.ts +1 -1
- package/dist/questions/hotspot.d.ts +21 -0
- package/dist/questions/index.d.ts +11 -1
- package/dist/questions/inline-choice.d.ts +21 -0
- package/dist/questions/matching.d.ts +22 -0
- package/dist/questions/multiple-choice.d.ts +1 -1
- package/dist/questions/numeric.d.ts +11 -0
- package/dist/questions/ordering.d.ts +12 -0
- package/dist/questions/question-renderer.d.ts +1 -1
- 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/true-false.d.ts +1 -1
- package/dist/questions/types.d.ts +106 -1
- package/dist/questions/use-drag-reorder.d.ts +17 -0
- 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 +13 -5
- 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 +12 -1
- 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 +8 -0
- package/dist/sections/ForumBoard/types.d.ts +78 -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 +12 -1
- package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
- package/dist/sections/RequirementsChecklist/types.d.ts +43 -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 +9 -0
- package/dist/sections/RubricView/types.d.ts +56 -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 +13 -1
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +282 -1786
- package/dist/social/post-card.d.ts +1 -1
- package/dist/tabs-BsfVo2Bl.cjs +173 -0
- package/dist/tabs-BuY1iNJE.js +22305 -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 +5 -0
- package/dist/ui/progress.d.ts +1 -1
- package/dist/ui/rich-text-editor.d.ts +32 -0
- package/dist/ui/rich-text-toolbar.d.ts +8 -0
- package/dist/ui/toast.d.ts +43 -0
- package/dist/utils/array-utils.d.ts +4 -0
- package/dist/utils/debounce.d.ts +5 -1
- 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/pick-palette-color.d.ts +19 -0
- package/dist/utils/shuffle.d.ts +1 -0
- package/dist/utils/string-utils.d.ts +12 -0
- package/dist/video/types.d.ts +15 -0
- package/dist/video/video-bookmark.d.ts +1 -1
- package/dist/video/video-player.d.ts +1 -1
- package/dist/video/video-playlist-item.d.ts +1 -1
- package/dist/withProGate-BWqcKdPM.js +137 -0
- package/dist/withProGate-DX6XqKLp.cjs +1 -0
- package/package.json +40 -137
- 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 +13 -36
- package/src/assessment-toolbar/timer-display.tsx +6 -5
- package/src/assessment-toolbar/types.ts +54 -4
- package/src/assessment-toolbar/use-countdown.ts +153 -0
- package/src/common/empty-state.tsx +1 -0
- package/src/common/index.ts +5 -0
- package/src/common/pagination.tsx +135 -0
- package/src/common/search-input.tsx +8 -6
- package/src/common/stepper.tsx +100 -0
- package/src/common/types.ts +41 -0
- package/src/content/attachment-list.tsx +92 -0
- package/src/content/audio-player.tsx +196 -0
- package/src/content/code-block.tsx +113 -0
- package/src/content/content-block.tsx +68 -2
- package/src/content/embed-block.tsx +78 -0
- package/src/content/file-upload-zone.tsx +11 -6
- package/src/content/index.ts +9 -0
- package/src/content/types.ts +46 -0
- package/src/curriculum/course-card.tsx +199 -0
- package/src/curriculum/curriculum-item.tsx +9 -5
- package/src/curriculum/curriculum-tree.tsx +20 -13
- package/src/curriculum/index.ts +2 -0
- package/src/curriculum/types.ts +2 -2
- package/src/feedback/feedback-banner.tsx +12 -14
- package/src/flashcards/flashcard-deck.tsx +1 -9
- package/src/flashcards/flashcard.tsx +29 -9
- 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 +314 -0
- package/src/modules/AssignmentModule/types.ts +77 -0
- package/src/modules/CertificateModule/CertificateModule.tsx +173 -0
- package/src/modules/CertificateModule/types.ts +49 -0
- package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
- package/src/modules/CourseCatalogModule/types.ts +47 -0
- package/src/modules/CoursePlayer/CoursePlayer.tsx +80 -69
- package/src/modules/DiscussionModule/DiscussionModule.tsx +145 -0
- package/src/modules/DiscussionModule/types.ts +54 -0
- package/src/modules/ExamModule/ExamModule.tsx +151 -0
- package/src/modules/ExamModule/types.ts +57 -0
- package/src/modules/FlashcardLab/FlashcardLab.tsx +39 -21
- package/src/modules/FlashcardLab/types.ts +2 -0
- package/src/modules/GradeCenterModule/GradeCenterModule.tsx +174 -0
- package/src/modules/GradeCenterModule/types.ts +65 -0
- package/src/modules/QuizModule/QuizModule.tsx +58 -178
- package/src/modules/QuizModule/types.ts +10 -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 +185 -0
- package/src/modules/SurveyModule/types.ts +53 -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 +33 -0
- package/src/progress/achievement-badge.tsx +52 -0
- package/src/progress/activity-timeline.tsx +84 -0
- package/src/progress/grade-indicator.tsx +9 -1
- package/src/progress/index.ts +7 -0
- package/src/progress/progress-ring.tsx +2 -1
- package/src/progress/stat-card.tsx +37 -18
- package/src/progress/streak-badge.tsx +35 -0
- package/src/progress/types.ts +103 -0
- package/src/provider/HydraProvider.tsx +15 -6
- package/src/questions/choice.tsx +19 -14
- package/src/questions/confidence-indicator.tsx +107 -0
- package/src/questions/essay.tsx +28 -28
- package/src/questions/fill-in-the-blank.tsx +20 -19
- package/src/questions/hotspot.tsx +154 -0
- package/src/questions/index.ts +18 -0
- package/src/questions/inline-choice.tsx +152 -0
- package/src/questions/matching.tsx +229 -0
- package/src/questions/multiple-choice.tsx +19 -14
- package/src/questions/numeric.tsx +106 -0
- package/src/questions/ordering.tsx +167 -0
- package/src/questions/question-renderer.tsx +24 -2
- package/src/questions/scenario.tsx +140 -0
- package/src/questions/scoring.ts +201 -0
- package/src/questions/spreadsheet.tsx +260 -0
- package/src/questions/true-false.tsx +19 -14
- package/src/questions/types.ts +123 -1
- package/src/questions/use-drag-reorder.ts +80 -0
- package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +66 -23
- package/src/sections/AnnouncementFeed/types.ts +15 -1
- package/src/sections/AssessmentReview/AssessmentReview.tsx +50 -2
- package/src/sections/AssessmentReview/types.ts +6 -0
- package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +44 -6
- package/src/sections/AssignmentSubmission/types.ts +6 -0
- package/src/sections/CertificateViewer/CertificateViewer.tsx +215 -60
- package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
- package/src/sections/CertificateViewer/types.ts +19 -5
- package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
- package/src/sections/CourseCatalog/types.ts +76 -0
- package/src/sections/CourseOutline/CourseOutline.tsx +45 -14
- package/src/sections/CourseOutline/types.ts +6 -0
- package/src/sections/DiscussionThread/DiscussionThread.tsx +55 -11
- 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 +125 -82
- package/src/sections/ExamSession/types.ts +12 -1
- package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
- package/src/sections/FlashcardStudySession/types.ts +6 -0
- package/src/sections/ForumBoard/ForumBoard.tsx +342 -0
- package/src/sections/ForumBoard/types.ts +81 -0
- package/src/sections/GradebookTable/GradebookTable.tsx +55 -2
- 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 +40 -13
- package/src/sections/LessonPage/types.ts +6 -0
- package/src/sections/PracticeQuiz/PracticeQuiz.tsx +119 -98
- package/src/sections/PracticeQuiz/types.ts +6 -0
- package/src/sections/ProgressDashboard/ProgressDashboard.tsx +121 -67
- package/src/sections/ProgressDashboard/types.ts +6 -0
- package/src/sections/QuizSession/QuizSession.tsx +115 -67
- package/src/sections/QuizSession/types.ts +12 -1
- package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +147 -0
- package/src/sections/RequirementsChecklist/types.ts +44 -0
- package/src/sections/ResourceLibrary/ResourceLibrary.tsx +68 -17
- package/src/sections/ResourceLibrary/types.ts +15 -1
- package/src/sections/RubricView/RubricView.tsx +174 -0
- package/src/sections/RubricView/types.ts +58 -0
- package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +58 -23
- 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 +40 -10
- 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 +42 -1
- package/src/social/post-card.tsx +8 -19
- package/src/social/user-avatar.tsx +10 -5
- package/src/styles/globals.css +52 -41
- package/src/ui/badge.tsx +8 -0
- package/src/ui/drawer.tsx +600 -0
- package/src/ui/index.ts +21 -0
- package/src/ui/progress.tsx +4 -0
- package/src/ui/rich-text-editor.tsx +119 -0
- package/src/ui/rich-text-toolbar.tsx +157 -0
- package/src/ui/toast.tsx +170 -0
- package/src/utils/array-utils.ts +17 -0
- package/src/utils/debounce.ts +8 -2
- 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/pick-palette-color.ts +33 -0
- package/src/utils/shuffle.ts +8 -0
- package/src/utils/string-utils.ts +30 -0
- package/src/video/types.ts +16 -0
- package/src/video/video-bookmark.tsx +4 -3
- package/src/video/video-chapter-list.tsx +9 -4
- package/src/video/video-player.tsx +24 -5
- 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,289 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { ArrowLeft, User, BookOpen, Trophy, Award } from "lucide-react";
|
|
3
|
+
import { StudentProfile } from "../../sections/StudentProfile/StudentProfile";
|
|
4
|
+
import { CertificateViewer } from "../../sections/CertificateViewer/CertificateViewer";
|
|
5
|
+
import { AchievementBadge } from "../../progress/achievement-badge";
|
|
6
|
+
import { ProgressRing } from "../../progress/progress-ring";
|
|
7
|
+
import { UserAvatar } from "../../social/user-avatar";
|
|
8
|
+
import { Button } from "../../ui/button";
|
|
9
|
+
import { Card, CardContent } from "../../ui/card";
|
|
10
|
+
import { Badge } from "../../ui/badge";
|
|
11
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../../ui/tabs";
|
|
12
|
+
import { Separator } from "../../ui/separator";
|
|
13
|
+
import { cn } from "../../lib/utils";
|
|
14
|
+
import { formatTimestamp } from "../../utils/format-timestamp";
|
|
15
|
+
import { withProGate } from "../../license/withProGate";
|
|
16
|
+
import type { StudentProfileModuleProps } from "./types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* StudentProfileModule — a tabbed student profile page with certificate drill-down.
|
|
20
|
+
*
|
|
21
|
+
* Uses a tabbed layout with Profile, Courses, Achievements, and Certificates tabs.
|
|
22
|
+
* Clicking a certificate (when certificateData is provided) drills down into a
|
|
23
|
+
* full CertificateViewer with a back button to return to the tabbed view.
|
|
24
|
+
*/
|
|
25
|
+
function StudentProfileModuleBase({
|
|
26
|
+
student,
|
|
27
|
+
enrolledCourses = [],
|
|
28
|
+
achievements = [],
|
|
29
|
+
certificates = [],
|
|
30
|
+
stats,
|
|
31
|
+
onCourseClick,
|
|
32
|
+
certificateData,
|
|
33
|
+
readOnly = false,
|
|
34
|
+
className,
|
|
35
|
+
style,
|
|
36
|
+
}: StudentProfileModuleProps) {
|
|
37
|
+
const [drillDownCertUid, setDrillDownCertUid] = useState<string | null>(null);
|
|
38
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
42
|
+
}, [drillDownCertUid]);
|
|
43
|
+
|
|
44
|
+
function handleCertificateClick(certUid: string) {
|
|
45
|
+
if (readOnly) return;
|
|
46
|
+
if (certificateData?.[certUid]) {
|
|
47
|
+
setDrillDownCertUid(certUid);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleBack() {
|
|
52
|
+
setDrillDownCertUid(null);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const drillDownData = drillDownCertUid
|
|
56
|
+
? certificateData?.[drillDownCertUid]
|
|
57
|
+
: null;
|
|
58
|
+
const drillDownCert = drillDownCertUid
|
|
59
|
+
? certificates.find((c) => c.uid === drillDownCertUid)
|
|
60
|
+
: null;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
ref={contentRef}
|
|
65
|
+
tabIndex={-1}
|
|
66
|
+
className={cn("outline-none", className)}
|
|
67
|
+
style={style}
|
|
68
|
+
>
|
|
69
|
+
{/* Header */}
|
|
70
|
+
<div className="flex items-center gap-4 mb-6">
|
|
71
|
+
<UserAvatar
|
|
72
|
+
displayName={student.displayName}
|
|
73
|
+
avatarUrl={student.avatarUrl}
|
|
74
|
+
size="medium"
|
|
75
|
+
/>
|
|
76
|
+
<div className="flex-1 min-w-0">
|
|
77
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
78
|
+
<h2 className="text-xl font-bold text-foreground">
|
|
79
|
+
{student.displayName}
|
|
80
|
+
</h2>
|
|
81
|
+
{student.role && (
|
|
82
|
+
<Badge variant="secondary">{student.role}</Badge>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
{student.email && (
|
|
86
|
+
<p className="text-sm text-muted-foreground">{student.email}</p>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<Separator className="mb-6" />
|
|
92
|
+
|
|
93
|
+
{/* Drill-down view */}
|
|
94
|
+
{drillDownCert && drillDownData ? (
|
|
95
|
+
<div>
|
|
96
|
+
<Button
|
|
97
|
+
variant="ghost"
|
|
98
|
+
size="sm"
|
|
99
|
+
onClick={handleBack}
|
|
100
|
+
className="mb-4"
|
|
101
|
+
>
|
|
102
|
+
<ArrowLeft className="size-4 mr-1.5" />
|
|
103
|
+
Back to Profile
|
|
104
|
+
</Button>
|
|
105
|
+
<CertificateViewer {...drillDownData} showActions />
|
|
106
|
+
</div>
|
|
107
|
+
) : (
|
|
108
|
+
<Tabs defaultValue="profile">
|
|
109
|
+
<TabsList className="mb-6">
|
|
110
|
+
<TabsTrigger value="profile">
|
|
111
|
+
<User className="size-4 mr-1.5" />
|
|
112
|
+
Profile
|
|
113
|
+
</TabsTrigger>
|
|
114
|
+
<TabsTrigger value="courses">
|
|
115
|
+
<BookOpen className="size-4 mr-1.5" />
|
|
116
|
+
Courses
|
|
117
|
+
</TabsTrigger>
|
|
118
|
+
<TabsTrigger value="achievements">
|
|
119
|
+
<Trophy className="size-4 mr-1.5" />
|
|
120
|
+
Achievements
|
|
121
|
+
</TabsTrigger>
|
|
122
|
+
<TabsTrigger value="certificates">
|
|
123
|
+
<Award className="size-4 mr-1.5" />
|
|
124
|
+
Certificates
|
|
125
|
+
</TabsTrigger>
|
|
126
|
+
</TabsList>
|
|
127
|
+
|
|
128
|
+
{/* Profile tab */}
|
|
129
|
+
<TabsContent value="profile">
|
|
130
|
+
<StudentProfile
|
|
131
|
+
student={student}
|
|
132
|
+
enrolledCourses={enrolledCourses}
|
|
133
|
+
achievements={achievements}
|
|
134
|
+
stats={stats}
|
|
135
|
+
onCourseClick={onCourseClick}
|
|
136
|
+
showCourses={false}
|
|
137
|
+
showAchievements={false}
|
|
138
|
+
readOnly
|
|
139
|
+
/>
|
|
140
|
+
</TabsContent>
|
|
141
|
+
|
|
142
|
+
{/* Courses tab */}
|
|
143
|
+
<TabsContent value="courses">
|
|
144
|
+
{enrolledCourses.length > 0 ? (
|
|
145
|
+
<Card>
|
|
146
|
+
<CardContent className="p-0 divide-y divide-border">
|
|
147
|
+
{enrolledCourses.map((course) => (
|
|
148
|
+
<div
|
|
149
|
+
key={course.uid}
|
|
150
|
+
className={cn(
|
|
151
|
+
"flex items-center gap-3 px-4 py-3 transition-colors",
|
|
152
|
+
onCourseClick && !readOnly && "cursor-pointer hover:bg-muted/50",
|
|
153
|
+
)}
|
|
154
|
+
onClick={
|
|
155
|
+
onCourseClick && !readOnly
|
|
156
|
+
? () => onCourseClick(course.uid)
|
|
157
|
+
: undefined
|
|
158
|
+
}
|
|
159
|
+
>
|
|
160
|
+
<ProgressRing
|
|
161
|
+
value={course.progress}
|
|
162
|
+
size={32}
|
|
163
|
+
strokeWidth={3}
|
|
164
|
+
/>
|
|
165
|
+
<div className="flex-1 min-w-0">
|
|
166
|
+
<p className="text-sm font-medium text-foreground truncate">
|
|
167
|
+
{course.title}
|
|
168
|
+
</p>
|
|
169
|
+
{course.lastAccessedAt && (
|
|
170
|
+
<p className="text-xs text-muted-foreground">
|
|
171
|
+
{formatTimestamp(course.lastAccessedAt)}
|
|
172
|
+
</p>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
|
176
|
+
{Math.round(course.progress)}%
|
|
177
|
+
</span>
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</CardContent>
|
|
181
|
+
</Card>
|
|
182
|
+
) : (
|
|
183
|
+
<div className="py-12 text-center">
|
|
184
|
+
<BookOpen className="size-10 text-muted-foreground mx-auto mb-3" />
|
|
185
|
+
<p className="text-sm font-medium text-foreground">
|
|
186
|
+
No courses yet
|
|
187
|
+
</p>
|
|
188
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
189
|
+
Enrolled courses will appear here.
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</TabsContent>
|
|
194
|
+
|
|
195
|
+
{/* Achievements tab */}
|
|
196
|
+
<TabsContent value="achievements">
|
|
197
|
+
{achievements.length > 0 ? (
|
|
198
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
|
|
199
|
+
{achievements.map((achievement) => (
|
|
200
|
+
<AchievementBadge
|
|
201
|
+
key={achievement.uid}
|
|
202
|
+
title={achievement.name}
|
|
203
|
+
description={achievement.description}
|
|
204
|
+
icon={
|
|
205
|
+
achievement.iconUrl ? (
|
|
206
|
+
<img
|
|
207
|
+
src={achievement.iconUrl}
|
|
208
|
+
alt={achievement.name}
|
|
209
|
+
className="w-12 h-12"
|
|
210
|
+
/>
|
|
211
|
+
) : undefined
|
|
212
|
+
}
|
|
213
|
+
earnedDate={achievement.earnedAt}
|
|
214
|
+
/>
|
|
215
|
+
))}
|
|
216
|
+
</div>
|
|
217
|
+
) : (
|
|
218
|
+
<div className="py-12 text-center">
|
|
219
|
+
<Trophy className="size-10 text-muted-foreground mx-auto mb-3" />
|
|
220
|
+
<p className="text-sm font-medium text-foreground">
|
|
221
|
+
No achievements yet
|
|
222
|
+
</p>
|
|
223
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
224
|
+
Achievements will appear here as they are earned.
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
</TabsContent>
|
|
229
|
+
|
|
230
|
+
{/* Certificates tab */}
|
|
231
|
+
<TabsContent value="certificates">
|
|
232
|
+
{certificates.length > 0 ? (
|
|
233
|
+
<Card>
|
|
234
|
+
<CardContent className="p-0 divide-y divide-border">
|
|
235
|
+
{certificates.map((cert) => (
|
|
236
|
+
<div
|
|
237
|
+
key={cert.uid}
|
|
238
|
+
className={cn(
|
|
239
|
+
"flex items-center gap-3 px-4 py-3 transition-colors",
|
|
240
|
+
certificateData?.[cert.uid] &&
|
|
241
|
+
!readOnly &&
|
|
242
|
+
"cursor-pointer hover:bg-muted/50",
|
|
243
|
+
)}
|
|
244
|
+
onClick={
|
|
245
|
+
certificateData?.[cert.uid] && !readOnly
|
|
246
|
+
? () => handleCertificateClick(cert.uid)
|
|
247
|
+
: undefined
|
|
248
|
+
}
|
|
249
|
+
>
|
|
250
|
+
<div className="w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
|
|
251
|
+
<Award size={20} />
|
|
252
|
+
</div>
|
|
253
|
+
<div className="flex-1 min-w-0">
|
|
254
|
+
<p className="text-sm font-medium text-foreground truncate">
|
|
255
|
+
{cert.courseName}
|
|
256
|
+
</p>
|
|
257
|
+
<p className="text-xs text-muted-foreground">
|
|
258
|
+
Issued{" "}
|
|
259
|
+
{new Date(cert.issuedAt).toLocaleDateString()}
|
|
260
|
+
</p>
|
|
261
|
+
</div>
|
|
262
|
+
{certificateData?.[cert.uid] && (
|
|
263
|
+
<Badge variant="secondary" className="text-xs">
|
|
264
|
+
View
|
|
265
|
+
</Badge>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
))}
|
|
269
|
+
</CardContent>
|
|
270
|
+
</Card>
|
|
271
|
+
) : (
|
|
272
|
+
<div className="py-12 text-center">
|
|
273
|
+
<Award className="size-10 text-muted-foreground mx-auto mb-3" />
|
|
274
|
+
<p className="text-sm font-medium text-foreground">
|
|
275
|
+
No certificates yet
|
|
276
|
+
</p>
|
|
277
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
278
|
+
Certificates will appear here once courses are completed.
|
|
279
|
+
</p>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
</TabsContent>
|
|
283
|
+
</Tabs>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export const StudentProfileModule = withProGate(StudentProfileModuleBase, "StudentProfileModule");
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StudentInfo,
|
|
3
|
+
EnrolledCourse,
|
|
4
|
+
ProfileAchievement,
|
|
5
|
+
ProfileCertificate,
|
|
6
|
+
} from "../../sections/StudentProfile/types";
|
|
7
|
+
import type { CertificateViewerProps } from "../../sections/CertificateViewer/types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* StudentProfileModule — a tabbed student profile page with certificate drill-down.
|
|
11
|
+
*
|
|
12
|
+
* Combines StudentProfile, course list, achievements, and CertificateViewer
|
|
13
|
+
* in a tabbed layout with drill-down into individual certificates.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* <StudentProfileModule
|
|
17
|
+
* student={{ uid: "s1", displayName: "Jane Doe" }}
|
|
18
|
+
* enrolledCourses={courses}
|
|
19
|
+
* achievements={badges}
|
|
20
|
+
* certificates={certs}
|
|
21
|
+
* certificateData={{ cert1: { recipientName: "Jane", courseTitle: "React 101", completionDate: "2025-01-01", organizationName: "Hydra Academy" } }}
|
|
22
|
+
* />
|
|
23
|
+
*/
|
|
24
|
+
export interface StudentProfileModuleProps {
|
|
25
|
+
/** Student information */
|
|
26
|
+
student: StudentInfo;
|
|
27
|
+
/** Enrolled courses */
|
|
28
|
+
enrolledCourses?: EnrolledCourse[];
|
|
29
|
+
/** Earned achievements */
|
|
30
|
+
achievements?: ProfileAchievement[];
|
|
31
|
+
/** Earned certificates */
|
|
32
|
+
certificates?: ProfileCertificate[];
|
|
33
|
+
/** Custom stat cards */
|
|
34
|
+
stats?: { label: string; value: string; icon?: React.ReactNode }[];
|
|
35
|
+
/** Called when a course is clicked */
|
|
36
|
+
onCourseClick?: (courseUid: string) => void;
|
|
37
|
+
/** Certificate data keyed by certificate UID — enables drill-down on click */
|
|
38
|
+
certificateData?: Record<string, CertificateViewerProps>;
|
|
39
|
+
/** When true, disables interactions for preview/demo mode. @default false */
|
|
40
|
+
readOnly?: boolean;
|
|
41
|
+
/** CSS class name for the root element */
|
|
42
|
+
className?: string;
|
|
43
|
+
/** Inline styles for the root element */
|
|
44
|
+
style?: React.CSSProperties;
|
|
45
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
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 { withProGate } from "../../license/withProGate";
|
|
19
|
+
import type { SurveyModuleProps, SurveyModuleResult } from "./types";
|
|
20
|
+
|
|
21
|
+
type InternalStep =
|
|
22
|
+
| { tag: "intro" }
|
|
23
|
+
| { tag: "survey" }
|
|
24
|
+
| { tag: "thankYou"; result: SurveyModuleResult };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* SurveyModule — a complete survey experience with intro, form, and thank-you steps.
|
|
28
|
+
*
|
|
29
|
+
* Steps: Intro → SurveyForm → Thank You with response stats.
|
|
30
|
+
*/
|
|
31
|
+
function SurveyModuleBase({
|
|
32
|
+
title,
|
|
33
|
+
description,
|
|
34
|
+
questions = [],
|
|
35
|
+
requireAll = false,
|
|
36
|
+
showProgress = true,
|
|
37
|
+
thankYouTitle = "Thank You!",
|
|
38
|
+
thankYouMessage,
|
|
39
|
+
onComplete,
|
|
40
|
+
allowRestart = false,
|
|
41
|
+
readOnly = false,
|
|
42
|
+
className,
|
|
43
|
+
style,
|
|
44
|
+
}: SurveyModuleProps) {
|
|
45
|
+
const [step, setStep] = useState<InternalStep>({ tag: "intro" });
|
|
46
|
+
const startTimeRef = useRef<number | null>(null);
|
|
47
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
51
|
+
}, [step.tag]);
|
|
52
|
+
|
|
53
|
+
function handleSubmit(answers: SurveyAnswer[]) {
|
|
54
|
+
const elapsed = startTimeRef.current
|
|
55
|
+
? Math.floor((Date.now() - startTimeRef.current) / 1000)
|
|
56
|
+
: 0;
|
|
57
|
+
const result: SurveyModuleResult = {
|
|
58
|
+
answers,
|
|
59
|
+
totalQuestions: questions.length,
|
|
60
|
+
answeredCount: answers.length,
|
|
61
|
+
timeElapsedSeconds: elapsed,
|
|
62
|
+
};
|
|
63
|
+
setStep({ tag: "thankYou", result });
|
|
64
|
+
onComplete?.(result);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function handleRestart() {
|
|
68
|
+
startTimeRef.current = null;
|
|
69
|
+
setStep({ tag: "intro" });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleStart() {
|
|
73
|
+
startTimeRef.current = Date.now();
|
|
74
|
+
setStep({ tag: "survey" });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Intro Screen ───
|
|
78
|
+
if (step.tag === "intro") {
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
ref={contentRef}
|
|
82
|
+
tabIndex={-1}
|
|
83
|
+
className={cn("max-w-2xl mx-auto outline-none", className)}
|
|
84
|
+
style={style}
|
|
85
|
+
>
|
|
86
|
+
<Card>
|
|
87
|
+
<CardContent className="pt-8 pb-8 text-center">
|
|
88
|
+
<div className="mx-auto mb-4 w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
|
|
89
|
+
<ClipboardList className="size-7 text-primary" />
|
|
90
|
+
</div>
|
|
91
|
+
<h2 className="text-2xl font-bold text-foreground mb-2">{title}</h2>
|
|
92
|
+
{description && (
|
|
93
|
+
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
|
|
94
|
+
{description}
|
|
95
|
+
</p>
|
|
96
|
+
)}
|
|
97
|
+
<div className="flex flex-wrap justify-center gap-2 mb-8">
|
|
98
|
+
<Badge variant="outline" className="gap-1.5">
|
|
99
|
+
<HelpCircle className="size-3.5" />
|
|
100
|
+
{questions.length} questions
|
|
101
|
+
</Badge>
|
|
102
|
+
</div>
|
|
103
|
+
<Button size="lg" onClick={handleStart} disabled={readOnly}>
|
|
104
|
+
<Play className="size-4 mr-2" />
|
|
105
|
+
Begin Survey
|
|
106
|
+
</Button>
|
|
107
|
+
</CardContent>
|
|
108
|
+
</Card>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Survey Screen ───
|
|
114
|
+
if (step.tag === "survey") {
|
|
115
|
+
return (
|
|
116
|
+
<div
|
|
117
|
+
ref={contentRef}
|
|
118
|
+
tabIndex={-1}
|
|
119
|
+
className={cn("outline-none", className)}
|
|
120
|
+
style={style}
|
|
121
|
+
>
|
|
122
|
+
<SurveyForm
|
|
123
|
+
title={title}
|
|
124
|
+
questions={questions}
|
|
125
|
+
requireAll={requireAll}
|
|
126
|
+
showProgress={showProgress}
|
|
127
|
+
onSubmit={handleSubmit}
|
|
128
|
+
readOnly={readOnly}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Thank You Screen ───
|
|
135
|
+
const { result } = step;
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
ref={contentRef}
|
|
140
|
+
tabIndex={-1}
|
|
141
|
+
className={cn("max-w-2xl mx-auto outline-none", className)}
|
|
142
|
+
style={style}
|
|
143
|
+
>
|
|
144
|
+
<Card>
|
|
145
|
+
<CardContent className="pt-8 pb-8 text-center">
|
|
146
|
+
<div className="mx-auto mb-4 w-14 h-14 rounded-full bg-success/10 flex items-center justify-center">
|
|
147
|
+
<CheckCircle2 className="size-7 text-success" />
|
|
148
|
+
</div>
|
|
149
|
+
<h2 className="text-2xl font-bold text-foreground mb-2">
|
|
150
|
+
{thankYouTitle}
|
|
151
|
+
</h2>
|
|
152
|
+
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
|
|
153
|
+
{thankYouMessage ?? "Your responses have been recorded. Thank you for your feedback!"}
|
|
154
|
+
</p>
|
|
155
|
+
|
|
156
|
+
{/* Stats */}
|
|
157
|
+
<div className="grid grid-cols-2 gap-3 max-w-sm mx-auto mb-6">
|
|
158
|
+
<StatCard
|
|
159
|
+
icon={<HelpCircle />}
|
|
160
|
+
label="Answered"
|
|
161
|
+
description="Questions completed"
|
|
162
|
+
value={`${result.answeredCount}/${result.totalQuestions}`}
|
|
163
|
+
/>
|
|
164
|
+
<StatCard
|
|
165
|
+
icon={<Clock />}
|
|
166
|
+
label="Time"
|
|
167
|
+
description="Total elapsed"
|
|
168
|
+
value={formatDuration(result.timeElapsedSeconds)}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{/* Actions */}
|
|
173
|
+
{allowRestart && (
|
|
174
|
+
<Button variant="outline" onClick={handleRestart} disabled={readOnly}>
|
|
175
|
+
<RotateCcw className="size-4 mr-2" />
|
|
176
|
+
Take Again
|
|
177
|
+
</Button>
|
|
178
|
+
)}
|
|
179
|
+
</CardContent>
|
|
180
|
+
</Card>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const SurveyModule = withProGate(SurveyModuleBase, "SurveyModule");
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
/** When true, disables interactions for preview/demo mode. @default false */
|
|
37
|
+
readOnly?: boolean;
|
|
38
|
+
/** CSS class name for the root element */
|
|
39
|
+
className?: string;
|
|
40
|
+
/** Inline styles for the root element */
|
|
41
|
+
style?: React.CSSProperties;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SurveyModuleResult {
|
|
45
|
+
/** The user's submitted answers */
|
|
46
|
+
answers: SurveyAnswer[];
|
|
47
|
+
/** Total number of questions */
|
|
48
|
+
totalQuestions: number;
|
|
49
|
+
/** Number of questions answered */
|
|
50
|
+
answeredCount: number;
|
|
51
|
+
/** Total time taken in seconds */
|
|
52
|
+
timeElapsedSeconds: number;
|
|
53
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { HelpCircle, Clock, CheckCircle2, Play } from "lucide-react";
|
|
3
|
+
import { Button } from "../../ui/button";
|
|
4
|
+
import { Badge } from "../../ui/badge";
|
|
5
|
+
import { Card, CardContent } from "../../ui/card";
|
|
6
|
+
import { formatDuration } from "../../utils/format-duration";
|
|
7
|
+
|
|
8
|
+
export interface AssessmentIntroProps {
|
|
9
|
+
icon: ReactNode;
|
|
10
|
+
title: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
questionCount: number;
|
|
13
|
+
timeLimitSeconds?: number;
|
|
14
|
+
passingScore?: number;
|
|
15
|
+
startLabel: string;
|
|
16
|
+
onStart: () => void;
|
|
17
|
+
/** Extra content rendered between the description and metadata badges. */
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
/** When true, disables interactions for preview/demo mode. @default false */
|
|
20
|
+
readOnly?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function AssessmentIntro({
|
|
24
|
+
icon,
|
|
25
|
+
title,
|
|
26
|
+
description,
|
|
27
|
+
questionCount,
|
|
28
|
+
timeLimitSeconds,
|
|
29
|
+
passingScore,
|
|
30
|
+
startLabel,
|
|
31
|
+
onStart,
|
|
32
|
+
children,
|
|
33
|
+
readOnly = false,
|
|
34
|
+
}: AssessmentIntroProps) {
|
|
35
|
+
return (
|
|
36
|
+
<Card>
|
|
37
|
+
<CardContent className="pt-8 pb-8 text-center">
|
|
38
|
+
<div className="mx-auto mb-4 w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
|
|
39
|
+
{icon}
|
|
40
|
+
</div>
|
|
41
|
+
<h2 className="text-2xl font-bold text-foreground mb-2">{title}</h2>
|
|
42
|
+
{description && (
|
|
43
|
+
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
|
|
44
|
+
{description}
|
|
45
|
+
</p>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
{children}
|
|
49
|
+
|
|
50
|
+
<div className="flex flex-wrap justify-center gap-2 mb-8">
|
|
51
|
+
<Badge variant="outline" className="gap-1.5">
|
|
52
|
+
<HelpCircle className="size-3.5" />
|
|
53
|
+
{questionCount} questions
|
|
54
|
+
</Badge>
|
|
55
|
+
{timeLimitSeconds != null && (
|
|
56
|
+
<Badge variant="outline" className="gap-1.5">
|
|
57
|
+
<Clock className="size-3.5" />
|
|
58
|
+
{formatDuration(timeLimitSeconds)} time limit
|
|
59
|
+
</Badge>
|
|
60
|
+
)}
|
|
61
|
+
{passingScore !== undefined && (
|
|
62
|
+
<Badge variant="outline" className="gap-1.5">
|
|
63
|
+
<CheckCircle2 className="size-3.5" />
|
|
64
|
+
{passingScore}% to pass
|
|
65
|
+
</Badge>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
<Button size="lg" onClick={onStart} disabled={readOnly}>
|
|
69
|
+
<Play className="size-4 mr-2" />
|
|
70
|
+
{startLabel}
|
|
71
|
+
</Button>
|
|
72
|
+
</CardContent>
|
|
73
|
+
</Card>
|
|
74
|
+
);
|
|
75
|
+
}
|