@academy-sdk/sdk 0.1.1 → 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 (238) 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/{dist/components/atoms/index.d.ts → src/components/atoms/index.ts} +0 -1
  13. package/{dist/components/index.d.ts → src/components/index.ts} +7 -1
  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/{dist/components/molecules/index.d.ts → src/components/molecules/index.ts} +0 -1
  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/{dist/components/organisms/index.d.ts → src/components/organisms/index.ts} +0 -1
  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/{dist/components/pages/index.d.ts → src/components/pages/index.ts} +0 -1
  41. package/src/components/utils.ts +6 -0
  42. package/src/contracts/components.contract.ts +89 -0
  43. package/{dist/contracts/index.d.ts → src/contracts/index.ts} +0 -1
  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/{dist/hooks/index.d.ts → src/hooks/index.ts} +15 -1
  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/{dist/hooks/useCourseSearch.d.ts → src/hooks/useCourseSearch.ts} +8 -5
  53. package/{dist/hooks/useDebounce.d.ts → src/hooks/useDebounce.ts} +8 -2
  54. package/{dist/hooks/useMyBundles.d.ts → src/hooks/useMyBundles.ts} +8 -6
  55. package/{dist/hooks/useMyCourses.d.ts → src/hooks/useMyCourses.ts} +8 -6
  56. package/src/hooks/useNotes.ts +35 -0
  57. package/src/hooks/useNotifications.ts +16 -0
  58. package/{dist/hooks/useTheme.d.ts → src/hooks/useTheme.ts} +8 -5
  59. package/src/hooks/useToast.ts +17 -0
  60. package/{dist/hooks/useUser.d.ts → src/hooks/useUser.ts} +13 -9
  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/{dist/types/index.d.ts → src/types/index.ts} +0 -1
  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/Avatar.d.ts +0 -9
  84. package/dist/components/atoms/Avatar.d.ts.map +0 -1
  85. package/dist/components/atoms/Badge.d.ts +0 -10
  86. package/dist/components/atoms/Badge.d.ts.map +0 -1
  87. package/dist/components/atoms/Button.d.ts +0 -11
  88. package/dist/components/atoms/Button.d.ts.map +0 -1
  89. package/dist/components/atoms/Card.d.ts +0 -11
  90. package/dist/components/atoms/Card.d.ts.map +0 -1
  91. package/dist/components/atoms/Input.d.ts +0 -7
  92. package/dist/components/atoms/Input.d.ts.map +0 -1
  93. package/dist/components/atoms/ProgressBar.d.ts +0 -11
  94. package/dist/components/atoms/ProgressBar.d.ts.map +0 -1
  95. package/dist/components/atoms/Tabs.d.ts +0 -16
  96. package/dist/components/atoms/Tabs.d.ts.map +0 -1
  97. package/dist/components/atoms/index.cjs +0 -318
  98. package/dist/components/atoms/index.d.ts.map +0 -1
  99. package/dist/components/atoms/index.js +0 -288
  100. package/dist/components/index.cjs +0 -1275
  101. package/dist/components/index.d.ts.map +0 -1
  102. package/dist/components/index.js +0 -1245
  103. package/dist/components/molecules/CourseCard.d.ts +0 -25
  104. package/dist/components/molecules/CourseCard.d.ts.map +0 -1
  105. package/dist/components/molecules/EmptyState.d.ts +0 -10
  106. package/dist/components/molecules/EmptyState.d.ts.map +0 -1
  107. package/dist/components/molecules/LoadingSpinner.d.ts +0 -7
  108. package/dist/components/molecules/LoadingSpinner.d.ts.map +0 -1
  109. package/dist/components/molecules/PageHeader.d.ts +0 -8
  110. package/dist/components/molecules/PageHeader.d.ts.map +0 -1
  111. package/dist/components/molecules/Pagination.d.ts +0 -13
  112. package/dist/components/molecules/Pagination.d.ts.map +0 -1
  113. package/dist/components/molecules/SearchInput.d.ts +0 -8
  114. package/dist/components/molecules/SearchInput.d.ts.map +0 -1
  115. package/dist/components/molecules/index.cjs +0 -334
  116. package/dist/components/molecules/index.d.ts.map +0 -1
  117. package/dist/components/molecules/index.js +0 -311
  118. package/dist/components/organisms/CourseSidebar.d.ts +0 -37
  119. package/dist/components/organisms/CourseSidebar.d.ts.map +0 -1
  120. package/dist/components/organisms/LearnerNavbar.d.ts +0 -8
  121. package/dist/components/organisms/LearnerNavbar.d.ts.map +0 -1
  122. package/dist/components/organisms/LearnerSidebar.d.ts +0 -16
  123. package/dist/components/organisms/LearnerSidebar.d.ts.map +0 -1
  124. package/dist/components/organisms/LessonBookmarks.d.ts +0 -8
  125. package/dist/components/organisms/LessonBookmarks.d.ts.map +0 -1
  126. package/dist/components/organisms/LessonNotes.d.ts +0 -8
  127. package/dist/components/organisms/LessonNotes.d.ts.map +0 -1
  128. package/dist/components/organisms/index.cjs +0 -855
  129. package/dist/components/organisms/index.d.ts.map +0 -1
  130. package/dist/components/organisms/index.js +0 -825
  131. package/dist/components/pages/BundleDetailPage.d.ts +0 -3
  132. package/dist/components/pages/BundleDetailPage.d.ts.map +0 -1
  133. package/dist/components/pages/CatalogBundlesPage.d.ts +0 -3
  134. package/dist/components/pages/CatalogBundlesPage.d.ts.map +0 -1
  135. package/dist/components/pages/CatalogCoursesPage.d.ts +0 -3
  136. package/dist/components/pages/CatalogCoursesPage.d.ts.map +0 -1
  137. package/dist/components/pages/CourseDetailPage.d.ts +0 -3
  138. package/dist/components/pages/CourseDetailPage.d.ts.map +0 -1
  139. package/dist/components/pages/CoursePlayerPage.d.ts +0 -8
  140. package/dist/components/pages/CoursePlayerPage.d.ts.map +0 -1
  141. package/dist/components/pages/CreatorProfilePage.d.ts +0 -3
  142. package/dist/components/pages/CreatorProfilePage.d.ts.map +0 -1
  143. package/dist/components/pages/LearnerSettingsPage.d.ts +0 -3
  144. package/dist/components/pages/LearnerSettingsPage.d.ts.map +0 -1
  145. package/dist/components/pages/ManualReviewDetailPage.d.ts +0 -3
  146. package/dist/components/pages/ManualReviewDetailPage.d.ts.map +0 -1
  147. package/dist/components/pages/ManualReviewPage.d.ts +0 -3
  148. package/dist/components/pages/ManualReviewPage.d.ts.map +0 -1
  149. package/dist/components/pages/MessagesPage.d.ts +0 -3
  150. package/dist/components/pages/MessagesPage.d.ts.map +0 -1
  151. package/dist/components/pages/MyLearningPage.d.ts +0 -3
  152. package/dist/components/pages/MyLearningPage.d.ts.map +0 -1
  153. package/dist/components/pages/PaymentCancelPage.d.ts +0 -3
  154. package/dist/components/pages/PaymentCancelPage.d.ts.map +0 -1
  155. package/dist/components/pages/PaymentSuccessPage.d.ts +0 -3
  156. package/dist/components/pages/PaymentSuccessPage.d.ts.map +0 -1
  157. package/dist/components/pages/index.cjs +0 -3306
  158. package/dist/components/pages/index.d.ts.map +0 -1
  159. package/dist/components/pages/index.js +0 -3315
  160. package/dist/components/utils.d.ts +0 -3
  161. package/dist/components/utils.d.ts.map +0 -1
  162. package/dist/contracts/components.contract.d.ts +0 -87
  163. package/dist/contracts/components.contract.d.ts.map +0 -1
  164. package/dist/contracts/index.cjs +0 -52
  165. package/dist/contracts/index.d.ts.map +0 -1
  166. package/dist/contracts/index.js +0 -29
  167. package/dist/contracts/layout.contract.d.ts +0 -35
  168. package/dist/contracts/layout.contract.d.ts.map +0 -1
  169. package/dist/contracts/pages.contract.d.ts +0 -192
  170. package/dist/contracts/pages.contract.d.ts.map +0 -1
  171. package/dist/contracts/template.contract.d.ts +0 -49
  172. package/dist/contracts/template.contract.d.ts.map +0 -1
  173. package/dist/hooks/index.cjs +0 -165
  174. package/dist/hooks/index.d.ts.map +0 -1
  175. package/dist/hooks/index.js +0 -142
  176. package/dist/hooks/sdk-context.d.ts +0 -125
  177. package/dist/hooks/sdk-context.d.ts.map +0 -1
  178. package/dist/hooks/useAiCoach.d.ts +0 -32
  179. package/dist/hooks/useAiCoach.d.ts.map +0 -1
  180. package/dist/hooks/useBookmarks.d.ts +0 -31
  181. package/dist/hooks/useBookmarks.d.ts.map +0 -1
  182. package/dist/hooks/useCourseSearch.d.ts.map +0 -1
  183. package/dist/hooks/useDebounce.d.ts.map +0 -1
  184. package/dist/hooks/useMyBundles.d.ts.map +0 -1
  185. package/dist/hooks/useMyCourses.d.ts.map +0 -1
  186. package/dist/hooks/useNotes.d.ts +0 -31
  187. package/dist/hooks/useNotes.d.ts.map +0 -1
  188. package/dist/hooks/useNotifications.d.ts +0 -19
  189. package/dist/hooks/useNotifications.d.ts.map +0 -1
  190. package/dist/hooks/useTheme.d.ts.map +0 -1
  191. package/dist/hooks/useToast.d.ts +0 -17
  192. package/dist/hooks/useToast.d.ts.map +0 -1
  193. package/dist/hooks/useUser.d.ts.map +0 -1
  194. package/dist/index.cjs +0 -630
  195. package/dist/index.d.ts +0 -17
  196. package/dist/index.d.ts.map +0 -1
  197. package/dist/index.js +0 -600
  198. package/dist/layouts/DefaultLayout.d.ts +0 -9
  199. package/dist/layouts/DefaultLayout.d.ts.map +0 -1
  200. package/dist/types/ai-coach.d.ts +0 -22
  201. package/dist/types/ai-coach.d.ts.map +0 -1
  202. package/dist/types/bookmarks.d.ts +0 -19
  203. package/dist/types/bookmarks.d.ts.map +0 -1
  204. package/dist/types/bundle.d.ts +0 -114
  205. package/dist/types/bundle.d.ts.map +0 -1
  206. package/dist/types/common.d.ts +0 -23
  207. package/dist/types/common.d.ts.map +0 -1
  208. package/dist/types/course.d.ts +0 -127
  209. package/dist/types/course.d.ts.map +0 -1
  210. package/dist/types/enrollment.d.ts +0 -34
  211. package/dist/types/enrollment.d.ts.map +0 -1
  212. package/dist/types/index.cjs +0 -18
  213. package/dist/types/index.d.ts.map +0 -1
  214. package/dist/types/index.js +0 -0
  215. package/dist/types/lesson.d.ts +0 -105
  216. package/dist/types/lesson.d.ts.map +0 -1
  217. package/dist/types/manual-review.d.ts +0 -123
  218. package/dist/types/manual-review.d.ts.map +0 -1
  219. package/dist/types/messaging.d.ts +0 -101
  220. package/dist/types/messaging.d.ts.map +0 -1
  221. package/dist/types/notification.d.ts +0 -28
  222. package/dist/types/notification.d.ts.map +0 -1
  223. package/dist/types/payment.d.ts +0 -38
  224. package/dist/types/payment.d.ts.map +0 -1
  225. package/dist/types/progress.d.ts +0 -18
  226. package/dist/types/progress.d.ts.map +0 -1
  227. package/dist/types/rating.d.ts +0 -20
  228. package/dist/types/rating.d.ts.map +0 -1
  229. package/dist/types/search.d.ts +0 -28
  230. package/dist/types/search.d.ts.map +0 -1
  231. package/dist/types/user.d.ts +0 -15
  232. package/dist/types/user.d.ts.map +0 -1
  233. package/dist/utils/formatters.d.ts +0 -25
  234. package/dist/utils/formatters.d.ts.map +0 -1
  235. package/dist/utils/index.cjs +0 -80
  236. package/dist/utils/index.d.ts +0 -2
  237. package/dist/utils/index.d.ts.map +0 -1
  238. package/dist/utils/index.js +0 -57
