@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.
- package/dist/academy-sdk-sdk-v1.0.0.zip +0 -0
- package/dist/bundle.js +70 -0
- package/dist/manifest.json +5 -0
- package/dist/styles.css +3307 -0
- package/package.json +41 -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/{dist/components/atoms/index.d.ts → src/components/atoms/index.ts} +0 -1
- package/{dist/components/index.d.ts → src/components/index.ts} +7 -1
- 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/{dist/components/molecules/index.d.ts → src/components/molecules/index.ts} +0 -1
- 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/{dist/components/organisms/index.d.ts → src/components/organisms/index.ts} +0 -1
- 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/{dist/components/pages/index.d.ts → src/components/pages/index.ts} +0 -1
- package/src/components/utils.ts +6 -0
- package/src/contracts/components.contract.ts +89 -0
- package/{dist/contracts/index.d.ts → src/contracts/index.ts} +0 -1
- 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/{dist/hooks/index.d.ts → src/hooks/index.ts} +15 -1
- package/src/hooks/sdk-context.tsx +152 -0
- package/src/hooks/useAiCoach.ts +27 -0
- package/src/hooks/useBookmarks.ts +35 -0
- package/{dist/hooks/useCourseSearch.d.ts → src/hooks/useCourseSearch.ts} +8 -5
- package/{dist/hooks/useDebounce.d.ts → src/hooks/useDebounce.ts} +8 -2
- package/{dist/hooks/useMyBundles.d.ts → src/hooks/useMyBundles.ts} +8 -6
- package/{dist/hooks/useMyCourses.d.ts → src/hooks/useMyCourses.ts} +8 -6
- package/src/hooks/useNotes.ts +35 -0
- package/src/hooks/useNotifications.ts +16 -0
- package/{dist/hooks/useTheme.d.ts → src/hooks/useTheme.ts} +8 -5
- package/src/hooks/useToast.ts +17 -0
- package/{dist/hooks/useUser.d.ts → src/hooks/useUser.ts} +13 -9
- 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/{dist/types/index.d.ts → src/types/index.ts} +0 -1
- 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/Avatar.d.ts +0 -9
- package/dist/components/atoms/Avatar.d.ts.map +0 -1
- package/dist/components/atoms/Badge.d.ts +0 -10
- package/dist/components/atoms/Badge.d.ts.map +0 -1
- package/dist/components/atoms/Button.d.ts +0 -11
- package/dist/components/atoms/Button.d.ts.map +0 -1
- package/dist/components/atoms/Card.d.ts +0 -11
- package/dist/components/atoms/Card.d.ts.map +0 -1
- package/dist/components/atoms/Input.d.ts +0 -7
- package/dist/components/atoms/Input.d.ts.map +0 -1
- package/dist/components/atoms/ProgressBar.d.ts +0 -11
- package/dist/components/atoms/ProgressBar.d.ts.map +0 -1
- package/dist/components/atoms/Tabs.d.ts +0 -16
- package/dist/components/atoms/Tabs.d.ts.map +0 -1
- package/dist/components/atoms/index.cjs +0 -318
- package/dist/components/atoms/index.d.ts.map +0 -1
- package/dist/components/atoms/index.js +0 -288
- package/dist/components/index.cjs +0 -1275
- package/dist/components/index.d.ts.map +0 -1
- package/dist/components/index.js +0 -1245
- package/dist/components/molecules/CourseCard.d.ts +0 -25
- package/dist/components/molecules/CourseCard.d.ts.map +0 -1
- package/dist/components/molecules/EmptyState.d.ts +0 -10
- package/dist/components/molecules/EmptyState.d.ts.map +0 -1
- package/dist/components/molecules/LoadingSpinner.d.ts +0 -7
- package/dist/components/molecules/LoadingSpinner.d.ts.map +0 -1
- package/dist/components/molecules/PageHeader.d.ts +0 -8
- package/dist/components/molecules/PageHeader.d.ts.map +0 -1
- package/dist/components/molecules/Pagination.d.ts +0 -13
- package/dist/components/molecules/Pagination.d.ts.map +0 -1
- package/dist/components/molecules/SearchInput.d.ts +0 -8
- package/dist/components/molecules/SearchInput.d.ts.map +0 -1
- package/dist/components/molecules/index.cjs +0 -334
- package/dist/components/molecules/index.d.ts.map +0 -1
- package/dist/components/molecules/index.js +0 -311
- package/dist/components/organisms/CourseSidebar.d.ts +0 -37
- package/dist/components/organisms/CourseSidebar.d.ts.map +0 -1
- package/dist/components/organisms/LearnerNavbar.d.ts +0 -8
- package/dist/components/organisms/LearnerNavbar.d.ts.map +0 -1
- package/dist/components/organisms/LearnerSidebar.d.ts +0 -16
- package/dist/components/organisms/LearnerSidebar.d.ts.map +0 -1
- package/dist/components/organisms/LessonBookmarks.d.ts +0 -8
- package/dist/components/organisms/LessonBookmarks.d.ts.map +0 -1
- package/dist/components/organisms/LessonNotes.d.ts +0 -8
- package/dist/components/organisms/LessonNotes.d.ts.map +0 -1
- package/dist/components/organisms/index.cjs +0 -855
- package/dist/components/organisms/index.d.ts.map +0 -1
- package/dist/components/organisms/index.js +0 -825
- package/dist/components/pages/BundleDetailPage.d.ts +0 -3
- package/dist/components/pages/BundleDetailPage.d.ts.map +0 -1
- package/dist/components/pages/CatalogBundlesPage.d.ts +0 -3
- package/dist/components/pages/CatalogBundlesPage.d.ts.map +0 -1
- package/dist/components/pages/CatalogCoursesPage.d.ts +0 -3
- package/dist/components/pages/CatalogCoursesPage.d.ts.map +0 -1
- package/dist/components/pages/CourseDetailPage.d.ts +0 -3
- package/dist/components/pages/CourseDetailPage.d.ts.map +0 -1
- package/dist/components/pages/CoursePlayerPage.d.ts +0 -8
- package/dist/components/pages/CoursePlayerPage.d.ts.map +0 -1
- package/dist/components/pages/CreatorProfilePage.d.ts +0 -3
- package/dist/components/pages/CreatorProfilePage.d.ts.map +0 -1
- package/dist/components/pages/LearnerSettingsPage.d.ts +0 -3
- package/dist/components/pages/LearnerSettingsPage.d.ts.map +0 -1
- package/dist/components/pages/ManualReviewDetailPage.d.ts +0 -3
- package/dist/components/pages/ManualReviewDetailPage.d.ts.map +0 -1
- package/dist/components/pages/ManualReviewPage.d.ts +0 -3
- package/dist/components/pages/ManualReviewPage.d.ts.map +0 -1
- package/dist/components/pages/MessagesPage.d.ts +0 -3
- package/dist/components/pages/MessagesPage.d.ts.map +0 -1
- package/dist/components/pages/MyLearningPage.d.ts +0 -3
- package/dist/components/pages/MyLearningPage.d.ts.map +0 -1
- package/dist/components/pages/PaymentCancelPage.d.ts +0 -3
- package/dist/components/pages/PaymentCancelPage.d.ts.map +0 -1
- package/dist/components/pages/PaymentSuccessPage.d.ts +0 -3
- package/dist/components/pages/PaymentSuccessPage.d.ts.map +0 -1
- package/dist/components/pages/index.cjs +0 -3306
- package/dist/components/pages/index.d.ts.map +0 -1
- package/dist/components/pages/index.js +0 -3315
- package/dist/components/utils.d.ts +0 -3
- package/dist/components/utils.d.ts.map +0 -1
- package/dist/contracts/components.contract.d.ts +0 -87
- package/dist/contracts/components.contract.d.ts.map +0 -1
- package/dist/contracts/index.cjs +0 -52
- package/dist/contracts/index.d.ts.map +0 -1
- package/dist/contracts/index.js +0 -29
- package/dist/contracts/layout.contract.d.ts +0 -35
- package/dist/contracts/layout.contract.d.ts.map +0 -1
- package/dist/contracts/pages.contract.d.ts +0 -192
- package/dist/contracts/pages.contract.d.ts.map +0 -1
- package/dist/contracts/template.contract.d.ts +0 -49
- package/dist/contracts/template.contract.d.ts.map +0 -1
- package/dist/hooks/index.cjs +0 -165
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -142
- package/dist/hooks/sdk-context.d.ts +0 -125
- package/dist/hooks/sdk-context.d.ts.map +0 -1
- package/dist/hooks/useAiCoach.d.ts +0 -32
- package/dist/hooks/useAiCoach.d.ts.map +0 -1
- package/dist/hooks/useBookmarks.d.ts +0 -31
- package/dist/hooks/useBookmarks.d.ts.map +0 -1
- package/dist/hooks/useCourseSearch.d.ts.map +0 -1
- package/dist/hooks/useDebounce.d.ts.map +0 -1
- package/dist/hooks/useMyBundles.d.ts.map +0 -1
- package/dist/hooks/useMyCourses.d.ts.map +0 -1
- package/dist/hooks/useNotes.d.ts +0 -31
- package/dist/hooks/useNotes.d.ts.map +0 -1
- package/dist/hooks/useNotifications.d.ts +0 -19
- package/dist/hooks/useNotifications.d.ts.map +0 -1
- package/dist/hooks/useTheme.d.ts.map +0 -1
- package/dist/hooks/useToast.d.ts +0 -17
- package/dist/hooks/useToast.d.ts.map +0 -1
- package/dist/hooks/useUser.d.ts.map +0 -1
- package/dist/index.cjs +0 -630
- package/dist/index.d.ts +0 -17
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -600
- package/dist/layouts/DefaultLayout.d.ts +0 -9
- package/dist/layouts/DefaultLayout.d.ts.map +0 -1
- package/dist/types/ai-coach.d.ts +0 -22
- package/dist/types/ai-coach.d.ts.map +0 -1
- package/dist/types/bookmarks.d.ts +0 -19
- package/dist/types/bookmarks.d.ts.map +0 -1
- package/dist/types/bundle.d.ts +0 -114
- package/dist/types/bundle.d.ts.map +0 -1
- package/dist/types/common.d.ts +0 -23
- package/dist/types/common.d.ts.map +0 -1
- package/dist/types/course.d.ts +0 -127
- package/dist/types/course.d.ts.map +0 -1
- package/dist/types/enrollment.d.ts +0 -34
- package/dist/types/enrollment.d.ts.map +0 -1
- package/dist/types/index.cjs +0 -18
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -0
- package/dist/types/lesson.d.ts +0 -105
- package/dist/types/lesson.d.ts.map +0 -1
- package/dist/types/manual-review.d.ts +0 -123
- package/dist/types/manual-review.d.ts.map +0 -1
- package/dist/types/messaging.d.ts +0 -101
- package/dist/types/messaging.d.ts.map +0 -1
- package/dist/types/notification.d.ts +0 -28
- package/dist/types/notification.d.ts.map +0 -1
- package/dist/types/payment.d.ts +0 -38
- package/dist/types/payment.d.ts.map +0 -1
- package/dist/types/progress.d.ts +0 -18
- package/dist/types/progress.d.ts.map +0 -1
- package/dist/types/rating.d.ts +0 -20
- package/dist/types/rating.d.ts.map +0 -1
- package/dist/types/search.d.ts +0 -28
- package/dist/types/search.d.ts.map +0 -1
- package/dist/types/user.d.ts +0 -15
- package/dist/types/user.d.ts.map +0 -1
- package/dist/utils/formatters.d.ts +0 -25
- package/dist/utils/formatters.d.ts.map +0 -1
- package/dist/utils/index.cjs +0 -80
- package/dist/utils/index.d.ts +0 -2
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -57
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
ChevronLeft, ChevronRight, FileText, StickyNote, Bookmark,
|
|
6
|
+
FileAudio, FolderOpen, HelpCircle, List, X, Clock3,
|
|
7
|
+
CheckCircle2, PlayCircle, Lock, Award, Search,
|
|
8
|
+
} from 'lucide-react';
|
|
9
|
+
import type { CoursePlayerPageProps } from '../../contracts/pages.contract';
|
|
10
|
+
import type { CoursePlayerCourse, CoursePlayerModule } from '../../types/course';
|
|
11
|
+
import { CourseSidebar } from '../organisms/CourseSidebar';
|
|
12
|
+
import { LoadingSpinner } from '../molecules/LoadingSpinner';
|
|
13
|
+
|
|
14
|
+
/* ─────────────────────────── helpers ─────────────────────────── */
|
|
15
|
+
|
|
16
|
+
function formatHeaderDuration(duration?: string): string {
|
|
17
|
+
if (!duration) return '';
|
|
18
|
+
const text = duration.trim();
|
|
19
|
+
if (!text) return '';
|
|
20
|
+
if (/[a-zA-Z]/.test(text)) return text;
|
|
21
|
+
const parts = text.split(':').map(Number);
|
|
22
|
+
if (parts.some(isNaN)) return text;
|
|
23
|
+
if (parts.length === 3) {
|
|
24
|
+
const [h, m] = parts;
|
|
25
|
+
if (h > 0 && m > 0) return `${h} ${h === 1 ? 'hour' : 'hours'} ${m} min`;
|
|
26
|
+
if (h > 0) return `${h} ${h === 1 ? 'hour' : 'hours'}`;
|
|
27
|
+
return `${m} min`;
|
|
28
|
+
}
|
|
29
|
+
if (parts.length === 2) {
|
|
30
|
+
const [m, s] = parts;
|
|
31
|
+
return `${m > 0 ? m : s > 0 ? 1 : 0} min`;
|
|
32
|
+
}
|
|
33
|
+
const s = parts[0];
|
|
34
|
+
return `${s > 0 ? Math.max(1, Math.round(s / 60)) : 0} min`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ─────────────────────────── component ─────────────────────────── */
|
|
38
|
+
|
|
39
|
+
export function CoursePlayerPage(props: Partial<CoursePlayerPageProps> & {
|
|
40
|
+
course?: CoursePlayerCourse | null;
|
|
41
|
+
onBack?: () => void;
|
|
42
|
+
onNavigateToLesson?: (lessonId: string) => void;
|
|
43
|
+
}) {
|
|
44
|
+
const {
|
|
45
|
+
course,
|
|
46
|
+
isLoading = false,
|
|
47
|
+
courseNotFound = false,
|
|
48
|
+
onBack,
|
|
49
|
+
onNavigateToLesson,
|
|
50
|
+
} = props;
|
|
51
|
+
|
|
52
|
+
/* internal state – no external router needed */
|
|
53
|
+
const allLessons = useMemo(
|
|
54
|
+
() => (course?.modules ?? []).flatMap((m) => m.lessons),
|
|
55
|
+
[course],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const [currentLessonIdx, setCurrentLessonIdx] = useState(0);
|
|
59
|
+
const [expandedModules, setExpandedModules] = useState<Set<string>>(
|
|
60
|
+
() => new Set(course?.modules?.[0]?.id ? [course.modules[0].id] : []),
|
|
61
|
+
);
|
|
62
|
+
const [activeTab, setActiveTab] = useState('overview');
|
|
63
|
+
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
|
64
|
+
|
|
65
|
+
const currentLesson = allLessons[currentLessonIdx] ?? null;
|
|
66
|
+
const completedCount = allLessons.filter((l) => l.completed).length;
|
|
67
|
+
const totalCount = allLessons.length;
|
|
68
|
+
|
|
69
|
+
/* ── navigation ── */
|
|
70
|
+
const goToLesson = (lessonId: string) => {
|
|
71
|
+
const idx = allLessons.findIndex((l) => l.id === lessonId);
|
|
72
|
+
if (idx === -1) return;
|
|
73
|
+
const lesson = allLessons[idx] as any;
|
|
74
|
+
if (lesson?.locked) return;
|
|
75
|
+
setCurrentLessonIdx(idx);
|
|
76
|
+
setIsMobileSidebarOpen(false);
|
|
77
|
+
onNavigateToLesson?.(lessonId);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const goPrev = () => {
|
|
81
|
+
if (currentLessonIdx > 0) {
|
|
82
|
+
setCurrentLessonIdx((i) => i - 1);
|
|
83
|
+
onNavigateToLesson?.(allLessons[currentLessonIdx - 1].id);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const goNext = () => {
|
|
88
|
+
if (currentLessonIdx < allLessons.length - 1) {
|
|
89
|
+
setCurrentLessonIdx((i) => i + 1);
|
|
90
|
+
onNavigateToLesson?.(allLessons[currentLessonIdx + 1].id);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const toggleModule = (id: string) => {
|
|
95
|
+
setExpandedModules((prev) => {
|
|
96
|
+
const s = new Set(prev);
|
|
97
|
+
s.has(id) ? s.delete(id) : s.add(id);
|
|
98
|
+
return s;
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/* ── early returns ── */
|
|
103
|
+
if (isLoading) return <LoadingSpinner className="py-20" text="Loading course..." />;
|
|
104
|
+
|
|
105
|
+
if (courseNotFound || !course) {
|
|
106
|
+
return (
|
|
107
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
108
|
+
<div className="text-center space-y-4">
|
|
109
|
+
<div className="text-6xl">📚</div>
|
|
110
|
+
<h1 className="text-3xl font-bold text-theme-text-primary">Course Not Found</h1>
|
|
111
|
+
<p className="text-theme-text-secondary max-w-md mx-auto">
|
|
112
|
+
The course you're looking for doesn't exist or has been removed.
|
|
113
|
+
</p>
|
|
114
|
+
<button
|
|
115
|
+
onClick={onBack ?? (() => window.history.back())}
|
|
116
|
+
className="mt-4 px-6 py-2.5 rounded-lg bg-theme-accent-primary text-white font-medium hover:opacity-90 transition-opacity cursor-pointer"
|
|
117
|
+
>
|
|
118
|
+
Go Back
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ── sidebar props ── */
|
|
126
|
+
const sidebarProps = {
|
|
127
|
+
courseTitle: course.title,
|
|
128
|
+
modules: course.modules as any[],
|
|
129
|
+
currentLessonId: currentLesson?.id ?? '',
|
|
130
|
+
expandedModules,
|
|
131
|
+
completedCount,
|
|
132
|
+
totalCount,
|
|
133
|
+
onToggleModule: toggleModule,
|
|
134
|
+
onSelectLesson: goToLesson,
|
|
135
|
+
onBack: onBack ?? (() => window.history.back()),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/* ── tabs ── */
|
|
139
|
+
const tabs = [
|
|
140
|
+
{ id: 'overview', label: 'Overview', icon: <FileText className="w-4 h-4" /> },
|
|
141
|
+
{ id: 'notes', label: 'Notes', icon: <StickyNote className="w-4 h-4" /> },
|
|
142
|
+
{ id: 'bookmarks', label: 'Bookmarks', icon: <Bookmark className="w-4 h-4" /> },
|
|
143
|
+
{ id: 'transcript', label: 'Transcript', icon: <FileAudio className="w-4 h-4" /> },
|
|
144
|
+
{ id: 'resources', label: 'Resources', icon: <FolderOpen className="w-4 h-4" /> },
|
|
145
|
+
{ id: 'faq', label: 'FAQ', icon: <HelpCircle className="w-4 h-4" /> },
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
/* ── video URL ── */
|
|
149
|
+
const videoUrl = (currentLesson as any)?.videoUrl ?? '';
|
|
150
|
+
const videoProvider = (currentLesson as any)?.videoProvider ?? 'YOUTUBE';
|
|
151
|
+
|
|
152
|
+
const getEmbedSrc = () => {
|
|
153
|
+
if (!videoUrl) return null;
|
|
154
|
+
if (videoProvider === 'YOUTUBE') return `https://www.youtube.com/embed/${videoUrl}?rel=0`;
|
|
155
|
+
if (videoProvider === 'VIMEO') return `https://player.vimeo.com/video/${videoUrl}`;
|
|
156
|
+
if (videoProvider === 'LOOM') return `https://www.loom.com/embed/${videoUrl}`;
|
|
157
|
+
return videoUrl;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const embedSrc = getEmbedSrc();
|
|
161
|
+
|
|
162
|
+
/* ── lesson metadata ── */
|
|
163
|
+
const lessonLocked = (currentLesson as any)?.locked ?? false;
|
|
164
|
+
const lessonType = (currentLesson as any)?.type;
|
|
165
|
+
const duration = formatHeaderDuration((currentLesson as any)?.duration);
|
|
166
|
+
const summary = (currentLesson as any)?.summary ?? course.description ?? '';
|
|
167
|
+
const learningObjectives: string[] = course.learningObjectives ?? [];
|
|
168
|
+
const faqs: { question: string; answer: string }[] = (currentLesson as any)?.settings?.faqs ?? [];
|
|
169
|
+
const secondaryResources: any[] = (currentLesson as any)?.secondaryResources ?? [];
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div className="min-h-screen bg-[#f7f9fc] flex lg:flex-row lg:-mx-6">
|
|
173
|
+
{/* ── Desktop Sidebar ── */}
|
|
174
|
+
<div className="hidden lg:block">
|
|
175
|
+
<CourseSidebar {...sidebarProps} />
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{/* ── Mobile Sidebar Drawer ── */}
|
|
179
|
+
{isMobileSidebarOpen && (
|
|
180
|
+
<div className="lg:hidden fixed inset-0 z-50">
|
|
181
|
+
<button
|
|
182
|
+
onClick={() => setIsMobileSidebarOpen(false)}
|
|
183
|
+
className="absolute inset-0 bg-black/40 cursor-pointer"
|
|
184
|
+
aria-label="Close lessons drawer"
|
|
185
|
+
/>
|
|
186
|
+
<div className="relative h-full w-[88vw] max-w-[360px] bg-white shadow-xl overflow-y-auto">
|
|
187
|
+
<button
|
|
188
|
+
onClick={() => setIsMobileSidebarOpen(false)}
|
|
189
|
+
className="absolute top-3 right-3 z-10 w-9 h-9 rounded-full bg-white/90 border border-gray-200 text-gray-700 flex items-center justify-center cursor-pointer"
|
|
190
|
+
aria-label="Close"
|
|
191
|
+
>
|
|
192
|
+
<X className="w-4 h-4" />
|
|
193
|
+
</button>
|
|
194
|
+
<CourseSidebar {...sidebarProps} onSelectLesson={(id) => { goToLesson(id); setIsMobileSidebarOpen(false); }} />
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{/* ── Main Area ── */}
|
|
200
|
+
<div className="flex-1 flex flex-col min-w-0 bg-[#dce8f5]">
|
|
201
|
+
{/* Mobile top bar */}
|
|
202
|
+
<div className="lg:hidden sticky top-0 z-20 bg-white/95 backdrop-blur border-b border-gray-200 px-4 py-3 flex items-center justify-between">
|
|
203
|
+
<button
|
|
204
|
+
onClick={() => setIsMobileSidebarOpen(true)}
|
|
205
|
+
className="inline-flex items-center gap-2 px-3 py-2 rounded-md bg-theme-accent-primary text-white text-sm font-medium cursor-pointer"
|
|
206
|
+
>
|
|
207
|
+
<List className="w-4 h-4" />
|
|
208
|
+
Lessons
|
|
209
|
+
</button>
|
|
210
|
+
<span className="text-xs text-gray-600 font-medium">
|
|
211
|
+
Lesson {currentLessonIdx + 1} / {allLessons.length}
|
|
212
|
+
</span>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* ── Locked lesson screen ── */}
|
|
216
|
+
{lessonLocked ? (
|
|
217
|
+
<div className="flex items-center justify-center min-h-[70vh] p-4">
|
|
218
|
+
<div className="max-w-lg w-full bg-white rounded-2xl border border-gray-200 p-12 text-center space-y-6">
|
|
219
|
+
<div className="flex justify-center">
|
|
220
|
+
<Lock className="w-16 h-16 text-orange-500" />
|
|
221
|
+
</div>
|
|
222
|
+
<h1 className="text-2xl font-bold text-theme-text-primary">Lesson Locked</h1>
|
|
223
|
+
<p className="text-theme-text-secondary">
|
|
224
|
+
Complete the previous lesson to unlock this content.
|
|
225
|
+
</p>
|
|
226
|
+
<p className="text-lg font-semibold text-theme-text-primary">{currentLesson?.title}</p>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
) : (
|
|
230
|
+
<>
|
|
231
|
+
{/* ── Teal lesson title banner ── */}
|
|
232
|
+
<div className="bg-theme-accent-primary px-4 sm:px-6 lg:px-8 py-4 sm:py-6 text-white">
|
|
233
|
+
<div className="flex items-start">
|
|
234
|
+
<div className="flex-1 min-w-0">
|
|
235
|
+
<h2 className="text-2xl lg:text-3xl font-bold">
|
|
236
|
+
{currentLesson?.title
|
|
237
|
+
? `${currentLesson.title.charAt(0).toUpperCase()}${currentLesson.title.slice(1)}`
|
|
238
|
+
: 'Select a lesson'}
|
|
239
|
+
</h2>
|
|
240
|
+
<div className="mt-1 flex items-center gap-3">
|
|
241
|
+
<p className="text-white/90 text-sm lg:text-base opacity-90 line-clamp-1 flex-1 min-w-0">
|
|
242
|
+
{summary}
|
|
243
|
+
</p>
|
|
244
|
+
{duration && (
|
|
245
|
+
<span className="inline-flex items-center gap-1.5 whitespace-nowrap text-white/90 text-sm lg:text-base shrink-0">
|
|
246
|
+
<Clock3 className="w-4 h-4" />
|
|
247
|
+
{duration}
|
|
248
|
+
</span>
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<div className="p-4 lg:p-8 space-y-6 lg:space-y-8 max-w-5xl w-full">
|
|
256
|
+
{/* ── Video / Assignment card ── */}
|
|
257
|
+
<div className="bg-white rounded-2xl shadow-sm">
|
|
258
|
+
{/* Video area */}
|
|
259
|
+
<div className="relative group">
|
|
260
|
+
<div className="overflow-hidden rounded-t-2xl">
|
|
261
|
+
{embedSrc ? (
|
|
262
|
+
<div className="relative w-full" style={{ paddingBottom: '56.25%' }}>
|
|
263
|
+
<iframe
|
|
264
|
+
src={embedSrc}
|
|
265
|
+
className="absolute inset-0 w-full h-full"
|
|
266
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
267
|
+
allowFullScreen
|
|
268
|
+
title={currentLesson?.title ?? 'Lesson video'}
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
) : (
|
|
272
|
+
<div className="relative w-full bg-gray-900 flex items-center justify-center" style={{ paddingBottom: '56.25%' }}>
|
|
273
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 text-white/60">
|
|
274
|
+
<PlayCircle className="w-16 h-16 opacity-40" />
|
|
275
|
+
<p className="text-sm">No video available for this lesson</p>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{/* Prev / Next hover buttons */}
|
|
282
|
+
<button
|
|
283
|
+
onClick={goPrev}
|
|
284
|
+
disabled={currentLessonIdx === 0}
|
|
285
|
+
className="hidden md:flex absolute -left-5 top-1/2 -translate-y-1/2 z-10 w-10 h-10 items-center justify-center rounded-full bg-[#F48C06] text-white opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed"
|
|
286
|
+
title="Previous lesson"
|
|
287
|
+
>
|
|
288
|
+
<ChevronLeft className="w-5 h-5" />
|
|
289
|
+
</button>
|
|
290
|
+
<button
|
|
291
|
+
onClick={goNext}
|
|
292
|
+
disabled={currentLessonIdx >= allLessons.length - 1}
|
|
293
|
+
className="hidden md:flex absolute -right-5 top-1/2 -translate-y-1/2 z-10 w-10 h-10 items-center justify-center rounded-full bg-[#F48C06] text-white opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed"
|
|
294
|
+
title="Next lesson"
|
|
295
|
+
>
|
|
296
|
+
<ChevronRight className="w-5 h-5" />
|
|
297
|
+
</button>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
{/* Video controls bar */}
|
|
301
|
+
<div className="px-4 sm:px-6 py-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 border-t border-gray-100">
|
|
302
|
+
<div className="flex items-center gap-3">
|
|
303
|
+
{(currentLesson as any)?.completed && (
|
|
304
|
+
<span className="inline-flex items-center gap-1 text-xs text-green-600 font-medium">
|
|
305
|
+
<CheckCircle2 className="w-4 h-4" /> Completed
|
|
306
|
+
</span>
|
|
307
|
+
)}
|
|
308
|
+
<span className="text-sm text-gray-500">
|
|
309
|
+
Lesson {currentLessonIdx + 1} of {allLessons.length}
|
|
310
|
+
</span>
|
|
311
|
+
</div>
|
|
312
|
+
<div className="flex items-center gap-2">
|
|
313
|
+
<button
|
|
314
|
+
onClick={goPrev}
|
|
315
|
+
disabled={currentLessonIdx === 0}
|
|
316
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-gray-200 text-sm text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
|
317
|
+
>
|
|
318
|
+
<ChevronLeft className="w-4 h-4" /> Prev
|
|
319
|
+
</button>
|
|
320
|
+
<button
|
|
321
|
+
onClick={goNext}
|
|
322
|
+
disabled={currentLessonIdx >= allLessons.length - 1}
|
|
323
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-theme-accent-primary text-white text-sm hover:opacity-90 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
|
324
|
+
>
|
|
325
|
+
Next <ChevronRight className="w-4 h-4" />
|
|
326
|
+
</button>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
{/* ── Tabs ── */}
|
|
332
|
+
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
|
333
|
+
{/* Tab bar */}
|
|
334
|
+
<div className="flex overflow-x-auto border-b border-gray-200 scrollbar-hide">
|
|
335
|
+
{tabs.map((tab) => (
|
|
336
|
+
<button
|
|
337
|
+
key={tab.id}
|
|
338
|
+
onClick={() => setActiveTab(tab.id)}
|
|
339
|
+
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 transition-colors cursor-pointer ${
|
|
340
|
+
activeTab === tab.id
|
|
341
|
+
? 'border-theme-accent-primary text-theme-accent-primary'
|
|
342
|
+
: 'border-transparent text-gray-500 hover:text-gray-700'
|
|
343
|
+
}`}
|
|
344
|
+
>
|
|
345
|
+
{tab.icon}
|
|
346
|
+
{tab.label}
|
|
347
|
+
</button>
|
|
348
|
+
))}
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
{/* Tab content */}
|
|
352
|
+
<div className="px-6 py-6" style={{ minHeight: 480 }}>
|
|
353
|
+
{/* Overview */}
|
|
354
|
+
{activeTab === 'overview' && (
|
|
355
|
+
<div className="space-y-6">
|
|
356
|
+
<div>
|
|
357
|
+
<h3 className="text-base font-semibold text-gray-900 mb-2">About This Lesson</h3>
|
|
358
|
+
{summary ? (
|
|
359
|
+
<p className="text-sm text-gray-700 leading-relaxed">{summary}</p>
|
|
360
|
+
) : (
|
|
361
|
+
<p className="text-sm text-gray-400 italic">No description available.</p>
|
|
362
|
+
)}
|
|
363
|
+
</div>
|
|
364
|
+
{learningObjectives.length > 0 && (
|
|
365
|
+
<div>
|
|
366
|
+
<h3 className="text-base font-semibold text-gray-900 mb-3">What You'll Learn</h3>
|
|
367
|
+
<ul className="space-y-2">
|
|
368
|
+
{learningObjectives.map((obj, i) => (
|
|
369
|
+
<li key={i} className="flex items-start gap-2.5 text-sm text-gray-700">
|
|
370
|
+
<CheckCircle2 className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
|
|
371
|
+
{obj}
|
|
372
|
+
</li>
|
|
373
|
+
))}
|
|
374
|
+
</ul>
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
{/* Notes */}
|
|
381
|
+
{activeTab === 'notes' && (
|
|
382
|
+
<div className="flex flex-col items-center justify-center py-12 text-center gap-3">
|
|
383
|
+
<StickyNote className="w-12 h-12 text-gray-300" />
|
|
384
|
+
<p className="text-sm font-medium text-gray-500">Your notes will appear here</p>
|
|
385
|
+
<p className="text-xs text-gray-400">Take notes while watching to revisit key points later.</p>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
|
|
389
|
+
{/* Bookmarks */}
|
|
390
|
+
{activeTab === 'bookmarks' && (
|
|
391
|
+
<div className="flex flex-col items-center justify-center py-12 text-center gap-3">
|
|
392
|
+
<Bookmark className="w-12 h-12 text-gray-300" />
|
|
393
|
+
<p className="text-sm font-medium text-gray-500">No bookmarks yet</p>
|
|
394
|
+
<p className="text-xs text-gray-400">Bookmark key moments in the video to revisit them.</p>
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
397
|
+
|
|
398
|
+
{/* Transcript */}
|
|
399
|
+
{activeTab === 'transcript' && (
|
|
400
|
+
<div className="flex flex-col items-center justify-center py-12 text-center gap-3">
|
|
401
|
+
<FileAudio className="w-12 h-12 text-gray-300" />
|
|
402
|
+
<p className="text-sm font-medium text-gray-500">Transcript not available</p>
|
|
403
|
+
<p className="text-xs text-gray-400">The transcript for this lesson has not been generated yet.</p>
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
{/* Resources */}
|
|
408
|
+
{activeTab === 'resources' && (
|
|
409
|
+
<div>
|
|
410
|
+
{secondaryResources.length > 0 ? (
|
|
411
|
+
<div className="space-y-3">
|
|
412
|
+
{secondaryResources.map((res: any, i: number) => (
|
|
413
|
+
<a
|
|
414
|
+
key={i}
|
|
415
|
+
href={res.url ?? res.metadataJson?.url ?? '#'}
|
|
416
|
+
target="_blank"
|
|
417
|
+
rel="noopener noreferrer"
|
|
418
|
+
className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:border-theme-accent-primary hover:bg-theme-accent-primary/5 transition-colors"
|
|
419
|
+
>
|
|
420
|
+
<FolderOpen className="w-5 h-5 text-theme-accent-primary flex-shrink-0" />
|
|
421
|
+
<span className="text-sm text-gray-700">{res.title ?? res.name ?? `Resource ${i + 1}`}</span>
|
|
422
|
+
</a>
|
|
423
|
+
))}
|
|
424
|
+
</div>
|
|
425
|
+
) : (
|
|
426
|
+
<div className="flex flex-col items-center justify-center py-12 text-center gap-3">
|
|
427
|
+
<FolderOpen className="w-12 h-12 text-gray-300" />
|
|
428
|
+
<p className="text-sm font-medium text-gray-500">No resources available</p>
|
|
429
|
+
<p className="text-xs text-gray-400">Downloadable files and links will appear here.</p>
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
)}
|
|
434
|
+
|
|
435
|
+
{/* FAQ */}
|
|
436
|
+
{activeTab === 'faq' && (
|
|
437
|
+
<div>
|
|
438
|
+
{faqs.length > 0 ? (
|
|
439
|
+
<FAQAccordion faqs={faqs} />
|
|
440
|
+
) : (
|
|
441
|
+
<div className="flex flex-col items-center justify-center py-12 text-center gap-3">
|
|
442
|
+
<HelpCircle className="w-12 h-12 text-gray-300" />
|
|
443
|
+
<p className="text-sm font-medium text-gray-500">No FAQs for this lesson</p>
|
|
444
|
+
</div>
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
</>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* ── FAQ sub-component ── */
|
|
459
|
+
function FAQAccordion({ faqs }: { faqs: { question: string; answer: string }[] }) {
|
|
460
|
+
const [openIdx, setOpenIdx] = useState<number | null>(null);
|
|
461
|
+
return (
|
|
462
|
+
<div className="space-y-2">
|
|
463
|
+
{faqs.map((faq, i) => (
|
|
464
|
+
<div key={i} className="border border-gray-200 rounded-lg overflow-hidden">
|
|
465
|
+
<button
|
|
466
|
+
onClick={() => setOpenIdx(openIdx === i ? null : i)}
|
|
467
|
+
className="w-full flex items-center justify-between px-4 py-3 text-left text-sm font-medium text-gray-800 hover:bg-gray-50 transition-colors cursor-pointer"
|
|
468
|
+
>
|
|
469
|
+
{faq.question}
|
|
470
|
+
<ChevronLeft className={`w-4 h-4 text-gray-400 transition-transform duration-200 ${openIdx === i ? '-rotate-90' : 'rotate-180'}`} />
|
|
471
|
+
</button>
|
|
472
|
+
{openIdx === i && (
|
|
473
|
+
<div className="px-4 pb-4 text-sm text-gray-600 border-t border-gray-100 pt-3">
|
|
474
|
+
{faq.answer}
|
|
475
|
+
</div>
|
|
476
|
+
)}
|
|
477
|
+
</div>
|
|
478
|
+
))}
|
|
479
|
+
</div>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Mail, BookOpen, Award } from 'lucide-react';
|
|
2
|
+
import type { CreatorProfilePageProps } from '../../contracts/pages.contract';
|
|
3
|
+
import { CourseCard } from '../molecules/CourseCard';
|
|
4
|
+
import { LoadingSpinner } from '../molecules/LoadingSpinner';
|
|
5
|
+
import { EmptyState } from '../molecules/EmptyState';
|
|
6
|
+
import { User } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
export function CreatorProfilePage(props: Partial<CreatorProfilePageProps>) {
|
|
9
|
+
const noop = (..._args: any[]) => {};
|
|
10
|
+
const { creator, courses = [], isLoading = false, onCourseClick = noop } = props;
|
|
11
|
+
|
|
12
|
+
if (isLoading) return <LoadingSpinner className="py-20" />;
|
|
13
|
+
if (!creator) return <EmptyState icon={<User className="w-16 h-16" />} title="Creator not found" />;
|
|
14
|
+
|
|
15
|
+
const name = creator.name || 'Course Creator';
|
|
16
|
+
const email = creator.email || '';
|
|
17
|
+
const bio = creator.bio || 'An experienced educator and software developer with expertise in modern web development and software engineering. With years of industry experience, they create comprehensive courses that help students master cutting-edge technologies and build real-world applications.';
|
|
18
|
+
const expertise: string[] = creator.expertise || [];
|
|
19
|
+
const avatarInitial = name.charAt(0).toUpperCase();
|
|
20
|
+
const totalCourses = creator.totalCourses ?? courses.length;
|
|
21
|
+
const totalLessons = creator.totalLessons;
|
|
22
|
+
const totalStudents = creator.totalStudents;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="min-h-screen">
|
|
26
|
+
<div>
|
|
27
|
+
{/* Hero Header */}
|
|
28
|
+
<div className="mb-12">
|
|
29
|
+
<div className="flex flex-col md:flex-row items-center gap-10">
|
|
30
|
+
{/* Left: Info */}
|
|
31
|
+
<div className="flex-1 text-gray-900 text-center md:text-left space-y-4">
|
|
32
|
+
<div className="inline-flex items-center gap-2 bg-indigo-100 px-4 py-2 rounded-full text-sm font-medium text-indigo-700">
|
|
33
|
+
<Award className="h-4 w-4" />
|
|
34
|
+
Professional Creator
|
|
35
|
+
</div>
|
|
36
|
+
<h1 className="text-5xl md:text-6xl font-bold tracking-tight">
|
|
37
|
+
{name}
|
|
38
|
+
</h1>
|
|
39
|
+
{email && (
|
|
40
|
+
<div className="flex items-center gap-2 text-gray-600 justify-center md:justify-start">
|
|
41
|
+
<Mail className="h-4 w-4" />
|
|
42
|
+
<span className="text-lg">{email}</span>
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
<div className="flex items-center gap-6 pt-4 justify-center md:justify-start">
|
|
46
|
+
<div className="text-center">
|
|
47
|
+
<div className="text-3xl font-bold text-gray-900">{totalCourses}</div>
|
|
48
|
+
<div className="text-sm text-gray-600">Courses</div>
|
|
49
|
+
</div>
|
|
50
|
+
{totalLessons != null && (
|
|
51
|
+
<>
|
|
52
|
+
<div className="w-px h-12 bg-gray-300"></div>
|
|
53
|
+
<div className="text-center">
|
|
54
|
+
<div className="text-3xl font-bold text-gray-900">{totalLessons}</div>
|
|
55
|
+
<div className="text-sm text-gray-600">Lessons</div>
|
|
56
|
+
</div>
|
|
57
|
+
</>
|
|
58
|
+
)}
|
|
59
|
+
{totalStudents != null && (
|
|
60
|
+
<>
|
|
61
|
+
<div className="w-px h-12 bg-gray-300"></div>
|
|
62
|
+
<div className="text-center">
|
|
63
|
+
<div className="text-3xl font-bold text-gray-900">{totalStudents}</div>
|
|
64
|
+
<div className="text-sm text-gray-600">Students</div>
|
|
65
|
+
</div>
|
|
66
|
+
</>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Right: Avatar */}
|
|
72
|
+
<div className="flex-shrink-0">
|
|
73
|
+
<div className="relative">
|
|
74
|
+
{creator.profileImage ? (
|
|
75
|
+
<img
|
|
76
|
+
src={creator.profileImage}
|
|
77
|
+
alt={name}
|
|
78
|
+
className="w-48 h-48 rounded-full object-cover shadow-2xl"
|
|
79
|
+
/>
|
|
80
|
+
) : (
|
|
81
|
+
<div className="w-48 h-48 rounded-full bg-gradient-to-br from-indigo-600 to-purple-600 flex items-center justify-center text-white text-7xl font-bold shadow-2xl">
|
|
82
|
+
{avatarInitial}
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
<div className="absolute -bottom-2 -right-2 bg-green-500 h-12 w-12 rounded-full border-4 border-white shadow-lg"></div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* About Section */}
|
|
92
|
+
<div className="mb-12">
|
|
93
|
+
<div className="flex items-center gap-3 mb-6">
|
|
94
|
+
<div className="h-1 w-12 bg-gradient-to-r from-indigo-600 to-purple-600 rounded-full"></div>
|
|
95
|
+
<h2 className="text-3xl font-bold text-gray-900">About</h2>
|
|
96
|
+
</div>
|
|
97
|
+
<p className="text-gray-700 leading-relaxed text-lg">{bio}</p>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Expertise Section */}
|
|
101
|
+
{expertise.length > 0 && (
|
|
102
|
+
<div className="mb-12">
|
|
103
|
+
<div className="flex items-center gap-3 mb-6">
|
|
104
|
+
<div className="h-1 w-12 bg-gradient-to-r from-indigo-600 to-purple-600 rounded-full"></div>
|
|
105
|
+
<h2 className="text-3xl font-bold text-gray-900">Expertise</h2>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="flex flex-wrap gap-3">
|
|
108
|
+
{expertise.map((skill: string) => (
|
|
109
|
+
<span
|
|
110
|
+
key={skill}
|
|
111
|
+
className="px-4 py-2 bg-indigo-50 text-indigo-700 rounded-full text-sm font-medium"
|
|
112
|
+
>
|
|
113
|
+
{skill}
|
|
114
|
+
</span>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{/* Courses Section */}
|
|
121
|
+
<div>
|
|
122
|
+
<div className="flex items-center justify-between mb-8">
|
|
123
|
+
<div className="flex items-center gap-3">
|
|
124
|
+
<div className="h-1 w-12 bg-gradient-to-r from-indigo-600 to-purple-600 rounded-full"></div>
|
|
125
|
+
<h2 className="text-3xl font-bold text-gray-900">Featured Courses</h2>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="flex items-center gap-2 bg-indigo-50 px-4 py-2 rounded-full">
|
|
128
|
+
<BookOpen className="h-5 w-5 text-indigo-600" />
|
|
129
|
+
<span className="font-semibold text-indigo-600">{courses.length} Courses</span>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{courses.length === 0 ? (
|
|
134
|
+
<EmptyState title="No courses yet" />
|
|
135
|
+
) : (
|
|
136
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
137
|
+
{courses.map((course: any) => (
|
|
138
|
+
<CourseCard
|
|
139
|
+
key={course.id}
|
|
140
|
+
id={course.id}
|
|
141
|
+
title={course.title || ''}
|
|
142
|
+
thumbnail={course.thumbnail}
|
|
143
|
+
totalLessons={course.totalLessons || 0}
|
|
144
|
+
progress={0}
|
|
145
|
+
isFree={course.isFree ?? false}
|
|
146
|
+
price={course.price}
|
|
147
|
+
showProgress={false}
|
|
148
|
+
showPrice={true}
|
|
149
|
+
description={course.description || course.summary?.replace(/<[^>]*>/g, '') || ''}
|
|
150
|
+
instructorName={creator.name}
|
|
151
|
+
category={course.category}
|
|
152
|
+
onClick={() => onCourseClick(course.id)}
|
|
153
|
+
/>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|