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