ajaxter-chat 2.0.1 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +3 -3
  24. package/dist/config/index.js +18 -26
  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 +22 -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 +22 -28
  52. package/src/hooks/useChat.ts +31 -14
  53. package/src/hooks/useRemoteConfig.ts +20 -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
@@ -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 CHANGED
@@ -1,18 +1,29 @@
1
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';
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
+
14
21
  export type {
15
- ChatUser, ChatMessage, ChatConfig, ChatWidgetTheme,
16
- ChatWidgetProps, ChatStatus, ChatType, UserType,
17
- Screen, BottomTab, UserListContext, Ticket,
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,
18
29
  } from './types';
@@ -1,80 +1,125 @@
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
1
+ // ─── Remote Config (from chatData.json) ────────────────────────────────────
2
+ export interface WidgetConfig {
3
+ id: string;
4
+ apiKey: string;
5
+ status: ChatStatus;
6
+ chatType: ChatType;
7
+ primaryColor: string;
8
+ buttonLabel: string;
9
+ buttonPosition: 'bottom-right' | 'bottom-left';
10
+ welcomeTitle: string;
11
+ welcomeSubtitle: string;
12
+ allowVoiceMessage: boolean;
13
+ allowAttachment: boolean;
14
+ allowEmoji: boolean;
15
+ allowWebCall: boolean;
16
+ maxEmojiCount: number;
17
+ allowTranscriptDownload: boolean;
18
+ allowReport: boolean;
19
+ allowBlock: boolean;
20
+ }
15
21
 
16
- // ─── Bottom Tabs ──────────────────────────────────────────────────────────────
17
- export type BottomTab = 'home' | 'chats' | 'tickets';
22
+ export interface RemoteChatData {
23
+ widget: WidgetConfig;
24
+ developers: ChatUser[];
25
+ users: ChatUser[];
26
+ sampleChats: Record<string, ChatMessage[]>;
27
+ sampleTickets: Ticket[];
28
+ blockedUsers: string[];
29
+ }
18
30
 
19
- // ─── Widget Size ──────────────────────────────────────────────────────────────
20
- export type WidgetSize = 'normal' | 'maximized';
31
+ // ─── Enums ──────────────────────────────────────────────────────────────────
32
+ export type ChatStatus = 'ACTIVE' | 'DISABLE' | 'MAINTENANCE';
33
+ export type ChatType = 'SUPPORT' | 'CHAT' | 'BOTH';
34
+ export type UserType = 'developer' | 'user';
35
+ export type OnlineStatus = 'online' | 'away' | 'offline';
36
+ export type BottomTab = 'home' | 'chats' | 'tickets';
37
+ export type Screen =
38
+ | 'home'
39
+ | 'user-list'
40
+ | 'chat'
41
+ | 'recent-chats'
42
+ | 'tickets'
43
+ | 'block-list'
44
+ | 'call';
45
+ export type UserListContext = 'support' | 'conversation';
46
+ export type MessageType = 'text' | 'voice' | 'attachment' | 'emoji';
21
47
 
22
- // ─── Data Models ──────────────────────────────────────────────────────────────
48
+ // ─── User ───────────────────────────────────────────────────────────────────
23
49
  export interface ChatUser {
24
- name: string;
25
50
  uid: string;
51
+ name: string;
26
52
  email: string;
27
53
  mobile: string;
28
54
  project: string;
29
55
  type: UserType;
56
+ avatar: string | null;
57
+ status: OnlineStatus;
58
+ designation: string;
30
59
  }
31
60
 
61
+ // ─── Message ────────────────────────────────────────────────────────────────
32
62
  export interface ChatMessage {
33
63
  id: string;
34
64
  senderId: string;
35
65
  receiverId: string;
36
66
  text: string;
37
- timestamp: Date;
67
+ timestamp: string;
68
+ type: MessageType;
38
69
  status: 'sent' | 'delivered' | 'read';
70
+ attachmentName?: string;
71
+ attachmentSize?: string;
72
+ voiceDuration?: number; // seconds
73
+ }
74
+
75
+ // ─── Ticket ─────────────────────────────────────────────────────────────────
76
+ export interface Ticket {
77
+ id: string;
78
+ title: string;
79
+ description: string;
80
+ status: 'open' | 'in-progress' | 'resolved' | 'closed';
81
+ priority: 'low' | 'medium' | 'high';
82
+ createdAt: string;
83
+ updatedAt: string;
84
+ assignedTo: string | null;
39
85
  }
