@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
|
@@ -20,6 +20,7 @@ import { Alert, AlertDescription } from "../../ui/alert";
|
|
|
20
20
|
import { Separator } from "../../ui/separator";
|
|
21
21
|
import { cn } from "../../lib/utils";
|
|
22
22
|
import type { SubmissionData } from "../../sections/AssignmentSubmission/types";
|
|
23
|
+
import { withProGate } from "../../license/withProGate";
|
|
23
24
|
import type { AssignmentModuleProps } from "./types";
|
|
24
25
|
|
|
25
26
|
type InternalStep =
|
|
@@ -39,19 +40,21 @@ const TYPE_LABELS: Record<string, string> = {
|
|
|
39
40
|
*
|
|
40
41
|
* Steps: Instructions → Work (AssignmentSubmission) → Confirmation.
|
|
41
42
|
*/
|
|
42
|
-
|
|
43
|
+
function AssignmentModuleBase({
|
|
43
44
|
title,
|
|
44
45
|
instructions,
|
|
45
46
|
dueDate,
|
|
46
47
|
maxScore,
|
|
47
|
-
submissionTypes,
|
|
48
|
+
submissionTypes = ["text", "file"],
|
|
48
49
|
fileConstraints,
|
|
49
50
|
rubric,
|
|
50
51
|
existingSubmission,
|
|
51
52
|
status = "not_started",
|
|
52
53
|
grade,
|
|
53
54
|
onSubmit,
|
|
55
|
+
onComplete,
|
|
54
56
|
onSaveDraft,
|
|
57
|
+
readOnly = false,
|
|
55
58
|
className,
|
|
56
59
|
style,
|
|
57
60
|
}: AssignmentModuleProps) {
|
|
@@ -71,7 +74,7 @@ export function AssignmentModule({
|
|
|
71
74
|
}, [step.tag]);
|
|
72
75
|
|
|
73
76
|
const rubricMaxScore = useMemo(() => {
|
|
74
|
-
if (!rubric) return 0;
|
|
77
|
+
if (!Array.isArray(rubric)) return 0;
|
|
75
78
|
return rubric.reduce(
|
|
76
79
|
(sum, c) => sum + Math.max(...c.levels.map((l) => l.points)),
|
|
77
80
|
0
|
|
@@ -80,11 +83,13 @@ export function AssignmentModule({
|
|
|
80
83
|
|
|
81
84
|
function handleSubmit(submission: SubmissionData) {
|
|
82
85
|
onSubmit?.(submission);
|
|
86
|
+
onComplete?.({ submission, status: "submitted" });
|
|
83
87
|
setStep({ tag: "confirmation", submission });
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
function handleSaveDraft(submission: SubmissionData) {
|
|
87
91
|
onSaveDraft?.(submission);
|
|
92
|
+
onComplete?.({ submission, status: "draft" });
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
const canEdit =
|
|
@@ -138,7 +143,7 @@ export function AssignmentModule({
|
|
|
138
143
|
</div>
|
|
139
144
|
|
|
140
145
|
{/* Rubric preview */}
|
|
141
|
-
{rubric && rubric.length > 0 && (
|
|
146
|
+
{Array.isArray(rubric) && rubric.length > 0 && (
|
|
142
147
|
<>
|
|
143
148
|
<Separator className="my-6" />
|
|
144
149
|
<RubricView
|
|
@@ -149,11 +154,12 @@ export function AssignmentModule({
|
|
|
149
154
|
)}
|
|
150
155
|
|
|
151
156
|
{/* Action */}
|
|
152
|
-
{canEdit && (
|
|
157
|
+
{canEdit && !readOnly && (
|
|
153
158
|
<div className="text-center mt-8">
|
|
154
159
|
<Button
|
|
155
160
|
size="lg"
|
|
156
161
|
onClick={() => setStep({ tag: "work" })}
|
|
162
|
+
disabled={readOnly}
|
|
157
163
|
>
|
|
158
164
|
<Play className="size-4 mr-2" />
|
|
159
165
|
Start Assignment
|
|
@@ -196,6 +202,7 @@ export function AssignmentModule({
|
|
|
196
202
|
grade={grade}
|
|
197
203
|
onSubmit={handleSubmit}
|
|
198
204
|
onSaveDraft={handleSaveDraft}
|
|
205
|
+
readOnly={readOnly}
|
|
199
206
|
/>
|
|
200
207
|
</div>
|
|
201
208
|
);
|
|
@@ -243,7 +250,7 @@ export function AssignmentModule({
|
|
|
243
250
|
<AlertDescription>{grade.feedback}</AlertDescription>
|
|
244
251
|
</Alert>
|
|
245
252
|
)}
|
|
246
|
-
{rubric && grade.rubricLevels && (
|
|
253
|
+
{Array.isArray(rubric) && grade.rubricLevels && (
|
|
247
254
|
<>
|
|
248
255
|
<Separator className="my-6" />
|
|
249
256
|
<RubricView
|
|
@@ -266,7 +273,7 @@ export function AssignmentModule({
|
|
|
266
273
|
{submission.textContent && (
|
|
267
274
|
<div className="flex items-start gap-2">
|
|
268
275
|
<FileText className="size-4 mt-0.5 shrink-0" />
|
|
269
|
-
<span className="line-clamp-2">{submission.textContent}</span>
|
|
276
|
+
<span className="line-clamp-2">{submission.textContent.replace(/<[^>]*>/g, "")}</span>
|
|
270
277
|
</div>
|
|
271
278
|
)}
|
|
272
279
|
{submission.files && submission.files.length > 0 && (
|
|
@@ -287,7 +294,7 @@ export function AssignmentModule({
|
|
|
287
294
|
</div>
|
|
288
295
|
|
|
289
296
|
{/* Edit button */}
|
|
290
|
-
{canEdit && (
|
|
297
|
+
{canEdit && !readOnly && (
|
|
291
298
|
<div className="text-center mt-8">
|
|
292
299
|
<Button
|
|
293
300
|
variant="outline"
|
|
@@ -303,3 +310,5 @@ export function AssignmentModule({
|
|
|
303
310
|
</div>
|
|
304
311
|
);
|
|
305
312
|
}
|
|
313
|
+
|
|
314
|
+
export const AssignmentModule = withProGate(AssignmentModuleBase, "AssignmentModule");
|
|
@@ -57,8 +57,12 @@ export interface AssignmentModuleProps {
|
|
|
57
57
|
};
|
|
58
58
|
/** Called on final submission */
|
|
59
59
|
onSubmit?: (submission: SubmissionData) => void;
|
|
60
|
+
/** Called when the assignment is submitted or draft-saved, with the full result */
|
|
61
|
+
onComplete?: (result: AssignmentModuleResult) => void;
|
|
60
62
|
/** Called on draft save */
|
|
61
63
|
onSaveDraft?: (submission: SubmissionData) => void;
|
|
64
|
+
/** When true, disables interactions for preview/demo mode. @default false */
|
|
65
|
+
readOnly?: boolean;
|
|
62
66
|
/** CSS class name for the root element */
|
|
63
67
|
className?: string;
|
|
64
68
|
/** Inline styles for the root element */
|
|
@@ -69,5 +73,5 @@ export interface AssignmentModuleResult {
|
|
|
69
73
|
/** The submitted data */
|
|
70
74
|
submission: SubmissionData;
|
|
71
75
|
/** Status after submission */
|
|
72
|
-
status:
|
|
76
|
+
status: "not_started" | "draft" | "submitted" | "late" | "graded" | "resubmit";
|
|
73
77
|
}
|
|
@@ -6,6 +6,7 @@ import { ProgressRing } from "../../progress/progress-ring";
|
|
|
6
6
|
import { Button } from "../../ui/button";
|
|
7
7
|
import { Card, CardContent } from "../../ui/card";
|
|
8
8
|
import { cn } from "../../lib/utils";
|
|
9
|
+
import { withProGate } from "../../license/withProGate";
|
|
9
10
|
import type { CertificateModuleProps } from "./types";
|
|
10
11
|
|
|
11
12
|
type InternalStep = { tag: "requirements" } | { tag: "certificate" };
|
|
@@ -17,7 +18,7 @@ type InternalStep = { tag: "requirements" } | { tag: "certificate" };
|
|
|
17
18
|
* Steps: Requirements (checklist + progress) → Certificate (CertificateViewer).
|
|
18
19
|
* The certificate step is only accessible when all requirements are completed.
|
|
19
20
|
*/
|
|
20
|
-
|
|
21
|
+
function CertificateModuleBase({
|
|
21
22
|
courseTitle,
|
|
22
23
|
recipientName,
|
|
23
24
|
organizationName,
|
|
@@ -28,15 +29,20 @@ export function CertificateModule({
|
|
|
28
29
|
requirements,
|
|
29
30
|
overallProgress,
|
|
30
31
|
onRequirementClick,
|
|
31
|
-
|
|
32
|
+
onComplete,
|
|
33
|
+
readOnly = false,
|
|
32
34
|
className,
|
|
33
35
|
style,
|
|
34
36
|
}: CertificateModuleProps) {
|
|
35
|
-
const { allComplete, completedCount } = useMemo(() => {
|
|
37
|
+
const { allComplete, completedCount, derivedProgress } = useMemo(() => {
|
|
36
38
|
const count = requirements.filter((r) => r.completed).length;
|
|
37
|
-
|
|
39
|
+
const total = requirements.length;
|
|
40
|
+
const derived = total > 0 ? Math.round((count / total) * 100) : 0;
|
|
41
|
+
return { allComplete: count === total, completedCount: count, derivedProgress: derived };
|
|
38
42
|
}, [requirements]);
|
|
39
43
|
|
|
44
|
+
const displayProgress = overallProgress ?? derivedProgress;
|
|
45
|
+
|
|
40
46
|
const [step, setStep] = useState<InternalStep>({ tag: "requirements" });
|
|
41
47
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
42
48
|
const earnedFiredRef = useRef(false);
|
|
@@ -45,13 +51,16 @@ export function CertificateModule({
|
|
|
45
51
|
contentRef.current?.focus({ preventScroll: true });
|
|
46
52
|
}, [step.tag]);
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
const onCompleteRef = useRef(onComplete);
|
|
55
|
+
onCompleteRef.current = onComplete;
|
|
56
|
+
|
|
57
|
+
// Fire onComplete once when certificate step is first shown
|
|
49
58
|
useEffect(() => {
|
|
50
59
|
if (step.tag === "certificate" && !earnedFiredRef.current) {
|
|
51
60
|
earnedFiredRef.current = true;
|
|
52
|
-
|
|
61
|
+
onCompleteRef.current?.();
|
|
53
62
|
}
|
|
54
|
-
}, [step.tag
|
|
63
|
+
}, [step.tag]);
|
|
55
64
|
|
|
56
65
|
// ─── Requirements Screen ───
|
|
57
66
|
if (step.tag === "requirements") {
|
|
@@ -67,7 +76,7 @@ export function CertificateModule({
|
|
|
67
76
|
{/* Header with progress */}
|
|
68
77
|
<div className="text-center mb-6">
|
|
69
78
|
<ProgressRing
|
|
70
|
-
value={
|
|
79
|
+
value={displayProgress}
|
|
71
80
|
size={100}
|
|
72
81
|
strokeWidth={8}
|
|
73
82
|
color={
|
|
@@ -88,7 +97,7 @@ export function CertificateModule({
|
|
|
88
97
|
{/* Requirements checklist */}
|
|
89
98
|
<RequirementsChecklist
|
|
90
99
|
requirements={requirements}
|
|
91
|
-
onRequirementClick={onRequirementClick}
|
|
100
|
+
onRequirementClick={readOnly ? undefined : onRequirementClick}
|
|
92
101
|
className="mb-6"
|
|
93
102
|
/>
|
|
94
103
|
|
|
@@ -98,6 +107,7 @@ export function CertificateModule({
|
|
|
98
107
|
<Button
|
|
99
108
|
size="lg"
|
|
100
109
|
onClick={() => setStep({ tag: "certificate" })}
|
|
110
|
+
disabled={readOnly}
|
|
101
111
|
>
|
|
102
112
|
<Award className="size-4 mr-2" />
|
|
103
113
|
View Certificate
|
|
@@ -159,3 +169,5 @@ export function CertificateModule({
|
|
|
159
169
|
</div>
|
|
160
170
|
);
|
|
161
171
|
}
|
|
172
|
+
|
|
173
|
+
export const CertificateModule = withProGate(CertificateModuleBase, "CertificateModule");
|
|
@@ -34,12 +34,14 @@ export interface CertificateModuleProps {
|
|
|
34
34
|
certificateVariant?: CertificateVariant;
|
|
35
35
|
/** Completion requirements */
|
|
36
36
|
requirements: Requirement[];
|
|
37
|
-
/** Overall course progress (0-100) */
|
|
38
|
-
overallProgress
|
|
37
|
+
/** Overall course progress (0-100). When omitted, derived from requirements completion. */
|
|
38
|
+
overallProgress?: number;
|
|
39
39
|
/** Called when a requirement item is clicked */
|
|
40
40
|
onRequirementClick?: (uid: string) => void;
|
|
41
|
-
/** Called when the certificate
|
|
42
|
-
|
|
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;
|
|
43
45
|
/** CSS class name for the root element */
|
|
44
46
|
className?: string;
|
|
45
47
|
/** Inline styles for the root element */
|
|
@@ -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,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo, useRef, useEffect } from "react";
|
|
1
|
+
import { useCallback, useState, useMemo, useRef, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
3
|
ChevronLeft,
|
|
4
4
|
ChevronRight,
|
|
@@ -23,9 +23,10 @@ import { Button } from "../../ui/button";
|
|
|
23
23
|
import { flattenLeaves } from "../../utils/flatten-leaves";
|
|
24
24
|
import { cn } from "../../lib/utils";
|
|
25
25
|
import type { CurriculumItem } from "../../curriculum/types";
|
|
26
|
+
import { withProGate } from "../../license/withProGate";
|
|
26
27
|
import type { CoursePlayerProps, CoursePlayerItem } from "./types";
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
function CoursePlayerBase({
|
|
29
30
|
courseTitle,
|
|
30
31
|
curriculum,
|
|
31
32
|
progress,
|
|
@@ -71,33 +72,49 @@ export function CoursePlayer({
|
|
|
71
72
|
|
|
72
73
|
const activeItem = itemMap.get(activeUid);
|
|
73
74
|
|
|
75
|
+
const videoConfig = useMemo(
|
|
76
|
+
() => activeItem?.type === "video"
|
|
77
|
+
? { src: activeItem.src, poster: activeItem.poster, title: activeItem.title }
|
|
78
|
+
: null,
|
|
79
|
+
// activeItem is looked up from a stable Map — uid change means new item
|
|
80
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
81
|
+
[activeItem?.uid],
|
|
82
|
+
);
|
|
83
|
+
|
|
74
84
|
useEffect(() => {
|
|
75
85
|
contentRef.current?.focus({ preventScroll: true });
|
|
76
86
|
}, [activeUid]);
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
const onItemChangeRef = useRef(onItemChange);
|
|
89
|
+
onItemChangeRef.current = onItemChange;
|
|
90
|
+
const onItemCompleteRef = useRef(onItemComplete);
|
|
91
|
+
onItemCompleteRef.current = onItemComplete;
|
|
92
|
+
|
|
93
|
+
const navigateTo = useCallback((uid: string) => {
|
|
79
94
|
setActiveUid(uid);
|
|
80
|
-
|
|
81
|
-
}
|
|
95
|
+
onItemChangeRef.current?.(uid);
|
|
96
|
+
}, []);
|
|
82
97
|
|
|
83
|
-
|
|
98
|
+
const handleItemClick = useCallback((item: CurriculumItem) => {
|
|
84
99
|
if (!item.children || item.children.length === 0) {
|
|
85
100
|
navigateTo(item.uid);
|
|
86
101
|
}
|
|
87
|
-
}
|
|
102
|
+
}, [navigateTo]);
|
|
88
103
|
|
|
89
|
-
|
|
104
|
+
const handleMarkComplete = useCallback(() => {
|
|
90
105
|
setCompletedUids((prev) => new Set(prev).add(activeUid));
|
|
91
|
-
|
|
92
|
-
}
|
|
106
|
+
onItemCompleteRef.current?.(activeUid);
|
|
107
|
+
}, [activeUid]);
|
|
93
108
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
const handleNext = useCallback(() => {
|
|
110
|
+
const idx = leafUids.indexOf(activeUid);
|
|
111
|
+
if (idx < leafUids.length - 1) navigateTo(leafUids[idx + 1]);
|
|
112
|
+
}, [leafUids, activeUid, navigateTo]);
|
|
97
113
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
114
|
+
const handlePrevious = useCallback(() => {
|
|
115
|
+
const idx = leafUids.indexOf(activeUid);
|
|
116
|
+
if (idx > 0) navigateTo(leafUids[idx - 1]);
|
|
117
|
+
}, [leafUids, activeUid, navigateTo]);
|
|
101
118
|
|
|
102
119
|
// Build combined progress for CourseOutline
|
|
103
120
|
const combinedProgress = useMemo(() => {
|
|
@@ -169,7 +186,7 @@ export function CoursePlayer({
|
|
|
169
186
|
{/* Content */}
|
|
170
187
|
<div ref={contentRef} tabIndex={-1} className="flex-1 overflow-y-auto p-6 outline-none">
|
|
171
188
|
{activeItem ? (
|
|
172
|
-
renderContent(activeItem, readOnly, handleMarkComplete, isCurrentCompleted, handleNext, hasNext, nextItem)
|
|
189
|
+
renderContent(activeItem, readOnly, handleMarkComplete, isCurrentCompleted, handleNext, hasNext, nextItem, videoConfig)
|
|
173
190
|
) : (
|
|
174
191
|
<EmptyState
|
|
175
192
|
title="No content selected"
|
|
@@ -233,6 +250,7 @@ function renderContent(
|
|
|
233
250
|
onNext: () => void,
|
|
234
251
|
hasNext: boolean,
|
|
235
252
|
nextItem: CoursePlayerItem | null | undefined,
|
|
253
|
+
videoConfig: { src: string; poster?: string; title: string } | null,
|
|
236
254
|
) {
|
|
237
255
|
switch (item.type) {
|
|
238
256
|
case "lesson":
|
|
@@ -250,11 +268,8 @@ function renderContent(
|
|
|
250
268
|
case "video":
|
|
251
269
|
return (
|
|
252
270
|
<LecturePlayer
|
|
253
|
-
video={{
|
|
254
|
-
|
|
255
|
-
poster: item.poster,
|
|
256
|
-
title: item.title,
|
|
257
|
-
}}
|
|
271
|
+
video={videoConfig ?? { src: "", title: item.title }}
|
|
272
|
+
onComplete={isCompleted ? undefined : onMarkComplete}
|
|
258
273
|
/>
|
|
259
274
|
);
|
|
260
275
|
case "quiz":
|
|
@@ -275,3 +290,5 @@ function renderContent(
|
|
|
275
290
|
);
|
|
276
291
|
}
|
|
277
292
|
}
|
|
293
|
+
|
|
294
|
+
export const CoursePlayer = withProGate(CoursePlayerBase, "CoursePlayer");
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from "react";
|
|
1
|
+
import { useCallback, useState, useRef, useEffect } from "react";
|
|
2
2
|
import { ArrowLeft } from "lucide-react";
|
|
3
3
|
import { ForumBoard } from "../../sections/ForumBoard/ForumBoard";
|
|
4
4
|
import { DiscussionThread } from "../../sections/DiscussionThread/DiscussionThread";
|
|
5
|
+
import { EmptyState } from "../../common";
|
|
5
6
|
import { Button } from "../../ui/button";
|
|
6
7
|
import { cn } from "../../lib/utils";
|
|
7
8
|
import type { ForumSortOrder } from "../../sections/ForumBoard/types";
|
|
9
|
+
import { withProGate } from "../../license/withProGate";
|
|
8
10
|
import type { DiscussionModuleProps } from "./types";
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -13,11 +15,11 @@ import type { DiscussionModuleProps } from "./types";
|
|
|
13
15
|
* Panel-based layout: ForumBoard (topic listing) ↔ DiscussionThread (thread detail).
|
|
14
16
|
* Clicking a topic drills into the thread; a back button returns to the board.
|
|
15
17
|
*/
|
|
16
|
-
|
|
18
|
+
function DiscussionModuleBase({
|
|
17
19
|
forumTitle,
|
|
18
|
-
topics,
|
|
20
|
+
topics = [],
|
|
19
21
|
currentUser,
|
|
20
|
-
threads,
|
|
22
|
+
threads = {},
|
|
21
23
|
onCreateTopic,
|
|
22
24
|
onReply,
|
|
23
25
|
onToggleLike,
|
|
@@ -36,20 +38,44 @@ export function DiscussionModule({
|
|
|
36
38
|
contentRef.current?.focus({ preventScroll: true });
|
|
37
39
|
}, [activeTopicUid]);
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
const onTopicOpenRef = useRef(onTopicOpen);
|
|
42
|
+
onTopicOpenRef.current = onTopicOpen;
|
|
43
|
+
|
|
44
|
+
const handleTopicClick = useCallback((topicUid: string) => {
|
|
40
45
|
setActiveTopicUid(topicUid);
|
|
41
|
-
|
|
42
|
-
}
|
|
46
|
+
onTopicOpenRef.current?.(topicUid);
|
|
47
|
+
}, []);
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
const handleBack = useCallback(() => {
|
|
45
50
|
setActiveTopicUid(null);
|
|
46
|
-
}
|
|
51
|
+
}, []);
|
|
47
52
|
|
|
48
53
|
const activeThread = activeTopicUid ? threads[activeTopicUid] : null;
|
|
49
54
|
const activeTopic = activeTopicUid
|
|
50
55
|
? topics.find((t) => t.uid === activeTopicUid)
|
|
51
56
|
: null;
|
|
52
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
|
+
|
|
53
79
|
return (
|
|
54
80
|
<div
|
|
55
81
|
ref={contentRef}
|
|
@@ -74,22 +100,29 @@ export function DiscussionModule({
|
|
|
74
100
|
rootPost={activeThread.rootPost}
|
|
75
101
|
replies={activeThread.replies}
|
|
76
102
|
currentUser={currentUser}
|
|
77
|
-
onReply={
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
onToggleLike={
|
|
81
|
-
onToggleLike
|
|
82
|
-
? (postUid) => onToggleLike(activeTopicUid, postUid)
|
|
83
|
-
: undefined
|
|
84
|
-
}
|
|
85
|
-
onMarkAnswer={
|
|
86
|
-
onMarkAnswer
|
|
87
|
-
? (postUid) => onMarkAnswer(activeTopicUid, postUid)
|
|
88
|
-
: undefined
|
|
89
|
-
}
|
|
103
|
+
onReply={handleReply}
|
|
104
|
+
onToggleLike={onToggleLike ? handleToggleLike : undefined}
|
|
105
|
+
onMarkAnswer={onMarkAnswer ? handleMarkAnswer : undefined}
|
|
90
106
|
readOnly={readOnly}
|
|
91
107
|
/>
|
|
92
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>
|
|
93
126
|
) : (
|
|
94
127
|
/* ─── Forum Board View ─── */
|
|
95
128
|
<ForumBoard
|
|
@@ -108,3 +141,5 @@ export function DiscussionModule({
|
|
|
108
141
|
</div>
|
|
109
142
|
);
|
|
110
143
|
}
|
|
144
|
+
|
|
145
|
+
export const DiscussionModule = withProGate(DiscussionModuleBase, "DiscussionModule");
|