ajaxter-chat 3.0.9 → 3.0.11
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.js +256 -34
- package/dist/components/ChatWidget.js +21 -3
- package/dist/components/HomeScreen/index.d.ts +5 -1
- package/dist/components/HomeScreen/index.js +6 -4
- package/dist/components/RecentChatsScreen/index.d.ts +1 -0
- package/dist/components/RecentChatsScreen/index.js +28 -6
- package/dist/components/TicketFormScreen/index.js +19 -13
- package/dist/components/TicketScreen/index.d.ts +1 -0
- package/dist/components/TicketScreen/index.js +31 -9
- package/dist/components/UserListScreen/index.d.ts +4 -0
- package/dist/components/UserListScreen/index.js +40 -12
- package/dist/types/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/components/ChatScreen/index.tsx +365 -62
- package/src/components/ChatWidget.tsx +28 -6
- package/src/components/HomeScreen/index.tsx +12 -5
- package/src/components/RecentChatsScreen/index.tsx +97 -44
- package/src/components/TicketFormScreen/index.tsx +17 -6
- package/src/components/TicketScreen/index.tsx +63 -11
- package/src/components/UserListScreen/index.tsx +87 -15
- package/src/types/index.ts +2 -0
|
@@ -50,6 +50,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
50
50
|
const [chatReturnCtx, setChatReturnCtx] = useState<UserListContext>('conversation');
|
|
51
51
|
const [viewingTicketId, setViewingTicketId] = useState<string | null>(null);
|
|
52
52
|
const [messageSoundEnabled, setMessageSoundEnabledState] = useState(true);
|
|
53
|
+
/** Stagger list animation only when opening from home burger menu */
|
|
54
|
+
const [listEntranceAnimation, setListEntranceAnimation] = useState(false);
|
|
53
55
|
|
|
54
56
|
/* App state */
|
|
55
57
|
const [tickets, setTickets] = useState<Ticket[]>(data?.sampleTickets ?? []);
|
|
@@ -172,7 +174,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
172
174
|
}, [data?.widget]);
|
|
173
175
|
|
|
174
176
|
/* ── Navigation ──────────────────────────────────────────────────────── */
|
|
175
|
-
const handleCardClick = useCallback((ctx: UserListContext | 'ticket') => {
|
|
177
|
+
const handleCardClick = useCallback((ctx: UserListContext | 'ticket', options?: { fromMenu?: boolean }) => {
|
|
178
|
+
setListEntranceAnimation(!!options?.fromMenu);
|
|
176
179
|
if (ctx === 'ticket') {
|
|
177
180
|
setActiveTab('tickets');
|
|
178
181
|
setScreen('tickets');
|
|
@@ -183,6 +186,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
183
186
|
}, []);
|
|
184
187
|
|
|
185
188
|
const handleNavFromMenu = useCallback((ctx: UserListContext | 'ticket') => {
|
|
189
|
+
setListEntranceAnimation(false);
|
|
186
190
|
clearChat();
|
|
187
191
|
if (ctx === 'ticket') {
|
|
188
192
|
setActiveTab('tickets');
|
|
@@ -199,6 +203,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
199
203
|
}, []);
|
|
200
204
|
|
|
201
205
|
const handleSelectUser = useCallback((user: ChatUser, returnCtxOverride?: UserListContext) => {
|
|
206
|
+
setListEntranceAnimation(false);
|
|
202
207
|
setChatReturnCtx(returnCtxOverride ?? userListCtx);
|
|
203
208
|
const history = data?.sampleChats[user.uid] ?? [];
|
|
204
209
|
selectUser(user, history);
|
|
@@ -211,22 +216,31 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
211
216
|
}, [data, selectUser, userListCtx]);
|
|
212
217
|
|
|
213
218
|
const handleBackFromChat = useCallback(() => {
|
|
219
|
+
setListEntranceAnimation(false);
|
|
214
220
|
clearChat();
|
|
215
221
|
setUserListCtx(chatReturnCtx);
|
|
216
222
|
setScreen('user-list');
|
|
217
223
|
}, [clearChat, chatReturnCtx]);
|
|
218
224
|
|
|
219
225
|
const handleOpenTicket = useCallback((id: string) => {
|
|
226
|
+
setListEntranceAnimation(false);
|
|
220
227
|
setViewingTicketId(id);
|
|
221
228
|
setScreen('ticket-detail');
|
|
222
229
|
setActiveTab('tickets');
|
|
223
230
|
}, []);
|
|
224
231
|
|
|
225
232
|
const handleTabChange = useCallback((tab: BottomTab) => {
|
|
233
|
+
setListEntranceAnimation(false);
|
|
226
234
|
setActiveTab(tab);
|
|
227
235
|
setScreen(tab === 'home' ? 'home' : tab === 'chats' ? 'recent-chats' : 'tickets');
|
|
228
236
|
}, []);
|
|
229
237
|
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (!listEntranceAnimation) return;
|
|
240
|
+
const t = window.setTimeout(() => setListEntranceAnimation(false), 520);
|
|
241
|
+
return () => window.clearTimeout(t);
|
|
242
|
+
}, [listEntranceAnimation]);
|
|
243
|
+
|
|
230
244
|
/* ── Block/Unblock ───────────────────────────────────────────────────── */
|
|
231
245
|
const handleBlock = useCallback(() => {
|
|
232
246
|
if (!activeUser) return;
|
|
@@ -409,10 +423,10 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
409
423
|
</button>
|
|
410
424
|
)}
|
|
411
425
|
|
|
412
|
-
{/* ── Backdrop (
|
|
426
|
+
{/* ── Backdrop (visual only — does not close widget on click) ── */}
|
|
413
427
|
{isOpen && (
|
|
414
428
|
<div
|
|
415
|
-
|
|
429
|
+
aria-hidden
|
|
416
430
|
style={{
|
|
417
431
|
position: 'fixed', inset: 0, zIndex: 9997,
|
|
418
432
|
backgroundColor: 'rgba(0,0,0,0.35)',
|
|
@@ -517,9 +531,11 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
517
531
|
users={filteredUsers}
|
|
518
532
|
primaryColor={primaryColor}
|
|
519
533
|
viewerType={widgetConfig.viewerType ?? 'user'}
|
|
520
|
-
onBack={() => setScreen('home')}
|
|
534
|
+
onBack={() => { setListEntranceAnimation(false); setScreen('home'); }}
|
|
521
535
|
onSelectUser={handleSelectUser}
|
|
522
536
|
onBlockList={userListCtx === 'conversation' ? () => setScreen('block-list') : undefined}
|
|
537
|
+
useHomeHeader={userListCtx === 'support' && widgetConfig.viewerType !== 'developer'}
|
|
538
|
+
animateEntrance={listEntranceAnimation}
|
|
523
539
|
/>
|
|
524
540
|
)}
|
|
525
541
|
|
|
@@ -563,6 +579,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
563
579
|
chats={recentChats}
|
|
564
580
|
config={widgetConfig}
|
|
565
581
|
onSelectChat={u => handleSelectUser(u, listCtxForUser(u, viewerIsDev))}
|
|
582
|
+
animateEntrance={listEntranceAnimation}
|
|
566
583
|
/>
|
|
567
584
|
)}
|
|
568
585
|
|
|
@@ -570,8 +587,13 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
570
587
|
<TicketScreen
|
|
571
588
|
tickets={tickets}
|
|
572
589
|
config={widgetConfig}
|
|
573
|
-
onNewTicket={() => setScreen('ticket-new')}
|
|
574
|
-
onSelectTicket={id => {
|
|
590
|
+
onNewTicket={() => { setListEntranceAnimation(false); setScreen('ticket-new'); }}
|
|
591
|
+
onSelectTicket={id => {
|
|
592
|
+
setListEntranceAnimation(false);
|
|
593
|
+
setViewingTicketId(id);
|
|
594
|
+
setScreen('ticket-detail');
|
|
595
|
+
}}
|
|
596
|
+
animateEntrance={listEntranceAnimation}
|
|
575
597
|
/>
|
|
576
598
|
)}
|
|
577
599
|
|
|
@@ -3,9 +3,14 @@ import { WidgetConfig, UserListContext, Ticket } from '../../types';
|
|
|
3
3
|
import { SlideNavMenu } from '../SlideNavMenu';
|
|
4
4
|
import { truncateWords } from '../../utils/chat';
|
|
5
5
|
|
|
6
|
+
export interface HomeNavigateOptions {
|
|
7
|
+
/** When true, list screens play stagger animation (home burger menu only) */
|
|
8
|
+
fromMenu?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
interface HomeScreenProps {
|
|
7
12
|
config: WidgetConfig;
|
|
8
|
-
onNavigate: (ctx: UserListContext | 'ticket') => void;
|
|
13
|
+
onNavigate: (ctx: UserListContext | 'ticket', options?: HomeNavigateOptions) => void;
|
|
9
14
|
/** Open a specific pending ticket (full detail) */
|
|
10
15
|
onOpenTicket: (ticketId: string) => void;
|
|
11
16
|
tickets: Ticket[];
|
|
@@ -46,7 +51,9 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, onOp
|
|
|
46
51
|
primaryColor={config.primaryColor}
|
|
47
52
|
chatType={config.chatType}
|
|
48
53
|
viewerType={config.viewerType ?? 'user'}
|
|
49
|
-
onSelect={
|
|
54
|
+
onSelect={ctx => {
|
|
55
|
+
onNavigate(ctx, { fromMenu: true });
|
|
56
|
+
}}
|
|
50
57
|
/>
|
|
51
58
|
|
|
52
59
|
{/* Top bar — burger left */}
|
|
@@ -105,7 +112,7 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, onOp
|
|
|
105
112
|
</p>
|
|
106
113
|
|
|
107
114
|
{/* Continue Conversations */}
|
|
108
|
-
<h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>Continue
|
|
115
|
+
<h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>Continue with tickets</h2>
|
|
109
116
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 }}>
|
|
110
117
|
{pendingTickets.length > 0 ? (
|
|
111
118
|
pendingTickets.map(t => (
|
|
@@ -163,7 +170,7 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, onOp
|
|
|
163
170
|
|
|
164
171
|
{/* Talk to our experts / staff tools */}
|
|
165
172
|
<h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>
|
|
166
|
-
{viewerIsDev ? 'Support tools' : 'Talk to
|
|
173
|
+
{viewerIsDev ? 'Support tools' : 'Talk to support experts'}
|
|
167
174
|
</h2>
|
|
168
175
|
|
|
169
176
|
{showSupport && (
|
|
@@ -306,7 +313,7 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, onOp
|
|
|
306
313
|
fill="#fff"
|
|
307
314
|
/>
|
|
308
315
|
</svg>
|
|
309
|
-
|
|
316
|
+
Get Free Widget
|
|
310
317
|
</button>
|
|
311
318
|
</div>
|
|
312
319
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
|
2
2
|
import { ChatUser, WidgetConfig } from '../../types';
|
|
3
3
|
import { avatarColor, initials, formatTime } from '../../utils/chat';
|
|
4
4
|
|
|
@@ -10,54 +10,107 @@ interface RecentChatsScreenProps {
|
|
|
10
10
|
chats: RecentChat[];
|
|
11
11
|
config: WidgetConfig;
|
|
12
12
|
onSelectChat: (user: ChatUser) => void;
|
|
13
|
+
animateEntrance?: boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
function matchesChat(chat: RecentChat, q: string): boolean {
|
|
17
|
+
if (!q.trim()) return true;
|
|
18
|
+
const s = q.trim().toLowerCase();
|
|
19
|
+
return (
|
|
20
|
+
chat.user.name.toLowerCase().includes(s) ||
|
|
21
|
+
chat.lastMessage.toLowerCase().includes(s)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const RecentChatsScreen: React.FC<RecentChatsScreenProps> = ({
|
|
26
|
+
chats, config, onSelectChat, animateEntrance = false,
|
|
27
|
+
}) => {
|
|
28
|
+
const [query, setQuery] = useState('');
|
|
29
|
+
const searchRef = useRef<HTMLInputElement>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
searchRef.current?.focus();
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
const filtered = useMemo(() => chats.filter(c => matchesChat(c, query)), [chats, query]);
|
|
21
36
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
return (
|
|
38
|
+
<div style={{ display:'flex', flexDirection:'column', height:'100%' }}>
|
|
39
|
+
<div style={{ background:`linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`, padding:'18px 18px 14px', flexShrink:0 }}>
|
|
40
|
+
<h2 style={{ margin:0, fontSize:20, fontWeight:800, color:'#fff', letterSpacing:'-0.02em' }}>Recent Chats</h2>
|
|
41
|
+
<p style={{ margin:'3px 0 0', fontSize:12, color:'rgba(255,255,255,0.8)' }}>Your conversation history</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div style={{ padding: '10px 14px', background: '#fff', borderBottom: '1px solid #eef0f5', flexShrink: 0 }}>
|
|
45
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', borderRadius: 10, border: '1.5px solid #e5e7eb', background: '#f8fafc' }}>
|
|
46
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0, opacity: 0.55 }}>
|
|
47
|
+
<circle cx="11" cy="11" r="7" stroke="#64748b" strokeWidth="2" />
|
|
48
|
+
<path d="M20 20l-4-4" stroke="#64748b" strokeWidth="2" strokeLinecap="round" />
|
|
49
|
+
</svg>
|
|
50
|
+
<input
|
|
51
|
+
ref={searchRef}
|
|
52
|
+
type="search"
|
|
53
|
+
value={query}
|
|
54
|
+
onChange={e => setQuery(e.target.value)}
|
|
55
|
+
placeholder="Search chats…"
|
|
56
|
+
autoComplete="off"
|
|
57
|
+
aria-label="Search chats"
|
|
58
|
+
style={{
|
|
59
|
+
flex: 1,
|
|
60
|
+
minWidth: 0,
|
|
61
|
+
border: 'none',
|
|
62
|
+
outline: 'none',
|
|
63
|
+
background: 'transparent',
|
|
64
|
+
fontSize: 14,
|
|
65
|
+
padding: '10px 0',
|
|
66
|
+
fontFamily: 'inherit',
|
|
67
|
+
color: '#1a2332',
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
28
70
|
</div>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
onMouseLeave={e=>(e.currentTarget as HTMLElement).style.background='transparent'}
|
|
38
|
-
>
|
|
39
|
-
<div style={{ position:'relative', flexShrink:0 }}>
|
|
40
|
-
<div style={{ width:46, height:46, borderRadius:'50%', backgroundColor:avatarColor(chat.user.name), display:'flex', alignItems:'center', justifyContent:'center', color:'#fff', fontWeight:700, fontSize:15 }}>
|
|
41
|
-
{initials(chat.user.name)}
|
|
42
|
-
</div>
|
|
43
|
-
{chat.unread > 0 && (
|
|
44
|
-
<span style={{ position:'absolute', top:-2, right:-2, width:18, height:18, borderRadius:'50%', background:'#ef4444', color:'#fff', fontSize:10, fontWeight:700, display:'flex', alignItems:'center', justifyContent:'center', border:'2px solid #fff' }}>
|
|
45
|
-
{chat.unread}
|
|
46
|
-
</span>
|
|
47
|
-
)}
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div style={{ flex:1, overflowY:'auto' }}>
|
|
74
|
+
{filtered.length === 0 ? (
|
|
75
|
+
<div style={{ padding:'50px 24px', textAlign:'center' }}>
|
|
76
|
+
<div style={{ fontSize:36, marginBottom:10 }}>{query.trim() ? '🔍' : '💬'}</div>
|
|
77
|
+
<div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>{query.trim() ? 'No matches' : 'No chats yet'}</div>
|
|
78
|
+
<div style={{ fontSize:13, color:'#7b8fa1' }}>{query.trim() ? 'Try a different search' : 'Start a conversation from home'}</div>
|
|
48
79
|
</div>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
80
|
+
) : filtered.map((chat, i) => (
|
|
81
|
+
<button key={chat.id} onClick={() => onSelectChat(chat.user)} style={{
|
|
82
|
+
width:'100%', padding:'13px 16px', display:'flex', alignItems:'center', gap:13,
|
|
83
|
+
background:'transparent', border:'none', borderBottom:'1px solid #f0f2f5',
|
|
84
|
+
cursor:'pointer', textAlign:'left',
|
|
85
|
+
...(animateEntrance ? { animation: `cw-fadeUp 0.28s ease both`, animationDelay: `${i * 0.05}s` } : {}),
|
|
86
|
+
transition:'background 0.14s',
|
|
87
|
+
}}
|
|
88
|
+
onMouseEnter={e=>(e.currentTarget as HTMLElement).style.background='#f8faff'}
|
|
89
|
+
onMouseLeave={e=>(e.currentTarget as HTMLElement).style.background='transparent'}
|
|
90
|
+
>
|
|
91
|
+
<div style={{ position:'relative', flexShrink:0 }}>
|
|
92
|
+
<div style={{ width:46, height:46, borderRadius:'50%', backgroundColor:avatarColor(chat.user.name), display:'flex', alignItems:'center', justifyContent:'center', color:'#fff', fontWeight:700, fontSize:15 }}>
|
|
93
|
+
{initials(chat.user.name)}
|
|
94
|
+
</div>
|
|
95
|
+
{chat.unread > 0 && (
|
|
96
|
+
<span style={{ position:'absolute', top:-2, right:-2, width:18, height:18, borderRadius:'50%', background:'#ef4444', color:'#fff', fontSize:10, fontWeight:700, display:'flex', alignItems:'center', justifyContent:'center', border:'2px solid #fff' }}>
|
|
97
|
+
{chat.unread}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
53
100
|
</div>
|
|
54
|
-
<div style={{
|
|
55
|
-
|
|
56
|
-
|
|
101
|
+
<div style={{ flex:1, minWidth:0 }}>
|
|
102
|
+
<div style={{ display:'flex', justifyContent:'space-between', marginBottom:3 }}>
|
|
103
|
+
<span style={{ fontWeight:700, fontSize:14, color:'#1a2332' }}>{chat.user.name}</span>
|
|
104
|
+
<span style={{ fontSize:11, color:'#b0bec5' }}>{formatTime(chat.lastTime)}</span>
|
|
105
|
+
</div>
|
|
106
|
+
<div style={{ fontSize:13, color:'#7b8fa1', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', display:'flex', alignItems:'center', gap:5 }}>
|
|
107
|
+
{chat.isPaused && <span style={{ fontSize:10, background:'#fef3c7', color:'#92400e', padding:'1px 5px', borderRadius:4, fontWeight:700 }}>PAUSED</span>}
|
|
108
|
+
{chat.lastMessage}
|
|
109
|
+
</div>
|
|
57
110
|
</div>
|
|
58
|
-
</
|
|
59
|
-
|
|
60
|
-
|
|
111
|
+
</button>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
61
114
|
</div>
|
|
62
|
-
|
|
63
|
-
|
|
115
|
+
);
|
|
116
|
+
};
|
|
@@ -39,7 +39,7 @@ export const TicketFormScreen: React.FC<TicketFormScreenProps> = ({ config, onSu
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
return (
|
|
42
|
-
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: '#fff' }}>
|
|
42
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: '#fff', minHeight: 0 }}>
|
|
43
43
|
<div
|
|
44
44
|
style={{
|
|
45
45
|
background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
|
|
@@ -70,12 +70,12 @@ export const TicketFormScreen: React.FC<TicketFormScreenProps> = ({ config, onSu
|
|
|
70
70
|
</svg>
|
|
71
71
|
</button>
|
|
72
72
|
<div>
|
|
73
|
-
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 800, color: '#fff' }}>
|
|
74
|
-
<p style={{ margin: '4px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.85)' }}>
|
|
73
|
+
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 800, color: '#fff' }}>Raise a ticket</h2>
|
|
74
|
+
<p style={{ margin: '4px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.85)' }}>Please fill out the ticket form below and we will get back to you as soon as possible.</p>
|
|
75
75
|
</div>
|
|
76
76
|
</div>
|
|
77
77
|
|
|
78
|
-
<div className="cw-scroll" style={{ flex: 1, overflowY: 'auto', padding: '20px 18px' }}>
|
|
78
|
+
<div className="cw-scroll" style={{ flex: 1, overflowY: 'auto', padding: '20px 18px', minHeight: 0 }}>
|
|
79
79
|
<input
|
|
80
80
|
placeholder="Title *"
|
|
81
81
|
value={title}
|
|
@@ -93,7 +93,7 @@ export const TicketFormScreen: React.FC<TicketFormScreenProps> = ({ config, onSu
|
|
|
93
93
|
onFocus={e => (e.target.style.borderColor = config.primaryColor)}
|
|
94
94
|
onBlur={e => (e.target.style.borderColor = '#e5e7eb')}
|
|
95
95
|
/>
|
|
96
|
-
<div style={{ display: 'flex', gap: 8, marginTop: 12,
|
|
96
|
+
<div style={{ display: 'flex', gap: 8, marginTop: 12, paddingBottom: 8 }}>
|
|
97
97
|
{(['low', 'medium', 'high'] as Ticket['priority'][]).map(p => (
|
|
98
98
|
<button
|
|
99
99
|
key={p}
|
|
@@ -116,13 +116,24 @@ export const TicketFormScreen: React.FC<TicketFormScreenProps> = ({ config, onSu
|
|
|
116
116
|
</button>
|
|
117
117
|
))}
|
|
118
118
|
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div
|
|
122
|
+
style={{
|
|
123
|
+
flexShrink: 0,
|
|
124
|
+
padding: '12px 18px 18px',
|
|
125
|
+
borderTop: '1px solid #eef0f5',
|
|
126
|
+
background: '#fff',
|
|
127
|
+
boxShadow: '0 -4px 20px rgba(15,23,42,0.06)',
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
119
130
|
<button
|
|
120
131
|
type="button"
|
|
121
132
|
onClick={handleSubmit}
|
|
122
133
|
disabled={!title.trim()}
|
|
123
134
|
style={{
|
|
124
135
|
width: '100%',
|
|
125
|
-
padding: '
|
|
136
|
+
padding: '14px',
|
|
126
137
|
borderRadius: 10,
|
|
127
138
|
border: 'none',
|
|
128
139
|
background: title.trim() ? config.primaryColor : '#e5e7eb',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
|
2
2
|
import { Ticket, WidgetConfig } from '../../types';
|
|
3
3
|
|
|
4
4
|
interface TicketScreenProps {
|
|
@@ -6,9 +6,31 @@ interface TicketScreenProps {
|
|
|
6
6
|
config: WidgetConfig;
|
|
7
7
|
onNewTicket: () => void;
|
|
8
8
|
onSelectTicket:(id: string) => void;
|
|
9
|
+
animateEntrance?: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
function matchesTicket(t: Ticket, q: string): boolean {
|
|
13
|
+
if (!q.trim()) return true;
|
|
14
|
+
const s = q.trim().toLowerCase();
|
|
15
|
+
return (
|
|
16
|
+
t.title.toLowerCase().includes(s) ||
|
|
17
|
+
t.description.toLowerCase().includes(s) ||
|
|
18
|
+
t.id.toLowerCase().includes(s)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const TicketScreen: React.FC<TicketScreenProps> = ({
|
|
23
|
+
tickets, config, onNewTicket, onSelectTicket, animateEntrance = false,
|
|
24
|
+
}) => {
|
|
25
|
+
const [query, setQuery] = useState('');
|
|
26
|
+
const searchRef = useRef<HTMLInputElement>(null);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
searchRef.current?.focus();
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const filtered = useMemo(() => tickets.filter(t => matchesTicket(t, query)), [tickets, query]);
|
|
33
|
+
|
|
12
34
|
const sm: Record<Ticket['status'], { label: string; bg: string; color: string }> = {
|
|
13
35
|
open: { label:'Open', bg:`${config.primaryColor}14`, color: config.primaryColor },
|
|
14
36
|
'in-progress': { label:'In Progress', bg:'#fef3c7', color:'#d97706' },
|
|
@@ -26,10 +48,10 @@ export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets, config, onN
|
|
|
26
48
|
<div style={{ display:'flex', flexDirection:'column', height:'100%' }}>
|
|
27
49
|
<div style={{
|
|
28
50
|
background:`linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
|
|
29
|
-
padding:'18px 18px
|
|
51
|
+
padding:'18px 18px 14px', flexShrink:0, position:'relative',
|
|
30
52
|
}}>
|
|
31
|
-
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start' }}>
|
|
32
|
-
<div>
|
|
53
|
+
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', gap: 12 }}>
|
|
54
|
+
<div style={{ minWidth: 0 }}>
|
|
33
55
|
<h2 style={{ margin:0, fontSize:20, fontWeight:800, color:'#fff', letterSpacing:'-0.02em' }}>Tickets</h2>
|
|
34
56
|
<p style={{ margin:'3px 0 0', fontSize:12, color:'rgba(255,255,255,0.8)' }}>
|
|
35
57
|
{tickets.length} ticket{tickets.length!==1?'s':''} raised
|
|
@@ -39,27 +61,57 @@ export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets, config, onN
|
|
|
39
61
|
background:'rgba(255,255,255,0.22)', border:'none', borderRadius:20,
|
|
40
62
|
padding:'7px 14px', color:'#fff', fontWeight:700, fontSize:13,
|
|
41
63
|
cursor:'pointer', display:'flex', alignItems:'center', gap:5,
|
|
64
|
+
flexShrink: 0,
|
|
42
65
|
}}>
|
|
43
66
|
+ New
|
|
44
67
|
</button>
|
|
45
68
|
</div>
|
|
46
69
|
</div>
|
|
47
70
|
|
|
71
|
+
<div style={{ padding: '10px 14px', background: '#fff', borderBottom: '1px solid #eef0f5', flexShrink: 0 }}>
|
|
72
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', borderRadius: 10, border: '1.5px solid #e5e7eb', background: '#f8fafc' }}>
|
|
73
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0, opacity: 0.55 }}>
|
|
74
|
+
<circle cx="11" cy="11" r="7" stroke="#64748b" strokeWidth="2" />
|
|
75
|
+
<path d="M20 20l-4-4" stroke="#64748b" strokeWidth="2" strokeLinecap="round" />
|
|
76
|
+
</svg>
|
|
77
|
+
<input
|
|
78
|
+
ref={searchRef}
|
|
79
|
+
type="search"
|
|
80
|
+
value={query}
|
|
81
|
+
onChange={e => setQuery(e.target.value)}
|
|
82
|
+
placeholder="Search tickets…"
|
|
83
|
+
autoComplete="off"
|
|
84
|
+
aria-label="Search tickets"
|
|
85
|
+
style={{
|
|
86
|
+
flex: 1,
|
|
87
|
+
minWidth: 0,
|
|
88
|
+
border: 'none',
|
|
89
|
+
outline: 'none',
|
|
90
|
+
background: 'transparent',
|
|
91
|
+
fontSize: 14,
|
|
92
|
+
padding: '10px 0',
|
|
93
|
+
fontFamily: 'inherit',
|
|
94
|
+
color: '#1a2332',
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
48
100
|
<div style={{ flex:1, overflowY:'auto' }}>
|
|
49
|
-
{
|
|
101
|
+
{filtered.length === 0 ? (
|
|
50
102
|
<div style={{ padding:'50px 24px', textAlign:'center' }}>
|
|
51
|
-
<div style={{ fontSize:36, marginBottom:10 }}
|
|
52
|
-
<div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>No tickets yet</div>
|
|
53
|
-
<div style={{ fontSize:13, color:'#7b8fa1' }}>Raise a ticket for major issues</div>
|
|
103
|
+
<div style={{ fontSize:36, marginBottom:10 }}>{query.trim() ? '🔍' : '🎫'}</div>
|
|
104
|
+
<div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>{query.trim() ? 'No matches' : 'No tickets yet'}</div>
|
|
105
|
+
<div style={{ fontSize:13, color:'#7b8fa1' }}>{query.trim() ? 'Try a different search' : 'Raise a ticket for major issues'}</div>
|
|
54
106
|
</div>
|
|
55
|
-
) :
|
|
107
|
+
) : filtered.map((t, i) => (
|
|
56
108
|
<button
|
|
57
109
|
key={t.id}
|
|
58
110
|
type="button"
|
|
59
111
|
onClick={() => onSelectTicket(t.id)}
|
|
60
112
|
style={{
|
|
61
113
|
width:'100%', padding:'14px 16px', borderBottom:'1px solid #f0f2f5',
|
|
62
|
-
animation
|
|
114
|
+
...(animateEntrance ? { animation: `cw-fadeUp 0.3s ease both`, animationDelay: `${i * 0.05}s` } : {}),
|
|
63
115
|
background:'transparent', borderLeft:'none', borderRight:'none', borderTop:'none',
|
|
64
116
|
cursor:'pointer', textAlign:'left', fontFamily:'inherit',
|
|
65
117
|
}}
|