@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type { QuestionData, SessionAnswer } from "../../questions/types";
|
|
2
|
+
import type { QuestionData, QuestionMaterial, SessionAnswer } from "../../questions/types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* ExamSession section — a formal timed exam experience.
|
|
@@ -44,8 +44,19 @@ export interface ExamSessionProps {
|
|
|
44
44
|
instructions?: ReactNode;
|
|
45
45
|
/** Whether the submit action is in flight */
|
|
46
46
|
isSubmitting?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Related materials keyed by question UID. When provided, a "Materials"
|
|
49
|
+
* button appears in the question header, opening a drawer with content blocks.
|
|
50
|
+
*/
|
|
51
|
+
questionMaterials?: QuestionMaterial[];
|
|
47
52
|
/** When true, all inputs are disabled */
|
|
48
53
|
readOnly?: boolean;
|
|
54
|
+
/** Render skeleton placeholders instead of content */
|
|
55
|
+
isLoading?: boolean;
|
|
56
|
+
/** Error message — renders an error state with optional retry */
|
|
57
|
+
error?: string | null;
|
|
58
|
+
/** Called when the user clicks retry in the error state */
|
|
59
|
+
onRetry?: () => void;
|
|
49
60
|
/** CSS class name for the root element */
|
|
50
61
|
className?: string;
|
|
51
62
|
/** Inline styles for the root element */
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { CheckCircle } from "lucide-react";
|
|
3
3
|
import { FlashcardDeck } from "../../flashcards";
|
|
4
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
4
5
|
import { Button } from "../../ui/button";
|
|
5
6
|
import { Card, CardContent } from "../../ui/card";
|
|
7
|
+
import { SectionShell } from "../_shared/section-shell";
|
|
6
8
|
import type {
|
|
7
9
|
FlashcardStudySessionProps,
|
|
8
10
|
FlashcardSessionStats,
|
|
@@ -16,6 +18,9 @@ export function FlashcardStudySession({
|
|
|
16
18
|
shuffled = false,
|
|
17
19
|
onComplete,
|
|
18
20
|
readOnly = false,
|
|
21
|
+
isLoading,
|
|
22
|
+
error,
|
|
23
|
+
onRetry,
|
|
19
24
|
className,
|
|
20
25
|
style,
|
|
21
26
|
}: FlashcardStudySessionProps) {
|
|
@@ -35,42 +40,54 @@ export function FlashcardStudySession({
|
|
|
35
40
|
setIsComplete(false);
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
if (isComplete) {
|
|
39
|
-
return (
|
|
40
|
-
<div className={cn("flex flex-col items-center", className)} style={style}>
|
|
41
|
-
<Card>
|
|
42
|
-
<CardContent className="pt-6 text-center flex flex-col items-center gap-2">
|
|
43
|
-
<CheckCircle size={48} className="text-success" />
|
|
44
|
-
<span className="text-xl font-bold text-foreground">Deck complete!</span>
|
|
45
|
-
{title && (
|
|
46
|
-
<span className="text-muted-foreground">
|
|
47
|
-
You finished <strong>{title}</strong>
|
|
48
|
-
</span>
|
|
49
|
-
)}
|
|
50
|
-
<span className="text-sm text-muted-foreground">
|
|
51
|
-
{stats.totalCards} card{stats.totalCards !== 1 ? "s" : ""} studied
|
|
52
|
-
{stats.wasShuffled ? " (shuffled)" : ""}
|
|
53
|
-
</span>
|
|
54
|
-
<Button className="mt-2" onClick={handleStudyAgain}>
|
|
55
|
-
Study Again
|
|
56
|
-
</Button>
|
|
57
|
-
</CardContent>
|
|
58
|
-
</Card>
|
|
59
|
-
</div>
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
43
|
return (
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
44
|
+
<SectionShell
|
|
45
|
+
isLoading={isLoading}
|
|
46
|
+
error={error}
|
|
47
|
+
onRetry={onRetry}
|
|
48
|
+
className={className}
|
|
49
|
+
style={style}
|
|
50
|
+
skeleton={
|
|
51
|
+
<>
|
|
52
|
+
<Skeleton className="h-6 w-48" />
|
|
53
|
+
<Skeleton className="h-64 w-full rounded-lg" />
|
|
54
|
+
</>
|
|
55
|
+
}
|
|
56
|
+
>
|
|
57
|
+
{isComplete ? (
|
|
58
|
+
<div className={cn("flex flex-col items-center", className)} style={style}>
|
|
59
|
+
<Card>
|
|
60
|
+
<CardContent className="pt-6 text-center flex flex-col items-center gap-2">
|
|
61
|
+
<CheckCircle size={48} className="text-success" />
|
|
62
|
+
<span className="text-xl font-bold text-foreground">Deck complete!</span>
|
|
63
|
+
{title && (
|
|
64
|
+
<span className="text-muted-foreground">
|
|
65
|
+
You finished <strong>{title}</strong>
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
<span className="text-sm text-muted-foreground">
|
|
69
|
+
{stats.totalCards} card{stats.totalCards !== 1 ? "s" : ""} studied
|
|
70
|
+
{stats.wasShuffled ? " (shuffled)" : ""}
|
|
71
|
+
</span>
|
|
72
|
+
<Button className="mt-2" onClick={handleStudyAgain}>
|
|
73
|
+
Study Again
|
|
74
|
+
</Button>
|
|
75
|
+
</CardContent>
|
|
76
|
+
</Card>
|
|
77
|
+
</div>
|
|
78
|
+
) : (
|
|
79
|
+
<div className={cn("flex flex-col items-center", className)} style={style}>
|
|
80
|
+
<FlashcardDeck
|
|
81
|
+
cards={cards}
|
|
82
|
+
deckName={title}
|
|
83
|
+
deckDescription={description}
|
|
84
|
+
shuffled={shuffled}
|
|
85
|
+
showProgress
|
|
86
|
+
onComplete={handleComplete}
|
|
87
|
+
readOnly={readOnly}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</SectionShell>
|
|
75
92
|
);
|
|
76
93
|
}
|
|
@@ -28,6 +28,12 @@ export interface FlashcardStudySessionProps {
|
|
|
28
28
|
onComplete?: (stats: FlashcardSessionStats) => void;
|
|
29
29
|
/** When true, disables card flipping */
|
|
30
30
|
readOnly?: boolean;
|
|
31
|
+
/** Render skeleton placeholders instead of content */
|
|
32
|
+
isLoading?: boolean;
|
|
33
|
+
/** Error message — renders an error state with optional retry */
|
|
34
|
+
error?: string | null;
|
|
35
|
+
/** Called when the user clicks retry in the error state */
|
|
36
|
+
onRetry?: () => void;
|
|
31
37
|
/** CSS class name for the root element */
|
|
32
38
|
className?: string;
|
|
33
39
|
/** Inline styles for the root element */
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { useState, useMemo } from "react";
|
|
2
|
+
import {
|
|
3
|
+
AlertCircle,
|
|
4
|
+
Plus,
|
|
5
|
+
MessageSquare,
|
|
6
|
+
Heart,
|
|
7
|
+
Pin,
|
|
8
|
+
CheckCircle2,
|
|
9
|
+
ArrowUpDown,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
12
|
+
import { SearchInput } from "../../common/search-input";
|
|
13
|
+
import { UserAvatar } from "../../social/user-avatar";
|
|
14
|
+
import { EmptyState } from "../../common/empty-state";
|
|
15
|
+
import { Button } from "../../ui/button";
|
|
16
|
+
import { Card, CardContent } from "../../ui/card";
|
|
17
|
+
import { Badge } from "../../ui/badge";
|
|
18
|
+
import { Separator } from "../../ui/separator";
|
|
19
|
+
import { Input } from "../../ui/input";
|
|
20
|
+
import { RichTextEditor } from "../../ui/rich-text-editor";
|
|
21
|
+
import { isEmptyHtml } from "../../utils/is-empty-html";
|
|
22
|
+
import { cn } from "../../lib/utils";
|
|
23
|
+
import { formatTimestamp } from "../../utils/format-timestamp";
|
|
24
|
+
import { Pagination } from "../../common/pagination";
|
|
25
|
+
import type { ForumBoardProps, ForumTopic, ForumSortOrder } from "./types";
|
|
26
|
+
|
|
27
|
+
const SORT_LABELS: Record<ForumSortOrder, string> = {
|
|
28
|
+
newest: "Newest",
|
|
29
|
+
oldest: "Oldest",
|
|
30
|
+
most_replies: "Most Replies",
|
|
31
|
+
most_liked: "Most Liked",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* ForumBoard — a topic listing view for a discussion forum.
|
|
36
|
+
*
|
|
37
|
+
* Displays a searchable, sortable list of discussion topics with author info,
|
|
38
|
+
* reply/like counts, timestamps, and new topic creation.
|
|
39
|
+
*/
|
|
40
|
+
export function ForumBoard({
|
|
41
|
+
title,
|
|
42
|
+
topics,
|
|
43
|
+
onTopicClick,
|
|
44
|
+
onCreateTopic,
|
|
45
|
+
sortOrder = "newest",
|
|
46
|
+
onSortChange,
|
|
47
|
+
searchQuery = "",
|
|
48
|
+
onSearchChange,
|
|
49
|
+
readOnly,
|
|
50
|
+
isLoading,
|
|
51
|
+
error,
|
|
52
|
+
onRetry,
|
|
53
|
+
pageSize,
|
|
54
|
+
currentPage = 1,
|
|
55
|
+
totalItems,
|
|
56
|
+
onPageChange,
|
|
57
|
+
className,
|
|
58
|
+
style,
|
|
59
|
+
}: ForumBoardProps) {
|
|
60
|
+
const [showCreateForm, setShowCreateForm] = useState(false);
|
|
61
|
+
const [newTitle, setNewTitle] = useState("");
|
|
62
|
+
const [newContent, setNewContent] = useState("");
|
|
63
|
+
const [localSearch, setLocalSearch] = useState(searchQuery);
|
|
64
|
+
const [localSort, setLocalSort] = useState(sortOrder);
|
|
65
|
+
|
|
66
|
+
const activeSearch = onSearchChange !== undefined ? searchQuery : localSearch;
|
|
67
|
+
const activeSort = onSortChange !== undefined ? sortOrder : localSort;
|
|
68
|
+
|
|
69
|
+
function handleSearchChange(query: string) {
|
|
70
|
+
if (onSearchChange) {
|
|
71
|
+
onSearchChange(query);
|
|
72
|
+
} else {
|
|
73
|
+
setLocalSearch(query);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleSortChange() {
|
|
78
|
+
const order: Array<"newest" | "oldest" | "most_replies" | "most_liked"> = [
|
|
79
|
+
"newest",
|
|
80
|
+
"oldest",
|
|
81
|
+
"most_replies",
|
|
82
|
+
"most_liked",
|
|
83
|
+
];
|
|
84
|
+
const idx = order.indexOf(activeSort);
|
|
85
|
+
const next = order[(idx + 1) % order.length];
|
|
86
|
+
if (onSortChange) {
|
|
87
|
+
onSortChange(next);
|
|
88
|
+
} else {
|
|
89
|
+
setLocalSort(next);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function handleCreateTopic() {
|
|
94
|
+
if (!newTitle.trim() || isEmptyHtml(newContent)) return;
|
|
95
|
+
onCreateTopic?.(newTitle.trim(), newContent);
|
|
96
|
+
setNewTitle("");
|
|
97
|
+
setNewContent("");
|
|
98
|
+
setShowCreateForm(false);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Filter and sort
|
|
102
|
+
const sorted = useMemo(() => {
|
|
103
|
+
const filtered = topics.filter((t) => {
|
|
104
|
+
if (!activeSearch) return true;
|
|
105
|
+
const q = activeSearch.toLowerCase();
|
|
106
|
+
return (
|
|
107
|
+
t.title.toLowerCase().includes(q) ||
|
|
108
|
+
t.preview?.toLowerCase().includes(q) ||
|
|
109
|
+
t.author.displayName.toLowerCase().includes(q)
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return [...filtered].sort((a, b) => {
|
|
114
|
+
if (a.isPinned && !b.isPinned) return -1;
|
|
115
|
+
if (!a.isPinned && b.isPinned) return 1;
|
|
116
|
+
switch (activeSort) {
|
|
117
|
+
case "oldest":
|
|
118
|
+
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
119
|
+
case "most_replies":
|
|
120
|
+
return b.replyCount - a.replyCount;
|
|
121
|
+
case "most_liked":
|
|
122
|
+
return b.likeCount - a.likeCount;
|
|
123
|
+
case "newest":
|
|
124
|
+
default:
|
|
125
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}, [topics, activeSearch, activeSort]);
|
|
129
|
+
|
|
130
|
+
if (isLoading) {
|
|
131
|
+
return (
|
|
132
|
+
<div className={cn("space-y-4", className)} style={style}>
|
|
133
|
+
<Skeleton className="h-9 w-full" />
|
|
134
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
135
|
+
<div key={i} className="flex items-center gap-3">
|
|
136
|
+
<Skeleton className="h-8 w-8 rounded-full" />
|
|
137
|
+
<div className="flex-1 space-y-2">
|
|
138
|
+
<Skeleton className="h-5 w-64" />
|
|
139
|
+
<Skeleton className="h-4 w-32" />
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (error) {
|
|
148
|
+
return (
|
|
149
|
+
<div className={cn("py-12", className)} style={style}>
|
|
150
|
+
<EmptyState
|
|
151
|
+
icon={<AlertCircle className="size-10 text-destructive" />}
|
|
152
|
+
title="Something went wrong"
|
|
153
|
+
description={error}
|
|
154
|
+
action={
|
|
155
|
+
onRetry ? (
|
|
156
|
+
<Button variant="outline" onClick={onRetry}>
|
|
157
|
+
Retry
|
|
158
|
+
</Button>
|
|
159
|
+
) : undefined
|
|
160
|
+
}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div className={cn("flex flex-col gap-4", className)} style={style}>
|
|
168
|
+
{/* Header */}
|
|
169
|
+
<div className="flex items-center justify-between gap-4">
|
|
170
|
+
{title && (
|
|
171
|
+
<h2 className="text-xl font-bold text-foreground">{title}</h2>
|
|
172
|
+
)}
|
|
173
|
+
{!readOnly && onCreateTopic && (
|
|
174
|
+
<Button
|
|
175
|
+
size="sm"
|
|
176
|
+
onClick={() => setShowCreateForm(!showCreateForm)}
|
|
177
|
+
>
|
|
178
|
+
<Plus className="size-4 mr-1.5" />
|
|
179
|
+
New Topic
|
|
180
|
+
</Button>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<Separator />
|
|
185
|
+
|
|
186
|
+
{/* Create topic form */}
|
|
187
|
+
{showCreateForm && (
|
|
188
|
+
<Card>
|
|
189
|
+
<CardContent className="pt-4 space-y-3">
|
|
190
|
+
<Input
|
|
191
|
+
placeholder="Topic title"
|
|
192
|
+
value={newTitle}
|
|
193
|
+
onChange={(e) => setNewTitle(e.target.value)}
|
|
194
|
+
/>
|
|
195
|
+
<RichTextEditor
|
|
196
|
+
placeholder="What would you like to discuss?"
|
|
197
|
+
value={newContent}
|
|
198
|
+
onChange={(html) => setNewContent(html)}
|
|
199
|
+
variant="minimal"
|
|
200
|
+
/>
|
|
201
|
+
<div className="flex justify-end gap-2">
|
|
202
|
+
<Button
|
|
203
|
+
variant="outline"
|
|
204
|
+
size="sm"
|
|
205
|
+
onClick={() => {
|
|
206
|
+
setShowCreateForm(false);
|
|
207
|
+
setNewTitle("");
|
|
208
|
+
setNewContent("");
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
Cancel
|
|
212
|
+
</Button>
|
|
213
|
+
<Button
|
|
214
|
+
size="sm"
|
|
215
|
+
onClick={handleCreateTopic}
|
|
216
|
+
disabled={!newTitle.trim() || isEmptyHtml(newContent)}
|
|
217
|
+
>
|
|
218
|
+
Post Topic
|
|
219
|
+
</Button>
|
|
220
|
+
</div>
|
|
221
|
+
</CardContent>
|
|
222
|
+
</Card>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
{/* Search & sort bar */}
|
|
226
|
+
<div className="flex items-center gap-2">
|
|
227
|
+
<div className="flex-1">
|
|
228
|
+
<SearchInput
|
|
229
|
+
value={activeSearch}
|
|
230
|
+
onChange={handleSearchChange}
|
|
231
|
+
placeholder="Search topics..."
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
<Button variant="outline" size="sm" onClick={handleSortChange}>
|
|
235
|
+
<ArrowUpDown className="size-3.5 mr-1.5" />
|
|
236
|
+
{SORT_LABELS[activeSort]}
|
|
237
|
+
</Button>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
{/* Topic list */}
|
|
241
|
+
{sorted.length === 0 ? (
|
|
242
|
+
<EmptyState
|
|
243
|
+
icon={<MessageSquare className="size-10 text-muted-foreground" />}
|
|
244
|
+
title="No topics found"
|
|
245
|
+
description={
|
|
246
|
+
activeSearch
|
|
247
|
+
? "Try a different search term."
|
|
248
|
+
: "Be the first to start a discussion!"
|
|
249
|
+
}
|
|
250
|
+
/>
|
|
251
|
+
) : (
|
|
252
|
+
<div className="flex flex-col gap-2">
|
|
253
|
+
{(onPageChange && pageSize
|
|
254
|
+
? sorted.slice((currentPage - 1) * pageSize, currentPage * pageSize)
|
|
255
|
+
: sorted
|
|
256
|
+
).map((topic) => (
|
|
257
|
+
<TopicRow
|
|
258
|
+
key={topic.uid}
|
|
259
|
+
topic={topic}
|
|
260
|
+
onClick={() => onTopicClick(topic.uid)}
|
|
261
|
+
/>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{onPageChange && pageSize && sorted.length > 0 && (
|
|
267
|
+
<Pagination
|
|
268
|
+
currentPage={currentPage}
|
|
269
|
+
totalPages={Math.ceil((totalItems ?? sorted.length) / pageSize)}
|
|
270
|
+
onPageChange={onPageChange}
|
|
271
|
+
className="mt-4"
|
|
272
|
+
/>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function TopicRow({
|
|
279
|
+
topic,
|
|
280
|
+
onClick,
|
|
281
|
+
}: {
|
|
282
|
+
topic: ForumTopic;
|
|
283
|
+
onClick: () => void;
|
|
284
|
+
}) {
|
|
285
|
+
return (
|
|
286
|
+
<button
|
|
287
|
+
type="button"
|
|
288
|
+
className="w-full text-left"
|
|
289
|
+
onClick={onClick}
|
|
290
|
+
>
|
|
291
|
+
<Card
|
|
292
|
+
className={cn(
|
|
293
|
+
"transition-colors hover:bg-muted/30",
|
|
294
|
+
topic.isPinned && "border-l-2 border-l-warning"
|
|
295
|
+
)}
|
|
296
|
+
>
|
|
297
|
+
<CardContent className="py-3 px-4">
|
|
298
|
+
<div className="flex items-start gap-3">
|
|
299
|
+
<UserAvatar
|
|
300
|
+
displayName={topic.author.displayName}
|
|
301
|
+
avatarUrl={topic.author.avatarUrl}
|
|
302
|
+
size="small"
|
|
303
|
+
/>
|
|
304
|
+
<div className="flex-1 min-w-0">
|
|
305
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
306
|
+
<span className="font-medium text-foreground text-sm truncate">
|
|
307
|
+
{topic.title}
|
|
308
|
+
</span>
|
|
309
|
+
{topic.isPinned && (
|
|
310
|
+
<Pin className="size-3 text-warning shrink-0" />
|
|
311
|
+
)}
|
|
312
|
+
{topic.isAnswered && (
|
|
313
|
+
<Badge variant="success" className="text-xs shrink-0">
|
|
314
|
+
<CheckCircle2 className="size-3 mr-0.5" />
|
|
315
|
+
Answered
|
|
316
|
+
</Badge>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
{topic.preview && (
|
|
320
|
+
<p className="text-xs text-muted-foreground line-clamp-1">
|
|
321
|
+
{topic.preview}
|
|
322
|
+
</p>
|
|
323
|
+
)}
|
|
324
|
+
<div className="flex items-center gap-3 mt-1.5 text-xs text-muted-foreground">
|
|
325
|
+
<span>{topic.author.displayName}</span>
|
|
326
|
+
<span>{formatTimestamp(topic.createdAt)}</span>
|
|
327
|
+
<Badge variant="muted" className="gap-0.5 text-xs px-1.5 py-0">
|
|
328
|
+
<MessageSquare className="size-3" />
|
|
329
|
+
{topic.replyCount}
|
|
330
|
+
</Badge>
|
|
331
|
+
<Badge variant="muted" className="gap-0.5 text-xs px-1.5 py-0">
|
|
332
|
+
<Heart className="size-3" />
|
|
333
|
+
{topic.likeCount}
|
|
334
|
+
</Badge>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</CardContent>
|
|
339
|
+
</Card>
|
|
340
|
+
</button>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { DiscussionUser } from "../DiscussionThread/types";
|
|
2
|
+
|
|
3
|
+
export type ForumSortOrder = "newest" | "oldest" | "most_replies" | "most_liked";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ForumBoard section — a topic listing view for a discussion forum.
|
|
7
|
+
*
|
|
8
|
+
* Displays a searchable, sortable list of discussion topics with author info,
|
|
9
|
+
* reply/like counts, and timestamps. Supports creating new topics inline.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <ForumBoard
|
|
13
|
+
* title="Class Discussion"
|
|
14
|
+
* topics={topics}
|
|
15
|
+
* onTopicClick={(uid) => openThread(uid)}
|
|
16
|
+
* onCreateTopic={(title, content) => createTopic(title, content)}
|
|
17
|
+
* />
|
|
18
|
+
*/
|
|
19
|
+
export interface ForumBoardProps {
|
|
20
|
+
/** Forum title */
|
|
21
|
+
title?: string;
|
|
22
|
+
/** List of discussion topics */
|
|
23
|
+
topics: ForumTopic[];
|
|
24
|
+
/** The currently authenticated user (reserved for future use) */
|
|
25
|
+
currentUser?: DiscussionUser;
|
|
26
|
+
/** Called when a topic is clicked */
|
|
27
|
+
onTopicClick: (topicUid: string) => void;
|
|
28
|
+
/** Called when a new topic is created */
|
|
29
|
+
onCreateTopic?: (title: string, content: string) => void;
|
|
30
|
+
/** Current sort order */
|
|
31
|
+
sortOrder?: ForumSortOrder;
|
|
32
|
+
/** Called when sort order changes */
|
|
33
|
+
onSortChange?: (sort: ForumSortOrder) => void;
|
|
34
|
+
/** Current search query (controlled) */
|
|
35
|
+
searchQuery?: string;
|
|
36
|
+
/** Called when search query changes */
|
|
37
|
+
onSearchChange?: (query: string) => void;
|
|
38
|
+
/** When true, disables create and interactions */
|
|
39
|
+
readOnly?: boolean;
|
|
40
|
+
/** Render skeleton placeholders instead of content */
|
|
41
|
+
isLoading?: boolean;
|
|
42
|
+
/** Error message — renders an error state with optional retry */
|
|
43
|
+
error?: string | null;
|
|
44
|
+
/** Called when the user clicks retry in the error state */
|
|
45
|
+
onRetry?: () => void;
|
|
46
|
+
/** Number of items per page (enables pagination when set with onPageChange) */
|
|
47
|
+
pageSize?: number;
|
|
48
|
+
/** Current page (1-indexed) */
|
|
49
|
+
currentPage?: number;
|
|
50
|
+
/** Total number of items (for server-side pagination) */
|
|
51
|
+
totalItems?: number;
|
|
52
|
+
/** Called when the user navigates to a different page */
|
|
53
|
+
onPageChange?: (page: number) => void;
|
|
54
|
+
/** CSS class name for the root element */
|
|
55
|
+
className?: string;
|
|
56
|
+
/** Inline styles for the root element */
|
|
57
|
+
style?: React.CSSProperties;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ForumTopic {
|
|
61
|
+
/** Unique identifier */
|
|
62
|
+
uid: string;
|
|
63
|
+
/** Topic title */
|
|
64
|
+
title: string;
|
|
65
|
+
/** Topic author */
|
|
66
|
+
author: DiscussionUser;
|
|
67
|
+
/** Creation timestamp */
|
|
68
|
+
createdAt: string;
|
|
69
|
+
/** Number of replies */
|
|
70
|
+
replyCount: number;
|
|
71
|
+
/** Number of likes */
|
|
72
|
+
likeCount: number;
|
|
73
|
+
/** Timestamp of the most recent reply */
|
|
74
|
+
lastReplyAt?: string;
|
|
75
|
+
/** Whether this topic is pinned to the top */
|
|
76
|
+
isPinned?: boolean;
|
|
77
|
+
/** Whether this topic has a marked answer */
|
|
78
|
+
isAnswered?: boolean;
|
|
79
|
+
/** Preview text (first ~100 chars of content) */
|
|
80
|
+
preview?: string;
|
|
81
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Fragment, useMemo, useState } from "react";
|
|
2
|
-
import { ArrowUp, ArrowDown } from "lucide-react";
|
|
2
|
+
import { ArrowUp, ArrowDown, AlertCircle } from "lucide-react";
|
|
3
3
|
import { GradeIndicator } from "../../progress";
|
|
4
4
|
import { StatusBadge, DueDateDisplay } from "../../common";
|
|
5
5
|
import { Card, CardContent } from "../../ui/card";
|
|
6
6
|
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "../../ui/table";
|
|
7
|
+
import { Skeleton } from "../../ui/skeleton";
|
|
8
|
+
import { Button } from "../../ui/button";
|
|
9
|
+
import { EmptyState } from "../../common/empty-state";
|
|
10
|
+
import { Pagination } from "../../common/pagination";
|
|
7
11
|
import type { GradebookTableProps, GradeCategory } from "./types";
|
|
8
12
|
import { cn } from "../../lib/utils";
|
|
9
13
|
|
|
@@ -53,6 +57,13 @@ export function GradebookTable({
|
|
|
53
57
|
showCategoryTotals = true,
|
|
54
58
|
onItemClick,
|
|
55
59
|
readOnly = false,
|
|
60
|
+
isLoading,
|
|
61
|
+
error,
|
|
62
|
+
onRetry,
|
|
63
|
+
pageSize,
|
|
64
|
+
currentPage = 1,
|
|
65
|
+
totalItems,
|
|
66
|
+
onPageChange,
|
|
56
67
|
className,
|
|
57
68
|
style,
|
|
58
69
|
}: GradebookTableProps) {
|
|
@@ -104,6 +115,39 @@ export function GradebookTable({
|
|
|
104
115
|
|
|
105
116
|
const colCount = showWeights ? 5 : 4;
|
|
106
117
|
|
|
118
|
+
if (isLoading) {
|
|
119
|
+
return (
|
|
120
|
+
<div className={cn("space-y-4", className)} style={style}>
|
|
121
|
+
<Skeleton className="h-20 w-full" />
|
|
122
|
+
<Skeleton className="h-10 w-full" />
|
|
123
|
+
<Skeleton className="h-12 w-full" />
|
|
124
|
+
<Skeleton className="h-12 w-full" />
|
|
125
|
+
<Skeleton className="h-12 w-full" />
|
|
126
|
+
<Skeleton className="h-12 w-full" />
|
|
127
|
+
<Skeleton className="h-12 w-full" />
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (error) {
|
|
133
|
+
return (
|
|
134
|
+
<div className={cn("py-12", className)} style={style}>
|
|
135
|
+
<EmptyState
|
|
136
|
+
icon={<AlertCircle className="size-10 text-destructive" />}
|
|
137
|
+
title="Something went wrong"
|
|
138
|
+
description={error}
|
|
139
|
+
action={
|
|
140
|
+
onRetry ? (
|
|
141
|
+
<Button variant="outline" onClick={onRetry}>
|
|
142
|
+
Retry
|
|
143
|
+
</Button>
|
|
144
|
+
) : undefined
|
|
145
|
+
}
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
107
151
|
return (
|
|
108
152
|
<div className={className} style={style}>
|
|
109
153
|
{/* Overall grade */}
|
|
@@ -169,7 +213,7 @@ export function GradebookTable({
|
|
|
169
213
|
</TableHeader>
|
|
170
214
|
<TableBody>
|
|
171
215
|
{grouped.map((group, gi) => (
|
|
172
|
-
<Fragment key={gi}>
|
|
216
|
+
<Fragment key={group.category?.uid ?? `ungrouped-${gi}`}>
|
|
173
217
|
{group.category && showCategoryTotals && (
|
|
174
218
|
<TableRow className="bg-muted hover:bg-muted">
|
|
175
219
|
<TableCell colSpan={colCount}>
|
|
@@ -224,6 +268,15 @@ export function GradebookTable({
|
|
|
224
268
|
</TableBody>
|
|
225
269
|
</Table>
|
|
226
270
|
</Card>
|
|
271
|
+
|
|
272
|
+
{onPageChange && pageSize && (
|
|
273
|
+
<Pagination
|
|
274
|
+
currentPage={currentPage}
|
|
275
|
+
totalPages={Math.ceil((totalItems ?? items.length) / pageSize)}
|
|
276
|
+
onPageChange={onPageChange}
|
|
277
|
+
className="mt-4"
|
|
278
|
+
/>
|
|
279
|
+
)}
|
|
227
280
|
</div>
|
|
228
281
|
);
|
|
229
282
|
}
|
|
@@ -27,6 +27,20 @@ export interface GradebookTableProps {
|
|
|
27
27
|
onItemClick?: (item: GradeItem) => void;
|
|
28
28
|
/** When true, disables interactions */
|
|
29
29
|
readOnly?: boolean;
|
|
30
|
+
/** Render skeleton placeholders instead of content */
|
|
31
|
+
isLoading?: boolean;
|
|
32
|
+
/** Error message — renders an error state with optional retry */
|
|
33
|
+
error?: string | null;
|
|
34
|
+
/** Called when the user clicks retry in the error state */
|
|
35
|
+
onRetry?: () => void;
|
|
36
|
+
/** Number of items per page (enables pagination when set with onPageChange) */
|
|
37
|
+
pageSize?: number;
|
|
38
|
+
/** Current page (1-indexed) */
|
|
39
|
+
currentPage?: number;
|
|
40
|
+
/** Total number of items (for server-side pagination) */
|
|
41
|
+
totalItems?: number;
|
|
42
|
+
/** Called when the user navigates to a different page */
|
|
43
|
+
onPageChange?: (page: number) => void;
|
|
30
44
|
/** CSS class name for the root element */
|
|
31
45
|
className?: string;
|
|
32
46
|
/** Inline styles for the root element */
|