hey-pharmacist-ecommerce 1.1.13 → 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.
Files changed (45) hide show
  1. package/dist/index.d.mts +2 -4
  2. package/dist/index.d.ts +2 -4
  3. package/dist/index.js +1039 -857
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1039 -856
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +3 -3
  8. package/src/components/AccountAddressesTab.tsx +209 -0
  9. package/src/components/AccountOrdersTab.tsx +151 -0
  10. package/src/components/AccountOverviewTab.tsx +209 -0
  11. package/src/components/AccountPaymentTab.tsx +116 -0
  12. package/src/components/AccountSavedItemsTab.tsx +76 -0
  13. package/src/components/AccountSettingsTab.tsx +116 -0
  14. package/src/components/AddressFormModal.tsx +23 -10
  15. package/src/components/CartItem.tsx +60 -56
  16. package/src/components/Header.tsx +69 -16
  17. package/src/components/Notification.tsx +148 -0
  18. package/src/components/ProductCard.tsx +215 -178
  19. package/src/components/QuickViewModal.tsx +314 -0
  20. package/src/components/TabNavigation.tsx +48 -0
  21. package/src/components/ui/Button.tsx +1 -1
  22. package/src/components/ui/ConfirmModal.tsx +84 -0
  23. package/src/hooks/usePaymentMethods.ts +58 -0
  24. package/src/index.ts +0 -1
  25. package/src/providers/CartProvider.tsx +22 -6
  26. package/src/providers/EcommerceProvider.tsx +8 -7
  27. package/src/providers/FavoritesProvider.tsx +10 -3
  28. package/src/providers/NotificationProvider.tsx +79 -0
  29. package/src/providers/WishlistProvider.tsx +34 -9
  30. package/src/screens/AddressesScreen.tsx +72 -61
  31. package/src/screens/CartScreen.tsx +48 -32
  32. package/src/screens/ChangePasswordScreen.tsx +155 -0
  33. package/src/screens/CheckoutScreen.tsx +162 -125
  34. package/src/screens/EditProfileScreen.tsx +165 -0
  35. package/src/screens/LoginScreen.tsx +59 -72
  36. package/src/screens/NewAddressScreen.tsx +16 -10
  37. package/src/screens/ProductDetailScreen.tsx +334 -234
  38. package/src/screens/ProfileScreen.tsx +190 -200
  39. package/src/screens/RegisterScreen.tsx +51 -70
  40. package/src/screens/SearchResultsScreen.tsx +2 -1
  41. package/src/screens/ShopScreen.tsx +260 -384
  42. package/src/screens/WishlistScreen.tsx +226 -224
  43. package/src/styles/globals.css +9 -0
  44. package/src/screens/CategoriesScreen.tsx +0 -122
  45. package/src/screens/HomeScreen.tsx +0 -211
