@hydralms/components 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/StudentProfile-BVfZMbnV.cjs +1 -0
- package/dist/StudentProfile-DeMxdrL3.js +3275 -0
- package/dist/assessment-toolbar/assessment-toolbar.d.ts +1 -1
- package/dist/assessment-toolbar/index.d.ts +5 -1
- package/dist/assessment-toolbar/question-header-bar.d.ts +2 -0
- package/dist/assessment-toolbar/question-materials-drawer.d.ts +2 -0
- package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
- package/dist/assessment-toolbar/timer-display.d.ts +1 -1
- package/dist/assessment-toolbar/types.d.ts +52 -4
- package/dist/assessment-toolbar/use-countdown.d.ts +43 -0
- package/dist/common/index.d.ts +3 -1
- package/dist/common/pagination.d.ts +26 -0
- package/dist/common/stepper.d.ts +6 -0
- package/dist/common/types.d.ts +38 -0
- package/dist/components.css +1 -1
- package/dist/content/attachment-list.d.ts +6 -0
- package/dist/content/audio-player.d.ts +22 -0
- package/dist/content/code-block.d.ts +30 -0
- package/dist/content/content-block.d.ts +1 -1
- package/dist/content/embed-block.d.ts +28 -0
- package/dist/content/index.d.ts +8 -1
- package/dist/content/types.d.ts +63 -0
- package/dist/curriculum/course-card.d.ts +51 -0
- package/dist/curriculum/curriculum-item.d.ts +1 -1
- package/dist/curriculum/index.d.ts +2 -0
- package/dist/curriculum/types.d.ts +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +597 -308
- package/dist/license/HydraContext.d.ts +16 -0
- package/dist/license/ProBadge.d.ts +6 -0
- package/dist/license/index.d.ts +7 -0
- package/dist/license/tiers.d.ts +3 -0
- package/dist/license/useHydraLicense.d.ts +6 -0
- package/dist/license/validate.d.ts +13 -0
- package/dist/license/withProGate.d.ts +6 -0
- package/dist/modules/AssignmentModule/AssignmentModule.d.ts +5 -0
- package/dist/modules/AssignmentModule/types.d.ts +69 -0
- package/dist/modules/CertificateModule/CertificateModule.d.ts +5 -0
- package/dist/modules/CertificateModule/types.d.ts +51 -0
- package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
- package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
- package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
- package/dist/modules/DiscussionModule/DiscussionModule.d.ts +5 -0
- package/dist/modules/DiscussionModule/types.d.ts +47 -0
- package/dist/modules/ExamModule/ExamModule.d.ts +5 -0
- package/dist/modules/ExamModule/types.d.ts +55 -0
- package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
- package/dist/modules/FlashcardLab/types.d.ts +2 -0
- package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +5 -0
- package/dist/modules/GradeCenterModule/types.d.ts +56 -0
- package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
- package/dist/modules/QuizModule/types.d.ts +10 -14
- package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
- package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
- package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
- package/dist/modules/StudentProfileModule/types.d.ts +43 -0
- package/dist/modules/SurveyModule/SurveyModule.d.ts +5 -0
- package/dist/modules/SurveyModule/types.d.ts +51 -0
- package/dist/modules/_shared/assessment-intro.d.ts +16 -0
- package/dist/modules/_shared/assessment-results.d.ts +23 -0
- package/dist/modules/_shared/types.d.ts +10 -0
- package/dist/modules/_shared/use-timer.d.ts +9 -0
- package/dist/modules/index.d.ts +18 -0
- package/dist/modules.cjs +1 -0
- package/dist/modules.js +1834 -0
- package/dist/progress/achievement-badge.d.ts +6 -0
- package/dist/progress/activity-timeline.d.ts +6 -0
- package/dist/progress/index.d.ts +4 -1
- package/dist/progress/stat-card.d.ts +1 -1
- package/dist/progress/streak-badge.d.ts +6 -0
- package/dist/progress/types.d.ts +99 -0
- package/dist/provider/HydraProvider.d.ts +5 -1
- package/dist/questions/choice.d.ts +1 -1
- package/dist/questions/confidence-indicator.d.ts +37 -0
- package/dist/questions/essay.d.ts +2 -2
- package/dist/questions/fill-in-the-blank.d.ts +1 -1
- package/dist/questions/hotspot.d.ts +21 -0
- package/dist/questions/index.d.ts +11 -1
- package/dist/questions/inline-choice.d.ts +21 -0
- package/dist/questions/matching.d.ts +22 -0
- package/dist/questions/multiple-choice.d.ts +1 -1
- package/dist/questions/numeric.d.ts +11 -0
- package/dist/questions/ordering.d.ts +12 -0
- package/dist/questions/question-renderer.d.ts +1 -1
- package/dist/questions/scenario.d.ts +23 -0
- package/dist/questions/scoring.d.ts +22 -0
- package/dist/questions/spreadsheet.d.ts +29 -0
- package/dist/questions/true-false.d.ts +1 -1
- package/dist/questions/types.d.ts +106 -1
- package/dist/questions/use-drag-reorder.d.ts +17 -0
- package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
- package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
- package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
- package/dist/sections/AssessmentReview/types.d.ts +6 -0
- package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
- package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
- package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
- package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
- package/dist/sections/CertificateViewer/types.d.ts +13 -5
- package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
- package/dist/sections/CourseCatalog/types.d.ts +80 -0
- package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
- package/dist/sections/CourseOutline/types.d.ts +6 -0
- package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
- package/dist/sections/DiscussionThread/types.d.ts +6 -0
- package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
- package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
- package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
- package/dist/sections/ExamSession/types.d.ts +12 -1
- package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
- package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
- package/dist/sections/ForumBoard/ForumBoard.d.ts +8 -0
- package/dist/sections/ForumBoard/types.d.ts +78 -0
- package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
- package/dist/sections/GradebookTable/types.d.ts +14 -0
- package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
- package/dist/sections/LecturePlayer/types.d.ts +8 -0
- package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
- package/dist/sections/LessonPage/types.d.ts +6 -0
- package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
- package/dist/sections/PracticeQuiz/types.d.ts +6 -0
- package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
- package/dist/sections/ProgressDashboard/types.d.ts +6 -0
- package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
- package/dist/sections/QuizSession/types.d.ts +12 -1
- package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
- package/dist/sections/RequirementsChecklist/types.d.ts +43 -0
- package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
- package/dist/sections/ResourceLibrary/types.d.ts +15 -1
- package/dist/sections/RubricView/RubricView.d.ts +9 -0
- package/dist/sections/RubricView/types.d.ts +56 -0
- package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
- package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
- package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
- package/dist/sections/StudentProfile/types.d.ts +98 -0
- package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
- package/dist/sections/SurveyForm/types.d.ts +6 -0
- package/dist/sections/_shared/merge-answers.d.ts +9 -0
- package/dist/sections/_shared/section-shell.d.ts +20 -0
- package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
- package/dist/sections/index.d.ts +13 -1
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +282 -1786
- package/dist/social/post-card.d.ts +1 -1
- package/dist/tabs-BsfVo2Bl.cjs +173 -0
- package/dist/tabs-BuY1iNJE.js +22305 -0
- package/dist/ui/alert.d.ts +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/button.d.ts +1 -1
- package/dist/ui/drawer.d.ts +84 -0
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/progress.d.ts +1 -1
- package/dist/ui/rich-text-editor.d.ts +32 -0
- package/dist/ui/rich-text-toolbar.d.ts +8 -0
- package/dist/ui/toast.d.ts +43 -0
- package/dist/utils/array-utils.d.ts +4 -0
- package/dist/utils/debounce.d.ts +5 -1
- package/dist/utils/flatten-leaves.d.ts +6 -0
- package/dist/utils/format-file-size.d.ts +1 -0
- package/dist/utils/format-timestamp.d.ts +1 -0
- package/dist/utils/is-empty-html.d.ts +5 -0
- package/dist/utils/pick-palette-color.d.ts +19 -0
- package/dist/utils/shuffle.d.ts +1 -0
- package/dist/utils/string-utils.d.ts +12 -0
- package/dist/video/types.d.ts +15 -0
- package/dist/video/video-bookmark.d.ts +1 -1
- package/dist/video/video-player.d.ts +1 -1
- package/dist/video/video-playlist-item.d.ts +1 -1
- package/dist/withProGate-BWqcKdPM.js +137 -0
- package/dist/withProGate-DX6XqKLp.cjs +1 -0
- package/package.json +40 -137
- package/src/assessment-toolbar/assessment-toolbar.tsx +54 -49
- package/src/assessment-toolbar/index.ts +6 -0
- package/src/assessment-toolbar/question-header-bar.tsx +61 -0
- package/src/assessment-toolbar/question-materials-drawer.tsx +55 -0
- package/src/assessment-toolbar/question-navigator.tsx +13 -36
- package/src/assessment-toolbar/timer-display.tsx +6 -5
- package/src/assessment-toolbar/types.ts +54 -4
- package/src/assessment-toolbar/use-countdown.ts +153 -0
- package/src/common/empty-state.tsx +1 -0
- package/src/common/index.ts +5 -0
- package/src/common/pagination.tsx +135 -0
- package/src/common/search-input.tsx +8 -6
- package/src/common/stepper.tsx +100 -0
- package/src/common/types.ts +41 -0
- package/src/content/attachment-list.tsx +92 -0
- package/src/content/audio-player.tsx +196 -0
- package/src/content/code-block.tsx +113 -0
- package/src/content/content-block.tsx +68 -2
- package/src/content/embed-block.tsx +78 -0
- package/src/content/file-upload-zone.tsx +11 -6
- package/src/content/index.ts +9 -0
- package/src/content/types.ts +46 -0
- package/src/curriculum/course-card.tsx +199 -0
- package/src/curriculum/curriculum-item.tsx +9 -5
- package/src/curriculum/curriculum-tree.tsx +20 -13
- package/src/curriculum/index.ts +2 -0
- package/src/curriculum/types.ts +2 -2
- package/src/feedback/feedback-banner.tsx +12 -14
- package/src/flashcards/flashcard-deck.tsx +1 -9
- package/src/flashcards/flashcard.tsx +29 -9
- package/src/index.ts +3 -0
- package/src/license/HydraContext.tsx +62 -0
- package/src/license/ProBadge.tsx +43 -0
- package/src/license/index.ts +7 -0
- package/src/license/tiers.ts +24 -0
- package/src/license/useHydraLicense.ts +10 -0
- package/src/license/validate.ts +90 -0
- package/src/license/withProGate.tsx +21 -0
- package/src/modules/AssignmentModule/AssignmentModule.tsx +314 -0
- package/src/modules/AssignmentModule/types.ts +77 -0
- package/src/modules/CertificateModule/CertificateModule.tsx +173 -0
- package/src/modules/CertificateModule/types.ts +49 -0
- package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
- package/src/modules/CourseCatalogModule/types.ts +47 -0
- package/src/modules/CoursePlayer/CoursePlayer.tsx +80 -69
- package/src/modules/DiscussionModule/DiscussionModule.tsx +145 -0
- package/src/modules/DiscussionModule/types.ts +54 -0
- package/src/modules/ExamModule/ExamModule.tsx +151 -0
- package/src/modules/ExamModule/types.ts +57 -0
- package/src/modules/FlashcardLab/FlashcardLab.tsx +39 -21
- package/src/modules/FlashcardLab/types.ts +2 -0
- package/src/modules/GradeCenterModule/GradeCenterModule.tsx +174 -0
- package/src/modules/GradeCenterModule/types.ts +65 -0
- package/src/modules/QuizModule/QuizModule.tsx +58 -178
- package/src/modules/QuizModule/types.ts +10 -15
- package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
- package/src/modules/StudentDashboardModule/types.ts +56 -0
- package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
- package/src/modules/StudentProfileModule/types.ts +45 -0
- package/src/modules/SurveyModule/SurveyModule.tsx +185 -0
- package/src/modules/SurveyModule/types.ts +53 -0
- package/src/modules/_shared/assessment-intro.tsx +75 -0
- package/src/modules/_shared/assessment-results.tsx +133 -0
- package/src/modules/_shared/types.ts +11 -0
- package/src/modules/_shared/use-timer.ts +49 -0
- package/src/modules/index.ts +33 -0
- package/src/progress/achievement-badge.tsx +52 -0
- package/src/progress/activity-timeline.tsx +84 -0
- package/src/progress/grade-indicator.tsx +9 -1
- package/src/progress/index.ts +7 -0
- package/src/progress/progress-ring.tsx +2 -1
- package/src/progress/stat-card.tsx +37 -18
- package/src/progress/streak-badge.tsx +35 -0
- package/src/progress/types.ts +103 -0
- package/src/provider/HydraProvider.tsx +15 -6
- package/src/questions/choice.tsx +19 -14
- package/src/questions/confidence-indicator.tsx +107 -0
- package/src/questions/essay.tsx +28 -28
- package/src/questions/fill-in-the-blank.tsx +20 -19
- package/src/questions/hotspot.tsx +154 -0
- package/src/questions/index.ts +18 -0
- package/src/questions/inline-choice.tsx +152 -0
- package/src/questions/matching.tsx +229 -0
- package/src/questions/multiple-choice.tsx +19 -14
- package/src/questions/numeric.tsx +106 -0
- package/src/questions/ordering.tsx +167 -0
- package/src/questions/question-renderer.tsx +24 -2
- package/src/questions/scenario.tsx +140 -0
- package/src/questions/scoring.ts +201 -0
- package/src/questions/spreadsheet.tsx +260 -0
- package/src/questions/true-false.tsx +19 -14
- package/src/questions/types.ts +123 -1
- package/src/questions/use-drag-reorder.ts +80 -0
- package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +66 -23
- package/src/sections/AnnouncementFeed/types.ts +15 -1
- package/src/sections/AssessmentReview/AssessmentReview.tsx +50 -2
- package/src/sections/AssessmentReview/types.ts +6 -0
- package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +44 -6
- package/src/sections/AssignmentSubmission/types.ts +6 -0
- package/src/sections/CertificateViewer/CertificateViewer.tsx +215 -60
- package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
- package/src/sections/CertificateViewer/types.ts +19 -5
- package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
- package/src/sections/CourseCatalog/types.ts +76 -0
- package/src/sections/CourseOutline/CourseOutline.tsx +45 -14
- package/src/sections/CourseOutline/types.ts +6 -0
- package/src/sections/DiscussionThread/DiscussionThread.tsx +55 -11
- package/src/sections/DiscussionThread/types.ts +6 -0
- package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
- package/src/sections/EnrollmentWizard/types.ts +65 -0
- package/src/sections/ExamSession/ExamSession.tsx +125 -82
- package/src/sections/ExamSession/types.ts +12 -1
- package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
- package/src/sections/FlashcardStudySession/types.ts +6 -0
- package/src/sections/ForumBoard/ForumBoard.tsx +342 -0
- package/src/sections/ForumBoard/types.ts +81 -0
- package/src/sections/GradebookTable/GradebookTable.tsx +55 -2
- package/src/sections/GradebookTable/types.ts +14 -0
- package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
- package/src/sections/LecturePlayer/types.ts +8 -0
- package/src/sections/LessonPage/LessonPage.tsx +40 -13
- package/src/sections/LessonPage/types.ts +6 -0
- package/src/sections/PracticeQuiz/PracticeQuiz.tsx +119 -98
- package/src/sections/PracticeQuiz/types.ts +6 -0
- package/src/sections/ProgressDashboard/ProgressDashboard.tsx +121 -67
- package/src/sections/ProgressDashboard/types.ts +6 -0
- package/src/sections/QuizSession/QuizSession.tsx +115 -67
- package/src/sections/QuizSession/types.ts +12 -1
- package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +147 -0
- package/src/sections/RequirementsChecklist/types.ts +44 -0
- package/src/sections/ResourceLibrary/ResourceLibrary.tsx +68 -17
- package/src/sections/ResourceLibrary/types.ts +15 -1
- package/src/sections/RubricView/RubricView.tsx +174 -0
- package/src/sections/RubricView/types.ts +58 -0
- package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +58 -23
- package/src/sections/ScrollableQuiz/types.ts +6 -0
- package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
- package/src/sections/StudentProfile/types.ts +99 -0
- package/src/sections/SurveyForm/SurveyForm.tsx +40 -10
- package/src/sections/SurveyForm/types.ts +6 -0
- package/src/sections/_shared/merge-answers.ts +22 -0
- package/src/sections/_shared/section-shell.tsx +64 -0
- package/src/sections/_shared/use-assessment-session.ts +125 -0
- package/src/sections/index.ts +42 -1
- package/src/social/post-card.tsx +8 -19
- package/src/social/user-avatar.tsx +10 -5
- package/src/styles/globals.css +52 -41
- package/src/ui/badge.tsx +8 -0
- package/src/ui/drawer.tsx +600 -0
- package/src/ui/index.ts +21 -0
- package/src/ui/progress.tsx +4 -0
- package/src/ui/rich-text-editor.tsx +119 -0
- package/src/ui/rich-text-toolbar.tsx +157 -0
- package/src/ui/toast.tsx +170 -0
- package/src/utils/array-utils.ts +17 -0
- package/src/utils/debounce.ts +8 -2
- package/src/utils/flatten-leaves.ts +17 -0
- package/src/utils/format-file-size.ts +5 -0
- package/src/utils/format-timestamp.ts +13 -0
- package/src/utils/is-empty-html.ts +7 -0
- package/src/utils/pick-palette-color.ts +33 -0
- package/src/utils/shuffle.ts +8 -0
- package/src/utils/string-utils.ts +30 -0
- package/src/video/types.ts +16 -0
- package/src/video/video-bookmark.tsx +4 -3
- package/src/video/video-chapter-list.tsx +9 -4
- package/src/video/video-player.tsx +24 -5
- package/src/video/video-playlist-item.tsx +8 -3
- package/src/video/video-thumbnail-card.tsx +4 -0
- package/src/video/video-transcript.tsx +8 -5
- package/dist/table-BrS5cDQu.js +0 -2510
- package/dist/table-D6AkBBEo.cjs +0 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// ─── Shared License Validation ──────────────────────────────
|
|
2
|
+
|
|
3
|
+
const CACHE_KEY = 'hydra_key_v';
|
|
4
|
+
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
5
|
+
|
|
6
|
+
export type KeyResult = 'valid-pro' | 'valid-free' | 'invalid' | 'no-key' | 'skip';
|
|
7
|
+
|
|
8
|
+
export interface ValidationResponse {
|
|
9
|
+
valid: boolean;
|
|
10
|
+
plan: 'pro' | 'enterprise' | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Read a cached validation result from sessionStorage. */
|
|
14
|
+
function readCache(rawKey: string): KeyResult | null {
|
|
15
|
+
try {
|
|
16
|
+
const cached = sessionStorage.getItem(CACHE_KEY);
|
|
17
|
+
if (cached) {
|
|
18
|
+
const { result, ts, key } = JSON.parse(cached) as { result: KeyResult; ts: number; key: string };
|
|
19
|
+
if (key === rawKey && Date.now() - ts < CACHE_TTL) return result;
|
|
20
|
+
}
|
|
21
|
+
} catch { /* sessionStorage unavailable */ }
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Write a validation result to sessionStorage cache. */
|
|
26
|
+
function writeCache(rawKey: string, result: KeyResult): void {
|
|
27
|
+
try {
|
|
28
|
+
sessionStorage.setItem(CACHE_KEY, JSON.stringify({ result, ts: Date.now(), key: rawKey }));
|
|
29
|
+
} catch { /* full or unavailable */ }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** On network failure, trust a previously cached valid-pro result. */
|
|
33
|
+
function fallbackFromCache(rawKey: string): KeyResult {
|
|
34
|
+
try {
|
|
35
|
+
const cached = sessionStorage.getItem(CACHE_KEY);
|
|
36
|
+
if (cached) {
|
|
37
|
+
const { result, key } = JSON.parse(cached) as { result: KeyResult; key: string };
|
|
38
|
+
if (key === rawKey && result === 'valid-pro') return 'valid-pro';
|
|
39
|
+
}
|
|
40
|
+
} catch { /* ignore */ }
|
|
41
|
+
return 'invalid';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate a license key against the validation endpoint.
|
|
46
|
+
* @param rawKey - The license key string, or null if not provided.
|
|
47
|
+
* @param validateUrl - The validation endpoint URL. Empty string skips validation (dev mode).
|
|
48
|
+
*/
|
|
49
|
+
export async function validateKey(rawKey: string | null, validateUrl: string): Promise<KeyResult> {
|
|
50
|
+
// No validation URL configured (local dev) — skip entirely
|
|
51
|
+
if (!validateUrl) return 'skip';
|
|
52
|
+
|
|
53
|
+
// No key provided
|
|
54
|
+
if (!rawKey) return 'no-key';
|
|
55
|
+
|
|
56
|
+
// Check sessionStorage cache
|
|
57
|
+
const cached = readCache(rawKey);
|
|
58
|
+
if (cached) return cached;
|
|
59
|
+
|
|
60
|
+
// Network validation
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(`${validateUrl}?key=${encodeURIComponent(rawKey)}`, {
|
|
63
|
+
method: 'GET',
|
|
64
|
+
signal: AbortSignal.timeout(4000),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!res.ok) return fallbackFromCache(rawKey);
|
|
68
|
+
|
|
69
|
+
const data = await res.json() as ValidationResponse;
|
|
70
|
+
let result: KeyResult;
|
|
71
|
+
|
|
72
|
+
if (!data.valid) {
|
|
73
|
+
result = 'invalid';
|
|
74
|
+
} else if (data.plan === 'pro' || data.plan === 'enterprise') {
|
|
75
|
+
result = 'valid-pro';
|
|
76
|
+
} else {
|
|
77
|
+
result = 'valid-free';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
writeCache(rawKey, result);
|
|
81
|
+
return result;
|
|
82
|
+
} catch {
|
|
83
|
+
return fallbackFromCache(rawKey);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Whether a watermark should be shown for the given key result. */
|
|
88
|
+
export function shouldShowWatermark(result: KeyResult): boolean {
|
|
89
|
+
return result === 'no-key' || result === 'invalid' || result === 'valid-free';
|
|
90
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useHydraLicense } from './useHydraLicense';
|
|
3
|
+
import { ProBadge } from './ProBadge';
|
|
4
|
+
|
|
5
|
+
/** Higher-order component that wraps a pro-tier module with ProBadge when unlicensed. */
|
|
6
|
+
export function withProGate<P extends object>(
|
|
7
|
+
Component: React.ComponentType<P>,
|
|
8
|
+
moduleName: string,
|
|
9
|
+
) {
|
|
10
|
+
const Gated = (props: P) => {
|
|
11
|
+
const { isPro } = useHydraLicense();
|
|
12
|
+
if (isPro) return <Component {...props} />;
|
|
13
|
+
return (
|
|
14
|
+
<ProBadge feature={moduleName}>
|
|
15
|
+
<Component {...props} />
|
|
16
|
+
</ProBadge>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
Gated.displayName = `ProGated(${moduleName})`;
|
|
20
|
+
return Gated;
|
|
21
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useMemo } from "react";
|
|
2
|
+
import {
|
|
3
|
+
FileEdit,
|
|
4
|
+
CheckCircle2,
|
|
5
|
+
Play,
|
|
6
|
+
ArrowLeft,
|
|
7
|
+
FileText,
|
|
8
|
+
Link as LinkIcon,
|
|
9
|
+
Paperclip,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { AssignmentSubmission } from "../../sections/AssignmentSubmission/AssignmentSubmission";
|
|
12
|
+
import { RubricView } from "../../sections/RubricView/RubricView";
|
|
13
|
+
import { GradeIndicator } from "../../progress/grade-indicator";
|
|
14
|
+
import { StatusBadge } from "../../common/status-badge";
|
|
15
|
+
import { DueDateDisplay } from "../../common/due-date-display";
|
|
16
|
+
import { Button } from "../../ui/button";
|
|
17
|
+
import { Badge } from "../../ui/badge";
|
|
18
|
+
import { Card, CardContent } from "../../ui/card";
|
|
19
|
+
import { Alert, AlertDescription } from "../../ui/alert";
|
|
20
|
+
import { Separator } from "../../ui/separator";
|
|
21
|
+
import { cn } from "../../lib/utils";
|
|
22
|
+
import type { SubmissionData } from "../../sections/AssignmentSubmission/types";
|
|
23
|
+
import { withProGate } from "../../license/withProGate";
|
|
24
|
+
import type { AssignmentModuleProps } from "./types";
|
|
25
|
+
|
|
26
|
+
type InternalStep =
|
|
27
|
+
| { tag: "instructions" }
|
|
28
|
+
| { tag: "work" }
|
|
29
|
+
| { tag: "confirmation"; submission: SubmissionData };
|
|
30
|
+
|
|
31
|
+
const TYPE_LABELS: Record<string, string> = {
|
|
32
|
+
text: "Text Entry",
|
|
33
|
+
file: "File Upload",
|
|
34
|
+
url: "URL Submission",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* AssignmentModule — a complete assignment experience with instructions,
|
|
39
|
+
* submission work area, and confirmation/grade review.
|
|
40
|
+
*
|
|
41
|
+
* Steps: Instructions → Work (AssignmentSubmission) → Confirmation.
|
|
42
|
+
*/
|
|
43
|
+
function AssignmentModuleBase({
|
|
44
|
+
title,
|
|
45
|
+
instructions,
|
|
46
|
+
dueDate,
|
|
47
|
+
maxScore,
|
|
48
|
+
submissionTypes = ["text", "file"],
|
|
49
|
+
fileConstraints,
|
|
50
|
+
rubric,
|
|
51
|
+
existingSubmission,
|
|
52
|
+
status = "not_started",
|
|
53
|
+
grade,
|
|
54
|
+
onSubmit,
|
|
55
|
+
onComplete,
|
|
56
|
+
onSaveDraft,
|
|
57
|
+
readOnly = false,
|
|
58
|
+
className,
|
|
59
|
+
style,
|
|
60
|
+
}: AssignmentModuleProps) {
|
|
61
|
+
const initialStep: InternalStep =
|
|
62
|
+
status === "submitted" || status === "graded"
|
|
63
|
+
? {
|
|
64
|
+
tag: "confirmation",
|
|
65
|
+
submission: existingSubmission ?? {},
|
|
66
|
+
}
|
|
67
|
+
: { tag: "instructions" };
|
|
68
|
+
|
|
69
|
+
const [step, setStep] = useState<InternalStep>(initialStep);
|
|
70
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
74
|
+
}, [step.tag]);
|
|
75
|
+
|
|
76
|
+
const rubricMaxScore = useMemo(() => {
|
|
77
|
+
if (!Array.isArray(rubric)) return 0;
|
|
78
|
+
return rubric.reduce(
|
|
79
|
+
(sum, c) => sum + Math.max(...c.levels.map((l) => l.points)),
|
|
80
|
+
0
|
|
81
|
+
);
|
|
82
|
+
}, [rubric]);
|
|
83
|
+
|
|
84
|
+
function handleSubmit(submission: SubmissionData) {
|
|
85
|
+
onSubmit?.(submission);
|
|
86
|
+
onComplete?.({ submission, status: "submitted" });
|
|
87
|
+
setStep({ tag: "confirmation", submission });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function handleSaveDraft(submission: SubmissionData) {
|
|
91
|
+
onSaveDraft?.(submission);
|
|
92
|
+
onComplete?.({ submission, status: "draft" });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const canEdit =
|
|
96
|
+
status === "not_started" ||
|
|
97
|
+
status === "draft" ||
|
|
98
|
+
status === "resubmit";
|
|
99
|
+
|
|
100
|
+
// ─── Instructions Screen ───
|
|
101
|
+
if (step.tag === "instructions") {
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
ref={contentRef}
|
|
105
|
+
tabIndex={-1}
|
|
106
|
+
className={cn("max-w-2xl mx-auto outline-none", className)}
|
|
107
|
+
style={style}
|
|
108
|
+
>
|
|
109
|
+
<Card>
|
|
110
|
+
<CardContent className="pt-8 pb-8">
|
|
111
|
+
<div className="text-center mb-6">
|
|
112
|
+
<div className="mx-auto mb-4 w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
|
|
113
|
+
<FileEdit className="size-7 text-primary" />
|
|
114
|
+
</div>
|
|
115
|
+
<h2 className="text-2xl font-bold text-foreground mb-2">
|
|
116
|
+
{title}
|
|
117
|
+
</h2>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Metadata chips */}
|
|
121
|
+
<div className="flex flex-wrap justify-center gap-2 mb-6">
|
|
122
|
+
{dueDate && (
|
|
123
|
+
<DueDateDisplay dueDate={dueDate} size="small" />
|
|
124
|
+
)}
|
|
125
|
+
{maxScore !== undefined && (
|
|
126
|
+
<Badge variant="outline" className="gap-1.5">
|
|
127
|
+
<CheckCircle2 className="size-3.5" />
|
|
128
|
+
{maxScore} points
|
|
129
|
+
</Badge>
|
|
130
|
+
)}
|
|
131
|
+
<Badge variant="outline" className="gap-1.5">
|
|
132
|
+
<Paperclip className="size-3.5" />
|
|
133
|
+
{submissionTypes.map((t) => TYPE_LABELS[t]).join(", ")}
|
|
134
|
+
</Badge>
|
|
135
|
+
<StatusBadge status={status} />
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<Separator className="my-6" />
|
|
139
|
+
|
|
140
|
+
{/* Instructions content */}
|
|
141
|
+
<div className="prose prose-sm text-foreground mb-6">
|
|
142
|
+
{instructions}
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Rubric preview */}
|
|
146
|
+
{Array.isArray(rubric) && rubric.length > 0 && (
|
|
147
|
+
<>
|
|
148
|
+
<Separator className="my-6" />
|
|
149
|
+
<RubricView
|
|
150
|
+
criteria={rubric}
|
|
151
|
+
maxScore={rubricMaxScore}
|
|
152
|
+
/>
|
|
153
|
+
</>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{/* Action */}
|
|
157
|
+
{canEdit && !readOnly && (
|
|
158
|
+
<div className="text-center mt-8">
|
|
159
|
+
<Button
|
|
160
|
+
size="lg"
|
|
161
|
+
onClick={() => setStep({ tag: "work" })}
|
|
162
|
+
disabled={readOnly}
|
|
163
|
+
>
|
|
164
|
+
<Play className="size-4 mr-2" />
|
|
165
|
+
Start Assignment
|
|
166
|
+
</Button>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</CardContent>
|
|
170
|
+
</Card>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Work Screen ───
|
|
176
|
+
if (step.tag === "work") {
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
ref={contentRef}
|
|
180
|
+
tabIndex={-1}
|
|
181
|
+
className={cn("outline-none", className)}
|
|
182
|
+
style={style}
|
|
183
|
+
>
|
|
184
|
+
<Button
|
|
185
|
+
variant="ghost"
|
|
186
|
+
size="sm"
|
|
187
|
+
onClick={() => setStep({ tag: "instructions" })}
|
|
188
|
+
className="mb-4"
|
|
189
|
+
>
|
|
190
|
+
<ArrowLeft className="size-4 mr-1.5" />
|
|
191
|
+
Back to Instructions
|
|
192
|
+
</Button>
|
|
193
|
+
<AssignmentSubmission
|
|
194
|
+
title={title}
|
|
195
|
+
instructions={instructions}
|
|
196
|
+
dueDate={dueDate}
|
|
197
|
+
maxScore={maxScore}
|
|
198
|
+
status={status}
|
|
199
|
+
submissionTypes={submissionTypes}
|
|
200
|
+
existingSubmission={existingSubmission}
|
|
201
|
+
fileConstraints={fileConstraints}
|
|
202
|
+
grade={grade}
|
|
203
|
+
onSubmit={handleSubmit}
|
|
204
|
+
onSaveDraft={handleSaveDraft}
|
|
205
|
+
readOnly={readOnly}
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─── Confirmation Screen ───
|
|
212
|
+
const { submission } = step;
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div
|
|
216
|
+
ref={contentRef}
|
|
217
|
+
tabIndex={-1}
|
|
218
|
+
className={cn("max-w-2xl mx-auto outline-none", className)}
|
|
219
|
+
style={style}
|
|
220
|
+
>
|
|
221
|
+
<Card>
|
|
222
|
+
<CardContent className="pt-8 pb-8">
|
|
223
|
+
<div className="text-center mb-6">
|
|
224
|
+
<div className="mx-auto mb-4 w-14 h-14 rounded-full bg-success/10 flex items-center justify-center">
|
|
225
|
+
<CheckCircle2 className="size-7 text-success" />
|
|
226
|
+
</div>
|
|
227
|
+
<h2 className="text-xl font-bold text-foreground mb-2">
|
|
228
|
+
{status === "graded" ? "Assignment Graded" : "Submission Received"}
|
|
229
|
+
</h2>
|
|
230
|
+
<div className="flex justify-center gap-2 mb-4">
|
|
231
|
+
<StatusBadge status={status} />
|
|
232
|
+
{dueDate && <DueDateDisplay dueDate={dueDate} size="small" />}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* Grade display */}
|
|
237
|
+
{status === "graded" && grade && (
|
|
238
|
+
<div className="text-center mb-6">
|
|
239
|
+
<GradeIndicator
|
|
240
|
+
percentage={
|
|
241
|
+
maxScore ? Math.round((grade.score / maxScore) * 100) : 0
|
|
242
|
+
}
|
|
243
|
+
size="large"
|
|
244
|
+
/>
|
|
245
|
+
<p className="text-sm text-muted-foreground mt-2">
|
|
246
|
+
{grade.score}/{maxScore} points
|
|
247
|
+
</p>
|
|
248
|
+
{grade.feedback && (
|
|
249
|
+
<Alert className="text-left mt-4">
|
|
250
|
+
<AlertDescription>{grade.feedback}</AlertDescription>
|
|
251
|
+
</Alert>
|
|
252
|
+
)}
|
|
253
|
+
{Array.isArray(rubric) && grade.rubricLevels && (
|
|
254
|
+
<>
|
|
255
|
+
<Separator className="my-6" />
|
|
256
|
+
<RubricView
|
|
257
|
+
criteria={rubric}
|
|
258
|
+
selectedLevels={grade.rubricLevels}
|
|
259
|
+
totalScore={grade.score}
|
|
260
|
+
maxScore={rubricMaxScore}
|
|
261
|
+
/>
|
|
262
|
+
</>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
{/* Submission summary */}
|
|
268
|
+
<Separator className="my-6" />
|
|
269
|
+
<h3 className="text-sm font-semibold text-foreground mb-3">
|
|
270
|
+
What You Submitted
|
|
271
|
+
</h3>
|
|
272
|
+
<div className="space-y-2 text-sm text-muted-foreground">
|
|
273
|
+
{submission.textContent && (
|
|
274
|
+
<div className="flex items-start gap-2">
|
|
275
|
+
<FileText className="size-4 mt-0.5 shrink-0" />
|
|
276
|
+
<span className="line-clamp-2">{submission.textContent.replace(/<[^>]*>/g, "")}</span>
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
{submission.files && submission.files.length > 0 && (
|
|
280
|
+
<div className="flex items-start gap-2">
|
|
281
|
+
<Paperclip className="size-4 mt-0.5 shrink-0" />
|
|
282
|
+
<span>
|
|
283
|
+
{submission.files.length} file
|
|
284
|
+
{submission.files.length !== 1 ? "s" : ""} uploaded
|
|
285
|
+
</span>
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
{submission.url && (
|
|
289
|
+
<div className="flex items-start gap-2">
|
|
290
|
+
<LinkIcon className="size-4 mt-0.5 shrink-0" />
|
|
291
|
+
<span className="truncate">{submission.url}</span>
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
{/* Edit button */}
|
|
297
|
+
{canEdit && !readOnly && (
|
|
298
|
+
<div className="text-center mt-8">
|
|
299
|
+
<Button
|
|
300
|
+
variant="outline"
|
|
301
|
+
onClick={() => setStep({ tag: "work" })}
|
|
302
|
+
>
|
|
303
|
+
<FileEdit className="size-4 mr-2" />
|
|
304
|
+
Edit Submission
|
|
305
|
+
</Button>
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
</CardContent>
|
|
309
|
+
</Card>
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export const AssignmentModule = withProGate(AssignmentModuleBase, "AssignmentModule");
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { SubmissionData } from "../../sections/AssignmentSubmission/types";
|
|
3
|
+
import type { RubricCriterion } from "../../sections/RubricView/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AssignmentModule — a complete assignment experience with instructions,
|
|
7
|
+
* submission, and confirmation/grade review.
|
|
8
|
+
*
|
|
9
|
+
* Steps: Instructions → Work (AssignmentSubmission) → Confirmation.
|
|
10
|
+
* Optionally shows a grading rubric in both the instructions and
|
|
11
|
+
* graded confirmation views.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* <AssignmentModule
|
|
15
|
+
* title="Week 3 Essay"
|
|
16
|
+
* instructions={<p>Write a 500-word essay on React hooks.</p>}
|
|
17
|
+
* submissionTypes={["text", "file"]}
|
|
18
|
+
* rubric={rubricCriteria}
|
|
19
|
+
* onSubmit={(submission) => submitAssignment(submission)}
|
|
20
|
+
* />
|
|
21
|
+
*/
|
|
22
|
+
export interface AssignmentModuleProps {
|
|
23
|
+
/** Assignment title */
|
|
24
|
+
title: string;
|
|
25
|
+
/** Assignment instructions (rich content) */
|
|
26
|
+
instructions: ReactNode;
|
|
27
|
+
/** Due date as ISO string */
|
|
28
|
+
dueDate?: string;
|
|
29
|
+
/** Maximum score points */
|
|
30
|
+
maxScore?: number;
|
|
31
|
+
/** Allowed submission types */
|
|
32
|
+
submissionTypes: ("text" | "file" | "url")[];
|
|
33
|
+
/** File upload constraints */
|
|
34
|
+
fileConstraints?: {
|
|
35
|
+
maxFiles?: number;
|
|
36
|
+
maxSizeMB?: number;
|
|
37
|
+
acceptedTypes?: string;
|
|
38
|
+
};
|
|
39
|
+
/** Rubric criteria for grading (shown in instructions and graded confirmation) */
|
|
40
|
+
rubric?: RubricCriterion[];
|
|
41
|
+
/** Existing submission for editing/viewing */
|
|
42
|
+
existingSubmission?: SubmissionData;
|
|
43
|
+
/** Current submission status. @default "not_started" */
|
|
44
|
+
status?:
|
|
45
|
+
| "not_started"
|
|
46
|
+
| "draft"
|
|
47
|
+
| "submitted"
|
|
48
|
+
| "late"
|
|
49
|
+
| "graded"
|
|
50
|
+
| "resubmit";
|
|
51
|
+
/** Grade data when graded */
|
|
52
|
+
grade?: {
|
|
53
|
+
score: number;
|
|
54
|
+
feedback?: ReactNode;
|
|
55
|
+
/** Selected rubric level UIDs per criterion UID */
|
|
56
|
+
rubricLevels?: Record<string, string>;
|
|
57
|
+
};
|
|
58
|
+
/** Called on final submission */
|
|
59
|
+
onSubmit?: (submission: SubmissionData) => void;
|
|
60
|
+
/** Called when the assignment is submitted or draft-saved, with the full result */
|
|
61
|
+
onComplete?: (result: AssignmentModuleResult) => void;
|
|
62
|
+
/** Called on draft save */
|
|
63
|
+
onSaveDraft?: (submission: SubmissionData) => void;
|
|
64
|
+
/** When true, disables interactions for preview/demo mode. @default false */
|
|
65
|
+
readOnly?: boolean;
|
|
66
|
+
/** CSS class name for the root element */
|
|
67
|
+
className?: string;
|
|
68
|
+
/** Inline styles for the root element */
|
|
69
|
+
style?: React.CSSProperties;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface AssignmentModuleResult {
|
|
73
|
+
/** The submitted data */
|
|
74
|
+
submission: SubmissionData;
|
|
75
|
+
/** Status after submission */
|
|
76
|
+
status: "not_started" | "draft" | "submitted" | "late" | "graded" | "resubmit";
|
|
77
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
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 { withProGate } from "../../license/withProGate";
|
|
10
|
+
import type { CertificateModuleProps } from "./types";
|
|
11
|
+
|
|
12
|
+
type InternalStep = { tag: "requirements" } | { tag: "certificate" };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* CertificateModule — a certificate-earning flow with requirements tracking
|
|
16
|
+
* and certificate display.
|
|
17
|
+
*
|
|
18
|
+
* Steps: Requirements (checklist + progress) → Certificate (CertificateViewer).
|
|
19
|
+
* The certificate step is only accessible when all requirements are completed.
|
|
20
|
+
*/
|
|
21
|
+
function CertificateModuleBase({
|
|
22
|
+
courseTitle,
|
|
23
|
+
recipientName,
|
|
24
|
+
organizationName,
|
|
25
|
+
organizationLogo,
|
|
26
|
+
signatory,
|
|
27
|
+
completionDate,
|
|
28
|
+
certificateVariant = "modern",
|
|
29
|
+
requirements,
|
|
30
|
+
overallProgress,
|
|
31
|
+
onRequirementClick,
|
|
32
|
+
onComplete,
|
|
33
|
+
readOnly = false,
|
|
34
|
+
className,
|
|
35
|
+
style,
|
|
36
|
+
}: CertificateModuleProps) {
|
|
37
|
+
const { allComplete, completedCount, derivedProgress } = useMemo(() => {
|
|
38
|
+
const count = requirements.filter((r) => r.completed).length;
|
|
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 };
|
|
42
|
+
}, [requirements]);
|
|
43
|
+
|
|
44
|
+
const displayProgress = overallProgress ?? derivedProgress;
|
|
45
|
+
|
|
46
|
+
const [step, setStep] = useState<InternalStep>({ tag: "requirements" });
|
|
47
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
48
|
+
const earnedFiredRef = useRef(false);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
52
|
+
}, [step.tag]);
|
|
53
|
+
|
|
54
|
+
const onCompleteRef = useRef(onComplete);
|
|
55
|
+
onCompleteRef.current = onComplete;
|
|
56
|
+
|
|
57
|
+
// Fire onComplete once when certificate step is first shown
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (step.tag === "certificate" && !earnedFiredRef.current) {
|
|
60
|
+
earnedFiredRef.current = true;
|
|
61
|
+
onCompleteRef.current?.();
|
|
62
|
+
}
|
|
63
|
+
}, [step.tag]);
|
|
64
|
+
|
|
65
|
+
// ─── Requirements Screen ───
|
|
66
|
+
if (step.tag === "requirements") {
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
ref={contentRef}
|
|
70
|
+
tabIndex={-1}
|
|
71
|
+
className={cn("max-w-2xl mx-auto outline-none", className)}
|
|
72
|
+
style={style}
|
|
73
|
+
>
|
|
74
|
+
<Card>
|
|
75
|
+
<CardContent className="pt-8 pb-8">
|
|
76
|
+
{/* Header with progress */}
|
|
77
|
+
<div className="text-center mb-6">
|
|
78
|
+
<ProgressRing
|
|
79
|
+
value={displayProgress}
|
|
80
|
+
size={100}
|
|
81
|
+
strokeWidth={8}
|
|
82
|
+
color={
|
|
83
|
+
allComplete ? "var(--success)" : "var(--primary)"
|
|
84
|
+
}
|
|
85
|
+
className="mx-auto mb-4 text-foreground"
|
|
86
|
+
/>
|
|
87
|
+
<h2 className="text-xl font-bold text-foreground mb-2">
|
|
88
|
+
{courseTitle}
|
|
89
|
+
</h2>
|
|
90
|
+
<p className="text-sm text-muted-foreground">
|
|
91
|
+
{allComplete
|
|
92
|
+
? "All requirements met — your certificate is ready!"
|
|
93
|
+
: `${completedCount} of ${requirements.length} requirements complete`}
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Requirements checklist */}
|
|
98
|
+
<RequirementsChecklist
|
|
99
|
+
requirements={requirements}
|
|
100
|
+
onRequirementClick={readOnly ? undefined : onRequirementClick}
|
|
101
|
+
className="mb-6"
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
{/* View Certificate button */}
|
|
105
|
+
{allComplete && (
|
|
106
|
+
<div className="text-center mt-6">
|
|
107
|
+
<Button
|
|
108
|
+
size="lg"
|
|
109
|
+
onClick={() => setStep({ tag: "certificate" })}
|
|
110
|
+
disabled={readOnly}
|
|
111
|
+
>
|
|
112
|
+
<Award className="size-4 mr-2" />
|
|
113
|
+
View Certificate
|
|
114
|
+
</Button>
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</CardContent>
|
|
118
|
+
</Card>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Certificate Screen ───
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
ref={contentRef}
|
|
127
|
+
tabIndex={-1}
|
|
128
|
+
className={cn("outline-none", className)}
|
|
129
|
+
style={style}
|
|
130
|
+
>
|
|
131
|
+
{/* Back link */}
|
|
132
|
+
<Button
|
|
133
|
+
variant="ghost"
|
|
134
|
+
size="sm"
|
|
135
|
+
onClick={() => setStep({ tag: "requirements" })}
|
|
136
|
+
className="mb-4"
|
|
137
|
+
>
|
|
138
|
+
<ArrowLeft className="size-4 mr-1.5" />
|
|
139
|
+
Back to Requirements
|
|
140
|
+
</Button>
|
|
141
|
+
|
|
142
|
+
{/* Congratulations header */}
|
|
143
|
+
<div className="text-center mb-8">
|
|
144
|
+
<div className="mx-auto mb-4 w-14 h-14 rounded-full bg-success/10 flex items-center justify-center">
|
|
145
|
+
<PartyPopper className="size-7 text-success" />
|
|
146
|
+
</div>
|
|
147
|
+
<h2 className="text-2xl font-bold text-foreground mb-2">
|
|
148
|
+
Congratulations!
|
|
149
|
+
</h2>
|
|
150
|
+
<p className="text-muted-foreground">
|
|
151
|
+
You've completed all requirements for{" "}
|
|
152
|
+
<span className="font-medium text-foreground">{courseTitle}</span>
|
|
153
|
+
</p>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Certificate */}
|
|
157
|
+
<CertificateViewer
|
|
158
|
+
recipientName={recipientName}
|
|
159
|
+
courseTitle={courseTitle}
|
|
160
|
+
completionDate={
|
|
161
|
+
completionDate ?? new Date().toISOString().split("T")[0]
|
|
162
|
+
}
|
|
163
|
+
organizationName={organizationName}
|
|
164
|
+
organizationLogo={organizationLogo}
|
|
165
|
+
signatory={signatory}
|
|
166
|
+
variant={certificateVariant}
|
|
167
|
+
showActions
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const CertificateModule = withProGate(CertificateModuleBase, "CertificateModule");
|