ajaxter-chat 2.0.1 → 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 (94) hide show
  1. package/README.md +119 -128
  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 +10 -3
  7. package/dist/components/ChatScreen/index.js +142 -57
  8. package/dist/components/ChatWidget.js +192 -98
  9. package/dist/components/EmojiPicker/index.d.ts +8 -0
  10. package/dist/components/EmojiPicker/index.js +18 -0
  11. package/dist/components/HomeScreen/index.d.ts +2 -3
  12. package/dist/components/HomeScreen/index.js +25 -41
  13. package/dist/components/MaintenanceView/index.d.ts +0 -1
  14. package/dist/components/MaintenanceView/index.js +4 -6
  15. package/dist/components/RecentChatsScreen/index.d.ts +4 -3
  16. package/dist/components/RecentChatsScreen/index.js +7 -37
  17. package/dist/components/Tabs/BottomTabs.d.ts +1 -1
  18. package/dist/components/Tabs/BottomTabs.js +25 -20
  19. package/dist/components/TicketScreen/index.d.ts +3 -3
  20. package/dist/components/TicketScreen/index.js +39 -56
  21. package/dist/components/UserListScreen/index.d.ts +2 -4
  22. package/dist/components/UserListScreen/index.js +33 -62
  23. package/dist/config/index.d.ts +3 -3
  24. package/dist/config/index.js +18 -26
  25. package/dist/hooks/useChat.d.ts +8 -3
  26. package/dist/hooks/useChat.js +22 -18
  27. package/dist/hooks/useRemoteConfig.d.ts +6 -0
  28. package/dist/hooks/useRemoteConfig.js +22 -0
  29. package/dist/hooks/useWebRTC.d.ts +11 -0
  30. package/dist/hooks/useWebRTC.js +112 -0
  31. package/dist/index.d.ts +9 -5
  32. package/dist/index.js +8 -4
  33. package/dist/types/index.d.ts +62 -21
  34. package/dist/utils/chat.d.ts +13 -0
  35. package/dist/utils/chat.js +62 -0
  36. package/dist/utils/theme.d.ts +3 -1
  37. package/dist/utils/theme.js +14 -7
  38. package/package.json +4 -4
  39. package/public/chatData.json +162 -0
  40. package/src/components/BlockList/index.tsx +94 -0
  41. package/src/components/CallScreen/index.tsx +144 -0
  42. package/src/components/ChatScreen/index.tsx +403 -139
  43. package/src/components/ChatWidget.tsx +394 -250
  44. package/src/components/EmojiPicker/index.tsx +48 -0
  45. package/src/components/HomeScreen/index.tsx +58 -82
  46. package/src/components/MaintenanceView/index.tsx +6 -9
  47. package/src/components/RecentChatsScreen/index.tsx +51 -96
  48. package/src/components/Tabs/BottomTabs.tsx +45 -37
  49. package/src/components/TicketScreen/index.tsx +87 -133
  50. package/src/components/UserListScreen/index.tsx +75 -153
  51. package/src/config/index.ts +22 -28
  52. package/src/hooks/useChat.ts +31 -14
  53. package/src/hooks/useRemoteConfig.ts +20 -0
  54. package/src/hooks/useWebRTC.ts +130 -0
  55. package/src/index.ts +26 -15
  56. package/src/types/index.ts +85 -40
  57. package/src/utils/chat.ts +70 -0
  58. package/src/utils/theme.ts +18 -7
  59. package/dist/hooks/useUsers.d.ts +0 -7
  60. package/dist/hooks/useUsers.js +0 -26
  61. package/dist/services/userService.d.ts +0 -2
  62. package/dist/services/userService.js +0 -9
  63. package/dist/src/components/ChatScreen/index.d.ts +0 -12
  64. package/dist/src/components/ChatScreen/index.js +0 -83
  65. package/dist/src/components/ChatWidget.d.ts +0 -4
  66. package/dist/src/components/ChatWidget.js +0 -141
  67. package/dist/src/components/HomeScreen/index.d.ts +0 -9
  68. package/dist/src/components/HomeScreen/index.js +0 -71
  69. package/dist/src/components/MaintenanceView/index.d.ts +0 -7
  70. package/dist/src/components/MaintenanceView/index.js +0 -16
  71. package/dist/src/components/RecentChatsScreen/index.d.ts +0 -16
  72. package/dist/src/components/RecentChatsScreen/index.js +0 -38
  73. package/dist/src/components/Tabs/BottomTabs.d.ts +0 -10
  74. package/dist/src/components/Tabs/BottomTabs.js +0 -29
  75. package/dist/src/components/TicketScreen/index.d.ts +0 -9
  76. package/dist/src/components/TicketScreen/index.js +0 -71
  77. package/dist/src/components/UserListScreen/index.d.ts +0 -13
  78. package/dist/src/components/UserListScreen/index.js +0 -64
  79. package/dist/src/config/index.d.ts +0 -3
  80. package/dist/src/config/index.js +0 -38
  81. package/dist/src/hooks/useChat.d.ts +0 -8
  82. package/dist/src/hooks/useChat.js +0 -26
  83. package/dist/src/hooks/useUsers.d.ts +0 -7
  84. package/dist/src/hooks/useUsers.js +0 -26
  85. package/dist/src/index.d.ts +0 -14
  86. package/dist/src/index.js +0 -13
  87. package/dist/src/services/userService.d.ts +0 -2
  88. package/dist/src/services/userService.js +0 -9
  89. package/dist/src/types/index.d.ts +0 -59
  90. package/dist/src/types/index.js +0 -1
  91. package/dist/src/utils/theme.d.ts +0 -3
  92. package/dist/src/utils/theme.js +0 -13
  93. package/src/hooks/useUsers.ts +0 -27
  94. package/src/services/userService.ts +0 -9
