ajaxter-chat 2.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +119 -128
  2. package/dist/components/BlockList/index.d.ts +10 -0
  3. package/dist/components/BlockList/index.js +33 -0
  4. package/dist/components/CallScreen/index.d.ts +13 -0
  5. package/dist/components/CallScreen/index.js +48 -0
  6. package/dist/components/ChatScreen/index.d.ts +10 -3
  7. package/dist/components/ChatScreen/index.js +142 -57
  8. package/dist/components/ChatWidget.js +192 -98
  9. package/dist/components/EmojiPicker/index.d.ts +8 -0
  10. package/dist/components/EmojiPicker/index.js +18 -0
  11. package/dist/components/HomeScreen/index.d.ts +2 -3
  12. package/dist/components/HomeScreen/index.js +25 -41
  13. package/dist/components/MaintenanceView/index.d.ts +0 -1
  14. package/dist/components/MaintenanceView/index.js +4 -6
  15. package/dist/components/RecentChatsScreen/index.d.ts +4 -3
  16. package/dist/components/RecentChatsScreen/index.js +7 -37
  17. package/dist/components/Tabs/BottomTabs.d.ts +1 -1
  18. package/dist/components/Tabs/BottomTabs.js +25 -20
  19. package/dist/components/TicketScreen/index.d.ts +3 -3
  20. package/dist/components/TicketScreen/index.js +39 -56
  21. package/dist/components/UserListScreen/index.d.ts +2 -4
  22. package/dist/components/UserListScreen/index.js +33 -62
  23. package/dist/config/index.d.ts +7 -3
  24. package/dist/config/index.js +28 -25
  25. package/dist/hooks/useChat.d.ts +8 -3
  26. package/dist/hooks/useChat.js +22 -18
  27. package/dist/hooks/useRemoteConfig.d.ts +6 -0
  28. package/dist/hooks/useRemoteConfig.js +26 -0
  29. package/dist/hooks/useWebRTC.d.ts +11 -0
  30. package/dist/hooks/useWebRTC.js +112 -0
  31. package/dist/index.d.ts +9 -5
  32. package/dist/index.js +8 -4
  33. package/dist/types/index.d.ts +62 -21
  34. package/dist/utils/chat.d.ts +13 -0
  35. package/dist/utils/chat.js +62 -0
  36. package/dist/utils/theme.d.ts +3 -1
  37. package/dist/utils/theme.js +14 -7
  38. package/package.json +4 -4
  39. package/public/chatData.json +162 -0
  40. package/src/components/BlockList/index.tsx +94 -0
  41. package/src/components/CallScreen/index.tsx +144 -0
  42. package/src/components/ChatScreen/index.tsx +403 -139
  43. package/src/components/ChatWidget.tsx +394 -250
  44. package/src/components/EmojiPicker/index.tsx +48 -0
  45. package/src/components/HomeScreen/index.tsx +58 -82
  46. package/src/components/MaintenanceView/index.tsx +6 -9
  47. package/src/components/RecentChatsScreen/index.tsx +51 -96
  48. package/src/components/Tabs/BottomTabs.tsx +45 -37
  49. package/src/components/TicketScreen/index.tsx +87 -133
  50. package/src/components/UserListScreen/index.tsx +75 -153
  51. package/src/config/index.ts +32 -26
  52. package/src/hooks/useChat.ts +31 -14
  53. package/src/hooks/useRemoteConfig.ts +26 -0
  54. package/src/hooks/useWebRTC.ts +130 -0
  55. package/src/index.ts +26 -15
  56. package/src/types/index.ts +85 -40
  57. package/src/utils/chat.ts +70 -0
  58. package/src/utils/theme.ts +18 -7
  59. package/dist/hooks/useUsers.d.ts +0 -7
  60. package/dist/hooks/useUsers.js +0 -26
  61. package/dist/services/userService.d.ts +0 -2
  62. package/dist/services/userService.js +0 -9
  63. package/dist/src/components/ChatScreen/index.d.ts +0 -12
  64. package/dist/src/components/ChatScreen/index.js +0 -83
  65. package/dist/src/components/ChatWidget.d.ts +0 -4
  66. package/dist/src/components/ChatWidget.js +0 -141
  67. package/dist/src/components/HomeScreen/index.d.ts +0 -9
  68. package/dist/src/components/HomeScreen/index.js +0 -71
  69. package/dist/src/components/MaintenanceView/index.d.ts +0 -7
  70. package/dist/src/components/MaintenanceView/index.js +0 -16
  71. package/dist/src/components/RecentChatsScreen/index.d.ts +0 -16
  72. package/dist/src/components/RecentChatsScreen/index.js +0 -38
  73. package/dist/src/components/Tabs/BottomTabs.d.ts +0 -10
  74. package/dist/src/components/Tabs/BottomTabs.js +0 -29
  75. package/dist/src/components/TicketScreen/index.d.ts +0 -9
  76. package/dist/src/components/TicketScreen/index.js +0 -71
  77. package/dist/src/components/UserListScreen/index.d.ts +0 -13
  78. package/dist/src/components/UserListScreen/index.js +0 -64
  79. package/dist/src/config/index.d.ts +0 -3
  80. package/dist/src/config/index.js +0 -38
  81. package/dist/src/hooks/useChat.d.ts +0 -8
  82. package/dist/src/hooks/useChat.js +0 -26
  83. package/dist/src/hooks/useUsers.d.ts +0 -7
  84. package/dist/src/hooks/useUsers.js +0 -26
  85. package/dist/src/index.d.ts +0 -14
  86. package/dist/src/index.js +0 -13
  87. package/dist/src/services/userService.d.ts +0 -2
  88. package/dist/src/services/userService.js +0 -9
  89. package/dist/src/types/index.d.ts +0 -59
  90. package/dist/src/types/index.js +0 -1
  91. package/dist/src/utils/theme.d.ts +0 -3
  92. package/dist/src/utils/theme.js +0 -13
  93. package/src/hooks/useUsers.ts +0 -27
  94. package/src/services/userService.ts +0 -9
