ajaxter-chat 3.0.8 → 3.0.10
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 +2 -0
- package/dist/components/ChatScreen/index.js +283 -34
- package/dist/components/ChatWidget.js +111 -15
- package/dist/components/HomeScreen/index.d.ts +2 -0
- package/dist/components/HomeScreen/index.js +2 -2
- package/dist/components/Tabs/BottomTabs.d.ts +0 -1
- package/dist/components/Tabs/BottomTabs.js +13 -20
- package/dist/components/TicketDetailScreen/index.d.ts +9 -0
- package/dist/components/TicketDetailScreen/index.js +46 -0
- package/dist/components/TicketFormScreen/index.d.ts +9 -0
- package/dist/components/TicketFormScreen/index.js +76 -0
- package/dist/components/TicketScreen/index.d.ts +2 -1
- package/dist/components/TicketScreen/index.js +8 -35
- package/dist/components/UserListScreen/index.d.ts +4 -0
- package/dist/components/UserListScreen/index.js +21 -3
- package/dist/types/index.d.ts +3 -1
- package/dist/utils/fileName.d.ts +2 -0
- package/dist/utils/fileName.js +7 -0
- package/dist/utils/messageSound.d.ts +4 -0
- package/dist/utils/messageSound.js +51 -0
- package/dist/utils/widgetSession.d.ts +13 -0
- package/dist/utils/widgetSession.js +24 -0
- package/package.json +1 -1
- package/src/components/ChatScreen/index.tsx +415 -58
- package/src/components/ChatWidget.tsx +140 -17
- package/src/components/HomeScreen/index.tsx +4 -2
- package/src/components/Tabs/BottomTabs.tsx +2 -22
- package/src/components/TicketDetailScreen/index.tsx +111 -0
- package/src/components/TicketFormScreen/index.tsx +151 -0
- package/src/components/TicketScreen/index.tsx +18 -58
- package/src/components/UserListScreen/index.tsx +51 -5
- package/src/types/index.ts +4 -0
- package/src/utils/fileName.ts +6 -0
- package/src/utils/messageSound.ts +47 -0
- package/src/utils/widgetSession.ts +34 -0
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
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';
|
|
8
8
|
import { useChat } from '../hooks/useChat';
|
|
9
9
|
import { useWebRTC } from '../hooks/useWebRTC';
|
|
10
|
+
import { saveSession, loadSession } from '../utils/widgetSession';
|
|
11
|
+
import { playMessageSound, getMessageSoundEnabled, setMessageSoundEnabled } from '../utils/messageSound';
|
|
10
12
|
|
|
11
13
|
import { HomeScreen } from './HomeScreen';
|
|
12
14
|
import { UserListScreen } from './UserListScreen';
|
|
13
15
|
import { ChatScreen } from './ChatScreen';
|
|
14
16
|
import { RecentChatsScreen } from './RecentChatsScreen';
|
|
15
17
|
import { TicketScreen } from './TicketScreen';
|
|
18
|
+
import { TicketDetailScreen } from './TicketDetailScreen';
|
|
19
|
+
import { TicketFormScreen } from './TicketFormScreen';
|
|
16
20
|
import { BlockListScreen } from './BlockList';
|
|
17
21
|
import { CallScreen } from './CallScreen';
|
|
18
22
|
import { MaintenanceView } from './MaintenanceView';
|
|
@@ -43,6 +47,9 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
43
47
|
const [activeTab, setActiveTab] = useState<BottomTab>('home');
|
|
44
48
|
const [screen, setScreen] = useState<Screen>('home');
|
|
45
49
|
const [userListCtx, setUserListCtx] = useState<UserListContext>('support');
|
|
50
|
+
const [chatReturnCtx, setChatReturnCtx] = useState<UserListContext>('conversation');
|
|
51
|
+
const [viewingTicketId, setViewingTicketId] = useState<string | null>(null);
|
|
52
|
+
const [messageSoundEnabled, setMessageSoundEnabledState] = useState(true);
|
|
46
53
|
|
|
47
54
|
/* App state */
|
|
48
55
|
const [tickets, setTickets] = useState<Ticket[]>(data?.sampleTickets ?? []);
|
|
@@ -88,16 +95,81 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
88
95
|
setIsOpen(true);
|
|
89
96
|
};
|
|
90
97
|
|
|
98
|
+
const persistWidgetState = useCallback(() => {
|
|
99
|
+
const w = data?.widget;
|
|
100
|
+
if (!w) return;
|
|
101
|
+
saveSession(w.id, {
|
|
102
|
+
screen,
|
|
103
|
+
activeTab,
|
|
104
|
+
userListCtx,
|
|
105
|
+
activeUserUid: activeUser?.uid ?? null,
|
|
106
|
+
messages,
|
|
107
|
+
viewingTicketId,
|
|
108
|
+
chatReturnCtx,
|
|
109
|
+
});
|
|
110
|
+
}, [data?.widget, screen, activeTab, userListCtx, activeUser?.uid, messages, viewingTicketId, chatReturnCtx]);
|
|
111
|
+
|
|
91
112
|
const closeDrawer = useCallback(() => {
|
|
113
|
+
persistWidgetState();
|
|
92
114
|
setClosing(true);
|
|
93
115
|
setTimeout(() => {
|
|
94
116
|
setIsOpen(false);
|
|
95
117
|
setClosing(false);
|
|
96
|
-
setScreen('home');
|
|
97
|
-
setActiveTab('home');
|
|
98
|
-
clearChat();
|
|
99
118
|
}, 300);
|
|
100
|
-
}, [
|
|
119
|
+
}, [persistWidgetState]);
|
|
120
|
+
|
|
121
|
+
const restoredRef = useRef(false);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!data?.widget || restoredRef.current) return;
|
|
124
|
+
const w = data.widget;
|
|
125
|
+
setMessageSoundEnabledState(getMessageSoundEnabled(w.id));
|
|
126
|
+
const p = loadSession(w.id);
|
|
127
|
+
if (p) {
|
|
128
|
+
setScreen(p.screen);
|
|
129
|
+
setActiveTab(p.activeTab);
|
|
130
|
+
setUserListCtx(p.userListCtx);
|
|
131
|
+
setViewingTicketId(p.viewingTicketId ?? null);
|
|
132
|
+
setChatReturnCtx(p.chatReturnCtx ?? 'conversation');
|
|
133
|
+
if (p.activeUserUid) {
|
|
134
|
+
const u = [...data.developers, ...data.users].find(x => x.uid === p.activeUserUid);
|
|
135
|
+
if (u) {
|
|
136
|
+
const hist = Array.isArray(p.messages) && p.messages.length
|
|
137
|
+
? p.messages
|
|
138
|
+
: (data.sampleChats[u.uid] ?? []);
|
|
139
|
+
selectUser(u, hist);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
restoredRef.current = true;
|
|
144
|
+
}, [data, selectUser]);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (!data?.widget) return;
|
|
148
|
+
persistWidgetState();
|
|
149
|
+
}, [data?.widget?.id, screen, activeTab, userListCtx, activeUser?.uid, messages, viewingTicketId, chatReturnCtx, persistWidgetState]);
|
|
150
|
+
|
|
151
|
+
const incomingSoundRef = useRef(0);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
incomingSoundRef.current = messages.length;
|
|
154
|
+
}, [activeUser?.uid]);
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (!messageSoundEnabled || !activeUser || !data?.widget) return;
|
|
158
|
+
if (messages.length < incomingSoundRef.current) {
|
|
159
|
+
incomingSoundRef.current = messages.length;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const added = messages.slice(incomingSoundRef.current);
|
|
163
|
+
incomingSoundRef.current = messages.length;
|
|
164
|
+
if (added.some(m => m.senderId !== 'me')) playMessageSound();
|
|
165
|
+
}, [messages, messageSoundEnabled, activeUser, data?.widget]);
|
|
166
|
+
|
|
167
|
+
const toggleMessageSound = useCallback((enabled: boolean) => {
|
|
168
|
+
const w = data?.widget;
|
|
169
|
+
if (!w) return;
|
|
170
|
+
setMessageSoundEnabled(w.id, enabled);
|
|
171
|
+
setMessageSoundEnabledState(enabled);
|
|
172
|
+
}, [data?.widget]);
|
|
101
173
|
|
|
102
174
|
/* ── Navigation ──────────────────────────────────────────────────────── */
|
|
103
175
|
const handleCardClick = useCallback((ctx: UserListContext | 'ticket') => {
|
|
@@ -121,18 +193,34 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
121
193
|
}
|
|
122
194
|
}, [clearChat]);
|
|
123
195
|
|
|
124
|
-
const
|
|
125
|
-
|
|
196
|
+
const listCtxForUser = useCallback((user: ChatUser, viewerIsDev: boolean): UserListContext => {
|
|
197
|
+
if (viewerIsDev) return user.type === 'user' ? 'support' : 'conversation';
|
|
198
|
+
return user.type === 'developer' ? 'support' : 'conversation';
|
|
199
|
+
}, []);
|
|
200
|
+
|
|
201
|
+
const handleSelectUser = useCallback((user: ChatUser, returnCtxOverride?: UserListContext) => {
|
|
202
|
+
setChatReturnCtx(returnCtxOverride ?? userListCtx);
|
|
126
203
|
const history = data?.sampleChats[user.uid] ?? [];
|
|
127
204
|
selectUser(user, history);
|
|
128
205
|
setScreen('chat');
|
|
129
|
-
// Update recent chats
|
|
130
206
|
setRecentChats(prev => {
|
|
131
207
|
const exists = prev.find(r => r.user.uid === user.uid);
|
|
132
208
|
if (exists) return prev;
|
|
133
209
|
return [{ id: `rc_${user.uid}`, user, lastMessage: '', lastTime: new Date().toISOString(), unread: 0, isPaused: false }, ...prev];
|
|
134
210
|
});
|
|
135
|
-
}, [data, selectUser]);
|
|
211
|
+
}, [data, selectUser, userListCtx]);
|
|
212
|
+
|
|
213
|
+
const handleBackFromChat = useCallback(() => {
|
|
214
|
+
clearChat();
|
|
215
|
+
setUserListCtx(chatReturnCtx);
|
|
216
|
+
setScreen('user-list');
|
|
217
|
+
}, [clearChat, chatReturnCtx]);
|
|
218
|
+
|
|
219
|
+
const handleOpenTicket = useCallback((id: string) => {
|
|
220
|
+
setViewingTicketId(id);
|
|
221
|
+
setScreen('ticket-detail');
|
|
222
|
+
setActiveTab('tickets');
|
|
223
|
+
}, []);
|
|
136
224
|
|
|
137
225
|
const handleTabChange = useCallback((tab: BottomTab) => {
|
|
138
226
|
setActiveTab(tab);
|
|
@@ -144,7 +232,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
144
232
|
if (!activeUser) return;
|
|
145
233
|
setBlockedUids(prev => [...prev, activeUser.uid]);
|
|
146
234
|
clearChat();
|
|
147
|
-
setScreen('
|
|
235
|
+
setScreen('block-list');
|
|
148
236
|
setActiveTab('home');
|
|
149
237
|
}, [activeUser, clearChat]);
|
|
150
238
|
|
|
@@ -161,7 +249,10 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
161
249
|
updatedAt: new Date().toISOString(),
|
|
162
250
|
assignedTo: null,
|
|
163
251
|
};
|
|
164
|
-
setTickets(prev => [
|
|
252
|
+
setTickets(prev => [...prev, t]);
|
|
253
|
+
setViewingTicketId(t.id);
|
|
254
|
+
setScreen('ticket-detail');
|
|
255
|
+
setActiveTab('tickets');
|
|
165
256
|
}, []);
|
|
166
257
|
|
|
167
258
|
/* ── Pause sync back into recent chats ──────────────────────────────── */
|
|
@@ -412,7 +503,12 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
412
503
|
<div className="cw-scroll" style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
413
504
|
|
|
414
505
|
{screen === 'home' && (
|
|
415
|
-
<HomeScreen
|
|
506
|
+
<HomeScreen
|
|
507
|
+
config={widgetConfig}
|
|
508
|
+
onNavigate={handleCardClick}
|
|
509
|
+
onOpenTicket={handleOpenTicket}
|
|
510
|
+
tickets={tickets}
|
|
511
|
+
/>
|
|
416
512
|
)}
|
|
417
513
|
|
|
418
514
|
{screen === 'user-list' && (
|
|
@@ -423,6 +519,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
423
519
|
viewerType={widgetConfig.viewerType ?? 'user'}
|
|
424
520
|
onBack={() => setScreen('home')}
|
|
425
521
|
onSelectUser={handleSelectUser}
|
|
522
|
+
onBlockList={userListCtx === 'conversation' ? () => setScreen('block-list') : undefined}
|
|
523
|
+
useHomeHeader={userListCtx === 'support' && widgetConfig.viewerType !== 'developer'}
|
|
426
524
|
/>
|
|
427
525
|
)}
|
|
428
526
|
|
|
@@ -435,7 +533,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
435
533
|
isReported={isReported}
|
|
436
534
|
isBlocked={isBlocked}
|
|
437
535
|
onSend={sendMessage}
|
|
438
|
-
onBack={
|
|
536
|
+
onBack={handleBackFromChat}
|
|
439
537
|
onClose={closeDrawer}
|
|
440
538
|
onTogglePause={handleTogglePause}
|
|
441
539
|
onReport={reportChat}
|
|
@@ -444,6 +542,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
444
542
|
onNavAction={handleNavFromMenu}
|
|
445
543
|
otherDevelopers={otherDevelopers}
|
|
446
544
|
onTransferToDeveloper={handleTransferToDeveloper}
|
|
545
|
+
messageSoundEnabled={messageSoundEnabled}
|
|
546
|
+
onToggleMessageSound={toggleMessageSound}
|
|
447
547
|
/>
|
|
448
548
|
)}
|
|
449
549
|
|
|
@@ -463,7 +563,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
463
563
|
<RecentChatsScreen
|
|
464
564
|
chats={recentChats}
|
|
465
565
|
config={widgetConfig}
|
|
466
|
-
onSelectChat={handleSelectUser}
|
|
566
|
+
onSelectChat={u => handleSelectUser(u, listCtxForUser(u, viewerIsDev))}
|
|
467
567
|
/>
|
|
468
568
|
)}
|
|
469
569
|
|
|
@@ -471,10 +571,32 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
471
571
|
<TicketScreen
|
|
472
572
|
tickets={tickets}
|
|
473
573
|
config={widgetConfig}
|
|
474
|
-
|
|
574
|
+
onNewTicket={() => setScreen('ticket-new')}
|
|
575
|
+
onSelectTicket={id => { setViewingTicketId(id); setScreen('ticket-detail'); }}
|
|
475
576
|
/>
|
|
476
577
|
)}
|
|
477
578
|
|
|
579
|
+
{screen === 'ticket-new' && (
|
|
580
|
+
<TicketFormScreen
|
|
581
|
+
config={widgetConfig}
|
|
582
|
+
onSubmit={handleRaiseTicket}
|
|
583
|
+
onCancel={() => setScreen('tickets')}
|
|
584
|
+
/>
|
|
585
|
+
)}
|
|
586
|
+
|
|
587
|
+
{screen === 'ticket-detail' && viewingTicketId && (
|
|
588
|
+
(() => {
|
|
589
|
+
const t = tickets.find(x => x.id === viewingTicketId);
|
|
590
|
+
return t ? (
|
|
591
|
+
<TicketDetailScreen
|
|
592
|
+
ticket={t}
|
|
593
|
+
config={widgetConfig}
|
|
594
|
+
onBack={() => { setViewingTicketId(null); setScreen('tickets'); }}
|
|
595
|
+
/>
|
|
596
|
+
) : null;
|
|
597
|
+
})()
|
|
598
|
+
)}
|
|
599
|
+
|
|
478
600
|
{screen === 'block-list' && (
|
|
479
601
|
<BlockListScreen
|
|
480
602
|
blockedUsers={blockedUsers}
|
|
@@ -491,12 +613,13 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
491
613
|
screen !== 'chat' &&
|
|
492
614
|
screen !== 'call' &&
|
|
493
615
|
screen !== 'user-list' &&
|
|
494
|
-
screen !== 'block-list' &&
|
|
616
|
+
screen !== 'block-list' &&
|
|
617
|
+
screen !== 'ticket-detail' &&
|
|
618
|
+
screen !== 'ticket-new' && (
|
|
495
619
|
<BottomTabs
|
|
496
620
|
active={activeTab}
|
|
497
621
|
onChange={handleTabChange}
|
|
498
622
|
primaryColor={primaryColor}
|
|
499
|
-
onBlockList={() => setScreen('block-list')}
|
|
500
623
|
/>
|
|
501
624
|
)}
|
|
502
625
|
</>
|
|
@@ -6,10 +6,12 @@ import { truncateWords } from '../../utils/chat';
|
|
|
6
6
|
interface HomeScreenProps {
|
|
7
7
|
config: WidgetConfig;
|
|
8
8
|
onNavigate: (ctx: UserListContext | 'ticket') => void;
|
|
9
|
+
/** Open a specific pending ticket (full detail) */
|
|
10
|
+
onOpenTicket: (ticketId: string) => void;
|
|
9
11
|
tickets: Ticket[];
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tickets }) => {
|
|
14
|
+
export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, onOpenTicket, tickets }) => {
|
|
13
15
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
14
16
|
const showSupport = config.chatType === 'SUPPORT' || config.chatType === 'BOTH';
|
|
15
17
|
const showChat = config.chatType === 'CHAT' || config.chatType === 'BOTH';
|
|
@@ -110,7 +112,7 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
|
|
|
110
112
|
<button
|
|
111
113
|
key={t.id}
|
|
112
114
|
type="button"
|
|
113
|
-
onClick={() =>
|
|
115
|
+
onClick={() => onOpenTicket(t.id)}
|
|
114
116
|
style={{
|
|
115
117
|
width: '100%',
|
|
116
118
|
textAlign: 'left',
|
|
@@ -5,10 +5,9 @@ interface BottomTabsProps {
|
|
|
5
5
|
active: BottomTab;
|
|
6
6
|
onChange: (tab: BottomTab) => void;
|
|
7
7
|
primaryColor: string;
|
|
8
|
-
onBlockList: () => void;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
export const BottomTabs: React.FC<BottomTabsProps> = ({ active, onChange, primaryColor
|
|
10
|
+
export const BottomTabs: React.FC<BottomTabsProps> = ({ active, onChange, primaryColor }) => {
|
|
12
11
|
const tabs: { key: BottomTab; label: string; Icon: React.FC<{ a: boolean; c: string }> }[] = [
|
|
13
12
|
{ key: 'home', label: 'Home', Icon: HomeIcon },
|
|
14
13
|
{ key: 'chats', label: 'Chats', Icon: ChatsIcon },
|
|
@@ -25,6 +24,7 @@ export const BottomTabs: React.FC<BottomTabsProps> = ({ active, onChange, primar
|
|
|
25
24
|
return (
|
|
26
25
|
<button
|
|
27
26
|
key={tab.key}
|
|
27
|
+
type="button"
|
|
28
28
|
onClick={() => onChange(tab.key)}
|
|
29
29
|
style={{
|
|
30
30
|
flex: 1, padding: '10px 0 8px',
|
|
@@ -42,26 +42,6 @@ export const BottomTabs: React.FC<BottomTabsProps> = ({ active, onChange, primar
|
|
|
42
42
|
</button>
|
|
43
43
|
);
|
|
44
44
|
})}
|
|
45
|
-
|
|
46
|
-
{/* Block list icon */}
|
|
47
|
-
<button
|
|
48
|
-
onClick={onBlockList}
|
|
49
|
-
style={{
|
|
50
|
-
padding: '10px 14px 8px',
|
|
51
|
-
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
|
|
52
|
-
background: 'transparent', border: 'none', cursor: 'pointer',
|
|
53
|
-
fontSize: '10px', fontWeight: 500, color: '#9aa3af',
|
|
54
|
-
borderTop: '2px solid transparent',
|
|
55
|
-
transition: 'color 0.15s', fontFamily: 'inherit',
|
|
56
|
-
}}
|
|
57
|
-
title="Block List"
|
|
58
|
-
>
|
|
59
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
|
60
|
-
<circle cx="12" cy="12" r="10" stroke="#b0bec5" strokeWidth="1.8" />
|
|
61
|
-
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07" stroke="#b0bec5" strokeWidth="1.8" strokeLinecap="round" />
|
|
62
|
-
</svg>
|
|
63
|
-
Blocked
|
|
64
|
-
</button>
|
|
65
45
|
</div>
|
|
66
46
|
);
|
|
67
47
|
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Ticket, WidgetConfig } from '../../types';
|
|
3
|
+
|
|
4
|
+
interface TicketDetailScreenProps {
|
|
5
|
+
ticket: Ticket;
|
|
6
|
+
config: WidgetConfig;
|
|
7
|
+
onBack: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const sm: Record<Ticket['status'], { label: string; bg: string; color: string }> = {
|
|
11
|
+
open: { label: 'Open', bg: '', color: '' },
|
|
12
|
+
'in-progress': { label: 'In Progress', bg: '#fef3c7', color: '#d97706' },
|
|
13
|
+
resolved: { label: 'Resolved', bg: '#f0fdf4', color: '#16a34a' },
|
|
14
|
+
closed: { label: 'Closed', bg: '#f3f4f6', color: '#6b7280' },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const pm: Record<Ticket['priority'], { label: string; color: string }> = {
|
|
18
|
+
low: { label: 'Low', color: '#6b7280' },
|
|
19
|
+
medium: { label: 'Medium', color: '#d97706' },
|
|
20
|
+
high: { label: 'High', color: '#ef4444' },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const TicketDetailScreen: React.FC<TicketDetailScreenProps> = ({ ticket, config, onBack }) => {
|
|
24
|
+
const st = sm[ticket.status];
|
|
25
|
+
const pr = pm[ticket.priority];
|
|
26
|
+
const stBg = ticket.status === 'open' ? `${config.primaryColor}14` : st.bg;
|
|
27
|
+
const stColor = ticket.status === 'open' ? config.primaryColor : st.color;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideIn 0.22s ease' }}>
|
|
31
|
+
<div
|
|
32
|
+
style={{
|
|
33
|
+
background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
|
|
34
|
+
padding: '14px 18px',
|
|
35
|
+
display: 'flex',
|
|
36
|
+
alignItems: 'center',
|
|
37
|
+
gap: 12,
|
|
38
|
+
flexShrink: 0,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
onClick={onBack}
|
|
44
|
+
style={{
|
|
45
|
+
background: 'rgba(255,255,255,0.22)',
|
|
46
|
+
border: 'none',
|
|
47
|
+
borderRadius: '50%',
|
|
48
|
+
width: 36,
|
|
49
|
+
height: 36,
|
|
50
|
+
display: 'flex',
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
justifyContent: 'center',
|
|
53
|
+
cursor: 'pointer',
|
|
54
|
+
flexShrink: 0,
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
58
|
+
<path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
59
|
+
</svg>
|
|
60
|
+
</button>
|
|
61
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
62
|
+
<div style={{ fontWeight: 800, fontSize: 16, color: '#fff', lineHeight: 1.25 }}>{ticket.title}</div>
|
|
63
|
+
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.85)', marginTop: 2 }}>#{ticket.id}</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="cw-scroll" style={{ flex: 1, overflowY: 'auto', padding: '20px 18px' }}>
|
|
68
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 16 }}>
|
|
69
|
+
<span
|
|
70
|
+
style={{
|
|
71
|
+
fontSize: 11,
|
|
72
|
+
fontWeight: 700,
|
|
73
|
+
padding: '5px 12px',
|
|
74
|
+
borderRadius: 20,
|
|
75
|
+
backgroundColor: stBg,
|
|
76
|
+
color: stColor,
|
|
77
|
+
textTransform: 'uppercase',
|
|
78
|
+
letterSpacing: '0.04em',
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{st.label}
|
|
82
|
+
</span>
|
|
83
|
+
<span style={{ fontSize: 11, fontWeight: 700, padding: '5px 12px', borderRadius: 20, color: pr.color, background: `${pr.color}15` }}>
|
|
84
|
+
● {pr.label} priority
|
|
85
|
+
</span>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<h3 style={{ margin: '0 0 8px', fontSize: 13, fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
|
|
89
|
+
Description
|
|
90
|
+
</h3>
|
|
91
|
+
<p style={{ margin: '0 0 24px', fontSize: 15, color: '#1e293b', lineHeight: 1.65, whiteSpace: 'pre-wrap' }}>
|
|
92
|
+
{ticket.description || '—'}
|
|
93
|
+
</p>
|
|
94
|
+
|
|
95
|
+
<div style={{ fontSize: 13, color: '#94a3b8', display: 'grid', gap: 8 }}>
|
|
96
|
+
<div>
|
|
97
|
+
<strong style={{ color: '#64748b' }}>Created</strong> · {new Date(ticket.createdAt).toLocaleString()}
|
|
98
|
+
</div>
|
|
99
|
+
<div>
|
|
100
|
+
<strong style={{ color: '#64748b' }}>Updated</strong> · {new Date(ticket.updatedAt).toLocaleString()}
|
|
101
|
+
</div>
|
|
102
|
+
{ticket.assignedTo && (
|
|
103
|
+
<div>
|
|
104
|
+
<strong style={{ color: '#64748b' }}>Assigned</strong> · {ticket.assignedTo}
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Ticket, WidgetConfig } from '../../types';
|
|
3
|
+
|
|
4
|
+
interface TicketFormScreenProps {
|
|
5
|
+
config: WidgetConfig;
|
|
6
|
+
onSubmit: (title: string, desc: string, priority: Ticket['priority']) => void;
|
|
7
|
+
onCancel: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function inputStyle(primaryColor: string): React.CSSProperties {
|
|
11
|
+
return {
|
|
12
|
+
width: '100%',
|
|
13
|
+
padding: '11px 14px',
|
|
14
|
+
borderRadius: 10,
|
|
15
|
+
border: '1.5px solid #e5e7eb',
|
|
16
|
+
outline: 'none',
|
|
17
|
+
fontSize: 14,
|
|
18
|
+
color: '#1a2332',
|
|
19
|
+
boxSizing: 'border-box',
|
|
20
|
+
fontFamily: 'inherit',
|
|
21
|
+
transition: 'border-color 0.2s',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const TicketFormScreen: React.FC<TicketFormScreenProps> = ({ config, onSubmit, onCancel }) => {
|
|
26
|
+
const [title, setTitle] = useState('');
|
|
27
|
+
const [desc, setDesc] = useState('');
|
|
28
|
+
const [priority, setPriority] = useState<Ticket['priority']>('medium');
|
|
29
|
+
|
|
30
|
+
const pm: Record<Ticket['priority'], { label: string; color: string }> = {
|
|
31
|
+
low: { label: 'Low', color: '#6b7280' },
|
|
32
|
+
medium: { label: 'Medium', color: '#d97706' },
|
|
33
|
+
high: { label: 'High', color: '#ef4444' },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleSubmit = () => {
|
|
37
|
+
if (!title.trim()) return;
|
|
38
|
+
onSubmit(title.trim(), desc.trim(), priority);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: '#fff', minHeight: 0 }}>
|
|
43
|
+
<div
|
|
44
|
+
style={{
|
|
45
|
+
background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
|
|
46
|
+
padding: '14px 18px',
|
|
47
|
+
display: 'flex',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
gap: 12,
|
|
50
|
+
flexShrink: 0,
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={onCancel}
|
|
56
|
+
style={{
|
|
57
|
+
background: 'rgba(255,255,255,0.22)',
|
|
58
|
+
border: 'none',
|
|
59
|
+
borderRadius: '50%',
|
|
60
|
+
width: 36,
|
|
61
|
+
height: 36,
|
|
62
|
+
display: 'flex',
|
|
63
|
+
alignItems: 'center',
|
|
64
|
+
justifyContent: 'center',
|
|
65
|
+
cursor: 'pointer',
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
69
|
+
<path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
70
|
+
</svg>
|
|
71
|
+
</button>
|
|
72
|
+
<div>
|
|
73
|
+
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 800, color: '#fff' }}>New ticket</h2>
|
|
74
|
+
<p style={{ margin: '4px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.85)' }}>Describe your issue below</p>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className="cw-scroll" style={{ flex: 1, overflowY: 'auto', padding: '20px 18px', minHeight: 0 }}>
|
|
79
|
+
<input
|
|
80
|
+
placeholder="Title *"
|
|
81
|
+
value={title}
|
|
82
|
+
onChange={e => setTitle(e.target.value)}
|
|
83
|
+
style={inputStyle(config.primaryColor)}
|
|
84
|
+
onFocus={e => (e.target.style.borderColor = config.primaryColor)}
|
|
85
|
+
onBlur={e => (e.target.style.borderColor = '#e5e7eb')}
|
|
86
|
+
/>
|
|
87
|
+
<textarea
|
|
88
|
+
placeholder="Describe the issue…"
|
|
89
|
+
value={desc}
|
|
90
|
+
onChange={e => setDesc(e.target.value)}
|
|
91
|
+
rows={5}
|
|
92
|
+
style={{ ...inputStyle(config.primaryColor), resize: 'none', marginTop: 12 }}
|
|
93
|
+
onFocus={e => (e.target.style.borderColor = config.primaryColor)}
|
|
94
|
+
onBlur={e => (e.target.style.borderColor = '#e5e7eb')}
|
|
95
|
+
/>
|
|
96
|
+
<div style={{ display: 'flex', gap: 8, marginTop: 12, paddingBottom: 8 }}>
|
|
97
|
+
{(['low', 'medium', 'high'] as Ticket['priority'][]).map(p => (
|
|
98
|
+
<button
|
|
99
|
+
key={p}
|
|
100
|
+
type="button"
|
|
101
|
+
onClick={() => setPriority(p)}
|
|
102
|
+
style={{
|
|
103
|
+
flex: 1,
|
|
104
|
+
padding: '8px',
|
|
105
|
+
border: `1.5px solid ${priority === p ? pm[p].color : '#e5e7eb'}`,
|
|
106
|
+
borderRadius: 8,
|
|
107
|
+
background: priority === p ? `${pm[p].color}15` : '#fff',
|
|
108
|
+
color: pm[p].color,
|
|
109
|
+
fontWeight: 700,
|
|
110
|
+
fontSize: 12,
|
|
111
|
+
cursor: 'pointer',
|
|
112
|
+
textTransform: 'capitalize',
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{pm[p].label}
|
|
116
|
+
</button>
|
|
117
|
+
))}
|
|
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
|
+
>
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
onClick={handleSubmit}
|
|
133
|
+
disabled={!title.trim()}
|
|
134
|
+
style={{
|
|
135
|
+
width: '100%',
|
|
136
|
+
padding: '14px',
|
|
137
|
+
borderRadius: 10,
|
|
138
|
+
border: 'none',
|
|
139
|
+
background: title.trim() ? config.primaryColor : '#e5e7eb',
|
|
140
|
+
color: title.trim() ? '#fff' : '#9ca3af',
|
|
141
|
+
fontWeight: 700,
|
|
142
|
+
fontSize: 15,
|
|
143
|
+
cursor: title.trim() ? 'pointer' : 'not-allowed',
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
Submit ticket
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
};
|