ajaxter-chat 3.0.3 β†’ 3.0.5

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.
@@ -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] ?? [];
@@ -366,7 +377,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
366
377
  <div className="cw-scroll" style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
367
378
 
368
379
  {screen === 'home' && (
369
- <HomeScreen config={widgetConfig} onNavigate={handleCardClick} />
380
+ <HomeScreen config={widgetConfig} onNavigate={handleCardClick} tickets={tickets} />
370
381
  )}
371
382
 
372
383
  {screen === 'user-list' && (
@@ -394,6 +405,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
394
405
  onReport={reportChat}
395
406
  onBlock={handleBlock}
396
407
  onStartCall={handleStartCall}
408
+ onNavAction={handleNavFromMenu}
397
409
  />
398
410
  )}
399
411
 
@@ -1,106 +1,266 @@
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 = config.chatType === 'CHAT' || config.chatType === 'BOTH';
14
+ const showChat = config.chatType === 'CHAT' || config.chatType === 'BOTH';
12
15
 
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 }>;
16
+ const continueItems = tickets.slice(0, 2);
17
+
18
+ const handleCallUs = () => {
19
+ const raw = config.supportPhone?.trim();
20
+ if (!raw) return;
21
+ window.location.href = `tel:${raw.replace(/\s/g, '')}`;
22
+ };
36
23
 
37
24
  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' }}>
25
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', overflow: 'hidden', background: '#fafbfc' }}>
26
+ <SlideNavMenu
27
+ open={menuOpen}
28
+ onClose={() => setMenuOpen(false)}
29
+ primaryColor={config.primaryColor}
30
+ chatType={config.chatType}
31
+ onSelect={onNavigate}
32
+ />
33
+
34
+ {/* Top bar β€” burger left */}
35
+ <div
36
+ style={{
37
+ flexShrink: 0,
38
+ padding: '14px 16px 10px',
39
+ display: 'flex',
40
+ alignItems: 'center',
41
+ gap: 12,
42
+ background: '#fff',
43
+ borderBottom: '1px solid #eef0f5',
44
+ }}
45
+ >
46
+ <button
47
+ type="button"
48
+ aria-label="Open menu"
49
+ onClick={() => setMenuOpen(true)}
50
+ style={{
51
+ width: 40,
52
+ height: 40,
53
+ borderRadius: 10,
54
+ border: 'none',
55
+ background: '#f1f5f9',
56
+ cursor: 'pointer',
57
+ display: 'flex',
58
+ flexDirection: 'column',
59
+ alignItems: 'center',
60
+ justifyContent: 'center',
61
+ gap: 5,
62
+ flexShrink: 0,
63
+ }}
64
+ >
65
+ <span style={{ width: 18, height: 2, background: '#334155', borderRadius: 1 }} />
66
+ <span style={{ width: 18, height: 2, background: '#334155', borderRadius: 1 }} />
67
+ <span style={{ width: 18, height: 2, background: '#334155', borderRadius: 1 }} />
68
+ </button>
69
+ </div>
70
+
71
+ <div className="cw-scroll" style={{ flex: 1, overflowY: 'auto', padding: '20px 18px 28px' }}>
72
+ {/* Title + description */}
73
+ <h1
74
+ style={{
75
+ margin: '0 0 8px',
76
+ fontSize: 24,
77
+ fontWeight: 800,
78
+ color: '#0f172a',
79
+ letterSpacing: '-0.03em',
80
+ lineHeight: 1.2,
81
+ }}
82
+ >
51
83
  {config.welcomeTitle}
52
84
  </h1>
53
- <p style={{ margin:0, fontSize:14, color:'rgba(255,255,255,0.85)', lineHeight:1.6 }}>
85
+ <p style={{ margin: '0 0 28px', fontSize: 14, color: '#64748b', lineHeight: 1.55 }}>
54
86
  {config.welcomeSubtitle}
55
87
  </p>
