customer-module-frontend 1.0.2
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/.cursor/rules/context.md +306 -0
- package/.cursor/rules/guardrails.md +35 -0
- package/.env +1 -0
- package/.github/workflows/publish-to-npm-beta.yml +56 -0
- package/.github/workflows/publish-to-npm.yml +58 -0
- package/README.md +73 -0
- package/eslint.config.js +23 -0
- package/index.html +13 -0
- package/package.json +43 -0
- package/postcss-unwrap-layers.js +31 -0
- package/postcss.config.js +11 -0
- package/public/vite.svg +1 -0
- package/src/App.css +40 -0
- package/src/App.tsx +58 -0
- package/src/assets/accounts.svg +3 -0
- package/src/assets/at_the_rate.svg +10 -0
- package/src/assets/buildings.svg +3 -0
- package/src/assets/chat.svg +3 -0
- package/src/assets/close.svg +3 -0
- package/src/assets/contacts.svg +3 -0
- package/src/assets/conversation.svg +10 -0
- package/src/assets/customers.svg +10 -0
- package/src/assets/details.svg +3 -0
- package/src/assets/domain.svg +10 -0
- package/src/assets/edit.svg +15 -0
- package/src/assets/email.svg +3 -0
- package/src/assets/google.svg +8 -0
- package/src/assets/inbox.svg +0 -0
- package/src/assets/message.svg +3 -0
- package/src/assets/no-data.svg +9 -0
- package/src/assets/open_in_a_new_tab.svg +10 -0
- package/src/assets/phone.svg +3 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/search.svg +3 -0
- package/src/assets/search_typing.svg +4 -0
- package/src/assets/sm_contacts.svg +3 -0
- package/src/assets/sm_inbox.svg +3 -0
- package/src/assets/sm_slider.svg +3 -0
- package/src/assets/status-resolved.svg +3 -0
- package/src/assets/status-snoozed.svg +4 -0
- package/src/assets/status_open.svg +1 -0
- package/src/components/AccountContacts/index.tsx +107 -0
- package/src/components/AccountDetails/index.tsx +102 -0
- package/src/components/AccountsConversation/index.tsx +75 -0
- package/src/components/Avatar/constants.tsx +45 -0
- package/src/components/Avatar/index.tsx +42 -0
- package/src/components/BreadcrumbsSection/index.tsx +16 -0
- package/src/components/Card/index.tsx +31 -0
- package/src/components/Card/types.ts +10 -0
- package/src/components/ContactConversation/Converation.tsx +14 -0
- package/src/components/ContactConversation/index.tsx +81 -0
- package/src/components/ContactDetails/index.tsx +111 -0
- package/src/components/Contacts/EditContact.tsx +213 -0
- package/src/components/Contacts/constants/index.tsx +24 -0
- package/src/components/Contacts/index.tsx +171 -0
- package/src/components/ConversationBox/constants.tsx +99 -0
- package/src/components/ConversationBox/index.tsx +147 -0
- package/src/components/ConversationBox/types.ts +20 -0
- package/src/components/CustomersLayout/index.tsx +20 -0
- package/src/components/DetailsCard/index.tsx +31 -0
- package/src/components/DetailsCard/types.ts +10 -0
- package/src/components/EmptyData/NoDataFound.tsx +31 -0
- package/src/components/Header/index.tsx +55 -0
- package/src/components/Icon/index.tsx +93 -0
- package/src/components/Icon/types.ts +13 -0
- package/src/components/Listing/AccountTable.tsx +47 -0
- package/src/components/Listing/ContactTable.tsx +76 -0
- package/src/components/RightPanel/AccountPanel.tsx +123 -0
- package/src/components/RightPanel/ContactPanel.tsx +142 -0
- package/src/components/RightPanel/index.tsx +167 -0
- package/src/components/Search/SearchDialog.tsx +150 -0
- package/src/components/TabsSection/index.tsx +49 -0
- package/src/constants/index.tsx +645 -0
- package/src/hooks/useBreadcrumb.tsx +93 -0
- package/src/index.css +315 -0
- package/src/main.tsx +14 -0
- package/src/pages/AccountDetail.tsx +68 -0
- package/src/pages/Accounts.tsx +12 -0
- package/src/pages/ContactDetail.tsx +55 -0
- package/src/pages/Contacts.tsx +12 -0
- package/src/stores/count.tsx +17 -0
- package/src/types/index.ts +0 -0
- package/tailwind.config.js +179 -0
- package/tsconfig.app.json +36 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +31 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Dialog, Button, TextField } from "hiver-ui-kit-extended";
|
|
3
|
+
import Avatar from "../Avatar";
|
|
4
|
+
import closeIcon from "../../assets/close.svg";
|
|
5
|
+
import Icon from "../Icon";
|
|
6
|
+
|
|
7
|
+
interface Contact {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
email: string;
|
|
11
|
+
phone_number?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface EditContactProps {
|
|
15
|
+
contact?: Contact;
|
|
16
|
+
open?: boolean;
|
|
17
|
+
onClose?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const EditContact = ({ contact, open = false, onClose }: EditContactProps) => {
|
|
21
|
+
const [dialogOpen, setDialogOpen] = useState(open);
|
|
22
|
+
const [formData, setFormData] = useState({
|
|
23
|
+
name: contact?.name || "",
|
|
24
|
+
email: contact?.email || "",
|
|
25
|
+
phone_number: contact?.phone_number || "",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const [errors, setErrors] = useState({
|
|
29
|
+
name: "",
|
|
30
|
+
email: "",
|
|
31
|
+
phone_number: "",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const handleClick = () => {
|
|
35
|
+
setDialogOpen(true);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleClose = () => {
|
|
39
|
+
setDialogOpen(false);
|
|
40
|
+
setFormData({
|
|
41
|
+
name: contact?.name || "",
|
|
42
|
+
email: contact?.email || "",
|
|
43
|
+
phone_number: contact?.phone_number || "",
|
|
44
|
+
});
|
|
45
|
+
setErrors({ name: "", email: "", phone_number: "" });
|
|
46
|
+
onClose?.();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const validateForm = () => {
|
|
50
|
+
const newErrors = {
|
|
51
|
+
name: "",
|
|
52
|
+
email: "",
|
|
53
|
+
phone_number: "",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let isValid = true;
|
|
57
|
+
|
|
58
|
+
if (!formData.name.trim()) {
|
|
59
|
+
newErrors.name = "Name is required";
|
|
60
|
+
isValid = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!formData.email.trim()) {
|
|
64
|
+
newErrors.email = "Email is required";
|
|
65
|
+
isValid = false;
|
|
66
|
+
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
67
|
+
newErrors.email = "Please enter a valid email";
|
|
68
|
+
isValid = false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setErrors(newErrors);
|
|
72
|
+
return isValid;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleSubmit = () => {
|
|
76
|
+
if (validateForm()) {
|
|
77
|
+
console.log("Form submitted:", formData);
|
|
78
|
+
alert("Contact updated successfully!");
|
|
79
|
+
handleClose();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleInputChange = (field: keyof typeof formData, value: string) => {
|
|
84
|
+
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
85
|
+
// Clear error when user starts typing
|
|
86
|
+
if (errors[field]) {
|
|
87
|
+
setErrors((prev) => ({ ...prev, [field]: "" }));
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const renderHeader = () => {
|
|
92
|
+
return (
|
|
93
|
+
<div className="cm:flex cm:items-center cm:justify-between cm:w-full">
|
|
94
|
+
<div className="cm:text-slate-text-title cm:text-base cm:font-semibold cm:leading-6">
|
|
95
|
+
{contact ? "Edit Contact" : "Add Contact"}
|
|
96
|
+
</div>
|
|
97
|
+
<Button
|
|
98
|
+
onClick={handleClose}
|
|
99
|
+
variant="text"
|
|
100
|
+
icon={closeIcon}
|
|
101
|
+
size="small"
|
|
102
|
+
color="primary"
|
|
103
|
+
className="cm:p-0"
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const renderContent = () => {
|
|
110
|
+
return (
|
|
111
|
+
<div className="" onClick={(e) => e.stopPropagation()}>
|
|
112
|
+
{/* Contact Avatar and Info */}
|
|
113
|
+
{contact && (
|
|
114
|
+
<div className="cm:flex cm:items-center cm:gap-3 cm:mb-6">
|
|
115
|
+
<Avatar name={formData.name} labelName={formData.name} />
|
|
116
|
+
<div className="cm:flex cm:flex-col">
|
|
117
|
+
<p className="cm:text-slate-text-title cm:text-sm cm:font-medium cm:leading-5 cm:m-0">
|
|
118
|
+
{contact.name}
|
|
119
|
+
</p>
|
|
120
|
+
<p className="cm:text-slate-text-subtle cm:text-xs cm:font-normal cm:leading-[18px] cm:m-0">
|
|
121
|
+
{contact.email}
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{/* Form Fields */}
|
|
128
|
+
<div className="cm:flex cm:flex-col cm:gap-4 cm:mb-6">
|
|
129
|
+
{/* Name Field */}
|
|
130
|
+
<div className="cm:flex cm:flex-col cm:gap-1.5">
|
|
131
|
+
<label className="cm:text-slate-text-title cm:text-sm cm:font-medium cm:leading-5">
|
|
132
|
+
Name <span className="cm:text-error">*</span>
|
|
133
|
+
</label>
|
|
134
|
+
<TextField
|
|
135
|
+
placeholder="Enter contact name"
|
|
136
|
+
value={formData.name}
|
|
137
|
+
onChange={(e) => handleInputChange("name", e.target.value)}
|
|
138
|
+
className="cm:w-full"
|
|
139
|
+
error={!!errors.name}
|
|
140
|
+
/>
|
|
141
|
+
{errors.name && (
|
|
142
|
+
<span className="cm:text-error cm:text-xs cm:font-normal cm:leading-[18px]">
|
|
143
|
+
{errors.name}
|
|
144
|
+
</span>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{/* Email Field */}
|
|
149
|
+
<div className="cm:flex cm:flex-col cm:gap-1.5">
|
|
150
|
+
<label className="cm:text-slate-text-title cm:text-sm cm:font-medium cm:leading-5">
|
|
151
|
+
Email <span className="cm:text-error">*</span>
|
|
152
|
+
</label>
|
|
153
|
+
<TextField
|
|
154
|
+
placeholder="Enter email address"
|
|
155
|
+
value={formData.email}
|
|
156
|
+
onChange={(e) => handleInputChange("email", e.target.value)}
|
|
157
|
+
className="cm:w-full"
|
|
158
|
+
error={!!errors.email}
|
|
159
|
+
/>
|
|
160
|
+
{errors.email && (
|
|
161
|
+
<span className="cm:text-error cm:text-xs cm:font-normal cm:leading-[18px]">
|
|
162
|
+
{errors.email}
|
|
163
|
+
</span>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{/* Phone Field */}
|
|
168
|
+
<div className="cm:flex cm:flex-col cm:gap-1.5">
|
|
169
|
+
<label className="cm:text-slate-text-title cm:text-sm cm:font-medium cm:leading-5">
|
|
170
|
+
Phone Number
|
|
171
|
+
</label>
|
|
172
|
+
<TextField
|
|
173
|
+
placeholder="Enter phone number"
|
|
174
|
+
value={formData.phone_number}
|
|
175
|
+
onChange={(e) => handleInputChange("phone_number", e.target.value)}
|
|
176
|
+
className="cm:w-full"
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{/* Action Buttons */}
|
|
182
|
+
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<div>
|
|
189
|
+
|
|
190
|
+
<Icon name="edit" size={16} type="active" color="#0F172A" onClick={handleClick}/>
|
|
191
|
+
|
|
192
|
+
<Dialog
|
|
193
|
+
open={dialogOpen}
|
|
194
|
+
onClose={handleClose}
|
|
195
|
+
maxWidth="sm"
|
|
196
|
+
title={renderHeader()}
|
|
197
|
+
className="cm:bg-slate-surface-white cm:w-[400px]"
|
|
198
|
+
footer={ <div className="cm:flex cm:gap-3 cm:items-center cm:justify-end cm:w-full">
|
|
199
|
+
<Button variant="outlined" size="medium" onClick={handleClose}>
|
|
200
|
+
Cancel
|
|
201
|
+
</Button>
|
|
202
|
+
<Button onClick={handleSubmit} size="medium">
|
|
203
|
+
Save
|
|
204
|
+
</Button>
|
|
205
|
+
</div>}
|
|
206
|
+
>
|
|
207
|
+
{renderContent()}
|
|
208
|
+
</Dialog>
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export default EditContact;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const mockContacts = [
|
|
2
|
+
{
|
|
3
|
+
id: '1',
|
|
4
|
+
name: 'Bob Johnson',
|
|
5
|
+
email: 'bobjohnson@company.com',
|
|
6
|
+
initial: 'B',
|
|
7
|
+
avatarColor: 'bg-[#ac80b7]', // pastel-violet
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: '2',
|
|
11
|
+
name: 'Martin George',
|
|
12
|
+
email: 'martingeorge@company.com',
|
|
13
|
+
initial: 'M',
|
|
14
|
+
avatarColor: 'bg-[#d04b4f]', // pastel-red
|
|
15
|
+
isSelected: true,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: '3',
|
|
19
|
+
name: 'Alice Smith',
|
|
20
|
+
email: 'alicesmith@company.com',
|
|
21
|
+
initial: 'A',
|
|
22
|
+
avatarColor: 'bg-[#5398cf]', // pastel-light-blue
|
|
23
|
+
},
|
|
24
|
+
];
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Dialog, Button, TextField } from "hiver-ui-kit-extended";
|
|
3
|
+
import { mockContacts } from "./constants";
|
|
4
|
+
import Avatar from "../Avatar";
|
|
5
|
+
import closeIcon from "../../assets/close.svg";
|
|
6
|
+
import Icon from "../Icon";
|
|
7
|
+
|
|
8
|
+
type DialogState = "add-primary-contact" | "primary-contact-confirmation";
|
|
9
|
+
|
|
10
|
+
interface Contact {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
email: string;
|
|
14
|
+
isSelected?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PrimaryContactsDialog = () => {
|
|
18
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
19
|
+
const [dialogState, setDialogState] = useState<DialogState>("add-primary-contact");
|
|
20
|
+
const [searchValue, setSearchValue] = useState("");
|
|
21
|
+
const [selectedContact, setSelectedContact] = useState<Contact | null>(null);
|
|
22
|
+
|
|
23
|
+
const handleClick = () => {
|
|
24
|
+
setDialogOpen(true);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const handleClose = () => {
|
|
28
|
+
setDialogOpen(false);
|
|
29
|
+
setDialogState("add-primary-contact");
|
|
30
|
+
setSearchValue("");
|
|
31
|
+
setSelectedContact(null);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const filteredContacts = mockContacts.filter(
|
|
35
|
+
(contact) =>
|
|
36
|
+
contact.name.toLowerCase().includes(searchValue.toLowerCase()) ||
|
|
37
|
+
contact.email.toLowerCase().includes(searchValue.toLowerCase())
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const handleContactSelect = (contact: Contact) => {
|
|
41
|
+
setSelectedContact(contact);
|
|
42
|
+
setDialogState("primary-contact-confirmation");
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const renderHeader = () => {
|
|
46
|
+
if (dialogState === "add-primary-contact") {
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex items-center justify-between w-full">
|
|
49
|
+
<div>Change Primary Contact</div>
|
|
50
|
+
<Button
|
|
51
|
+
onClick={handleClose}
|
|
52
|
+
variant="text"
|
|
53
|
+
icon={closeIcon}
|
|
54
|
+
size="small"
|
|
55
|
+
color="primary"
|
|
56
|
+
className="p-0"
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return <div>Primary Contact Confirmation</div>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const renderContent = () => {
|
|
65
|
+
if (dialogState === "primary-contact-confirmation") {
|
|
66
|
+
return (
|
|
67
|
+
<div className="w-[380px]" onClick={(e) => e.stopPropagation()}>
|
|
68
|
+
<p className="font-['Hanken_Grotesk',sans-serif] font-normal leading-5 text-sm text-[var(--slate-text-subtle,#64758b)] m-0 w-full mb-4">
|
|
69
|
+
Are you sure you want to change the contact to {selectedContact?.name}? This will replace
|
|
70
|
+
the existing contact for this conversation.
|
|
71
|
+
</p>
|
|
72
|
+
<div className="flex gap-3 items-center justify-end w-full">
|
|
73
|
+
<Button
|
|
74
|
+
variant="outlined"
|
|
75
|
+
size="medium"
|
|
76
|
+
onClick={() => setDialogState("add-primary-contact")}
|
|
77
|
+
>
|
|
78
|
+
Cancel
|
|
79
|
+
</Button>
|
|
80
|
+
<Button
|
|
81
|
+
onClick={() => {
|
|
82
|
+
alert("Contact changed!");
|
|
83
|
+
handleClose();
|
|
84
|
+
}}
|
|
85
|
+
size="medium"
|
|
86
|
+
>
|
|
87
|
+
Change
|
|
88
|
+
</Button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="w-[380px]" onClick={(e) => e.stopPropagation()}>
|
|
96
|
+
<div className="w-full mb-4">
|
|
97
|
+
<TextField
|
|
98
|
+
placeholder="Search name or email"
|
|
99
|
+
value={searchValue}
|
|
100
|
+
onChange={(e) => setSearchValue(e.target.value)}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
<div className="flex flex-col gap-2 items-start w-full">
|
|
104
|
+
<p className="font-['Hanken_Grotesk',sans-serif] font-normal leading-5 text-sm text-[#64758b]">
|
|
105
|
+
Contacts from this conversation
|
|
106
|
+
</p>
|
|
107
|
+
<div className="flex flex-col items-start w-full gap-0">
|
|
108
|
+
{filteredContacts.map((contact: Contact) => (
|
|
109
|
+
<ContactItem
|
|
110
|
+
key={contact.id}
|
|
111
|
+
contact={contact}
|
|
112
|
+
onSelect={() => handleContactSelect(contact)}
|
|
113
|
+
/>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div>
|
|
123
|
+
|
|
124
|
+
<Icon name="edit" size={16} type="active" color="#0F172A" onClick={handleClick} />
|
|
125
|
+
<Dialog
|
|
126
|
+
open={dialogOpen}
|
|
127
|
+
variant="confirmation"
|
|
128
|
+
onClose={handleClose}
|
|
129
|
+
onBack={() => setDialogState("add-primary-contact")}
|
|
130
|
+
maxWidth="sm"
|
|
131
|
+
title={renderHeader()}
|
|
132
|
+
>
|
|
133
|
+
{renderContent()}
|
|
134
|
+
</Dialog>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export default PrimaryContactsDialog;
|
|
140
|
+
|
|
141
|
+
const ContactItem = ({
|
|
142
|
+
contact,
|
|
143
|
+
onSelect,
|
|
144
|
+
}: {
|
|
145
|
+
contact: Contact;
|
|
146
|
+
onSelect: () => void;
|
|
147
|
+
}) => {
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
className="bg-white flex gap-3 items-center px-0 py-2 rounded-lg w-full cursor-pointer hover:bg-gray-50 transition-colors"
|
|
151
|
+
onClick={onSelect}
|
|
152
|
+
>
|
|
153
|
+
<div className="flex gap-3 items-center flex-1 min-w-0">
|
|
154
|
+
<Avatar name={contact.name} labelName={contact.name} />
|
|
155
|
+
<div className="flex flex-col items-start justify-center flex-1 min-w-0">
|
|
156
|
+
<p className="font-['Hanken_Grotesk',sans-serif] font-normal leading-5 text-sm text-[#334155] truncate w-full">
|
|
157
|
+
{contact.name}
|
|
158
|
+
</p>
|
|
159
|
+
<p className="font-['Hanken_Grotesk',sans-serif] font-normal leading-[18px] text-xs text-[#64758b] truncate w-full">
|
|
160
|
+
{contact.email}
|
|
161
|
+
</p>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
{contact.isSelected && (
|
|
165
|
+
<div className="overflow-clip relative shrink-0 w-3.5 h-3.5">
|
|
166
|
+
<i className="pi pi-check text-[#276cf0] text-sm"></i>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ConversationStatus, ConversationBoxSize } from "./types";
|
|
3
|
+
import statusOpenIcon from '../../assets/status_open.svg';
|
|
4
|
+
import statusPendingIcon from '../../assets/status-snoozed.svg';
|
|
5
|
+
import statusClosedIcon from '../../assets/status-resolved.svg';
|
|
6
|
+
import conversationIcon from '../../assets/conversation.svg';
|
|
7
|
+
import emailIcon from '../../assets/email.svg';
|
|
8
|
+
import sharedMailboxIcon from '../../assets/sm_inbox.svg';
|
|
9
|
+
|
|
10
|
+
// Status configuration with colors and icons
|
|
11
|
+
export const statusConfig: Record<ConversationStatus, { label: string; color: string; iconColor: string; icon: string }> = {
|
|
12
|
+
open: { label: 'Open', color: '#E37144', iconColor: '#E3692C', icon: statusOpenIcon },
|
|
13
|
+
pending: { label: 'Pending', color: '#5398CF', iconColor: '#EC8A54', icon: statusPendingIcon },
|
|
14
|
+
closed: { label: 'Closed', color: '#2DBB6D', iconColor: '#276CF0', icon: statusClosedIcon },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Size configuration
|
|
18
|
+
export const sizeConfig: Record<ConversationBoxSize, {
|
|
19
|
+
padding: { email: string; chat: string };
|
|
20
|
+
gap: string;
|
|
21
|
+
iconSize: string;
|
|
22
|
+
fontSize: { title: string; body: string; timestamp: string };
|
|
23
|
+
avatarSize: string;
|
|
24
|
+
avatarText: string;
|
|
25
|
+
maxWidth: string;
|
|
26
|
+
dividerHeight: string;
|
|
27
|
+
}> = {
|
|
28
|
+
default: {
|
|
29
|
+
padding: {
|
|
30
|
+
email: 'cm:px-5 cm:py-4',
|
|
31
|
+
chat: 'cm:px-5 cm:py-4',
|
|
32
|
+
},
|
|
33
|
+
gap: 'cm:gap-1.5',
|
|
34
|
+
iconSize: 'cm:w-3.5 cm:h-3.5',
|
|
35
|
+
fontSize: {
|
|
36
|
+
title: 'cm:text-sm',
|
|
37
|
+
body: 'cm:text-sm',
|
|
38
|
+
timestamp: 'cm:text-[13px]',
|
|
39
|
+
},
|
|
40
|
+
avatarSize: 'cm:w-4 cm:h-4',
|
|
41
|
+
avatarText: 'cm:text-[9px]',
|
|
42
|
+
maxWidth: 'cm:max-w-[914px]',
|
|
43
|
+
dividerHeight: 'cm:h-4',
|
|
44
|
+
},
|
|
45
|
+
small: {
|
|
46
|
+
padding: {
|
|
47
|
+
email: 'cm:p-4',
|
|
48
|
+
chat: 'cm:p-4',
|
|
49
|
+
},
|
|
50
|
+
gap: 'cm:gap-1',
|
|
51
|
+
iconSize: 'cm:w-3 cm:h-3',
|
|
52
|
+
fontSize: {
|
|
53
|
+
title: 'cm:text-xs',
|
|
54
|
+
body: 'cm:text-xs',
|
|
55
|
+
timestamp: 'cm:text-xs',
|
|
56
|
+
},
|
|
57
|
+
avatarSize: 'cm:w-3 cm:h-3',
|
|
58
|
+
avatarText: 'cm:text-[8px]',
|
|
59
|
+
maxWidth: 'cm:max-w-[330px]',
|
|
60
|
+
dividerHeight: 'cm:h-3',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Icon components
|
|
65
|
+
interface IconProps {
|
|
66
|
+
iconSize: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const ChatIcon: React.FC<IconProps> = ({ iconSize }) => (
|
|
70
|
+
<img
|
|
71
|
+
src={conversationIcon}
|
|
72
|
+
alt="Chat"
|
|
73
|
+
className={iconSize}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
export const EnvelopeIcon: React.FC<IconProps> = ({ iconSize }) => (
|
|
78
|
+
<img
|
|
79
|
+
src={emailIcon}
|
|
80
|
+
alt="Email"
|
|
81
|
+
className={iconSize}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
export const SharedMailboxIcon: React.FC<IconProps> = ({ iconSize }) => (
|
|
86
|
+
<img
|
|
87
|
+
src={sharedMailboxIcon}
|
|
88
|
+
alt="Shared Mailbox"
|
|
89
|
+
className={iconSize}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
export const StatusIcon: React.FC<IconProps & { icon: string; label: string }> = ({ iconSize, icon, label }) => (
|
|
94
|
+
<img
|
|
95
|
+
src={icon}
|
|
96
|
+
alt={label}
|
|
97
|
+
className={iconSize}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import type { ConversationBoxProps } from './types';
|
|
3
|
+
import { statusConfig, sizeConfig, ChatIcon, EnvelopeIcon, SharedMailboxIcon, StatusIcon } from './constants';
|
|
4
|
+
import Avatar from '../Avatar';
|
|
5
|
+
import openInANewTab from '../../assets/open_in_a_new_tab.svg';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const ConversationBox: React.FC<ConversationBoxProps> = ({
|
|
9
|
+
type,
|
|
10
|
+
status,
|
|
11
|
+
size = 'default',
|
|
12
|
+
senderName,
|
|
13
|
+
timestamp,
|
|
14
|
+
messagePreview,
|
|
15
|
+
heading,
|
|
16
|
+
mailboxName,
|
|
17
|
+
assignedAgent,
|
|
18
|
+
onClick,
|
|
19
|
+
}) => {
|
|
20
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
21
|
+
const statusInfo = statusConfig[status];
|
|
22
|
+
const sizeStyles = sizeConfig[size];
|
|
23
|
+
|
|
24
|
+
// Divider line (vertical)
|
|
25
|
+
const Divider = () => (
|
|
26
|
+
<div className={`${sizeStyles.dividerHeight} cm:w-px cm:bg-slate-border-light cm:shrink-0`} />
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const handleActionClick = (e: React.MouseEvent) => {
|
|
30
|
+
e.stopPropagation(); // Prevent triggering the onClick handler
|
|
31
|
+
// Handle action button click (e.g., show menu)
|
|
32
|
+
console.log('Action button clicked');
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
className={`cm:border cm:p-3.5 cm:border-slate-border-light cm:rounded-[10px] cm:flex cm:flex-col ${sizeStyles.gap} cm:w-full ${sizeStyles.maxWidth} cm:cursor-pointer hover:cm:bg-slate-surface-subtle_100 cm:transition-colors cm:relative ${type === 'email' ? sizeStyles.padding.email : sizeStyles.padding.chat
|
|
38
|
+
}`}
|
|
39
|
+
onClick={onClick}
|
|
40
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
41
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
42
|
+
>
|
|
43
|
+
{/* Top row: Sender, Status, Timestamp */}
|
|
44
|
+
<div className={`cm:flex ${size === 'small' ? 'cm:gap-2' : 'cm:gap-2.5'} cm:items-center cm:w-full`}>
|
|
45
|
+
<div className={`cm:flex-1 cm:flex ${size === 'small' ? 'cm:gap-2' : 'cm:gap-2.5'} cm:items-center cm:min-w-0`}>
|
|
46
|
+
{/* Icon and Sender Name */}
|
|
47
|
+
<div className={`cm:flex ${size === 'small' ? 'cm:gap-1.5' : 'cm:gap-2'} cm:items-center cm:shrink-0`}>
|
|
48
|
+
<div className={`${sizeStyles.iconSize} cm:shrink-0 cm:flex cm:items-center cm:justify-center cm:text-slate-icons-subtle`}>
|
|
49
|
+
{type === 'chat' ? <ChatIcon iconSize={sizeStyles.iconSize} /> : <EnvelopeIcon iconSize={sizeStyles.iconSize} />}
|
|
50
|
+
</div>
|
|
51
|
+
<div className="cm:flex cm:gap-1 cm:items-center">
|
|
52
|
+
<p className={`cm:font-medium cm:leading-5 ${sizeStyles.fontSize.title} cm:text-slate-text-title cm:whitespace-nowrap`}>
|
|
53
|
+
{senderName}
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Divider */}
|
|
59
|
+
<Divider />
|
|
60
|
+
|
|
61
|
+
{/* Status Chip */}
|
|
62
|
+
<div className={`cm:flex ${size === 'small' ? 'cm:gap-1.5' : 'cm:gap-2'} cm:items-center cm:justify-center cm:rounded-2xl cm:shrink-0`}>
|
|
63
|
+
<div className={`${sizeStyles.iconSize} cm:shrink-0 cm:flex cm:items-center cm:justify-center`}>
|
|
64
|
+
<StatusIcon iconSize={sizeStyles.iconSize} icon={statusInfo.icon} label={statusInfo.label} />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Timestamp or Action Button - fixed width container to prevent layout shift */}
|
|
70
|
+
<div className="cm:relative cm:shrink-0 cm:min-w-[100px] cm:flex cm:items-center cm:justify-end">
|
|
71
|
+
{/* Timestamp - always rendered but hidden on hover */}
|
|
72
|
+
<p
|
|
73
|
+
className={`cm:font-normal cm:leading-[18px] ${sizeStyles.fontSize.timestamp} cm:text-slate-text-subtle cm:whitespace-nowrap cm:text-right cm:transition-opacity ${
|
|
74
|
+
isHovered ? 'cm:opacity-0' : 'cm:opacity-100'
|
|
75
|
+
}`}
|
|
76
|
+
>
|
|
77
|
+
{timestamp}
|
|
78
|
+
</p>
|
|
79
|
+
|
|
80
|
+
{/* Action Button - absolutely positioned, shown on hover */}
|
|
81
|
+
<button
|
|
82
|
+
onClick={handleActionClick}
|
|
83
|
+
className={`cm:absolute cm:right-0 cm:bg-slate-surface-white cm:border cm:border-slate-border-light cm:rounded-[6px] cm:flex cm:items-center cm:justify-center cm:p-1.5 cm:w-7 cm:h-7 cm:hover:bg-slate-surface-subtle cm:transition-all ${
|
|
84
|
+
isHovered ? 'cm:opacity-100 cm:pointer-events-auto' : 'cm:opacity-0 cm:pointer-events-none'
|
|
85
|
+
}`}
|
|
86
|
+
aria-label="Open in a new tab"
|
|
87
|
+
>
|
|
88
|
+
<div className="cm:w-3.5 cm:h-3.5 cm:flex cm:items-center cm:justify-center">
|
|
89
|
+
<img src={openInANewTab} alt="Open in a new tab" className="cm:w-3.5 cm:h-3.5" />
|
|
90
|
+
</div>
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Content Section */}
|
|
96
|
+
<div className="cm:flex cm:flex-col cm:items-start cm:w-full">
|
|
97
|
+
{/* Heading (only for email) */}
|
|
98
|
+
{type === 'email' && heading && (
|
|
99
|
+
<div className={`cm:flex ${size === 'small' ? 'cm:gap-2' : 'cm:gap-3'} cm:items-center cm:w-full`}>
|
|
100
|
+
<div className={`cm:flex-1 cm:flex cm:flex-col cm:font-normal cm:justify-center cm:leading-none cm:overflow-hidden ${sizeStyles.fontSize.body} cm:text-slate-text-body`}>
|
|
101
|
+
<p className="cm:leading-5 cm:text-left cm:overflow-ellipsis cm:overflow-hidden">
|
|
102
|
+
{heading}
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{/* Message Preview and Footer */}
|
|
109
|
+
<div className={`cm:flex ${size === 'small' ? 'cm:flex-col cm:gap-1' : 'cm:flex-row cm:items-center cm:justify-between'} cm:w-full`}>
|
|
110
|
+
<p className={`cm:font-normal cm:leading-5 cm:overflow-ellipsis cm:overflow-hidden ${sizeStyles.fontSize.body} cm:text-slate-text-subtle cm:whitespace-nowrap ${size === 'small' ? 'cm:w-full' : 'cm:max-w-[560px]'}`}>
|
|
111
|
+
{messagePreview}
|
|
112
|
+
</p>
|
|
113
|
+
|
|
114
|
+
{/* MailboxName and Assigned Agent */}
|
|
115
|
+
<div className={`cm:flex ${size === 'small' ? 'cm:gap-2 cm:w-full' : 'cm:gap-2.5'} cm:items-center cm:shrink-0`}>
|
|
116
|
+
{/* mailboxName */}
|
|
117
|
+
{mailboxName && (
|
|
118
|
+
<>
|
|
119
|
+
<div className="cm:flex cm:gap-1 cm:items-center cm:justify-center cm:py-0.5 cm:rounded cm:shrink-0">
|
|
120
|
+
<div className={`${sizeStyles.iconSize} cm:overflow-hidden cm:shrink-0 cm:flex cm:items-center cm:justify-center cm:text-slate-icons-subtle`}>
|
|
121
|
+
<SharedMailboxIcon iconSize={sizeStyles.iconSize} />
|
|
122
|
+
</div>
|
|
123
|
+
<p className={`cm:font-normal cm:leading-5 ${sizeStyles.fontSize.body} cm:text-slate-text-subtle cm:whitespace-nowrap`}>
|
|
124
|
+
{mailboxName}
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
<Divider />
|
|
128
|
+
</>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* Assigned Agent */}
|
|
132
|
+
{assignedAgent && (
|
|
133
|
+
<div className={`cm:flex ${size === 'small' ? 'cm:gap-1' : 'cm:gap-1.5'} cm:items-center cm:shrink-0`}>
|
|
134
|
+
<Avatar name={assignedAgent.name} size={size} />
|
|
135
|
+
<p className={`cm:font-normal cm:leading-5 ${sizeStyles.fontSize.body} cm:text-slate-text-subtle cm:whitespace-nowrap`}>
|
|
136
|
+
{assignedAgent.name}
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export default ConversationBox;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type ConversationType = 'chat' | 'email';
|
|
2
|
+
export type ConversationStatus = 'open' | 'pending' | 'closed';
|
|
3
|
+
export type ConversationBoxSize = 'default' | 'small';
|
|
4
|
+
|
|
5
|
+
export interface ConversationBoxProps {
|
|
6
|
+
type: ConversationType;
|
|
7
|
+
status: ConversationStatus;
|
|
8
|
+
size?: ConversationBoxSize;
|
|
9
|
+
senderName: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
messagePreview: string;
|
|
12
|
+
heading?: string; // Only used for email type
|
|
13
|
+
mailboxName?: string;
|
|
14
|
+
assignedAgent?: {
|
|
15
|
+
name: string;
|
|
16
|
+
avatarColor?: string;
|
|
17
|
+
avatarInitial?: string;
|
|
18
|
+
};
|
|
19
|
+
onClick?: () => void;
|
|
20
|
+
}
|