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.
@@ -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: product.productMedia?.[0]?.file || '/placeholder-product.jpg',
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 h-[420px] flex-col"
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 h-48 w-full overflow-hidden bg-gray-50">
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
- {product.productMedia?.[0]?.file && (
123
- <Image
124
- src={product.productMedia?.[0]?.file || '/placeholder-product.jpg'}
125
- alt={product.name || 'Product image'}
126
- fill
127
- className={`h-full w-full object-cover object-center transition-opacity duration-300 ${product.inventoryCount === 0 ? 'opacity-60' : ''
128
- } ${isImageLoaded ? 'opacity-100' : 'opacity-0'}`}
129
- sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, 33vw"
130
- priority={false}
131
- onLoad={handleImageLoad}
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-current' : ''}`} />
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-2">
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
- {product.name}
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={`w-full flex items-center justify-center rounded-md px-3 py-2 text-sm font-medium bg-primary-600 hover:bg-primary-700 text-white`}
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 Product
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
  );
@@ -37,6 +37,7 @@ export function useOrders(
37
37
  resolvedUserId
38
38
  );
39
39
  setOrders(response.data.data || []);
40
+ console.log(response.data);
40
41
  setPagination({
41
42
  page: response.data.page || page,
42
43
  limit: response.data.limit || limit,
@@ -34,6 +34,7 @@ export interface ProductFilters {
34
34
  inStock?: boolean;
35
35
  ids?: string[];
36
36
  newArrivals?: boolean;
37
+ brand?: string;
37
38
  }
38
39
 
39
40
  // Order Types
@@ -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
- const response = await new CartApi(getApiConfiguration()).handleUserCart({ items: [{ productVariantId: variantId || productId, quantity }] });
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
- const response = await new CartApi(getApiConfiguration()).handleUserCart({ items: [{ productVariantId: productId, quantity }] });
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
- const response = await new CartApi(getApiConfiguration()).handleUserCart({ items: [{ productVariantId: productId, quantity: 0 }] });
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');