ajaxter-chat 1.0.3 → 2.0.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/README.md +96 -204
- package/dist/components/ChatScreen/index.d.ts +12 -0
- package/dist/components/ChatScreen/index.js +83 -0
- package/dist/components/ChatWidget.d.ts +0 -24
- package/dist/components/ChatWidget.js +129 -38
- package/dist/components/HomeScreen/index.d.ts +9 -0
- package/dist/components/HomeScreen/index.js +71 -0
- package/dist/components/MaintenanceView/index.d.ts +1 -1
- package/dist/components/MaintenanceView/index.js +15 -52
- package/dist/components/RecentChatsScreen/index.d.ts +16 -0
- package/dist/components/RecentChatsScreen/index.js +38 -0
- package/dist/components/Tabs/BottomTabs.d.ts +10 -0
- package/dist/components/Tabs/BottomTabs.js +29 -0
- package/dist/components/TicketScreen/index.d.ts +9 -0
- package/dist/components/TicketScreen/index.js +71 -0
- package/dist/components/UserListScreen/index.d.ts +13 -0
- package/dist/components/UserListScreen/index.js +64 -0
- package/dist/config/index.d.ts +0 -13
- package/dist/config/index.js +20 -95
- package/dist/hooks/useChat.d.ts +3 -7
- package/dist/hooks/useChat.js +8 -30
- package/dist/hooks/useUsers.d.ts +3 -10
- package/dist/hooks/useUsers.js +5 -11
- package/dist/index.d.ts +8 -7
- package/dist/index.js +7 -12
- package/dist/services/userService.d.ts +0 -5
- package/dist/services/userService.js +6 -15
- package/dist/src/components/ChatScreen/index.d.ts +12 -0
- package/dist/src/components/ChatScreen/index.js +83 -0
- package/dist/src/components/ChatWidget.d.ts +4 -0
- package/dist/src/components/ChatWidget.js +141 -0
- package/dist/src/components/HomeScreen/index.d.ts +9 -0
- package/dist/src/components/HomeScreen/index.js +71 -0
- package/dist/src/components/MaintenanceView/index.d.ts +7 -0
- package/dist/src/components/MaintenanceView/index.js +16 -0
- package/dist/src/components/RecentChatsScreen/index.d.ts +16 -0
- package/dist/src/components/RecentChatsScreen/index.js +38 -0
- package/dist/src/components/Tabs/BottomTabs.d.ts +10 -0
- package/dist/src/components/Tabs/BottomTabs.js +29 -0
- package/dist/src/components/TicketScreen/index.d.ts +9 -0
- package/dist/src/components/TicketScreen/index.js +71 -0
- package/dist/src/components/UserListScreen/index.d.ts +13 -0
- package/dist/src/components/UserListScreen/index.js +64 -0
- package/dist/src/config/index.d.ts +3 -0
- package/dist/src/config/index.js +38 -0
- package/dist/src/hooks/useChat.d.ts +8 -0
- package/dist/src/hooks/useChat.js +26 -0
- package/dist/src/hooks/useUsers.d.ts +7 -0
- package/dist/src/hooks/useUsers.js +26 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +13 -0
- package/dist/src/services/userService.d.ts +2 -0
- package/dist/src/services/userService.js +9 -0
- package/dist/src/types/index.d.ts +59 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/utils/theme.d.ts +3 -0
- package/dist/src/utils/theme.js +13 -0
- package/dist/types/index.d.ts +23 -36
- package/dist/utils/theme.d.ts +0 -1
- package/dist/utils/theme.js +3 -18
- package/package.json +10 -20
- package/src/components/ChatScreen/index.tsx +205 -0
- package/src/components/ChatWidget.tsx +327 -0
- package/src/components/HomeScreen/index.tsx +130 -0
- package/src/components/MaintenanceView/index.tsx +41 -0
- package/src/components/RecentChatsScreen/index.tsx +108 -0
- package/src/components/Tabs/BottomTabs.tsx +82 -0
- package/src/components/TicketScreen/index.tsx +170 -0
- package/src/components/UserListScreen/index.tsx +181 -0
- package/src/config/index.ts +46 -0
- package/src/hooks/useChat.ts +31 -0
- package/src/hooks/useUsers.ts +27 -0
- package/src/index.ts +18 -0
- package/src/services/userService.ts +9 -0
- package/src/types/index.ts +82 -0
- package/src/utils/theme.ts +16 -0
- package/dist/components/BottomNav/index.d.ts +0 -10
- package/dist/components/BottomNav/index.js +0 -32
- package/dist/components/ChatBox/index.d.ts +0 -15
- package/dist/components/ChatBox/index.js +0 -228
- package/dist/components/ChatButton/index.d.ts +0 -9
- package/dist/components/ChatButton/index.js +0 -17
- package/dist/components/ChatWindow/index.d.ts +0 -10
- package/dist/components/ChatWindow/index.js +0 -286
- package/dist/components/HomeView/index.d.ts +0 -12
- package/dist/components/HomeView/index.js +0 -51
- package/dist/components/UserList/index.d.ts +0 -13
- package/dist/components/UserList/index.js +0 -136
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { ChatWidgetProps, BottomTab, Screen, UserListContext, ChatUser, Ticket } from '../types';
|
|
5
|
+
import { loadChatConfig, buildUserListUrl } from '../config';
|
|
6
|
+
import { mergeTheme } from '../utils/theme';
|
|
7
|
+
import { useUsers } from '../hooks/useUsers';
|
|
8
|
+
import { useChat } from '../hooks/useChat';
|
|
9
|
+
|
|
10
|
+
// Screens
|
|
11
|
+
import { HomeScreen } from './HomeScreen';
|
|
12
|
+
import { UserListScreen } from './UserListScreen';
|
|
13
|
+
import { ChatScreen } from './ChatScreen';
|
|
14
|
+
import { RecentChatsScreen } from './RecentChatsScreen';
|
|
15
|
+
import { TicketScreen } from './TicketScreen';
|
|
16
|
+
import { MaintenanceView } from './MaintenanceView';
|
|
17
|
+
import { BottomTabs } from './Tabs/BottomTabs';
|
|
18
|
+
|
|
19
|
+
export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme }) => {
|
|
20
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
21
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
22
|
+
const [isMaximized, setIsMaximized] = useState(false);
|
|
23
|
+
const [activeTab, setActiveTab] = useState<BottomTab>('home');
|
|
24
|
+
const [screen, setScreen] = useState<Screen>('home');
|
|
25
|
+
const [userListCtx, setUserListCtx] = useState<UserListContext>('support');
|
|
26
|
+
const [tickets, setTickets] = useState<Ticket[]>([]);
|
|
27
|
+
|
|
28
|
+
// SSR guard
|
|
29
|
+
useEffect(() => { setIsMounted(true); }, []);
|
|
30
|
+
|
|
31
|
+
const config = loadChatConfig();
|
|
32
|
+
const t = mergeTheme(theme);
|
|
33
|
+
const apiUrl = buildUserListUrl(config);
|
|
34
|
+
|
|
35
|
+
// Determine filter based on context
|
|
36
|
+
const filterType = userListCtx === 'support' ? 'developer' : 'user';
|
|
37
|
+
const { users, loading, error } = useUsers(
|
|
38
|
+
apiUrl,
|
|
39
|
+
filterType,
|
|
40
|
+
config.status === 'ACTIVE' && screen === 'user-list'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const { messages, activeUser, selectUser, sendMessage, clearChat } = useChat();
|
|
44
|
+
|
|
45
|
+
// ── Navigation helpers ─────────────────────────────────────────────────────
|
|
46
|
+
const goHome = useCallback(() => { setScreen('home'); setActiveTab('home'); }, []);
|
|
47
|
+
|
|
48
|
+
const handleTabChange = useCallback((tab: BottomTab) => {
|
|
49
|
+
setActiveTab(tab);
|
|
50
|
+
if (tab === 'home') { setScreen('home'); }
|
|
51
|
+
if (tab === 'chats') { setScreen('recent-chats'); }
|
|
52
|
+
if (tab === 'tickets') { setScreen('tickets'); }
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const handleCardClick = useCallback((ctx: UserListContext | 'ticket') => {
|
|
56
|
+
if (ctx === 'ticket') {
|
|
57
|
+
setActiveTab('tickets');
|
|
58
|
+
setScreen('tickets');
|
|
59
|
+
} else {
|
|
60
|
+
setUserListCtx(ctx as UserListContext);
|
|
61
|
+
setScreen('user-list');
|
|
62
|
+
}
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const handleSelectUser = useCallback((user: ChatUser) => {
|
|
66
|
+
selectUser(user);
|
|
67
|
+
setScreen('chat');
|
|
68
|
+
}, [selectUser]);
|
|
69
|
+
|
|
70
|
+
const handleBackFromUserList = useCallback(() => {
|
|
71
|
+
setScreen('home');
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const handleBackFromChat = useCallback(() => {
|
|
75
|
+
clearChat();
|
|
76
|
+
setScreen('user-list');
|
|
77
|
+
}, [clearChat]);
|
|
78
|
+
|
|
79
|
+
const handleRaiseTicket = useCallback((title: string, description: string) => {
|
|
80
|
+
const t: Ticket = {
|
|
81
|
+
id: `ticket_${Date.now()}`,
|
|
82
|
+
title,
|
|
83
|
+
description,
|
|
84
|
+
status: 'open',
|
|
85
|
+
priority: 'medium',
|
|
86
|
+
createdAt: new Date(),
|
|
87
|
+
updatedAt: new Date(),
|
|
88
|
+
};
|
|
89
|
+
setTickets(prev => [t, ...prev]);
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
// ── Sizing ─────────────────────────────────────────────────────────────────
|
|
93
|
+
const normalW = 380;
|
|
94
|
+
const normalH = 560;
|
|
95
|
+
const maxW = 480;
|
|
96
|
+
const maxH = 720;
|
|
97
|
+
|
|
98
|
+
const width = isMaximized ? maxW : normalW;
|
|
99
|
+
const height = isMaximized ? maxH : normalH;
|
|
100
|
+
|
|
101
|
+
const posStyle: React.CSSProperties = t.buttonPosition === 'bottom-left'
|
|
102
|
+
? { left: 24, right: 'auto' }
|
|
103
|
+
: { right: 24, left: 'auto' };
|
|
104
|
+
|
|
105
|
+
if (!isMounted) return null;
|
|
106
|
+
if (config.status === 'DISABLE') return null;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<>
|
|
110
|
+
{/* ── Global keyframes ── */}
|
|
111
|
+
<style>{`
|
|
112
|
+
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&display=swap');
|
|
113
|
+
@keyframes cw-fadeUp {
|
|
114
|
+
from { opacity:0; transform:translateY(10px); }
|
|
115
|
+
to { opacity:1; transform:translateY(0); }
|
|
116
|
+
}
|
|
117
|
+
@keyframes cw-slideInRight {
|
|
118
|
+
from { opacity:0; transform:translateX(18px); }
|
|
119
|
+
to { opacity:1; transform:translateX(0); }
|
|
120
|
+
}
|
|
121
|
+
@keyframes cw-popIn {
|
|
122
|
+
from { opacity:0; transform:scale(0.88) translateY(16px); }
|
|
123
|
+
to { opacity:1; transform:scale(1) translateY(0); }
|
|
124
|
+
}
|
|
125
|
+
.cw-scrollbar::-webkit-scrollbar { width:4px; }
|
|
126
|
+
.cw-scrollbar::-webkit-scrollbar-track { background:transparent; }
|
|
127
|
+
.cw-scrollbar::-webkit-scrollbar-thumb { background:#e0e0e0; border-radius:4px; }
|
|
128
|
+
`}</style>
|
|
129
|
+
|
|
130
|
+
{/* ── Floating Button ── */}
|
|
131
|
+
<button
|
|
132
|
+
onClick={() => setIsOpen(o => !o)}
|
|
133
|
+
aria-label={isOpen ? 'Close chat' : t.buttonLabel}
|
|
134
|
+
style={{
|
|
135
|
+
position: 'fixed',
|
|
136
|
+
bottom: 24,
|
|
137
|
+
...posStyle,
|
|
138
|
+
zIndex: 9999,
|
|
139
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
140
|
+
padding: isOpen ? '14px' : '13px 22px',
|
|
141
|
+
backgroundColor: t.buttonColor,
|
|
142
|
+
color: t.buttonTextColor,
|
|
143
|
+
border: 'none', borderRadius: 50,
|
|
144
|
+
cursor: 'pointer',
|
|
145
|
+
fontFamily: t.fontFamily,
|
|
146
|
+
fontSize: '15px', fontWeight: 700,
|
|
147
|
+
boxShadow: `0 8px 28px ${t.buttonColor}55`,
|
|
148
|
+
transition: 'all 0.3s cubic-bezier(0.34,1.56,0.64,1)',
|
|
149
|
+
transform: isOpen ? 'scale(0.94)' : 'scale(1)',
|
|
150
|
+
minWidth: isOpen ? 50 : 'auto',
|
|
151
|
+
justifyContent: 'center',
|
|
152
|
+
}}
|
|
153
|
+
onMouseEnter={e => {
|
|
154
|
+
if (!isOpen) {
|
|
155
|
+
(e.currentTarget as HTMLElement).style.transform = 'scale(1.06) translateY(-2px)';
|
|
156
|
+
(e.currentTarget as HTMLElement).style.boxShadow = `0 12px 36px ${t.buttonColor}77`;
|
|
157
|
+
}
|
|
158
|
+
}}
|
|
159
|
+
onMouseLeave={e => {
|
|
160
|
+
(e.currentTarget as HTMLElement).style.transform = isOpen ? 'scale(0.94)' : 'scale(1)';
|
|
161
|
+
(e.currentTarget as HTMLElement).style.boxShadow = `0 8px 28px ${t.buttonColor}55`;
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
{isOpen
|
|
165
|
+
? <CloseIcon color={t.buttonTextColor} />
|
|
166
|
+
: <><ChatBubbleIcon color={t.buttonTextColor} /><span>{t.buttonLabel}</span></>
|
|
167
|
+
}
|
|
168
|
+
</button>
|
|
169
|
+
|
|
170
|
+
{/* ── Chat Window ── */}
|
|
171
|
+
{isOpen && (
|
|
172
|
+
<div
|
|
173
|
+
style={{
|
|
174
|
+
position: 'fixed',
|
|
175
|
+
bottom: 86,
|
|
176
|
+
...posStyle,
|
|
177
|
+
zIndex: 9998,
|
|
178
|
+
width,
|
|
179
|
+
height,
|
|
180
|
+
maxWidth: 'calc(100vw - 32px)',
|
|
181
|
+
maxHeight: 'calc(100vh - 110px)',
|
|
182
|
+
backgroundColor: '#fff',
|
|
183
|
+
borderRadius: t.borderRadius,
|
|
184
|
+
boxShadow: '0 20px 70px rgba(0,0,0,0.2), 0 6px 20px rgba(0,0,0,0.08)',
|
|
185
|
+
display: 'flex',
|
|
186
|
+
flexDirection: 'column',
|
|
187
|
+
overflow: 'hidden',
|
|
188
|
+
fontFamily: t.fontFamily,
|
|
189
|
+
animation: 'cw-popIn 0.3s cubic-bezier(0.34,1.56,0.64,1)',
|
|
190
|
+
transition: 'width 0.3s ease, height 0.3s ease',
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
{/* Resize toggle button */}
|
|
194
|
+
{screen !== 'chat' && (
|
|
195
|
+
<button
|
|
196
|
+
onClick={() => setIsMaximized(m => !m)}
|
|
197
|
+
title={isMaximized ? 'Minimize' : 'Maximize'}
|
|
198
|
+
style={{
|
|
199
|
+
position: 'absolute', top: 12, right: 48, zIndex: 10,
|
|
200
|
+
background: 'rgba(255,255,255,0.22)', border: 'none', borderRadius: '50%',
|
|
201
|
+
width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
202
|
+
cursor: 'pointer', transition: 'background 0.15s',
|
|
203
|
+
}}
|
|
204
|
+
onMouseEnter={e => (e.currentTarget as HTMLElement).style.background = 'rgba(255,255,255,0.38)'}
|
|
205
|
+
onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = 'rgba(255,255,255,0.22)'}
|
|
206
|
+
>
|
|
207
|
+
{isMaximized
|
|
208
|
+
? <MinimizeIcon />
|
|
209
|
+
: <MaximizeIcon />
|
|
210
|
+
}
|
|
211
|
+
</button>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{/* Close button (top-right X) */}
|
|
215
|
+
{screen !== 'chat' && (
|
|
216
|
+
<button
|
|
217
|
+
onClick={() => setIsOpen(false)}
|
|
218
|
+
title="Close"
|
|
219
|
+
style={{
|
|
220
|
+
position: 'absolute', top: 12, right: 12, zIndex: 10,
|
|
221
|
+
background: 'rgba(255,255,255,0.22)', border: 'none', borderRadius: '50%',
|
|
222
|
+
width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
223
|
+
cursor: 'pointer',
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
|
227
|
+
<path d="M18 6L6 18M6 6l12 12" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"/>
|
|
228
|
+
</svg>
|
|
229
|
+
</button>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{/* ── Screen Router ── */}
|
|
233
|
+
<div className="cw-scrollbar" style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
|
234
|
+
|
|
235
|
+
{config.status === 'MAINTENANCE' && (
|
|
236
|
+
<MaintenanceView primaryColor={t.primaryColor} fontFamily={t.fontFamily} />
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{config.status === 'ACTIVE' && (
|
|
240
|
+
<>
|
|
241
|
+
{screen === 'home' && (
|
|
242
|
+
<HomeScreen config={config} theme={theme} onNavigate={handleCardClick} />
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{screen === 'user-list' && (
|
|
246
|
+
<UserListScreen
|
|
247
|
+
context={userListCtx}
|
|
248
|
+
users={users}
|
|
249
|
+
loading={loading}
|
|
250
|
+
error={error}
|
|
251
|
+
theme={theme}
|
|
252
|
+
onBack={handleBackFromUserList}
|
|
253
|
+
onSelectUser={handleSelectUser}
|
|
254
|
+
/>
|
|
255
|
+
)}
|
|
256
|
+
|
|
257
|
+
{screen === 'chat' && activeUser && (
|
|
258
|
+
<ChatScreen
|
|
259
|
+
activeUser={activeUser}
|
|
260
|
+
messages={messages}
|
|
261
|
+
onSend={sendMessage}
|
|
262
|
+
onBack={handleBackFromChat}
|
|
263
|
+
onClose={() => setIsOpen(false)}
|
|
264
|
+
theme={theme}
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
{screen === 'recent-chats' && (
|
|
269
|
+
<RecentChatsScreen
|
|
270
|
+
chats={[]}
|
|
271
|
+
theme={theme}
|
|
272
|
+
onSelectChat={handleSelectUser}
|
|
273
|
+
/>
|
|
274
|
+
)}
|
|
275
|
+
|
|
276
|
+
{screen === 'tickets' && (
|
|
277
|
+
<TicketScreen
|
|
278
|
+
tickets={tickets}
|
|
279
|
+
theme={theme}
|
|
280
|
+
onRaiseTicket={handleRaiseTicket}
|
|
281
|
+
/>
|
|
282
|
+
)}
|
|
283
|
+
</>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{/* ── Bottom Tabs (hidden in chat screen) ── */}
|
|
288
|
+
{screen !== 'chat' && screen !== 'user-list' && config.status !== 'MAINTENANCE' && (
|
|
289
|
+
<BottomTabs
|
|
290
|
+
active={activeTab}
|
|
291
|
+
onChange={handleTabChange}
|
|
292
|
+
primaryColor={t.primaryColor}
|
|
293
|
+
fontFamily={t.fontFamily}
|
|
294
|
+
/>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
</>
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export default ChatWidget;
|
|
303
|
+
|
|
304
|
+
// ── Tiny SVG icons ─────────────────────────────────────────────────────────────
|
|
305
|
+
const ChatBubbleIcon: React.FC<{ color: string }> = ({ color }) => (
|
|
306
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
|
307
|
+
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"
|
|
308
|
+
stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
309
|
+
</svg>
|
|
310
|
+
);
|
|
311
|
+
const CloseIcon: React.FC<{ color: string }> = ({ color }) => (
|
|
312
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
313
|
+
<path d="M18 6L6 18M6 6l12 12" stroke={color} strokeWidth="2.5" strokeLinecap="round"/>
|
|
314
|
+
</svg>
|
|
315
|
+
);
|
|
316
|
+
const MaximizeIcon = () => (
|
|
317
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none">
|
|
318
|
+
<path d="M8 3H5a2 2 0 00-2 2v3M21 8V5a2 2 0 00-2-2h-3M3 16v3a2 2 0 002 2h3M16 21h3a2 2 0 002-2v-3"
|
|
319
|
+
stroke="#fff" strokeWidth="2.2" strokeLinecap="round"/>
|
|
320
|
+
</svg>
|
|
321
|
+
);
|
|
322
|
+
const MinimizeIcon = () => (
|
|
323
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none">
|
|
324
|
+
<path d="M8 3v5H3M21 8h-5V3M3 16h5v5M16 21v-5h5"
|
|
325
|
+
stroke="#fff" strokeWidth="2.2" strokeLinecap="round"/>
|
|
326
|
+
</svg>
|
|
327
|
+
);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChatConfig, ChatWidgetTheme, UserListContext } from '../../types';
|
|
3
|
+
import { mergeTheme } from '../../utils/theme';
|
|
4
|
+
|
|
5
|
+
interface HomeScreenProps {
|
|
6
|
+
config: ChatConfig;
|
|
7
|
+
theme?: ChatWidgetTheme;
|
|
8
|
+
onNavigate: (ctx: UserListContext | 'ticket') => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const HomeScreen: React.FC<HomeScreenProps> = ({ config, theme, onNavigate }) => {
|
|
12
|
+
const t = mergeTheme(theme);
|
|
13
|
+
const showSupport = config.chatType === 'SUPPORT' || config.chatType === 'BOTH';
|
|
14
|
+
const showConversation = config.chatType === 'CHAT' || config.chatType === 'BOTH';
|
|
15
|
+
|
|
16
|
+
const cards = [
|
|
17
|
+
showSupport && {
|
|
18
|
+
key: 'support' as UserListContext,
|
|
19
|
+
title: 'Need Support',
|
|
20
|
+
subtitle: 'We typically reply in a few minutes',
|
|
21
|
+
onClick: () => onNavigate('support'),
|
|
22
|
+
},
|
|
23
|
+
showConversation && {
|
|
24
|
+
key: 'conversation' as UserListContext,
|
|
25
|
+
title: 'New Conversation',
|
|
26
|
+
subtitle: 'With your colleague',
|
|
27
|
+
onClick: () => onNavigate('conversation'),
|
|
28
|
+
},
|
|
29
|
+
// Raise Ticket is always shown
|
|
30
|
+
{
|
|
31
|
+
key: 'ticket' as const,
|
|
32
|
+
title: 'Raise Ticket',
|
|
33
|
+
subtitle: 'For major changes',
|
|
34
|
+
onClick: () => onNavigate('ticket'),
|
|
35
|
+
},
|
|
36
|
+
].filter(Boolean) as Array<{ key: string; title: string; subtitle: string; onClick: () => void }>;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
40
|
+
{/* Teal hero header */}
|
|
41
|
+
<div
|
|
42
|
+
style={{
|
|
43
|
+
backgroundColor: t.primaryColor,
|
|
44
|
+
padding: '32px 24px 40px',
|
|
45
|
+
flexShrink: 0,
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<h1
|
|
49
|
+
style={{
|
|
50
|
+
margin: '0 0 8px',
|
|
51
|
+
fontSize: '28px',
|
|
52
|
+
fontWeight: 800,
|
|
53
|
+
color: '#fff',
|
|
54
|
+
letterSpacing: '-0.03em',
|
|
55
|
+
fontFamily: t.fontFamily,
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
Hi there 👋
|
|
59
|
+
</h1>
|
|
60
|
+
<p style={{ margin: 0, fontSize: '15px', color: 'rgba(255,255,255,0.85)', fontFamily: t.fontFamily }}>
|
|
61
|
+
Need help? start a conversation:
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Cards — float over the header with negative margin trick */}
|
|
66
|
+
<div
|
|
67
|
+
style={{
|
|
68
|
+
flex: 1,
|
|
69
|
+
overflowY: 'auto',
|
|
70
|
+
padding: '0 16px 16px',
|
|
71
|
+
marginTop: '-24px',
|
|
72
|
+
display: 'flex',
|
|
73
|
+
flexDirection: 'column',
|
|
74
|
+
gap: '10px',
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
{cards.map((card, i) => (
|
|
78
|
+
<button
|
|
79
|
+
key={card.key}
|
|
80
|
+
onClick={card.onClick}
|
|
81
|
+
style={{
|
|
82
|
+
width: '100%',
|
|
83
|
+
background: '#fff',
|
|
84
|
+
border: 'none',
|
|
85
|
+
borderRadius: '14px',
|
|
86
|
+
padding: '18px 20px',
|
|
87
|
+
display: 'flex',
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
justifyContent: 'space-between',
|
|
90
|
+
cursor: 'pointer',
|
|
91
|
+
textAlign: 'left',
|
|
92
|
+
boxShadow: '0 2px 12px rgba(0,0,0,0.09)',
|
|
93
|
+
transition: 'transform 0.15s ease, box-shadow 0.15s ease',
|
|
94
|
+
animation: `cw-fadeUp 0.35s ease both`,
|
|
95
|
+
animationDelay: `${i * 0.08}s`,
|
|
96
|
+
fontFamily: t.fontFamily,
|
|
97
|
+
}}
|
|
98
|
+
onMouseEnter={e => {
|
|
99
|
+
(e.currentTarget as HTMLElement).style.transform = 'translateY(-2px)';
|
|
100
|
+
(e.currentTarget as HTMLElement).style.boxShadow = '0 6px 20px rgba(0,0,0,0.13)';
|
|
101
|
+
}}
|
|
102
|
+
onMouseLeave={e => {
|
|
103
|
+
(e.currentTarget as HTMLElement).style.transform = 'translateY(0)';
|
|
104
|
+
(e.currentTarget as HTMLElement).style.boxShadow = '0 2px 12px rgba(0,0,0,0.09)';
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<div>
|
|
108
|
+
<div style={{ fontWeight: 700, fontSize: '15px', color: '#1a2332', marginBottom: '3px' }}>
|
|
109
|
+
{card.title}
|
|
110
|
+
</div>
|
|
111
|
+
<div style={{ fontSize: '13px', color: '#7b8fa1' }}>
|
|
112
|
+
{card.subtitle}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<SendArrow color={t.primaryColor} />
|
|
116
|
+
</button>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const SendArrow: React.FC<{ color: string }> = ({ color }) => (
|
|
124
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
|
|
125
|
+
<path
|
|
126
|
+
d="M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z"
|
|
127
|
+
stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
|
128
|
+
/>
|
|
129
|
+
</svg>
|
|
130
|
+
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface MaintenanceViewProps {
|
|
4
|
+
primaryColor: string;
|
|
5
|
+
fontFamily: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const MaintenanceView: React.FC<MaintenanceViewProps> = ({ primaryColor, fontFamily }) => (
|
|
9
|
+
<div style={{
|
|
10
|
+
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
|
11
|
+
justifyContent: 'center', height: '100%', padding: '32px',
|
|
12
|
+
fontFamily, textAlign: 'center', gap: 16,
|
|
13
|
+
}}>
|
|
14
|
+
<div style={{
|
|
15
|
+
width: 72, height: 72, borderRadius: '50%',
|
|
16
|
+
backgroundColor: `${primaryColor}15`,
|
|
17
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
18
|
+
}}>
|
|
19
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none">
|
|
20
|
+
<path d="M12 9v4M12 17h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"
|
|
21
|
+
stroke={primaryColor} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
22
|
+
</svg>
|
|
23
|
+
</div>
|
|
24
|
+
<h3 style={{ margin: 0, fontSize: '17px', fontWeight: 800, color: '#1a2332', letterSpacing: '-0.02em' }}>
|
|
25
|
+
Under Maintenance
|
|
26
|
+
</h3>
|
|
27
|
+
<p style={{ margin: 0, fontSize: '14px', color: '#7b8fa1', lineHeight: 1.6, maxWidth: 220 }}>
|
|
28
|
+
Chat is under maintenance. We'll be back shortly!
|
|
29
|
+
</p>
|
|
30
|
+
<span style={{
|
|
31
|
+
display: 'inline-flex', alignItems: 'center', gap: 6,
|
|
32
|
+
padding: '6px 14px', borderRadius: 20,
|
|
33
|
+
backgroundColor: '#fff3cd', color: '#856404',
|
|
34
|
+
fontSize: '12px', fontWeight: 700,
|
|
35
|
+
border: '1px solid #ffc10730',
|
|
36
|
+
}}>
|
|
37
|
+
<span style={{ width: 6, height: 6, borderRadius: '50%', backgroundColor: '#ffc107', display: 'inline-block' }} />
|
|
38
|
+
Temporarily Unavailable
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChatUser, ChatWidgetTheme } from '../../types';
|
|
3
|
+
import { mergeTheme } from '../../utils/theme';
|
|
4
|
+
|
|
5
|
+
interface RecentChat {
|
|
6
|
+
id: string;
|
|
7
|
+
user: ChatUser;
|
|
8
|
+
lastMessage: string;
|
|
9
|
+
lastTime: Date;
|
|
10
|
+
unread: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface RecentChatsScreenProps {
|
|
14
|
+
chats: RecentChat[];
|
|
15
|
+
theme?: ChatWidgetTheme;
|
|
16
|
+
onSelectChat: (user: ChatUser) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const RecentChatsScreen: React.FC<RecentChatsScreenProps> = ({ chats, theme, onSelectChat }) => {
|
|
20
|
+
const t = mergeTheme(theme);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
24
|
+
{/* Header */}
|
|
25
|
+
<div style={{
|
|
26
|
+
backgroundColor: t.primaryColor,
|
|
27
|
+
padding: '20px 20px 24px',
|
|
28
|
+
flexShrink: 0,
|
|
29
|
+
}}>
|
|
30
|
+
<h2 style={{ margin: 0, fontSize: '20px', fontWeight: 800, color: '#fff', fontFamily: t.fontFamily, letterSpacing: '-0.02em' }}>
|
|
31
|
+
Recent Chats
|
|
32
|
+
</h2>
|
|
33
|
+
<p style={{ margin: '4px 0 0', fontSize: '13px', color: 'rgba(255,255,255,0.8)', fontFamily: t.fontFamily }}>
|
|
34
|
+
Your conversation history
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div style={{ flex: 1, overflowY: 'auto' }}>
|
|
39
|
+
{chats.length === 0 ? (
|
|
40
|
+
<div style={{ padding: '50px 24px', textAlign: 'center', fontFamily: t.fontFamily }}>
|
|
41
|
+
<div style={{ fontSize: '36px', marginBottom: 12 }}>💬</div>
|
|
42
|
+
<div style={{ fontWeight: 700, color: '#1a2332', marginBottom: 6 }}>No chats yet</div>
|
|
43
|
+
<div style={{ fontSize: '13px', color: '#7b8fa1' }}>
|
|
44
|
+
Start a conversation from the home tab
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
) : (
|
|
48
|
+
chats.map((chat, i) => {
|
|
49
|
+
const initials = chat.user.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
|
50
|
+
const avatarColors = ['#1aaa96','#2563EB','#7C3AED','#D97706','#DC2626'];
|
|
51
|
+
const bg = avatarColors[chat.user.name.charCodeAt(0) % avatarColors.length];
|
|
52
|
+
const timeStr = new Date(chat.lastTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<button
|
|
56
|
+
key={chat.id}
|
|
57
|
+
onClick={() => onSelectChat(chat.user)}
|
|
58
|
+
style={{
|
|
59
|
+
width: '100%', padding: '14px 20px',
|
|
60
|
+
display: 'flex', alignItems: 'center', gap: '14px',
|
|
61
|
+
background: 'transparent', border: 'none',
|
|
62
|
+
borderBottom: '1px solid #f3f4f6',
|
|
63
|
+
cursor: 'pointer', textAlign: 'left',
|
|
64
|
+
fontFamily: t.fontFamily,
|
|
65
|
+
animation: `cw-fadeUp 0.3s ease both`,
|
|
66
|
+
animationDelay: `${i * 0.05}s`,
|
|
67
|
+
transition: 'background 0.15s',
|
|
68
|
+
}}
|
|
69
|
+
onMouseEnter={e => (e.currentTarget as HTMLElement).style.background = '#f8fdfc'}
|
|
70
|
+
onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = 'transparent'}
|
|
71
|
+
>
|
|
72
|
+
<div style={{
|
|
73
|
+
width: 46, height: 46, borderRadius: '50%', backgroundColor: bg,
|
|
74
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
75
|
+
color: '#fff', fontWeight: 700, fontSize: '15px', flexShrink: 0,
|
|
76
|
+
position: 'relative',
|
|
77
|
+
}}>
|
|
78
|
+
{initials}
|
|
79
|
+
{chat.unread > 0 && (
|
|
80
|
+
<span style={{
|
|
81
|
+
position: 'absolute', top: -2, right: -2,
|
|
82
|
+
width: 18, height: 18, borderRadius: '50%',
|
|
83
|
+
backgroundColor: '#ff4757', color: '#fff',
|
|
84
|
+
fontSize: '10px', fontWeight: 700,
|
|
85
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
86
|
+
border: '2px solid #fff',
|
|
87
|
+
}}>
|
|
88
|
+
{chat.unread}
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
93
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 3 }}>
|
|
94
|
+
<span style={{ fontWeight: 700, fontSize: '14px', color: '#1a2332' }}>{chat.user.name}</span>
|
|
95
|
+
<span style={{ fontSize: '11px', color: '#b0bec5' }}>{timeStr}</span>
|
|
96
|
+
</div>
|
|
97
|
+
<div style={{ fontSize: '13px', color: '#7b8fa1', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
98
|
+
{chat.lastMessage}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</button>
|
|
102
|
+
);
|
|
103
|
+
})
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
};
|