56
- </div>
57
88
 
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) => (
89
+ {/* Continue Conversations */}
90
+ <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>Continue Conversations</h2>
91
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 }}>
92
+ {continueItems.length > 0 ? (
93
+ continueItems.map(t => (
94
+ <button
95
+ key={t.id}
96
+ type="button"
97
+ onClick={() => onNavigate('ticket')}
98
+ style={{
99
+ width: '100%',
100
+ textAlign: 'left',
101
+ padding: '14px 16px',
102
+ borderRadius: 14,
103
+ border: 'none',
104
+ background: '#e0f2fe',
105
+ color: '#0369a1',
106
+ fontSize: 14,
107
+ fontWeight: 600,
108
+ cursor: 'pointer',
109
+ boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
110
+ }}
111
+ >
112
+ {t.title}
113
+ </button>
114
+ ))
115
+ ) : (
116
+ <>
117
+ <div
118
+ style={{
119
+ padding: '14px 16px',
120
+ borderRadius: 14,
121
+ background: '#e0f2fe',
122
+ color: '#64748b',
123
+ fontSize: 14,
124
+ fontWeight: 500,
125
+ }}
126
+ >
127
+ No open tickets yet
128
+ </div>
129
+ <div
130
+ style={{
131
+ padding: '14px 16px',
132
+ borderRadius: 14,
133
+ background: '#e0f2fe',
134
+ color: '#64748b',
135
+ fontSize: 14,
136
+ fontWeight: 500,
137
+ }}
138
+ >
139
+ Start via Raise ticket below
140
+ </div>
141
+ </>
142
+ )}
143
+ </div>
144
+
145
+ {/* Talk to our experts */}
146
+ <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>Talk to our experts</h2>
147
+
148
+ {showSupport && (
61
149
  <button
62
- key={card.key}
63
- onClick={card.onClick}
150
+ type="button"
151
+ onClick={() => onNavigate('support')}
64
152
  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',
153
+ width: '100%',
154
+ display: 'flex',
155
+ alignItems: 'center',
156
+ justifyContent: 'center',
157
+ gap: 10,
158
+ padding: '14px 18px',
159
+ marginBottom: showChat ? 10 : 14,
160
+ borderRadius: 14,
161
+ border: 'none',
162
+ background: '#ede9fe',
163
+ color: '#5b21b6',
164
+ fontSize: 15,
165
+ fontWeight: 700,
166
+ cursor: 'pointer',
167
+ boxShadow: '0 2px 8px rgba(91,33,182,0.12)',
72
168
  }}
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)';
169
+ >
170
+ <span style={{ fontSize: 18 }}>πŸ‘€</span>
171
+ Support
172
+ </button>
173
+ )}
174
+
175
+ {showChat && showSupport && (
176
+ <button
177
+ type="button"
178
+ onClick={() => onNavigate('conversation')}
179
+ style={{
180
+ width: '100%',
181
+ padding: '12px 16px',
182
+ marginBottom: 14,
183
+ borderRadius: 12,
184
+ border: '1.5px solid #e9d5ff',
185
+ background: '#fff',
186
+ color: '#6d28d9',
187
+ fontSize: 14,
188
+ fontWeight: 600,
189
+ cursor: 'pointer',
76
190
  }}
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)';
191
+ >
192
+ New Conversation
193
+ </button>
194
+ )}
195
+
196
+ {showChat && !showSupport && (
197
+ <button
198
+ type="button"
199
+ onClick={() => onNavigate('conversation')}
200
+ style={{
201
+ width: '100%',
202
+ display: 'flex',
203
+ alignItems: 'center',
204
+ justifyContent: 'center',
205
+ gap: 10,
206
+ padding: '14px 18px',
207
+ marginBottom: 14,
208
+ borderRadius: 14,
209
+ border: 'none',
210
+ background: '#ede9fe',
211
+ color: '#5b21b6',
212
+ fontSize: 15,
213
+ fontWeight: 700,
214
+ cursor: 'pointer',
80
215
  }}
81
216
  >
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} />
217
+ <span style={{ fontSize: 18 }}>πŸ’¬</span>
218
+ New Conversation
95
219
  </button>
96
- ))}
220
+ )}
221
+
222
+ <div
223
+ style={{
224
+ borderRadius: 18,
225
+ padding: '22px 20px 20px',
226
+ background: 'linear-gradient(145deg, #fce7f3 0%, #e9d5ff 45%, #ddd6fe 100%)',
227
+ position: 'relative',
228
+ overflow: 'hidden',
229
+ }}
230
+ >
231
+ <div style={{ position: 'absolute', top: -20, right: -20, width: 100, height: 100, borderRadius: '50%', background: 'rgba(255,255,255,0.35)' }} />
232
+ <p style={{ margin: '0 0 16px', fontSize: 15, fontWeight: 700, color: '#4c1d95', lineHeight: 1.45, position: 'relative' }}>
233
+ Need specialized help? Our teams are ready to assist you with any questions.
234
+ </p>
235
+ <button
236
+ type="button"
237
+ onClick={handleCallUs}
238
+ disabled={!config.supportPhone}
239
+ style={{
240
+ display: 'inline-flex',
241
+ alignItems: 'center',
242
+ gap: 8,
243
+ padding: '10px 18px',
244
+ borderRadius: 12,
245
+ border: 'none',
246
+ background: config.supportPhone ? config.primaryColor : '#94a3b8',
247
+ color: '#fff',
248
+ fontSize: 14,
249
+ fontWeight: 700,
250
+ cursor: config.supportPhone ? 'pointer' : 'not-allowed',
251
+ position: 'relative',
252
+ }}
253
+ >
254
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
255
+ <path
256
+ 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"
257
+ fill="#fff"
258
+ />
259
+ </svg>
260
+ Call Us
261
+ </button>
262
+ </div>
97
263
  </div>