@@ -1,57 +1,110 @@
1
1
  'use client';
2
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';
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
6
  import { mergeTheme } from '../utils/theme';
7
- import { useUsers } from '../hooks/useUsers';
7
+ import { useRemoteConfig } from '../hooks/useRemoteConfig';
8
8
  import { useChat } from '../hooks/useChat';
9
+ import { useWebRTC } from '../hooks/useWebRTC';
9
10
 
10
- // Screens
11
11
  import { HomeScreen } from './HomeScreen';
12
12
  import { UserListScreen } from './UserListScreen';
13
13
  import { ChatScreen } from './ChatScreen';
14
14
  import { RecentChatsScreen } from './RecentChatsScreen';
15
15
  import { TicketScreen } from './TicketScreen';
16
+ import { BlockListScreen } from './BlockList';
17
+ import { CallScreen } from './CallScreen';
16
18
  import { MaintenanceView } from './MaintenanceView';
17
19
  import { BottomTabs } from './Tabs/BottomTabs';
18
20
 
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
- );
21
+ /* ─── Drawer width ─────────────────────────────────────────────────────────── */
22
+ const DRAWER_W_NORMAL = 380;
23
+ const DRAWER_W_MAX = 480;
42
24
 
43
- const { messages, activeUser, selectUser, sendMessage, clearChat } = useChat();
25
+ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) => {
26
+ /* SSR guard */
27
+ const [mounted, setMounted] = useState(false);
28
+ useEffect(() => { setMounted(true); }, []);
44
29
 
45
- // ── Navigation helpers ─────────────────────────────────────────────────────
46
- const goHome = useCallback(() => { setScreen('home'); setActiveTab('home'); }, []);
30
+ /* Env config */
31
+ const { apiKey, widgetId } = loadLocalConfig();
47
32
 
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
- }, []);
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
+ );
54
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 ──────────────────────────────────────────────────────── */
55
108
  const handleCardClick = useCallback((ctx: UserListContext | 'ticket') => {
56
109
  if (ctx === 'ticket') {
57
110
  setActiveTab('tickets');
@@ -63,235 +116,340 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme }) => {
63
116
  }, []);
