ajaxter-chat 1.0.0
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 +290 -0
- package/dist/components/ChatBox/index.d.ts +11 -0
- package/dist/components/ChatBox/index.js +163 -0
- package/dist/components/ChatButton/index.d.ts +9 -0
- package/dist/components/ChatButton/index.js +17 -0
- package/dist/components/ChatWidget.d.ts +28 -0
- package/dist/components/ChatWidget.js +50 -0
- package/dist/components/ChatWindow/index.d.ts +9 -0
- package/dist/components/ChatWindow/index.js +132 -0
- package/dist/components/MaintenanceView/index.d.ts +7 -0
- package/dist/components/MaintenanceView/index.js +53 -0
- package/dist/components/UserList/index.d.ts +13 -0
- package/dist/components/UserList/index.js +136 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.js +51 -0
- package/dist/hooks/useChat.d.ts +10 -0
- package/dist/hooks/useChat.js +29 -0
- package/dist/hooks/useUsers.d.ts +14 -0
- package/dist/hooks/useUsers.js +32 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +18 -0
- package/dist/services/userService.d.ts +7 -0
- package/dist/services/userService.js +16 -0
- package/dist/types/index.d.ts +40 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/theme.d.ts +4 -0
- package/dist/utils/theme.js +23 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# react-chat-widget-extension
|
|
2
|
+
|
|
3
|
+
A reusable, fully configurable floating chat widget for **React.js** and **Next.js** applications.
|
|
4
|
+
|
|
5
|
+
- ✅ Environment-variable-driven behavior (status, type, API endpoint)
|
|
6
|
+
- ✅ Fully themeable via a `theme` prop (colors, fonts, button, position)
|
|
7
|
+
- ✅ SSR-safe — works with Next.js App Router and Pages Router
|
|
8
|
+
- ✅ TypeScript-first
|
|
9
|
+
- ✅ WebSocket-ready architecture
|
|
10
|
+
- ✅ Loading skeletons, empty states, error handling built in
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Folder Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
react-chat-widget-extension/
|
|
18
|
+
├── src/
|
|
19
|
+
│ ├── index.ts # Public API exports
|
|
20
|
+
│ ├── types/
|
|
21
|
+
│ │ └── index.ts # All TypeScript types & interfaces
|
|
22
|
+
│ ├── config/
|
|
23
|
+
│ │ └── index.ts # Env variable loader (Next.js + React safe)
|
|
24
|
+
│ ├── services/
|
|
25
|
+
│ │ └── userService.ts # Fetch users from API
|
|
26
|
+
│ ├── hooks/
|
|
27
|
+
│ │ ├── useUsers.ts # Fetch & filter users hook
|
|
28
|
+
│ │ └── useChat.ts # Chat state & message management hook
|
|
29
|
+
│ ├── utils/
|
|
30
|
+
│ │ └── theme.ts # Theme merging & CSS variable utilities
|
|
31
|
+
│ └── components/
|
|
32
|
+
│ ├── ChatWidget.tsx # 🏠 Root widget — mount this in your app
|
|
33
|
+
│ ├── ChatButton/
|
|
34
|
+
│ │ └── index.tsx # Floating action button
|
|
35
|
+
│ ├── ChatWindow/
|
|
36
|
+
│ │ └── index.tsx # Expandable chat panel
|
|
37
|
+
│ ├── UserList/
|
|
38
|
+
│ │ └── index.tsx # User list with loading/error/empty states
|
|
39
|
+
│ ├── ChatBox/
|
|
40
|
+
│ │ └── index.tsx # Conversation panel + message input
|
|
41
|
+
│ └── MaintenanceView/
|
|
42
|
+
│ └── index.tsx # Shown when CHAT_STATUS=MAINTENANCE
|
|
43
|
+
├── examples/
|
|
44
|
+
│ ├── react-app/
|
|
45
|
+
│ │ ├── .env.example # React env variables template
|
|
46
|
+
│ │ └── App.tsx # React usage example
|
|
47
|
+
│ └── nextjs-app/
|
|
48
|
+
│ ├── .env.local.example # Next.js env variables template
|
|
49
|
+
│ ├── app/
|
|
50
|
+
│ │ ├── layout.tsx # App Router: root layout
|
|
51
|
+
│ │ ├── ChatWidgetWrapper.tsx # App Router: 'use client' boundary
|
|
52
|
+
│ │ └── page.tsx # App Router: home page
|
|
53
|
+
│ └── pages/
|
|
54
|
+
│ ├── _app.tsx # Pages Router: global app
|
|
55
|
+
│ └── index.tsx # Pages Router: index page
|
|
56
|
+
├── package.json
|
|
57
|
+
├── tsconfig.json
|
|
58
|
+
└── README.md
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install react-chat-widget-extension
|
|
67
|
+
# or
|
|
68
|
+
yarn add react-chat-widget-extension
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Environment Variables
|
|
74
|
+
|
|
75
|
+
### React.js (.env)
|
|
76
|
+
|
|
77
|
+
```env
|
|
78
|
+
REACT_APP_CHAT_HOST_URL=http://your-api.com
|
|
79
|
+
REACT_APP_CHAT_HOST_PORT=4000
|
|
80
|
+
REACT_APP_CHAT_USER_LIST=api/v1/chat/users
|
|
81
|
+
REACT_APP_CHAT_STATUS=ACTIVE
|
|
82
|
+
REACT_APP_CHAT_TYPE=BOTH
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Next.js (.env.local)
|
|
86
|
+
|
|
87
|
+
```env
|
|
88
|
+
NEXT_PUBLIC_CHAT_HOST_URL=http://your-api.com
|
|
89
|
+
NEXT_PUBLIC_CHAT_HOST_PORT=4000
|
|
90
|
+
NEXT_PUBLIC_CHAT_USER_LIST=api/v1/chat/users
|
|
91
|
+
NEXT_PUBLIC_CHAT_STATUS=ACTIVE
|
|
92
|
+
NEXT_PUBLIC_CHAT_TYPE=BOTH
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Variable Reference
|
|
96
|
+
|
|
97
|
+
| Variable | Type | Description |
|
|
98
|
+
|--------------------|---------------------------------------|------------------------------------------|
|
|
99
|
+
| `CHAT_HOST_URL` | string | Base URL of your chat/user API |
|
|
100
|
+
| `CHAT_HOST_PORT` | number | Port for your API server |
|
|
101
|
+
| `CHAT_USER_LIST` | string | Endpoint path to fetch users |
|
|
102
|
+
| `CHAT_STATUS` | `ACTIVE` \| `DISABLE` \| `MAINTENANCE` | Controls widget visibility & state |
|
|
103
|
+
| `CHAT_TYPE` | `SUPPORT` \| `CHAT` \| `BOTH` | Controls which users are shown |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## CHAT_STATUS Behavior
|
|
108
|
+
|
|
109
|
+
| Value | Behavior |
|
|
110
|
+
|-----------------|---------------------------------------------------------------|
|
|
111
|
+
| `ACTIVE` | Widget is fully enabled |
|
|
112
|
+
| `DISABLE` | Widget is **not rendered at all** — zero DOM footprint |
|
|
113
|
+
| `MAINTENANCE` | Widget opens but shows a maintenance message (non-interactive)|
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## CHAT_TYPE Behavior
|
|
118
|
+
|
|
119
|
+
| Value | User List Shown | UI |
|
|
120
|
+
|-----------|------------------------------------------|-----------------------------|
|
|
121
|
+
| `SUPPORT` | Only `type: "developer"` users | Single panel |
|
|
122
|
+
| `CHAT` | Only `type: "user"` users | Single panel |
|
|
123
|
+
| `BOTH` | Both developers and users | Two tabs (Support / Users) |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## User List API
|
|
128
|
+
|
|
129
|
+
The widget fetches users from:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
GET ${CHAT_HOST_URL}:${CHAT_HOST_PORT}/${CHAT_USER_LIST}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Expected response:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
[
|
|
139
|
+
{
|
|
140
|
+
"name": "Alice Dev",
|
|
141
|
+
"uid": "uid_001",
|
|
142
|
+
"email": "alice@company.com",
|
|
143
|
+
"mobile": "+1234567890",
|
|
144
|
+
"project": "Platform Team",
|
|
145
|
+
"type": "developer"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"name": "Bob Smith",
|
|
149
|
+
"uid": "uid_002",
|
|
150
|
+
"email": "bob@client.com",
|
|
151
|
+
"mobile": "+0987654321",
|
|
152
|
+
"project": "Client Portal",
|
|
153
|
+
"type": "user"
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Usage
|
|
161
|
+
|
|
162
|
+
### React.js
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
// App.tsx
|
|
166
|
+
import { ChatWidget } from 'react-chat-widget-extension';
|
|
167
|
+
|
|
168
|
+
function App() {
|
|
169
|
+
return (
|
|
170
|
+
<div>
|
|
171
|
+
<main>Your app content</main>
|
|
172
|
+
|
|
173
|
+
<ChatWidget
|
|
174
|
+
theme={{
|
|
175
|
+
primaryColor: '#6C63FF',
|
|
176
|
+
buttonColor: '#6C63FF',
|
|
177
|
+
buttonTextColor: '#ffffff',
|
|
178
|
+
buttonLabel: 'Chat with us',
|
|
179
|
+
buttonPosition: 'bottom-right',
|
|
180
|
+
fontFamily: "'DM Sans', sans-serif",
|
|
181
|
+
borderRadius: '16px',
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Next.js — App Router
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
// app/ChatWidgetWrapper.tsx
|
|
193
|
+
'use client';
|
|
194
|
+
import { ChatWidget } from 'react-chat-widget-extension';
|
|
195
|
+
|
|
196
|
+
export function ChatWidgetWrapper() {
|
|
197
|
+
return <ChatWidget theme={{ primaryColor: '#0ea5e9' }} />;
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
// app/layout.tsx
|
|
203
|
+
import { ChatWidgetWrapper } from './ChatWidgetWrapper';
|
|
204
|
+
|
|
205
|
+
export default function RootLayout({ children }) {
|
|
206
|
+
return (
|
|
207
|
+
<html lang="en">
|
|
208
|
+
<body>
|
|
209
|
+
{children}
|
|
210
|
+
<ChatWidgetWrapper />
|
|
211
|
+
</body>
|
|
212
|
+
</html>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Next.js — Pages Router
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
// pages/_app.tsx
|
|
221
|
+
import { ChatWidget } from 'react-chat-widget-extension';
|
|
222
|
+
|
|
223
|
+
export default function MyApp({ Component, pageProps }) {
|
|
224
|
+
return (
|
|
225
|
+
<>
|
|
226
|
+
<Component {...pageProps} />
|
|
227
|
+
<ChatWidget theme={{ primaryColor: '#10b981' }} />
|
|
228
|
+
</>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Theme Props
|
|
236
|
+
|
|
237
|
+
All theme properties are **optional**. Defaults are used when omitted.
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
interface ChatWidgetTheme {
|
|
241
|
+
fontFamily?: string; // Default: "'DM Sans', 'Segoe UI', sans-serif"
|
|
242
|
+
primaryColor?: string; // Default: '#6C63FF' — header, accents, active states
|
|
243
|
+
backgroundColor?: string; // Default: '#ffffff' — widget panel background
|
|
244
|
+
buttonColor?: string; // Default: '#6C63FF' — floating button background
|
|
245
|
+
buttonTextColor?: string; // Default: '#ffffff' — floating button text/icon color
|
|
246
|
+
buttonLabel?: string; // Default: 'Chat with us'
|
|
247
|
+
buttonPosition?: 'bottom-right' | 'bottom-left'; // Default: 'bottom-right'
|
|
248
|
+
borderRadius?: string; // Default: '16px'
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## WebSocket Integration
|
|
255
|
+
|
|
256
|
+
The `useChat` hook is ready for WebSocket integration. Look for the `TODO` comments in:
|
|
257
|
+
|
|
258
|
+
- `src/hooks/useChat.ts` → Add `socket.emit('message', newMsg)` in `sendMessage`
|
|
259
|
+
- `src/hooks/useChat.ts` → Add `socket.on('message', ...)` listener in `selectUser`
|
|
260
|
+
- `src/components/ChatWindow/index.tsx` → Initialize socket connection on mount
|
|
261
|
+
|
|
262
|
+
Example with socket.io:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
// In useChat.ts — sendMessage
|
|
266
|
+
socket.emit('chat:message', { to: activeUser.uid, text });
|
|
267
|
+
|
|
268
|
+
// In useChat.ts — selectUser
|
|
269
|
+
socket.on('chat:message', (msg: ChatMessage) => {
|
|
270
|
+
setMessages((prev) => [...prev, msg]);
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Build
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
cd react-chat-widget-extension
|
|
280
|
+
npm install
|
|
281
|
+
npm run build # Compile TypeScript → dist/
|
|
282
|
+
npm run type-check # Verify types without emitting
|
|
283
|
+
npm run dev # Watch mode
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## License
|
|
289
|
+
|
|
290
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChatMessage, ChatUser } from '../../types';
|
|
3
|
+
interface ChatBoxProps {
|
|
4
|
+
activeUser: ChatUser | null;
|
|
5
|
+
messages: ChatMessage[];
|
|
6
|
+
onSendMessage: (text: string) => void;
|
|
7
|
+
primaryColor: string;
|
|
8
|
+
fontFamily: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const ChatBox: React.FC<ChatBoxProps>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useEffect } from 'react';
|
|
3
|
+
export const ChatBox = ({ activeUser, messages, onSendMessage, primaryColor, fontFamily, }) => {
|
|
4
|
+
const [inputText, setInputText] = useState('');
|
|
5
|
+
const messagesEndRef = useRef(null);
|
|
6
|
+
const inputRef = useRef(null);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
var _a;
|
|
9
|
+
(_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: 'smooth' });
|
|
10
|
+
}, [messages]);
|
|
11
|
+
const handleSend = () => {
|
|
12
|
+
var _a;
|
|
13
|
+
if (!inputText.trim())
|
|
14
|
+
return;
|
|
15
|
+
onSendMessage(inputText);
|
|
16
|
+
setInputText('');
|
|
17
|
+
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
18
|
+
};
|
|
19
|
+
const handleKeyDown = (e) => {
|
|
20
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
handleSend();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
if (!activeUser) {
|
|
26
|
+
return (_jsxs("div", { style: {
|
|
27
|
+
flex: 1,
|
|
28
|
+
display: 'flex',
|
|
29
|
+
flexDirection: 'column',
|
|
30
|
+
alignItems: 'center',
|
|
31
|
+
justifyContent: 'center',
|
|
32
|
+
fontFamily,
|
|
33
|
+
gap: '16px',
|
|
34
|
+
padding: '24px',
|
|
35
|
+
textAlign: 'center',
|
|
36
|
+
}, children: [_jsx("div", { style: {
|
|
37
|
+
width: '80px',
|
|
38
|
+
height: '80px',
|
|
39
|
+
borderRadius: '50%',
|
|
40
|
+
backgroundColor: `${primaryColor}12`,
|
|
41
|
+
display: 'flex',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
justifyContent: 'center',
|
|
44
|
+
}, children: _jsx("svg", { width: "36", height: "36", viewBox: "0 0 24 24", fill: "none", 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: primaryColor, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", fill: `${primaryColor}15` }) }) }), _jsxs("div", { children: [_jsx("p", { style: {
|
|
45
|
+
margin: '0 0 6px',
|
|
46
|
+
fontWeight: 700,
|
|
47
|
+
fontSize: '16px',
|
|
48
|
+
color: '#1a1a2e',
|
|
49
|
+
letterSpacing: '-0.02em',
|
|
50
|
+
}, children: "Select a conversation" }), _jsx("p", { style: { margin: 0, fontSize: '13px', color: '#aaa', lineHeight: 1.6 }, children: "Choose someone from the list to start chatting" })] })] }));
|
|
51
|
+
}
|
|
52
|
+
const initials = activeUser.name
|
|
53
|
+
.split(' ')
|
|
54
|
+
.map((n) => n[0])
|
|
55
|
+
.join('')
|
|
56
|
+
.toUpperCase()
|
|
57
|
+
.slice(0, 2);
|
|
58
|
+
return (_jsxs("div", { style: {
|
|
59
|
+
flex: 1,
|
|
60
|
+
display: 'flex',
|
|
61
|
+
flexDirection: 'column',
|
|
62
|
+
fontFamily,
|
|
63
|
+
overflow: 'hidden',
|
|
64
|
+
}, children: [_jsxs("div", { style: {
|
|
65
|
+
padding: '14px 20px',
|
|
66
|
+
borderBottom: '1px solid #f0f0f5',
|
|
67
|
+
display: 'flex',
|
|
68
|
+
alignItems: 'center',
|
|
69
|
+
gap: '12px',
|
|
70
|
+
background: '#fafafa',
|
|
71
|
+
}, children: [_jsx("div", { style: {
|
|
72
|
+
width: '38px',
|
|
73
|
+
height: '38px',
|
|
74
|
+
borderRadius: '50%',
|
|
75
|
+
backgroundColor: primaryColor,
|
|
76
|
+
display: 'flex',
|
|
77
|
+
alignItems: 'center',
|
|
78
|
+
justifyContent: 'center',
|
|
79
|
+
color: '#fff',
|
|
80
|
+
fontSize: '13px',
|
|
81
|
+
fontWeight: 700,
|
|
82
|
+
}, children: initials }), _jsxs("div", { children: [_jsx("div", { style: { fontWeight: 700, fontSize: '14px', color: '#1a1a2e' }, children: activeUser.name }), _jsxs("div", { style: { fontSize: '12px', color: '#aaa', display: 'flex', alignItems: 'center', gap: '5px' }, children: [_jsx("span", { style: {
|
|
83
|
+
width: '6px',
|
|
84
|
+
height: '6px',
|
|
85
|
+
borderRadius: '50%',
|
|
86
|
+
backgroundColor: '#4caf50',
|
|
87
|
+
display: 'inline-block',
|
|
88
|
+
} }), "Online"] })] })] }), _jsxs("div", { style: {
|
|
89
|
+
flex: 1,
|
|
90
|
+
overflowY: 'auto',
|
|
91
|
+
padding: '20px 16px',
|
|
92
|
+
display: 'flex',
|
|
93
|
+
flexDirection: 'column',
|
|
94
|
+
gap: '10px',
|
|
95
|
+
backgroundColor: '#f8f8fc',
|
|
96
|
+
}, children: [messages.length === 0 ? (_jsxs("div", { style: {
|
|
97
|
+
textAlign: 'center',
|
|
98
|
+
color: '#ccc',
|
|
99
|
+
fontSize: '13px',
|
|
100
|
+
marginTop: 'auto',
|
|
101
|
+
marginBottom: 'auto',
|
|
102
|
+
}, 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: {
|
|
103
|
+
padding: '12px 16px',
|
|
104
|
+
borderTop: '1px solid #f0f0f5',
|
|
105
|
+
backgroundColor: '#fff',
|
|
106
|
+
display: 'flex',
|
|
107
|
+
alignItems: 'flex-end',
|
|
108
|
+
gap: '10px',
|
|
109
|
+
}, children: [_jsx("textarea", { ref: inputRef, value: inputText, onChange: (e) => setInputText(e.target.value), onKeyDown: handleKeyDown, placeholder: "Type a message... (Enter to send)", rows: 1, style: {
|
|
110
|
+
flex: 1,
|
|
111
|
+
resize: 'none',
|
|
112
|
+
border: '1.5px solid #eee',
|
|
113
|
+
borderRadius: '12px',
|
|
114
|
+
padding: '10px 14px',
|
|
115
|
+
fontFamily,
|
|
116
|
+
fontSize: '14px',
|
|
117
|
+
outline: 'none',
|
|
118
|
+
lineHeight: '1.5',
|
|
119
|
+
maxHeight: '90px',
|
|
120
|
+
overflowY: 'auto',
|
|
121
|
+
transition: 'border-color 0.2s',
|
|
122
|
+
color: '#1a1a2e',
|
|
123
|
+
}, onFocus: (e) => {
|
|
124
|
+
e.target.style.borderColor = primaryColor;
|
|
125
|
+
}, onBlur: (e) => {
|
|
126
|
+
e.target.style.borderColor = '#eee';
|
|
127
|
+
} }), _jsx("button", { onClick: handleSend, disabled: !inputText.trim(), "aria-label": "Send message", style: {
|
|
128
|
+
width: '42px',
|
|
129
|
+
height: '42px',
|
|
130
|
+
borderRadius: '50%',
|
|
131
|
+
backgroundColor: inputText.trim() ? primaryColor : '#eee',
|
|
132
|
+
border: 'none',
|
|
133
|
+
cursor: inputText.trim() ? 'pointer' : 'not-allowed',
|
|
134
|
+
display: 'flex',
|
|
135
|
+
alignItems: 'center',
|
|
136
|
+
justifyContent: 'center',
|
|
137
|
+
flexShrink: 0,
|
|
138
|
+
transition: 'all 0.2s ease',
|
|
139
|
+
}, 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
|
+
};
|
|
141
|
+
const MessageBubble = ({ message, primaryColor }) => {
|
|
142
|
+
const isMe = message.senderId === 'me';
|
|
143
|
+
const time = new Date(message.timestamp).toLocaleTimeString([], {
|
|
144
|
+
hour: '2-digit',
|
|
145
|
+
minute: '2-digit',
|
|
146
|
+
});
|
|
147
|
+
return (_jsxs("div", { style: {
|
|
148
|
+
display: 'flex',
|
|
149
|
+
flexDirection: 'column',
|
|
150
|
+
alignItems: isMe ? 'flex-end' : 'flex-start',
|
|
151
|
+
gap: '3px',
|
|
152
|
+
}, children: [_jsx("div", { style: {
|
|
153
|
+
maxWidth: '78%',
|
|
154
|
+
padding: '10px 14px',
|
|
155
|
+
borderRadius: isMe ? '18px 18px 4px 18px' : '18px 18px 18px 4px',
|
|
156
|
+
backgroundColor: isMe ? primaryColor : '#fff',
|
|
157
|
+
color: isMe ? '#fff' : '#1a1a2e',
|
|
158
|
+
fontSize: '14px',
|
|
159
|
+
lineHeight: '1.5',
|
|
160
|
+
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
|
|
161
|
+
wordBreak: 'break-word',
|
|
162
|
+
}, children: message.text }), _jsx("span", { style: { fontSize: '11px', color: '#bbb', paddingLeft: '4px', paddingRight: '4px' }, children: time })] }));
|
|
163
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { mergeTheme } from '../../utils/theme';
|
|
3
|
+
export const ChatButton = ({ isOpen, onClick, theme, }) => {
|
|
4
|
+
const t = mergeTheme(theme);
|
|
5
|
+
const positionStyle = t.buttonPosition === 'bottom-left'
|
|
6
|
+
? { left: '24px', right: 'auto' }
|
|
7
|
+
: { right: '24px', left: 'auto' };
|
|
8
|
+
return (_jsx("button", { onClick: onClick, "aria-label": isOpen ? 'Close chat' : t.buttonLabel, style: Object.assign(Object.assign({ position: 'fixed', bottom: '24px' }, positionStyle), { zIndex: 9999, display: 'flex', alignItems: 'center', gap: '10px', padding: isOpen ? '14px' : '14px 22px', backgroundColor: t.buttonColor, color: t.buttonTextColor, border: 'none', borderRadius: '50px', cursor: 'pointer', fontFamily: t.fontFamily, fontSize: '15px', fontWeight: 600, boxShadow: `0 8px 32px ${t.buttonColor}55`, transition: 'all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)', transform: isOpen ? 'scale(0.95)' : 'scale(1)', letterSpacing: '0.01em', minWidth: isOpen ? '52px' : 'auto', justifyContent: 'center' }), onMouseEnter: (e) => {
|
|
9
|
+
e.currentTarget.style.transform = 'scale(1.06) translateY(-2px)';
|
|
10
|
+
e.currentTarget.style.boxShadow = `0 12px 40px ${t.buttonColor}77`;
|
|
11
|
+
}, onMouseLeave: (e) => {
|
|
12
|
+
e.currentTarget.style.transform = isOpen ? 'scale(0.95)' : 'scale(1)';
|
|
13
|
+
e.currentTarget.style.boxShadow = `0 8px 32px ${t.buttonColor}55`;
|
|
14
|
+
}, children: isOpen ? (_jsx(CloseIcon, { color: t.buttonTextColor })) : (_jsxs(_Fragment, { children: [_jsx(ChatIcon, { color: t.buttonTextColor }), _jsx("span", { children: t.buttonLabel })] })) }));
|
|
15
|
+
};
|
|
16
|
+
const ChatIcon = ({ color }) => (_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", 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: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }) }));
|
|
17
|
+
const CloseIcon = ({ color }) => (_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: color, strokeWidth: "2.5", strokeLinecap: "round" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18", stroke: color, strokeWidth: "2.5", strokeLinecap: "round" })] }));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChatWidgetProps } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* ChatWidget
|
|
5
|
+
*
|
|
6
|
+
* Drop-in chat widget for React.js and Next.js apps.
|
|
7
|
+
* All behavior is configured via environment variables.
|
|
8
|
+
* All UI styling is configured via the `theme` prop.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Basic usage
|
|
12
|
+
* <ChatWidget />
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // With custom theme
|
|
16
|
+
* <ChatWidget
|
|
17
|
+
* theme={{
|
|
18
|
+
* primaryColor: '#FF6B6B',
|
|
19
|
+
* buttonColor: '#FF6B6B',
|
|
20
|
+
* buttonLabel: 'Need Help?',
|
|
21
|
+
* buttonPosition: 'bottom-left',
|
|
22
|
+
* fontFamily: "'Inter', sans-serif",
|
|
23
|
+
* borderRadius: '12px',
|
|
24
|
+
* }}
|
|
25
|
+
* />
|
|
26
|
+
*/
|
|
27
|
+
export declare const ChatWidget: React.FC<ChatWidgetProps>;
|
|
28
|
+
export default ChatWidget;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client'; // Next.js App Router directive (ignored in React/Pages Router)
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { loadChatConfig } from '../config';
|
|
5
|
+
import { ChatButton } from './ChatButton';
|
|
6
|
+
import { ChatWindow } from './ChatWindow';
|
|
7
|
+
import { mergeTheme } from '../utils/theme';
|
|
8
|
+
/**
|
|
9
|
+
* ChatWidget
|
|
10
|
+
*
|
|
11
|
+
* Drop-in chat widget for React.js and Next.js apps.
|
|
12
|
+
* All behavior is configured via environment variables.
|
|
13
|
+
* All UI styling is configured via the `theme` prop.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // Basic usage
|
|
17
|
+
* <ChatWidget />
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // With custom theme
|
|
21
|
+
* <ChatWidget
|
|
22
|
+
* theme={{
|
|
23
|
+
* primaryColor: '#FF6B6B',
|
|
24
|
+
* buttonColor: '#FF6B6B',
|
|
25
|
+
* buttonLabel: 'Need Help?',
|
|
26
|
+
* buttonPosition: 'bottom-left',
|
|
27
|
+
* fontFamily: "'Inter', sans-serif",
|
|
28
|
+
* borderRadius: '12px',
|
|
29
|
+
* }}
|
|
30
|
+
* />
|
|
31
|
+
*/
|
|
32
|
+
export const ChatWidget = ({ theme }) => {
|
|
33
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
34
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
35
|
+
// SSR Safety: Only render on the client
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
setIsMounted(true);
|
|
38
|
+
}, []);
|
|
39
|
+
// Load config from env (safe on both client and server)
|
|
40
|
+
const config = loadChatConfig();
|
|
41
|
+
const t = mergeTheme(theme);
|
|
42
|
+
// Don't render anything server-side
|
|
43
|
+
if (!isMounted)
|
|
44
|
+
return null;
|
|
45
|
+
// DISABLE: render nothing at all
|
|
46
|
+
if (config.status === 'DISABLE')
|
|
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 })] }));
|
|
49
|
+
};
|
|
50
|
+
export default ChatWidget;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChatConfig, ChatWidgetTheme } from '../../types';
|
|
3
|
+
interface ChatWindowProps {
|
|
4
|
+
config: ChatConfig;
|
|
5
|
+
theme?: ChatWidgetTheme;
|
|
6
|
+
buttonPosition: 'bottom-right' | 'bottom-left';
|
|
7
|
+
}
|
|
8
|
+
export declare const ChatWindow: React.FC<ChatWindowProps>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { UserList } from '../UserList';
|
|
4
|
+
import { ChatBox } from '../ChatBox';
|
|
5
|
+
import { MaintenanceView } from '../MaintenanceView';
|
|
6
|
+
import { useUsers } from '../../hooks/useUsers';
|
|
7
|
+
import { useChat } from '../../hooks/useChat';
|
|
8
|
+
import { buildUserListUrl } from '../../config';
|
|
9
|
+
import { mergeTheme } from '../../utils/theme';
|
|
10
|
+
export const ChatWindow = ({ config, theme, buttonPosition, }) => {
|
|
11
|
+
var _a;
|
|
12
|
+
const t = mergeTheme(theme);
|
|
13
|
+
const userListUrl = buildUserListUrl(config);
|
|
14
|
+
const [activeTab, setActiveTab] = useState('developers');
|
|
15
|
+
const filterType = config.chatType === 'SUPPORT'
|
|
16
|
+
? 'developer'
|
|
17
|
+
: config.chatType === 'CHAT'
|
|
18
|
+
? 'user'
|
|
19
|
+
: activeTab === 'developers'
|
|
20
|
+
? 'developer'
|
|
21
|
+
: 'user';
|
|
22
|
+
const { users, loading, error } = useUsers({
|
|
23
|
+
url: userListUrl,
|
|
24
|
+
filterType,
|
|
25
|
+
enabled: config.status === 'ACTIVE',
|
|
26
|
+
});
|
|
27
|
+
const { messages, activeUser, selectUser, sendMessage } = useChat();
|
|
28
|
+
const positionStyle = buttonPosition === 'bottom-left'
|
|
29
|
+
? { left: '24px', right: 'auto' }
|
|
30
|
+
: { right: '24px', left: 'auto' };
|
|
31
|
+
return (_jsxs("div", { style: Object.assign(Object.assign({ position: 'fixed', bottom: '90px' }, positionStyle), { zIndex: 9998, width: '680px', maxWidth: 'calc(100vw - 32px)', height: '520px', 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
|
+
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap');
|
|
33
|
+
@keyframes cw-slideUp {
|
|
34
|
+
from { opacity: 0; transform: translateY(20px) scale(0.97); }
|
|
35
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
36
|
+
}
|
|
37
|
+
@keyframes shimmer {
|
|
38
|
+
0% { background-position: 200% 0; }
|
|
39
|
+
100% { background-position: -200% 0; }
|
|
40
|
+
}
|
|
41
|
+
::-webkit-scrollbar { width: 4px; }
|
|
42
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
43
|
+
::-webkit-scrollbar-thumb { background: #e0e0e0; border-radius: 4px; }
|
|
44
|
+
` }), _jsxs("div", { style: {
|
|
45
|
+
padding: '18px 20px',
|
|
46
|
+
background: `linear-gradient(135deg, ${t.primaryColor}, ${adjustColor(t.primaryColor, -20)})`,
|
|
47
|
+
color: '#fff',
|
|
48
|
+
display: 'flex',
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
justifyContent: 'space-between',
|
|
51
|
+
flexShrink: 0,
|
|
52
|
+
}, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '12px' }, children: [_jsx("div", { style: {
|
|
53
|
+
width: '38px',
|
|
54
|
+
height: '38px',
|
|
55
|
+
borderRadius: '50%',
|
|
56
|
+
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
57
|
+
display: 'flex',
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
justifyContent: 'center',
|
|
60
|
+
}, children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", 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: "#fff", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsxs("div", { children: [_jsx("div", { style: { fontWeight: 700, fontSize: '16px', letterSpacing: '-0.02em' }, children: config.chatType === 'SUPPORT'
|
|
61
|
+
? 'Support Chat'
|
|
62
|
+
: config.chatType === 'CHAT'
|
|
63
|
+
? 'Team Chat'
|
|
64
|
+
: 'Chat Center' }), _jsx("div", { style: { fontSize: '12px', opacity: 0.8 }, children: config.status === 'ACTIVE' ? 'We\'re online' : 'Unavailable' })] })] }), _jsx(StatusBadge, { status: config.status })] }), config.chatType === 'BOTH' && config.status === 'ACTIVE' && (_jsx("div", { style: {
|
|
65
|
+
display: 'flex',
|
|
66
|
+
borderBottom: '1px solid #f0f0f5',
|
|
67
|
+
backgroundColor: '#fafafa',
|
|
68
|
+
flexShrink: 0,
|
|
69
|
+
}, children: ['developers', 'users'].map((tab) => (_jsx("button", { onClick: () => setActiveTab(tab), style: {
|
|
70
|
+
flex: 1,
|
|
71
|
+
padding: '11px',
|
|
72
|
+
border: 'none',
|
|
73
|
+
background: 'transparent',
|
|
74
|
+
cursor: 'pointer',
|
|
75
|
+
fontSize: '13px',
|
|
76
|
+
fontWeight: 600,
|
|
77
|
+
fontFamily: t.fontFamily,
|
|
78
|
+
color: activeTab === tab ? t.primaryColor : '#999',
|
|
79
|
+
borderBottom: activeTab === tab ? `2px solid ${t.primaryColor}` : '2px solid transparent',
|
|
80
|
+
transition: 'all 0.2s',
|
|
81
|
+
textTransform: 'capitalize',
|
|
82
|
+
}, children: tab === 'developers' ? '🛠 Support (Devs)' : '👥 Users' }, tab))) })), config.status === 'MAINTENANCE' ? (_jsx(MaintenanceView, { fontFamily: t.fontFamily, primaryColor: t.primaryColor })) : (_jsxs("div", { style: { display: 'flex', flex: 1, overflow: 'hidden' }, children: [_jsxs("div", { style: {
|
|
83
|
+
width: '240px',
|
|
84
|
+
borderRight: '1px solid #f0f0f5',
|
|
85
|
+
display: 'flex',
|
|
86
|
+
flexDirection: 'column',
|
|
87
|
+
flexShrink: 0,
|
|
88
|
+
overflow: 'hidden',
|
|
89
|
+
}, children: [_jsxs("div", { style: {
|
|
90
|
+
padding: '12px 16px',
|
|
91
|
+
borderBottom: '1px solid #f5f5f5',
|
|
92
|
+
fontSize: '11px',
|
|
93
|
+
fontWeight: 700,
|
|
94
|
+
color: '#bbb',
|
|
95
|
+
textTransform: 'uppercase',
|
|
96
|
+
letterSpacing: '0.08em',
|
|
97
|
+
}, children: [filterType === 'developer' ? 'Developers' : 'Users', ' ', !loading && !error && `(${users.length})`] }), _jsx(UserList, { users: users, loading: loading, error: error, activeUserId: (_a = activeUser === null || activeUser === void 0 ? void 0 : activeUser.uid) !== null && _a !== void 0 ? _a : null, onSelectUser: selectUser, primaryColor: t.primaryColor, fontFamily: t.fontFamily })] }), _jsx(ChatBox, { activeUser: activeUser, messages: messages, onSendMessage: sendMessage, primaryColor: t.primaryColor, fontFamily: t.fontFamily })] }))] }));
|
|
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] }));
|
|
124
|
+
};
|
|
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,53 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export const MaintenanceView = ({ fontFamily, primaryColor, }) => {
|
|
3
|
+
return (_jsxs("div", { style: {
|
|
4
|
+
display: 'flex',
|
|
5
|
+
flexDirection: 'column',
|
|
6
|
+
alignItems: 'center',
|
|
7
|
+
justifyContent: 'center',
|
|
8
|
+
height: '100%',
|
|
9
|
+
padding: '32px',
|
|
10
|
+
fontFamily,
|
|
11
|
+
textAlign: 'center',
|
|
12
|
+
gap: '16px',
|
|
13
|
+
}, children: [_jsx("div", { style: {
|
|
14
|
+
width: '72px',
|
|
15
|
+
height: '72px',
|
|
16
|
+
borderRadius: '50%',
|
|
17
|
+
backgroundColor: `${primaryColor}15`,
|
|
18
|
+
display: 'flex',
|
|
19
|
+
alignItems: 'center',
|
|
20
|
+
justifyContent: 'center',
|
|
21
|
+
marginBottom: '8px',
|
|
22
|
+
}, children: _jsx("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M12 9v4M12 17h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z", stroke: primaryColor, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsx("h3", { style: {
|
|
23
|
+
margin: 0,
|
|
24
|
+
fontSize: '17px',
|
|
25
|
+
fontWeight: 700,
|
|
26
|
+
color: '#1a1a2e',
|
|
27
|
+
letterSpacing: '-0.02em',
|
|
28
|
+
}, children: "Under Maintenance" }), _jsx("p", { style: {
|
|
29
|
+
margin: 0,
|
|
30
|
+
fontSize: '14px',
|
|
31
|
+
color: '#888',
|
|
32
|
+
lineHeight: 1.6,
|
|
33
|
+
maxWidth: '220px',
|
|
34
|
+
}, children: "Chat is under maintenance. We'll be back shortly. Thank you for your patience!" }), _jsxs("span", { style: {
|
|
35
|
+
display: 'inline-flex',
|
|
36
|
+
alignItems: 'center',
|
|
37
|
+
gap: '6px',
|
|
38
|
+
padding: '6px 14px',
|
|
39
|
+
borderRadius: '20px',
|
|
40
|
+
backgroundColor: '#fff3cd',
|
|
41
|
+
color: '#856404',
|
|
42
|
+
fontSize: '12px',
|
|
43
|
+
fontWeight: 600,
|
|
44
|
+
marginTop: '8px',
|
|
45
|
+
border: '1px solid #ffc10730',
|
|
46
|
+
}, children: [_jsx("span", { style: {
|
|
47
|
+
width: '6px',
|
|
48
|
+
height: '6px',
|
|
49
|
+
borderRadius: '50%',
|
|
50
|
+
backgroundColor: '#ffc107',
|
|
51
|
+
display: 'inline-block',
|
|
52
|
+
} }), "Temporarily Unavailable"] })] }));
|
|
53
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChatUser } from '../../types';
|
|
3
|
+
interface UserListProps {
|
|
4
|
+
users: ChatUser[];
|
|
5
|
+
loading: boolean;
|
|
6
|
+
error: string | null;
|
|
7
|
+
activeUserId: string | null;
|
|
8
|
+
onSelectUser: (user: ChatUser) => void;
|
|
9
|
+
primaryColor: string;
|
|
10
|
+
fontFamily: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const UserList: React.FC<UserListProps>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export const UserList = ({ users, loading, error, activeUserId, onSelectUser, primaryColor, fontFamily, }) => {
|
|
3
|
+
if (loading) {
|
|
4
|
+
return (_jsx("div", { style: { padding: '16px', fontFamily }, children: [1, 2, 3, 4].map((i) => (_jsx(SkeletonUser, {}, i))) }));
|
|
5
|
+
}
|
|
6
|
+
if (error) {
|
|
7
|
+
return (_jsxs("div", { style: {
|
|
8
|
+
padding: '32px 20px',
|
|
9
|
+
textAlign: 'center',
|
|
10
|
+
fontFamily,
|
|
11
|
+
display: 'flex',
|
|
12
|
+
flexDirection: 'column',
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
gap: '10px',
|
|
15
|
+
}, children: [_jsxs("svg", { width: "36", height: "36", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "12", r: "10", stroke: "#ff6b6b", strokeWidth: "2" }), _jsx("line", { x1: "12", y1: "8", x2: "12", y2: "12", stroke: "#ff6b6b", strokeWidth: "2", strokeLinecap: "round" }), _jsx("line", { x1: "12", y1: "16", x2: "12.01", y2: "16", stroke: "#ff6b6b", strokeWidth: "2", strokeLinecap: "round" })] }), _jsx("p", { style: { margin: 0, fontSize: '13px', color: '#ff6b6b', fontWeight: 600 }, children: "Failed to load users" }), _jsx("p", { style: { margin: 0, fontSize: '12px', color: '#999', lineHeight: 1.5 }, children: error })] }));
|
|
16
|
+
}
|
|
17
|
+
if (users.length === 0) {
|
|
18
|
+
return (_jsxs("div", { style: {
|
|
19
|
+
padding: '40px 20px',
|
|
20
|
+
textAlign: 'center',
|
|
21
|
+
fontFamily,
|
|
22
|
+
display: 'flex',
|
|
23
|
+
flexDirection: 'column',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
gap: '10px',
|
|
26
|
+
}, children: [_jsxs("svg", { width: "40", height: "40", viewBox: "0 0 24 24", fill: "none", children: [_jsx("path", { d: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2", stroke: "#ccc", strokeWidth: "2", strokeLinecap: "round" }), _jsx("circle", { cx: "9", cy: "7", r: "4", stroke: "#ccc", strokeWidth: "2" }), _jsx("path", { d: "M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75", stroke: "#ccc", strokeWidth: "2", strokeLinecap: "round" })] }), _jsx("p", { style: { margin: 0, fontSize: '13px', color: '#bbb', fontWeight: 600 }, children: "No users available" }), _jsx("p", { style: { margin: 0, fontSize: '12px', color: '#ccc' }, children: "Check back later" })] }));
|
|
27
|
+
}
|
|
28
|
+
return (_jsx("div", { style: { overflowY: 'auto', flex: 1, fontFamily }, children: users.map((user) => {
|
|
29
|
+
const isActive = user.uid === activeUserId;
|
|
30
|
+
const initials = user.name
|
|
31
|
+
.split(' ')
|
|
32
|
+
.map((n) => n[0])
|
|
33
|
+
.join('')
|
|
34
|
+
.toUpperCase()
|
|
35
|
+
.slice(0, 2);
|
|
36
|
+
return (_jsxs("button", { onClick: () => onSelectUser(user), style: {
|
|
37
|
+
width: '100%',
|
|
38
|
+
padding: '12px 16px',
|
|
39
|
+
display: 'flex',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
gap: '12px',
|
|
42
|
+
background: isActive ? `${primaryColor}12` : 'transparent',
|
|
43
|
+
border: 'none',
|
|
44
|
+
borderLeft: isActive ? `3px solid ${primaryColor}` : '3px solid transparent',
|
|
45
|
+
cursor: 'pointer',
|
|
46
|
+
textAlign: 'left',
|
|
47
|
+
transition: 'all 0.18s ease',
|
|
48
|
+
borderRadius: '0',
|
|
49
|
+
}, onMouseEnter: (e) => {
|
|
50
|
+
if (!isActive) {
|
|
51
|
+
e.currentTarget.style.backgroundColor = '#f8f8ff';
|
|
52
|
+
}
|
|
53
|
+
}, onMouseLeave: (e) => {
|
|
54
|
+
if (!isActive) {
|
|
55
|
+
e.currentTarget.style.backgroundColor = 'transparent';
|
|
56
|
+
}
|
|
57
|
+
}, children: [_jsx("div", { style: {
|
|
58
|
+
width: '40px',
|
|
59
|
+
height: '40px',
|
|
60
|
+
borderRadius: '50%',
|
|
61
|
+
backgroundColor: isActive ? primaryColor : stringToColor(user.name),
|
|
62
|
+
display: 'flex',
|
|
63
|
+
alignItems: 'center',
|
|
64
|
+
justifyContent: 'center',
|
|
65
|
+
color: '#fff',
|
|
66
|
+
fontSize: '13px',
|
|
67
|
+
fontWeight: 700,
|
|
68
|
+
flexShrink: 0,
|
|
69
|
+
transition: 'background 0.2s',
|
|
70
|
+
}, children: initials }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: {
|
|
71
|
+
fontSize: '14px',
|
|
72
|
+
fontWeight: 600,
|
|
73
|
+
color: isActive ? primaryColor : '#1a1a2e',
|
|
74
|
+
whiteSpace: 'nowrap',
|
|
75
|
+
overflow: 'hidden',
|
|
76
|
+
textOverflow: 'ellipsis',
|
|
77
|
+
}, children: user.name }), _jsx("div", { style: {
|
|
78
|
+
fontSize: '12px',
|
|
79
|
+
color: '#999',
|
|
80
|
+
whiteSpace: 'nowrap',
|
|
81
|
+
overflow: 'hidden',
|
|
82
|
+
textOverflow: 'ellipsis',
|
|
83
|
+
}, children: user.project || user.email })] }), _jsx("span", { style: {
|
|
84
|
+
fontSize: '10px',
|
|
85
|
+
fontWeight: 700,
|
|
86
|
+
padding: '3px 8px',
|
|
87
|
+
borderRadius: '20px',
|
|
88
|
+
textTransform: 'uppercase',
|
|
89
|
+
letterSpacing: '0.05em',
|
|
90
|
+
flexShrink: 0,
|
|
91
|
+
backgroundColor: user.type === 'developer' ? '#e8f5e9' : '#e3f2fd',
|
|
92
|
+
color: user.type === 'developer' ? '#2e7d32' : '#1565c0',
|
|
93
|
+
}, children: user.type === 'developer' ? 'Dev' : 'User' })] }, user.uid));
|
|
94
|
+
}) }));
|
|
95
|
+
};
|
|
96
|
+
const SkeletonUser = () => (_jsxs("div", { style: {
|
|
97
|
+
display: 'flex',
|
|
98
|
+
alignItems: 'center',
|
|
99
|
+
gap: '12px',
|
|
100
|
+
padding: '12px 16px',
|
|
101
|
+
}, children: [_jsx("div", { style: {
|
|
102
|
+
width: '40px',
|
|
103
|
+
height: '40px',
|
|
104
|
+
borderRadius: '50%',
|
|
105
|
+
background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
|
|
106
|
+
backgroundSize: '200% 100%',
|
|
107
|
+
animation: 'shimmer 1.5s infinite',
|
|
108
|
+
flexShrink: 0,
|
|
109
|
+
} }), _jsxs("div", { style: { flex: 1 }, children: [_jsx("div", { style: {
|
|
110
|
+
height: '12px',
|
|
111
|
+
width: '60%',
|
|
112
|
+
borderRadius: '6px',
|
|
113
|
+
background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
|
|
114
|
+
backgroundSize: '200% 100%',
|
|
115
|
+
animation: 'shimmer 1.5s infinite',
|
|
116
|
+
marginBottom: '8px',
|
|
117
|
+
} }), _jsx("div", { style: {
|
|
118
|
+
height: '10px',
|
|
119
|
+
width: '40%',
|
|
120
|
+
borderRadius: '6px',
|
|
121
|
+
background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
|
|
122
|
+
backgroundSize: '200% 100%',
|
|
123
|
+
animation: 'shimmer 1.5s infinite',
|
|
124
|
+
} })] })] }));
|
|
125
|
+
// Generate consistent color from name string
|
|
126
|
+
function stringToColor(str) {
|
|
127
|
+
let hash = 0;
|
|
128
|
+
for (let i = 0; i < str.length; i++) {
|
|
129
|
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
130
|
+
}
|
|
131
|
+
const colors = [
|
|
132
|
+
'#7C3AED', '#2563EB', '#059669', '#D97706',
|
|
133
|
+
'#DC2626', '#7C2D12', '#0E7490', '#4F46E5',
|
|
134
|
+
];
|
|
135
|
+
return colors[Math.abs(hash) % colors.length];
|
|
136
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safely reads an environment variable.
|
|
3
|
+
* Supports both Next.js (NEXT_PUBLIC_) and React (REACT_APP_) prefixes.
|
|
4
|
+
* Falls back gracefully if running in SSR or non-browser environments.
|
|
5
|
+
*/
|
|
6
|
+
function getEnvVar(key) {
|
|
7
|
+
var _a, _b, _c;
|
|
8
|
+
const nextKey = `NEXT_PUBLIC_${key}`;
|
|
9
|
+
const reactKey = `REACT_APP_${key}`;
|
|
10
|
+
// Next.js / Node.js environment
|
|
11
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
12
|
+
return ((_c = (_b = (_a = process.env[nextKey]) !== null && _a !== void 0 ? _a : process.env[reactKey]) !== null && _b !== void 0 ? _b : process.env[key]) !== null && _c !== void 0 ? _c : undefined);
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
function validateStatus(value) {
|
|
17
|
+
const valid = ['ACTIVE', 'DISABLE', 'MAINTENANCE'];
|
|
18
|
+
if (value && valid.includes(value)) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
console.warn(`[ChatWidget] Invalid CHAT_STATUS "${value}". Defaulting to "DISABLE".`);
|
|
22
|
+
return 'DISABLE';
|
|
23
|
+
}
|
|
24
|
+
function validateChatType(value) {
|
|
25
|
+
const valid = ['SUPPORT', 'CHAT', 'BOTH'];
|
|
26
|
+
if (value && valid.includes(value)) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
console.warn(`[ChatWidget] Invalid CHAT_TYPE "${value}". Defaulting to "SUPPORT".`);
|
|
30
|
+
return 'SUPPORT';
|
|
31
|
+
}
|
|
32
|
+
export function loadChatConfig() {
|
|
33
|
+
var _a, _b, _c;
|
|
34
|
+
const hostUrl = (_a = getEnvVar('CHAT_HOST_URL')) !== null && _a !== void 0 ? _a : 'http://localhost';
|
|
35
|
+
const hostPort = parseInt((_b = getEnvVar('CHAT_HOST_PORT')) !== null && _b !== void 0 ? _b : '3001', 10);
|
|
36
|
+
const userListEndpoint = (_c = getEnvVar('CHAT_USER_LIST')) !== null && _c !== void 0 ? _c : 'api/users';
|
|
37
|
+
const status = validateStatus(getEnvVar('CHAT_STATUS'));
|
|
38
|
+
const chatType = validateChatType(getEnvVar('CHAT_TYPE'));
|
|
39
|
+
return {
|
|
40
|
+
hostUrl,
|
|
41
|
+
hostPort,
|
|
42
|
+
userListEndpoint,
|
|
43
|
+
status,
|
|
44
|
+
chatType,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function buildUserListUrl(config) {
|
|
48
|
+
const base = config.hostUrl.replace(/\/$/, '');
|
|
49
|
+
const endpoint = config.userListEndpoint.replace(/^\//, '');
|
|
50
|
+
return `${base}:${config.hostPort}/${endpoint}`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ChatMessage, ChatUser } from '../types';
|
|
2
|
+
interface UseChatReturn {
|
|
3
|
+
messages: ChatMessage[];
|
|
4
|
+
activeUser: ChatUser | null;
|
|
5
|
+
selectUser: (user: ChatUser) => void;
|
|
6
|
+
sendMessage: (text: string) => void;
|
|
7
|
+
clearChat: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function useChat(): UseChatReturn;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
export function useChat() {
|
|
3
|
+
const [messages, setMessages] = useState([]);
|
|
4
|
+
const [activeUser, setActiveUser] = useState(null);
|
|
5
|
+
const selectUser = useCallback((user) => {
|
|
6
|
+
setActiveUser(user);
|
|
7
|
+
setMessages([]);
|
|
8
|
+
// TODO: Connect WebSocket here — load message history for this user
|
|
9
|
+
}, []);
|
|
10
|
+
const sendMessage = useCallback((text) => {
|
|
11
|
+
if (!activeUser || !text.trim())
|
|
12
|
+
return;
|
|
13
|
+
const newMsg = {
|
|
14
|
+
id: `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
15
|
+
senderId: 'me',
|
|
16
|
+
receiverId: activeUser.uid,
|
|
17
|
+
text: text.trim(),
|
|
18
|
+
timestamp: new Date(),
|
|
19
|
+
status: 'sent',
|
|
20
|
+
};
|
|
21
|
+
setMessages((prev) => [...prev, newMsg]);
|
|
22
|
+
// TODO: Emit via WebSocket — socket.emit('message', newMsg)
|
|
23
|
+
}, [activeUser]);
|
|
24
|
+
const clearChat = useCallback(() => {
|
|
25
|
+
setMessages([]);
|
|
26
|
+
setActiveUser(null);
|
|
27
|
+
}, []);
|
|
28
|
+
return { messages, activeUser, selectUser, sendMessage, clearChat };
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ChatUser, UserType } from '../types';
|
|
2
|
+
interface UseUsersOptions {
|
|
3
|
+
url: string;
|
|
4
|
+
filterType?: UserType;
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface UseUsersReturn {
|
|
8
|
+
users: ChatUser[];
|
|
9
|
+
loading: boolean;
|
|
10
|
+
error: string | null;
|
|
11
|
+
refetch: () => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function useUsers({ url, filterType, enabled, }: UseUsersOptions): UseUsersReturn;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { fetchUsers } from '../services/userService';
|
|
3
|
+
export function useUsers({ url, filterType, enabled = true, }) {
|
|
4
|
+
const [users, setUsers] = useState([]);
|
|
5
|
+
const [loading, setLoading] = useState(false);
|
|
6
|
+
const [error, setError] = useState(null);
|
|
7
|
+
const load = useCallback(async () => {
|
|
8
|
+
if (!enabled)
|
|
9
|
+
return;
|
|
10
|
+
setLoading(true);
|
|
11
|
+
setError(null);
|
|
12
|
+
try {
|
|
13
|
+
const data = await fetchUsers(url);
|
|
14
|
+
const filtered = filterType
|
|
15
|
+
? data.filter((u) => u.type === filterType)
|
|
16
|
+
: data;
|
|
17
|
+
setUsers(filtered);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
const message = err instanceof Error ? err.message : 'Unknown error occurred.';
|
|
21
|
+
setError(message);
|
|
22
|
+
setUsers([]);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
setLoading(false);
|
|
26
|
+
}
|
|
27
|
+
}, [url, filterType, enabled]);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
load();
|
|
30
|
+
}, [load]);
|
|
31
|
+
return { users, loading, error, refetch: load };
|
|
32
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { ChatWidget } from './components/ChatWidget';
|
|
2
|
+
export { default } from './components/ChatWidget';
|
|
3
|
+
export { ChatButton } from './components/ChatButton';
|
|
4
|
+
export { ChatWindow } from './components/ChatWindow';
|
|
5
|
+
export { UserList } from './components/UserList';
|
|
6
|
+
export { ChatBox } from './components/ChatBox';
|
|
7
|
+
export { MaintenanceView } from './components/MaintenanceView';
|
|
8
|
+
export { useUsers } from './hooks/useUsers';
|
|
9
|
+
export { useChat } from './hooks/useChat';
|
|
10
|
+
export { loadChatConfig, buildUserListUrl } from './config';
|
|
11
|
+
export { fetchUsers } from './services/userService';
|
|
12
|
+
export type { ChatUser, ChatMessage, ChatConfig, ChatWidgetTheme, ChatWidgetProps, ChatStatus, ChatType, UserType, TabType, } from './types';
|
|
13
|
+
export { defaultTheme, mergeTheme } from './utils/theme';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Main component
|
|
2
|
+
export { ChatWidget } from './components/ChatWidget';
|
|
3
|
+
export { default } from './components/ChatWidget';
|
|
4
|
+
// Sub-components (for advanced customization)
|
|
5
|
+
export { ChatButton } from './components/ChatButton';
|
|
6
|
+
export { ChatWindow } from './components/ChatWindow';
|
|
7
|
+
export { UserList } from './components/UserList';
|
|
8
|
+
export { ChatBox } from './components/ChatBox';
|
|
9
|
+
export { MaintenanceView } from './components/MaintenanceView';
|
|
10
|
+
// Hooks
|
|
11
|
+
export { useUsers } from './hooks/useUsers';
|
|
12
|
+
export { useChat } from './hooks/useChat';
|
|
13
|
+
// Config utilities
|
|
14
|
+
export { loadChatConfig, buildUserListUrl } from './config';
|
|
15
|
+
// Services
|
|
16
|
+
export { fetchUsers } from './services/userService';
|
|
17
|
+
// Theme utilities
|
|
18
|
+
export { defaultTheme, mergeTheme } from './utils/theme';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export async function fetchUsers(url) {
|
|
2
|
+
const response = await fetch(url, {
|
|
3
|
+
method: 'GET',
|
|
4
|
+
headers: {
|
|
5
|
+
'Content-Type': 'application/json',
|
|
6
|
+
},
|
|
7
|
+
});
|
|
8
|
+
if (!response.ok) {
|
|
9
|
+
throw new Error(`[ChatWidget] Failed to fetch users: ${response.status} ${response.statusText}`);
|
|
10
|
+
}
|
|
11
|
+
const data = await response.json();
|
|
12
|
+
if (!Array.isArray(data)) {
|
|
13
|
+
throw new Error('[ChatWidget] User list API did not return an array.');
|
|
14
|
+
}
|
|
15
|
+
return data;
|
|
16
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type ChatStatus = 'ACTIVE' | 'DISABLE' | 'MAINTENANCE';
|
|
2
|
+
export type ChatType = 'SUPPORT' | 'CHAT' | 'BOTH';
|
|
3
|
+
export type UserType = 'developer' | 'user';
|
|
4
|
+
export type TabType = 'developers' | 'users';
|
|
5
|
+
export interface ChatUser {
|
|
6
|
+
name: string;
|
|
7
|
+
uid: string;
|
|
8
|
+
email: string;
|
|
9
|
+
mobile: string;
|
|
10
|
+
project: string;
|
|
11
|
+
type: UserType;
|
|
12
|
+
}
|
|
13
|
+
export interface ChatMessage {
|
|
14
|
+
id: string;
|
|
15
|
+
senderId: string;
|
|
16
|
+
receiverId: string;
|
|
17
|
+
text: string;
|
|
18
|
+
timestamp: Date;
|
|
19
|
+
status: 'sent' | 'delivered' | 'read';
|
|
20
|
+
}
|
|
21
|
+
export interface ChatConfig {
|
|
22
|
+
hostUrl: string;
|
|
23
|
+
hostPort: number;
|
|
24
|
+
userListEndpoint: string;
|
|
25
|
+
status: ChatStatus;
|
|
26
|
+
chatType: ChatType;
|
|
27
|
+
}
|
|
28
|
+
export interface ChatWidgetTheme {
|
|
29
|
+
fontFamily?: string;
|
|
30
|
+
primaryColor?: string;
|
|
31
|
+
backgroundColor?: string;
|
|
32
|
+
buttonColor?: string;
|
|
33
|
+
buttonTextColor?: string;
|
|
34
|
+
buttonLabel?: string;
|
|
35
|
+
buttonPosition?: 'bottom-right' | 'bottom-left';
|
|
36
|
+
borderRadius?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ChatWidgetProps {
|
|
39
|
+
theme?: ChatWidgetTheme;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ChatWidgetTheme } from '../types';
|
|
2
|
+
export declare const defaultTheme: Required<ChatWidgetTheme>;
|
|
3
|
+
export declare function mergeTheme(custom?: ChatWidgetTheme): Required<ChatWidgetTheme>;
|
|
4
|
+
export declare function themeToCSS(theme: Required<ChatWidgetTheme>): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const defaultTheme = {
|
|
2
|
+
fontFamily: "'DM Sans', 'Segoe UI', sans-serif",
|
|
3
|
+
primaryColor: '#6C63FF',
|
|
4
|
+
backgroundColor: '#ffffff',
|
|
5
|
+
buttonColor: '#6C63FF',
|
|
6
|
+
buttonTextColor: '#ffffff',
|
|
7
|
+
buttonLabel: 'Chat with us',
|
|
8
|
+
buttonPosition: 'bottom-right',
|
|
9
|
+
borderRadius: '16px',
|
|
10
|
+
};
|
|
11
|
+
export function mergeTheme(custom) {
|
|
12
|
+
return Object.assign(Object.assign({}, defaultTheme), custom);
|
|
13
|
+
}
|
|
14
|
+
export function themeToCSS(theme) {
|
|
15
|
+
return `
|
|
16
|
+
--cw-font: ${theme.fontFamily};
|
|
17
|
+
--cw-primary: ${theme.primaryColor};
|
|
18
|
+
--cw-bg: ${theme.backgroundColor};
|
|
19
|
+
--cw-btn-bg: ${theme.buttonColor};
|
|
20
|
+
--cw-btn-text: ${theme.buttonTextColor};
|
|
21
|
+
--cw-radius: ${theme.borderRadius};
|
|
22
|
+
`;
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ajaxter-chat",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A reusable, configurable chat widget for React.js and Next.js applications.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.esm.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
16
|
+
"type-check": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"react": ">=17.0.0",
|
|
20
|
+
"react-dom": ">=17.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^25.5.0",
|
|
24
|
+
"@types/react": "^18.3.28",
|
|
25
|
+
"@types/react-dom": "^18.3.7",
|
|
26
|
+
"typescript": "^5.9.3"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"react",
|
|
30
|
+
"nextjs",
|
|
31
|
+
"chat",
|
|
32
|
+
"widget",
|
|
33
|
+
"support",
|
|
34
|
+
"typescript",
|
|
35
|
+
"ajaxter"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|