ajaxter-chat 3.0.6 → 3.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ChatScreen/index.js +44 -1
- package/dist/components/ChatWidget.js +26 -13
- package/dist/components/HomeScreen/index.js +41 -22
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/utils/chat.d.ts +2 -0
- package/dist/utils/chat.js +9 -0
- package/dist/utils/privacyConsent.d.ts +4 -0
- package/dist/utils/privacyConsent.js +33 -0
- package/package.json +1 -1
- package/public/chatData.json +40 -7
- package/src/components/ChatScreen/index.tsx +75 -0
- package/src/components/ChatWidget.tsx +26 -20
- package/src/components/HomeScreen/index.tsx +80 -36
- package/src/index.ts +2 -1
- package/src/types/index.ts +10 -0
- package/src/utils/chat.ts +8 -0
- package/src/utils/privacyConsent.ts +32 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
3
3
|
import { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText } from '../../utils/chat';
|
|
4
|
+
import { shouldShowPrivacyNotice, dismissPrivacyNotice } from '../../utils/privacyConsent';
|
|
4
5
|
import { EmojiPicker } from '../EmojiPicker';
|
|
5
6
|
import { SlideNavMenu } from '../SlideNavMenu';
|
|
6
7
|
export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported, isBlocked, onSend, onBack, onClose, onTogglePause, onReport, onBlock, onStartCall, onNavAction, otherDevelopers = [], onTransferToDeveloper, }) => {
|
|
@@ -13,6 +14,7 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
|
|
|
13
14
|
const [isRecording, setIsRecording] = useState(false);
|
|
14
15
|
const [recordSec, setRecordSec] = useState(0);
|
|
15
16
|
const [showConfirm, setShowConfirm] = useState(null);
|
|
17
|
+
const [showPrivacy, setShowPrivacy] = useState(false);
|
|
16
18
|
const endRef = useRef(null);
|
|
17
19
|
const inputRef = useRef(null);
|
|
18
20
|
const fileRef = useRef(null);
|
|
@@ -20,6 +22,24 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
|
|
|
20
22
|
const mediaRecorder = useRef(null);
|
|
21
23
|
const recordChunks = useRef([]);
|
|
22
24
|
useEffect(() => { var _a; (_a = endRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
|
|
25
|
+
const privacyEnabled = config.showPrivacyNotice !== false;
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!privacyEnabled)
|
|
28
|
+
return;
|
|
29
|
+
setShowPrivacy(shouldShowPrivacyNotice(config.id));
|
|
30
|
+
}, [config.id, privacyEnabled]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!privacyEnabled)
|
|
33
|
+
return;
|
|
34
|
+
const id = window.setInterval(() => {
|
|
35
|
+
setShowPrivacy(shouldShowPrivacyNotice(config.id));
|
|
36
|
+
}, 60000);
|
|
37
|
+
return () => window.clearInterval(id);
|
|
38
|
+
}, [config.id, privacyEnabled]);
|
|
39
|
+
const dismissPrivacy = useCallback(() => {
|
|
40
|
+
dismissPrivacyNotice(config.id);
|
|
41
|
+
setShowPrivacy(false);
|
|
42
|
+
}, [config.id]);
|
|
23
43
|
const handleSend = useCallback(() => {
|
|
24
44
|
var _a;
|
|
25
45
|
if (!text.trim() || isPaused || isBlocked)
|
|
@@ -105,7 +125,30 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
|
|
|
105
125
|
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideIn 0.22s ease', position: 'relative', overflow: 'hidden' }, children: [_jsx(SlideNavMenu, { open: slideMenuOpen, onClose: () => setSlideMenuOpen(false), primaryColor: config.primaryColor, chatType: config.chatType, viewerType: (_a = config.viewerType) !== null && _a !== void 0 ? _a : 'user', onSelect: onNavAction, onBackHome: onBack }), _jsxs("div", { style: {
|
|
106
126
|
background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
|
|
107
127
|
padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0,
|
|
108
|
-
}, children: [_jsx("button", { type: "button", onClick: () => setSlideMenuOpen(true), style: hdrBtn, "aria-label": "Open menu", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "3", y1: "6", x2: "21", y2: "6", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" }), _jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" }), _jsx("line", { x1: "3", y1: "18", x2: "21", y2: "18", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" })] }) }), _jsxs("div", { style: { width: 36, height: 36, borderRadius: '50%', backgroundColor: peerAvatar, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontWeight: 700, fontSize: 13, flexShrink: 0, position: 'relative' }, children: [peerInit, _jsx("span", { style: { position: 'absolute', bottom: 0, right: 0, width: 9, height: 9, borderRadius: '50%', border: '2px solid', borderColor: 'transparent', backgroundColor: activeUser.status === 'online' ? '#22c55e' : activeUser.status === 'away' ? '#f59e0b' : '#9ca3af' } })] }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 14, color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: activeUser.name }), _jsx("div", { style: { fontSize: 11, color: 'rgba(255,255,255,0.8)' }, children: activeUser.designation })] }), _jsx("span", { style: { fontSize: 13, fontWeight: 700, color: '#fff', opacity: 0.95, flexShrink: 0 }, children: headerRole }), config.allowWebCall && (_jsx("button", { type: "button", onClick: () => onStartCall(false), style: hdrBtn, title: "Voice Call", children: _jsx("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.68A2 2 0 012 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 14.92v2z", fill: "#fff" }) }) })), _jsx("button", { type: "button", onClick: () => setShowMenu(v => !v), style: Object.assign(Object.assign({}, hdrBtn), { background: 'rgba(255,255,255,0.2)' }), title: "More options", "aria-expanded": showMenu, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "12", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "19", r: "1.5", fill: "#fff" })] }) })] }), showMenu && (_jsxs("div", { style: { position: 'absolute', top: 52, right: 12, zIndex: 120, background: '#fff', borderRadius: 12, boxShadow: '0 8px 30px rgba(0,0,0,0.16)', padding: '6px', minWidth: 200, animation: 'cw-fadeUp 0.18s ease' }, children: [config.allowTranscriptDownload && (_jsx(MenuItem, { icon: "\uD83D\uDCE5", label: "Download Transcript", onClick: handleTranscript })), viewerIsDev && activeUser.type === 'user' && otherDevelopers.length > 0 && onTransferToDeveloper && (_jsx(MenuItem, { icon: "\uD83D\uDD00", label: "Transfer to developer", onClick: () => { setShowMenu(false); setTransferOpen(true); } })), _jsx(MenuItem, { icon: isPaused ? '▶️' : '⏸', label: isPaused ? 'Resume Chat' : 'Pause Chat', onClick: () => { setShowMenu(false); setShowConfirm('pause'); } }), config.allowReport && !isReported && (_jsx(MenuItem, { icon: "\u26A0\uFE0F", label: "Report Chat", onClick: () => { setShowMenu(false); setShowConfirm('report'); } })), config.allowBlock && activeUser.type === 'user' && !isBlocked && (_jsx(MenuItem, { icon: "\uD83D\uDEAB", label: "Block User", onClick: () => { setShowMenu(false); setShowConfirm('block'); }, danger: true })), _jsx("div", { style: { borderTop: '1px solid #f0f2f5', margin: '4px 0' } }), _jsx(MenuItem, { icon: "\u2715", label: "Close Chat", onClick: onClose })] })), isPaused && (_jsxs("div", { style: { background: '#fef3c7', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#92400e', textAlign: 'center', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }, children: ["\u23F8 Chat is paused \u2014 users cannot send messages", _jsx("button", { type: "button", onClick: onTogglePause, style: { background: '#92400e', color: '#fff', border: 'none', borderRadius: 6, padding: '2px 8px', fontSize: 11, cursor: 'pointer', marginLeft: 4 }, children: "Resume" })] })), isBlocked && (_jsx("div", { style: { background: '#fee2e2', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#991b1b', textAlign: 'center', flexShrink: 0 }, children: "\uD83D\uDEAB This user is blocked" })), isReported && (_jsx("div", { style: { background: '#fef3c7', padding: '6px 16px', fontSize: 11, color: '#92400e', textAlign: 'center', flexShrink: 0 }, children: "\u26A0\uFE0F This chat has been reported" })), _jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '14px', display: 'flex', flexDirection: 'column', gap: 10, background: '#f8f9fc' }, className: "cw-scroll", children: [grouped.map(({ date, msgs }) => (_jsxs(React.Fragment, { children: [_jsx(DateDivider, { label: date }), msgs.map(msg => (_jsx(Bubble, { msg: msg, primaryColor: config.primaryColor }, msg.id)))] }, date))), messages.length === 0 && (_jsxs("div", { style: { margin: 'auto', textAlign: 'center', color: '#c4cad4', fontSize: 13 }, children: [_jsx("div", { style: { fontSize: 28, marginBottom: 8 }, children: "\uD83D\uDCAC" }), "Say hello to ", activeUser.name, "!"] })), _jsx("div", { ref: endRef })] }), _jsxs("div", { style: { borderTop: '1px solid #eef0f5', padding: '10px 12px 8px', background: '#fff', flexShrink: 0, position: 'relative' }, children: [
|
|
128
|
+
}, children: [_jsx("button", { type: "button", onClick: () => setSlideMenuOpen(true), style: hdrBtn, "aria-label": "Open menu", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "3", y1: "6", x2: "21", y2: "6", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" }), _jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" }), _jsx("line", { x1: "3", y1: "18", x2: "21", y2: "18", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" })] }) }), _jsxs("div", { style: { width: 36, height: 36, borderRadius: '50%', backgroundColor: peerAvatar, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontWeight: 700, fontSize: 13, flexShrink: 0, position: 'relative' }, children: [peerInit, _jsx("span", { style: { position: 'absolute', bottom: 0, right: 0, width: 9, height: 9, borderRadius: '50%', border: '2px solid', borderColor: 'transparent', backgroundColor: activeUser.status === 'online' ? '#22c55e' : activeUser.status === 'away' ? '#f59e0b' : '#9ca3af' } })] }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 14, color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: activeUser.name }), _jsx("div", { style: { fontSize: 11, color: 'rgba(255,255,255,0.8)' }, children: activeUser.designation })] }), _jsx("span", { style: { fontSize: 13, fontWeight: 700, color: '#fff', opacity: 0.95, flexShrink: 0 }, children: headerRole }), config.allowWebCall && (_jsx("button", { type: "button", onClick: () => onStartCall(false), style: hdrBtn, title: "Voice Call", children: _jsx("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.68A2 2 0 012 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 14.92v2z", fill: "#fff" }) }) })), _jsx("button", { type: "button", onClick: () => setShowMenu(v => !v), style: Object.assign(Object.assign({}, hdrBtn), { background: 'rgba(255,255,255,0.2)' }), title: "More options", "aria-expanded": showMenu, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "12", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "19", r: "1.5", fill: "#fff" })] }) })] }), showMenu && (_jsxs("div", { style: { position: 'absolute', top: 52, right: 12, zIndex: 120, background: '#fff', borderRadius: 12, boxShadow: '0 8px 30px rgba(0,0,0,0.16)', padding: '6px', minWidth: 200, animation: 'cw-fadeUp 0.18s ease' }, children: [config.allowTranscriptDownload && (_jsx(MenuItem, { icon: "\uD83D\uDCE5", label: "Download Transcript", onClick: handleTranscript })), viewerIsDev && activeUser.type === 'user' && otherDevelopers.length > 0 && onTransferToDeveloper && (_jsx(MenuItem, { icon: "\uD83D\uDD00", label: "Transfer to developer", onClick: () => { setShowMenu(false); setTransferOpen(true); } })), _jsx(MenuItem, { icon: isPaused ? '▶️' : '⏸', label: isPaused ? 'Resume Chat' : 'Pause Chat', onClick: () => { setShowMenu(false); setShowConfirm('pause'); } }), config.allowReport && !isReported && (_jsx(MenuItem, { icon: "\u26A0\uFE0F", label: "Report Chat", onClick: () => { setShowMenu(false); setShowConfirm('report'); } })), config.allowBlock && activeUser.type === 'user' && !isBlocked && (_jsx(MenuItem, { icon: "\uD83D\uDEAB", label: "Block User", onClick: () => { setShowMenu(false); setShowConfirm('block'); }, danger: true })), _jsx("div", { style: { borderTop: '1px solid #f0f2f5', margin: '4px 0' } }), _jsx(MenuItem, { icon: "\u2715", label: "Close Chat", onClick: onClose })] })), isPaused && (_jsxs("div", { style: { background: '#fef3c7', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#92400e', textAlign: 'center', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }, children: ["\u23F8 Chat is paused \u2014 users cannot send messages", _jsx("button", { type: "button", onClick: onTogglePause, style: { background: '#92400e', color: '#fff', border: 'none', borderRadius: 6, padding: '2px 8px', fontSize: 11, cursor: 'pointer', marginLeft: 4 }, children: "Resume" })] })), isBlocked && (_jsx("div", { style: { background: '#fee2e2', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#991b1b', textAlign: 'center', flexShrink: 0 }, children: "\uD83D\uDEAB This user is blocked" })), isReported && (_jsx("div", { style: { background: '#fef3c7', padding: '6px 16px', fontSize: 11, color: '#92400e', textAlign: 'center', flexShrink: 0 }, children: "\u26A0\uFE0F This chat has been reported" })), _jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '14px', display: 'flex', flexDirection: 'column', gap: 10, background: '#f8f9fc' }, className: "cw-scroll", children: [grouped.map(({ date, msgs }) => (_jsxs(React.Fragment, { children: [_jsx(DateDivider, { label: date }), msgs.map(msg => (_jsx(Bubble, { msg: msg, primaryColor: config.primaryColor }, msg.id)))] }, date))), messages.length === 0 && (_jsxs("div", { style: { margin: 'auto', textAlign: 'center', color: '#c4cad4', fontSize: 13 }, children: [_jsx("div", { style: { fontSize: 28, marginBottom: 8 }, children: "\uD83D\uDCAC" }), "Say hello to ", activeUser.name, "!"] })), _jsx("div", { ref: endRef })] }), _jsxs("div", { style: { borderTop: '1px solid #eef0f5', padding: '10px 12px 8px', background: '#fff', flexShrink: 0, position: 'relative' }, children: [privacyEnabled && showPrivacy && (_jsxs("div", { style: {
|
|
129
|
+
position: 'relative',
|
|
130
|
+
marginBottom: 10,
|
|
131
|
+
padding: '12px 36px 12px 12px',
|
|
132
|
+
borderRadius: 12,
|
|
133
|
+
background: '#fff',
|
|
134
|
+
border: '1px solid #e8ecf1',
|
|
135
|
+
boxShadow: '0 1px 4px rgba(15,23,42,0.06)',
|
|
136
|
+
}, children: [_jsx("button", { type: "button", "aria-label": "Dismiss privacy notice", onClick: dismissPrivacy, style: {
|
|
137
|
+
position: 'absolute',
|
|
138
|
+
top: 8,
|
|
139
|
+
right: 8,
|
|
140
|
+
width: 26,
|
|
141
|
+
height: 26,
|
|
142
|
+
borderRadius: '50%',
|
|
143
|
+
border: 'none',
|
|
144
|
+
background: '#f1f5f9',
|
|
145
|
+
cursor: 'pointer',
|
|
146
|
+
display: 'flex',
|
|
147
|
+
alignItems: 'center',
|
|
148
|
+
justifyContent: 'center',
|
|
149
|
+
padding: 0,
|
|
150
|
+
lineHeight: 1,
|
|
151
|
+
}, children: _jsx("span", { style: { fontSize: 14, color: '#475569', fontWeight: 700 }, children: "\u00D7" }) }), _jsxs("p", { style: { margin: 0, fontSize: 12, color: '#64748b', lineHeight: 1.55 }, children: ["By chatting here, you agree we and authorized partners may process, monitor, and record this chat and your data in line with", ' ', config.privacyPolicyUrl ? (_jsx("a", { href: config.privacyPolicyUrl, target: "_blank", rel: "noopener noreferrer", style: { color: config.primaryColor, textDecoration: 'underline', fontWeight: 600 }, children: "Privacy Policy" })) : (_jsx("span", { style: { textDecoration: 'underline', fontWeight: 600 }, children: "Privacy Policy" })), "."] })] })), showEmoji && config.allowEmoji && (_jsx(EmojiPicker, { primaryColor: config.primaryColor, onSelect: e => setText(t => t + e), onClose: () => setShowEmoji(false) })), isRecording && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8, padding: '8px 12px', background: '#fee2e2', borderRadius: 10 }, children: [_jsx("span", { style: { width: 8, height: 8, borderRadius: '50%', background: '#ef4444', display: 'inline-block', animation: 'cw-pulse 1s infinite' } }), _jsxs("span", { style: { fontSize: 13, color: '#991b1b', fontWeight: 600 }, children: ["Recording ", recordSec, "s"] }), _jsx("button", { type: "button", onClick: stopRecording, style: { marginLeft: 'auto', background: '#ef4444', color: '#fff', border: 'none', borderRadius: 6, padding: '4px 12px', fontSize: 12, cursor: 'pointer', fontWeight: 600 }, children: "Stop & send" })] })), _jsxs("div", { style: {
|
|
109
152
|
border: `1.5px solid ${isPaused || isBlocked ? '#e5e7eb' : '#bfdbfe'}`,
|
|
110
153
|
borderRadius: 16,
|
|
111
154
|
padding: '10px 12px 8px',
|
|
@@ -15,9 +15,6 @@ import { BlockListScreen } from './BlockList';
|
|
|
15
15
|
import { CallScreen } from './CallScreen';
|
|
16
16
|
import { MaintenanceView } from './MaintenanceView';
|
|
17
17
|
import { BottomTabs } from './Tabs/BottomTabs';
|
|
18
|
-
/* ─── Drawer width ─────────────────────────────────────────────────────────── */
|
|
19
|
-
const DRAWER_W_NORMAL = 380;
|
|
20
|
-
const DRAWER_W_MAX = 480;
|
|
21
18
|
export const ChatWidget = ({ theme: localTheme }) => {
|
|
22
19
|
var _a, _b, _c, _d;
|
|
23
20
|
/* SSR guard */
|
|
@@ -31,7 +28,6 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
31
28
|
const theme = mergeTheme((data === null || data === void 0 ? void 0 : data.widget) ? { primaryColor: data.widget.primaryColor, buttonLabel: data.widget.buttonLabel, buttonPosition: data.widget.buttonPosition } : undefined, localTheme);
|
|
32
29
|
/* Drawer open state */
|
|
33
30
|
const [isOpen, setIsOpen] = useState(false);
|
|
34
|
-
const [isMaximized, setIsMaximized] = useState(false);
|
|
35
31
|
const [closing, setClosing] = useState(false); // for slide-out animation
|
|
36
32
|
/* Navigation */
|
|
37
33
|
const [activeTab, setActiveTab] = useState('home');
|
|
@@ -168,7 +164,6 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
168
164
|
}, [endCall]);
|
|
169
165
|
/* ── Derived ─────────────────────────────────────────────────────────── */
|
|
170
166
|
const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
|
|
171
|
-
const drawerW = isMaximized ? DRAWER_W_MAX : DRAWER_W_NORMAL;
|
|
172
167
|
const widgetConfig = data === null || data === void 0 ? void 0 : data.widget;
|
|
173
168
|
const primaryColor = theme.primaryColor;
|
|
174
169
|
const allUsers = data ? [...data.developers, ...data.users] : [];
|
|
@@ -209,9 +204,20 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
209
204
|
const posStyle = theme.buttonPosition === 'bottom-left'
|
|
210
205
|
? { left: 24, right: 'auto' }
|
|
211
206
|
: { right: 24, left: 'auto' };
|
|
207
|
+
/* No radius on top-left / bottom-left; left-docked panel keeps inner TR/BR curve */
|
|
212
208
|
const drawerPosStyle = theme.buttonPosition === 'bottom-left'
|
|
213
|
-
? {
|
|
214
|
-
|
|
209
|
+
? {
|
|
210
|
+
left: 0,
|
|
211
|
+
borderTopLeftRadius: 0,
|
|
212
|
+
borderBottomLeftRadius: 0,
|
|
213
|
+
borderTopRightRadius: 16,
|
|
214
|
+
borderBottomRightRadius: 16,
|
|
215
|
+
}
|
|
216
|
+
: {
|
|
217
|
+
right: 0,
|
|
218
|
+
borderTopLeftRadius: 0,
|
|
219
|
+
borderBottomLeftRadius: 0,
|
|
220
|
+
};
|
|
215
221
|
/* ── Don't render until mounted (SSR safe) ──────────────────────────── */
|
|
216
222
|
if (!mounted)
|
|
217
223
|
return null;
|
|
@@ -235,6 +241,15 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
235
241
|
|
|
236
242
|
.cw-drawer-enter { animation: ${theme.buttonPosition === 'bottom-left' ? 'cw-slideInLeft' : 'cw-slideInRight'} 0.32s cubic-bezier(0.22,1,0.36,1) both; }
|
|
237
243
|
.cw-drawer-exit { animation: ${theme.buttonPosition === 'bottom-left' ? 'cw-slideOutLeft' : 'cw-slideOutRight'} 0.28s cubic-bezier(0.55,0,1,0.45) both; }
|
|
244
|
+
|
|
245
|
+
.cw-drawer-panel {
|
|
246
|
+
width: 30%;
|
|
247
|
+
max-width: 100vw;
|
|
248
|
+
min-width: 0;
|
|
249
|
+
}
|
|
250
|
+
@media (max-width: 1024px) {
|
|
251
|
+
.cw-drawer-panel { width: 100%; }
|
|
252
|
+
}
|
|
238
253
|
` }), !isOpen && (_jsxs("button", { className: "cw-root", onClick: openDrawer, "aria-label": theme.buttonLabel, style: Object.assign(Object.assign({ position: 'fixed', bottom: 24, zIndex: 9999 }, posStyle), { display: 'flex', alignItems: 'center', gap: 10, padding: '13px 22px', backgroundColor: theme.buttonColor, color: theme.buttonTextColor, border: 'none', borderRadius: 50, cursor: 'pointer', fontSize: 15, fontWeight: 700, boxShadow: `0 8px 28px ${theme.buttonColor}55`, animation: 'cw-btnPop 0.4s cubic-bezier(0.34,1.56,0.64,1)', transition: 'transform 0.2s, box-shadow 0.2s' }), onMouseEnter: e => {
|
|
239
254
|
e.currentTarget.style.transform = 'scale(1.06) translateY(-2px)';
|
|
240
255
|
e.currentTarget.style.boxShadow = `0 14px 36px ${theme.buttonColor}66`;
|
|
@@ -246,21 +261,19 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
246
261
|
backgroundColor: 'rgba(0,0,0,0.35)',
|
|
247
262
|
opacity: closing ? 0 : 1,
|
|
248
263
|
transition: 'opacity 0.3s',
|
|
249
|
-
} })), isOpen && (_jsxs("div", { className: `cw-root ${closing ? 'cw-drawer-exit' : 'cw-drawer-enter'}`, style: Object.assign(Object.assign({ position: 'fixed', top: 0, bottom: 0 }, drawerPosStyle), { zIndex: 9998,
|
|
264
|
+
} })), isOpen && (_jsxs("div", { className: `cw-root cw-drawer-panel ${closing ? 'cw-drawer-exit' : 'cw-drawer-enter'}`, style: Object.assign(Object.assign({ position: 'fixed', top: 0, bottom: 0 }, drawerPosStyle), { zIndex: 9998, backgroundColor: '#fff', boxShadow: theme.buttonPosition === 'bottom-left'
|
|
250
265
|
? '4px 0 40px rgba(0,0,0,0.18)'
|
|
251
|
-
: '-4px 0 40px rgba(0,0,0,0.18)', display: 'flex', flexDirection: 'column', overflow: 'hidden'
|
|
266
|
+
: '-4px 0 40px rgba(0,0,0,0.18)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }), children: [cfgLoading && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 16 }, children: [_jsx("div", { style: {
|
|
252
267
|
width: 40, height: 40, borderRadius: '50%',
|
|
253
268
|
border: `3px solid ${primaryColor}30`,
|
|
254
269
|
borderTopColor: primaryColor,
|
|
255
270
|
animation: 'spin 0.8s linear infinite',
|
|
256
|
-
} }), _jsx("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` }), _jsx("p", { style: { fontSize: 14, color: '#7b8fa1' }, children: "Loading chat\u2026" })] })), cfgError && !cfgLoading && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 12, padding: 32, textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\u26A0\uFE0F" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Could not load chat configuration" }), _jsx("p", { style: { fontSize: 13, color: '#7b8fa1', lineHeight: 1.6 }, children: cfgError }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), !cfgLoading && !cfgError && widgetConfig && (_jsxs(_Fragment, { children: [screen !== 'chat' && screen !== 'call' && (
|
|
271
|
+
} }), _jsx("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` }), _jsx("p", { style: { fontSize: 14, color: '#7b8fa1' }, children: "Loading chat\u2026" })] })), cfgError && !cfgLoading && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 12, padding: 32, textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\u26A0\uFE0F" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Could not load chat configuration" }), _jsx("p", { style: { fontSize: 13, color: '#7b8fa1', lineHeight: 1.6 }, children: cfgError }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), !cfgLoading && !cfgError && widgetConfig && (_jsxs(_Fragment, { children: [screen !== 'chat' && screen !== 'call' && (_jsx("div", { style: {
|
|
257
272
|
position: 'absolute', top: 12,
|
|
258
273
|
right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
|
|
259
274
|
left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
|
|
260
275
|
zIndex: 20, display: 'flex', gap: 6,
|
|
261
|
-
}, children:
|
|
262
|
-
? _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M8 3v5H3M21 8h-5V3M3 16h5v5M16 21v-5h5", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" }) })
|
|
263
|
-
: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M8 3H5a2 2 0 00-2 2v3M21 8V5a2 2 0 00-2-2h-3M3 16v3a2 2 0 002 2h3M16 21h3a2 2 0 002-2v-3", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" }) }) }), _jsx(CornerBtn, { onClick: closeDrawer, title: "Close", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) })] })), widgetConfig.status === 'MAINTENANCE' && (_jsx(MaintenanceView, { primaryColor: primaryColor })), widgetConfig.status === 'DISABLE' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12 }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Chat is disabled" }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), widgetConfig.status === 'ACTIVE' && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, onNavigate: handleCardClick, tickets: tickets })), screen === 'user-list' && (_jsx(UserListScreen, { context: userListCtx, users: filteredUsers, primaryColor: primaryColor, viewerType: (_d = widgetConfig.viewerType) !== null && _d !== void 0 ? _d : 'user', onBack: () => setScreen('home'), onSelectUser: handleSelectUser })), screen === 'chat' && activeUser && (_jsx(ChatScreen, { activeUser: activeUser, messages: messages, config: widgetConfig, isPaused: isPaused, isReported: isReported, isBlocked: isBlocked, onSend: sendMessage, onBack: () => { clearChat(); setScreen('home'); setActiveTab('home'); }, onClose: closeDrawer, onTogglePause: handleTogglePause, onReport: reportChat, onBlock: handleBlock, onStartCall: handleStartCall, onNavAction: handleNavFromMenu, otherDevelopers: otherDevelopers, onTransferToDeveloper: handleTransferToDeveloper })), screen === 'call' && callSession.peer && (_jsx(CallScreen, { session: callSession, localVideoRef: localVideoRef, remoteVideoRef: remoteVideoRef, onEnd: handleEndCall, onToggleMute: toggleMute, onToggleCamera: toggleCamera, primaryColor: primaryColor })), screen === 'recent-chats' && (_jsx(RecentChatsScreen, { chats: recentChats, config: widgetConfig, onSelectChat: handleSelectUser })), screen === 'tickets' && (_jsx(TicketScreen, { tickets: tickets, config: widgetConfig, onRaiseTicket: handleRaiseTicket })), screen === 'block-list' && (_jsx(BlockListScreen, { blockedUsers: blockedUsers, config: widgetConfig, onUnblock: handleUnblock, onBack: () => { setScreen('home'); setActiveTab('home'); } }))] })), widgetConfig.status === 'ACTIVE' &&
|
|
276
|
+
}, children: _jsx(CornerBtn, { onClick: closeDrawer, title: "Close", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) }) })), widgetConfig.status === 'MAINTENANCE' && (_jsx(MaintenanceView, { primaryColor: primaryColor })), widgetConfig.status === 'DISABLE' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12 }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Chat is disabled" }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), widgetConfig.status === 'ACTIVE' && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, onNavigate: handleCardClick, tickets: tickets })), screen === 'user-list' && (_jsx(UserListScreen, { context: userListCtx, users: filteredUsers, primaryColor: primaryColor, viewerType: (_d = widgetConfig.viewerType) !== null && _d !== void 0 ? _d : 'user', onBack: () => setScreen('home'), onSelectUser: handleSelectUser })), screen === 'chat' && activeUser && (_jsx(ChatScreen, { activeUser: activeUser, messages: messages, config: widgetConfig, isPaused: isPaused, isReported: isReported, isBlocked: isBlocked, onSend: sendMessage, onBack: () => { clearChat(); setScreen('home'); setActiveTab('home'); }, onClose: closeDrawer, onTogglePause: handleTogglePause, onReport: reportChat, onBlock: handleBlock, onStartCall: handleStartCall, onNavAction: handleNavFromMenu, otherDevelopers: otherDevelopers, onTransferToDeveloper: handleTransferToDeveloper })), screen === 'call' && callSession.peer && (_jsx(CallScreen, { session: callSession, localVideoRef: localVideoRef, remoteVideoRef: remoteVideoRef, onEnd: handleEndCall, onToggleMute: toggleMute, onToggleCamera: toggleCamera, primaryColor: primaryColor })), screen === 'recent-chats' && (_jsx(RecentChatsScreen, { chats: recentChats, config: widgetConfig, onSelectChat: handleSelectUser })), screen === 'tickets' && (_jsx(TicketScreen, { tickets: tickets, config: widgetConfig, onRaiseTicket: handleRaiseTicket })), screen === 'block-list' && (_jsx(BlockListScreen, { blockedUsers: blockedUsers, config: widgetConfig, onUnblock: handleUnblock, onBack: () => { setScreen('home'); setActiveTab('home'); } }))] })), widgetConfig.status === 'ACTIVE' &&
|
|
264
277
|
screen !== 'chat' &&
|
|
265
278
|
screen !== 'call' &&
|
|
266
279
|
screen !== 'user-list' &&
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useState, useMemo } from 'react';
|
|
3
3
|
import { SlideNavMenu } from '../SlideNavMenu';
|
|
4
|
+
import { truncateWords } from '../../utils/chat';
|
|
4
5
|
export const HomeScreen = ({ config, onNavigate, tickets }) => {
|
|
5
|
-
var _a;
|
|
6
|
+
var _a, _b, _c, _d;
|
|
6
7
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
7
8
|
const showSupport = config.chatType === 'SUPPORT' || config.chatType === 'BOTH';
|
|
8
9
|
const showChat = config.chatType === 'CHAT' || config.chatType === 'BOTH';
|
|
9
10
|
const viewerIsDev = config.viewerType === 'developer';
|
|
10
|
-
const
|
|
11
|
+
const pendingTickets = useMemo(() => tickets
|
|
12
|
+
.filter(t => t.status === 'open' || t.status === 'in-progress')
|
|
13
|
+
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
|
14
|
+
.slice(0, 5), [tickets]);
|
|
15
|
+
const brand = ((_a = config.brandName) === null || _a === void 0 ? void 0 : _a.trim()) || 'Ajaxter';
|
|
16
|
+
const promotionLead = ((_b = config.promotionLead) === null || _b === void 0 ? void 0 : _b.trim()) ||
|
|
17
|
+
'Need specialized help? Our teams are ready to assist you with any questions.';
|
|
18
|
+
const tourUrl = (_c = config.websiteTourUrl) === null || _c === void 0 ? void 0 : _c.trim();
|
|
11
19
|
const handleCallUs = () => {
|
|
12
20
|
var _a;
|
|
13
21
|
const raw = (_a = config.supportPhone) === null || _a === void 0 ? void 0 : _a.trim();
|
|
@@ -15,7 +23,7 @@ export const HomeScreen = ({ config, onNavigate, tickets }) => {
|
|
|
15
23
|
return;
|
|
16
24
|
window.location.href = `tel:${raw.replace(/\s/g, '')}`;
|
|
17
25
|
};
|
|
18
|
-
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', overflow: 'hidden', background: '#fafbfc' }, children: [_jsx(SlideNavMenu, { open: menuOpen, onClose: () => setMenuOpen(false), primaryColor: config.primaryColor, chatType: config.chatType, viewerType: (
|
|
26
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', overflow: 'hidden', background: '#fafbfc' }, children: [_jsx(SlideNavMenu, { open: menuOpen, onClose: () => setMenuOpen(false), primaryColor: config.primaryColor, chatType: config.chatType, viewerType: (_d = config.viewerType) !== null && _d !== void 0 ? _d : 'user', onSelect: onNavigate }), _jsx("div", { style: {
|
|
19
27
|
flexShrink: 0,
|
|
20
28
|
padding: '14px 16px 10px',
|
|
21
29
|
display: 'flex',
|
|
@@ -43,7 +51,7 @@ export const HomeScreen = ({ config, onNavigate, tickets }) => {
|
|
|
43
51
|
color: '#0f172a',
|
|
44
52
|
letterSpacing: '-0.03em',
|
|
45
53
|
lineHeight: 1.2,
|
|
46
|
-
}, children: config.welcomeTitle }), _jsx("p", { style: { margin: '0 0 28px', fontSize: 14, color: '#64748b', lineHeight: 1.55 }, children: config.welcomeSubtitle }), _jsx("h2", { style: { margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }, children: "Continue Conversations" }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 }, children:
|
|
54
|
+
}, children: config.welcomeTitle }), _jsx("p", { style: { margin: '0 0 28px', fontSize: 14, color: '#64748b', lineHeight: 1.55 }, children: config.welcomeSubtitle }), _jsx("h2", { style: { margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }, children: "Continue Conversations" }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 }, children: pendingTickets.length > 0 ? (pendingTickets.map(t => (_jsxs("button", { type: "button", onClick: () => onNavigate('ticket'), style: {
|
|
47
55
|
width: '100%',
|
|
48
56
|
textAlign: 'left',
|
|
49
57
|
padding: '14px 16px',
|
|
@@ -51,11 +59,9 @@ export const HomeScreen = ({ config, onNavigate, tickets }) => {
|
|
|
51
59
|
border: 'none',
|
|
52
60
|
background: '#e0f2fe',
|
|
53
61
|
color: '#0369a1',
|
|
54
|
-
fontSize: 14,
|
|
55
|
-
fontWeight: 600,
|
|
56
62
|
cursor: 'pointer',
|
|
57
63
|
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
|
|
58
|
-
}, children: t.title }, t.id)))) : (_jsxs(_Fragment, { children: [_jsx("div", { style: {
|
|
64
|
+
}, children: [_jsx("div", { style: { fontSize: 14, fontWeight: 700, color: '#0c4a6e', marginBottom: 6 }, children: t.title }), _jsx("div", { style: { fontSize: 12, fontWeight: 500, color: '#64748b', lineHeight: 1.45 }, children: truncateWords(t.description, 50) })] }, t.id)))) : (_jsxs(_Fragment, { children: [_jsx("div", { style: {
|
|
59
65
|
padding: '14px 16px',
|
|
60
66
|
borderRadius: 14,
|
|
61
67
|
background: '#e0f2fe',
|
|
@@ -117,18 +123,31 @@ export const HomeScreen = ({ config, onNavigate, tickets }) => {
|
|
|
117
123
|
background: 'linear-gradient(145deg, #fce7f3 0%, #e9d5ff 45%, #ddd6fe 100%)',
|
|
118
124
|
position: 'relative',
|
|
119
125
|
overflow: 'hidden',
|
|
120
|
-
}, children: [_jsx("div", { style: { position: 'absolute', top: -20, right: -20, width: 100, height: 100, borderRadius: '50%', background: 'rgba(255,255,255,0.35)' } }), _jsx("p", { style: { margin: '0 0
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
126
|
+
}, children: [_jsx("div", { style: { position: 'absolute', top: -20, right: -20, width: 100, height: 100, borderRadius: '50%', background: 'rgba(255,255,255,0.35)' } }), _jsx("p", { style: { margin: '0 0 10px', fontSize: 15, fontWeight: 700, color: '#4c1d95', lineHeight: 1.45, position: 'relative' }, children: promotionLead }), _jsxs("p", { style: { margin: '0 0 16px', fontSize: 13, fontWeight: 500, color: '#5b21b6', lineHeight: 1.5, position: 'relative' }, children: [_jsx("strong", { style: { fontWeight: 800 }, children: brand }), " \u2014 embedded chat for your workspace.", ' ', _jsx("span", { style: { whiteSpace: 'nowrap' }, children: "Free for users." }), " 24\u00D77 availability. Dedicated workspace experience."] }), _jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 10, position: 'relative' }, children: [tourUrl && (_jsx("a", { href: tourUrl, target: "_blank", rel: "noopener noreferrer", style: {
|
|
127
|
+
display: 'inline-flex',
|
|
128
|
+
alignItems: 'center',
|
|
129
|
+
justifyContent: 'center',
|
|
130
|
+
gap: 6,
|
|
131
|
+
padding: '10px 16px',
|
|
132
|
+
borderRadius: 12,
|
|
133
|
+
border: 'none',
|
|
134
|
+
background: '#fff',
|
|
135
|
+
color: '#5b21b6',
|
|
136
|
+
fontSize: 13,
|
|
137
|
+
fontWeight: 700,
|
|
138
|
+
textDecoration: 'none',
|
|
139
|
+
boxShadow: '0 2px 8px rgba(91,33,182,0.15)',
|
|
140
|
+
}, children: "Take a Website Tour" })), _jsxs("button", { type: "button", onClick: handleCallUs, disabled: !config.supportPhone, style: {
|
|
141
|
+
display: 'inline-flex',
|
|
142
|
+
alignItems: 'center',
|
|
143
|
+
gap: 8,
|
|
144
|
+
padding: '10px 18px',
|
|
145
|
+
borderRadius: 12,
|
|
146
|
+
border: 'none',
|
|
147
|
+
background: config.supportPhone ? config.primaryColor : '#94a3b8',
|
|
148
|
+
color: '#fff',
|
|
149
|
+
fontSize: 14,
|
|
150
|
+
fontWeight: 700,
|
|
151
|
+
cursor: config.supportPhone ? 'pointer' : 'not-allowed',
|
|
152
|
+
}, children: [_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.68A2 2 0 012 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 14.92v2z", fill: "#fff" }) }), "Call Us"] })] })] })] })] }));
|
|
134
153
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,8 @@ export { SlideNavMenu } from './components/SlideNavMenu';
|
|
|
13
13
|
export { useChat } from './hooks/useChat';
|
|
14
14
|
export { useWebRTC } from './hooks/useWebRTC';
|
|
15
15
|
export { useRemoteConfig } from './hooks/useRemoteConfig';
|
|
16
|
+
export { shouldShowPrivacyNotice, dismissPrivacyNotice, getPrivacyDismissedAt } from './utils/privacyConsent';
|
|
16
17
|
export { loadLocalConfig, fetchRemoteChatData } from './config';
|
|
17
18
|
export { mergeTheme, darken } from './utils/theme';
|
|
18
|
-
export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText } from './utils/chat';
|
|
19
|
+
export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText, truncateWords } from './utils/chat';
|
|
19
20
|
export type { ChatWidgetProps, ChatWidgetTheme, WidgetConfig, RemoteChatData, ChatUser, ChatMessage, Ticket, RecentChat, CallSession, CallState, ChatStatus, ChatType, UserType, OnlineStatus, Screen, BottomTab, UserListContext, MessageType, LocalEnvConfig, } from './types';
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export { SlideNavMenu } from './components/SlideNavMenu';
|
|
|
13
13
|
export { useChat } from './hooks/useChat';
|
|
14
14
|
export { useWebRTC } from './hooks/useWebRTC';
|
|
15
15
|
export { useRemoteConfig } from './hooks/useRemoteConfig';
|
|
16
|
+
export { shouldShowPrivacyNotice, dismissPrivacyNotice, getPrivacyDismissedAt } from './utils/privacyConsent';
|
|
16
17
|
export { loadLocalConfig, fetchRemoteChatData } from './config';
|
|
17
18
|
export { mergeTheme, darken } from './utils/theme';
|
|
18
|
-
export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText } from './utils/chat';
|
|
19
|
+
export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText, truncateWords } from './utils/chat';
|
package/dist/types/index.d.ts
CHANGED
|
@@ -23,6 +23,16 @@ export interface WidgetConfig {
|
|
|
23
23
|
viewerUid?: string;
|
|
24
24
|
/** Display name for transfer notes (optional) */
|
|
25
25
|
viewerName?: string;
|
|
26
|
+
/** Privacy Policy URL (linked from chat consent banner) */
|
|
27
|
+
privacyPolicyUrl?: string;
|
|
28
|
+
/** Set false to hide the consent note above the composer */
|
|
29
|
+
showPrivacyNotice?: boolean;
|
|
30
|
+
/** Product brand (e.g. Ajaxter) */
|
|
31
|
+
brandName?: string;
|
|
32
|
+
/** Home promotion: “Take a Website Tour” link */
|
|
33
|
+
websiteTourUrl?: string;
|
|
34
|
+
/** Optional override for the lead line in the promotion card */
|
|
35
|
+
promotionLead?: string;
|
|
26
36
|
allowVoiceMessage: boolean;
|
|
27
37
|
allowAttachment: boolean;
|
|
28
38
|
allowEmoji: boolean;
|
package/dist/utils/chat.d.ts
CHANGED
|
@@ -9,5 +9,7 @@ export declare function formatTime(ts: string | Date): string;
|
|
|
9
9
|
export declare function formatDate(ts: string | Date): string;
|
|
10
10
|
/** Generate a plain-text transcript from messages */
|
|
11
11
|
export declare function generateTranscript(messages: ChatMessage[], peer: ChatUser, myName?: string): string;
|
|
12
|
+
/** Truncate to max words, append ellipsis (…) if shortened */
|
|
13
|
+
export declare function truncateWords(text: string, maxWords: number): string;
|
|
12
14
|
/** Trigger a file download in the browser */
|
|
13
15
|
export declare function downloadText(content: string, filename: string): void;
|
package/dist/utils/chat.js
CHANGED
|
@@ -50,6 +50,15 @@ export function generateTranscript(messages, peer, myName = 'Me') {
|
|
|
50
50
|
}).join('\n');
|
|
51
51
|
return header + body;
|
|
52
52
|
}
|
|
53
|
+
/** Truncate to max words, append ellipsis (…) if shortened */
|
|
54
|
+
export function truncateWords(text, maxWords) {
|
|
55
|
+
const w = text.trim().split(/\s+/).filter(Boolean);
|
|
56
|
+
if (w.length === 0)
|
|
57
|
+
return '';
|
|
58
|
+
if (w.length <= maxWords)
|
|
59
|
+
return w.join(' ');
|
|
60
|
+
return `${w.slice(0, maxWords).join(' ')}…`;
|
|
61
|
+
}
|
|
53
62
|
/** Trigger a file download in the browser */
|
|
54
63
|
export function downloadText(content, filename) {
|
|
55
64
|
const blob = new Blob([content], { type: 'text/plain' });
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function getPrivacyDismissedAt(widgetId: string): number | null;
|
|
2
|
+
/** After dismiss, banner stays hidden until one hour has passed */
|
|
3
|
+
export declare function shouldShowPrivacyNotice(widgetId: string): boolean;
|
|
4
|
+
export declare function dismissPrivacyNotice(widgetId: string): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
2
|
+
function key(widgetId) {
|
|
3
|
+
return `ajaxter_privacy_dismiss_${widgetId}`;
|
|
4
|
+
}
|
|
5
|
+
export function getPrivacyDismissedAt(widgetId) {
|
|
6
|
+
if (typeof window === 'undefined')
|
|
7
|
+
return null;
|
|
8
|
+
try {
|
|
9
|
+
const v = localStorage.getItem(key(widgetId));
|
|
10
|
+
if (v == null)
|
|
11
|
+
return null;
|
|
12
|
+
const n = parseInt(v, 10);
|
|
13
|
+
return Number.isFinite(n) ? n : null;
|
|
14
|
+
}
|
|
15
|
+
catch (_a) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** After dismiss, banner stays hidden until one hour has passed */
|
|
20
|
+
export function shouldShowPrivacyNotice(widgetId) {
|
|
21
|
+
const at = getPrivacyDismissedAt(widgetId);
|
|
22
|
+
if (at == null)
|
|
23
|
+
return true;
|
|
24
|
+
return Date.now() - at >= HOUR_MS;
|
|
25
|
+
}
|
|
26
|
+
export function dismissPrivacyNotice(widgetId) {
|
|
27
|
+
try {
|
|
28
|
+
localStorage.setItem(key(widgetId), String(Date.now()));
|
|
29
|
+
}
|
|
30
|
+
catch (_a) {
|
|
31
|
+
/* ignore quota / private mode */
|
|
32
|
+
}
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ajaxter-chat",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.8",
|
|
4
4
|
"description": "Drawer-based chat widget with support chat, tickets, WebRTC calling, voice messages, block list, and transcript download.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/public/chatData.json
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"branch": "Mumbai HQ",
|
|
16
16
|
"footerPoweredBy": "Answers by",
|
|
17
17
|
"supportPhone": "+919876543210",
|
|
18
|
+
"privacyPolicyUrl": "https://ajaxter.com/privacy",
|
|
19
|
+
"websiteTourUrl": "https://ajaxter.com",
|
|
20
|
+
"brandName": "Ajaxter",
|
|
18
21
|
"allowVoiceMessage": true,
|
|
19
22
|
"allowAttachment": true,
|
|
20
23
|
"allowEmoji": true,
|
|
@@ -136,31 +139,61 @@
|
|
|
136
139
|
{
|
|
137
140
|
"id": "TKT-0001",
|
|
138
141
|
"title": "Login page 500 error",
|
|
139
|
-
"description": "Getting a server error when logging in with valid credentials.",
|
|
140
|
-
"status": "
|
|
142
|
+
"description": "Getting a server error when logging in with valid credentials. Stack trace references timeout on auth service. Reproduced on Chrome and Safari. Need priority fix before release branch cut.",
|
|
143
|
+
"status": "open",
|
|
141
144
|
"priority": "high",
|
|
142
145
|
"createdAt": "2025-03-25T10:00:00Z",
|
|
143
|
-
"updatedAt": "2025-03-
|
|
146
|
+
"updatedAt": "2025-03-28T11:00:00Z",
|
|
144
147
|
"assignedTo": "dev_001"
|
|
145
148
|
},
|
|
146
149
|
{
|
|
147
150
|
"id": "TKT-0002",
|
|
148
151
|
"title": "Export to CSV not working",
|
|
149
|
-
"description": "The export button on the reports page does nothing when clicked.",
|
|
152
|
+
"description": "The export button on the reports page does nothing when clicked. Browser console shows CORS blocked response. Works on staging but fails in production behind new CDN rules.",
|
|
150
153
|
"status": "in-progress",
|
|
151
154
|
"priority": "medium",
|
|
152
155
|
"createdAt": "2025-03-26T14:30:00Z",
|
|
153
|
-
"updatedAt": "2025-03-
|
|
156
|
+
"updatedAt": "2025-03-28T10:30:00Z",
|
|
154
157
|
"assignedTo": "dev_002"
|
|
155
158
|
},
|
|
156
159
|
{
|
|
157
160
|
"id": "TKT-0003",
|
|
158
161
|
"title": "Add dark mode support",
|
|
159
|
-
"description": "Feature request to add a dark mode toggle for the dashboard.",
|
|
162
|
+
"description": "Feature request to add a dark mode toggle for the dashboard and persist preference per user account across sessions and devices.",
|
|
160
163
|
"status": "open",
|
|
161
164
|
"priority": "low",
|
|
162
165
|
"createdAt": "2025-03-27T07:00:00Z",
|
|
163
|
-
"updatedAt": "2025-03-
|
|
166
|
+
"updatedAt": "2025-03-28T09:15:00Z",
|
|
167
|
+
"assignedTo": null
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"id": "TKT-0004",
|
|
171
|
+
"title": "Webhook retries failing",
|
|
172
|
+
"description": "Outbound webhooks to partner endpoints return 429 after deploy. Need backoff review and idempotency keys. Partner asked for ETA this week.",
|
|
173
|
+
"status": "open",
|
|
174
|
+
"priority": "high",
|
|
175
|
+
"createdAt": "2025-03-28T06:00:00Z",
|
|
176
|
+
"updatedAt": "2025-03-28T09:00:00Z",
|
|
177
|
+
"assignedTo": "dev_001"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"id": "TKT-0005",
|
|
181
|
+
"title": "Mobile layout overflow",
|
|
182
|
+
"description": "On small screens the chat drawer overlaps the footer actions. QA attached screenshots. Repro on iPhone 14 and Pixel 8.",
|
|
183
|
+
"status": "in-progress",
|
|
184
|
+
"priority": "medium",
|
|
185
|
+
"createdAt": "2025-03-27T16:00:00Z",
|
|
186
|
+
"updatedAt": "2025-03-28T08:45:00Z",
|
|
187
|
+
"assignedTo": "dev_002"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"id": "TKT-0006",
|
|
191
|
+
"title": "Audit log export",
|
|
192
|
+
"description": "Compliance needs CSV export of admin actions for last 90 days with filters by actor and resource type including soft-deleted records.",
|
|
193
|
+
"status": "open",
|
|
194
|
+
"priority": "low",
|
|
195
|
+
"createdAt": "2025-03-28T05:00:00Z",
|
|
196
|
+
"updatedAt": "2025-03-28T07:30:00Z",
|
|
164
197
|
"assignedTo": null
|
|
165
198
|
}
|
|
166
199
|
],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
2
|
import { ChatMessage, ChatUser, WidgetConfig, UserListContext } from '../../types';
|
|
3
3
|
import { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText } from '../../utils/chat';
|
|
4
|
+
import { shouldShowPrivacyNotice, dismissPrivacyNotice } from '../../utils/privacyConsent';
|
|
4
5
|
import { EmojiPicker } from '../EmojiPicker';
|
|
5
6
|
import { SlideNavMenu } from '../SlideNavMenu';
|
|
6
7
|
|
|
@@ -38,6 +39,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
|
|
|
38
39
|
const [isRecording, setIsRecording] = useState(false);
|
|
39
40
|
const [recordSec, setRecordSec] = useState(0);
|
|
40
41
|
const [showConfirm, setShowConfirm] = useState<'report'|'block'|'pause'|null>(null);
|
|
42
|
+
const [showPrivacy, setShowPrivacy] = useState(false);
|
|
41
43
|
|
|
42
44
|
const endRef = useRef<HTMLDivElement>(null);
|
|
43
45
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
@@ -48,6 +50,25 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
|
|
|
48
50
|
|
|
49
51
|
useEffect(() => { endRef.current?.scrollIntoView({ behavior:'smooth' }); }, [messages]);
|
|
50
52
|
|
|
53
|
+
const privacyEnabled = config.showPrivacyNotice !== false;
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!privacyEnabled) return;
|
|
56
|
+
setShowPrivacy(shouldShowPrivacyNotice(config.id));
|
|
57
|
+
}, [config.id, privacyEnabled]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!privacyEnabled) return;
|
|
61
|
+
const id = window.setInterval(() => {
|
|
62
|
+
setShowPrivacy(shouldShowPrivacyNotice(config.id));
|
|
63
|
+
}, 60_000);
|
|
64
|
+
return () => window.clearInterval(id);
|
|
65
|
+
}, [config.id, privacyEnabled]);
|
|
66
|
+
|
|
67
|
+
const dismissPrivacy = useCallback(() => {
|
|
68
|
+
dismissPrivacyNotice(config.id);
|
|
69
|
+
setShowPrivacy(false);
|
|
70
|
+
}, [config.id]);
|
|
71
|
+
|
|
51
72
|
const handleSend = useCallback(() => {
|
|
52
73
|
if (!text.trim() || isPaused || isBlocked) return;
|
|
53
74
|
onSend(text.trim());
|
|
@@ -244,6 +265,60 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
|
|
|
244
265
|
{/* Composer — reference layout */}
|
|
245
266
|
<div style={{ borderTop:'1px solid #eef0f5', padding:'10px 12px 8px', background:'#fff', flexShrink:0, position:'relative' }}>
|
|
246
267
|
|
|
268
|
+
{privacyEnabled && showPrivacy && (
|
|
269
|
+
<div
|
|
270
|
+
style={{
|
|
271
|
+
position: 'relative',
|
|
272
|
+
marginBottom: 10,
|
|
273
|
+
padding: '12px 36px 12px 12px',
|
|
274
|
+
borderRadius: 12,
|
|
275
|
+
background: '#fff',
|
|
276
|
+
border: '1px solid #e8ecf1',
|
|
277
|
+
boxShadow: '0 1px 4px rgba(15,23,42,0.06)',
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
<button
|
|
281
|
+
type="button"
|
|
282
|
+
aria-label="Dismiss privacy notice"
|
|
283
|
+
onClick={dismissPrivacy}
|
|
284
|
+
style={{
|
|
285
|
+
position: 'absolute',
|
|
286
|
+
top: 8,
|
|
287
|
+
right: 8,
|
|
288
|
+
width: 26,
|
|
289
|
+
height: 26,
|
|
290
|
+
borderRadius: '50%',
|
|
291
|
+
border: 'none',
|
|
292
|
+
background: '#f1f5f9',
|
|
293
|
+
cursor: 'pointer',
|
|
294
|
+
display: 'flex',
|
|
295
|
+
alignItems: 'center',
|
|
296
|
+
justifyContent: 'center',
|
|
297
|
+
padding: 0,
|
|
298
|
+
lineHeight: 1,
|
|
299
|
+
}}
|
|
300
|
+
>
|
|
301
|
+
<span style={{ fontSize: 14, color: '#475569', fontWeight: 700 }}>×</span>
|
|
302
|
+
</button>
|
|
303
|
+
<p style={{ margin: 0, fontSize: 12, color: '#64748b', lineHeight: 1.55 }}>
|
|
304
|
+
By chatting here, you agree we and authorized partners may process, monitor, and record this chat and your data in line with{' '}
|
|
305
|
+
{config.privacyPolicyUrl ? (
|
|
306
|
+
<a
|
|
307
|
+
href={config.privacyPolicyUrl}
|
|
308
|
+
target="_blank"
|
|
309
|
+
rel="noopener noreferrer"
|
|
310
|
+
style={{ color: config.primaryColor, textDecoration: 'underline', fontWeight: 600 }}
|
|
311
|
+
>
|
|
312
|
+
Privacy Policy
|
|
313
|
+
</a>
|
|
314
|
+
) : (
|
|
315
|
+
<span style={{ textDecoration: 'underline', fontWeight: 600 }}>Privacy Policy</span>
|
|
316
|
+
)}
|
|
317
|
+
.
|
|
318
|
+
</p>
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
|
|
247
322
|
{showEmoji && config.allowEmoji && (
|
|
248
323
|
<EmojiPicker
|
|
249
324
|
primaryColor={config.primaryColor}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect, useCallback
|
|
3
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
4
4
|
import { ChatWidgetProps, BottomTab, Screen, UserListContext, ChatUser, Ticket, RecentChat, ChatMessage } from '../types';
|
|
5
5
|
import { loadLocalConfig } from '../config';
|
|
6
6
|
import { mergeTheme } from '../utils/theme';
|
|
@@ -18,10 +18,6 @@ import { CallScreen } from './CallScreen';
|
|
|
18
18
|
import { MaintenanceView } from './MaintenanceView';
|
|
19
19
|
import { BottomTabs } from './Tabs/BottomTabs';
|
|
20
20
|
|
|
21
|
-
/* ─── Drawer width ─────────────────────────────────────────────────────────── */
|
|
22
|
-
const DRAWER_W_NORMAL = 380;
|
|
23
|
-
const DRAWER_W_MAX = 480;
|
|
24
|
-
|
|
25
21
|
export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) => {
|
|
26
22
|
/* SSR guard */
|
|
27
23
|
const [mounted, setMounted] = useState(false);
|
|
@@ -41,7 +37,6 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
41
37
|
|
|
42
38
|
/* Drawer open state */
|
|
43
39
|
const [isOpen, setIsOpen] = useState(false);
|
|
44
|
-
const [isMaximized, setIsMaximized] = useState(false);
|
|
45
40
|
const [closing, setClosing] = useState(false); // for slide-out animation
|
|
46
41
|
|
|
47
42
|
/* Navigation */
|
|
@@ -191,7 +186,6 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
191
186
|
|
|
192
187
|
/* ── Derived ─────────────────────────────────────────────────────────── */
|
|
193
188
|
const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
|
|
194
|
-
const drawerW = isMaximized ? DRAWER_W_MAX : DRAWER_W_NORMAL;
|
|
195
189
|
const widgetConfig = data?.widget;
|
|
196
190
|
const primaryColor = theme.primaryColor;
|
|
197
191
|
|
|
@@ -235,9 +229,21 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
235
229
|
? { left: 24, right: 'auto' }
|
|
236
230
|
: { right: 24, left: 'auto' };
|
|
237
231
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
232
|
+
/* No radius on top-left / bottom-left; left-docked panel keeps inner TR/BR curve */
|
|
233
|
+
const drawerPosStyle: React.CSSProperties =
|
|
234
|
+
theme.buttonPosition === 'bottom-left'
|
|
235
|
+
? {
|
|
236
|
+
left: 0,
|
|
237
|
+
borderTopLeftRadius: 0,
|
|
238
|
+
borderBottomLeftRadius: 0,
|
|
239
|
+
borderTopRightRadius: 16,
|
|
240
|
+
borderBottomRightRadius: 16,
|
|
241
|
+
}
|
|
242
|
+
: {
|
|
243
|
+
right: 0,
|
|
244
|
+
borderTopLeftRadius: 0,
|
|
245
|
+
borderBottomLeftRadius: 0,
|
|
246
|
+
};
|
|
241
247
|
|
|
242
248
|
/* ── Don't render until mounted (SSR safe) ──────────────────────────── */
|
|
243
249
|
if (!mounted) return null;
|
|
@@ -265,6 +271,15 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
265
271
|
|
|
266
272
|
.cw-drawer-enter { animation: ${theme.buttonPosition === 'bottom-left' ? 'cw-slideInLeft' : 'cw-slideInRight'} 0.32s cubic-bezier(0.22,1,0.36,1) both; }
|
|
267
273
|
.cw-drawer-exit { animation: ${theme.buttonPosition === 'bottom-left' ? 'cw-slideOutLeft' : 'cw-slideOutRight'} 0.28s cubic-bezier(0.55,0,1,0.45) both; }
|
|
274
|
+
|
|
275
|
+
.cw-drawer-panel {
|
|
276
|
+
width: 30%;
|
|
277
|
+
max-width: 100vw;
|
|
278
|
+
min-width: 0;
|
|
279
|
+
}
|
|
280
|
+
@media (max-width: 1024px) {
|
|
281
|
+
.cw-drawer-panel { width: 100%; }
|
|
282
|
+
}
|
|
268
283
|
`}</style>
|
|
269
284
|
|
|
270
285
|
{/* ── Floating Button ── */}
|
|
@@ -319,15 +334,13 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
319
334
|
{/* ── Drawer / Slider ── */}
|
|
320
335
|
{isOpen && (
|
|
321
336
|
<div
|
|
322
|
-
className={`cw-root ${closing ? 'cw-drawer-exit' : 'cw-drawer-enter'}`}
|
|
337
|
+
className={`cw-root cw-drawer-panel ${closing ? 'cw-drawer-exit' : 'cw-drawer-enter'}`}
|
|
323
338
|
style={{
|
|
324
339
|
position: 'fixed',
|
|
325
340
|
top: 0,
|
|
326
341
|
bottom: 0,
|
|
327
342
|
...drawerPosStyle,
|
|
328
343
|
zIndex: 9998,
|
|
329
|
-
width: drawerW,
|
|
330
|
-
maxWidth: '100vw',
|
|
331
344
|
backgroundColor: '#fff',
|
|
332
345
|
boxShadow: theme.buttonPosition === 'bottom-left'
|
|
333
346
|
? '4px 0 40px rgba(0,0,0,0.18)'
|
|
@@ -335,7 +348,6 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
335
348
|
display: 'flex',
|
|
336
349
|
flexDirection: 'column',
|
|
337
350
|
overflow: 'hidden',
|
|
338
|
-
transition: 'width 0.28s ease',
|
|
339
351
|
}}
|
|
340
352
|
>
|
|
341
353
|
{/* ── Loading state ── */}
|
|
@@ -373,12 +385,6 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
373
385
|
left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
|
|
374
386
|
zIndex: 20, display: 'flex', gap: 6,
|
|
375
387
|
}}>
|
|
376
|
-
<CornerBtn onClick={() => setIsMaximized(m => !m)} title={isMaximized ? 'Minimize' : 'Maximize'}>
|
|
377
|
-
{isMaximized
|
|
378
|
-
? <svg width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M8 3v5H3M21 8h-5V3M3 16h5v5M16 21v-5h5" stroke="#fff" strokeWidth="2.2" strokeLinecap="round"/></svg>
|
|
379
|
-
: <svg width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M8 3H5a2 2 0 00-2 2v3M21 8V5a2 2 0 00-2-2h-3M3 16v3a2 2 0 002 2h3M16 21h3a2 2 0 002-2v-3" stroke="#fff" strokeWidth="2.2" strokeLinecap="round"/></svg>
|
|
380
|
-
}
|
|
381
|
-
</CornerBtn>
|
|
382
388
|
<CornerBtn onClick={closeDrawer} title="Close">
|
|
383
389
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none">
|
|
384
390
|
<path d="M18 6L6 18M6 6l12 12" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
2
|
import { WidgetConfig, UserListContext, Ticket } from '../../types';
|
|
3
3
|
import { SlideNavMenu } from '../SlideNavMenu';
|
|
4
|
+
import { truncateWords } from '../../utils/chat';
|
|
4
5
|
|
|
5
6
|
interface HomeScreenProps {
|
|
6
7
|
config: WidgetConfig;
|
|
@@ -14,7 +15,20 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
|
|
|
14
15
|
const showChat = config.chatType === 'CHAT' || config.chatType === 'BOTH';
|
|
15
16
|
const viewerIsDev = config.viewerType === 'developer';
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
+
const pendingTickets = useMemo(
|
|
19
|
+
() =>
|
|
20
|
+
tickets
|
|
21
|
+
.filter(t => t.status === 'open' || t.status === 'in-progress')
|
|
22
|
+
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
|
23
|
+
.slice(0, 5),
|
|
24
|
+
[tickets]
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const brand = config.brandName?.trim() || 'Ajaxter';
|
|
28
|
+
const promotionLead =
|
|
29
|
+
config.promotionLead?.trim() ||
|
|
30
|
+
'Need specialized help? Our teams are ready to assist you with any questions.';
|
|
31
|
+
const tourUrl = config.websiteTourUrl?.trim();
|
|
18
32
|
|
|
19
33
|
const handleCallUs = () => {
|
|
20
34
|
const raw = config.supportPhone?.trim();
|
|
@@ -91,8 +105,8 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
|
|
|
91
105
|
{/* Continue Conversations */}
|
|
92
106
|
<h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>Continue Conversations</h2>
|
|
93
107
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 }}>
|
|
94
|
-
{
|
|
95
|
-
|
|
108
|
+
{pendingTickets.length > 0 ? (
|
|
109
|
+
pendingTickets.map(t => (
|
|
96
110
|
<button
|
|
97
111
|
key={t.id}
|
|
98
112
|
type="button"
|
|
@@ -105,13 +119,14 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
|
|
|
105
119
|
border: 'none',
|
|
106
120
|
background: '#e0f2fe',
|
|
107
121
|
color: '#0369a1',
|
|
108
|
-
fontSize: 14,
|
|
109
|
-
fontWeight: 600,
|
|
110
122
|
cursor: 'pointer',
|
|
111
123
|
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
|
|
112
124
|
}}
|
|
113
125
|
>
|
|
114
|
-
{t.title}
|
|
126
|
+
<div style={{ fontSize: 14, fontWeight: 700, color: '#0c4a6e', marginBottom: 6 }}>{t.title}</div>
|
|
127
|
+
<div style={{ fontSize: 12, fontWeight: 500, color: '#64748b', lineHeight: 1.45 }}>
|
|
128
|
+
{truncateWords(t.description, 50)}
|
|
129
|
+
</div>
|
|
115
130
|
</button>
|
|
116
131
|
))
|
|
117
132
|
) : (
|
|
@@ -233,36 +248,65 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
|
|
|
233
248
|
}}
|
|
234
249
|
>
|
|
235
250
|
<div style={{ position: 'absolute', top: -20, right: -20, width: 100, height: 100, borderRadius: '50%', background: 'rgba(255,255,255,0.35)' }} />
|
|
236
|
-
<p style={{ margin: '0 0
|
|
237
|
-
|
|
251
|
+
<p style={{ margin: '0 0 10px', fontSize: 15, fontWeight: 700, color: '#4c1d95', lineHeight: 1.45, position: 'relative' }}>
|
|
252
|
+
{promotionLead}
|
|
238
253
|
</p>
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
254
|
+
<p style={{ margin: '0 0 16px', fontSize: 13, fontWeight: 500, color: '#5b21b6', lineHeight: 1.5, position: 'relative' }}>
|
|
255
|
+
<strong style={{ fontWeight: 800 }}>{brand}</strong> — embedded chat for your workspace.{' '}
|
|
256
|
+
<span style={{ whiteSpace: 'nowrap' }}>Free for users.</span> 24×7 availability. Dedicated workspace experience.
|
|
257
|
+
</p>
|
|
258
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, position: 'relative' }}>
|
|
259
|
+
{tourUrl && (
|
|
260
|
+
<a
|
|
261
|
+
href={tourUrl}
|
|
262
|
+
target="_blank"
|
|
263
|
+
rel="noopener noreferrer"
|
|
264
|
+
style={{
|
|
265
|
+
display: 'inline-flex',
|
|
266
|
+
alignItems: 'center',
|
|
267
|
+
justifyContent: 'center',
|
|
268
|
+
gap: 6,
|
|
269
|
+
padding: '10px 16px',
|
|
270
|
+
borderRadius: 12,
|
|
271
|
+
border: 'none',
|
|
272
|
+
background: '#fff',
|
|
273
|
+
color: '#5b21b6',
|
|
274
|
+
fontSize: 13,
|
|
275
|
+
fontWeight: 700,
|
|
276
|
+
textDecoration: 'none',
|
|
277
|
+
boxShadow: '0 2px 8px rgba(91,33,182,0.15)',
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
Take a Website Tour
|
|
281
|
+
</a>
|
|
282
|
+
)}
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
onClick={handleCallUs}
|
|
286
|
+
disabled={!config.supportPhone}
|
|
287
|
+
style={{
|
|
288
|
+
display: 'inline-flex',
|
|
289
|
+
alignItems: 'center',
|
|
290
|
+
gap: 8,
|
|
291
|
+
padding: '10px 18px',
|
|
292
|
+
borderRadius: 12,
|
|
293
|
+
border: 'none',
|
|
294
|
+
background: config.supportPhone ? config.primaryColor : '#94a3b8',
|
|
295
|
+
color: '#fff',
|
|
296
|
+
fontSize: 14,
|
|
297
|
+
fontWeight: 700,
|
|
298
|
+
cursor: config.supportPhone ? 'pointer' : 'not-allowed',
|
|
299
|
+
}}
|
|
300
|
+
>
|
|
301
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
|
302
|
+
<path
|
|
303
|
+
d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.68A2 2 0 012 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 14.92v2z"
|
|
304
|
+
fill="#fff"
|
|
305
|
+
/>
|
|
306
|
+
</svg>
|
|
307
|
+
Call Us
|
|
308
|
+
</button>
|
|
309
|
+
</div>
|
|
266
310
|
</div>
|
|
267
311
|
</div>
|
|
268
312
|
</div>
|
package/src/index.ts
CHANGED
|
@@ -15,9 +15,10 @@ export { useChat } from './hooks/useChat';
|
|
|
15
15
|
export { useWebRTC } from './hooks/useWebRTC';
|
|
16
16
|
export { useRemoteConfig } from './hooks/useRemoteConfig';
|
|
17
17
|
|
|
18
|
+
export { shouldShowPrivacyNotice, dismissPrivacyNotice, getPrivacyDismissedAt } from './utils/privacyConsent';
|
|
18
19
|
export { loadLocalConfig, fetchRemoteChatData } from './config';
|
|
19
20
|
export { mergeTheme, darken } from './utils/theme';
|
|
20
|
-
export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText } from './utils/chat';
|
|
21
|
+
export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText, truncateWords } from './utils/chat';
|
|
21
22
|
|
|
22
23
|
export type {
|
|
23
24
|
ChatWidgetProps, ChatWidgetTheme,
|
package/src/types/index.ts
CHANGED
|
@@ -24,6 +24,16 @@ export interface WidgetConfig {
|
|
|
24
24
|
viewerUid?: string;
|
|
25
25
|
/** Display name for transfer notes (optional) */
|
|
26
26
|
viewerName?: string;
|
|
27
|
+
/** Privacy Policy URL (linked from chat consent banner) */
|
|
28
|
+
privacyPolicyUrl?: string;
|
|
29
|
+
/** Set false to hide the consent note above the composer */
|
|
30
|
+
showPrivacyNotice?: boolean;
|
|
31
|
+
/** Product brand (e.g. Ajaxter) */
|
|
32
|
+
brandName?: string;
|
|
33
|
+
/** Home promotion: “Take a Website Tour” link */
|
|
34
|
+
websiteTourUrl?: string;
|
|
35
|
+
/** Optional override for the lead line in the promotion card */
|
|
36
|
+
promotionLead?: string;
|
|
27
37
|
allowVoiceMessage: boolean;
|
|
28
38
|
allowAttachment: boolean;
|
|
29
39
|
allowEmoji: boolean;
|
package/src/utils/chat.ts
CHANGED
|
@@ -58,6 +58,14 @@ export function generateTranscript(
|
|
|
58
58
|
return header + body;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** Truncate to max words, append ellipsis (…) if shortened */
|
|
62
|
+
export function truncateWords(text: string, maxWords: number): string {
|
|
63
|
+
const w = text.trim().split(/\s+/).filter(Boolean);
|
|
64
|
+
if (w.length === 0) return '';
|
|
65
|
+
if (w.length <= maxWords) return w.join(' ');
|
|
66
|
+
return `${w.slice(0, maxWords).join(' ')}…`;
|
|
67
|
+
}
|
|
68
|
+
|
|
61
69
|
/** Trigger a file download in the browser */
|
|
62
70
|
export function downloadText(content: string, filename: string) {
|
|
63
71
|
const blob = new Blob([content], { type: 'text/plain' });
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
2
|
+
|
|
3
|
+
function key(widgetId: string): string {
|
|
4
|
+
return `ajaxter_privacy_dismiss_${widgetId}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getPrivacyDismissedAt(widgetId: string): number | null {
|
|
8
|
+
if (typeof window === 'undefined') return null;
|
|
9
|
+
try {
|
|
10
|
+
const v = localStorage.getItem(key(widgetId));
|
|
11
|
+
if (v == null) return null;
|
|
12
|
+
const n = parseInt(v, 10);
|
|
13
|
+
return Number.isFinite(n) ? n : null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** After dismiss, banner stays hidden until one hour has passed */
|
|
20
|
+
export function shouldShowPrivacyNotice(widgetId: string): boolean {
|
|
21
|
+
const at = getPrivacyDismissedAt(widgetId);
|
|
22
|
+
if (at == null) return true;
|
|
23
|
+
return Date.now() - at >= HOUR_MS;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function dismissPrivacyNotice(widgetId: string): void {
|
|
27
|
+
try {
|
|
28
|
+
localStorage.setItem(key(widgetId), String(Date.now()));
|
|
29
|
+
} catch {
|
|
30
|
+
/* ignore quota / private mode */
|
|
31
|
+
}
|
|
32
|
+
}
|