@academy-sdk/sdk 0.1.0 → 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/bundle.js +70 -0
- package/dist/manifest.json +5 -0
- package/dist/styles.css +3307 -0
- package/package.json +40 -46
- package/src/components/atoms/Avatar.tsx +38 -0
- package/src/components/atoms/Badge.tsx +32 -0
- package/src/components/atoms/Button.tsx +48 -0
- package/src/components/atoms/Card.tsx +33 -0
- package/src/components/atoms/Input.tsx +39 -0
- package/src/components/atoms/ProgressBar.tsx +52 -0
- package/src/components/atoms/Tabs.tsx +47 -0
- package/src/components/atoms/index.ts +10 -0
- package/src/components/index.ts +11 -0
- package/src/components/molecules/CourseCard.tsx +215 -0
- package/src/components/molecules/EmptyState.tsx +23 -0
- package/src/components/molecules/LoadingSpinner.tsx +27 -0
- package/src/components/molecules/PageHeader.tsx +22 -0
- package/src/components/molecules/Pagination.tsx +82 -0
- package/src/components/molecules/SearchInput.tsx +35 -0
- package/src/components/molecules/index.ts +12 -0
- package/src/components/organisms/CourseSidebar.tsx +276 -0
- package/src/components/organisms/LearnerNavbar.tsx +129 -0
- package/src/components/organisms/LearnerSidebar.tsx +148 -0
- package/src/components/organisms/LessonBookmarks.tsx +128 -0
- package/src/components/organisms/LessonNotes.tsx +153 -0
- package/src/components/organisms/index.ts +10 -0
- package/src/components/pages/BundleDetailPage.tsx +388 -0
- package/src/components/pages/CatalogBundlesPage.tsx +96 -0
- package/src/components/pages/CatalogCoursesPage.tsx +299 -0
- package/src/components/pages/CourseDetailPage.tsx +582 -0
- package/src/components/pages/CoursePlayerPage.tsx +481 -0
- package/src/components/pages/CreatorProfilePage.tsx +161 -0
- package/src/components/pages/LearnerSettingsPage.tsx +58 -0
- package/src/components/pages/ManualReviewDetailPage.tsx +254 -0
- package/src/components/pages/ManualReviewPage.tsx +228 -0
- package/src/components/pages/MessagesPage.tsx +285 -0
- package/src/components/pages/MyLearningPage.tsx +239 -0
- package/src/components/pages/PaymentCancelPage.tsx +74 -0
- package/src/components/pages/PaymentSuccessPage.tsx +73 -0
- package/src/components/pages/index.ts +13 -0
- package/src/components/utils.ts +6 -0
- package/src/contracts/components.contract.ts +89 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/layout.contract.ts +36 -0
- package/src/contracts/pages.contract.ts +275 -0
- package/src/contracts/template.contract.ts +100 -0
- package/src/default-template.tsx +52 -0
- package/src/hooks/index.ts +28 -0
- package/src/hooks/sdk-context.tsx +152 -0
- package/src/hooks/useAiCoach.ts +27 -0
- package/src/hooks/useBookmarks.ts +35 -0
- package/src/hooks/useCourseSearch.ts +18 -0
- package/src/hooks/useDebounce.ts +19 -0
- package/src/hooks/useMyBundles.ts +21 -0
- package/src/hooks/useMyCourses.ts +20 -0
- package/src/hooks/useNotes.ts +35 -0
- package/src/hooks/useNotifications.ts +16 -0
- package/src/hooks/useTheme.ts +17 -0
- package/src/hooks/useToast.ts +17 -0
- package/src/hooks/useUser.ts +33 -0
- package/src/index.ts +33 -0
- package/src/layouts/DefaultLayout.tsx +58 -0
- package/src/manifest.json +5 -0
- package/src/styles.css +43 -0
- package/src/types/ai-coach.ts +25 -0
- package/src/types/bookmarks.ts +20 -0
- package/src/types/bundle.ts +119 -0
- package/src/types/common.ts +24 -0
- package/src/types/course.ts +135 -0
- package/src/types/enrollment.ts +35 -0
- package/src/types/index.ts +15 -0
- package/src/types/lesson.ts +106 -0
- package/src/types/manual-review.ts +116 -0
- package/src/types/messaging.ts +109 -0
- package/src/types/notification.ts +30 -0
- package/src/types/payment.ts +40 -0
- package/src/types/progress.ts +19 -0
- package/src/types/rating.ts +20 -0
- package/src/types/search.ts +31 -0
- package/src/types/user.ts +16 -0
- package/src/utils/formatters.ts +74 -0
- package/src/utils/index.ts +8 -0
- package/dist/components/atoms/index.cjs +0 -318
- package/dist/components/atoms/index.js +0 -288
- package/dist/components/index.cjs +0 -1275
- package/dist/components/index.js +0 -1245
- package/dist/components/molecules/index.cjs +0 -334
- package/dist/components/molecules/index.js +0 -311
- package/dist/components/organisms/index.cjs +0 -855
- package/dist/components/organisms/index.js +0 -825
- package/dist/components/pages/index.cjs +0 -3306
- package/dist/components/pages/index.js +0 -3315
- package/dist/contracts/index.cjs +0 -52
- package/dist/contracts/index.js +0 -29
- package/dist/hooks/index.cjs +0 -165
- package/dist/hooks/index.js +0 -142
- package/dist/index.cjs +0 -630
- package/dist/index.js +0 -600
- package/dist/types/index.cjs +0 -18
- package/dist/types/index.js +0 -0
- package/dist/utils/index.cjs +0 -80
- package/dist/utils/index.js +0 -57
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Mail } from 'lucide-react';
|
|
3
|
+
import type { LearnerSettingsPageProps } from '../../contracts/pages.contract';
|
|
4
|
+
|
|
5
|
+
export function LearnerSettingsPage(props: Partial<LearnerSettingsPageProps>) {
|
|
6
|
+
const [marketingEmails, setMarketingEmails] = useState(true);
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="space-y-8">
|
|
10
|
+
{/* Header */}
|
|
11
|
+
<div>
|
|
12
|
+
<h1 className="text-3xl font-bold text-theme-text-primary">Settings</h1>
|
|
13
|
+
<p className="mt-2 text-lg text-theme-text-secondary">Manage your account settings and preferences</p>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
{/* Email Preferences Section */}
|
|
17
|
+
<div className="space-y-4">
|
|
18
|
+
<div>
|
|
19
|
+
<h2 className="text-2xl font-bold text-theme-text-primary">Email Preferences</h2>
|
|
20
|
+
<p className="mt-1 text-theme-text-secondary">Control what emails you receive from us</p>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
{/* Marketing Email Settings Card */}
|
|
24
|
+
<div className="p-6 w-full max-w-2xl rounded-lg border border-theme-border-primary bg-theme-bg-secondary">
|
|
25
|
+
<div className="flex items-start justify-between">
|
|
26
|
+
<div className="flex items-start gap-4">
|
|
27
|
+
<div className="rounded-lg bg-theme-bg-tertiary p-3">
|
|
28
|
+
<Mail className="h-5 w-5 text-theme-accent-primary" />
|
|
29
|
+
</div>
|
|
30
|
+
<div>
|
|
31
|
+
<h3 className="text-base font-semibold text-theme-text-primary">Marketing Emails</h3>
|
|
32
|
+
<p className="mt-1 text-sm text-theme-text-secondary">
|
|
33
|
+
Receive promotional updates, new courses, and special offers about our academy
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Toggle Switch */}
|
|
39
|
+
<button
|
|
40
|
+
onClick={() => setMarketingEmails(!marketingEmails)}
|
|
41
|
+
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
|
42
|
+
marketingEmails ? 'bg-theme-accent-primary' : 'bg-gray-200'
|
|
43
|
+
}`}
|
|
44
|
+
role="switch"
|
|
45
|
+
aria-checked={marketingEmails}
|
|
46
|
+
>
|
|
47
|
+
<span
|
|
48
|
+
className={`pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200 ${
|
|
49
|
+
marketingEmails ? 'translate-x-5' : 'translate-x-0'
|
|
50
|
+
}`}
|
|
51
|
+
/>
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { ArrowLeft, Calendar, BookOpen, Award, AlertCircle, CheckCircle, Clock, FileText } from 'lucide-react';
|
|
2
|
+
import type { ManualReviewDetailPageProps } from '../../contracts/pages.contract';
|
|
3
|
+
import { Button } from '../atoms/Button';
|
|
4
|
+
import { Badge } from '../atoms/Badge';
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../atoms/Card';
|
|
6
|
+
import { LoadingSpinner } from '../molecules/LoadingSpinner';
|
|
7
|
+
import { EmptyState } from '../molecules/EmptyState';
|
|
8
|
+
|
|
9
|
+
function formatDate(dateString: string) {
|
|
10
|
+
return new Date(dateString).toLocaleDateString('en-US', {
|
|
11
|
+
month: 'long',
|
|
12
|
+
day: 'numeric',
|
|
13
|
+
year: 'numeric',
|
|
14
|
+
hour: '2-digit',
|
|
15
|
+
minute: '2-digit',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatQuestionType(type: string) {
|
|
20
|
+
switch (type) {
|
|
21
|
+
case 'TRUE_FALSE': return 'True/False';
|
|
22
|
+
case 'SINGLE_CHOICE': return 'Single Choice';
|
|
23
|
+
case 'MULTIPLE_SELECT': return 'Multiple Select';
|
|
24
|
+
case 'TEXT': return 'Short Answer';
|
|
25
|
+
case 'ESSAY': return 'Essay';
|
|
26
|
+
default: return type;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getStatusBadge(status: string) {
|
|
31
|
+
switch (status) {
|
|
32
|
+
case 'IN_REVIEW':
|
|
33
|
+
return (
|
|
34
|
+
<Badge variant="warning" className="flex items-center gap-1">
|
|
35
|
+
<Clock className="h-3 w-3" />
|
|
36
|
+
Pending Review
|
|
37
|
+
</Badge>
|
|
38
|
+
);
|
|
39
|
+
case 'REQUEST_CHANGE':
|
|
40
|
+
return (
|
|
41
|
+
<Badge variant="destructive" className="flex items-center gap-1">
|
|
42
|
+
<AlertCircle className="h-3 w-3" />
|
|
43
|
+
Changes Requested
|
|
44
|
+
</Badge>
|
|
45
|
+
);
|
|
46
|
+
case 'GRADED':
|
|
47
|
+
return (
|
|
48
|
+
<Badge variant="success" className="flex items-center gap-1">
|
|
49
|
+
<CheckCircle className="h-3 w-3" />
|
|
50
|
+
Approved
|
|
51
|
+
</Badge>
|
|
52
|
+
);
|
|
53
|
+
default:
|
|
54
|
+
return <Badge variant="secondary">{status}</Badge>;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function ManualReviewDetailPage(props: Partial<ManualReviewDetailPageProps>) {
|
|
59
|
+
const noop = (..._args: any[]) => {};
|
|
60
|
+
const { submission, isLoading = false, notFound = false, onBack = noop } = props;
|
|
61
|
+
|
|
62
|
+
if (isLoading) return <LoadingSpinner className="py-20" text="Loading submission..." />;
|
|
63
|
+
|
|
64
|
+
if (notFound || !submission) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="container mx-auto p-6">
|
|
67
|
+
<div className="text-center py-12">
|
|
68
|
+
<AlertCircle className="h-12 w-12 text-theme-text-tertiary mx-auto mb-4" />
|
|
69
|
+
<h2 className="text-xl font-semibold text-theme-text-primary mb-2">Submission Not Found</h2>
|
|
70
|
+
<p className="text-theme-text-secondary mb-4">
|
|
71
|
+
The submission you're looking for doesn't exist or has been removed.
|
|
72
|
+
</p>
|
|
73
|
+
<Button onClick={onBack}>Back to Manual Review</Button>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const detail = submission as any;
|
|
80
|
+
const status = detail.context?.status || detail.status || 'IN_REVIEW';
|
|
81
|
+
const assignmentTitle = detail.context?.assignment?.title || detail.lessonTitle || 'Submission Detail';
|
|
82
|
+
const courseName = detail.context?.course?.name || detail.courseName || '';
|
|
83
|
+
const submittedAt = detail.context?.submittedAt || detail.submittedAt || '';
|
|
84
|
+
const totalQuestions = detail.context?.assignment?.totalQuestions || 0;
|
|
85
|
+
const totalPoints = detail.context?.assignment?.totalPoints || 0;
|
|
86
|
+
const score = detail.latestReview?.score ?? detail.answers?.reduce((sum: number, a: any) => sum + (a.pointsAwarded || 0), 0) ?? 0;
|
|
87
|
+
const answers = detail.answers || [];
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="container mx-auto space-y-6 max-w-6xl">
|
|
91
|
+
{/* Header */}
|
|
92
|
+
<div className="flex items-center gap-4">
|
|
93
|
+
<Button variant="ghost" size="icon" onClick={onBack}>
|
|
94
|
+
<ArrowLeft className="h-5 w-5" />
|
|
95
|
+
</Button>
|
|
96
|
+
<div className="flex-1">
|
|
97
|
+
<h1 className="text-3xl font-bold text-theme-text-primary">{assignmentTitle}</h1>
|
|
98
|
+
{courseName && <p className="text-theme-text-secondary mt-1">{courseName}</p>}
|
|
99
|
+
</div>
|
|
100
|
+
{getStatusBadge(status)}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Context Information */}
|
|
104
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
105
|
+
<Card>
|
|
106
|
+
<CardContent className="pt-6">
|
|
107
|
+
<Calendar className="h-5 w-5 text-purple-600 dark:text-purple-400 mb-3" />
|
|
108
|
+
<div>
|
|
109
|
+
<p className="text-sm text-theme-text-secondary">Submitted</p>
|
|
110
|
+
<p className="font-medium text-theme-text-primary">
|
|
111
|
+
{submittedAt ? formatDate(submittedAt) : '—'}
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
</CardContent>
|
|
115
|
+
</Card>
|
|
116
|
+
|
|
117
|
+
<Card>
|
|
118
|
+
<CardContent className="pt-6">
|
|
119
|
+
<BookOpen className="h-5 w-5 text-green-600 dark:text-green-400 mb-3" />
|
|
120
|
+
<div>
|
|
121
|
+
<p className="text-sm text-theme-text-secondary">Assignment Info</p>
|
|
122
|
+
<p className="font-medium text-theme-text-primary">{totalQuestions} Questions</p>
|
|
123
|
+
</div>
|
|
124
|
+
</CardContent>
|
|
125
|
+
</Card>
|
|
126
|
+
|
|
127
|
+
{status === 'IN_REVIEW' ? (
|
|
128
|
+
<Card>
|
|
129
|
+
<CardContent className="pt-6">
|
|
130
|
+
<Clock className="h-5 w-5 text-yellow-600 dark:text-yellow-400 mb-3" />
|
|
131
|
+
<div>
|
|
132
|
+
<p className="text-sm text-theme-text-secondary">Status</p>
|
|
133
|
+
<p className="font-medium text-theme-text-primary">Waiting for Review</p>
|
|
134
|
+
<p className="text-xs text-theme-text-tertiary mt-1">
|
|
135
|
+
Your submission is being reviewed by the instructor.
|
|
136
|
+
</p>
|
|
137
|
+
</div>
|
|
138
|
+
</CardContent>
|
|
139
|
+
</Card>
|
|
140
|
+
) : (
|
|
141
|
+
<Card>
|
|
142
|
+
<CardContent className="pt-6">
|
|
143
|
+
<Award className="h-5 w-5 text-blue-600 dark:text-blue-400 mb-3" />
|
|
144
|
+
<div>
|
|
145
|
+
<p className="text-sm text-theme-text-secondary">Score</p>
|
|
146
|
+
<p className="text-2xl font-bold text-theme-accent-primary">
|
|
147
|
+
{score}/{totalPoints}
|
|
148
|
+
</p>
|
|
149
|
+
{status === 'REQUEST_CHANGE' && (
|
|
150
|
+
<p className="text-xs text-theme-text-tertiary mt-1">
|
|
151
|
+
Score is provisional until review completes.
|
|
152
|
+
</p>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
</CardContent>
|
|
156
|
+
</Card>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* Changes Requested Card */}
|
|
161
|
+
{status === 'REQUEST_CHANGE' && (
|
|
162
|
+
<Card className="border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/10">
|
|
163
|
+
<CardContent className="pt-6">
|
|
164
|
+
<div className="flex items-start gap-4">
|
|
165
|
+
<AlertCircle className="h-6 w-6 text-red-600 dark:text-red-400 flex-shrink-0 mt-1" />
|
|
166
|
+
<div className="flex-1">
|
|
167
|
+
<h3 className="font-semibold text-theme-text-primary mb-2">Changes Requested</h3>
|
|
168
|
+
<p className="text-sm text-theme-text-secondary">
|
|
169
|
+
Your instructor has requested changes to your submission. Please review the
|
|
170
|
+
feedback below and resubmit your assignment through the course player.
|
|
171
|
+
</p>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</CardContent>
|
|
175
|
+
</Card>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{/* Submitted Answers */}
|
|
179
|
+
{answers.length > 0 && (
|
|
180
|
+
<Card>
|
|
181
|
+
<CardHeader>
|
|
182
|
+
<CardTitle className="flex items-center gap-2">
|
|
183
|
+
<FileText className="h-5 w-5" />
|
|
184
|
+
Submitted Answers ({answers.length})
|
|
185
|
+
</CardTitle>
|
|
186
|
+
</CardHeader>
|
|
187
|
+
<CardContent className="space-y-6">
|
|
188
|
+
{answers.map((answer: any, index: number) => (
|
|
189
|
+
<div
|
|
190
|
+
key={answer.questionId || index}
|
|
191
|
+
className="pb-6 border-b border-theme-border-primary last:border-b-0 last:pb-0"
|
|
192
|
+
>
|
|
193
|
+
<div className="flex items-start gap-3 mb-3">
|
|
194
|
+
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-theme-accent-primary text-white flex items-center justify-center font-semibold text-sm">
|
|
195
|
+
{index + 1}
|
|
196
|
+
</div>
|
|
197
|
+
<div className="flex-1">
|
|
198
|
+
<div className="flex items-start justify-between gap-4 mb-2">
|
|
199
|
+
<h4 className="font-medium text-theme-text-primary">{answer.questionText}</h4>
|
|
200
|
+
<div className="flex items-center gap-2 flex-shrink-0">
|
|
201
|
+
<Badge variant="secondary" className="text-xs">
|
|
202
|
+
{formatQuestionType(answer.questionType)}
|
|
203
|
+
</Badge>
|
|
204
|
+
<span className="text-sm text-theme-text-secondary">
|
|
205
|
+
{answer.questionPoints} pts
|
|
206
|
+
</span>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div className="mt-3 space-y-3">
|
|
211
|
+
{answer.selectedOptionText && (
|
|
212
|
+
<div className="p-4 bg-theme-bg-secondary rounded-lg border border-theme-border-primary">
|
|
213
|
+
<p className="text-xs font-medium text-theme-text-secondary mb-2">Your Answer:</p>
|
|
214
|
+
<p className="text-theme-text-primary font-medium">{answer.selectedOptionText}</p>
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{answer.selectedOptionIds && answer.selectedOptionIds.length > 0 && !answer.selectedOptionText && (
|
|
219
|
+
<div className="p-4 bg-theme-bg-secondary rounded-lg border border-theme-border-primary">
|
|
220
|
+
<p className="text-xs font-medium text-theme-text-secondary mb-2">Selected Options:</p>
|
|
221
|
+
<div className="space-y-2">
|
|
222
|
+
{answer.selectedOptions && answer.selectedOptions.length > 0 ? (
|
|
223
|
+
answer.selectedOptions.map((option: any, idx: number) => (
|
|
224
|
+
<div key={idx} className="flex items-center gap-2">
|
|
225
|
+
<div className="h-2 w-2 rounded-full bg-theme-accent-primary" />
|
|
226
|
+
<span className="text-theme-text-primary">{option.text}</span>
|
|
227
|
+
</div>
|
|
228
|
+
))
|
|
229
|
+
) : (
|
|
230
|
+
<p className="text-theme-text-secondary text-sm">
|
|
231
|
+
{answer.selectedOptionIds.length} option{answer.selectedOptionIds.length !== 1 ? 's' : ''} selected
|
|
232
|
+
</p>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{answer.answerText && (
|
|
239
|
+
<div className="p-4 bg-theme-bg-secondary rounded-lg border border-theme-border-primary">
|
|
240
|
+
<p className="text-xs font-medium text-theme-text-secondary mb-2">Your Answer:</p>
|
|
241
|
+
<p className="text-theme-text-primary whitespace-pre-wrap">{answer.answerText}</p>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
))}
|
|
249
|
+
</CardContent>
|
|
250
|
+
</Card>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { ClipboardList, CheckCircle, AlertCircle, FileText, Calendar, Search } from 'lucide-react';
|
|
2
|
+
import type { ManualReviewPageProps } from '../../contracts/pages.contract';
|
|
3
|
+
import { Badge } from '../atoms/Badge';
|
|
4
|
+
import { Input } from '../atoms/Input';
|
|
5
|
+
import { Pagination } from '../molecules/Pagination';
|
|
6
|
+
import { EmptyState } from '../molecules/EmptyState';
|
|
7
|
+
import { LoadingSpinner } from '../molecules/LoadingSpinner';
|
|
8
|
+
import { cn } from '../utils';
|
|
9
|
+
|
|
10
|
+
const statusDisplay: Record<string, { label: string; variant: 'success' | 'warning' | 'destructive' | 'secondary' }> = {
|
|
11
|
+
IN_REVIEW: { label: 'Pending Review', variant: 'warning' },
|
|
12
|
+
REQUEST_CHANGE: { label: 'Changes Requested', variant: 'destructive' },
|
|
13
|
+
GRADED: { label: 'Approved', variant: 'success' },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function formatDate(dateString: string) {
|
|
17
|
+
return new Date(dateString).toLocaleDateString('en-US', {
|
|
18
|
+
month: 'short',
|
|
19
|
+
day: 'numeric',
|
|
20
|
+
year: 'numeric',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ManualReviewPage(props: Partial<ManualReviewPageProps>) {
|
|
25
|
+
const noop = (..._args: any[]) => {};
|
|
26
|
+
const {
|
|
27
|
+
submissions = [], summary, pagination, isLoading = false,
|
|
28
|
+
filterStatus = 'ALL' as const, onFilterStatusChange = noop,
|
|
29
|
+
searchQuery = '', onSearchChange = noop,
|
|
30
|
+
currentPage = 1, onPageChange = noop,
|
|
31
|
+
onSubmissionClick = noop,
|
|
32
|
+
} = props;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="space-y-6">
|
|
36
|
+
{/* Header */}
|
|
37
|
+
<div className="flex items-center justify-between">
|
|
38
|
+
<div>
|
|
39
|
+
<h1 className="text-3xl font-bold text-theme-text-primary">Manual Review</h1>
|
|
40
|
+
<p className="text-theme-text-secondary mt-1">
|
|
41
|
+
Track your assignment submissions and review status
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{/* Status Cards */}
|
|
47
|
+
<div className="grid grid-cols-2 gap-6 md:grid-cols-4">
|
|
48
|
+
<button
|
|
49
|
+
onClick={() => { onFilterStatusChange('IN_REVIEW'); onPageChange(1); }}
|
|
50
|
+
className={cn(
|
|
51
|
+
'relative rounded-lg border-2 p-4 text-left transition-all duration-300 ease-in-out hover:scale-105 hover:shadow-md hover:z-20 cursor-pointer',
|
|
52
|
+
filterStatus === 'IN_REVIEW'
|
|
53
|
+
? 'border-yellow-500 bg-yellow-50 dark:bg-yellow-900/20 scale-105 shadow-md z-20'
|
|
54
|
+
: 'border-theme-border-primary hover:border-yellow-500'
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
<div className="flex items-center justify-between">
|
|
58
|
+
<div>
|
|
59
|
+
<p className="text-sm font-medium text-theme-text-muted">Pending Review</p>
|
|
60
|
+
<p className="text-2xl font-bold text-yellow-600">{summary?.inReviewSubmissions ?? 0}</p>
|
|
61
|
+
</div>
|
|
62
|
+
<ClipboardList className="h-6 w-6 text-yellow-600" />
|
|
63
|
+
</div>
|
|
64
|
+
<p className="mt-1 text-xs text-theme-text-muted">Awaiting review</p>
|
|
65
|
+
</button>
|
|
66
|
+
|
|
67
|
+
<button
|
|
68
|
+
onClick={() => { onFilterStatusChange('REQUEST_CHANGE'); onPageChange(1); }}
|
|
69
|
+
className={cn(
|
|
70
|
+
'relative rounded-lg border-2 p-4 text-left transition-all duration-300 ease-in-out hover:scale-105 hover:shadow-md hover:z-20 cursor-pointer',
|
|
71
|
+
filterStatus === 'REQUEST_CHANGE'
|
|
72
|
+
? 'border-red-500 bg-red-50 dark:bg-red-900/20 scale-105 shadow-md z-20'
|
|
73
|
+
: 'border-theme-border-primary hover:border-red-500'
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
<div className="flex items-center justify-between">
|
|
77
|
+
<div>
|
|
78
|
+
<p className="text-sm font-medium text-theme-text-muted">Changes Requested</p>
|
|
79
|
+
<p className="text-2xl font-bold text-red-600">{summary?.requestChangeSubmissions ?? 0}</p>
|
|
80
|
+
</div>
|
|
81
|
+
<AlertCircle className="h-6 w-6 text-red-600" />
|
|
82
|
+
</div>
|
|
83
|
+
<p className="mt-1 text-xs text-theme-text-muted">Needs revision</p>
|
|
84
|
+
</button>
|
|
85
|
+
|
|
86
|
+
<button
|
|
87
|
+
onClick={() => { onFilterStatusChange('GRADED'); onPageChange(1); }}
|
|
88
|
+
className={cn(
|
|
89
|
+
'relative rounded-lg border-2 p-4 text-left transition-all duration-300 ease-in-out hover:scale-105 hover:shadow-md hover:z-20 cursor-pointer',
|
|
90
|
+
filterStatus === 'GRADED'
|
|
91
|
+
? 'border-green-500 bg-green-50 dark:bg-green-900/20 scale-105 shadow-md z-20'
|
|
92
|
+
: 'border-theme-border-primary hover:border-green-500'
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
<div className="flex items-center justify-between">
|
|
96
|
+
<div>
|
|
97
|
+
<p className="text-sm font-medium text-theme-text-muted">Approved</p>
|
|
98
|
+
<p className="text-2xl font-bold text-green-600">{summary?.gradedSubmissions ?? 0}</p>
|
|
99
|
+
</div>
|
|
100
|
+
<CheckCircle className="h-6 w-6 text-green-600" />
|
|
101
|
+
</div>
|
|
102
|
+
<p className="mt-1 text-xs text-theme-text-muted">Completed</p>
|
|
103
|
+
</button>
|
|
104
|
+
|
|
105
|
+
<button
|
|
106
|
+
onClick={() => { onFilterStatusChange('ALL'); onPageChange(1); }}
|
|
107
|
+
className={cn(
|
|
108
|
+
'relative rounded-lg border-2 p-4 text-left transition-all duration-300 ease-in-out hover:scale-105 hover:shadow-md hover:z-20 cursor-pointer',
|
|
109
|
+
filterStatus === 'ALL'
|
|
110
|
+
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 scale-105 shadow-md z-20'
|
|
111
|
+
: 'border-theme-border-primary hover:border-blue-500'
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
<div className="flex items-center justify-between">
|
|
115
|
+
<div>
|
|
116
|
+
<p className="text-sm font-medium text-theme-text-muted">Total</p>
|
|
117
|
+
<p className="text-2xl font-bold text-blue-600">{summary?.totalSubmissions ?? 0}</p>
|
|
118
|
+
</div>
|
|
119
|
+
<FileText className="h-6 w-6 text-blue-600" />
|
|
120
|
+
</div>
|
|
121
|
+
<p className="mt-1 text-xs text-theme-text-muted">All submissions</p>
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{/* Search and Filters */}
|
|
126
|
+
<div className="space-y-4">
|
|
127
|
+
<div className="flex flex-col gap-3 sm:flex-row">
|
|
128
|
+
<div className="relative flex-1">
|
|
129
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-theme-text-tertiary z-10 pointer-events-none" />
|
|
130
|
+
<Input
|
|
131
|
+
type="text"
|
|
132
|
+
placeholder="Search learner, course, or assignment..."
|
|
133
|
+
value={searchQuery}
|
|
134
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
135
|
+
className="pl-10"
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
<div className="w-full sm:w-auto">
|
|
139
|
+
<select
|
|
140
|
+
value={filterStatus}
|
|
141
|
+
onChange={(e) => { onFilterStatusChange(e.target.value as any); onPageChange(1); }}
|
|
142
|
+
className="w-full h-10 px-3 rounded-md border border-theme-border-primary bg-theme-bg-primary text-theme-text-primary text-sm focus:outline-none focus:ring-2 focus:ring-theme-accent-primary cursor-pointer"
|
|
143
|
+
>
|
|
144
|
+
<option value="ALL">All Statuses</option>
|
|
145
|
+
<option value="IN_REVIEW">Pending Review</option>
|
|
146
|
+
<option value="REQUEST_CHANGE">Changes Requested</option>
|
|
147
|
+
<option value="GRADED">Approved</option>
|
|
148
|
+
</select>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{/* Data Table */}
|
|
154
|
+
{isLoading ? (
|
|
155
|
+
<LoadingSpinner className="py-20" text="Loading submissions..." />
|
|
156
|
+
) : submissions.length === 0 ? (
|
|
157
|
+
<EmptyState
|
|
158
|
+
icon={<ClipboardList className="w-16 h-16" />}
|
|
159
|
+
title="No submissions found"
|
|
160
|
+
description="You haven't submitted anything for manual review yet."
|
|
161
|
+
/>
|
|
162
|
+
) : (
|
|
163
|
+
<>
|
|
164
|
+
<div className="bg-theme-bg-primary rounded-lg border border-theme-border-primary">
|
|
165
|
+
{/* Table Header */}
|
|
166
|
+
<div className="hidden sm:grid grid-cols-12 gap-4 px-5 py-3 bg-theme-bg-secondary text-xs font-semibold text-theme-text-muted uppercase tracking-wider border-b border-theme-border-primary rounded-t-lg">
|
|
167
|
+
<div className="col-span-5">Assignment</div>
|
|
168
|
+
<div className="col-span-2">Submitted</div>
|
|
169
|
+
<div className="col-span-3">Status</div>
|
|
170
|
+
<div className="col-span-2 text-right">Score</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* Table Rows */}
|
|
174
|
+
<div className="divide-y divide-theme-border-primary">
|
|
175
|
+
{submissions.map((sub: any) => (
|
|
176
|
+
<div
|
|
177
|
+
key={sub.id}
|
|
178
|
+
onClick={() => onSubmissionClick(sub.id)}
|
|
179
|
+
className="grid grid-cols-1 sm:grid-cols-12 gap-2 sm:gap-4 px-5 py-4 hover:bg-theme-bg-secondary/50 transition-colors cursor-pointer group"
|
|
180
|
+
>
|
|
181
|
+
{/* Assignment */}
|
|
182
|
+
<div className="sm:col-span-5">
|
|
183
|
+
<div className="font-medium text-theme-text-primary group-hover:text-theme-accent-primary transition-colors truncate max-w-xs">
|
|
184
|
+
{sub.assignment?.title || sub.lessonTitle || sub.title || 'Submission'}
|
|
185
|
+
</div>
|
|
186
|
+
<div className="text-xs text-theme-text-tertiary">
|
|
187
|
+
{sub.assignment?.courseName || sub.courseName || ''}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
{/* Date */}
|
|
191
|
+
<div className="sm:col-span-2 flex items-center gap-1 text-sm text-theme-text-secondary">
|
|
192
|
+
<Calendar className="h-4 w-4" />
|
|
193
|
+
{sub.submittedAt ? formatDate(sub.submittedAt) : '—'}
|
|
194
|
+
</div>
|
|
195
|
+
{/* Status */}
|
|
196
|
+
<div className="sm:col-span-3 flex items-center">
|
|
197
|
+
<Badge variant={statusDisplay[sub.status]?.variant || 'secondary'}>
|
|
198
|
+
{statusDisplay[sub.status]?.label || sub.status}
|
|
199
|
+
</Badge>
|
|
200
|
+
</div>
|
|
201
|
+
{/* Score */}
|
|
202
|
+
<div className="sm:col-span-2 flex items-center justify-end">
|
|
203
|
+
<span className="font-medium text-theme-text-primary">
|
|
204
|
+
{sub.score ?? 0}/{sub.totalPoints ?? 0}
|
|
205
|
+
</span>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{pagination && pagination.totalPages > 1 && (
|
|
213
|
+
<Pagination
|
|
214
|
+
currentPage={currentPage}
|
|
215
|
+
totalPages={pagination.totalPages}
|
|
216
|
+
total={pagination.totalItems}
|
|
217
|
+
pageSize={pagination.itemsPerPage || 10}
|
|
218
|
+
hasNextPage={pagination.hasNextPage}
|
|
219
|
+
hasPreviousPage={pagination.hasPreviousPage}
|
|
220
|
+
onPageChange={onPageChange}
|
|
221
|
+
className="mt-4"
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
224
|
+
</>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|