@@ -1,170 +1,124 @@
1
1
  import React, { useState } from 'react';
2
- import { ChatWidgetTheme, Ticket } from '../../types';
3
- import { mergeTheme } from '../../utils/theme';
2
+ import { Ticket, WidgetConfig } from '../../types';
4
3
 
5
4
  interface TicketScreenProps {
6
- tickets: Ticket[];
7
- theme?: ChatWidgetTheme;
8
- onRaiseTicket: (title: string, description: string) => void;
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, theme, onRaiseTicket }) => {
12
- const t = mergeTheme(theme);
13
- const [showForm, setShowForm] = useState(false);
14
- const [title, setTitle] = useState('');
15
- const [desc, setDesc] = useState('');
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 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' },
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 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' },
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: 'flex', flexDirection: 'column', height: '100%' }}>
36
+ <div style={{ display:'flex', flexDirection:'column', height:'100%' }}>
38
37
  {/* Header */}
39
- <div style={{ backgroundColor: t.primaryColor, padding: '20px 20px 24px', flexShrink: 0 }}>
40
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
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: 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
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
- 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'}
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
- 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')}
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
- 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')}
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
- <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>
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
- {/* Ticket List */}
116
- <div style={{ flex: 1, overflowY: 'auto' }}>
88
+ {/* List */}
89
+ <div style={{ flex:1, overflowY:'auto' }}>
117
90
  {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
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, ChatWidgetTheme, UserListContext } from '../../types';
3
- import { mergeTheme } from '../../utils/theme';
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
- loading: boolean;
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, loading, error, theme, onBack, onSelectUser,
14
+ context, users, primaryColor, onBack, onSelectUser,
17
15
  }) => {
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';
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: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideInRight 0.25s ease' }}>
20
+ <div style={{ display:'flex', flexDirection:'column', height:'100%', animation:'cw-slideIn 0.22s ease' }}>
26
21
  {/* 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>
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: 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>
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
- {/* 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
- />
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
- // ── 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>
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 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>
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
  );
@@ -1,4 +1,9 @@
1
- import { ChatConfig, ChatStatus, ChatType } from '../types';
1
+ import { LocalEnvConfig, RemoteChatData } from '../types';
2
+
3
+ /** Default JSON endpoint; override with REACT_APP_CHAT_CONFIG_URL / NEXT_PUBLIC_CHAT_CONFIG_URL */
4
+ const DEFAULT_CHAT_DATA_BASE = 'https://window.mscorpres.com/TEST/chatData.json';
5
+ const DEMO_API_KEY = 'demo1234';
6
+ const DEMO_WIDGET_ID = 'demo';
2
7
 
3
8
  function getEnv(key: string): string | undefined {
4
9
  if (typeof process !== 'undefined' && process.env) {
@@ -12,35 +17,36 @@ function getEnv(key: string): string | undefined {
12
17
  return undefined;
13
18
  }
14
19
 
15
- function 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';
20
+ function getChatDataBaseUrl(): string {
21
+ return getEnv('CHAT_CONFIG_URL')?.trim() || DEFAULT_CHAT_DATA_BASE;
19
22
  }
20
23
 
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
- }
24
+ /**
25
+ * Loads remote widget config once via GET. Uses query params `key` and `widget` so the
26
+ * server can validate the request without custom headers (avoids CORS preflight failures).
27
+ */
28
+ export async function fetchRemoteChatData(
29
+ apiKey: string,
30
+ widgetId: string
31
+ ): Promise<RemoteChatData> {
32
+ const base = getChatDataBaseUrl();
33
+ const url = new URL(base, typeof window !== 'undefined' ? window.location.origin : 'https://localhost');
34
+ url.searchParams.set('key', apiKey);
35
+ url.searchParams.set('widget', widgetId);
26
36
 
27
- export function loadChatConfig(): ChatConfig {
28
- const portStr = getEnv('CHAT_HOST_PORT');
29
- const hostPort = portStr ? parseInt(portStr, 10) : null;
37
+ const res = await fetch(url.toString(), {
38
+ method: 'GET',
39
+ credentials: 'omit',
40
+ mode: 'cors',
41
+ headers: { Accept: 'application/json' },
42
+ });
43
+ if (!res.ok) throw new Error(`Failed to load chat config: ${res.status}`);
44
+ return res.json() as Promise<RemoteChatData>;
45
+ }
30
46
 
47
+ export function loadLocalConfig(): LocalEnvConfig {
31
48
  return {
32
- 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')),
49
+ apiKey: getEnv('CHAT_API_KEY') ?? DEMO_API_KEY,
50
+ widgetId: getEnv('CHAT_WIDGET_ID') ?? DEMO_WIDGET_ID,
37
51
  };
38
52
  }
39
-
40
- export function buildUserListUrl(config: ChatConfig): string {
41
- const base = config.hostUrl.replace(/\/$/, '');
42
- const endpoint = config.userListEndpoint.replace(/^\//, '');
43
- // Port is optional
44
- const portPart = config.hostPort ? `:${config.hostPort}` : '';
45
- return `${base}${portPart}/${endpoint}`;
46
- }