@@ -0,0 +1,148 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState } from 'react';
4
+ import { motion, AnimatePresence } from 'framer-motion';
5
+ import { CheckCircle2, XCircle, AlertCircle, Info, X } from 'lucide-react';
6
+
7
+ export type NotificationType = 'success' | 'error' | 'warning' | 'info';
8
+
9
+ export interface NotificationData {
10
+ id: string;
11
+ type: NotificationType;
12
+ message: string;
13
+ description?: string;
14
+ duration?: number;
15
+ }
16
+
17
+ interface NotificationProps {
18
+ notification: NotificationData;
19
+ onDismiss: (id: string) => void;
20
+ }
21
+
22
+ const notificationConfig = {
23
+ success: {
24
+ icon: CheckCircle2,
25
+ gradient: 'from-emerald-500 to-green-600',
26
+ iconColor: 'text-emerald-600',
27
+ bgColor: 'bg-emerald-50',
28
+ borderColor: 'border-emerald-200',
29
+ },
30
+ error: {
31
+ icon: XCircle,
32
+ gradient: 'from-red-500 to-rose-600',
33
+ iconColor: 'text-red-600',
34
+ bgColor: 'bg-red-50',
35
+ borderColor: 'border-red-200',
36
+ },
37
+ warning: {
38
+ icon: AlertCircle,
39
+ gradient: 'from-orange-500 to-amber-600',
40
+ iconColor: 'text-orange-600',
41
+ bgColor: 'bg-orange-50',
42
+ borderColor: 'border-orange-200',
43
+ },
44
+ info: {
45
+ icon: Info,
46
+ gradient: 'from-blue-500 to-indigo-600',
47
+ iconColor: 'text-blue-600',
48
+ bgColor: 'bg-blue-50',
49
+ borderColor: 'border-blue-200',
50
+ },
51
+ };
52
+
53
+ export function Notification({ notification, onDismiss }: NotificationProps) {
54
+ const [progress, setProgress] = useState(100);
55
+ const config = notificationConfig[notification.type];
56
+ const Icon = config.icon;
57
+ const duration = notification.duration || 4000;
58
+
59
+ useEffect(() => {
60
+ const startTime = Date.now();
61
+ const timer = setInterval(() => {
62
+ const elapsed = Date.now() - startTime;
63
+ const remaining = Math.max(0, 100 - (elapsed / duration) * 100);
64
+ setProgress(remaining);
65
+
66
+ if (remaining === 0) {
67
+ clearInterval(timer);
68
+ onDismiss(notification.id);
69
+ }
70
+ }, 16);
71
+
72
+ return () => clearInterval(timer);
73
+ }, [notification.id, duration, onDismiss]);
74
+
75
+ return (
76
+ <motion.div
77
+ initial={{ opacity: 0, y: -20, scale: 0.95 }}
78
+ animate={{ opacity: 1, y: 0, scale: 1 }}
79
+ exit={{ opacity: 0, x: 100, scale: 0.95 }}
80
+ transition={{ type: 'spring', stiffness: 500, damping: 30 }}
81
+ className={`relative bg-white rounded-2xl border-2 ${config.borderColor} shadow-xl overflow-hidden min-w-[320px] max-w-[420px]`}
82
+ >
83
+ {/* Gradient accent bar */}
84
+ <div className={`h-1 bg-gradient-to-r ${config.gradient}`} />
85
+
86
+ <div className="p-4 flex items-start gap-3">
87
+ {/* Icon */}
88
+ <motion.div
89
+ initial={{ scale: 0, rotate: -180 }}
90
+ animate={{ scale: 1, rotate: 0 }}
91
+ transition={{ delay: 0.1, type: 'spring', stiffness: 500 }}
92
+ className={`size-10 rounded-full ${config.bgColor} flex items-center justify-center shrink-0`}
93
+ >
94
+ <Icon className={`size-5 ${config.iconColor}`} />
95
+ </motion.div>
96
+
97
+ {/* Content */}
98
+ <div className="flex-1 min-w-0">
99
+ <p className="font-['Poppins',sans-serif] font-semibold text-[14px] text-[#2B4B7C] mb-1">
100
+ {notification.message}
101
+ </p>
102
+ {notification.description && (
103
+ <p className="font-['Poppins',sans-serif] text-[12px] text-[#676c80] leading-relaxed">
104
+ {notification.description}
105
+ </p>
106
+ )}
107
+ </div>
108
+
109
+ {/* Close button */}
110
+ <button
111
+ onClick={() => onDismiss(notification.id)}
112
+ className="p-1.5 hover:bg-gray-100 rounded-full transition-colors shrink-0"
113
+ aria-label="Dismiss notification"
114
+ >
115
+ <X className="size-4 text-[#676c80]" />
116
+ </button>
117
+ </div>
118
+
119
+ {/* Progress bar */}
120
+ <div className="h-1 bg-gray-100">
121
+ <motion.div
122
+ className={`h-full bg-gradient-to-r ${config.gradient}`}
123
+ style={{ width: `${progress}%` }}
124
+ transition={{ duration: 0.016, ease: 'linear' }}
125
+ />
126
+ </div>
127
+ </motion.div>
128
+ );
129
+ }
130
+
131
+ interface NotificationContainerProps {
132
+ notifications: NotificationData[];
133
+ onDismiss: (id: string) => void;
134
+ }
135
+
136
+ export function NotificationContainer({ notifications, onDismiss }: NotificationContainerProps) {
137
+ return (
138
+ <div className="fixed top-4 right-4 z-[9999] flex flex-col gap-3 pointer-events-none">
139
+ <AnimatePresence mode="popLayout">
140
+ {notifications.map((notification) => (
141
+ <div key={notification.id} className="pointer-events-auto">
142
+ <Notification notification={notification} onDismiss={onDismiss} />
143
+ </div>
144
+ ))}
145
+ </AnimatePresence>
146
+ </div>
147
+ );
148
+ }
@@ -2,16 +2,16 @@
2
2
 
