ajaxter-chat 1.0.3 → 2.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 +96 -204
- package/dist/components/ChatScreen/index.d.ts +12 -0
- package/dist/components/ChatScreen/index.js +83 -0
- package/dist/components/ChatWidget.d.ts +0 -24
- package/dist/components/ChatWidget.js +129 -38
- package/dist/components/HomeScreen/index.d.ts +9 -0
- package/dist/components/HomeScreen/index.js +71 -0
- package/dist/components/MaintenanceView/index.d.ts +1 -1
- package/dist/components/MaintenanceView/index.js +15 -52
- package/dist/components/RecentChatsScreen/index.d.ts +16 -0
- package/dist/components/RecentChatsScreen/index.js +38 -0
- package/dist/components/Tabs/BottomTabs.d.ts +10 -0
- package/dist/components/Tabs/BottomTabs.js +29 -0
- package/dist/components/TicketScreen/index.d.ts +9 -0
- package/dist/components/TicketScreen/index.js +71 -0
- package/dist/components/UserListScreen/index.d.ts +13 -0
- package/dist/components/UserListScreen/index.js +64 -0
- package/dist/config/index.d.ts +0 -13
- package/dist/config/index.js +20 -95
- package/dist/hooks/useChat.d.ts +3 -7
- package/dist/hooks/useChat.js +8 -30
- package/dist/hooks/useUsers.d.ts +3 -10
- package/dist/hooks/useUsers.js +5 -11
- package/dist/index.d.ts +8 -7
- package/dist/index.js +7 -12
- package/dist/services/userService.d.ts +0 -5
- package/dist/services/userService.js +6 -15
- package/dist/src/components/ChatScreen/index.d.ts +12 -0
- package/dist/src/components/ChatScreen/index.js +83 -0
- package/dist/src/components/ChatWidget.d.ts +4 -0
- package/dist/src/components/ChatWidget.js +141 -0
- package/dist/src/components/HomeScreen/index.d.ts +9 -0
- package/dist/src/components/HomeScreen/index.js +71 -0
- package/dist/src/components/MaintenanceView/index.d.ts +7 -0
- package/dist/src/components/MaintenanceView/index.js +16 -0
- package/dist/src/components/RecentChatsScreen/index.d.ts +16 -0
- package/dist/src/components/RecentChatsScreen/index.js +38 -0
- package/dist/src/components/Tabs/BottomTabs.d.ts +10 -0
- package/dist/src/components/Tabs/BottomTabs.js +29 -0
- package/dist/src/components/TicketScreen/index.d.ts +9 -0
- package/dist/src/components/TicketScreen/index.js +71 -0
- package/dist/src/components/UserListScreen/index.d.ts +13 -0
- package/dist/src/components/UserListScreen/index.js +64 -0
- package/dist/src/config/index.d.ts +3 -0
- package/dist/src/config/index.js +38 -0
- package/dist/src/hooks/useChat.d.ts +8 -0
- package/dist/src/hooks/useChat.js +26 -0
- package/dist/src/hooks/useUsers.d.ts +7 -0
- package/dist/src/hooks/useUsers.js +26 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +13 -0
- package/dist/src/services/userService.d.ts +2 -0
- package/dist/src/services/userService.js +9 -0
- package/dist/src/types/index.d.ts +59 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/utils/theme.d.ts +3 -0
- package/dist/src/utils/theme.js +13 -0
- package/dist/types/index.d.ts +23 -36
- package/dist/utils/theme.d.ts +0 -1
- package/dist/utils/theme.js +3 -18
- package/package.json +10 -20
- package/src/components/ChatScreen/index.tsx +205 -0
- package/src/components/ChatWidget.tsx +327 -0
- package/src/components/HomeScreen/index.tsx +130 -0
- package/src/components/MaintenanceView/index.tsx +41 -0
- package/src/components/RecentChatsScreen/index.tsx +108 -0
- package/src/components/Tabs/BottomTabs.tsx +82 -0
- package/src/components/TicketScreen/index.tsx +170 -0
- package/src/components/UserListScreen/index.tsx +181 -0
- package/src/config/index.ts +46 -0
- package/src/hooks/useChat.ts +31 -0
- package/src/hooks/useUsers.ts +27 -0
- package/src/index.ts +18 -0
- package/src/services/userService.ts +9 -0
- package/src/types/index.ts +82 -0
- package/src/utils/theme.ts +16 -0
- package/dist/components/BottomNav/index.d.ts +0 -10
- package/dist/components/BottomNav/index.js +0 -32
- package/dist/components/ChatBox/index.d.ts +0 -15
- package/dist/components/ChatBox/index.js +0 -228
- package/dist/components/ChatButton/index.d.ts +0 -9
- package/dist/components/ChatButton/index.js +0 -17
- package/dist/components/ChatWindow/index.d.ts +0 -10
- package/dist/components/ChatWindow/index.js +0 -286
- package/dist/components/HomeView/index.d.ts +0 -12
- package/dist/components/HomeView/index.js +0 -51
- package/dist/components/UserList/index.d.ts +0 -13
- package/dist/components/UserList/index.js +0 -136
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BottomTab } from '../../types';
|
|
3
|
+
|
|
4
|
+
interface BottomTabsProps {
|
|
5
|
+
active: BottomTab;
|
|
6
|
+
onChange: (tab: BottomTab) => void;
|
|
7
|
+
primaryColor: string;
|
|
8
|
+
fontFamily: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const BottomTabs: React.FC<BottomTabsProps> = ({ active, onChange, primaryColor, fontFamily }) => {
|
|
12
|
+
const tabs: { key: BottomTab; label: string; Icon: React.FC<{ active: boolean; color: string }> }[] = [
|
|
13
|
+
{ key: 'home', label: 'Home', Icon: HomeIcon },
|
|
14
|
+
{ key: 'chats', label: 'Chats', Icon: ChatsIcon },
|
|
15
|
+
{ key: 'tickets', label: 'Tickets', Icon: TicketsIcon },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div style={{
|
|
20
|
+
display: 'flex',
|
|
21
|
+
borderTop: '1px solid #eef0f5',
|
|
22
|
+
backgroundColor: '#fff',
|
|
23
|
+
flexShrink: 0,
|
|
24
|
+
}}>
|
|
25
|
+
{tabs.map(tab => {
|
|
26
|
+
const isActive = active === tab.key;
|
|
27
|
+
return (
|
|
28
|
+
<button
|
|
29
|
+
key={tab.key}
|
|
30
|
+
onClick={() => onChange(tab.key)}
|
|
31
|
+
style={{
|
|
32
|
+
flex: 1, padding: '10px 0 8px',
|
|
33
|
+
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
|
|
34
|
+
background: 'transparent', border: 'none', cursor: 'pointer',
|
|
35
|
+
fontFamily, fontSize: '10px', fontWeight: isActive ? 700 : 500,
|
|
36
|
+
color: isActive ? primaryColor : '#9aa3af',
|
|
37
|
+
transition: 'color 0.15s',
|
|
38
|
+
borderTop: isActive ? `2px solid ${primaryColor}` : '2px solid transparent',
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<tab.Icon active={isActive} color={isActive ? primaryColor : '#b0bec5'} />
|
|
42
|
+
{tab.label}
|
|
43
|
+
</button>
|
|
44
|
+
);
|
|
45
|
+
})}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ── Icons ─────────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
const HomeIcon: React.FC<{ active: boolean; color: string }> = ({ active, color }) => (
|
|
53
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
|
54
|
+
<path
|
|
55
|
+
d="M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z"
|
|
56
|
+
stroke={color} strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round"
|
|
57
|
+
fill={active ? `${color}20` : 'none'}
|
|
58
|
+
/>
|
|
59
|
+
<path d="M9 21V12h6v9" stroke={color} strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round"/>
|
|
60
|
+
</svg>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const ChatsIcon: React.FC<{ active: boolean; color: string }> = ({ active, color }) => (
|
|
64
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
|
65
|
+
<path
|
|
66
|
+
d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"
|
|
67
|
+
stroke={color} strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round"
|
|
68
|
+
fill={active ? `${color}20` : 'none'}
|
|
69
|
+
/>
|
|
70
|
+
</svg>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const TicketsIcon: React.FC<{ active: boolean; color: string }> = ({ active, color }) => (
|
|
74
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
|
75
|
+
<path
|
|
76
|
+
d="M15 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V9l-4-4z"
|
|
77
|
+
stroke={color} strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round"
|
|
78
|
+
fill={active ? `${color}20` : 'none'}
|
|
79
|
+
/>
|
|
80
|
+
<path d="M15 5v4h4M9 13h6M9 17h4" stroke={color} strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round"/>
|
|
81
|
+
</svg>
|
|
82
|
+
);
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { ChatWidgetTheme, Ticket } from '../../types';
|
|
3
|
+
import { mergeTheme } from '../../utils/theme';
|
|
4
|
+
|
|
5
|
+
interface TicketScreenProps {
|
|
6
|
+
tickets: Ticket[];
|
|
7
|
+
theme?: ChatWidgetTheme;
|
|
8
|
+
onRaiseTicket: (title: string, description: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets, theme, onRaiseTicket }) => {
|
|
12
|
+
const t = mergeTheme(theme);
|
|
13
|
+
const [showForm, setShowForm] = useState(false);
|
|
14
|
+
const [title, setTitle] = useState('');
|
|
15
|
+
const [desc, setDesc] = useState('');
|
|
16
|
+
|
|
17
|
+
const handleSubmit = () => {
|
|
18
|
+
if (!title.trim()) return;
|
|
19
|
+
onRaiseTicket(title.trim(), desc.trim());
|
|
20
|
+
setTitle(''); setDesc(''); setShowForm(false);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const statusMeta: Record<Ticket['status'], { label: string; bg: string; color: string }> = {
|
|
24
|
+
'open': { label: 'Open', bg: '#e6faf8', color: t.primaryColor },
|
|
25
|
+
'in-progress': { label: 'In Progress', bg: '#fffbeb', color: '#d97706' },
|
|
26
|
+
'resolved': { label: 'Resolved', bg: '#f0fdf4', color: '#16a34a' },
|
|
27
|
+
'closed': { label: 'Closed', bg: '#f3f4f6', color: '#6b7280' },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const priorityMeta: Record<Ticket['priority'], { label: string; color: string }> = {
|
|
31
|
+
low: { label: '↓ Low', color: '#6b7280' },
|
|
32
|
+
medium: { label: '→ Medium', color: '#d97706' },
|
|
33
|
+
high: { label: '↑ High', color: '#dc2626' },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
38
|
+
{/* Header */}
|
|
39
|
+
<div style={{ backgroundColor: t.primaryColor, padding: '20px 20px 24px', flexShrink: 0 }}>
|
|
40
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
41
|
+
<div>
|
|
42
|
+
<h2 style={{ margin: 0, fontSize: '20px', fontWeight: 800, color: '#fff', fontFamily: t.fontFamily, letterSpacing: '-0.02em' }}>
|
|
43
|
+
Tickets
|
|
44
|
+
</h2>
|
|
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
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
<button
|
|
50
|
+
onClick={() => setShowForm(v => !v)}
|
|
51
|
+
style={{
|
|
52
|
+
background: 'rgba(255,255,255,0.25)', border: 'none', borderRadius: '20px',
|
|
53
|
+
padding: '8px 16px', color: '#fff', fontWeight: 700, fontSize: '13px',
|
|
54
|
+
cursor: 'pointer', fontFamily: t.fontFamily, display: 'flex', alignItems: 'center', gap: 5,
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{showForm ? '✕ Cancel' : '+ New Ticket'}
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* New Ticket Form */}
|
|
63
|
+
{showForm && (
|
|
64
|
+
<div style={{
|
|
65
|
+
padding: '16px 20px',
|
|
66
|
+
borderBottom: '1px solid #eef0f5',
|
|
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')}
|
|
83
|
+
/>
|
|
84
|
+
<textarea
|
|
85
|
+
placeholder="Describe the issue (optional)"
|
|
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')}
|
|
97
|
+
/>
|
|
98
|
+
<button
|
|
99
|
+
onClick={handleSubmit}
|
|
100
|
+
disabled={!title.trim()}
|
|
101
|
+
style={{
|
|
102
|
+
width: '100%', padding: '11px', borderRadius: 10,
|
|
103
|
+
backgroundColor: title.trim() ? t.primaryColor : '#e2e8f0',
|
|
104
|
+
color: title.trim() ? '#fff' : '#9aa3af',
|
|
105
|
+
border: 'none', cursor: title.trim() ? 'pointer' : 'not-allowed',
|
|
106
|
+
fontWeight: 700, fontSize: '14px', fontFamily: t.fontFamily,
|
|
107
|
+
transition: 'background 0.2s',
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
Submit Ticket
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{/* Ticket List */}
|
|
116
|
+
<div style={{ flex: 1, overflowY: 'auto' }}>
|
|
117
|
+
{tickets.length === 0 ? (
|
|
118
|
+
<div style={{ padding: '50px 24px', textAlign: 'center', fontFamily: t.fontFamily }}>
|
|
119
|
+
<div style={{ fontSize: '36px', marginBottom: 12 }}>🎫</div>
|
|
120
|
+
<div style={{ fontWeight: 700, color: '#1a2332', marginBottom: 6 }}>No tickets yet</div>
|
|
121
|
+
<div style={{ fontSize: '13px', color: '#7b8fa1' }}>
|
|
122
|
+
Raise a ticket for major issues or changes
|
|
123
|
+
</div>
|
|
124
|
+
</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
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChatUser, ChatWidgetTheme, UserListContext } from '../../types';
|
|
3
|
+
import { mergeTheme } from '../../utils/theme';
|
|
4
|
+
|
|
5
|
+
interface UserListScreenProps {
|
|
6
|
+
context: UserListContext;
|
|
7
|
+
users: ChatUser[];
|
|
8
|
+
loading: boolean;
|
|
9
|
+
error: string | null;
|
|
10
|
+
theme?: ChatWidgetTheme;
|
|
11
|
+
onBack: () => void;
|
|
12
|
+
onSelectUser: (user: ChatUser) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const UserListScreen: React.FC<UserListScreenProps> = ({
|
|
16
|
+
context, users, loading, error, theme, onBack, onSelectUser,
|
|
17
|
+
}) => {
|
|
18
|
+
const t = mergeTheme(theme);
|
|
19
|
+
const title = context === 'support' ? 'Need Support' : 'New Conversation';
|
|
20
|
+
const subtitle = context === 'support'
|
|
21
|
+
? 'Choose a support agent'
|
|
22
|
+
: 'Choose a colleague to chat with';
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideInRight 0.25s ease' }}>
|
|
26
|
+
{/* Header */}
|
|
27
|
+
<div
|
|
28
|
+
style={{
|
|
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>
|
|
56
|
+
<div>
|
|
57
|
+
<div style={{ fontWeight: 700, fontSize: '16px', color: '#fff', fontFamily: t.fontFamily }}>
|
|
58
|
+
{title}
|
|
59
|
+
</div>
|
|
60
|
+
<div style={{ fontSize: '12px', color: 'rgba(255,255,255,0.8)', fontFamily: t.fontFamily }}>
|
|
61
|
+
{subtitle}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{/* List */}
|
|
67
|
+
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 0' }}>
|
|
68
|
+
{loading && <UserListSkeleton />}
|
|
69
|
+
{error && <ErrorState message={error} color={t.primaryColor} font={t.fontFamily} />}
|
|
70
|
+
{!loading && !error && users.length === 0 && (
|
|
71
|
+
<EmptyState color={t.primaryColor} font={t.fontFamily} />
|
|
72
|
+
)}
|
|
73
|
+
{!loading && !error && users.map((user, i) => (
|
|
74
|
+
<UserRow
|
|
75
|
+
key={user.uid}
|
|
76
|
+
user={user}
|
|
77
|
+
index={i}
|
|
78
|
+
primaryColor={t.primaryColor}
|
|
79
|
+
fontFamily={t.fontFamily}
|
|
80
|
+
onClick={() => onSelectUser(user)}
|
|
81
|
+
/>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// ── Sub-components ─────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
const UserRow: React.FC<{
|
|
91
|
+
user: ChatUser; index: number;
|
|
92
|
+
primaryColor: string; fontFamily: string;
|
|
93
|
+
onClick: () => void;
|
|
94
|
+
}> = ({ user, index, primaryColor, fontFamily, onClick }) => {
|
|
95
|
+
const initials = user.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
|
96
|
+
const avatarColors = ['#1aaa96','#2563EB','#7C3AED','#D97706','#DC2626','#059669'];
|
|
97
|
+
const bg = avatarColors[user.name.charCodeAt(0) % avatarColors.length];
|
|
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>
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const EmptyState: React.FC<{ color: string; font: string }> = ({ color, font }) => (
|
|
176
|
+
<div style={{ padding: '40px 24px', textAlign: 'center', fontFamily: font }}>
|
|
177
|
+
<div style={{ fontSize: '32px', marginBottom: '10px' }}>👥</div>
|
|
178
|
+
<div style={{ fontWeight: 700, color: '#1a2332', marginBottom: '6px' }}>No users available</div>
|
|
179
|
+
<div style={{ fontSize: '13px', color: '#7b8fa1' }}>Check back later</div>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ChatConfig, ChatStatus, ChatType } from '../types';
|
|
2
|
+
|
|
3
|
+
function getEnv(key: string): string | undefined {
|
|
4
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
5
|
+
return (
|
|
6
|
+
process.env[`NEXT_PUBLIC_${key}`] ??
|
|
7
|
+
process.env[`REACT_APP_${key}`] ??
|
|
8
|
+
process.env[key] ??
|
|
9
|
+
undefined
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function validateStatus(v?: string): ChatStatus {
|
|
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
|
+
|
|
31
|
+
return {
|
|
32
|
+
hostUrl: getEnv('CHAT_HOST_URL') ?? 'http://localhost',
|
|
33
|
+
hostPort,
|
|
34
|
+
userListEndpoint: getEnv('CHAT_USER_LIST') ?? 'api/users',
|
|
35
|
+
status: validateStatus(getEnv('CHAT_STATUS')),
|
|
36
|
+
chatType: validateChatType(getEnv('CHAT_TYPE')),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { ChatMessage, ChatUser } from '../types';
|
|
3
|
+
|
|
4
|
+
export function useChat() {
|
|
5
|
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
6
|
+
const [activeUser, setActiveUser] = useState<ChatUser | null>(null);
|
|
7
|
+
|
|
8
|
+
const selectUser = useCallback((user: ChatUser) => {
|
|
9
|
+
setActiveUser(user);
|
|
10
|
+
setMessages([]);
|
|
11
|
+
// TODO: socket.emit('join', user.uid); socket.on('message', handler)
|
|
12
|
+
}, []);
|
|
13
|
+
|
|
14
|
+
const sendMessage = useCallback((text: string) => {
|
|
15
|
+
if (!activeUser || !text.trim()) return;
|
|
16
|
+
const msg: ChatMessage = {
|
|
17
|
+
id: `msg_${Date.now()}`,
|
|
18
|
+
senderId: 'me',
|
|
19
|
+
receiverId: activeUser.uid,
|
|
20
|
+
text: text.trim(),
|
|
21
|
+
timestamp: new Date(),
|
|
22
|
+
status: 'sent',
|
|
23
|
+
};
|
|
24
|
+
setMessages(prev => [...prev, msg]);
|
|
25
|
+
// TODO: socket.emit('message', msg)
|
|
26
|
+
}, [activeUser]);
|
|
27
|
+
|
|
28
|
+
const clearChat = useCallback(() => { setMessages([]); setActiveUser(null); }, []);
|
|
29
|
+
|
|
30
|
+
return { messages, activeUser, selectUser, sendMessage, clearChat };
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { ChatUser, UserType } from '../types';
|
|
3
|
+
import { fetchUsers } from '../services/userService';
|
|
4
|
+
|
|
5
|
+
export function useUsers(url: string, filterType?: UserType, enabled = true) {
|
|
6
|
+
const [users, setUsers] = useState<ChatUser[]>([]);
|
|
7
|
+
const [loading, setLoading] = useState(false);
|
|
8
|
+
const [error, setError] = useState<string | null>(null);
|
|
9
|
+
|
|
10
|
+
const load = useCallback(async () => {
|
|
11
|
+
if (!enabled) return;
|
|
12
|
+
setLoading(true); setError(null);
|
|
13
|
+
try {
|
|
14
|
+
const data = await fetchUsers(url);
|
|
15
|
+
setUsers(filterType ? data.filter(u => u.type === filterType) : data);
|
|
16
|
+
} catch (e) {
|
|
17
|
+
setError(e instanceof Error ? e.message : 'Unknown error');
|
|
18
|
+
setUsers([]);
|
|
19
|
+
} finally {
|
|
20
|
+
setLoading(false);
|
|
21
|
+
}
|
|
22
|
+
}, [url, filterType, enabled]);
|
|
23
|
+
|
|
24
|
+
useEffect(() => { load(); }, [load]);
|
|
25
|
+
|
|
26
|
+
return { users, loading, error, refetch: load };
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { ChatWidget, default } from './components/ChatWidget';
|
|
2
|
+
export { HomeScreen } from './components/HomeScreen';
|
|
3
|
+
export { UserListScreen } from './components/UserListScreen';
|
|
4
|
+
export { ChatScreen } from './components/ChatScreen';
|
|
5
|
+
export { RecentChatsScreen } from './components/RecentChatsScreen';
|
|
6
|
+
export { TicketScreen } from './components/TicketScreen';
|
|
7
|
+
export { MaintenanceView } from './components/MaintenanceView';
|
|
8
|
+
export { BottomTabs } from './components/Tabs/BottomTabs';
|
|
9
|
+
export { useUsers } from './hooks/useUsers';
|
|
10
|
+
export { useChat } from './hooks/useChat';
|
|
11
|
+
export { loadChatConfig, buildUserListUrl } from './config';
|
|
12
|
+
export { fetchUsers } from './services/userService';
|
|
13
|
+
export { defaultTheme, mergeTheme } from './utils/theme';
|
|
14
|
+
export type {
|
|
15
|
+
ChatUser, ChatMessage, ChatConfig, ChatWidgetTheme,
|
|
16
|
+
ChatWidgetProps, ChatStatus, ChatType, UserType,
|
|
17
|
+
Screen, BottomTab, UserListContext, Ticket,
|
|
18
|
+
} from './types';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ChatUser } from '../types';
|
|
2
|
+
|
|
3
|
+
export async function fetchUsers(url: string): Promise<ChatUser[]> {
|
|
4
|
+
const res = await fetch(url, { headers: { 'Content-Type': 'application/json' } });
|
|
5
|
+
if (!res.ok) throw new Error(`Failed to fetch users: ${res.status}`);
|
|
6
|
+
const data = await res.json();
|
|
7
|
+
if (!Array.isArray(data)) throw new Error('User API did not return an array');
|
|
8
|
+
return data as ChatUser[];
|
|
9
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// ─── Status & Type ────────────────────────────────────────────────────────────
|
|
2
|
+
export type ChatStatus = 'ACTIVE' | 'DISABLE' | 'MAINTENANCE';
|
|
3
|
+
export type ChatType = 'SUPPORT' | 'CHAT' | 'BOTH';
|
|
4
|
+
export type UserType = 'developer' | 'user';
|
|
5
|
+
|
|
6
|
+
// ─── Navigation Screens ───────────────────────────────────────────────────────
|
|
7
|
+
export type Screen =
|
|
8
|
+
| 'home'
|
|
9
|
+
| 'user-list' // slide-in: pick a user before chat
|
|
10
|
+
| 'chat' // active chat conversation
|
|
11
|
+
| 'recent-chats' // bottom tab 2
|
|
12
|
+
| 'tickets'; // bottom tab 3
|
|
13
|
+
|
|
14
|
+
export type UserListContext = 'support' | 'conversation'; // which card was clicked
|
|
15
|
+
|
|
16
|
+
// ─── Bottom Tabs ──────────────────────────────────────────────────────────────
|
|
17
|
+
export type BottomTab = 'home' | 'chats' | 'tickets';
|
|
18
|
+
|
|
19
|
+
// ─── Widget Size ──────────────────────────────────────────────────────────────
|
|
20
|
+
export type WidgetSize = 'normal' | 'maximized';
|
|
21
|
+
|
|
22
|
+
// ─── Data Models ──────────────────────────────────────────────────────────────
|
|
23
|
+
export interface ChatUser {
|
|
24
|
+
name: string;
|
|
25
|
+
uid: string;
|
|
26
|
+
email: string;
|
|
27
|
+
mobile: string;
|
|
28
|
+
project: string;
|
|
29
|
+
type: UserType;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ChatMessage {
|
|
33
|
+
id: string;
|
|
34
|
+
senderId: string;
|
|
35
|
+
receiverId: string;
|
|
36
|
+
text: string;
|
|
37
|
+
timestamp: Date;
|
|
38
|
+
status: 'sent' | 'delivered' | 'read';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RecentChat {
|
|
42
|
+
id: string;
|
|
43
|
+
user: ChatUser;
|
|
44
|
+
lastMessage: string;
|
|
45
|
+
lastTime: Date;
|
|
46
|
+
unread: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface Ticket {
|
|
50
|
+
id: string;
|
|
51
|
+
title: string;
|
|
52
|
+
description: string;
|
|
53
|
+
status: 'open' | 'in-progress' | 'resolved' | 'closed';
|
|
54
|
+
priority: 'low' | 'medium' | 'high';
|
|
55
|
+
createdAt: Date;
|
|
56
|
+
updatedAt: Date;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
60
|
+
export interface ChatConfig {
|
|
61
|
+
hostUrl: string;
|
|
62
|
+
hostPort: number | null; // optional
|
|
63
|
+
userListEndpoint: string;
|
|
64
|
+
status: ChatStatus;
|
|
65
|
+
chatType: ChatType;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Theme ────────────────────────────────────────────────────────────────────
|
|
69
|
+
export interface ChatWidgetTheme {
|
|
70
|
+
primaryColor?: string; // teal header + accents (default: #1aaa96)
|
|
71
|
+
fontFamily?: string;
|
|
72
|
+
buttonColor?: string;
|
|
73
|
+
buttonTextColor?: string;
|
|
74
|
+
buttonLabel?: string;
|
|
75
|
+
buttonPosition?: 'bottom-right' | 'bottom-left';
|
|
76
|
+
borderRadius?: string;
|
|
77
|
+
backgroundColor?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ChatWidgetProps {
|
|
81
|
+
theme?: ChatWidgetTheme;
|
|
82
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ChatWidgetTheme } from '../types';
|
|
2
|
+
|
|
3
|
+
export const defaultTheme: Required<ChatWidgetTheme> = {
|
|
4
|
+
primaryColor: '#1aaa96',
|
|
5
|
+
fontFamily: "'DM Sans', 'Segoe UI', sans-serif",
|
|
6
|
+
buttonColor: '#1aaa96',
|
|
7
|
+
buttonTextColor: '#ffffff',
|
|
8
|
+
buttonLabel: 'Chat with us',
|
|
9
|
+
buttonPosition: 'bottom-right',
|
|
10
|
+
borderRadius: '16px',
|
|
11
|
+
backgroundColor: '#ffffff',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function mergeTheme(custom?: ChatWidgetTheme): Required<ChatWidgetTheme> {
|
|
15
|
+
return { ...defaultTheme, ...custom };
|
|
16
|
+
}
|