ajaxter-chat 1.0.3 → 3.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.
Files changed (75) hide show
  1. package/README.md +124 -241
  2. package/dist/components/BlockList/index.d.ts +10 -0
  3. package/dist/components/BlockList/index.js +33 -0
  4. package/dist/components/CallScreen/index.d.ts +13 -0
  5. package/dist/components/CallScreen/index.js +48 -0
  6. package/dist/components/ChatScreen/index.d.ts +19 -0
  7. package/dist/components/ChatScreen/index.js +168 -0
  8. package/dist/components/ChatWidget.d.ts +0 -24
  9. package/dist/components/ChatWidget.js +228 -43
  10. package/dist/components/EmojiPicker/index.d.ts +8 -0
  11. package/dist/components/EmojiPicker/index.js +18 -0
  12. package/dist/components/HomeScreen/index.d.ts +8 -0
  13. package/dist/components/HomeScreen/index.js +55 -0
  14. package/dist/components/MaintenanceView/index.d.ts +0 -1
  15. package/dist/components/MaintenanceView/index.js +13 -52
  16. package/dist/components/RecentChatsScreen/index.d.ts +17 -0
  17. package/dist/components/RecentChatsScreen/index.js +8 -0
  18. package/dist/components/Tabs/BottomTabs.d.ts +10 -0
  19. package/dist/components/Tabs/BottomTabs.js +34 -0
  20. package/dist/components/TicketScreen/index.d.ts +9 -0
  21. package/dist/components/TicketScreen/index.js +54 -0
  22. package/dist/components/UserListScreen/index.d.ts +11 -0
  23. package/dist/components/UserListScreen/index.js +35 -0
  24. package/dist/config/index.d.ts +3 -16
  25. package/dist/config/index.js +20 -103
  26. package/dist/hooks/useChat.d.ts +10 -9
  27. package/dist/hooks/useChat.js +22 -40
  28. package/dist/hooks/useRemoteConfig.d.ts +6 -0
  29. package/dist/hooks/useRemoteConfig.js +22 -0
  30. package/dist/hooks/useWebRTC.d.ts +11 -0
  31. package/dist/hooks/useWebRTC.js +112 -0
  32. package/dist/index.d.ts +16 -11
  33. package/dist/index.js +15 -16
  34. package/dist/types/index.d.ts +66 -38
  35. package/dist/utils/chat.d.ts +13 -0
  36. package/dist/utils/chat.js +62 -0
  37. package/dist/utils/theme.d.ts +3 -2
  38. package/dist/utils/theme.js +13 -21
  39. package/package.json +10 -20
  40. package/public/chatData.json +162 -0
  41. package/src/components/BlockList/index.tsx +94 -0
  42. package/src/components/CallScreen/index.tsx +144 -0
  43. package/src/components/ChatScreen/index.tsx +469 -0
  44. package/src/components/ChatWidget.tsx +471 -0
  45. package/src/components/EmojiPicker/index.tsx +48 -0
  46. package/src/components/HomeScreen/index.tsx +106 -0
  47. package/src/components/MaintenanceView/index.tsx +38 -0
  48. package/src/components/RecentChatsScreen/index.tsx +63 -0
  49. package/src/components/Tabs/BottomTabs.tsx +90 -0
  50. package/src/components/TicketScreen/index.tsx +124 -0
  51. package/src/components/UserListScreen/index.tsx +103 -0
  52. package/src/config/index.ts +40 -0
  53. package/src/hooks/useChat.ts +48 -0
  54. package/src/hooks/useRemoteConfig.ts +20 -0
  55. package/src/hooks/useWebRTC.ts +130 -0
  56. package/src/index.ts +29 -0
  57. package/src/types/index.ts +127 -0
  58. package/src/utils/chat.ts +70 -0
  59. package/src/utils/theme.ts +27 -0
  60. package/dist/components/BottomNav/index.d.ts +0 -10
  61. package/dist/components/BottomNav/index.js +0 -32
  62. package/dist/components/ChatBox/index.d.ts +0 -15
  63. package/dist/components/ChatBox/index.js +0 -228
  64. package/dist/components/ChatButton/index.d.ts +0 -9
  65. package/dist/components/ChatButton/index.js +0 -17
  66. package/dist/components/ChatWindow/index.d.ts +0 -10
  67. package/dist/components/ChatWindow/index.js +0 -286
  68. package/dist/components/HomeView/index.d.ts +0 -12
  69. package/dist/components/HomeView/index.js +0 -51
  70. package/dist/components/UserList/index.d.ts +0 -13
  71. package/dist/components/UserList/index.js +0 -136
  72. package/dist/hooks/useUsers.d.ts +0 -14
  73. package/dist/hooks/useUsers.js +0 -32
  74. package/dist/services/userService.d.ts +0 -7
  75. package/dist/services/userService.js +0 -18