@@ -0,0 +1,23 @@
1
+ import type { ReactNode } from 'react';
2
+ import { cn } from '../utils';
3
+
4
+ export interface EmptyStateProps {
5
+ icon?: ReactNode;
6
+ title: string;
7
+ description?: string;
8
+ action?: ReactNode;
9
+ className?: string;
10
+ }
11
+
12
+ export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
13
+ return (
14
+ <div className={cn('flex flex-col items-center justify-center py-12 text-center', className)}>
15
+ {icon && <div className="mb-4 text-theme-text-muted">{icon}</div>}
16
+ <h3 className="text-lg font-semibold text-theme-text-primary">{title}</h3>
17
+ {description && (
18
+ <p className="mt-2 text-sm text-theme-text-secondary max-w-md">{description}</p>
19
+ )}
20
+ {action && <div className="mt-4">{action}</div>}
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,27 @@
1
+ import { cn } from '../utils';
2
+
3
+ export interface LoadingSpinnerProps {
4
+ size?: 'sm' | 'md' | 'lg';
5
+ className?: string;
6
+ text?: string;
7
+ }
8
+
9
+ const sizeClasses = {
10
+ sm: 'h-4 w-4',
11
+ md: 'h-8 w-8',
12
+ lg: 'h-12 w-12',
13
+ };
14
+
15
+ export function LoadingSpinner({ size = 'md', className, text }: LoadingSpinnerProps) {
16
+ return (
17
+ <div className={cn('flex flex-col items-center justify-center gap-3', className)}>
18
+ <div
19
+ className={cn(
20
+ 'animate-spin rounded-full border-2 border-theme-bg-tertiary border-t-theme-accent-primary',
21
+ sizeClasses[size]
22
+ )}
23
+ />
24
+ {text && <p className="text-sm text-theme-text-secondary">{text}</p>}
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,22 @@
1
+ import { cn } from '../utils';
2
+
3
+ export interface PageHeaderProps {
4
+ title: string;
5
+ description?: string;
6
+ actions?: React.ReactNode;
7
+ className?: string;
8
+ }
9
+
10
+ export function PageHeader({ title, description, actions, className }: PageHeaderProps) {
11
+ return (
12
+ <div className={cn('flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6', className)}>
13
+ <div>
14
+ <h1 className="text-2xl font-bold text-theme-text-primary">{title}</h1>
15
+ {description && (
16
+ <p className="text-sm text-theme-text-secondary mt-1">{description}</p>
17
+ )}
18
+ </div>
19
+ {actions && <div className="flex items-center gap-2">{actions}</div>}
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,82 @@
1
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
2
+ import { cn } from '../utils';
3
+
4
+ export interface PaginationProps {
5
+ currentPage: number;
6
+ totalPages: number;
7
+ total: number;
8
+ pageSize: number;
9
+ hasNextPage: boolean;
10
+ hasPreviousPage: boolean;
11
+ onPageChange: (page: number) => void;
12
+ maxVisiblePages?: number;
13
+ className?: string;
14
+ }
15
+
16
+ export function Pagination({
17
+ currentPage,
18
+ totalPages,
19
+ total,
20
+ pageSize,
21
+ hasNextPage,
22
+ hasPreviousPage,
23
+ onPageChange,
24
+ maxVisiblePages = 5,
25
+ className,
26
+ }: PaginationProps) {
27
+ const renderPageNumbers = () => {
28
+ const pages = [];
29
+ let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
30
+ let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
31
+
32
+ if (endPage - startPage < maxVisiblePages - 1) {
33
+ startPage = Math.max(1, endPage - maxVisiblePages + 1);
34
+ }
35
+
36
+ for (let i = startPage; i <= endPage; i++) {
37
+ pages.push(
38
+ <button
39
+ key={i}
40
+ onClick={() => onPageChange(i)}
41
+ className={cn(
42
+ 'px-3 py-1 rounded cursor-pointer transition-colors',
43
+ i === currentPage
44
+ ? 'bg-theme-accent-primary text-white'
45
+ : 'text-theme-text-primary border border-theme-border-primary hover:bg-theme-bg-tertiary'
46
+ )}
47
+ >
48
+ {i}
49
+ </button>
50
+ );
51
+ }
52
+
53
+ return pages;
54
+ };
55
+
56
+ return (
57
+ <div className={cn('flex items-center justify-between border-t border-theme-border-primary pt-4', className)}>
58
+ <div className="text-sm text-theme-text-secondary">
59
+ Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, total)} of {total} results
60
+ </div>
61
+ <div className="flex items-center gap-2">
62
+ <button
63
+ onClick={() => onPageChange(currentPage - 1)}
64
+ disabled={!hasPreviousPage}
65
+ className="p-2 rounded border border-theme-border-primary hover:bg-theme-bg-tertiary disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-colors"
66
+ aria-label="Previous page"
67
+ >
68
+ <ChevronLeft className="h-4 w-4" />
69
+ </button>
70
+ {renderPageNumbers()}
71
+ <button
72
+ onClick={() => onPageChange(currentPage + 1)}
73
+ disabled={!hasNextPage}
74
+ className="p-2 rounded border border-theme-border-primary hover:bg-theme-bg-tertiary disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-colors"
75
+ aria-label="Next page"
76
+ >
77
+ <ChevronRight className="h-4 w-4" />
78
+ </button>
79
+ </div>
80
+ </div>
81
+ );
82
+ }
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+
3
+ import { Search, X } from 'lucide-react';
4
+ import { cn } from '../utils';
5
+
6
+ export interface SearchInputProps {
7
+ value: string;
8
+ onChange: (value: string) => void;
9
+ placeholder?: string;
10
+ className?: string;
11
+ }
12
+
13
+ export function SearchInput({ value, onChange, placeholder = 'Search...', className }: SearchInputProps) {
14
+ return (
15
+ <div className={cn('relative', className)}>
16
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-theme-text-muted" />
17
+ <input
18
+ type="text"
19
+ value={value}
20
+ onChange={(e) => onChange(e.target.value)}
21
+ placeholder={placeholder}
22
+ className="w-full pl-10 pr-10 py-2 text-sm rounded-lg border border-theme-border-primary bg-theme-bg-primary text-theme-text-primary placeholder:text-[rgb(var(--text-muted))] focus:outline-none focus:border-theme-accent-primary transition-colors"
23
+ />
24
+ {value && (
25
+ <button
26
+ onClick={() => onChange('')}
27
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-theme-text-muted hover:text-theme-text-primary cursor-pointer"
28
+ aria-label="Clear search"
29
+ >
30
+ <X className="h-4 w-4" />
31
+ </button>
32
+ )}
33
+ </div>
34
+ );
35
+ }
@@ -10,4 +10,3 @@ export { EmptyState } from './EmptyState';
10
10
  export type { EmptyStateProps } from './EmptyState';
11
11
  export { LoadingSpinner } from './LoadingSpinner';
12
12
  export type { LoadingSpinnerProps } from './LoadingSpinner';
13
- //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,276 @@
1
+ 'use client';
2
+
3
+ import { useState, useMemo } from 'react';
4
+ import { ArrowLeft, Search, X, Lock, ClipboardList, Award, Check, ChevronRight } from 'lucide-react';
5
+ import { cn } from '../utils';
6
+ import { Button } from '../atoms/Button';
7
+ import { ProgressBar } from '../atoms/ProgressBar';
8
+
9
+ export interface CourseSidebarLesson {
10
+ id: string;
11
+ title: string;
12
+ duration?: string;
13
+ completed?: boolean;
14
+ locked?: boolean;
15
+ availableAt?: string;
16
+ hasPrerequisite?: boolean;
17
+ lessonIcon?: string;
18
+ type?: string;
19
+ }
20
+
21
+ export interface CourseSidebarModule {
22
+ id: string;
23
+ title: string;
24
+ lessons: CourseSidebarLesson[];
25
+ }
26
+
27
+ export interface CourseSidebarProps {
28
+ courseTitle: string;
29
+ modules: CourseSidebarModule[];
30
+ currentLessonId: string;
31
+ expandedModules: Set<string>;
32
+ completedCount: number;
33
+ totalCount: number;
34
+ onToggleModule: (moduleId: string) => void;
35
+ onSelectLesson: (lessonId: string) => void;
36
+ hideProgress?: boolean;
37
+ hideSearch?: boolean;
38
+ certificateEnabled?: boolean;
39
+ isCompleted?: boolean;
40
+ onDownloadCertificate?: () => void;
41
+ isDownloadingCertificate?: boolean;
42
+ onBack?: () => void;
43
+ showBackButton?: boolean;
44
+ className?: string;
45
+ }
46
+
47
+ const lessonColors = [
48
+ 'bg-[#E2F0FF] border-l-4 border-[#3aa3a5]',
49
+ 'bg-[#fcddb5] border-l-4 border-[#d67a05]',
50
+ 'bg-[#FAD1CE] border-l-4 border-[#7ab3f0]',
51
+ ];
52
+
53
+ export function CourseSidebar({
54
+ courseTitle,
55
+ modules,
56
+ currentLessonId,
57
+ expandedModules,
58
+ completedCount,
59
+ totalCount,
60
+ onToggleModule,
61
+ onSelectLesson,
62
+ hideProgress = false,
63
+ hideSearch = false,
64
+ certificateEnabled = false,
65
+ isCompleted = false,
66
+ onDownloadCertificate,
67
+ isDownloadingCertificate = false,
68
+ onBack,
69
+ showBackButton = true,
70
+ className,
71
+ }: CourseSidebarProps) {
72
+ const [searchQuery, setSearchQuery] = useState('');
73
+
74
+ const filteredModules = useMemo(() => {
75
+ if (!searchQuery.trim()) return modules;
76
+ const query = searchQuery.toLowerCase();
77
+ return modules
78
+ .map((mod) => ({
79
+ ...mod,
80
+ lessons: mod.lessons.filter((l) => l.title.toLowerCase().includes(query)),
81
+ }))
82
+ .filter((mod) => mod.lessons.length > 0);
83
+ }, [modules, searchQuery]);
84
+
85
+ const totalResults = filteredModules.reduce((sum, m) => sum + m.lessons.length, 0);
86
+ const progressPct = totalCount > 0 ? (completedCount / totalCount) * 100 : 0;
87
+
88
+ const formatDuration = (duration?: string): string => {
89
+ if (!duration) return '';
90
+ const text = duration.trim();
91
+ if (!text) return '';
92
+ if (/[a-zA-Z]/.test(text)) return text;
93
+ const parts = text.split(':').map(Number);
94
+ if (parts.some(isNaN)) return text;
95
+ if (parts.length === 3) {
96
+ const [hrs, mins] = parts;
97
+ if (hrs > 0 && mins > 0) return `${hrs} ${hrs === 1 ? 'hour' : 'hours'} ${mins} min`;
98
+ if (hrs > 0) return `${hrs} ${hrs === 1 ? 'hour' : 'hours'}`;
99
+ return `${mins} min`;
100
+ }
101
+ if (parts.length === 2) {
102
+ const [mins, secs] = parts;
103
+ return `${mins > 0 ? mins : secs > 0 ? 1 : 0} min`;
104
+ }
105
+ return `${Math.max(1, Math.round(parts[0] / 60))} min`;
106
+ };
107
+
108
+ let globalLessonIndex = 0;
109
+
110
+ return (
111
+ <div className={cn('w-full lg:w-[350px] bg-white flex-shrink-0 flex flex-col border-r border-gray-100 lg:h-screen lg:sticky lg:top-0 overflow-y-auto', className)}>
112
+ <div className="p-6">
113
+ {showBackButton && onBack && (
114
+ <button
115
+ onClick={onBack}
116
+ className="w-10 h-10 bg-[#4ecdc4] rounded-md flex items-center justify-center text-white hover:bg-[#3dbdb4] transition-colors mb-6 cursor-pointer"
117
+ aria-label="Go back"
118
+ >
119
+ <ArrowLeft className="w-5 h-5" />
120
+ </button>
121
+ )}
122
+
123
+ <h3 className="text-3xl font-semibold text-[#1f2937] leading-tight">
124
+ {courseTitle ? `${courseTitle.charAt(0).toUpperCase()}${courseTitle.slice(1)}` : ''}
125
+ </h3>
126
+
127
+ {!hideProgress && (
128
+ <div className="mt-4">
129
+ <ProgressBar value={progressPct} size="sm" />
130
+ <p className="text-xs text-gray-400 mt-1 text-right">
131
+ {completedCount} / {totalCount} lessons · {Math.round(progressPct)}%
132
+ </p>
133
+ </div>
134
+ )}
135
+
136
+ {certificateEnabled && isCompleted && onDownloadCertificate && completedCount === totalCount && totalCount > 0 && (
137
+ <Button
138
+ onClick={onDownloadCertificate}
139
+ disabled={isDownloadingCertificate}
140
+ className="w-full mt-3 bg-green-600 hover:bg-green-700 disabled:opacity-50 text-white"
141
+ >
142
+ <Award className="w-4 h-4 mr-2" />
143
+ {isDownloadingCertificate ? 'Downloading...' : 'Download Certificate'}
144
+ </Button>
145
+ )}
146
+
147
+ {!hideSearch && (
148
+ <div className="mt-3 relative">
149
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
150
+ <input
151
+ type="text"
152
+ placeholder="Search lessons..."
153
+ value={searchQuery}
154
+ onChange={(e) => setSearchQuery(e.target.value)}
155
+ className="w-full pl-9 pr-9 py-2 text-sm bg-gray-50 border border-gray-200 text-gray-700 placeholder:text-gray-400 rounded-lg focus:outline-none focus:border-[#49BBBD] transition-colors"
156
+ />
157
+ {searchQuery && (
158
+ <>
159
+ <button
160
+ onClick={() => setSearchQuery('')}
161
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 cursor-pointer"
162
+ aria-label="Clear search"
163
+ >
164
+ <X className="w-4 h-4" />
165
+ </button>
166
+ <p className="text-xs text-gray-400 mt-1">
167
+ {totalResults} {totalResults === 1 ? 'lesson' : 'lessons'} found
168
+ </p>
169
+ </>
170
+ )}
171
+ </div>
172
+ )}
173
+ </div>
174
+
175
+ <div className="flex-1 px-4 space-y-8 pb-10">
176
+ {filteredModules.length === 0 ? (
177
+ <div className="text-center py-8">
178
+ <Search className="w-10 h-10 text-gray-300 mx-auto mb-2" />
179
+ <p className="text-sm text-gray-400">No lessons found for &ldquo;{searchQuery}&rdquo;</p>
180
+ <button onClick={() => setSearchQuery('')} className="mt-2 text-sm text-[#49BBBD] hover:underline font-medium cursor-pointer">
181
+ Clear search
182
+ </button>
183
+ </div>
184
+ ) : (
185
+ filteredModules.map((module) => {
186
+ const isExpanded = expandedModules.has(module.id) || !!searchQuery.trim();
187
+ return (
188
+ <div key={module.id}>
189
+ <button
190
+ type="button"
191
+ onClick={() => onToggleModule(module.id)}
192
+ aria-expanded={isExpanded}
193
+ className="w-full flex items-center justify-between text-left mb-3 px-2 cursor-pointer"
194
+ >
195
+ <h3 className="text-sm font-bold text-gray-500 uppercase tracking-wider">{module.title}</h3>
196
+ <ChevronRight className={cn('w-4 h-4 transition-transform', isExpanded ? 'rotate-90 text-[#49BBBD]' : 'text-gray-400')} />
197
+ </button>
198
+
199
+ {isExpanded && (
200
+ <div className="space-y-3">
201
+ {module.lessons.map((lesson) => {
202
+ const isCurrent = currentLessonId === lesson.id;
203
+ const isLocked = lesson.locked === true;
204
+ const colorClass = lessonColors[globalLessonIndex % lessonColors.length];
205
+ globalLessonIndex++;
206
+
207
+ let availabilityText = '';
208
+ if (isLocked) {
209
+ if (lesson.availableAt) {
210
+ const diffDays = Math.ceil((new Date(lesson.availableAt).getTime() - Date.now()) / 86400000);
211
+ availabilityText = diffDays > 0 ? `Available in ${diffDays}d` : `Available on ${new Date(lesson.availableAt).toLocaleDateString()}`;
212
+ } else if (lesson.hasPrerequisite) {
213
+ availabilityText = 'Complete previous lesson';
214
+ }
215
+ }
216
+
217
+ return (
218
+ <button
219
+ key={lesson.id}
220
+ title={lesson.title}
221
+ onClick={() => !isLocked && onSelectLesson(lesson.id)}
222
+ disabled={isLocked}
223
+ className={cn(
224
+ 'w-full text-left p-4 rounded-lg flex items-center justify-between transition-transform',
225
+ isLocked ? 'opacity-50 cursor-not-allowed' : 'hover:scale-[1.02] active:scale-[0.98] cursor-pointer',
226
+ isCurrent ? 'bg-[#49BBBD] text-white shadow-md ring-2 ring-[#2a9ea0]/40' : colorClass
227
+ )}
228
+ >
229
+ <div className="flex items-center gap-3 overflow-hidden">
230
+ {lesson.completed ? (
231
+ <div className="w-5 h-5 rounded-full border-2 bg-green-500 border-green-500 flex items-center justify-center flex-shrink-0">
232
+ <Check className="w-3 h-3 text-white" />
233
+ </div>
234
+ ) : isLocked ? (
235
+ <div className="w-5 h-5 rounded-full border-2 border-gray-400 flex items-center justify-center flex-shrink-0">
236
+ <Lock className="w-2.5 h-2.5 text-gray-500" />
237
+ </div>
238
+ ) : (
239
+ <div className={cn('w-5 h-5 rounded-full border-2 flex items-center justify-center flex-shrink-0', isCurrent ? 'border-white/60' : 'border-gray-400')}>
240
+ <div className={cn('w-2.5 h-2.5 rounded-sm', isCurrent ? 'bg-white/80' : 'bg-[#1f2a44]')} />
241
+ </div>
242
+ )}
243
+ <div className="overflow-hidden">
244
+ <div className="flex items-center gap-1.5">
245
+ {lesson.type === 'ASSESSMENT' ? (
246
+ <ClipboardList className={cn('w-3.5 h-3.5 flex-shrink-0', isCurrent ? 'text-white' : 'text-gray-600')} />
247
+ ) : null}
248
+ <span className={cn('text-sm font-medium truncate', isCurrent ? 'text-white' : 'text-gray-800')}>
249
+ {lesson.title}
250
+ </span>
251
+ </div>
252
+ {isLocked && availabilityText && (
253
+ <p className="text-xs text-gray-600 mt-0.5 truncate">{availabilityText}</p>
254
+ )}
255
+ </div>
256
+ </div>
257
+ {lesson.type === 'ASSESSMENT' ? (
258
+ <span className={cn('text-xs font-semibold flex-shrink-0 ml-2', isCurrent ? 'text-white/90' : 'text-gray-600')}>Quiz</span>
259
+ ) : lesson.duration ? (
260
+ <span className={cn('text-xs font-semibold flex-shrink-0 ml-2', isCurrent ? 'text-white/90' : 'text-gray-600')}>
261
+ {formatDuration(lesson.duration)}
262
+ </span>
263
+ ) : null}
264
+ </button>
265
+ );
266
+ })}
267
+ </div>
268
+ )}
269
+ </div>
270
+ );
271
+ })
272
+ )}
273
+ </div>
274
+ </div>
275
+ );
276
+ }
@@ -0,0 +1,129 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { User, Menu, ChevronDown, LogOut, BookOpen } from 'lucide-react';
5
+ import { cn } from '../utils';
6
+ import { useSDK } from '../../hooks/sdk-context';
7
+
8
+ export interface LearnerNavbarProps {
9
+ hideMenuButton?: boolean;
10
+ onMenuClick?: () => void;
11
+ onProfileClick?: () => void;
12
+ onLogoClick?: () => void;
13
+ }
14
+
15
+ export function LearnerNavbar({ hideMenuButton = false, onMenuClick, onProfileClick, onLogoClick }: LearnerNavbarProps) {
16
+ const sdk = useSDK();
17
+ const { user } = sdk.useUser();
18
+ const { logout } = sdk.useLogout();
19
+ const [profileDropdownOpen, setProfileDropdownOpen] = useState(false);
20
+ const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
21
+
22
+ useEffect(() => {
23
+ return () => {
24
+ if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
25
+ };
26
+ }, []);
27
+
28
+ const openDropdown = () => {
29
+ if (closeTimeoutRef.current) {
30
+ clearTimeout(closeTimeoutRef.current);
31
+ closeTimeoutRef.current = null;
32
+ }
33
+ setProfileDropdownOpen(true);
34
+ };
35
+
36
+ const closeDropdownWithDelay = () => {
37
+ if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
38
+ closeTimeoutRef.current = setTimeout(() => {
39
+ setProfileDropdownOpen(false);
40
+ closeTimeoutRef.current = null;
41
+ }, 150);
42
+ };
43
+
44
+ return (
45
+ <header style={{ top: 'var(--preview-toolbar-h, 0px)' }} className="fixed left-0 right-0 z-30 h-20 border-b border-theme-border-primary bg-theme-bg-secondary">
46
+ <div className="flex h-full items-center justify-between px-4 sm:px-6 lg:px-10 xl:px-12 2xl:px-16">
47
+ <div className="flex items-center gap-4">
48
+ {!hideMenuButton && (
49
+ <button
50
+ onClick={onMenuClick}
51
+ className="rounded-lg p-2 hover:bg-theme-bg-tertiary lg:hidden"
52
+ >
53
+ <Menu className="h-5 w-5 text-theme-text-primary" />
54
+ </button>
55
+ )}
56
+ </div>
57
+
58
+ <div className="ml-auto flex items-center">
59
+ <div
60
+ className="relative group ml-2 sm:ml-4 border-l border-theme-border-primary pl-3 sm:pl-5"
61
+ onMouseEnter={openDropdown}
62
+ onMouseLeave={closeDropdownWithDelay}
63
+ >
64
+ <button
65
+ onClick={() => setProfileDropdownOpen((o) => !o)}
66
+ className="flex items-center gap-2 sm:gap-3 hover:opacity-80 cursor-pointer transition-opacity"
67
+ >
68
+ <div className="relative flex h-8 w-8 sm:h-10 sm:w-10 items-center justify-center rounded-full bg-theme-accent-primary text-sm font-semibold text-white overflow-hidden">
69
+ {user?.profileImage ? (
70
+ <img src={user.profileImage} alt="Profile" className="w-full h-full object-cover" />
71
+ ) : (
72
+ user?.name?.charAt(0) || 'S'
73
+ )}
74
+ </div>
75
+ {user && (
76
+ <div className="hidden md:flex items-center gap-3">
77
+ <div className="text-sm font-medium text-theme-text-primary">{user.name}</div>
78
+ <ChevronDown
79
+ className={cn(
80
+ 'h-4 w-4 text-theme-text-secondary transition-transform group-hover:rotate-180',
81
+ profileDropdownOpen && 'rotate-180'
82
+ )}
83
+ />
84
+ </div>
85
+ )}
86
+ </button>
87
+
88
+ {profileDropdownOpen && (
89
+ <div
90
+ className="absolute right-0 top-full z-50 mt-2 w-56 rounded-lg border border-theme-border-primary bg-theme-bg-secondary shadow-lg"
91
+ onMouseEnter={openDropdown}
92
+ onMouseLeave={closeDropdownWithDelay}
93
+ >
94
+ <div className="border-b border-theme-border-primary p-3">
95
+ <p className="text-sm font-medium text-theme-text-primary">{user?.name}</p>
96
+ <p className="text-xs text-theme-text-muted">{user?.email}</p>
97
+ </div>
98
+ <div className="p-2">
99
+ <button
100
+ onClick={() => {
101
+ onProfileClick?.();
102
+ setProfileDropdownOpen(false);
103
+ }}
104
+ className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-theme-text-secondary transition-colors hover:bg-[rgb(var(--accent-primary))]/15 hover:text-[rgb(var(--accent-primary))] cursor-pointer"
105
+ >
106
+ <User className="h-4 w-4" />
107
+ My Profile
108
+ </button>
109
+ </div>
110
+ <div className="border-t border-theme-border-primary p-2">
111
+ <button
112
+ onClick={() => {
113
+ logout();
114
+ setProfileDropdownOpen(false);
115
+ }}
116
+ className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-theme-text-secondary transition-colors hover:bg-[rgb(var(--accent-primary))]/15 hover:text-[rgb(var(--accent-primary))] cursor-pointer"
117
+ >
118
+ <LogOut className="h-4 w-4" />
119
+ Logout
120
+ </button>
121
+ </div>
122
+ </div>
123
+ )}
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </header>
128
+ );
129
+ }