@academy-sdk/sdk 0.1.1 → 0.2.1

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 (239) hide show
  1. package/dist/academy-sdk-sdk-v1.0.0.zip +0 -0
  2. package/dist/bundle.js +70 -0
  3. package/dist/manifest.json +5 -0
  4. package/dist/styles.css +3307 -0
  5. package/package.json +41 -46
  6. package/src/components/atoms/Avatar.tsx +38 -0
  7. package/src/components/atoms/Badge.tsx +32 -0
  8. package/src/components/atoms/Button.tsx +48 -0
  9. package/src/components/atoms/Card.tsx +33 -0
  10. package/src/components/atoms/Input.tsx +39 -0
  11. package/src/components/atoms/ProgressBar.tsx +52 -0
  12. package/src/components/atoms/Tabs.tsx +47 -0
  13. package/{dist/components/atoms/index.d.ts → src/components/atoms/index.ts} +0 -1
  14. package/{dist/components/index.d.ts → src/components/index.ts} +7 -1
  15. package/src/components/molecules/CourseCard.tsx +215 -0
  16. package/src/components/molecules/EmptyState.tsx +23 -0
  17. package/src/components/molecules/LoadingSpinner.tsx +27 -0
  18. package/src/components/molecules/PageHeader.tsx +22 -0
  19. package/src/components/molecules/Pagination.tsx +82 -0
  20. package/src/components/molecules/SearchInput.tsx +35 -0
  21. package/{dist/components/molecules/index.d.ts → src/components/molecules/index.ts} +0 -1
  22. package/src/components/organisms/CourseSidebar.tsx +276 -0
  23. package/src/components/organisms/LearnerNavbar.tsx +129 -0
  24. package/src/components/organisms/LearnerSidebar.tsx +148 -0
  25. package/src/components/organisms/LessonBookmarks.tsx +128 -0
  26. package/src/components/organisms/LessonNotes.tsx +153 -0
  27. package/{dist/components/organisms/index.d.ts → src/components/organisms/index.ts} +0 -1
  28. package/src/components/pages/BundleDetailPage.tsx +388 -0
  29. package/src/components/pages/CatalogBundlesPage.tsx +96 -0
  30. package/src/components/pages/CatalogCoursesPage.tsx +299 -0
  31. package/src/components/pages/CourseDetailPage.tsx +582 -0
  32. package/src/components/pages/CoursePlayerPage.tsx +481 -0
  33. package/src/components/pages/CreatorProfilePage.tsx +161 -0
  34. package/src/components/pages/LearnerSettingsPage.tsx +58 -0
  35. package/src/components/pages/ManualReviewDetailPage.tsx +254 -0
  36. package/src/components/pages/ManualReviewPage.tsx +228 -0
  37. package/src/components/pages/MessagesPage.tsx +285 -0
  38. package/src/components/pages/MyLearningPage.tsx +239 -0
  39. package/src/components/pages/PaymentCancelPage.tsx +74 -0
  40. package/src/components/pages/PaymentSuccessPage.tsx +73 -0
  41. package/{dist/components/pages/index.d.ts → src/components/pages/index.ts} +0 -1
  42. package/src/components/utils.ts +6 -0
  43. package/src/contracts/components.contract.ts +89 -0
  44. package/{dist/contracts/index.d.ts → src/contracts/index.ts} +0 -1
  45. package/src/contracts/layout.contract.ts +36 -0
  46. package/src/contracts/pages.contract.ts +275 -0
  47. package/src/contracts/template.contract.ts +100 -0
  48. package/src/default-template.tsx +52 -0
  49. package/{dist/hooks/index.d.ts → src/hooks/index.ts} +15 -1
  50. package/src/hooks/sdk-context.tsx +152 -0
  51. package/src/hooks/useAiCoach.ts +27 -0
  52. package/src/hooks/useBookmarks.ts +35 -0
  53. package/{dist/hooks/useCourseSearch.d.ts → src/hooks/useCourseSearch.ts} +8 -5
  54. package/{dist/hooks/useDebounce.d.ts → src/hooks/useDebounce.ts} +8 -2
  55. package/{dist/hooks/useMyBundles.d.ts → src/hooks/useMyBundles.ts} +8 -6
  56. package/{dist/hooks/useMyCourses.d.ts → src/hooks/useMyCourses.ts} +8 -6
  57. package/src/hooks/useNotes.ts +35 -0
  58. package/src/hooks/useNotifications.ts +16 -0
  59. package/{dist/hooks/useTheme.d.ts → src/hooks/useTheme.ts} +8 -5
  60. package/src/hooks/useToast.ts +17 -0
  61. package/{dist/hooks/useUser.d.ts → src/hooks/useUser.ts} +13 -9
  62. package/src/index.ts +33 -0
  63. package/src/layouts/DefaultLayout.tsx +58 -0
  64. package/src/manifest.json +5 -0
  65. package/src/styles.css +43 -0
  66. package/src/types/ai-coach.ts +25 -0
  67. package/src/types/bookmarks.ts +20 -0
  68. package/src/types/bundle.ts +119 -0
  69. package/src/types/common.ts +24 -0
  70. package/src/types/course.ts +135 -0
  71. package/src/types/enrollment.ts +35 -0
  72. package/{dist/types/index.d.ts → src/types/index.ts} +0 -1
  73. package/src/types/lesson.ts +106 -0
  74. package/src/types/manual-review.ts +116 -0
  75. package/src/types/messaging.ts +109 -0
  76. package/src/types/notification.ts +30 -0
  77. package/src/types/payment.ts +40 -0
  78. package/src/types/progress.ts +19 -0
  79. package/src/types/rating.ts +20 -0
  80. package/src/types/search.ts +31 -0
  81. package/src/types/user.ts +16 -0
  82. package/src/utils/formatters.ts +74 -0
  83. package/src/utils/index.ts +8 -0
  84. package/dist/components/atoms/Avatar.d.ts +0 -9
  85. package/dist/components/atoms/Avatar.d.ts.map +0 -1
  86. package/dist/components/atoms/Badge.d.ts +0 -10
  87. package/dist/components/atoms/Badge.d.ts.map +0 -1
  88. package/dist/components/atoms/Button.d.ts +0 -11
  89. package/dist/components/atoms/Button.d.ts.map +0 -1
  90. package/dist/components/atoms/Card.d.ts +0 -11
  91. package/dist/components/atoms/Card.d.ts.map +0 -1
  92. package/dist/components/atoms/Input.d.ts +0 -7
  93. package/dist/components/atoms/Input.d.ts.map +0 -1
  94. package/dist/components/atoms/ProgressBar.d.ts +0 -11
  95. package/dist/components/atoms/ProgressBar.d.ts.map +0 -1
  96. package/dist/components/atoms/Tabs.d.ts +0 -16
  97. package/dist/components/atoms/Tabs.d.ts.map +0 -1
  98. package/dist/components/atoms/index.cjs +0 -318
  99. package/dist/components/atoms/index.d.ts.map +0 -1
  100. package/dist/components/atoms/index.js +0 -288
  101. package/dist/components/index.cjs +0 -1275
  102. package/dist/components/index.d.ts.map +0 -1
  103. package/dist/components/index.js +0 -1245
  104. package/dist/components/molecules/CourseCard.d.ts +0 -25
  105. package/dist/components/molecules/CourseCard.d.ts.map +0 -1
  106. package/dist/components/molecules/EmptyState.d.ts +0 -10
  107. package/dist/components/molecules/EmptyState.d.ts.map +0 -1
  108. package/dist/components/molecules/LoadingSpinner.d.ts +0 -7
  109. package/dist/components/molecules/LoadingSpinner.d.ts.map +0 -1
  110. package/dist/components/molecules/PageHeader.d.ts +0 -8
  111. package/dist/components/molecules/PageHeader.d.ts.map +0 -1
  112. package/dist/components/molecules/Pagination.d.ts +0 -13
  113. package/dist/components/molecules/Pagination.d.ts.map +0 -1
  114. package/dist/components/molecules/SearchInput.d.ts +0 -8
  115. package/dist/components/molecules/SearchInput.d.ts.map +0 -1
  116. package/dist/components/molecules/index.cjs +0 -334
  117. package/dist/components/molecules/index.d.ts.map +0 -1
  118. package/dist/components/molecules/index.js +0 -311
  119. package/dist/components/organisms/CourseSidebar.d.ts +0 -37
  120. package/dist/components/organisms/CourseSidebar.d.ts.map +0 -1
  121. package/dist/components/organisms/LearnerNavbar.d.ts +0 -8
  122. package/dist/components/organisms/LearnerNavbar.d.ts.map +0 -1
  123. package/dist/components/organisms/LearnerSidebar.d.ts +0 -16
  124. package/dist/components/organisms/LearnerSidebar.d.ts.map +0 -1
  125. package/dist/components/organisms/LessonBookmarks.d.ts +0 -8
  126. package/dist/components/organisms/LessonBookmarks.d.ts.map +0 -1
  127. package/dist/components/organisms/LessonNotes.d.ts +0 -8
  128. package/dist/components/organisms/LessonNotes.d.ts.map +0 -1
  129. package/dist/components/organisms/index.cjs +0 -855
  130. package/dist/components/organisms/index.d.ts.map +0 -1
  131. package/dist/components/organisms/index.js +0 -825
  132. package/dist/components/pages/BundleDetailPage.d.ts +0 -3
  133. package/dist/components/pages/BundleDetailPage.d.ts.map +0 -1
  134. package/dist/components/pages/CatalogBundlesPage.d.ts +0 -3
  135. package/dist/components/pages/CatalogBundlesPage.d.ts.map +0 -1
  136. package/dist/components/pages/CatalogCoursesPage.d.ts +0 -3
  137. package/dist/components/pages/CatalogCoursesPage.d.ts.map +0 -1
  138. package/dist/components/pages/CourseDetailPage.d.ts +0 -3
  139. package/dist/components/pages/CourseDetailPage.d.ts.map +0 -1
  140. package/dist/components/pages/CoursePlayerPage.d.ts +0 -8
  141. package/dist/components/pages/CoursePlayerPage.d.ts.map +0 -1
  142. package/dist/components/pages/CreatorProfilePage.d.ts +0 -3
  143. package/dist/components/pages/CreatorProfilePage.d.ts.map +0 -1
  144. package/dist/components/pages/LearnerSettingsPage.d.ts +0 -3
  145. package/dist/components/pages/LearnerSettingsPage.d.ts.map +0 -1
  146. package/dist/components/pages/ManualReviewDetailPage.d.ts +0 -3
  147. package/dist/components/pages/ManualReviewDetailPage.d.ts.map +0 -1
  148. package/dist/components/pages/ManualReviewPage.d.ts +0 -3
  149. package/dist/components/pages/ManualReviewPage.d.ts.map +0 -1
  150. package/dist/components/pages/MessagesPage.d.ts +0 -3
  151. package/dist/components/pages/MessagesPage.d.ts.map +0 -1
  152. package/dist/components/pages/MyLearningPage.d.ts +0 -3
  153. package/dist/components/pages/MyLearningPage.d.ts.map +0 -1
  154. package/dist/components/pages/PaymentCancelPage.d.ts +0 -3
  155. package/dist/components/pages/PaymentCancelPage.d.ts.map +0 -1
  156. package/dist/components/pages/PaymentSuccessPage.d.ts +0 -3
  157. package/dist/components/pages/PaymentSuccessPage.d.ts.map +0 -1
  158. package/dist/components/pages/index.cjs +0 -3306
  159. package/dist/components/pages/index.d.ts.map +0 -1
  160. package/dist/components/pages/index.js +0 -3315
  161. package/dist/components/utils.d.ts +0 -3
  162. package/dist/components/utils.d.ts.map +0 -1
  163. package/dist/contracts/components.contract.d.ts +0 -87
  164. package/dist/contracts/components.contract.d.ts.map +0 -1
  165. package/dist/contracts/index.cjs +0 -52
  166. package/dist/contracts/index.d.ts.map +0 -1
  167. package/dist/contracts/index.js +0 -29
  168. package/dist/contracts/layout.contract.d.ts +0 -35
  169. package/dist/contracts/layout.contract.d.ts.map +0 -1
  170. package/dist/contracts/pages.contract.d.ts +0 -192
  171. package/dist/contracts/pages.contract.d.ts.map +0 -1
  172. package/dist/contracts/template.contract.d.ts +0 -49
  173. package/dist/contracts/template.contract.d.ts.map +0 -1
  174. package/dist/hooks/index.cjs +0 -165
  175. package/dist/hooks/index.d.ts.map +0 -1
  176. package/dist/hooks/index.js +0 -142
  177. package/dist/hooks/sdk-context.d.ts +0 -125
  178. package/dist/hooks/sdk-context.d.ts.map +0 -1
  179. package/dist/hooks/useAiCoach.d.ts +0 -32
  180. package/dist/hooks/useAiCoach.d.ts.map +0 -1
  181. package/dist/hooks/useBookmarks.d.ts +0 -31
  182. package/dist/hooks/useBookmarks.d.ts.map +0 -1
  183. package/dist/hooks/useCourseSearch.d.ts.map +0 -1
  184. package/dist/hooks/useDebounce.d.ts.map +0 -1
  185. package/dist/hooks/useMyBundles.d.ts.map +0 -1
  186. package/dist/hooks/useMyCourses.d.ts.map +0 -1
  187. package/dist/hooks/useNotes.d.ts +0 -31
  188. package/dist/hooks/useNotes.d.ts.map +0 -1
  189. package/dist/hooks/useNotifications.d.ts +0 -19
  190. package/dist/hooks/useNotifications.d.ts.map +0 -1
  191. package/dist/hooks/useTheme.d.ts.map +0 -1
  192. package/dist/hooks/useToast.d.ts +0 -17
  193. package/dist/hooks/useToast.d.ts.map +0 -1
  194. package/dist/hooks/useUser.d.ts.map +0 -1
  195. package/dist/index.cjs +0 -630
  196. package/dist/index.d.ts +0 -17
  197. package/dist/index.d.ts.map +0 -1
  198. package/dist/index.js +0 -600
  199. package/dist/layouts/DefaultLayout.d.ts +0 -9
  200. package/dist/layouts/DefaultLayout.d.ts.map +0 -1
  201. package/dist/types/ai-coach.d.ts +0 -22
  202. package/dist/types/ai-coach.d.ts.map +0 -1
  203. package/dist/types/bookmarks.d.ts +0 -19
  204. package/dist/types/bookmarks.d.ts.map +0 -1
  205. package/dist/types/bundle.d.ts +0 -114
  206. package/dist/types/bundle.d.ts.map +0 -1
  207. package/dist/types/common.d.ts +0 -23
  208. package/dist/types/common.d.ts.map +0 -1
  209. package/dist/types/course.d.ts +0 -127
  210. package/dist/types/course.d.ts.map +0 -1
  211. package/dist/types/enrollment.d.ts +0 -34
  212. package/dist/types/enrollment.d.ts.map +0 -1
  213. package/dist/types/index.cjs +0 -18
  214. package/dist/types/index.d.ts.map +0 -1
  215. package/dist/types/index.js +0 -0
  216. package/dist/types/lesson.d.ts +0 -105
  217. package/dist/types/lesson.d.ts.map +0 -1
  218. package/dist/types/manual-review.d.ts +0 -123
  219. package/dist/types/manual-review.d.ts.map +0 -1
  220. package/dist/types/messaging.d.ts +0 -101
  221. package/dist/types/messaging.d.ts.map +0 -1
  222. package/dist/types/notification.d.ts +0 -28
  223. package/dist/types/notification.d.ts.map +0 -1
  224. package/dist/types/payment.d.ts +0 -38
  225. package/dist/types/payment.d.ts.map +0 -1
  226. package/dist/types/progress.d.ts +0 -18
  227. package/dist/types/progress.d.ts.map +0 -1
  228. package/dist/types/rating.d.ts +0 -20
  229. package/dist/types/rating.d.ts.map +0 -1
  230. package/dist/types/search.d.ts +0 -28
  231. package/dist/types/search.d.ts.map +0 -1
  232. package/dist/types/user.d.ts +0 -15
  233. package/dist/types/user.d.ts.map +0 -1
  234. package/dist/utils/formatters.d.ts +0 -25
  235. package/dist/utils/formatters.d.ts.map +0 -1
  236. package/dist/utils/index.cjs +0 -80
  237. package/dist/utils/index.d.ts +0 -2
  238. package/dist/utils/index.d.ts.map +0 -1
  239. package/dist/utils/index.js +0 -57
