@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.
Files changed (43) hide show
  1. package/dist/index.cjs +1236 -235
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +15 -3
  4. package/dist/index.d.ts +15 -3
  5. package/dist/index.js +1160 -160
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/chat/InboxPopup.tsx +105 -42
  9. package/src/chat/SinglePopup.tsx +59 -14
  10. package/src/icons/index.tsx +55 -0
  11. package/src/index.ts +14 -12
  12. package/src/modals/ChatAddressModal.tsx +844 -0
  13. package/src/modals/{chat/ChatConfirmModal.tsx → ChatConfirmModal.tsx} +2 -2
  14. package/src/modals/ChatTranslateSettingsModal.tsx +182 -0
  15. package/src/styles/index.build.css +15 -0
  16. package/src/styles/index.css +10 -2
  17. package/src/ui/{chat/AttachmentPreviewStrip.tsx → AttachmentPreviewStrip.tsx} +2 -2
  18. package/src/ui/{chat/ChatComposerBar.tsx → ChatComposerBar.tsx} +2 -2
  19. package/src/ui/{chat/ChatFooter.tsx → ChatFooter.tsx} +102 -8
  20. package/src/ui/{chat/ChatHeader.tsx → ChatHeader.tsx} +7 -4
  21. package/src/ui/{chat/ChatIdentity.tsx → ChatIdentity.tsx} +2 -2
  22. package/src/ui/{chat/ChatInquiryBar.tsx → ChatInquiryBar.tsx} +1 -1
  23. package/src/ui/ChatKebabMenu.tsx +125 -0
  24. package/src/ui/{chat/ChatListHeader.tsx → ChatListHeader.tsx} +49 -25
  25. package/src/ui/{chat/ChatMessageItem.tsx → ChatMessageItem.tsx} +1 -1
  26. package/src/ui/ChatScroll.tsx +59 -0
  27. package/src/ui/{chat/ChatSpinner.tsx → ChatSpinner.tsx} +1 -1
  28. package/src/ui/{chat/ChatThreadItem.tsx → ChatThreadItem.tsx} +9 -16
  29. package/src/ui/{chat/MessageHoverActions.tsx → MessageHoverActions.tsx} +2 -2
  30. package/src/ui/{chat/ReplyCard.tsx → ReplyCard.tsx} +2 -2
  31. package/src/ui/{chat/TypingIndicator.tsx → TypingIndicator.tsx} +1 -1
  32. package/src/ui/{chat/drop-up → drop-up}/BusinessCardDropup.tsx +15 -3
  33. package/src/ui/{chat/drop-up → drop-up}/EmojiDropup.tsx +1 -1
  34. package/src/ui/{chat/message-items → message-items}/ChatAddressCard.tsx +4 -4
  35. package/src/ui/{chat/message-items → message-items}/ChatBubbleFiles.tsx +1 -1
  36. package/src/ui/{chat/message-items → message-items}/ChatBubbleImages.tsx +2 -2
  37. package/src/ui/{chat/message-items → message-items}/ChatBusinessCard.tsx +1 -1
  38. package/src/ui/{chat/scrollToMessage.ts → scrollToMessage.ts} +1 -1
  39. package/src/ui/{chat/types.ts → types.ts} +2 -2
  40. package/src/modals/chat/ChatTranslateSettingsModal.tsx +0 -180
  41. package/src/ui/chat/ChatScroll.tsx +0 -52
  42. /package/src/ui/{chat/message-items → message-items}/ChatBubbleAudio.tsx +0 -0
  43. /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;