@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
|
@@ -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 */
|