hey-pharmacist-ecommerce 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -1
- package/dist/index.d.mts +3636 -316
- package/dist/index.d.ts +3636 -316
- package/dist/index.js +6802 -3865
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6756 -3817
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -14
- package/src/components/AddressFormModal.tsx +171 -0
- package/src/components/CartItem.tsx +17 -12
- package/src/components/FilterChips.tsx +195 -0
- package/src/components/Header.tsx +121 -71
- package/src/components/OrderCard.tsx +18 -25
- package/src/components/ProductCard.tsx +209 -72
- package/src/components/ui/Button.tsx +13 -5
- package/src/components/ui/Card.tsx +46 -0
- package/src/hooks/useAddresses.ts +83 -0
- package/src/hooks/useOrders.ts +37 -19
- package/src/hooks/useProducts.ts +55 -63
- package/src/hooks/useWishlistProducts.ts +75 -0
- package/src/index.ts +3 -19
- package/src/lib/Apis/api.ts +1 -0
- package/src/lib/Apis/apis/cart-api.ts +3 -3
- package/src/lib/Apis/apis/inventory-api.ts +0 -108
- package/src/lib/Apis/apis/stores-api.ts +70 -0
- package/src/lib/Apis/apis/wishlist-api.ts +447 -0
- package/src/lib/Apis/models/cart-item-populated.ts +0 -1
- package/src/lib/Apis/models/create-single-variant-product-dto.ts +3 -10
- package/src/lib/Apis/models/create-variant-dto.ts +26 -33
- package/src/lib/Apis/models/extended-product-dto.ts +20 -24
- package/src/lib/Apis/models/index.ts +2 -1
- package/src/lib/Apis/models/order-time-line-dto.ts +49 -0
- package/src/lib/Apis/models/order.ts +3 -8
- package/src/lib/Apis/models/populated-order.ts +3 -8
- package/src/lib/Apis/models/product-variant.ts +29 -0
- package/src/lib/Apis/models/update-product-variant-dto.ts +16 -23
- package/src/lib/Apis/models/wishlist.ts +51 -0
- package/src/lib/Apis/wrapper.ts +18 -7
- package/src/lib/api-adapter/index.ts +0 -12
- package/src/lib/types/index.ts +16 -61
- package/src/lib/utils/colors.ts +7 -4
- package/src/lib/utils/format.ts +1 -1
- package/src/lib/validations/address.ts +14 -0
- package/src/providers/AuthProvider.tsx +61 -31
- package/src/providers/CartProvider.tsx +18 -28
- package/src/providers/EcommerceProvider.tsx +7 -0
- package/src/providers/FavoritesProvider.tsx +86 -0
- package/src/providers/ThemeProvider.tsx +16 -1
- package/src/providers/WishlistProvider.tsx +174 -0
- package/src/screens/AddressesScreen.tsx +484 -0
- package/src/screens/CartScreen.tsx +120 -84
- package/src/screens/CategoriesScreen.tsx +120 -0
- package/src/screens/CheckoutScreen.tsx +919 -241
- package/src/screens/CurrentOrdersScreen.tsx +125 -61
- package/src/screens/HomeScreen.tsx +209 -0
- package/src/screens/LoginScreen.tsx +133 -88
- package/src/screens/NewAddressScreen.tsx +187 -0
- package/src/screens/OrdersScreen.tsx +162 -50
- package/src/screens/ProductDetailScreen.tsx +641 -190
- package/src/screens/ProfileScreen.tsx +192 -116
- package/src/screens/RegisterScreen.tsx +193 -144
- package/src/screens/SearchResultsScreen.tsx +165 -0
- package/src/screens/ShopScreen.tsx +1110 -146
- package/src/screens/WishlistScreen.tsx +428 -0
- package/src/lib/Apis/models/inventory-paginated-response.ts +0 -75
- package/src/lib/api/auth.ts +0 -81
- package/src/lib/api/cart.ts +0 -42
- package/src/lib/api/orders.ts +0 -53
- package/src/lib/api/products.ts +0 -51
- package/src/lib/api-adapter/auth-adapter.ts +0 -196
- package/src/lib/api-adapter/cart-adapter.ts +0 -193
- package/src/lib/api-adapter/mappers.ts +0 -147
- package/src/lib/api-adapter/orders-adapter.ts +0 -195
- package/src/lib/api-adapter/products-adapter.ts +0 -194
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { motion } from 'framer-motion';
|
|
5
|
+
import { useForm } from 'react-hook-form';
|
|
6
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import {
|
|
9
|
+
Crown,
|
|
10
|
+
Edit3,
|
|
11
|
+
Globe,
|
|
12
|
+
Home,
|
|
13
|
+
MapPin,
|
|
14
|
+
Phone,
|
|
15
|
+
Plus,
|
|
16
|
+
Sparkles,
|
|
17
|
+
Star,
|
|
18
|
+
Trash2,
|
|
19
|
+
User,
|
|
20
|
+
} from 'lucide-react';
|
|
21
|
+
import { Button } from '@/components/ui/Button';
|
|
22
|
+
import { Input } from '@/components/ui/Input';
|
|
23
|
+
import { Modal } from '@/components/ui/Modal';
|
|
24
|
+
import { EmptyState } from '@/components/EmptyState';
|
|
25
|
+
import { useAddresses } from '@/hooks/useAddresses';
|
|
26
|
+
import { toast } from 'sonner';
|
|
27
|
+
import { Address, AddressAddressTypeEnum, CreateAddressDtoAddressTypeEnum, UpdateAddressDto, UpdateAddressDtoAddressTypeEnum } from '@/lib/Apis';
|
|
28
|
+
|
|
29
|
+
const addressFormSchema = z.object({
|
|
30
|
+
fullName: z.string().min(2, 'Please enter a full name'),
|
|
31
|
+
phone: z.string().min(7, 'Please enter a valid phone number'),
|
|
32
|
+
addressLine1: z.string().min(4, 'Address line 1 is required'),
|
|
33
|
+
addressLine2: z.string().optional(),
|
|
34
|
+
city: z.string().min(2, 'City is required'),
|
|
35
|
+
state: z.string().min(2, 'State / Region is required'),
|
|
36
|
+
zipCode: z.string().min(3, 'Postal code is required'),
|
|
37
|
+
country: z.string().min(2, 'Country is required'),
|
|
38
|
+
addressType: z.enum(['Billing', 'Shipping', 'Both']).default('Shipping'),
|
|
39
|
+
isDefault: z.boolean().optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
type AddressFormValues = z.infer<typeof addressFormSchema>;
|
|
43
|
+
|
|
44
|
+
const FORM_DEFAULTS: AddressFormValues = {
|
|
45
|
+
fullName: '',
|
|
46
|
+
phone: '',
|
|
47
|
+
addressLine1: '',
|
|
48
|
+
addressLine2: '',
|
|
49
|
+
city: '',
|
|
50
|
+
state: '',
|
|
51
|
+
zipCode: '',
|
|
52
|
+
country: 'United States',
|
|
53
|
+
addressType: 'Shipping',
|
|
54
|
+
isDefault: false,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function formatAddressSnippet(address: Address) {
|
|
58
|
+
return [address.street1, address.street2, `${address.city}, ${address.state} ${address.zip}`, address.country]
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join(' • ');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getAddressTypeCopy(type?: AddressAddressTypeEnum) {
|
|
64
|
+
switch (type) {
|
|
65
|
+
case 'Billing':
|
|
66
|
+
return { label: 'Billing', icon: <Home className="h-4 w-4" /> };
|
|
67
|
+
case 'Both':
|
|
68
|
+
return { label: 'Billing & Shipping', icon: <Globe className="h-4 w-4" /> };
|
|
69
|
+
case 'Shipping':
|
|
70
|
+
default:
|
|
71
|
+
return { label: 'Shipping', icon: <MapPin className="h-4 w-4" /> };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function AddressesScreen() {
|
|
76
|
+
const {
|
|
77
|
+
addresses,
|
|
78
|
+
defaultAddress,
|
|
79
|
+
isLoading,
|
|
80
|
+
error,
|
|
81
|
+
addAddress,
|
|
82
|
+
updateAddress,
|
|
83
|
+
removeAddress,
|
|
84
|
+
markAsDefault,
|
|
85
|
+
} = useAddresses();
|
|
86
|
+
|
|
87
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
88
|
+
const [editingAddress, setEditingAddress] = useState<Address | null>(null);
|
|
89
|
+
|
|
90
|
+
const {
|
|
91
|
+
register,
|
|
92
|
+
handleSubmit,
|
|
93
|
+
reset,
|
|
94
|
+
formState: { errors, isSubmitting },
|
|
95
|
+
} = useForm<AddressFormValues>({
|
|
96
|
+
resolver: zodResolver(addressFormSchema),
|
|
97
|
+
defaultValues: FORM_DEFAULTS,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (editingAddress) {
|
|
102
|
+
reset({
|
|
103
|
+
fullName: editingAddress.name,
|
|
104
|
+
phone: editingAddress.phone,
|
|
105
|
+
addressLine1: editingAddress.street1,
|
|
106
|
+
addressLine2: editingAddress.street2,
|
|
107
|
+
city: editingAddress.city,
|
|
108
|
+
state: editingAddress.state,
|
|
109
|
+
zipCode: editingAddress.zip,
|
|
110
|
+
country: editingAddress.country,
|
|
111
|
+
addressType: editingAddress.addressType as AddressAddressTypeEnum,
|
|
112
|
+
isDefault: editingAddress.isDefault ?? false,
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
reset(FORM_DEFAULTS);
|
|
116
|
+
}
|
|
117
|
+
}, [editingAddress, reset]);
|
|
118
|
+
|
|
119
|
+
const openCreateModal = () => {
|
|
120
|
+
setEditingAddress(null);
|
|
121
|
+
setIsModalOpen(true);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const openEditModal = (address: Address) => {
|
|
125
|
+
setEditingAddress(address);
|
|
126
|
+
setIsModalOpen(true);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const closeModal = () => {
|
|
130
|
+
setIsModalOpen(false);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const onSubmit = async (values: AddressFormValues) => {
|
|
134
|
+
const payload: UpdateAddressDto = {
|
|
135
|
+
name: values.fullName,
|
|
136
|
+
phone: values.phone,
|
|
137
|
+
street1: values.addressLine1,
|
|
138
|
+
street2: values.addressLine2 || '',
|
|
139
|
+
city: values.city,
|
|
140
|
+
state: values.state,
|
|
141
|
+
zip: values.zipCode,
|
|
142
|
+
country: values.country,
|
|
143
|
+
isDefault: values.isDefault ?? false,
|
|
144
|
+
addressType: values.addressType as UpdateAddressDtoAddressTypeEnum,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (editingAddress) {
|
|
148
|
+
const response = await updateAddress(editingAddress.id, payload);
|
|
149
|
+
if (response) {
|
|
150
|
+
toast.success('Address updated');
|
|
151
|
+
closeModal();
|
|
152
|
+
} else {
|
|
153
|
+
toast.error('Unable to update address');
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
const response = await addAddress({
|
|
157
|
+
name: values.fullName,
|
|
158
|
+
phone: values.phone,
|
|
159
|
+
street1: values.addressLine1,
|
|
160
|
+
street2: values.addressLine2 || '',
|
|
161
|
+
city: values.city,
|
|
162
|
+
state: values.state,
|
|
163
|
+
zip: values.zipCode,
|
|
164
|
+
country: values.country,
|
|
165
|
+
addressType: values.addressType as CreateAddressDtoAddressTypeEnum,
|
|
166
|
+
});
|
|
167
|
+
if (response) {
|
|
168
|
+
toast.success('Address added successfu lly');
|
|
169
|
+
closeModal();
|
|
170
|
+
} else {
|
|
171
|
+
toast.error('Failed to add address');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const handleDelete = async (address: Address) => {
|
|
177
|
+
const confirmDelete = window.confirm(
|
|
178
|
+
`Remove ${address.name}'s address?\nYou can add it back at any time.`
|
|
179
|
+
);
|
|
180
|
+
if (!confirmDelete) return;
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await removeAddress(address.id);
|
|
184
|
+
toast.success('Address removed successfully');
|
|
185
|
+
} catch (error) {
|
|
186
|
+
toast.error('Failed to remove address', {
|
|
187
|
+
description: error instanceof Error ? error.message : 'An unexpected error occurred',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const handleSetDefault = async (address: Address) => {
|
|
193
|
+
try {
|
|
194
|
+
await markAsDefault(address.id);
|
|
195
|
+
toast.success(`${address.name} is now your default address`);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
toast.error('Failed to set default address', {
|
|
198
|
+
description: error instanceof Error ? error.message : 'An unexpected error occurred',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const stats = useMemo(() => {
|
|
204
|
+
const shippingCount = addresses.filter((address) => address.addressType !== 'Billing').length;
|
|
205
|
+
const billingCount = addresses.filter((address) => address.addressType !== 'Shipping').length;
|
|
206
|
+
return [
|
|
207
|
+
{
|
|
208
|
+
id: 'total',
|
|
209
|
+
label: 'Saved addresses',
|
|
210
|
+
value: addresses.length,
|
|
211
|
+
helper: 'All delivery and billing locations',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'shipping',
|
|
215
|
+
label: 'Shipping ready',
|
|
216
|
+
value: shippingCount,
|
|
217
|
+
helper: 'Optimized for fast dispatch',
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'billing',
|
|
221
|
+
label: 'Billing profiles',
|
|
222
|
+
value: billingCount,
|
|
223
|
+
helper: 'Simplifies checkout',
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
}, [addresses]);
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<div className="min-h-screen bg-slate-50">
|
|
230
|
+
<section className="relative overflow-hidden bg-gradient-to-br from-[rgb(var(--header-from))] via-[rgb(var(--header-via))] to-[rgb(var(--header-to))] text-white mb-8">
|
|
231
|
+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(255,255,255,0.3),_transparent_60%)]" />
|
|
232
|
+
<div className="relative container mx-auto px-4 py-16">
|
|
233
|
+
<div className="flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
|
|
234
|
+
<motion.div
|
|
235
|
+
initial={{ opacity: 0, y: 24 }}
|
|
236
|
+
animate={{ opacity: 1, y: 0 }}
|
|
237
|
+
className="space-y-5"
|
|
238
|
+
>
|
|
239
|
+
<span className="inline-flex items-center gap-2 rounded-full bg-white/15 px-3 py-1 text-sm font-semibold uppercase tracking-[0.35em] text-white/70 backdrop-blur">
|
|
240
|
+
<MapPin className="h-4 w-4" />
|
|
241
|
+
Address book
|
|
242
|
+
</span>
|
|
243
|
+
<h1 className="text-4xl font-bold md:text-5xl">
|
|
244
|
+
Manage where your care arrives
|
|
245
|
+
</h1>
|
|
246
|
+
<p className="max-w-2xl text-white/80 md:text-lg">
|
|
247
|
+
Add home, office, or loved ones' addresses and toggle a default for lightning-fast
|
|
248
|
+
checkout and delivery.
|
|
249
|
+
</p>
|
|
250
|
+
<div className="flex flex-wrap items-center gap-3 text-sm text-white/75">
|
|
251
|
+
<span className="inline-flex items-center gap-2 rounded-full bg-white/10 px-3 py-1">
|
|
252
|
+
<Sparkles className="h-4 w-4 text-white" />
|
|
253
|
+
Default address: {defaultAddress ? defaultAddress.name : 'Not set'}
|
|
254
|
+
</span>
|
|
255
|
+
<Button variant="ghost" className="text-white hover:bg-white/10" onClick={openCreateModal}>
|
|
256
|
+
<Plus className="h-5 w-5" />
|
|
257
|
+
Add address
|
|
258
|
+
</Button>
|
|
259
|
+
</div>
|
|
260
|
+
</motion.div>
|
|
261
|
+
|
|
262
|
+
<motion.div
|
|
263
|
+
initial={{ opacity: 0, y: 24 }}
|
|
264
|
+
animate={{ opacity: 1, y: 0 }}
|
|
265
|
+
transition={{ delay: 0.1 }}
|
|
266
|
+
className="grid gap-4 rounded-3xl bg-white/15 p-6 text-sm text-white/80 backdrop-blur"
|
|
267
|
+
>
|
|
268
|
+
{stats.map((stat) => (
|
|
269
|
+
<div key={stat.id} className="flex items-center justify-between">
|
|
270
|
+
<div>
|
|
271
|
+
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-white/70">
|
|
272
|
+
{stat.label}
|
|
273
|
+
</p>
|
|
274
|
+
<p className="text-white">{stat.helper}</p>
|
|
275
|
+
</div>
|
|
276
|
+
<span className="text-3xl font-semibold text-white">{stat.value}</span>
|
|
277
|
+
</div>
|
|
278
|
+
))}
|
|
279
|
+
</motion.div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</section>
|
|
283
|
+
|
|
284
|
+
<div className="relative -mt-16 pb-20">
|
|
285
|
+
<div className="container mx-auto px-4">
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
{isLoading ? (
|
|
289
|
+
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
290
|
+
{Array.from({ length: 3 }).map((_, index) => (
|
|
291
|
+
<div
|
|
292
|
+
key={index}
|
|
293
|
+
className="h-56 animate-pulse rounded-3xl border border-slate-100 bg-white"
|
|
294
|
+
/>
|
|
295
|
+
))}
|
|
296
|
+
</div>
|
|
297
|
+
) : error ? (
|
|
298
|
+
<div className="rounded-3xl border border-red-100 bg-red-50 p-6 text-sm text-red-700">
|
|
299
|
+
{error.message}
|
|
300
|
+
</div>
|
|
301
|
+
) : addresses.length === 0 ? (
|
|
302
|
+
<div className="rounded-3xl border border-slate-100 bg-white p-12 shadow-sm">
|
|
303
|
+
<EmptyState
|
|
304
|
+
icon={MapPin}
|
|
305
|
+
title="No addresses yet"
|
|
306
|
+
description="Save a shipping or billing address to speed through checkout the next time you order."
|
|
307
|
+
actionLabel="Add your first address"
|
|
308
|
+
onAction={openCreateModal}
|
|
309
|
+
/>
|
|
310
|
+
</div>
|
|
311
|
+
) : (
|
|
312
|
+
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
313
|
+
{addresses.map((address) => {
|
|
314
|
+
const typeCopy = getAddressTypeCopy(address.addressType);
|
|
315
|
+
return (
|
|
316
|
+
<motion.div
|
|
317
|
+
key={address.id}
|
|
318
|
+
initial={{ opacity: 0, y: 24 }}
|
|
319
|
+
animate={{ opacity: 1, y: 0 }}
|
|
320
|
+
className="group relative flex h-full flex-col rounded-3xl border border-slate-100 bg-white p-6 shadow-sm transition hover:-translate-y-1 hover:shadow-xl"
|
|
321
|
+
>
|
|
322
|
+
{address.isDefault && (
|
|
323
|
+
<span className="absolute right-6 top-6 inline-flex items-center gap-2 rounded-full bg-amber-100 px-3 py-1 text-xs font-semibold text-amber-700">
|
|
324
|
+
<Crown className="h-4 w-4" />
|
|
325
|
+
Default
|
|
326
|
+
</span>
|
|
327
|
+
)}
|
|
328
|
+
<div className="flex items-center gap-3">
|
|
329
|
+
<span className="rounded-full bg-primary-50 p-3 text-primary-600">
|
|
330
|
+
<User className="h-5 w-5" />
|
|
331
|
+
</span>
|
|
332
|
+
<div>
|
|
333
|
+
<p className="text-base font-semibold text-slate-900">
|
|
334
|
+
{address.name}
|
|
335
|
+
</p>
|
|
336
|
+
<p className="text-xs uppercase tracking-[0.3em] text-slate-400">
|
|
337
|
+
{typeCopy.label}
|
|
338
|
+
</p>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<div className="mt-6 flex flex-1 flex-col gap-3 text-sm text-slate-600">
|
|
343
|
+
<p>{formatAddressSnippet(address)}</p>
|
|
344
|
+
<p className="inline-flex items-center gap-2 text-slate-500">
|
|
345
|
+
<Phone className="h-4 w-4 text-slate-400" />
|
|
346
|
+
{address.phone || 'Not provided'}
|
|
347
|
+
</p>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<div className="mt-6 flex items-center justify-between gap-3">
|
|
351
|
+
<button
|
|
352
|
+
type="button"
|
|
353
|
+
onClick={() => openEditModal(address)}
|
|
354
|
+
className="inline-flex items-center gap-2 rounded-full border border-slate-200 px-3 py-1 text-sm font-medium text-slate-600 transition hover:border-primary-300 hover:text-primary-600"
|
|
355
|
+
>
|
|
356
|
+
<Edit3 className="h-4 w-4" />
|
|
357
|
+
Edit
|
|
358
|
+
</button>
|
|
359
|
+
<div className="flex items-center gap-2">
|
|
360
|
+
{!address.isDefault && (
|
|
361
|
+
<button
|
|
362
|
+
type="button"
|
|
363
|
+
onClick={() => handleSetDefault(address)}
|
|
364
|
+
className="inline-flex items-center gap-2 rounded-full border border-amber-200 px-3 py-1 text-sm font-semibold text-amber-600 transition hover:border-amber-300 hover:text-amber-700"
|
|
365
|
+
>
|
|
366
|
+
<Star className="h-4 w-4" />
|
|
367
|
+
Make default
|
|
368
|
+
</button>
|
|
369
|
+
)}
|
|
370
|
+
<button
|
|
371
|
+
type="button"
|
|
372
|
+
onClick={() => handleDelete(address)}
|
|
373
|
+
className="inline-flex items-center gap-2 rounded-full border border-red-200 px-3 py-1 text-sm font-semibold text-red-600 transition hover:border-red-300 hover:text-red-700"
|
|
374
|
+
>
|
|
375
|
+
<Trash2 className="h-4 w-4" />
|
|
376
|
+
Delete
|
|
377
|
+
</button>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
</motion.div>
|
|
381
|
+
);
|
|
382
|
+
})}
|
|
383
|
+
</div>
|
|
384
|
+
)}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
<Modal
|
|
389
|
+
isOpen={isModalOpen}
|
|
390
|
+
onClose={closeModal}
|
|
391
|
+
title={editingAddress ? 'Edit address' : 'Add new address'}
|
|
392
|
+
size="lg"
|
|
393
|
+
>
|
|
394
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
|
395
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
396
|
+
<Input
|
|
397
|
+
label="Full name"
|
|
398
|
+
placeholder="Taylor Pharmacist"
|
|
399
|
+
{...register('fullName')}
|
|
400
|
+
error={errors.fullName?.message}
|
|
401
|
+
/>
|
|
402
|
+
<Input
|
|
403
|
+
label="Phone number"
|
|
404
|
+
placeholder="+1 (555) 123-4567"
|
|
405
|
+
{...register('phone')}
|
|
406
|
+
error={errors.phone?.message}
|
|
407
|
+
/>
|
|
408
|
+
</div>
|
|
409
|
+
<Input
|
|
410
|
+
label="Address line 1"
|
|
411
|
+
placeholder="123 Wellness Avenue"
|
|
412
|
+
{...register('addressLine1')}
|
|
413
|
+
error={errors.addressLine1?.message}
|
|
414
|
+
/>
|
|
415
|
+
<Input
|
|
416
|
+
label="Address line 2 (optional)"
|
|
417
|
+
placeholder="Suite, apartment, etc."
|
|
418
|
+
{...register('addressLine2')}
|
|
419
|
+
error={errors.addressLine2?.message}
|
|
420
|
+
/>
|
|
421
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
422
|
+
<Input
|
|
423
|
+
label="City"
|
|
424
|
+
placeholder="Seattle"
|
|
425
|
+
{...register('city')}
|
|
426
|
+
error={errors.city?.message}
|
|
427
|
+
/>
|
|
428
|
+
<Input
|
|
429
|
+
label="State / Region"
|
|
430
|
+
placeholder="Washington"
|
|
431
|
+
{...register('state')}
|
|
432
|
+
error={errors.state?.message}
|
|
433
|
+
/>
|
|
434
|
+
</div>
|
|
435
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
436
|
+
<Input
|
|
437
|
+
label="Postal code"
|
|
438
|
+
placeholder="98101"
|
|
439
|
+
{...register('zipCode')}
|
|
440
|
+
error={errors.zipCode?.message}
|
|
441
|
+
/>
|
|
442
|
+
<Input
|
|
443
|
+
label="Country"
|
|
444
|
+
placeholder="United States"
|
|
445
|
+
{...register('country')}
|
|
446
|
+
error={errors.country?.message}
|
|
447
|
+
/>
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
451
|
+
<label className="flex flex-col gap-2 rounded-2xl border border-slate-200 p-4">
|
|
452
|
+
<span className="text-sm font-semibold text-slate-700">Address type</span>
|
|
453
|
+
<select
|
|
454
|
+
{...register('addressType')}
|
|
455
|
+
className="rounded-xl border border-slate-200 px-3 py-2 text-sm text-slate-700 focus:border-primary-400 focus:outline-none focus:ring-2 focus:ring-primary-500/20"
|
|
456
|
+
>
|
|
457
|
+
<option value="Shipping">Shipping</option>
|
|
458
|
+
<option value="Billing">Billing</option>
|
|
459
|
+
<option value="Both">Billing & Shipping</option>
|
|
460
|
+
</select>
|
|
461
|
+
</label>
|
|
462
|
+
<label className="flex items-center justify-between gap-4 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm font-medium text-slate-700">
|
|
463
|
+
<span>Set as default address</span>
|
|
464
|
+
<input
|
|
465
|
+
type="checkbox"
|
|
466
|
+
{...register('isDefault')}
|
|
467
|
+
className="h-4 w-4 rounded border-primary-300 text-primary-600 focus:ring-primary-500"
|
|
468
|
+
/>
|
|
469
|
+
</label>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<div className="flex items-center justify-end gap-3">
|
|
473
|
+
<Button type="button" variant="outline" onClick={closeModal}>
|
|
474
|
+
Cancel
|
|
475
|
+
</Button>
|
|
476
|
+
<Button type="submit" isLoading={isSubmitting}>
|
|
477
|
+
{editingAddress ? 'Save changes' : 'Save address'}
|
|
478
|
+
</Button>
|
|
479
|
+
</div>
|
|
480
|
+
</form>
|
|
481
|
+
</Modal>
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
}
|