@hydralms/components 0.1.1 → 0.1.3
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/package.json +52 -1
- package/src/__tests__/setup.ts +1 -0
- package/src/assessment-toolbar/assessment-toolbar.tsx +96 -0
- package/src/assessment-toolbar/index.ts +10 -0
- package/src/assessment-toolbar/question-navigator.tsx +86 -0
- package/src/assessment-toolbar/timer-display.tsx +73 -0
- package/src/assessment-toolbar/types.ts +92 -0
- package/src/assets/hydra-icon.png +0 -0
- package/src/assets/hydra-icon.svg +18 -0
- package/src/assets/hydra-lms-icon.png +0 -0
- package/src/assets/hydra-lms-icon.svg +9 -0
- package/src/common/confirm-dialog.tsx +60 -0
- package/src/common/due-date-display.tsx +64 -0
- package/src/common/empty-state.tsx +24 -0
- package/src/common/index.ts +12 -0
- package/src/common/search-input.tsx +68 -0
- package/src/common/status-badge.test.tsx +43 -0
- package/src/common/status-badge.tsx +81 -0
- package/src/common/types.ts +129 -0
- package/src/content/content-block.tsx +116 -0
- package/src/content/file-upload-zone.tsx +109 -0
- package/src/content/index.ts +7 -0
- package/src/content/types.ts +76 -0
- package/src/curriculum/curriculum-item.tsx +81 -0
- package/src/curriculum/curriculum-tree.tsx +69 -0
- package/src/curriculum/index.ts +11 -0
- package/src/curriculum/learning-object-icon.tsx +44 -0
- package/src/curriculum/types.ts +83 -0
- package/src/feedback/feedback-banner.tsx +46 -0
- package/src/feedback/index.ts +8 -0
- package/src/feedback/likert-scale.tsx +58 -0
- package/src/feedback/star-rating.tsx +65 -0
- package/src/feedback/types.ts +86 -0
- package/src/flashcards/flashcard-deck.tsx +130 -0
- package/src/flashcards/flashcard.tsx +108 -0
- package/src/flashcards/index.ts +3 -0
- package/src/flashcards/types.ts +60 -0
- package/src/index.ts +38 -0
- package/src/lib/utils.ts +6 -0
- package/src/modules/CoursePlayer/CoursePlayer.tsx +281 -0
- package/src/modules/CoursePlayer/types.ts +48 -0
- package/src/modules/FlashcardLab/FlashcardLab.tsx +275 -0
- package/src/modules/FlashcardLab/types.ts +58 -0
- package/src/modules/QuizModule/QuizModule.tsx +241 -0
- package/src/modules/QuizModule/types.ts +56 -0
- package/src/modules/index.ts +12 -0
- package/src/progress/grade-indicator.tsx +65 -0
- package/src/progress/index.ts +8 -0
- package/src/progress/progress-ring.tsx +56 -0
- package/src/progress/stat-card.tsx +42 -0
- package/src/progress/types.ts +73 -0
- package/src/provider/HydraProvider.tsx +26 -0
- package/src/provider/index.ts +2 -0
- package/src/questions/choice.tsx +90 -0
- package/src/questions/essay.tsx +59 -0
- package/src/questions/fill-in-the-blank.tsx +69 -0
- package/src/questions/index.ts +14 -0
- package/src/questions/multiple-choice.test.tsx +104 -0
- package/src/questions/multiple-choice.tsx +97 -0
- package/src/questions/question-renderer.tsx +37 -0
- package/src/questions/true-false.test.tsx +89 -0
- package/src/questions/true-false.tsx +90 -0
- package/src/questions/types.ts +53 -0
- package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +141 -0
- package/src/sections/AnnouncementFeed/types.ts +50 -0
- package/src/sections/AssessmentReview/AssessmentReview.tsx +148 -0
- package/src/sections/AssessmentReview/types.ts +61 -0
- package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +190 -0
- package/src/sections/AssignmentSubmission/types.ts +60 -0
- package/src/sections/CertificateViewer/CertificateViewer.tsx +117 -0
- package/src/sections/CertificateViewer/types.ts +45 -0
- package/src/sections/CourseOutline/CourseOutline.tsx +79 -0
- package/src/sections/CourseOutline/types.ts +53 -0
- package/src/sections/DiscussionThread/DiscussionThread.tsx +186 -0
- package/src/sections/DiscussionThread/types.ts +77 -0
- package/src/sections/ExamSession/ExamSession.tsx +182 -0
- package/src/sections/ExamSession/types.ts +64 -0
- package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +76 -0
- package/src/sections/FlashcardStudySession/types.ts +42 -0
- package/src/sections/GradebookTable/GradebookTable.tsx +229 -0
- package/src/sections/GradebookTable/types.ts +75 -0
- package/src/sections/LecturePlayer/LecturePlayer.tsx +60 -0
- package/src/sections/LecturePlayer/types.ts +48 -0
- package/src/sections/LessonPage/LessonPage.tsx +91 -0
- package/src/sections/LessonPage/types.ts +41 -0
- package/src/sections/PracticeQuiz/PracticeQuiz.tsx +199 -0
- package/src/sections/PracticeQuiz/types.ts +44 -0
- package/src/sections/ProgressDashboard/ProgressDashboard.tsx +140 -0
- package/src/sections/ProgressDashboard/types.ts +74 -0
- package/src/sections/QuizSession/QuizSession.tsx +113 -0
- package/src/sections/QuizSession/types.ts +47 -0
- package/src/sections/ResourceLibrary/ResourceLibrary.tsx +218 -0
- package/src/sections/ResourceLibrary/types.ts +57 -0
- package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +170 -0
- package/src/sections/ScrollableQuiz/types.ts +40 -0
- package/src/sections/SurveyForm/SurveyForm.tsx +180 -0
- package/src/sections/SurveyForm/types.ts +69 -0
- package/src/sections/index.ts +90 -0
- package/src/social/index.ts +3 -0
- package/src/social/post-card.tsx +91 -0
- package/src/social/types.ts +57 -0
- package/src/social/user-avatar.tsx +76 -0
- package/src/styles/globals.css +125 -0
- package/src/ui/alert-dialog.tsx +343 -0
- package/src/ui/alert.tsx +65 -0
- package/src/ui/avatar.tsx +52 -0
- package/src/ui/badge.tsx +53 -0
- package/src/ui/button.tsx +62 -0
- package/src/ui/card.tsx +92 -0
- package/src/ui/index.ts +44 -0
- package/src/ui/input.tsx +21 -0
- package/src/ui/progress.tsx +73 -0
- package/src/ui/separator.tsx +29 -0
- package/src/ui/skeleton.tsx +15 -0
- package/src/ui/slot.tsx +48 -0
- package/src/ui/table.tsx +108 -0
- package/src/ui/tabs.tsx +147 -0
- package/src/ui/textarea.tsx +20 -0
- package/src/ui/tooltip.tsx +177 -0
- package/src/utils/debounce.test.ts +59 -0
- package/src/utils/debounce.ts +10 -0
- package/src/utils/format-duration.test.ts +55 -0
- package/src/utils/format-duration.ts +30 -0
- package/src/video/index.ts +17 -0
- package/src/video/types.ts +216 -0
- package/src/video/video-bookmark.tsx +76 -0
- package/src/video/video-chapter-list.tsx +93 -0
- package/src/video/video-player.tsx +103 -0
- package/src/video/video-playlist-item.tsx +90 -0
- package/src/video/video-thumbnail-card.tsx +74 -0
- package/src/video/video-transcript.tsx +102 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { AnswerOption } from "../../questions/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SurveyForm section — an ungraded survey/feedback form.
|
|
6
|
+
*
|
|
7
|
+
* Supports Likert scale, star rating, open-text, single-choice,
|
|
8
|
+
* and multiple-choice question types. Questions can be displayed
|
|
9
|
+
* all at once or one at a time.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <SurveyForm
|
|
13
|
+
* title="Course Evaluation"
|
|
14
|
+
* questions={surveyQuestions}
|
|
15
|
+
* onSubmit={(answers) => submitSurvey(answers)}
|
|
16
|
+
* />
|
|
17
|
+
*/
|
|
18
|
+
export interface SurveyFormProps {
|
|
19
|
+
/** Survey title displayed at the top */
|
|
20
|
+
title: string;
|
|
21
|
+
/** Optional introductory text or description */
|
|
22
|
+
description?: ReactNode;
|
|
23
|
+
/** Survey questions */
|
|
24
|
+
questions: SurveyQuestion[];
|
|
25
|
+
/** Pre-populated answers */
|
|
26
|
+
initialAnswers?: SurveyAnswer[];
|
|
27
|
+
/** Called on submission with all answers */
|
|
28
|
+
onSubmit: (answers: SurveyAnswer[]) => void;
|
|
29
|
+
/** Called whenever any answer changes */
|
|
30
|
+
onAnswerChange?: (answers: SurveyAnswer[]) => void;
|
|
31
|
+
/** Whether to show a progress indicator */
|
|
32
|
+
showProgress?: boolean;
|
|
33
|
+
/** Whether all questions must be answered before submit */
|
|
34
|
+
requireAll?: boolean;
|
|
35
|
+
/** Label for the submit button */
|
|
36
|
+
submitLabel?: string;
|
|
37
|
+
/** Whether the submit action is in flight */
|
|
38
|
+
isSubmitting?: boolean;
|
|
39
|
+
/** When true, all inputs are disabled */
|
|
40
|
+
readOnly?: boolean;
|
|
41
|
+
/** CSS class name for the root element */
|
|
42
|
+
className?: string;
|
|
43
|
+
/** Inline styles for the root element */
|
|
44
|
+
style?: React.CSSProperties;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SurveyQuestion {
|
|
48
|
+
/** Unique identifier */
|
|
49
|
+
uid: string;
|
|
50
|
+
/** Question type */
|
|
51
|
+
type: "likert" | "rating" | "open_text" | "choice" | "multiple_choice";
|
|
52
|
+
/** Question text */
|
|
53
|
+
content: string;
|
|
54
|
+
/** For likert: labels for scale endpoints */
|
|
55
|
+
scaleLabels?: { low: string; high: string };
|
|
56
|
+
/** For likert: number of scale points */
|
|
57
|
+
scalePoints?: 5 | 7;
|
|
58
|
+
/** For choice/multiple_choice: answer options */
|
|
59
|
+
answers?: AnswerOption[];
|
|
60
|
+
/** Whether this question is required */
|
|
61
|
+
required?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SurveyAnswer {
|
|
65
|
+
/** Question UID */
|
|
66
|
+
questionUid: string;
|
|
67
|
+
/** Answer value — string for text, number for likert/rating, uid for choice */
|
|
68
|
+
value: string | number;
|
|
69
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export { QuizSession } from "./QuizSession/QuizSession";
|
|
2
|
+
export type { QuizSessionProps } from "./QuizSession/types";
|
|
3
|
+
|
|
4
|
+
export { LecturePlayer } from "./LecturePlayer/LecturePlayer";
|
|
5
|
+
export type { LecturePlayerProps } from "./LecturePlayer/types";
|
|
6
|
+
|
|
7
|
+
export { FlashcardStudySession } from "./FlashcardStudySession/FlashcardStudySession";
|
|
8
|
+
export type {
|
|
9
|
+
FlashcardStudySessionProps,
|
|
10
|
+
FlashcardSessionStats,
|
|
11
|
+
} from "./FlashcardStudySession/types";
|
|
12
|
+
|
|
13
|
+
export { AssessmentReview } from "./AssessmentReview/AssessmentReview";
|
|
14
|
+
export type {
|
|
15
|
+
AssessmentReviewProps,
|
|
16
|
+
AssessmentScore,
|
|
17
|
+
AssessmentReviewGroup,
|
|
18
|
+
} from "./AssessmentReview/types";
|
|
19
|
+
|
|
20
|
+
export { CourseOutline } from "./CourseOutline/CourseOutline";
|
|
21
|
+
export type { CourseOutlineProps } from "./CourseOutline/types";
|
|
22
|
+
|
|
23
|
+
export { ScrollableQuiz } from "./ScrollableQuiz/ScrollableQuiz";
|
|
24
|
+
export type { ScrollableQuizProps } from "./ScrollableQuiz/types";
|
|
25
|
+
|
|
26
|
+
export { PracticeQuiz } from "./PracticeQuiz/PracticeQuiz";
|
|
27
|
+
export type {
|
|
28
|
+
PracticeQuizProps,
|
|
29
|
+
PracticeQuizStats,
|
|
30
|
+
} from "./PracticeQuiz/types";
|
|
31
|
+
|
|
32
|
+
export { ExamSession } from "./ExamSession/ExamSession";
|
|
33
|
+
export type {
|
|
34
|
+
ExamSessionProps,
|
|
35
|
+
ExamSubmitMetadata,
|
|
36
|
+
} from "./ExamSession/types";
|
|
37
|
+
|
|
38
|
+
export { SurveyForm } from "./SurveyForm/SurveyForm";
|
|
39
|
+
export type {
|
|
40
|
+
SurveyFormProps,
|
|
41
|
+
SurveyQuestion,
|
|
42
|
+
SurveyAnswer,
|
|
43
|
+
} from "./SurveyForm/types";
|
|
44
|
+
|
|
45
|
+
export { LessonPage } from "./LessonPage/LessonPage";
|
|
46
|
+
export type { LessonPageProps } from "./LessonPage/types";
|
|
47
|
+
|
|
48
|
+
export { ResourceLibrary } from "./ResourceLibrary/ResourceLibrary";
|
|
49
|
+
export type {
|
|
50
|
+
ResourceLibraryProps,
|
|
51
|
+
Resource,
|
|
52
|
+
} from "./ResourceLibrary/types";
|
|
53
|
+
|
|
54
|
+
export { AnnouncementFeed } from "./AnnouncementFeed/AnnouncementFeed";
|
|
55
|
+
export type {
|
|
56
|
+
AnnouncementFeedProps,
|
|
57
|
+
Announcement,
|
|
58
|
+
} from "./AnnouncementFeed/types";
|
|
59
|
+
|
|
60
|
+
export { DiscussionThread } from "./DiscussionThread/DiscussionThread";
|
|
61
|
+
export type {
|
|
62
|
+
DiscussionThreadProps,
|
|
63
|
+
DiscussionPost,
|
|
64
|
+
DiscussionUser,
|
|
65
|
+
} from "./DiscussionThread/types";
|
|
66
|
+
|
|
67
|
+
export { AssignmentSubmission } from "./AssignmentSubmission/AssignmentSubmission";
|
|
68
|
+
export type {
|
|
69
|
+
AssignmentSubmissionProps,
|
|
70
|
+
SubmissionData,
|
|
71
|
+
} from "./AssignmentSubmission/types";
|
|
72
|
+
|
|
73
|
+
export { GradebookTable } from "./GradebookTable/GradebookTable";
|
|
74
|
+
export type {
|
|
75
|
+
GradebookTableProps,
|
|
76
|
+
GradeItem,
|
|
77
|
+
GradeCategory,
|
|
78
|
+
OverallGrade,
|
|
79
|
+
} from "./GradebookTable/types";
|
|
80
|
+
|
|
81
|
+
export { ProgressDashboard } from "./ProgressDashboard/ProgressDashboard";
|
|
82
|
+
export type {
|
|
83
|
+
ProgressDashboardProps,
|
|
84
|
+
ModuleProgress,
|
|
85
|
+
ActivityItem,
|
|
86
|
+
Achievement,
|
|
87
|
+
} from "./ProgressDashboard/types";
|
|
88
|
+
|
|
89
|
+
export { CertificateViewer } from "./CertificateViewer/CertificateViewer";
|
|
90
|
+
export type { CertificateViewerProps } from "./CertificateViewer/types";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Pin } from "lucide-react";
|
|
2
|
+
import { UserAvatar } from "./user-avatar";
|
|
3
|
+
import { Separator } from "../ui/separator";
|
|
4
|
+
import type { PostCardProps } from "./types";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
function formatTimestamp(iso: string): string {
|
|
8
|
+
const date = new Date(iso);
|
|
9
|
+
const now = new Date();
|
|
10
|
+
const diffMs = now.getTime() - date.getTime();
|
|
11
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
12
|
+
if (diffMins < 1) return "Just now";
|
|
13
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
14
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
15
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
16
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
17
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
18
|
+
return date.toLocaleDateString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const HIGHLIGHT_BORDERS: Record<string, string> = {
|
|
22
|
+
pinned: "var(--warning)",
|
|
23
|
+
answer: "var(--success)",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function PostCard({
|
|
27
|
+
author,
|
|
28
|
+
content,
|
|
29
|
+
createdAt,
|
|
30
|
+
updatedAt,
|
|
31
|
+
actions,
|
|
32
|
+
highlight = "none",
|
|
33
|
+
indentLevel = 0,
|
|
34
|
+
className,
|
|
35
|
+
style,
|
|
36
|
+
}: PostCardProps) {
|
|
37
|
+
const borderColor = HIGHLIGHT_BORDERS[highlight];
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
className={cn("rounded-md border border-border p-3", className)}
|
|
42
|
+
style={{
|
|
43
|
+
marginLeft: indentLevel ? `calc(${indentLevel} * 1rem)` : undefined,
|
|
44
|
+
...(borderColor && {
|
|
45
|
+
borderLeftWidth: "3px",
|
|
46
|
+
borderLeftColor: borderColor,
|
|
47
|
+
}),
|
|
48
|
+
...style,
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<div className="flex gap-2">
|
|
52
|
+
<UserAvatar
|
|
53
|
+
displayName={author.displayName}
|
|
54
|
+
avatarUrl={author.avatarUrl}
|
|
55
|
+
role={author.role as "student" | "instructor" | "ta" | "admin" | undefined}
|
|
56
|
+
size="medium"
|
|
57
|
+
/>
|
|
58
|
+
<div className="flex-1 min-w-0">
|
|
59
|
+
<div className="flex items-center gap-1.5 flex-wrap text-sm">
|
|
60
|
+
<span className="font-semibold">{author.displayName}</span>
|
|
61
|
+
{author.role && author.role !== "student" && (
|
|
62
|
+
<span className="rounded-full bg-primary/10 px-1.5 py-0.5 text-xs font-medium text-primary capitalize">
|
|
63
|
+
{author.role}
|
|
64
|
+
</span>
|
|
65
|
+
)}
|
|
66
|
+
<span className="text-xs text-muted-foreground">
|
|
67
|
+
{formatTimestamp(createdAt)}
|
|
68
|
+
</span>
|
|
69
|
+
{updatedAt && (
|
|
70
|
+
<span className="text-xs text-muted-foreground italic">
|
|
71
|
+
(edited)
|
|
72
|
+
</span>
|
|
73
|
+
)}
|
|
74
|
+
{highlight === "pinned" && <Pin size={14} />}
|
|
75
|
+
</div>
|
|
76
|
+
<p className="mt-1 text-sm whitespace-pre-wrap">
|
|
77
|
+
{content}
|
|
78
|
+
</p>
|
|
79
|
+
{actions && (
|
|
80
|
+
<>
|
|
81
|
+
<Separator className="my-2" />
|
|
82
|
+
<div className="flex items-center gap-1">
|
|
83
|
+
{actions}
|
|
84
|
+
</div>
|
|
85
|
+
</>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UserAvatar renders a user avatar image with an initials fallback
|
|
5
|
+
* and an optional role badge.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <UserAvatar displayName="Jane Smith" role="instructor" />
|
|
9
|
+
*/
|
|
10
|
+
export interface UserAvatarProps {
|
|
11
|
+
/** User's display name (used for initials fallback) */
|
|
12
|
+
displayName: string;
|
|
13
|
+
/** URL of the avatar image */
|
|
14
|
+
avatarUrl?: string;
|
|
15
|
+
/** User role — shown as a tooltip or small badge */
|
|
16
|
+
role?: "student" | "instructor" | "ta" | "admin";
|
|
17
|
+
/** Whether to show the role badge */
|
|
18
|
+
showRoleBadge?: boolean;
|
|
19
|
+
/** Avatar size */
|
|
20
|
+
size?: "small" | "medium" | "large";
|
|
21
|
+
/** CSS class name for the root element */
|
|
22
|
+
className?: string;
|
|
23
|
+
/** Inline styles for the root element */
|
|
24
|
+
style?: React.CSSProperties;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* PostCard renders a single discussion post or announcement with author info,
|
|
29
|
+
* timestamp, content body, and optional action buttons.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* <PostCard
|
|
33
|
+
* author={{ displayName: "Jane Smith", role: "instructor" }}
|
|
34
|
+
* content="Welcome to the course!"
|
|
35
|
+
* createdAt="2025-01-15T10:30:00Z"
|
|
36
|
+
* />
|
|
37
|
+
*/
|
|
38
|
+
export interface PostCardProps {
|
|
39
|
+
/** Post author information */
|
|
40
|
+
author: { displayName: string; avatarUrl?: string; role?: string };
|
|
41
|
+
/** Post body content */
|
|
42
|
+
content: string;
|
|
43
|
+
/** Creation timestamp as ISO string */
|
|
44
|
+
createdAt: string;
|
|
45
|
+
/** Last update timestamp */
|
|
46
|
+
updatedAt?: string;
|
|
47
|
+
/** Action buttons rendered in the card footer */
|
|
48
|
+
actions?: ReactNode;
|
|
49
|
+
/** Highlight variant for special posts */
|
|
50
|
+
highlight?: "pinned" | "answer" | "none";
|
|
51
|
+
/** Indentation level for nested replies */
|
|
52
|
+
indentLevel?: number;
|
|
53
|
+
/** CSS class name for the root element */
|
|
54
|
+
className?: string;
|
|
55
|
+
/** Inline styles for the root element */
|
|
56
|
+
style?: React.CSSProperties;
|
|
57
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { UserAvatarProps } from "./types";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
const SIZES = { small: 28, medium: 36, large: 48 };
|
|
6
|
+
|
|
7
|
+
const ROLE_LABELS: Record<string, string> = {
|
|
8
|
+
student: "Student",
|
|
9
|
+
instructor: "Instructor",
|
|
10
|
+
ta: "Teaching Assistant",
|
|
11
|
+
admin: "Admin",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const ROLE_COLORS: Record<string, string> = {
|
|
15
|
+
instructor: "var(--primary)",
|
|
16
|
+
ta: "var(--purple)",
|
|
17
|
+
admin: "var(--destructive)",
|
|
18
|
+
student: "var(--muted-foreground)",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getInitials(name: string): string {
|
|
22
|
+
return name
|
|
23
|
+
.split(" ")
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.map((w) => w[0])
|
|
26
|
+
.slice(0, 2)
|
|
27
|
+
.join("")
|
|
28
|
+
.toUpperCase();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function UserAvatar({
|
|
32
|
+
displayName,
|
|
33
|
+
avatarUrl,
|
|
34
|
+
role,
|
|
35
|
+
showRoleBadge = false,
|
|
36
|
+
size = "medium",
|
|
37
|
+
className,
|
|
38
|
+
style,
|
|
39
|
+
}: UserAvatarProps) {
|
|
40
|
+
const px = SIZES[size];
|
|
41
|
+
const fontSize = px * 0.4;
|
|
42
|
+
const [imgError, setImgError] = useState(false);
|
|
43
|
+
|
|
44
|
+
const showBadge = showRoleBadge && role && role !== "student";
|
|
45
|
+
const title = showBadge ? ROLE_LABELS[role] ?? role : undefined;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className={cn("relative inline-flex", className)}
|
|
50
|
+
style={style}
|
|
51
|
+
title={title}
|
|
52
|
+
>
|
|
53
|
+
<div
|
|
54
|
+
className="flex items-center justify-center rounded-full bg-muted text-muted-foreground overflow-hidden"
|
|
55
|
+
style={{ width: `${px}px`, height: `${px}px`, fontSize: `${fontSize}px` }}
|
|
56
|
+
>
|
|
57
|
+
{avatarUrl && !imgError ? (
|
|
58
|
+
<img
|
|
59
|
+
className="size-full object-cover"
|
|
60
|
+
src={avatarUrl}
|
|
61
|
+
alt={displayName}
|
|
62
|
+
onError={() => setImgError(true)}
|
|
63
|
+
/>
|
|
64
|
+
) : (
|
|
65
|
+
<span>{getInitials(displayName)}</span>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
{showBadge && (
|
|
69
|
+
<span
|
|
70
|
+
className="absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full border-2 border-background"
|
|
71
|
+
style={{ background: ROLE_COLORS[role] ?? "var(--muted-foreground)" }}
|
|
72
|
+
/>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@custom-variant dark (&:is(.dark *));
|
|
4
|
+
|
|
5
|
+
@theme inline {
|
|
6
|
+
--color-background: var(--background);
|
|
7
|
+
--color-foreground: var(--foreground);
|
|
8
|
+
--color-card: var(--card);
|
|
9
|
+
--color-card-foreground: var(--card-foreground);
|
|
10
|
+
--color-popover: var(--popover);
|
|
11
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
12
|
+
--color-primary: var(--primary);
|
|
13
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
14
|
+
--color-secondary: var(--secondary);
|
|
15
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
16
|
+
--color-muted: var(--muted);
|
|
17
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
18
|
+
--color-accent: var(--accent);
|
|
19
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
20
|
+
--color-destructive: var(--destructive);
|
|
21
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
22
|
+
--color-border: var(--border);
|
|
23
|
+
--color-input: var(--input);
|
|
24
|
+
--color-ring: var(--ring);
|
|
25
|
+
--color-success: var(--success);
|
|
26
|
+
--color-success-foreground: var(--success-foreground);
|
|
27
|
+
--color-warning: var(--warning);
|
|
28
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
29
|
+
--color-info: var(--info);
|
|
30
|
+
--color-info-foreground: var(--info-foreground);
|
|
31
|
+
--color-purple: var(--purple);
|
|
32
|
+
--color-teal: var(--teal);
|
|
33
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
34
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
35
|
+
--radius-lg: var(--radius);
|
|
36
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.hydra-root {
|
|
40
|
+
--radius: 0.625rem;
|
|
41
|
+
--background: oklch(1 0 0);
|
|
42
|
+
--foreground: oklch(0.145 0 0);
|
|
43
|
+
--card: oklch(1 0 0);
|
|
44
|
+
--card-foreground: oklch(0.145 0 0);
|
|
45
|
+
--popover: oklch(1 0 0);
|
|
46
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
47
|
+
--primary: oklch(0.52 0.22 22);
|
|
48
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
49
|
+
--secondary: oklch(0.961 0.01 22);
|
|
50
|
+
--secondary-foreground: oklch(0.4 0.12 22);
|
|
51
|
+
--muted: oklch(0.961 0.005 22);
|
|
52
|
+
--muted-foreground: oklch(0.5 0.02 22);
|
|
53
|
+
--accent: oklch(0.952 0.012 22);
|
|
54
|
+
--accent-foreground: oklch(0.4 0.12 22);
|
|
55
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
56
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
57
|
+
--border: oklch(0.905 0.008 22);
|
|
58
|
+
--input: oklch(0.905 0.008 22);
|
|
59
|
+
--ring: oklch(0.52 0.22 22);
|
|
60
|
+
|
|
61
|
+
/* LMS feedback colors */
|
|
62
|
+
--success: oklch(0.723 0.191 142.5);
|
|
63
|
+
--success-foreground: oklch(0.962 0.044 156);
|
|
64
|
+
--warning: oklch(0.702 0.183 55);
|
|
65
|
+
--warning-foreground: oklch(0.98 0.016 73);
|
|
66
|
+
--info: oklch(0.623 0.214 259);
|
|
67
|
+
--info-foreground: oklch(0.97 0.014 254);
|
|
68
|
+
--purple: oklch(0.627 0.265 303);
|
|
69
|
+
--teal: oklch(0.697 0.146 182);
|
|
70
|
+
|
|
71
|
+
color: var(--foreground);
|
|
72
|
+
background: var(--background);
|
|
73
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.hydra-root.dark,
|
|
77
|
+
.dark .hydra-root {
|
|
78
|
+
--background: oklch(0.145 0 0);
|
|
79
|
+
--foreground: oklch(0.985 0 0);
|
|
80
|
+
--card: oklch(0.205 0.008 22);
|
|
81
|
+
--card-foreground: oklch(0.985 0 0);
|
|
82
|
+
--popover: oklch(0.205 0.008 22);
|
|
83
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
84
|
+
--primary: oklch(0.68 0.20 22);
|
|
85
|
+
--primary-foreground: oklch(0.145 0 0);
|
|
86
|
+
--secondary: oklch(0.269 0.015 22);
|
|
87
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
88
|
+
--muted: oklch(0.269 0.015 22);
|
|
89
|
+
--muted-foreground: oklch(0.65 0.02 22);
|
|
90
|
+
--accent: oklch(0.269 0.015 22);
|
|
91
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
92
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
93
|
+
--destructive-foreground: oklch(0.145 0 0);
|
|
94
|
+
--border: oklch(1 0 0 / 10%);
|
|
95
|
+
--input: oklch(1 0 0 / 15%);
|
|
96
|
+
--ring: oklch(0.68 0.20 22);
|
|
97
|
+
|
|
98
|
+
--success: oklch(0.723 0.191 142.5);
|
|
99
|
+
--success-foreground: oklch(0.15 0.05 150);
|
|
100
|
+
--warning: oklch(0.702 0.183 55);
|
|
101
|
+
--warning-foreground: oklch(0.15 0.05 60);
|
|
102
|
+
--info: oklch(0.623 0.214 259);
|
|
103
|
+
--info-foreground: oklch(0.15 0.05 255);
|
|
104
|
+
--purple: oklch(0.627 0.265 303);
|
|
105
|
+
--teal: oklch(0.697 0.146 182);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Rich text support for flashcard back content */
|
|
109
|
+
.flashcard-back-content ul,
|
|
110
|
+
.flashcard-back-content ol {
|
|
111
|
+
padding-left: 1.25em;
|
|
112
|
+
margin: 0.25em 0;
|
|
113
|
+
}
|
|
114
|
+
.flashcard-back-content ul {
|
|
115
|
+
list-style-type: disc;
|
|
116
|
+
}
|
|
117
|
+
.flashcard-back-content ol {
|
|
118
|
+
list-style-type: decimal;
|
|
119
|
+
}
|
|
120
|
+
.flashcard-back-content li {
|
|
121
|
+
margin: 0.15em 0;
|
|
122
|
+
}
|
|
123
|
+
.flashcard-back-content p {
|
|
124
|
+
margin: 0.25em 0;
|
|
125
|
+
}
|