@hydralms/components 0.1.3 → 0.2.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/ForumBoard-CHXU3mjC.js +2207 -0
- package/dist/ForumBoard-d1w5-r6n.cjs +1 -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/types.d.ts +52 -4
- package/dist/assessment-toolbar/use-countdown.d.ts +43 -0
- package/dist/common/index.d.ts +2 -1
- package/dist/common/stepper.d.ts +6 -0
- package/dist/common/types.d.ts +37 -0
- package/dist/components.css +1 -1
- package/dist/content/attachment-list.d.ts +6 -0
- package/dist/content/content-block.d.ts +1 -1
- package/dist/content/index.d.ts +2 -1
- package/dist/content/types.d.ts +39 -0
- package/dist/curriculum/curriculum-item.d.ts +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +551 -312
- package/dist/modules/AssignmentModule/AssignmentModule.d.ts +8 -0
- package/dist/modules/AssignmentModule/types.d.ts +65 -0
- package/dist/modules/CertificateModule/CertificateModule.d.ts +9 -0
- package/dist/modules/CertificateModule/types.d.ts +49 -0
- package/dist/modules/DiscussionModule/DiscussionModule.d.ts +8 -0
- package/dist/modules/DiscussionModule/types.d.ts +47 -0
- package/dist/modules/ExamModule/ExamModule.d.ts +8 -0
- package/dist/modules/ExamModule/types.d.ts +64 -0
- package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +9 -0
- package/dist/modules/GradeCenterModule/types.d.ts +54 -0
- package/dist/modules/QuizModule/QuizModule.d.ts +1 -1
- package/dist/modules/QuizModule/types.d.ts +6 -1
- package/dist/modules/SurveyModule/SurveyModule.d.ts +7 -0
- package/dist/modules/SurveyModule/types.d.ts +49 -0
- package/dist/modules/index.d.ts +12 -0
- package/dist/modules.cjs +1 -0
- package/dist/modules.js +1422 -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 +97 -0
- package/dist/questions/essay.d.ts +1 -1
- package/dist/questions/hotspot.d.ts +21 -0
- package/dist/questions/index.d.ts +9 -1
- package/dist/questions/inline-choice.d.ts +21 -0
- package/dist/questions/matching.d.ts +22 -0
- package/dist/questions/numeric.d.ts +11 -0
- package/dist/questions/ordering.d.ts +12 -0
- 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/types.d.ts +106 -1
- package/dist/questions/use-drag-reorder.d.ts +17 -0
- package/dist/sections/CertificateViewer/types.d.ts +7 -5
- package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
- package/dist/sections/ExamSession/types.d.ts +6 -1
- package/dist/sections/ForumBoard/ForumBoard.d.ts +8 -0
- package/dist/sections/ForumBoard/types.d.ts +64 -0
- package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
- package/dist/sections/QuizSession/types.d.ts +6 -1
- package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
- package/dist/sections/RequirementsChecklist/types.d.ts +37 -0
- package/dist/sections/RubricView/RubricView.d.ts +9 -0
- package/dist/sections/RubricView/types.d.ts +50 -0
- package/dist/sections/index.d.ts +7 -1
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +250 -1715
- package/dist/social/post-card.d.ts +1 -1
- package/dist/tabs-DRM2Iq_J.cjs +172 -0
- package/dist/tabs-Wf3h_Cx3.js +21580 -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 +3 -0
- package/dist/ui/progress.d.ts +1 -1
- package/dist/ui/rich-text-editor.d.ts +30 -0
- package/dist/ui/rich-text-toolbar.d.ts +8 -0
- package/dist/utils/array-utils.d.ts +4 -0
- 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/shuffle.d.ts +1 -0
- package/dist/utils/string-utils.d.ts +12 -0
- package/dist/video/video-bookmark.d.ts +1 -1
- package/dist/video/video-playlist-item.d.ts +1 -1
- package/package.json +92 -3
- 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 +3 -31
- package/src/assessment-toolbar/timer-display.tsx +2 -2
- package/src/assessment-toolbar/types.ts +54 -4
- package/src/assessment-toolbar/use-countdown.ts +153 -0
- package/src/common/index.ts +3 -0
- package/src/common/search-input.tsx +7 -6
- package/src/common/stepper.tsx +100 -0
- package/src/common/types.ts +39 -0
- package/src/content/attachment-list.tsx +90 -0
- package/src/content/content-block.tsx +4 -2
- package/src/content/file-upload-zone.tsx +1 -6
- package/src/content/index.ts +3 -0
- package/src/content/types.ts +41 -0
- package/src/curriculum/curriculum-item.tsx +7 -3
- package/src/feedback/feedback-banner.tsx +12 -14
- package/src/flashcards/flashcard-deck.tsx +1 -9
- package/src/flashcards/flashcard.tsx +1 -1
- package/src/modules/AssignmentModule/AssignmentModule.tsx +305 -0
- package/src/modules/AssignmentModule/types.ts +73 -0
- package/src/modules/CertificateModule/CertificateModule.tsx +161 -0
- package/src/modules/CertificateModule/types.ts +47 -0
- package/src/modules/CoursePlayer/CoursePlayer.tsx +44 -48
- package/src/modules/DiscussionModule/DiscussionModule.tsx +110 -0
- package/src/modules/DiscussionModule/types.ts +54 -0
- package/src/modules/ExamModule/ExamModule.tsx +285 -0
- package/src/modules/ExamModule/types.ts +66 -0
- package/src/modules/FlashcardLab/FlashcardLab.tsx +29 -16
- package/src/modules/GradeCenterModule/GradeCenterModule.tsx +169 -0
- package/src/modules/GradeCenterModule/types.ts +63 -0
- package/src/modules/QuizModule/QuizModule.tsx +88 -88
- package/src/modules/QuizModule/types.ts +6 -1
- package/src/modules/SurveyModule/SurveyModule.tsx +180 -0
- package/src/modules/SurveyModule/types.ts +51 -0
- package/src/modules/index.ts +24 -0
- package/src/progress/achievement-badge.tsx +52 -0
- package/src/progress/activity-timeline.tsx +84 -0
- package/src/progress/index.ts +7 -0
- package/src/progress/stat-card.tsx +30 -18
- package/src/progress/streak-badge.tsx +35 -0
- package/src/progress/types.ts +101 -0
- package/src/questions/choice.tsx +7 -9
- package/src/questions/essay.tsx +23 -25
- package/src/questions/fill-in-the-blank.tsx +13 -16
- package/src/questions/hotspot.tsx +154 -0
- package/src/questions/index.ts +16 -0
- package/src/questions/inline-choice.tsx +151 -0
- package/src/questions/matching.tsx +228 -0
- package/src/questions/multiple-choice.tsx +7 -9
- package/src/questions/numeric.tsx +102 -0
- package/src/questions/ordering.tsx +159 -0
- package/src/questions/question-renderer.tsx +21 -0
- package/src/questions/scenario.tsx +140 -0
- package/src/questions/scoring.ts +201 -0
- package/src/questions/spreadsheet.tsx +259 -0
- package/src/questions/true-false.tsx +7 -9
- package/src/questions/types.ts +123 -1
- package/src/questions/use-drag-reorder.ts +80 -0
- package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +2 -15
- package/src/sections/AssessmentReview/AssessmentReview.tsx +13 -2
- package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +7 -5
- package/src/sections/CertificateViewer/CertificateViewer.tsx +409 -56
- package/src/sections/CertificateViewer/types.ts +13 -5
- package/src/sections/CourseOutline/CourseOutline.tsx +4 -14
- package/src/sections/DiscussionThread/DiscussionThread.tsx +13 -10
- package/src/sections/ExamSession/ExamSession.tsx +44 -7
- package/src/sections/ExamSession/types.ts +6 -1
- package/src/sections/ForumBoard/ForumBoard.tsx +284 -0
- package/src/sections/ForumBoard/types.ts +67 -0
- package/src/sections/GradebookTable/GradebookTable.tsx +1 -1
- package/src/sections/LecturePlayer/LecturePlayer.tsx +1 -1
- package/src/sections/LessonPage/LessonPage.tsx +5 -9
- package/src/sections/PracticeQuiz/PracticeQuiz.tsx +15 -26
- package/src/sections/ProgressDashboard/ProgressDashboard.tsx +65 -65
- package/src/sections/QuizSession/QuizSession.tsx +67 -8
- package/src/sections/QuizSession/types.ts +6 -1
- package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +107 -0
- package/src/sections/RequirementsChecklist/types.ts +38 -0
- package/src/sections/ResourceLibrary/ResourceLibrary.tsx +4 -9
- package/src/sections/RubricView/RubricView.tsx +138 -0
- package/src/sections/RubricView/types.ts +52 -0
- package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +23 -9
- package/src/sections/SurveyForm/SurveyForm.tsx +8 -5
- package/src/sections/index.ts +20 -1
- package/src/social/post-card.tsx +8 -19
- package/src/social/user-avatar.tsx +1 -0
- package/src/styles/globals.css +13 -0
- package/src/ui/drawer.tsx +600 -0
- package/src/ui/index.ts +19 -0
- package/src/ui/rich-text-editor.tsx +109 -0
- package/src/ui/rich-text-toolbar.tsx +156 -0
- package/src/utils/array-utils.ts +17 -0
- 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/shuffle.ts +8 -0
- package/src/utils/string-utils.ts +30 -0
- package/src/video/video-bookmark.tsx +4 -3
- package/src/video/video-chapter-list.tsx +9 -4
- package/src/video/video-player.tsx +11 -4
- 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,161 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useMemo } from "react";
|
|
2
|
+
import { Award, ArrowLeft, PartyPopper } from "lucide-react";
|
|
3
|
+
import { RequirementsChecklist } from "../../sections/RequirementsChecklist/RequirementsChecklist";
|
|
4
|
+
import { CertificateViewer } from "../../sections/CertificateViewer/CertificateViewer";
|
|
5
|
+
import { ProgressRing } from "../../progress/progress-ring";
|
|
6
|
+
import { Button } from "../../ui/button";
|
|
7
|
+
import { Card, CardContent } from "../../ui/card";
|
|
8
|
+
import { cn } from "../../lib/utils";
|
|
9
|
+
import type { CertificateModuleProps } from "./types";
|
|
10
|
+
|
|
11
|
+
type InternalStep = { tag: "requirements" } | { tag: "certificate" };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CertificateModule — a certificate-earning flow with requirements tracking
|
|
15
|
+
* and certificate display.
|
|
16
|
+
*
|
|
17
|
+
* Steps: Requirements (checklist + progress) → Certificate (CertificateViewer).
|
|
18
|
+
* The certificate step is only accessible when all requirements are completed.
|
|
19
|
+
*/
|
|
20
|
+
export function CertificateModule({
|
|
21
|
+
courseTitle,
|
|
22
|
+
recipientName,
|
|
23
|
+
organizationName,
|
|
24
|
+
organizationLogo,
|
|
25
|
+
signatory,
|
|
26
|
+
completionDate,
|
|
27
|
+
certificateVariant = "modern",
|
|
28
|
+
requirements,
|
|
29
|
+
overallProgress,
|
|
30
|
+
onRequirementClick,
|
|
31
|
+
onCertificateEarned,
|
|
32
|
+
className,
|
|
33
|
+
style,
|
|
34
|
+
}: CertificateModuleProps) {
|
|
35
|
+
const { allComplete, completedCount } = useMemo(() => {
|
|
36
|
+
const count = requirements.filter((r) => r.completed).length;
|
|
37
|
+
return { allComplete: count === requirements.length, completedCount: count };
|
|
38
|
+
}, [requirements]);
|
|
39
|
+
|
|
40
|
+
const [step, setStep] = useState<InternalStep>({ tag: "requirements" });
|
|
41
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
42
|
+
const earnedFiredRef = useRef(false);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
46
|
+
}, [step.tag]);
|
|
47
|
+
|
|
48
|
+
// Fire onCertificateEarned once when certificate step is first shown
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (step.tag === "certificate" && !earnedFiredRef.current) {
|
|
51
|
+
earnedFiredRef.current = true;
|
|
52
|
+
onCertificateEarned?.();
|
|
53
|
+
}
|
|
54
|
+
}, [step.tag, onCertificateEarned]);
|
|
55
|
+
|
|
56
|
+
// ─── Requirements Screen ───
|
|
57
|
+
if (step.tag === "requirements") {
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
ref={contentRef}
|
|
61
|
+
tabIndex={-1}
|
|
62
|
+
className={cn("max-w-2xl mx-auto outline-none", className)}
|
|
63
|
+
style={style}
|
|
64
|
+
>
|
|
65
|
+
<Card>
|
|
66
|
+
<CardContent className="pt-8 pb-8">
|
|
67
|
+
{/* Header with progress */}
|
|
68
|
+
<div className="text-center mb-6">
|
|
69
|
+
<ProgressRing
|
|
70
|
+
value={overallProgress}
|
|
71
|
+
size={100}
|
|
72
|
+
strokeWidth={8}
|
|
73
|
+
color={
|
|
74
|
+
allComplete ? "var(--success)" : "var(--primary)"
|
|
75
|
+
}
|
|
76
|
+
className="mx-auto mb-4 text-foreground"
|
|
77
|
+
/>
|
|
78
|
+
<h2 className="text-xl font-bold text-foreground mb-2">
|
|
79
|
+
{courseTitle}
|
|
80
|
+
</h2>
|
|
81
|
+
<p className="text-sm text-muted-foreground">
|
|
82
|
+
{allComplete
|
|
83
|
+
? "All requirements met — your certificate is ready!"
|
|
84
|
+
: `${completedCount} of ${requirements.length} requirements complete`}
|
|
85
|
+
</p>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Requirements checklist */}
|
|
89
|
+
<RequirementsChecklist
|
|
90
|
+
requirements={requirements}
|
|
91
|
+
onRequirementClick={onRequirementClick}
|
|
92
|
+
className="mb-6"
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
{/* View Certificate button */}
|
|
96
|
+
{allComplete && (
|
|
97
|
+
<div className="text-center mt-6">
|
|
98
|
+
<Button
|
|
99
|
+
size="lg"
|
|
100
|
+
onClick={() => setStep({ tag: "certificate" })}
|
|
101
|
+
>
|
|
102
|
+
<Award className="size-4 mr-2" />
|
|
103
|
+
View Certificate
|
|
104
|
+
</Button>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</CardContent>
|
|
108
|
+
</Card>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Certificate Screen ───
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
ref={contentRef}
|
|
117
|
+
tabIndex={-1}
|
|
118
|
+
className={cn("outline-none", className)}
|
|
119
|
+
style={style}
|
|
120
|
+
>
|
|
121
|
+
{/* Back link */}
|
|
122
|
+
<Button
|
|
123
|
+
variant="ghost"
|
|
124
|
+
size="sm"
|
|
125
|
+
onClick={() => setStep({ tag: "requirements" })}
|
|
126
|
+
className="mb-4"
|
|
127
|
+
>
|
|
128
|
+
<ArrowLeft className="size-4 mr-1.5" />
|
|
129
|
+
Back to Requirements
|
|
130
|
+
</Button>
|
|
131
|
+
|
|
132
|
+
{/* Congratulations header */}
|
|
133
|
+
<div className="text-center mb-8">
|
|
134
|
+
<div className="mx-auto mb-4 w-14 h-14 rounded-full bg-success/10 flex items-center justify-center">
|
|
135
|
+
<PartyPopper className="size-7 text-success" />
|
|
136
|
+
</div>
|
|
137
|
+
<h2 className="text-2xl font-bold text-foreground mb-2">
|
|
138
|
+
Congratulations!
|
|
139
|
+
</h2>
|
|
140
|
+
<p className="text-muted-foreground">
|
|
141
|
+
You've completed all requirements for{" "}
|
|
142
|
+
<span className="font-medium text-foreground">{courseTitle}</span>
|
|
143
|
+
</p>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* Certificate */}
|
|
147
|
+
<CertificateViewer
|
|
148
|
+
recipientName={recipientName}
|
|
149
|
+
courseTitle={courseTitle}
|
|
150
|
+
completionDate={
|
|
151
|
+
completionDate ?? new Date().toISOString().split("T")[0]
|
|
152
|
+
}
|
|
153
|
+
organizationName={organizationName}
|
|
154
|
+
organizationLogo={organizationLogo}
|
|
155
|
+
signatory={signatory}
|
|
156
|
+
variant={certificateVariant}
|
|
157
|
+
showActions
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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) */
|
|
38
|
+
overallProgress: number;
|
|
39
|
+
/** Called when a requirement item is clicked */
|
|
40
|
+
onRequirementClick?: (uid: string) => void;
|
|
41
|
+
/** Called when the certificate screen is first displayed */
|
|
42
|
+
onCertificateEarned?: () => void;
|
|
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 { 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,22 +20,11 @@ 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";
|
|
18
26
|
import type { CoursePlayerProps, CoursePlayerItem } from "./types";
|
|
19
27
|
|
|
20
|
-
function flattenLeaves(items: CurriculumItem[]): string[] {
|
|
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
28
|
export function CoursePlayer({
|
|
33
29
|
courseTitle,
|
|
34
30
|
curriculum,
|
|
@@ -42,6 +38,7 @@ export function CoursePlayer({
|
|
|
42
38
|
className,
|
|
43
39
|
style,
|
|
44
40
|
}: CoursePlayerProps) {
|
|
41
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
45
42
|
const leafUids = useMemo(() => flattenLeaves(curriculum), [curriculum]);
|
|
46
43
|
const itemMap = useMemo(
|
|
47
44
|
() => new Map(items.map((item) => [item.uid, item])),
|
|
@@ -74,6 +71,10 @@ export function CoursePlayer({
|
|
|
74
71
|
|
|
75
72
|
const activeItem = itemMap.get(activeUid);
|
|
76
73
|
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
76
|
+
}, [activeUid]);
|
|
77
|
+
|
|
77
78
|
function navigateTo(uid: string) {
|
|
78
79
|
setActiveUid(uid);
|
|
79
80
|
onItemChange?.(uid);
|
|
@@ -121,45 +122,40 @@ export function CoursePlayer({
|
|
|
121
122
|
className={cn("flex h-full overflow-hidden", className)}
|
|
122
123
|
style={style}
|
|
123
124
|
>
|
|
124
|
-
{/* Sidebar */}
|
|
125
|
-
{sidebarOpen
|
|
126
|
-
<
|
|
127
|
-
<
|
|
128
|
-
<
|
|
125
|
+
{/* Sidebar Drawer */}
|
|
126
|
+
<Drawer open={sidebarOpen} onOpenChange={setSidebarOpen} side="left">
|
|
127
|
+
<DrawerContent size="sm" scrollLock={false}>
|
|
128
|
+
<DrawerHeader className="flex-row items-center justify-between">
|
|
129
|
+
<DrawerTitle className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
|
129
130
|
Course
|
|
130
|
-
</
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
readOnly={readOnly}
|
|
146
|
-
/>
|
|
147
|
-
</aside>
|
|
148
|
-
)}
|
|
131
|
+
</DrawerTitle>
|
|
132
|
+
<DrawerClose />
|
|
133
|
+
</DrawerHeader>
|
|
134
|
+
<DrawerBody className="px-2 pb-2">
|
|
135
|
+
<CourseOutline
|
|
136
|
+
items={curriculum}
|
|
137
|
+
progress={combinedProgress}
|
|
138
|
+
courseTitle={courseTitle}
|
|
139
|
+
activeItemUid={activeUid}
|
|
140
|
+
onItemClick={handleItemClick}
|
|
141
|
+
readOnly={readOnly}
|
|
142
|
+
/>
|
|
143
|
+
</DrawerBody>
|
|
144
|
+
</DrawerContent>
|
|
145
|
+
</Drawer>
|
|
149
146
|
|
|
150
147
|
{/* Main content area */}
|
|
151
148
|
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
|
152
149
|
{/* Toolbar */}
|
|
153
150
|
<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
|
-
)}
|
|
151
|
+
<Button
|
|
152
|
+
variant="ghost"
|
|
153
|
+
size="sm"
|
|
154
|
+
className="size-7 p-0 mr-1"
|
|
155
|
+
onClick={() => setSidebarOpen(true)}
|
|
156
|
+
>
|
|
157
|
+
<PanelLeft className="size-4" />
|
|
158
|
+
</Button>
|
|
163
159
|
<div className="flex-1 min-w-0">
|
|
164
160
|
<span className="text-sm font-semibold text-foreground truncate block">
|
|
165
161
|
{activeItem?.title ?? "Select an item"}
|
|
@@ -171,7 +167,7 @@ export function CoursePlayer({
|
|
|
171
167
|
</div>
|
|
172
168
|
|
|
173
169
|
{/* Content */}
|
|
174
|
-
<div className="flex-1 overflow-y-auto p-6">
|
|
170
|
+
<div ref={contentRef} tabIndex={-1} className="flex-1 overflow-y-auto p-6 outline-none">
|
|
175
171
|
{activeItem ? (
|
|
176
172
|
renderContent(activeItem, readOnly, handleMarkComplete, isCurrentCompleted, handleNext, hasNext, nextItem)
|
|
177
173
|
) : (
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { 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 { Button } from "../../ui/button";
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
import type { ForumSortOrder } from "../../sections/ForumBoard/types";
|
|
8
|
+
import type { DiscussionModuleProps } from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* DiscussionModule — a master-detail discussion forum with topic list and thread view.
|
|
12
|
+
*
|
|
13
|
+
* Panel-based layout: ForumBoard (topic listing) ↔ DiscussionThread (thread detail).
|
|
14
|
+
* Clicking a topic drills into the thread; a back button returns to the board.
|
|
15
|
+
*/
|
|
16
|
+
export function DiscussionModule({
|
|
17
|
+
forumTitle,
|
|
18
|
+
topics,
|
|
19
|
+
currentUser,
|
|
20
|
+
threads,
|
|
21
|
+
onCreateTopic,
|
|
22
|
+
onReply,
|
|
23
|
+
onToggleLike,
|
|
24
|
+
onMarkAnswer,
|
|
25
|
+
onTopicOpen,
|
|
26
|
+
readOnly,
|
|
27
|
+
className,
|
|
28
|
+
style,
|
|
29
|
+
}: DiscussionModuleProps) {
|
|
30
|
+
const [activeTopicUid, setActiveTopicUid] = useState<string | null>(null);
|
|
31
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
32
|
+
const [sortOrder, setSortOrder] = useState<ForumSortOrder>("newest");
|
|
33
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
37
|
+
}, [activeTopicUid]);
|
|
38
|
+
|
|
39
|
+
function handleTopicClick(topicUid: string) {
|
|
40
|
+
setActiveTopicUid(topicUid);
|
|
41
|
+
onTopicOpen?.(topicUid);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function handleBack() {
|
|
45
|
+
setActiveTopicUid(null);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const activeThread = activeTopicUid ? threads[activeTopicUid] : null;
|
|
49
|
+
const activeTopic = activeTopicUid
|
|
50
|
+
? topics.find((t) => t.uid === activeTopicUid)
|
|
51
|
+
: null;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
ref={contentRef}
|
|
56
|
+
tabIndex={-1}
|
|
57
|
+
className={cn("outline-none", className)}
|
|
58
|
+
style={style}
|
|
59
|
+
>
|
|
60
|
+
{activeTopicUid && activeThread ? (
|
|
61
|
+
/* ─── Thread View ─── */
|
|
62
|
+
<div>
|
|
63
|
+
<Button
|
|
64
|
+
variant="ghost"
|
|
65
|
+
size="sm"
|
|
66
|
+
onClick={handleBack}
|
|
67
|
+
className="mb-4"
|
|
68
|
+
>
|
|
69
|
+
<ArrowLeft className="size-4 mr-1.5" />
|
|
70
|
+
Back to Topics
|
|
71
|
+
</Button>
|
|
72
|
+
<DiscussionThread
|
|
73
|
+
title={activeTopic?.title ?? ""}
|
|
74
|
+
rootPost={activeThread.rootPost}
|
|
75
|
+
replies={activeThread.replies}
|
|
76
|
+
currentUser={currentUser}
|
|
77
|
+
onReply={(parentUid, content) =>
|
|
78
|
+
onReply?.(activeTopicUid, parentUid, content)
|
|
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
|
+
}
|
|
90
|
+
readOnly={readOnly}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
) : (
|
|
94
|
+
/* ─── Forum Board View ─── */
|
|
95
|
+
<ForumBoard
|
|
96
|
+
title={forumTitle}
|
|
97
|
+
topics={topics}
|
|
98
|
+
currentUser={currentUser}
|
|
99
|
+
onTopicClick={handleTopicClick}
|
|
100
|
+
onCreateTopic={onCreateTopic}
|
|
101
|
+
sortOrder={sortOrder}
|
|
102
|
+
onSortChange={setSortOrder}
|
|
103
|
+
searchQuery={searchQuery}
|
|
104
|
+
onSearchChange={setSearchQuery}
|
|
105
|
+
readOnly={readOnly}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -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
|
+
}
|