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.
Files changed (74) hide show
  1. package/README.md +107 -1
  2. package/dist/index.d.mts +3636 -316
  3. package/dist/index.d.ts +3636 -316
  4. package/dist/index.js +6802 -3865
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +6756 -3817
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +17 -14
  9. package/src/components/AddressFormModal.tsx +171 -0
  10. package/src/components/CartItem.tsx +17 -12
  11. package/src/components/FilterChips.tsx +195 -0
  12. package/src/components/Header.tsx +121 -71
  13. package/src/components/OrderCard.tsx +18 -25
  14. package/src/components/ProductCard.tsx +209 -72
  15. package/src/components/ui/Button.tsx +13 -5
  16. package/src/components/ui/Card.tsx +46 -0
  17. package/src/hooks/useAddresses.ts +83 -0
  18. package/src/hooks/useOrders.ts +37 -19
  19. package/src/hooks/useProducts.ts +55 -63
  20. package/src/hooks/useWishlistProducts.ts +75 -0
  21. package/src/index.ts +3 -19
  22. package/src/lib/Apis/api.ts +1 -0
  23. package/src/lib/Apis/apis/cart-api.ts +3 -3
  24. package/src/lib/Apis/apis/inventory-api.ts +0 -108
  25. package/src/lib/Apis/apis/stores-api.ts +70 -0
  26. package/src/lib/Apis/apis/wishlist-api.ts +447 -0
  27. package/src/lib/Apis/models/cart-item-populated.ts +0 -1
  28. package/src/lib/Apis/models/create-single-variant-product-dto.ts +3 -10
  29. package/src/lib/Apis/models/create-variant-dto.ts +26 -33
  30. package/src/lib/Apis/models/extended-product-dto.ts +20 -24
  31. package/src/lib/Apis/models/index.ts +2 -1
  32. package/src/lib/Apis/models/order-time-line-dto.ts +49 -0
  33. package/src/lib/Apis/models/order.ts +3 -8
  34. package/src/lib/Apis/models/populated-order.ts +3 -8
  35. package/src/lib/Apis/models/product-variant.ts +29 -0
  36. package/src/lib/Apis/models/update-product-variant-dto.ts +16 -23
  37. package/src/lib/Apis/models/wishlist.ts +51 -0
  38. package/src/lib/Apis/wrapper.ts +18 -7
  39. package/src/lib/api-adapter/index.ts +0 -12
  40. package/src/lib/types/index.ts +16 -61
  41. package/src/lib/utils/colors.ts +7 -4
  42. package/src/lib/utils/format.ts +1 -1
  43. package/src/lib/validations/address.ts +14 -0
  44. package/src/providers/AuthProvider.tsx +61 -31
  45. package/src/providers/CartProvider.tsx +18 -28
  46. package/src/providers/EcommerceProvider.tsx +7 -0
  47. package/src/providers/FavoritesProvider.tsx +86 -0
  48. package/src/providers/ThemeProvider.tsx +16 -1
  49. package/src/providers/WishlistProvider.tsx +174 -0
  50. package/src/screens/AddressesScreen.tsx +484 -0
  51. package/src/screens/CartScreen.tsx +120 -84
  52. package/src/screens/CategoriesScreen.tsx +120 -0
  53. package/src/screens/CheckoutScreen.tsx +919 -241
  54. package/src/screens/CurrentOrdersScreen.tsx +125 -61
  55. package/src/screens/HomeScreen.tsx +209 -0
  56. package/src/screens/LoginScreen.tsx +133 -88
  57. package/src/screens/NewAddressScreen.tsx +187 -0
  58. package/src/screens/OrdersScreen.tsx +162 -50
  59. package/src/screens/ProductDetailScreen.tsx +641 -190
  60. package/src/screens/ProfileScreen.tsx +192 -116
  61. package/src/screens/RegisterScreen.tsx +193 -144
  62. package/src/screens/SearchResultsScreen.tsx +165 -0
  63. package/src/screens/ShopScreen.tsx +1110 -146
  64. package/src/screens/WishlistScreen.tsx +428 -0
  65. package/src/lib/Apis/models/inventory-paginated-response.ts +0 -75
  66. package/src/lib/api/auth.ts +0 -81
  67. package/src/lib/api/cart.ts +0 -42
  68. package/src/lib/api/orders.ts +0 -53
  69. package/src/lib/api/products.ts +0 -51
  70. package/src/lib/api-adapter/auth-adapter.ts +0 -196
  71. package/src/lib/api-adapter/cart-adapter.ts +0 -193
  72. package/src/lib/api-adapter/mappers.ts +0 -147
  73. package/src/lib/api-adapter/orders-adapter.ts +0 -195
  74. 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&apos; 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
+ }