64
117
 
65
118
  const handleSelectUser = useCallback((user: ChatUser) => {
66
- selectUser(user);
119
+ // Load history from sample chats if available
120
+ const history = data?.sampleChats[user.uid] ?? [];
121
+ selectUser(user, history);
67
122
  setScreen('chat');
68
- }, [selectUser]);
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]);
69
130
 
70
- const handleBackFromUserList = useCallback(() => {
71
- setScreen('home');
131
+ const handleTabChange = useCallback((tab: BottomTab) => {
132
+ setActiveTab(tab);
133
+ setScreen(tab === 'home' ? 'home' : tab === 'chats' ? 'recent-chats' : 'tickets');
72
134
  }, []);
73
135
 
74
- const handleBackFromChat = useCallback(() => {
136
+ /* ── Block/Unblock ───────────────────────────────────────────────────── */
137
+ const handleBlock = useCallback(() => {
138
+ if (!activeUser) return;
139
+ setBlockedUids(prev => [...prev, activeUser.uid]);
75
140
  clearChat();
76
- setScreen('user-list');
77
- }, [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
+ }, []);
78
148
 
79
- const handleRaiseTicket = useCallback((title: string, description: string) => {
149
+ /* ── Tickets ─────────────────────────────────────────────────────────── */
150
+ const handleRaiseTicket = useCallback((title: string, desc: string, priority: Ticket['priority']) => {
80
151
  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(),
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,
88
157
  };
89
158
  setTickets(prev => [t, ...prev]);
90
159
  }, []);
91
160
 
92
- // ── Sizing ─────────────────────────────────────────────────────────────────
93
- const normalW = 380;
94
- const normalH = 560;
95
- const maxW = 480;
96
- const maxH = 720;
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]);
97
168
 
98
- const width = isMaximized ? maxW : normalW;
99
- const height = isMaximized ? maxH : normalH;
169
+ /* ── Call ────────────────────────────────────────────────────────────── */
170
+ const handleStartCall = useCallback((withVideo: boolean) => {
171
+ if (!activeUser) return;
172
+ startCall(activeUser, withVideo);
173
+ setScreen('call');
174
+ }, [activeUser, startCall]);
100
175
 
101
- const posStyle: React.CSSProperties = t.buttonPosition === 'bottom-left'
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'
102
195
  ? { left: 24, right: 'auto' }
103
196
  : { right: 24, left: 'auto' };
104
197
 
105
- if (!isMounted) return null;
106
- if (config.status === 'DISABLE') return null;
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;
107
204
 
108
205
  return (
109
206
  <>
110
- {/* ── Global keyframes ── */}
207
+ {/* ── Global styles ── */}
111
208
  <style>{`
112
209
  @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; }
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; }
128
228
  `}</style>
129
229
 
130
230
  {/* ── 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 ── */}
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) ── */}
171
267
  {isOpen && (
172
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'}`}
173
283
  style={{
174
284
  position: 'fixed',
175
- bottom: 86,
176
- ...posStyle,
285
+ top: 0,
286
+ bottom: 0,
287
+ ...drawerPosStyle,
177
288
  zIndex: 9998,
178
- width,
179
- height,
180
- maxWidth: 'calc(100vw - 32px)',
181
- maxHeight: 'calc(100vh - 110px)',
289
+ width: drawerW,
290
+ maxWidth: '100vw',
182
291
  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)',
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)',
185
295
  display: 'flex',
186
296
  flexDirection: 'column',
187
297
  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',
298
+ transition: 'width 0.28s ease',
191
299
  }}
192
300
  >
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>
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>
212
313
  )}
213
314
 
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>
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>
230
323
  )}
231
324
 
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
- />
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
+ </>
295
453
  )}
296
454
  </div>
297
455
  )}
@@ -301,27 +459,13 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme }) => {
301
459
 
302
460
  export default ChatWidget;
303
461
 
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>
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>
327
471
  );