@hydralms/components 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/StudentProfile-BVfZMbnV.cjs +1 -0
- package/dist/StudentProfile-DeMxdrL3.js +3275 -0
- package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
- package/dist/assessment-toolbar/timer-display.d.ts +1 -1
- package/dist/common/index.d.ts +2 -1
- package/dist/common/pagination.d.ts +26 -0
- package/dist/common/types.d.ts +1 -0
- package/dist/components.css +1 -1
- package/dist/content/audio-player.d.ts +22 -0
- package/dist/content/code-block.d.ts +30 -0
- package/dist/content/embed-block.d.ts +28 -0
- package/dist/content/index.d.ts +6 -0
- package/dist/content/types.d.ts +24 -0
- package/dist/curriculum/course-card.d.ts +51 -0
- package/dist/curriculum/index.d.ts +2 -0
- package/dist/curriculum/types.d.ts +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +494 -444
- package/dist/license/HydraContext.d.ts +16 -0
- package/dist/license/ProBadge.d.ts +6 -0
- package/dist/license/index.d.ts +7 -0
- package/dist/license/tiers.d.ts +3 -0
- package/dist/license/useHydraLicense.d.ts +6 -0
- package/dist/license/validate.d.ts +13 -0
- package/dist/license/withProGate.d.ts +6 -0
- package/dist/modules/AssignmentModule/AssignmentModule.d.ts +4 -7
- package/dist/modules/AssignmentModule/types.d.ts +5 -1
- package/dist/modules/CertificateModule/CertificateModule.d.ts +4 -8
- package/dist/modules/CertificateModule/types.d.ts +6 -4
- package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
- package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
- package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
- package/dist/modules/DiscussionModule/DiscussionModule.d.ts +4 -7
- package/dist/modules/ExamModule/ExamModule.d.ts +4 -7
- package/dist/modules/ExamModule/types.d.ts +5 -14
- package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
- package/dist/modules/FlashcardLab/types.d.ts +2 -0
- package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +4 -8
- package/dist/modules/GradeCenterModule/types.d.ts +2 -0
- package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
- package/dist/modules/QuizModule/types.d.ts +5 -14
- package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
- package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
- package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
- package/dist/modules/StudentProfileModule/types.d.ts +43 -0
- package/dist/modules/SurveyModule/SurveyModule.d.ts +4 -6
- package/dist/modules/SurveyModule/types.d.ts +2 -0
- package/dist/modules/_shared/assessment-intro.d.ts +16 -0
- package/dist/modules/_shared/assessment-results.d.ts +23 -0
- package/dist/modules/_shared/types.d.ts +10 -0
- package/dist/modules/_shared/use-timer.d.ts +9 -0
- package/dist/modules/index.d.ts +6 -0
- package/dist/modules.cjs +1 -1
- package/dist/modules.js +1266 -854
- package/dist/progress/types.d.ts +2 -0
- package/dist/provider/HydraProvider.d.ts +5 -1
- package/dist/questions/choice.d.ts +1 -1
- package/dist/questions/confidence-indicator.d.ts +37 -0
- package/dist/questions/essay.d.ts +1 -1
- package/dist/questions/fill-in-the-blank.d.ts +1 -1
- package/dist/questions/hotspot.d.ts +1 -1
- package/dist/questions/index.d.ts +2 -0
- package/dist/questions/inline-choice.d.ts +1 -1
- package/dist/questions/matching.d.ts +1 -1
- package/dist/questions/multiple-choice.d.ts +1 -1
- package/dist/questions/numeric.d.ts +1 -1
- package/dist/questions/ordering.d.ts +1 -1
- package/dist/questions/question-renderer.d.ts +1 -1
- package/dist/questions/scenario.d.ts +1 -1
- package/dist/questions/spreadsheet.d.ts +1 -1
- package/dist/questions/true-false.d.ts +1 -1
- package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
- package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
- package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
- package/dist/sections/AssessmentReview/types.d.ts +6 -0
- package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
- package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
- package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
- package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
- package/dist/sections/CertificateViewer/types.d.ts +6 -0
- package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
- package/dist/sections/CourseCatalog/types.d.ts +80 -0
- package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
- package/dist/sections/CourseOutline/types.d.ts +6 -0
- package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
- package/dist/sections/DiscussionThread/types.d.ts +6 -0
- package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
- package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
- package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
- package/dist/sections/ExamSession/types.d.ts +6 -0
- package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
- package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
- package/dist/sections/ForumBoard/ForumBoard.d.ts +1 -1
- package/dist/sections/ForumBoard/types.d.ts +14 -0
- package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
- package/dist/sections/GradebookTable/types.d.ts +14 -0
- package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
- package/dist/sections/LecturePlayer/types.d.ts +8 -0
- package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
- package/dist/sections/LessonPage/types.d.ts +6 -0
- package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
- package/dist/sections/PracticeQuiz/types.d.ts +6 -0
- package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
- package/dist/sections/ProgressDashboard/types.d.ts +6 -0
- package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
- package/dist/sections/QuizSession/types.d.ts +6 -0
- package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +1 -1
- package/dist/sections/RequirementsChecklist/types.d.ts +6 -0
- package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
- package/dist/sections/ResourceLibrary/types.d.ts +15 -1
- package/dist/sections/RubricView/RubricView.d.ts +1 -1
- package/dist/sections/RubricView/types.d.ts +6 -0
- package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
- package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
- package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
- package/dist/sections/StudentProfile/types.d.ts +98 -0
- package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
- package/dist/sections/SurveyForm/types.d.ts +6 -0
- package/dist/sections/_shared/merge-answers.d.ts +9 -0
- package/dist/sections/_shared/section-shell.d.ts +20 -0
- package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
- package/dist/sections/index.d.ts +6 -0
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +268 -307
- package/dist/tabs-BsfVo2Bl.cjs +173 -0
- package/dist/{tabs-Wf3h_Cx3.js → tabs-BuY1iNJE.js} +7532 -6807
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/progress.d.ts +1 -1
- package/dist/ui/rich-text-editor.d.ts +3 -1
- package/dist/ui/toast.d.ts +43 -0
- package/dist/utils/debounce.d.ts +5 -1
- package/dist/utils/pick-palette-color.d.ts +19 -0
- package/dist/video/types.d.ts +15 -0
- package/dist/video/video-player.d.ts +1 -1
- package/dist/withProGate-BWqcKdPM.js +137 -0
- package/dist/withProGate-DX6XqKLp.cjs +1 -0
- package/package.json +34 -220
- package/src/assessment-toolbar/question-navigator.tsx +10 -5
- package/src/assessment-toolbar/timer-display.tsx +4 -3
- package/src/assessment-toolbar/use-countdown.ts +1 -1
- package/src/common/empty-state.tsx +1 -0
- package/src/common/index.ts +2 -0
- package/src/common/pagination.tsx +135 -0
- package/src/common/search-input.tsx +2 -1
- package/src/common/types.ts +2 -0
- package/src/content/attachment-list.tsx +2 -0
- package/src/content/audio-player.tsx +196 -0
- package/src/content/code-block.tsx +113 -0
- package/src/content/content-block.tsx +64 -0
- package/src/content/embed-block.tsx +78 -0
- package/src/content/file-upload-zone.tsx +10 -0
- package/src/content/index.ts +6 -0
- package/src/content/types.ts +5 -0
- package/src/curriculum/course-card.tsx +199 -0
- package/src/curriculum/curriculum-item.tsx +3 -3
- package/src/curriculum/curriculum-tree.tsx +20 -13
- package/src/curriculum/index.ts +2 -0
- package/src/curriculum/types.ts +2 -2
- package/src/flashcards/flashcard.tsx +28 -8
- package/src/index.ts +3 -0
- package/src/license/HydraContext.tsx +62 -0
- package/src/license/ProBadge.tsx +43 -0
- package/src/license/index.ts +7 -0
- package/src/license/tiers.ts +24 -0
- package/src/license/useHydraLicense.ts +10 -0
- package/src/license/validate.ts +90 -0
- package/src/license/withProGate.tsx +21 -0
- package/src/modules/AssignmentModule/AssignmentModule.tsx +17 -8
- package/src/modules/AssignmentModule/types.ts +5 -1
- package/src/modules/CertificateModule/CertificateModule.tsx +21 -9
- package/src/modules/CertificateModule/types.ts +6 -4
- package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
- package/src/modules/CourseCatalogModule/types.ts +47 -0
- package/src/modules/CoursePlayer/CoursePlayer.tsx +37 -22
- package/src/modules/DiscussionModule/DiscussionModule.tsx +57 -22
- package/src/modules/ExamModule/ExamModule.tsx +64 -198
- package/src/modules/ExamModule/types.ts +5 -14
- package/src/modules/FlashcardLab/FlashcardLab.tsx +10 -5
- package/src/modules/FlashcardLab/types.ts +2 -0
- package/src/modules/GradeCenterModule/GradeCenterModule.tsx +7 -2
- package/src/modules/GradeCenterModule/types.ts +2 -0
- package/src/modules/QuizModule/QuizModule.tsx +49 -169
- package/src/modules/QuizModule/types.ts +5 -15
- package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
- package/src/modules/StudentDashboardModule/types.ts +56 -0
- package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
- package/src/modules/StudentProfileModule/types.ts +45 -0
- package/src/modules/SurveyModule/SurveyModule.tsx +9 -4
- package/src/modules/SurveyModule/types.ts +2 -0
- package/src/modules/_shared/assessment-intro.tsx +75 -0
- package/src/modules/_shared/assessment-results.tsx +133 -0
- package/src/modules/_shared/types.ts +11 -0
- package/src/modules/_shared/use-timer.ts +49 -0
- package/src/modules/index.ts +9 -0
- package/src/progress/achievement-badge.tsx +3 -3
- package/src/progress/grade-indicator.tsx +9 -1
- package/src/progress/progress-ring.tsx +2 -1
- package/src/progress/stat-card.tsx +8 -1
- package/src/progress/types.ts +2 -0
- package/src/provider/HydraProvider.tsx +15 -6
- package/src/questions/choice.tsx +13 -6
- package/src/questions/confidence-indicator.tsx +107 -0
- package/src/questions/essay.tsx +6 -4
- package/src/questions/fill-in-the-blank.tsx +8 -4
- package/src/questions/hotspot.tsx +4 -4
- package/src/questions/index.ts +2 -0
- package/src/questions/inline-choice.tsx +5 -4
- package/src/questions/matching.tsx +5 -4
- package/src/questions/multiple-choice.tsx +13 -6
- package/src/questions/numeric.tsx +8 -4
- package/src/questions/ordering.tsx +12 -4
- package/src/questions/question-renderer.tsx +3 -2
- package/src/questions/scenario.tsx +4 -4
- package/src/questions/spreadsheet.tsx +5 -4
- package/src/questions/true-false.tsx +13 -6
- package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +64 -8
- package/src/sections/AnnouncementFeed/types.ts +15 -1
- package/src/sections/AssessmentReview/AssessmentReview.tsx +37 -0
- package/src/sections/AssessmentReview/types.ts +6 -0
- package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +37 -1
- package/src/sections/AssignmentSubmission/types.ts +6 -0
- package/src/sections/CertificateViewer/CertificateViewer.tsx +29 -227
- package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
- package/src/sections/CertificateViewer/types.ts +6 -0
- package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
- package/src/sections/CourseCatalog/types.ts +76 -0
- package/src/sections/CourseOutline/CourseOutline.tsx +41 -0
- package/src/sections/CourseOutline/types.ts +6 -0
- package/src/sections/DiscussionThread/DiscussionThread.tsx +42 -1
- package/src/sections/DiscussionThread/types.ts +6 -0
- package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
- package/src/sections/EnrollmentWizard/types.ts +65 -0
- package/src/sections/ExamSession/ExamSession.tsx +100 -94
- package/src/sections/ExamSession/types.ts +6 -0
- package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
- package/src/sections/FlashcardStudySession/types.ts +6 -0
- package/src/sections/ForumBoard/ForumBoard.tsx +59 -1
- package/src/sections/ForumBoard/types.ts +14 -0
- package/src/sections/GradebookTable/GradebookTable.tsx +54 -1
- package/src/sections/GradebookTable/types.ts +14 -0
- package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
- package/src/sections/LecturePlayer/types.ts +8 -0
- package/src/sections/LessonPage/LessonPage.tsx +36 -5
- package/src/sections/LessonPage/types.ts +6 -0
- package/src/sections/PracticeQuiz/PracticeQuiz.tsx +106 -74
- package/src/sections/PracticeQuiz/types.ts +6 -0
- package/src/sections/ProgressDashboard/ProgressDashboard.tsx +64 -10
- package/src/sections/ProgressDashboard/types.ts +6 -0
- package/src/sections/QuizSession/QuizSession.tsx +71 -82
- package/src/sections/QuizSession/types.ts +6 -0
- package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +41 -1
- package/src/sections/RequirementsChecklist/types.ts +6 -0
- package/src/sections/ResourceLibrary/ResourceLibrary.tsx +64 -8
- package/src/sections/ResourceLibrary/types.ts +15 -1
- package/src/sections/RubricView/RubricView.tsx +37 -1
- package/src/sections/RubricView/types.ts +6 -0
- package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +36 -15
- package/src/sections/ScrollableQuiz/types.ts +6 -0
- package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
- package/src/sections/StudentProfile/types.ts +99 -0
- package/src/sections/SurveyForm/SurveyForm.tsx +32 -5
- package/src/sections/SurveyForm/types.ts +6 -0
- package/src/sections/_shared/merge-answers.ts +22 -0
- package/src/sections/_shared/section-shell.tsx +64 -0
- package/src/sections/_shared/use-assessment-session.ts +125 -0
- package/src/sections/index.ts +22 -0
- package/src/social/user-avatar.tsx +9 -5
- package/src/styles/globals.css +39 -41
- package/src/ui/badge.tsx +8 -0
- package/src/ui/index.ts +2 -0
- package/src/ui/progress.tsx +4 -0
- package/src/ui/rich-text-editor.tsx +10 -0
- package/src/ui/rich-text-toolbar.tsx +2 -1
- package/src/ui/toast.tsx +170 -0
- package/src/utils/debounce.ts +8 -2
- package/src/utils/pick-palette-color.ts +33 -0
- package/src/video/types.ts +16 -0
- package/src/video/video-player.tsx +13 -1
- package/dist/ForumBoard-CHXU3mjC.js +0 -2207
- package/dist/ForumBoard-d1w5-r6n.cjs +0 -1
- package/dist/tabs-DRM2Iq_J.cjs +0 -172
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { AlertCircle, Grid, List as ListIcon, GraduationCap } from "lucide-react";
|
|
3
|
+
import { CourseCard } from "../../curriculum/course-card";
|
|
4
|
+
import { SearchInput, EmptyState } from "../../common";
|
|
5
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
6
|
+
import { Button } from "../../ui/button";
|
|
7
|
+
import { Tabs, TabsList, TabsTrigger } from "../../ui/tabs";
|
|
8
|
+
import { Tooltip, TooltipTrigger, TooltipContent } from "../../ui/tooltip";
|
|
9
|
+
import { Pagination } from "../../common/pagination";
|
|
10
|
+
import type { CourseCatalogProps } from "./types";
|
|
11
|
+
import { cn } from "../../lib/utils";
|
|
12
|
+
|
|
13
|
+
export function CourseCatalog({
|
|
14
|
+
courses = [],
|
|
15
|
+
categories,
|
|
16
|
+
onCourseClick,
|
|
17
|
+
onEnroll,
|
|
18
|
+
viewMode: initialViewMode = "grid",
|
|
19
|
+
allowViewToggle = true,
|
|
20
|
+
showSearch = true,
|
|
21
|
+
emptyMessage = "No courses found",
|
|
22
|
+
readOnly = false,
|
|
23
|
+
isLoading,
|
|
24
|
+
error,
|
|
25
|
+
onRetry,
|
|
26
|
+
pageSize,
|
|
27
|
+
currentPage = 1,
|
|
28
|
+
totalItems,
|
|
29
|
+
onPageChange,
|
|
30
|
+
className,
|
|
31
|
+
style,
|
|
32
|
+
}: CourseCatalogProps) {
|
|
33
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
34
|
+
const [activeCategoryUid, setActiveCategoryUid] = useState<string | null>(null);
|
|
35
|
+
const [viewMode, setViewMode] = useState(initialViewMode);
|
|
36
|
+
|
|
37
|
+
const filtered = useMemo(() => {
|
|
38
|
+
let result = courses;
|
|
39
|
+
if (activeCategoryUid) {
|
|
40
|
+
result = result.filter((c) => c.categoryUid === activeCategoryUid);
|
|
41
|
+
}
|
|
42
|
+
if (searchQuery.trim()) {
|
|
43
|
+
const q = searchQuery.toLowerCase();
|
|
44
|
+
result = result.filter(
|
|
45
|
+
(c) =>
|
|
46
|
+
c.title.toLowerCase().includes(q) ||
|
|
47
|
+
c.description?.toLowerCase().includes(q) ||
|
|
48
|
+
c.instructor?.displayName.toLowerCase().includes(q),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}, [courses, activeCategoryUid, searchQuery]);
|
|
53
|
+
|
|
54
|
+
if (isLoading) {
|
|
55
|
+
return (
|
|
56
|
+
<div className={cn("space-y-4", className)} style={style}>
|
|
57
|
+
<Skeleton className="h-9 w-full" />
|
|
58
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
59
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
60
|
+
<Skeleton key={i} className="h-64 w-full rounded-lg" />
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (error) {
|
|
68
|
+
return (
|
|
69
|
+
<div className={cn("py-12", className)} style={style}>
|
|
70
|
+
<EmptyState
|
|
71
|
+
icon={<AlertCircle className="size-10 text-destructive" />}
|
|
72
|
+
title="Something went wrong"
|
|
73
|
+
description={error}
|
|
74
|
+
action={
|
|
75
|
+
onRetry ? (
|
|
76
|
+
<Button variant="outline" onClick={onRetry}>
|
|
77
|
+
Retry
|
|
78
|
+
</Button>
|
|
79
|
+
) : undefined
|
|
80
|
+
}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const paginatedItems =
|
|
87
|
+
onPageChange && pageSize
|
|
88
|
+
? filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize)
|
|
89
|
+
: filtered;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className={className} style={style}>
|
|
93
|
+
{/* Toolbar */}
|
|
94
|
+
<div className="flex gap-2 items-center mb-2">
|
|
95
|
+
{showSearch && (
|
|
96
|
+
<div className="flex-1 max-w-80">
|
|
97
|
+
<SearchInput
|
|
98
|
+
value={searchQuery}
|
|
99
|
+
onChange={setSearchQuery}
|
|
100
|
+
placeholder="Search courses..."
|
|
101
|
+
size="small"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
{allowViewToggle && (
|
|
106
|
+
<div className="flex gap-0.5">
|
|
107
|
+
<Tooltip>
|
|
108
|
+
<TooltipTrigger>
|
|
109
|
+
<Button
|
|
110
|
+
variant="ghost"
|
|
111
|
+
size="icon-xs"
|
|
112
|
+
aria-label="Grid view"
|
|
113
|
+
className={cn(viewMode === "grid" && "text-primary")}
|
|
114
|
+
onClick={() => setViewMode("grid")}
|
|
115
|
+
>
|
|
116
|
+
<Grid size={18} />
|
|
117
|
+
</Button>
|
|
118
|
+
</TooltipTrigger>
|
|
119
|
+
<TooltipContent>Grid view</TooltipContent>
|
|
120
|
+
</Tooltip>
|
|
121
|
+
<Tooltip>
|
|
122
|
+
<TooltipTrigger>
|
|
123
|
+
<Button
|
|
124
|
+
variant="ghost"
|
|
125
|
+
size="icon-xs"
|
|
126
|
+
aria-label="List view"
|
|
127
|
+
className={cn(viewMode === "list" && "text-primary")}
|
|
128
|
+
onClick={() => setViewMode("list")}
|
|
129
|
+
>
|
|
130
|
+
<ListIcon size={18} />
|
|
131
|
+
</Button>
|
|
132
|
+
</TooltipTrigger>
|
|
133
|
+
<TooltipContent>List view</TooltipContent>
|
|
134
|
+
</Tooltip>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Category tabs */}
|
|
140
|
+
{categories && categories.length > 0 && (
|
|
141
|
+
<Tabs
|
|
142
|
+
value={activeCategoryUid ?? "all"}
|
|
143
|
+
onValueChange={(v) => setActiveCategoryUid(v === "all" ? null : v)}
|
|
144
|
+
className="mb-2"
|
|
145
|
+
>
|
|
146
|
+
<TabsList>
|
|
147
|
+
<TabsTrigger value="all">All</TabsTrigger>
|
|
148
|
+
{categories.map((cat) => (
|
|
149
|
+
<TabsTrigger key={cat.uid} value={cat.uid}>{cat.label}</TabsTrigger>
|
|
150
|
+
))}
|
|
151
|
+
</TabsList>
|
|
152
|
+
</Tabs>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
{/* Course grid/list */}
|
|
156
|
+
{filtered.length === 0 ? (
|
|
157
|
+
<EmptyState
|
|
158
|
+
icon={<GraduationCap />}
|
|
159
|
+
title={emptyMessage}
|
|
160
|
+
description="Try adjusting your search or filter."
|
|
161
|
+
/>
|
|
162
|
+
) : viewMode === "grid" ? (
|
|
163
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
164
|
+
{paginatedItems.map((course) => (
|
|
165
|
+
<CourseCard
|
|
166
|
+
key={course.uid}
|
|
167
|
+
uid={course.uid}
|
|
168
|
+
title={course.title}
|
|
169
|
+
description={course.description}
|
|
170
|
+
thumbnailUrl={course.thumbnailUrl}
|
|
171
|
+
instructor={course.instructor}
|
|
172
|
+
progress={course.progress}
|
|
173
|
+
enrollmentStatus={course.enrollmentStatus}
|
|
174
|
+
studentCount={course.studentCount}
|
|
175
|
+
duration={course.duration}
|
|
176
|
+
layout="vertical"
|
|
177
|
+
onClick={readOnly ? undefined : () => onCourseClick(course)}
|
|
178
|
+
onEnroll={
|
|
179
|
+
onEnroll && !readOnly ? () => onEnroll(course) : undefined
|
|
180
|
+
}
|
|
181
|
+
className={cn(readOnly && "opacity-70")}
|
|
182
|
+
/>
|
|
183
|
+
))}
|
|
184
|
+
</div>
|
|
185
|
+
) : (
|
|
186
|
+
<div className="flex flex-col gap-2">
|
|
187
|
+
{paginatedItems.map((course) => (
|
|
188
|
+
<CourseCard
|
|
189
|
+
key={course.uid}
|
|
190
|
+
uid={course.uid}
|
|
191
|
+
title={course.title}
|
|
192
|
+
description={course.description}
|
|
193
|
+
thumbnailUrl={course.thumbnailUrl}
|
|
194
|
+
instructor={course.instructor}
|
|
195
|
+
progress={course.progress}
|
|
196
|
+
enrollmentStatus={course.enrollmentStatus}
|
|
197
|
+
studentCount={course.studentCount}
|
|
198
|
+
duration={course.duration}
|
|
199
|
+
layout="horizontal"
|
|
200
|
+
onClick={readOnly ? undefined : () => onCourseClick(course)}
|
|
201
|
+
onEnroll={
|
|
202
|
+
onEnroll && !readOnly ? () => onEnroll(course) : undefined
|
|
203
|
+
}
|
|
204
|
+
className={cn(readOnly && "opacity-70")}
|
|
205
|
+
/>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{onPageChange && pageSize && filtered.length > 0 && (
|
|
211
|
+
<Pagination
|
|
212
|
+
currentPage={currentPage}
|
|
213
|
+
totalPages={Math.ceil((totalItems ?? filtered.length) / pageSize)}
|
|
214
|
+
onPageChange={onPageChange}
|
|
215
|
+
className="mt-4"
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* CourseCatalog section — a searchable, filterable course catalog.
|
|
4
|
+
*
|
|
5
|
+
* Displays courses in a grid or list view with search, category tabs,
|
|
6
|
+
* view mode toggling, and optional pagination.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <CourseCatalog
|
|
10
|
+
* courses={courses}
|
|
11
|
+
* categories={categories}
|
|
12
|
+
* onCourseClick={(c) => navigate(`/courses/${c.uid}`)}
|
|
13
|
+
* onEnroll={(c) => enroll(c.uid)}
|
|
14
|
+
* />
|
|
15
|
+
*/
|
|
16
|
+
export interface CourseCatalogProps {
|
|
17
|
+
/** Courses to display */
|
|
18
|
+
courses: CourseInfo[];
|
|
19
|
+
/** Optional categories for tab filtering */
|
|
20
|
+
categories?: { uid: string; label: string }[];
|
|
21
|
+
/** Called when the user clicks a course */
|
|
22
|
+
onCourseClick: (course: CourseInfo) => void;
|
|
23
|
+
/** Called when the user clicks enroll on a course */
|
|
24
|
+
onEnroll?: (course: CourseInfo) => void;
|
|
25
|
+
/** Layout view mode */
|
|
26
|
+
viewMode?: "grid" | "list";
|
|
27
|
+
/** Whether the user can toggle between grid and list */
|
|
28
|
+
allowViewToggle?: boolean;
|
|
29
|
+
/** Whether to show search */
|
|
30
|
+
showSearch?: boolean;
|
|
31
|
+
/** Empty state message */
|
|
32
|
+
emptyMessage?: string;
|
|
33
|
+
/** When true, disables interactions */
|
|
34
|
+
readOnly?: boolean;
|
|
35
|
+
/** Render skeleton placeholders instead of content */
|
|
36
|
+
isLoading?: boolean;
|
|
37
|
+
/** Error message — renders an error state with optional retry */
|
|
38
|
+
error?: string | null;
|
|
39
|
+
/** Called when the user clicks retry in the error state */
|
|
40
|
+
onRetry?: () => void;
|
|
41
|
+
/** Number of items per page (enables pagination when set with onPageChange) */
|
|
42
|
+
pageSize?: number;
|
|
43
|
+
/** Current page (1-indexed) */
|
|
44
|
+
currentPage?: number;
|
|
45
|
+
/** Total number of items (for server-side pagination) */
|
|
46
|
+
totalItems?: number;
|
|
47
|
+
/** Called when the user navigates to a different page */
|
|
48
|
+
onPageChange?: (page: number) => void;
|
|
49
|
+
/** CSS class name for the root element */
|
|
50
|
+
className?: string;
|
|
51
|
+
/** Inline styles for the root element */
|
|
52
|
+
style?: React.CSSProperties;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CourseInfo {
|
|
56
|
+
/** Unique identifier */
|
|
57
|
+
uid: string;
|
|
58
|
+
/** Course title */
|
|
59
|
+
title: string;
|
|
60
|
+
/** Course description */
|
|
61
|
+
description?: string;
|
|
62
|
+
/** Thumbnail image URL */
|
|
63
|
+
thumbnailUrl?: string;
|
|
64
|
+
/** Instructor info */
|
|
65
|
+
instructor?: { displayName: string; avatarUrl?: string };
|
|
66
|
+
/** Category UID for filtering */
|
|
67
|
+
categoryUid?: string;
|
|
68
|
+
/** Progress percentage (0-100) */
|
|
69
|
+
progress?: number;
|
|
70
|
+
/** Enrollment status */
|
|
71
|
+
enrollmentStatus?: "enrolled" | "completed" | "available" | "locked";
|
|
72
|
+
/** Number of enrolled students */
|
|
73
|
+
studentCount?: number;
|
|
74
|
+
/** Estimated duration (e.g. "12 hours") */
|
|
75
|
+
duration?: string;
|
|
76
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
+
import { AlertCircle } from "lucide-react";
|
|
2
3
|
import { CurriculumTree } from "../../curriculum";
|
|
3
4
|
import { Progress } from "../../ui/progress";
|
|
4
5
|
import { Separator } from "../../ui/separator";
|
|
6
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
7
|
+
import { Button } from "../../ui/button";
|
|
8
|
+
import { EmptyState } from "../../common/empty-state";
|
|
5
9
|
import { flattenLeaves } from "../../utils/flatten-leaves";
|
|
6
10
|
import type { CourseOutlineProps } from "./types";
|
|
7
11
|
import { cn } from "../../lib/utils";
|
|
@@ -16,6 +20,9 @@ export function CourseOutline({
|
|
|
16
20
|
showDuration = true,
|
|
17
21
|
showIcons = true,
|
|
18
22
|
readOnly = false,
|
|
23
|
+
isLoading,
|
|
24
|
+
error,
|
|
25
|
+
onRetry,
|
|
19
26
|
className,
|
|
20
27
|
style,
|
|
21
28
|
}: CourseOutlineProps) {
|
|
@@ -34,6 +41,40 @@ export function CourseOutline({
|
|
|
34
41
|
};
|
|
35
42
|
}, [items, progress]);
|
|
36
43
|
|
|
44
|
+
if (isLoading) {
|
|
45
|
+
return (
|
|
46
|
+
<div className={cn("space-y-4", className)} style={style}>
|
|
47
|
+
<Skeleton className="h-6 w-48" />
|
|
48
|
+
<Skeleton className="h-2 w-full" />
|
|
49
|
+
<Skeleton className="h-8 w-full" />
|
|
50
|
+
<Skeleton className="h-8 w-full ml-6" />
|
|
51
|
+
<Skeleton className="h-8 w-full ml-6" />
|
|
52
|
+
<Skeleton className="h-8 w-full" />
|
|
53
|
+
<Skeleton className="h-8 w-full ml-6" />
|
|
54
|
+
<Skeleton className="h-8 w-full" />
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (error) {
|
|
60
|
+
return (
|
|
61
|
+
<div className={cn("py-12", className)} style={style}>
|
|
62
|
+
<EmptyState
|
|
63
|
+
icon={<AlertCircle className="size-10 text-destructive" />}
|
|
64
|
+
title="Something went wrong"
|
|
65
|
+
description={error}
|
|
66
|
+
action={
|
|
67
|
+
onRetry ? (
|
|
68
|
+
<Button variant="outline" onClick={onRetry}>
|
|
69
|
+
Retry
|
|
70
|
+
</Button>
|
|
71
|
+
) : undefined
|
|
72
|
+
}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
37
78
|
return (
|
|
38
79
|
<div className={cn(className)} style={style}>
|
|
39
80
|
{(courseTitle || showOverallProgress) && (
|
|
@@ -46,6 +46,12 @@ export interface CourseOutlineProps {
|
|
|
46
46
|
showIcons?: boolean;
|
|
47
47
|
/** When true, disables all click interactions */
|
|
48
48
|
readOnly?: boolean;
|
|
49
|
+
/** Render skeleton placeholders instead of content */
|
|
50
|
+
isLoading?: boolean;
|
|
51
|
+
/** Error message — renders an error state with optional retry */
|
|
52
|
+
error?: string | null;
|
|
53
|
+
/** Called when the user clicks retry in the error state */
|
|
54
|
+
onRetry?: () => void;
|
|
49
55
|
/** CSS class name for the root element */
|
|
50
56
|
className?: string;
|
|
51
57
|
/** Inline styles for the root element */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useMemo, useState } from "react";
|
|
2
|
-
import { CheckCircle, Heart, MessageSquare, Reply } from "lucide-react";
|
|
2
|
+
import { AlertCircle, CheckCircle, Heart, MessageSquare, Reply } from "lucide-react";
|
|
3
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
4
|
+
import { EmptyState } from "../../common/empty-state";
|
|
3
5
|
import { PostCard } from "../../social";
|
|
4
6
|
import { Button } from "../../ui/button";
|
|
5
7
|
import { RichTextEditor } from "../../ui/rich-text-editor";
|
|
@@ -23,6 +25,9 @@ export function DiscussionThread({
|
|
|
23
25
|
allowReplies = true,
|
|
24
26
|
sortOrder = "oldest",
|
|
25
27
|
readOnly = false,
|
|
28
|
+
isLoading,
|
|
29
|
+
error,
|
|
30
|
+
onRetry,
|
|
26
31
|
className,
|
|
27
32
|
style,
|
|
28
33
|
}: DiscussionThreadProps) {
|
|
@@ -49,6 +54,42 @@ export function DiscussionThread({
|
|
|
49
54
|
return childrenMap;
|
|
50
55
|
}, [replies, rootPost.uid, sortOrder]);
|
|
51
56
|
|
|
57
|
+
if (isLoading) {
|
|
58
|
+
return (
|
|
59
|
+
<div className={cn("space-y-4", className)} style={style}>
|
|
60
|
+
<Skeleton className="h-7 w-64" />
|
|
61
|
+
<div className="flex items-start gap-3">
|
|
62
|
+
<Skeleton className="h-8 w-8 rounded-full shrink-0" />
|
|
63
|
+
<Skeleton className="h-32 w-full" />
|
|
64
|
+
</div>
|
|
65
|
+
{Array.from({ length: 2 }).map((_, i) => (
|
|
66
|
+
<div key={i} className="ml-8">
|
|
67
|
+
<Skeleton className="h-20 w-full" />
|
|
68
|
+
</div>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (error) {
|
|
75
|
+
return (
|
|
76
|
+
<div className={cn("py-12", className)} style={style}>
|
|
77
|
+
<EmptyState
|
|
78
|
+
icon={<AlertCircle className="size-10 text-destructive" />}
|
|
79
|
+
title="Something went wrong"
|
|
80
|
+
description={error}
|
|
81
|
+
action={
|
|
82
|
+
onRetry ? (
|
|
83
|
+
<Button variant="outline" onClick={onRetry}>
|
|
84
|
+
Retry
|
|
85
|
+
</Button>
|
|
86
|
+
) : undefined
|
|
87
|
+
}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
52
93
|
function handleSubmitReply(parentUid: string) {
|
|
53
94
|
if (isEmptyHtml(replyContent)) return;
|
|
54
95
|
onReply(parentUid, replyContent);
|
|
@@ -38,6 +38,12 @@ export interface DiscussionThreadProps {
|
|
|
38
38
|
sortOrder?: "newest" | "oldest" | "most_liked";
|
|
39
39
|
/** When true, disables interactions */
|
|
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 */
|