@hydralms/components 0.2.0 → 0.3.1
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-BPsZBaJj.cjs +1 -0
- package/dist/StudentProfile-Cw2p-RZn.js +3273 -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 +495 -439
- 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 +6 -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 +1267 -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/AdaptiveLearningPath/AdaptiveLearningPath.d.ts +5 -0
- package/dist/sections/AdaptiveLearningPath/path-connector.d.ts +8 -0
- package/dist/sections/AdaptiveLearningPath/path-milestone-marker.d.ts +7 -0
- package/dist/sections/AdaptiveLearningPath/path-node-card.d.ts +10 -0
- package/dist/sections/AdaptiveLearningPath/path-skill-bar.d.ts +8 -0
- package/dist/sections/AdaptiveLearningPath/types.d.ts +136 -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 +6 -0
- package/dist/sections/ContentAuthoringStudio/ContentAuthoringStudio.d.ts +5 -0
- package/dist/sections/ContentAuthoringStudio/block-editor-item.d.ts +14 -0
- package/dist/sections/ContentAuthoringStudio/block-type-picker.d.ts +12 -0
- package/dist/sections/ContentAuthoringStudio/types.d.ts +67 -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 +10 -0
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +1361 -307
- 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-BJdu1T9Y.cjs +2 -0
- package/dist/withProGate-BvFc7Jwy.js +4975 -0
- package/package.json +57 -226
- 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 +34 -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 +39 -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 +14 -2
- 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/AdaptiveLearningPath/AdaptiveLearningPath.tsx +251 -0
- package/src/sections/AdaptiveLearningPath/path-connector.tsx +27 -0
- package/src/sections/AdaptiveLearningPath/path-milestone-marker.tsx +50 -0
- package/src/sections/AdaptiveLearningPath/path-node-card.tsx +166 -0
- package/src/sections/AdaptiveLearningPath/path-skill-bar.tsx +49 -0
- package/src/sections/AdaptiveLearningPath/types.ts +159 -0
- 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/ContentAuthoringStudio/ContentAuthoringStudio.tsx +289 -0
- package/src/sections/ContentAuthoringStudio/block-editor-item.tsx +487 -0
- package/src/sections/ContentAuthoringStudio/block-type-picker.tsx +123 -0
- package/src/sections/ContentAuthoringStudio/types.ts +67 -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 +67 -7
- 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 +34 -6
- 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 +40 -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 +27 -6
- package/dist/ForumBoard-CHXU3mjC.js +0 -2207
- package/dist/ForumBoard-d1w5-r6n.cjs +0 -1
- package/dist/tabs-DRM2Iq_J.cjs +0 -172
- package/dist/tabs-Wf3h_Cx3.js +0 -21580
|
@@ -18,6 +18,12 @@ export interface RequirementsChecklistProps {
|
|
|
18
18
|
requirements: Requirement[];
|
|
19
19
|
/** Called when the user clicks an incomplete requirement */
|
|
20
20
|
onRequirementClick?: (uid: string) => void;
|
|
21
|
+
/** Render skeleton placeholders instead of content */
|
|
22
|
+
isLoading?: boolean;
|
|
23
|
+
/** Error message — renders an error state with optional retry */
|
|
24
|
+
error?: string | null;
|
|
25
|
+
/** Called when the user clicks retry in the error state */
|
|
26
|
+
onRetry?: () => void;
|
|
21
27
|
/** CSS class name for the root element */
|
|
22
28
|
className?: string;
|
|
23
29
|
/** Inline styles for the root element */
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { useMemo, useState } from "react";
|
|
2
|
-
import { Download, Grid, List as ListIcon } from "lucide-react";
|
|
2
|
+
import { AlertCircle, Download, Grid, List as ListIcon } from "lucide-react";
|
|
3
3
|
import { LearningObjectIcon } from "../../curriculum";
|
|
4
4
|
import { SearchInput, EmptyState } from "../../common";
|
|
5
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
5
6
|
import { Button } from "../../ui/button";
|
|
6
7
|
import { Card, CardContent } from "../../ui/card";
|
|
7
8
|
import { Tabs, TabsList, TabsTrigger } from "../../ui/tabs";
|
|
8
9
|
import { Tooltip, TooltipTrigger, TooltipContent } from "../../ui/tooltip";
|
|
10
|
+
import { Pagination } from "../../common/pagination";
|
|
9
11
|
import type { ResourceLibraryProps, Resource } from "./types";
|
|
10
12
|
import { cn } from "../../lib/utils";
|
|
11
13
|
import { formatFileSize } from "../../utils/format-file-size";
|
|
@@ -30,6 +32,13 @@ export function ResourceLibrary({
|
|
|
30
32
|
showSearch = true,
|
|
31
33
|
emptyMessage = "No resources found",
|
|
32
34
|
readOnly = false,
|
|
35
|
+
isLoading,
|
|
36
|
+
error,
|
|
37
|
+
onRetry,
|
|
38
|
+
pageSize,
|
|
39
|
+
currentPage = 1,
|
|
40
|
+
totalItems,
|
|
41
|
+
onPageChange,
|
|
33
42
|
className,
|
|
34
43
|
style,
|
|
35
44
|
}: ResourceLibraryProps) {
|
|
@@ -53,6 +62,38 @@ export function ResourceLibrary({
|
|
|
53
62
|
return result;
|
|
54
63
|
}, [resources, activeCategoryUid, searchQuery]);
|
|
55
64
|
|
|
65
|
+
if (isLoading) {
|
|
66
|
+
return (
|
|
67
|
+
<div className={cn("space-y-4", className)} style={style}>
|
|
68
|
+
<Skeleton className="h-9 w-full" />
|
|
69
|
+
<div className="grid grid-cols-3 gap-4">
|
|
70
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
71
|
+
<Skeleton key={i} className="h-32 w-full rounded-lg" />
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (error) {
|
|
79
|
+
return (
|
|
80
|
+
<div className={cn("py-12", className)} style={style}>
|
|
81
|
+
<EmptyState
|
|
82
|
+
icon={<AlertCircle className="size-10 text-destructive" />}
|
|
83
|
+
title="Something went wrong"
|
|
84
|
+
description={error}
|
|
85
|
+
action={
|
|
86
|
+
onRetry ? (
|
|
87
|
+
<Button variant="outline" onClick={onRetry}>
|
|
88
|
+
Retry
|
|
89
|
+
</Button>
|
|
90
|
+
) : undefined
|
|
91
|
+
}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
56
97
|
function renderResource(resource: Resource) {
|
|
57
98
|
const iconType = TYPE_TO_ICON[resource.type] ?? "document";
|
|
58
99
|
|
|
@@ -61,10 +102,10 @@ export function ResourceLibrary({
|
|
|
61
102
|
key={resource.uid}
|
|
62
103
|
className={cn(
|
|
63
104
|
"flex items-center gap-3 px-3 py-2",
|
|
64
|
-
!readOnly && "cursor-pointer hover:bg-muted",
|
|
65
|
-
readOnly && "opacity-70",
|
|
105
|
+
!readOnly && onResourceClick && "cursor-pointer hover:bg-muted",
|
|
106
|
+
(readOnly || !onResourceClick) && "opacity-70",
|
|
66
107
|
)}
|
|
67
|
-
onClick={() => !readOnly && onResourceClick(resource)}
|
|
108
|
+
onClick={() => !readOnly && onResourceClick?.(resource)}
|
|
68
109
|
>
|
|
69
110
|
<div className="min-w-10">
|
|
70
111
|
<LearningObjectIcon type={iconType} size={20} />
|
|
@@ -173,18 +214,24 @@ export function ResourceLibrary({
|
|
|
173
214
|
<EmptyState title={emptyMessage} description="Try adjusting your search or filter." />
|
|
174
215
|
) : viewMode === "list" ? (
|
|
175
216
|
<Card>
|
|
176
|
-
{
|
|
217
|
+
{(onPageChange && pageSize
|
|
218
|
+
? filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize)
|
|
219
|
+
: filtered
|
|
220
|
+
).map(renderResource)}
|
|
177
221
|
</Card>
|
|
178
222
|
) : (
|
|
179
223
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-2">
|
|
180
|
-
{
|
|
224
|
+
{(onPageChange && pageSize
|
|
225
|
+
? filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize)
|
|
226
|
+
: filtered
|
|
227
|
+
).map((resource) => (
|
|
181
228
|
<Card
|
|
182
229
|
key={resource.uid}
|
|
183
230
|
className={cn(
|
|
184
231
|
"transition-colors",
|
|
185
|
-
!readOnly && "cursor-pointer hover:border-primary",
|
|
232
|
+
!readOnly && onResourceClick && "cursor-pointer hover:border-primary",
|
|
186
233
|
)}
|
|
187
|
-
onClick={() => !readOnly && onResourceClick(resource)}
|
|
234
|
+
onClick={() => !readOnly && onResourceClick?.(resource)}
|
|
188
235
|
>
|
|
189
236
|
<CardContent className="pt-4 pb-4">
|
|
190
237
|
<div className="flex gap-1 items-center mb-1">
|
|
@@ -208,6 +255,15 @@ export function ResourceLibrary({
|
|
|
208
255
|
))}
|
|
209
256
|
</div>
|
|
210
257
|
)}
|
|
258
|
+
|
|
259
|
+
{onPageChange && pageSize && filtered.length > 0 && (
|
|
260
|
+
<Pagination
|
|
261
|
+
currentPage={currentPage}
|
|
262
|
+
totalPages={Math.ceil((totalItems ?? filtered.length) / pageSize)}
|
|
263
|
+
onPageChange={onPageChange}
|
|
264
|
+
className="mt-4"
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
211
267
|
</div>
|
|
212
268
|
);
|
|
213
269
|
}
|
|
@@ -18,7 +18,7 @@ export interface ResourceLibraryProps {
|
|
|
18
18
|
/** Optional categories for tab filtering */
|
|
19
19
|
categories?: { uid: string; label: string }[];
|
|
20
20
|
/** Called when the user clicks a resource */
|
|
21
|
-
onResourceClick
|
|
21
|
+
onResourceClick?: (resource: Resource) => void;
|
|
22
22
|
/** Called when the user downloads a resource */
|
|
23
23
|
onDownload?: (resource: Resource) => void;
|
|
24
24
|
/** Layout view mode */
|
|
@@ -31,6 +31,20 @@ export interface ResourceLibraryProps {
|
|
|
31
31
|
emptyMessage?: string;
|
|
32
32
|
/** When true, disables interactions */
|
|
33
33
|
readOnly?: boolean;
|
|
34
|
+
/** Render skeleton placeholders instead of content */
|
|
35
|
+
isLoading?: boolean;
|
|
36
|
+
/** Error message — renders an error state with optional retry */
|
|
37
|
+
error?: string | null;
|
|
38
|
+
/** Called when the user clicks retry in the error state */
|
|
39
|
+
onRetry?: () => void;
|
|
40
|
+
/** Number of items per page (enables pagination when set with onPageChange) */
|
|
41
|
+
pageSize?: number;
|
|
42
|
+
/** Current page (1-indexed) */
|
|
43
|
+
currentPage?: number;
|
|
44
|
+
/** Total number of items (for server-side pagination) */
|
|
45
|
+
totalItems?: number;
|
|
46
|
+
/** Called when the user navigates to a different page */
|
|
47
|
+
onPageChange?: (page: number) => void;
|
|
34
48
|
/** CSS class name for the root element */
|
|
35
49
|
className?: string;
|
|
36
50
|
/** Inline styles for the root element */
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { CheckCircle2 } from "lucide-react";
|
|
1
|
+
import { AlertCircle, CheckCircle2 } from "lucide-react";
|
|
2
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
3
|
+
import { EmptyState } from "../../common/empty-state";
|
|
4
|
+
import { Button } from "../../ui/button";
|
|
2
5
|
import { Card, CardContent } from "../../ui/card";
|
|
3
6
|
import {
|
|
4
7
|
Table,
|
|
@@ -26,11 +29,44 @@ export function RubricView({
|
|
|
26
29
|
totalScore,
|
|
27
30
|
maxScore,
|
|
28
31
|
feedback,
|
|
32
|
+
isLoading,
|
|
33
|
+
error,
|
|
34
|
+
onRetry,
|
|
29
35
|
className,
|
|
30
36
|
style,
|
|
31
37
|
}: RubricViewProps) {
|
|
32
38
|
const isScored = selectedLevels && Object.keys(selectedLevels).length > 0;
|
|
33
39
|
|
|
40
|
+
if (isLoading) {
|
|
41
|
+
return (
|
|
42
|
+
<div className={cn("space-y-4", className)} style={style}>
|
|
43
|
+
<Skeleton className="h-10 w-full" />
|
|
44
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
45
|
+
<Skeleton key={i} className="h-16 w-full" />
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (error) {
|
|
52
|
+
return (
|
|
53
|
+
<div className={cn("py-12", className)} style={style}>
|
|
54
|
+
<EmptyState
|
|
55
|
+
icon={<AlertCircle className="size-10 text-destructive" />}
|
|
56
|
+
title="Something went wrong"
|
|
57
|
+
description={error}
|
|
58
|
+
action={
|
|
59
|
+
onRetry ? (
|
|
60
|
+
<Button variant="outline" onClick={onRetry}>
|
|
61
|
+
Retry
|
|
62
|
+
</Button>
|
|
63
|
+
) : undefined
|
|
64
|
+
}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
34
70
|
return (
|
|
35
71
|
<div className={cn("flex flex-col gap-4", className)} style={style}>
|
|
36
72
|
{/* Score header */}
|
|
@@ -23,6 +23,12 @@ export interface RubricViewProps {
|
|
|
23
23
|
maxScore?: number;
|
|
24
24
|
/** Instructor feedback text */
|
|
25
25
|
feedback?: string;
|
|
26
|
+
/** Render skeleton placeholders instead of content */
|
|
27
|
+
isLoading?: boolean;
|
|
28
|
+
/** Error message — renders an error state with optional retry */
|
|
29
|
+
error?: string | null;
|
|
30
|
+
/** Called when the user clicks retry in the error state */
|
|
31
|
+
onRetry?: () => void;
|
|
26
32
|
/** CSS class name for the root element */
|
|
27
33
|
className?: string;
|
|
28
34
|
/** Inline styles for the root element */
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { QuestionRenderer } from "../../questions";
|
|
3
3
|
import type { SessionAnswer } from "../../questions/types";
|
|
4
|
+
|
|
5
|
+
const EMPTY_ANSWERS: SessionAnswer[] = [];
|
|
4
6
|
import { Button } from "../../ui/button";
|
|
5
7
|
import { Card, CardContent } from "../../ui/card";
|
|
6
8
|
import { Separator } from "../../ui/separator";
|
|
9
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
7
10
|
import { cn } from "../../lib/utils";
|
|
11
|
+
import { mergeSessionAnswers } from "../_shared/merge-answers";
|
|
12
|
+
import { SectionShell } from "../_shared/section-shell";
|
|
8
13
|
import type { ScrollableQuizProps } from "./types";
|
|
9
14
|
|
|
10
15
|
export function ScrollableQuiz({
|
|
@@ -17,6 +22,9 @@ export function ScrollableQuiz({
|
|
|
17
22
|
questionGroups,
|
|
18
23
|
isSubmitting = false,
|
|
19
24
|
readOnly = false,
|
|
25
|
+
isLoading,
|
|
26
|
+
error,
|
|
27
|
+
onRetry,
|
|
20
28
|
className,
|
|
21
29
|
style,
|
|
22
30
|
}: ScrollableQuizProps) {
|
|
@@ -60,19 +68,17 @@ export function ScrollableQuiz({
|
|
|
60
68
|
else questionRefs.current.delete(uid);
|
|
61
69
|
}, []);
|
|
62
70
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
75
|
-
}
|
|
71
|
+
const onAnswerChangeRef = useRef(onAnswerChange);
|
|
72
|
+
onAnswerChangeRef.current = onAnswerChange;
|
|
73
|
+
|
|
74
|
+
const handleAnswer = useCallback(
|
|
75
|
+
(questionUid: string, rawAnswers: { uid: string; content?: string }[]) => {
|
|
76
|
+
setSessionAnswers((prev) =>
|
|
77
|
+
mergeSessionAnswers(prev, questionUid, rawAnswers, onAnswerChangeRef.current),
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
[],
|
|
81
|
+
);
|
|
76
82
|
|
|
77
83
|
function scrollToQuestion(uid: string) {
|
|
78
84
|
questionRefs.current.get(uid)?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
@@ -97,7 +103,21 @@ export function ScrollableQuiz({
|
|
|
97
103
|
let globalIndex = 0;
|
|
98
104
|
|
|
99
105
|
return (
|
|
100
|
-
<
|
|
106
|
+
<SectionShell
|
|
107
|
+
isLoading={isLoading}
|
|
108
|
+
error={error}
|
|
109
|
+
onRetry={onRetry}
|
|
110
|
+
className={className}
|
|
111
|
+
style={style}
|
|
112
|
+
skeleton={
|
|
113
|
+
<>
|
|
114
|
+
<Skeleton className="h-32 w-full" />
|
|
115
|
+
<Skeleton className="h-32 w-full" />
|
|
116
|
+
<Skeleton className="h-32 w-full" />
|
|
117
|
+
</>
|
|
118
|
+
}
|
|
119
|
+
>
|
|
120
|
+
<div className="flex gap-3">
|
|
101
121
|
{/* Main content */}
|
|
102
122
|
<div className="flex-1 min-w-0">
|
|
103
123
|
{orderedQuestions.map((group, gi) => (
|
|
@@ -124,7 +144,7 @@ export function ScrollableQuiz({
|
|
|
124
144
|
)}
|
|
125
145
|
<QuestionRenderer
|
|
126
146
|
question={q}
|
|
127
|
-
sessionAnswers={answersByQuestion.get(q.uid) ??
|
|
147
|
+
sessionAnswers={answersByQuestion.get(q.uid) ?? EMPTY_ANSWERS}
|
|
128
148
|
onAnswer={(answers) => handleAnswer(q.uid, answers)}
|
|
129
149
|
readOnly={readOnly}
|
|
130
150
|
/>
|
|
@@ -180,5 +200,6 @@ export function ScrollableQuiz({
|
|
|
180
200
|
</Card>
|
|
181
201
|
)}
|
|
182
202
|
</div>
|
|
203
|
+
</SectionShell>
|
|
183
204
|
);
|
|
184
205
|
}
|
|
@@ -33,6 +33,12 @@ export interface ScrollableQuizProps {
|
|
|
33
33
|
isSubmitting?: boolean;
|
|
34
34
|
/** When true, all inputs are disabled */
|
|
35
35
|
readOnly?: boolean;
|
|
36
|
+
/** Render skeleton placeholders instead of content */
|
|
37
|
+
isLoading?: boolean;
|
|
38
|
+
/** Error message — renders an error state with optional retry */
|
|
39
|
+
error?: string | null;
|
|
40
|
+
/** Called when the user clicks retry in the error state */
|
|
41
|
+
onRetry?: () => void;
|
|
36
42
|
/** CSS class name for the root element */
|
|
37
43
|
className?: string;
|
|
38
44
|
/** Inline styles for the root element */
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { Award, BookOpen, GraduationCap, Calendar } from "lucide-react";
|
|
2
|
+
import { UserAvatar } from "../../social/user-avatar";
|
|
3
|
+
import { StatCard } from "../../progress/stat-card";
|
|
4
|
+
import { AchievementBadge } from "../../progress/achievement-badge";
|
|
5
|
+
import { ProgressRing } from "../../progress/progress-ring";
|
|
6
|
+
import { Card, CardContent } from "../../ui/card";
|
|
7
|
+
import { Badge } from "../../ui/badge";
|
|
8
|
+
import { Button } from "../../ui/button";
|
|
9
|
+
import { Separator } from "../../ui/separator";
|
|
10
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
11
|
+
import { EmptyState } from "../../common/empty-state";
|
|
12
|
+
import { SectionShell } from "../_shared/section-shell";
|
|
13
|
+
import { cn } from "../../lib/utils";
|
|
14
|
+
import { formatTimestamp } from "../../utils/format-timestamp";
|
|
15
|
+
import type { StudentProfileProps } from "./types";
|
|
16
|
+
|
|
17
|
+
export function StudentProfile({
|
|
18
|
+
student,
|
|
19
|
+
enrolledCourses = [],
|
|
20
|
+
achievements = [],
|
|
21
|
+
certificates = [],
|
|
22
|
+
stats,
|
|
23
|
+
showCourses = true,
|
|
24
|
+
showAchievements = true,
|
|
25
|
+
onCourseClick,
|
|
26
|
+
onCertificateClick,
|
|
27
|
+
readOnly = false,
|
|
28
|
+
isLoading,
|
|
29
|
+
error,
|
|
30
|
+
onRetry,
|
|
31
|
+
className,
|
|
32
|
+
style,
|
|
33
|
+
}: StudentProfileProps) {
|
|
34
|
+
const completedCourses = enrolledCourses.filter((c) => c.progress >= 100);
|
|
35
|
+
|
|
36
|
+
const defaultStats = [
|
|
37
|
+
{
|
|
38
|
+
label: "Enrolled",
|
|
39
|
+
value: String(enrolledCourses.length),
|
|
40
|
+
icon: <BookOpen size={24} />,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: "Completed",
|
|
44
|
+
value: String(completedCourses.length),
|
|
45
|
+
icon: <GraduationCap size={24} />,
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const displayStats = stats ?? defaultStats;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<SectionShell
|
|
53
|
+
isLoading={isLoading}
|
|
54
|
+
error={error}
|
|
55
|
+
onRetry={onRetry}
|
|
56
|
+
className={className}
|
|
57
|
+
style={style}
|
|
58
|
+
skeleton={
|
|
59
|
+
<>
|
|
60
|
+
{/* Profile header skeleton */}
|
|
61
|
+
<Card>
|
|
62
|
+
<CardContent className="p-4 flex items-center gap-4">
|
|
63
|
+
<Skeleton className="size-12 rounded-full" />
|
|
64
|
+
<div className="flex-1 space-y-2">
|
|
65
|
+
<Skeleton className="h-5 w-40" />
|
|
66
|
+
<Skeleton className="h-4 w-56" />
|
|
67
|
+
</div>
|
|
68
|
+
</CardContent>
|
|
69
|
+
</Card>
|
|
70
|
+
{/* Stat skeletons */}
|
|
71
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
72
|
+
<Skeleton className="h-24" />
|
|
73
|
+
<Skeleton className="h-24" />
|
|
74
|
+
<Skeleton className="h-24" />
|
|
75
|
+
<Skeleton className="h-24" />
|
|
76
|
+
</div>
|
|
77
|
+
{/* List skeletons */}
|
|
78
|
+
<Skeleton className="h-8 w-full" />
|
|
79
|
+
<Skeleton className="h-8 w-full" />
|
|
80
|
+
<Skeleton className="h-8 w-full" />
|
|
81
|
+
</>
|
|
82
|
+
}
|
|
83
|
+
>
|
|
84
|
+
<div className={cn("space-y-4", className)} style={style}>
|
|
85
|
+
{/* Profile Header */}
|
|
86
|
+
<Card>
|
|
87
|
+
<CardContent className="p-4 flex items-start gap-4">
|
|
88
|
+
<UserAvatar
|
|
89
|
+
displayName={student.displayName}
|
|
90
|
+
avatarUrl={student.avatarUrl}
|
|
91
|
+
size="large"
|
|
92
|
+
/>
|
|
93
|
+
<div className="flex-1 min-w-0">
|
|
94
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
95
|
+
<h2 className="text-xl font-bold text-foreground">
|
|
96
|
+
{student.displayName}
|
|
97
|
+
</h2>
|
|
98
|
+
{student.role && (
|
|
99
|
+
<Badge variant="secondary">{student.role}</Badge>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
{student.email && (
|
|
103
|
+
<p className="text-sm text-muted-foreground">{student.email}</p>
|
|
104
|
+
)}
|
|
105
|
+
{student.bio && (
|
|
106
|
+
<p className="text-sm text-foreground mt-1">{student.bio}</p>
|
|
107
|
+
)}
|
|
108
|
+
{student.joinedAt && (
|
|
109
|
+
<p className="text-xs text-muted-foreground mt-1 flex items-center gap-1">
|
|
110
|
+
<Calendar size={12} />
|
|
111
|
+
Joined {new Date(student.joinedAt).toLocaleDateString()}
|
|
112
|
+
</p>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
</CardContent>
|
|
116
|
+
</Card>
|
|
117
|
+
|
|
118
|
+
{/* Stats Row */}
|
|
119
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
120
|
+
{displayStats.map((stat) => (
|
|
121
|
+
<StatCard
|
|
122
|
+
key={stat.label}
|
|
123
|
+
icon={stat.icon}
|
|
124
|
+
label={stat.label}
|
|
125
|
+
value={stat.value}
|
|
126
|
+
/>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Enrolled Courses */}
|
|
131
|
+
{showCourses && (
|
|
132
|
+
<>
|
|
133
|
+
<Separator />
|
|
134
|
+
<div className="flex items-center gap-2 mb-2">
|
|
135
|
+
<p className="text-lg font-semibold text-foreground">Enrolled Courses</p>
|
|
136
|
+
<Badge variant="secondary">{enrolledCourses.length}</Badge>
|
|
137
|
+
</div>
|
|
138
|
+
{enrolledCourses.length > 0 ? (
|
|
139
|
+
<Card>
|
|
140
|
+
<CardContent className="p-0 divide-y divide-border">
|
|
141
|
+
{enrolledCourses.map((course) => (
|
|
142
|
+
<div
|
|
143
|
+
key={course.uid}
|
|
144
|
+
className={cn(
|
|
145
|
+
"flex items-center gap-3 px-4 py-3 transition-colors",
|
|
146
|
+
!readOnly && onCourseClick && "cursor-pointer hover:bg-muted/50",
|
|
147
|
+
)}
|
|
148
|
+
onClick={
|
|
149
|
+
!readOnly && onCourseClick
|
|
150
|
+
? () => onCourseClick(course.uid)
|
|
151
|
+
: undefined
|
|
152
|
+
}
|
|
153
|
+
>
|
|
154
|
+
<ProgressRing value={course.progress} size={32} strokeWidth={3} />
|
|
155
|
+
<div className="flex-1 min-w-0">
|
|
156
|
+
<p className="text-sm font-medium text-foreground truncate">
|
|
157
|
+
{course.title}
|
|
158
|
+
</p>
|
|
159
|
+
{course.lastAccessedAt && (
|
|
160
|
+
<p className="text-xs text-muted-foreground">
|
|
161
|
+
{formatTimestamp(course.lastAccessedAt)}
|
|
162
|
+
</p>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
|
166
|
+
{Math.round(course.progress)}%
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
169
|
+
))}
|
|
170
|
+
</CardContent>
|
|
171
|
+
</Card>
|
|
172
|
+
) : (
|
|
173
|
+
<EmptyState
|
|
174
|
+
icon={<BookOpen />}
|
|
175
|
+
title="No courses yet"
|
|
176
|
+
description="This student has not enrolled in any courses."
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
</>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{/* Achievements */}
|
|
183
|
+
{showAchievements && (
|
|
184
|
+
<>
|
|
185
|
+
<Separator />
|
|
186
|
+
<div className="flex items-center gap-2 mb-2">
|
|
187
|
+
<p className="text-lg font-semibold text-foreground">Achievements</p>
|
|
188
|
+
<Badge variant="secondary">{achievements.length}</Badge>
|
|
189
|
+
</div>
|
|
190
|
+
{achievements.length > 0 ? (
|
|
191
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
|
|
192
|
+
{achievements.map((achievement) => (
|
|
193
|
+
<AchievementBadge
|
|
194
|
+
key={achievement.uid}
|
|
195
|
+
title={achievement.name}
|
|
196
|
+
description={achievement.description}
|
|
197
|
+
icon={
|
|
198
|
+
achievement.iconUrl ? (
|
|
199
|
+
<img
|
|
200
|
+
src={achievement.iconUrl}
|
|
201
|
+
alt={achievement.name}
|
|
202
|
+
className="w-12 h-12"
|
|
203
|
+
/>
|
|
204
|
+
) : undefined
|
|
205
|
+
}
|
|
206
|
+
earnedDate={achievement.earnedAt}
|
|
207
|
+
/>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
) : (
|
|
211
|
+
<EmptyState
|
|
212
|
+
icon={<Award />}
|
|
213
|
+
title="No achievements yet"
|
|
214
|
+
description="Achievements will appear here as they are earned."
|
|
215
|
+
/>
|
|
216
|
+
)}
|
|
217
|
+
</>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Certificates */}
|
|
221
|
+
<Separator />
|
|
222
|
+
<div className="flex items-center gap-2 mb-2">
|
|
223
|
+
<p className="text-lg font-semibold text-foreground">Certificates</p>
|
|
224
|
+
<Badge variant="secondary">{certificates.length}</Badge>
|
|
225
|
+
</div>
|
|
226
|
+
{certificates.length > 0 ? (
|
|
227
|
+
<Card>
|
|
228
|
+
<CardContent className="p-0 divide-y divide-border">
|
|
229
|
+
{certificates.map((cert) => (
|
|
230
|
+
<div
|
|
231
|
+
key={cert.uid}
|
|
232
|
+
className={cn(
|
|
233
|
+
"flex items-center gap-3 px-4 py-3 transition-colors",
|
|
234
|
+
!readOnly && onCertificateClick && "cursor-pointer hover:bg-muted/50",
|
|
235
|
+
)}
|
|
236
|
+
onClick={
|
|
237
|
+
!readOnly && onCertificateClick
|
|
238
|
+
? () => onCertificateClick(cert.uid)
|
|
239
|
+
: undefined
|
|
240
|
+
}
|
|
241
|
+
>
|
|
242
|
+
<div className="w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
|
|
243
|
+
<Award size={20} />
|
|
244
|
+
</div>
|
|
245
|
+
<div className="flex-1 min-w-0">
|
|
246
|
+
<p className="text-sm font-medium text-foreground truncate">
|
|
247
|
+
{cert.courseName}
|
|
248
|
+
</p>
|
|
249
|
+
<p className="text-xs text-muted-foreground">
|
|
250
|
+
Issued {new Date(cert.issuedAt).toLocaleDateString()}
|
|
251
|
+
</p>
|
|
252
|
+
</div>
|
|
253
|
+
{cert.certificateUrl && !readOnly && (
|
|
254
|
+
<Button
|
|
255
|
+
variant="outline"
|
|
256
|
+
size="sm"
|
|
257
|
+
onClick={(e) => {
|
|
258
|
+
e.stopPropagation();
|
|
259
|
+
window.open(cert.certificateUrl, "_blank");
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
View
|
|
263
|
+
</Button>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
))}
|
|
267
|
+
</CardContent>
|
|
268
|
+
</Card>
|
|
269
|
+
) : (
|
|
270
|
+
<EmptyState
|
|
271
|
+
icon={<GraduationCap />}
|
|
272
|
+
title="No certificates yet"
|
|
273
|
+
description="Certificates will appear here once courses are completed."
|
|
274
|
+
/>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
</SectionShell>
|
|
278
|
+
);
|
|
279
|
+
}
|