3
3
  import React, { useState, useEffect, useMemo, useCallback } from 'react';
4
4
  import { motion, AnimatePresence } from 'framer-motion';
5
- import { Heart, ShoppingCart } from 'lucide-react';
5
+ import { Star, ShoppingCart, Eye } 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
9
  import { useCart } from '@/providers/CartProvider';
10
10
  import Image from 'next/image';
11
- import { toast } from 'sonner';
12
11
  import { useRouter } from 'next/navigation';
13
12
  import { useBasePath } from '@/providers/BasePathProvider';
14
-
13
+ import { QuickViewModal } from './QuickViewModal';
14
+ import { useNotification } from '@/providers/NotificationProvider';
15
15
 
16
16
  interface ProductCardProps {
17
17
  product: ExtendedProductDTO;
@@ -38,7 +38,8 @@ export function ProductCard({
38
38
  const [isImageLoaded, setIsImageLoaded] = useState(false);
39
39
  const [selectedVariantImage, setSelectedVariantImage] = useState<string | null>(null);
40
40
  const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
41
-
41
+ const [showQuickView, setShowQuickView] = useState(false);
42
+ const notification = useNotification();
42
43
  // Handle image load state
43
44
  const handleImageLoad = useCallback(() => {
44
45
  setIsImageLoaded(true);
@@ -59,31 +60,49 @@ export function ProductCard({
59
60
  if (isInWishlist(product?._id || '')) {
60
61
  await removeFromWishlist(product?._id || '');
61
62
  setIsFavorite(false);
62
- toast.success('Removed from wishlist');
63
+ notification.success(
64
+ 'Removed from wishlist',
65
+ `${product.name} was removed from your wishlist.`
66
+ );
63
67
  } else {
64
68
  await addToWishlist(product as unknown as Product);
65
69
  setIsFavorite(true);
66
- toast.success('Added to wishlist');
70
+ notification.success(
71
+ 'Added to wishlist',
72
+ `${product.name} was added to your wishlist.`
73
+ );
67
74
  }
68
75
  onFavorite?.(product);
69
76
  } catch (error) {
70
77
  console.error('Error updating wishlist:', error);
71
- toast.error('Failed to update wishlist');
78
+ notification.error(
79
+ 'Could not update wishlist',
80
+ 'We could not update your wishlist. Please try again.'
81
+ );
72
82
  }
73
83
  };
74
84
 
75
85
  // Update favorite state when props or wishlist changes
76
86
  useEffect(() => {
77
87
  setIsFavorite(isInWishlist(product?._id || '') || isFavorited);
78
- // eslint-disable-next-line react-hooks/exhaustive-deps
88
+ // eslint-disable-next-line react-hooks/exhaustive-deps
79
89
  }, [isFavorited, isInWishlist, product?._id]);
80
90
 
81
- // Reset selected variant image when product changes
91
+ // Reset selected variant image when product changes and auto-select first variant
82
92
  useEffect(() => {
83
93
  setSelectedVariantImage(null);
84
94
  setSelectedVariantId(null);
85
95
  setIsImageLoaded(false);
86
- }, [product._id]);
96
+
97
+ // Auto-select first variant if available
98
+ if (product.productVariants && product.productVariants.length > 0) {
99
+ const firstVariant = product.productVariants[0];
100
+ if (firstVariant.productMedia && firstVariant.productMedia.length > 0) {
101
+ setSelectedVariantImage(firstVariant.productMedia[0].file);
102
+ setSelectedVariantId(firstVariant.id || firstVariant._id || null);
103
+ }
104
+ }
105
+ }, [product._id, product.productVariants]);
87
106
 
88
107
  // Handle keyboard navigation
89
108
  const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -138,108 +157,190 @@ export function ProductCard({
138
157
 
139
158
 
140
159
  return (
141
- <motion.article
142
- className=
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"
144
-
145
- whileHover={{ y: -4 }}
146
- onMouseEnter={() => setIsHovered(true)}
147
- onMouseLeave={() => setIsHovered(false)}
148
- aria-labelledby={`product-${product.id}-title`}
149
- role="article"
150
- tabIndex={0}
151
- onClick={handleCardClick}
152
- onKeyDown={handleKeyDown}
153
- >
154
- {/* Image Container */}
155
- <div className="relative aspect-square w-full overflow-hidden bg-gray-50">
156
- <AnimatePresence>
157
- {!isImageLoaded && (
158
- <motion.div
159
- className="absolute inset-0 bg-gray-200 animate-pulse"
160
- initial={{ opacity: 1 }}
161
- exit={{ opacity: 0 }}
162
- transition={{ duration: 0.2 }}
163
- />
164
- )}
165
- </AnimatePresence>
166
-
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
- />
179
-
180
- {/* Badges */}
181
- <div className="absolute top-3 left-3 flex flex-col gap-2 z-10">
182
- {product.isDiscounted && (
183
- <motion.span
184
- initial={{ scale: 0.9, opacity: 0 }}
185
- animate={{ scale: 1, opacity: 1 }}
186
- className="inline-flex items-center justify-center px-2.5 py-1 rounded-full text-xs font-bold text-white bg-green-600 shadow-md"
187
- >
188
- -{product.discountAmount}%
189
- </motion.span>
190
- )}
160
+ <>
161
+ <motion.div
162
+ className="bg-white rounded-[16px] overflow-hidden border-2 border-gray-100 hover:border-[#5B9BD5] hover:shadow-lg transition-all duration-300 group h-full flex flex-col"
163
+ whileHover={{ y: -4 }}
164
+ onMouseEnter={() => setIsHovered(true)}
165
+ onMouseLeave={() => setIsHovered(false)}
166
+ aria-labelledby={`product-${product.id}-title`}
167
+ role="article"
168
+ tabIndex={0}
169
+ onClick={handleCardClick}
170
+ onKeyDown={handleKeyDown}
171
+ >
172
+ <div
173
+ onClick={handleCardClick}
174
+ className="relative aspect-square overflow-hidden bg-gray-50 cursor-pointer flex-shrink-0"
175
+ >
176
+ <Image
177
+ src={imageSource.src}
178
+ alt={imageSource.alt}
179
+ className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
180
+ height={300}
181
+ width={300}
182
+ priority={false}
183
+ onLoad={handleImageLoad}
184
+ onError={() => setIsImageLoaded(true)}
185
+ key={imageSource.src}
186
+ />
187
+ <button
188
+ onClick={(e) => {
189
+ e.stopPropagation();
190
+ setShowQuickView(true);
191
+ }}
192
+ className="absolute top-2 left-2 p-2 rounded-full bg-white/90 backdrop-blur-sm hover:bg-white shadow-md opacity-0 group-hover:opacity-100 transition-all"
193
+ >
194
+ <Eye className="size-4 text-[#2B4B7C]" />
195
+ </button>
191
196
 
192
197
  {product.inventoryCount === 0 && (
193
- <motion.span
194
- initial={{ scale: 0.9, opacity: 0 }}
195
- animate={{ scale: 1, opacity: 1 }}
196
- className="inline-flex items-center justify-center px-2.5 py-1 rounded-full text-xs font-bold text-white bg-red-600"
197
- >
198
- Out of Stock
199
- </motion.span>
198
+ <div className="absolute inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center">
199
+ <div className="bg-white rounded-full px-4 py-2">
200
+ <span className="font-['Poppins',sans-serif] font-bold text-[11px] text-[#2B4B7C] uppercase">
201
+ Out of Stock
202
+ </span>
203
+ </div>
204
+ </div>
200
205
  )}
201
206
  </div>
202
207
 
203
- {/* Favorite Button */}
204
- {showFavoriteButton && (
205
- <motion.button
206
- type="button"
207
- onClick={handleFavorite}
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"
209
- whileHover={{ scale: 1.1 }}
210
- whileTap={{ scale: 0.95 }}
211
- aria-label={isFavorite ? 'Remove from wishlist' : 'Add to wishlist'}
212
- >
213
- <Heart className={`w-5 h-5 ${isFavorite ? 'fill-red-500 text-red-500' : ''}`} />
214
- </motion.button>
215
- )}
208
+ <div className="p-4 flex-1 flex flex-col">
209
+ {/* Product Info */}
210
+ <div className="p-0 flex-1 flex flex-col">
211
+ {/* ALL Status Badges */}
212
+ <div className="flex items-center gap-1 mb-2 flex-wrap">
213
+ {product.isDiscounted && (
214
+ <span className="bg-[#E67E50] text-white rounded-full px-2 py-0.5 flex items-center gap-1">
215
+ <span className="font-['Poppins',sans-serif] font-bold text-[8px] uppercase">-{product.discountAmount}%</span>
216
+ </span>
217
+ )}
218
+ {/* {product.dealOfWeek && (
219
+ <span className="bg-gradient-to-r from-[#E67E50] to-[#d66f45] text-white rounded-full px-2 py-0.5 flex items-center gap-1">
220
+ <TrendingUp className="size-2.5" />
221
+ <span className="font-['Poppins',sans-serif] font-semibold text-[8px] uppercase">Deal</span>
222
+ </span>
223
+ )}
224
+ {product.bestseller && !product.dealOfWeek && (
225
+ <span className="bg-[#2B4B7C] text-white rounded-full px-2 py-0.5 flex items-center gap-1">
226
+ <TrendingUp className="size-2.5" />
227
+ <span className="font-['Poppins',sans-serif] font-semibold text-[8px] uppercase">Best</span>
228
+ </span>
229
+ )}
230
+ {product.newArrival && (
231
+ <span className="bg-[#5B9BD5] text-white rounded-full px-2 py-0.5 flex items-center gap-1">
232
+ <Sparkles className="size-2.5" />
233
+ <span className="font-['Poppins',sans-serif] font-semibold text-[8px] uppercase">New</span>
234
+ </span>
235
+ )} */}
236
+ </div>
237
+ <div className="mb-1">
238
+ <p className="font-['Poppins',sans-serif] text-xs text-[#5B9BD5] uppercase tracking-wide font-medium">
239
+ {product.brand}
240
+ </p>
241
+ </div>
216
242
 
217
- </div>
218
- <div className="absolute top-4 left-4 flex flex-col gap-2">
219
- {product.inventoryCount === 0 && (
220
- <span className="px-3 py-1 rounded-full text-sm font-bold bg-red-100 text-red-800">
221
- Out of Stock
222
- </span>
223
- )}
224
- </div>
243
+ <h3 className="text-sm font-['Poppins',sans-serif] font-semibold text-[#2B4B7C] mb-3">
244
+ {displayName}
245
+ </h3>
246
+ {/* Rating */}
247
+ <div className="flex items-center gap-1.5 mb-2">
248
+ <div className="flex items-center gap-0.5">
249
+ {[...Array(5)].map((_, i) => (
250
+ <Star
251
+ key={i}
252
+ className={`size-4 ${i < Math.floor(product.rating ? product.rating : 0)
253
+ ? 'text-[#E67E50] fill-[#E67E50]'
254
+ : 'text-gray-300'
255
+ }`}
256
+ />
257
+ ))}
258
+ </div>
259
+ <span className="font-['Poppins',sans-serif] text-[10px] text-[#676c80]">
260
+ ({product.reviews?.length || 0})
261
+ </span>
262
+ </div>
225
263
 
226
- <div className="p-4">
227
- {/* Category */}
228
- {product.parentCategories && product.parentCategories?.length > 0 && (
229
- <p className="text-xs text-gray-500 uppercase tracking-wider mb-1">
230
- {product.parentCategories?.map((category) => category?.name).join(', ') || 'No categories'}
231
- </p>
232
- )}
264
+ {/* Price */}
265
+ <div className="flex items-center gap-1.5 mb-3">
266
+ <span className="font-['Poppins',sans-serif] font-bold text-md text-primary-600">
267
+ ${product.finalPrice.toFixed(2)}
268
+ </span>
269
+ {product.isDiscounted && (
270
+ <span className="font-['Poppins',sans-serif] text-sm text-[#676c80] line-through">
271
+ ${formatPrice(product.priceBeforeDiscount)}
272
+ </span>
273
+ )}
274
+ </div>
233
275
 
234
- {/* Product Name and Variant */}
235
- <div className="mb-2">
236
- <h3 className="text-lg font-semibold text-gray-900 line-clamp-1 group-hover:text-primary-600 transition-colors">
237
- {displayName}
238
- </h3>
276
+ {/* Variant Image Swatches */}
277
+ {variantImages.length > 0 && (
278
+ <div className="flex items-center gap-1.5 mb-3">
279
+ {variantImages.map((variant, index) => (
280
+ <button
281
+ key={variant.variantId || index}
282
+ type="button"
283
+ onClick={(e) => {
284
+ e.stopPropagation();
285
+ // Toggle: if clicking the same variant, deselect it
286
+ if (selectedVariantId === variant.variantId) {
287
+ setSelectedVariantImage(null);
288
+ setSelectedVariantId(null);
289
+ } else {
290
+ setSelectedVariantImage(variant.image);
291
+ setSelectedVariantId(variant.variantId || null);
292
+ }
293
+ setIsImageLoaded(false);
294
+ }}
295
+ className={`relative w-8 h-8 rounded-full overflow-hidden border-2 transition-all ${selectedVariantId === variant.variantId
296
+ ? 'border-primary-500 ring-2 ring-primary-200'
297
+ : 'border-gray-200 hover:border-primary-300'
298
+ }`}
299
+ aria-label={`Select ${variant.color || 'variant'} color`}
300
+ >
301
+ <Image
302
+ src={variant.image}
303
+ alt={variant.color || `Variant ${index + 1}`}
304
+ fill
305
+ className="object-cover"
306
+ sizes="32px"
307
+ />
308
+ </button>
309
+ ))}
310
+ </div>
311
+ )}
312
+ </div>
313
+ <button
314
+ type="button"
315
+ onClick={async (e) => {
316
+ e.stopPropagation();
317
+ if (!selectedVariantId && variantImages.length > 0) {
318
+ notification.error(
319
+ 'Select a variant',
320
+ 'Please choose a variant before adding this item to your cart.'
321
+ );
322
+ return;
323
+ }
324
+ try {
325
+ await addToCart(
326
+ product._id || product.id,
327
+ 1,
328
+ selectedVariantId || undefined
329
+ );
330
+ } catch (error) {
331
+ console.error('Failed to add to cart', error);
332
+ }
333
+ }}
334
+ disabled={isAddingToCart || (variantImages.length > 0 && !selectedVariantId)}
335
+ className="w-full font-['Poppins',sans-serif] font-medium text-[11px] px-3 py-2 rounded-full bg-[#5B9BD5] text-white hover:bg-[#4a8ac4] hover:shadow-lg transition-all duration-300 flex items-center justify-center gap-1.5 disabled:opacity-50 disabled:cursor-not-allowed"
336
+ >
337
+ <ShoppingCart className="h-4 w-4" />
338
+ {selectedVariant?.inventoryCount === 0 ? 'Out of Stock' : 'Add to Cart'}
339
+ </button>
239
340
  </div>
240
341
 
241
- {/* Price */}
242
- <div className="flex items-baseline gap-2">
342
+ {/* prev Price */}
343
+ {/* <div className="flex items-baseline gap-2">
243
344
  <div className="flex flex-col">
244
345
  <span className="text-2xl font-bold text-gray-900">
245
346
  {formatPrice(product.finalPrice)}
@@ -255,83 +356,19 @@ export function ProductCard({
255
356
  {formatPrice(product.priceBeforeDiscount)}
256
357
  </span>
257
358
  )}
258
- </div>
359
+ </div> */}
259
360
 
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
- )}
297
- </div>
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
361
 
324
- <button
325
- type="button"
326
- onClick={(e) => {
327
- e.stopPropagation();
328
- router.push(buildPath(`/products/${product._id}`));
329
- }}
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"
331
- >
332
- View More
333
- </button>
334
- </div>
335
- </motion.article>
362
+ </motion.div>
363
+
364
+
365
+ {showQuickView && (
366
+ <QuickViewModal
367
+ product={product}
368
+ onClose={() => setShowQuickView(false)}
369
+ onNavigateToProduct={() => handleCardClick}
370
+ />
371
+ )}
372
+ </>
336
373
  );
337
374
  }