@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,285 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { MessageSquare, Send, ArrowLeft, Plus, ChevronUp, Loader2, Search } from 'lucide-react';
|
|
5
|
+
import type { MessagesPageProps } from '../../contracts/pages.contract';
|
|
6
|
+
import { cn } from '../utils';
|
|
7
|
+
|
|
8
|
+
function formatTime(timestamp: string) {
|
|
9
|
+
try {
|
|
10
|
+
const date = new Date(timestamp);
|
|
11
|
+
const now = new Date();
|
|
12
|
+
const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
13
|
+
if (diffDays === 0) return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
|
14
|
+
if (diffDays === 1) return 'Yesterday';
|
|
15
|
+
if (diffDays < 7) return date.toLocaleDateString('en-US', { weekday: 'short' });
|
|
16
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
17
|
+
} catch { return ''; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function timeAgo(timestamp: string) {
|
|
21
|
+
try {
|
|
22
|
+
const diff = Date.now() - new Date(timestamp).getTime();
|
|
23
|
+
const mins = Math.floor(diff / 60000);
|
|
24
|
+
if (mins < 1) return 'just now';
|
|
25
|
+
if (mins < 60) return `${mins}m`;
|
|
26
|
+
const hrs = Math.floor(mins / 60);
|
|
27
|
+
if (hrs < 24) return `${hrs}h`;
|
|
28
|
+
return `${Math.floor(hrs / 24)}d`;
|
|
29
|
+
} catch { return ''; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function MessagesPage(props: Partial<MessagesPageProps>) {
|
|
33
|
+
const noop = (..._args: any[]) => {};
|
|
34
|
+
const {
|
|
35
|
+
conversations = [], isLoadingConversations = false,
|
|
36
|
+
selectedConversation, onSelectConversation = noop,
|
|
37
|
+
messages = [], isLoadingMessages = false, isLoadingMore = false, hasMoreMessages = false, onLoadMoreMessages = noop,
|
|
38
|
+
onSendMessage = noop as any, isSending = false,
|
|
39
|
+
searchQuery = '', onSearchChange = noop,
|
|
40
|
+
showNewMessageDialog = false, onNewMessageDialogChange = noop,
|
|
41
|
+
mobileView = 'list' as const, onMobileViewChange = noop,
|
|
42
|
+
currentUserId = 'student-current',
|
|
43
|
+
} = props;
|
|
44
|
+
|
|
45
|
+
const [messageText, setMessageText] = useState('');
|
|
46
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
50
|
+
}, [messages]);
|
|
51
|
+
|
|
52
|
+
const handleSend = async () => {
|
|
53
|
+
if (!messageText.trim()) return;
|
|
54
|
+
const text = messageText.trim();
|
|
55
|
+
setMessageText('');
|
|
56
|
+
await onSendMessage(text);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const selectedName = selectedConversation?.participant?.name || selectedConversation?.name || 'Conversation';
|
|
60
|
+
const selectedCourse = selectedConversation?.courseContext?.courseName || '';
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex h-[calc(100vh-4rem)] -mx-4 lg:-mx-6 lg:-my-4 pt-3 overflow-hidden bg-theme-bg-secondary">
|
|
64
|
+
|
|
65
|
+
{/* ── Left panel: Conversation List ── */}
|
|
66
|
+
<div className={cn(
|
|
67
|
+
'w-full lg:w-80 shrink-0 border-r border-theme-border-primary flex flex-col bg-theme-bg-secondary',
|
|
68
|
+
mobileView === 'chat' ? 'hidden lg:flex' : 'flex'
|
|
69
|
+
)}>
|
|
70
|
+
{/* Header */}
|
|
71
|
+
<div className="px-4 pt-5 pb-3">
|
|
72
|
+
<div className="flex items-center justify-between mb-4">
|
|
73
|
+
<h1 className="text-xl font-bold text-theme-text-primary">Messages</h1>
|
|
74
|
+
<button
|
|
75
|
+
onClick={() => onNewMessageDialogChange(true)}
|
|
76
|
+
title="New message"
|
|
77
|
+
className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-[rgb(var(--accent-primary))] text-white hover:opacity-90 transition-opacity shadow-sm cursor-pointer"
|
|
78
|
+
>
|
|
79
|
+
<Plus className="h-4 w-4" />
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
{/* Search */}
|
|
83
|
+
<div className="relative">
|
|
84
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-theme-text-secondary" />
|
|
85
|
+
<input
|
|
86
|
+
type="text"
|
|
87
|
+
value={searchQuery}
|
|
88
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
89
|
+
placeholder="Search messages..."
|
|
90
|
+
className="w-full rounded-lg border border-theme-border-primary bg-theme-bg-primary pl-10 pr-4 py-2.5 text-sm text-theme-text-primary placeholder:text-theme-text-secondary focus:outline-none focus:ring-2 focus:ring-theme-accent-primary/30 focus:border-theme-accent-primary transition-colors"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Conversation list */}
|
|
96
|
+
<div className="flex-1 overflow-y-auto">
|
|
97
|
+
{isLoadingConversations ? (
|
|
98
|
+
<div className="flex justify-center py-8">
|
|
99
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-theme-border-primary border-t-theme-accent-primary" />
|
|
100
|
+
</div>
|
|
101
|
+
) : conversations.length === 0 ? (
|
|
102
|
+
<div className="flex flex-col items-center justify-center py-12 px-4 text-center">
|
|
103
|
+
<div className="w-16 h-16 rounded-full bg-theme-bg-tertiary flex items-center justify-center mb-4">
|
|
104
|
+
<MessageSquare className="w-8 h-8 text-theme-text-secondary" />
|
|
105
|
+
</div>
|
|
106
|
+
<p className="text-sm text-theme-text-secondary">No conversations found</p>
|
|
107
|
+
</div>
|
|
108
|
+
) : (
|
|
109
|
+
conversations.map((conv: any) => {
|
|
110
|
+
const convId = conv.id || conv.conversationId;
|
|
111
|
+
const isSelected = selectedConversation?.id === convId;
|
|
112
|
+
const name = conv.participant?.name || conv.name || conv.courseContext?.courseName || 'Conversation';
|
|
113
|
+
const initials = name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2);
|
|
114
|
+
const lastMsg = conv.lastMessage?.content || '';
|
|
115
|
+
const lastTime = conv.lastMessage?.timestamp;
|
|
116
|
+
const courseName = conv.courseContext?.courseName;
|
|
117
|
+
const unread = conv.unreadCount || 0;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<button
|
|
121
|
+
key={convId}
|
|
122
|
+
onClick={() => { onSelectConversation(conv); onMobileViewChange('chat'); }}
|
|
123
|
+
className={cn(
|
|
124
|
+
'w-full flex items-start gap-3 px-4 py-3.5 text-left transition-colors border-l-[3px] border-transparent hover:bg-theme-bg-tertiary/50 cursor-pointer',
|
|
125
|
+
isSelected && 'bg-theme-accent-primary/5 border-l-[rgb(var(--accent-primary))]'
|
|
126
|
+
)}
|
|
127
|
+
>
|
|
128
|
+
{/* Avatar initials */}
|
|
129
|
+
<div className="relative shrink-0">
|
|
130
|
+
<div className="w-10 h-10 rounded-full bg-theme-accent-primary/10 flex items-center justify-center">
|
|
131
|
+
<span className="text-sm font-semibold text-[rgb(var(--accent-primary))]">{initials}</span>
|
|
132
|
+
</div>
|
|
133
|
+
{unread > 0 && (
|
|
134
|
+
<span className="absolute -top-1 -right-1 w-5 h-5 rounded-full bg-[rgb(var(--accent-primary))] text-white text-[10px] font-bold flex items-center justify-center">
|
|
135
|
+
{unread > 9 ? '9+' : unread}
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
{/* Content */}
|
|
140
|
+
<div className="flex-1 min-w-0">
|
|
141
|
+
<div className="flex items-center justify-between gap-2">
|
|
142
|
+
<span className="text-sm font-semibold text-theme-text-primary truncate">{name}</span>
|
|
143
|
+
{lastTime && (
|
|
144
|
+
<span className="text-xs text-theme-text-secondary whitespace-nowrap">{timeAgo(lastTime)}</span>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
<p className="text-xs text-theme-text-secondary truncate mt-0.5">{lastMsg}</p>
|
|
148
|
+
{courseName && (
|
|
149
|
+
<p className="text-xs text-[rgb(var(--accent-primary))] mt-1 truncate">{courseName}</p>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</button>
|
|
153
|
+
);
|
|
154
|
+
})
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* ── Right: Chat Area ── */}
|
|
160
|
+
<div className={cn(
|
|
161
|
+
'flex-1 min-w-0 flex flex-col bg-theme-bg-primary',
|
|
162
|
+
mobileView === 'list' ? 'hidden lg:flex' : 'flex'
|
|
163
|
+
)}>
|
|
164
|
+
{!selectedConversation ? (
|
|
165
|
+
<div className="flex-1 flex flex-col items-center justify-center gap-3 text-center px-4">
|
|
166
|
+
<div className="w-20 h-20 rounded-full bg-theme-bg-tertiary flex items-center justify-center">
|
|
167
|
+
<MessageSquare className="w-10 h-10 text-theme-text-secondary" />
|
|
168
|
+
</div>
|
|
169
|
+
<h2 className="text-lg font-semibold text-theme-text-primary">Select a conversation</h2>
|
|
170
|
+
<p className="text-sm text-theme-text-secondary max-w-xs">Choose a conversation from the list or start a new one</p>
|
|
171
|
+
<button
|
|
172
|
+
onClick={() => onNewMessageDialogChange(true)}
|
|
173
|
+
className="mt-2 inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-[rgb(var(--accent-primary))] text-white text-sm font-medium hover:opacity-90 transition-opacity cursor-pointer"
|
|
174
|
+
>
|
|
175
|
+
<Plus className="w-4 h-4" />
|
|
176
|
+
New Message
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
) : (
|
|
180
|
+
<>
|
|
181
|
+
{/* Chat Header */}
|
|
182
|
+
<div className="px-4 py-3 border-b border-theme-border-primary flex items-center gap-3 bg-theme-bg-secondary">
|
|
183
|
+
<button
|
|
184
|
+
onClick={() => onMobileViewChange('list')}
|
|
185
|
+
className="lg:hidden text-theme-text-secondary hover:text-theme-text-primary cursor-pointer"
|
|
186
|
+
>
|
|
187
|
+
<ArrowLeft className="w-5 h-5" />
|
|
188
|
+
</button>
|
|
189
|
+
<div className="w-9 h-9 rounded-full bg-theme-accent-primary/10 flex items-center justify-center shrink-0">
|
|
190
|
+
<span className="text-sm font-semibold text-[rgb(var(--accent-primary))]">
|
|
191
|
+
{selectedName.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2)}
|
|
192
|
+
</span>
|
|
193
|
+
</div>
|
|
194
|
+
<div className="min-w-0">
|
|
195
|
+
<p className="font-semibold text-sm text-theme-text-primary truncate">{selectedName}</p>
|
|
196
|
+
{selectedCourse && (
|
|
197
|
+
<p className="text-xs text-[rgb(var(--accent-primary))] truncate">{selectedCourse}</p>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{/* Messages */}
|
|
203
|
+
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-3">
|
|
204
|
+
{hasMoreMessages && (
|
|
205
|
+
<div className="flex justify-center pb-2">
|
|
206
|
+
<button
|
|
207
|
+
onClick={onLoadMoreMessages}
|
|
208
|
+
disabled={isLoadingMore}
|
|
209
|
+
className="inline-flex items-center gap-1 text-xs text-[rgb(var(--accent-primary))] hover:opacity-80 cursor-pointer disabled:opacity-50"
|
|
210
|
+
>
|
|
211
|
+
{isLoadingMore ? <Loader2 className="w-3 h-3 animate-spin" /> : <ChevronUp className="w-3 h-3" />}
|
|
212
|
+
Load older messages
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
{isLoadingMessages ? (
|
|
218
|
+
<div className="flex justify-center py-8">
|
|
219
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-theme-border-primary border-t-theme-accent-primary" />
|
|
220
|
+
</div>
|
|
221
|
+
) : messages.length === 0 ? (
|
|
222
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
223
|
+
<MessageSquare className="w-10 h-10 text-theme-text-secondary mb-3" />
|
|
224
|
+
<p className="text-sm text-theme-text-secondary">No messages yet. Say hello!</p>
|
|
225
|
+
</div>
|
|
226
|
+
) : (
|
|
227
|
+
messages.map((msg: any) => {
|
|
228
|
+
const isOwn = msg.senderId === currentUserId || msg.senderRole === 'STUDENT';
|
|
229
|
+
return (
|
|
230
|
+
<div key={msg.id} className={cn('flex', isOwn ? 'justify-end' : 'justify-start')}>
|
|
231
|
+
<div className={cn('flex items-end gap-2 max-w-[75%]', isOwn && 'flex-row-reverse')}>
|
|
232
|
+
{!isOwn && (
|
|
233
|
+
<div className="w-7 h-7 rounded-full bg-theme-accent-primary/10 flex items-center justify-center shrink-0">
|
|
234
|
+
<span className="text-[10px] font-semibold text-[rgb(var(--accent-primary))]">
|
|
235
|
+
{selectedName.charAt(0).toUpperCase()}
|
|
236
|
+
</span>
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
<div>
|
|
240
|
+
<div className={cn(
|
|
241
|
+
'rounded-2xl px-4 py-2.5 text-sm leading-relaxed',
|
|
242
|
+
isOwn
|
|
243
|
+
? 'bg-[rgb(var(--accent-primary))] text-white rounded-br-md'
|
|
244
|
+
: 'bg-theme-bg-tertiary text-theme-text-primary rounded-bl-md'
|
|
245
|
+
)}>
|
|
246
|
+
{msg.content}
|
|
247
|
+
</div>
|
|
248
|
+
<p className={cn('text-[10px] text-theme-text-muted mt-1', isOwn ? 'text-right' : 'text-left')}>
|
|
249
|
+
{msg.timestamp ? formatTime(msg.timestamp) : ''}
|
|
250
|
+
</p>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
})
|
|
256
|
+
)}
|
|
257
|
+
<div ref={messagesEndRef} />
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{/* Send Input */}
|
|
261
|
+
<div className="px-4 py-3 border-t border-theme-border-primary bg-theme-bg-secondary">
|
|
262
|
+
<div className="flex items-center gap-2">
|
|
263
|
+
<input
|
|
264
|
+
type="text"
|
|
265
|
+
value={messageText}
|
|
266
|
+
onChange={(e) => setMessageText(e.target.value)}
|
|
267
|
+
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }}
|
|
268
|
+
placeholder="Type a message..."
|
|
269
|
+
className="flex-1 h-10 px-4 text-sm rounded-full border border-theme-border-primary bg-theme-bg-primary text-theme-text-primary placeholder:text-theme-text-muted focus:outline-none focus:border-theme-accent-primary transition-colors"
|
|
270
|
+
/>
|
|
271
|
+
<button
|
|
272
|
+
onClick={handleSend}
|
|
273
|
+
disabled={isSending || !messageText.trim()}
|
|
274
|
+
className="flex items-center justify-center w-10 h-10 rounded-full bg-[rgb(var(--accent-primary))] text-white hover:opacity-90 disabled:opacity-50 transition-opacity shrink-0 cursor-pointer"
|
|
275
|
+
>
|
|
276
|
+
{isSending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
|
|
277
|
+
</button>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Search, X, ChevronDown } from 'lucide-react';
|
|
3
|
+
import type { MyLearningPageProps } from '../../contracts/pages.contract';
|
|
4
|
+
import { CourseCard } from '../molecules/CourseCard';
|
|
5
|
+
import { Pagination } from '../molecules/Pagination';
|
|
6
|
+
|
|
7
|
+
export function MyLearningPage(props: Partial<MyLearningPageProps>) {
|
|
8
|
+
const noop = () => {};
|
|
9
|
+
const {
|
|
10
|
+
courses = [], coursesMeta, coursesLoading, coursesPage = 1, onCoursesPageChange = noop,
|
|
11
|
+
bundles = [], bundlesMeta, bundlesLoading, bundlesPage = 1, onBundlesPageChange = noop,
|
|
12
|
+
searchQuery = '', onSearchChange = noop,
|
|
13
|
+
activeTab = 'courses', onTabChange = noop,
|
|
14
|
+
statusFilter = 'all', onStatusFilterChange = noop,
|
|
15
|
+
onCourseClick = noop, onBundleClick = noop,
|
|
16
|
+
} = props;
|
|
17
|
+
|
|
18
|
+
const [showStatusDropdown, setShowStatusDropdown] = useState(false);
|
|
19
|
+
|
|
20
|
+
const isLoading = activeTab === 'courses' ? !!coursesLoading : !!bundlesLoading;
|
|
21
|
+
const currentPage = activeTab === 'courses' ? coursesPage : bundlesPage;
|
|
22
|
+
const meta = activeTab === 'courses' ? coursesMeta : bundlesMeta;
|
|
23
|
+
const onPageChange = activeTab === 'courses' ? onCoursesPageChange : onBundlesPageChange;
|
|
24
|
+
const isSearchMode = Boolean(searchQuery.trim());
|
|
25
|
+
|
|
26
|
+
const getStatusLabel = () => {
|
|
27
|
+
switch (statusFilter) {
|
|
28
|
+
case 'in-progress': return 'In Progress';
|
|
29
|
+
case 'completed': return 'Completed';
|
|
30
|
+
default: return 'All Status';
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const handleStatusFilterChange = (status: string) => {
|
|
35
|
+
onStatusFilterChange(status);
|
|
36
|
+
setShowStatusDropdown(false);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="container mx-auto space-y-6">
|
|
41
|
+
{/* Header */}
|
|
42
|
+
<div>
|
|
43
|
+
<h1 className="text-3xl font-bold text-theme-text-primary">My Learning Content</h1>
|
|
44
|
+
<p className="mt-2 text-lg text-theme-text-secondary">Continue your learning journey with courses and bundles</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Search Bar and Status Filter */}
|
|
48
|
+
<div className="flex gap-4">
|
|
49
|
+
<div className="relative flex-1">
|
|
50
|
+
<Search className="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-theme-text-secondary z-10" />
|
|
51
|
+
<input
|
|
52
|
+
type="text"
|
|
53
|
+
placeholder={`Search ${activeTab}...`}
|
|
54
|
+
value={searchQuery}
|
|
55
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
56
|
+
className="w-full rounded-lg border border-theme-border-primary bg-theme-bg-secondary pl-10 pr-10 py-2 text-sm text-theme-text-primary placeholder:text-theme-text-secondary focus:outline-none focus:ring-2 focus:ring-theme-accent-primary/30 focus:border-theme-accent-primary transition-all"
|
|
57
|
+
/>
|
|
58
|
+
{searchQuery && (
|
|
59
|
+
<button
|
|
60
|
+
onClick={() => onSearchChange('')}
|
|
61
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
62
|
+
>
|
|
63
|
+
<X className="h-5 w-5" />
|
|
64
|
+
</button>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Status Filter Dropdown */}
|
|
69
|
+
{!isSearchMode && (
|
|
70
|
+
<div className="relative">
|
|
71
|
+
<button
|
|
72
|
+
onClick={() => setShowStatusDropdown(!showStatusDropdown)}
|
|
73
|
+
className="flex items-center gap-2 min-w-[150px] justify-between px-4 py-2 border border-theme-border-primary rounded-lg text-sm text-theme-text-primary bg-theme-bg-secondary hover:bg-theme-bg-tertiary transition-colors cursor-pointer"
|
|
74
|
+
>
|
|
75
|
+
{getStatusLabel()}
|
|
76
|
+
<ChevronDown className="h-4 w-4" />
|
|
77
|
+
</button>
|
|
78
|
+
{showStatusDropdown && (
|
|
79
|
+
<div className="absolute right-0 mt-2 w-48 rounded-lg border border-theme-border-primary bg-theme-bg-secondary shadow-lg z-10">
|
|
80
|
+
{(['all', 'in-progress', 'completed'] as const).map((s) => (
|
|
81
|
+
<button
|
|
82
|
+
key={s}
|
|
83
|
+
onClick={() => handleStatusFilterChange(s)}
|
|
84
|
+
className={`w-full text-left px-4 py-2 hover:bg-theme-bg-tertiary transition-colors ${statusFilter === s ? 'bg-theme-bg-tertiary text-theme-accent-primary font-medium' : 'text-theme-text-primary'}`}
|
|
85
|
+
>
|
|
86
|
+
{s === 'all' ? 'All Status' : s === 'in-progress' ? 'In Progress' : 'Completed'}
|
|
87
|
+
</button>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Tabs */}
|
|
96
|
+
<div className="flex gap-6 border-b border-theme-border-primary">
|
|
97
|
+
{(['courses', 'bundles'] as const).map((tab) => (
|
|
98
|
+
<button
|
|
99
|
+
key={tab}
|
|
100
|
+
onClick={() => { onTabChange(tab); onSearchChange(''); onStatusFilterChange('all'); }}
|
|
101
|
+
className={`pb-3 px-0 bg-transparent border-0 font-medium text-sm cursor-pointer capitalize ${
|
|
102
|
+
activeTab === tab
|
|
103
|
+
? 'border-b-2 border-theme-accent-primary text-theme-accent-primary'
|
|
104
|
+
: 'text-theme-text-secondary hover:text-theme-text-primary border-b-2 border-transparent'
|
|
105
|
+
}`}
|
|
106
|
+
>
|
|
107
|
+
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
|
108
|
+
</button>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Loading */}
|
|
113
|
+
{isLoading && (
|
|
114
|
+
<div className="flex flex-col items-center justify-center py-16">
|
|
115
|
+
<div className="h-12 w-12 animate-spin rounded-full border-4 border-theme-border-primary border-t-theme-accent-primary"></div>
|
|
116
|
+
<p className="mt-4 text-theme-text-secondary">Loading {activeTab}...</p>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{/* Courses Empty State */}
|
|
121
|
+
{!isLoading && activeTab === 'courses' && courses.length === 0 && (
|
|
122
|
+
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-theme-border-primary bg-theme-bg-secondary py-16 text-center">
|
|
123
|
+
<div className="text-4xl mb-4">📚</div>
|
|
124
|
+
<h3 className="text-xl font-semibold text-theme-text-primary">No courses found</h3>
|
|
125
|
+
<p className="mt-2 max-w-md text-theme-text-secondary">
|
|
126
|
+
{statusFilter === 'all'
|
|
127
|
+
? "You don't have any enrolled courses yet"
|
|
128
|
+
: `You don't have any ${statusFilter === 'in-progress' ? 'in progress' : 'completed'} courses yet`}
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
{/* Bundles Empty State */}
|
|
134
|
+
{!isLoading && activeTab === 'bundles' && bundles.length === 0 && (
|
|
135
|
+
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-theme-border-primary bg-theme-bg-secondary py-16 text-center">
|
|
136
|
+
<div className="text-4xl mb-4">📦</div>
|
|
137
|
+
<h3 className="text-xl font-semibold text-theme-text-primary">No bundles found</h3>
|
|
138
|
+
<p className="mt-2 max-w-md text-theme-text-secondary">
|
|
139
|
+
{statusFilter === 'all'
|
|
140
|
+
? "You don't have any enrolled bundles yet"
|
|
141
|
+
: `You don't have any ${statusFilter === 'in-progress' ? 'in progress' : 'completed'} bundles yet`}
|
|
142
|
+
</p>
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{/* Courses Grid */}
|
|
147
|
+
{!isLoading && activeTab === 'courses' && courses.length > 0 && (
|
|
148
|
+
<>
|
|
149
|
+
<div className="grid gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
150
|
+
{courses.map((enrollment: any) => {
|
|
151
|
+
const course = enrollment.course || enrollment;
|
|
152
|
+
const title = course?.title || enrollment.title || '';
|
|
153
|
+
const thumbnail = course?.thumbnail || enrollment.thumbnail;
|
|
154
|
+
const totalLessons = enrollment.totalNumberOfActivities || enrollment.totalLessons || 0;
|
|
155
|
+
const progress = Math.min(enrollment.progressPercentage || enrollment.progress || 0, 100);
|
|
156
|
+
const status: 'in-progress' | 'completed' | undefined =
|
|
157
|
+
enrollment.status === 'COMPLETED' ? 'completed' :
|
|
158
|
+
enrollment.status === 'IN_PROGRESS' ? 'in-progress' : undefined;
|
|
159
|
+
const description = (course?.summary || enrollment.description || '').replace(/<[^>]*>/g, '');
|
|
160
|
+
return (
|
|
161
|
+
<CourseCard
|
|
162
|
+
key={enrollment.id}
|
|
163
|
+
id={enrollment.id}
|
|
164
|
+
title={title}
|
|
165
|
+
thumbnail={thumbnail}
|
|
166
|
+
totalLessons={totalLessons}
|
|
167
|
+
progress={progress}
|
|
168
|
+
status={status}
|
|
169
|
+
isFree={true}
|
|
170
|
+
price={0}
|
|
171
|
+
showPrice={false}
|
|
172
|
+
showProgress={true}
|
|
173
|
+
description={description}
|
|
174
|
+
onClick={() => (onCourseClick as any)(course?.id || enrollment.courseId || enrollment.id, enrollment.id)}
|
|
175
|
+
/>
|
|
176
|
+
);
|
|
177
|
+
})}
|
|
178
|
+
</div>
|
|
179
|
+
{meta && (
|
|
180
|
+
<Pagination
|
|
181
|
+
currentPage={currentPage}
|
|
182
|
+
totalPages={meta.totalPages}
|
|
183
|
+
total={meta.total}
|
|
184
|
+
pageSize={meta.limit || 12}
|
|
185
|
+
hasNextPage={currentPage < meta.totalPages}
|
|
186
|
+
hasPreviousPage={currentPage > 1}
|
|
187
|
+
onPageChange={onPageChange}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
</>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{/* Bundles Grid */}
|
|
194
|
+
{!isLoading && activeTab === 'bundles' && bundles.length > 0 && (
|
|
195
|
+
<>
|
|
196
|
+
<div className="grid gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
197
|
+
{bundles.map((enrollment: any) => {
|
|
198
|
+
const thumbnails = enrollment.courses?.slice(0, 3).map((c: any) => c.thumbnail).filter(Boolean) as string[] | undefined;
|
|
199
|
+
const description = enrollment.courses?.[0]?.summary || '';
|
|
200
|
+
const status: 'in-progress' | 'completed' | undefined =
|
|
201
|
+
enrollment.status === 'COMPLETED' ? 'completed' :
|
|
202
|
+
enrollment.status === 'IN_PROGRESS' ? 'in-progress' : undefined;
|
|
203
|
+
return (
|
|
204
|
+
<CourseCard
|
|
205
|
+
key={enrollment.id}
|
|
206
|
+
id={enrollment.id}
|
|
207
|
+
title={enrollment.bundleTitle || enrollment.title || ''}
|
|
208
|
+
thumbnails={thumbnails}
|
|
209
|
+
totalLessons={enrollment.totalCourses || 0}
|
|
210
|
+
progress={Math.min(enrollment.progressPercentage || 0, 100)}
|
|
211
|
+
status={status}
|
|
212
|
+
isFree={true}
|
|
213
|
+
price={0}
|
|
214
|
+
showPrice={false}
|
|
215
|
+
showProgress={true}
|
|
216
|
+
description={description}
|
|
217
|
+
isBundle={true}
|
|
218
|
+
onClick={() => (onBundleClick as any)(enrollment.sourceBundleId || enrollment.bundleId || enrollment.id)}
|
|
219
|
+
/>
|
|
220
|
+
);
|
|
221
|
+
})}
|
|
222
|
+
</div>
|
|
223
|
+
{meta && (
|
|
224
|
+
<Pagination
|
|
225
|
+
currentPage={currentPage}
|
|
226
|
+
totalPages={meta.totalPages}
|
|
227
|
+
total={meta.total}
|
|
228
|
+
pageSize={meta.limit || 12}
|
|
229
|
+
hasNextPage={currentPage < meta.totalPages}
|
|
230
|
+
hasPreviousPage={currentPage > 1}
|
|
231
|
+
onPageChange={onPageChange}
|
|
232
|
+
/>
|
|
233
|
+
)}
|
|
234
|
+
</>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { XCircle } from 'lucide-react';
|
|
3
|
+
import type { PaymentCancelPageProps } from '../../contracts/pages.contract';
|
|
4
|
+
|
|
5
|
+
export function PaymentCancelPage({ onRetry = () => {}, onGoBack = () => {} }: Partial<PaymentCancelPageProps>) {
|
|
6
|
+
const [countdown, setCountdown] = useState(10);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const timer = setInterval(() => {
|
|
10
|
+
setCountdown((prev) => {
|
|
11
|
+
if (prev <= 1) {
|
|
12
|
+
clearInterval(timer);
|
|
13
|
+
onGoBack();
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
return prev - 1;
|
|
17
|
+
});
|
|
18
|
+
}, 1000);
|
|
19
|
+
return () => clearInterval(timer);
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center px-4 py-12">
|
|
24
|
+
<div className="max-w-lg w-full">
|
|
25
|
+
<div>
|
|
26
|
+
{/* Cancel Icon */}
|
|
27
|
+
<div className="mb-8 flex justify-center">
|
|
28
|
+
<div className="bg-gradient-to-br from-orange-400 to-orange-500 rounded-full p-5 shadow-lg">
|
|
29
|
+
<XCircle className="h-16 w-16 text-white" strokeWidth={2.5} />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
{/* Cancel Message */}
|
|
34
|
+
<h1 className="text-2xl font-bold text-gray-900 mb-3 text-center">
|
|
35
|
+
Payment Cancelled
|
|
36
|
+
</h1>
|
|
37
|
+
<p className="text-gray-600 text-sm mb-8 text-center leading-relaxed">
|
|
38
|
+
Your payment was cancelled. No charges have been made to your account.
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
{/* Info Box */}
|
|
42
|
+
<div className="mb-6 p-4 bg-blue-50 rounded-xl border border-blue-100">
|
|
43
|
+
<p className="text-sm text-blue-600 text-center font-medium leading-relaxed">
|
|
44
|
+
You can complete your purchase anytime. The course will be waiting for you!
|
|
45
|
+
</p>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{/* Auto-redirect Notice */}
|
|
49
|
+
<div className="mb-8 p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl border border-gray-200">
|
|
50
|
+
<p className="text-sm text-gray-600 text-center font-medium">
|
|
51
|
+
Redirecting in {countdown} second{countdown !== 1 ? 's' : ''}...
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Manual Actions */}
|
|
56
|
+
<div className="flex gap-4 justify-center">
|
|
57
|
+
<button
|
|
58
|
+
onClick={onRetry}
|
|
59
|
+
className="px-6 py-2 bg-theme-accent-primary text-white rounded-lg font-medium hover:opacity-90 transition-opacity cursor-pointer"
|
|
60
|
+
>
|
|
61
|
+
Try Again
|
|
62
|
+
</button>
|
|
63
|
+
<button
|
|
64
|
+
onClick={onGoBack}
|
|
65
|
+
className="px-6 py-2 border border-theme-border-primary rounded-lg font-medium text-theme-text-primary hover:bg-theme-bg-secondary transition-colors cursor-pointer"
|
|
66
|
+
>
|
|
67
|
+
Go Back
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { CheckCircle, Loader2 } from 'lucide-react';
|
|
3
|
+
import type { PaymentSuccessPageProps } from '../../contracts/pages.contract';
|
|
4
|
+
|
|
5
|
+
export function PaymentSuccessPage({ onContinueLearning = () => {} }: Partial<PaymentSuccessPageProps>) {
|
|
6
|
+
const [countdown, setCountdown] = useState(5);
|
|
7
|
+
const [isRedirecting, setIsRedirecting] = useState(false);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const timer = setInterval(() => {
|
|
11
|
+
setCountdown((prev) => {
|
|
12
|
+
if (prev <= 1) {
|
|
13
|
+
clearInterval(timer);
|
|
14
|
+
setIsRedirecting(true);
|
|
15
|
+
onContinueLearning();
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
return prev - 1;
|
|
19
|
+
});
|
|
20
|
+
}, 1000);
|
|
21
|
+
return () => clearInterval(timer);
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center px-4 py-12">
|
|
26
|
+
<div className="max-w-lg w-full">
|
|
27
|
+
<div>
|
|
28
|
+
{/* Success Icon */}
|
|
29
|
+
<div className="mb-8 flex justify-center">
|
|
30
|
+
<div className="relative">
|
|
31
|
+
<div className="absolute inset-0 bg-green-400 rounded-full animate-ping opacity-20"></div>
|
|
32
|
+
<div className="relative bg-gradient-to-br from-green-400 to-green-500 rounded-full p-5 shadow-lg">
|
|
33
|
+
<CheckCircle className="h-16 w-16 text-white" strokeWidth={2.5} />
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Success Message */}
|
|
39
|
+
<h1 className="text-2xl font-bold text-gray-900 mb-3 text-center">
|
|
40
|
+
Payment Successful!
|
|
41
|
+
</h1>
|
|
42
|
+
<p className="text-gray-600 text-sm mb-8 text-center leading-relaxed">
|
|
43
|
+
Thank you for your purchase. You're now enrolled in the course and can start learning immediately.
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
{/* Auto-redirect Notice */}
|
|
47
|
+
<div className="mb-8 p-4 bg-blue-50 rounded-xl border border-blue-100">
|
|
48
|
+
{isRedirecting ? (
|
|
49
|
+
<div className="flex items-center justify-center gap-2 text-blue-600">
|
|
50
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
51
|
+
<span className="text-sm font-medium">Redirecting...</span>
|
|
52
|
+
</div>
|
|
53
|
+
) : (
|
|
54
|
+
<p className="text-sm text-blue-600 text-center font-medium">
|
|
55
|
+
Redirecting to course page in {countdown} second{countdown !== 1 ? 's' : ''}...
|
|
56
|
+
</p>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{/* Manual action */}
|
|
61
|
+
<div className="text-center">
|
|
62
|
+
<button
|
|
63
|
+
onClick={onContinueLearning}
|
|
64
|
+
className="inline-flex items-center gap-2 px-6 py-2 bg-green-500 text-white rounded-lg font-medium hover:bg-green-600 transition-colors cursor-pointer"
|
|
65
|
+
>
|
|
66
|
+
Continue Learning
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|