98
264
  </div>
99
265
  );
100
266
  };
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,138 @@
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
+ onSelect: (ctx: UserListContext | 'ticket') => void;
10
+ /** When set, shows β€œBack to home” at the bottom (e.g. chat screen) */
11
+ onBackHome?: () => void;
12
+ }
13
+
14
+ export const SlideNavMenu: React.FC<SlideNavMenuProps> = ({
15
+ open,
16
+ onClose,
17
+ primaryColor,
18
+ chatType,
19
+ onSelect,
20
+ onBackHome,
21
+ }) => {
22
+ const showSupport = chatType === 'SUPPORT' || chatType === 'BOTH';
23
+ const showChat = chatType === 'CHAT' || chatType === 'BOTH';
24
+
25
+ const items: Array<{ key: UserListContext | 'ticket'; icon: string; title: string } | null> = [
26
+ showSupport ? { key: 'support', icon: 'πŸ› ', title: 'Need Support' } : null,
27
+ showChat ? { key: 'conversation', icon: 'πŸ’¬', title: 'New Conversation' } : null,
28
+ { key: 'ticket', icon: '🎫', title: 'Raise ticket' },
29
+ ];
30
+
31
+ if (!open) return null;
32
+
33
+ return (
34
+ <>
35
+ <button
36
+ type="button"
37
+ aria-label="Close menu"
38
+ onClick={onClose}
39
+ style={{
40
+ position: 'absolute',
41
+ inset: 0,
42
+ zIndex: 200,
43
+ background: 'rgba(15,23,42,0.45)',
44
+ border: 'none',
45
+ cursor: 'pointer',
46
+ animation: 'cw-fadeIn 0.2s ease',
47
+ }}
48
+ />
49
+ <style>{`@keyframes cw-fadeIn { from { opacity: 0; } to { opacity: 1; } }`}</style>
50
+ <nav
51
+ style={{
52
+ position: 'absolute',
53
+ top: 0,
54
+ left: 0,
55
+ bottom: 0,
56
+ width: 'min(300px, 88%)',
57
+ zIndex: 210,
58
+ background: '#fff',
59
+ boxShadow: '8px 0 32px rgba(0,0,0,0.12)',
60
+ display: 'flex',
61
+ flexDirection: 'column',
62
+ padding: '20px 0 16px',
63
+ animation: 'cw-slideNavIn 0.28s cubic-bezier(0.22, 1, 0.36, 1)',
64
+ }}
65
+ >
66
+ <style>{`@keyframes cw-slideNavIn { from { transform: translateX(-100%); } to { transform: translateX(0); } }`}</style>
67
+ <div style={{ padding: '0 20px 16px', borderBottom: '1px solid #eef0f5' }}>
68
+ <p style={{ margin: 0, fontSize: 13, fontWeight: 700, color: '#64748b', letterSpacing: '0.04em' }}>Menu</p>
69
+ </div>
70
+ <div style={{ flex: 1, overflowY: 'auto', padding: '12px 12px' }}>
71
+ {items.filter(Boolean).map(item => {
72
+ const it = item!;
73
+ return (
74
+ <button
75
+ key={it.key}
76
+ type="button"
77
+ onClick={() => {
78
+ onSelect(it.key);
79
+ onClose();
80
+ }}
81
+ style={{
82
+ width: '100%',
83
+ display: 'flex',
84
+ alignItems: 'center',
85
+ gap: 12,
86
+ padding: '14px 14px',
87
+ marginBottom: 6,
88
+ border: 'none',
89
+ borderRadius: 12,
90
+ background: '#f8fafc',
91
+ cursor: 'pointer',
92
+ textAlign: 'left',
93
+ fontSize: 15,
94
+ fontWeight: 600,
95
+ color: '#1e293b',
96
+ transition: 'background 0.15s',
97
+ }}
98
+ onMouseEnter={e => {
99
+ (e.currentTarget as HTMLButtonElement).style.background = `${primaryColor}12`;
100
+ }}
101
+ onMouseLeave={e => {
102
+ (e.currentTarget as HTMLButtonElement).style.background = '#f8fafc';
103
+ }}
104
+ >
105
+ <span style={{ fontSize: 20 }}>{it.icon}</span>
106
+ {it.title}
107
+ </button>
108
+ );
109
+ })}
110
+ </div>
111
+ {onBackHome && (
112
+ <div style={{ padding: '0 12px', borderTop: '1px solid #eef0f5', paddingTop: 12 }}>
113
+ <button
114
+ type="button"
115
+ onClick={() => {
116
+ onBackHome();
117
+ onClose();
118
+ }}
119
+ style={{
120
+ width: '100%',
121
+ padding: '12px 14px',
122
+ border: '1.5px solid #e2e8f0',
123
+ borderRadius: 12,
124
+ background: '#fff',
125
+ fontSize: 14,
126
+ fontWeight: 600,
127
+ color: '#475569',
128
+ cursor: 'pointer',
129
+ }}
130
+ >
131
+ ← Back to home
132
+ </button>
133
+ </div>
134
+ )}
135
+ </nav>
136
+ </>
137
+ );
138
+ };
@@ -1,6 +1,10 @@
1
1
  import { LocalEnvConfig, RemoteChatData } from '../types';
