@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,148 @@
1
+ 'use client';
2
+
3
+ import { BookOpen, GraduationCap, MessageSquare, ClipboardCheck, UserCircle, Settings, LogOut, ChevronLeft, ChevronRight } from 'lucide-react';
4
+ import { cn } from '../utils';
5
+ import { Button } from '../atoms/Button';
6
+ import { useSDK } from '../../hooks/sdk-context';
7
+
8
+ export interface SidebarItem {
9
+ label: string;
10
+ href: string;
11
+ icon: React.ElementType;
12
+ }
13
+
14
+ export interface LearnerSidebarProps {
15
+ isOpen: boolean;
16
+ isCollapsed: boolean;
17
+ onClose: () => void;
18
+ onToggle: () => void;
19
+ currentPath: string;
20
+ onNavigate: (href: string) => void;
21
+ items?: SidebarItem[];
22
+ }
23
+
24
+ const defaultItems: SidebarItem[] = [
25
+ { label: 'My Learning', href: '/my-learning', icon: BookOpen },
26
+ { label: 'Catalog', href: '/catalog/courses', icon: GraduationCap },
27
+ { label: 'Messages', href: '/messages', icon: MessageSquare },
28
+ { label: 'Manual Review', href: '/manual-review', icon: ClipboardCheck },
29
+ { label: 'Account', href: '/account', icon: UserCircle },
30
+ ];
31
+
32
+ export function LearnerSidebar({
33
+ isOpen,
34
+ isCollapsed,
35
+ onClose,
36
+ onToggle,
37
+ currentPath,
38
+ onNavigate,
39
+ items = defaultItems,
40
+ }: LearnerSidebarProps) {
41
+ const sdk = useSDK();
42
+ const { user } = sdk.useUser();
43
+ const { logout } = sdk.useLogout();
44
+
45
+ const isActive = (href: string, label: string) => {
46
+ if (label === 'Catalog' && currentPath.startsWith('/catalog')) return true;
47
+ return currentPath === href || currentPath.startsWith(href + '/');
48
+ };
49
+
50
+ const handleNav = (href: string) => {
51
+ onNavigate(href);
52
+ onClose();
53
+ };
54
+
55
+ return (
56
+ <>
57
+ <aside
58
+ style={{ top: 'var(--preview-toolbar-h, 0px)', height: 'calc(100vh - var(--preview-toolbar-h, 0px))' }}
59
+ className={cn(
60
+ 'fixed left-0 z-40 border-r border-theme-border-primary bg-theme-bg-secondary transition-all duration-300 flex flex-col',
61
+ isOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
62
+ isCollapsed ? 'w-16' : 'w-64'
63
+ )}
64
+ >
65
+ <div className="flex h-20 items-center justify-between border-b border-theme-border-primary px-4 bg-theme-bg-secondary/50 backdrop-blur-sm">
66
+ {!isCollapsed && (
67
+ <button
68
+ onClick={() => handleNav('/my-learning')}
69
+ className="flex items-center gap-2 sm:gap-3 hover:opacity-80 transition-opacity cursor-pointer"
70
+ >
71
+ <div className="flex h-8 w-8 items-center justify-center rounded bg-theme-accent-primary">
72
+ <BookOpen className="h-5 w-5 text-white" />
73
+ </div>
74
+ <span className="text-xl font-bold text-theme-text-primary leading-none whitespace-nowrap">Academy</span>
75
+ </button>
76
+ )}
77
+ <Button
78
+ variant="ghost"
79
+ size="icon"
80
+ onClick={onToggle}
81
+ className={cn('hover:bg-theme-bg-tertiary rounded-lg transition-colors', isCollapsed && 'mx-auto')}
82
+ >
83
+ {isCollapsed ? <ChevronRight className="h-5 w-5" /> : <ChevronLeft className="h-5 w-5" />}
84
+ </Button>
85
+ </div>
86
+
87
+ <nav className="flex-1 overflow-y-auto p-4 space-y-1">
88
+ {items.map((item) => {
89
+ const Icon = item.icon;
90
+ const active = isActive(item.href, item.label);
91
+ return (
92
+ <button
93
+ key={item.href}
94
+ onClick={() => handleNav(item.href)}
95
+ className={cn(
96
+ 'flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors cursor-pointer',
97
+ active
98
+ ? 'bg-theme-accent-primary text-white hover:opacity-90'
99
+ : 'text-theme-text-secondary hover:bg-theme-bg-tertiary hover:text-theme-text-primary',
100
+ isCollapsed && 'justify-center'
101
+ )}
102
+ title={isCollapsed ? item.label : undefined}
103
+ >
104
+ <Icon className="h-5 w-5 shrink-0" />
105
+ {!isCollapsed && <span className="whitespace-nowrap">{item.label}</span>}
106
+ </button>
107
+ );
108
+ })}
109
+ </nav>
110
+
111
+ {user && (
112
+ <div className="border-t border-theme-border-primary p-4">
113
+ <div className="space-y-1">
114
+ <button
115
+ onClick={() => handleNav('/learner-settings')}
116
+ className={cn(
117
+ 'flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors cursor-pointer',
118
+ currentPath === '/learner-settings'
119
+ ? 'bg-theme-accent-primary text-white'
120
+ : 'text-theme-text-secondary hover:bg-theme-bg-tertiary hover:text-theme-text-primary',
121
+ isCollapsed && 'justify-center'
122
+ )}
123
+ title={isCollapsed ? 'Settings' : undefined}
124
+ >
125
+ <Settings className="h-5 w-5 shrink-0" />
126
+ {!isCollapsed && <span className="whitespace-nowrap">Settings</span>}
127
+ </button>
128
+ <button
129
+ onClick={logout}
130
+ className={cn(
131
+ 'flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors hover:bg-red-50 hover:text-red-600 text-theme-text-secondary cursor-pointer',
132
+ isCollapsed && 'justify-center'
133
+ )}
134
+ >
135
+ <LogOut className="h-5 w-5 shrink-0" />
136
+ {!isCollapsed && <span className="whitespace-nowrap">Logout</span>}
137
+ </button>
138
+ </div>
139
+ </div>
140
+ )}
141
+ </aside>
142
+
143
+ {isOpen && (
144
+ <div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={onClose} />
145
+ )}
146
+ </>
147
+ );
148
+ }
@@ -0,0 +1,128 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { Pencil, Trash2, Bookmark as BookmarkIcon } from 'lucide-react';
5
+ import { cn } from '../utils';
6
+ import { Button } from '../atoms/Button';
7
+ import { Input } from '../atoms/Input';
8
+ import { useSDK } from '../../hooks/sdk-context';
9
+
10
+ export interface LessonBookmarksProps {
11
+ activityId: string;
12
+ currentVideoTime?: number;
13
+ getLiveCurrentTime?: () => number;
14
+ onTimestampClick?: (time: number) => void;
15
+ }
16
+
17
+ function formatTime(seconds: number): string {
18
+ const hrs = Math.floor(seconds / 3600);
19
+ const mins = Math.floor((seconds % 3600) / 60);
20
+ const secs = Math.floor(seconds % 60);
21
+ if (hrs > 0) return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
22
+ return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
23
+ }
24
+
25
+ export function LessonBookmarks({ activityId, currentVideoTime, getLiveCurrentTime, onTimestampClick }: LessonBookmarksProps) {
26
+ const sdk = useSDK();
27
+ const { data: apiBookmarks, isLoading, refetch } = sdk.useLessonBookmarks(activityId);
28
+ const { createBookmark, isLoading: isCreating } = sdk.useCreateBookmark();
29
+ const { updateBookmark, isLoading: isUpdating } = sdk.useUpdateBookmark();
30
+ const { deleteBookmark, isLoading: isDeleting } = sdk.useDeleteBookmark();
31
+
32
+ const [label, setLabel] = useState('');
33
+ const [editingId, setEditingId] = useState<string | null>(null);
34
+ const [editingLabel, setEditingLabel] = useState('');
35
+
36
+ const bookmarks = Array.isArray(apiBookmarks) ? [...apiBookmarks].sort((a: any, b: any) => a.second - b.second) : [];
37
+
38
+ useEffect(() => {
39
+ setLabel('');
40
+ setEditingId(null);
41
+ setEditingLabel('');
42
+ }, [activityId]);
43
+
44
+ const handleAdd = async () => {
45
+ const effectiveTime = getLiveCurrentTime ? getLiveCurrentTime() : currentVideoTime;
46
+ if (effectiveTime === undefined) return;
47
+ const result = await createBookmark({ activityId, second: Math.floor(effectiveTime), label: label.trim() || undefined });
48
+ if (result) { setLabel(''); await refetch(); }
49
+ };
50
+
51
+ const handleSaveEdit = async (id: string) => {
52
+ const result = await updateBookmark(id, { label: editingLabel.trim() || undefined });
53
+ if (result) { setEditingId(null); setEditingLabel(''); refetch(); }
54
+ };
55
+
56
+ const handleDelete = async (id: string) => {
57
+ const success = await deleteBookmark(id);
58
+ if (success) refetch();
59
+ };
60
+
61
+ const handleClick = (second: number) => {
62
+ onTimestampClick?.(second);
63
+ window.scrollTo({ top: 0, behavior: 'smooth' });
64
+ };
65
+
66
+ return (
67
+ <div className="flex flex-col h-full">
68
+ <div className="p-4 border-b border-theme-border-primary bg-theme-bg-secondary sticky top-0 z-10">
69
+ <h3 className="text-lg font-bold text-theme-text-primary mb-2">Bookmarks for this lesson</h3>
70
+ <p className="text-base text-theme-text-secondary mb-4">Save key moments in the video for quick access</p>
71
+ <div className="space-y-2">
72
+ <Input value={label} onChange={(e) => setLabel(e.target.value)} placeholder="Optional label..." className="w-full" />
73
+ <div className="flex justify-end">
74
+ <Button onClick={handleAdd} disabled={isCreating || currentVideoTime === undefined} className="bg-theme-accent-primary hover:bg-theme-accent-primary/90 text-white">
75
+ <BookmarkIcon className="h-4 w-4 mr-2" />
76
+ Add Bookmark
77
+ </Button>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
+ <div className="flex-1 overflow-y-auto p-4">
83
+ {isLoading ? (
84
+ <div className="text-theme-text-secondary">Loading bookmarks...</div>
85
+ ) : bookmarks.length === 0 ? (
86
+ <div className="text-center py-8">
87
+ <BookmarkIcon className="h-12 w-12 mx-auto text-theme-text-muted mb-3" />
88
+ <h3 className="text-xl font-bold text-theme-text-primary">No bookmarks yet</h3>
89
+ <p className="text-base text-theme-text-secondary mt-2">Click &ldquo;Add Bookmark&rdquo; to save important moments</p>
90
+ </div>
91
+ ) : (
92
+ <div className="space-y-3">
93
+ {bookmarks.map((bm: any) => (
94
+ <div key={bm.id} className="rounded-lg border border-theme-border-primary bg-theme-bg-secondary p-3 hover:border-theme-accent-primary transition-colors">
95
+ {editingId === bm.id ? (
96
+ <div className="space-y-2">
97
+ <Input value={editingLabel} onChange={(e) => setEditingLabel(e.target.value)} placeholder="Label..." className="w-full" autoFocus />
98
+ <div className="flex gap-2 justify-end">
99
+ <Button onClick={() => handleSaveEdit(bm.id)} disabled={isUpdating} className="bg-theme-accent-primary hover:bg-theme-accent-primary/90 text-white">Save</Button>
100
+ <Button variant="outline" onClick={() => { setEditingId(null); setEditingLabel(''); }} disabled={isUpdating}>Cancel</Button>
101
+ </div>
102
+ </div>
103
+ ) : (
104
+ <div className="flex items-start justify-between gap-3">
105
+ <div className="flex-1 min-w-0">
106
+ <button onClick={() => handleClick(bm.second)} className="text-theme-accent-primary font-mono font-semibold hover:underline text-sm">
107
+ {formatTime(bm.second)}
108
+ </button>
109
+ {bm.label && <p className="text-theme-text-secondary text-sm mt-1 break-words">{bm.label}</p>}
110
+ </div>
111
+ <div className="flex gap-1 flex-shrink-0">
112
+ <Button onClick={() => { setEditingId(bm.id); setEditingLabel(bm.label || ''); }} variant="ghost" size="icon" className="text-theme-accent-primary hover:text-theme-accent-primary/80 h-8 w-8" title="Edit">
113
+ <Pencil className="h-4 w-4" />
114
+ </Button>
115
+ <Button onClick={() => handleDelete(bm.id)} disabled={isDeleting} variant="ghost" size="icon" className="text-red-600 hover:text-red-800 h-8 w-8" title="Delete">
116
+ <Trash2 className="h-4 w-4" />
117
+ </Button>
118
+ </div>
119
+ </div>
120
+ )}
121
+ </div>
122
+ ))}
123
+ </div>
124
+ )}
125
+ </div>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,153 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { Pencil, Trash2 } from 'lucide-react';
5
+ import { cn } from '../utils';
6
+ import { Button } from '../atoms/Button';
7
+ import { useSDK } from '../../hooks/sdk-context';
8
+
9
+ export interface LessonNotesProps {
10
+ activityId: string;
11
+ currentVideoTime?: number;
12
+ getLiveCurrentTime?: () => number;
13
+ onTimestampClick?: (time: number) => void;
14
+ }
15
+
16
+ function formatTimestamp(seconds?: number): string {
17
+ if (seconds === undefined || seconds === null) return '';
18
+ const total = Math.floor(seconds);
19
+ const hrs = Math.floor(total / 3600);
20
+ const mins = Math.floor((total % 3600) / 60);
21
+ const secs = Math.floor(total % 60);
22
+ if (hrs > 0) return `${String(hrs).padStart(2, '0')}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
23
+ return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
24
+ }
25
+
26
+ export function LessonNotes({ activityId, currentVideoTime, getLiveCurrentTime, onTimestampClick }: LessonNotesProps) {
27
+ const sdk = useSDK();
28
+ const { data: apiNotes, isLoading, refetch } = sdk.useLessonNotes(activityId);
29
+ const { createNote, isLoading: isCreating } = sdk.useCreateNote();
30
+ const { updateNote, isLoading: isUpdating } = sdk.useUpdateNote();
31
+ const { deleteNote, isLoading: isDeleting } = sdk.useDeleteNote();
32
+
33
+ const [noteText, setNoteText] = useState('');
34
+ const [editingNoteId, setEditingNoteId] = useState<string | null>(null);
35
+ const [editingText, setEditingText] = useState('');
36
+
37
+ const notes = Array.isArray(apiNotes) ? apiNotes : [];
38
+
39
+ useEffect(() => {
40
+ setNoteText('');
41
+ setEditingNoteId(null);
42
+ setEditingText('');
43
+ }, [activityId]);
44
+
45
+ const handleSaveNote = async () => {
46
+ if (!noteText.trim()) return;
47
+ const effectiveTime = getLiveCurrentTime ? getLiveCurrentTime() : currentVideoTime;
48
+ const result = await createNote({
49
+ activityId,
50
+ text: noteText.trim(),
51
+ timestamp: effectiveTime !== undefined ? Math.floor(effectiveTime) : undefined,
52
+ });
53
+ if (result) {
54
+ setNoteText('');
55
+ refetch();
56
+ }
57
+ };
58
+
59
+ const handleSaveEdit = async (noteId: string) => {
60
+ if (!editingText.trim()) return;
61
+ const result = await updateNote(noteId, { text: editingText.trim() });
62
+ if (result) {
63
+ setEditingNoteId(null);
64
+ setEditingText('');
65
+ refetch();
66
+ }
67
+ };
68
+
69
+ const handleDeleteNote = async (noteId: string) => {
70
+ const success = await deleteNote(noteId);
71
+ if (success) refetch();
72
+ };
73
+
74
+ const handleTimestampClick = (timestamp: number) => {
75
+ onTimestampClick?.(timestamp);
76
+ window.scrollTo({ top: 0, behavior: 'smooth' });
77
+ };
78
+
79
+ return (
80
+ <div className="space-y-6">
81
+ {isLoading ? (
82
+ <div className="flex items-center justify-center py-8">
83
+ <div className="text-theme-text-secondary">Loading notes...</div>
84
+ </div>
85
+ ) : (
86
+ <>
87
+ {/* Note Input */}
88
+ <div className="space-y-3">
89
+ <textarea
90
+ placeholder="Write a note..."
91
+ value={noteText}
92
+ onChange={(e) => setNoteText(e.target.value)}
93
+ rows={notes.length === 0 ? 6 : 4}
94
+ className="w-full resize-none rounded-md border border-theme-border-primary bg-theme-bg-primary px-3 py-2 text-sm text-theme-text-primary placeholder:text-[rgb(var(--text-muted))] focus-visible:outline-none focus-visible:border-theme-accent-primary transition-colors"
95
+ />
96
+ <div className="flex justify-end">
97
+ <Button
98
+ onClick={handleSaveNote}
99
+ disabled={!noteText.trim() || isCreating}
100
+ className="bg-theme-accent-primary hover:bg-theme-accent-primary/90 text-white"
101
+ >
102
+ {isCreating ? 'Saving...' : 'Save note'}
103
+ </Button>
104
+ </div>
105
+ </div>
106
+
107
+ {notes.length === 0 ? (
108
+ <div className="space-y-2">
109
+ <h3 className="text-xl font-bold text-theme-text-primary">No notes yet</h3>
110
+ <p className="text-base text-theme-text-secondary">Add notes for this lesson to remember key points.</p>
111
+ </div>
112
+ ) : (
113
+ <div className="space-y-4">
114
+ <h3 className="text-lg font-bold text-theme-text-primary">Your notes for this lesson</h3>
115
+ <div className="space-y-3">
116
+ {notes.map((note: any) => (
117
+ <div key={note.id} className="border border-theme-border-primary bg-theme-bg-secondary rounded-lg p-3 md:p-4 space-y-2 relative w-full">
118
+ {editingNoteId !== note.id && (
119
+ <div className="absolute top-3 right-3 flex gap-2">
120
+ <Button variant="ghost" size="sm" onClick={() => { setEditingNoteId(note.id); setEditingText(note.text); }} disabled={isDeleting} className="text-theme-accent-primary hover:text-theme-accent-primary/80 p-1 h-auto">
121
+ <Pencil className="w-4 h-4" />
122
+ </Button>
123
+ <Button variant="ghost" size="sm" onClick={() => handleDeleteNote(note.id)} disabled={isDeleting} className="text-red-600 hover:text-red-800 p-1 h-auto">
124
+ <Trash2 className="w-4 h-4" />
125
+ </Button>
126
+ </div>
127
+ )}
128
+ {note.timestamp !== undefined && note.timestamp !== null && (
129
+ <button onClick={() => handleTimestampClick(note.timestamp)} className="text-theme-accent-primary font-mono font-semibold hover:underline text-sm" disabled={!onTimestampClick}>
130
+ {note.formattedTimestamp || formatTimestamp(note.timestamp)}
131
+ </button>
132
+ )}
133
+ {editingNoteId === note.id ? (
134
+ <div className="space-y-2">
135
+ <textarea value={editingText} onChange={(e) => setEditingText(e.target.value)} rows={3} className="w-full resize-none rounded-md border border-theme-border-primary bg-theme-bg-primary px-3 py-2 text-sm text-theme-text-primary focus-visible:outline-none focus-visible:border-theme-accent-primary transition-colors" />
136
+ <div className="flex gap-2 justify-end">
137
+ <Button variant="outline" onClick={() => { setEditingNoteId(null); setEditingText(''); }} disabled={isUpdating}>Cancel</Button>
138
+ <Button onClick={() => handleSaveEdit(note.id)} disabled={!editingText.trim() || isUpdating} className="bg-theme-accent-primary hover:bg-theme-accent-primary/90 text-white">{isUpdating ? 'Saving...' : 'Save'}</Button>
139
+ </div>
140
+ </div>
141
+ ) : (
142
+ <p className="text-theme-text-primary">{note.text}</p>
143
+ )}
144
+ </div>
145
+ ))}
146
+ </div>
147
+ </div>
148
+ )}
149
+ </>
150
+ )}
151
+ </div>
152
+ );
153
+ }
@@ -0,0 +1,10 @@
1
+ export { LearnerNavbar } from './LearnerNavbar';
2
+ export type { LearnerNavbarProps } from './LearnerNavbar';
3
+ export { LearnerSidebar } from './LearnerSidebar';
4
+ export type { LearnerSidebarProps, SidebarItem } from './LearnerSidebar';
5
+ export { CourseSidebar } from './CourseSidebar';
6
+ export type { CourseSidebarProps, CourseSidebarModule, CourseSidebarLesson } from './CourseSidebar';
7
+ export { LessonNotes } from './LessonNotes';
8
+ export type { LessonNotesProps } from './LessonNotes';
9
+ export { LessonBookmarks } from './LessonBookmarks';
10
+ export type { LessonBookmarksProps } from './LessonBookmarks';