@hydralms/components 0.1.2 → 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 +141 -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
|
@@ -1,15 +1,320 @@
|
|
|
1
1
|
import { Award, Download, Printer } from "lucide-react";
|
|
2
|
+
import { cva } from "class-variance-authority";
|
|
2
3
|
import { Button } from "../../ui/button";
|
|
3
|
-
import {
|
|
4
|
-
import type { CertificateViewerProps } from "./types";
|
|
4
|
+
import type { CertificateViewerProps, CertificateVariant } from "./types";
|
|
5
5
|
import { cn } from "../../lib/utils";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
// ─── Frame CVAs ────────────────────────────────────────────────
|
|
8
|
+
// Double-frame variants (classic, elegant, academic) use outer + inner borders
|
|
9
|
+
// with a gap between them. Single-frame variants collapse the outer.
|
|
10
|
+
|
|
11
|
+
const outerFrameVariants = cva("mx-auto max-w-4xl", {
|
|
12
|
+
variants: {
|
|
13
|
+
variant: {
|
|
14
|
+
classic:
|
|
15
|
+
"border-[3px] border-double border-warning/50 rounded-lg p-1.5",
|
|
16
|
+
modern: "rounded-lg",
|
|
17
|
+
elegant:
|
|
18
|
+
"border border-foreground/15 rounded-lg p-2.5 dark:border-foreground/10",
|
|
19
|
+
academic: "border-2 border-info/30 rounded-lg p-1.5",
|
|
20
|
+
minimal: "rounded-lg",
|
|
21
|
+
bold: "rounded-lg",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: { variant: "classic" },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const innerFrameVariants = cva(
|
|
28
|
+
"relative text-center p-8 sm:p-12 md:p-16 overflow-hidden",
|
|
29
|
+
{
|
|
30
|
+
variants: {
|
|
31
|
+
variant: {
|
|
32
|
+
classic: "border border-warning/25 rounded bg-warning/5",
|
|
33
|
+
modern:
|
|
34
|
+
"border border-primary/20 rounded-lg bg-linear-to-br from-primary/5 to-primary/8 border-t-[3px] border-t-primary/60",
|
|
35
|
+
elegant:
|
|
36
|
+
"border border-foreground/8 rounded bg-linear-to-b from-background to-muted/20 dark:border-foreground/5 dark:to-muted/10",
|
|
37
|
+
academic: "border border-info/15 rounded bg-info/3",
|
|
38
|
+
minimal: "border border-border rounded-lg bg-background",
|
|
39
|
+
bold: "border-[3px] border-primary rounded-lg bg-linear-to-br from-primary/8 to-purple/8",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
defaultVariants: { variant: "classic" },
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// ─── Corner ornaments ──────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
interface CornerConfig {
|
|
49
|
+
size: number;
|
|
50
|
+
paths: { d: string; strokeWidth: number }[];
|
|
51
|
+
dots?: { cx: number; cy: number; r: number }[];
|
|
52
|
+
color: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const CORNER_CONFIGS: Record<CertificateVariant, CornerConfig | null> = {
|
|
56
|
+
classic: {
|
|
57
|
+
size: 40,
|
|
58
|
+
paths: [
|
|
59
|
+
{ d: "M 2 38 L 2 10 Q 2 2 10 2 L 38 2", strokeWidth: 1.5 },
|
|
60
|
+
{ d: "M 7 38 L 7 13 Q 7 7 13 7 L 38 7", strokeWidth: 0.75 },
|
|
61
|
+
],
|
|
62
|
+
color: "text-warning/50",
|
|
63
|
+
},
|
|
64
|
+
modern: null,
|
|
65
|
+
elegant: {
|
|
66
|
+
size: 32,
|
|
67
|
+
paths: [{ d: "M 0 30 L 0 0 L 30 0", strokeWidth: 0.5 }],
|
|
68
|
+
dots: [{ cx: 2.5, cy: 2.5, r: 1.5 }],
|
|
69
|
+
color: "text-foreground/20",
|
|
70
|
+
},
|
|
71
|
+
academic: {
|
|
72
|
+
size: 36,
|
|
73
|
+
paths: [
|
|
74
|
+
{ d: "M 0 36 L 0 0 L 36 0", strokeWidth: 2 },
|
|
75
|
+
{ d: "M 5 36 L 5 5 L 36 5", strokeWidth: 1 },
|
|
76
|
+
],
|
|
77
|
+
color: "text-info/35",
|
|
78
|
+
},
|
|
79
|
+
minimal: null,
|
|
80
|
+
bold: {
|
|
81
|
+
size: 20,
|
|
82
|
+
paths: [],
|
|
83
|
+
dots: [{ cx: 5, cy: 5, r: 5 }],
|
|
84
|
+
color: "text-primary/40",
|
|
85
|
+
},
|
|
11
86
|
};
|
|
12
87
|
|
|
88
|
+
const CORNER_POSITIONS = [
|
|
89
|
+
{ key: "tl", pos: "top-3 left-3", transform: undefined },
|
|
90
|
+
{ key: "tr", pos: "top-3 right-3", transform: "scaleX(-1)" },
|
|
91
|
+
{ key: "bl", pos: "bottom-3 left-3", transform: "scaleY(-1)" },
|
|
92
|
+
{ key: "br", pos: "bottom-3 right-3", transform: "scale(-1)" },
|
|
93
|
+
] as const;
|
|
94
|
+
|
|
95
|
+
function CornerOrnaments({ variant }: { variant: CertificateVariant }) {
|
|
96
|
+
const config = CORNER_CONFIGS[variant];
|
|
97
|
+
if (!config) return null;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
{CORNER_POSITIONS.map(({ key, pos, transform }) => (
|
|
102
|
+
<svg
|
|
103
|
+
key={key}
|
|
104
|
+
className={cn("absolute pointer-events-none", pos, config.color)}
|
|
105
|
+
width={config.size}
|
|
106
|
+
height={config.size}
|
|
107
|
+
viewBox={`0 0 ${config.size} ${config.size}`}
|
|
108
|
+
fill="none"
|
|
109
|
+
stroke="currentColor"
|
|
110
|
+
style={transform ? { transform } : undefined}
|
|
111
|
+
aria-hidden="true"
|
|
112
|
+
>
|
|
113
|
+
{config.paths.map((p, i) => (
|
|
114
|
+
<path key={i} d={p.d} strokeWidth={p.strokeWidth} />
|
|
115
|
+
))}
|
|
116
|
+
{config.dots?.map((dot, i) => (
|
|
117
|
+
<circle
|
|
118
|
+
key={`d${i}`}
|
|
119
|
+
cx={dot.cx}
|
|
120
|
+
cy={dot.cy}
|
|
121
|
+
r={dot.r}
|
|
122
|
+
fill="currentColor"
|
|
123
|
+
stroke="none"
|
|
124
|
+
/>
|
|
125
|
+
))}
|
|
126
|
+
</svg>
|
|
127
|
+
))}
|
|
128
|
+
</>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Ornamental dividers ───────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
interface DividerConfig {
|
|
135
|
+
lineColor: string;
|
|
136
|
+
motif: React.ReactNode;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const DIVIDER_CONFIGS: Record<CertificateVariant, DividerConfig> = {
|
|
140
|
+
classic: {
|
|
141
|
+
lineColor: "bg-warning/30",
|
|
142
|
+
motif: (
|
|
143
|
+
<svg
|
|
144
|
+
width="12"
|
|
145
|
+
height="12"
|
|
146
|
+
viewBox="0 0 12 12"
|
|
147
|
+
className="text-warning/50 shrink-0"
|
|
148
|
+
aria-hidden="true"
|
|
149
|
+
>
|
|
150
|
+
<path d="M6 0 L12 6 L6 12 L0 6 Z" fill="currentColor" />
|
|
151
|
+
</svg>
|
|
152
|
+
),
|
|
153
|
+
},
|
|
154
|
+
modern: {
|
|
155
|
+
lineColor: "bg-primary/20",
|
|
156
|
+
motif: (
|
|
157
|
+
<svg
|
|
158
|
+
width="8"
|
|
159
|
+
height="8"
|
|
160
|
+
viewBox="0 0 8 8"
|
|
161
|
+
className="text-primary/40 shrink-0"
|
|
162
|
+
aria-hidden="true"
|
|
163
|
+
>
|
|
164
|
+
<circle cx="4" cy="4" r="3" fill="currentColor" />
|
|
165
|
+
</svg>
|
|
166
|
+
),
|
|
167
|
+
},
|
|
168
|
+
elegant: {
|
|
169
|
+
lineColor: "bg-foreground/10",
|
|
170
|
+
motif: (
|
|
171
|
+
<svg
|
|
172
|
+
width="10"
|
|
173
|
+
height="10"
|
|
174
|
+
viewBox="0 0 10 10"
|
|
175
|
+
className="text-foreground/15 shrink-0"
|
|
176
|
+
aria-hidden="true"
|
|
177
|
+
>
|
|
178
|
+
<path
|
|
179
|
+
d="M5 0 L6 4 L10 5 L6 6 L5 10 L4 6 L0 5 L4 4 Z"
|
|
180
|
+
fill="currentColor"
|
|
181
|
+
/>
|
|
182
|
+
</svg>
|
|
183
|
+
),
|
|
184
|
+
},
|
|
185
|
+
academic: {
|
|
186
|
+
lineColor: "bg-info/25",
|
|
187
|
+
motif: (
|
|
188
|
+
<svg
|
|
189
|
+
width="10"
|
|
190
|
+
height="10"
|
|
191
|
+
viewBox="0 0 10 10"
|
|
192
|
+
className="text-info/40 shrink-0"
|
|
193
|
+
aria-hidden="true"
|
|
194
|
+
>
|
|
195
|
+
<path
|
|
196
|
+
d="M4 0 L6 0 L6 4 L10 4 L10 6 L6 6 L6 10 L4 10 L4 6 L0 6 L0 4 L4 4 Z"
|
|
197
|
+
fill="currentColor"
|
|
198
|
+
/>
|
|
199
|
+
</svg>
|
|
200
|
+
),
|
|
201
|
+
},
|
|
202
|
+
minimal: {
|
|
203
|
+
lineColor: "bg-border",
|
|
204
|
+
motif: null,
|
|
205
|
+
},
|
|
206
|
+
bold: {
|
|
207
|
+
lineColor: "bg-primary/40",
|
|
208
|
+
motif: (
|
|
209
|
+
<svg
|
|
210
|
+
width="10"
|
|
211
|
+
height="10"
|
|
212
|
+
viewBox="0 0 10 10"
|
|
213
|
+
className="text-primary/50 shrink-0"
|
|
214
|
+
aria-hidden="true"
|
|
215
|
+
>
|
|
216
|
+
<rect x="1" y="1" width="8" height="8" fill="currentColor" />
|
|
217
|
+
</svg>
|
|
218
|
+
),
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
function OrnamentalDivider({
|
|
223
|
+
variant,
|
|
224
|
+
className,
|
|
225
|
+
}: {
|
|
226
|
+
variant: CertificateVariant;
|
|
227
|
+
className?: string;
|
|
228
|
+
}) {
|
|
229
|
+
const config = DIVIDER_CONFIGS[variant];
|
|
230
|
+
return (
|
|
231
|
+
<div
|
|
232
|
+
className={cn(
|
|
233
|
+
"flex items-center justify-center gap-3 my-5 mx-auto max-w-70",
|
|
234
|
+
className,
|
|
235
|
+
)}
|
|
236
|
+
role="separator"
|
|
237
|
+
aria-hidden="true"
|
|
238
|
+
>
|
|
239
|
+
<span className={cn("flex-1 h-px", config.lineColor)} />
|
|
240
|
+
{config.motif}
|
|
241
|
+
<span className={cn("flex-1 h-px", config.lineColor)} />
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ─── Background glow ───────────────────────────────────────────
|
|
247
|
+
// Subtle radial glows using CSS vars for dark mode compat
|
|
248
|
+
|
|
249
|
+
const BACKGROUND_GLOW: Record<
|
|
250
|
+
CertificateVariant,
|
|
251
|
+
React.CSSProperties | null
|
|
252
|
+
> = {
|
|
253
|
+
classic: {
|
|
254
|
+
background:
|
|
255
|
+
"radial-gradient(ellipse at center, var(--color-warning) 0%, transparent 65%)",
|
|
256
|
+
opacity: 0.06,
|
|
257
|
+
},
|
|
258
|
+
modern: null,
|
|
259
|
+
elegant: {
|
|
260
|
+
background:
|
|
261
|
+
"radial-gradient(ellipse at center, var(--color-foreground) 0%, transparent 70%)",
|
|
262
|
+
opacity: 0.03,
|
|
263
|
+
},
|
|
264
|
+
academic: {
|
|
265
|
+
background:
|
|
266
|
+
"radial-gradient(ellipse at center, var(--color-info) 0%, transparent 65%)",
|
|
267
|
+
opacity: 0.05,
|
|
268
|
+
},
|
|
269
|
+
minimal: null,
|
|
270
|
+
bold: {
|
|
271
|
+
background:
|
|
272
|
+
"radial-gradient(ellipse at 30% 50%, var(--color-primary) 0%, transparent 50%), radial-gradient(ellipse at 70% 50%, var(--color-purple) 0%, transparent 50%)",
|
|
273
|
+
opacity: 0.06,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// ─── Typography maps ───────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
const ICON_COLORS: Record<CertificateVariant, string> = {
|
|
280
|
+
classic: "text-warning",
|
|
281
|
+
modern: "text-primary",
|
|
282
|
+
elegant: "text-foreground/60",
|
|
283
|
+
academic: "text-info",
|
|
284
|
+
minimal: "text-muted-foreground",
|
|
285
|
+
bold: "text-primary",
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const HEADING_STYLES: Record<CertificateVariant, string> = {
|
|
289
|
+
classic:
|
|
290
|
+
"uppercase tracking-[4px] text-base font-medium text-foreground/80 font-serif",
|
|
291
|
+
modern: "uppercase tracking-[3px] text-base font-medium text-primary/80",
|
|
292
|
+
elegant:
|
|
293
|
+
"uppercase tracking-[5px] text-sm font-light text-foreground/50",
|
|
294
|
+
academic:
|
|
295
|
+
"uppercase tracking-[3px] text-base font-semibold text-info/80",
|
|
296
|
+
minimal:
|
|
297
|
+
"uppercase tracking-[2px] text-sm font-normal text-muted-foreground",
|
|
298
|
+
bold: "uppercase tracking-[3px] text-lg font-black text-primary",
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const COURSE_TITLE_COLORS: Record<CertificateVariant, string> = {
|
|
302
|
+
classic: "text-warning",
|
|
303
|
+
modern: "text-primary",
|
|
304
|
+
elegant: "text-foreground/80",
|
|
305
|
+
academic: "text-info",
|
|
306
|
+
minimal: "text-foreground",
|
|
307
|
+
bold: "text-primary",
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const SERIF_VARIANTS: ReadonlySet<CertificateVariant> = new Set([
|
|
311
|
+
"classic",
|
|
312
|
+
"elegant",
|
|
313
|
+
"academic",
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
// ─── Main component ────────────────────────────────────────────
|
|
317
|
+
|
|
13
318
|
export function CertificateViewer({
|
|
14
319
|
recipientName,
|
|
15
320
|
courseTitle,
|
|
@@ -45,63 +350,111 @@ export function CertificateViewer({
|
|
|
45
350
|
}
|
|
46
351
|
}
|
|
47
352
|
|
|
353
|
+
const glowStyle = BACKGROUND_GLOW[variant];
|
|
354
|
+
|
|
48
355
|
return (
|
|
49
356
|
<div className={className} style={style}>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<p className="text-
|
|
92
|
-
|
|
93
|
-
|
|
357
|
+
{/* Outer certificate frame */}
|
|
358
|
+
<div className={outerFrameVariants({ variant })}>
|
|
359
|
+
{/* Inner certificate frame */}
|
|
360
|
+
<div className={innerFrameVariants({ variant })}>
|
|
361
|
+
{/* Corner ornaments */}
|
|
362
|
+
<CornerOrnaments variant={variant} />
|
|
363
|
+
|
|
364
|
+
{/* Background radial glow */}
|
|
365
|
+
{glowStyle && (
|
|
366
|
+
<div
|
|
367
|
+
className="absolute inset-0 pointer-events-none rounded-[inherit]"
|
|
368
|
+
style={glowStyle}
|
|
369
|
+
aria-hidden="true"
|
|
370
|
+
/>
|
|
371
|
+
)}
|
|
372
|
+
|
|
373
|
+
{/* Content */}
|
|
374
|
+
<div className="relative">
|
|
375
|
+
{/* Logo / Icon */}
|
|
376
|
+
{organizationLogo ? (
|
|
377
|
+
<img
|
|
378
|
+
src={organizationLogo}
|
|
379
|
+
alt={organizationName}
|
|
380
|
+
className="h-20 mb-4 mx-auto block"
|
|
381
|
+
/>
|
|
382
|
+
) : (
|
|
383
|
+
<Award
|
|
384
|
+
size={64}
|
|
385
|
+
className={cn("mx-auto mb-6", ICON_COLORS[variant])}
|
|
386
|
+
/>
|
|
387
|
+
)}
|
|
388
|
+
|
|
389
|
+
{/* Certificate heading */}
|
|
390
|
+
<p className={cn("mb-2", HEADING_STYLES[variant])}>
|
|
391
|
+
Certificate of Completion
|
|
392
|
+
</p>
|
|
393
|
+
|
|
394
|
+
{/* Top ornamental divider */}
|
|
395
|
+
<OrnamentalDivider variant={variant} />
|
|
396
|
+
|
|
397
|
+
{/* Certify text */}
|
|
398
|
+
<p className="text-base text-foreground/70 mb-2">
|
|
399
|
+
This is to certify that
|
|
400
|
+
</p>
|
|
94
401
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
402
|
+
{/* Recipient name */}
|
|
403
|
+
<p
|
|
404
|
+
className={cn(
|
|
405
|
+
"text-4xl font-bold mb-4 text-foreground",
|
|
406
|
+
SERIF_VARIANTS.has(variant) && "font-serif",
|
|
407
|
+
)}
|
|
408
|
+
>
|
|
409
|
+
{recipientName}
|
|
410
|
+
</p>
|
|
411
|
+
|
|
412
|
+
{/* Completion text */}
|
|
413
|
+
<p className="text-base text-foreground/70 mb-2">
|
|
414
|
+
has successfully completed
|
|
415
|
+
</p>
|
|
416
|
+
|
|
417
|
+
{/* Course title */}
|
|
418
|
+
<p
|
|
419
|
+
className={cn(
|
|
420
|
+
"text-2xl font-semibold mb-4",
|
|
421
|
+
COURSE_TITLE_COLORS[variant],
|
|
422
|
+
)}
|
|
423
|
+
>
|
|
424
|
+
{courseTitle}
|
|
425
|
+
</p>
|
|
426
|
+
|
|
427
|
+
{/* Issuance line */}
|
|
428
|
+
<p className="text-base text-foreground/60 mb-6">
|
|
429
|
+
Issued by {organizationName} on {formattedDate}
|
|
430
|
+
</p>
|
|
431
|
+
|
|
432
|
+
{/* Signatory */}
|
|
433
|
+
{signatory && (
|
|
434
|
+
<div className="mt-6 mb-4">
|
|
435
|
+
<OrnamentalDivider variant={variant} />
|
|
436
|
+
<p className="font-semibold text-base text-foreground">
|
|
437
|
+
{signatory.name}
|
|
438
|
+
</p>
|
|
439
|
+
<p className="text-sm text-muted-foreground">
|
|
440
|
+
{signatory.title}
|
|
441
|
+
</p>
|
|
442
|
+
</div>
|
|
443
|
+
)}
|
|
444
|
+
|
|
445
|
+
{/* Certificate ID */}
|
|
446
|
+
{certificateId && (
|
|
447
|
+
<span className="block text-xs text-muted-foreground mt-4">
|
|
448
|
+
Certificate ID: {certificateId}
|
|
449
|
+
</span>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
100
453
|
</div>
|
|
101
454
|
|
|
102
455
|
{/* Actions */}
|
|
103
456
|
{showActions && (
|
|
104
|
-
<div className="flex justify-center gap-
|
|
457
|
+
<div className="flex justify-center gap-3 mt-6">
|
|
105
458
|
<Button variant="outline" onClick={handlePrint}>
|
|
106
459
|
<Printer size={16} /> Print
|
|
107
460
|
</Button>
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
/** Available certificate visual styles */
|
|
2
|
+
export type CertificateVariant =
|
|
3
|
+
| "classic"
|
|
4
|
+
| "modern"
|
|
5
|
+
| "elegant"
|
|
6
|
+
| "academic"
|
|
7
|
+
| "minimal"
|
|
8
|
+
| "bold";
|
|
1
9
|
|
|
2
10
|
/**
|
|
3
11
|
* CertificateViewer section — a printable completion certificate.
|
|
4
12
|
*
|
|
5
13
|
* Displays a certificate with recipient details, course information,
|
|
6
|
-
* signatory, and verification ID. Supports
|
|
7
|
-
* classic, modern, and
|
|
14
|
+
* signatory, and verification ID. Supports six visual variants:
|
|
15
|
+
* classic, modern, elegant, academic, minimal, and bold.
|
|
8
16
|
*
|
|
9
17
|
* @example
|
|
10
18
|
* <CertificateViewer
|
|
@@ -12,7 +20,7 @@
|
|
|
12
20
|
* courseTitle="Advanced React"
|
|
13
21
|
* completionDate="2025-03-01"
|
|
14
22
|
* organizationName="HydraLMS Academy"
|
|
15
|
-
* variant="
|
|
23
|
+
* variant="elegant"
|
|
16
24
|
* />
|
|
17
25
|
*/
|
|
18
26
|
export interface CertificateViewerProps {
|
|
@@ -30,8 +38,8 @@ export interface CertificateViewerProps {
|
|
|
30
38
|
signatory?: { name: string; title: string };
|
|
31
39
|
/** Unique certificate ID */
|
|
32
40
|
certificateId?: string;
|
|
33
|
-
/** Certificate template variant */
|
|
34
|
-
variant?:
|
|
41
|
+
/** Certificate template variant @default "classic" */
|
|
42
|
+
variant?: CertificateVariant;
|
|
35
43
|
/** Whether to show print/download actions */
|
|
36
44
|
showActions?: boolean;
|
|
37
45
|
/** Called when print is triggered */
|
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { CurriculumTree } from "../../curriculum";
|
|
3
|
-
import type { CurriculumItem } from "../../curriculum/types";
|
|
4
3
|
import { Progress } from "../../ui/progress";
|
|
4
|
+
import { Separator } from "../../ui/separator";
|
|
5
|
+
import { flattenLeaves } from "../../utils/flatten-leaves";
|
|
5
6
|
import type { CourseOutlineProps } from "./types";
|
|
6
7
|
import { cn } from "../../lib/utils";
|
|
7
8
|
|
|
8
|
-
function flattenLeaves(items: CurriculumItem[]): string[] {
|
|
9
|
-
const leaves: string[] = [];
|
|
10
|
-
for (const item of items) {
|
|
11
|
-
if (!item.children || item.children.length === 0) {
|
|
12
|
-
leaves.push(item.uid);
|
|
13
|
-
} else {
|
|
14
|
-
leaves.push(...flattenLeaves(item.children));
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return leaves;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
9
|
export function CourseOutline({
|
|
21
10
|
items,
|
|
22
11
|
progress,
|
|
@@ -48,7 +37,7 @@ export function CourseOutline({
|
|
|
48
37
|
return (
|
|
49
38
|
<div className={cn(className)} style={style}>
|
|
50
39
|
{(courseTitle || showOverallProgress) && (
|
|
51
|
-
<div className="px-2 pt-2 pb-
|
|
40
|
+
<div className="px-2 pt-2 pb-2">
|
|
52
41
|
{courseTitle && (
|
|
53
42
|
<p className={cn("font-semibold text-sm text-foreground", showOverallProgress && "mb-1")}>
|
|
54
43
|
{courseTitle}
|
|
@@ -64,6 +53,7 @@ export function CourseOutline({
|
|
|
64
53
|
)}
|
|
65
54
|
</div>
|
|
66
55
|
)}
|
|
56
|
+
{(courseTitle || showOverallProgress) && <Separator />}
|
|
67
57
|
<CurriculumTree
|
|
68
58
|
items={items}
|
|
69
59
|
progress={progress}
|
|
@@ -2,7 +2,9 @@ import { useMemo, useState } from "react";
|
|
|
2
2
|
import { CheckCircle, Heart, MessageSquare, Reply } from "lucide-react";
|
|
3
3
|
import { PostCard } from "../../social";
|
|
4
4
|
import { Button } from "../../ui/button";
|
|
5
|
-
import {
|
|
5
|
+
import { RichTextEditor } from "../../ui/rich-text-editor";
|
|
6
|
+
import { isEmptyHtml } from "../../utils/is-empty-html";
|
|
7
|
+
import { Badge } from "../../ui/badge";
|
|
6
8
|
import { Separator } from "../../ui/separator";
|
|
7
9
|
import { Card, CardContent } from "../../ui/card";
|
|
8
10
|
import { Tooltip, TooltipTrigger, TooltipContent } from "../../ui/tooltip";
|
|
@@ -48,8 +50,8 @@ export function DiscussionThread({
|
|
|
48
50
|
}, [replies, rootPost.uid, sortOrder]);
|
|
49
51
|
|
|
50
52
|
function handleSubmitReply(parentUid: string) {
|
|
51
|
-
if (
|
|
52
|
-
onReply(parentUid, replyContent
|
|
53
|
+
if (isEmptyHtml(replyContent)) return;
|
|
54
|
+
onReply(parentUid, replyContent);
|
|
53
55
|
setReplyContent("");
|
|
54
56
|
setReplyingToUid(null);
|
|
55
57
|
}
|
|
@@ -132,18 +134,19 @@ export function DiscussionThread({
|
|
|
132
134
|
className="mb-2"
|
|
133
135
|
style={{ marginLeft: `${(effectiveDepth + 1) * 16}px` }}
|
|
134
136
|
>
|
|
135
|
-
<CardContent className="
|
|
136
|
-
<
|
|
137
|
+
<CardContent className="py-4">
|
|
138
|
+
<RichTextEditor
|
|
137
139
|
className="min-h-15 mb-2"
|
|
138
140
|
placeholder="Write a reply..."
|
|
139
141
|
value={replyContent}
|
|
140
|
-
onChange={(
|
|
142
|
+
onChange={(html) => setReplyContent(html)}
|
|
143
|
+
variant="minimal"
|
|
141
144
|
/>
|
|
142
145
|
<div className="flex items-center gap-2">
|
|
143
146
|
<Button
|
|
144
147
|
size="sm"
|
|
145
148
|
onClick={() => handleSubmitReply(post.uid)}
|
|
146
|
-
disabled={
|
|
149
|
+
disabled={isEmptyHtml(replyContent)}
|
|
147
150
|
>
|
|
148
151
|
Post Reply
|
|
149
152
|
</Button>
|
|
@@ -170,12 +173,12 @@ export function DiscussionThread({
|
|
|
170
173
|
|
|
171
174
|
return (
|
|
172
175
|
<div className={className} style={style}>
|
|
173
|
-
<div className="flex items-center gap-
|
|
176
|
+
<div className="flex items-center gap-2 mb-2">
|
|
174
177
|
<MessageSquare size={20} className="text-foreground shrink-0" />
|
|
175
178
|
<span className="text-lg font-semibold text-foreground">{title}</span>
|
|
176
|
-
<
|
|
179
|
+
<Badge variant="muted" className="text-xs">
|
|
177
180
|
{replies.length} {replies.length === 1 ? "reply" : "replies"}
|
|
178
|
-
</
|
|
181
|
+
</Badge>
|
|
179
182
|
</div>
|
|
180
183
|
|
|
181
184
|
<Separator className="mb-2" />
|