2
2
 
3
- /** Default JSON endpoint; override with REACT_APP_CHAT_CONFIG_URL / NEXT_PUBLIC_CHAT_CONFIG_URL */
3
+ /**
4
+ * Default JSON endpoint. Override with `REACT_APP_CHAT_CONFIG_URL` / `NEXT_PUBLIC_CHAT_CONFIG_URL`.
5
+ * If the remote host does not send CORS headers, set this to a same-origin path (e.g. `/api/chat-config`)
6
+ * and proxy the JSON from your server β€” see `examples/next-app-router-chat-proxy.ts`.
7
+ */
4
8
  const DEFAULT_CHAT_DATA_BASE = 'https://window.mscorpres.com/TEST/chatData.json';
5
9
  const DEMO_API_KEY = 'demo1234';
6
10
  const DEMO_WIDGET_ID = 'demo';
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';
@@ -9,6 +9,12 @@ 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;
12
18
  allowVoiceMessage: boolean;
13
19
  allowAttachment: boolean;
14
20
  allowEmoji: boolean;
@@ -69,7 +75,11 @@ export interface ChatMessage {
69
75
  status: 'sent' | 'delivered' | 'read';
70
76
  attachmentName?: string;
71
77
  attachmentSize?: string;
78
+ /** Blob URL for attachment download (local send) */
79
+ attachmentUrl?: string;
72
80
  voiceDuration?: number; // seconds
81
+ /** Blob URL for in-bubble audio playback (local recording) */
82
+ voiceUrl?: string;
73
83
  }
74
84
 
75
85
  // ─── Ticket ─────────────────────────────────────────────────────────────────