hey-pharmacist-ecommerce 1.1.11 → 1.1.13
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 +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +632 -511
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +633 -512
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/CartItem.tsx +63 -42
- package/src/components/FilterChips.tsx +54 -80
- package/src/components/OrderCard.tsx +89 -56
- package/src/components/ProductCard.tsx +131 -55
- package/src/hooks/useOrders.ts +1 -0
- package/src/lib/types/index.ts +1 -0
- package/src/providers/CartProvider.tsx +47 -3
- package/src/screens/CartScreen.tsx +146 -231
- package/src/screens/CheckoutScreen.tsx +30 -61
- package/src/screens/LoginScreen.tsx +1 -1
- package/src/screens/OrdersScreen.tsx +91 -148
- package/src/screens/ProductDetailScreen.tsx +355 -362
- package/src/screens/RegisterScreen.tsx +1 -1
- package/src/screens/ShopScreen.tsx +439 -268
- package/src/screens/WishlistScreen.tsx +80 -76
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
4
4
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
|
-
import { Heart } from 'lucide-react';
|
|
5
|
+
import { Heart, ShoppingCart } from 'lucide-react';
|
|
6
6
|
import { ExtendedProductDTO, Product } from '@/lib/Apis';
|
|
7
7
|
import { formatPrice } from '@/lib/utils/format';
|
|
8
8
|
import { useWishlist } from '@/providers/WishlistProvider';
|
|
9
|
+
import { useCart } from '@/providers/CartProvider';
|
|
9
10
|
import Image from 'next/image';
|
|
10
11
|
import { toast } from 'sonner';
|
|
11
12
|
import { useRouter } from 'next/navigation';
|
|
@@ -32,8 +33,11 @@ export function ProductCard({
|
|
|
32
33
|
const { buildPath } = useBasePath();
|
|
33
34
|
const [isFavorite, setIsFavorite] = useState(isFavorited);
|
|
34
35
|
const { addToWishlist, removeFromWishlist, isInWishlist } = useWishlist();
|
|
36
|
+
const { addToCart, isLoading: isAddingToCart } = useCart();
|
|
35
37
|
const [isHovered, setIsHovered] = useState(false);
|
|
36
38
|
const [isImageLoaded, setIsImageLoaded] = useState(false);
|
|
39
|
+
const [selectedVariantImage, setSelectedVariantImage] = useState<string | null>(null);
|
|
40
|
+
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
|
37
41
|
|
|
38
42
|
// Handle image load state
|
|
39
43
|
const handleImageLoad = useCallback(() => {
|
|
@@ -74,6 +78,13 @@ export function ProductCard({
|
|
|
74
78
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
75
79
|
}, [isFavorited, isInWishlist, product?._id]);
|
|
76
80
|
|
|
81
|
+
// Reset selected variant image when product changes
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
setSelectedVariantImage(null);
|
|
84
|
+
setSelectedVariantId(null);
|
|
85
|
+
setIsImageLoaded(false);
|
|
86
|
+
}, [product._id]);
|
|
87
|
+
|
|
77
88
|
// Handle keyboard navigation
|
|
78
89
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
79
90
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
@@ -83,19 +94,53 @@ export function ProductCard({
|
|
|
83
94
|
};
|
|
84
95
|
|
|
85
96
|
|
|
97
|
+
// Get variant images (first image from each variant that has images)
|
|
98
|
+
const variantImages = useMemo(() => {
|
|
99
|
+
if (!product.productVariants || product.productVariants.length === 0) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
return product.productVariants
|
|
103
|
+
.filter((variant) => variant.productMedia && variant.productMedia.length > 0)
|
|
104
|
+
.map((variant) => ({
|
|
105
|
+
variantId: variant.id || variant._id,
|
|
106
|
+
variantName: variant.name,
|
|
107
|
+
image: variant.productMedia[0].file,
|
|
108
|
+
color: variant.attribute?.color || variant.attribute?.Color || null,
|
|
109
|
+
}));
|
|
110
|
+
}, [product.productVariants]);
|
|
111
|
+
|
|
112
|
+
// Get selected variant
|
|
113
|
+
const selectedVariant = useMemo(() => {
|
|
114
|
+
if (!selectedVariantId || !product.productVariants) return null;
|
|
115
|
+
return product.productVariants.find(
|
|
116
|
+
(variant) => (variant.id || variant._id) === selectedVariantId
|
|
117
|
+
);
|
|
118
|
+
}, [selectedVariantId, product.productVariants]);
|
|
119
|
+
|
|
120
|
+
// Get display name (variant name if selected, otherwise product name)
|
|
121
|
+
const displayName = useMemo(() => {
|
|
122
|
+
return selectedVariant?.name || product.name;
|
|
123
|
+
}, [selectedVariant, product.name]);
|
|
124
|
+
|
|
86
125
|
const imageSource = useMemo(() => {
|
|
126
|
+
const src = selectedVariantImage || product.productMedia?.[0]?.file || '/placeholder-product.jpg';
|
|
87
127
|
return {
|
|
88
|
-
src
|
|
128
|
+
src,
|
|
89
129
|
alt: product.name || 'Product image'
|
|
90
130
|
};
|
|
91
|
-
}, [product.productMedia, product.name]);
|
|
131
|
+
}, [product.productMedia, product.name, selectedVariantImage]);
|
|
132
|
+
|
|
133
|
+
// Reset image loaded state when image source changes
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
setIsImageLoaded(false);
|
|
136
|
+
}, [imageSource.src]);
|
|
92
137
|
|
|
93
138
|
|
|
94
139
|
|
|
95
140
|
return (
|
|
96
141
|
<motion.article
|
|
97
142
|
className=
|
|
98
|
-
"relative group bg-white rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 hover:border-gray-200 flex
|
|
143
|
+
"relative group bg-white rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 hover:border-gray-200 flex flex-col"
|
|
99
144
|
|
|
100
145
|
whileHover={{ y: -4 }}
|
|
101
146
|
onMouseEnter={() => setIsHovered(true)}
|
|
@@ -107,7 +152,7 @@ export function ProductCard({
|
|
|
107
152
|
onKeyDown={handleKeyDown}
|
|
108
153
|
>
|
|
109
154
|
{/* Image Container */}
|
|
110
|
-
<div className="relative
|
|
155
|
+
<div className="relative aspect-square w-full overflow-hidden bg-gray-50">
|
|
111
156
|
<AnimatePresence>
|
|
112
157
|
{!isImageLoaded && (
|
|
113
158
|
<motion.div
|
|
@@ -119,18 +164,18 @@ export function ProductCard({
|
|
|
119
164
|
)}
|
|
120
165
|
</AnimatePresence>
|
|
121
166
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
167
|
+
<Image
|
|
168
|
+
src={imageSource.src}
|
|
169
|
+
alt={imageSource.alt}
|
|
170
|
+
fill
|
|
171
|
+
className={`h-full w-full object-cover object-center transition-opacity duration-300 ${product.inventoryCount === 0 ? 'opacity-60' : ''
|
|
172
|
+
} ${isImageLoaded ? 'opacity-100' : 'opacity-0'}`}
|
|
173
|
+
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, 33vw"
|
|
174
|
+
priority={false}
|
|
175
|
+
onLoad={handleImageLoad}
|
|
176
|
+
onError={() => setIsImageLoaded(true)}
|
|
177
|
+
key={imageSource.src}
|
|
178
|
+
/>
|
|
134
179
|
|
|
135
180
|
{/* Badges */}
|
|
136
181
|
<div className="absolute top-3 left-3 flex flex-col gap-2 z-10">
|
|
@@ -160,14 +205,12 @@ export function ProductCard({
|
|
|
160
205
|
<motion.button
|
|
161
206
|
type="button"
|
|
162
207
|
onClick={handleFavorite}
|
|
163
|
-
className=
|
|
164
|
-
`absolute top-2 right-2 p-2 rounded-full z-10 transition-colors bg-white/90 backdrop-blur-sm shadow-md hover:shadow-lg ${isFavorite ? 'text-red-500' : 'text-primary-600 hover:text-red-500'} ${isHovered || isFavorite ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`
|
|
165
|
-
}
|
|
208
|
+
className="absolute top-2 right-2 p-2 rounded-full z-10 transition-colors bg-white shadow-md hover:shadow-lg text-primary-600 hover:text-red-500"
|
|
166
209
|
whileHover={{ scale: 1.1 }}
|
|
167
210
|
whileTap={{ scale: 0.95 }}
|
|
168
211
|
aria-label={isFavorite ? 'Remove from wishlist' : 'Add to wishlist'}
|
|
169
212
|
>
|
|
170
|
-
<Heart className={`w-5 h-5 ${isFavorite ? 'fill-
|
|
213
|
+
<Heart className={`w-5 h-5 ${isFavorite ? 'fill-red-500 text-red-500' : ''}`} />
|
|
171
214
|
</motion.button>
|
|
172
215
|
)}
|
|
173
216
|
|
|
@@ -180,23 +223,10 @@ export function ProductCard({
|
|
|
180
223
|
)}
|
|
181
224
|
</div>
|
|
182
225
|
|
|
183
|
-
{/* Favorite Button */}
|
|
184
|
-
{showFavoriteButton && (
|
|
185
|
-
<button
|
|
186
|
-
type="button"
|
|
187
|
-
onClick={handleFavorite}
|
|
188
|
-
className={`absolute top-2 right-2 p-2 rounded-full transition-colors ${isFavorite ? 'text-red-500' : 'text-gray-400 hover:text-red-500'
|
|
189
|
-
}`}
|
|
190
|
-
aria-label={isFavorite ? 'Remove from wishlist' : 'Add to wishlist'}
|
|
191
|
-
>
|
|
192
|
-
<Heart className={`w-5 h-5 ${isFavorite ? 'fill-current' : ''}`} />
|
|
193
|
-
</button>
|
|
194
|
-
)}
|
|
195
|
-
|
|
196
226
|
<div className="p-4">
|
|
197
227
|
{/* Category */}
|
|
198
228
|
{product.parentCategories && product.parentCategories?.length > 0 && (
|
|
199
|
-
<p className="text-xs text-gray-500 uppercase tracking-wider mb-
|
|
229
|
+
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">
|
|
200
230
|
{product.parentCategories?.map((category) => category?.name).join(', ') || 'No categories'}
|
|
201
231
|
</p>
|
|
202
232
|
)}
|
|
@@ -204,11 +234,8 @@ export function ProductCard({
|
|
|
204
234
|
{/* Product Name and Variant */}
|
|
205
235
|
<div className="mb-2">
|
|
206
236
|
<h3 className="text-lg font-semibold text-gray-900 line-clamp-1 group-hover:text-primary-600 transition-colors">
|
|
207
|
-
{
|
|
237
|
+
{displayName}
|
|
208
238
|
</h3>
|
|
209
|
-
{product?.sku && (
|
|
210
|
-
<p className="text-xs text-gray-400 mt-1">SKU: {product.sku}</p>
|
|
211
|
-
)}
|
|
212
239
|
</div>
|
|
213
240
|
|
|
214
241
|
{/* Price */}
|
|
@@ -229,32 +256,81 @@ export function ProductCard({
|
|
|
229
256
|
</span>
|
|
230
257
|
)}
|
|
231
258
|
</div>
|
|
259
|
+
|
|
260
|
+
{/* Variant Image Swatches */}
|
|
261
|
+
{variantImages.length > 0 && (
|
|
262
|
+
<div className="flex items-center gap-2 mt-3">
|
|
263
|
+
{variantImages.map((variant, index) => (
|
|
264
|
+
<button
|
|
265
|
+
key={variant.variantId || index}
|
|
266
|
+
type="button"
|
|
267
|
+
onClick={(e) => {
|
|
268
|
+
e.stopPropagation();
|
|
269
|
+
// Toggle: if clicking the same variant, deselect it
|
|
270
|
+
if (selectedVariantId === variant.variantId) {
|
|
271
|
+
setSelectedVariantImage(null);
|
|
272
|
+
setSelectedVariantId(null);
|
|
273
|
+
} else {
|
|
274
|
+
setSelectedVariantImage(variant.image);
|
|
275
|
+
setSelectedVariantId(variant.variantId || null);
|
|
276
|
+
}
|
|
277
|
+
setIsImageLoaded(false);
|
|
278
|
+
}}
|
|
279
|
+
className={`relative w-8 h-8 rounded-full overflow-hidden border-2 transition-all ${
|
|
280
|
+
selectedVariantId === variant.variantId
|
|
281
|
+
? 'border-primary-500 ring-2 ring-primary-200'
|
|
282
|
+
: 'border-gray-200 hover:border-primary-300'
|
|
283
|
+
}`}
|
|
284
|
+
aria-label={`Select ${variant.color || 'variant'} color`}
|
|
285
|
+
>
|
|
286
|
+
<Image
|
|
287
|
+
src={variant.image}
|
|
288
|
+
alt={variant.color || `Variant ${index + 1}`}
|
|
289
|
+
fill
|
|
290
|
+
className="object-cover"
|
|
291
|
+
sizes="32px"
|
|
292
|
+
/>
|
|
293
|
+
</button>
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
232
297
|
</div>
|
|
233
298
|
<div className="mt-auto p-4 pt-0">
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
onClick={async (e) => {
|
|
302
|
+
e.stopPropagation();
|
|
303
|
+
if (!selectedVariantId && variantImages.length > 0) {
|
|
304
|
+
toast.error('Please select a variant');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
await addToCart(
|
|
309
|
+
product._id || product.id,
|
|
310
|
+
1,
|
|
311
|
+
selectedVariantId || undefined
|
|
312
|
+
);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error('Failed to add to cart', error);
|
|
315
|
+
}
|
|
316
|
+
}}
|
|
317
|
+
disabled={isAddingToCart || (variantImages.length > 0 && !selectedVariantId)}
|
|
318
|
+
className="w-full flex items-center justify-center gap-2 rounded-full px-3 py-2.5 text-sm font-medium bg-primary-400 hover:bg-primary-500 text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
319
|
+
>
|
|
320
|
+
<ShoppingCart className="h-4 w-4" />
|
|
321
|
+
Add to Cart
|
|
322
|
+
</button>
|
|
323
|
+
|
|
234
324
|
<button
|
|
235
325
|
type="button"
|
|
236
326
|
onClick={(e) => {
|
|
237
327
|
e.stopPropagation();
|
|
238
328
|
router.push(buildPath(`/products/${product._id}`));
|
|
239
329
|
}}
|
|
240
|
-
className=
|
|
330
|
+
className="mt-2 w-full flex items-center justify-center rounded-full border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
|
|
241
331
|
>
|
|
242
|
-
View
|
|
332
|
+
View More
|
|
243
333
|
</button>
|
|
244
|
-
|
|
245
|
-
{showFavoriteButton && (
|
|
246
|
-
<button
|
|
247
|
-
type="button"
|
|
248
|
-
onClick={handleFavorite}
|
|
249
|
-
className="mt-2 w-full flex items-center justify-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-primary-600 hover:bg-gray-50"
|
|
250
|
-
aria-label={isFavorite ? 'Remove from wishlist' : 'Add to wishlist'}
|
|
251
|
-
>
|
|
252
|
-
<Heart
|
|
253
|
-
className={`mr-2 h-4 w-4 ${isFavorite ? 'fill-red-500 text-red-500' : 'text-primary-600'}`}
|
|
254
|
-
/>
|
|
255
|
-
{isFavorite ? 'Saved' : 'Save for later'}
|
|
256
|
-
</button>
|
|
257
|
-
)}
|
|
258
334
|
</div>
|
|
259
335
|
</motion.article>
|
|
260
336
|
);
|
package/src/hooks/useOrders.ts
CHANGED
package/src/lib/types/index.ts
CHANGED
|
@@ -57,7 +57,33 @@ export function CartProvider({ children }: CartProviderProps) {
|
|
|
57
57
|
const addToCart = async (productId: string, quantity: number = 1, variantId?: string) => {
|
|
58
58
|
setIsLoading(true);
|
|
59
59
|
try {
|
|
60
|
-
|
|
60
|
+
// Get current cart items
|
|
61
|
+
const currentItems = cart?.cartBody?.items || [];
|
|
62
|
+
const targetVariantId = variantId || productId;
|
|
63
|
+
|
|
64
|
+
// Check if item already exists in cart
|
|
65
|
+
const existingItemIndex = currentItems.findIndex(
|
|
66
|
+
(item: any) => item.productVariantId === targetVariantId
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Build the items array
|
|
70
|
+
const items = [...currentItems];
|
|
71
|
+
|
|
72
|
+
if (existingItemIndex >= 0) {
|
|
73
|
+
// Update quantity if item exists
|
|
74
|
+
items[existingItemIndex] = {
|
|
75
|
+
...items[existingItemIndex],
|
|
76
|
+
quantity: (items[existingItemIndex].quantity || 0) + quantity
|
|
77
|
+
};
|
|
78
|
+
} else {
|
|
79
|
+
// Add new item
|
|
80
|
+
(items as any).push({
|
|
81
|
+
productVariantId: targetVariantId,
|
|
82
|
+
quantity,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const response = await new CartApi(getApiConfiguration()).handleUserCart({ items });
|
|
61
87
|
setCart(response.data);
|
|
62
88
|
toast.success('Added to cart!');
|
|
63
89
|
} catch (error: any) {
|
|
@@ -71,7 +97,21 @@ export function CartProvider({ children }: CartProviderProps) {
|
|
|
71
97
|
const updateQuantity = async (productId: string, quantity: number) => {
|
|
72
98
|
setIsLoading(true);
|
|
73
99
|
try {
|
|
74
|
-
|
|
100
|
+
// Get current cart items
|
|
101
|
+
const currentItems = cart?.cartBody?.items || [];
|
|
102
|
+
|
|
103
|
+
// Build the items array with updated quantity
|
|
104
|
+
const items = currentItems.map((item: any) => {
|
|
105
|
+
if (item.productVariantId === productId) {
|
|
106
|
+
return {
|
|
107
|
+
...item,
|
|
108
|
+
quantity
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return item;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const response = await new CartApi(getApiConfiguration()).handleUserCart({ items });
|
|
75
115
|
setCart(response.data);
|
|
76
116
|
} catch (error: any) {
|
|
77
117
|
toast.error(error.response?.data?.message || 'Failed to update cart');
|
|
@@ -84,7 +124,11 @@ export function CartProvider({ children }: CartProviderProps) {
|
|
|
84
124
|
const removeFromCart = async (productId: string) => {
|
|
85
125
|
setIsLoading(true);
|
|
86
126
|
try {
|
|
87
|
-
|
|
127
|
+
// Get current cart items and filter out the item to remove
|
|
128
|
+
const currentItems = cart?.cartBody?.items || [];
|
|
129
|
+
const items = currentItems.filter((item: any) => item.productVariantId !== productId);
|
|
130
|
+
|
|
131
|
+
const response = await new CartApi(getApiConfiguration()).handleUserCart({ items });
|
|
88
132
|
setCart(response.data);
|
|
89
133
|
} catch (error: any) {
|
|
90
134
|
toast.error(error.response?.data?.message || 'Failed to remove from cart');
|