@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.
Files changed (102) hide show
  1. package/dist/bundle.js +70 -0
  2. package/dist/manifest.json +5 -0
  3. package/dist/styles.css +3307 -0
  4. package/package.json +40 -46
  5. package/src/components/atoms/Avatar.tsx +38 -0
  6. package/src/components/atoms/Badge.tsx +32 -0
  7. package/src/components/atoms/Button.tsx +48 -0
  8. package/src/components/atoms/Card.tsx +33 -0
  9. package/src/components/atoms/Input.tsx +39 -0
  10. package/src/components/atoms/ProgressBar.tsx +52 -0
  11. package/src/components/atoms/Tabs.tsx +47 -0
  12. package/src/components/atoms/index.ts +10 -0
  13. package/src/components/index.ts +11 -0
  14. package/src/components/molecules/CourseCard.tsx +215 -0
  15. package/src/components/molecules/EmptyState.tsx +23 -0
  16. package/src/components/molecules/LoadingSpinner.tsx +27 -0
  17. package/src/components/molecules/PageHeader.tsx +22 -0
  18. package/src/components/molecules/Pagination.tsx +82 -0
  19. package/src/components/molecules/SearchInput.tsx +35 -0
  20. package/src/components/molecules/index.ts +12 -0
  21. package/src/components/organisms/CourseSidebar.tsx +276 -0
  22. package/src/components/organisms/LearnerNavbar.tsx +129 -0
  23. package/src/components/organisms/LearnerSidebar.tsx +148 -0
  24. package/src/components/organisms/LessonBookmarks.tsx +128 -0
  25. package/src/components/organisms/LessonNotes.tsx +153 -0
  26. package/src/components/organisms/index.ts +10 -0
  27. package/src/components/pages/BundleDetailPage.tsx +388 -0
  28. package/src/components/pages/CatalogBundlesPage.tsx +96 -0
  29. package/src/components/pages/CatalogCoursesPage.tsx +299 -0
  30. package/src/components/pages/CourseDetailPage.tsx +582 -0
  31. package/src/components/pages/CoursePlayerPage.tsx +481 -0
  32. package/src/components/pages/CreatorProfilePage.tsx +161 -0
  33. package/src/components/pages/LearnerSettingsPage.tsx +58 -0
  34. package/src/components/pages/ManualReviewDetailPage.tsx +254 -0
  35. package/src/components/pages/ManualReviewPage.tsx +228 -0
  36. package/src/components/pages/MessagesPage.tsx +285 -0
  37. package/src/components/pages/MyLearningPage.tsx +239 -0
  38. package/src/components/pages/PaymentCancelPage.tsx +74 -0
  39. package/src/components/pages/PaymentSuccessPage.tsx +73 -0
  40. package/src/components/pages/index.ts +13 -0
  41. package/src/components/utils.ts +6 -0
  42. package/src/contracts/components.contract.ts +89 -0
  43. package/src/contracts/index.ts +4 -0
  44. package/src/contracts/layout.contract.ts +36 -0
  45. package/src/contracts/pages.contract.ts +275 -0
  46. package/src/contracts/template.contract.ts +100 -0
  47. package/src/default-template.tsx +52 -0
  48. package/src/hooks/index.ts +28 -0
  49. package/src/hooks/sdk-context.tsx +152 -0
  50. package/src/hooks/useAiCoach.ts +27 -0
  51. package/src/hooks/useBookmarks.ts +35 -0
  52. package/src/hooks/useCourseSearch.ts +18 -0
  53. package/src/hooks/useDebounce.ts +19 -0
  54. package/src/hooks/useMyBundles.ts +21 -0
  55. package/src/hooks/useMyCourses.ts +20 -0
  56. package/src/hooks/useNotes.ts +35 -0
  57. package/src/hooks/useNotifications.ts +16 -0
  58. package/src/hooks/useTheme.ts +17 -0
  59. package/src/hooks/useToast.ts +17 -0
  60. package/src/hooks/useUser.ts +33 -0
  61. package/src/index.ts +33 -0
  62. package/src/layouts/DefaultLayout.tsx +58 -0
  63. package/src/manifest.json +5 -0
  64. package/src/styles.css +43 -0
  65. package/src/types/ai-coach.ts +25 -0
  66. package/src/types/bookmarks.ts +20 -0
  67. package/src/types/bundle.ts +119 -0
  68. package/src/types/common.ts +24 -0
  69. package/src/types/course.ts +135 -0
  70. package/src/types/enrollment.ts +35 -0
  71. package/src/types/index.ts +15 -0
  72. package/src/types/lesson.ts +106 -0
  73. package/src/types/manual-review.ts +116 -0
  74. package/src/types/messaging.ts +109 -0
  75. package/src/types/notification.ts +30 -0
  76. package/src/types/payment.ts +40 -0
  77. package/src/types/progress.ts +19 -0
  78. package/src/types/rating.ts +20 -0
  79. package/src/types/search.ts +31 -0
  80. package/src/types/user.ts +16 -0
  81. package/src/utils/formatters.ts +74 -0
  82. package/src/utils/index.ts +8 -0
  83. package/dist/components/atoms/index.cjs +0 -318
  84. package/dist/components/atoms/index.js +0 -288
  85. package/dist/components/index.cjs +0 -1275
  86. package/dist/components/index.js +0 -1245
  87. package/dist/components/molecules/index.cjs +0 -334
  88. package/dist/components/molecules/index.js +0 -311
  89. package/dist/components/organisms/index.cjs +0 -855
  90. package/dist/components/organisms/index.js +0 -825
  91. package/dist/components/pages/index.cjs +0 -3306
  92. package/dist/components/pages/index.js +0 -3315
  93. package/dist/contracts/index.cjs +0 -52
  94. package/dist/contracts/index.js +0 -29
  95. package/dist/hooks/index.cjs +0 -165
  96. package/dist/hooks/index.js +0 -142
  97. package/dist/index.cjs +0 -630
  98. package/dist/index.js +0 -600
  99. package/dist/types/index.cjs +0 -18
  100. package/dist/types/index.js +0 -0
  101. package/dist/utils/index.cjs +0 -80
  102. 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
+ }