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.
Files changed (88) hide show
  1. package/README.md +96 -204
  2. package/dist/components/ChatScreen/index.d.ts +12 -0
  3. package/dist/components/ChatScreen/index.js +83 -0
  4. package/dist/components/ChatWidget.d.ts +0 -24
  5. package/dist/components/ChatWidget.js +129 -38
  6. package/dist/components/HomeScreen/index.d.ts +9 -0
  7. package/dist/components/HomeScreen/index.js +71 -0
  8. package/dist/components/MaintenanceView/index.d.ts +1 -1
  9. package/dist/components/MaintenanceView/index.js +15 -52
  10. package/dist/components/RecentChatsScreen/index.d.ts +16 -0
  11. package/dist/components/RecentChatsScreen/index.js +38 -0
  12. package/dist/components/Tabs/BottomTabs.d.ts +10 -0
  13. package/dist/components/Tabs/BottomTabs.js +29 -0
  14. package/dist/components/TicketScreen/index.d.ts +9 -0
  15. package/dist/components/TicketScreen/index.js +71 -0
  16. package/dist/components/UserListScreen/index.d.ts +13 -0
  17. package/dist/components/UserListScreen/index.js +64 -0
  18. package/dist/config/index.d.ts +0 -13
  19. package/dist/config/index.js +20 -95
  20. package/dist/hooks/useChat.d.ts +3 -7
  21. package/dist/hooks/useChat.js +8 -30
  22. package/dist/hooks/useUsers.d.ts +3 -10
  23. package/dist/hooks/useUsers.js +5 -11
  24. package/dist/index.d.ts +8 -7
  25. package/dist/index.js +7 -12
  26. package/dist/services/userService.d.ts +0 -5
  27. package/dist/services/userService.js +6 -15
  28. package/dist/src/components/ChatScreen/index.d.ts +12 -0
  29. package/dist/src/components/ChatScreen/index.js +83 -0
  30. package/dist/src/components/ChatWidget.d.ts +4 -0
  31. package/dist/src/components/ChatWidget.js +141 -0
  32. package/dist/src/components/HomeScreen/index.d.ts +9 -0
  33. package/dist/src/components/HomeScreen/index.js +71 -0
  34. package/dist/src/components/MaintenanceView/index.d.ts +7 -0
  35. package/dist/src/components/MaintenanceView/index.js +16 -0
  36. package/dist/src/components/RecentChatsScreen/index.d.ts +16 -0
  37. package/dist/src/components/RecentChatsScreen/index.js +38 -0
  38. package/dist/src/components/Tabs/BottomTabs.d.ts +10 -0
  39. package/dist/src/components/Tabs/BottomTabs.js +29 -0
  40. package/dist/src/components/TicketScreen/index.d.ts +9 -0
  41. package/dist/src/components/TicketScreen/index.js +71 -0
  42. package/dist/src/components/UserListScreen/index.d.ts +13 -0
  43. package/dist/src/components/UserListScreen/index.js +64 -0
  44. package/dist/src/config/index.d.ts +3 -0
  45. package/dist/src/config/index.js +38 -0
  46. package/dist/src/hooks/useChat.d.ts +8 -0
  47. package/dist/src/hooks/useChat.js +26 -0
  48. package/dist/src/hooks/useUsers.d.ts +7 -0
  49. package/dist/src/hooks/useUsers.js +26 -0
  50. package/dist/src/index.d.ts +14 -0
  51. package/dist/src/index.js +13 -0
  52. package/dist/src/services/userService.d.ts +2 -0
  53. package/dist/src/services/userService.js +9 -0
  54. package/dist/src/types/index.d.ts +59 -0
  55. package/dist/src/types/index.js +1 -0
  56. package/dist/src/utils/theme.d.ts +3 -0
  57. package/dist/src/utils/theme.js +13 -0
  58. package/dist/types/index.d.ts +23 -36
  59. package/dist/utils/theme.d.ts +0 -1
  60. package/dist/utils/theme.js +3 -18
  61. package/package.json +10 -20
  62. package/src/components/ChatScreen/index.tsx +205 -0
  63. package/src/components/ChatWidget.tsx +327 -0
  64. package/src/components/HomeScreen/index.tsx +130 -0
  65. package/src/components/MaintenanceView/index.tsx +41 -0
  66. package/src/components/RecentChatsScreen/index.tsx +108 -0
  67. package/src/components/Tabs/BottomTabs.tsx +82 -0
  68. package/src/components/TicketScreen/index.tsx +170 -0
  69. package/src/components/UserListScreen/index.tsx +181 -0
  70. package/src/config/index.ts +46 -0
  71. package/src/hooks/useChat.ts +31 -0
  72. package/src/hooks/useUsers.ts +27 -0
  73. package/src/index.ts +18 -0
  74. package/src/services/userService.ts +9 -0
  75. package/src/types/index.ts +82 -0
  76. package/src/utils/theme.ts +16 -0
  77. package/dist/components/BottomNav/index.d.ts +0 -10
  78. package/dist/components/BottomNav/index.js +0 -32
  79. package/dist/components/ChatBox/index.d.ts +0 -15
  80. package/dist/components/ChatBox/index.js +0 -228
  81. package/dist/components/ChatButton/index.d.ts +0 -9
  82. package/dist/components/ChatButton/index.js +0 -17
  83. package/dist/components/ChatWindow/index.d.ts +0 -10
  84. package/dist/components/ChatWindow/index.js +0 -286
  85. package/dist/components/HomeView/index.d.ts +0 -12
  86. package/dist/components/HomeView/index.js +0 -51
  87. package/dist/components/UserList/index.d.ts +0 -13
  88. 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
+ }