@hydralms/components 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ForumBoard-CHXU3mjC.js +2207 -0
- package/dist/ForumBoard-d1w5-r6n.cjs +1 -0
- package/dist/assessment-toolbar/assessment-toolbar.d.ts +1 -1
- package/dist/assessment-toolbar/index.d.ts +5 -1
- package/dist/assessment-toolbar/question-header-bar.d.ts +2 -0
- package/dist/assessment-toolbar/question-materials-drawer.d.ts +2 -0
- package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
- package/dist/assessment-toolbar/types.d.ts +52 -4
- package/dist/assessment-toolbar/use-countdown.d.ts +43 -0
- package/dist/common/index.d.ts +2 -1
- package/dist/common/stepper.d.ts +6 -0
- package/dist/common/types.d.ts +37 -0
- package/dist/components.css +1 -1
- package/dist/content/attachment-list.d.ts +6 -0
- package/dist/content/content-block.d.ts +1 -1
- package/dist/content/index.d.ts +2 -1
- package/dist/content/types.d.ts +39 -0
- package/dist/curriculum/curriculum-item.d.ts +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +551 -312
- package/dist/modules/AssignmentModule/AssignmentModule.d.ts +8 -0
- package/dist/modules/AssignmentModule/types.d.ts +65 -0
- package/dist/modules/CertificateModule/CertificateModule.d.ts +9 -0
- package/dist/modules/CertificateModule/types.d.ts +49 -0
- package/dist/modules/DiscussionModule/DiscussionModule.d.ts +8 -0
- package/dist/modules/DiscussionModule/types.d.ts +47 -0
- package/dist/modules/ExamModule/ExamModule.d.ts +8 -0
- package/dist/modules/ExamModule/types.d.ts +64 -0
- package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +9 -0
- package/dist/modules/GradeCenterModule/types.d.ts +54 -0
- package/dist/modules/QuizModule/QuizModule.d.ts +1 -1
- package/dist/modules/QuizModule/types.d.ts +6 -1
- package/dist/modules/SurveyModule/SurveyModule.d.ts +7 -0
- package/dist/modules/SurveyModule/types.d.ts +49 -0
- package/dist/modules/index.d.ts +12 -0
- package/dist/modules.cjs +1 -0
- package/dist/modules.js +1422 -0
- package/dist/progress/achievement-badge.d.ts +6 -0
- package/dist/progress/activity-timeline.d.ts +6 -0
- package/dist/progress/index.d.ts +4 -1
- package/dist/progress/stat-card.d.ts +1 -1
- package/dist/progress/streak-badge.d.ts +6 -0
- package/dist/progress/types.d.ts +97 -0
- package/dist/questions/essay.d.ts +1 -1
- package/dist/questions/hotspot.d.ts +21 -0
- package/dist/questions/index.d.ts +9 -1
- package/dist/questions/inline-choice.d.ts +21 -0
- package/dist/questions/matching.d.ts +22 -0
- package/dist/questions/numeric.d.ts +11 -0
- package/dist/questions/ordering.d.ts +12 -0
- package/dist/questions/scenario.d.ts +23 -0
- package/dist/questions/scoring.d.ts +22 -0
- package/dist/questions/spreadsheet.d.ts +29 -0
- package/dist/questions/types.d.ts +106 -1
- package/dist/questions/use-drag-reorder.d.ts +17 -0
- package/dist/sections/CertificateViewer/types.d.ts +7 -5
- package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
- package/dist/sections/ExamSession/types.d.ts +6 -1
- package/dist/sections/ForumBoard/ForumBoard.d.ts +8 -0
- package/dist/sections/ForumBoard/types.d.ts +64 -0
- package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
- package/dist/sections/QuizSession/types.d.ts +6 -1
- package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
- package/dist/sections/RequirementsChecklist/types.d.ts +37 -0
- package/dist/sections/RubricView/RubricView.d.ts +9 -0
- package/dist/sections/RubricView/types.d.ts +50 -0
- package/dist/sections/index.d.ts +7 -1
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +250 -1715
- package/dist/social/post-card.d.ts +1 -1
- package/dist/tabs-DRM2Iq_J.cjs +172 -0
- package/dist/tabs-Wf3h_Cx3.js +21580 -0
- package/dist/ui/alert.d.ts +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/button.d.ts +1 -1
- package/dist/ui/drawer.d.ts +84 -0
- package/dist/ui/index.d.ts +3 -0
- package/dist/ui/progress.d.ts +1 -1
- package/dist/ui/rich-text-editor.d.ts +30 -0
- package/dist/ui/rich-text-toolbar.d.ts +8 -0
- package/dist/utils/array-utils.d.ts +4 -0
- package/dist/utils/flatten-leaves.d.ts +6 -0
- package/dist/utils/format-file-size.d.ts +1 -0
- package/dist/utils/format-timestamp.d.ts +1 -0
- package/dist/utils/is-empty-html.d.ts +5 -0
- package/dist/utils/shuffle.d.ts +1 -0
- package/dist/utils/string-utils.d.ts +12 -0
- package/dist/video/video-bookmark.d.ts +1 -1
- package/dist/video/video-playlist-item.d.ts +1 -1
- package/package.json +92 -3
- package/src/assessment-toolbar/assessment-toolbar.tsx +54 -49
- package/src/assessment-toolbar/index.ts +6 -0
- package/src/assessment-toolbar/question-header-bar.tsx +61 -0
- package/src/assessment-toolbar/question-materials-drawer.tsx +55 -0
- package/src/assessment-toolbar/question-navigator.tsx +3 -31
- package/src/assessment-toolbar/timer-display.tsx +2 -2
- package/src/assessment-toolbar/types.ts +54 -4
- package/src/assessment-toolbar/use-countdown.ts +153 -0
- package/src/common/index.ts +3 -0
- package/src/common/search-input.tsx +7 -6
- package/src/common/stepper.tsx +100 -0
- package/src/common/types.ts +39 -0
- package/src/content/attachment-list.tsx +90 -0
- package/src/content/content-block.tsx +4 -2
- package/src/content/file-upload-zone.tsx +1 -6
- package/src/content/index.ts +3 -0
- package/src/content/types.ts +41 -0
- package/src/curriculum/curriculum-item.tsx +7 -3
- package/src/feedback/feedback-banner.tsx +12 -14
- package/src/flashcards/flashcard-deck.tsx +1 -9
- package/src/flashcards/flashcard.tsx +1 -1
- package/src/modules/AssignmentModule/AssignmentModule.tsx +305 -0
- package/src/modules/AssignmentModule/types.ts +73 -0
- package/src/modules/CertificateModule/CertificateModule.tsx +161 -0
- package/src/modules/CertificateModule/types.ts +47 -0
- package/src/modules/CoursePlayer/CoursePlayer.tsx +44 -48
- package/src/modules/DiscussionModule/DiscussionModule.tsx +110 -0
- package/src/modules/DiscussionModule/types.ts +54 -0
- package/src/modules/ExamModule/ExamModule.tsx +285 -0
- package/src/modules/ExamModule/types.ts +66 -0
- package/src/modules/FlashcardLab/FlashcardLab.tsx +29 -16
- package/src/modules/GradeCenterModule/GradeCenterModule.tsx +169 -0
- package/src/modules/GradeCenterModule/types.ts +63 -0
- package/src/modules/QuizModule/QuizModule.tsx +88 -88
- package/src/modules/QuizModule/types.ts +6 -1
- package/src/modules/SurveyModule/SurveyModule.tsx +180 -0
- package/src/modules/SurveyModule/types.ts +51 -0
- package/src/modules/index.ts +24 -0
- package/src/progress/achievement-badge.tsx +52 -0
- package/src/progress/activity-timeline.tsx +84 -0
- package/src/progress/index.ts +7 -0
- package/src/progress/stat-card.tsx +30 -18
- package/src/progress/streak-badge.tsx +35 -0
- package/src/progress/types.ts +101 -0
- package/src/questions/choice.tsx +7 -9
- package/src/questions/essay.tsx +23 -25
- package/src/questions/fill-in-the-blank.tsx +13 -16
- package/src/questions/hotspot.tsx +154 -0
- package/src/questions/index.ts +16 -0
- package/src/questions/inline-choice.tsx +151 -0
- package/src/questions/matching.tsx +228 -0
- package/src/questions/multiple-choice.tsx +7 -9
- package/src/questions/numeric.tsx +102 -0
- package/src/questions/ordering.tsx +159 -0
- package/src/questions/question-renderer.tsx +21 -0
- package/src/questions/scenario.tsx +140 -0
- package/src/questions/scoring.ts +201 -0
- package/src/questions/spreadsheet.tsx +259 -0
- package/src/questions/true-false.tsx +7 -9
- package/src/questions/types.ts +123 -1
- package/src/questions/use-drag-reorder.ts +80 -0
- package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +2 -15
- package/src/sections/AssessmentReview/AssessmentReview.tsx +13 -2
- package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +7 -5
- package/src/sections/CertificateViewer/CertificateViewer.tsx +409 -56
- package/src/sections/CertificateViewer/types.ts +13 -5
- package/src/sections/CourseOutline/CourseOutline.tsx +4 -14
- package/src/sections/DiscussionThread/DiscussionThread.tsx +13 -10
- package/src/sections/ExamSession/ExamSession.tsx +44 -7
- package/src/sections/ExamSession/types.ts +6 -1
- package/src/sections/ForumBoard/ForumBoard.tsx +284 -0
- package/src/sections/ForumBoard/types.ts +67 -0
- package/src/sections/GradebookTable/GradebookTable.tsx +1 -1
- package/src/sections/LecturePlayer/LecturePlayer.tsx +1 -1
- package/src/sections/LessonPage/LessonPage.tsx +5 -9
- package/src/sections/PracticeQuiz/PracticeQuiz.tsx +15 -26
- package/src/sections/ProgressDashboard/ProgressDashboard.tsx +65 -65
- package/src/sections/QuizSession/QuizSession.tsx +67 -8
- package/src/sections/QuizSession/types.ts +6 -1
- package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +107 -0
- package/src/sections/RequirementsChecklist/types.ts +38 -0
- package/src/sections/ResourceLibrary/ResourceLibrary.tsx +4 -9
- package/src/sections/RubricView/RubricView.tsx +138 -0
- package/src/sections/RubricView/types.ts +52 -0
- package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +23 -9
- package/src/sections/SurveyForm/SurveyForm.tsx +8 -5
- package/src/sections/index.ts +20 -1
- package/src/social/post-card.tsx +8 -19
- package/src/social/user-avatar.tsx +1 -0
- package/src/styles/globals.css +13 -0
- package/src/ui/drawer.tsx +600 -0
- package/src/ui/index.ts +19 -0
- package/src/ui/rich-text-editor.tsx +109 -0
- package/src/ui/rich-text-toolbar.tsx +156 -0
- package/src/utils/array-utils.ts +17 -0
- package/src/utils/flatten-leaves.ts +17 -0
- package/src/utils/format-file-size.ts +5 -0
- package/src/utils/format-timestamp.ts +13 -0
- package/src/utils/is-empty-html.ts +7 -0
- package/src/utils/shuffle.ts +8 -0
- package/src/utils/string-utils.ts +30 -0
- package/src/video/video-bookmark.tsx +4 -3
- package/src/video/video-chapter-list.tsx +9 -4
- package/src/video/video-player.tsx +11 -4
- package/src/video/video-playlist-item.tsx +8 -3
- package/src/video/video-thumbnail-card.tsx +4 -0
- package/src/video/video-transcript.tsx +8 -5
- package/dist/table-BrS5cDQu.js +0 -2510
- package/dist/table-D6AkBBEo.cjs +0 -1
package/src/content/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export { ContentBlock } from "./content-block";
|
|
2
2
|
export { FileUploadZone } from "./file-upload-zone";
|
|
3
|
+
export { AttachmentList } from "./attachment-list";
|
|
3
4
|
export type {
|
|
4
5
|
LessonBlock,
|
|
5
6
|
ContentBlockProps,
|
|
6
7
|
FileUploadZoneProps,
|
|
8
|
+
AttachmentListProps,
|
|
9
|
+
AttachmentFile,
|
|
7
10
|
} from "./types";
|
package/src/content/types.ts
CHANGED
|
@@ -74,3 +74,44 @@ export interface FileUploadZoneProps {
|
|
|
74
74
|
/** Inline styles for the root element */
|
|
75
75
|
style?: React.CSSProperties;
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A single file attachment for display in AttachmentList.
|
|
80
|
+
*/
|
|
81
|
+
export interface AttachmentFile {
|
|
82
|
+
/** File name with extension */
|
|
83
|
+
name: string;
|
|
84
|
+
/** File size in bytes */
|
|
85
|
+
size: number;
|
|
86
|
+
/** MIME type (e.g. "application/pdf", "image/png") */
|
|
87
|
+
type: string;
|
|
88
|
+
/** Optional download URL */
|
|
89
|
+
url?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* AttachmentList displays a read-only list of file attachments
|
|
94
|
+
* with type icons, formatted sizes, and optional download/remove actions.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* <AttachmentList
|
|
98
|
+
* files={[
|
|
99
|
+
* { name: "syllabus.pdf", size: 245000, type: "application/pdf", url: "/files/syllabus.pdf" },
|
|
100
|
+
* ]}
|
|
101
|
+
* onDownload={(file) => window.open(file.url)}
|
|
102
|
+
* />
|
|
103
|
+
*/
|
|
104
|
+
export interface AttachmentListProps {
|
|
105
|
+
/** List of file attachments to display */
|
|
106
|
+
files: AttachmentFile[];
|
|
107
|
+
/** Called when the user clicks a file's download action */
|
|
108
|
+
onDownload?: (file: AttachmentFile) => void;
|
|
109
|
+
/** Called when the user clicks a file's remove button (only shown when not readOnly) */
|
|
110
|
+
onRemove?: (file: AttachmentFile) => void;
|
|
111
|
+
/** When true, hides the remove button. @default true */
|
|
112
|
+
readOnly?: boolean;
|
|
113
|
+
/** CSS class name for the root element */
|
|
114
|
+
className?: string;
|
|
115
|
+
/** Inline styles for the root element */
|
|
116
|
+
style?: React.CSSProperties;
|
|
117
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
1
2
|
import { ChevronRight, ChevronDown, CheckCircle2, Circle } from "lucide-react";
|
|
2
3
|
import { LearningObjectIcon } from "./learning-object-icon";
|
|
3
4
|
import { formatDuration } from "../utils/format-duration";
|
|
4
5
|
import type { CurriculumItemRowProps } from "./types";
|
|
5
6
|
import { cn } from "../lib/utils";
|
|
6
7
|
|
|
7
|
-
export const CurriculumItemRow = ({
|
|
8
|
+
export const CurriculumItemRow = memo(function CurriculumItemRow({
|
|
8
9
|
item,
|
|
9
10
|
level,
|
|
10
11
|
isActive = false,
|
|
@@ -16,7 +17,7 @@ export const CurriculumItemRow = ({
|
|
|
16
17
|
showDuration = true,
|
|
17
18
|
showIcon = true,
|
|
18
19
|
showProgress = true,
|
|
19
|
-
}: CurriculumItemRowProps)
|
|
20
|
+
}: CurriculumItemRowProps) {
|
|
20
21
|
const isModule = hasChildren;
|
|
21
22
|
|
|
22
23
|
const fontWeightClass = isModule ? "font-semibold" : isActive ? "font-medium" : "font-normal";
|
|
@@ -30,6 +31,9 @@ export const CurriculumItemRow = ({
|
|
|
30
31
|
)}
|
|
31
32
|
style={{ paddingLeft: `${(level * 20) + 8}px` }}
|
|
32
33
|
onClick={onClick}
|
|
34
|
+
role={onClick ? "button" : undefined}
|
|
35
|
+
tabIndex={onClick ? 0 : undefined}
|
|
36
|
+
onKeyDown={onClick ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(); } } : undefined}
|
|
33
37
|
>
|
|
34
38
|
<div className="shrink-0 flex items-center justify-center size-5">
|
|
35
39
|
{showProgress && isCompleted ? (
|
|
@@ -78,4 +82,4 @@ export const CurriculumItemRow = ({
|
|
|
78
82
|
)}
|
|
79
83
|
</div>
|
|
80
84
|
);
|
|
81
|
-
};
|
|
85
|
+
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { CheckCircle, XCircle } from "lucide-react";
|
|
2
|
+
import { Alert } from "../ui/alert";
|
|
3
|
+
import { Button } from "../ui/button";
|
|
2
4
|
import type { FeedbackBannerProps } from "./types";
|
|
3
5
|
import { cn } from "../lib/utils";
|
|
4
6
|
|
|
@@ -11,18 +13,12 @@ export function FeedbackBanner({
|
|
|
11
13
|
style,
|
|
12
14
|
}: FeedbackBannerProps) {
|
|
13
15
|
return (
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
className={cn(
|
|
17
|
-
"flex items-start gap-2 rounded-md border p-3 text-sm",
|
|
18
|
-
isCorrect
|
|
19
|
-
? "border-success/50 bg-success/5"
|
|
20
|
-
: "border-destructive/50 bg-destructive/5",
|
|
21
|
-
className,
|
|
22
|
-
)}
|
|
16
|
+
<Alert
|
|
17
|
+
variant={isCorrect ? "success" : "destructive"}
|
|
18
|
+
className={cn(className)}
|
|
23
19
|
style={style}
|
|
24
20
|
>
|
|
25
|
-
<span className=
|
|
21
|
+
<span className="shrink-0">
|
|
26
22
|
{isCorrect ? <CheckCircle size={20} /> : <XCircle size={20} />}
|
|
27
23
|
</span>
|
|
28
24
|
<div className="flex-1">
|
|
@@ -32,15 +28,17 @@ export function FeedbackBanner({
|
|
|
32
28
|
{explanation && <p className="mt-1 text-foreground">{explanation}</p>}
|
|
33
29
|
</div>
|
|
34
30
|
{!isCorrect && onRetry && (
|
|
35
|
-
<
|
|
36
|
-
|
|
31
|
+
<Button
|
|
32
|
+
variant="outline"
|
|
33
|
+
size="sm"
|
|
34
|
+
className="shrink-0"
|
|
37
35
|
onClick={onRetry}
|
|
38
36
|
>
|
|
39
37
|
{retryLabel}
|
|
40
|
-
</
|
|
38
|
+
</Button>
|
|
41
39
|
)}
|
|
42
40
|
</div>
|
|
43
41
|
</div>
|
|
44
|
-
</
|
|
42
|
+
</Alert>
|
|
45
43
|
);
|
|
46
44
|
}
|
|
@@ -3,15 +3,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
|
3
3
|
import { Flashcard } from "./flashcard";
|
|
4
4
|
import { Button } from "../ui/button";
|
|
5
5
|
import type { FlashcardDeckProps } from "./types";
|
|
6
|
-
|
|
7
|
-
function shuffle<T>(array: T[]): T[] {
|
|
8
|
-
const shuffled = [...array];
|
|
9
|
-
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
10
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
11
|
-
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
12
|
-
}
|
|
13
|
-
return shuffled;
|
|
14
|
-
}
|
|
6
|
+
import { shuffle } from "../utils/shuffle";
|
|
15
7
|
|
|
16
8
|
export const FlashcardDeck = ({
|
|
17
9
|
cards,
|
|
@@ -50,7 +50,7 @@ export const Flashcard = ({
|
|
|
50
50
|
>
|
|
51
51
|
<div
|
|
52
52
|
className={cn(
|
|
53
|
-
"relative size-full transition-transform duration-500 transform-3d",
|
|
53
|
+
"relative size-full motion-safe:transition-transform motion-safe:duration-500 transform-3d",
|
|
54
54
|
isFlipped && "transform-[rotateY(180deg)]",
|
|
55
55
|
)}
|
|
56
56
|
>
|
|
@@ -0,0 +1,305 @@
|
|
|
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 type { AssignmentModuleProps } from "./types";
|
|
24
|
+
|
|
25
|
+
type InternalStep =
|
|
26
|
+
| { tag: "instructions" }
|
|
27
|
+
| { tag: "work" }
|
|
28
|
+
| { tag: "confirmation"; submission: SubmissionData };
|
|
29
|
+
|
|
30
|
+
const TYPE_LABELS: Record<string, string> = {
|
|
31
|
+
text: "Text Entry",
|
|
32
|
+
file: "File Upload",
|
|
33
|
+
url: "URL Submission",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* AssignmentModule — a complete assignment experience with instructions,
|
|
38
|
+
* submission work area, and confirmation/grade review.
|
|
39
|
+
*
|
|
40
|
+
* Steps: Instructions → Work (AssignmentSubmission) → Confirmation.
|
|
41
|
+
*/
|
|
42
|
+
export function AssignmentModule({
|
|
43
|
+
title,
|
|
44
|
+
instructions,
|
|
45
|
+
dueDate,
|
|
46
|
+
maxScore,
|
|
47
|
+
submissionTypes,
|
|
48
|
+
fileConstraints,
|
|
49
|
+
rubric,
|
|
50
|
+
existingSubmission,
|
|
51
|
+
status = "not_started",
|
|
52
|
+
grade,
|
|
53
|
+
onSubmit,
|
|
54
|
+
onSaveDraft,
|
|
55
|
+
className,
|
|
56
|
+
style,
|
|
57
|
+
}: AssignmentModuleProps) {
|
|
58
|
+
const initialStep: InternalStep =
|
|
59
|
+
status === "submitted" || status === "graded"
|
|
60
|
+
? {
|
|
61
|
+
tag: "confirmation",
|
|
62
|
+
submission: existingSubmission ?? {},
|
|
63
|
+
}
|
|
64
|
+
: { tag: "instructions" };
|
|
65
|
+
|
|
66
|
+
const [step, setStep] = useState<InternalStep>(initialStep);
|
|
67
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
71
|
+
}, [step.tag]);
|
|
72
|
+
|
|
73
|
+
const rubricMaxScore = useMemo(() => {
|
|
74
|
+
if (!rubric) return 0;
|
|
75
|
+
return rubric.reduce(
|
|
76
|
+
(sum, c) => sum + Math.max(...c.levels.map((l) => l.points)),
|
|
77
|
+
0
|
|
78
|
+
);
|
|
79
|
+
}, [rubric]);
|
|
80
|
+
|
|
81
|
+
function handleSubmit(submission: SubmissionData) {
|
|
82
|
+
onSubmit?.(submission);
|
|
83
|
+
setStep({ tag: "confirmation", submission });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function handleSaveDraft(submission: SubmissionData) {
|
|
87
|
+
onSaveDraft?.(submission);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const canEdit =
|
|
91
|
+
status === "not_started" ||
|
|
92
|
+
status === "draft" ||
|
|
93
|
+
status === "resubmit";
|
|
94
|
+
|
|
95
|
+
// ─── Instructions Screen ───
|
|
96
|
+
if (step.tag === "instructions") {
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
ref={contentRef}
|
|
100
|
+
tabIndex={-1}
|
|
101
|
+
className={cn("max-w-2xl mx-auto outline-none", className)}
|
|
102
|
+
style={style}
|
|
103
|
+
>
|
|
104
|
+
<Card>
|
|
105
|
+
<CardContent className="pt-8 pb-8">
|
|
106
|
+
<div className="text-center mb-6">
|
|
107
|
+
<div className="mx-auto mb-4 w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
|
|
108
|
+
<FileEdit className="size-7 text-primary" />
|
|
109
|
+
</div>
|
|
110
|
+
<h2 className="text-2xl font-bold text-foreground mb-2">
|
|
111
|
+
{title}
|
|
112
|
+
</h2>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Metadata chips */}
|
|
116
|
+
<div className="flex flex-wrap justify-center gap-2 mb-6">
|
|
117
|
+
{dueDate && (
|
|
118
|
+
<DueDateDisplay dueDate={dueDate} size="small" />
|
|
119
|
+
)}
|
|
120
|
+
{maxScore !== undefined && (
|
|
121
|
+
<Badge variant="outline" className="gap-1.5">
|
|
122
|
+
<CheckCircle2 className="size-3.5" />
|
|
123
|
+
{maxScore} points
|
|
124
|
+
</Badge>
|
|
125
|
+
)}
|
|
126
|
+
<Badge variant="outline" className="gap-1.5">
|
|
127
|
+
<Paperclip className="size-3.5" />
|
|
128
|
+
{submissionTypes.map((t) => TYPE_LABELS[t]).join(", ")}
|
|
129
|
+
</Badge>
|
|
130
|
+
<StatusBadge status={status} />
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<Separator className="my-6" />
|
|
134
|
+
|
|
135
|
+
{/* Instructions content */}
|
|
136
|
+
<div className="prose prose-sm text-foreground mb-6">
|
|
137
|
+
{instructions}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{/* Rubric preview */}
|
|
141
|
+
{rubric && rubric.length > 0 && (
|
|
142
|
+
<>
|
|
143
|
+
<Separator className="my-6" />
|
|
144
|
+
<RubricView
|
|
145
|
+
criteria={rubric}
|
|
146
|
+
maxScore={rubricMaxScore}
|
|
147
|
+
/>
|
|
148
|
+
</>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{/* Action */}
|
|
152
|
+
{canEdit && (
|
|
153
|
+
<div className="text-center mt-8">
|
|
154
|
+
<Button
|
|
155
|
+
size="lg"
|
|
156
|
+
onClick={() => setStep({ tag: "work" })}
|
|
157
|
+
>
|
|
158
|
+
<Play className="size-4 mr-2" />
|
|
159
|
+
Start Assignment
|
|
160
|
+
</Button>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</CardContent>
|
|
164
|
+
</Card>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─── Work Screen ───
|
|
170
|
+
if (step.tag === "work") {
|
|
171
|
+
return (
|
|
172
|
+
<div
|
|
173
|
+
ref={contentRef}
|
|
174
|
+
tabIndex={-1}
|
|
175
|
+
className={cn("outline-none", className)}
|
|
176
|
+
style={style}
|
|
177
|
+
>
|
|
178
|
+
<Button
|
|
179
|
+
variant="ghost"
|
|
180
|
+
size="sm"
|
|
181
|
+
onClick={() => setStep({ tag: "instructions" })}
|
|
182
|
+
className="mb-4"
|
|
183
|
+
>
|
|
184
|
+
<ArrowLeft className="size-4 mr-1.5" />
|
|
185
|
+
Back to Instructions
|
|
186
|
+
</Button>
|
|
187
|
+
<AssignmentSubmission
|
|
188
|
+
title={title}
|
|
189
|
+
instructions={instructions}
|
|
190
|
+
dueDate={dueDate}
|
|
191
|
+
maxScore={maxScore}
|
|
192
|
+
status={status}
|
|
193
|
+
submissionTypes={submissionTypes}
|
|
194
|
+
existingSubmission={existingSubmission}
|
|
195
|
+
fileConstraints={fileConstraints}
|
|
196
|
+
grade={grade}
|
|
197
|
+
onSubmit={handleSubmit}
|
|
198
|
+
onSaveDraft={handleSaveDraft}
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─── Confirmation Screen ───
|
|
205
|
+
const { submission } = step;
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div
|
|
209
|
+
ref={contentRef}
|
|
210
|
+
tabIndex={-1}
|
|
211
|
+
className={cn("max-w-2xl mx-auto outline-none", className)}
|
|
212
|
+
style={style}
|
|
213
|
+
>
|
|
214
|
+
<Card>
|
|
215
|
+
<CardContent className="pt-8 pb-8">
|
|
216
|
+
<div className="text-center mb-6">
|
|
217
|
+
<div className="mx-auto mb-4 w-14 h-14 rounded-full bg-success/10 flex items-center justify-center">
|
|
218
|
+
<CheckCircle2 className="size-7 text-success" />
|
|
219
|
+
</div>
|
|
220
|
+
<h2 className="text-xl font-bold text-foreground mb-2">
|
|
221
|
+
{status === "graded" ? "Assignment Graded" : "Submission Received"}
|
|
222
|
+
</h2>
|
|
223
|
+
<div className="flex justify-center gap-2 mb-4">
|
|
224
|
+
<StatusBadge status={status} />
|
|
225
|
+
{dueDate && <DueDateDisplay dueDate={dueDate} size="small" />}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Grade display */}
|
|
230
|
+
{status === "graded" && grade && (
|
|
231
|
+
<div className="text-center mb-6">
|
|
232
|
+
<GradeIndicator
|
|
233
|
+
percentage={
|
|
234
|
+
maxScore ? Math.round((grade.score / maxScore) * 100) : 0
|
|
235
|
+
}
|
|
236
|
+
size="large"
|
|
237
|
+
/>
|
|
238
|
+
<p className="text-sm text-muted-foreground mt-2">
|
|
239
|
+
{grade.score}/{maxScore} points
|
|
240
|
+
</p>
|
|
241
|
+
{grade.feedback && (
|
|
242
|
+
<Alert className="text-left mt-4">
|
|
243
|
+
<AlertDescription>{grade.feedback}</AlertDescription>
|
|
244
|
+
</Alert>
|
|
245
|
+
)}
|
|
246
|
+
{rubric && grade.rubricLevels && (
|
|
247
|
+
<>
|
|
248
|
+
<Separator className="my-6" />
|
|
249
|
+
<RubricView
|
|
250
|
+
criteria={rubric}
|
|
251
|
+
selectedLevels={grade.rubricLevels}
|
|
252
|
+
totalScore={grade.score}
|
|
253
|
+
maxScore={rubricMaxScore}
|
|
254
|
+
/>
|
|
255
|
+
</>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
|
+
{/* Submission summary */}
|
|
261
|
+
<Separator className="my-6" />
|
|
262
|
+
<h3 className="text-sm font-semibold text-foreground mb-3">
|
|
263
|
+
What You Submitted
|
|
264
|
+
</h3>
|
|
265
|
+
<div className="space-y-2 text-sm text-muted-foreground">
|
|
266
|
+
{submission.textContent && (
|
|
267
|
+
<div className="flex items-start gap-2">
|
|
268
|
+
<FileText className="size-4 mt-0.5 shrink-0" />
|
|
269
|
+
<span className="line-clamp-2">{submission.textContent}</span>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
{submission.files && submission.files.length > 0 && (
|
|
273
|
+
<div className="flex items-start gap-2">
|
|
274
|
+
<Paperclip className="size-4 mt-0.5 shrink-0" />
|
|
275
|
+
<span>
|
|
276
|
+
{submission.files.length} file
|
|
277
|
+
{submission.files.length !== 1 ? "s" : ""} uploaded
|
|
278
|
+
</span>
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
{submission.url && (
|
|
282
|
+
<div className="flex items-start gap-2">
|
|
283
|
+
<LinkIcon className="size-4 mt-0.5 shrink-0" />
|
|
284
|
+
<span className="truncate">{submission.url}</span>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
{/* Edit button */}
|
|
290
|
+
{canEdit && (
|
|
291
|
+
<div className="text-center mt-8">
|
|
292
|
+
<Button
|
|
293
|
+
variant="outline"
|
|
294
|
+
onClick={() => setStep({ tag: "work" })}
|
|
295
|
+
>
|
|
296
|
+
<FileEdit className="size-4 mr-2" />
|
|
297
|
+
Edit Submission
|
|
298
|
+
</Button>
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
</CardContent>
|
|
302
|
+
</Card>
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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 on draft save */
|
|
61
|
+
onSaveDraft?: (submission: SubmissionData) => void;
|
|
62
|
+
/** CSS class name for the root element */
|
|
63
|
+
className?: string;
|
|
64
|
+
/** Inline styles for the root element */
|
|
65
|
+
style?: React.CSSProperties;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface AssignmentModuleResult {
|
|
69
|
+
/** The submitted data */
|
|
70
|
+
submission: SubmissionData;
|
|
71
|
+
/** Status after submission */
|
|
72
|
+
status: string;
|
|
73
|
+
}
|