@academy-sdk/sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle.js +70 -0
- package/dist/manifest.json +5 -0
- package/dist/styles.css +3307 -0
- package/package.json +40 -46
- package/src/components/atoms/Avatar.tsx +38 -0
- package/src/components/atoms/Badge.tsx +32 -0
- package/src/components/atoms/Button.tsx +48 -0
- package/src/components/atoms/Card.tsx +33 -0
- package/src/components/atoms/Input.tsx +39 -0
- package/src/components/atoms/ProgressBar.tsx +52 -0
- package/src/components/atoms/Tabs.tsx +47 -0
- package/src/components/atoms/index.ts +10 -0
- package/src/components/index.ts +11 -0
- package/src/components/molecules/CourseCard.tsx +215 -0
- package/src/components/molecules/EmptyState.tsx +23 -0
- package/src/components/molecules/LoadingSpinner.tsx +27 -0
- package/src/components/molecules/PageHeader.tsx +22 -0
- package/src/components/molecules/Pagination.tsx +82 -0
- package/src/components/molecules/SearchInput.tsx +35 -0
- package/src/components/molecules/index.ts +12 -0
- package/src/components/organisms/CourseSidebar.tsx +276 -0
- package/src/components/organisms/LearnerNavbar.tsx +129 -0
- package/src/components/organisms/LearnerSidebar.tsx +148 -0
- package/src/components/organisms/LessonBookmarks.tsx +128 -0
- package/src/components/organisms/LessonNotes.tsx +153 -0
- package/src/components/organisms/index.ts +10 -0
- package/src/components/pages/BundleDetailPage.tsx +388 -0
- package/src/components/pages/CatalogBundlesPage.tsx +96 -0
- package/src/components/pages/CatalogCoursesPage.tsx +299 -0
- package/src/components/pages/CourseDetailPage.tsx +582 -0
- package/src/components/pages/CoursePlayerPage.tsx +481 -0
- package/src/components/pages/CreatorProfilePage.tsx +161 -0
- package/src/components/pages/LearnerSettingsPage.tsx +58 -0
- package/src/components/pages/ManualReviewDetailPage.tsx +254 -0
- package/src/components/pages/ManualReviewPage.tsx +228 -0
- package/src/components/pages/MessagesPage.tsx +285 -0
- package/src/components/pages/MyLearningPage.tsx +239 -0
- package/src/components/pages/PaymentCancelPage.tsx +74 -0
- package/src/components/pages/PaymentSuccessPage.tsx +73 -0
- package/src/components/pages/index.ts +13 -0
- package/src/components/utils.ts +6 -0
- package/src/contracts/components.contract.ts +89 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/layout.contract.ts +36 -0
- package/src/contracts/pages.contract.ts +275 -0
- package/src/contracts/template.contract.ts +100 -0
- package/src/default-template.tsx +52 -0
- package/src/hooks/index.ts +28 -0
- package/src/hooks/sdk-context.tsx +152 -0
- package/src/hooks/useAiCoach.ts +27 -0
- package/src/hooks/useBookmarks.ts +35 -0
- package/src/hooks/useCourseSearch.ts +18 -0
- package/src/hooks/useDebounce.ts +19 -0
- package/src/hooks/useMyBundles.ts +21 -0
- package/src/hooks/useMyCourses.ts +20 -0
- package/src/hooks/useNotes.ts +35 -0
- package/src/hooks/useNotifications.ts +16 -0
- package/src/hooks/useTheme.ts +17 -0
- package/src/hooks/useToast.ts +17 -0
- package/src/hooks/useUser.ts +33 -0
- package/src/index.ts +33 -0
- package/src/layouts/DefaultLayout.tsx +58 -0
- package/src/manifest.json +5 -0
- package/src/styles.css +43 -0
- package/src/types/ai-coach.ts +25 -0
- package/src/types/bookmarks.ts +20 -0
- package/src/types/bundle.ts +119 -0
- package/src/types/common.ts +24 -0
- package/src/types/course.ts +135 -0
- package/src/types/enrollment.ts +35 -0
- package/src/types/index.ts +15 -0
- package/src/types/lesson.ts +106 -0
- package/src/types/manual-review.ts +116 -0
- package/src/types/messaging.ts +109 -0
- package/src/types/notification.ts +30 -0
- package/src/types/payment.ts +40 -0
- package/src/types/progress.ts +19 -0
- package/src/types/rating.ts +20 -0
- package/src/types/search.ts +31 -0
- package/src/types/user.ts +16 -0
- package/src/utils/formatters.ts +74 -0
- package/src/utils/index.ts +8 -0
- package/dist/components/atoms/index.cjs +0 -318
- package/dist/components/atoms/index.js +0 -288
- package/dist/components/index.cjs +0 -1275
- package/dist/components/index.js +0 -1245
- package/dist/components/molecules/index.cjs +0 -334
- package/dist/components/molecules/index.js +0 -311
- package/dist/components/organisms/index.cjs +0 -855
- package/dist/components/organisms/index.js +0 -825
- package/dist/components/pages/index.cjs +0 -3306
- package/dist/components/pages/index.js +0 -3315
- package/dist/contracts/index.cjs +0 -52
- package/dist/contracts/index.js +0 -29
- package/dist/hooks/index.cjs +0 -165
- package/dist/hooks/index.js +0 -142
- package/dist/index.cjs +0 -630
- package/dist/index.js +0 -600
- package/dist/types/index.cjs +0 -18
- package/dist/types/index.js +0 -0
- package/dist/utils/index.cjs +0 -80
- package/dist/utils/index.js +0 -57
|
@@ -0,0 +1,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 “Add Bookmark” 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';
|