@banbox/chat 1.0.7 → 1.0.9
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/dist/index.cjs +1236 -235
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.js +1160 -160
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chat/InboxPopup.tsx +105 -42
- package/src/chat/SinglePopup.tsx +59 -14
- package/src/icons/index.tsx +55 -0
- package/src/index.ts +14 -12
- package/src/modals/ChatAddressModal.tsx +844 -0
- package/src/modals/{chat/ChatConfirmModal.tsx → ChatConfirmModal.tsx} +2 -2
- package/src/modals/ChatTranslateSettingsModal.tsx +182 -0
- package/src/styles/index.build.css +15 -0
- package/src/styles/index.css +10 -2
- package/src/ui/{chat/AttachmentPreviewStrip.tsx → AttachmentPreviewStrip.tsx} +2 -2
- package/src/ui/{chat/ChatComposerBar.tsx → ChatComposerBar.tsx} +2 -2
- package/src/ui/{chat/ChatFooter.tsx → ChatFooter.tsx} +102 -8
- package/src/ui/{chat/ChatHeader.tsx → ChatHeader.tsx} +7 -4
- package/src/ui/{chat/ChatIdentity.tsx → ChatIdentity.tsx} +2 -2
- package/src/ui/{chat/ChatInquiryBar.tsx → ChatInquiryBar.tsx} +1 -1
- package/src/ui/ChatKebabMenu.tsx +125 -0
- package/src/ui/{chat/ChatListHeader.tsx → ChatListHeader.tsx} +49 -25
- package/src/ui/{chat/ChatMessageItem.tsx → ChatMessageItem.tsx} +1 -1
- package/src/ui/ChatScroll.tsx +59 -0
- package/src/ui/{chat/ChatSpinner.tsx → ChatSpinner.tsx} +1 -1
- package/src/ui/{chat/ChatThreadItem.tsx → ChatThreadItem.tsx} +9 -16
- package/src/ui/{chat/MessageHoverActions.tsx → MessageHoverActions.tsx} +2 -2
- package/src/ui/{chat/ReplyCard.tsx → ReplyCard.tsx} +2 -2
- package/src/ui/{chat/TypingIndicator.tsx → TypingIndicator.tsx} +1 -1
- package/src/ui/{chat/drop-up → drop-up}/BusinessCardDropup.tsx +15 -3
- package/src/ui/{chat/drop-up → drop-up}/EmojiDropup.tsx +1 -1
- package/src/ui/{chat/message-items → message-items}/ChatAddressCard.tsx +4 -4
- package/src/ui/{chat/message-items → message-items}/ChatBubbleFiles.tsx +1 -1
- package/src/ui/{chat/message-items → message-items}/ChatBubbleImages.tsx +2 -2
- package/src/ui/{chat/message-items → message-items}/ChatBusinessCard.tsx +1 -1
- package/src/ui/{chat/scrollToMessage.ts → scrollToMessage.ts} +1 -1
- package/src/ui/{chat/types.ts → types.ts} +2 -2
- package/src/modals/chat/ChatTranslateSettingsModal.tsx +0 -180
- package/src/ui/chat/ChatScroll.tsx +0 -52
- /package/src/ui/{chat/message-items → message-items}/ChatBubbleAudio.tsx +0 -0
- /package/src/ui/{chat/message-items → message-items}/ChatBubbleText.tsx +0 -0
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
// modals/ChatAddressModal.tsx
|
|
2
|
+
// 100% UI clone of marketplace ChatAddressModal + CustomerAddressCard + CustomerDeliveryAddressForm
|
|
3
|
+
"use client";
|
|
4
|
+
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
MapPinIcon,
|
|
10
|
+
ChatXIcon,
|
|
11
|
+
EditIcon,
|
|
12
|
+
TrashIcon,
|
|
13
|
+
EmailIcon,
|
|
14
|
+
BadgeHomeAddrIcon,
|
|
15
|
+
BadgeOfficeIcon,
|
|
16
|
+
CheckboxFilledIcon,
|
|
17
|
+
MapIcon2,
|
|
18
|
+
} from "../icons";
|
|
19
|
+
import type { AddressCard } from "../types";
|
|
20
|
+
|
|
21
|
+
/* ─────────────────────── Types ─────────────────────────── */
|
|
22
|
+
|
|
23
|
+
type Mode = "list" | "add" | "edit";
|
|
24
|
+
|
|
25
|
+
type AddressData = {
|
|
26
|
+
id: string;
|
|
27
|
+
firstName?: string;
|
|
28
|
+
lastName?: string;
|
|
29
|
+
businessName?: string;
|
|
30
|
+
mobileNumber?: string;
|
|
31
|
+
email?: string | null;
|
|
32
|
+
addressLabel?: "Home" | "Office" | string;
|
|
33
|
+
isDefault?: boolean;
|
|
34
|
+
country?: string;
|
|
35
|
+
district?: string | null;
|
|
36
|
+
policeStation?: string | null;
|
|
37
|
+
state?: string | null;
|
|
38
|
+
city?: string | null;
|
|
39
|
+
postalCode?: string;
|
|
40
|
+
addressLine?: string;
|
|
41
|
+
landMark?: string | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type Props = {
|
|
45
|
+
open: boolean;
|
|
46
|
+
onClose: () => void;
|
|
47
|
+
onSend: (card: AddressCard) => void;
|
|
48
|
+
initialId?: string;
|
|
49
|
+
className?: string;
|
|
50
|
+
variant?: "single" | "group";
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/* ─────────────────────── Mock data (same as marketplace) ─────────────────────── */
|
|
54
|
+
|
|
55
|
+
const MOCK_ADDRESSES: AddressData[] = [
|
|
56
|
+
{
|
|
57
|
+
id: "ca-1",
|
|
58
|
+
isDefault: false,
|
|
59
|
+
addressLabel: "Office",
|
|
60
|
+
businessName: "Oceanget",
|
|
61
|
+
firstName: "Ariful",
|
|
62
|
+
lastName: "Islam",
|
|
63
|
+
mobileNumber: "+11712345678",
|
|
64
|
+
email: "ariful.ca@example.com",
|
|
65
|
+
country: "Canada",
|
|
66
|
+
postalCode: "M5V 3K2",
|
|
67
|
+
addressLine: "1001 Birchwood Ave, Apt 12",
|
|
68
|
+
landMark: "Near Harbourfront",
|
|
69
|
+
district: null,
|
|
70
|
+
policeStation: null,
|
|
71
|
+
state: "Ontario",
|
|
72
|
+
city: "Toronto",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "bd-1",
|
|
76
|
+
isDefault: true,
|
|
77
|
+
addressLabel: "Home",
|
|
78
|
+
firstName: "Ariful",
|
|
79
|
+
lastName: "Islam",
|
|
80
|
+
mobileNumber: "+8801712345678",
|
|
81
|
+
email: null,
|
|
82
|
+
country: "Bangladesh",
|
|
83
|
+
postalCode: "1212",
|
|
84
|
+
addressLine: "House 12, Road 34, Gulshan 2",
|
|
85
|
+
landMark: "N/A",
|
|
86
|
+
district: "Dhaka",
|
|
87
|
+
policeStation: "Gulshan",
|
|
88
|
+
state: null,
|
|
89
|
+
city: null,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "bd-2",
|
|
93
|
+
isDefault: false,
|
|
94
|
+
addressLabel: "Office",
|
|
95
|
+
businessName: "Oceanget",
|
|
96
|
+
firstName: "Ariful",
|
|
97
|
+
lastName: "Islam",
|
|
98
|
+
mobileNumber: "+8801712345678",
|
|
99
|
+
email: null,
|
|
100
|
+
country: "Bangladesh",
|
|
101
|
+
postalCode: "1212",
|
|
102
|
+
addressLine: "House 12, Road 34, Gulshan 2",
|
|
103
|
+
landMark: "N/A",
|
|
104
|
+
district: "Dhaka",
|
|
105
|
+
policeStation: "Gulshan",
|
|
106
|
+
state: null,
|
|
107
|
+
city: null,
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const toAddressCard = (a: AddressData): AddressCard => ({
|
|
112
|
+
name: `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim(),
|
|
113
|
+
label: (a.addressLabel as "Home" | "Office" | undefined) ?? (a.isDefault ? "Home" : undefined),
|
|
114
|
+
businessName: a.businessName ?? undefined,
|
|
115
|
+
mobileNumber: a.mobileNumber,
|
|
116
|
+
country: a.country,
|
|
117
|
+
district: a.district ?? null,
|
|
118
|
+
policeStation: a.policeStation ?? null,
|
|
119
|
+
state: a.state ?? null,
|
|
120
|
+
city: a.city ?? null,
|
|
121
|
+
postalCode: a.postalCode ?? undefined,
|
|
122
|
+
addressLine: a.addressLine,
|
|
123
|
+
landMark: a.landMark ?? null,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/* ─────────────────────── FieldRow (exact marketplace clone) ─────────────────────── */
|
|
127
|
+
|
|
128
|
+
const FieldRow: React.FC<{
|
|
129
|
+
icon: React.ReactNode;
|
|
130
|
+
label: string;
|
|
131
|
+
value?: string | null;
|
|
132
|
+
highlight?: boolean;
|
|
133
|
+
isAction?: boolean;
|
|
134
|
+
}> = ({ icon, label, value, highlight = false, isAction = true }) => {
|
|
135
|
+
if (value === null || value === undefined) return null;
|
|
136
|
+
return (
|
|
137
|
+
<div className="flex items-start gap-2 text-xs tracking-[0.25px]">
|
|
138
|
+
<span className={clsx("flex items-center justify-center text-black", isAction ? "w-4 h-4" : "w-3.5 h-3.5")}>
|
|
139
|
+
{icon}
|
|
140
|
+
</span>
|
|
141
|
+
<strong className={clsx("text-[12px] font-medium text-black", isAction ? "min-w-[106px]" : "min-w-[100px]")}>
|
|
142
|
+
{label}
|
|
143
|
+
</strong>
|
|
144
|
+
<span className={clsx("font-normal", highlight ? "text-black" : "text-[#636363]")}>
|
|
145
|
+
{value ?? "N/A"}
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/* ─────────────────────── BadgeLabel (mirrors BadgeButton secondary) ─────────────────────── */
|
|
152
|
+
|
|
153
|
+
const BadgeLabel: React.FC<{
|
|
154
|
+
label: string;
|
|
155
|
+
icon: React.ReactNode;
|
|
156
|
+
}> = ({ label, icon }) => (
|
|
157
|
+
<span className="inline-flex items-center gap-1 rounded-xs bg-[#FFDBCF] px-2 py-[3px] text-[#FF5300] select-none">
|
|
158
|
+
<span className="inline-flex">{icon}</span>
|
|
159
|
+
<span className="text-xs font-medium tracking-[0.5px]">{label}</span>
|
|
160
|
+
</span>
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
/* ─────────────────────── RadioPill (exact marketplace clone) ─────────────────────── */
|
|
164
|
+
|
|
165
|
+
const RadioPill: React.FC<{
|
|
166
|
+
name: string;
|
|
167
|
+
value: string;
|
|
168
|
+
label: string;
|
|
169
|
+
checked: boolean;
|
|
170
|
+
onChange: (v: string) => void;
|
|
171
|
+
}> = ({ name, value, label, checked, onChange }) => (
|
|
172
|
+
<label className="group inline-flex h-[30px] select-none overflow-hidden rounded-xs bg-[#F8F8F8] cursor-pointer">
|
|
173
|
+
<input
|
|
174
|
+
type="radio"
|
|
175
|
+
name={name}
|
|
176
|
+
value={value}
|
|
177
|
+
checked={checked}
|
|
178
|
+
onChange={() => onChange(value)}
|
|
179
|
+
className="sr-only"
|
|
180
|
+
/>
|
|
181
|
+
{/* Radio indicator */}
|
|
182
|
+
<span className="flex h-[30px] w-[30px] items-center justify-center rounded-xs border border-[#F8F8F8] bg-white">
|
|
183
|
+
<span
|
|
184
|
+
className={clsx(
|
|
185
|
+
"flex h-3.5 w-3.5 items-center justify-center rounded-full border-[1.5px] transition-all duration-200 ease-out group-hover:border-2",
|
|
186
|
+
checked ? "border-[#3D3D3D]" : "",
|
|
187
|
+
)}
|
|
188
|
+
>
|
|
189
|
+
<span
|
|
190
|
+
className={clsx(
|
|
191
|
+
"rounded-full bg-[#3D3D3D] transition-all duration-200 ease-out",
|
|
192
|
+
checked ? "h-2 w-2 opacity-100" : "h-2 w-2 opacity-0",
|
|
193
|
+
"group-hover:h-[6px] group-hover:w-[6px]",
|
|
194
|
+
)}
|
|
195
|
+
/>
|
|
196
|
+
</span>
|
|
197
|
+
</span>
|
|
198
|
+
{/* Label */}
|
|
199
|
+
<span className="flex h-full items-center px-4 text-sm font-medium tracking-[0.25px] text-[#2C2C2C]">
|
|
200
|
+
{label}
|
|
201
|
+
</span>
|
|
202
|
+
</label>
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
/* ─────────────────────── CustomerAddressCard (exact marketplace clone) ─────────────────────── */
|
|
206
|
+
|
|
207
|
+
const CustomerAddressCard: React.FC<{
|
|
208
|
+
addr: AddressData;
|
|
209
|
+
isSelected: boolean;
|
|
210
|
+
onSelect: () => void;
|
|
211
|
+
onEdit: () => void;
|
|
212
|
+
onDelete: () => void;
|
|
213
|
+
}> = ({ addr, isSelected, onSelect, onEdit, onDelete }) => {
|
|
214
|
+
const fullName = [addr.firstName, addr.lastName].filter(Boolean).join(" ");
|
|
215
|
+
|
|
216
|
+
const addressParts = [
|
|
217
|
+
addr.landMark,
|
|
218
|
+
addr.addressLine,
|
|
219
|
+
addr.policeStation,
|
|
220
|
+
addr.district,
|
|
221
|
+
addr.state,
|
|
222
|
+
addr.city,
|
|
223
|
+
addr.postalCode,
|
|
224
|
+
addr.country,
|
|
225
|
+
].filter((part) => part && String(part).trim().length > 0);
|
|
226
|
+
|
|
227
|
+
const combinedAddress = addressParts.length ? addressParts.join(", ") : undefined;
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div
|
|
231
|
+
className={clsx(
|
|
232
|
+
"border rounded-sm p-3 relative cursor-pointer hover:border-[#636363] flex gap-3",
|
|
233
|
+
isSelected ? "text-black border-[#636363]" : "border-[#ededed]",
|
|
234
|
+
)}
|
|
235
|
+
onClick={onSelect}
|
|
236
|
+
onKeyDown={(e) => {
|
|
237
|
+
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onSelect(); }
|
|
238
|
+
}}
|
|
239
|
+
role="radio"
|
|
240
|
+
aria-checked={isSelected}
|
|
241
|
+
tabIndex={0}
|
|
242
|
+
>
|
|
243
|
+
<div className="flex-1">
|
|
244
|
+
{/* Header row */}
|
|
245
|
+
<div className="flex justify-between items-center h-[30px]">
|
|
246
|
+
<div className="flex items-center gap-[18px]">
|
|
247
|
+
<h3 className="text-xl font-semibold tracking-[0.5px] text-black">
|
|
248
|
+
{fullName || "Recipient"}
|
|
249
|
+
</h3>
|
|
250
|
+
{addr.addressLabel && (
|
|
251
|
+
<BadgeLabel
|
|
252
|
+
label={addr.addressLabel}
|
|
253
|
+
icon={
|
|
254
|
+
addr.addressLabel === "Home"
|
|
255
|
+
? <BadgeHomeAddrIcon className="w-3 h-3" />
|
|
256
|
+
: <BadgeOfficeIcon className="w-3 h-3" />
|
|
257
|
+
}
|
|
258
|
+
/>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
{addr.isDefault && (
|
|
262
|
+
<span className="flex items-center text-[14px] font-medium tracking-[0.5px] text-[#636363]">
|
|
263
|
+
<CheckboxFilledIcon className="h-[16px] w-[16px]" />
|
|
264
|
+
<span className="ml-1.5">Default</span>
|
|
265
|
+
</span>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Fields */}
|
|
270
|
+
<div className="grid grid-cols-1 text-sm mt-3 max-w-[80%] gap-y-1.5">
|
|
271
|
+
{addr.businessName && (
|
|
272
|
+
<FieldRow
|
|
273
|
+
icon={<BadgeOfficeIcon className="w-4 h-4" />}
|
|
274
|
+
label="Business Name"
|
|
275
|
+
value={addr.businessName}
|
|
276
|
+
highlight
|
|
277
|
+
/>
|
|
278
|
+
)}
|
|
279
|
+
<FieldRow
|
|
280
|
+
icon={<MapIcon2 className="w-4 h-4" />}
|
|
281
|
+
label="Mobile Number"
|
|
282
|
+
value={addr.mobileNumber}
|
|
283
|
+
highlight
|
|
284
|
+
/>
|
|
285
|
+
{addr.email !== null && addr.email !== undefined && (
|
|
286
|
+
<FieldRow
|
|
287
|
+
icon={<EmailIcon className="w-4 h-4" />}
|
|
288
|
+
label="Email"
|
|
289
|
+
value={addr.email}
|
|
290
|
+
/>
|
|
291
|
+
)}
|
|
292
|
+
<FieldRow
|
|
293
|
+
icon={<MapIcon2 className="w-4 h-4" />}
|
|
294
|
+
label="Address"
|
|
295
|
+
value={combinedAddress}
|
|
296
|
+
/>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
{/* Action buttons */}
|
|
300
|
+
<div className="absolute flex justify-end bottom-[12px] right-3 gap-3">
|
|
301
|
+
<button
|
|
302
|
+
type="button"
|
|
303
|
+
className="text-[#2C2C2C] hover:bg-black/10 rounded-full cursor-pointer w-9 h-9 flex items-center justify-center"
|
|
304
|
+
aria-label="Edit address"
|
|
305
|
+
onClick={(e) => { e.stopPropagation(); onEdit(); }}
|
|
306
|
+
>
|
|
307
|
+
<EditIcon className="w-5 h-5 text-black" />
|
|
308
|
+
</button>
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
311
|
+
className="text-[#2C2C2C] hover:bg-black/10 rounded-full cursor-pointer w-9 h-9 flex items-center justify-center"
|
|
312
|
+
aria-label="Delete address"
|
|
313
|
+
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
|
314
|
+
>
|
|
315
|
+
<TrashIcon className="w-5 h-5 text-[#ff5200]" />
|
|
316
|
+
</button>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/* ─────────────────────── InputField (marketplace style) ─────────────────────── */
|
|
324
|
+
|
|
325
|
+
const Field: React.FC<{
|
|
326
|
+
label: string;
|
|
327
|
+
required?: boolean;
|
|
328
|
+
optional?: boolean;
|
|
329
|
+
children: React.ReactNode;
|
|
330
|
+
}> = ({ label, required, optional, children }) => (
|
|
331
|
+
<div className="flex flex-col gap-1">
|
|
332
|
+
<label className="text-[12px] font-medium text-[#374151] tracking-[0.25px]">
|
|
333
|
+
{label}
|
|
334
|
+
{required && <span className="ml-0.5 text-[#ff5200]">*</span>}
|
|
335
|
+
{optional && <span className="ml-1 text-[#9ca3af] font-normal">(Optional)</span>}
|
|
336
|
+
</label>
|
|
337
|
+
{children}
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const Input: React.FC<React.InputHTMLAttributes<HTMLInputElement>> = (props) => (
|
|
342
|
+
<input
|
|
343
|
+
{...props}
|
|
344
|
+
className={clsx(
|
|
345
|
+
"rounded-sm border border-[#ededed] px-3 py-0 text-sm text-black placeholder-[#9ca3af]",
|
|
346
|
+
"outline-none focus:border-[#2753fb] focus:ring-1 focus:ring-[#2753fb]/20",
|
|
347
|
+
"h-[34px] w-full",
|
|
348
|
+
props.className,
|
|
349
|
+
)}
|
|
350
|
+
/>
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const SelectInput: React.FC<{
|
|
354
|
+
value: string;
|
|
355
|
+
onChange: (v: string) => void;
|
|
356
|
+
options: string[];
|
|
357
|
+
placeholder: string;
|
|
358
|
+
disabled?: boolean;
|
|
359
|
+
}> = ({ value, onChange, options, placeholder, disabled }) => (
|
|
360
|
+
<select
|
|
361
|
+
value={value}
|
|
362
|
+
onChange={(e) => onChange(e.target.value)}
|
|
363
|
+
disabled={disabled}
|
|
364
|
+
className={clsx(
|
|
365
|
+
"rounded-sm border border-[#ededed] px-3 py-0 text-sm text-black bg-white",
|
|
366
|
+
"outline-none focus:border-[#2753fb] focus:ring-1 focus:ring-[#2753fb]/20",
|
|
367
|
+
"h-[34px] w-full appearance-none",
|
|
368
|
+
!value && "text-[#9ca3af]",
|
|
369
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
370
|
+
)}
|
|
371
|
+
>
|
|
372
|
+
<option value="" disabled>{placeholder}</option>
|
|
373
|
+
{options.map((o) => <option key={o} value={o}>{o}</option>)}
|
|
374
|
+
</select>
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
/* ─────────────────────── AddressForm (mirrors CustomerDeliveryAddressForm) ─────────────────────── */
|
|
378
|
+
|
|
379
|
+
const BANGLADESH_DISTRICTS = [
|
|
380
|
+
"Dhaka", "Chittagong", "Sylhet", "Rajshahi", "Khulna", "Barisal", "Rangpur", "Mymensingh",
|
|
381
|
+
"Gazipur", "Narayanganj", "Tangail", "Faridpur", "Comilla", "Noakhali", "Cox's Bazar",
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
const COUNTRIES = ["Bangladesh", "India", "United States", "United Kingdom", "Canada", "Australia", "Germany", "France", "Saudi Arabia", "UAE"];
|
|
385
|
+
|
|
386
|
+
const AddressForm: React.FC<{
|
|
387
|
+
initial?: Partial<AddressData>;
|
|
388
|
+
onSave: (data: AddressData) => void;
|
|
389
|
+
onBack: () => void;
|
|
390
|
+
mode: "add" | "edit";
|
|
391
|
+
}> = ({ initial, onSave, onBack, mode }) => {
|
|
392
|
+
const [form, setForm] = useState<Partial<AddressData>>(
|
|
393
|
+
initial ?? { country: "Bangladesh", isDefault: false },
|
|
394
|
+
);
|
|
395
|
+
const [addressLabel, setAddressLabel] = useState<"home" | "office" | null>(
|
|
396
|
+
initial?.addressLabel?.toLowerCase() === "home" ? "home"
|
|
397
|
+
: initial?.addressLabel?.toLowerCase() === "office" ? "office"
|
|
398
|
+
: null,
|
|
399
|
+
);
|
|
400
|
+
const [isDefault, setIsDefault] = useState(Boolean(initial?.isDefault));
|
|
401
|
+
|
|
402
|
+
const set = (k: keyof AddressData, v: string | boolean | null) =>
|
|
403
|
+
setForm((prev) => ({ ...prev, [k]: v }));
|
|
404
|
+
|
|
405
|
+
const isBD = (form.country ?? "").toLowerCase() === "bangladesh";
|
|
406
|
+
|
|
407
|
+
const handleSave = () => {
|
|
408
|
+
onSave({
|
|
409
|
+
id: initial?.id ?? `addr-${Date.now()}`,
|
|
410
|
+
...form,
|
|
411
|
+
isDefault,
|
|
412
|
+
addressLabel: addressLabel === "home" ? "Home" : addressLabel === "office" ? "Office" : undefined,
|
|
413
|
+
} as AddressData);
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
return (
|
|
417
|
+
<form onSubmit={(e) => { e.preventDefault(); handleSave(); }} className="space-y-4">
|
|
418
|
+
{/* Name row */}
|
|
419
|
+
<div className="grid grid-cols-2 gap-4">
|
|
420
|
+
<Field label="First Name" required>
|
|
421
|
+
<Input value={form.firstName ?? ""} onChange={(e) => set("firstName", e.target.value)} placeholder="First Name" />
|
|
422
|
+
</Field>
|
|
423
|
+
<Field label="Last Name">
|
|
424
|
+
<Input value={form.lastName ?? ""} onChange={(e) => set("lastName", e.target.value)} placeholder="Last Name" />
|
|
425
|
+
</Field>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
{/* Mobile */}
|
|
429
|
+
<Field label="Mobile Number" required>
|
|
430
|
+
<Input value={form.mobileNumber ?? ""} onChange={(e) => set("mobileNumber", e.target.value)} placeholder="+880..." />
|
|
431
|
+
</Field>
|
|
432
|
+
|
|
433
|
+
{/* Email */}
|
|
434
|
+
<Field label="Email">
|
|
435
|
+
<Input type="email" value={(form.email as string) ?? ""} onChange={(e) => set("email", e.target.value)} placeholder="name@example.com" />
|
|
436
|
+
</Field>
|
|
437
|
+
|
|
438
|
+
{/* Country */}
|
|
439
|
+
<Field label={isBD ? "Country" : "Country / Region"} required>
|
|
440
|
+
<SelectInput
|
|
441
|
+
value={form.country ?? ""}
|
|
442
|
+
onChange={(v) => { set("country", v); set("district", ""); set("policeStation", ""); set("state", ""); set("city", ""); }}
|
|
443
|
+
options={COUNTRIES}
|
|
444
|
+
placeholder="Select country"
|
|
445
|
+
/>
|
|
446
|
+
</Field>
|
|
447
|
+
|
|
448
|
+
{/* BD vs International fields */}
|
|
449
|
+
{isBD ? (
|
|
450
|
+
<>
|
|
451
|
+
<div className="grid grid-cols-2 gap-4">
|
|
452
|
+
<Field label="District" required>
|
|
453
|
+
<SelectInput
|
|
454
|
+
value={form.district ?? ""}
|
|
455
|
+
onChange={(v) => { set("district", v); set("policeStation", ""); }}
|
|
456
|
+
options={BANGLADESH_DISTRICTS}
|
|
457
|
+
placeholder="District"
|
|
458
|
+
disabled={!form.country}
|
|
459
|
+
/>
|
|
460
|
+
</Field>
|
|
461
|
+
<Field label="Police Station" required>
|
|
462
|
+
<Input value={form.policeStation ?? ""} onChange={(e) => set("policeStation", e.target.value)} placeholder="Police Station" />
|
|
463
|
+
</Field>
|
|
464
|
+
</div>
|
|
465
|
+
<Field label="Postal Code" optional>
|
|
466
|
+
<Input value={form.postalCode ?? ""} onChange={(e) => set("postalCode", e.target.value)} placeholder="Postal Code" />
|
|
467
|
+
</Field>
|
|
468
|
+
</>
|
|
469
|
+
) : (
|
|
470
|
+
<>
|
|
471
|
+
<div className="grid grid-cols-2 gap-4">
|
|
472
|
+
<Field label="State" required>
|
|
473
|
+
<Input value={form.state ?? ""} onChange={(e) => set("state", e.target.value)} placeholder="State" />
|
|
474
|
+
</Field>
|
|
475
|
+
<Field label="City" required>
|
|
476
|
+
<Input value={form.city ?? ""} onChange={(e) => set("city", e.target.value)} placeholder="City" />
|
|
477
|
+
</Field>
|
|
478
|
+
</div>
|
|
479
|
+
<Field label="Postal Code" required>
|
|
480
|
+
<Input value={form.postalCode ?? ""} onChange={(e) => set("postalCode", e.target.value)} placeholder="Postal Code" />
|
|
481
|
+
</Field>
|
|
482
|
+
</>
|
|
483
|
+
)}
|
|
484
|
+
|
|
485
|
+
{/* Address line */}
|
|
486
|
+
<Field label="Address Line" required>
|
|
487
|
+
<Input
|
|
488
|
+
value={form.addressLine ?? ""}
|
|
489
|
+
onChange={(e) => set("addressLine", e.target.value)}
|
|
490
|
+
placeholder={isBD ? "House/Road/Block/Sector" : "Street, Apt, Suite, etc."}
|
|
491
|
+
/>
|
|
492
|
+
</Field>
|
|
493
|
+
|
|
494
|
+
{/* Landmark */}
|
|
495
|
+
<Field label="Land Mark" optional>
|
|
496
|
+
<Input value={(form.landMark as string) ?? ""} onChange={(e) => set("landMark", e.target.value)} placeholder="Nearby landmark" />
|
|
497
|
+
</Field>
|
|
498
|
+
|
|
499
|
+
{/* Default checkbox + Home/Office radio pills */}
|
|
500
|
+
<div className="flex items-center justify-between">
|
|
501
|
+
{/* Checkbox */}
|
|
502
|
+
<label className="flex items-center gap-1.5 cursor-pointer w-fit select-none text-sm text-[#2C2C2C] font-normal">
|
|
503
|
+
<input
|
|
504
|
+
type="checkbox"
|
|
505
|
+
checked={isDefault}
|
|
506
|
+
onChange={(e) => setIsDefault(e.target.checked)}
|
|
507
|
+
className="sr-only"
|
|
508
|
+
/>
|
|
509
|
+
<span
|
|
510
|
+
className={clsx(
|
|
511
|
+
"flex h-4 w-4 items-center justify-center rounded border transition-colors",
|
|
512
|
+
isDefault ? "bg-black border-black" : "border-[#d1d5db] bg-white",
|
|
513
|
+
)}
|
|
514
|
+
>
|
|
515
|
+
{isDefault && (
|
|
516
|
+
<svg viewBox="0 0 10 8" className="h-3 w-3 text-white" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
517
|
+
<path d="M1 4L3.5 6.5L9 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
518
|
+
</svg>
|
|
519
|
+
)}
|
|
520
|
+
</span>
|
|
521
|
+
Set as default delivery address
|
|
522
|
+
</label>
|
|
523
|
+
|
|
524
|
+
{/* Radio pills */}
|
|
525
|
+
<div className="flex items-center gap-[17px]">
|
|
526
|
+
<RadioPill name="addr-label" value="home" checked={addressLabel === "home"} label="Home" onChange={(v) => setAddressLabel(v as "home")} />
|
|
527
|
+
<RadioPill name="addr-label" value="office" checked={addressLabel === "office"} label="Office" onChange={(v) => setAddressLabel(v as "office")} />
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
|
|
531
|
+
{/* Form-level back/save buttons — these are rendered in modal footer, so just keep a hidden submit */}
|
|
532
|
+
<button type="submit" className="sr-only" id="addr-form-submit">Submit</button>
|
|
533
|
+
</form>
|
|
534
|
+
);
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
/* ─────────────────────── Delete Confirm dialog ─────────────────────── */
|
|
538
|
+
|
|
539
|
+
const DeleteConfirm: React.FC<{
|
|
540
|
+
onConfirm: () => void;
|
|
541
|
+
onCancel: () => void;
|
|
542
|
+
}> = ({ onConfirm, onCancel }) => (
|
|
543
|
+
<div className="fixed inset-0 z-[10010] flex items-center justify-center" onClick={onCancel}>
|
|
544
|
+
<div className="fixed inset-0 bg-black/40" />
|
|
545
|
+
<div
|
|
546
|
+
role="dialog"
|
|
547
|
+
aria-modal="true"
|
|
548
|
+
className="relative z-[10011] w-[420px] max-w-[95vw] rounded-md bg-white shadow-[0_12px_30px_rgba(0,0,0,0.18)] overflow-hidden"
|
|
549
|
+
onClick={(e) => e.stopPropagation()}
|
|
550
|
+
>
|
|
551
|
+
<div className="flex h-[44px] items-center bg-[#f8f8f8] px-6">
|
|
552
|
+
<h2 className="text-xl font-semibold text-black">Delete Address</h2>
|
|
553
|
+
</div>
|
|
554
|
+
<div className="px-6 py-4 space-y-1">
|
|
555
|
+
<p className="text-sm font-medium text-black">Are you sure you want to delete this address?</p>
|
|
556
|
+
<p className="text-xs text-[#636363]">The address is permanently removed. This action cannot be undone.</p>
|
|
557
|
+
</div>
|
|
558
|
+
<div className="flex h-[52px] items-center justify-end gap-3 bg-[#f0f4ff] px-6">
|
|
559
|
+
<button
|
|
560
|
+
type="button"
|
|
561
|
+
onClick={onCancel}
|
|
562
|
+
className="h-[34px] cursor-pointer rounded-[4px] border border-[#d1d5db] bg-white px-4 text-[13px] font-medium text-black hover:bg-[#f9fafb]"
|
|
563
|
+
>
|
|
564
|
+
Cancel
|
|
565
|
+
</button>
|
|
566
|
+
<button
|
|
567
|
+
type="button"
|
|
568
|
+
onClick={onConfirm}
|
|
569
|
+
className="h-[34px] cursor-pointer rounded-[4px] border-none bg-[#ff5200] px-4 text-[13px] font-medium text-white hover:bg-[#e64a00]"
|
|
570
|
+
>
|
|
571
|
+
Delete
|
|
572
|
+
</button>
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
/* ─────────────────────── Main Modal ─────────────────────── */
|
|
579
|
+
|
|
580
|
+
const ChatAddressModal: React.FC<Props> = ({
|
|
581
|
+
open,
|
|
582
|
+
onClose,
|
|
583
|
+
onSend,
|
|
584
|
+
initialId,
|
|
585
|
+
className,
|
|
586
|
+
variant = "group",
|
|
587
|
+
}) => {
|
|
588
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
589
|
+
const bodyRef = useRef<HTMLDivElement>(null);
|
|
590
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
591
|
+
|
|
592
|
+
const [mode, setMode] = useState<Mode>("list");
|
|
593
|
+
const [addresses, setAddresses] = useState<AddressData[]>(MOCK_ADDRESSES);
|
|
594
|
+
const [editData, setEditData] = useState<AddressData | null>(null);
|
|
595
|
+
const [deleteTarget, setDeleteTarget] = useState<AddressData | null>(null);
|
|
596
|
+
const [formKey, setFormKey] = useState(0);
|
|
597
|
+
|
|
598
|
+
const defaultId =
|
|
599
|
+
initialId ??
|
|
600
|
+
addresses.find((a) => a.isDefault)?.id ??
|
|
601
|
+
addresses[0]?.id;
|
|
602
|
+
|
|
603
|
+
const [selectedId, setSelectedId] = useState<string>(defaultId || "");
|
|
604
|
+
|
|
605
|
+
const selectedAddress = useMemo(
|
|
606
|
+
() => addresses.find((a) => a.id === selectedId) || null,
|
|
607
|
+
[addresses, selectedId],
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
useEffect(() => {
|
|
611
|
+
bodyRef.current?.scrollTo({ top: 0, behavior: "auto" });
|
|
612
|
+
}, [mode]);
|
|
613
|
+
|
|
614
|
+
useEffect(() => {
|
|
615
|
+
if (!open) return;
|
|
616
|
+
const onEsc = (e: KeyboardEvent) => e.key === "Escape" && onClose();
|
|
617
|
+
window.addEventListener("keydown", onEsc);
|
|
618
|
+
return () => window.removeEventListener("keydown", onEsc);
|
|
619
|
+
}, [open, onClose]);
|
|
620
|
+
|
|
621
|
+
// Reset to list on open
|
|
622
|
+
useEffect(() => {
|
|
623
|
+
if (open) setMode("list");
|
|
624
|
+
}, [open]);
|
|
625
|
+
|
|
626
|
+
if (!open) return null;
|
|
627
|
+
|
|
628
|
+
const isSingle = variant === "single";
|
|
629
|
+
|
|
630
|
+
const handleSave = (data: AddressData) => {
|
|
631
|
+
setAddresses((prev) => {
|
|
632
|
+
const idx = prev.findIndex((a) => a.id === data.id);
|
|
633
|
+
if (idx >= 0) {
|
|
634
|
+
const next = [...prev];
|
|
635
|
+
next[idx] = data;
|
|
636
|
+
return next;
|
|
637
|
+
}
|
|
638
|
+
return [...prev, data];
|
|
639
|
+
});
|
|
640
|
+
setSelectedId(data.id);
|
|
641
|
+
setMode("list");
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const handleDelete = (id: string) => {
|
|
645
|
+
setAddresses((prev) => prev.filter((a) => a.id !== id));
|
|
646
|
+
if (selectedId === id) {
|
|
647
|
+
const remaining = addresses.filter((a) => a.id !== id);
|
|
648
|
+
setSelectedId(remaining[0]?.id ?? "");
|
|
649
|
+
}
|
|
650
|
+
setDeleteTarget(null);
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const renderHeader = () => {
|
|
654
|
+
if (mode === "list") {
|
|
655
|
+
return (
|
|
656
|
+
<div className="flex justify-between items-center w-full">
|
|
657
|
+
<h2 className="text-xl tracking-[0.5px] capitalize font-semibold text-black">
|
|
658
|
+
Delivery Address
|
|
659
|
+
</h2>
|
|
660
|
+
<button
|
|
661
|
+
type="button"
|
|
662
|
+
onClick={() => { setEditData(null); setFormKey(k => k + 1); setMode("add"); }}
|
|
663
|
+
className="text-black hover:text-[#ff5200] cursor-pointer text-sm font-medium tracking-[0.5px] underline underline-offset-4"
|
|
664
|
+
>
|
|
665
|
+
Add New Address
|
|
666
|
+
</button>
|
|
667
|
+
</div>
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
if (mode === "add") {
|
|
671
|
+
return (
|
|
672
|
+
<div className="flex justify-between items-center capitalize w-full">
|
|
673
|
+
<h2 className="text-xl tracking-[0.5px] font-semibold text-black">
|
|
674
|
+
Add New Address
|
|
675
|
+
</h2>
|
|
676
|
+
<button type="button" className="text-[#005694] text-sm font-normal tracking-[0.25px] underline underline-offset-4 flex items-center gap-2 cursor-pointer">
|
|
677
|
+
<MapPinIcon className="w-[18px] h-[18px]" />
|
|
678
|
+
<span>Use current location</span>
|
|
679
|
+
</button>
|
|
680
|
+
</div>
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
if (mode === "edit") {
|
|
684
|
+
return (
|
|
685
|
+
<div className="flex justify-between items-center w-full">
|
|
686
|
+
<h2 className="text-xl tracking-[0.5px] uppercase font-semibold text-black">
|
|
687
|
+
Edit Address
|
|
688
|
+
</h2>
|
|
689
|
+
<button type="button" className="text-[#005694] text-sm font-normal tracking-[0.25px] underline underline-offset-4 flex items-center gap-2 cursor-pointer">
|
|
690
|
+
<MapPinIcon className="w-[18px] h-[18px]" />
|
|
691
|
+
<span>Use current location</span>
|
|
692
|
+
</button>
|
|
693
|
+
</div>
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
return null;
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
return (
|
|
700
|
+
<>
|
|
701
|
+
<div
|
|
702
|
+
className={clsx(
|
|
703
|
+
isSingle
|
|
704
|
+
? "fixed inset-0 z-[10003] flex"
|
|
705
|
+
: "absolute inset-0 z-[70] flex items-center justify-center",
|
|
706
|
+
)}
|
|
707
|
+
onClick={(e) => { e.stopPropagation(); onClose(); }}
|
|
708
|
+
>
|
|
709
|
+
{/* Backdrop */}
|
|
710
|
+
<div className="fixed inset-0 bg-black/30" />
|
|
711
|
+
|
|
712
|
+
<div
|
|
713
|
+
ref={panelRef}
|
|
714
|
+
role="dialog"
|
|
715
|
+
aria-modal="true"
|
|
716
|
+
aria-labelledby="chat-address-modal-title"
|
|
717
|
+
onClick={(e) => e.stopPropagation()}
|
|
718
|
+
className={clsx(
|
|
719
|
+
isSingle
|
|
720
|
+
? "fixed bottom-6 right-6 w-[700px] max-w-[95vw]"
|
|
721
|
+
: "relative w-[700px] max-w-[95vw]",
|
|
722
|
+
"overflow-hidden rounded-md bg-white shadow-[0_12px_30px_rgba(0,0,0,0.18)] z-[10004]",
|
|
723
|
+
className,
|
|
724
|
+
)}
|
|
725
|
+
>
|
|
726
|
+
{/* Header */}
|
|
727
|
+
<div className="h-[44px] px-8 py-[7px] flex items-center w-full bg-[var(--color-banbox-f8,#f8f8f8)] rounded-t-md">
|
|
728
|
+
{renderHeader()}
|
|
729
|
+
</div>
|
|
730
|
+
|
|
731
|
+
{/* Body */}
|
|
732
|
+
<div ref={bodyRef} className="px-8 py-3 overflow-y-auto h-[446px] custom-scroll">
|
|
733
|
+
{mode === "list" && (
|
|
734
|
+
<div className="space-y-4">
|
|
735
|
+
{addresses.map((addr) => (
|
|
736
|
+
<CustomerAddressCard
|
|
737
|
+
key={addr.id}
|
|
738
|
+
addr={addr}
|
|
739
|
+
isSelected={selectedId === addr.id}
|
|
740
|
+
onSelect={() => setSelectedId(addr.id)}
|
|
741
|
+
onEdit={() => { setEditData(addr); setFormKey(k => k + 1); setMode("edit"); }}
|
|
742
|
+
onDelete={() => setDeleteTarget(addr)}
|
|
743
|
+
/>
|
|
744
|
+
))}
|
|
745
|
+
</div>
|
|
746
|
+
)}
|
|
747
|
+
|
|
748
|
+
{(mode === "add" || mode === "edit") && (
|
|
749
|
+
<AddressForm
|
|
750
|
+
key={formKey}
|
|
751
|
+
initial={editData ?? undefined}
|
|
752
|
+
onSave={handleSave}
|
|
753
|
+
onBack={() => setMode("list")}
|
|
754
|
+
mode={mode}
|
|
755
|
+
/>
|
|
756
|
+
)}
|
|
757
|
+
</div>
|
|
758
|
+
|
|
759
|
+
{/* Footer */}
|
|
760
|
+
<div className="h-[52px] bg-[var(--color-banbox-primary-container,#f0f4ff)] w-full px-6 py-2 flex justify-end items-center gap-6 rounded-b-[6px]">
|
|
761
|
+
<div className="flex items-center justify-end gap-3">
|
|
762
|
+
{mode === "list" && (
|
|
763
|
+
<>
|
|
764
|
+
<button
|
|
765
|
+
type="button"
|
|
766
|
+
onClick={onClose}
|
|
767
|
+
className="h-[34px] cursor-pointer rounded-[4px] border border-[#d1d5db] bg-white px-4 text-[13px] font-medium text-black hover:bg-[#f9fafb]"
|
|
768
|
+
>
|
|
769
|
+
Cancel
|
|
770
|
+
</button>
|
|
771
|
+
<button
|
|
772
|
+
type="button"
|
|
773
|
+
disabled={!selectedAddress}
|
|
774
|
+
onClick={() => {
|
|
775
|
+
if (!selectedAddress) return;
|
|
776
|
+
onSend(toAddressCard(selectedAddress));
|
|
777
|
+
onClose();
|
|
778
|
+
}}
|
|
779
|
+
className="h-[34px] cursor-pointer rounded-[4px] border-none bg-[#ff5200] px-4 text-[13px] font-medium text-white hover:bg-[#e64a00] disabled:opacity-50"
|
|
780
|
+
>
|
|
781
|
+
Send
|
|
782
|
+
</button>
|
|
783
|
+
</>
|
|
784
|
+
)}
|
|
785
|
+
|
|
786
|
+
{mode === "add" && (
|
|
787
|
+
<>
|
|
788
|
+
<button
|
|
789
|
+
type="button"
|
|
790
|
+
onClick={() => setMode("list")}
|
|
791
|
+
className="h-[34px] cursor-pointer rounded-[4px] border border-[#d1d5db] bg-white px-4 text-[13px] font-medium text-black hover:bg-[#f9fafb]"
|
|
792
|
+
>
|
|
793
|
+
Back
|
|
794
|
+
</button>
|
|
795
|
+
<button
|
|
796
|
+
type="button"
|
|
797
|
+
onClick={() => {
|
|
798
|
+
// Trigger the hidden submit button inside the form
|
|
799
|
+
document.getElementById("addr-form-submit")?.click();
|
|
800
|
+
}}
|
|
801
|
+
className="h-[34px] cursor-pointer rounded-[4px] border-none bg-[#ff5200] px-4 text-[13px] font-medium text-white hover:bg-[#e64a00]"
|
|
802
|
+
>
|
|
803
|
+
Save Address
|
|
804
|
+
</button>
|
|
805
|
+
</>
|
|
806
|
+
)}
|
|
807
|
+
|
|
808
|
+
{mode === "edit" && (
|
|
809
|
+
<>
|
|
810
|
+
<button
|
|
811
|
+
type="button"
|
|
812
|
+
onClick={() => setMode("list")}
|
|
813
|
+
className="h-[34px] cursor-pointer rounded-[4px] border border-[#d1d5db] bg-white px-4 text-[13px] font-medium text-black hover:bg-[#f9fafb]"
|
|
814
|
+
>
|
|
815
|
+
Back
|
|
816
|
+
</button>
|
|
817
|
+
<button
|
|
818
|
+
type="button"
|
|
819
|
+
onClick={() => {
|
|
820
|
+
document.getElementById("addr-form-submit")?.click();
|
|
821
|
+
}}
|
|
822
|
+
className="h-[34px] cursor-pointer rounded-[4px] border-none bg-[#ff5200] px-4 text-[13px] font-medium text-white hover:bg-[#e64a00]"
|
|
823
|
+
>
|
|
824
|
+
Update
|
|
825
|
+
</button>
|
|
826
|
+
</>
|
|
827
|
+
)}
|
|
828
|
+
</div>
|
|
829
|
+
</div>
|
|
830
|
+
</div>
|
|
831
|
+
</div>
|
|
832
|
+
|
|
833
|
+
{/* Delete Confirmation */}
|
|
834
|
+
{deleteTarget && (
|
|
835
|
+
<DeleteConfirm
|
|
836
|
+
onConfirm={() => handleDelete(deleteTarget.id)}
|
|
837
|
+
onCancel={() => setDeleteTarget(null)}
|
|
838
|
+
/>
|
|
839
|
+
)}
|
|
840
|
+
</>
|
|
841
|
+
);
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
export default ChatAddressModal;
|