ajaxter-chat 1.0.0 → 1.0.3
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 +23 -10
- package/dist/components/BottomNav/index.d.ts +10 -0
- package/dist/components/BottomNav/index.js +32 -0
- package/dist/components/ChatBox/index.d.ts +6 -2
- package/dist/components/ChatBox/index.js +66 -1
- package/dist/components/ChatWidget.js +1 -1
- package/dist/components/ChatWindow/index.d.ts +1 -0
- package/dist/components/ChatWindow/index.js +253 -99
- package/dist/components/HomeView/index.d.ts +12 -0
- package/dist/components/HomeView/index.js +51 -0
- package/dist/config/index.d.ts +13 -0
- package/dist/config/index.js +67 -5
- package/dist/hooks/useChat.d.ts +3 -1
- package/dist/hooks/useChat.js +23 -4
- package/dist/index.d.ts +1 -1
- package/dist/services/userService.js +2 -0
- package/dist/types/index.d.ts +33 -1
- package/dist/utils/theme.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Chat Widget
|
|
2
2
|
|
|
3
3
|
A reusable, fully configurable floating chat widget for **React.js** and **Next.js** applications.
|
|
4
4
|
|
|
@@ -14,7 +14,7 @@ A reusable, fully configurable floating chat widget for **React.js** and **Next.
|
|
|
14
14
|
## Folder Structure
|
|
15
15
|
|
|
16
16
|
```
|
|
17
|
-
|
|
17
|
+
my_first_project/
|
|
18
18
|
├── src/
|
|
19
19
|
│ ├── index.ts # Public API exports
|
|
20
20
|
│ ├── types/
|
|
@@ -63,9 +63,9 @@ react-chat-widget-extension/
|
|
|
63
63
|
## Installation
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
npm install
|
|
66
|
+
npm install ajaxter-chat
|
|
67
67
|
# or
|
|
68
|
-
yarn add
|
|
68
|
+
yarn add ajaxter-chat
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
---
|
|
@@ -98,7 +98,7 @@ NEXT_PUBLIC_CHAT_TYPE=BOTH
|
|
|
98
98
|
|--------------------|---------------------------------------|------------------------------------------|
|
|
99
99
|
| `CHAT_HOST_URL` | string | Base URL of your chat/user API |
|
|
100
100
|
| `CHAT_HOST_PORT` | number | Port for your API server |
|
|
101
|
-
| `CHAT_USER_LIST` | string |
|
|
101
|
+
| `CHAT_USER_LIST` | string | User list URL — see **User List API** below |
|
|
102
102
|
| `CHAT_STATUS` | `ACTIVE` \| `DISABLE` \| `MAINTENANCE` | Controls widget visibility & state |
|
|
103
103
|
| `CHAT_TYPE` | `SUPPORT` \| `CHAT` \| `BOTH` | Controls which users are shown |
|
|
104
104
|
|
|
@@ -126,7 +126,20 @@ NEXT_PUBLIC_CHAT_TYPE=BOTH
|
|
|
126
126
|
|
|
127
127
|
## User List API
|
|
128
128
|
|
|
129
|
-
The widget
|
|
129
|
+
The widget calls `CHAT_USER_LIST` in three ways:
|
|
130
|
+
|
|
131
|
+
1. **Same-origin / BFF (recommended)** — value starts with `/`, e.g. `/api/v1/chat/users`. The browser only shows a request to **your** app; your route handler proxies to the real API server-side, so the upstream URL (e.g. `http://your-api.com:4000/...`) does not appear as the client request URL in DevTools.
|
|
132
|
+
2. **Full URL** — value starts with `http://` or `https://`; that exact URL is fetched (visible in Network).
|
|
133
|
+
3. **Legacy** — otherwise it is built as
|
|
134
|
+
`CHAT_HOST_URL` + optional `:CHAT_HOST_PORT` + path.
|
|
135
|
+
|
|
136
|
+
Example (Next.js route at `app/api/v1/chat/users/route.ts` that forwards to your backend):
|
|
137
|
+
|
|
138
|
+
```env
|
|
139
|
+
NEXT_PUBLIC_CHAT_USER_LIST=/api/v1/chat/users
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Legacy example:
|
|
130
143
|
|
|
131
144
|
```
|
|
132
145
|
GET ${CHAT_HOST_URL}:${CHAT_HOST_PORT}/${CHAT_USER_LIST}
|
|
@@ -163,7 +176,7 @@ Expected response:
|
|
|
163
176
|
|
|
164
177
|
```tsx
|
|
165
178
|
// App.tsx
|
|
166
|
-
import { ChatWidget } from '
|
|
179
|
+
import { ChatWidget } from 'ajaxter-chat';
|
|
167
180
|
|
|
168
181
|
function App() {
|
|
169
182
|
return (
|
|
@@ -191,7 +204,7 @@ function App() {
|
|
|
191
204
|
```tsx
|
|
192
205
|
// app/ChatWidgetWrapper.tsx
|
|
193
206
|
'use client';
|
|
194
|
-
import { ChatWidget } from '
|
|
207
|
+
import { ChatWidget } from 'ajaxter-chat';
|
|
195
208
|
|
|
196
209
|
export function ChatWidgetWrapper() {
|
|
197
210
|
return <ChatWidget theme={{ primaryColor: '#0ea5e9' }} />;
|
|
@@ -218,7 +231,7 @@ export default function RootLayout({ children }) {
|
|
|
218
231
|
|
|
219
232
|
```tsx
|
|
220
233
|
// pages/_app.tsx
|
|
221
|
-
import { ChatWidget } from '
|
|
234
|
+
import { ChatWidget } from 'ajaxter-chat';
|
|
222
235
|
|
|
223
236
|
export default function MyApp({ Component, pageProps }) {
|
|
224
237
|
return (
|
|
@@ -276,7 +289,7 @@ socket.on('chat:message', (msg: ChatMessage) => {
|
|
|
276
289
|
## Build
|
|
277
290
|
|
|
278
291
|
```bash
|
|
279
|
-
cd
|
|
292
|
+
cd my_first_project
|
|
280
293
|
npm install
|
|
281
294
|
npm run build # Compile TypeScript → dist/
|
|
282
295
|
npm run type-check # Verify types without emitting
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BottomNavTab } from '../../types';
|
|
3
|
+
interface BottomNavProps {
|
|
4
|
+
active: BottomNavTab;
|
|
5
|
+
onChange: (tab: BottomNavTab) => void;
|
|
6
|
+
primaryColor: string;
|
|
7
|
+
fontFamily: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const BottomNav: React.FC<BottomNavProps>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export const BottomNav = ({ active, onChange, primaryColor, fontFamily, }) => {
|
|
3
|
+
const item = (tab, label, icon) => {
|
|
4
|
+
const isOn = active === tab;
|
|
5
|
+
return (_jsxs("button", { type: "button", onClick: () => onChange(tab), style: {
|
|
6
|
+
flex: 1,
|
|
7
|
+
display: 'flex',
|
|
8
|
+
flexDirection: 'column',
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
justifyContent: 'center',
|
|
11
|
+
gap: '4px',
|
|
12
|
+
padding: '10px 8px',
|
|
13
|
+
border: 'none',
|
|
14
|
+
background: 'transparent',
|
|
15
|
+
cursor: 'pointer',
|
|
16
|
+
fontFamily,
|
|
17
|
+
color: isOn ? primaryColor : '#b0b0b0',
|
|
18
|
+
transition: 'color 0.2s',
|
|
19
|
+
}, children: [_jsx("span", { style: { display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: icon }), _jsx("span", { style: { fontSize: '11px', fontWeight: isOn ? 600 : 500 }, children: label })] }));
|
|
20
|
+
};
|
|
21
|
+
return (_jsxs("nav", { style: {
|
|
22
|
+
flexShrink: 0,
|
|
23
|
+
display: 'flex',
|
|
24
|
+
alignItems: 'stretch',
|
|
25
|
+
backgroundColor: '#fff',
|
|
26
|
+
borderTop: '1px solid #eee',
|
|
27
|
+
paddingBottom: 'env(safe-area-inset-bottom, 0)',
|
|
28
|
+
}, "aria-label": "Widget sections", children: [item('home', 'Home', _jsx(HomeIcon, { active: active === 'home', color: primaryColor, muted: "#b0b0b0" })), item('chats', 'Chat', _jsx(ChatBubbleIcon, { active: active === 'chats', color: primaryColor, muted: "#b0b0b0" })), item('tickets', 'Tickets', _jsx(TicketIcon, { active: active === 'tickets', color: primaryColor, muted: "#b0b0b0" }))] }));
|
|
29
|
+
};
|
|
30
|
+
const HomeIcon = ({ active, color, muted, }) => (_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, children: _jsx("path", { d: "M3 9.5L12 3l9 6.5V20a1 1 0 0 1-1 1h-5v-7H9v7H4a1 1 0 0 1-1-1V9.5z", stroke: active ? color : muted, strokeWidth: "2", strokeLinejoin: "round" }) }));
|
|
31
|
+
const ChatBubbleIcon = ({ active, color, muted, }) => (_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, children: _jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z", stroke: active ? color : muted, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
32
|
+
const TicketIcon = ({ active, color, muted, }) => (_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, children: _jsx("path", { d: "M4 4h16v4H4V4zm0 6h10v10H4V10zm12 0h4v10h-4V10z", stroke: active ? color : muted, strokeWidth: "2", strokeLinejoin: "round" }) }));
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { ChatMessage, ChatUser } from '../../types';
|
|
3
|
-
interface ChatBoxProps {
|
|
3
|
+
export interface ChatBoxProps {
|
|
4
4
|
activeUser: ChatUser | null;
|
|
5
5
|
messages: ChatMessage[];
|
|
6
6
|
onSendMessage: (text: string) => void;
|
|
7
7
|
primaryColor: string;
|
|
8
8
|
fontFamily: string;
|
|
9
|
+
/** Minimal: teal bar with back/close, footer with extra actions (reference UI). */
|
|
10
|
+
variant?: 'default' | 'minimal';
|
|
11
|
+
onBack?: () => void;
|
|
12
|
+
onClose?: () => void;
|
|
13
|
+
headerTitle?: string;
|
|
9
14
|
}
|
|
10
15
|
export declare const ChatBox: React.FC<ChatBoxProps>;
|
|
11
|
-
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useRef, useEffect } from 'react';
|
|
3
|
-
export const ChatBox = ({ activeUser, messages, onSendMessage, primaryColor, fontFamily, }) => {
|
|
3
|
+
export const ChatBox = ({ activeUser, messages, onSendMessage, primaryColor, fontFamily, variant = 'default', onBack, onClose, headerTitle, }) => {
|
|
4
4
|
const [inputText, setInputText] = useState('');
|
|
5
5
|
const messagesEndRef = useRef(null);
|
|
6
6
|
const inputRef = useRef(null);
|
|
@@ -22,6 +22,61 @@ export const ChatBox = ({ activeUser, messages, onSendMessage, primaryColor, fon
|
|
|
22
22
|
handleSend();
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
|
+
if (variant === 'minimal') {
|
|
26
|
+
return (_jsxs("div", { style: {
|
|
27
|
+
flex: 1,
|
|
28
|
+
display: 'flex',
|
|
29
|
+
flexDirection: 'column',
|
|
30
|
+
fontFamily,
|
|
31
|
+
overflow: 'hidden',
|
|
32
|
+
backgroundColor: '#fff',
|
|
33
|
+
}, children: [_jsxs("div", { style: {
|
|
34
|
+
flexShrink: 0,
|
|
35
|
+
padding: '14px 16px',
|
|
36
|
+
backgroundColor: primaryColor,
|
|
37
|
+
color: '#fff',
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'space-between',
|
|
41
|
+
}, children: [_jsx("button", { type: "button", onClick: onBack, "aria-label": "Back", style: {
|
|
42
|
+
background: 'transparent',
|
|
43
|
+
border: 'none',
|
|
44
|
+
color: '#fff',
|
|
45
|
+
cursor: 'pointer',
|
|
46
|
+
padding: '4px',
|
|
47
|
+
display: 'flex',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
}, children: _jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M15 18L9 12L15 6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsx("div", { style: { fontWeight: 600, fontSize: '15px', flex: 1, textAlign: 'center' }, children: headerTitle !== null && headerTitle !== void 0 ? headerTitle : (activeUser ? activeUser.name : 'Chat') }), _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close", style: {
|
|
50
|
+
background: 'transparent',
|
|
51
|
+
border: 'none',
|
|
52
|
+
color: '#fff',
|
|
53
|
+
cursor: 'pointer',
|
|
54
|
+
padding: '4px',
|
|
55
|
+
display: 'flex',
|
|
56
|
+
}, children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }) })] }), _jsxs("div", { style: {
|
|
57
|
+
flex: 1,
|
|
58
|
+
overflowY: 'auto',
|
|
59
|
+
padding: '16px',
|
|
60
|
+
backgroundColor: '#fff',
|
|
61
|
+
}, children: [!activeUser ? (_jsx("div", { style: { textAlign: 'center', color: '#bbb', fontSize: '14px', marginTop: '32px' }, children: "Select someone to chat" })) : messages.length === 0 ? (_jsxs("div", { style: { textAlign: 'center', color: '#ccc', fontSize: '13px', marginTop: '32px' }, children: ["Say hello to ", activeUser.name, "! \uD83D\uDC4B"] })) : (messages.map((msg) => (_jsx(MessageBubble, { message: msg, primaryColor: primaryColor }, msg.id)))), _jsx("div", { ref: messagesEndRef })] }), _jsxs("div", { style: {
|
|
62
|
+
borderTop: '1px solid #eee',
|
|
63
|
+
backgroundColor: '#fff',
|
|
64
|
+
padding: '10px 12px',
|
|
65
|
+
display: 'flex',
|
|
66
|
+
alignItems: 'center',
|
|
67
|
+
gap: '8px',
|
|
68
|
+
}, children: [_jsx("textarea", { ref: inputRef, value: inputText, onChange: (e) => setInputText(e.target.value), onKeyDown: handleKeyDown, placeholder: "Type and press [enter]..", rows: 1, disabled: !activeUser, style: {
|
|
69
|
+
flex: 1,
|
|
70
|
+
resize: 'none',
|
|
71
|
+
border: 'none',
|
|
72
|
+
outline: 'none',
|
|
73
|
+
fontFamily,
|
|
74
|
+
fontSize: '14px',
|
|
75
|
+
color: '#1a1a2e',
|
|
76
|
+
maxHeight: '80px',
|
|
77
|
+
lineHeight: 1.45,
|
|
78
|
+
} }), _jsx(IconButton, { label: "Like", disabled: !activeUser, children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "#888", strokeWidth: "1.8", children: _jsx("path", { d: "M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" }) }) }), _jsx(IconButton, { label: "Attach", disabled: !activeUser, children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "#888", strokeWidth: "1.8", children: _jsx("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }) }) }), _jsx(IconButton, { label: "Emoji", disabled: !activeUser, children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "#888", strokeWidth: "1.8", children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01" })] }) })] })] }));
|
|
79
|
+
}
|
|
25
80
|
if (!activeUser) {
|
|
26
81
|
return (_jsxs("div", { style: {
|
|
27
82
|
flex: 1,
|
|
@@ -138,6 +193,16 @@ export const ChatBox = ({ activeUser, messages, onSendMessage, primaryColor, fon
|
|
|
138
193
|
transition: 'all 0.2s ease',
|
|
139
194
|
}, children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z", stroke: inputText.trim() ? '#fff' : '#bbb', strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })] }));
|
|
140
195
|
};
|
|
196
|
+
const IconButton = ({ label, disabled, children }) => (_jsx("button", { type: "button", "aria-label": label, disabled: disabled, style: {
|
|
197
|
+
background: 'transparent',
|
|
198
|
+
border: 'none',
|
|
199
|
+
padding: '6px',
|
|
200
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
201
|
+
opacity: disabled ? 0.45 : 1,
|
|
202
|
+
display: 'flex',
|
|
203
|
+
alignItems: 'center',
|
|
204
|
+
justifyContent: 'center',
|
|
205
|
+
}, children: children }));
|
|
141
206
|
const MessageBubble = ({ message, primaryColor }) => {
|
|
142
207
|
const isMe = message.senderId === 'me';
|
|
143
208
|
const time = new Date(message.timestamp).toLocaleTimeString([], {
|
|
@@ -45,6 +45,6 @@ export const ChatWidget = ({ theme }) => {
|
|
|
45
45
|
// DISABLE: render nothing at all
|
|
46
46
|
if (config.status === 'DISABLE')
|
|
47
47
|
return null;
|
|
48
|
-
return (_jsxs(_Fragment, { children: [isOpen && (_jsx(ChatWindow, { config: config, theme: theme, buttonPosition: t.buttonPosition })), _jsx(ChatButton, { isOpen: isOpen, onClick: () => setIsOpen((prev) => !prev), theme: theme })] }));
|
|
48
|
+
return (_jsxs(_Fragment, { children: [isOpen && (_jsx(ChatWindow, { config: config, theme: theme, buttonPosition: t.buttonPosition, onClose: () => setIsOpen(false) })), _jsx(ChatButton, { isOpen: isOpen, onClick: () => setIsOpen((prev) => !prev), theme: theme })] }));
|
|
49
49
|
};
|
|
50
50
|
export default ChatWidget;
|
|
@@ -1,39 +1,112 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo, useCallback } from 'react';
|
|
3
3
|
import { UserList } from '../UserList';
|
|
4
4
|
import { ChatBox } from '../ChatBox';
|
|
5
5
|
import { MaintenanceView } from '../MaintenanceView';
|
|
6
|
+
import { HomeView } from '../HomeView';
|
|
7
|
+
import { BottomNav } from '../BottomNav';
|
|
6
8
|
import { useUsers } from '../../hooks/useUsers';
|
|
7
9
|
import { useChat } from '../../hooks/useChat';
|
|
8
10
|
import { buildUserListUrl } from '../../config';
|
|
9
11
|
import { mergeTheme } from '../../utils/theme';
|
|
10
|
-
export const ChatWindow = ({ config, theme, buttonPosition, }) => {
|
|
11
|
-
var _a;
|
|
12
|
-
const t = mergeTheme(theme);
|
|
12
|
+
export const ChatWindow = ({ config, theme, buttonPosition, onClose, }) => {
|
|
13
|
+
var _a, _b, _c, _d, _e, _f;
|
|
14
|
+
const t = mergeTheme(Object.assign(Object.assign({}, theme), { widgetMinWidth: (_a = theme === null || theme === void 0 ? void 0 : theme.widgetMinWidth) !== null && _a !== void 0 ? _a : config.widgetMinWidth, widgetMaxWidth: (_b = theme === null || theme === void 0 ? void 0 : theme.widgetMaxWidth) !== null && _b !== void 0 ? _b : config.widgetMaxWidth, widgetMinHeight: (_c = theme === null || theme === void 0 ? void 0 : theme.widgetMinHeight) !== null && _c !== void 0 ? _c : config.widgetMinHeight, widgetMaxHeight: (_d = theme === null || theme === void 0 ? void 0 : theme.widgetMaxHeight) !== null && _d !== void 0 ? _d : config.widgetMaxHeight, widgetDefaultSize: (_e = theme === null || theme === void 0 ? void 0 : theme.widgetDefaultSize) !== null && _e !== void 0 ? _e : config.widgetDefaultSize }));
|
|
15
|
+
const minW = Math.min(t.widgetMinWidth, t.widgetMaxWidth);
|
|
16
|
+
const maxW = Math.max(t.widgetMinWidth, t.widgetMaxWidth);
|
|
17
|
+
const minH = Math.min(t.widgetMinHeight, t.widgetMaxHeight);
|
|
18
|
+
const maxH = Math.max(t.widgetMinHeight, t.widgetMaxHeight);
|
|
19
|
+
const [sizeRatio, setSizeRatio] = useState(t.widgetDefaultSize);
|
|
20
|
+
const widthPx = minW + (maxW - minW) * sizeRatio;
|
|
21
|
+
const heightPx = minH + (maxH - minH) * sizeRatio;
|
|
22
|
+
const [bottomTab, setBottomTab] = useState('home');
|
|
23
|
+
const [homeFlow, setHomeFlow] = useState('home');
|
|
24
|
+
const [pickUserMode, setPickUserMode] = useState(null);
|
|
25
|
+
const [tickets, setTickets] = useState([]);
|
|
26
|
+
const [ticketSubject, setTicketSubject] = useState('');
|
|
27
|
+
const [ticketBody, setTicketBody] = useState('');
|
|
13
28
|
const userListUrl = buildUserListUrl(config);
|
|
14
|
-
const
|
|
15
|
-
const filterType = config.chatType === 'SUPPORT'
|
|
29
|
+
const filterType = pickUserMode === 'support'
|
|
16
30
|
? 'developer'
|
|
17
|
-
:
|
|
31
|
+
: pickUserMode === 'conversation'
|
|
18
32
|
? 'user'
|
|
19
|
-
:
|
|
33
|
+
: config.chatType === 'SUPPORT'
|
|
20
34
|
? 'developer'
|
|
21
|
-
: '
|
|
35
|
+
: config.chatType === 'CHAT'
|
|
36
|
+
? 'user'
|
|
37
|
+
: 'developer';
|
|
22
38
|
const { users, loading, error } = useUsers({
|
|
23
39
|
url: userListUrl,
|
|
24
40
|
filterType,
|
|
25
|
-
enabled: config.status === 'ACTIVE',
|
|
41
|
+
enabled: config.status === 'ACTIVE' && homeFlow === 'pickUser',
|
|
26
42
|
});
|
|
27
|
-
const { messages, activeUser, selectUser, sendMessage } = useChat();
|
|
43
|
+
const { messages, activeUser, recentChats, selectUser, sendMessage, openRecent, } = useChat();
|
|
28
44
|
const positionStyle = buttonPosition === 'bottom-left'
|
|
29
45
|
? { left: '24px', right: 'auto' }
|
|
30
46
|
: { right: '24px', left: 'auto' };
|
|
31
|
-
|
|
47
|
+
const goHomeCards = useCallback(() => {
|
|
48
|
+
setHomeFlow('home');
|
|
49
|
+
setPickUserMode(null);
|
|
50
|
+
}, []);
|
|
51
|
+
const handleBackFromChat = useCallback(() => {
|
|
52
|
+
if (pickUserMode) {
|
|
53
|
+
setHomeFlow('pickUser');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
setBottomTab('chats');
|
|
57
|
+
setHomeFlow('home');
|
|
58
|
+
}, [pickUserMode]);
|
|
59
|
+
const handleBottomTab = useCallback((tab) => {
|
|
60
|
+
setBottomTab(tab);
|
|
61
|
+
goHomeCards();
|
|
62
|
+
}, [goHomeCards]);
|
|
63
|
+
const startSupport = useCallback(() => {
|
|
64
|
+
setPickUserMode('support');
|
|
65
|
+
setHomeFlow('pickUser');
|
|
66
|
+
}, []);
|
|
67
|
+
const startConversation = useCallback(() => {
|
|
68
|
+
setPickUserMode('conversation');
|
|
69
|
+
setHomeFlow('pickUser');
|
|
70
|
+
}, []);
|
|
71
|
+
const startRaiseTicket = useCallback(() => {
|
|
72
|
+
setHomeFlow('raiseTicket');
|
|
73
|
+
setTicketSubject('');
|
|
74
|
+
setTicketBody('');
|
|
75
|
+
}, []);
|
|
76
|
+
const submitTicket = useCallback(() => {
|
|
77
|
+
if (!ticketSubject.trim())
|
|
78
|
+
return;
|
|
79
|
+
const id = `tkt_${Date.now()}`;
|
|
80
|
+
setTickets((prev) => [
|
|
81
|
+
{
|
|
82
|
+
id,
|
|
83
|
+
subject: ticketSubject.trim(),
|
|
84
|
+
body: ticketBody.trim(),
|
|
85
|
+
createdAt: new Date(),
|
|
86
|
+
status: 'open',
|
|
87
|
+
},
|
|
88
|
+
...prev,
|
|
89
|
+
]);
|
|
90
|
+
setHomeFlow('home');
|
|
91
|
+
setBottomTab('tickets');
|
|
92
|
+
}, [ticketSubject, ticketBody]);
|
|
93
|
+
const pickUserHeader = useMemo(() => {
|
|
94
|
+
if (pickUserMode === 'support')
|
|
95
|
+
return 'Support';
|
|
96
|
+
if (pickUserMode === 'conversation')
|
|
97
|
+
return 'New conversation';
|
|
98
|
+
return 'People';
|
|
99
|
+
}, [pickUserMode]);
|
|
100
|
+
return (_jsxs("div", { style: Object.assign(Object.assign({ position: 'fixed', bottom: '90px' }, positionStyle), { zIndex: 9998, width: `${widthPx}px`, maxWidth: 'calc(100vw - 32px)', height: `${heightPx}px`, maxHeight: 'calc(100vh - 120px)', backgroundColor: t.backgroundColor, borderRadius: t.borderRadius, boxShadow: '0 24px 80px rgba(0,0,0,0.18), 0 8px 24px rgba(0,0,0,0.08)', display: 'flex', flexDirection: 'column', overflow: 'hidden', fontFamily: t.fontFamily, animation: 'cw-slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)' }), children: [_jsx("style", { children: `
|
|
32
101
|
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap');
|
|
33
102
|
@keyframes cw-slideUp {
|
|
34
103
|
from { opacity: 0; transform: translateY(20px) scale(0.97); }
|
|
35
104
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
36
105
|
}
|
|
106
|
+
@keyframes cw-slideFromRight {
|
|
107
|
+
from { opacity: 0; transform: translateX(24px); }
|
|
108
|
+
to { opacity: 1; transform: translateX(0); }
|
|
109
|
+
}
|
|
37
110
|
@keyframes shimmer {
|
|
38
111
|
0% { background-position: 200% 0; }
|
|
39
112
|
100% { background-position: -200% 0; }
|
|
@@ -41,92 +114,173 @@ export const ChatWindow = ({ config, theme, buttonPosition, }) => {
|
|
|
41
114
|
::-webkit-scrollbar { width: 4px; }
|
|
42
115
|
::-webkit-scrollbar-track { background: transparent; }
|
|
43
116
|
::-webkit-scrollbar-thumb { background: #e0e0e0; border-radius: 4px; }
|
|
44
|
-
` }), _jsxs("div", { style: {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
117
|
+
` }), config.status === 'MAINTENANCE' ? (_jsx(MaintenanceView, { fontFamily: t.fontFamily, primaryColor: t.primaryColor })) : (_jsxs(_Fragment, { children: [_jsxs("div", { style: { flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }, children: [bottomTab === 'home' && homeFlow === 'home' && (_jsx(HomeView, { primaryColor: t.primaryColor, fontFamily: t.fontFamily, showNeedSupport: config.showNeedSupport, showNewConversation: config.showNewConversation, onNeedSupport: startSupport, onNewConversation: startConversation, onRaiseTicket: startRaiseTicket, onClose: onClose })), bottomTab === 'home' && homeFlow === 'pickUser' && (_jsxs("div", { style: {
|
|
118
|
+
flex: 1,
|
|
119
|
+
display: 'flex',
|
|
120
|
+
flexDirection: 'column',
|
|
121
|
+
minHeight: 0,
|
|
122
|
+
backgroundColor: '#fff',
|
|
123
|
+
animation: 'cw-slideFromRight 0.28s ease-out',
|
|
124
|
+
}, children: [_jsxs("div", { style: {
|
|
125
|
+
flexShrink: 0,
|
|
126
|
+
padding: '14px 16px',
|
|
127
|
+
backgroundColor: t.primaryColor,
|
|
128
|
+
color: '#fff',
|
|
129
|
+
display: 'flex',
|
|
130
|
+
alignItems: 'center',
|
|
131
|
+
gap: '12px',
|
|
132
|
+
}, children: [_jsx("button", { type: "button", onClick: goHomeCards, "aria-label": "Back", style: {
|
|
133
|
+
background: 'transparent',
|
|
134
|
+
border: 'none',
|
|
135
|
+
color: '#fff',
|
|
136
|
+
cursor: 'pointer',
|
|
137
|
+
padding: '4px',
|
|
138
|
+
}, children: _jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M15 18L9 12L15 6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsx("div", { style: { fontWeight: 700, fontSize: '16px' }, children: pickUserHeader })] }), _jsxs("div", { style: { flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }, children: [_jsx("div", { style: {
|
|
139
|
+
padding: '10px 16px',
|
|
140
|
+
fontSize: '11px',
|
|
141
|
+
fontWeight: 700,
|
|
142
|
+
color: '#bbb',
|
|
143
|
+
textTransform: 'uppercase',
|
|
144
|
+
letterSpacing: '0.08em',
|
|
145
|
+
borderBottom: '1px solid #f0f0f0',
|
|
146
|
+
}, children: "Select a contact" }), _jsx(UserList, { users: users, loading: loading, error: error, activeUserId: (_f = activeUser === null || activeUser === void 0 ? void 0 : activeUser.uid) !== null && _f !== void 0 ? _f : null, onSelectUser: (u) => {
|
|
147
|
+
selectUser(u);
|
|
148
|
+
setHomeFlow('chat');
|
|
149
|
+
}, primaryColor: t.primaryColor, fontFamily: t.fontFamily })] })] })), bottomTab === 'home' && homeFlow === 'chat' && (_jsx("div", { style: {
|
|
150
|
+
flex: 1,
|
|
151
|
+
display: 'flex',
|
|
152
|
+
flexDirection: 'column',
|
|
153
|
+
minHeight: 0,
|
|
154
|
+
animation: 'cw-slideFromRight 0.28s ease-out',
|
|
155
|
+
}, children: _jsx(ChatBox, { activeUser: activeUser, messages: messages, onSendMessage: sendMessage, primaryColor: t.primaryColor, fontFamily: t.fontFamily, variant: "minimal", onBack: handleBackFromChat, onClose: onClose, headerTitle: activeUser === null || activeUser === void 0 ? void 0 : activeUser.name }) })), bottomTab === 'home' && homeFlow === 'raiseTicket' && (_jsxs("div", { style: {
|
|
156
|
+
flex: 1,
|
|
57
157
|
display: 'flex',
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
158
|
+
flexDirection: 'column',
|
|
159
|
+
minHeight: 0,
|
|
160
|
+
backgroundColor: '#fff',
|
|
161
|
+
animation: 'cw-slideFromRight 0.28s ease-out',
|
|
162
|
+
}, children: [_jsxs("div", { style: {
|
|
163
|
+
flexShrink: 0,
|
|
164
|
+
padding: '14px 16px',
|
|
165
|
+
backgroundColor: t.primaryColor,
|
|
166
|
+
color: '#fff',
|
|
167
|
+
display: 'flex',
|
|
168
|
+
alignItems: 'center',
|
|
169
|
+
justifyContent: 'space-between',
|
|
170
|
+
}, children: [_jsx("button", { type: "button", onClick: goHomeCards, "aria-label": "Back", style: {
|
|
171
|
+
background: 'transparent',
|
|
172
|
+
border: 'none',
|
|
173
|
+
color: '#fff',
|
|
174
|
+
cursor: 'pointer',
|
|
175
|
+
padding: '4px',
|
|
176
|
+
}, children: _jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M15 18L9 12L15 6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsx("span", { style: { fontWeight: 700, fontSize: '16px' }, children: "Raise ticket" }), _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close", style: {
|
|
177
|
+
background: 'transparent',
|
|
178
|
+
border: 'none',
|
|
179
|
+
color: '#fff',
|
|
180
|
+
cursor: 'pointer',
|
|
181
|
+
padding: '4px',
|
|
182
|
+
}, children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }) })] }), _jsxs("div", { style: { padding: '20px', flex: 1, overflowY: 'auto' }, children: [_jsx("label", { style: { display: 'block', fontSize: '12px', fontWeight: 600, color: '#666', marginBottom: '6px' }, children: "Subject" }), _jsx("input", { value: ticketSubject, onChange: (e) => setTicketSubject(e.target.value), placeholder: "Brief summary", style: {
|
|
183
|
+
width: '100%',
|
|
184
|
+
boxSizing: 'border-box',
|
|
185
|
+
padding: '10px 12px',
|
|
186
|
+
borderRadius: '10px',
|
|
187
|
+
border: '1px solid #e5e5e5',
|
|
188
|
+
fontFamily: t.fontFamily,
|
|
189
|
+
fontSize: '14px',
|
|
190
|
+
marginBottom: '16px',
|
|
191
|
+
} }), _jsx("label", { style: { display: 'block', fontSize: '12px', fontWeight: 600, color: '#666', marginBottom: '6px' }, children: "Description" }), _jsx("textarea", { value: ticketBody, onChange: (e) => setTicketBody(e.target.value), placeholder: "Describe the issue or change\u2026", rows: 5, style: {
|
|
192
|
+
width: '100%',
|
|
193
|
+
boxSizing: 'border-box',
|
|
194
|
+
padding: '10px 12px',
|
|
195
|
+
borderRadius: '10px',
|
|
196
|
+
border: '1px solid #e5e5e5',
|
|
197
|
+
fontFamily: t.fontFamily,
|
|
198
|
+
fontSize: '14px',
|
|
199
|
+
resize: 'vertical',
|
|
200
|
+
} }), _jsx("button", { type: "button", onClick: submitTicket, style: {
|
|
201
|
+
marginTop: '18px',
|
|
202
|
+
width: '100%',
|
|
203
|
+
padding: '12px',
|
|
204
|
+
borderRadius: '10px',
|
|
205
|
+
border: 'none',
|
|
206
|
+
backgroundColor: t.primaryColor,
|
|
207
|
+
color: '#fff',
|
|
208
|
+
fontWeight: 700,
|
|
209
|
+
fontFamily: t.fontFamily,
|
|
210
|
+
cursor: 'pointer',
|
|
211
|
+
fontSize: '15px',
|
|
212
|
+
}, children: "Submit ticket" })] })] })), bottomTab === 'chats' && (_jsxs("div", { style: {
|
|
213
|
+
flex: 1,
|
|
214
|
+
display: 'flex',
|
|
215
|
+
flexDirection: 'column',
|
|
216
|
+
minHeight: 0,
|
|
217
|
+
backgroundColor: '#fff',
|
|
218
|
+
}, children: [_jsxs("div", { style: {
|
|
219
|
+
padding: '16px 20px',
|
|
220
|
+
backgroundColor: t.primaryColor,
|
|
221
|
+
color: '#fff',
|
|
222
|
+
fontWeight: 700,
|
|
223
|
+
fontSize: '17px',
|
|
224
|
+
display: 'flex',
|
|
225
|
+
alignItems: 'center',
|
|
226
|
+
justifyContent: 'space-between',
|
|
227
|
+
}, children: ["Recent chats", _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close", style: {
|
|
228
|
+
background: 'transparent',
|
|
229
|
+
border: 'none',
|
|
230
|
+
color: '#fff',
|
|
231
|
+
cursor: 'pointer',
|
|
232
|
+
}, children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }) })] }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: recentChats.length === 0 ? (_jsx("p", { style: { padding: '24px', textAlign: 'center', color: '#aaa', fontSize: '14px', margin: 0 }, children: "No recent chats yet" })) : (recentChats.map((r) => (_jsxs("button", { type: "button", onClick: () => {
|
|
233
|
+
openRecent(r.user);
|
|
234
|
+
setBottomTab('home');
|
|
235
|
+
setHomeFlow('chat');
|
|
236
|
+
setPickUserMode(null);
|
|
237
|
+
}, style: {
|
|
238
|
+
width: '100%',
|
|
239
|
+
padding: '14px 18px',
|
|
240
|
+
border: 'none',
|
|
241
|
+
borderBottom: '1px solid #f0f0f0',
|
|
242
|
+
background: '#fff',
|
|
243
|
+
textAlign: 'left',
|
|
244
|
+
cursor: 'pointer',
|
|
245
|
+
fontFamily: t.fontFamily,
|
|
246
|
+
}, children: [_jsx("div", { style: { fontWeight: 600, color: '#1a1a2e' }, children: r.user.name }), _jsx("div", { style: { fontSize: '13px', color: '#888', marginTop: '4px' }, children: r.lastMessage }), _jsx("div", { style: { fontSize: '11px', color: '#bbb', marginTop: '6px' }, children: r.updatedAt.toLocaleString() })] }, r.user.uid)))) })] })), bottomTab === 'tickets' && (_jsxs("div", { style: {
|
|
247
|
+
flex: 1,
|
|
248
|
+
display: 'flex',
|
|
249
|
+
flexDirection: 'column',
|
|
250
|
+
minHeight: 0,
|
|
251
|
+
backgroundColor: '#fff',
|
|
252
|
+
}, children: [_jsxs("div", { style: {
|
|
253
|
+
padding: '16px 20px',
|
|
254
|
+
backgroundColor: t.primaryColor,
|
|
255
|
+
color: '#fff',
|
|
256
|
+
fontWeight: 700,
|
|
257
|
+
fontSize: '17px',
|
|
258
|
+
display: 'flex',
|
|
259
|
+
alignItems: 'center',
|
|
260
|
+
justifyContent: 'space-between',
|
|
261
|
+
}, children: ["Tickets", _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close", style: {
|
|
262
|
+
background: 'transparent',
|
|
263
|
+
border: 'none',
|
|
264
|
+
color: '#fff',
|
|
265
|
+
cursor: 'pointer',
|
|
266
|
+
}, children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }) })] }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: tickets.length === 0 ? (_jsx("p", { style: { padding: '24px', textAlign: 'center', color: '#aaa', fontSize: '14px', margin: 0 }, children: "No tickets yet. Use Home \u2192 Raise Ticket." })) : (tickets.map((tk) => (_jsxs("div", { style: {
|
|
267
|
+
padding: '14px 18px',
|
|
268
|
+
borderBottom: '1px solid #f0f0f0',
|
|
269
|
+
fontFamily: t.fontFamily,
|
|
270
|
+
}, children: [_jsx("div", { style: { fontWeight: 600, color: '#1a1a2e' }, children: tk.subject }), tk.body ? (_jsx("div", { style: { fontSize: '13px', color: '#666', marginTop: '6px', lineHeight: 1.45 }, children: tk.body })) : null, _jsxs("div", { style: { fontSize: '11px', color: '#bbb', marginTop: '8px' }, children: [tk.createdAt.toLocaleString(), " \u00B7 ", tk.status] })] }, tk.id)))) })] }))] }), _jsx("div", { style: {
|
|
271
|
+
padding: '8px 14px 6px',
|
|
272
|
+
borderTop: '1px solid #eee',
|
|
273
|
+
backgroundColor: '#fafafa',
|
|
87
274
|
flexShrink: 0,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
};
|
|
99
|
-
const StatusBadge = ({ status }) => {
|
|
100
|
-
var _a;
|
|
101
|
-
const map = {
|
|
102
|
-
ACTIVE: { label: 'Active', bg: 'rgba(255,255,255,0.2)', dot: '#4caf50' },
|
|
103
|
-
MAINTENANCE: { label: 'Maintenance', bg: 'rgba(255,193,7,0.3)', dot: '#ffc107' },
|
|
104
|
-
DISABLE: { label: 'Disabled', bg: 'rgba(0,0,0,0.2)', dot: '#f44336' },
|
|
105
|
-
};
|
|
106
|
-
const s = (_a = map[status]) !== null && _a !== void 0 ? _a : map['DISABLE'];
|
|
107
|
-
return (_jsxs("span", { style: {
|
|
108
|
-
display: 'inline-flex',
|
|
109
|
-
alignItems: 'center',
|
|
110
|
-
gap: '5px',
|
|
111
|
-
padding: '4px 10px',
|
|
112
|
-
borderRadius: '20px',
|
|
113
|
-
backgroundColor: s.bg,
|
|
114
|
-
fontSize: '11px',
|
|
115
|
-
fontWeight: 600,
|
|
116
|
-
color: '#fff',
|
|
117
|
-
}, children: [_jsx("span", { style: {
|
|
118
|
-
width: '6px',
|
|
119
|
-
height: '6px',
|
|
120
|
-
borderRadius: '50%',
|
|
121
|
-
backgroundColor: s.dot,
|
|
122
|
-
display: 'inline-block',
|
|
123
|
-
} }), s.label] }));
|
|
275
|
+
}, children: _jsxs("div", { style: {
|
|
276
|
+
display: 'flex',
|
|
277
|
+
alignItems: 'center',
|
|
278
|
+
gap: '10px',
|
|
279
|
+
fontSize: '11px',
|
|
280
|
+
color: '#888',
|
|
281
|
+
fontFamily: t.fontFamily,
|
|
282
|
+
}, children: [_jsx("span", { style: { flexShrink: 0 }, children: "Size" }), _jsx("input", { type: "range", min: 0, max: 1, step: 0.02, value: sizeRatio, onChange: (e) => setSizeRatio(parseFloat(e.target.value)), "aria-label": "Widget size", style: {
|
|
283
|
+
flex: 1,
|
|
284
|
+
accentColor: t.primaryColor,
|
|
285
|
+
} })] }) }), _jsx(BottomNav, { active: bottomTab, onChange: handleBottomTab, primaryColor: t.primaryColor, fontFamily: t.fontFamily })] }))] }));
|
|
124
286
|
};
|
|
125
|
-
// Simple color darkener for gradient
|
|
126
|
-
function adjustColor(hex, amount) {
|
|
127
|
-
const num = parseInt(hex.replace('#', ''), 16);
|
|
128
|
-
const r = Math.max(0, Math.min(255, (num >> 16) + amount));
|
|
129
|
-
const g = Math.max(0, Math.min(255, ((num >> 8) & 0xff) + amount));
|
|
130
|
-
const b = Math.max(0, Math.min(255, (num & 0xff) + amount));
|
|
131
|
-
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
|
|
132
|
-
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface HomeViewProps {
|
|
3
|
+
primaryColor: string;
|
|
4
|
+
fontFamily: string;
|
|
5
|
+
showNeedSupport: boolean;
|
|
6
|
+
showNewConversation: boolean;
|
|
7
|
+
onNeedSupport: () => void;
|
|
8
|
+
onNewConversation: () => void;
|
|
9
|
+
onRaiseTicket: () => void;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
}
|
|
12
|
+
export declare const HomeView: React.FC<HomeViewProps>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const SendIcon = ({ color }) => (_jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, children: _jsx("path", { d: "M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
3
|
+
export const HomeView = ({ primaryColor, fontFamily, showNeedSupport, showNewConversation, onNeedSupport, onNewConversation, onRaiseTicket, onClose, }) => {
|
|
4
|
+
const cardStyle = {
|
|
5
|
+
width: '100%',
|
|
6
|
+
backgroundColor: '#fff',
|
|
7
|
+
borderRadius: '12px',
|
|
8
|
+
padding: '16px 18px',
|
|
9
|
+
display: 'flex',
|
|
10
|
+
alignItems: 'center',
|
|
11
|
+
justifyContent: 'space-between',
|
|
12
|
+
gap: '12px',
|
|
13
|
+
border: 'none',
|
|
14
|
+
cursor: 'pointer',
|
|
15
|
+
textAlign: 'left',
|
|
16
|
+
fontFamily,
|
|
17
|
+
boxShadow: '0 2px 12px rgba(0,0,0,0.06)',
|
|
18
|
+
transition: 'transform 0.15s ease, box-shadow 0.15s ease',
|
|
19
|
+
};
|
|
20
|
+
return (_jsxs("div", { style: {
|
|
21
|
+
flex: 1,
|
|
22
|
+
display: 'flex',
|
|
23
|
+
flexDirection: 'column',
|
|
24
|
+
backgroundColor: primaryColor,
|
|
25
|
+
fontFamily,
|
|
26
|
+
minHeight: 0,
|
|
27
|
+
}, children: [_jsx("div", { style: {
|
|
28
|
+
padding: '20px 20px 8px',
|
|
29
|
+
display: 'flex',
|
|
30
|
+
justifyContent: 'flex-end',
|
|
31
|
+
}, children: _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close", style: {
|
|
32
|
+
background: 'transparent',
|
|
33
|
+
border: 'none',
|
|
34
|
+
color: '#fff',
|
|
35
|
+
cursor: 'pointer',
|
|
36
|
+
padding: '4px',
|
|
37
|
+
}, children: _jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }) }) }), _jsxs("div", { style: { padding: '0 24px 20px', flexShrink: 0 }, children: [_jsx("h1", { style: {
|
|
38
|
+
margin: 0,
|
|
39
|
+
color: '#fff',
|
|
40
|
+
fontSize: '26px',
|
|
41
|
+
fontWeight: 700,
|
|
42
|
+
letterSpacing: '-0.02em',
|
|
43
|
+
}, children: "Hi there \uD83D\uDC4B" }), _jsx("p", { style: { margin: '10px 0 0', color: 'rgba(255,255,255,0.92)', fontSize: '14px', lineHeight: 1.5 }, children: "Need help? start a conversation:" })] }), _jsxs("div", { style: {
|
|
44
|
+
flex: 1,
|
|
45
|
+
padding: '0 20px 16px',
|
|
46
|
+
display: 'flex',
|
|
47
|
+
flexDirection: 'column',
|
|
48
|
+
gap: '12px',
|
|
49
|
+
overflowY: 'auto',
|
|
50
|
+
}, children: [showNeedSupport && (_jsxs("button", { type: "button", onClick: onNeedSupport, style: cardStyle, children: [_jsxs("div", { children: [_jsx("div", { style: { fontWeight: 700, fontSize: '15px', color: '#1a1a2e' }, children: "Need Support" }), _jsx("div", { style: { fontSize: '13px', color: '#888', marginTop: '4px' }, children: "We typically reply in a few minutes" })] }), _jsx(SendIcon, { color: primaryColor })] })), showNewConversation && (_jsxs("button", { type: "button", onClick: onNewConversation, style: cardStyle, children: [_jsxs("div", { children: [_jsx("div", { style: { fontWeight: 700, fontSize: '15px', color: '#1a1a2e' }, children: "New Conversation" }), _jsx("div", { style: { fontSize: '13px', color: '#888', marginTop: '4px' }, children: "With your colleague" })] }), _jsx(SendIcon, { color: primaryColor })] })), _jsxs("button", { type: "button", onClick: onRaiseTicket, style: cardStyle, children: [_jsxs("div", { children: [_jsx("div", { style: { fontWeight: 700, fontSize: '15px', color: '#1a1a2e' }, children: "Raise Ticket" }), _jsx("div", { style: { fontSize: '13px', color: '#888', marginTop: '4px' }, children: "For major changes" })] }), _jsx(SendIcon, { color: primaryColor })] })] })] }));
|
|
51
|
+
};
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
1
|
import { ChatConfig } from '../types';
|
|
2
2
|
export declare function loadChatConfig(): ChatConfig;
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the URL used by `fetchUsers`.
|
|
5
|
+
*
|
|
6
|
+
* **Same-origin / internal proxy (recommended):** If `CHAT_USER_LIST` starts with `/`
|
|
7
|
+
* (e.g. `/api/v1/chat/users`), the browser only requests your app origin. The real
|
|
8
|
+
* backend URL stays on the server (Next.js route, BFF, etc.) and does not appear in
|
|
9
|
+
* the client Network tab as a cross-origin call.
|
|
10
|
+
*
|
|
11
|
+
* **Absolute URL:** If `CHAT_USER_LIST` is a full `http(s)://...` string, it is used as-is
|
|
12
|
+
* (visible in Network as that host).
|
|
13
|
+
*
|
|
14
|
+
* **Legacy:** Otherwise the path is joined with `CHAT_HOST_URL` and optional `CHAT_HOST_PORT`.
|
|
15
|
+
*/
|
|
3
16
|
export declare function buildUserListUrl(config: ChatConfig): string;
|
package/dist/config/index.js
CHANGED
|
@@ -29,23 +29,85 @@ function validateChatType(value) {
|
|
|
29
29
|
console.warn(`[ChatWidget] Invalid CHAT_TYPE "${value}". Defaulting to "SUPPORT".`);
|
|
30
30
|
return 'SUPPORT';
|
|
31
31
|
}
|
|
32
|
+
function parseBool(value, defaultValue) {
|
|
33
|
+
if (value === undefined || value === '')
|
|
34
|
+
return defaultValue;
|
|
35
|
+
const v = value.toLowerCase().trim();
|
|
36
|
+
if (v === 'true' || v === '1' || v === 'yes')
|
|
37
|
+
return true;
|
|
38
|
+
if (v === 'false' || v === '0' || v === 'no')
|
|
39
|
+
return false;
|
|
40
|
+
return defaultValue;
|
|
41
|
+
}
|
|
42
|
+
function parsePositiveInt(value, fallback) {
|
|
43
|
+
if (value === undefined || value === '')
|
|
44
|
+
return fallback;
|
|
45
|
+
const n = parseInt(value, 10);
|
|
46
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
47
|
+
}
|
|
48
|
+
function parseOptionalPort(value) {
|
|
49
|
+
if (value === undefined || value.trim() === '')
|
|
50
|
+
return undefined;
|
|
51
|
+
const n = parseInt(value, 10);
|
|
52
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
53
|
+
}
|
|
54
|
+
function parseSizeRatio(value, fallback) {
|
|
55
|
+
if (value === undefined || value === '')
|
|
56
|
+
return fallback;
|
|
57
|
+
const n = parseFloat(value);
|
|
58
|
+
if (!Number.isFinite(n))
|
|
59
|
+
return fallback;
|
|
60
|
+
return Math.min(1, Math.max(0, n));
|
|
61
|
+
}
|
|
32
62
|
export function loadChatConfig() {
|
|
33
|
-
var _a, _b
|
|
63
|
+
var _a, _b;
|
|
34
64
|
const hostUrl = (_a = getEnvVar('CHAT_HOST_URL')) !== null && _a !== void 0 ? _a : 'http://localhost';
|
|
35
|
-
const hostPort =
|
|
36
|
-
const userListEndpoint = (
|
|
65
|
+
const hostPort = parseOptionalPort(getEnvVar('CHAT_HOST_PORT'));
|
|
66
|
+
const userListEndpoint = (_b = getEnvVar('CHAT_USER_LIST')) !== null && _b !== void 0 ? _b : 'api/users';
|
|
37
67
|
const status = validateStatus(getEnvVar('CHAT_STATUS'));
|
|
38
68
|
const chatType = validateChatType(getEnvVar('CHAT_TYPE'));
|
|
69
|
+
const showNeedSupport = parseBool(getEnvVar('CHAT_SHOW_NEED_SUPPORT'), true);
|
|
70
|
+
const showNewConversation = parseBool(getEnvVar('CHAT_SHOW_NEW_CONVERSATION'), true);
|
|
39
71
|
return {
|
|
40
72
|
hostUrl,
|
|
41
73
|
hostPort,
|
|
42
74
|
userListEndpoint,
|
|
43
75
|
status,
|
|
44
76
|
chatType,
|
|
77
|
+
showNeedSupport,
|
|
78
|
+
showNewConversation,
|
|
79
|
+
widgetMinWidth: parsePositiveInt(getEnvVar('CHAT_WIDGET_MIN_WIDTH'), 320),
|
|
80
|
+
widgetMaxWidth: parsePositiveInt(getEnvVar('CHAT_WIDGET_MAX_WIDTH'), 720),
|
|
81
|
+
widgetMinHeight: parsePositiveInt(getEnvVar('CHAT_WIDGET_MIN_HEIGHT'), 420),
|
|
82
|
+
widgetMaxHeight: parsePositiveInt(getEnvVar('CHAT_WIDGET_MAX_HEIGHT'), 720),
|
|
83
|
+
widgetDefaultSize: parseSizeRatio(getEnvVar('CHAT_WIDGET_DEFAULT_SIZE'), 0.45),
|
|
45
84
|
};
|
|
46
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolves the URL used by `fetchUsers`.
|
|
88
|
+
*
|
|
89
|
+
* **Same-origin / internal proxy (recommended):** If `CHAT_USER_LIST` starts with `/`
|
|
90
|
+
* (e.g. `/api/v1/chat/users`), the browser only requests your app origin. The real
|
|
91
|
+
* backend URL stays on the server (Next.js route, BFF, etc.) and does not appear in
|
|
92
|
+
* the client Network tab as a cross-origin call.
|
|
93
|
+
*
|
|
94
|
+
* **Absolute URL:** If `CHAT_USER_LIST` is a full `http(s)://...` string, it is used as-is
|
|
95
|
+
* (visible in Network as that host).
|
|
96
|
+
*
|
|
97
|
+
* **Legacy:** Otherwise the path is joined with `CHAT_HOST_URL` and optional `CHAT_HOST_PORT`.
|
|
98
|
+
*/
|
|
47
99
|
export function buildUserListUrl(config) {
|
|
100
|
+
const raw = config.userListEndpoint.trim();
|
|
101
|
+
if (raw.startsWith('/')) {
|
|
102
|
+
return raw;
|
|
103
|
+
}
|
|
104
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
105
|
+
return raw;
|
|
106
|
+
}
|
|
48
107
|
const base = config.hostUrl.replace(/\/$/, '');
|
|
49
|
-
const endpoint =
|
|
50
|
-
|
|
108
|
+
const endpoint = raw.replace(/^\//, '');
|
|
109
|
+
if (config.hostPort !== undefined) {
|
|
110
|
+
return `${base}:${config.hostPort}/${endpoint}`;
|
|
111
|
+
}
|
|
112
|
+
return `${base}/${endpoint}`;
|
|
51
113
|
}
|
package/dist/hooks/useChat.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { ChatMessage, ChatUser } from '../types';
|
|
1
|
+
import { ChatMessage, ChatUser, RecentChat } from '../types';
|
|
2
2
|
interface UseChatReturn {
|
|
3
3
|
messages: ChatMessage[];
|
|
4
4
|
activeUser: ChatUser | null;
|
|
5
|
+
recentChats: RecentChat[];
|
|
5
6
|
selectUser: (user: ChatUser) => void;
|
|
6
7
|
sendMessage: (text: string) => void;
|
|
7
8
|
clearChat: () => void;
|
|
9
|
+
openRecent: (user: ChatUser) => void;
|
|
8
10
|
}
|
|
9
11
|
export declare function useChat(): UseChatReturn;
|
|
10
12
|
export {};
|
package/dist/hooks/useChat.js
CHANGED
|
@@ -2,10 +2,21 @@ import { useState, useCallback } from 'react';
|
|
|
2
2
|
export function useChat() {
|
|
3
3
|
const [messages, setMessages] = useState([]);
|
|
4
4
|
const [activeUser, setActiveUser] = useState(null);
|
|
5
|
+
const [recentChats, setRecentChats] = useState([]);
|
|
6
|
+
const upsertRecent = useCallback((user, lastMessage) => {
|
|
7
|
+
const updatedAt = new Date();
|
|
8
|
+
setRecentChats((prev) => {
|
|
9
|
+
const rest = prev.filter((r) => r.user.uid !== user.uid);
|
|
10
|
+
return [{ user, lastMessage, updatedAt }, ...rest].slice(0, 50);
|
|
11
|
+
});
|
|
12
|
+
}, []);
|
|
5
13
|
const selectUser = useCallback((user) => {
|
|
6
14
|
setActiveUser(user);
|
|
7
15
|
setMessages([]);
|
|
8
|
-
|
|
16
|
+
}, []);
|
|
17
|
+
const openRecent = useCallback((user) => {
|
|
18
|
+
setActiveUser(user);
|
|
19
|
+
setMessages([]);
|
|
9
20
|
}, []);
|
|
10
21
|
const sendMessage = useCallback((text) => {
|
|
11
22
|
if (!activeUser || !text.trim())
|
|
@@ -19,11 +30,19 @@ export function useChat() {
|
|
|
19
30
|
status: 'sent',
|
|
20
31
|
};
|
|
21
32
|
setMessages((prev) => [...prev, newMsg]);
|
|
22
|
-
|
|
23
|
-
}, [activeUser]);
|
|
33
|
+
upsertRecent(activeUser, text.trim());
|
|
34
|
+
}, [activeUser, upsertRecent]);
|
|
24
35
|
const clearChat = useCallback(() => {
|
|
25
36
|
setMessages([]);
|
|
26
37
|
setActiveUser(null);
|
|
27
38
|
}, []);
|
|
28
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
messages,
|
|
41
|
+
activeUser,
|
|
42
|
+
recentChats,
|
|
43
|
+
selectUser,
|
|
44
|
+
sendMessage,
|
|
45
|
+
clearChat,
|
|
46
|
+
openRecent,
|
|
47
|
+
};
|
|
29
48
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,5 +9,5 @@ export { useUsers } from './hooks/useUsers';
|
|
|
9
9
|
export { useChat } from './hooks/useChat';
|
|
10
10
|
export { loadChatConfig, buildUserListUrl } from './config';
|
|
11
11
|
export { fetchUsers } from './services/userService';
|
|
12
|
-
export type { ChatUser, ChatMessage, ChatConfig, ChatWidgetTheme, ChatWidgetProps, ChatStatus, ChatType, UserType, TabType, } from './types';
|
|
12
|
+
export type { ChatUser, ChatMessage, ChatConfig, ChatWidgetTheme, ChatWidgetProps, ChatStatus, ChatType, UserType, TabType, BottomNavTab, HomeFlow, RaisedTicket, RecentChat, } from './types';
|
|
13
13
|
export { defaultTheme, mergeTheme } from './utils/theme';
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export async function fetchUsers(url) {
|
|
2
|
+
const sameOriginPath = url.startsWith('/');
|
|
2
3
|
const response = await fetch(url, {
|
|
3
4
|
method: 'GET',
|
|
4
5
|
headers: {
|
|
5
6
|
'Content-Type': 'application/json',
|
|
6
7
|
},
|
|
8
|
+
credentials: sameOriginPath ? 'same-origin' : 'omit',
|
|
7
9
|
});
|
|
8
10
|
if (!response.ok) {
|
|
9
11
|
throw new Error(`[ChatWidget] Failed to fetch users: ${response.status} ${response.statusText}`);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export type ChatStatus = 'ACTIVE' | 'DISABLE' | 'MAINTENANCE';
|
|
|
2
2
|
export type ChatType = 'SUPPORT' | 'CHAT' | 'BOTH';
|
|
3
3
|
export type UserType = 'developer' | 'user';
|
|
4
4
|
export type TabType = 'developers' | 'users';
|
|
5
|
+
export type BottomNavTab = 'home' | 'chats' | 'tickets';
|
|
6
|
+
export type HomeFlow = 'home' | 'pickUser' | 'chat' | 'raiseTicket';
|
|
5
7
|
export interface ChatUser {
|
|
6
8
|
name: string;
|
|
7
9
|
uid: string;
|
|
@@ -20,10 +22,34 @@ export interface ChatMessage {
|
|
|
20
22
|
}
|
|
21
23
|
export interface ChatConfig {
|
|
22
24
|
hostUrl: string;
|
|
23
|
-
|
|
25
|
+
/** When omitted, URLs use `hostUrl` only (no `:port` segment). */
|
|
26
|
+
hostPort?: number;
|
|
24
27
|
userListEndpoint: string;
|
|
25
28
|
status: ChatStatus;
|
|
26
29
|
chatType: ChatType;
|
|
30
|
+
/** Show “Need Support” card on home (env `CHAT_SHOW_NEED_SUPPORT`). */
|
|
31
|
+
showNeedSupport: boolean;
|
|
32
|
+
/** Show “New Conversation” card on home (env `CHAT_SHOW_NEW_CONVERSATION`). */
|
|
33
|
+
showNewConversation: boolean;
|
|
34
|
+
/** Pixel bounds for the resize slider (env `CHAT_WIDGET_*`). */
|
|
35
|
+
widgetMinWidth: number;
|
|
36
|
+
widgetMaxWidth: number;
|
|
37
|
+
widgetMinHeight: number;
|
|
38
|
+
widgetMaxHeight: number;
|
|
39
|
+
/** Default widget size as ratio 0–1 between min and max. */
|
|
40
|
+
widgetDefaultSize: number;
|
|
41
|
+
}
|
|
42
|
+
export interface RaisedTicket {
|
|
43
|
+
id: string;
|
|
44
|
+
subject: string;
|
|
45
|
+
body: string;
|
|
46
|
+
createdAt: Date;
|
|
47
|
+
status: 'open' | 'closed';
|
|
48
|
+
}
|
|
49
|
+
export interface RecentChat {
|
|
50
|
+
user: ChatUser;
|
|
51
|
+
lastMessage: string;
|
|
52
|
+
updatedAt: Date;
|
|
27
53
|
}
|
|
28
54
|
export interface ChatWidgetTheme {
|
|
29
55
|
fontFamily?: string;
|
|
@@ -34,6 +60,12 @@ export interface ChatWidgetTheme {
|
|
|
34
60
|
buttonLabel?: string;
|
|
35
61
|
buttonPosition?: 'bottom-right' | 'bottom-left';
|
|
36
62
|
borderRadius?: string;
|
|
63
|
+
/** Override config min width (px). */
|
|
64
|
+
widgetMinWidth?: number;
|
|
65
|
+
widgetMaxWidth?: number;
|
|
66
|
+
widgetMinHeight?: number;
|
|
67
|
+
widgetMaxHeight?: number;
|
|
68
|
+
widgetDefaultSize?: number;
|
|
37
69
|
}
|
|
38
70
|
export interface ChatWidgetProps {
|
|
39
71
|
theme?: ChatWidgetTheme;
|
package/dist/utils/theme.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
export const defaultTheme = {
|
|
2
2
|
fontFamily: "'DM Sans', 'Segoe UI', sans-serif",
|
|
3
|
-
primaryColor: '#
|
|
3
|
+
primaryColor: '#13947e',
|
|
4
4
|
backgroundColor: '#ffffff',
|
|
5
|
-
buttonColor: '#
|
|
5
|
+
buttonColor: '#13947e',
|
|
6
6
|
buttonTextColor: '#ffffff',
|
|
7
7
|
buttonLabel: 'Chat with us',
|
|
8
8
|
buttonPosition: 'bottom-right',
|
|
9
9
|
borderRadius: '16px',
|
|
10
|
+
widgetMinWidth: 320,
|
|
11
|
+
widgetMaxWidth: 720,
|
|
12
|
+
widgetMinHeight: 420,
|
|
13
|
+
widgetMaxHeight: 720,
|
|
14
|
+
widgetDefaultSize: 0.45,
|
|
10
15
|
};
|
|
11
16
|
export function mergeTheme(custom) {
|
|
12
17
|
return Object.assign(Object.assign({}, defaultTheme), custom);
|