40
86
 
87
+ // ─── Recent Chat ─────────────────────────────────────────────────────────────
41
88
  export interface RecentChat {
42
89
  id: string;
43
90
  user: ChatUser;
44
91
  lastMessage: string;
45
- lastTime: Date;
92
+ lastTime: string;
46
93
  unread: number;
94
+ isPaused: boolean;
47
95
  }
48
96
 
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;
97
+ // ─── Call ───────────────────────────────────────────────────────────────────
98
+ export type CallState = 'idle' | 'calling' | 'connected' | 'ended';
99
+
100
+ export interface CallSession {
101
+ state: CallState;
102
+ peer: ChatUser | null;
103
+ startedAt: Date | null;
104
+ isMuted: boolean;
105
+ isCameraOn: boolean;
57
106
  }
58
107
 
59
- // ─── Config ───────────────────────────────────────────────────────────────────
60
- export interface ChatConfig {
61
- hostUrl: string;
62
- hostPort: number | null; // optional
63
- userListEndpoint: string;
64
- status: ChatStatus;
65
- chatType: ChatType;
108
+ // ─── Local env config ────────────────────────────────────────────────────────
109
+ export interface LocalEnvConfig {
110
+ apiKey: string;
111
+ widgetId: string;
66
112
  }
67
113
 
68
- // ─── Theme ────────────────────────────────────────────────────────────────────
114
+ // ─── Theme prop (still overridable) ─────────────────────────────────────────
69
115
  export interface ChatWidgetTheme {
70
- primaryColor?: string; // teal header + accents (default: #1aaa96)
116
+ primaryColor?: string;
71
117
  fontFamily?: string;
72
118
  buttonColor?: string;
73
119
  buttonTextColor?: string;
74
120
  buttonLabel?: string;
75
121
  buttonPosition?: 'bottom-right' | 'bottom-left';
76
122
  borderRadius?: string;
77
- backgroundColor?: string;
78
123
  }
79
124
 
80
125
  export interface ChatWidgetProps {
@@ -0,0 +1,70 @@
1
+ import { ChatMessage, ChatUser } from '../types';
2
+
3
+ /** Generate a consistent avatar color from a name */
4
+ export function avatarColor(name: string): string {
5
+ const palette = [
6
+ '#2563EB','#7C3AED','#059669','#D97706',
7
+ '#DC2626','#0891B2','#4F46E5','#BE185D',
8
+ ];
9
+ let hash = 0;
10
+ for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
11
+ return palette[Math.abs(hash) % palette.length];
12
+ }
13
+
14
+ /** Get initials from full name */
15
+ export function initials(name: string): string {
16
+ return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
17
+ }
18
+
19
+ /** Format timestamp to readable time string */
20
+ export function formatTime(ts: string | Date): string {
21
+ return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
22
+ }
23
+
24
+ /** Format date for chat separators */
25
+ export function formatDate(ts: string | Date): string {
26
+ const d = new Date(ts);
27
+ const today = new Date();
28
+ const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1);
29
+ if (d.toDateString() === today.toDateString()) return 'Today';
30
+ if (d.toDateString() === yesterday.toDateString()) return 'Yesterday';
31
+ return d.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' });
32
+ }
33
+
34
+ /** Generate a plain-text transcript from messages */
35
+ export function generateTranscript(
36
+ messages: ChatMessage[],
37
+ peer: ChatUser,
38
+ myName = 'Me'
39
+ ): string {
40
+ const header = [
41
+ '═══════════════════════════════════════',
42
+ ' CHAT TRANSCRIPT',
43
+ ` Conversation with: ${peer.name} (${peer.email})`,
44
+ ` Downloaded: ${new Date().toLocaleString()}`,
45
+ '═══════════════════════════════════════',
46
+ '',
47
+ ].join('\n');
48
+
49
+ const body = messages.map(m => {
50
+ const sender = m.senderId === 'me' ? myName : peer.name;
51
+ const time = new Date(m.timestamp).toLocaleString();
52
+ const label = m.type === 'voice' ? '[Voice Message]'
53
+ : m.type === 'attachment' ? `[Attachment: ${m.attachmentName ?? 'file'}]`
54
+ : m.text;
55
+ return `[${time}] ${sender}: ${label}`;
56
+ }).join('\n');
57
+
58
+ return header + body;
59
+ }
60
+
61
+ /** Trigger a file download in the browser */
62
+ export function downloadText(content: string, filename: string) {
63
+ const blob = new Blob([content], { type: 'text/plain' });
64
+ const url = URL.createObjectURL(blob);
65
+ const a = document.createElement('a');
66
+ a.href = url;
67
+ a.download = filename;
68
+ a.click();
69
+ URL.revokeObjectURL(url);
70
+ }
@@ -1,16 +1,27 @@
1
1
  import { ChatWidgetTheme } from '../types';
2
2
 
3
3
  export const defaultTheme: Required<ChatWidgetTheme> = {
4
- primaryColor: '#1aaa96',
4
+ primaryColor: '#2563EB',
5
5
  fontFamily: "'DM Sans', 'Segoe UI', sans-serif",
6
- buttonColor: '#1aaa96',
6
+ buttonColor: '#2563EB',
7
7
  buttonTextColor: '#ffffff',
8
- buttonLabel: 'Chat with us',
8
+ buttonLabel: 'Support',
9
9
  buttonPosition: 'bottom-right',
10
- borderRadius: '16px',
11
- backgroundColor: '#ffffff',
10
+ borderRadius: '0px', // drawer uses 16px only on top-left / bottom-left
12
11
  };
13
12
 
14
- export function mergeTheme(custom?: ChatWidgetTheme): Required<ChatWidgetTheme> {
15
- return { ...defaultTheme, ...custom };
13
+ export function mergeTheme(
14
+ remote?: Partial<ChatWidgetTheme>,
15
+ local?: ChatWidgetTheme
16
+ ): Required<ChatWidgetTheme> {
17
+ return { ...defaultTheme, ...remote, ...local };
18
+ }
19
+
20
+ /** Darken a hex color by `amount` (0-255) */
21
+ export function darken(hex: string, amount = 20): string {
22
+ const n = parseInt(hex.replace('#', ''), 16);
23
+ const r = Math.max(0, (n >> 16) - amount);
24
+ const g = Math.max(0, ((n >> 8) & 0xff) - amount);
25
+ const b = Math.max(0, (n & 0xff) - amount);
26
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
16
27
  }
@@ -1,7 +0,0 @@
1
- import { ChatUser, UserType } from '../types';
2
- export declare function useUsers(url: string, filterType?: UserType, enabled?: boolean): {
3
- users: ChatUser[];
4
- loading: boolean;
5
- error: string | null;
6
- refetch: () => Promise<void>;
7
- };
@@ -1,26 +0,0 @@
1
- import { useState, useEffect, useCallback } from 'react';
2
- import { fetchUsers } from '../services/userService';
3
- export function useUsers(url, filterType, enabled = true) {
4
- const [users, setUsers] = useState([]);
5
- const [loading, setLoading] = useState(false);
6
- const [error, setError] = useState(null);
7
- const load = useCallback(async () => {
8
- if (!enabled)
9
- return;
10
- setLoading(true);
11
- setError(null);
12
- try {
13
- const data = await fetchUsers(url);
14
- setUsers(filterType ? data.filter(u => u.type === filterType) : data);
15
- }
16
- catch (e) {
17
- setError(e instanceof Error ? e.message : 'Unknown error');
18
- setUsers([]);
19
- }
20
- finally {
21
- setLoading(false);
22
- }
23
- }, [url, filterType, enabled]);
24
- useEffect(() => { load(); }, [load]);
25
- return { users, loading, error, refetch: load };
26
- }
@@ -1,2 +0,0 @@
1
- import { ChatUser } from '../types';
2
- export declare function fetchUsers(url: string): Promise<ChatUser[]>;
@@ -1,9 +0,0 @@
1
- export async function fetchUsers(url) {
2
- const res = await fetch(url, { headers: { 'Content-Type': 'application/json' } });
3
- if (!res.ok)
4
- throw new Error(`Failed to fetch users: ${res.status}`);
5
- const data = await res.json();
6
- if (!Array.isArray(data))
7
- throw new Error('User API did not return an array');
8
- return data;
9
- }
@@ -1,12 +0,0 @@
1
- import React from 'react';
2
- import { ChatMessage, ChatUser, ChatWidgetTheme } from '../../types';
3
- interface ChatScreenProps {
4
- activeUser: ChatUser;
5
- messages: ChatMessage[];
6
- onSend: (text: string) => void;
7
- onBack: () => void;
8
- onClose: () => void;
9
- theme?: ChatWidgetTheme;
10
- }
11
- export declare const ChatScreen: React.FC<ChatScreenProps>;
12
- export {};
@@ -1,83 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useRef, useEffect } from 'react';
3
- import { mergeTheme } from '../../utils/theme';
4
- export const ChatScreen = ({ activeUser, messages, onSend, onBack, onClose, theme, }) => {
5
- const t = mergeTheme(theme);
6
- const [text, setText] = useState('');
7
- const endRef = useRef(null);
8
- const inputRef = useRef(null);
9
- useEffect(() => { var _a; (_a = endRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
10
- const handleSend = () => {
11
- var _a;
12
- if (!text.trim())
13
- return;
14
- onSend(text);
15
- setText('');
16
- (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
17
- };
18
- const handleKey = (e) => {
19
- if (e.key === 'Enter' && !e.shiftKey) {
20
- e.preventDefault();
21
- handleSend();
22
- }
23
- };
24
- const initials = activeUser.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
25
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideInRight 0.25s ease' }, children: [_jsxs("div", { style: {
26
- backgroundColor: t.primaryColor,
27
- padding: '14px 18px',
28
- display: 'flex',
29
- alignItems: 'center',
30
- gap: '10px',
31
- flexShrink: 0,
32
- }, children: [_jsx("button", { onClick: onBack, style: iconBtnStyle, children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M19 12H5M5 12L12 19M5 12L12 5", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsx("div", { style: {
33
- width: 36, height: 36, borderRadius: '50%',
34
- backgroundColor: 'rgba(255,255,255,0.25)',
35
- display: 'flex', alignItems: 'center', justifyContent: 'center',
36
- fontWeight: 700, fontSize: '13px', color: '#fff', flexShrink: 0,
37
- }, children: initials }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: '14px', color: '#fff', fontFamily: t.fontFamily }, children: activeUser.name }), _jsxs("div", { style: { fontSize: '11px', color: 'rgba(255,255,255,0.8)', display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("span", { style: { width: 6, height: 6, borderRadius: '50%', backgroundColor: '#a8f0c6', display: 'inline-block' } }), "Online"] })] }), _jsx("button", { onClick: onClose, style: iconBtnStyle, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) })] }), _jsxs("div", { style: {
38
- flex: 1, overflowY: 'auto', padding: '18px 16px',
39
- display: 'flex', flexDirection: 'column', gap: '10px',
40
- backgroundColor: '#f7f8fc',
41
- }, children: [messages.length === 0 && (_jsxs("div", { style: {
42
- margin: 'auto', textAlign: 'center',
43
- fontSize: '13px', color: '#b0bec5',
44
- fontFamily: t.fontFamily,
45
- }, children: [_jsx("div", { style: { fontSize: '28px', marginBottom: 8 }, children: "\uD83D\uDCAC" }), "Say hi to ", activeUser.name, "!"] })), messages.map(msg => (_jsx(Bubble, { msg: msg, primaryColor: t.primaryColor, font: t.fontFamily }, msg.id))), _jsx("div", { ref: endRef })] }), _jsxs("div", { style: {
46
- borderTop: '1px solid #eef0f5',
47
- padding: '10px 14px',
48
- backgroundColor: '#fff',
49
- display: 'flex',
50
- alignItems: 'flex-end',
51
- gap: '10px',
52
- flexShrink: 0,
53
- }, children: [_jsx("textarea", { ref: inputRef, value: text, onChange: e => setText(e.target.value), onKeyDown: handleKey, placeholder: "Type and press [enter]..", rows: 1, style: {
54
- flex: 1, resize: 'none', border: 'none', outline: 'none',
55
- fontFamily: t.fontFamily, fontSize: '14px', lineHeight: '1.5',
56
- maxHeight: '80px', overflowY: 'auto', color: '#1a2332',
57
- padding: '6px 0', backgroundColor: 'transparent',
58
- } }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }, children: [_jsx(IconBtn, { onClick: () => { }, title: "Reaction", children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: [_jsx("path", { d: "M14 9h.01M10 9h.01M12 2a10 10 0 100 20A10 10 0 0012 2zm0 14s-4-1.5-4-4", stroke: "#9aa3af", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M8 15s1.5 2 4 2 4-2 4-2", stroke: "#9aa3af", strokeWidth: "1.8", strokeLinecap: "round" })] }) }), _jsx(IconBtn, { onClick: () => { }, title: "Attach", children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66L9.41 17.41a2 2 0 01-2.83-2.83l8.49-8.48", stroke: "#9aa3af", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsx(IconBtn, { onClick: () => { }, title: "Emoji", children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "12", r: "10", stroke: "#9aa3af", strokeWidth: "1.8" }), _jsx("path", { d: "M8 14s1.5 2 4 2 4-2 4-2", stroke: "#9aa3af", strokeWidth: "1.8", strokeLinecap: "round" }), _jsx("line", { x1: "9", y1: "9", x2: "9.01", y2: "9", stroke: "#9aa3af", strokeWidth: "2.5", strokeLinecap: "round" }), _jsx("line", { x1: "15", y1: "9", x2: "15.01", y2: "9", stroke: "#9aa3af", strokeWidth: "2.5", strokeLinecap: "round" })] }) }), text.trim() && (_jsx("button", { onClick: handleSend, style: {
59
- width: 36, height: 36, borderRadius: '50%',
60
- backgroundColor: t.primaryColor, border: 'none',
61
- cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
62
- transition: 'transform 0.15s',
63
- }, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z", stroke: "#fff", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }))] })] })] }));
64
- };
65
- const Bubble = ({ msg, primaryColor, font }) => {
66
- const isMe = msg.senderId === 'me';
67
- const time = new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
68
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: isMe ? 'flex-end' : 'flex-start', gap: 3 }, children: [_jsx("div", { style: {
69
- maxWidth: '75%', padding: '10px 14px',
70
- borderRadius: isMe ? '18px 18px 4px 18px' : '18px 18px 18px 4px',
71
- backgroundColor: isMe ? primaryColor : '#fff',
72
- color: isMe ? '#fff' : '#1a2332',
73
- fontSize: '14px', lineHeight: '1.5',
74
- boxShadow: '0 1px 4px rgba(0,0,0,0.07)',
75
- fontFamily: font, wordBreak: 'break-word',
76
- }, children: msg.text }), _jsx("span", { style: { fontSize: '11px', color: '#b0bec5', padding: '0 4px' }, children: time })] }));
77
- };
78
- const IconBtn = ({ onClick, title, children }) => (_jsx("button", { onClick: onClick, title: title, style: { background: 'none', border: 'none', cursor: 'pointer', padding: '4px', display: 'flex', alignItems: 'center', borderRadius: '6px' }, onMouseEnter: e => e.currentTarget.style.background = '#f3f4f6', onMouseLeave: e => e.currentTarget.style.background = 'none', children: children }));
79
- const iconBtnStyle = {
80
- background: 'rgba(255,255,255,0.2)', border: 'none', borderRadius: '50%',
81
- width: 34, height: 34, display: 'flex', alignItems: 'center', justifyContent: 'center',
82
- cursor: 'pointer', flexShrink: 0,
83
- };
@@ -1,4 +0,0 @@
1
- import React from 'react';
2
- import { ChatWidgetProps } from '../types';
3
- export declare const ChatWidget: React.FC<ChatWidgetProps>;
4
- export default ChatWidget;