@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,49 @@
|
|
|
1
|
+
import type { Requirement } from "../../sections/RequirementsChecklist/types";
|
|
2
|
+
import type { CertificateVariant } from "../../sections/CertificateViewer/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CertificateModule — a certificate-earning flow with requirements tracking
|
|
6
|
+
* and certificate display.
|
|
7
|
+
*
|
|
8
|
+
* Steps: Requirements (checklist + progress) → Certificate (CertificateViewer).
|
|
9
|
+
* The certificate step is only accessible when all requirements are met.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <CertificateModule
|
|
13
|
+
* courseTitle="React Fundamentals"
|
|
14
|
+
* recipientName="Jane Smith"
|
|
15
|
+
* organizationName="HydraLMS Academy"
|
|
16
|
+
* requirements={requirements}
|
|
17
|
+
* overallProgress={100}
|
|
18
|
+
* />
|
|
19
|
+
*/
|
|
20
|
+
export interface CertificateModuleProps {
|
|
21
|
+
/** Course or program title */
|
|
22
|
+
courseTitle: string;
|
|
23
|
+
/** Recipient's full name */
|
|
24
|
+
recipientName: string;
|
|
25
|
+
/** Issuing organization name */
|
|
26
|
+
organizationName: string;
|
|
27
|
+
/** Organization logo URL */
|
|
28
|
+
organizationLogo?: string;
|
|
29
|
+
/** Signatory information */
|
|
30
|
+
signatory?: { name: string; title: string };
|
|
31
|
+
/** Completion date (used on the certificate) */
|
|
32
|
+
completionDate?: string;
|
|
33
|
+
/** Certificate visual variant. @default "modern" */
|
|
34
|
+
certificateVariant?: CertificateVariant;
|
|
35
|
+
/** Completion requirements */
|
|
36
|
+
requirements: Requirement[];
|
|
37
|
+
/** Overall course progress (0-100). When omitted, derived from requirements completion. */
|
|
38
|
+
overallProgress?: number;
|
|
39
|
+
/** Called when a requirement item is clicked */
|
|
40
|
+
onRequirementClick?: (uid: string) => void;
|
|
41
|
+
/** Called when the certificate is first displayed. @default undefined */
|
|
42
|
+
onComplete?: () => void;
|
|
43
|
+
/** When true, disables interactions for preview/demo mode. @default false */
|
|
44
|
+
readOnly?: boolean;
|
|
45
|
+
/** CSS class name for the root element */
|
|
46
|
+
className?: string;
|
|
47
|
+
/** Inline styles for the root element */
|
|
48
|
+
style?: React.CSSProperties;
|
|
49
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { ArrowLeft } from "lucide-react";
|
|
3
|
+
import { CourseCatalog } from "../../sections/CourseCatalog/CourseCatalog";
|
|
4
|
+
import { EnrollmentWizard } from "../../sections/EnrollmentWizard/EnrollmentWizard";
|
|
5
|
+
import { Button } from "../../ui/button";
|
|
6
|
+
import { Card, CardContent } from "../../ui/card";
|
|
7
|
+
import { cn } from "../../lib/utils";
|
|
8
|
+
import { withProGate } from "../../license/withProGate";
|
|
9
|
+
import type { CourseCatalogModuleProps } from "./types";
|
|
10
|
+
|
|
11
|
+
type InternalStep =
|
|
12
|
+
| { tag: "browse" }
|
|
13
|
+
| { tag: "enroll"; courseUid: string };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* CourseCatalogModule — a browse-and-enroll course catalog module.
|
|
17
|
+
*
|
|
18
|
+
* Master-detail layout: CourseCatalog (browse) ↔ EnrollmentWizard (enroll).
|
|
19
|
+
* Clicking a course drills into the enrollment flow; a back button returns to browsing.
|
|
20
|
+
*/
|
|
21
|
+
function CourseCatalogModuleBase({
|
|
22
|
+
title,
|
|
23
|
+
courses = [],
|
|
24
|
+
categories = [],
|
|
25
|
+
enrollmentData,
|
|
26
|
+
onEnroll,
|
|
27
|
+
onCourseOpen,
|
|
28
|
+
readOnly = false,
|
|
29
|
+
className,
|
|
30
|
+
style,
|
|
31
|
+
}: CourseCatalogModuleProps) {
|
|
32
|
+
const [step, setStep] = useState<InternalStep>({ tag: "browse" });
|
|
33
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
37
|
+
}, [step.tag]);
|
|
38
|
+
|
|
39
|
+
function handleCourseClick(courseUid: string) {
|
|
40
|
+
if (enrollmentData?.[courseUid]) {
|
|
41
|
+
setStep({ tag: "enroll", courseUid });
|
|
42
|
+
} else {
|
|
43
|
+
onCourseOpen?.(courseUid);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function handleBack() {
|
|
48
|
+
setStep({ tag: "browse" });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleEnrollConfirm(courseUid: string) {
|
|
52
|
+
onEnroll(courseUid);
|
|
53
|
+
setStep({ tag: "browse" });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
ref={contentRef}
|
|
59
|
+
tabIndex={-1}
|
|
60
|
+
className={cn("outline-none", className)}
|
|
61
|
+
style={style}
|
|
62
|
+
>
|
|
63
|
+
{title && step.tag === "browse" && (
|
|
64
|
+
<h2 className="text-xl font-bold text-foreground mb-4">{title}</h2>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{step.tag === "enroll" ? (
|
|
68
|
+
/* --- Enrollment View --- */
|
|
69
|
+
<div>
|
|
70
|
+
<Button
|
|
71
|
+
variant="ghost"
|
|
72
|
+
size="sm"
|
|
73
|
+
onClick={handleBack}
|
|
74
|
+
className="mb-4"
|
|
75
|
+
>
|
|
76
|
+
<ArrowLeft className="size-4 mr-1.5" />
|
|
77
|
+
Back to Catalog
|
|
78
|
+
</Button>
|
|
79
|
+
{enrollmentData?.[step.courseUid] ? (
|
|
80
|
+
<EnrollmentWizard
|
|
81
|
+
course={enrollmentData[step.courseUid].course}
|
|
82
|
+
prerequisites={enrollmentData[step.courseUid].prerequisites}
|
|
83
|
+
onEnroll={handleEnrollConfirm}
|
|
84
|
+
onCancel={handleBack}
|
|
85
|
+
/>
|
|
86
|
+
) : (
|
|
87
|
+
/* Fallback when enrollment data is not yet available */
|
|
88
|
+
<Card>
|
|
89
|
+
<CardContent className="pt-6">
|
|
90
|
+
<h3 className="text-lg font-semibold text-foreground mb-2">
|
|
91
|
+
{courses.find((c) => c.uid === step.courseUid)?.title ??
|
|
92
|
+
"Course"}
|
|
93
|
+
</h3>
|
|
94
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
95
|
+
Enrollment details are loading. You can enroll directly below.
|
|
96
|
+
</p>
|
|
97
|
+
<div className="flex gap-2">
|
|
98
|
+
<Button variant="outline" onClick={handleBack}>
|
|
99
|
+
Cancel
|
|
100
|
+
</Button>
|
|
101
|
+
<Button
|
|
102
|
+
onClick={() => handleEnrollConfirm(step.courseUid)}
|
|
103
|
+
disabled={readOnly}
|
|
104
|
+
>
|
|
105
|
+
Enroll Now
|
|
106
|
+
</Button>
|
|
107
|
+
</div>
|
|
108
|
+
</CardContent>
|
|
109
|
+
</Card>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
) : (
|
|
113
|
+
/* --- Browse View --- */
|
|
114
|
+
<CourseCatalog
|
|
115
|
+
courses={courses}
|
|
116
|
+
categories={categories}
|
|
117
|
+
onCourseClick={(course) => handleCourseClick(course.uid)}
|
|
118
|
+
onEnroll={(course) => handleCourseClick(course.uid)}
|
|
119
|
+
readOnly={readOnly}
|
|
120
|
+
/>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const CourseCatalogModule = withProGate(CourseCatalogModuleBase, "CourseCatalogModule");
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CourseInfo } from "../../sections/CourseCatalog/types";
|
|
2
|
+
import type {
|
|
3
|
+
EnrollmentCourse,
|
|
4
|
+
Prerequisite,
|
|
5
|
+
} from "../../sections/EnrollmentWizard/types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CourseCatalogModule — a browse-and-enroll course catalog module.
|
|
9
|
+
*
|
|
10
|
+
* Master-detail layout: CourseCatalog (browse) ↔ EnrollmentWizard (enroll).
|
|
11
|
+
* Clicking a course drills into the enrollment flow; a back button returns to browsing.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* <CourseCatalogModule
|
|
15
|
+
* title="Course Catalog"
|
|
16
|
+
* courses={courses}
|
|
17
|
+
* categories={categories}
|
|
18
|
+
* enrollmentData={enrollmentData}
|
|
19
|
+
* onEnroll={(courseUid) => handleEnroll(courseUid)}
|
|
20
|
+
* />
|
|
21
|
+
*/
|
|
22
|
+
export interface CourseCatalogModuleProps {
|
|
23
|
+
/** Catalog title */
|
|
24
|
+
title?: string;
|
|
25
|
+
/** Courses to display */
|
|
26
|
+
courses: CourseInfo[];
|
|
27
|
+
/** Optional categories for filtering */
|
|
28
|
+
categories?: { uid: string; label: string }[];
|
|
29
|
+
/** Enrollment data keyed by course UID — provides course details and prerequisites for the enrollment wizard */
|
|
30
|
+
enrollmentData?: Record<
|
|
31
|
+
string,
|
|
32
|
+
{
|
|
33
|
+
course: EnrollmentCourse;
|
|
34
|
+
prerequisites?: Prerequisite[];
|
|
35
|
+
}
|
|
36
|
+
>;
|
|
37
|
+
/** Called when a user confirms enrollment */
|
|
38
|
+
onEnroll: (courseUid: string) => void;
|
|
39
|
+
/** Called when a course is opened (for lazy-loading enrollment data) */
|
|
40
|
+
onCourseOpen?: (courseUid: string) => void;
|
|
41
|
+
/** When true, disables interactions for preview/demo mode. @default false */
|
|
42
|
+
readOnly?: boolean;
|
|
43
|
+
/** CSS class name for the root element */
|
|
44
|
+
className?: string;
|
|
45
|
+
/** Inline styles for the root element */
|
|
46
|
+
style?: React.CSSProperties;
|
|
47
|
+
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import { useState, useMemo } from "react";
|
|
1
|
+
import { useCallback, useState, useMemo, useRef, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
3
|
ChevronLeft,
|
|
4
4
|
ChevronRight,
|
|
5
5
|
Check,
|
|
6
|
-
PanelLeftClose,
|
|
7
6
|
PanelLeft,
|
|
8
7
|
} from "lucide-react";
|
|
8
|
+
import {
|
|
9
|
+
Drawer,
|
|
10
|
+
DrawerContent,
|
|
11
|
+
DrawerHeader,
|
|
12
|
+
DrawerBody,
|
|
13
|
+
DrawerTitle,
|
|
14
|
+
DrawerClose,
|
|
15
|
+
} from "../../ui/drawer";
|
|
9
16
|
import { CourseOutline } from "../../sections/CourseOutline/CourseOutline";
|
|
10
17
|
import { LessonPage } from "../../sections/LessonPage/LessonPage";
|
|
11
18
|
import { LecturePlayer } from "../../sections/LecturePlayer/LecturePlayer";
|
|
@@ -13,23 +20,13 @@ import { PracticeQuiz } from "../../sections/PracticeQuiz/PracticeQuiz";
|
|
|
13
20
|
import { EmptyState } from "../../common";
|
|
14
21
|
import { Progress } from "../../ui/progress";
|
|
15
22
|
import { Button } from "../../ui/button";
|
|
23
|
+
import { flattenLeaves } from "../../utils/flatten-leaves";
|
|
16
24
|
import { cn } from "../../lib/utils";
|
|
17
25
|
import type { CurriculumItem } from "../../curriculum/types";
|
|
26
|
+
import { withProGate } from "../../license/withProGate";
|
|
18
27
|
import type { CoursePlayerProps, CoursePlayerItem } from "./types";
|
|
19
28
|
|
|
20
|
-
function
|
|
21
|
-
const leaves: string[] = [];
|
|
22
|
-
for (const item of items) {
|
|
23
|
-
if (!item.children || item.children.length === 0) {
|
|
24
|
-
leaves.push(item.uid);
|
|
25
|
-
} else {
|
|
26
|
-
leaves.push(...flattenLeaves(item.children));
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return leaves;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function CoursePlayer({
|
|
29
|
+
function CoursePlayerBase({
|
|
33
30
|
courseTitle,
|
|
34
31
|
curriculum,
|
|
35
32
|
progress,
|
|
@@ -42,6 +39,7 @@ export function CoursePlayer({
|
|
|
42
39
|
className,
|
|
43
40
|
style,
|
|
44
41
|
}: CoursePlayerProps) {
|
|
42
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
45
43
|
const leafUids = useMemo(() => flattenLeaves(curriculum), [curriculum]);
|
|
46
44
|
const itemMap = useMemo(
|
|
47
45
|
() => new Map(items.map((item) => [item.uid, item])),
|
|
@@ -74,29 +72,47 @@ export function CoursePlayer({
|
|
|
74
72
|
|
|
75
73
|
const activeItem = itemMap.get(activeUid);
|
|
76
74
|
|
|
77
|
-
|
|
75
|
+
const videoConfig = useMemo(
|
|
76
|
+
() => activeItem?.type === "video"
|
|
77
|
+
? { src: activeItem.src, poster: activeItem.poster, title: activeItem.title }
|
|
78
|
+
: null,
|
|
79
|
+
[activeItem],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
84
|
+
}, [activeUid]);
|
|
85
|
+
|
|
86
|
+
const onItemChangeRef = useRef(onItemChange);
|
|
87
|
+
onItemChangeRef.current = onItemChange;
|
|
88
|
+
const onItemCompleteRef = useRef(onItemComplete);
|
|
89
|
+
onItemCompleteRef.current = onItemComplete;
|
|
90
|
+
|
|
91
|
+
const navigateTo = useCallback((uid: string) => {
|
|
78
92
|
setActiveUid(uid);
|
|
79
|
-
|
|
80
|
-
}
|
|
93
|
+
onItemChangeRef.current?.(uid);
|
|
94
|
+
}, []);
|
|
81
95
|
|
|
82
|
-
|
|
96
|
+
const handleItemClick = useCallback((item: CurriculumItem) => {
|
|
83
97
|
if (!item.children || item.children.length === 0) {
|
|
84
98
|
navigateTo(item.uid);
|
|
85
99
|
}
|
|
86
|
-
}
|
|
100
|
+
}, [navigateTo]);
|
|
87
101
|
|
|
88
|
-
|
|
102
|
+
const handleMarkComplete = useCallback(() => {
|
|
89
103
|
setCompletedUids((prev) => new Set(prev).add(activeUid));
|
|
90
|
-
|
|
91
|
-
}
|
|
104
|
+
onItemCompleteRef.current?.(activeUid);
|
|
105
|
+
}, [activeUid]);
|
|
92
106
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
107
|
+
const handleNext = useCallback(() => {
|
|
108
|
+
const idx = leafUids.indexOf(activeUid);
|
|
109
|
+
if (idx < leafUids.length - 1) navigateTo(leafUids[idx + 1]);
|
|
110
|
+
}, [leafUids, activeUid, navigateTo]);
|
|
96
111
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
const handlePrevious = useCallback(() => {
|
|
113
|
+
const idx = leafUids.indexOf(activeUid);
|
|
114
|
+
if (idx > 0) navigateTo(leafUids[idx - 1]);
|
|
115
|
+
}, [leafUids, activeUid, navigateTo]);
|
|
100
116
|
|
|
101
117
|
// Build combined progress for CourseOutline
|
|
102
118
|
const combinedProgress = useMemo(() => {
|
|
@@ -121,45 +137,40 @@ export function CoursePlayer({
|
|
|
121
137
|
className={cn("flex h-full overflow-hidden", className)}
|
|
122
138
|
style={style}
|
|
123
139
|
>
|
|
124
|
-
{/* Sidebar */}
|
|
125
|
-
{sidebarOpen
|
|
126
|
-
<
|
|
127
|
-
<
|
|
128
|
-
<
|
|
140
|
+
{/* Sidebar Drawer */}
|
|
141
|
+
<Drawer open={sidebarOpen} onOpenChange={setSidebarOpen} side="left">
|
|
142
|
+
<DrawerContent size="sm" scrollLock={false}>
|
|
143
|
+
<DrawerHeader className="flex-row items-center justify-between">
|
|
144
|
+
<DrawerTitle className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
|
129
145
|
Course
|
|
130
|
-
</
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
readOnly={readOnly}
|
|
146
|
-
/>
|
|
147
|
-
</aside>
|
|
148
|
-
)}
|
|
146
|
+
</DrawerTitle>
|
|
147
|
+
<DrawerClose />
|
|
148
|
+
</DrawerHeader>
|
|
149
|
+
<DrawerBody className="px-2 pb-2">
|
|
150
|
+
<CourseOutline
|
|
151
|
+
items={curriculum}
|
|
152
|
+
progress={combinedProgress}
|
|
153
|
+
courseTitle={courseTitle}
|
|
154
|
+
activeItemUid={activeUid}
|
|
155
|
+
onItemClick={handleItemClick}
|
|
156
|
+
readOnly={readOnly}
|
|
157
|
+
/>
|
|
158
|
+
</DrawerBody>
|
|
159
|
+
</DrawerContent>
|
|
160
|
+
</Drawer>
|
|
149
161
|
|
|
150
162
|
{/* Main content area */}
|
|
151
163
|
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
|
152
164
|
{/* Toolbar */}
|
|
153
165
|
<div className="flex items-center gap-2 px-4 py-2 border-b border-border bg-background shrink-0">
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
)}
|
|
166
|
+
<Button
|
|
167
|
+
variant="ghost"
|
|
168
|
+
size="sm"
|
|
169
|
+
className="size-7 p-0 mr-1"
|
|
170
|
+
onClick={() => setSidebarOpen(true)}
|
|
171
|
+
>
|
|
172
|
+
<PanelLeft className="size-4" />
|
|
173
|
+
</Button>
|
|
163
174
|
<div className="flex-1 min-w-0">
|
|
164
175
|
<span className="text-sm font-semibold text-foreground truncate block">
|
|
165
176
|
{activeItem?.title ?? "Select an item"}
|
|
@@ -171,9 +182,9 @@ export function CoursePlayer({
|
|
|
171
182
|
</div>
|
|
172
183
|
|
|
173
184
|
{/* Content */}
|
|
174
|
-
<div className="flex-1 overflow-y-auto p-6">
|
|
185
|
+
<div ref={contentRef} tabIndex={-1} className="flex-1 overflow-y-auto p-6 outline-none">
|
|
175
186
|
{activeItem ? (
|
|
176
|
-
renderContent(activeItem, readOnly, handleMarkComplete, isCurrentCompleted, handleNext, hasNext, nextItem)
|
|
187
|
+
renderContent(activeItem, readOnly, handleMarkComplete, isCurrentCompleted, handleNext, hasNext, nextItem, videoConfig)
|
|
177
188
|
) : (
|
|
178
189
|
<EmptyState
|
|
179
190
|
title="No content selected"
|
|
@@ -237,6 +248,7 @@ function renderContent(
|
|
|
237
248
|
onNext: () => void,
|
|
238
249
|
hasNext: boolean,
|
|
239
250
|
nextItem: CoursePlayerItem | null | undefined,
|
|
251
|
+
videoConfig: { src: string; poster?: string; title: string } | null,
|
|
240
252
|
) {
|
|
241
253
|
switch (item.type) {
|
|
242
254
|
case "lesson":
|
|
@@ -254,11 +266,8 @@ function renderContent(
|
|
|
254
266
|
case "video":
|
|
255
267
|
return (
|
|
256
268
|
<LecturePlayer
|
|
257
|
-
video={{
|
|
258
|
-
|
|
259
|
-
poster: item.poster,
|
|
260
|
-
title: item.title,
|
|
261
|
-
}}
|
|
269
|
+
video={videoConfig ?? { src: "", title: item.title }}
|
|
270
|
+
onComplete={isCompleted ? undefined : onMarkComplete}
|
|
262
271
|
/>
|
|
263
272
|
);
|
|
264
273
|
case "quiz":
|
|
@@ -279,3 +288,5 @@ function renderContent(
|
|
|
279
288
|
);
|
|
280
289
|
}
|
|
281
290
|
}
|
|
291
|
+
|
|
292
|
+
export const CoursePlayer = withProGate(CoursePlayerBase, "CoursePlayer");
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useCallback, useState, useRef, useEffect } from "react";
|
|
2
|
+
import { ArrowLeft } from "lucide-react";
|
|
3
|
+
import { ForumBoard } from "../../sections/ForumBoard/ForumBoard";
|
|
4
|
+
import { DiscussionThread } from "../../sections/DiscussionThread/DiscussionThread";
|
|
5
|
+
import { EmptyState } from "../../common";
|
|
6
|
+
import { Button } from "../../ui/button";
|
|
7
|
+
import { cn } from "../../lib/utils";
|
|
8
|
+
import type { ForumSortOrder } from "../../sections/ForumBoard/types";
|
|
9
|
+
import { withProGate } from "../../license/withProGate";
|
|
10
|
+
import type { DiscussionModuleProps } from "./types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* DiscussionModule — a master-detail discussion forum with topic list and thread view.
|
|
14
|
+
*
|
|
15
|
+
* Panel-based layout: ForumBoard (topic listing) ↔ DiscussionThread (thread detail).
|
|
16
|
+
* Clicking a topic drills into the thread; a back button returns to the board.
|
|
17
|
+
*/
|
|
18
|
+
function DiscussionModuleBase({
|
|
19
|
+
forumTitle,
|
|
20
|
+
topics = [],
|
|
21
|
+
currentUser,
|
|
22
|
+
threads = {},
|
|
23
|
+
onCreateTopic,
|
|
24
|
+
onReply,
|
|
25
|
+
onToggleLike,
|
|
26
|
+
onMarkAnswer,
|
|
27
|
+
onTopicOpen,
|
|
28
|
+
readOnly,
|
|
29
|
+
className,
|
|
30
|
+
style,
|
|
31
|
+
}: DiscussionModuleProps) {
|
|
32
|
+
const [activeTopicUid, setActiveTopicUid] = useState<string | null>(null);
|
|
33
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
34
|
+
const [sortOrder, setSortOrder] = useState<ForumSortOrder>("newest");
|
|
35
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
39
|
+
}, [activeTopicUid]);
|
|
40
|
+
|
|
41
|
+
const onTopicOpenRef = useRef(onTopicOpen);
|
|
42
|
+
onTopicOpenRef.current = onTopicOpen;
|
|
43
|
+
|
|
44
|
+
const handleTopicClick = useCallback((topicUid: string) => {
|
|
45
|
+
setActiveTopicUid(topicUid);
|
|
46
|
+
onTopicOpenRef.current?.(topicUid);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const handleBack = useCallback(() => {
|
|
50
|
+
setActiveTopicUid(null);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const activeThread = activeTopicUid ? threads[activeTopicUid] : null;
|
|
54
|
+
const activeTopic = activeTopicUid
|
|
55
|
+
? topics.find((t) => t.uid === activeTopicUid)
|
|
56
|
+
: null;
|
|
57
|
+
|
|
58
|
+
const handleReply = useCallback(
|
|
59
|
+
(parentUid: string, content: string) => {
|
|
60
|
+
if (activeTopicUid) onReply?.(activeTopicUid, parentUid, content);
|
|
61
|
+
},
|
|
62
|
+
[activeTopicUid, onReply],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const handleToggleLike = useCallback(
|
|
66
|
+
(postUid: string) => {
|
|
67
|
+
if (activeTopicUid) onToggleLike?.(activeTopicUid, postUid);
|
|
68
|
+
},
|
|
69
|
+
[activeTopicUid, onToggleLike],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const handleMarkAnswer = useCallback(
|
|
73
|
+
(postUid: string) => {
|
|
74
|
+
if (activeTopicUid) onMarkAnswer?.(activeTopicUid, postUid);
|
|
75
|
+
},
|
|
76
|
+
[activeTopicUid, onMarkAnswer],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
ref={contentRef}
|
|
82
|
+
tabIndex={-1}
|
|
83
|
+
className={cn("outline-none", className)}
|
|
84
|
+
style={style}
|
|
85
|
+
>
|
|
86
|
+
{activeTopicUid && activeThread ? (
|
|
87
|
+
/* ─── Thread View ─── */
|
|
88
|
+
<div>
|
|
89
|
+
<Button
|
|
90
|
+
variant="ghost"
|
|
91
|
+
size="sm"
|
|
92
|
+
onClick={handleBack}
|
|
93
|
+
className="mb-4"
|
|
94
|
+
>
|
|
95
|
+
<ArrowLeft className="size-4 mr-1.5" />
|
|
96
|
+
Back to Topics
|
|
97
|
+
</Button>
|
|
98
|
+
<DiscussionThread
|
|
99
|
+
title={activeTopic?.title ?? ""}
|
|
100
|
+
rootPost={activeThread.rootPost}
|
|
101
|
+
replies={activeThread.replies}
|
|
102
|
+
currentUser={currentUser}
|
|
103
|
+
onReply={handleReply}
|
|
104
|
+
onToggleLike={onToggleLike ? handleToggleLike : undefined}
|
|
105
|
+
onMarkAnswer={onMarkAnswer ? handleMarkAnswer : undefined}
|
|
106
|
+
readOnly={readOnly}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
) : activeTopicUid && !activeThread ? (
|
|
110
|
+
/* ─── Empty Thread State ─── */
|
|
111
|
+
<div>
|
|
112
|
+
<Button
|
|
113
|
+
variant="ghost"
|
|
114
|
+
size="sm"
|
|
115
|
+
onClick={handleBack}
|
|
116
|
+
className="mb-4"
|
|
117
|
+
>
|
|
118
|
+
<ArrowLeft className="size-4 mr-1.5" />
|
|
119
|
+
Back to Topics
|
|
120
|
+
</Button>
|
|
121
|
+
<EmptyState
|
|
122
|
+
title="No discussion yet"
|
|
123
|
+
description="Be the first to start this conversation."
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
) : (
|
|
127
|
+
/* ─── Forum Board View ─── */
|
|
128
|
+
<ForumBoard
|
|
129
|
+
title={forumTitle}
|
|
130
|
+
topics={topics}
|
|
131
|
+
currentUser={currentUser}
|
|
132
|
+
onTopicClick={handleTopicClick}
|
|
133
|
+
onCreateTopic={onCreateTopic}
|
|
134
|
+
sortOrder={sortOrder}
|
|
135
|
+
onSortChange={setSortOrder}
|
|
136
|
+
searchQuery={searchQuery}
|
|
137
|
+
onSearchChange={setSearchQuery}
|
|
138
|
+
readOnly={readOnly}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const DiscussionModule = withProGate(DiscussionModuleBase, "DiscussionModule");
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DiscussionUser,
|
|
3
|
+
DiscussionPost,
|
|
4
|
+
} from "../../sections/DiscussionThread/types";
|
|
5
|
+
import type { ForumTopic } from "../../sections/ForumBoard/types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* DiscussionModule — a master-detail discussion forum with topic list and thread view.
|
|
9
|
+
*
|
|
10
|
+
* Panel-based layout: ForumBoard (topic list) ↔ DiscussionThread (thread detail).
|
|
11
|
+
* Clicking a topic drills into the thread; a back button returns to the board.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* <DiscussionModule
|
|
15
|
+
* forumTitle="Class Discussion"
|
|
16
|
+
* topics={topics}
|
|
17
|
+
* threads={threadData}
|
|
18
|
+
* currentUser={user}
|
|
19
|
+
* onCreateTopic={(title, content) => createTopic(title, content)}
|
|
20
|
+
* onReply={(topicUid, parentUid, content) => postReply(topicUid, parentUid, content)}
|
|
21
|
+
* />
|
|
22
|
+
*/
|
|
23
|
+
export interface DiscussionModuleProps {
|
|
24
|
+
/** Forum title */
|
|
25
|
+
forumTitle?: string;
|
|
26
|
+
/** List of discussion topics */
|
|
27
|
+
topics: ForumTopic[];
|
|
28
|
+
/** The currently authenticated user */
|
|
29
|
+
currentUser: DiscussionUser;
|
|
30
|
+
/** Thread data keyed by topic UID */
|
|
31
|
+
threads: Record<
|
|
32
|
+
string,
|
|
33
|
+
{
|
|
34
|
+
rootPost: DiscussionPost;
|
|
35
|
+
replies: DiscussionPost[];
|
|
36
|
+
}
|
|
37
|
+
>;
|
|
38
|
+
/** Called when a new topic is created */
|
|
39
|
+
onCreateTopic?: (title: string, content: string) => void;
|
|
40
|
+
/** Called when a reply is posted */
|
|
41
|
+
onReply?: (topicUid: string, parentUid: string, content: string) => void;
|
|
42
|
+
/** Called when a like is toggled */
|
|
43
|
+
onToggleLike?: (topicUid: string, postUid: string) => void;
|
|
44
|
+
/** Called when a post is marked as the answer */
|
|
45
|
+
onMarkAnswer?: (topicUid: string, postUid: string) => void;
|
|
46
|
+
/** Called when a topic is opened (for lazy loading thread data) */
|
|
47
|
+
onTopicOpen?: (topicUid: string) => void;
|
|
48
|
+
/** When true, disables all interactions */
|
|
49
|
+
readOnly?: boolean;
|
|
50
|
+
/** CSS class name for the root element */
|
|
51
|
+
className?: string;
|
|
52
|
+
/** Inline styles for the root element */
|
|
53
|
+
style?: React.CSSProperties;
|
|
54
|
+
}
|