ajaxter-chat 1.0.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -191
- package/dist/components/ChatScreen/index.d.ts +12 -0
- package/dist/components/ChatScreen/index.js +83 -0
- package/dist/components/ChatWidget.d.ts +0 -24
- package/dist/components/ChatWidget.js +129 -38
- package/dist/components/HomeScreen/index.d.ts +9 -0
- package/dist/components/HomeScreen/index.js +71 -0
- package/dist/components/MaintenanceView/index.d.ts +1 -1
- package/dist/components/MaintenanceView/index.js +15 -52
- package/dist/components/RecentChatsScreen/index.d.ts +16 -0
- package/dist/components/RecentChatsScreen/index.js +38 -0
- package/dist/components/Tabs/BottomTabs.d.ts +10 -0
- package/dist/components/Tabs/BottomTabs.js +29 -0
- package/dist/components/TicketScreen/index.d.ts +9 -0
- package/dist/components/TicketScreen/index.js +71 -0
- package/dist/components/UserListScreen/index.d.ts +13 -0
- package/dist/components/UserListScreen/index.js +64 -0
- package/dist/config/index.js +19 -74
- package/dist/hooks/useChat.d.ts +3 -7
- package/dist/hooks/useChat.js +8 -30
- package/dist/hooks/useUsers.d.ts +3 -10
- package/dist/hooks/useUsers.js +5 -11
- package/dist/index.d.ts +8 -7
- package/dist/index.js +7 -12
- package/dist/services/userService.d.ts +0 -5
- package/dist/services/userService.js +6 -13
- package/dist/src/components/ChatScreen/index.d.ts +12 -0
- package/dist/src/components/ChatScreen/index.js +83 -0
- package/dist/src/components/ChatWidget.d.ts +4 -0
- package/dist/src/components/ChatWidget.js +141 -0
- package/dist/src/components/HomeScreen/index.d.ts +9 -0
- package/dist/src/components/HomeScreen/index.js +71 -0
- package/dist/src/components/MaintenanceView/index.d.ts +7 -0
- package/dist/src/components/MaintenanceView/index.js +16 -0
- package/dist/src/components/RecentChatsScreen/index.d.ts +16 -0
- package/dist/src/components/RecentChatsScreen/index.js +38 -0
- package/dist/src/components/Tabs/BottomTabs.d.ts +10 -0
- package/dist/src/components/Tabs/BottomTabs.js +29 -0
- package/dist/src/components/TicketScreen/index.d.ts +9 -0
- package/dist/src/components/TicketScreen/index.js +71 -0
- package/dist/src/components/UserListScreen/index.d.ts +13 -0
- package/dist/src/components/UserListScreen/index.js +64 -0
- package/dist/src/config/index.d.ts +3 -0
- package/dist/src/config/index.js +38 -0
- package/dist/src/hooks/useChat.d.ts +8 -0
- package/dist/src/hooks/useChat.js +26 -0
- package/dist/src/hooks/useUsers.d.ts +7 -0
- package/dist/src/hooks/useUsers.js +26 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +13 -0
- package/dist/src/services/userService.d.ts +2 -0
- package/dist/src/services/userService.js +9 -0
- package/dist/src/types/index.d.ts +59 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/utils/theme.d.ts +3 -0
- package/dist/src/utils/theme.js +13 -0
- package/dist/types/index.d.ts +23 -36
- package/dist/utils/theme.d.ts +0 -1
- package/dist/utils/theme.js +3 -18
- package/package.json +10 -20
- package/src/components/ChatScreen/index.tsx +205 -0
- package/src/components/ChatWidget.tsx +327 -0
- package/src/components/HomeScreen/index.tsx +130 -0
- package/src/components/MaintenanceView/index.tsx +41 -0
- package/src/components/RecentChatsScreen/index.tsx +108 -0
- package/src/components/Tabs/BottomTabs.tsx +82 -0
- package/src/components/TicketScreen/index.tsx +170 -0
- package/src/components/UserListScreen/index.tsx +181 -0
- package/src/config/index.ts +46 -0
- package/src/hooks/useChat.ts +31 -0
- package/src/hooks/useUsers.ts +27 -0
- package/src/index.ts +18 -0
- package/src/services/userService.ts +9 -0
- package/src/types/index.ts +82 -0
- package/src/utils/theme.ts +16 -0
- package/dist/components/BottomNav/index.d.ts +0 -10
- package/dist/components/BottomNav/index.js +0 -32
- package/dist/components/ChatBox/index.d.ts +0 -15
- package/dist/components/ChatBox/index.js +0 -228
- package/dist/components/ChatButton/index.d.ts +0 -9
- package/dist/components/ChatButton/index.js +0 -17
- package/dist/components/ChatWindow/index.d.ts +0 -10
- package/dist/components/ChatWindow/index.js +0 -286
- package/dist/components/HomeView/index.d.ts +0 -12
- package/dist/components/HomeView/index.js +0 -51
- package/dist/components/UserList/index.d.ts +0 -13
- package/dist/components/UserList/index.js +0 -136
package/dist/types/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export type ChatStatus = 'ACTIVE' | 'DISABLE' | 'MAINTENANCE';
|
|
2
2
|
export type ChatType = 'SUPPORT' | 'CHAT' | 'BOTH';
|
|
3
3
|
export type UserType = 'developer' | 'user';
|
|
4
|
-
export type
|
|
5
|
-
export type
|
|
6
|
-
export type
|
|
4
|
+
export type Screen = 'home' | 'user-list' | 'chat' | 'recent-chats' | 'tickets';
|
|
5
|
+
export type UserListContext = 'support' | 'conversation';
|
|
6
|
+
export type BottomTab = 'home' | 'chats' | 'tickets';
|
|
7
|
+
export type WidgetSize = 'normal' | 'maximized';
|
|
7
8
|
export interface ChatUser {
|
|
8
9
|
name: string;
|
|
9
10
|
uid: string;
|
|
@@ -20,52 +21,38 @@ export interface ChatMessage {
|
|
|
20
21
|
timestamp: Date;
|
|
21
22
|
status: 'sent' | 'delivered' | 'read';
|
|
22
23
|
}
|
|
24
|
+
export interface RecentChat {
|
|
25
|
+
id: string;
|
|
26
|
+
user: ChatUser;
|
|
27
|
+
lastMessage: string;
|
|
28
|
+
lastTime: Date;
|
|
29
|
+
unread: number;
|
|
30
|
+
}
|
|
31
|
+
export interface Ticket {
|
|
32
|
+
id: string;
|
|
33
|
+
title: string;
|
|
34
|
+
description: string;
|
|
35
|
+
status: 'open' | 'in-progress' | 'resolved' | 'closed';
|
|
36
|
+
priority: 'low' | 'medium' | 'high';
|
|
37
|
+
createdAt: Date;
|
|
38
|
+
updatedAt: Date;
|
|
39
|
+
}
|
|
23
40
|
export interface ChatConfig {
|
|
24
41
|
hostUrl: string;
|
|
25
|
-
|
|
26
|
-
hostPort?: number;
|
|
42
|
+
hostPort: number | null;
|
|
27
43
|
userListEndpoint: string;
|
|
28
44
|
status: ChatStatus;
|
|
29
45
|
chatType: ChatType;
|
|
30
|
-
/** Show “Need Support” card on home (env `CHAT_SHOW_NEED_SUPPORT`). */
|
|
31
|
-
showNeedSupport: boolean;
|
|
32
|
-
/** Show “New Conversation” card on home (env `CHAT_SHOW_NEW_CONVERSATION`). */
|
|
33
|
-
showNewConversation: boolean;
|
|
34
|
-
/** Pixel bounds for the resize slider (env `CHAT_WIDGET_*`). */
|
|
35
|
-
widgetMinWidth: number;
|
|
36
|
-
widgetMaxWidth: number;
|
|
37
|
-
widgetMinHeight: number;
|
|
38
|
-
widgetMaxHeight: number;
|
|
39
|
-
/** Default widget size as ratio 0–1 between min and max. */
|
|
40
|
-
widgetDefaultSize: number;
|
|
41
|
-
}
|
|
42
|
-
export interface RaisedTicket {
|
|
43
|
-
id: string;
|
|
44
|
-
subject: string;
|
|
45
|
-
body: string;
|
|
46
|
-
createdAt: Date;
|
|
47
|
-
status: 'open' | 'closed';
|
|
48
|
-
}
|
|
49
|
-
export interface RecentChat {
|
|
50
|
-
user: ChatUser;
|
|
51
|
-
lastMessage: string;
|
|
52
|
-
updatedAt: Date;
|
|
53
46
|
}
|
|
54
47
|
export interface ChatWidgetTheme {
|
|
55
|
-
fontFamily?: string;
|
|
56
48
|
primaryColor?: string;
|
|
57
|
-
|
|
49
|
+
fontFamily?: string;
|
|
58
50
|
buttonColor?: string;
|
|
59
51
|
buttonTextColor?: string;
|
|
60
52
|
buttonLabel?: string;
|
|
61
53
|
buttonPosition?: 'bottom-right' | 'bottom-left';
|
|
62
54
|
borderRadius?: string;
|
|
63
|
-
|
|
64
|
-
widgetMinWidth?: number;
|
|
65
|
-
widgetMaxWidth?: number;
|
|
66
|
-
widgetMinHeight?: number;
|
|
67
|
-
widgetMaxHeight?: number;
|
|
68
|
-
widgetDefaultSize?: number;
|
|
55
|
+
backgroundColor?: string;
|
|
69
56
|
}
|
|
70
57
|
export interface ChatWidgetProps {
|
|
71
58
|
theme?: ChatWidgetTheme;
|
package/dist/utils/theme.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import { ChatWidgetTheme } from '../types';
|
|
2
2
|
export declare const defaultTheme: Required<ChatWidgetTheme>;
|
|
3
3
|
export declare function mergeTheme(custom?: ChatWidgetTheme): Required<ChatWidgetTheme>;
|
|
4
|
-
export declare function themeToCSS(theme: Required<ChatWidgetTheme>): string;
|
package/dist/utils/theme.js
CHANGED
|
@@ -1,28 +1,13 @@
|
|
|
1
1
|
export const defaultTheme = {
|
|
2
|
+
primaryColor: '#1aaa96',
|
|
2
3
|
fontFamily: "'DM Sans', 'Segoe UI', sans-serif",
|
|
3
|
-
|
|
4
|
-
backgroundColor: '#ffffff',
|
|
5
|
-
buttonColor: '#13947e',
|
|
4
|
+
buttonColor: '#1aaa96',
|
|
6
5
|
buttonTextColor: '#ffffff',
|
|
7
6
|
buttonLabel: 'Chat with us',
|
|
8
7
|
buttonPosition: 'bottom-right',
|
|
9
8
|
borderRadius: '16px',
|
|
10
|
-
|
|
11
|
-
widgetMaxWidth: 720,
|
|
12
|
-
widgetMinHeight: 420,
|
|
13
|
-
widgetMaxHeight: 720,
|
|
14
|
-
widgetDefaultSize: 0.45,
|
|
9
|
+
backgroundColor: '#ffffff',
|
|
15
10
|
};
|
|
16
11
|
export function mergeTheme(custom) {
|
|
17
12
|
return Object.assign(Object.assign({}, defaultTheme), custom);
|
|
18
13
|
}
|
|
19
|
-
export function themeToCSS(theme) {
|
|
20
|
-
return `
|
|
21
|
-
--cw-font: ${theme.fontFamily};
|
|
22
|
-
--cw-primary: ${theme.primaryColor};
|
|
23
|
-
--cw-bg: ${theme.backgroundColor};
|
|
24
|
-
--cw-btn-bg: ${theme.buttonColor};
|
|
25
|
-
--cw-btn-text: ${theme.buttonTextColor};
|
|
26
|
-
--cw-radius: ${theme.borderRadius};
|
|
27
|
-
`;
|
|
28
|
-
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ajaxter-chat",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A reusable
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "A reusable configurable chat widget for React.js and Next.js with support chat, user chat, and ticket raising.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
|
-
"module": "dist/index.esm.js",
|
|
7
6
|
"types": "dist/index.d.ts",
|
|
8
|
-
"files": [
|
|
9
|
-
"dist"
|
|
10
|
-
],
|
|
7
|
+
"files": ["dist", "src"],
|
|
11
8
|
"sideEffects": false,
|
|
12
9
|
"scripts": {
|
|
13
10
|
"build": "tsc",
|
|
14
11
|
"dev": "tsc --watch",
|
|
15
|
-
"lint": "eslint src --ext .ts,.tsx",
|
|
16
12
|
"type-check": "tsc --noEmit"
|
|
17
13
|
},
|
|
18
14
|
"peerDependencies": {
|
|
@@ -20,19 +16,13 @@
|
|
|
20
16
|
"react-dom": ">=17.0.0"
|
|
21
17
|
},
|
|
22
18
|
"devDependencies": {
|
|
23
|
-
"@types/node": "^
|
|
24
|
-
"@types/react": "^18.
|
|
25
|
-
"@types/react-dom": "^18.
|
|
26
|
-
"
|
|
19
|
+
"@types/node": "^22.0.0",
|
|
20
|
+
"@types/react": "^18.0.0",
|
|
21
|
+
"@types/react-dom": "^18.0.0",
|
|
22
|
+
"react": "^18.0.0",
|
|
23
|
+
"react-dom": "^18.0.0",
|
|
24
|
+
"typescript": "^5.0.0"
|
|
27
25
|
},
|
|
28
|
-
"keywords": [
|
|
29
|
-
"react",
|
|
30
|
-
"nextjs",
|
|
31
|
-
"chat",
|
|
32
|
-
"widget",
|
|
33
|
-
"support",
|
|
34
|
-
"typescript",
|
|
35
|
-
"ajaxter"
|
|
36
|
-
],
|
|
26
|
+
"keywords": ["react", "nextjs", "chat", "widget", "support", "tickets", "typescript"],
|
|
37
27
|
"license": "MIT"
|
|
38
28
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { ChatMessage, ChatUser, ChatWidgetTheme } from '../../types';
|
|
3
|
+
import { mergeTheme } from '../../utils/theme';
|
|
4
|
+
|
|
5
|
+
interface ChatScreenProps {
|
|
6
|
+
activeUser: ChatUser;
|
|
7
|
+
messages: ChatMessage[];
|
|
8
|
+
onSend: (text: string) => void;
|
|
9
|
+
onBack: () => void;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
theme?: ChatWidgetTheme;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ChatScreen: React.FC<ChatScreenProps> = ({
|
|
15
|
+
activeUser, messages, onSend, onBack, onClose, theme,
|
|
16
|
+
}) => {
|
|
17
|
+
const t = mergeTheme(theme);
|
|
18
|
+
const [text, setText] = useState('');
|
|
19
|
+
const endRef = useRef<HTMLDivElement>(null);
|
|
20
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
|
|
23
|
+
|
|
24
|
+
const handleSend = () => {
|
|
25
|
+
if (!text.trim()) return;
|
|
26
|
+
onSend(text);
|
|
27
|
+
setText('');
|
|
28
|
+
inputRef.current?.focus();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleKey = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
32
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); }
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const initials = activeUser.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideInRight 0.25s ease' }}>
|
|
39
|
+
{/* Teal header */}
|
|
40
|
+
<div style={{
|
|
41
|
+
backgroundColor: t.primaryColor,
|
|
42
|
+
padding: '14px 18px',
|
|
43
|
+
display: 'flex',
|
|
44
|
+
alignItems: 'center',
|
|
45
|
+
gap: '10px',
|
|
46
|
+
flexShrink: 0,
|
|
47
|
+
}}>
|
|
48
|
+
<button onClick={onBack} style={iconBtnStyle}>
|
|
49
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
50
|
+
<path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
51
|
+
</svg>
|
|
52
|
+
</button>
|
|
53
|
+
|
|
54
|
+
<div style={{
|
|
55
|
+
width: 36, height: 36, borderRadius: '50%',
|
|
56
|
+
backgroundColor: 'rgba(255,255,255,0.25)',
|
|
57
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
58
|
+
fontWeight: 700, fontSize: '13px', color: '#fff', flexShrink: 0,
|
|
59
|
+
}}>
|
|
60
|
+
{initials}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
64
|
+
<div style={{ fontWeight: 700, fontSize: '14px', color: '#fff', fontFamily: t.fontFamily }}>
|
|
65
|
+
{activeUser.name}
|
|
66
|
+
</div>
|
|
67
|
+
<div style={{ fontSize: '11px', color: 'rgba(255,255,255,0.8)', display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
68
|
+
<span style={{ width: 6, height: 6, borderRadius: '50%', backgroundColor: '#a8f0c6', display: 'inline-block' }} />
|
|
69
|
+
Online
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<button onClick={onClose} style={iconBtnStyle}>
|
|
74
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
|
75
|
+
<path d="M18 6L6 18M6 6l12 12" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" />
|
|
76
|
+
</svg>
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Messages area */}
|
|
81
|
+
<div style={{
|
|
82
|
+
flex: 1, overflowY: 'auto', padding: '18px 16px',
|
|
83
|
+
display: 'flex', flexDirection: 'column', gap: '10px',
|
|
84
|
+
backgroundColor: '#f7f8fc',
|
|
85
|
+
}}>
|
|
86
|
+
{messages.length === 0 && (
|
|
87
|
+
<div style={{
|
|
88
|
+
margin: 'auto', textAlign: 'center',
|
|
89
|
+
fontSize: '13px', color: '#b0bec5',
|
|
90
|
+
fontFamily: t.fontFamily,
|
|
91
|
+
}}>
|
|
92
|
+
<div style={{ fontSize: '28px', marginBottom: 8 }}>💬</div>
|
|
93
|
+
Say hi to {activeUser.name}!
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
{messages.map(msg => (
|
|
97
|
+
<Bubble key={msg.id} msg={msg} primaryColor={t.primaryColor} font={t.fontFamily} />
|
|
98
|
+
))}
|
|
99
|
+
<div ref={endRef} />
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Input bar — matching image 2 exactly */}
|
|
103
|
+
<div style={{
|
|
104
|
+
borderTop: '1px solid #eef0f5',
|
|
105
|
+
padding: '10px 14px',
|
|
106
|
+
backgroundColor: '#fff',
|
|
107
|
+
display: 'flex',
|
|
108
|
+
alignItems: 'flex-end',
|
|
109
|
+
gap: '10px',
|
|
110
|
+
flexShrink: 0,
|
|
111
|
+
}}>
|
|
112
|
+
<textarea
|
|
113
|
+
ref={inputRef}
|
|
114
|
+
value={text}
|
|
115
|
+
onChange={e => setText(e.target.value)}
|
|
116
|
+
onKeyDown={handleKey}
|
|
117
|
+
placeholder="Type and press [enter].."
|
|
118
|
+
rows={1}
|
|
119
|
+
style={{
|
|
120
|
+
flex: 1, resize: 'none', border: 'none', outline: 'none',
|
|
121
|
+
fontFamily: t.fontFamily, fontSize: '14px', lineHeight: '1.5',
|
|
122
|
+
maxHeight: '80px', overflowY: 'auto', color: '#1a2332',
|
|
123
|
+
padding: '6px 0', backgroundColor: 'transparent',
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
{/* Action icons: like, attachment, emoji — matching image 2 */}
|
|
127
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}>
|
|
128
|
+
<IconBtn onClick={() => {}} title="Reaction">
|
|
129
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
|
130
|
+
<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"/>
|
|
131
|
+
<path d="M8 15s1.5 2 4 2 4-2 4-2" stroke="#9aa3af" strokeWidth="1.8" strokeLinecap="round"/>
|
|
132
|
+
</svg>
|
|
133
|
+
</IconBtn>
|
|
134
|
+
<IconBtn onClick={() => {}} title="Attach">
|
|
135
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
|
136
|
+
<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"/>
|
|
137
|
+
</svg>
|
|
138
|
+
</IconBtn>
|
|
139
|
+
<IconBtn onClick={() => {}} title="Emoji">
|
|
140
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
|
141
|
+
<circle cx="12" cy="12" r="10" stroke="#9aa3af" strokeWidth="1.8"/>
|
|
142
|
+
<path d="M8 14s1.5 2 4 2 4-2 4-2" stroke="#9aa3af" strokeWidth="1.8" strokeLinecap="round"/>
|
|
143
|
+
<line x1="9" y1="9" x2="9.01" y2="9" stroke="#9aa3af" strokeWidth="2.5" strokeLinecap="round"/>
|
|
144
|
+
<line x1="15" y1="9" x2="15.01" y2="9" stroke="#9aa3af" strokeWidth="2.5" strokeLinecap="round"/>
|
|
145
|
+
</svg>
|
|
146
|
+
</IconBtn>
|
|
147
|
+
{text.trim() && (
|
|
148
|
+
<button
|
|
149
|
+
onClick={handleSend}
|
|
150
|
+
style={{
|
|
151
|
+
width: 36, height: 36, borderRadius: '50%',
|
|
152
|
+
backgroundColor: t.primaryColor, border: 'none',
|
|
153
|
+
cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
154
|
+
transition: 'transform 0.15s',
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
|
158
|
+
<path d="M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
159
|
+
</svg>
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const Bubble: React.FC<{ msg: ChatMessage; primaryColor: string; font: string }> = ({ msg, primaryColor, font }) => {
|
|
169
|
+
const isMe = msg.senderId === 'me';
|
|
170
|
+
const time = new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
171
|
+
return (
|
|
172
|
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: isMe ? 'flex-end' : 'flex-start', gap: 3 }}>
|
|
173
|
+
<div style={{
|
|
174
|
+
maxWidth: '75%', padding: '10px 14px',
|
|
175
|
+
borderRadius: isMe ? '18px 18px 4px 18px' : '18px 18px 18px 4px',
|
|
176
|
+
backgroundColor: isMe ? primaryColor : '#fff',
|
|
177
|
+
color: isMe ? '#fff' : '#1a2332',
|
|
178
|
+
fontSize: '14px', lineHeight: '1.5',
|
|
179
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.07)',
|
|
180
|
+
fontFamily: font, wordBreak: 'break-word',
|
|
181
|
+
}}>
|
|
182
|
+
{msg.text}
|
|
183
|
+
</div>
|
|
184
|
+
<span style={{ fontSize: '11px', color: '#b0bec5', padding: '0 4px' }}>{time}</span>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const IconBtn: React.FC<{ onClick: () => void; title: string; children: React.ReactNode }> = ({ onClick, title, children }) => (
|
|
190
|
+
<button
|
|
191
|
+
onClick={onClick}
|
|
192
|
+
title={title}
|
|
193
|
+
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '4px', display: 'flex', alignItems: 'center', borderRadius: '6px' }}
|
|
194
|
+
onMouseEnter={e => (e.currentTarget as HTMLElement).style.background = '#f3f4f6'}
|
|
195
|
+
onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = 'none'}
|
|
196
|
+
>
|
|
197
|
+
{children}
|
|
198
|
+
</button>
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const iconBtnStyle: React.CSSProperties = {
|
|
202
|
+
background: 'rgba(255,255,255,0.2)', border: 'none', borderRadius: '50%',
|
|
203
|
+
width: 34, height: 34, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
204
|
+
cursor: 'pointer', flexShrink: 0,
|
|
205
|
+
};
|