ajaxter-chat 2.0.1 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -128
- package/dist/components/BlockList/index.d.ts +10 -0
- package/dist/components/BlockList/index.js +33 -0
- package/dist/components/CallScreen/index.d.ts +13 -0
- package/dist/components/CallScreen/index.js +48 -0
- package/dist/components/ChatScreen/index.d.ts +10 -3
- package/dist/components/ChatScreen/index.js +142 -57
- package/dist/components/ChatWidget.js +192 -98
- package/dist/components/EmojiPicker/index.d.ts +8 -0
- package/dist/components/EmojiPicker/index.js +18 -0
- package/dist/components/HomeScreen/index.d.ts +2 -3
- package/dist/components/HomeScreen/index.js +25 -41
- package/dist/components/MaintenanceView/index.d.ts +0 -1
- package/dist/components/MaintenanceView/index.js +4 -6
- package/dist/components/RecentChatsScreen/index.d.ts +4 -3
- package/dist/components/RecentChatsScreen/index.js +7 -37
- package/dist/components/Tabs/BottomTabs.d.ts +1 -1
- package/dist/components/Tabs/BottomTabs.js +25 -20
- package/dist/components/TicketScreen/index.d.ts +3 -3
- package/dist/components/TicketScreen/index.js +39 -56
- package/dist/components/UserListScreen/index.d.ts +2 -4
- package/dist/components/UserListScreen/index.js +33 -62
- package/dist/config/index.d.ts +7 -3
- package/dist/config/index.js +28 -25
- package/dist/hooks/useChat.d.ts +8 -3
- package/dist/hooks/useChat.js +22 -18
- package/dist/hooks/useRemoteConfig.d.ts +6 -0
- package/dist/hooks/useRemoteConfig.js +26 -0
- package/dist/hooks/useWebRTC.d.ts +11 -0
- package/dist/hooks/useWebRTC.js +112 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +8 -4
- package/dist/types/index.d.ts +62 -21
- package/dist/utils/chat.d.ts +13 -0
- package/dist/utils/chat.js +62 -0
- package/dist/utils/theme.d.ts +3 -1
- package/dist/utils/theme.js +14 -7
- package/package.json +4 -4
- package/public/chatData.json +162 -0
- package/src/components/BlockList/index.tsx +94 -0
- package/src/components/CallScreen/index.tsx +144 -0
- package/src/components/ChatScreen/index.tsx +403 -139
- package/src/components/ChatWidget.tsx +394 -250
- package/src/components/EmojiPicker/index.tsx +48 -0
- package/src/components/HomeScreen/index.tsx +58 -82
- package/src/components/MaintenanceView/index.tsx +6 -9
- package/src/components/RecentChatsScreen/index.tsx +51 -96
- package/src/components/Tabs/BottomTabs.tsx +45 -37
- package/src/components/TicketScreen/index.tsx +87 -133
- package/src/components/UserListScreen/index.tsx +75 -153
- package/src/config/index.ts +32 -26
- package/src/hooks/useChat.ts +31 -14
- package/src/hooks/useRemoteConfig.ts +26 -0
- package/src/hooks/useWebRTC.ts +130 -0
- package/src/index.ts +26 -15
- package/src/types/index.ts +85 -40
- package/src/utils/chat.ts +70 -0
- package/src/utils/theme.ts +18 -7
- package/dist/hooks/useUsers.d.ts +0 -7
- package/dist/hooks/useUsers.js +0 -26
- package/dist/services/userService.d.ts +0 -2
- package/dist/services/userService.js +0 -9
- package/dist/src/components/ChatScreen/index.d.ts +0 -12
- package/dist/src/components/ChatScreen/index.js +0 -83
- package/dist/src/components/ChatWidget.d.ts +0 -4
- package/dist/src/components/ChatWidget.js +0 -141
- package/dist/src/components/HomeScreen/index.d.ts +0 -9
- package/dist/src/components/HomeScreen/index.js +0 -71
- package/dist/src/components/MaintenanceView/index.d.ts +0 -7
- package/dist/src/components/MaintenanceView/index.js +0 -16
- package/dist/src/components/RecentChatsScreen/index.d.ts +0 -16
- package/dist/src/components/RecentChatsScreen/index.js +0 -38
- package/dist/src/components/Tabs/BottomTabs.d.ts +0 -10
- package/dist/src/components/Tabs/BottomTabs.js +0 -29
- package/dist/src/components/TicketScreen/index.d.ts +0 -9
- package/dist/src/components/TicketScreen/index.js +0 -71
- package/dist/src/components/UserListScreen/index.d.ts +0 -13
- package/dist/src/components/UserListScreen/index.js +0 -64
- package/dist/src/config/index.d.ts +0 -3
- package/dist/src/config/index.js +0 -38
- package/dist/src/hooks/useChat.d.ts +0 -8
- package/dist/src/hooks/useChat.js +0 -26
- package/dist/src/hooks/useUsers.d.ts +0 -7
- package/dist/src/hooks/useUsers.js +0 -26
- package/dist/src/index.d.ts +0 -14
- package/dist/src/index.js +0 -13
- package/dist/src/services/userService.d.ts +0 -2
- package/dist/src/services/userService.js +0 -9
- package/dist/src/types/index.d.ts +0 -59
- package/dist/src/types/index.js +0 -1
- package/dist/src/utils/theme.d.ts +0 -3
- package/dist/src/utils/theme.js +0 -13
- package/src/hooks/useUsers.ts +0 -27
- package/src/services/userService.ts +0 -9
|
@@ -1,170 +1,124 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import { mergeTheme } from '../../utils/theme';
|
|
2
|
+
import { Ticket, WidgetConfig } from '../../types';
|
|
4
3
|
|
|
5
4
|
interface TicketScreenProps {
|
|
6
|
-
tickets:
|
|
7
|
-
|
|
8
|
-
onRaiseTicket: (title: string,
|
|
5
|
+
tickets: Ticket[];
|
|
6
|
+
config: WidgetConfig;
|
|
7
|
+
onRaiseTicket: (title: string, desc: string, priority: Ticket['priority']) => void;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets,
|
|
12
|
-
const
|
|
13
|
-
const [
|
|
14
|
-
const [
|
|
15
|
-
const [
|
|
10
|
+
export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets, config, onRaiseTicket }) => {
|
|
11
|
+
const [showForm, setShowForm] = useState(false);
|
|
12
|
+
const [title, setTitle] = useState('');
|
|
13
|
+
const [desc, setDesc] = useState('');
|
|
14
|
+
const [priority, setPriority] = useState<Ticket['priority']>('medium');
|
|
16
15
|
|
|
17
16
|
const handleSubmit = () => {
|
|
18
17
|
if (!title.trim()) return;
|
|
19
|
-
onRaiseTicket(title.trim(), desc.trim());
|
|
20
|
-
setTitle(''); setDesc(''); setShowForm(false);
|
|
18
|
+
onRaiseTicket(title.trim(), desc.trim(), priority);
|
|
19
|
+
setTitle(''); setDesc(''); setPriority('medium'); setShowForm(false);
|
|
21
20
|
};
|
|
22
21
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
'in-progress': { label:
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const sm: Record<Ticket['status'], { label: string; bg: string; color: string }> = {
|
|
23
|
+
open: { label:'Open', bg:`${config.primaryColor}14`, color: config.primaryColor },
|
|
24
|
+
'in-progress': { label:'In Progress', bg:'#fef3c7', color:'#d97706' },
|
|
25
|
+
resolved: { label:'Resolved', bg:'#f0fdf4', color:'#16a34a' },
|
|
26
|
+
closed: { label:'Closed', bg:'#f3f4f6', color:'#6b7280' },
|
|
28
27
|
};
|
|
29
28
|
|
|
30
|
-
const
|
|
31
|
-
low: { label:
|
|
32
|
-
medium: { label:
|
|
33
|
-
high: { label:
|
|
29
|
+
const pm: Record<Ticket['priority'], { label: string; color: string }> = {
|
|
30
|
+
low: { label:'Low', color:'#6b7280' },
|
|
31
|
+
medium: { label:'Medium', color:'#d97706' },
|
|
32
|
+
high: { label:'High', color:'#ef4444' },
|
|
34
33
|
};
|
|
35
34
|
|
|
36
35
|
return (
|
|
37
|
-
<div style={{ display:
|
|
36
|
+
<div style={{ display:'flex', flexDirection:'column', height:'100%' }}>
|
|
38
37
|
{/* Header */}
|
|
39
|
-
<div style={{
|
|
40
|
-
|
|
38
|
+
<div style={{
|
|
39
|
+
background:`linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
|
|
40
|
+
padding:'18px 18px 22px', flexShrink:0, position:'relative',
|
|
41
|
+
}}>
|
|
42
|
+
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start' }}>
|
|
41
43
|
<div>
|
|
42
|
-
<h2 style={{ margin:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<p style={{ margin: '4px 0 0', fontSize: '13px', color: 'rgba(255,255,255,0.8)', fontFamily: t.fontFamily }}>
|
|
46
|
-
{tickets.length} ticket{tickets.length !== 1 ? 's' : ''} raised
|
|
44
|
+
<h2 style={{ margin:0, fontSize:20, fontWeight:800, color:'#fff', letterSpacing:'-0.02em' }}>Tickets</h2>
|
|
45
|
+
<p style={{ margin:'3px 0 0', fontSize:12, color:'rgba(255,255,255,0.8)' }}>
|
|
46
|
+
{tickets.length} ticket{tickets.length!==1?'s':''} raised
|
|
47
47
|
</p>
|
|
48
48
|
</div>
|
|
49
|
-
<button
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}}
|
|
56
|
-
>
|
|
57
|
-
{showForm ? '✕ Cancel' : '+ New Ticket'}
|
|
49
|
+
<button onClick={() => setShowForm(v => !v)} style={{
|
|
50
|
+
background:'rgba(255,255,255,0.22)', border:'none', borderRadius:20,
|
|
51
|
+
padding:'7px 14px', color:'#fff', fontWeight:700, fontSize:13,
|
|
52
|
+
cursor:'pointer', display:'flex', alignItems:'center', gap:5,
|
|
53
|
+
}}>
|
|
54
|
+
{showForm ? '✕ Cancel' : '+ New'}
|
|
58
55
|
</button>
|
|
59
56
|
</div>
|
|
60
57
|
</div>
|
|
61
58
|
|
|
62
59
|
{/* New Ticket Form */}
|
|
63
60
|
{showForm && (
|
|
64
|
-
<div style={{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
backgroundColor: '#fafcff',
|
|
68
|
-
animation: 'cw-fadeUp 0.2s ease',
|
|
69
|
-
flexShrink: 0,
|
|
70
|
-
}}>
|
|
71
|
-
<input
|
|
72
|
-
placeholder="Ticket title *"
|
|
73
|
-
value={title}
|
|
74
|
-
onChange={e => setTitle(e.target.value)}
|
|
75
|
-
style={{
|
|
76
|
-
width: '100%', padding: '10px 14px', borderRadius: 10,
|
|
77
|
-
border: '1.5px solid #e2e8f0', outline: 'none',
|
|
78
|
-
fontFamily: t.fontFamily, fontSize: '14px', color: '#1a2332',
|
|
79
|
-
marginBottom: 10, boxSizing: 'border-box',
|
|
80
|
-
}}
|
|
81
|
-
onFocus={e => (e.target.style.borderColor = t.primaryColor)}
|
|
82
|
-
onBlur={e => (e.target.style.borderColor = '#e2e8f0')}
|
|
61
|
+
<div style={{ padding:'16px', borderBottom:'1px solid #eef0f5', background:'#fafcff', flexShrink:0, animation:'cw-fadeUp 0.2s ease' }}>
|
|
62
|
+
<input placeholder="Title *" value={title} onChange={e => setTitle(e.target.value)}
|
|
63
|
+
style={inputStyle(config.primaryColor)} onFocus={e=>(e.target.style.borderColor=config.primaryColor)} onBlur={e=>(e.target.style.borderColor='#e5e7eb')}
|
|
83
64
|
/>
|
|
84
|
-
<textarea
|
|
85
|
-
|
|
86
|
-
value={desc}
|
|
87
|
-
onChange={e => setDesc(e.target.value)}
|
|
88
|
-
rows={3}
|
|
89
|
-
style={{
|
|
90
|
-
width: '100%', padding: '10px 14px', borderRadius: 10,
|
|
91
|
-
border: '1.5px solid #e2e8f0', outline: 'none', resize: 'none',
|
|
92
|
-
fontFamily: t.fontFamily, fontSize: '14px', color: '#1a2332',
|
|
93
|
-
marginBottom: 10, boxSizing: 'border-box',
|
|
94
|
-
}}
|
|
95
|
-
onFocus={e => (e.target.style.borderColor = t.primaryColor)}
|
|
96
|
-
onBlur={e => (e.target.style.borderColor = '#e2e8f0')}
|
|
65
|
+
<textarea placeholder="Describe the issue…" value={desc} onChange={e => setDesc(e.target.value)} rows={3}
|
|
66
|
+
style={{ ...inputStyle(config.primaryColor), resize:'none', marginTop:8 }} onFocus={e=>(e.target.style.borderColor=config.primaryColor)} onBlur={e=>(e.target.style.borderColor='#e5e7eb')}
|
|
97
67
|
/>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
68
|
+
{/* Priority */}
|
|
69
|
+
<div style={{ display:'flex', gap:8, marginTop:8, marginBottom:12 }}>
|
|
70
|
+
{(['low','medium','high'] as Ticket['priority'][]).map(p => (
|
|
71
|
+
<button key={p} onClick={() => setPriority(p)} style={{
|
|
72
|
+
flex:1, padding:'6px', border:`1.5px solid ${priority===p ? pm[p].color : '#e5e7eb'}`,
|
|
73
|
+
borderRadius:8, background: priority===p ? pm[p].color+'15' : '#fff',
|
|
74
|
+
color: pm[p].color, fontWeight:700, fontSize:12, cursor:'pointer', textTransform:'capitalize',
|
|
75
|
+
transition:'all 0.15s',
|
|
76
|
+
}}>{pm[p].label}</button>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
<button onClick={handleSubmit} disabled={!title.trim()} style={{
|
|
80
|
+
width:'100%', padding:'10px', borderRadius:10, border:'none',
|
|
81
|
+
background: title.trim() ? config.primaryColor : '#e5e7eb',
|
|
82
|
+
color: title.trim() ? '#fff' : '#9ca3af',
|
|
83
|
+
fontWeight:700, fontSize:14, cursor: title.trim() ? 'pointer' : 'not-allowed',
|
|
84
|
+
}}>Submit Ticket</button>
|
|
112
85
|
</div>
|
|
113
86
|
)}
|
|
114
87
|
|
|
115
|
-
{/*
|
|
116
|
-
<div style={{ flex:
|
|
88
|
+
{/* List */}
|
|
89
|
+
<div style={{ flex:1, overflowY:'auto' }}>
|
|
117
90
|
{tickets.length === 0 ? (
|
|
118
|
-
<div style={{ padding:
|
|
119
|
-
<div style={{ fontSize:
|
|
120
|
-
<div style={{ fontWeight:
|
|
121
|
-
<div style={{ fontSize:
|
|
122
|
-
|
|
91
|
+
<div style={{ padding:'50px 24px', textAlign:'center' }}>
|
|
92
|
+
<div style={{ fontSize:36, marginBottom:10 }}>🎫</div>
|
|
93
|
+
<div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>No tickets yet</div>
|
|
94
|
+
<div style={{ fontSize:13, color:'#7b8fa1' }}>Raise a ticket for major issues</div>
|
|
95
|
+
</div>
|
|
96
|
+
) : tickets.map((t, i) => (
|
|
97
|
+
<div key={t.id} style={{ padding:'14px 16px', borderBottom:'1px solid #f0f2f5', animation:`cw-fadeUp 0.3s ease both`, animationDelay:`${i*0.05}s` }}>
|
|
98
|
+
<div style={{ display:'flex', alignItems:'flex-start', justifyContent:'space-between', marginBottom:5 }}>
|
|
99
|
+
<span style={{ fontWeight:700, fontSize:14, color:'#1a2332', flex:1, paddingRight:10 }}>{t.title}</span>
|
|
100
|
+
<span style={{ fontSize:10, fontWeight:700, padding:'3px 9px', borderRadius:20, backgroundColor:sm[t.status].bg, color:sm[t.status].color, whiteSpace:'nowrap', textTransform:'uppercase', letterSpacing:'0.04em', flexShrink:0 }}>
|
|
101
|
+
{sm[t.status].label}
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
{t.description && <p style={{ margin:'0 0 7px', fontSize:13, color:'#7b8fa1', lineHeight:1.5 }}>{t.description}</p>}
|
|
105
|
+
<div style={{ display:'flex', gap:10, fontSize:11, color:'#b0bec5' }}>
|
|
106
|
+
<span style={{ color:pm[t.priority].color, fontWeight:700 }}>● {pm[t.priority].label}</span>
|
|
107
|
+
<span>#{t.id}</span>
|
|
108
|
+
<span>{new Date(t.createdAt).toLocaleDateString([], { month:'short', day:'numeric' })}</span>
|
|
123
109
|
</div>
|
|
124
110
|
</div>
|
|
125
|
-
)
|
|
126
|
-
tickets.map((ticket, i) => {
|
|
127
|
-
const sm = statusMeta[ticket.status];
|
|
128
|
-
const pm = priorityMeta[ticket.priority];
|
|
129
|
-
const date = new Date(ticket.createdAt).toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
130
|
-
return (
|
|
131
|
-
<div
|
|
132
|
-
key={ticket.id}
|
|
133
|
-
style={{
|
|
134
|
-
padding: '14px 20px',
|
|
135
|
-
borderBottom: '1px solid #f3f4f6',
|
|
136
|
-
fontFamily: t.fontFamily,
|
|
137
|
-
animation: `cw-fadeUp 0.3s ease both`,
|
|
138
|
-
animationDelay: `${i * 0.05}s`,
|
|
139
|
-
}}
|
|
140
|
-
>
|
|
141
|
-
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
142
|
-
<span style={{ fontWeight: 700, fontSize: '14px', color: '#1a2332', flex: 1, paddingRight: 12 }}>
|
|
143
|
-
{ticket.title}
|
|
144
|
-
</span>
|
|
145
|
-
<span style={{
|
|
146
|
-
fontSize: '10px', fontWeight: 700, padding: '3px 9px', borderRadius: 20,
|
|
147
|
-
backgroundColor: sm.bg, color: sm.color, whiteSpace: 'nowrap',
|
|
148
|
-
textTransform: 'uppercase', letterSpacing: '0.04em',
|
|
149
|
-
}}>
|
|
150
|
-
{sm.label}
|
|
151
|
-
</span>
|
|
152
|
-
</div>
|
|
153
|
-
{ticket.description && (
|
|
154
|
-
<div style={{ fontSize: '13px', color: '#7b8fa1', marginBottom: 8, lineHeight: 1.5 }}>
|
|
155
|
-
{ticket.description}
|
|
156
|
-
</div>
|
|
157
|
-
)}
|
|
158
|
-
<div style={{ display: 'flex', gap: 12, fontSize: '11px', color: '#b0bec5' }}>
|
|
159
|
-
<span style={{ color: pm.color, fontWeight: 600 }}>{pm.label}</span>
|
|
160
|
-
<span>#{ticket.id.slice(-6).toUpperCase()}</span>
|
|
161
|
-
<span>{date}</span>
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
);
|
|
165
|
-
})
|
|
166
|
-
)}
|
|
111
|
+
))}
|
|
167
112
|
</div>
|
|
168
113
|
</div>
|
|
169
114
|
);
|
|
170
115
|
};
|
|
116
|
+
|
|
117
|
+
function inputStyle(primaryColor: string): React.CSSProperties {
|
|
118
|
+
return {
|
|
119
|
+
width:'100%', padding:'9px 13px', borderRadius:10,
|
|
120
|
+
border:'1.5px solid #e5e7eb', outline:'none',
|
|
121
|
+
fontSize:14, color:'#1a2332', boxSizing:'border-box',
|
|
122
|
+
fontFamily:'inherit', transition:'border-color 0.2s',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -1,181 +1,103 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { ChatUser,
|
|
3
|
-
import {
|
|
2
|
+
import { ChatUser, UserListContext } from '../../types';
|
|
3
|
+
import { avatarColor, initials } from '../../utils/chat';
|
|
4
4
|
|
|
5
5
|
interface UserListScreenProps {
|
|
6
6
|
context: UserListContext;
|
|
7
7
|
users: ChatUser[];
|
|
8
|
-
|
|
9
|
-
error: string | null;
|
|
10
|
-
theme?: ChatWidgetTheme;
|
|
8
|
+
primaryColor: string;
|
|
11
9
|
onBack: () => void;
|
|
12
10
|
onSelectUser: (user: ChatUser) => void;
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
export const UserListScreen: React.FC<UserListScreenProps> = ({
|
|
16
|
-
context, users,
|
|
14
|
+
context, users, primaryColor, onBack, onSelectUser,
|
|
17
15
|
}) => {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const subtitle = context === 'support'
|
|
21
|
-
? 'Choose a support agent'
|
|
22
|
-
: 'Choose a colleague to chat with';
|
|
16
|
+
const title = context === 'support' ? 'Need Support' : 'New Conversation';
|
|
17
|
+
const subtitle = context === 'support' ? 'Choose a support agent' : 'Choose a colleague';
|
|
23
18
|
|
|
24
19
|
return (
|
|
25
|
-
<div style={{ display:
|
|
20
|
+
<div style={{ display:'flex', flexDirection:'column', height:'100%', animation:'cw-slideIn 0.22s ease' }}>
|
|
26
21
|
{/* Header */}
|
|
27
|
-
<div
|
|
28
|
-
|
|
29
|
-
backgroundColor: t.primaryColor,
|
|
30
|
-
padding: '16px 20px',
|
|
31
|
-
display: 'flex',
|
|
32
|
-
alignItems: 'center',
|
|
33
|
-
gap: '12px',
|
|
34
|
-
flexShrink: 0,
|
|
35
|
-
}}
|
|
36
|
-
>
|
|
37
|
-
<button
|
|
38
|
-
onClick={onBack}
|
|
39
|
-
style={{
|
|
40
|
-
background: 'rgba(255,255,255,0.2)',
|
|
41
|
-
border: 'none',
|
|
42
|
-
borderRadius: '50%',
|
|
43
|
-
width: '34px',
|
|
44
|
-
height: '34px',
|
|
45
|
-
display: 'flex',
|
|
46
|
-
alignItems: 'center',
|
|
47
|
-
justifyContent: 'center',
|
|
48
|
-
cursor: 'pointer',
|
|
49
|
-
flexShrink: 0,
|
|
50
|
-
}}
|
|
51
|
-
>
|
|
52
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
53
|
-
<path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
54
|
-
</svg>
|
|
55
|
-
</button>
|
|
22
|
+
<div style={{ background:`linear-gradient(135deg,${primaryColor},${primaryColor}cc)`, padding:'14px 18px', display:'flex', alignItems:'center', gap:12, flexShrink:0 }}>
|
|
23
|
+
<BackBtn onClick={onBack} />
|
|
56
24
|
<div>
|
|
57
|
-
<div style={{ fontWeight:
|
|
58
|
-
|
|
59
|
-
</div>
|
|
60
|
-
<div style={{ fontSize: '12px', color: 'rgba(255,255,255,0.8)', fontFamily: t.fontFamily }}>
|
|
61
|
-
{subtitle}
|
|
62
|
-
</div>
|
|
25
|
+
<div style={{ fontWeight:700, fontSize:16, color:'#fff' }}>{title}</div>
|
|
26
|
+
<div style={{ fontSize:12, color:'rgba(255,255,255,0.8)' }}>{subtitle}</div>
|
|
63
27
|
</div>
|
|
64
28
|
</div>
|
|
65
29
|
|
|
66
|
-
{/*
|
|
67
|
-
<div style={{ flex:
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
30
|
+
{/* User list */}
|
|
31
|
+
<div style={{ flex:1, overflowY:'auto' }}>
|
|
32
|
+
{users.length === 0 ? (
|
|
33
|
+
<Empty />
|
|
34
|
+
) : users.map((u, i) => (
|
|
35
|
+
<button
|
|
36
|
+
key={u.uid}
|
|
37
|
+
onClick={() => onSelectUser(u)}
|
|
38
|
+
style={{
|
|
39
|
+
width:'100%', padding:'13px 18px', display:'flex',
|
|
40
|
+
alignItems:'center', gap:13, background:'transparent',
|
|
41
|
+
border:'none', borderBottom:'1px solid #f0f2f5',
|
|
42
|
+
cursor:'pointer', textAlign:'left',
|
|
43
|
+
animation:`cw-fadeUp 0.28s ease both`, animationDelay:`${i*0.05}s`,
|
|
44
|
+
transition:'background 0.14s',
|
|
45
|
+
}}
|
|
46
|
+
onMouseEnter={e => (e.currentTarget as HTMLElement).style.background = '#f8faff'}
|
|
47
|
+
onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = 'transparent'}
|
|
48
|
+
>
|
|
49
|
+
{/* Avatar with online dot */}
|
|
50
|
+
<div style={{ position:'relative', flexShrink:0 }}>
|
|
51
|
+
<div style={{
|
|
52
|
+
width:44, height:44, borderRadius:'50%',
|
|
53
|
+
backgroundColor: avatarColor(u.name),
|
|
54
|
+
display:'flex', alignItems:'center', justifyContent:'center',
|
|
55
|
+
color:'#fff', fontWeight:700, fontSize:14,
|
|
56
|
+
}}>{initials(u.name)}</div>
|
|
57
|
+
<span style={{
|
|
58
|
+
position:'absolute', bottom:1, right:1,
|
|
59
|
+
width:11, height:11, borderRadius:'50%', border:'2px solid #fff',
|
|
60
|
+
backgroundColor: u.status==='online' ? '#22c55e' : u.status==='away' ? '#f59e0b' : '#d1d5db',
|
|
61
|
+
}} />
|
|
62
|
+
</div>
|
|
63
|
+
{/* Info */}
|
|
64
|
+
<div style={{ flex:1, minWidth:0 }}>
|
|
65
|
+
<div style={{ fontWeight:700, fontSize:14, color:'#1a2332', marginBottom:2 }}>{u.name}</div>
|
|
66
|
+
<div style={{ fontSize:12, color:'#7b8fa1', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
|
|
67
|
+
{u.designation} · {u.project}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
{/* Type badge */}
|
|
71
|
+
<span style={{
|
|
72
|
+
fontSize:10, fontWeight:700, padding:'3px 9px', borderRadius:20,
|
|
73
|
+
textTransform:'uppercase', letterSpacing:'0.05em', flexShrink:0,
|
|
74
|
+
background: u.type==='developer' ? `${primaryColor}15` : '#f0fdf4',
|
|
75
|
+
color: u.type==='developer' ? primaryColor : '#16a34a',
|
|
76
|
+
border:`1px solid ${u.type==='developer' ? primaryColor+'30' : '#16a34a30'}`,
|
|
77
|
+
}}>{u.type==='developer' ? 'Dev' : 'User'}</span>
|
|
78
|
+
</button>
|
|
82
79
|
))}
|
|
83
80
|
</div>
|
|
84
81
|
</div>
|
|
85
82
|
);
|
|
86
83
|
};
|
|
87
84
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return (
|
|
100
|
-
<button
|
|
101
|
-
onClick={onClick}
|
|
102
|
-
style={{
|
|
103
|
-
width: '100%',
|
|
104
|
-
padding: '14px 20px',
|
|
105
|
-
display: 'flex',
|
|
106
|
-
alignItems: 'center',
|
|
107
|
-
gap: '14px',
|
|
108
|
-
background: 'transparent',
|
|
109
|
-
border: 'none',
|
|
110
|
-
borderBottom: '1px solid #f3f4f6',
|
|
111
|
-
cursor: 'pointer',
|
|
112
|
-
textAlign: 'left',
|
|
113
|
-
fontFamily,
|
|
114
|
-
animation: 'cw-fadeUp 0.3s ease both',
|
|
115
|
-
animationDelay: `${index * 0.05}s`,
|
|
116
|
-
transition: 'background 0.15s',
|
|
117
|
-
}}
|
|
118
|
-
onMouseEnter={e => (e.currentTarget as HTMLElement).style.background = '#f8fdfc'}
|
|
119
|
-
onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = 'transparent'}
|
|
120
|
-
>
|
|
121
|
-
{/* Avatar */}
|
|
122
|
-
<div style={{
|
|
123
|
-
width: '46px', height: '46px', borderRadius: '50%',
|
|
124
|
-
backgroundColor: bg, flexShrink: 0,
|
|
125
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
126
|
-
color: '#fff', fontWeight: 700, fontSize: '15px',
|
|
127
|
-
}}>
|
|
128
|
-
{initials}
|
|
129
|
-
</div>
|
|
130
|
-
{/* Info */}
|
|
131
|
-
<div style={{ flex: 1, minWidth: 0 }}>
|
|
132
|
-
<div style={{ fontWeight: 700, fontSize: '14px', color: '#1a2332', marginBottom: '2px' }}>
|
|
133
|
-
{user.name}
|
|
134
|
-
</div>
|
|
135
|
-
<div style={{ fontSize: '12px', color: '#7b8fa1', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
136
|
-
{user.project || user.email}
|
|
137
|
-
</div>
|
|
138
|
-
</div>
|
|
139
|
-
{/* Type badge */}
|
|
140
|
-
<span style={{
|
|
141
|
-
fontSize: '10px', fontWeight: 700, padding: '3px 9px',
|
|
142
|
-
borderRadius: '20px', textTransform: 'uppercase', letterSpacing: '0.05em',
|
|
143
|
-
backgroundColor: user.type === 'developer' ? '#e6faf8' : '#eff6ff',
|
|
144
|
-
color: user.type === 'developer' ? primaryColor : '#2563EB',
|
|
145
|
-
border: `1px solid ${user.type === 'developer' ? primaryColor + '30' : '#2563eb30'}`,
|
|
146
|
-
}}>
|
|
147
|
-
{user.type === 'developer' ? 'Dev' : 'User'}
|
|
148
|
-
</span>
|
|
149
|
-
</button>
|
|
150
|
-
);
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const UserListSkeleton: React.FC = () => (
|
|
154
|
-
<>
|
|
155
|
-
{[1,2,3,4].map(i => (
|
|
156
|
-
<div key={i} style={{ padding: '14px 20px', display: 'flex', gap: '14px', alignItems: 'center' }}>
|
|
157
|
-
<div style={{ width: 46, height: 46, borderRadius: '50%', background: '#f0f0f0', flexShrink: 0 }} />
|
|
158
|
-
<div style={{ flex: 1 }}>
|
|
159
|
-
<div style={{ height: 13, width: '55%', background: '#f0f0f0', borderRadius: 6, marginBottom: 7 }} />
|
|
160
|
-
<div style={{ height: 11, width: '38%', background: '#f0f0f0', borderRadius: 6 }} />
|
|
161
|
-
</div>
|
|
162
|
-
</div>
|
|
163
|
-
))}
|
|
164
|
-
</>
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
const ErrorState: React.FC<{ message: string; color: string; font: string }> = ({ message, color, font }) => (
|
|
168
|
-
<div style={{ padding: '40px 24px', textAlign: 'center', fontFamily: font }}>
|
|
169
|
-
<div style={{ fontSize: '32px', marginBottom: '10px' }}>⚠️</div>
|
|
170
|
-
<div style={{ fontWeight: 700, color: '#1a2332', marginBottom: '6px' }}>Could not load users</div>
|
|
171
|
-
<div style={{ fontSize: '13px', color: '#7b8fa1' }}>{message}</div>
|
|
172
|
-
</div>
|
|
85
|
+
const BackBtn: React.FC<{ onClick: () => void }> = ({ onClick }) => (
|
|
86
|
+
<button onClick={onClick} style={{
|
|
87
|
+
background:'rgba(255,255,255,0.22)', border:'none', borderRadius:'50%',
|
|
88
|
+
width:32, height:32, display:'flex', alignItems:'center', justifyContent:'center',
|
|
89
|
+
cursor:'pointer', flexShrink:0,
|
|
90
|
+
}}>
|
|
91
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
|
92
|
+
<path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
93
|
+
</svg>
|
|
94
|
+
</button>
|
|
173
95
|
);
|
|
174
96
|
|
|
175
|
-
const
|
|
176
|
-
<div style={{ padding:
|
|
177
|
-
<div style={{ fontSize:
|
|
178
|
-
<div style={{ fontWeight:
|
|
179
|
-
<div style={{ fontSize:
|
|
97
|
+
const Empty: React.FC = () => (
|
|
98
|
+
<div style={{ padding:'50px 24px', textAlign:'center' }}>
|
|
99
|
+
<div style={{ fontSize:36, marginBottom:10 }}>👥</div>
|
|
100
|
+
<div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>No users available</div>
|
|
101
|
+
<div style={{ fontSize:13, color:'#7b8fa1' }}>Check back later</div>
|
|
180
102
|
</div>
|
|
181
103
|
);
|
package/src/config/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LocalEnvConfig, RemoteChatData } from '../types';
|
|
2
|
+
|
|
3
|
+
/** Default JSON endpoint; override with REACT_APP_CHAT_CONFIG_URL / NEXT_PUBLIC_CHAT_CONFIG_URL */
|
|
4
|
+
const DEFAULT_CHAT_DATA_BASE = 'https://window.mscorpres.com/TEST/chatData.json';
|
|
5
|
+
const DEMO_API_KEY = 'demo1234';
|
|
6
|
+
const DEMO_WIDGET_ID = 'demo';
|
|
2
7
|
|
|
3
8
|
function getEnv(key: string): string | undefined {
|
|
4
9
|
if (typeof process !== 'undefined' && process.env) {
|
|
@@ -12,35 +17,36 @@ function getEnv(key: string): string | undefined {
|
|
|
12
17
|
return undefined;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
console.warn(`[ChatWidget] Invalid CHAT_STATUS "${v}". Defaulting to DISABLE.`);
|
|
18
|
-
return 'DISABLE';
|
|
20
|
+
function getChatDataBaseUrl(): string {
|
|
21
|
+
return getEnv('CHAT_CONFIG_URL')?.trim() || DEFAULT_CHAT_DATA_BASE;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Loads remote widget config once via GET. Uses query params `key` and `widget` so the
|
|
26
|
+
* server can validate the request without custom headers (avoids CORS preflight failures).
|
|
27
|
+
*/
|
|
28
|
+
export async function fetchRemoteChatData(
|
|
29
|
+
apiKey: string,
|
|
30
|
+
widgetId: string
|
|
31
|
+
): Promise<RemoteChatData> {
|
|
32
|
+
const base = getChatDataBaseUrl();
|
|
33
|
+
const url = new URL(base, typeof window !== 'undefined' ? window.location.origin : 'https://localhost');
|
|
34
|
+
url.searchParams.set('key', apiKey);
|
|
35
|
+
url.searchParams.set('widget', widgetId);
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
const res = await fetch(url.toString(), {
|
|
38
|
+
method: 'GET',
|
|
39
|
+
credentials: 'omit',
|
|
40
|
+
mode: 'cors',
|
|
41
|
+
headers: { Accept: 'application/json' },
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) throw new Error(`Failed to load chat config: ${res.status}`);
|
|
44
|
+
return res.json() as Promise<RemoteChatData>;
|
|
45
|
+
}
|
|
30
46
|
|
|
47
|
+
export function loadLocalConfig(): LocalEnvConfig {
|
|
31
48
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
userListEndpoint: getEnv('CHAT_USER_LIST') ?? 'api/users',
|
|
35
|
-
status: validateStatus(getEnv('CHAT_STATUS')),
|
|
36
|
-
chatType: validateChatType(getEnv('CHAT_TYPE')),
|
|
49
|
+
apiKey: getEnv('CHAT_API_KEY') ?? DEMO_API_KEY,
|
|
50
|
+
widgetId: getEnv('CHAT_WIDGET_ID') ?? DEMO_WIDGET_ID,
|
|
37
51
|
};
|
|
38
52
|
}
|
|
39
|
-
|
|
40
|
-
export function buildUserListUrl(config: ChatConfig): string {
|
|
41
|
-
const base = config.hostUrl.replace(/\/$/, '');
|
|
42
|
-
const endpoint = config.userListEndpoint.replace(/^\//, '');
|
|
43
|
-
// Port is optional
|
|
44
|
-
const portPart = config.hostPort ? `:${config.hostPort}` : '';
|
|
45
|
-
return `${base}${portPart}/${endpoint}`;
|
|
46
|
-
}
|