ajaxter-chat 2.0.1 → 3.0.1
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 +3 -3
- package/dist/config/index.js +18 -26
- 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 +22 -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 +22 -28
- package/src/hooks/useChat.ts +31 -14
- package/src/hooks/useRemoteConfig.ts +20 -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,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LocalEnvConfig, RemoteChatData } from '../types';
|
|
2
|
+
|
|
3
|
+
const REMOTE_URL = 'https://window.mscorpres.com/TEST/chatData.json';
|
|
4
|
+
const DEMO_API_KEY = 'demo1234';
|
|
5
|
+
const DEMO_WIDGET_ID = 'demo';
|
|
2
6
|
|
|
3
7
|
function getEnv(key: string): string | undefined {
|
|
4
8
|
if (typeof process !== 'undefined' && process.env) {
|
|
@@ -12,35 +16,25 @@ function getEnv(key: string): string | undefined {
|
|
|
12
16
|
return undefined;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
function
|
|
16
|
-
if (v === 'ACTIVE' || v === 'DISABLE' || v === 'MAINTENANCE') return v;
|
|
17
|
-
console.warn(`[ChatWidget] Invalid CHAT_STATUS "${v}". Defaulting to DISABLE.`);
|
|
18
|
-
return 'DISABLE';
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function validateChatType(v?: string): ChatType {
|
|
22
|
-
if (v === 'SUPPORT' || v === 'CHAT' || v === 'BOTH') return v;
|
|
23
|
-
console.warn(`[ChatWidget] Invalid CHAT_TYPE "${v}". Defaulting to SUPPORT.`);
|
|
24
|
-
return 'SUPPORT';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function loadChatConfig(): ChatConfig {
|
|
28
|
-
const portStr = getEnv('CHAT_HOST_PORT');
|
|
29
|
-
const hostPort = portStr ? parseInt(portStr, 10) : null;
|
|
30
|
-
|
|
19
|
+
export function loadLocalConfig(): LocalEnvConfig {
|
|
31
20
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
userListEndpoint: getEnv('CHAT_USER_LIST') ?? 'api/users',
|
|
35
|
-
status: validateStatus(getEnv('CHAT_STATUS')),
|
|
36
|
-
chatType: validateChatType(getEnv('CHAT_TYPE')),
|
|
21
|
+
apiKey: getEnv('CHAT_API_KEY') ?? DEMO_API_KEY,
|
|
22
|
+
widgetId: getEnv('CHAT_WIDGET_ID') ?? DEMO_WIDGET_ID,
|
|
37
23
|
};
|
|
38
24
|
}
|
|
39
25
|
|
|
40
|
-
export function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
26
|
+
export async function fetchRemoteChatData(
|
|
27
|
+
apiKey: string,
|
|
28
|
+
widgetId: string
|
|
29
|
+
): Promise<RemoteChatData> {
|
|
30
|
+
const url = REMOTE_URL;
|
|
31
|
+
const res = await fetch(url, {
|
|
32
|
+
headers: {
|
|
33
|
+
'X-Chat-Api-Key': apiKey,
|
|
34
|
+
'X-Chat-Widget-Id': widgetId,
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) throw new Error(`Failed to load chat config: ${res.status}`);
|
|
39
|
+
return res.json() as Promise<RemoteChatData>;
|
|
46
40
|
}
|
package/src/hooks/useChat.ts
CHANGED
|
@@ -1,31 +1,48 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react';
|
|
2
2
|
import { ChatMessage, ChatUser } from '../types';
|
|
3
3
|
|
|
4
|
-
export function useChat() {
|
|
5
|
-
const [messages, setMessages] = useState<ChatMessage[]>(
|
|
4
|
+
export function useChat(initialMessages: ChatMessage[] = []) {
|
|
5
|
+
const [messages, setMessages] = useState<ChatMessage[]>(initialMessages);
|
|
6
6
|
const [activeUser, setActiveUser] = useState<ChatUser | null>(null);
|
|
7
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
8
|
+
const [isReported, setIsReported] = useState(false);
|
|
7
9
|
|
|
8
|
-
const selectUser = useCallback((user: ChatUser) => {
|
|
10
|
+
const selectUser = useCallback((user: ChatUser, history: ChatMessage[] = []) => {
|
|
9
11
|
setActiveUser(user);
|
|
10
|
-
setMessages(
|
|
11
|
-
|
|
12
|
+
setMessages(history);
|
|
13
|
+
setIsPaused(false);
|
|
14
|
+
setIsReported(false);
|
|
15
|
+
// TODO: socket.emit('join', { roomId: user.uid });
|
|
16
|
+
// TODO: socket.on('message', msg => setMessages(prev => [...prev, msg]));
|
|
12
17
|
}, []);
|
|
13
18
|
|
|
14
|
-
const sendMessage = useCallback((
|
|
15
|
-
|
|
19
|
+
const sendMessage = useCallback((
|
|
20
|
+
text: string,
|
|
21
|
+
type: ChatMessage['type'] = 'text',
|
|
22
|
+
extra: Partial<ChatMessage> = {}
|
|
23
|
+
) => {
|
|
24
|
+
if (!activeUser || isPaused) return;
|
|
16
25
|
const msg: ChatMessage = {
|
|
17
|
-
id: `msg_${Date.now()}`,
|
|
26
|
+
id: `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
18
27
|
senderId: 'me',
|
|
19
28
|
receiverId: activeUser.uid,
|
|
20
|
-
text
|
|
21
|
-
timestamp: new Date(),
|
|
29
|
+
text,
|
|
30
|
+
timestamp: new Date().toISOString(),
|
|
31
|
+
type,
|
|
22
32
|
status: 'sent',
|
|
33
|
+
...extra,
|
|
23
34
|
};
|
|
24
35
|
setMessages(prev => [...prev, msg]);
|
|
25
|
-
// TODO: socket.emit('message', msg)
|
|
26
|
-
}, [activeUser]);
|
|
36
|
+
// TODO: socket.emit('message', msg);
|
|
37
|
+
}, [activeUser, isPaused]);
|
|
27
38
|
|
|
28
|
-
const
|
|
39
|
+
const togglePause = useCallback(() => setIsPaused(p => !p), []);
|
|
40
|
+
const reportChat = useCallback(() => { setIsReported(true); /* TODO: API call */ }, []);
|
|
41
|
+
const clearChat = useCallback(() => { setMessages([]); setActiveUser(null); }, []);
|
|
29
42
|
|
|
30
|
-
return {
|
|
43
|
+
return {
|
|
44
|
+
messages, activeUser, isPaused, isReported,
|
|
45
|
+
selectUser, sendMessage, togglePause, reportChat, clearChat,
|
|
46
|
+
setMessages,
|
|
47
|
+
};
|
|
31
48
|
}
|