@@ -0,0 +1,471 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { ChatWidgetProps, BottomTab, Screen, UserListContext, ChatUser, Ticket, RecentChat } from '../types';
5
+ import { loadLocalConfig } from '../config';
6
+ import { mergeTheme } from '../utils/theme';
7
+ import { useRemoteConfig } from '../hooks/useRemoteConfig';
8
+ import { useChat } from '../hooks/useChat';
9
+ import { useWebRTC } from '../hooks/useWebRTC';
10
+
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 { BlockListScreen } from './BlockList';
17
+ import { CallScreen } from './CallScreen';
18
+ import { MaintenanceView } from './MaintenanceView';
19
+ import { BottomTabs } from './Tabs/BottomTabs';
20
+
21
+ /* ─── Drawer width ─────────────────────────────────────────────────────────── */
22
+ const DRAWER_W_NORMAL = 380;
23
+ const DRAWER_W_MAX = 480;
24
+
25
+ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) => {
26
+ /* SSR guard */
27
+ const [mounted, setMounted] = useState(false);
28
+ useEffect(() => { setMounted(true); }, []);
29
+
30
+ /* Env config */
31
+ const { apiKey, widgetId } = loadLocalConfig();
32
+
33
+ /* Remote config */
34
+ const { data, loading: cfgLoading, error: cfgError } = useRemoteConfig(apiKey, widgetId);
35
+
36
+ /* Merged theme — remote config overrides defaults, local prop overrides both */
37
+ const theme = mergeTheme(
38
+ data?.widget ? { primaryColor: data.widget.primaryColor, buttonLabel: data.widget.buttonLabel, buttonPosition: data.widget.buttonPosition } : undefined,
39
+ localTheme
40
+ );
41
+
42
+ /* Drawer open state */
43
+ const [isOpen, setIsOpen] = useState(false);
44
+ const [isMaximized, setIsMaximized] = useState(false);
45
+ const [closing, setClosing] = useState(false); // for slide-out animation
46
+
47
+ /* Navigation */
48
+ const [activeTab, setActiveTab] = useState<BottomTab>('home');
49
+ const [screen, setScreen] = useState<Screen>('home');
50
+ const [userListCtx, setUserListCtx] = useState<UserListContext>('support');
51
+
52
+ /* App state */
53
+ const [tickets, setTickets] = useState<Ticket[]>(data?.sampleTickets ?? []);
54
+ const [recentChats, setRecentChats] = useState<RecentChat[]>([]);
55
+ const [blockedUids, setBlockedUids] = useState<string[]>(data?.blockedUsers ?? []);
56
+
57
+ /* Sync remote data into local state once loaded */
58
+ useEffect(() => {
59
+ if (data) {
60
+ setTickets(data.sampleTickets);
61
+ setBlockedUids(data.blockedUsers);
62
+ // Seed recent chats from sample chats
63
+ const all = [...(data.developers ?? []), ...(data.users ?? [])];
64
+ const recents: RecentChat[] = Object.entries(data.sampleChats).map(([uid, msgs]) => {
65
+ const user = all.find(u => u.uid === uid);
66
+ if (!user || msgs.length === 0) return null;
67
+ const last = msgs[msgs.length - 1];
68
+ return {
69
+ id: `rc_${uid}`,
70
+ user,
71
+ lastMessage: last.text,
72
+ lastTime: last.timestamp,
73
+ unread: Math.floor(Math.random() * 3),
74
+ isPaused: false,
75
+ };
76
+ }).filter(Boolean) as RecentChat[];
77
+ setRecentChats(recents);
78
+ }
79
+ }, [data]);
80
+
81
+ /* Chat hook */
82
+ const {
83
+ messages, activeUser, isPaused, isReported,
84
+ selectUser, sendMessage, togglePause, reportChat, clearChat, setMessages,
85
+ } = useChat();
86
+
87
+ /* WebRTC hook */
88
+ const { session: callSession, localVideoRef, remoteVideoRef, startCall, endCall, toggleMute, toggleCamera } = useWebRTC();
89
+
90
+ /* ── Drawer open/close with slide animation ───────────────────────────── */
91
+ const openDrawer = () => {
92
+ setClosing(false);
93
+ setIsOpen(true);
94
+ };
95
+
96
+ const closeDrawer = useCallback(() => {
97
+ setClosing(true);
98
+ setTimeout(() => {
99
+ setIsOpen(false);
100
+ setClosing(false);
101
+ setScreen('home');
102
+ setActiveTab('home');
103
+ clearChat();
104
+ }, 300);
105
+ }, [clearChat]);
106
+
107
+ /* ── Navigation ──────────────────────────────────────────────────────── */
108
+ const handleCardClick = useCallback((ctx: UserListContext | 'ticket') => {
109
+ if (ctx === 'ticket') {
110
+ setActiveTab('tickets');
111
+ setScreen('tickets');
112
+ } else {
113
+ setUserListCtx(ctx as UserListContext);
114
+ setScreen('user-list');
115
+ }
116
+ }, []);
117
+
118
+ const handleSelectUser = useCallback((user: ChatUser) => {
119
+ // Load history from sample chats if available
120
+ const history = data?.sampleChats[user.uid] ?? [];
121
+ selectUser(user, history);
122
+ setScreen('chat');
123
+ // Update recent chats
124
+ setRecentChats(prev => {
125
+ const exists = prev.find(r => r.user.uid === user.uid);
126
+ if (exists) return prev;
127
+ return [{ id: `rc_${user.uid}`, user, lastMessage: '', lastTime: new Date().toISOString(), unread: 0, isPaused: false }, ...prev];
128
+ });
129
+ }, [data, selectUser]);
130
+
131
+ const handleTabChange = useCallback((tab: BottomTab) => {
132
+ setActiveTab(tab);
133
+ setScreen(tab === 'home' ? 'home' : tab === 'chats' ? 'recent-chats' : 'tickets');
134
+ }, []);
135
+
136
+ /* ── Block/Unblock ───────────────────────────────────────────────────── */
137
+ const handleBlock = useCallback(() => {
138
+ if (!activeUser) return;
139
+ setBlockedUids(prev => [...prev, activeUser.uid]);
140
+ clearChat();
141
+ setScreen('home');
142
+ setActiveTab('home');
143
+ }, [activeUser, clearChat]);
144
+
145
+ const handleUnblock = useCallback((uid: string) => {
146
+ setBlockedUids(prev => prev.filter(id => id !== uid));
147
+ }, []);
148
+
149
+ /* ── Tickets ─────────────────────────────────────────────────────────── */
150
+ const handleRaiseTicket = useCallback((title: string, desc: string, priority: Ticket['priority']) => {
151
+ const t: Ticket = {
152
+ id: `TKT-${String(Date.now()).slice(-4)}`,
153
+ title, description: desc, status: 'open', priority,
154
+ createdAt: new Date().toISOString(),
155
+ updatedAt: new Date().toISOString(),
156
+ assignedTo: null,
157
+ };
158
+ setTickets(prev => [t, ...prev]);
159
+ }, []);
160
+
161
+ /* ── Pause sync back into recent chats ──────────────────────────────── */
162
+ const handleTogglePause = useCallback(() => {
163
+ togglePause();
164
+ if (activeUser) {
165
+ setRecentChats(prev => prev.map(r => r.user.uid === activeUser.uid ? { ...r, isPaused: !isPaused } : r));
166
+ }
167
+ }, [togglePause, activeUser, isPaused]);
168
+
169
+ /* ── Call ────────────────────────────────────────────────────────────── */
170
+ const handleStartCall = useCallback((withVideo: boolean) => {
171
+ if (!activeUser) return;
172
+ startCall(activeUser, withVideo);
173
+ setScreen('call');
174
+ }, [activeUser, startCall]);
175
+
176
+ const handleEndCall = useCallback(() => {
177
+ endCall();
178
+ setScreen('chat');
179
+ }, [endCall]);
180
+
181
+ /* ── Derived ─────────────────────────────────────────────────────────── */
182
+ const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
183
+ const drawerW = isMaximized ? DRAWER_W_MAX : DRAWER_W_NORMAL;
184
+ const widgetConfig = data?.widget;
185
+ const primaryColor = theme.primaryColor;
186
+
187
+ const allUsers = data ? [...data.developers, ...data.users] : [];
188
+ const filteredUsers = screen === 'user-list'
189
+ ? allUsers.filter(u => userListCtx === 'support' ? u.type === 'developer' : u.type === 'user')
190
+ : [];
191
+ const blockedUsers = allUsers.filter(u => blockedUids.includes(u.uid));
192
+
193
+ /* Position */
194
+ const posStyle: React.CSSProperties = theme.buttonPosition === 'bottom-left'
195
+ ? { left: 24, right: 'auto' }
196
+ : { right: 24, left: 'auto' };
197
+
198
+ const drawerPosStyle: React.CSSProperties = theme.buttonPosition === 'bottom-left'
199
+ ? { left: 0, borderRadius: '0 16px 16px 0' }
200
+ : { right: 0, borderRadius: '16px 0 0 16px' };
201
+
202
+ /* ── Don't render until mounted (SSR safe) ──────────────────────────── */
203
+ if (!mounted) return null;
204
+
205
+ return (
206
+ <>
207
+ {/* ── Global styles ── */}
208
+ <style>{`
209
+ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&display=swap');
210
+
211
+ .cw-root * { box-sizing: border-box; font-family: 'DM Sans', 'Segoe UI', sans-serif; }
212
+
213
+ @keyframes cw-slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
214
+ @keyframes cw-slideOutRight { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }
215
+ @keyframes cw-slideInLeft { from { transform: translateX(-100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
216
+ @keyframes cw-slideOutLeft { from { transform: translateX(0); opacity: 1; } to { transform: translateX(-100%); opacity: 0; } }
217
+ @keyframes cw-fadeUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
218
+ @keyframes cw-slideIn { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: translateX(0); } }
219
+ @keyframes cw-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
220
+ @keyframes cw-btnPop { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
221
+
222
+ .cw-scroll::-webkit-scrollbar { width: 4px; }
223
+ .cw-scroll::-webkit-scrollbar-track { background: transparent; }
224
+ .cw-scroll::-webkit-scrollbar-thumb { background: #e0e0e0; border-radius: 4px; }
225
+
226
+ .cw-drawer-enter { animation: ${theme.buttonPosition === 'bottom-left' ? 'cw-slideInLeft' : 'cw-slideInRight'} 0.32s cubic-bezier(0.22,1,0.36,1) both; }
227
+ .cw-drawer-exit { animation: ${theme.buttonPosition === 'bottom-left' ? 'cw-slideOutLeft' : 'cw-slideOutRight'} 0.28s cubic-bezier(0.55,0,1,0.45) both; }
228
+ `}</style>
229
+
230
+ {/* ── Floating Button ── */}
231
+ {!isOpen && (
232
+ <button
233
+ className="cw-root"
234
+ onClick={openDrawer}
235
+ aria-label={theme.buttonLabel}
236
+ style={{
237
+ position: 'fixed', bottom: 24, zIndex: 9999,
238
+ ...posStyle,
239
+ display: 'flex', alignItems: 'center', gap: 10,
240
+ padding: '13px 22px',
241
+ backgroundColor: theme.buttonColor,
242
+ color: theme.buttonTextColor,
243
+ border: 'none', borderRadius: 50,
244
+ cursor: 'pointer', fontSize: 15, fontWeight: 700,
245
+ boxShadow: `0 8px 28px ${theme.buttonColor}55`,
246
+ animation: 'cw-btnPop 0.4s cubic-bezier(0.34,1.56,0.64,1)',
247
+ transition: 'transform 0.2s, box-shadow 0.2s',
248
+ }}
249
+ onMouseEnter={e => {
250
+ (e.currentTarget as HTMLElement).style.transform = 'scale(1.06) translateY(-2px)';
251
+ (e.currentTarget as HTMLElement).style.boxShadow = `0 14px 36px ${theme.buttonColor}66`;
252
+ }}
253
+ onMouseLeave={e => {
254
+ (e.currentTarget as HTMLElement).style.transform = 'scale(1)';
255
+ (e.currentTarget as HTMLElement).style.boxShadow = `0 8px 28px ${theme.buttonColor}55`;
256
+ }}
257
+ >
258
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
259
+ <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"
260
+ stroke={theme.buttonTextColor} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
261
+ </svg>
262
+ <span>{theme.buttonLabel}</span>
263
+ </button>
264
+ )}
265
+
266
+ {/* ── Backdrop (mobile) ── */}
267
+ {isOpen && (
268
+ <div
269
+ onClick={closeDrawer}
270
+ style={{
271
+ position: 'fixed', inset: 0, zIndex: 9997,
272
+ backgroundColor: 'rgba(0,0,0,0.35)',
273
+ opacity: closing ? 0 : 1,
274
+ transition: 'opacity 0.3s',
275
+ }}
276
+ />
277
+ )}
278
+
279
+ {/* ── Drawer / Slider ── */}
280
+ {isOpen && (
281
+ <div
282
+ className={`cw-root ${closing ? 'cw-drawer-exit' : 'cw-drawer-enter'}`}
283
+ style={{
284
+ position: 'fixed',
285
+ top: 0,
286
+ bottom: 0,
287
+ ...drawerPosStyle,
288
+ zIndex: 9998,
289
+ width: drawerW,
290
+ maxWidth: '100vw',
291
+ backgroundColor: '#fff',
292
+ boxShadow: theme.buttonPosition === 'bottom-left'
293
+ ? '4px 0 40px rgba(0,0,0,0.18)'
294
+ : '-4px 0 40px rgba(0,0,0,0.18)',
295
+ display: 'flex',
296
+ flexDirection: 'column',
297
+ overflow: 'hidden',
298
+ transition: 'width 0.28s ease',
299
+ }}
300
+ >
301
+ {/* ── Loading state ── */}
302
+ {cfgLoading && (
303
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 16 }}>
304
+ <div style={{
305
+ width: 40, height: 40, borderRadius: '50%',
306
+ border: `3px solid ${primaryColor}30`,
307
+ borderTopColor: primaryColor,
308
+ animation: 'spin 0.8s linear infinite',
309
+ }} />
310
+ <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
311
+ <p style={{ fontSize: 14, color: '#7b8fa1' }}>Loading chat…</p>
312
+ </div>
313
+ )}
314
+
315
+ {/* ── Error state ── */}
316
+ {cfgError && !cfgLoading && (
317
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 12, padding: 32, textAlign: 'center' }}>
318
+ <div style={{ fontSize: 40 }}>⚠️</div>
319
+ <p style={{ fontWeight: 700, color: '#1a2332' }}>Could not load chat configuration</p>
320
+ <p style={{ fontSize: 13, color: '#7b8fa1', lineHeight: 1.6 }}>{cfgError}</p>
321
+ <button onClick={closeDrawer} style={{ padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }}>Close</button>
322
+ </div>
323
+ )}
324
+
325
+ {/* ── Main content ── */}
326
+ {!cfgLoading && !cfgError && widgetConfig && (
327
+ <>
328
+ {/* Resize + Close controls — shown outside chat/call screens */}
329
+ {screen !== 'chat' && screen !== 'call' && (
330
+ <div style={{
331
+ position: 'absolute', top: 12,
332
+ right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
333
+ left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
334
+ zIndex: 20, display: 'flex', gap: 6,
335
+ }}>
336
+ <CornerBtn onClick={() => setIsMaximized(m => !m)} title={isMaximized ? 'Minimize' : 'Maximize'}>
337
+ {isMaximized
338
+ ? <svg width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M8 3v5H3M21 8h-5V3M3 16h5v5M16 21v-5h5" stroke="#fff" strokeWidth="2.2" strokeLinecap="round"/></svg>
339
+ : <svg width="12" height="12" viewBox="0 0 24 24" fill="none"><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" stroke="#fff" strokeWidth="2.2" strokeLinecap="round"/></svg>
340
+ }
341
+ </CornerBtn>
342
+ <CornerBtn onClick={closeDrawer} title="Close">
343
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none">
344
+ <path d="M18 6L6 18M6 6l12 12" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"/>
345
+ </svg>
346
+ </CornerBtn>
347
+ </div>
348
+ )}
349
+
350
+ {/* ── MAINTENANCE ── */}
351
+ {widgetConfig.status === 'MAINTENANCE' && (
352
+ <MaintenanceView primaryColor={primaryColor} />
353
+ )}
354
+
355
+ {/* ── DISABLED ── */}
356
+ {widgetConfig.status === 'DISABLE' && (
357
+ <div style={{ display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',height:'100%',padding:32,textAlign:'center',gap:12 }}>
358
+ <div style={{ fontSize:40 }}>🔒</div>
359
+ <p style={{ fontWeight:700,color:'#1a2332' }}>Chat is disabled</p>
360
+ <button onClick={closeDrawer} style={{ padding:'9px 20px',borderRadius:10,border:'none',background:primaryColor,color:'#fff',cursor:'pointer',fontWeight:700 }}>Close</button>
361
+ </div>
362
+ )}
363
+
364
+ {/* ── ACTIVE ── */}
365
+ {widgetConfig.status === 'ACTIVE' && (
366
+ <div className="cw-scroll" style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
367
+
368
+ {screen === 'home' && (
369
+ <HomeScreen config={widgetConfig} onNavigate={handleCardClick} />
370
+ )}
371
+
372
+ {screen === 'user-list' && (
373
+ <UserListScreen
374
+ context={userListCtx}
375
+ users={filteredUsers}
376
+ primaryColor={primaryColor}
377
+ onBack={() => setScreen('home')}
378
+ onSelectUser={handleSelectUser}
379
+ />
380
+ )}
381
+
382
+ {screen === 'chat' && activeUser && (
383
+ <ChatScreen
384
+ activeUser={activeUser}
385
+ messages={messages}
386
+ config={widgetConfig}
387
+ isPaused={isPaused}
388
+ isReported={isReported}
389
+ isBlocked={isBlocked}
390
+ onSend={sendMessage}
391
+ onBack={() => { clearChat(); setScreen('home'); setActiveTab('home'); }}
392
+ onClose={closeDrawer}
393
+ onTogglePause={handleTogglePause}
394
+ onReport={reportChat}
395
+ onBlock={handleBlock}
396
+ onStartCall={handleStartCall}
397
+ />
398
+ )}
399
+
400
+ {screen === 'call' && callSession.peer && (
401
+ <CallScreen
402
+ session={callSession}
403
+ localVideoRef={localVideoRef}
404
+ remoteVideoRef={remoteVideoRef}
405
+ onEnd={handleEndCall}
406
+ onToggleMute={toggleMute}
407
+ onToggleCamera={toggleCamera}
408
+ primaryColor={primaryColor}
409
+ />
410
+ )}
411
+
412
+ {screen === 'recent-chats' && (
413
+ <RecentChatsScreen
414
+ chats={recentChats}
415
+ config={widgetConfig}
416
+ onSelectChat={handleSelectUser}
417
+ />
418
+ )}
419
+
420
+ {screen === 'tickets' && (
421
+ <TicketScreen
422
+ tickets={tickets}
423
+ config={widgetConfig}
424
+ onRaiseTicket={handleRaiseTicket}
425
+ />
426
+ )}
427
+
428
+ {screen === 'block-list' && (
429
+ <BlockListScreen
430
+ blockedUsers={blockedUsers}
431
+ config={widgetConfig}
432
+ onUnblock={handleUnblock}
433
+ onBack={() => { setScreen('home'); setActiveTab('home'); }}
434
+ />
435
+ )}
436
+ </div>
437
+ )}
438
+
439
+ {/* ── Bottom Tabs (hidden during chat/call/user-list/block-list) ── */}
440
+ {widgetConfig.status === 'ACTIVE' &&
441
+ screen !== 'chat' &&
442
+ screen !== 'call' &&
443
+ screen !== 'user-list' &&
444
+ screen !== 'block-list' && (
445
+ <BottomTabs
446
+ active={activeTab}
447
+ onChange={handleTabChange}
448
+ primaryColor={primaryColor}
449
+ onBlockList={() => setScreen('block-list')}
450
+ />
451
+ )}
452
+ </>
453
+ )}
454
+ </div>
455
+ )}
456
+ </>
457
+ );
458
+ };
459
+
460
+ export default ChatWidget;
461
+
462
+ /* ── Tiny corner button ────────────────────────────────────────────────────── */
463
+ const CornerBtn: React.FC<{ onClick: () => void; title: string; children: React.ReactNode }> = ({ onClick, title, children }) => (
464
+ <button onClick={onClick} title={title} style={{
465
+ width: 26, height: 26, borderRadius: '50%',
466
+ background: 'rgba(0,0,0,0.25)', border: 'none',
467
+ display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
468
+ }}>
469
+ {children}
470
+ </button>
471
+ );
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+
3
+ const EMOJIS = [
4
+ '😀','😂','😊','😍','🤔','😎','😢','😡',
5
+ '👍','👎','👏','🙏','🎉','❤️','🔥','✅',
6
+ '🚀','💡','⚠️','🎫',
7
+ ];
8
+
9
+ interface EmojiPickerProps {
10
+ onSelect: (emoji: string) => void;
11
+ onClose: () => void;
12
+ primaryColor: string;
13
+ }
14
+
15
+ export const EmojiPicker: React.FC<EmojiPickerProps> = ({ onSelect, onClose, primaryColor }) => (
16
+ <div style={{
17
+ position:'absolute', bottom:'100%', right:0,
18
+ background:'#fff', borderRadius:14,
19
+ boxShadow:'0 8px 32px rgba(0,0,0,0.18)',
20
+ padding:'12px', zIndex:100,
21
+ animation:'cw-fadeUp 0.18s ease',
22
+ marginBottom:8,
23
+ }}>
24
+ {/* Header */}
25
+ <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:8 }}>
26
+ <span style={{ fontSize:11, fontWeight:700, color:'#7b8fa1', textTransform:'uppercase', letterSpacing:'0.06em' }}>
27
+ Emojis
28
+ </span>
29
+ <button onClick={onClose} style={{ background:'none', border:'none', cursor:'pointer', padding:2, color:'#7b8fa1', fontSize:14 }}>✕</button>
30
+ </div>
31
+ {/* Grid */}
32
+ <div style={{ display:'grid', gridTemplateColumns:'repeat(5, 1fr)', gap:4, width:200 }}>
33
+ {EMOJIS.map(e => (
34
+ <button
35
+ key={e}
36
+ onClick={() => { onSelect(e); onClose(); }}
37
+ style={{
38
+ background:'none', border:'none', cursor:'pointer',
39
+ fontSize:22, padding:'6px', borderRadius:8,
40
+ transition:'background 0.12s',
41
+ }}
42
+ onMouseEnter={el => (el.currentTarget as HTMLElement).style.background = `${primaryColor}15`}
43
+ onMouseLeave={el => (el.currentTarget as HTMLElement).style.background = 'none'}
44
+ >{e}</button>
45
+ ))}
46
+ </div>
47
+ </div>
48
+ );
@@ -0,0 +1,106 @@
1
+ import React from 'react';
2
+ import { WidgetConfig, UserListContext } from '../../types';
3
+
4
+ interface HomeScreenProps {
5
+ config: WidgetConfig;
6
+ onNavigate: (ctx: UserListContext | 'ticket') => void;
7
+ }
8
+
9
+ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate }) => {
10
+ const showSupport = config.chatType === 'SUPPORT' || config.chatType === 'BOTH';
11
+ const showChat = config.chatType === 'CHAT' || config.chatType === 'BOTH';
12
+
13
+ const cards = [
14
+ showSupport && {
15
+ key: 'support' as UserListContext,
16
+ icon: '🛠',
17
+ title: 'Need Support',
18
+ subtitle: 'We typically reply in a few minutes',
19
+ onClick: () => onNavigate('support'),
20
+ },
21
+ showChat && {
22
+ key: 'conversation' as UserListContext,
23
+ icon: '💬',
24
+ title: 'New Conversation',
25
+ subtitle: 'With your colleague',
26
+ onClick: () => onNavigate('conversation'),
27
+ },
28
+ {
29
+ key: 'ticket',
30
+ icon: '🎫',
31
+ title: 'Raise Ticket',
32
+ subtitle: 'For major changes',
33
+ onClick: () => onNavigate('ticket'),
34
+ },
35
+ ].filter(Boolean) as Array<{ key: string; icon: string; title: string; subtitle: string; onClick: () => void }>;
36
+
37
+ return (
38
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
39
+ {/* Hero */}
40
+ <div style={{
41
+ background: `linear-gradient(145deg, ${config.primaryColor}, ${config.primaryColor}dd)`,
42
+ padding: '36px 24px 52px',
43
+ flexShrink: 0,
44
+ position: 'relative',
45
+ overflow: 'hidden',
46
+ }}>
47
+ {/* Decorative circles */}
48
+ <div style={{ position:'absolute', top:-40, right:-40, width:140, height:140, borderRadius:'50%', background:'rgba(255,255,255,0.07)' }} />
49
+ <div style={{ position:'absolute', bottom:-20, left:-20, width:90, height:90, borderRadius:'50%', background:'rgba(255,255,255,0.05)' }} />
50
+ <h1 style={{ margin:'0 0 8px', fontSize:26, fontWeight:800, color:'#fff', letterSpacing:'-0.03em' }}>
51
+ {config.welcomeTitle}
52
+ </h1>
53
+ <p style={{ margin:0, fontSize:14, color:'rgba(255,255,255,0.85)', lineHeight:1.6 }}>
54
+ {config.welcomeSubtitle}
55
+ </p>
56
+ </div>
57
+
58
+ {/* Cards float over hero */}
59
+ <div style={{ flex:1, overflowY:'auto', padding:'0 16px 20px', marginTop:-28, display:'flex', flexDirection:'column', gap:10 }}>
60
+ {cards.map((card, i) => (
61
+ <button
62
+ key={card.key}
63
+ onClick={card.onClick}
64
+ style={{
65
+ width:'100%', background:'#fff', border:'none', borderRadius:14,
66
+ padding:'18px 20px', display:'flex', alignItems:'center',
67
+ justifyContent:'space-between', cursor:'pointer', textAlign:'left',
68
+ boxShadow:'0 2px 14px rgba(0,0,0,0.10)',
69
+ animation:`cw-fadeUp 0.35s ease both`,
70
+ animationDelay:`${i * 0.08}s`,
71
+ transition:'transform 0.15s, box-shadow 0.15s',
72
+ }}
73
+ onMouseEnter={e => {
74
+ (e.currentTarget as HTMLElement).style.transform = 'translateY(-2px)';
75
+ (e.currentTarget as HTMLElement).style.boxShadow = '0 8px 24px rgba(0,0,0,0.14)';
76
+ }}
77
+ onMouseLeave={e => {
78
+ (e.currentTarget as HTMLElement).style.transform = 'translateY(0)';
79
+ (e.currentTarget as HTMLElement).style.boxShadow = '0 2px 14px rgba(0,0,0,0.10)';
80
+ }}
81
+ >
82
+ <div style={{ display:'flex', alignItems:'center', gap:14 }}>
83
+ <div style={{
84
+ width:44, height:44, borderRadius:12,
85
+ backgroundColor:`${config.primaryColor}14`,
86
+ display:'flex', alignItems:'center', justifyContent:'center',
87
+ fontSize:20, flexShrink:0,
88
+ }}>{card.icon}</div>
89
+ <div>
90
+ <div style={{ fontWeight:700, fontSize:15, color:'#1a2332', marginBottom:2 }}>{card.title}</div>
91
+ <div style={{ fontSize:12, color:'#7b8fa1' }}>{card.subtitle}</div>
92
+ </div>
93
+ </div>
94
+ <SendArrow color={config.primaryColor} />
95
+ </button>
96
+ ))}
97
+ </div>
98
+ </div>
99
+ );
100
+ };
101
+
102
+ const SendArrow: React.FC<{ color: string }> = ({ color }) => (
103
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" style={{ flexShrink:0 }}>
104
+ <path d="M5 12h14M12 5l7 7-7 7" stroke={color} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
105
+ </svg>
106
+ );
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+
3
+ interface MaintenanceViewProps {
4
+ primaryColor: string;
5
+ }
6
+
7
+ export const MaintenanceView: React.FC<MaintenanceViewProps> = ({ primaryColor }) => (
8
+ <div style={{
9
+ display: 'flex', flexDirection: 'column', alignItems: 'center',
10
+ justifyContent: 'center', height: '100%', padding: '32px', textAlign: 'center', gap: 16,
11
+ }}>
12
+ <div style={{
13
+ width: 72, height: 72, borderRadius: '50%',
14
+ backgroundColor: `${primaryColor}15`,
15
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
16
+ }}>
17
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none">
18
+ <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"
19
+ stroke={primaryColor} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
20
+ </svg>
21
+ </div>
22
+ <h3 style={{ margin: 0, fontSize: 17, fontWeight: 800, color: '#1a2332', letterSpacing: '-0.02em' }}>
23
+ Under Maintenance
24
+ </h3>
25
+ <p style={{ margin: 0, fontSize: 14, color: '#7b8fa1', lineHeight: 1.6, maxWidth: 220 }}>
26
+ Chat is under maintenance. We'll be back shortly!
27
+ </p>
28
+ <span style={{
29
+ display: 'inline-flex', alignItems: 'center', gap: 6,
30
+ padding: '6px 14px', borderRadius: 20,
31
+ backgroundColor: '#fff3cd', color: '#856404',
32
+ fontSize: 12, fontWeight: 700, border: '1px solid #ffc10730',
33
+ }}>
34
+ <span style={{ width: 6, height: 6, borderRadius: '50%', backgroundColor: '#ffc107', display: 'inline-block' }} />
35
+ Temporarily Unavailable
36
+ </span>
37
+ </div>
38
+ );