ajaxter-chat 3.0.4 → 3.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ChatScreen/index.d.ts +6 -1
- package/dist/components/ChatScreen/index.js +180 -47
- package/dist/components/ChatWidget.js +43 -3
- package/dist/components/HomeScreen/index.d.ts +2 -1
- package/dist/components/HomeScreen/index.js +130 -51
- package/dist/components/SlideNavMenu.d.ts +14 -0
- package/dist/components/SlideNavMenu.js +74 -0
- package/dist/components/UserListScreen/index.d.ts +2 -0
- package/dist/components/UserListScreen/index.js +8 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types/index.d.ts +19 -0
- package/package.json +1 -1
- package/public/chatData.json +6 -0
- package/src/components/ChatScreen/index.tsx +378 -171
- package/src/components/ChatWidget.tsx +47 -3
- package/src/components/HomeScreen/index.tsx +244 -80
- package/src/components/SlideNavMenu.tsx +142 -0
- package/src/components/UserListScreen/index.tsx +10 -3
- package/src/index.ts +1 -0
- package/src/types/index.ts +19 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
-
import { ChatWidgetProps, BottomTab, Screen, UserListContext, ChatUser, Ticket, RecentChat } from '../types';
|
|
4
|
+
import { ChatWidgetProps, BottomTab, Screen, UserListContext, ChatUser, Ticket, RecentChat, ChatMessage } from '../types';
|
|
5
5
|
import { loadLocalConfig } from '../config';
|
|
6
6
|
import { mergeTheme } from '../utils/theme';
|
|
7
7
|
import { useRemoteConfig } from '../hooks/useRemoteConfig';
|
|
@@ -115,6 +115,17 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
115
115
|
}
|
|
116
116
|
}, []);
|
|
117
117
|
|
|
118
|
+
const handleNavFromMenu = useCallback((ctx: UserListContext | 'ticket') => {
|
|
119
|
+
clearChat();
|
|
120
|
+
if (ctx === 'ticket') {
|
|
121
|
+
setActiveTab('tickets');
|
|
122
|
+
setScreen('tickets');
|
|
123
|
+
} else {
|
|
124
|
+
setUserListCtx(ctx);
|
|
125
|
+
setScreen('user-list');
|
|
126
|
+
}
|
|
127
|
+
}, [clearChat]);
|
|
128
|
+
|
|
118
129
|
const handleSelectUser = useCallback((user: ChatUser) => {
|
|
119
130
|
// Load history from sample chats if available
|
|
120
131
|
const history = data?.sampleChats[user.uid] ?? [];
|
|
@@ -185,11 +196,40 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
185
196
|
const primaryColor = theme.primaryColor;
|
|
186
197
|
|
|
187
198
|
const allUsers = data ? [...data.developers, ...data.users] : [];
|
|
199
|
+
const viewerIsDev = widgetConfig?.viewerType === 'developer';
|
|
200
|
+
const viewerUid = widgetConfig?.viewerUid;
|
|
201
|
+
|
|
188
202
|
const filteredUsers = screen === 'user-list'
|
|
189
|
-
? allUsers.filter(u =>
|
|
203
|
+
? allUsers.filter(u => {
|
|
204
|
+
if (userListCtx === 'support') {
|
|
205
|
+
if (viewerIsDev) return u.type === 'user';
|
|
206
|
+
return u.type === 'developer';
|
|
207
|
+
}
|
|
208
|
+
if (viewerIsDev) {
|
|
209
|
+
return u.type === 'developer' && u.uid !== viewerUid;
|
|
210
|
+
}
|
|
211
|
+
return u.type === 'user';
|
|
212
|
+
})
|
|
190
213
|
: [];
|
|
214
|
+
|
|
215
|
+
const otherDevelopers = data?.developers.filter(d => d.uid !== viewerUid) ?? [];
|
|
191
216
|
const blockedUsers = allUsers.filter(u => blockedUids.includes(u.uid));
|
|
192
217
|
|
|
218
|
+
const handleTransferToDeveloper = useCallback((dev: ChatUser) => {
|
|
219
|
+
if (!activeUser || !widgetConfig) return;
|
|
220
|
+
const agent = widgetConfig.viewerName?.trim() || 'Agent';
|
|
221
|
+
const transferNote: ChatMessage = {
|
|
222
|
+
id: `tr_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
223
|
+
senderId: 'me',
|
|
224
|
+
receiverId: dev.uid,
|
|
225
|
+
text: `— ${agent} transferred this conversation from ${activeUser.name} to ${dev.name} —`,
|
|
226
|
+
timestamp: new Date().toISOString(),
|
|
227
|
+
type: 'text',
|
|
228
|
+
status: 'sent',
|
|
229
|
+
};
|
|
230
|
+
selectUser(dev, [...messages, transferNote]);
|
|
231
|
+
}, [activeUser, messages, selectUser, widgetConfig]);
|
|
232
|
+
|
|
193
233
|
/* Position */
|
|
194
234
|
const posStyle: React.CSSProperties = theme.buttonPosition === 'bottom-left'
|
|
195
235
|
? { left: 24, right: 'auto' }
|
|
@@ -366,7 +406,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
366
406
|
<div className="cw-scroll" style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
367
407
|
|
|
368
408
|
{screen === 'home' && (
|
|
369
|
-
<HomeScreen config={widgetConfig} onNavigate={handleCardClick} />
|
|
409
|
+
<HomeScreen config={widgetConfig} onNavigate={handleCardClick} tickets={tickets} />
|
|
370
410
|
)}
|
|
371
411
|
|
|
372
412
|
{screen === 'user-list' && (
|
|
@@ -374,6 +414,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
374
414
|
context={userListCtx}
|
|
375
415
|
users={filteredUsers}
|
|
376
416
|
primaryColor={primaryColor}
|
|
417
|
+
viewerType={widgetConfig.viewerType ?? 'user'}
|
|
377
418
|
onBack={() => setScreen('home')}
|
|
378
419
|
onSelectUser={handleSelectUser}
|
|
379
420
|
/>
|
|
@@ -394,6 +435,9 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
394
435
|
onReport={reportChat}
|
|
395
436
|
onBlock={handleBlock}
|
|
396
437
|
onStartCall={handleStartCall}
|
|
438
|
+
onNavAction={handleNavFromMenu}
|
|
439
|
+
otherDevelopers={otherDevelopers}
|
|
440
|
+
onTransferToDeveloper={handleTransferToDeveloper}
|
|
397
441
|
/>
|
|
398
442
|
)}
|
|
399
443
|
|
|
@@ -1,106 +1,270 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { WidgetConfig, UserListContext } from '../../types';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { WidgetConfig, UserListContext, Ticket } from '../../types';
|
|
3
|
+
import { SlideNavMenu } from '../SlideNavMenu';
|
|
3
4
|
|
|
4
5
|
interface HomeScreenProps {
|
|
5
6
|
config: WidgetConfig;
|
|
6
7
|
onNavigate: (ctx: UserListContext | 'ticket') => void;
|
|
8
|
+
tickets: Ticket[];
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate }) => {
|
|
11
|
+
export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tickets }) => {
|
|
12
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
10
13
|
const showSupport = config.chatType === 'SUPPORT' || config.chatType === 'BOTH';
|
|
11
|
-
const showChat
|
|
14
|
+
const showChat = config.chatType === 'CHAT' || config.chatType === 'BOTH';
|
|
15
|
+
const viewerIsDev = config.viewerType === 'developer';
|
|
12
16
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 }>;
|
|
17
|
+
const continueItems = tickets.slice(0, 2);
|
|
18
|
+
|
|
19
|
+
const handleCallUs = () => {
|
|
20
|
+
const raw = config.supportPhone?.trim();
|
|
21
|
+
if (!raw) return;
|
|
22
|
+
window.location.href = `tel:${raw.replace(/\s/g, '')}`;
|
|
23
|
+
};
|
|
36
24
|
|
|
37
25
|
return (
|
|
38
|
-
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
26
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', overflow: 'hidden', background: '#fafbfc' }}>
|
|
27
|
+
<SlideNavMenu
|
|
28
|
+
open={menuOpen}
|
|
29
|
+
onClose={() => setMenuOpen(false)}
|
|
30
|
+
primaryColor={config.primaryColor}
|
|
31
|
+
chatType={config.chatType}
|
|
32
|
+
viewerType={config.viewerType ?? 'user'}
|
|
33
|
+
onSelect={onNavigate}
|
|
34
|
+
/>
|
|
35
|
+
|
|
36
|
+
{/* Top bar — burger left */}
|
|
37
|
+
<div
|
|
38
|
+
style={{
|
|
39
|
+
flexShrink: 0,
|
|
40
|
+
padding: '14px 16px 10px',
|
|
41
|
+
display: 'flex',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
gap: 12,
|
|
44
|
+
background: '#fff',
|
|
45
|
+
borderBottom: '1px solid #eef0f5',
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
aria-label="Open menu"
|
|
51
|
+
onClick={() => setMenuOpen(true)}
|
|
52
|
+
style={{
|
|
53
|
+
width: 40,
|
|
54
|
+
height: 40,
|
|
55
|
+
borderRadius: 10,
|
|
56
|
+
border: 'none',
|
|
57
|
+
background: '#f1f5f9',
|
|
58
|
+
cursor: 'pointer',
|
|
59
|
+
display: 'flex',
|
|
60
|
+
flexDirection: 'column',
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
justifyContent: 'center',
|
|
63
|
+
gap: 5,
|
|
64
|
+
flexShrink: 0,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<span style={{ width: 18, height: 2, background: '#334155', borderRadius: 1 }} />
|
|
68
|
+
<span style={{ width: 18, height: 2, background: '#334155', borderRadius: 1 }} />
|
|
69
|
+
<span style={{ width: 18, height: 2, background: '#334155', borderRadius: 1 }} />
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="cw-scroll" style={{ flex: 1, overflowY: 'auto', padding: '20px 18px 28px' }}>
|
|
74
|
+
{/* Title + description */}
|
|
75
|
+
<h1
|
|
76
|
+
style={{
|
|
77
|
+
margin: '0 0 8px',
|
|
78
|
+
fontSize: 24,
|
|
79
|
+
fontWeight: 800,
|
|
80
|
+
color: '#0f172a',
|
|
81
|
+
letterSpacing: '-0.03em',
|
|
82
|
+
lineHeight: 1.2,
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
51
85
|
{config.welcomeTitle}
|
|
52
86
|
</h1>
|
|
53
|
-
<p style={{ margin:0, fontSize:14, color:'
|
|
87
|
+
<p style={{ margin: '0 0 28px', fontSize: 14, color: '#64748b', lineHeight: 1.55 }}>
|
|
54
88
|
{config.welcomeSubtitle}
|
|
55
89
|
</p>
|
|
56
|
-
</div>
|
|
57
90
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
{
|
|
91
|
+
{/* Continue Conversations */}
|
|
92
|
+
<h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>Continue Conversations</h2>
|
|
93
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 }}>
|
|
94
|
+
{continueItems.length > 0 ? (
|
|
95
|
+
continueItems.map(t => (
|
|
96
|
+
<button
|
|
97
|
+
key={t.id}
|
|
98
|
+
type="button"
|
|
99
|
+
onClick={() => onNavigate('ticket')}
|
|
100
|
+
style={{
|
|
101
|
+
width: '100%',
|
|
102
|
+
textAlign: 'left',
|
|
103
|
+
padding: '14px 16px',
|
|
104
|
+
borderRadius: 14,
|
|
105
|
+
border: 'none',
|
|
106
|
+
background: '#e0f2fe',
|
|
107
|
+
color: '#0369a1',
|
|
108
|
+
fontSize: 14,
|
|
109
|
+
fontWeight: 600,
|
|
110
|
+
cursor: 'pointer',
|
|
111
|
+
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
{t.title}
|
|
115
|
+
</button>
|
|
116
|
+
))
|
|
117
|
+
) : (
|
|
118
|
+
<>
|
|
119
|
+
<div
|
|
120
|
+
style={{
|
|
121
|
+
padding: '14px 16px',
|
|
122
|
+
borderRadius: 14,
|
|
123
|
+
background: '#e0f2fe',
|
|
124
|
+
color: '#64748b',
|
|
125
|
+
fontSize: 14,
|
|
126
|
+
fontWeight: 500,
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
No open tickets yet
|
|
130
|
+
</div>
|
|
131
|
+
<div
|
|
132
|
+
style={{
|
|
133
|
+
padding: '14px 16px',
|
|
134
|
+
borderRadius: 14,
|
|
135
|
+
background: '#e0f2fe',
|
|
136
|
+
color: '#64748b',
|
|
137
|
+
fontSize: 14,
|
|
138
|
+
fontWeight: 500,
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
Start via Raise ticket below
|
|
142
|
+
</div>
|
|
143
|
+
</>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{/* Talk to our experts / staff tools */}
|
|
148
|
+
<h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>
|
|
149
|
+
{viewerIsDev ? 'Support tools' : 'Talk to our experts'}
|
|
150
|
+
</h2>
|
|
151
|
+
|
|
152
|
+
{showSupport && (
|
|
61
153
|
<button
|
|
62
|
-
|
|
63
|
-
onClick={
|
|
154
|
+
type="button"
|
|
155
|
+
onClick={() => onNavigate('support')}
|
|
64
156
|
style={{
|
|
65
|
-
width:'100%',
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
157
|
+
width: '100%',
|
|
158
|
+
display: 'flex',
|
|
159
|
+
alignItems: 'center',
|
|
160
|
+
justifyContent: 'center',
|
|
161
|
+
gap: 10,
|
|
162
|
+
padding: '14px 18px',
|
|
163
|
+
marginBottom: showChat ? 10 : 14,
|
|
164
|
+
borderRadius: 14,
|
|
165
|
+
border: 'none',
|
|
166
|
+
background: '#ede9fe',
|
|
167
|
+
color: '#5b21b6',
|
|
168
|
+
fontSize: 15,
|
|
169
|
+
fontWeight: 700,
|
|
170
|
+
cursor: 'pointer',
|
|
171
|
+
boxShadow: '0 2px 8px rgba(91,33,182,0.12)',
|
|
72
172
|
}}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
173
|
+
>
|
|
174
|
+
<span style={{ fontSize: 18 }}>👤</span>
|
|
175
|
+
{viewerIsDev ? 'Provide Support' : 'Support'}
|
|
176
|
+
</button>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{showChat && showSupport && (
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
onClick={() => onNavigate('conversation')}
|
|
183
|
+
style={{
|
|
184
|
+
width: '100%',
|
|
185
|
+
padding: '12px 16px',
|
|
186
|
+
marginBottom: 14,
|
|
187
|
+
borderRadius: 12,
|
|
188
|
+
border: '1.5px solid #e9d5ff',
|
|
189
|
+
background: '#fff',
|
|
190
|
+
color: '#6d28d9',
|
|
191
|
+
fontSize: 14,
|
|
192
|
+
fontWeight: 600,
|
|
193
|
+
cursor: 'pointer',
|
|
76
194
|
}}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
195
|
+
>
|
|
196
|
+
{viewerIsDev ? 'Chat with a developer' : 'New Conversation'}
|
|
197
|
+
</button>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
{showChat && !showSupport && (
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
onClick={() => onNavigate('conversation')}
|
|
204
|
+
style={{
|
|
205
|
+
width: '100%',
|
|
206
|
+
display: 'flex',
|
|
207
|
+
alignItems: 'center',
|
|
208
|
+
justifyContent: 'center',
|
|
209
|
+
gap: 10,
|
|
210
|
+
padding: '14px 18px',
|
|
211
|
+
marginBottom: 14,
|
|
212
|
+
borderRadius: 14,
|
|
213
|
+
border: 'none',
|
|
214
|
+
background: '#ede9fe',
|
|
215
|
+
color: '#5b21b6',
|
|
216
|
+
fontSize: 15,
|
|
217
|
+
fontWeight: 700,
|
|
218
|
+
cursor: 'pointer',
|
|
80
219
|
}}
|
|
81
220
|
>
|
|
82
|
-
<
|
|
83
|
-
|
|
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} />
|
|
221
|
+
<span style={{ fontSize: 18 }}>💬</span>
|
|
222
|
+
New Conversation
|
|
95
223
|
</button>
|
|
96
|
-
)
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
<div
|
|
227
|
+
style={{
|
|
228
|
+
borderRadius: 18,
|
|
229
|
+
padding: '22px 20px 20px',
|
|
230
|
+
background: 'linear-gradient(145deg, #fce7f3 0%, #e9d5ff 45%, #ddd6fe 100%)',
|
|
231
|
+
position: 'relative',
|
|
232
|
+
overflow: 'hidden',
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
<div style={{ position: 'absolute', top: -20, right: -20, width: 100, height: 100, borderRadius: '50%', background: 'rgba(255,255,255,0.35)' }} />
|
|
236
|
+
<p style={{ margin: '0 0 16px', fontSize: 15, fontWeight: 700, color: '#4c1d95', lineHeight: 1.45, position: 'relative' }}>
|
|
237
|
+
Need specialized help? Our teams are ready to assist you with any questions.
|
|
238
|
+
</p>
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
onClick={handleCallUs}
|
|
242
|
+
disabled={!config.supportPhone}
|
|
243
|
+
style={{
|
|
244
|
+
display: 'inline-flex',
|
|
245
|
+
alignItems: 'center',
|
|
246
|
+
gap: 8,
|
|
247
|
+
padding: '10px 18px',
|
|
248
|
+
borderRadius: 12,
|
|
249
|
+
border: 'none',
|
|
250
|
+
background: config.supportPhone ? config.primaryColor : '#94a3b8',
|
|
251
|
+
color: '#fff',
|
|
252
|
+
fontSize: 14,
|
|
253
|
+
fontWeight: 700,
|
|
254
|
+
cursor: config.supportPhone ? 'pointer' : 'not-allowed',
|
|
255
|
+
position: 'relative',
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
|
259
|
+
<path
|
|
260
|
+
d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.68A2 2 0 012 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 14.92v2z"
|
|
261
|
+
fill="#fff"
|
|
262
|
+
/>
|
|
263
|
+
</svg>
|
|
264
|
+
Call Us
|
|
265
|
+
</button>
|
|
266
|
+
</div>
|
|
97
267
|
</div>
|
|
98
268
|
</div>
|
|
99
269
|
);
|
|
100
270
|
};
|
|
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,142 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChatType, UserListContext } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface SlideNavMenuProps {
|
|
5
|
+
open: boolean;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
primaryColor: string;
|
|
8
|
+
chatType: ChatType;
|
|
9
|
+
/** When `developer`, relabels the first two entries for staff */
|
|
10
|
+
viewerType?: 'user' | 'developer';
|
|
11
|
+
onSelect: (ctx: UserListContext | 'ticket') => void;
|
|
12
|
+
/** When set, shows “Back to home” at the bottom (e.g. chat screen) */
|
|
13
|
+
onBackHome?: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const SlideNavMenu: React.FC<SlideNavMenuProps> = ({
|
|
17
|
+
open,
|
|
18
|
+
onClose,
|
|
19
|
+
primaryColor,
|
|
20
|
+
chatType,
|
|
21
|
+
viewerType = 'user',
|
|
22
|
+
onSelect,
|
|
23
|
+
onBackHome,
|
|
24
|
+
}) => {
|
|
25
|
+
const showSupport = chatType === 'SUPPORT' || chatType === 'BOTH';
|
|
26
|
+
const showChat = chatType === 'CHAT' || chatType === 'BOTH';
|
|
27
|
+
const isStaff = viewerType === 'developer';
|
|
28
|
+
|
|
29
|
+
const items: Array<{ key: UserListContext | 'ticket'; icon: string; title: string } | null> = [
|
|
30
|
+
showSupport ? { key: 'support', icon: '🛠', title: isStaff ? 'Provide Support' : 'Need Support' } : null,
|
|
31
|
+
showChat ? { key: 'conversation', icon: '💬', title: isStaff ? 'Chat with developer' : 'New Conversation' } : null,
|
|
32
|
+
{ key: 'ticket', icon: '🎫', title: 'Raise ticket' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
if (!open) return null;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
aria-label="Close menu"
|
|
42
|
+
onClick={onClose}
|
|
43
|
+
style={{
|
|
44
|
+
position: 'absolute',
|
|
45
|
+
inset: 0,
|
|
46
|
+
zIndex: 200,
|
|
47
|
+
background: 'rgba(15,23,42,0.45)',
|
|
48
|
+
border: 'none',
|
|
49
|
+
cursor: 'pointer',
|
|
50
|
+
animation: 'cw-fadeIn 0.2s ease',
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
<style>{`@keyframes cw-fadeIn { from { opacity: 0; } to { opacity: 1; } }`}</style>
|
|
54
|
+
<nav
|
|
55
|
+
style={{
|
|
56
|
+
position: 'absolute',
|
|
57
|
+
top: 0,
|
|
58
|
+
left: 0,
|
|
59
|
+
bottom: 0,
|
|
60
|
+
width: 'min(300px, 88%)',
|
|
61
|
+
zIndex: 210,
|
|
62
|
+
background: '#fff',
|
|
63
|
+
boxShadow: '8px 0 32px rgba(0,0,0,0.12)',
|
|
64
|
+
display: 'flex',
|
|
65
|
+
flexDirection: 'column',
|
|
66
|
+
padding: '20px 0 16px',
|
|
67
|
+
animation: 'cw-slideNavIn 0.28s cubic-bezier(0.22, 1, 0.36, 1)',
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<style>{`@keyframes cw-slideNavIn { from { transform: translateX(-100%); } to { transform: translateX(0); } }`}</style>
|
|
71
|
+
<div style={{ padding: '0 20px 16px', borderBottom: '1px solid #eef0f5' }}>
|
|
72
|
+
<p style={{ margin: 0, fontSize: 13, fontWeight: 700, color: '#64748b', letterSpacing: '0.04em' }}>Menu</p>
|
|
73
|
+
</div>
|
|
74
|
+
<div style={{ flex: 1, overflowY: 'auto', padding: '12px 12px' }}>
|
|
75
|
+
{items.filter(Boolean).map(item => {
|
|
76
|
+
const it = item!;
|
|
77
|
+
return (
|
|
78
|
+
<button
|
|
79
|
+
key={it.key}
|
|
80
|
+
type="button"
|
|
81
|
+
onClick={() => {
|
|
82
|
+
onSelect(it.key);
|
|
83
|
+
onClose();
|
|
84
|
+
}}
|
|
85
|
+
style={{
|
|
86
|
+
width: '100%',
|
|
87
|
+
display: 'flex',
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
gap: 12,
|
|
90
|
+
padding: '14px 14px',
|
|
91
|
+
marginBottom: 6,
|
|
92
|
+
border: 'none',
|
|
93
|
+
borderRadius: 12,
|
|
94
|
+
background: '#f8fafc',
|
|
95
|
+
cursor: 'pointer',
|
|
96
|
+
textAlign: 'left',
|
|
97
|
+
fontSize: 15,
|
|
98
|
+
fontWeight: 600,
|
|
99
|
+
color: '#1e293b',
|
|
100
|
+
transition: 'background 0.15s',
|
|
101
|
+
}}
|
|
102
|
+
onMouseEnter={e => {
|
|
103
|
+
(e.currentTarget as HTMLButtonElement).style.background = `${primaryColor}12`;
|
|
104
|
+
}}
|
|
105
|
+
onMouseLeave={e => {
|
|
106
|
+
(e.currentTarget as HTMLButtonElement).style.background = '#f8fafc';
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<span style={{ fontSize: 20 }}>{it.icon}</span>
|
|
110
|
+
{it.title}
|
|
111
|
+
</button>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
</div>
|
|
115
|
+
{onBackHome && (
|
|
116
|
+
<div style={{ padding: '0 12px', borderTop: '1px solid #eef0f5', paddingTop: 12 }}>
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
onClick={() => {
|
|
120
|
+
onBackHome();
|
|
121
|
+
onClose();
|
|
122
|
+
}}
|
|
123
|
+
style={{
|
|
124
|
+
width: '100%',
|
|
125
|
+
padding: '12px 14px',
|
|
126
|
+
border: '1.5px solid #e2e8f0',
|
|
127
|
+
borderRadius: 12,
|
|
128
|
+
background: '#fff',
|
|
129
|
+
fontSize: 14,
|
|
130
|
+
fontWeight: 600,
|
|
131
|
+
color: '#475569',
|
|
132
|
+
cursor: 'pointer',
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
← Back to home
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
</nav>
|
|
140
|
+
</>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
@@ -6,15 +6,22 @@ interface UserListScreenProps {
|
|
|
6
6
|
context: UserListContext;
|
|
7
7
|
users: ChatUser[];
|
|
8
8
|
primaryColor: string;
|
|
9
|
+
/** `developer` = staff using the widget (lists customers vs teammates) */
|
|
10
|
+
viewerType?: 'user' | 'developer';
|
|
9
11
|
onBack: () => void;
|
|
10
12
|
onSelectUser: (user: ChatUser) => void;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export const UserListScreen: React.FC<UserListScreenProps> = ({
|
|
14
|
-
context, users, primaryColor, onBack, onSelectUser,
|
|
16
|
+
context, users, primaryColor, viewerType = 'user', onBack, onSelectUser,
|
|
15
17
|
}) => {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
+
const isStaff = viewerType === 'developer';
|
|
19
|
+
const title = context === 'support'
|
|
20
|
+
? (isStaff ? 'Provide Support' : 'Need Support')
|
|
21
|
+
: (isStaff ? 'Developers' : 'New Conversation');
|
|
22
|
+
const subtitle = context === 'support'
|
|
23
|
+
? (isStaff ? 'All chat users — choose who to help' : 'Choose a support agent')
|
|
24
|
+
: (isStaff ? 'Chat with another developer or coordinate handoff' : 'Choose a colleague');
|
|
18
25
|
|
|
19
26
|
return (
|
|
20
27
|
<div style={{ display:'flex', flexDirection:'column', height:'100%', animation:'cw-slideIn 0.22s ease' }}>
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { CallScreen } from './components/CallScreen';
|
|
|
9
9
|
export { MaintenanceView } from './components/MaintenanceView';
|
|
10
10
|
export { BottomTabs } from './components/Tabs/BottomTabs';
|
|
11
11
|
export { EmojiPicker } from './components/EmojiPicker';
|
|
12
|
+
export { SlideNavMenu } from './components/SlideNavMenu';
|
|
12
13
|
|
|
13
14
|
export { useChat } from './hooks/useChat';
|
|
14
15
|
export { useWebRTC } from './hooks/useWebRTC';
|
package/src/types/index.ts
CHANGED
|
@@ -9,6 +9,21 @@ export interface WidgetConfig {
|
|
|
9
9
|
buttonPosition: 'bottom-right' | 'bottom-left';
|
|
10
10
|
welcomeTitle: string;
|
|
11
11
|
welcomeSubtitle: string;
|
|
12
|
+
/** Shown in footer (e.g. branch / location name) */
|
|
13
|
+
branch?: string;
|
|
14
|
+
/** Optional label above branch (e.g. "Answers by") */
|
|
15
|
+
footerPoweredBy?: string;
|
|
16
|
+
/** Shown on home “Call Us” (tel: link) */
|
|
17
|
+
supportPhone?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Who is using the widget. `developer` = support staff: “Need Support” becomes “Provide Support”
|
|
20
|
+
* and lists customers; “New Conversation” lists other developers (excl. viewerUid).
|
|
21
|
+
*/
|
|
22
|
+
viewerType?: 'user' | 'developer';
|
|
23
|
+
/** Current user id when viewerType is developer — excluded from developer pick lists */
|
|
24
|
+
viewerUid?: string;
|
|
25
|
+
/** Display name for transfer notes (optional) */
|
|
26
|
+
viewerName?: string;
|
|
12
27
|
allowVoiceMessage: boolean;
|
|
13
28
|
allowAttachment: boolean;
|
|
14
29
|
allowEmoji: boolean;
|
|
@@ -69,7 +84,11 @@ export interface ChatMessage {
|
|
|
69
84
|
status: 'sent' | 'delivered' | 'read';
|
|
70
85
|
attachmentName?: string;
|
|
71
86
|
attachmentSize?: string;
|
|
87
|
+
/** Blob URL for attachment download (local send) */
|
|
88
|
+
attachmentUrl?: string;
|
|
72
89
|
voiceDuration?: number; // seconds
|
|
90
|
+
/** Blob URL for in-bubble audio playback (local recording) */
|
|
91
|
+
voiceUrl?: string;
|
|
73
92
|
}
|
|
74
93
|
|
|
75
94
|
// ─── Ticket ─────────────────────────────────────────────────────────────────
|