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 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,9 @@
1
+ import React from 'react';
2
+ import { ChatWidgetTheme } from '../../types';
3
+ interface ChatButtonProps {
4
+ isOpen: boolean;
5
+ onClick: () => void;
6
+ theme?: ChatWidgetTheme;
7
+ }
8
+ export declare const ChatButton: React.FC<ChatButtonProps>;
9
+ export {};
@@ -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,7 @@
1
+ import React from 'react';
2
+ interface MaintenanceViewProps {
3
+ fontFamily: string;
4
+ primaryColor: string;
5
+ }
6
+ export declare const MaintenanceView: React.FC<MaintenanceViewProps>;
7
+ export {};
@@ -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,3 @@
1
+ import { ChatConfig } from '../types';
2
+ export declare function loadChatConfig(): ChatConfig;
3
+ export declare function buildUserListUrl(config: ChatConfig): string;
@@ -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
+ }
@@ -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,7 @@
1
+ import { ChatUser } from '../types';
2
+ export interface FetchUsersResult {
3
+ data: ChatUser[] | null;
4
+ error: string | null;
5
+ loading: boolean;
6
+ }
7
+ export declare function fetchUsers(url: string): Promise<ChatUser[]>;
@@ -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
+ }