@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,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
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* StudentProfile section — a student profile overview.
|
|
4
|
+
*
|
|
5
|
+
* Displays student info header, stat cards, enrolled courses list,
|
|
6
|
+
* achievements grid, and certificates list.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <StudentProfile
|
|
10
|
+
* student={{ uid: "s1", displayName: "Jane Doe", email: "jane@example.com" }}
|
|
11
|
+
* enrolledCourses={courses}
|
|
12
|
+
* achievements={badges}
|
|
13
|
+
* onCourseClick={(uid) => navigate(`/courses/${uid}`)}
|
|
14
|
+
* />
|
|
15
|
+
*/
|
|
16
|
+
export interface StudentProfileProps {
|
|
17
|
+
/** Student information */
|
|
18
|
+
student: StudentInfo;
|
|
19
|
+
/** Enrolled courses */
|
|
20
|
+
enrolledCourses?: EnrolledCourse[];
|
|
21
|
+
/** Earned achievements */
|
|
22
|
+
achievements?: ProfileAchievement[];
|
|
23
|
+
/** Earned certificates */
|
|
24
|
+
certificates?: ProfileCertificate[];
|
|
25
|
+
/** Custom stat cards to display */
|
|
26
|
+
stats?: { label: string; value: string; icon?: React.ReactNode }[];
|
|
27
|
+
/** When false, hides the Enrolled Courses section. @default true */
|
|
28
|
+
showCourses?: boolean;
|
|
29
|
+
/** When false, hides the Achievements section. @default true */
|
|
30
|
+
showAchievements?: boolean;
|
|
31
|
+
/** Called when a course is clicked */
|
|
32
|
+
onCourseClick?: (courseUid: string) => void;
|
|
33
|
+
/** Called when a certificate is clicked */
|
|
34
|
+
onCertificateClick?: (certificateUid: string) => void;
|
|
35
|
+
/** When true, disables interactions */
|
|
36
|
+
readOnly?: boolean;
|
|
37
|
+
/** Render skeleton placeholders instead of content */
|
|
38
|
+
isLoading?: boolean;
|
|
39
|
+
/** Error message — renders an error state with optional retry */
|
|
40
|
+
error?: string | null;
|
|
41
|
+
/** Called when the user clicks retry in the error state */
|
|
42
|
+
onRetry?: () => void;
|
|
43
|
+
/** CSS class name for the root element */
|
|
44
|
+
className?: string;
|
|
45
|
+
/** Inline styles for the root element */
|
|
46
|
+
style?: React.CSSProperties;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface StudentInfo {
|
|
50
|
+
/** Unique identifier */
|
|
51
|
+
uid: string;
|
|
52
|
+
/** Display name */
|
|
53
|
+
displayName: string;
|
|
54
|
+
/** Avatar URL */
|
|
55
|
+
avatarUrl?: string;
|
|
56
|
+
/** Email address */
|
|
57
|
+
email?: string;
|
|
58
|
+
/** Short bio */
|
|
59
|
+
bio?: string;
|
|
60
|
+
/** Join date as ISO string */
|
|
61
|
+
joinedAt?: string;
|
|
62
|
+
/** Role label */
|
|
63
|
+
role?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface EnrolledCourse {
|
|
67
|
+
/** Course UID */
|
|
68
|
+
uid: string;
|
|
69
|
+
/** Course title */
|
|
70
|
+
title: string;
|
|
71
|
+
/** Progress percentage (0-100) */
|
|
72
|
+
progress: number;
|
|
73
|
+
/** Last accessed as ISO string */
|
|
74
|
+
lastAccessedAt?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ProfileAchievement {
|
|
78
|
+
/** Unique identifier */
|
|
79
|
+
uid: string;
|
|
80
|
+
/** Achievement name */
|
|
81
|
+
name: string;
|
|
82
|
+
/** Achievement description */
|
|
83
|
+
description: string;
|
|
84
|
+
/** Icon URL */
|
|
85
|
+
iconUrl?: string;
|
|
86
|
+
/** Date earned as ISO string */
|
|
87
|
+
earnedAt: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ProfileCertificate {
|
|
91
|
+
/** Unique identifier */
|
|
92
|
+
uid: string;
|
|
93
|
+
/** Course name */
|
|
94
|
+
courseName: string;
|
|
95
|
+
/** Date issued as ISO string */
|
|
96
|
+
issuedAt: string;
|
|
97
|
+
/** Certificate URL */
|
|
98
|
+
certificateUrl?: string;
|
|
99
|
+
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { useState, useMemo } from "react";
|
|
2
2
|
import { LikertScale, StarRating } from "../../feedback";
|
|
3
|
+
|
|
3
4
|
import { Button } from "../../ui/button";
|
|
4
5
|
import { Card, CardContent } from "../../ui/card";
|
|
5
6
|
import { Progress } from "../../ui/progress";
|
|
6
|
-
import {
|
|
7
|
+
import { Separator } from "../../ui/separator";
|
|
8
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
9
|
+
import { RichTextEditor } from "../../ui/rich-text-editor";
|
|
10
|
+
import { SectionShell } from "../_shared/section-shell";
|
|
7
11
|
import type { SurveyFormProps, SurveyAnswer } from "./types";
|
|
8
12
|
|
|
9
13
|
export function SurveyForm({
|
|
10
14
|
title,
|
|
11
15
|
description,
|
|
12
|
-
questions,
|
|
16
|
+
questions = [],
|
|
13
17
|
initialAnswers = [],
|
|
14
18
|
onSubmit,
|
|
15
19
|
onAnswerChange,
|
|
@@ -18,6 +22,9 @@ export function SurveyForm({
|
|
|
18
22
|
submitLabel = "Submit Survey",
|
|
19
23
|
isSubmitting = false,
|
|
20
24
|
readOnly = false,
|
|
25
|
+
isLoading,
|
|
26
|
+
error,
|
|
27
|
+
onRetry,
|
|
21
28
|
className,
|
|
22
29
|
style,
|
|
23
30
|
}: SurveyFormProps) {
|
|
@@ -39,11 +46,31 @@ export function SurveyForm({
|
|
|
39
46
|
});
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
const answerMap = useMemo(() => {
|
|
50
|
+
const map = new Map<string, SurveyAnswer>();
|
|
51
|
+
for (const a of answers) {
|
|
52
|
+
map.set(a.questionUid, a);
|
|
53
|
+
}
|
|
54
|
+
return map;
|
|
55
|
+
}, [answers]);
|
|
45
56
|
|
|
46
57
|
return (
|
|
58
|
+
<SectionShell
|
|
59
|
+
isLoading={isLoading}
|
|
60
|
+
error={error}
|
|
61
|
+
onRetry={onRetry}
|
|
62
|
+
className={className}
|
|
63
|
+
style={style}
|
|
64
|
+
skeleton={
|
|
65
|
+
<>
|
|
66
|
+
<Skeleton className="h-6 w-48" />
|
|
67
|
+
<Skeleton className="h-2 w-full" />
|
|
68
|
+
<Skeleton className="h-16 w-full" />
|
|
69
|
+
<Skeleton className="h-16 w-full" />
|
|
70
|
+
<Skeleton className="h-16 w-full" />
|
|
71
|
+
</>
|
|
72
|
+
}
|
|
73
|
+
>
|
|
47
74
|
<div className={className} style={style}>
|
|
48
75
|
<p className="text-xl font-bold text-foreground mb-2">{title}</p>
|
|
49
76
|
{description && (
|
|
@@ -65,7 +92,7 @@ export function SurveyForm({
|
|
|
65
92
|
)}
|
|
66
93
|
|
|
67
94
|
{questions.map((q, index) => {
|
|
68
|
-
const answer =
|
|
95
|
+
const answer = answerMap.get(q.uid);
|
|
69
96
|
return (
|
|
70
97
|
<Card key={q.uid} className="mb-2"><CardContent className="pt-6">
|
|
71
98
|
<p className="font-medium text-foreground mb-2">
|
|
@@ -95,12 +122,13 @@ export function SurveyForm({
|
|
|
95
122
|
)}
|
|
96
123
|
|
|
97
124
|
{q.type === "open_text" && (
|
|
98
|
-
<
|
|
125
|
+
<RichTextEditor
|
|
99
126
|
placeholder="Type your response..."
|
|
100
127
|
value={(answer?.value as string) ?? ""}
|
|
101
|
-
onChange={(
|
|
102
|
-
|
|
128
|
+
onChange={(html) => setAnswer(q.uid, html)}
|
|
129
|
+
readOnly={readOnly}
|
|
103
130
|
className="min-h-24"
|
|
131
|
+
variant="minimal"
|
|
104
132
|
/>
|
|
105
133
|
)}
|
|
106
134
|
|
|
@@ -164,7 +192,8 @@ export function SurveyForm({
|
|
|
164
192
|
);
|
|
165
193
|
})}
|
|
166
194
|
|
|
167
|
-
<
|
|
195
|
+
<Separator className="my-3" />
|
|
196
|
+
<div className="flex justify-between items-center">
|
|
168
197
|
<span className="text-sm text-muted-foreground">
|
|
169
198
|
{answeredCount} of {questions.length} answered
|
|
170
199
|
</span>
|
|
@@ -176,5 +205,6 @@ export function SurveyForm({
|
|
|
176
205
|
</Button>
|
|
177
206
|
</div>
|
|
178
207
|
</div>
|
|
208
|
+
</SectionShell>
|
|
179
209
|
);
|
|
180
210
|
}
|
|
@@ -38,6 +38,12 @@ export interface SurveyFormProps {
|
|
|
38
38
|
isSubmitting?: boolean;
|
|
39
39
|
/** When true, all inputs are disabled */
|
|
40
40
|
readOnly?: boolean;
|
|
41
|
+
/** Render skeleton placeholders instead of content */
|
|
42
|
+
isLoading?: boolean;
|
|
43
|
+
/** Error message — renders an error state with optional retry */
|
|
44
|
+
error?: string | null;
|
|
45
|
+
/** Called when the user clicks retry in the error state */
|
|
46
|
+
onRetry?: () => void;
|
|
41
47
|
/** CSS class name for the root element */
|
|
42
48
|
className?: string;
|
|
43
49
|
/** Inline styles for the root element */
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SessionAnswer } from "../../questions/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Replaces all existing answers for `questionUid` with `rawAnswers`,
|
|
5
|
+
* then invokes an optional change callback with the merged array.
|
|
6
|
+
*/
|
|
7
|
+
export function mergeSessionAnswers(
|
|
8
|
+
prev: SessionAnswer[],
|
|
9
|
+
questionUid: string,
|
|
10
|
+
rawAnswers: { uid: string; content?: string }[],
|
|
11
|
+
onChange?: (merged: SessionAnswer[]) => void,
|
|
12
|
+
): SessionAnswer[] {
|
|
13
|
+
const newAnswers: SessionAnswer[] = rawAnswers.map((a) => ({
|
|
14
|
+
uid: questionUid,
|
|
15
|
+
answerUid: a.uid,
|
|
16
|
+
content: a.content,
|
|
17
|
+
}));
|
|
18
|
+
const filtered = prev.filter((a) => a.uid !== questionUid);
|
|
19
|
+
const merged = [...filtered, ...newAnswers];
|
|
20
|
+
onChange?.(merged);
|
|
21
|
+
return merged;
|
|
22
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { AlertCircle } from "lucide-react";
|
|
3
|
+
import { EmptyState } from "../../common";
|
|
4
|
+
import { Button } from "../../ui/button";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
export interface SectionShellProps {
|
|
8
|
+
isLoading?: boolean;
|
|
9
|
+
error?: string | null;
|
|
10
|
+
onRetry?: () => void;
|
|
11
|
+
/** Skeleton placeholder rendered when `isLoading` is true. */
|
|
12
|
+
skeleton?: ReactNode;
|
|
13
|
+
/** Extra class merged into the loading wrapper (default `"space-y-4"`). */
|
|
14
|
+
loadingClassName?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
style?: React.CSSProperties;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Shared wrapper that handles loading skeleton and error states
|
|
22
|
+
* so individual sections don't repeat the same early-return blocks.
|
|
23
|
+
*
|
|
24
|
+
* When neither loading nor error, renders children directly (no extra wrapper).
|
|
25
|
+
*/
|
|
26
|
+
export function SectionShell({
|
|
27
|
+
isLoading,
|
|
28
|
+
error,
|
|
29
|
+
onRetry,
|
|
30
|
+
skeleton,
|
|
31
|
+
loadingClassName = "space-y-4",
|
|
32
|
+
className,
|
|
33
|
+
style,
|
|
34
|
+
children,
|
|
35
|
+
}: SectionShellProps) {
|
|
36
|
+
if (isLoading) {
|
|
37
|
+
return (
|
|
38
|
+
<div className={cn(loadingClassName, className)} style={style}>
|
|
39
|
+
{skeleton}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (error) {
|
|
45
|
+
return (
|
|
46
|
+
<div className={cn("py-12", className)} style={style}>
|
|
47
|
+
<EmptyState
|
|
48
|
+
icon={<AlertCircle className="size-10 text-destructive" />}
|
|
49
|
+
title="Something went wrong"
|
|
50
|
+
description={error}
|
|
51
|
+
action={
|
|
52
|
+
onRetry ? (
|
|
53
|
+
<Button variant="outline" onClick={onRetry}>
|
|
54
|
+
Retry
|
|
55
|
+
</Button>
|
|
56
|
+
) : undefined
|
|
57
|
+
}
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return <>{children}</>;
|
|
64
|
+
}
|