hey-pharmacist-ecommerce 1.1.12 → 1.1.14
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.d.mts +2 -4
- package/dist/index.d.ts +2 -4
- package/dist/index.js +1123 -972
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1123 -971
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/components/AccountAddressesTab.tsx +209 -0
- package/src/components/AccountOrdersTab.tsx +151 -0
- package/src/components/AccountOverviewTab.tsx +209 -0
- package/src/components/AccountPaymentTab.tsx +116 -0
- package/src/components/AccountSavedItemsTab.tsx +76 -0
- package/src/components/AccountSettingsTab.tsx +116 -0
- package/src/components/AddressFormModal.tsx +23 -10
- package/src/components/CartItem.tsx +60 -56
- package/src/components/FilterChips.tsx +54 -80
- package/src/components/Header.tsx +69 -16
- package/src/components/Notification.tsx +148 -0
- package/src/components/OrderCard.tsx +89 -56
- package/src/components/ProductCard.tsx +215 -178
- package/src/components/QuickViewModal.tsx +314 -0
- package/src/components/TabNavigation.tsx +48 -0
- package/src/components/ui/Button.tsx +1 -1
- package/src/components/ui/ConfirmModal.tsx +84 -0
- package/src/hooks/useOrders.ts +1 -0
- package/src/hooks/usePaymentMethods.ts +58 -0
- package/src/index.ts +0 -1
- package/src/providers/CartProvider.tsx +22 -6
- package/src/providers/EcommerceProvider.tsx +8 -7
- package/src/providers/FavoritesProvider.tsx +10 -3
- package/src/providers/NotificationProvider.tsx +79 -0
- package/src/providers/WishlistProvider.tsx +34 -9
- package/src/screens/AddressesScreen.tsx +72 -61
- package/src/screens/CartScreen.tsx +48 -32
- package/src/screens/ChangePasswordScreen.tsx +155 -0
- package/src/screens/CheckoutScreen.tsx +162 -125
- package/src/screens/EditProfileScreen.tsx +165 -0
- package/src/screens/LoginScreen.tsx +59 -72
- package/src/screens/NewAddressScreen.tsx +16 -10
- package/src/screens/OrdersScreen.tsx +91 -148
- package/src/screens/ProductDetailScreen.tsx +334 -234
- package/src/screens/ProfileScreen.tsx +190 -200
- package/src/screens/RegisterScreen.tsx +51 -70
- package/src/screens/SearchResultsScreen.tsx +2 -1
- package/src/screens/ShopScreen.tsx +260 -384
- package/src/screens/WishlistScreen.tsx +226 -224
- package/src/styles/globals.css +9 -0
- package/src/screens/CategoriesScreen.tsx +0 -122
- package/src/screens/HomeScreen.tsx +0 -211
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { X, ShoppingCart, Check, Star, Package, ExternalLink, Minus, Plus } from 'lucide-react';
|
|
3
|
+
import { ExtendedProductDTO, Product } from '@/lib/Apis';
|
|
4
|
+
import { useCart } from '@/providers/CartProvider';
|
|
5
|
+
import Image from 'next/image';
|
|
6
|
+
import { ProductVariantInventoryStatusEnum } from '@/lib/Apis';
|
|
7
|
+
import { useNotification } from '@/providers/NotificationProvider';
|
|
8
|
+
|
|
9
|
+
interface QuickViewModalProps {
|
|
10
|
+
product: ExtendedProductDTO;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
onNavigateToProduct?: (productId: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function QuickViewModal({ product, onClose, onNavigateToProduct }: QuickViewModalProps) {
|
|
16
|
+
const [selectedVariantIndex, setSelectedVariantIndex] = useState(0);
|
|
17
|
+
const [selectedSizeIndex, setSelectedSizeIndex] = useState(0);
|
|
18
|
+
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
|
19
|
+
const [addedToCart, setAddedToCart] = useState(false);
|
|
20
|
+
const [quantity, setQuantity] = useState(1);
|
|
21
|
+
const { addToCart } = useCart();
|
|
22
|
+
const [isAddingToCart, setIsAddingToCart] = useState(false);
|
|
23
|
+
const notification = useNotification();
|
|
24
|
+
|
|
25
|
+
const handleQuantityChange = (newQuantity: number) => {
|
|
26
|
+
if (newQuantity >= 1 && newQuantity <= (selectedVariant.inventoryCount || 10)) {
|
|
27
|
+
setQuantity(newQuantity);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const selectedVariant = product.productVariants[selectedVariantIndex];
|
|
31
|
+
const selectedSize = selectedVariant.productMedia[selectedSizeIndex];
|
|
32
|
+
const displayPrice = product.finalPrice;
|
|
33
|
+
|
|
34
|
+
const handleAddToCart = async () => {
|
|
35
|
+
if (!product || !selectedVariant) return;
|
|
36
|
+
|
|
37
|
+
setIsAddingToCart(true);
|
|
38
|
+
try {
|
|
39
|
+
console.log(selectedVariant)
|
|
40
|
+
await addToCart(
|
|
41
|
+
product.id,
|
|
42
|
+
quantity,
|
|
43
|
+
selectedVariant._id
|
|
44
|
+
);
|
|
45
|
+
notification.success(
|
|
46
|
+
'Added to cart',
|
|
47
|
+
`${quantity} × ${product.name}${selectedVariant.name ? ` (${selectedVariant.name})` : ''} added to your cart.`
|
|
48
|
+
);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Failed to add to cart', error);
|
|
51
|
+
notification.error(
|
|
52
|
+
'Could not add to cart',
|
|
53
|
+
'Something went wrong while adding this item. Please try again.'
|
|
54
|
+
);
|
|
55
|
+
} finally {
|
|
56
|
+
setIsAddingToCart(false);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
|
|
63
|
+
onClick={onClose}
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
className="bg-white rounded-[32px] max-w-5xl w-full max-h-[90vh] overflow-y-auto"
|
|
67
|
+
onClick={(e) => e.stopPropagation()}
|
|
68
|
+
>
|
|
69
|
+
<div className="p-8">
|
|
70
|
+
{/* Header */}
|
|
71
|
+
<div className="flex items-start justify-between mb-6">
|
|
72
|
+
<div>
|
|
73
|
+
<p className="font-['Poppins',sans-serif] text-[11px] text-primary uppercase tracking-wide font-medium mb-2">
|
|
74
|
+
{product.brand} • {product.parentCategories[0].name}
|
|
75
|
+
</p>
|
|
76
|
+
<h2 className="font-['Poppins',sans-serif] font-semibold text-secondary tracking-[-1px]">
|
|
77
|
+
{product.name}
|
|
78
|
+
</h2>
|
|
79
|
+
|
|
80
|
+
{/* Rating */}
|
|
81
|
+
<div className="flex items-center gap-2 mt-2">
|
|
82
|
+
<div className="flex items-center gap-0.5">
|
|
83
|
+
{[...Array(5)].map((_, i) => (
|
|
84
|
+
<Star
|
|
85
|
+
key={i}
|
|
86
|
+
className={`size-4 ${
|
|
87
|
+
i < Math.floor(product.rating? product.rating : 0)
|
|
88
|
+
? 'text-accent fill-accent'
|
|
89
|
+
: 'text-gray-300'
|
|
90
|
+
}`}
|
|
91
|
+
/>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
<span className="font-['Poppins',sans-serif] text-[13px] text-muted">
|
|
95
|
+
{product.rating} ({product.reviews? product.reviews.length : 0} reviews)
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<button
|
|
100
|
+
onClick={onClose}
|
|
101
|
+
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
|
102
|
+
>
|
|
103
|
+
<X className="size-6 text-muted" />
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
108
|
+
{/* Product Image */}
|
|
109
|
+
<div className="space-y-4">
|
|
110
|
+
<div className="relative aspect-[3/4] rounded-[24px] overflow-hidden bg-gray-50">
|
|
111
|
+
<img
|
|
112
|
+
src={selectedVariant.productMedia[selectedImageIndex]?.file || selectedVariant.productMedia[0]?.file}
|
|
113
|
+
alt={product.name}
|
|
114
|
+
className="w-full h-full object-cover"
|
|
115
|
+
/>
|
|
116
|
+
{/* Badges */}
|
|
117
|
+
<div className="absolute top-4 left-4 flex flex-col gap-2">
|
|
118
|
+
{product.finalPrice && (
|
|
119
|
+
<div className="bg-accent text-white rounded-full px-3 py-1.5">
|
|
120
|
+
<span className="font-['Poppins',sans-serif] font-bold text-[11px] uppercase">
|
|
121
|
+
-{product.discountAmount}%
|
|
122
|
+
</span>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
{/* {product.bestseller && (
|
|
126
|
+
<div className="bg-secondary text-white rounded-full px-3 py-1.5">
|
|
127
|
+
<span className="font-['Poppins',sans-serif] font-semibold text-[10px] uppercase">
|
|
128
|
+
Bestseller
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
)} */}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Thumbnail Images */}
|
|
136
|
+
{selectedVariant.productMedia.length > 1 && (
|
|
137
|
+
<div className="grid grid-cols-4 gap-3">
|
|
138
|
+
{selectedVariant.productMedia.map((image: any, index: any) => (
|
|
139
|
+
<div
|
|
140
|
+
key={index}
|
|
141
|
+
className={`aspect-square rounded-xl overflow-hidden cursor-pointer transition-opacity ${selectedImageIndex === index ? 'ring-2 ring-primary' : 'bg-gray-50 hover:opacity-75'}`}
|
|
142
|
+
onClick={() => setSelectedImageIndex(index)}
|
|
143
|
+
>
|
|
144
|
+
<img
|
|
145
|
+
src={image.file}
|
|
146
|
+
alt={`${product.name} ${index + 1}`}
|
|
147
|
+
className="w-full h-full object-cover"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Product Details */}
|
|
156
|
+
<div className="flex flex-col">
|
|
157
|
+
{/* Price */}
|
|
158
|
+
<div className="flex items-center gap-3 mb-4">
|
|
159
|
+
<span className="font-['Poppins',sans-serif] font-bold text-[32px] text-accent">
|
|
160
|
+
${displayPrice.toFixed(2)}
|
|
161
|
+
</span>
|
|
162
|
+
{product.isDiscounted && (
|
|
163
|
+
<span className="font-['Poppins',sans-serif] text-[20px] text-muted line-through">
|
|
164
|
+
${product.finalPrice.toFixed(2)}
|
|
165
|
+
</span>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{/* Stock Status */}
|
|
170
|
+
<div className="mb-6">
|
|
171
|
+
{selectedVariant.inventoryCount === 0 ? (
|
|
172
|
+
<span className="font-['Poppins',sans-serif] text-[12px] text-red-500 font-medium">
|
|
173
|
+
Out of Stock
|
|
174
|
+
</span>
|
|
175
|
+
) : selectedVariant.inventoryCount <= 10 ? (
|
|
176
|
+
<span className="font-['Poppins',sans-serif] text-[12px] text-orange-500 font-medium flex items-center gap-1">
|
|
177
|
+
<Package className="size-3" />
|
|
178
|
+
Only {selectedVariant.inventoryCount} left in stock
|
|
179
|
+
</span>
|
|
180
|
+
) : (
|
|
181
|
+
<span className="font-['Poppins',sans-serif] text-[12px] text-green-600 font-medium flex items-center gap-1">
|
|
182
|
+
<Package className="size-3" />
|
|
183
|
+
In Stock
|
|
184
|
+
</span>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{/* Description */}
|
|
189
|
+
<p className="font-['Poppins',sans-serif] text-[14px] text-muted leading-[1.7] mb-6">
|
|
190
|
+
<div dangerouslySetInnerHTML={{ __html: product.description }} />
|
|
191
|
+
</p>
|
|
192
|
+
|
|
193
|
+
{/* Color Selection */}
|
|
194
|
+
<div className="mb-6">
|
|
195
|
+
<h3 className="font-['Poppins',sans-serif] font-semibold text-[13px] text-secondary mb-3">
|
|
196
|
+
Selected Variant: <span className="font-normal text-muted">{product.productVariants[selectedVariantIndex].name}</span>
|
|
197
|
+
</h3>
|
|
198
|
+
<div className="flex flex-wrap gap-3">
|
|
199
|
+
{product.productVariants.map((variant: any, index: any) => (
|
|
200
|
+
<button
|
|
201
|
+
key={variant.id}
|
|
202
|
+
onClick={() => {
|
|
203
|
+
setSelectedVariantIndex(index);
|
|
204
|
+
setSelectedSizeIndex(0);
|
|
205
|
+
setSelectedImageIndex(0); // Reset selected image index when variant changes
|
|
206
|
+
}}
|
|
207
|
+
className={`size-10 rounded-full border-2 transition-all ${
|
|
208
|
+
selectedVariantIndex === index
|
|
209
|
+
? 'border-primary scale-110'
|
|
210
|
+
: 'border-gray-200 hover:border-primary/50'
|
|
211
|
+
}`}
|
|
212
|
+
style={{ backgroundColor: variant.colorHex }}
|
|
213
|
+
title={variant.color}
|
|
214
|
+
>
|
|
215
|
+
<Image
|
|
216
|
+
src={variant.productMedia?.[0]?.file || ''}
|
|
217
|
+
alt={variant.color || `Variant ${index + 1}`}
|
|
218
|
+
className="object-cover"
|
|
219
|
+
height={32}
|
|
220
|
+
width={32}
|
|
221
|
+
/>
|
|
222
|
+
</button>
|
|
223
|
+
))}
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
{/* Quick Features */}
|
|
227
|
+
<div className="mb-6 p-4 bg-gray-50 rounded-xl">
|
|
228
|
+
<ul className="space-y-2">
|
|
229
|
+
{product.tags.slice(0, 3).map((feature: any, index: any) => (
|
|
230
|
+
<li key={index} className="flex items-start gap-2">
|
|
231
|
+
<Check className="size-4 text-primary shrink-0 mt-0.5" />
|
|
232
|
+
<span className="font-['Poppins',sans-serif] text-[12px] text-muted">
|
|
233
|
+
{feature}
|
|
234
|
+
</span>
|
|
235
|
+
</li>
|
|
236
|
+
))}
|
|
237
|
+
</ul>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
{/* Quantity Selector */}
|
|
241
|
+
<div className="mb-6">
|
|
242
|
+
<h3 className="font-['Poppins',sans-serif] font-semibold text-[13px] text-secondary mb-3">
|
|
243
|
+
Quantity
|
|
244
|
+
</h3>
|
|
245
|
+
<div className="flex items-center gap-4">
|
|
246
|
+
<button
|
|
247
|
+
onClick={() => handleQuantityChange(quantity - 1)}
|
|
248
|
+
disabled={quantity <= 1}
|
|
249
|
+
className="p-2 rounded-full border border-gray-200 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
250
|
+
>
|
|
251
|
+
<Minus className="size-4 text-secondary" />
|
|
252
|
+
</button>
|
|
253
|
+
<span className="w-8 text-center font-medium">{quantity}</span>
|
|
254
|
+
<button
|
|
255
|
+
onClick={() => handleQuantityChange(quantity + 1)}
|
|
256
|
+
disabled={quantity >= (selectedVariant.inventoryCount || 10)}
|
|
257
|
+
className="p-2 rounded-full border border-gray-200 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
258
|
+
>
|
|
259
|
+
<Plus className="size-4 text-secondary" />
|
|
260
|
+
</button>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
{/* Action Buttons */}
|
|
265
|
+
<div className="flex flex-col gap-3 mt-auto">
|
|
266
|
+
<button
|
|
267
|
+
onClick={handleAddToCart}
|
|
268
|
+
disabled={addedToCart || selectedVariant.inventoryCount === 0}
|
|
269
|
+
className={`w-full font-['Poppins',sans-serif] font-medium text-[14px] px-6 py-4 rounded-full transition-all duration-300 flex items-center justify-center gap-3 ${
|
|
270
|
+
addedToCart
|
|
271
|
+
? 'bg-green-500 text-white'
|
|
272
|
+
: 'bg-accent text-white hover:bg-[#d66f45] hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed'
|
|
273
|
+
}`}
|
|
274
|
+
>
|
|
275
|
+
{isAddingToCart ? (
|
|
276
|
+
<>
|
|
277
|
+
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
|
278
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
279
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
280
|
+
</svg>
|
|
281
|
+
Loading...
|
|
282
|
+
</>
|
|
283
|
+
) : (
|
|
284
|
+
<>
|
|
285
|
+
<ShoppingCart className="h-4 w-4" />
|
|
286
|
+
{!selectedVariant
|
|
287
|
+
? 'Select a variant'
|
|
288
|
+
: selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK
|
|
289
|
+
? 'Out of Stock'
|
|
290
|
+
: 'Add to Cart'}
|
|
291
|
+
|
|
292
|
+
</>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
</button>
|
|
296
|
+
|
|
297
|
+
<button
|
|
298
|
+
onClick={() => {
|
|
299
|
+
onClose();
|
|
300
|
+
onNavigateToProduct?.(product.id);
|
|
301
|
+
}}
|
|
302
|
+
className="w-full font-['Poppins',sans-serif] font-medium text-[13px] px-6 py-3 rounded-full bg-white text-secondary border-2 border-primary hover:bg-gray-50 transition-all flex items-center justify-center gap-2"
|
|
303
|
+
>
|
|
304
|
+
View Full Details
|
|
305
|
+
<ExternalLink className="size-4" />
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { LucideIcon } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface Tab {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
icon: LucideIcon;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface TabNavigationProps {
|
|
13
|
+
tabs: Tab[];
|
|
14
|
+
activeTab: string;
|
|
15
|
+
onTabChange: (tabId: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function TabNavigation({ tabs, activeTab, onTabChange }: TabNavigationProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="mx-auto flex items-center max-w-7xl px-4 py-4">
|
|
21
|
+
<nav className="flex overflow-x-auto scrollbar-hide justify-center gap-4" aria-label="Account tabs">
|
|
22
|
+
{tabs.map((tab) => {
|
|
23
|
+
const isActive = activeTab === tab.id;
|
|
24
|
+
const Icon = tab.icon;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<button
|
|
28
|
+
key={tab.id}
|
|
29
|
+
onClick={() => onTabChange(tab.id)}
|
|
30
|
+
className={`
|
|
31
|
+
flex items-center gap-2 px-6 py-3 text-sm font-medium whitespace-nowrap
|
|
32
|
+
border-b-2 transition-colors
|
|
33
|
+
${isActive
|
|
34
|
+
? 'bg-secondary text-white rounded-xl hover:transition-all hover:duration-300 hover:ease-in-out hover:-translate-y-1'
|
|
35
|
+
: 'bg-white text-muted rounded-xl hover:text-secondary hover:transition-all hover:duration-150 hover:ease-in-out hover:-translate-y-1'
|
|
36
|
+
}
|
|
37
|
+
`}
|
|
38
|
+
aria-current={isActive ? 'page' : undefined}
|
|
39
|
+
>
|
|
40
|
+
<Icon className="h-5 w-5" />
|
|
41
|
+
{tab.label}
|
|
42
|
+
</button>
|
|
43
|
+
);
|
|
44
|
+
})}
|
|
45
|
+
</nav>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -23,7 +23,7 @@ export function Button({
|
|
|
23
23
|
children,
|
|
24
24
|
...props
|
|
25
25
|
}: ButtonProps) {
|
|
26
|
-
const baseStyles = 'font-medium rounded-
|
|
26
|
+
const baseStyles = 'font-medium rounded-full transition-all duration-200 inline-flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-primary-500';
|
|
27
27
|
|
|
28
28
|
const variants = {
|
|
29
29
|
primary: 'bg-primary-600 text-white hover:bg-primary-700 shadow-lg shadow-primary-500/30 hover:shadow-xl hover:shadow-primary-500/40',
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Modal } from './Modal';
|
|
5
|
+
import { Button } from './Button';
|
|
6
|
+
import { AlertTriangle } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
interface ConfirmModalProps {
|
|
9
|
+
isOpen: boolean;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onConfirm: () => void;
|
|
12
|
+
title: string;
|
|
13
|
+
message: string;
|
|
14
|
+
confirmText?: string;
|
|
15
|
+
cancelText?: string;
|
|
16
|
+
variant?: 'danger' | 'warning' | 'info';
|
|
17
|
+
isLoading?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ConfirmModal({
|
|
21
|
+
isOpen,
|
|
22
|
+
onClose,
|
|
23
|
+
onConfirm,
|
|
24
|
+
title,
|
|
25
|
+
message,
|
|
26
|
+
confirmText = 'Confirm',
|
|
27
|
+
cancelText = 'Cancel',
|
|
28
|
+
variant = 'danger',
|
|
29
|
+
isLoading = false,
|
|
30
|
+
}: ConfirmModalProps) {
|
|
31
|
+
const handleConfirm = () => {
|
|
32
|
+
onConfirm();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const variantStyles = {
|
|
36
|
+
danger: {
|
|
37
|
+
icon: 'text-red-600 bg-red-100',
|
|
38
|
+
button: 'secondary' as const,
|
|
39
|
+
},
|
|
40
|
+
warning: {
|
|
41
|
+
icon: 'text-amber-600 bg-amber-100',
|
|
42
|
+
button: 'primary' as const,
|
|
43
|
+
},
|
|
44
|
+
info: {
|
|
45
|
+
icon: 'text-secondary bg-blue-100',
|
|
46
|
+
button: 'primary' as const,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const style = variantStyles[variant];
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Modal isOpen={isOpen} onClose={onClose} size="sm">
|
|
54
|
+
<div className="flex flex-col items-center text-center">
|
|
55
|
+
{/* Icon */}
|
|
56
|
+
<div className={`w-12 h-12 rounded-full ${style.icon} flex items-center justify-center mb-4`}>
|
|
57
|
+
<AlertTriangle className="w-6 h-6" />
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{/* Title */}
|
|
61
|
+
<h3 className="text-xl font-semibold text-secondary mb-2">{title}</h3>
|
|
62
|
+
|
|
63
|
+
{/* Message */}
|
|
64
|
+
<p className="text-muted mb-6">{message}</p>
|
|
65
|
+
|
|
66
|
+
{/* Actions */}
|
|
67
|
+
<div className="flex gap-3 w-full justify-center">
|
|
68
|
+
<button
|
|
69
|
+
className='px-6 py-2 border border-slate-200 rounded-full text-slate-700 hover:bg-slate-100 transition-colors'
|
|
70
|
+
onClick={onClose}
|
|
71
|
+
>
|
|
72
|
+
{cancelText}
|
|
73
|
+
</button>
|
|
74
|
+
<button
|
|
75
|
+
className='px-6 py-2 border border-red-600 rounded-full text-red-600 hover:bg-red-100 transition-colors bg-red-50'
|
|
76
|
+
onClick={handleConfirm}
|
|
77
|
+
>
|
|
78
|
+
{confirmText}
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</Modal>
|
|
83
|
+
);
|
|
84
|
+
}
|
package/src/hooks/useOrders.ts
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { PaymentMethodsApi } from '@/lib/Apis';
|
|
3
|
+
import { getApiConfiguration } from '@/lib/api-adapter';
|
|
4
|
+
import { PaymentMethod } from '@/lib/Apis/models';
|
|
5
|
+
|
|
6
|
+
export function usePaymentMethods() {
|
|
7
|
+
const [paymentMethods, setPaymentMethods] = useState<PaymentMethod[]>([]);
|
|
8
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
9
|
+
const [error, setError] = useState<Error | null>(null);
|
|
10
|
+
|
|
11
|
+
const fetchPaymentMethods = useCallback(async () => {
|
|
12
|
+
setIsLoading(true);
|
|
13
|
+
setError(null);
|
|
14
|
+
try {
|
|
15
|
+
const response = await new PaymentMethodsApi(getApiConfiguration()).getPaymentMethods();
|
|
16
|
+
setPaymentMethods(response.data.paymentMethods || []);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
setError(err as Error);
|
|
19
|
+
} finally {
|
|
20
|
+
setIsLoading(false);
|
|
21
|
+
}
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const deletePaymentMethod = useCallback(async (paymentMethodId: string) => {
|
|
25
|
+
try {
|
|
26
|
+
await new PaymentMethodsApi(getApiConfiguration()).detachPaymentMethod({
|
|
27
|
+
paymentMethodId,
|
|
28
|
+
});
|
|
29
|
+
await fetchPaymentMethods();
|
|
30
|
+
} catch (err) {
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}, [fetchPaymentMethods]);
|
|
34
|
+
|
|
35
|
+
const setDefaultPaymentMethod = useCallback(async (paymentMethodId: string) => {
|
|
36
|
+
try {
|
|
37
|
+
await new PaymentMethodsApi(getApiConfiguration()).changeDefaultPayment({
|
|
38
|
+
paymentMethodId,
|
|
39
|
+
});
|
|
40
|
+
await fetchPaymentMethods();
|
|
41
|
+
} catch (err) {
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
}, [fetchPaymentMethods]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
fetchPaymentMethods();
|
|
48
|
+
}, [fetchPaymentMethods]);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
paymentMethods,
|
|
52
|
+
isLoading,
|
|
53
|
+
error,
|
|
54
|
+
refetch: fetchPaymentMethods,
|
|
55
|
+
deletePaymentMethod,
|
|
56
|
+
setDefaultPaymentMethod,
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,6 @@ export { CurrentOrdersScreen } from './screens/CurrentOrdersScreen';
|
|
|
21
21
|
export { AddressesScreen } from './screens/AddressesScreen';
|
|
22
22
|
export { default as WishlistScreen } from './screens/WishlistScreen';
|
|
23
23
|
export { default as SearchResultsScreen } from './screens/SearchResultsScreen';
|
|
24
|
-
export { CategoriesScreen } from './screens/CategoriesScreen';
|
|
25
24
|
export { default as NewAddressScreen } from './screens/NewAddressScreen';
|
|
26
25
|
|
|
27
26
|
// Components
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
|
4
4
|
import { useAuth } from './AuthProvider';
|
|
5
|
-
import { toast } from 'sonner';
|
|
6
5
|
import { CartResponseDto } from '@/lib/Apis/models';
|
|
7
6
|
import { CartApi } from '@/lib/Apis';
|
|
8
7
|
import { getApiConfiguration } from '@/lib/api-adapter';
|
|
8
|
+
import { useNotification } from './NotificationProvider';
|
|
9
9
|
|
|
10
10
|
interface CartContextValue {
|
|
11
11
|
cart: CartResponseDto | null;
|
|
@@ -35,6 +35,7 @@ export function CartProvider({ children }: CartProviderProps) {
|
|
|
35
35
|
const [cart, setCart] = useState<CartResponseDto | null>(null);
|
|
36
36
|
const [isLoading, setIsLoading] = useState(false);
|
|
37
37
|
const { isAuthenticated } = useAuth();
|
|
38
|
+
const notification = useNotification();
|
|
38
39
|
|
|
39
40
|
const refreshCart = useCallback(async () => {
|
|
40
41
|
if (!isAuthenticated) {
|
|
@@ -85,9 +86,15 @@ export function CartProvider({ children }: CartProviderProps) {
|
|
|
85
86
|
|
|
86
87
|
const response = await new CartApi(getApiConfiguration()).handleUserCart({ items });
|
|
87
88
|
setCart(response.data);
|
|
88
|
-
|
|
89
|
+
notification.success(
|
|
90
|
+
'Added to cart',
|
|
91
|
+
'The item was added to your cart.'
|
|
92
|
+
);
|
|
89
93
|
} catch (error: any) {
|
|
90
|
-
|
|
94
|
+
notification.error(
|
|
95
|
+
'Could not add to cart',
|
|
96
|
+
error.response?.data?.message || 'Something went wrong while adding this item to your cart.'
|
|
97
|
+
);
|
|
91
98
|
throw error;
|
|
92
99
|
} finally {
|
|
93
100
|
setIsLoading(false);
|
|
@@ -114,7 +121,10 @@ export function CartProvider({ children }: CartProviderProps) {
|
|
|
114
121
|
const response = await new CartApi(getApiConfiguration()).handleUserCart({ items });
|
|
115
122
|
setCart(response.data);
|
|
116
123
|
} catch (error: any) {
|
|
117
|
-
|
|
124
|
+
notification.error(
|
|
125
|
+
'Could not update cart',
|
|
126
|
+
error.response?.data?.message || 'There was a problem updating the item quantity. Please try again.'
|
|
127
|
+
);
|
|
118
128
|
throw error;
|
|
119
129
|
} finally {
|
|
120
130
|
setIsLoading(false);
|
|
@@ -131,7 +141,10 @@ export function CartProvider({ children }: CartProviderProps) {
|
|
|
131
141
|
const response = await new CartApi(getApiConfiguration()).handleUserCart({ items });
|
|
132
142
|
setCart(response.data);
|
|
133
143
|
} catch (error: any) {
|
|
134
|
-
|
|
144
|
+
notification.error(
|
|
145
|
+
'Could not remove item',
|
|
146
|
+
error.response?.data?.message || 'There was a problem removing this item from your cart.'
|
|
147
|
+
);
|
|
135
148
|
throw error;
|
|
136
149
|
} finally {
|
|
137
150
|
setIsLoading(false);
|
|
@@ -144,7 +157,10 @@ export function CartProvider({ children }: CartProviderProps) {
|
|
|
144
157
|
const response = await new CartApi(getApiConfiguration()).clearCart();
|
|
145
158
|
setCart(null);
|
|
146
159
|
} catch (error: any) {
|
|
147
|
-
|
|
160
|
+
notification.error(
|
|
161
|
+
'Could not clear cart',
|
|
162
|
+
error.response?.data?.message || 'We could not clear your cart. Please try again.'
|
|
163
|
+
);
|
|
148
164
|
throw error;
|
|
149
165
|
} finally {
|
|
150
166
|
setIsLoading(false);
|
|
@@ -8,9 +8,9 @@ import { CartProvider } from './CartProvider';
|
|
|
8
8
|
import { WishlistProvider } from './WishlistProvider';
|
|
9
9
|
import { BasePathProvider } from './BasePathProvider';
|
|
10
10
|
import { initializeApiAdapter } from '@/lib/api-adapter';
|
|
11
|
-
import { Toaster } from 'sonner';
|
|
12
11
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
13
12
|
import { QueryClient } from '@tanstack/react-query';
|
|
13
|
+
import { NotificationProvider } from './NotificationProvider';
|
|
14
14
|
|
|
15
15
|
interface EcommerceProviderProps {
|
|
16
16
|
config: EcommerceConfig;
|
|
@@ -34,12 +34,13 @@ export function EcommerceProvider({ config, children, withToaster = true, basePa
|
|
|
34
34
|
<ThemeProvider config={config}>
|
|
35
35
|
<BasePathProvider basePath={basePath}>
|
|
36
36
|
<AuthProvider>
|
|
37
|
-
<
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
<NotificationProvider>
|
|
38
|
+
<CartProvider>
|
|
39
|
+
<WishlistProvider>
|
|
40
|
+
{children}
|
|
41
|
+
</WishlistProvider>
|
|
42
|
+
</CartProvider>
|
|
43
|
+
</NotificationProvider>
|
|
43
44
|
</AuthProvider>
|
|
44
45
|
</BasePathProvider>
|
|
45
46
|
</ThemeProvider>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { ExtendedProductDTO } from '@/lib/Apis';
|
|
4
4
|
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
|
5
|
-
import {
|
|
5
|
+
import { useNotification } from './NotificationProvider';
|
|
6
6
|
|
|
7
7
|
interface FavoritesContextType {
|
|
8
8
|
favorites: string[];
|
|
@@ -17,6 +17,7 @@ const FavoritesContext = createContext<FavoritesContextType | undefined>(undefin
|
|
|
17
17
|
export function FavoritesProvider({ children }: { children: ReactNode }) {
|
|
18
18
|
const [favorites, setFavorites] = useState<string[]>([]);
|
|
19
19
|
const [isClient, setIsClient] = useState(false);
|
|
20
|
+
const notification = useNotification();
|
|
20
21
|
|
|
21
22
|
const getFavoritesKey = () => {
|
|
22
23
|
if (typeof window === 'undefined') return 'favorites';
|
|
@@ -55,13 +56,19 @@ export function FavoritesProvider({ children }: { children: ReactNode }) {
|
|
|
55
56
|
const addToFavorites = (product: ExtendedProductDTO) => {
|
|
56
57
|
if (!favorites.includes(product.id)) {
|
|
57
58
|
setFavorites(prev => [...prev, product.id]);
|
|
58
|
-
|
|
59
|
+
notification.success(
|
|
60
|
+
'Added to favorites',
|
|
61
|
+
`${product.name} was added to your favorites.`
|
|
62
|
+
);
|
|
59
63
|
}
|
|
60
64
|
};
|
|
61
65
|
|
|
62
66
|
const removeFromFavorites = (productId: string) => {
|
|
63
67
|
setFavorites(prev => prev.filter(id => id !== productId));
|
|
64
|
-
|
|
68
|
+
notification.info(
|
|
69
|
+
'Removed from favorites',
|
|
70
|
+
'The item has been removed from your favorites.'
|
|
71
|
+
);
|
|
65
72
|
};
|
|
66
73
|
|
|
67
74
|
const toggleFavorite = (product: ExtendedProductDTO) => {
|