ajaxter-chat 3.0.10 β 3.0.12
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/README.md +4 -2
- 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 +1 -1
- package/dist/components/TicketScreen/index.d.ts +1 -0
- package/dist/components/TicketScreen/index.js +31 -9
- package/dist/components/UserListScreen/index.d.ts +2 -0
- package/dist/components/UserListScreen/index.js +34 -11
- package/package.json +1 -1
- package/src/components/ChatWidget.tsx +27 -6
- package/src/components/HomeScreen/index.tsx +12 -5
- package/src/components/RecentChatsScreen/index.tsx +97 -44
- package/src/components/TicketFormScreen/index.tsx +2 -2
- package/src/components/TicketScreen/index.tsx +63 -11
- package/src/components/UserListScreen/index.tsx +68 -13
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
A production-ready **drawer/slider-based** chat widget for React.js and Next.js.
|
|
4
4
|
All configuration is loaded remotely from your hosted `chatData.json`.
|
|
5
5
|
|
|
6
|
+
**Backend developers:** see **[BACKEND.md](./BACKEND.md)** for WebSocket/REST events, auth, payloads (`ChatMessage`, `Ticket`, users, history), and WebRTC signalling.
|
|
7
|
+
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
## Setup (2 env vars only)
|
|
@@ -109,7 +111,7 @@ export default function MyApp({ Component, pageProps }) {
|
|
|
109
111
|
|
|
110
112
|
| Feature | Details |
|
|
111
113
|
|---|---|
|
|
112
|
-
| **Drawer/Slider UI** | Slides in from right (or left) with smooth animation.
|
|
114
|
+
| **Drawer/Slider UI** | Slides in from right (or left) with smooth animation. |
|
|
113
115
|
| **Home Screen** | "Hi there π" hero, cards for Support / New Conversation / Raise Ticket |
|
|
114
116
|
| **User List** | Slide-in panel with online status dot, designation, project |
|
|
115
117
|
| **Chat Screen** | Matches the UI image β hamburger back, title "Support", phone + fullscreen icons |
|
|
@@ -127,7 +129,7 @@ export default function MyApp({ Component, pageProps }) {
|
|
|
127
129
|
| **Ticket Screen** | Raise tickets with title, description, priority selector |
|
|
128
130
|
| **Ticket History** | Shows all tickets with status badge and priority color |
|
|
129
131
|
| **Recent Chats Tab** | Shows past conversations with unread badges |
|
|
130
|
-
| **
|
|
132
|
+
| **Responsive width** | ~30% width on desktop; full width on small screens |
|
|
131
133
|
| **Slide Close** | Smooth slide-out animation on close |
|
|
132
134
|
| **SSR Safe** | isMounted guard, works in Next.js App Router and Pages Router |
|
|
133
135
|
| **CHAT_STATUS** | ACTIVE / DISABLE / MAINTENANCE handled |
|
|
@@ -40,6 +40,8 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
40
40
|
const [chatReturnCtx, setChatReturnCtx] = useState('conversation');
|
|
41
41
|
const [viewingTicketId, setViewingTicketId] = useState(null);
|
|
42
42
|
const [messageSoundEnabled, setMessageSoundEnabledState] = useState(true);
|
|
43
|
+
/** Stagger list animation only when opening from home burger menu */
|
|
44
|
+
const [listEntranceAnimation, setListEntranceAnimation] = useState(false);
|
|
43
45
|
/* App state */
|
|
44
46
|
const [tickets, setTickets] = useState((_a = data === null || data === void 0 ? void 0 : data.sampleTickets) !== null && _a !== void 0 ? _a : []);
|
|
45
47
|
const [recentChats, setRecentChats] = useState([]);
|
|
@@ -156,7 +158,8 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
156
158
|
setMessageSoundEnabledState(enabled);
|
|
157
159
|
}, [data === null || data === void 0 ? void 0 : data.widget]);
|
|
158
160
|
/* ββ Navigation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
159
|
-
const handleCardClick = useCallback((ctx) => {
|
|
161
|
+
const handleCardClick = useCallback((ctx, options) => {
|
|
162
|
+
setListEntranceAnimation(!!(options === null || options === void 0 ? void 0 : options.fromMenu));
|
|
160
163
|
if (ctx === 'ticket') {
|
|
161
164
|
setActiveTab('tickets');
|
|
162
165
|
setScreen('tickets');
|
|
@@ -167,6 +170,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
167
170
|
}
|
|
168
171
|
}, []);
|
|
169
172
|
const handleNavFromMenu = useCallback((ctx) => {
|
|
173
|
+
setListEntranceAnimation(false);
|
|
170
174
|
clearChat();
|
|
171
175
|
if (ctx === 'ticket') {
|
|
172
176
|
setActiveTab('tickets');
|
|
@@ -184,6 +188,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
184
188
|
}, []);
|
|
185
189
|
const handleSelectUser = useCallback((user, returnCtxOverride) => {
|
|
186
190
|
var _a;
|
|
191
|
+
setListEntranceAnimation(false);
|
|
187
192
|
setChatReturnCtx(returnCtxOverride !== null && returnCtxOverride !== void 0 ? returnCtxOverride : userListCtx);
|
|
188
193
|
const history = (_a = data === null || data === void 0 ? void 0 : data.sampleChats[user.uid]) !== null && _a !== void 0 ? _a : [];
|
|
189
194
|
selectUser(user, history);
|
|
@@ -196,19 +201,28 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
196
201
|
});
|
|
197
202
|
}, [data, selectUser, userListCtx]);
|
|
198
203
|
const handleBackFromChat = useCallback(() => {
|
|
204
|
+
setListEntranceAnimation(false);
|
|
199
205
|
clearChat();
|
|
200
206
|
setUserListCtx(chatReturnCtx);
|
|
201
207
|
setScreen('user-list');
|
|
202
208
|
}, [clearChat, chatReturnCtx]);
|
|
203
209
|
const handleOpenTicket = useCallback((id) => {
|
|
210
|
+
setListEntranceAnimation(false);
|
|
204
211
|
setViewingTicketId(id);
|
|
205
212
|
setScreen('ticket-detail');
|
|
206
213
|
setActiveTab('tickets');
|
|
207
214
|
}, []);
|
|
208
215
|
const handleTabChange = useCallback((tab) => {
|
|
216
|
+
setListEntranceAnimation(false);
|
|
209
217
|
setActiveTab(tab);
|
|
210
218
|
setScreen(tab === 'home' ? 'home' : tab === 'chats' ? 'recent-chats' : 'tickets');
|
|
211
219
|
}, []);
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (!listEntranceAnimation)
|
|
222
|
+
return;
|
|
223
|
+
const t = window.setTimeout(() => setListEntranceAnimation(false), 520);
|
|
224
|
+
return () => window.clearTimeout(t);
|
|
225
|
+
}, [listEntranceAnimation]);
|
|
212
226
|
/* ββ Block/Unblock βββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
213
227
|
const handleBlock = useCallback(() => {
|
|
214
228
|
if (!activeUser)
|
|
@@ -347,7 +361,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
347
361
|
}, onMouseLeave: e => {
|
|
348
362
|
e.currentTarget.style.transform = 'scale(1)';
|
|
349
363
|
e.currentTarget.style.boxShadow = `0 8px 28px ${theme.buttonColor}55`;
|
|
350
|
-
}, children: [_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z", stroke: theme.buttonTextColor, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }), _jsx("span", { children: theme.buttonLabel })] })), isOpen && (_jsx("div", {
|
|
364
|
+
}, children: [_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z", stroke: theme.buttonTextColor, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }), _jsx("span", { children: theme.buttonLabel })] })), isOpen && (_jsx("div", { "aria-hidden": true, style: {
|
|
351
365
|
position: 'fixed', inset: 0, zIndex: 9997,
|
|
352
366
|
backgroundColor: 'rgba(0,0,0,0.35)',
|
|
353
367
|
opacity: closing ? 0 : 1,
|
|
@@ -364,7 +378,11 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
364
378
|
right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
|
|
365
379
|
left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
|
|
366
380
|
zIndex: 20, display: 'flex', gap: 6,
|
|
367
|
-
}, children: _jsx(CornerBtn, { onClick: closeDrawer, title: "Close", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) }) })), widgetConfig.status === 'MAINTENANCE' && (_jsx(MaintenanceView, { primaryColor: primaryColor })), widgetConfig.status === 'DISABLE' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12 }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Chat is disabled" }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), widgetConfig.status === 'ACTIVE' && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, onNavigate: handleCardClick, onOpenTicket: handleOpenTicket, tickets: tickets })), screen === 'user-list' && (_jsx(UserListScreen, { context: userListCtx, users: filteredUsers, primaryColor: primaryColor, viewerType: (_e = widgetConfig.viewerType) !== null && _e !== void 0 ? _e : 'user', onBack: () => setScreen('home'), onSelectUser: handleSelectUser, onBlockList: userListCtx === 'conversation' ? () => setScreen('block-list') : undefined, useHomeHeader: userListCtx === 'support' && widgetConfig.viewerType !== 'developer' })), screen === 'chat' && activeUser && (_jsx(ChatScreen, { activeUser: activeUser, messages: messages, config: widgetConfig, isPaused: isPaused, isReported: isReported, isBlocked: isBlocked, onSend: sendMessage, onBack: handleBackFromChat, onClose: closeDrawer, onTogglePause: handleTogglePause, onReport: reportChat, onBlock: handleBlock, onStartCall: handleStartCall, onNavAction: handleNavFromMenu, otherDevelopers: otherDevelopers, onTransferToDeveloper: handleTransferToDeveloper, messageSoundEnabled: messageSoundEnabled, onToggleMessageSound: toggleMessageSound })), screen === 'call' && callSession.peer && (_jsx(CallScreen, { session: callSession, localVideoRef: localVideoRef, remoteVideoRef: remoteVideoRef, onEnd: handleEndCall, onToggleMute: toggleMute, onToggleCamera: toggleCamera, primaryColor: primaryColor })), screen === 'recent-chats' && (_jsx(RecentChatsScreen, { chats: recentChats, config: widgetConfig, onSelectChat: u => handleSelectUser(u, listCtxForUser(u, viewerIsDev)) })), screen === 'tickets' && (_jsx(TicketScreen, { tickets: tickets, config: widgetConfig, onNewTicket: () =>
|
|
381
|
+
}, children: _jsx(CornerBtn, { onClick: closeDrawer, title: "Close", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) }) })), widgetConfig.status === 'MAINTENANCE' && (_jsx(MaintenanceView, { primaryColor: primaryColor })), widgetConfig.status === 'DISABLE' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12 }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Chat is disabled" }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), widgetConfig.status === 'ACTIVE' && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, onNavigate: handleCardClick, onOpenTicket: handleOpenTicket, tickets: tickets })), screen === 'user-list' && (_jsx(UserListScreen, { context: userListCtx, users: filteredUsers, primaryColor: primaryColor, viewerType: (_e = widgetConfig.viewerType) !== null && _e !== void 0 ? _e : 'user', onBack: () => { setListEntranceAnimation(false); setScreen('home'); }, onSelectUser: handleSelectUser, onBlockList: userListCtx === 'conversation' ? () => setScreen('block-list') : undefined, useHomeHeader: userListCtx === 'support' && widgetConfig.viewerType !== 'developer', animateEntrance: listEntranceAnimation })), screen === 'chat' && activeUser && (_jsx(ChatScreen, { activeUser: activeUser, messages: messages, config: widgetConfig, isPaused: isPaused, isReported: isReported, isBlocked: isBlocked, onSend: sendMessage, onBack: handleBackFromChat, onClose: closeDrawer, onTogglePause: handleTogglePause, onReport: reportChat, onBlock: handleBlock, onStartCall: handleStartCall, onNavAction: handleNavFromMenu, otherDevelopers: otherDevelopers, onTransferToDeveloper: handleTransferToDeveloper, messageSoundEnabled: messageSoundEnabled, onToggleMessageSound: toggleMessageSound })), screen === 'call' && callSession.peer && (_jsx(CallScreen, { session: callSession, localVideoRef: localVideoRef, remoteVideoRef: remoteVideoRef, onEnd: handleEndCall, onToggleMute: toggleMute, onToggleCamera: toggleCamera, primaryColor: primaryColor })), screen === 'recent-chats' && (_jsx(RecentChatsScreen, { chats: recentChats, config: widgetConfig, onSelectChat: u => handleSelectUser(u, listCtxForUser(u, viewerIsDev)), animateEntrance: listEntranceAnimation })), screen === 'tickets' && (_jsx(TicketScreen, { tickets: tickets, config: widgetConfig, onNewTicket: () => { setListEntranceAnimation(false); setScreen('ticket-new'); }, onSelectTicket: id => {
|
|
382
|
+
setListEntranceAnimation(false);
|
|
383
|
+
setViewingTicketId(id);
|
|
384
|
+
setScreen('ticket-detail');
|
|
385
|
+
}, animateEntrance: listEntranceAnimation })), screen === 'ticket-new' && (_jsx(TicketFormScreen, { config: widgetConfig, onSubmit: handleRaiseTicket, onCancel: () => setScreen('tickets') })), screen === 'ticket-detail' && viewingTicketId && ((() => {
|
|
368
386
|
const t = tickets.find(x => x.id === viewingTicketId);
|
|
369
387
|
return t ? (_jsx(TicketDetailScreen, { ticket: t, config: widgetConfig, onBack: () => { setViewingTicketId(null); setScreen('tickets'); } })) : null;
|
|
370
388
|
})()), screen === 'block-list' && (_jsx(BlockListScreen, { blockedUsers: blockedUsers, config: widgetConfig, onUnblock: handleUnblock, onBack: () => { setScreen('home'); setActiveTab('home'); } }))] })), widgetConfig.status === 'ACTIVE' &&
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { WidgetConfig, UserListContext, Ticket } from '../../types';
|
|
3
|
+
export interface HomeNavigateOptions {
|
|
4
|
+
/** When true, list screens play stagger animation (home burger menu only) */
|
|
5
|
+
fromMenu?: boolean;
|
|
6
|
+
}
|
|
3
7
|
interface HomeScreenProps {
|
|
4
8
|
config: WidgetConfig;
|
|
5
|
-
onNavigate: (ctx: UserListContext | 'ticket') => void;
|
|
9
|
+
onNavigate: (ctx: UserListContext | 'ticket', options?: HomeNavigateOptions) => void;
|
|
6
10
|
/** Open a specific pending ticket (full detail) */
|
|
7
11
|
onOpenTicket: (ticketId: string) => void;
|
|
8
12
|
tickets: Ticket[];
|
|
@@ -23,7 +23,9 @@ export const HomeScreen = ({ config, onNavigate, onOpenTicket, tickets }) => {
|
|
|
23
23
|
return;
|
|
24
24
|
window.location.href = `tel:${raw.replace(/\s/g, '')}`;
|
|
25
25
|
};
|
|
26
|
-
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', overflow: 'hidden', background: '#fafbfc' }, children: [_jsx(SlideNavMenu, { open: menuOpen, onClose: () => setMenuOpen(false), primaryColor: config.primaryColor, chatType: config.chatType, viewerType: (_d = config.viewerType) !== null && _d !== void 0 ? _d : 'user', onSelect:
|
|
26
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', overflow: 'hidden', background: '#fafbfc' }, children: [_jsx(SlideNavMenu, { open: menuOpen, onClose: () => setMenuOpen(false), primaryColor: config.primaryColor, chatType: config.chatType, viewerType: (_d = config.viewerType) !== null && _d !== void 0 ? _d : 'user', onSelect: ctx => {
|
|
27
|
+
onNavigate(ctx, { fromMenu: true });
|
|
28
|
+
} }), _jsx("div", { style: {
|
|
27
29
|
flexShrink: 0,
|
|
28
30
|
padding: '14px 16px 10px',
|
|
29
31
|
display: 'flex',
|
|
@@ -51,7 +53,7 @@ export const HomeScreen = ({ config, onNavigate, onOpenTicket, tickets }) => {
|
|
|
51
53
|
color: '#0f172a',
|
|
52
54
|
letterSpacing: '-0.03em',
|
|
53
55
|
lineHeight: 1.2,
|
|
54
|
-
}, children: config.welcomeTitle }), _jsx("p", { style: { margin: '0 0 28px', fontSize: 14, color: '#64748b', lineHeight: 1.55 }, children: config.welcomeSubtitle }), _jsx("h2", { style: { margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }, children: "Continue
|
|
56
|
+
}, children: config.welcomeTitle }), _jsx("p", { style: { margin: '0 0 28px', fontSize: 14, color: '#64748b', lineHeight: 1.55 }, children: config.welcomeSubtitle }), _jsx("h2", { style: { margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }, children: "Continue with tickets" }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 }, children: pendingTickets.length > 0 ? (pendingTickets.map(t => (_jsxs("button", { type: "button", onClick: () => onOpenTicket(t.id), style: {
|
|
55
57
|
width: '100%',
|
|
56
58
|
textAlign: 'left',
|
|
57
59
|
padding: '14px 16px',
|
|
@@ -75,7 +77,7 @@ export const HomeScreen = ({ config, onNavigate, onOpenTicket, tickets }) => {
|
|
|
75
77
|
color: '#64748b',
|
|
76
78
|
fontSize: 14,
|
|
77
79
|
fontWeight: 500,
|
|
78
|
-
}, children: "Start via Raise ticket below" })] })) }), _jsx("h2", { style: { margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }, children: viewerIsDev ? 'Support tools' : 'Talk to
|
|
80
|
+
}, children: "Start via Raise ticket below" })] })) }), _jsx("h2", { style: { margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }, children: viewerIsDev ? 'Support tools' : 'Talk to support experts' }), showSupport && (_jsxs("button", { type: "button", onClick: () => onNavigate('support'), style: {
|
|
79
81
|
width: '100%',
|
|
80
82
|
display: 'flex',
|
|
81
83
|
alignItems: 'center',
|
|
@@ -149,5 +151,5 @@ export const HomeScreen = ({ config, onNavigate, onOpenTicket, tickets }) => {
|
|
|
149
151
|
fontSize: 14,
|
|
150
152
|
fontWeight: 700,
|
|
151
153
|
cursor: config.supportPhone ? 'pointer' : 'not-allowed',
|
|
152
|
-
}, children: [_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { 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", fill: "#fff" }) }), "
|
|
154
|
+
}, children: [_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { 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", fill: "#fff" }) }), "Get Free Widget"] })] })] })] })] }));
|
|
153
155
|
};
|
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo, useRef, useEffect } from 'react';
|
|
2
3
|
import { avatarColor, initials, formatTime } from '../../utils/chat';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
function matchesChat(chat, q) {
|
|
5
|
+
if (!q.trim())
|
|
6
|
+
return true;
|
|
7
|
+
const s = q.trim().toLowerCase();
|
|
8
|
+
return (chat.user.name.toLowerCase().includes(s) ||
|
|
9
|
+
chat.lastMessage.toLowerCase().includes(s));
|
|
10
|
+
}
|
|
11
|
+
export const RecentChatsScreen = ({ chats, config, onSelectChat, animateEntrance = false, }) => {
|
|
12
|
+
const [query, setQuery] = useState('');
|
|
13
|
+
const searchRef = useRef(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
var _a;
|
|
16
|
+
(_a = searchRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
17
|
+
}, []);
|
|
18
|
+
const filtered = useMemo(() => chats.filter(c => matchesChat(c, query)), [chats, query]);
|
|
19
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%' }, children: [_jsxs("div", { style: { background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`, padding: '18px 18px 14px', flexShrink: 0 }, children: [_jsx("h2", { style: { margin: 0, fontSize: 20, fontWeight: 800, color: '#fff', letterSpacing: '-0.02em' }, children: "Recent Chats" }), _jsx("p", { style: { margin: '3px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.8)' }, children: "Your conversation history" })] }), _jsx("div", { style: { padding: '10px 14px', background: '#fff', borderBottom: '1px solid #eef0f5', flexShrink: 0 }, children: _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', borderRadius: 10, border: '1.5px solid #e5e7eb', background: '#f8fafc' }, children: [_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0, opacity: 0.55 }, children: [_jsx("circle", { cx: "11", cy: "11", r: "7", stroke: "#64748b", strokeWidth: "2" }), _jsx("path", { d: "M20 20l-4-4", stroke: "#64748b", strokeWidth: "2", strokeLinecap: "round" })] }), _jsx("input", { ref: searchRef, type: "search", value: query, onChange: e => setQuery(e.target.value), placeholder: "Search chats\u2026", autoComplete: "off", "aria-label": "Search chats", style: {
|
|
20
|
+
flex: 1,
|
|
21
|
+
minWidth: 0,
|
|
22
|
+
border: 'none',
|
|
23
|
+
outline: 'none',
|
|
24
|
+
background: 'transparent',
|
|
25
|
+
fontSize: 14,
|
|
26
|
+
padding: '10px 0',
|
|
27
|
+
fontFamily: 'inherit',
|
|
28
|
+
color: '#1a2332',
|
|
29
|
+
} })] }) }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: filtered.length === 0 ? (_jsxs("div", { style: { padding: '50px 24px', textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 36, marginBottom: 10 }, children: query.trim() ? 'π' : 'π¬' }), _jsx("div", { style: { fontWeight: 700, color: '#1a2332', marginBottom: 6 }, children: query.trim() ? 'No matches' : 'No chats yet' }), _jsx("div", { style: { fontSize: 13, color: '#7b8fa1' }, children: query.trim() ? 'Try a different search' : 'Start a conversation from home' })] })) : filtered.map((chat, i) => (_jsxs("button", { onClick: () => onSelectChat(chat.user), style: Object.assign(Object.assign({ width: '100%', padding: '13px 16px', display: 'flex', alignItems: 'center', gap: 13, background: 'transparent', border: 'none', borderBottom: '1px solid #f0f2f5', cursor: 'pointer', textAlign: 'left' }, (animateEntrance ? { animation: `cw-fadeUp 0.28s ease both`, animationDelay: `${i * 0.05}s` } : {})), { transition: 'background 0.14s' }), onMouseEnter: e => e.currentTarget.style.background = '#f8faff', onMouseLeave: e => e.currentTarget.style.background = 'transparent', children: [_jsxs("div", { style: { position: 'relative', flexShrink: 0 }, children: [_jsx("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 }, children: initials(chat.user.name) }), chat.unread > 0 && (_jsx("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' }, children: chat.unread }))] }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', marginBottom: 3 }, children: [_jsx("span", { style: { fontWeight: 700, fontSize: 14, color: '#1a2332' }, children: chat.user.name }), _jsx("span", { style: { fontSize: 11, color: '#b0bec5' }, children: formatTime(chat.lastTime) })] }), _jsxs("div", { style: { fontSize: 13, color: '#7b8fa1', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 5 }, children: [chat.isPaused && _jsx("span", { style: { fontSize: 10, background: '#fef3c7', color: '#92400e', padding: '1px 5px', borderRadius: 4, fontWeight: 700 }, children: "PAUSED" }), chat.lastMessage] })] })] }, chat.id))) })] }));
|
|
30
|
+
};
|
|
@@ -45,7 +45,7 @@ export const TicketFormScreen = ({ config, onSubmit, onCancel }) => {
|
|
|
45
45
|
alignItems: 'center',
|
|
46
46
|
justifyContent: 'center',
|
|
47
47
|
cursor: 'pointer',
|
|
48
|
-
}, children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M19 12H5M5 12L12 19M5 12L12 5", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsxs("div", { children: [_jsx("h2", { style: { margin: 0, fontSize: 18, fontWeight: 800, color: '#fff' }, children: "
|
|
48
|
+
}, children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M19 12H5M5 12L12 19M5 12L12 5", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsxs("div", { children: [_jsx("h2", { style: { margin: 0, fontSize: 18, fontWeight: 800, color: '#fff' }, children: "Raise a ticket" }), _jsx("p", { style: { margin: '4px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.85)' }, children: "Please fill out the ticket form below and we will get back to you as soon as possible." })] })] }), _jsxs("div", { className: "cw-scroll", style: { flex: 1, overflowY: 'auto', padding: '20px 18px', minHeight: 0 }, children: [_jsx("input", { placeholder: "Title *", value: title, onChange: e => setTitle(e.target.value), style: inputStyle(config.primaryColor), onFocus: e => (e.target.style.borderColor = config.primaryColor), onBlur: e => (e.target.style.borderColor = '#e5e7eb') }), _jsx("textarea", { placeholder: "Describe the issue\u2026", value: desc, onChange: e => setDesc(e.target.value), rows: 5, style: Object.assign(Object.assign({}, inputStyle(config.primaryColor)), { resize: 'none', marginTop: 12 }), onFocus: e => (e.target.style.borderColor = config.primaryColor), onBlur: e => (e.target.style.borderColor = '#e5e7eb') }), _jsx("div", { style: { display: 'flex', gap: 8, marginTop: 12, paddingBottom: 8 }, children: ['low', 'medium', 'high'].map(p => (_jsx("button", { type: "button", onClick: () => setPriority(p), style: {
|
|
49
49
|
flex: 1,
|
|
50
50
|
padding: '8px',
|
|
51
51
|
border: `1.5px solid ${priority === p ? pm[p].color : '#e5e7eb'}`,
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
|
|
2
|
+
import { useState, useMemo, useRef, useEffect } from 'react';
|
|
3
|
+
function matchesTicket(t, q) {
|
|
4
|
+
if (!q.trim())
|
|
5
|
+
return true;
|
|
6
|
+
const s = q.trim().toLowerCase();
|
|
7
|
+
return (t.title.toLowerCase().includes(s) ||
|
|
8
|
+
t.description.toLowerCase().includes(s) ||
|
|
9
|
+
t.id.toLowerCase().includes(s));
|
|
10
|
+
}
|
|
11
|
+
export const TicketScreen = ({ tickets, config, onNewTicket, onSelectTicket, animateEntrance = false, }) => {
|
|
12
|
+
const [query, setQuery] = useState('');
|
|
13
|
+
const searchRef = useRef(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
var _a;
|
|
16
|
+
(_a = searchRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
17
|
+
}, []);
|
|
18
|
+
const filtered = useMemo(() => tickets.filter(t => matchesTicket(t, query)), [tickets, query]);
|
|
3
19
|
const sm = {
|
|
4
20
|
open: { label: 'Open', bg: `${config.primaryColor}14`, color: config.primaryColor },
|
|
5
21
|
'in-progress': { label: 'In Progress', bg: '#fef3c7', color: '#d97706' },
|
|
@@ -13,15 +29,21 @@ export const TicketScreen = ({ tickets, config, onNewTicket, onSelectTicket }) =
|
|
|
13
29
|
};
|
|
14
30
|
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%' }, children: [_jsx("div", { style: {
|
|
15
31
|
background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
|
|
16
|
-
padding: '18px 18px
|
|
17
|
-
}, children: _jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }, children: [_jsxs("div", { children: [_jsx("h2", { style: { margin: 0, fontSize: 20, fontWeight: 800, color: '#fff', letterSpacing: '-0.02em' }, children: "Tickets" }), _jsxs("p", { style: { margin: '3px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.8)' }, children: [tickets.length, " ticket", tickets.length !== 1 ? 's' : '', " raised"] })] }), _jsx("button", { type: "button", onClick: onNewTicket, style: {
|
|
32
|
+
padding: '18px 18px 14px', flexShrink: 0, position: 'relative',
|
|
33
|
+
}, children: _jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }, children: [_jsxs("div", { style: { minWidth: 0 }, children: [_jsx("h2", { style: { margin: 0, fontSize: 20, fontWeight: 800, color: '#fff', letterSpacing: '-0.02em' }, children: "Tickets" }), _jsxs("p", { style: { margin: '3px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.8)' }, children: [tickets.length, " ticket", tickets.length !== 1 ? 's' : '', " raised"] })] }), _jsx("button", { type: "button", onClick: onNewTicket, style: {
|
|
18
34
|
background: 'rgba(255,255,255,0.22)', border: 'none', borderRadius: 20,
|
|
19
35
|
padding: '7px 14px', color: '#fff', fontWeight: 700, fontSize: 13,
|
|
20
36
|
cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 5,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
flexShrink: 0,
|
|
38
|
+
}, children: "+ New" })] }) }), _jsx("div", { style: { padding: '10px 14px', background: '#fff', borderBottom: '1px solid #eef0f5', flexShrink: 0 }, children: _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', borderRadius: 10, border: '1.5px solid #e5e7eb', background: '#f8fafc' }, children: [_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0, opacity: 0.55 }, children: [_jsx("circle", { cx: "11", cy: "11", r: "7", stroke: "#64748b", strokeWidth: "2" }), _jsx("path", { d: "M20 20l-4-4", stroke: "#64748b", strokeWidth: "2", strokeLinecap: "round" })] }), _jsx("input", { ref: searchRef, type: "search", value: query, onChange: e => setQuery(e.target.value), placeholder: "Search tickets\u2026", autoComplete: "off", "aria-label": "Search tickets", style: {
|
|
39
|
+
flex: 1,
|
|
40
|
+
minWidth: 0,
|
|
41
|
+
border: 'none',
|
|
42
|
+
outline: 'none',
|
|
43
|
+
background: 'transparent',
|
|
44
|
+
fontSize: 14,
|
|
45
|
+
padding: '10px 0',
|
|
46
|
+
fontFamily: 'inherit',
|
|
47
|
+
color: '#1a2332',
|
|
48
|
+
} })] }) }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: filtered.length === 0 ? (_jsxs("div", { style: { padding: '50px 24px', textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 36, marginBottom: 10 }, children: query.trim() ? 'π' : 'π«' }), _jsx("div", { style: { fontWeight: 700, color: '#1a2332', marginBottom: 6 }, children: query.trim() ? 'No matches' : 'No tickets yet' }), _jsx("div", { style: { fontSize: 13, color: '#7b8fa1' }, children: query.trim() ? 'Try a different search' : 'Raise a ticket for major issues' })] })) : filtered.map((t, i) => (_jsxs("button", { type: "button", onClick: () => onSelectTicket(t.id), style: Object.assign(Object.assign({ width: '100%', padding: '14px 16px', borderBottom: '1px solid #f0f2f5' }, (animateEntrance ? { animation: `cw-fadeUp 0.3s ease both`, animationDelay: `${i * 0.05}s` } : {})), { background: 'transparent', borderLeft: 'none', borderRight: 'none', borderTop: 'none', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit' }), children: [_jsxs("div", { style: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 5 }, children: [_jsx("span", { style: { fontWeight: 700, fontSize: 14, color: '#1a2332', flex: 1, paddingRight: 10 }, children: t.title }), _jsx("span", { style: { fontSize: 10, fontWeight: 700, padding: '3px 9px', borderRadius: 20, backgroundColor: sm[t.status].bg, color: sm[t.status].color, whiteSpace: 'nowrap', textTransform: 'uppercase', letterSpacing: '0.04em', flexShrink: 0 }, children: sm[t.status].label })] }), t.description && _jsx("p", { style: { margin: '0 0 7px', fontSize: 13, color: '#7b8fa1', lineHeight: 1.5 }, children: t.description }), _jsxs("div", { style: { display: 'flex', gap: 10, fontSize: 11, color: '#b0bec5' }, children: [_jsxs("span", { style: { color: pm[t.priority].color, fontWeight: 700 }, children: ["\u25CF ", pm[t.priority].label] }), _jsxs("span", { children: ["#", t.id] }), _jsx("span", { children: new Date(t.createdAt).toLocaleDateString([], { month: 'short', day: 'numeric' }) })] })] }, t.id))) })] }));
|
|
27
49
|
};
|
|
@@ -12,6 +12,8 @@ interface UserListScreenProps {
|
|
|
12
12
|
onBlockList?: () => void;
|
|
13
13
|
/** βNeed Supportβ (user β agents): show home icon instead of back arrow */
|
|
14
14
|
useHomeHeader?: boolean;
|
|
15
|
+
/** Stagger animation β only when opening from home burger menu */
|
|
16
|
+
animateEntrance?: boolean;
|
|
15
17
|
}
|
|
16
18
|
export declare const UserListScreen: React.FC<UserListScreenProps>;
|
|
17
19
|
export {};
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo, useRef, useEffect } from 'react';
|
|
2
3
|
import { avatarColor, initials } from '../../utils/chat';
|
|
3
|
-
|
|
4
|
+
function matchesUser(u, q) {
|
|
5
|
+
if (!q.trim())
|
|
6
|
+
return true;
|
|
7
|
+
const s = q.trim().toLowerCase();
|
|
8
|
+
return (u.name.toLowerCase().includes(s) ||
|
|
9
|
+
u.email.toLowerCase().includes(s) ||
|
|
10
|
+
u.designation.toLowerCase().includes(s) ||
|
|
11
|
+
u.project.toLowerCase().includes(s));
|
|
12
|
+
}
|
|
13
|
+
export const UserListScreen = ({ context, users, primaryColor, viewerType = 'user', onBack, onSelectUser, onBlockList, useHomeHeader = false, animateEntrance = false, }) => {
|
|
14
|
+
const [query, setQuery] = useState('');
|
|
15
|
+
const searchRef = useRef(null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
var _a;
|
|
18
|
+
(_a = searchRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
19
|
+
}, []);
|
|
20
|
+
const filtered = useMemo(() => users.filter(u => matchesUser(u, query)), [users, query]);
|
|
4
21
|
const isStaff = viewerType === 'developer';
|
|
5
22
|
const title = context === 'support'
|
|
6
23
|
? (isStaff ? 'Provide Support' : 'Need Support')
|
|
@@ -8,7 +25,8 @@ export const UserListScreen = ({ context, users, primaryColor, viewerType = 'use
|
|
|
8
25
|
const subtitle = context === 'support'
|
|
9
26
|
? (isStaff ? 'All chat users β choose who to help' : 'Choose a support agent')
|
|
10
27
|
: (isStaff ? 'Chat with another developer or coordinate handoff' : 'Choose a colleague');
|
|
11
|
-
|
|
28
|
+
const rootAnim = animateEntrance ? 'cw-slideIn 0.22s ease' : undefined;
|
|
29
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', animation: rootAnim }, children: [_jsxs("div", { style: { background: `linear-gradient(135deg,${primaryColor},${primaryColor}cc)`, padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0, position: 'relative' }, children: [useHomeHeader ? _jsx(HomeBtn, { onClick: onBack }) : _jsx(BackBtn, { onClick: onBack }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 16, color: '#fff' }, children: title }), _jsx("div", { style: { fontSize: 12, color: 'rgba(255,255,255,0.8)' }, children: subtitle })] }), context === 'conversation' && onBlockList && (_jsxs("button", { type: "button", onClick: onBlockList, style: {
|
|
12
30
|
flexShrink: 0,
|
|
13
31
|
background: 'rgba(255,255,255,0.2)',
|
|
14
32
|
border: 'none',
|
|
@@ -21,14 +39,19 @@ export const UserListScreen = ({ context, users, primaryColor, viewerType = 'use
|
|
|
21
39
|
display: 'flex',
|
|
22
40
|
alignItems: 'center',
|
|
23
41
|
gap: 6,
|
|
24
|
-
}, title: "Blocked users", children: [_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "1.8" }), _jsx("line", { x1: "4.93", y1: "4.93", x2: "19.07", y2: "19.07", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round" })] }), "Blocked"] }))] }), _jsx("div", { style: {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
}, title: "Blocked users", children: [_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "1.8" }), _jsx("line", { x1: "4.93", y1: "4.93", x2: "19.07", y2: "19.07", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round" })] }), "Blocked"] }))] }), _jsx("div", { style: { padding: '10px 14px', background: '#fff', borderBottom: '1px solid #eef0f5', flexShrink: 0 }, children: _jsxs("label", { style: { display: 'block', margin: 0 }, children: [_jsx("span", { style: { position: 'absolute', width: 1, height: 1, padding: 0, overflow: 'hidden', clip: 'rect(0,0,0,0)' }, children: "Search" }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', borderRadius: 10, border: '1.5px solid #e5e7eb', background: '#f8fafc' }, children: [_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0, opacity: 0.55 }, children: [_jsx("circle", { cx: "11", cy: "11", r: "7", stroke: "#64748b", strokeWidth: "2" }), _jsx("path", { d: "M20 20l-4-4", stroke: "#64748b", strokeWidth: "2", strokeLinecap: "round" })] }), _jsx("input", { ref: searchRef, type: "search", value: query, onChange: e => setQuery(e.target.value), placeholder: "Search by name\u2026", autoComplete: "off", style: {
|
|
43
|
+
flex: 1,
|
|
44
|
+
minWidth: 0,
|
|
45
|
+
border: 'none',
|
|
46
|
+
outline: 'none',
|
|
47
|
+
background: 'transparent',
|
|
48
|
+
fontSize: 14,
|
|
49
|
+
padding: '10px 0',
|
|
50
|
+
fontFamily: 'inherit',
|
|
51
|
+
color: '#1a2332',
|
|
52
|
+
} })] })] }) }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: filtered.length === 0 ? (_jsx(Empty, { hasQuery: !!query.trim() })) : filtered.map((u, i) => (_jsxs("button", { onClick: () => onSelectUser(u), style: Object.assign(Object.assign({ width: '100%', padding: '13px 18px', display: 'flex', alignItems: 'center', gap: 13, background: 'transparent', border: 'none', borderBottom: '1px solid #f0f2f5', cursor: 'pointer', textAlign: 'left' }, (animateEntrance
|
|
53
|
+
? { animation: `cw-fadeUp 0.28s ease both`, animationDelay: `${i * 0.05}s` }
|
|
54
|
+
: {})), { transition: 'background 0.14s' }), onMouseEnter: e => e.currentTarget.style.background = '#f8faff', onMouseLeave: e => e.currentTarget.style.background = 'transparent', children: [_jsxs("div", { style: { position: 'relative', flexShrink: 0 }, children: [_jsx("div", { style: {
|
|
32
55
|
width: 44, height: 44, borderRadius: '50%',
|
|
33
56
|
backgroundColor: avatarColor(u.name),
|
|
34
57
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
@@ -55,4 +78,4 @@ const HomeBtn = ({ onClick }) => (_jsx("button", { type: "button", onClick: onCl
|
|
|
55
78
|
width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
56
79
|
cursor: 'pointer', flexShrink: 0,
|
|
57
80
|
}, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: [_jsx("path", { d: "M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z", stroke: "#fff", strokeWidth: "2.2", fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M9 21V12h6v9", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round" })] }) }));
|
|
58
|
-
const Empty = () => (_jsxs("div", { style: { padding: '50px 24px', textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 36, marginBottom: 10 }, children:
|
|
81
|
+
const Empty = ({ hasQuery }) => (_jsxs("div", { style: { padding: '50px 24px', textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 36, marginBottom: 10 }, children: hasQuery ? 'π' : 'π₯' }), _jsx("div", { style: { fontWeight: 700, color: '#1a2332', marginBottom: 6 }, children: hasQuery ? 'No matches' : 'No users available' }), _jsx("div", { style: { fontSize: 13, color: '#7b8fa1' }, children: hasQuery ? 'Try a different search' : 'Check back later' })] }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ajaxter-chat",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.12",
|
|
4
4
|
"description": "Drawer-based chat widget with support chat, tickets, WebRTC calling, voice messages, block list, and transcript download.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -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,10 +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}
|
|
523
537
|
useHomeHeader={userListCtx === 'support' && widgetConfig.viewerType !== 'developer'}
|
|
538
|
+
animateEntrance={listEntranceAnimation}
|
|
524
539
|
/>
|
|
525
540
|
)}
|
|
526
541
|
|
|
@@ -564,6 +579,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
564
579
|
chats={recentChats}
|
|
565
580
|
config={widgetConfig}
|
|
566
581
|
onSelectChat={u => handleSelectUser(u, listCtxForUser(u, viewerIsDev))}
|
|
582
|
+
animateEntrance={listEntranceAnimation}
|
|
567
583
|
/>
|
|
568
584
|
)}
|
|
569
585
|
|
|
@@ -571,8 +587,13 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
571
587
|
<TicketScreen
|
|
572
588
|
tickets={tickets}
|
|
573
589
|
config={widgetConfig}
|
|
574
|
-
onNewTicket={() => setScreen('ticket-new')}
|
|
575
|
-
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}
|
|
576
597
|
/>
|
|
577
598
|
)}
|
|
578
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
|
+
};
|
|
@@ -70,8 +70,8 @@ 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
|
|
|
@@ -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
|
}}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
|
2
2
|
import { ChatUser, UserListContext } from '../../types';
|
|
3
3
|
import { avatarColor, initials } from '../../utils/chat';
|
|
4
4
|
|
|
@@ -14,12 +14,35 @@ interface UserListScreenProps {
|
|
|
14
14
|
onBlockList?: () => void;
|
|
15
15
|
/** βNeed Supportβ (user β agents): show home icon instead of back arrow */
|
|
16
16
|
useHomeHeader?: boolean;
|
|
17
|
+
/** Stagger animation β only when opening from home burger menu */
|
|
18
|
+
animateEntrance?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function matchesUser(u: ChatUser, q: string): boolean {
|
|
22
|
+
if (!q.trim()) return true;
|
|
23
|
+
const s = q.trim().toLowerCase();
|
|
24
|
+
return (
|
|
25
|
+
u.name.toLowerCase().includes(s) ||
|
|
26
|
+
u.email.toLowerCase().includes(s) ||
|
|
27
|
+
u.designation.toLowerCase().includes(s) ||
|
|
28
|
+
u.project.toLowerCase().includes(s)
|
|
29
|
+
);
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
export const UserListScreen: React.FC<UserListScreenProps> = ({
|
|
20
33
|
context, users, primaryColor, viewerType = 'user', onBack, onSelectUser, onBlockList,
|
|
21
34
|
useHomeHeader = false,
|
|
35
|
+
animateEntrance = false,
|
|
22
36
|
}) => {
|
|
37
|
+
const [query, setQuery] = useState('');
|
|
38
|
+
const searchRef = useRef<HTMLInputElement>(null);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
searchRef.current?.focus();
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const filtered = useMemo(() => users.filter(u => matchesUser(u, query)), [users, query]);
|
|
45
|
+
|
|
23
46
|
const isStaff = viewerType === 'developer';
|
|
24
47
|
const title = context === 'support'
|
|
25
48
|
? (isStaff ? 'Provide Support' : 'Need Support')
|
|
@@ -28,8 +51,10 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
|
|
|
28
51
|
? (isStaff ? 'All chat users β choose who to help' : 'Choose a support agent')
|
|
29
52
|
: (isStaff ? 'Chat with another developer or coordinate handoff' : 'Choose a colleague');
|
|
30
53
|
|
|
54
|
+
const rootAnim = animateEntrance ? 'cw-slideIn 0.22s ease' : undefined;
|
|
55
|
+
|
|
31
56
|
return (
|
|
32
|
-
<div style={{ display:'flex', flexDirection:'column', height:'100%', animation:
|
|
57
|
+
<div style={{ display:'flex', flexDirection:'column', height:'100%', animation: rootAnim }}>
|
|
33
58
|
{/* Header */}
|
|
34
59
|
<div style={{ background:`linear-gradient(135deg,${primaryColor},${primaryColor}cc)`, padding:'14px 18px', display:'flex', alignItems:'center', gap:12, flexShrink:0, position:'relative' }}>
|
|
35
60
|
{useHomeHeader ? <HomeBtn onClick={onBack} /> : <BackBtn onClick={onBack} />}
|
|
@@ -66,11 +91,42 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
|
|
|
66
91
|
)}
|
|
67
92
|
</div>
|
|
68
93
|
|
|
94
|
+
<div style={{ padding: '10px 14px', background: '#fff', borderBottom: '1px solid #eef0f5', flexShrink: 0 }}>
|
|
95
|
+
<label style={{ display: 'block', margin: 0 }}>
|
|
96
|
+
<span style={{ position: 'absolute', width: 1, height: 1, padding: 0, overflow: 'hidden', clip: 'rect(0,0,0,0)' }}>Search</span>
|
|
97
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', borderRadius: 10, border: '1.5px solid #e5e7eb', background: '#f8fafc' }}>
|
|
98
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0, opacity: 0.55 }}>
|
|
99
|
+
<circle cx="11" cy="11" r="7" stroke="#64748b" strokeWidth="2" />
|
|
100
|
+
<path d="M20 20l-4-4" stroke="#64748b" strokeWidth="2" strokeLinecap="round" />
|
|
101
|
+
</svg>
|
|
102
|
+
<input
|
|
103
|
+
ref={searchRef}
|
|
104
|
+
type="search"
|
|
105
|
+
value={query}
|
|
106
|
+
onChange={e => setQuery(e.target.value)}
|
|
107
|
+
placeholder="Search by nameβ¦"
|
|
108
|
+
autoComplete="off"
|
|
109
|
+
style={{
|
|
110
|
+
flex: 1,
|
|
111
|
+
minWidth: 0,
|
|
112
|
+
border: 'none',
|
|
113
|
+
outline: 'none',
|
|
114
|
+
background: 'transparent',
|
|
115
|
+
fontSize: 14,
|
|
116
|
+
padding: '10px 0',
|
|
117
|
+
fontFamily: 'inherit',
|
|
118
|
+
color: '#1a2332',
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
</label>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
69
125
|
{/* User list */}
|
|
70
126
|
<div style={{ flex:1, overflowY:'auto' }}>
|
|
71
|
-
{
|
|
72
|
-
<Empty />
|
|
73
|
-
) :
|
|
127
|
+
{filtered.length === 0 ? (
|
|
128
|
+
<Empty hasQuery={!!query.trim()} />
|
|
129
|
+
) : filtered.map((u, i) => (
|
|
74
130
|
<button
|
|
75
131
|
key={u.uid}
|
|
76
132
|
onClick={() => onSelectUser(u)}
|
|
@@ -79,13 +135,14 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
|
|
|
79
135
|
alignItems:'center', gap:13, background:'transparent',
|
|
80
136
|
border:'none', borderBottom:'1px solid #f0f2f5',
|
|
81
137
|
cursor:'pointer', textAlign:'left',
|
|
82
|
-
|
|
138
|
+
...(animateEntrance
|
|
139
|
+
? { animation: `cw-fadeUp 0.28s ease both`, animationDelay: `${i * 0.05}s` }
|
|
140
|
+
: {}),
|
|
83
141
|
transition:'background 0.14s',
|
|
84
142
|
}}
|
|
85
143
|
onMouseEnter={e => (e.currentTarget as HTMLElement).style.background = '#f8faff'}
|
|
86
144
|
onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = 'transparent'}
|
|
87
145
|
>
|
|
88
|
-
{/* Avatar with online dot */}
|
|
89
146
|
<div style={{ position:'relative', flexShrink:0 }}>
|
|
90
147
|
<div style={{
|
|
91
148
|
width:44, height:44, borderRadius:'50%',
|
|
@@ -99,14 +156,12 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
|
|
|
99
156
|
backgroundColor: u.status==='online' ? '#22c55e' : u.status==='away' ? '#f59e0b' : '#d1d5db',
|
|
100
157
|
}} />
|
|
101
158
|
</div>
|
|
102
|
-
{/* Info */}
|
|
103
159
|
<div style={{ flex:1, minWidth:0 }}>
|
|
104
160
|
<div style={{ fontWeight:700, fontSize:14, color:'#1a2332', marginBottom:2 }}>{u.name}</div>
|
|
105
161
|
<div style={{ fontSize:12, color:'#7b8fa1', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
|
|
106
162
|
{u.designation} Β· {u.project}
|
|
107
163
|
</div>
|
|
108
164
|
</div>
|
|
109
|
-
{/* Type badge */}
|
|
110
165
|
<span style={{
|
|
111
166
|
fontSize:10, fontWeight:700, padding:'3px 9px', borderRadius:20,
|
|
112
167
|
textTransform:'uppercase', letterSpacing:'0.05em', flexShrink:0,
|
|
@@ -147,10 +202,10 @@ const HomeBtn: React.FC<{ onClick: () => void }> = ({ onClick }) => (
|
|
|
147
202
|
</button>
|
|
148
203
|
);
|
|
149
204
|
|
|
150
|
-
const Empty: React.FC = () => (
|
|
205
|
+
const Empty: React.FC<{ hasQuery: boolean }> = ({ hasQuery }) => (
|
|
151
206
|
<div style={{ padding:'50px 24px', textAlign:'center' }}>
|
|
152
|
-
<div style={{ fontSize:36, marginBottom:10 }}
|
|
153
|
-
<div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>No users available</div>
|
|
154
|
-
<div style={{ fontSize:13, color:'#7b8fa1' }}>Check back later</div>
|
|
207
|
+
<div style={{ fontSize:36, marginBottom:10 }}>{hasQuery ? 'π' : 'π₯'}</div>
|
|
208
|
+
<div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>{hasQuery ? 'No matches' : 'No users available'}</div>
|
|
209
|
+
<div style={{ fontSize:13, color:'#7b8fa1' }}>{hasQuery ? 'Try a different search' : 'Check back later'}</div>
|
|
155
210
|
</div>
|
|
156
211
|
);
|