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
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
Sparkles,
|
|
18
18
|
Star,
|
|
19
19
|
Truck,
|
|
20
|
+
ChevronLeft,
|
|
21
|
+
RotateCcw
|
|
20
22
|
} from 'lucide-react';
|
|
21
23
|
import Link from 'next/link';
|
|
22
24
|
import Image from 'next/image';
|
|
@@ -26,7 +28,7 @@ import { ProductCard } from '@/components/ProductCard';
|
|
|
26
28
|
import { useProduct } from '@/hooks/useProducts';
|
|
27
29
|
import { useCart } from '@/providers/CartProvider';
|
|
28
30
|
import { formatDate, formatPrice } from '@/lib/utils/format';
|
|
29
|
-
import {
|
|
31
|
+
import { useNotification } from '@/providers/NotificationProvider';
|
|
30
32
|
import { useRouter } from 'next/navigation';
|
|
31
33
|
import { ProductVariantsApi } from '@/lib/Apis/apis/product-variants-api';
|
|
32
34
|
import { AXIOS_CONFIG } from '@/lib/Apis/wrapper';
|
|
@@ -56,6 +58,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
56
58
|
const { buildPath } = useBasePath();
|
|
57
59
|
const { product: productData, isLoading } = useProduct(productId);
|
|
58
60
|
const { addToCart } = useCart();
|
|
61
|
+
const notification = useNotification();
|
|
59
62
|
|
|
60
63
|
// State declarations
|
|
61
64
|
const [selectedVariant, setSelectedVariant] = useState<ProductVariant | null>(null);
|
|
@@ -65,6 +68,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
65
68
|
const [relatedProducts, setRelatedProducts] = useState<any[]>([]);
|
|
66
69
|
const [initialProductData, setInitialProductData] = useState<any>(null);
|
|
67
70
|
const [activeImageIndex, setActiveImageIndex] = useState(0);
|
|
71
|
+
const [activeTab, setActiveTab] = useState<'description' | 'reviews'>('description');
|
|
68
72
|
|
|
69
73
|
|
|
70
74
|
useEffect(() => {
|
|
@@ -72,6 +76,9 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
72
76
|
}, [selectedVariant]);
|
|
73
77
|
|
|
74
78
|
const product = useMemo(() => {
|
|
79
|
+
console.log('productData', productData);
|
|
80
|
+
console.log('selectedVariant', selectedVariant);
|
|
81
|
+
console.log('initialProductData', initialProductData);
|
|
75
82
|
if (initialProductData && !productData) {
|
|
76
83
|
return initialProductData;
|
|
77
84
|
}
|
|
@@ -221,12 +228,16 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
221
228
|
quantity,
|
|
222
229
|
selectedVariant.id
|
|
223
230
|
);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
231
|
+
notification.success(
|
|
232
|
+
'Added to cart',
|
|
233
|
+
`${quantity} × ${product.name}${selectedVariant.name ? ` (${selectedVariant.name})` : ''} added to your cart.`
|
|
234
|
+
);
|
|
227
235
|
} catch (error) {
|
|
228
236
|
console.error('Failed to add to cart', error);
|
|
229
|
-
|
|
237
|
+
notification.error(
|
|
238
|
+
'Could not add to cart',
|
|
239
|
+
'Something went wrong while adding this item. Please try again.'
|
|
240
|
+
);
|
|
230
241
|
} finally {
|
|
231
242
|
setIsAddingToCart(false);
|
|
232
243
|
}
|
|
@@ -247,16 +258,25 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
247
258
|
try {
|
|
248
259
|
if (isFavorited) {
|
|
249
260
|
await removeFromWishlist(product.id);
|
|
250
|
-
|
|
261
|
+
notification.info(
|
|
262
|
+
'Removed from saved items',
|
|
263
|
+
`${product.name} was removed from your saved items.`
|
|
264
|
+
);
|
|
251
265
|
} else {
|
|
252
266
|
await addToWishlist(product);
|
|
253
|
-
|
|
267
|
+
notification.success(
|
|
268
|
+
'Saved to favorites',
|
|
269
|
+
`${product.name} was added to your favorites.`
|
|
270
|
+
);
|
|
254
271
|
}
|
|
255
272
|
// Update the local state after successful API call
|
|
256
273
|
setIsFavorited(!isFavorited);
|
|
257
274
|
} catch (error) {
|
|
258
275
|
console.error('Error updating wishlist:', error);
|
|
259
|
-
|
|
276
|
+
notification.error(
|
|
277
|
+
'Could not update favorites',
|
|
278
|
+
'We could not update your favorites. Please try again.'
|
|
279
|
+
);
|
|
260
280
|
}
|
|
261
281
|
};
|
|
262
282
|
|
|
@@ -312,22 +332,23 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
312
332
|
|
|
313
333
|
|
|
314
334
|
return (
|
|
315
|
-
<div className="bg-white">
|
|
335
|
+
<div className="min-h-screen bg-white">
|
|
336
|
+
{/* Breadcrumb */}
|
|
337
|
+
<div className="bg-gray-50 border-b border-gray-200 py-4">
|
|
338
|
+
<div className="max-w-[1400px] mx-auto px-8 md:px-12">
|
|
339
|
+
<button
|
|
340
|
+
onClick={() => router.push(buildPath('/shop'))}
|
|
341
|
+
className="flex items-center gap-2 font-['Poppins',sans-serif] text-[13px] text-muted hover:text-primary transition-colors"
|
|
342
|
+
>
|
|
343
|
+
<ChevronLeft className="size-4" />
|
|
344
|
+
Back to Shop
|
|
345
|
+
</button>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
316
348
|
|
|
317
|
-
<
|
|
349
|
+
<section className="py-12">
|
|
318
350
|
<div className="relative pb-20 pt-8">
|
|
319
351
|
<div className="container mx-auto px-4">
|
|
320
|
-
{/* Back Button */}
|
|
321
|
-
<div className="mb-6">
|
|
322
|
-
<Button
|
|
323
|
-
variant="ghost"
|
|
324
|
-
className="text-slate-600 hover:text-slate-900 hover:bg-slate-100"
|
|
325
|
-
onClick={() => router.push(buildPath('/shop'))}
|
|
326
|
-
>
|
|
327
|
-
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
328
|
-
Back to Shop
|
|
329
|
-
</Button>
|
|
330
|
-
</div>
|
|
331
352
|
<div className="grid gap-10 lg:grid-cols-[minmax(0,50fr)_minmax(0,50fr)]">
|
|
332
353
|
<div className="space-y-10">
|
|
333
354
|
<section className="rounded-3xl ">
|
|
@@ -358,22 +379,30 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
358
379
|
className="object-contain"
|
|
359
380
|
/>
|
|
360
381
|
) : null}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
className="
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
382
|
+
|
|
383
|
+
<div className="absolute top-6 left-6 flex flex-col gap-3">
|
|
384
|
+
{product.salePrice && (
|
|
385
|
+
<div className="bg-[#E67E50] text-white rounded-full px-4 py-2">
|
|
386
|
+
<span className="font-['Poppins',sans-serif] font-bold text-[12px] uppercase tracking-wide">
|
|
387
|
+
Save {product.discount}%
|
|
388
|
+
</span>
|
|
389
|
+
</div>
|
|
390
|
+
)}
|
|
391
|
+
{/* {product.bestseller && (
|
|
392
|
+
<div className="bg-secondary text-white rounded-full px-4 py-2">
|
|
393
|
+
<span className="font-['Poppins',sans-serif] font-semibold text-[11px] uppercase tracking-wide">
|
|
394
|
+
Bestseller
|
|
395
|
+
</span>
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
{product.newArrival && (
|
|
399
|
+
<div className="bg-primary text-white rounded-full px-4 py-2">
|
|
400
|
+
<span className="font-['Poppins',sans-serif] font-semibold text-[11px] uppercase tracking-wide">
|
|
401
|
+
New Arrival
|
|
402
|
+
</span>
|
|
403
|
+
</div>
|
|
404
|
+
)} */}
|
|
405
|
+
</div>
|
|
377
406
|
</motion.div>
|
|
378
407
|
|
|
379
408
|
{/* Variant Photos Below Main Photo */}
|
|
@@ -385,8 +414,8 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
385
414
|
type="button"
|
|
386
415
|
onClick={() => setActiveImageIndex(index)}
|
|
387
416
|
className={`relative aspect-square overflow-hidden rounded-lg border-2 transition-all ${activeImageIndex === index
|
|
388
|
-
? 'border-primary
|
|
389
|
-
: 'border-slate-200 hover:border-primary
|
|
417
|
+
? 'border-primary/50 ring-2 ring-primary/80 ring-offset-2 shadow-md'
|
|
418
|
+
: 'border-slate-200 hover:border-primary/50'
|
|
390
419
|
}`}
|
|
391
420
|
>
|
|
392
421
|
<Image
|
|
@@ -403,50 +432,104 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
403
432
|
)}
|
|
404
433
|
</div>
|
|
405
434
|
</section>
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
435
|
</div>
|
|
411
436
|
|
|
412
437
|
|
|
413
438
|
<aside className="space-y-6 lg:sticky lg:top-24">
|
|
414
|
-
<div className="">
|
|
415
439
|
{/* Category Path */}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
440
|
+
<div className="mb-4">
|
|
441
|
+
<p className="font-['Poppins',sans-serif] text-[12px] text-primary uppercase tracking-wide font-medium mb-2">
|
|
442
|
+
{product.brand} • {product.parentCategories?.[0].name}
|
|
443
|
+
</p>
|
|
444
|
+
<h1 className="text-3xl font-['Poppins',sans-serif] font-semibold text-secondary tracking-[-1.5px] mb-3">
|
|
445
|
+
{product.name}
|
|
446
|
+
</h1>
|
|
447
|
+
{/* Rating */}
|
|
448
|
+
<div className="flex items-center gap-3">
|
|
449
|
+
<div className="flex items-center gap-1">
|
|
450
|
+
{[...Array(5)].map((_, i) => (
|
|
451
|
+
<Star
|
|
452
|
+
key={i}
|
|
453
|
+
className={`size-4 ${
|
|
454
|
+
i < Math.floor(product.rating)
|
|
455
|
+
? 'text-[#E67E50] fill-[#E67E50]'
|
|
456
|
+
: 'text-gray-300'
|
|
457
|
+
}`}
|
|
458
|
+
/>
|
|
459
|
+
))}
|
|
460
|
+
</div>
|
|
461
|
+
<span className="font-['Poppins',sans-serif] text-[14px] text-muted">
|
|
462
|
+
{product.rating} ({product.reviewCount? product.reviewCount : 0} reviews)
|
|
463
|
+
</span>
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
<div className="flex items-center gap-3 mb-6 pb-6 border-b-2 border-gray-100">
|
|
467
|
+
<span className="font-['Poppins',sans-serif] font-bold text-[40px] text-[#E67E50]">
|
|
468
|
+
{selectedVariant ? formatPrice(selectedVariant.finalPrice) : formatPrice(product.finalPrice || product.price || 0)}
|
|
469
|
+
</span>
|
|
470
|
+
{variantComparePrice && variantComparePrice > variantPrice && (
|
|
471
|
+
<>
|
|
472
|
+
<span className="font-['Poppins',sans-serif] text-[24px] text-muted line-through">
|
|
473
|
+
${product.price.toFixed(2)}
|
|
474
|
+
</span>
|
|
475
|
+
<div className="px-3 py-1 rounded-full bg-[#E67E50]/10">
|
|
476
|
+
<span className="font-['Poppins',sans-serif] font-semibold text-[13px] text-[#E67E50]">
|
|
477
|
+
Save ${formatPrice(variantComparePrice)}
|
|
422
478
|
</span>
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
479
|
+
</div>
|
|
480
|
+
</>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
{/* Stock Status */}
|
|
485
|
+
{selectedVariant && (
|
|
486
|
+
<div className="mb-6">
|
|
487
|
+
<div className="flex items-center gap-2 mb-2">
|
|
488
|
+
{selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK ||
|
|
489
|
+
selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.LOWSTOCK ? (
|
|
490
|
+
<>
|
|
491
|
+
<div className="size-3 rounded-full bg-red-500" />
|
|
492
|
+
<span className="font-['Poppins',sans-serif] text-[13px] text-red-600 font-medium">
|
|
493
|
+
Out of Stock
|
|
431
494
|
</span>
|
|
432
|
-
|
|
495
|
+
</>
|
|
496
|
+
) : selectedVariant.inventoryCount <= 10 ? (
|
|
497
|
+
<>
|
|
498
|
+
<div className="size-3 rounded-full bg-orange-500 animate-pulse" />
|
|
499
|
+
<span className="font-['Poppins',sans-serif] text-[13px] text-orange-600 font-medium">
|
|
500
|
+
Only {selectedVariant.inventoryCount} left in stock - Order soon!
|
|
501
|
+
</span>
|
|
502
|
+
</>
|
|
503
|
+
) : (
|
|
504
|
+
<>
|
|
505
|
+
<div className="size-3 rounded-full bg-green-500" />
|
|
506
|
+
<span className="font-['Poppins',sans-serif] text-[13px] text-green-600 font-medium">
|
|
507
|
+
In Stock
|
|
508
|
+
</span>
|
|
433
509
|
</>
|
|
434
510
|
)}
|
|
511
|
+
|
|
435
512
|
</div>
|
|
513
|
+
<p className="font-['Poppins',sans-serif] text-[12px] text-muted">
|
|
514
|
+
SKU: {selectedVariant?.sku}
|
|
515
|
+
</p>
|
|
516
|
+
</div>
|
|
436
517
|
)}
|
|
437
518
|
|
|
438
|
-
{/*
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
519
|
+
{/* Description */}
|
|
520
|
+
{product.description && (
|
|
521
|
+
<p className="font-['Poppins',sans-serif] text-[14px] text-muted leading-[1.7] mb-8">
|
|
522
|
+
<div dangerouslySetInnerHTML={{ __html: product.description }} />
|
|
523
|
+
</p>
|
|
524
|
+
)}
|
|
442
525
|
|
|
443
526
|
{/* Variant Selector with Images */}
|
|
444
527
|
{product?.productVariants && product.productVariants.length > 0 && (
|
|
445
528
|
<div className="mb-6">
|
|
446
|
-
|
|
529
|
+
<h3 className="font-['Poppins',sans-serif] font-semibold text-[14px] text-secondary mb-3">
|
|
447
530
|
Select Variant
|
|
448
|
-
</
|
|
449
|
-
<div className="
|
|
531
|
+
</h3>
|
|
532
|
+
<div className="flex flex-wrap gap-3">
|
|
450
533
|
{product.productVariants.map((variant: ProductVariant) => {
|
|
451
534
|
const isSelected = selectedVariant?.id === variant.id;
|
|
452
535
|
const variantImage = variant.productMedia?.[0]?.file
|
|
@@ -458,13 +541,13 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
458
541
|
key={variant.id}
|
|
459
542
|
type="button"
|
|
460
543
|
onClick={() => handleVariantSelect(variant)}
|
|
461
|
-
className={`flex
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
544
|
+
className={`flex items-start gap-3 px-4 py-2.5 rounded-xl border-2 transition-all ${
|
|
545
|
+
isSelected
|
|
546
|
+
? 'border-primary bg-primary/5'
|
|
547
|
+
: 'border-gray-200 hover:border-primary/50'
|
|
548
|
+
}`}
|
|
465
549
|
>
|
|
466
|
-
<div className={`relative h-12 w-12 flex-shrink-0 overflow-hidden rounded-full border-2 ${isSelected ? 'border-primary
|
|
467
|
-
}`}>
|
|
550
|
+
<div className={`relative h-12 w-12 flex-shrink-0 overflow-hidden rounded-full border-2 ${isSelected ? 'border-primary' : 'border-slate-200'}`}>
|
|
468
551
|
<Image
|
|
469
552
|
src={variantImage}
|
|
470
553
|
alt={variant.name || 'Variant image'}
|
|
@@ -474,15 +557,15 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
474
557
|
/>
|
|
475
558
|
</div>
|
|
476
559
|
<div className="flex-1">
|
|
477
|
-
<p className={`text-sm font-medium ${isSelected ? 'text-
|
|
560
|
+
<p className={`text-start text-sm font-medium ${isSelected ? 'text-secondary' : 'text-slate-900'}`}>
|
|
478
561
|
{variant.name}
|
|
479
562
|
</p>
|
|
480
563
|
{variant.sku && (
|
|
481
|
-
<p className="text-xs text-slate-500 mt-0.5">SKU: {variant.sku}</p>
|
|
564
|
+
<p className="text-start text-xs text-slate-500 mt-0.5">SKU: {variant.sku}</p>
|
|
482
565
|
)}
|
|
483
566
|
</div>
|
|
484
567
|
{isSelected && (
|
|
485
|
-
<Check className="h-5 w-5 text-
|
|
568
|
+
<Check className="h-5 w-5 text-secondary flex-shrink-0" />
|
|
486
569
|
)}
|
|
487
570
|
</button>
|
|
488
571
|
);
|
|
@@ -491,79 +574,34 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
491
574
|
</div>
|
|
492
575
|
)}
|
|
493
576
|
|
|
494
|
-
{/* Price */}
|
|
495
|
-
<div className="flex items-baseline gap-3 mb-6">
|
|
496
|
-
<p className="text-3xl font-bold text-orange-600">
|
|
497
|
-
{selectedVariant ? formatPrice(selectedVariant.finalPrice) : formatPrice(product.finalPrice || product.price || 0)}
|
|
498
|
-
</p>
|
|
499
|
-
{variantComparePrice && variantComparePrice > variantPrice && (
|
|
500
|
-
<p className="text-base text-slate-400 line-through">
|
|
501
|
-
{formatPrice(variantComparePrice)}
|
|
502
|
-
</p>
|
|
503
|
-
)}
|
|
504
|
-
{discount > 0 && (
|
|
505
|
-
<Badge variant="danger" size="sm">
|
|
506
|
-
-{discount}%
|
|
507
|
-
</Badge>
|
|
508
|
-
)}
|
|
509
|
-
</div>
|
|
510
|
-
|
|
511
|
-
{/* Stock Status */}
|
|
512
|
-
{selectedVariant && (
|
|
513
|
-
<div className="mb-4 text-sm">
|
|
514
|
-
{selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK ||
|
|
515
|
-
selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.LOWSTOCK ? (
|
|
516
|
-
<div className="text-red-600 font-medium">Out of Stock</div>
|
|
517
|
-
) : (
|
|
518
|
-
<div className="text-orange-600 font-medium">
|
|
519
|
-
Only {selectedVariant.inventoryCount || product.inventoryCount || 0} left in stock - Order soon!
|
|
520
|
-
</div>
|
|
521
|
-
)}
|
|
522
|
-
</div>
|
|
523
|
-
)}
|
|
524
|
-
|
|
525
|
-
{/* SKU */}
|
|
526
|
-
{variantSku && variantSku !== 'N/A' && (
|
|
527
|
-
<div className="mb-4 text-sm text-slate-600">
|
|
528
|
-
<span className="font-medium">SKU:</span> {variantSku}
|
|
529
|
-
</div>
|
|
530
|
-
)}
|
|
531
|
-
|
|
532
|
-
{/* Description */}
|
|
533
|
-
{product.description && (
|
|
534
|
-
<p className="mb-6 text-sm text-slate-600">
|
|
535
|
-
{product.description.replace(/<[^>]*>/g, '').substring(0, 150)}
|
|
536
|
-
{product.description.length > 150 && '...'}
|
|
537
|
-
</p>
|
|
538
|
-
)}
|
|
539
577
|
|
|
540
578
|
{/* Quantity Selector */}
|
|
541
|
-
<div className="mb-
|
|
542
|
-
|
|
579
|
+
<div className="mb-8">
|
|
580
|
+
<h3 className="font-['Poppins',sans-serif] font-semibold text-[14px] text-secondary mb-3">
|
|
581
|
+
Quantity
|
|
582
|
+
</h3>
|
|
543
583
|
<div className="flex items-center gap-3">
|
|
544
|
-
<div className="flex items-center
|
|
584
|
+
<div className="flex items-center gap-4 bg-gray-100 rounded-full px-6 py-3">
|
|
585
|
+
|
|
545
586
|
<button
|
|
546
587
|
type="button"
|
|
547
588
|
onClick={() => setQuantity((current) => Math.max(1, current - 1))}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
>
|
|
551
|
-
<Minus className="h-4 w-4 text-slate-600" />
|
|
552
|
-
</button>
|
|
553
|
-
<span className="w-12 text-center text-sm font-semibold text-slate-900">
|
|
554
|
-
{quantity}
|
|
555
|
-
</span>
|
|
556
|
-
<button
|
|
557
|
-
type="button"
|
|
558
|
-
onClick={() => setQuantity((current) => {
|
|
559
|
-
const maxQty = selectedVariant?.inventoryCount || product.inventoryCount || 999;
|
|
560
|
-
return Math.min(maxQty, current + 1);
|
|
561
|
-
})}
|
|
562
|
-
className="px-3 py-2 hover:bg-slate-50"
|
|
589
|
+
|
|
590
|
+
className="font-['Poppins',sans-serif] font-bold text-[18px] text-secondary hover:text-primary transition-colors"
|
|
563
591
|
aria-label="Increase quantity"
|
|
564
592
|
>
|
|
565
|
-
|
|
593
|
+
-
|
|
566
594
|
</button>
|
|
595
|
+
<span className="font-['Poppins',sans-serif] font-semibold text-[16px] text-secondary min-w-[30px] text-center">
|
|
596
|
+
{quantity}
|
|
597
|
+
</span>
|
|
598
|
+
<button
|
|
599
|
+
onClick={() => setQuantity(Math.min(selectedVariant?.inventoryCount || product.inventoryCount || 999, quantity + 1))}
|
|
600
|
+
disabled={quantity >= (selectedVariant?.inventoryCount || product.inventoryCount || 0)}
|
|
601
|
+
className="font-['Poppins',sans-serif] font-bold text-[18px] text-secondary hover:text-primary transition-colors disabled:opacity-50"
|
|
602
|
+
>
|
|
603
|
+
+
|
|
604
|
+
</button>
|
|
567
605
|
</div>
|
|
568
606
|
{selectedVariant && (
|
|
569
607
|
<span className="text-sm text-slate-500">
|
|
@@ -574,128 +612,190 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
574
612
|
</div>
|
|
575
613
|
|
|
576
614
|
{/* Action Buttons */}
|
|
577
|
-
<div className="flex flex-col gap-
|
|
578
|
-
<
|
|
579
|
-
|
|
580
|
-
className="w-full bg-orange-500 hover:bg-orange-600 text-white py-2.5"
|
|
615
|
+
<div className="flex flex-col sm:flex-row gap-3 mb-8">
|
|
616
|
+
<button
|
|
617
|
+
className="flex-1 font-['Poppins',sans-serif] font-medium text-[14px] px-8 py-4 rounded-full transition-all duration-300 flex items-center justify-center gap-3 bg-[#E67E50] text-white hover:bg-[#d66f45] hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
|
581
618
|
onClick={handleAddToCart}
|
|
582
|
-
isLoading={isAddingToCart}
|
|
583
619
|
disabled={!selectedVariant || selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK}
|
|
584
620
|
>
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
621
|
+
{isAddingToCart ? (
|
|
622
|
+
<>
|
|
623
|
+
<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">
|
|
624
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
625
|
+
<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>
|
|
626
|
+
</svg>
|
|
627
|
+
Loading...
|
|
628
|
+
</>
|
|
629
|
+
) : (
|
|
630
|
+
<>
|
|
631
|
+
<ShoppingCart className="h-4 w-4" />
|
|
632
|
+
{!selectedVariant
|
|
633
|
+
? 'Select a variant'
|
|
634
|
+
: selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK
|
|
635
|
+
? 'Out of Stock'
|
|
636
|
+
: 'Add to Cart'}
|
|
637
|
+
|
|
638
|
+
</>
|
|
639
|
+
)
|
|
640
|
+
}
|
|
641
|
+
</button>
|
|
642
|
+
<button className="sm:w-auto px-6 py-4 rounded-full border-2 border-primary hover:bg-primary/5 transition-all flex items-center justify-center"
|
|
643
|
+
onClick={handleToggleFavorite}>
|
|
644
|
+
<Heart className={`h-4 w-4 ${isFavorited ? 'fill-red-500 text-red-500' : 'text-primary'}`}/>
|
|
645
|
+
</button>
|
|
603
646
|
</div>
|
|
604
647
|
|
|
605
648
|
</div>
|
|
606
649
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
</
|
|
650
|
+
{/* Benefits */}
|
|
651
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 p-6 bg-gradient-to-br from-[#5B9BD5]/5 to-[#2B4B7C]/5 rounded-[24px]">
|
|
652
|
+
<div className="flex items-start gap-3">
|
|
653
|
+
<div className="size-10 rounded-full bg-white flex items-center justify-center shrink-0">
|
|
654
|
+
<Truck className="size-5 text-primary" />
|
|
655
|
+
</div>
|
|
656
|
+
<div>
|
|
657
|
+
<p className="font-['Poppins',sans-serif] font-semibold text-[12px] text-secondary mb-1">
|
|
658
|
+
Free Shipping
|
|
659
|
+
</p>
|
|
660
|
+
<p className="font-['Poppins',sans-serif] text-[11px] text-muted">
|
|
661
|
+
On all orders
|
|
662
|
+
</p>
|
|
663
|
+
</div>
|
|
613
664
|
</div>
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
665
|
+
<div className="flex items-start gap-3">
|
|
666
|
+
<div className="size-10 rounded-full bg-white flex items-center justify-center shrink-0">
|
|
667
|
+
<RotateCcw className="size-5 text-primary" />
|
|
668
|
+
</div>
|
|
669
|
+
<div>
|
|
670
|
+
<p className="font-['Poppins',sans-serif] font-semibold text-[12px] text-secondary mb-1">
|
|
671
|
+
Easy Returns
|
|
672
|
+
</p>
|
|
673
|
+
<p className="font-['Poppins',sans-serif] text-[11px] text-muted">
|
|
674
|
+
30-day return policy
|
|
675
|
+
</p>
|
|
623
676
|
</div>
|
|
624
|
-
<span className="text-sm font-medium text-slate-500">
|
|
625
|
-
Rated 4.8 • Patients love the results
|
|
626
|
-
</span>
|
|
627
677
|
</div>
|
|
628
|
-
<div className="
|
|
629
|
-
|
|
630
|
-
<
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
678
|
+
<div className="flex items-start gap-3">
|
|
679
|
+
<div className="size-10 rounded-full bg-white flex items-center justify-center shrink-0">
|
|
680
|
+
<Shield className="size-5 text-primary" />
|
|
681
|
+
</div>
|
|
682
|
+
<div>
|
|
683
|
+
<p className="font-['Poppins',sans-serif] font-semibold text-[12px] text-secondary mb-1">
|
|
684
|
+
Secure Checkout
|
|
685
|
+
</p>
|
|
686
|
+
<p className="font-['Poppins',sans-serif] text-[11px] text-muted">
|
|
687
|
+
Safe & protected
|
|
688
|
+
</p>
|
|
689
|
+
</div>
|
|
690
|
+
</div>
|
|
691
|
+
<div className="flex items-start gap-3">
|
|
692
|
+
<div className="size-10 rounded-full bg-white flex items-center justify-center shrink-0">
|
|
693
|
+
<Package className="size-5 text-primary" />
|
|
694
|
+
</div>
|
|
695
|
+
<div>
|
|
696
|
+
<p className="font-['Poppins',sans-serif] font-semibold text-[12px] text-secondary mb-1">
|
|
697
|
+
Quality Guaranteed
|
|
698
|
+
</p>
|
|
699
|
+
<p className="font-['Poppins',sans-serif] text-[11px] text-muted">
|
|
700
|
+
Premium materials
|
|
701
|
+
</p>
|
|
702
|
+
</div>
|
|
635
703
|
</div>
|
|
636
704
|
</div>
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
<div className="rounded-2xl border border-slate-200 bg-slate-50/60 p-5 mt-10">
|
|
642
|
-
<p className="text- font-semibold uppercase tracking-[0.3em] text-slate-500">
|
|
643
|
-
Product details
|
|
644
|
-
</p>
|
|
645
|
-
<div className="mt-3 space-y-2 text-sm text-slate-600">
|
|
646
|
-
<p>
|
|
647
|
-
<span className="font-medium text-slate-700">Variant:</span> {currentVariant.name}
|
|
648
|
-
</p>
|
|
649
|
-
<p>
|
|
650
|
-
<span className="font-medium text-slate-700">SKU:</span> {variantSku}
|
|
651
|
-
</p>
|
|
652
|
-
<p>
|
|
653
|
-
<span className="font-medium text-slate-700">Status:</span>{' '}
|
|
654
|
-
<span className={variantInStock ? 'text-green-600' : 'text-amber-600'}>
|
|
655
|
-
{variantInStock ? 'In Stock' : 'Out of Stock'}
|
|
656
|
-
</span>
|
|
657
|
-
</p>
|
|
658
|
-
<p>
|
|
659
|
-
<span className="font-medium text-slate-700">Last updated:</span>{' '}
|
|
660
|
-
{lastUpdatedLabel}
|
|
661
|
-
</p>
|
|
662
|
-
<p>
|
|
663
|
-
<span className="font-medium text-slate-700">Ships from:</span> Local
|
|
664
|
-
pharmacy distribution center
|
|
665
|
-
</p>
|
|
666
|
-
</div>
|
|
705
|
+
</aside>
|
|
667
706
|
</div>
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
{/* Product Details Tabs */}
|
|
710
|
+
<div className="mb-16">
|
|
711
|
+
<div className="flex items-center gap-2 mb-8 border-b-2 border-gray-100">
|
|
712
|
+
{(['description', 'reviews'] as const).map((tab) => (
|
|
713
|
+
<button
|
|
714
|
+
key={tab}
|
|
715
|
+
onClick={() => setActiveTab(tab)}
|
|
716
|
+
className={`font-['Poppins',sans-serif] font-medium text-[14px] px-6 py-4 transition-all ${
|
|
717
|
+
activeTab === tab
|
|
718
|
+
? 'text-primary border-b-2 border-primary -mb-0.5'
|
|
719
|
+
: 'text-muted hover:text-primary'
|
|
720
|
+
}`}
|
|
721
|
+
>
|
|
722
|
+
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
|
723
|
+
</button>
|
|
724
|
+
))}
|
|
725
|
+
</div>
|
|
726
|
+
|
|
727
|
+
<div className="bg-white rounded-[24px] p-8 border-2 border-gray-100">
|
|
728
|
+
{activeTab === 'description' && (
|
|
671
729
|
<div>
|
|
672
|
-
<
|
|
673
|
-
|
|
674
|
-
|
|
730
|
+
<h3 className="font-['Poppins',sans-serif] font-semibold text-secondary mb-4">
|
|
731
|
+
Product Description
|
|
732
|
+
</h3>
|
|
733
|
+
<p className="font-['Poppins',sans-serif] text-[14px] text-muted leading-[1.8] mb-4">
|
|
734
|
+
<div dangerouslySetInnerHTML={{ __html: product.description }} />
|
|
735
|
+
|
|
675
736
|
</p>
|
|
737
|
+
|
|
738
|
+
<div className="mt-6">
|
|
739
|
+
<h4 className="font-['Poppins',sans-serif] font-semibold text-[13px] text-secondary mb-3">
|
|
740
|
+
Last updated:
|
|
741
|
+
</h4>
|
|
742
|
+
<p className="font-['Poppins',sans-serif] text-[14px] text-muted">
|
|
743
|
+
{lastUpdatedLabel}
|
|
744
|
+
</p>
|
|
745
|
+
</div>
|
|
746
|
+
|
|
747
|
+
{/* shipped from */}
|
|
748
|
+
<div className="mt-6">
|
|
749
|
+
<h4 className="font-['Poppins',sans-serif] font-semibold text-[13px] text-secondary mb-3">
|
|
750
|
+
Shipped from:
|
|
751
|
+
</h4>
|
|
752
|
+
<p className="font-['Poppins',sans-serif] text-[14px] text-muted">
|
|
753
|
+
Local pharmacy distribution center
|
|
754
|
+
</p>
|
|
755
|
+
</div>
|
|
676
756
|
</div>
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
757
|
+
)}
|
|
758
|
+
|
|
759
|
+
{activeTab === 'reviews' && (
|
|
760
|
+
<div className="text-center py-8">
|
|
761
|
+
<Star className="size-12 text-[#E67E50] mx-auto mb-4" />
|
|
762
|
+
<h3 className="font-['Poppins',sans-serif] font-semibold text-secondary mb-2">
|
|
763
|
+
{product.rating} out of 5 stars
|
|
764
|
+
</h3>
|
|
765
|
+
<p className="font-['Poppins',sans-serif] text-[14px] text-muted">
|
|
766
|
+
Based on {product.reviewCount} customer reviews
|
|
767
|
+
</p>
|
|
768
|
+
</div>
|
|
769
|
+
)}
|
|
770
|
+
</div>
|
|
771
|
+
</div>
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
{/* Related Products */}
|
|
775
|
+
{relatedProducts.length > 0 && (
|
|
776
|
+
<div>
|
|
777
|
+
<h2 className="font-['Poppins',sans-serif] font-semibold text-secondary mb-8 text-2xl">
|
|
778
|
+
You May Also Like
|
|
779
|
+
</h2>
|
|
780
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
781
|
+
{relatedProducts.map(relatedProduct => (
|
|
782
|
+
|
|
685
783
|
<ProductCard
|
|
686
|
-
key={relatedProduct.id
|
|
784
|
+
key={relatedProduct.id}
|
|
687
785
|
product={relatedProduct}
|
|
786
|
+
// viewMode="grid"
|
|
688
787
|
onClickProduct={(item) => {
|
|
689
788
|
router.push(buildPath(`/products/${item._id || item.id}`));
|
|
690
789
|
}}
|
|
691
790
|
/>
|
|
692
791
|
))}
|
|
693
792
|
</div>
|
|
694
|
-
</
|
|
793
|
+
</div>
|
|
695
794
|
)}
|
|
795
|
+
|
|
696
796
|
</div>
|
|
697
797
|
</div>
|
|
698
|
-
</
|
|
798
|
+
</section>
|
|
699
799
|
</div>
|
|
700
800
|
);
|
|
701
801
|
}
|