ajaxter-chat 1.0.3 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +124 -241
  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 +19 -0
  7. package/dist/components/ChatScreen/index.js +168 -0
  8. package/dist/components/ChatWidget.d.ts +0 -24
  9. package/dist/components/ChatWidget.js +228 -43
  10. package/dist/components/EmojiPicker/index.d.ts +8 -0
  11. package/dist/components/EmojiPicker/index.js +18 -0
  12. package/dist/components/HomeScreen/index.d.ts +8 -0
  13. package/dist/components/HomeScreen/index.js +55 -0
  14. package/dist/components/MaintenanceView/index.d.ts +0 -1
  15. package/dist/components/MaintenanceView/index.js +13 -52
  16. package/dist/components/RecentChatsScreen/index.d.ts +17 -0
  17. package/dist/components/RecentChatsScreen/index.js +8 -0
  18. package/dist/components/Tabs/BottomTabs.d.ts +10 -0
  19. package/dist/components/Tabs/BottomTabs.js +34 -0
  20. package/dist/components/TicketScreen/index.d.ts +9 -0
  21. package/dist/components/TicketScreen/index.js +54 -0
  22. package/dist/components/UserListScreen/index.d.ts +11 -0
  23. package/dist/components/UserListScreen/index.js +35 -0
  24. package/dist/config/index.d.ts +3 -16
  25. package/dist/config/index.js +20 -103
  26. package/dist/hooks/useChat.d.ts +10 -9
  27. package/dist/hooks/useChat.js +22 -40
  28. package/dist/hooks/useRemoteConfig.d.ts +6 -0
  29. package/dist/hooks/useRemoteConfig.js +22 -0
  30. package/dist/hooks/useWebRTC.d.ts +11 -0
  31. package/dist/hooks/useWebRTC.js +112 -0
  32. package/dist/index.d.ts +16 -11
  33. package/dist/index.js +15 -16
  34. package/dist/types/index.d.ts +66 -38
  35. package/dist/utils/chat.d.ts +13 -0
  36. package/dist/utils/chat.js +62 -0
  37. package/dist/utils/theme.d.ts +3 -2
  38. package/dist/utils/theme.js +13 -21
  39. package/package.json +10 -20
  40. package/public/chatData.json +162 -0
  41. package/src/components/BlockList/index.tsx +94 -0
  42. package/src/components/CallScreen/index.tsx +144 -0
  43. package/src/components/ChatScreen/index.tsx +469 -0
  44. package/src/components/ChatWidget.tsx +471 -0
  45. package/src/components/EmojiPicker/index.tsx +48 -0
  46. package/src/components/HomeScreen/index.tsx +106 -0
  47. package/src/components/MaintenanceView/index.tsx +38 -0
  48. package/src/components/RecentChatsScreen/index.tsx +63 -0
  49. package/src/components/Tabs/BottomTabs.tsx +90 -0
  50. package/src/components/TicketScreen/index.tsx +124 -0
  51. package/src/components/UserListScreen/index.tsx +103 -0
  52. package/src/config/index.ts +40 -0
  53. package/src/hooks/useChat.ts +48 -0
  54. package/src/hooks/useRemoteConfig.ts +20 -0
  55. package/src/hooks/useWebRTC.ts +130 -0
  56. package/src/index.ts +29 -0
  57. package/src/types/index.ts +127 -0
  58. package/src/utils/chat.ts +70 -0
  59. package/src/utils/theme.ts +27 -0
  60. package/dist/components/BottomNav/index.d.ts +0 -10
  61. package/dist/components/BottomNav/index.js +0 -32
  62. package/dist/components/ChatBox/index.d.ts +0 -15
  63. package/dist/components/ChatBox/index.js +0 -228
  64. package/dist/components/ChatButton/index.d.ts +0 -9
  65. package/dist/components/ChatButton/index.js +0 -17
  66. package/dist/components/ChatWindow/index.d.ts +0 -10
  67. package/dist/components/ChatWindow/index.js +0 -286
  68. package/dist/components/HomeView/index.d.ts +0 -12
  69. package/dist/components/HomeView/index.js +0 -51
  70. package/dist/components/UserList/index.d.ts +0 -13
  71. package/dist/components/UserList/index.js +0 -136
  72. package/dist/hooks/useUsers.d.ts +0 -14
  73. package/dist/hooks/useUsers.js +0 -32
  74. package/dist/services/userService.d.ts +0 -7
  75. package/dist/services/userService.js +0 -18
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { ChatUser, WidgetConfig } from '../../types';
3
+ import { avatarColor, initials, formatTime } from '../../utils/chat';
4
+
5
+ interface RecentChat {
6
+ id: string; user: ChatUser; lastMessage: string; lastTime: string; unread: number; isPaused: boolean;
7
+ }
8
+
9
+ interface RecentChatsScreenProps {
10
+ chats: RecentChat[];
11
+ config: WidgetConfig;
12
+ onSelectChat: (user: ChatUser) => void;
13
+ }
14
+
15
+ export const RecentChatsScreen: React.FC<RecentChatsScreenProps> = ({ chats, config, onSelectChat }) => (
16
+ <div style={{ display:'flex', flexDirection:'column', height:'100%' }}>
17
+ <div style={{ background:`linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`, padding:'18px 18px 22px', flexShrink:0 }}>
18
+ <h2 style={{ margin:0, fontSize:20, fontWeight:800, color:'#fff', letterSpacing:'-0.02em' }}>Recent Chats</h2>
19
+ <p style={{ margin:'3px 0 0', fontSize:12, color:'rgba(255,255,255,0.8)' }}>Your conversation history</p>
20
+ </div>
21
+
22
+ <div style={{ flex:1, overflowY:'auto' }}>
23
+ {chats.length === 0 ? (
24
+ <div style={{ padding:'50px 24px', textAlign:'center' }}>
25
+ <div style={{ fontSize:36, marginBottom:10 }}>💬</div>
26
+ <div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>No chats yet</div>
27
+ <div style={{ fontSize:13, color:'#7b8fa1' }}>Start a conversation from home</div>
28
+ </div>
29
+ ) : chats.map((chat, i) => (
30
+ <button key={chat.id} onClick={() => onSelectChat(chat.user)} style={{
31
+ width:'100%', padding:'13px 16px', display:'flex', alignItems:'center', gap:13,
32
+ background:'transparent', border:'none', borderBottom:'1px solid #f0f2f5',
33
+ cursor:'pointer', textAlign:'left', animation:`cw-fadeUp 0.28s ease both`, animationDelay:`${i*0.05}s`,
34
+ transition:'background 0.14s',
35
+ }}
36
+ onMouseEnter={e=>(e.currentTarget as HTMLElement).style.background='#f8faff'}
37
+ onMouseLeave={e=>(e.currentTarget as HTMLElement).style.background='transparent'}
38
+ >
39
+ <div style={{ position:'relative', flexShrink:0 }}>
40
+ <div style={{ width:46, height:46, borderRadius:'50%', backgroundColor:avatarColor(chat.user.name), display:'flex', alignItems:'center', justifyContent:'center', color:'#fff', fontWeight:700, fontSize:15 }}>
41
+ {initials(chat.user.name)}
42
+ </div>
43
+ {chat.unread > 0 && (
44
+ <span style={{ position:'absolute', top:-2, right:-2, width:18, height:18, borderRadius:'50%', background:'#ef4444', color:'#fff', fontSize:10, fontWeight:700, display:'flex', alignItems:'center', justifyContent:'center', border:'2px solid #fff' }}>
45
+ {chat.unread}
46
+ </span>
47
+ )}
48
+ </div>
49
+ <div style={{ flex:1, minWidth:0 }}>
50
+ <div style={{ display:'flex', justifyContent:'space-between', marginBottom:3 }}>
51
+ <span style={{ fontWeight:700, fontSize:14, color:'#1a2332' }}>{chat.user.name}</span>
52
+ <span style={{ fontSize:11, color:'#b0bec5' }}>{formatTime(chat.lastTime)}</span>
53
+ </div>
54
+ <div style={{ fontSize:13, color:'#7b8fa1', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', display:'flex', alignItems:'center', gap:5 }}>
55
+ {chat.isPaused && <span style={{ fontSize:10, background:'#fef3c7', color:'#92400e', padding:'1px 5px', borderRadius:4, fontWeight:700 }}>PAUSED</span>}
56
+ {chat.lastMessage}
57
+ </div>
58
+ </div>
59
+ </button>
60
+ ))}
61
+ </div>
62
+ </div>
63
+ );
@@ -0,0 +1,90 @@
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
+ onBlockList: () => void;
9
+ }
10
+
11
+ export const BottomTabs: React.FC<BottomTabsProps> = ({ active, onChange, primaryColor, onBlockList }) => {
12
+ const tabs: { key: BottomTab; label: string; Icon: React.FC<{ a: boolean; c: 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', borderTop: '1px solid #eef0f5',
21
+ backgroundColor: '#fff', flexShrink: 0,
22
+ }}>
23
+ {tabs.map(tab => {
24
+ const isActive = active === tab.key;
25
+ return (
26
+ <button
27
+ key={tab.key}
28
+ onClick={() => onChange(tab.key)}
29
+ style={{
30
+ flex: 1, padding: '10px 0 8px',
31
+ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
32
+ background: 'transparent', border: 'none', cursor: 'pointer',
33
+ fontSize: '10px', fontWeight: isActive ? 700 : 500,
34
+ color: isActive ? primaryColor : '#9aa3af',
35
+ borderTop: isActive ? `2px solid ${primaryColor}` : '2px solid transparent',
36
+ transition: 'color 0.15s',
37
+ fontFamily: 'inherit',
38
+ }}
39
+ >
40
+ <tab.Icon a={isActive} c={isActive ? primaryColor : '#b0bec5'} />
41
+ {tab.label}
42
+ </button>
43
+ );
44
+ })}
45
+
46
+ {/* Block list icon */}
47
+ <button
48
+ onClick={onBlockList}
49
+ style={{
50
+ padding: '10px 14px 8px',
51
+ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
52
+ background: 'transparent', border: 'none', cursor: 'pointer',
53
+ fontSize: '10px', fontWeight: 500, color: '#9aa3af',
54
+ borderTop: '2px solid transparent',
55
+ transition: 'color 0.15s', fontFamily: 'inherit',
56
+ }}
57
+ title="Block List"
58
+ >
59
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
60
+ <circle cx="12" cy="12" r="10" stroke="#b0bec5" strokeWidth="1.8" />
61
+ <line x1="4.93" y1="4.93" x2="19.07" y2="19.07" stroke="#b0bec5" strokeWidth="1.8" strokeLinecap="round" />
62
+ </svg>
63
+ Blocked
64
+ </button>
65
+ </div>
66
+ );
67
+ };
68
+
69
+ const HomeIcon: React.FC<{ a: boolean; c: string }> = ({ a, c }) => (
70
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
71
+ <path d="M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z"
72
+ stroke={c} strokeWidth={a ? 2.2 : 1.8} fill={a ? `${c}20` : 'none'} strokeLinecap="round" strokeLinejoin="round" />
73
+ <path d="M9 21V12h6v9" stroke={c} strokeWidth={a ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round" />
74
+ </svg>
75
+ );
76
+
77
+ const ChatsIcon: React.FC<{ a: boolean; c: string }> = ({ a, c }) => (
78
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
79
+ <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"
80
+ stroke={c} strokeWidth={a ? 2.2 : 1.8} fill={a ? `${c}20` : 'none'} strokeLinecap="round" strokeLinejoin="round" />
81
+ </svg>
82
+ );
83
+
84
+ const TicketsIcon: React.FC<{ a: boolean; c: string }> = ({ a, c }) => (
85
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
86
+ <path d="M15 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V9l-4-4z"
87
+ stroke={c} strokeWidth={a ? 2.2 : 1.8} fill={a ? `${c}20` : 'none'} strokeLinecap="round" strokeLinejoin="round" />
88
+ <path d="M15 5v4h4M9 13h6M9 17h4" stroke={c} strokeWidth={a ? 2.2 : 1.8} strokeLinecap="round" />
89
+ </svg>
90
+ );
@@ -0,0 +1,124 @@
1
+ import React, { useState } from 'react';
2
+ import { Ticket, WidgetConfig } from '../../types';
3
+
4
+ interface TicketScreenProps {
5
+ tickets: Ticket[];
6
+ config: WidgetConfig;
7
+ onRaiseTicket: (title: string, desc: string, priority: Ticket['priority']) => void;
8
+ }
9
+
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');
15
+
16
+ const handleSubmit = () => {
17
+ if (!title.trim()) return;
18
+ onRaiseTicket(title.trim(), desc.trim(), priority);
19
+ setTitle(''); setDesc(''); setPriority('medium'); setShowForm(false);
20
+ };
21
+
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' },
27
+ };
28
+
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' },
33
+ };
34
+
35
+ return (
36
+ <div style={{ display:'flex', flexDirection:'column', height:'100%' }}>
37
+ {/* Header */}
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' }}>
43
+ <div>
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
+ </p>
48
+ </div>
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'}
55
+ </button>
56
+ </div>
57
+ </div>
58
+
59
+ {/* New Ticket Form */}
60
+ {showForm && (
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')}
64
+ />
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')}
67
+ />
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>
85
+ </div>
86
+ )}
87
+
88
+ {/* List */}
89
+ <div style={{ flex:1, overflowY:'auto' }}>
90
+ {tickets.length === 0 ? (
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>
109
+ </div>
110
+ </div>
111
+ ))}
112
+ </div>
113
+ </div>
114
+ );
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
+ }
@@ -0,0 +1,103 @@
1
+ import React from 'react';
2
+ import { ChatUser, UserListContext } from '../../types';
3
+ import { avatarColor, initials } from '../../utils/chat';
4
+
5
+ interface UserListScreenProps {
6
+ context: UserListContext;
7
+ users: ChatUser[];
8
+ primaryColor: string;
9
+ onBack: () => void;
10
+ onSelectUser: (user: ChatUser) => void;
11
+ }
12
+
13
+ export const UserListScreen: React.FC<UserListScreenProps> = ({
14
+ context, users, primaryColor, onBack, onSelectUser,
15
+ }) => {
16
+ const title = context === 'support' ? 'Need Support' : 'New Conversation';
17
+ const subtitle = context === 'support' ? 'Choose a support agent' : 'Choose a colleague';
18
+
19
+ return (
20
+ <div style={{ display:'flex', flexDirection:'column', height:'100%', animation:'cw-slideIn 0.22s ease' }}>
21
+ {/* Header */}
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} />
24
+ <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>
27
+ </div>
28
+ </div>
29
+
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>
79
+ ))}
80
+ </div>
81
+ </div>
82
+ );
83
+ };
84
+
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>
95
+ );
96
+
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>
102
+ </div>
103
+ );
@@ -0,0 +1,40 @@
1
+ import { LocalEnvConfig, RemoteChatData } from '../types';
2
+
3
+ const REMOTE_URL = 'https://window.mscorpres.com/TEST/chatData.json';
4
+ const DEMO_API_KEY = 'demo1234';
5
+ const DEMO_WIDGET_ID = 'demo';
6
+
7
+ function getEnv(key: string): string | undefined {
8
+ if (typeof process !== 'undefined' && process.env) {
9
+ return (
10
+ process.env[`NEXT_PUBLIC_${key}`] ??
11
+ process.env[`REACT_APP_${key}`] ??
12
+ process.env[key] ??
13
+ undefined
14
+ );
15
+ }
16
+ return undefined;
17
+ }
18
+
19
+ export function loadLocalConfig(): LocalEnvConfig {
20
+ return {
21
+ apiKey: getEnv('CHAT_API_KEY') ?? DEMO_API_KEY,
22
+ widgetId: getEnv('CHAT_WIDGET_ID') ?? DEMO_WIDGET_ID,
23
+ };
24
+ }
25
+
26
+ export async function fetchRemoteChatData(
27
+ apiKey: string,
28
+ widgetId: string
29
+ ): Promise<RemoteChatData> {
30
+ const url = REMOTE_URL;
31
+ const res = await fetch(url, {
32
+ headers: {
33
+ 'X-Chat-Api-Key': apiKey,
34
+ 'X-Chat-Widget-Id': widgetId,
35
+ 'Content-Type': 'application/json',
36
+ },
37
+ });
38
+ if (!res.ok) throw new Error(`Failed to load chat config: ${res.status}`);
39
+ return res.json() as Promise<RemoteChatData>;
40
+ }
@@ -0,0 +1,48 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { ChatMessage, ChatUser } from '../types';
3
+
4
+ export function useChat(initialMessages: ChatMessage[] = []) {
5
+ const [messages, setMessages] = useState<ChatMessage[]>(initialMessages);
6
+ const [activeUser, setActiveUser] = useState<ChatUser | null>(null);
7
+ const [isPaused, setIsPaused] = useState(false);
8
+ const [isReported, setIsReported] = useState(false);
9
+
10
+ const selectUser = useCallback((user: ChatUser, history: ChatMessage[] = []) => {
11
+ setActiveUser(user);
12
+ setMessages(history);
13
+ setIsPaused(false);
14
+ setIsReported(false);
15
+ // TODO: socket.emit('join', { roomId: user.uid });
16
+ // TODO: socket.on('message', msg => setMessages(prev => [...prev, msg]));
17
+ }, []);
18
+
19
+ const sendMessage = useCallback((
20
+ text: string,
21
+ type: ChatMessage['type'] = 'text',
22
+ extra: Partial<ChatMessage> = {}
23
+ ) => {
24
+ if (!activeUser || isPaused) return;
25
+ const msg: ChatMessage = {
26
+ id: `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`,
27
+ senderId: 'me',
28
+ receiverId: activeUser.uid,
29
+ text,
30
+ timestamp: new Date().toISOString(),
31
+ type,
32
+ status: 'sent',
33
+ ...extra,
34
+ };
35
+ setMessages(prev => [...prev, msg]);
36
+ // TODO: socket.emit('message', msg);
37
+ }, [activeUser, isPaused]);
38
+
39
+ const togglePause = useCallback(() => setIsPaused(p => !p), []);
40
+ const reportChat = useCallback(() => { setIsReported(true); /* TODO: API call */ }, []);
41
+ const clearChat = useCallback(() => { setMessages([]); setActiveUser(null); }, []);
42
+
43
+ return {
44
+ messages, activeUser, isPaused, isReported,
45
+ selectUser, sendMessage, togglePause, reportChat, clearChat,
46
+ setMessages,
47
+ };
48
+ }
@@ -0,0 +1,20 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { RemoteChatData } from '../types';
3
+ import { fetchRemoteChatData } from '../config';
4
+
5
+ export function useRemoteConfig(apiKey: string, widgetId: string) {
6
+ const [data, setData] = useState<RemoteChatData | null>(null);
7
+ const [loading, setLoading] = useState(true);
8
+ const [error, setError] = useState<string | null>(null);
9
+
10
+ useEffect(() => {
11
+ let cancelled = false;
12
+ setLoading(true);
13
+ fetchRemoteChatData(apiKey, widgetId)
14
+ .then(d => { if (!cancelled) { setData(d); setLoading(false); } })
15
+ .catch(e => { if (!cancelled) { setError(e.message); setLoading(false); } });
16
+ return () => { cancelled = true; };
17
+ }, [apiKey, widgetId]);
18
+
19
+ return { data, loading, error };
20
+ }
@@ -0,0 +1,130 @@
1
+ import { useState, useRef, useCallback, useEffect } from 'react';
2
+ import { CallSession, CallState, ChatUser } from '../types';
3
+
4
+ const ICE_SERVERS: RTCIceServer[] = [
5
+ { urls: 'stun:stun.l.google.com:19302' },
6
+ { urls: 'stun:stun1.l.google.com:19302' },
7
+ ];
8
+
9
+ export function useWebRTC() {
10
+ const [session, setSession] = useState<CallSession>({
11
+ state: 'idle',
12
+ peer: null,
13
+ startedAt: null,
14
+ isMuted: false,
15
+ isCameraOn: false,
16
+ });
17
+
18
+ const pcRef = useRef<RTCPeerConnection | null>(null);
19
+ const localStream = useRef<MediaStream | null>(null);
20
+ const localVideoRef = useRef<HTMLVideoElement | null>(null);
21
+ const remoteVideoRef = useRef<HTMLVideoElement | null>(null);
22
+
23
+ const updateSession = (patch: Partial<CallSession>) =>
24
+ setSession(prev => ({ ...prev, ...patch }));
25
+
26
+ /** Start an outgoing call */
27
+ const startCall = useCallback(async (peer: ChatUser, withVideo = false) => {
28
+ updateSession({ state: 'calling', peer });
29
+
30
+ const stream = await navigator.mediaDevices.getUserMedia({
31
+ audio: true,
32
+ video: withVideo,
33
+ });
34
+ localStream.current = stream;
35
+ if (localVideoRef.current) localVideoRef.current.srcObject = stream;
36
+
37
+ const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
38
+ pcRef.current = pc;
39
+
40
+ stream.getTracks().forEach(t => pc.addTrack(t, stream));
41
+
42
+ pc.ontrack = e => {
43
+ if (remoteVideoRef.current) remoteVideoRef.current.srcObject = e.streams[0];
44
+ updateSession({ state: 'connected', startedAt: new Date(), isCameraOn: withVideo });
45
+ };
46
+
47
+ pc.onicecandidate = e => {
48
+ if (e.candidate) {
49
+ // TODO: socket.emit('ice-candidate', { candidate: e.candidate, to: peer.uid });
50
+ console.log('[WebRTC] ICE candidate ready — send via signalling:', e.candidate);
51
+ }
52
+ };
53
+
54
+ const offer = await pc.createOffer();
55
+ await pc.setLocalDescription(offer);
56
+ // TODO: socket.emit('call-offer', { offer, to: peer.uid, from: 'me' });
57
+ console.log('[WebRTC] Offer created — send via signalling server:', offer);
58
+ }, []);
59
+
60
+ /** Accept an incoming call offer */
61
+ const acceptCall = useCallback(async (
62
+ offer: RTCSessionDescriptionInit,
63
+ peer: ChatUser,
64
+ withVideo = false
65
+ ) => {
66
+ updateSession({ state: 'connected', peer, startedAt: new Date(), isCameraOn: withVideo });
67
+
68
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: withVideo });
69
+ localStream.current = stream;
70
+ if (localVideoRef.current) localVideoRef.current.srcObject = stream;
71
+
72
+ const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
73
+ pcRef.current = pc;
74
+ stream.getTracks().forEach(t => pc.addTrack(t, stream));
75
+ pc.ontrack = e => {
76
+ if (remoteVideoRef.current) remoteVideoRef.current.srcObject = e.streams[0];
77
+ };
78
+ pc.onicecandidate = e => {
79
+ if (e.candidate) {
80
+ // TODO: socket.emit('ice-candidate', { candidate: e.candidate, to: peer.uid });
81
+ }
82
+ };
83
+
84
+ await pc.setRemoteDescription(offer);
85
+ const answer = await pc.createAnswer();
86
+ await pc.setLocalDescription(answer);
87
+ // TODO: socket.emit('call-answer', { answer, to: peer.uid });
88
+ }, []);
89
+
90
+ /** Hang up */
91
+ const endCall = useCallback(() => {
92
+ localStream.current?.getTracks().forEach(t => t.stop());
93
+ pcRef.current?.close();
94
+ pcRef.current = null;
95
+ localStream.current = null;
96
+ updateSession({ state: 'ended', peer: null, startedAt: null });
97
+ setTimeout(() => updateSession({ state: 'idle' }), 1800);
98
+ }, []);
99
+
100
+ const toggleMute = useCallback(() => {
101
+ if (!localStream.current) return;
102
+ const enabled = !session.isMuted;
103
+ localStream.current.getAudioTracks().forEach(t => { t.enabled = enabled; });
104
+ updateSession({ isMuted: !session.isMuted });
105
+ }, [session.isMuted]);
106
+
107
+ const toggleCamera = useCallback(() => {
108
+ if (!localStream.current) return;
109
+ const enabled = !session.isCameraOn;
110
+ localStream.current.getVideoTracks().forEach(t => { t.enabled = enabled; });
111
+ updateSession({ isCameraOn: enabled });
112
+ }, [session.isCameraOn]);
113
+
114
+ // Cleanup on unmount
115
+ useEffect(() => () => {
116
+ localStream.current?.getTracks().forEach(t => t.stop());
117
+ pcRef.current?.close();
118
+ }, []);
119
+
120
+ return {
121
+ session,
122
+ localVideoRef,
123
+ remoteVideoRef,
124
+ startCall,
125
+ acceptCall,
126
+ endCall,
127
+ toggleMute,
128
+ toggleCamera,
129
+ };
130
+ }
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
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 { BlockListScreen } from './components/BlockList';
8
+ export { CallScreen } from './components/CallScreen';
9
+ export { MaintenanceView } from './components/MaintenanceView';
10
+ export { BottomTabs } from './components/Tabs/BottomTabs';
11
+ export { EmojiPicker } from './components/EmojiPicker';
12
+
13
+ export { useChat } from './hooks/useChat';
14
+ export { useWebRTC } from './hooks/useWebRTC';
15
+ export { useRemoteConfig } from './hooks/useRemoteConfig';
16
+
17
+ export { loadLocalConfig, fetchRemoteChatData } from './config';
18
+ export { mergeTheme, darken } from './utils/theme';
19
+ export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText } from './utils/chat';
20
+
21
+ export type {
22
+ ChatWidgetProps, ChatWidgetTheme,
23
+ WidgetConfig, RemoteChatData,
24
+ ChatUser, ChatMessage, Ticket, RecentChat,
25
+ CallSession, CallState,
26
+ ChatStatus, ChatType, UserType, OnlineStatus,
27
+ Screen, BottomTab, UserListContext, MessageType,
28
+ LocalEnvConfig,
29
+ } from './types';