@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,299 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Search, BookOpen, Package, Twitter, Facebook, Instagram } from 'lucide-react';
|
|
3
|
+
import type { CatalogCoursesPageProps } from '../../contracts/pages.contract';
|
|
4
|
+
import { Pagination } from '../molecules/Pagination';
|
|
5
|
+
import { CourseCard } from '../molecules/CourseCard';
|
|
6
|
+
|
|
7
|
+
const FILE_URL = 'https://fe-academy-file.dev.magicai-testing-domain.work';
|
|
8
|
+
|
|
9
|
+
const FEATURES = [
|
|
10
|
+
'Free E-book, video & consolation',
|
|
11
|
+
'Top instructors from around world',
|
|
12
|
+
'Top courses from your team',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const DEALS = [
|
|
16
|
+
{ id: 1, discount: '50%', title: 'Lorem ipsum dolor', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor', image: `${FILE_URL}/deal-1.png` },
|
|
17
|
+
{ id: 2, discount: '10%', title: 'Lorem ipsum dolor', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor', image: `${FILE_URL}/deal-2.png` },
|
|
18
|
+
{ id: 3, discount: '50%', title: 'Lorem ipsum dolor', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor', image: `${FILE_URL}/deal-3.png` },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const RECOMMENDED = [
|
|
22
|
+
{ id: 'd87b963f', title: 'testtgfsdfg dfs', thumbnail: `${FILE_URL}/uploads/4e2fd752-e8c0-440a-8622-3102d33e56e7-1771918805146.png`, totalLessons: 1, progress: 0, isFree: true, price: 0 },
|
|
23
|
+
{ id: '30ceedf8', title: 'tessst 11', thumbnail: `${FILE_URL}/uploads/ec2ebd93-f06e-48ce-914a-116b1b91c480-1771878020911.jpeg`, totalLessons: 1, progress: 0, isFree: true, price: 0 },
|
|
24
|
+
{ id: 'eae7f568', title: 'React', thumbnail: `${FILE_URL}/uploads/ca3257ea-618c-4a61-a4b6-5560c2bf5ec1-1771488717244.png`, totalLessons: 2, progress: 0, isFree: true, price: 0 },
|
|
25
|
+
{ id: '26a52485', title: 'll1', thumbnail: `${FILE_URL}/uploads/9bf7f1ca-214b-4a82-862d-f334801bcd53-1771486627052.png`, totalLessons: 1, progress: 0, isFree: true, price: 0 },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const SIDE_AVATARS = [`${FILE_URL}/avatar-1.png`, `${FILE_URL}/avatar-2.png`, `${FILE_URL}/avatar-3.png`, `${FILE_URL}/avatar-4.png`];
|
|
29
|
+
|
|
30
|
+
export function CatalogCoursesPage(props: Partial<CatalogCoursesPageProps>) {
|
|
31
|
+
const noop = (..._args: any[]) => {};
|
|
32
|
+
const {
|
|
33
|
+
courses = [], meta, loading = false, searchQuery = '', page = 1,
|
|
34
|
+
onSearchChange = noop, onPageChange = noop, onCourseClick = noop,
|
|
35
|
+
activeTab = 'courses', onTabChange,
|
|
36
|
+
bundleCourses = [], bundleMeta, bundlesLoading = false, bundlePage = 1,
|
|
37
|
+
onBundlePageChange = noop, onBundleClick = noop,
|
|
38
|
+
} = props as any;
|
|
39
|
+
|
|
40
|
+
const [localQuery, setLocalQuery] = useState(searchQuery);
|
|
41
|
+
const defaultHeroImage = `${FILE_URL}/catalog-hero-banner.png`;
|
|
42
|
+
|
|
43
|
+
const handleSearchSubmit = (e: React.FormEvent) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
onSearchChange(localQuery);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleSearchChange = (val: string) => {
|
|
49
|
+
setLocalQuery(val);
|
|
50
|
+
onSearchChange(val);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const displayCourses = activeTab === 'courses' ? courses : bundleCourses;
|
|
54
|
+
const displayMeta = activeTab === 'courses' ? meta : bundleMeta;
|
|
55
|
+
const displayLoading = activeTab === 'courses' ? loading : bundlesLoading;
|
|
56
|
+
const displayPage = activeTab === 'courses' ? page : bundlePage;
|
|
57
|
+
const displayPageChange = activeTab === 'courses' ? onPageChange : onBundlePageChange;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="container mx-auto space-y-10">
|
|
61
|
+
|
|
62
|
+
{/* Hero Search Bar */}
|
|
63
|
+
<div className="relative w-full overflow-hidden rounded-2xl">
|
|
64
|
+
<div
|
|
65
|
+
className="absolute inset-0 z-0 bg-cover bg-center"
|
|
66
|
+
style={{ backgroundImage: `var(--catalog-hero-image, url(${defaultHeroImage}))` }}
|
|
67
|
+
>
|
|
68
|
+
<div
|
|
69
|
+
className="absolute inset-0 backdrop-blur-[1px]"
|
|
70
|
+
style={{ background: 'linear-gradient(135deg, rgba(73,187,189,0.75) 0%, rgba(73,187,189,0.55) 50%, rgba(73,187,189,0.40) 100%)' }}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="relative z-10 flex flex-col items-center justify-center px-4 py-8 sm:px-6 sm:py-10 gap-4">
|
|
74
|
+
<form
|
|
75
|
+
onSubmit={handleSearchSubmit}
|
|
76
|
+
className="flex w-full max-w-[680px] bg-white rounded-2xl sm:rounded-full p-1 sm:p-[5px] sm:pl-0 shadow-lg flex-col sm:flex-row gap-1 sm:gap-0 sm:items-center"
|
|
77
|
+
>
|
|
78
|
+
<div className="relative flex-1 flex items-center w-full">
|
|
79
|
+
<Search className="absolute left-4 sm:left-5 w-5 h-5 text-gray-400 pointer-events-none" />
|
|
80
|
+
<input
|
|
81
|
+
type="text"
|
|
82
|
+
placeholder={activeTab === 'courses' ? 'Search your favourite course' : 'Search your favourite bundle'}
|
|
83
|
+
value={localQuery}
|
|
84
|
+
onChange={(e) => handleSearchChange(e.target.value)}
|
|
85
|
+
className="w-full py-3 sm:py-3.5 pl-12 sm:pl-13 pr-4 bg-transparent text-gray-700 text-sm sm:text-[15px] rounded-full border-none outline-none placeholder:text-gray-400"
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
<button
|
|
89
|
+
type="submit"
|
|
90
|
+
className="shrink-0 px-8 py-3 bg-theme-accent-primary text-white font-semibold text-sm sm:text-[15px] rounded-xl sm:rounded-full cursor-pointer transition-all duration-150 hover:brightness-110 hover:scale-[1.02] active:scale-[0.98] w-full sm:w-auto"
|
|
91
|
+
>
|
|
92
|
+
Search
|
|
93
|
+
</button>
|
|
94
|
+
</form>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Tab Bar */}
|
|
99
|
+
<div className="flex gap-2">
|
|
100
|
+
{[{ id: 'courses', label: 'Courses', Icon: BookOpen }, { id: 'bundles', label: 'Bundles', Icon: Package }].map(({ id, label, Icon }) => (
|
|
101
|
+
<button
|
|
102
|
+
key={id}
|
|
103
|
+
onClick={() => onTabChange?.(id)}
|
|
104
|
+
className={`flex items-center gap-2 px-4 py-2.5 font-medium transition-colors cursor-pointer ${
|
|
105
|
+
activeTab === id
|
|
106
|
+
? 'border-b-2 border-theme-accent-primary text-theme-accent-primary'
|
|
107
|
+
: 'text-theme-text-secondary hover:text-theme-text-primary'
|
|
108
|
+
}`}
|
|
109
|
+
>
|
|
110
|
+
<Icon className="h-4 w-4" />
|
|
111
|
+
{label}
|
|
112
|
+
</button>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{/* Course / Bundle Grid */}
|
|
117
|
+
{displayLoading ? (
|
|
118
|
+
<div className="flex flex-col items-center justify-center py-16">
|
|
119
|
+
<div className="h-12 w-12 animate-spin rounded-full border-4 border-theme-border-primary border-t-theme-accent-primary"></div>
|
|
120
|
+
<p className="mt-4 text-theme-text-secondary">Loading {activeTab}...</p>
|
|
121
|
+
</div>
|
|
122
|
+
) : displayCourses.length === 0 ? (
|
|
123
|
+
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-theme-border-primary bg-theme-bg-secondary py-16 text-center">
|
|
124
|
+
<div className="text-4xl mb-4">{activeTab === 'courses' ? '📚' : '📦'}</div>
|
|
125
|
+
<h3 className="text-xl font-semibold text-theme-text-primary">No {activeTab} found</h3>
|
|
126
|
+
<p className="mt-2 max-w-md text-theme-text-secondary">Try adjusting your search query or check back later</p>
|
|
127
|
+
</div>
|
|
128
|
+
) : (
|
|
129
|
+
<>
|
|
130
|
+
<div className="grid gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
131
|
+
{displayCourses.map((course: any) => {
|
|
132
|
+
const description = (course.summary || course.description || '').replace(/<[^>]*>/g, '');
|
|
133
|
+
const price = course.price ?? 0;
|
|
134
|
+
const isFree = course.isFree !== undefined ? course.isFree : price === 0;
|
|
135
|
+
return (
|
|
136
|
+
<CourseCard
|
|
137
|
+
key={course.id}
|
|
138
|
+
id={course.id}
|
|
139
|
+
title={course.title || ''}
|
|
140
|
+
thumbnail={course.thumbnail || ''}
|
|
141
|
+
totalLessons={course.totalLessons || 0}
|
|
142
|
+
progress={0}
|
|
143
|
+
showProgress={false}
|
|
144
|
+
isFree={isFree}
|
|
145
|
+
price={price}
|
|
146
|
+
showPrice={true}
|
|
147
|
+
currency={course.currency}
|
|
148
|
+
description={description}
|
|
149
|
+
instructorName={course.instructorName}
|
|
150
|
+
instructorAvatar={course.instructorAvatar}
|
|
151
|
+
category={course.category}
|
|
152
|
+
originalPrice={course.originalPrice}
|
|
153
|
+
isBundle={activeTab === 'bundles'}
|
|
154
|
+
thumbnails={course.thumbnails}
|
|
155
|
+
onClick={() => activeTab === 'courses' ? onCourseClick(course.id) : onBundleClick(course.id)}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
})}
|
|
159
|
+
</div>
|
|
160
|
+
{displayMeta && displayMeta.totalPages > 1 && (
|
|
161
|
+
<Pagination
|
|
162
|
+
currentPage={displayPage}
|
|
163
|
+
totalPages={displayMeta.totalPages}
|
|
164
|
+
total={displayMeta.total}
|
|
165
|
+
pageSize={displayMeta.limit || 8}
|
|
166
|
+
hasNextPage={displayPage < displayMeta.totalPages}
|
|
167
|
+
hasPreviousPage={displayPage > 1}
|
|
168
|
+
onPageChange={displayPageChange}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Know about learning platform */}
|
|
175
|
+
<section className="rounded-2xl bg-[#EEF4FA] p-6 sm:p-8 lg:p-10 overflow-hidden">
|
|
176
|
+
<div className="flex flex-col items-center gap-8 lg:flex-row lg:items-center lg:justify-between px-10">
|
|
177
|
+
<div className="space-y-6 lg:max-w-md shrink-0 py-4">
|
|
178
|
+
<h2 className="text-2xl font-bold text-gray-900 sm:text-3xl leading-snug">
|
|
179
|
+
Know about learning<br />learning platform
|
|
180
|
+
</h2>
|
|
181
|
+
<ul className="space-y-5">
|
|
182
|
+
{FEATURES.map((feature, i) => (
|
|
183
|
+
<li key={i} className="flex items-center gap-4 text-sm text-gray-600 font-medium">
|
|
184
|
+
<span className="h-4 w-4 rounded-full bg-[#B8F0E8] shrink-0" />
|
|
185
|
+
{feature}
|
|
186
|
+
</li>
|
|
187
|
+
))}
|
|
188
|
+
</ul>
|
|
189
|
+
<button className="inline-block rounded-lg bg-theme-accent-primary px-8 py-3.5 text-sm font-bold text-white shadow-md transition-all hover:brightness-110 hover:scale-[1.02] active:scale-[0.98]">
|
|
190
|
+
Start learning now
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
<div className="relative w-full max-w-[520px] lg:w-[55%]">
|
|
194
|
+
<div className="aspect-[16/10] w-full flex items-center justify-center bg-[#dbeafe] rounded-xl">
|
|
195
|
+
<span className="text-gray-400 text-sm">Platform Preview</span>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</section>
|
|
200
|
+
|
|
201
|
+
{/* Recommended for you */}
|
|
202
|
+
<div className="relative w-screen left-1/2 right-1/2 -ml-[50vw] -mr-[50vw] bg-[#EEF4FA]">
|
|
203
|
+
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-12">
|
|
204
|
+
<section className="space-y-6">
|
|
205
|
+
<div className="flex items-center justify-between">
|
|
206
|
+
<h2 className="text-xl font-bold text-gray-900 sm:text-2xl">Recommended for you</h2>
|
|
207
|
+
<button className="shrink-0 text-sm font-semibold text-[rgb(var(--accent-primary))] hover:underline cursor-pointer">See all</button>
|
|
208
|
+
</div>
|
|
209
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
210
|
+
{RECOMMENDED.map((course) => (
|
|
211
|
+
<CourseCard
|
|
212
|
+
key={course.id}
|
|
213
|
+
id={course.id}
|
|
214
|
+
title={course.title}
|
|
215
|
+
thumbnail={course.thumbnail}
|
|
216
|
+
totalLessons={course.totalLessons}
|
|
217
|
+
progress={course.progress}
|
|
218
|
+
showProgress={false}
|
|
219
|
+
isFree={course.isFree}
|
|
220
|
+
price={course.price}
|
|
221
|
+
showPrice={true}
|
|
222
|
+
onClick={() => onCourseClick(course.id)}
|
|
223
|
+
/>
|
|
224
|
+
))}
|
|
225
|
+
</div>
|
|
226
|
+
</section>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{/* What our students have to say */}
|
|
231
|
+
<div className="relative w-screen left-1/2 right-1/2 -ml-[50vw] -mr-[50vw] bg-[#EEF4FA]">
|
|
232
|
+
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-12">
|
|
233
|
+
<section className="space-y-6">
|
|
234
|
+
<h2 className="text-xl font-bold text-gray-900 sm:text-2xl">What our students have to say</h2>
|
|
235
|
+
<div className="rounded-2xl bg-white p-6 sm:p-8 lg:p-14 shadow-sm relative overflow-hidden">
|
|
236
|
+
<div className="flex flex-col items-center gap-6 lg:flex-row lg:items-center lg:gap-16">
|
|
237
|
+
<div className="relative shrink-0 w-48 h-48 sm:w-64 sm:h-64 bg-gray-100 rounded-full flex items-center justify-center">
|
|
238
|
+
<img src={`${FILE_URL}/student.png`} alt="Student" className="w-full h-full object-cover rounded-full" onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} />
|
|
239
|
+
</div>
|
|
240
|
+
<div className="flex-1 space-y-4 text-center lg:text-left min-w-0 z-10">
|
|
241
|
+
<div className="space-y-1">
|
|
242
|
+
<h3 className="text-xl font-bold text-gray-900">Savannah Nguyen</h3>
|
|
243
|
+
<p className="text-sm text-gray-500 font-medium tracking-wide">tanya.hill@example.com</p>
|
|
244
|
+
</div>
|
|
245
|
+
<div className="space-y-2 text-sm text-gray-500 leading-relaxed max-w-lg">
|
|
246
|
+
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor</p>
|
|
247
|
+
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor</p>
|
|
248
|
+
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor</p>
|
|
249
|
+
</div>
|
|
250
|
+
<div className="flex items-center gap-3 pt-2 justify-center lg:justify-start">
|
|
251
|
+
{[Twitter, Facebook, Instagram].map((Icon, i) => (
|
|
252
|
+
<button key={i} className="flex h-8 w-8 items-center justify-center rounded-full bg-theme-accent-primary text-white transition-all hover:scale-110 hover:shadow-md">
|
|
253
|
+
<Icon className="h-4 w-4" />
|
|
254
|
+
</button>
|
|
255
|
+
))}
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
<div className="hidden lg:flex flex-col gap-5 shrink-0 z-10">
|
|
259
|
+
{SIDE_AVATARS.map((avatar, i) => (
|
|
260
|
+
<div key={i} className="relative h-14 w-14 overflow-hidden rounded-full border-2 border-white shadow-md transition-all hover:scale-110 cursor-pointer hover:border-[#49BBBD]">
|
|
261
|
+
<img src={avatar} alt={`Student ${i + 1}`} className="w-full h-full object-cover" onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} />
|
|
262
|
+
</div>
|
|
263
|
+
))}
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</section>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
{/* Top Education Deals */}
|
|
272
|
+
<section className="space-y-6">
|
|
273
|
+
<div className="flex items-center justify-between">
|
|
274
|
+
<h2 className="text-xl font-bold text-gray-900 sm:text-2xl">Top Education offers and deals are listed here</h2>
|
|
275
|
+
<button className="shrink-0 text-sm font-semibold text-[rgb(var(--accent-primary))] hover:underline cursor-pointer">See all</button>
|
|
276
|
+
</div>
|
|
277
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
278
|
+
{DEALS.map((deal) => (
|
|
279
|
+
<div key={deal.id} className="group relative overflow-hidden rounded-2xl cursor-pointer h-[320px] sm:h-[350px]">
|
|
280
|
+
<img src={deal.image} alt={deal.title} className="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" onError={(e) => { (e.target as HTMLImageElement).style.background = '#e5e7eb'; }} />
|
|
281
|
+
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-black/10" />
|
|
282
|
+
<div className="absolute inset-0 flex flex-col justify-between p-5 sm:p-6">
|
|
283
|
+
<div>
|
|
284
|
+
<span className="inline-block rounded-lg bg-[rgb(var(--accent-primary))] px-4 py-2 text-xl font-bold text-white sm:text-2xl">{deal.discount}</span>
|
|
285
|
+
</div>
|
|
286
|
+
<div className="space-y-2">
|
|
287
|
+
<h3 className="text-lg font-bold text-white sm:text-xl">{deal.title}</h3>
|
|
288
|
+
<p className="text-sm text-white/80 leading-relaxed line-clamp-2">{deal.description}</p>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
</section>
|
|
295
|
+
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|