@@ -0,0 +1,582 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ CheckCircle2, ChevronDown, ChevronRight, PlayCircle, Download,
4
+ FileText, Clock, Monitor, Tag, X, Check, BookOpen,
5
+ } from 'lucide-react';
6
+ import type { CourseDetailPageProps } from '../../contracts/pages.contract';
7
+ import { LoadingSpinner } from '../molecules/LoadingSpinner';
8
+ import { EmptyState } from '../molecules/EmptyState';
9
+
10
+ function formatCoursePrice(price: number, isFree: boolean, currency = 'USD'): string {
11
+ if (isFree || price === 0) return 'Free';
12
+ try {
13
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(price);
14
+ } catch {
15
+ return `${currency} ${price.toFixed(2)}`;
16
+ }
17
+ }
18
+
19
+ function formatDuration(seconds: number): string {
20
+ const mins = Math.floor(seconds / 60);
21
+ if (mins >= 60) {
22
+ const hours = Math.floor(mins / 60);
23
+ const remainMins = mins % 60;
24
+ return `${hours}h ${remainMins}m`;
25
+ }
26
+ const secs = seconds % 60;
27
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
28
+ }
29
+
30
+ export function CourseDetailPage(props: Partial<CourseDetailPageProps>) {
31
+ const noop = (..._args: any[]) => {};
32
+ const {
33
+ course, isLoading = false, notFound = false,
34
+ isEnrolled = false, enrollmentId = null,
35
+ onEnroll = noop as any, onStartLearning = noop, isProcessingEnrollment = false,
36
+ paymentProviders = [], onCheckout = noop as any, isProcessingCheckout = false,
37
+ couponCode = '', onCouponCodeChange = noop, onApplyCoupon = noop as any,
38
+ appliedCoupon = null, couponError = null, isApplyingCoupon = false, discountedPrice,
39
+ activeTab = 'overview' as const, onTabChange = noop,
40
+ relatedCourses = [], isRelatedLoading = false, onRelatedCourseClick = noop,
41
+ ratings, ratingSummary,
42
+ } = props;
43
+
44
+ const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
45
+ const [selectedLesson, setSelectedLesson] = useState<string | null>(null);
46
+ const [showCouponInput, setShowCouponInput] = useState(false);
47
+ const [imgError, setImgError] = useState(false);
48
+
49
+ if (isLoading) return <LoadingSpinner className="py-20" text="Loading course..." />;
50
+ if (notFound || !course) {
51
+ return (
52
+ <EmptyState
53
+ icon={<BookOpen className="w-16 h-16" />}
54
+ title="Course not found"
55
+ description="The course you're looking for doesn't exist."
56
+ />
57
+ );
58
+ }
59
+
60
+ const pricingType = course.settings?.pricingType || course.pricingType || 'FREE';
61
+ const price = course.settings?.price ?? course.price ?? 0;
62
+ const currency = course.settings?.currency ?? course.currency ?? 'USD';
63
+ const isFree = pricingType === 'FREE' || price === 0;
64
+ const bannerImage = course.settings?.banner || course.bannerImage;
65
+ const thumbnail = course.thumbnail || '';
66
+ const totalLessons = (course.sections || []).reduce(
67
+ (sum: number, s: any) => sum + (s.activities?.length || 0), 0
68
+ );
69
+ const sections = course.sections || [];
70
+ const hasBanner = bannerImage && bannerImage.trim();
71
+
72
+ const toggleSection = (sectionId: string) => {
73
+ const next = new Set(expandedSections);
74
+ if (next.has(sectionId)) next.delete(sectionId);
75
+ else next.add(sectionId);
76
+ setExpandedSections(next);
77
+ };
78
+
79
+ const getButtonText = () => {
80
+ if (isEnrolled) return 'Continue Learning';
81
+ if (isFree) return 'Enroll for Free';
82
+ if (pricingType === 'SUBSCRIPTION') return 'Subscribe to Access';
83
+ if (pricingType === 'INSTALLMENT') return 'Make a Payment';
84
+ return 'Buy Course';
85
+ };
86
+
87
+ const handleEnrollClick = () => {
88
+ if (isEnrolled && enrollmentId) {
89
+ onStartLearning(enrollmentId);
90
+ return;
91
+ }
92
+ if (isFree) {
93
+ onEnroll();
94
+ return;
95
+ }
96
+ if (paymentProviders.length === 1) {
97
+ const p = paymentProviders[0] as any;
98
+ onCheckout(p.paymentProvider || p.id);
99
+ return;
100
+ }
101
+ onEnroll();
102
+ };
103
+
104
+ const renderEnrollmentCard = () => (
105
+ <div className="bg-white p-4 rounded-xl shadow-lg border border-gray-100">
106
+ {thumbnail && !imgError && (
107
+ <img
108
+ src={thumbnail}
109
+ alt={course.title}
110
+ className="w-full h-48 object-cover rounded-lg mb-4"
111
+ onError={() => setImgError(true)}
112
+ />
113
+ )}
114
+
115
+ {/* Enrolled status */}
116
+ {isEnrolled && (
117
+ <div className="mb-4 flex items-center justify-center gap-2 text-green-600">
118
+ <Check className="h-5 w-5" />
119
+ <span className="font-semibold text-sm">You're enrolled</span>
120
+ </div>
121
+ )}
122
+
123
+ {/* Price */}
124
+ <div className="text-center mb-4">
125
+ {appliedCoupon && discountedPrice !== undefined ? (
126
+ <div className="space-y-1">
127
+ <div className="text-2xl font-bold text-gray-900">
128
+ {formatCoursePrice(discountedPrice, false, currency)}
129
+ </div>
130
+ <div className="text-base line-through text-gray-400">
131
+ {formatCoursePrice(price, isFree, currency)}
132
+ </div>
133
+ <div className="text-sm font-semibold text-gray-900">
134
+ {(appliedCoupon as any).discountType === 'PERCENTAGE'
135
+ ? `${(appliedCoupon as any).discountValue}% Off`
136
+ : formatCoursePrice((appliedCoupon as any).discountValue, false, currency)}
137
+ </div>
138
+ </div>
139
+ ) : (
140
+ <div className="text-2xl font-bold text-gray-900">
141
+ {formatCoursePrice(price, isFree, currency)}
142
+ {pricingType === 'SUBSCRIPTION' && (
143
+ <span className="text-lg font-normal text-gray-500"> / month</span>
144
+ )}
145
+ </div>
146
+ )}
147
+ </div>
148
+
149
+ {/* Main action button */}
150
+ <button
151
+ onClick={handleEnrollClick}
152
+ disabled={isProcessingEnrollment || isProcessingCheckout}
153
+ className="w-full bg-theme-accent-primary hover:bg-[#3da5a7] text-white font-semibold py-3 px-6 rounded-md transition-colors mb-4 cursor-pointer disabled:opacity-50"
154
+ >
155
+ {isProcessingEnrollment || isProcessingCheckout ? 'Processing...' : getButtonText()}
156
+ </button>
157
+
158
+ {/* Multiple payment providers */}
159
+ {!isEnrolled && !isFree && paymentProviders.length > 1 &&
160
+ paymentProviders.map((provider: any) => (
161
+ <button
162
+ key={provider.paymentProvider || provider.id}
163
+ onClick={() => onCheckout(provider.paymentProvider || provider.id)}
164
+ disabled={isProcessingCheckout}
165
+ className="w-full bg-theme-accent-primary hover:bg-[#3da5a7] text-white font-semibold py-3 px-6 rounded-md transition-colors mb-2 cursor-pointer disabled:opacity-50"
166
+ >
167
+ Pay with {provider.paymentProvider}
168
+ </button>
169
+ ))
170
+ }
171
+
172
+ {/* Apply Coupon */}
173
+ {!isFree && !isEnrolled && (
174
+ <div className="mb-6">
175
+ {appliedCoupon ? (
176
+ <div className="p-3 bg-theme-accent-primary/10 border border-[#49BBBD]/20 rounded-lg">
177
+ <div className="flex items-center justify-between">
178
+ <div className="flex items-center gap-2">
179
+ <Tag className="h-4 w-4 text-[#49BBBD]" />
180
+ <span className="text-sm font-medium text-[#49BBBD]">
181
+ Coupon &quot;{(appliedCoupon as any).code}&quot; applied
182
+ </span>
183
+ </div>
184
+ <button
185
+ onClick={() => { onCouponCodeChange(''); }}
186
+ className="text-[#49BBBD] hover:text-[#3da5a7] transition-colors cursor-pointer"
187
+ >
188
+ <X className="h-4 w-4" />
189
+ </button>
190
+ </div>
191
+ </div>
192
+ ) : showCouponInput ? (
193
+ <div className="space-y-2">
194
+ <div className="flex gap-2">
195
+ <input
196
+ type="text"
197
+ placeholder="Enter coupon code"
198
+ value={couponCode}
199
+ onChange={(e) => onCouponCodeChange(e.target.value.toUpperCase())}
200
+ className={`flex-1 text-sm border rounded-md px-3 py-2 focus:outline-none focus:border-[#49BBBD] ${couponError ? 'border-red-500' : 'border-gray-300'}`}
201
+ />
202
+ <button
203
+ onClick={onApplyCoupon as any}
204
+ disabled={!couponCode || isApplyingCoupon}
205
+ className="px-3 py-2 text-sm bg-theme-accent-primary text-white rounded-md hover:bg-[#3da5a7] disabled:opacity-50 cursor-pointer"
206
+ >
207
+ {isApplyingCoupon ? 'Applying...' : 'Apply'}
208
+ </button>
209
+ </div>
210
+ {couponError && <p className="text-xs text-red-600">{couponError}</p>}
211
+ </div>
212
+ ) : (
213
+ <a
214
+ href="#"
215
+ onClick={(e) => { e.preventDefault(); setShowCouponInput(true); }}
216
+ className="block text-center text-sm font-medium text-[#49BBBD] hover:text-[#3da5a7] hover:underline cursor-pointer"
217
+ >
218
+ Apply Coupon
219
+ </a>
220
+ )}
221
+ </div>
222
+ )}
223
+
224
+ {/* This course included */}
225
+ <div className="border-t border-gray-200 pt-4">
226
+ <h3 className="font-semibold text-gray-900 mb-3 text-sm">This course included</h3>
227
+ <div className="space-y-2.5">
228
+ <div className="flex items-center gap-2.5 text-gray-700">
229
+ <div className="w-4 h-4 rounded-full bg-theme-accent-primary flex items-center justify-center flex-shrink-0">
230
+ <FileText className="h-2.5 w-2.5 text-white" />
231
+ </div>
232
+ <span className="text-sm">{totalLessons} lessons</span>
233
+ </div>
234
+ <div className="flex items-center gap-2.5 text-gray-700">
235
+ <div className="w-4 h-4 rounded-full bg-theme-accent-primary flex items-center justify-center flex-shrink-0">
236
+ <Clock className="h-2.5 w-2.5 text-white" />
237
+ </div>
238
+ <span className="text-sm">Self-paced learning</span>
239
+ </div>
240
+ <div className="flex items-center gap-2.5 text-gray-700">
241
+ <div className="w-4 h-4 rounded-full bg-theme-accent-primary flex items-center justify-center flex-shrink-0">
242
+ <Monitor className="h-2.5 w-2.5 text-white" />
243
+ </div>
244
+ <span className="text-sm">Access on mobile and desktop</span>
245
+ </div>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ );
250
+
251
+ return (
252
+ <div className="-mx-4 sm:-mx-4 md:-mx-4 lg:-mx-6 xl:-mx-6">
253
+ {/* Course Hero Section */}
254
+ <div
255
+ className="relative text-white px-4 lg:px-8 py-8 lg:py-40 mb-8"
256
+ style={
257
+ hasBanner
258
+ ? { backgroundImage: `url(${bannerImage})`, backgroundSize: 'cover', backgroundPosition: 'center' }
259
+ : { backgroundColor: '#1a2332' }
260
+ }
261
+ >
262
+ {hasBanner && (
263
+ <div className="absolute inset-0 bg-gradient-to-r from-black/60 via-black/50 to-black/40" />
264
+ )}
265
+
266
+ <div className="relative z-10">
267
+ {/* Course header text */}
268
+ <div className="max-w-7xl lg:pr-110">
269
+ <h1 className="text-3xl lg:text-4xl font-bold mb-2">{course.title}</h1>
270
+ {course.brief && (
271
+ <p className="text-base lg:text-lg text-gray-300 mb-4 leading-relaxed">{course.brief}</p>
272
+ )}
273
+ {course.creator && (
274
+ <div className="flex items-center gap-3 mt-4 lg:mt-6">
275
+ <div className="w-10 h-10 rounded-full bg-theme-accent-primary flex items-center justify-center text-white font-semibold">
276
+ {(course.creator.name || 'C').charAt(0).toUpperCase()}
277
+ </div>
278
+ <span className="text-sm">{course.creator.name}</span>
279
+ </div>
280
+ )}
281
+ </div>
282
+
283
+ {/* Mobile enrollment card — inside hero */}
284
+ <div className="lg:hidden mt-6">
285
+ {renderEnrollmentCard()}
286
+ </div>
287
+ </div>
288
+
289
+ {/* Desktop enrollment card — absolute positioned */}
290
+ <div className="hidden lg:block absolute right-8 top-15 w-80 z-20">
291
+ {renderEnrollmentCard()}
292
+ </div>
293
+ </div>
294
+
295
+ {/* Tab Navigation */}
296
+ <div className="px-4 lg:px-6 mb-6">
297
+ <div className="flex gap-3">
298
+ {(['overview', 'curriculum'] as const).map((tab) => (
299
+ <button
300
+ key={tab}
301
+ onClick={() => onTabChange(tab)}
302
+ className={`px-6 py-2.5 rounded-full text-sm font-medium transition-all cursor-pointer ${
303
+ activeTab === tab
304
+ ? 'bg-theme-accent-primary text-white shadow-md'
305
+ : 'bg-white text-gray-600 border border-gray-200 hover:border-[#49BBBD] hover:text-[#49BBBD]'
306
+ }`}
307
+ >
308
+ {tab.charAt(0).toUpperCase() + tab.slice(1)}
309
+ </button>
310
+ ))}
311
+ </div>
312
+ </div>
313
+
314
+ {/* Tab Content */}
315
+ <div className="px-4 lg:px-6">
316
+ {/* Overview Tab */}
317
+ {activeTab === 'overview' && (
318
+ <div className="lg:pr-110">
319
+ {/* About This Course */}
320
+ <div className="mb-8 pb-8 border-b border-gray-300">
321
+ <h2 className="text-xl font-bold text-gray-900 mb-4">About This Course</h2>
322
+ {course.summary && (
323
+ <div
324
+ className="text-gray-700 text-sm leading-relaxed mb-6"
325
+ dangerouslySetInnerHTML={{ __html: course.summary }}
326
+ />
327
+ )}
328
+ {course.description && (
329
+ <div
330
+ className="text-gray-700 text-sm leading-relaxed"
331
+ dangerouslySetInnerHTML={{ __html: course.description }}
332
+ />
333
+ )}
334
+ </div>
335
+
336
+ {/* What You'll Learn */}
337
+ {course.whatYouWillLearn && course.whatYouWillLearn.length > 0 && (
338
+ <div className="mb-8 pb-8 border-b border-gray-300">
339
+ <h2 className="text-xl font-bold text-gray-900 mb-4">What You'll Learn</h2>
340
+ <div className="grid md:grid-cols-2 gap-3">
341
+ {course.whatYouWillLearn.map((item: string, i: number) => (
342
+ <div key={i} className="flex items-start gap-3">
343
+ <CheckCircle2 className="h-5 w-5 text-green-600 flex-shrink-0" />
344
+ <span className="text-sm text-gray-700">{item}</span>
345
+ </div>
346
+ ))}
347
+ </div>
348
+ </div>
349
+ )}
350
+
351
+ {/* Requirements */}
352
+ {course.requirements && course.requirements.length > 0 && (
353
+ <div className="mb-8 pb-8 border-b border-gray-300">
354
+ <h2 className="text-xl font-bold text-gray-900 mb-4">Requirements</h2>
355
+ <ul className="space-y-2">
356
+ {course.requirements.map((item: string, i: number) => (
357
+ <li key={i} className="flex items-start gap-2">
358
+ <span className="text-gray-400 text-sm">•</span>
359
+ <span className="text-sm text-gray-700">{item}</span>
360
+ </li>
361
+ ))}
362
+ </ul>
363
+ </div>
364
+ )}
365
+
366
+ {/* Target Audience */}
367
+ {course.targetAudience && course.targetAudience.length > 0 && (
368
+ <div className="mb-8 pb-8 border-b border-gray-300">
369
+ <h2 className="text-xl font-bold text-gray-900 mb-4">Who This Course Is For</h2>
370
+ <ul className="space-y-2">
371
+ {course.targetAudience.map((item: string, i: number) => (
372
+ <li key={i} className="flex items-start gap-2">
373
+ <span className="text-gray-400 text-sm">•</span>
374
+ <span className="text-sm text-gray-700">{item}</span>
375
+ </li>
376
+ ))}
377
+ </ul>
378
+ </div>
379
+ )}
380
+
381
+ {/* Ratings */}
382
+ {ratingSummary && (
383
+ <div className="mb-8">
384
+ <div className="flex items-center gap-4 mb-6">
385
+ <div className="text-5xl font-bold text-yellow-500">
386
+ {ratingSummary.averageRating?.toFixed(1)}
387
+ </div>
388
+ <div>
389
+ <div className="flex items-center gap-1 mb-1">
390
+ {[1, 2, 3, 4, 5].map((star) => (
391
+ <svg
392
+ key={star}
393
+ className={`w-4 h-4 ${star <= Math.round(ratingSummary.averageRating || 0) ? 'text-yellow-400 fill-yellow-400' : 'text-gray-300 fill-gray-300'}`}
394
+ viewBox="0 0 24 24"
395
+ >
396
+ <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
397
+ </svg>
398
+ ))}
399
+ </div>
400
+ <p className="text-sm text-gray-600">
401
+ Course Rating • {ratingSummary.totalRatings} ratings
402
+ </p>
403
+ </div>
404
+ </div>
405
+
406
+ {ratings && ratings.slice(0, 2).map((review: any) => (
407
+ <div key={review.id} className="mb-4 p-4 border border-gray-200 rounded-lg">
408
+ <div className="flex items-center gap-3 mb-2">
409
+ <div className="w-9 h-9 rounded-full bg-theme-accent-primary flex items-center justify-center text-white font-semibold text-sm">
410
+ {(review.reviewerName || 'U').charAt(0)}
411
+ </div>
412
+ <div>
413
+ <p className="font-medium text-sm">{review.reviewerName}</p>
414
+ <div className="flex items-center gap-1">
415
+ {[1, 2, 3, 4, 5].map((star) => (
416
+ <svg
417
+ key={star}
418
+ className={`w-3 h-3 ${star <= (review.rating || 0) ? 'text-yellow-400 fill-yellow-400' : 'text-gray-300 fill-gray-300'}`}
419
+ viewBox="0 0 24 24"
420
+ >
421
+ <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
422
+ </svg>
423
+ ))}
424
+ </div>
425
+ </div>
426
+ </div>
427
+ {review.review && (
428
+ <p className="text-sm text-gray-700">{review.review}</p>
429
+ )}
430
+ </div>
431
+ ))}
432
+ </div>
433
+ )}
434
+ </div>
435
+ )}
436
+
437
+ {/* Curriculum Tab */}
438
+ {activeTab === 'curriculum' && (
439
+ <div className="mb-8">
440
+ <h2 className="text-xl font-bold text-gray-900 mb-4">Curriculum</h2>
441
+ <div className="space-y-1">
442
+ {sections.map((section: any) => (
443
+ <div key={section.id} className="border border-gray-200 rounded overflow-hidden">
444
+ <button
445
+ onClick={() => toggleSection(section.id)}
446
+ className="w-full flex items-center justify-between p-3 hover:bg-gray-50 transition-colors text-left cursor-pointer"
447
+ >
448
+ <div className="flex items-center gap-2">
449
+ {expandedSections.has(section.id) ? (
450
+ <ChevronDown className="h-4 w-4 text-gray-600" />
451
+ ) : (
452
+ <ChevronRight className="h-4 w-4 text-gray-600" />
453
+ )}
454
+ <div>
455
+ <span className="text-sm font-medium text-gray-900">{section.title}</span>
456
+ {section.description && (
457
+ <p className="text-xs text-gray-500 mt-0.5">{section.description}</p>
458
+ )}
459
+ </div>
460
+ </div>
461
+ <span className="text-xs text-gray-500">
462
+ {section.activities?.length || 0} lessons
463
+ </span>
464
+ </button>
465
+
466
+ {expandedSections.has(section.id) && (
467
+ <div className="bg-gray-50 border-t border-gray-200">
468
+ {section.activities?.map((activity: any) => {
469
+ const isSelected = selectedLesson === activity.id;
470
+ return (
471
+ <button
472
+ key={activity.id}
473
+ onClick={() => setSelectedLesson(activity.id)}
474
+ className={`w-full flex items-start justify-between gap-3 px-10 py-3 border-b border-gray-100 last:border-b-0 transition-colors text-left cursor-pointer ${
475
+ isSelected
476
+ ? 'bg-blue-50 border-l-4 border-l-blue-600'
477
+ : 'hover:bg-gray-100'
478
+ }`}
479
+ >
480
+ <div className="flex items-start gap-3 flex-1">
481
+ <PlayCircle
482
+ className={`h-4 w-4 flex-shrink-0 mt-0.5 ${isSelected ? 'text-blue-600' : 'text-gray-400'}`}
483
+ />
484
+ <div className="flex-1">
485
+ <div className="flex items-center gap-2">
486
+ <span className={`text-xs font-medium ${isSelected ? 'text-blue-600' : 'text-gray-700'}`}>
487
+ {activity.title}
488
+ </span>
489
+ {activity.freePreview && (
490
+ <span className="text-xs px-2 py-0.5 bg-blue-100 text-blue-700 rounded">
491
+ Preview
492
+ </span>
493
+ )}
494
+ </div>
495
+ </div>
496
+ </div>
497
+ <div className="flex items-center gap-3 flex-shrink-0">
498
+ {activity.downloadable && (
499
+ <Download className="h-3 w-3 text-gray-400" />
500
+ )}
501
+ {activity.duration && (
502
+ <span className="text-xs text-gray-500">
503
+ {typeof activity.duration === 'number'
504
+ ? formatDuration(activity.duration)
505
+ : activity.duration}
506
+ </span>
507
+ )}
508
+ </div>
509
+ </button>
510
+ );
511
+ })}
512
+ </div>
513
+ )}
514
+ </div>
515
+ ))}
516
+ </div>
517
+ </div>
518
+ )}
519
+ </div>
520
+
521
+ {/* Related Courses — only on overview */}
522
+ {activeTab === 'overview' && (relatedCourses.length > 0 || isRelatedLoading) && (
523
+ <div className="mt-8 bg-[#EEF4FA] border-t border-gray-200 pb-8">
524
+ <div className="px-4 lg:px-6">
525
+ <section className="py-10">
526
+ <div className="flex items-center justify-between mb-6">
527
+ <h2 className="text-xl font-bold text-gray-900">Related Courses</h2>
528
+ <button className="text-sm font-medium text-[#49BBBD] hover:underline cursor-pointer">
529
+ See all
530
+ </button>
531
+ </div>
532
+
533
+ {isRelatedLoading ? (
534
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
535
+ {[...Array(4)].map((_, i) => (
536
+ <div key={i} className="rounded-2xl bg-gray-100 animate-pulse h-64" />
537
+ ))}
538
+ </div>
539
+ ) : (
540
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
541
+ {relatedCourses.map((c: any) => {
542
+ const cIsFree = c.settings?.pricingType === 'FREE' || (!c.settings?.price && !c.price);
543
+ const cPrice = c.settings?.price ?? c.price ?? 0;
544
+ const cCurrency = c.settings?.currency ?? c.currency ?? 'USD';
545
+ return (
546
+ <div
547
+ key={c.id}
548
+ onClick={() => onRelatedCourseClick(c.id)}
549
+ className="bg-white rounded-2xl overflow-hidden shadow-sm border border-gray-100 hover:shadow-md transition-shadow cursor-pointer"
550
+ >
551
+ <div className="h-40 bg-gray-100 overflow-hidden">
552
+ {c.thumbnail && (
553
+ <img
554
+ src={c.thumbnail}
555
+ alt={c.title}
556
+ className="w-full h-full object-cover"
557
+ />
558
+ )}
559
+ </div>
560
+ <div className="p-4">
561
+ <h3 className="font-semibold text-gray-900 text-sm mb-2 line-clamp-2">
562
+ {c.title}
563
+ </h3>
564
+ <div className="flex items-center justify-between text-xs text-gray-500">
565
+ <span>{c.totalLessons ?? 0} lessons</span>
566
+ <span className={cIsFree ? 'text-green-600 font-medium' : 'font-semibold text-gray-900'}>
567
+ {cIsFree ? 'Free' : formatCoursePrice(cPrice, false, cCurrency)}
568
+ </span>
569
+ </div>
570
+ </div>
571
+ </div>
572
+ );
573
+ })}
574
+ </div>
575
+ )}
576
+ </section>
577
+ </div>
578
+ </div>
579
+ )}
580
+ </div>
581
+ );
582
+ }