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
|
@@ -33,6 +33,7 @@ import { AXIOS_CONFIG } from '@/lib/Apis/wrapper';
|
|
|
33
33
|
import { useWishlist } from '@/providers/WishlistProvider';
|
|
34
34
|
import { ProductsApi, ProductVariant, ProductVariantInventoryStatusEnum } from '@/lib/Apis';
|
|
35
35
|
import { useBasePath } from '@/providers/BasePathProvider';
|
|
36
|
+
import { Category } from '@/lib/types';
|
|
36
37
|
|
|
37
38
|
const safeFormatDate = (date?: Date | string, format: 'long' | 'short' = 'long'): string => {
|
|
38
39
|
if (!date) return 'N/A';
|
|
@@ -103,6 +104,18 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
103
104
|
}));
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
// Fallback to product media if no variant media
|
|
108
|
+
if (product?.productMedia?.length) {
|
|
109
|
+
return product.productMedia.map((media: any) => ({
|
|
110
|
+
src: media.file,
|
|
111
|
+
width: 800,
|
|
112
|
+
height: 1000,
|
|
113
|
+
alt: product?.name || 'Product image',
|
|
114
|
+
...media,
|
|
115
|
+
url: media.file,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
106
119
|
if (product?.images?.length) {
|
|
107
120
|
return product.images.map((image: string) => ({
|
|
108
121
|
src: image,
|
|
@@ -194,6 +207,8 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
194
207
|
|
|
195
208
|
const handleVariantSelect = async (variant: ProductVariant) => {
|
|
196
209
|
setSelectedVariant(variant);
|
|
210
|
+
// Reset to first image of the selected variant
|
|
211
|
+
setActiveImageIndex(0);
|
|
197
212
|
};
|
|
198
213
|
|
|
199
214
|
const handleAddToCart = async () => {
|
|
@@ -293,414 +308,392 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
293
308
|
);
|
|
294
309
|
}
|
|
295
310
|
|
|
296
|
-
const benefitPills =
|
|
297
|
-
product.tags && product.tags.length > 0
|
|
298
|
-
? product.tags.slice(0, 6)
|
|
299
|
-
: ['Pharmacist approved', 'Gentle on daily routines', 'Backed by real customers'];
|
|
300
|
-
|
|
301
|
-
const highlightCards = [
|
|
302
|
-
{
|
|
303
|
-
icon: ShieldCheck,
|
|
304
|
-
title: 'Pharmacy grade assurance',
|
|
305
|
-
description: 'Sourced from trusted suppliers and reviewed by licensed professionals.',
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
icon: Truck,
|
|
309
|
-
title: 'Fast, cold-chain ready shipping',
|
|
310
|
-
description: 'Carefully packed and dispatched within 24 hours on business days.',
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
icon: Award,
|
|
314
|
-
title: 'Loved by patients',
|
|
315
|
-
description: 'Average rating 4.8/5 with over 120 verified customer experiences.',
|
|
316
|
-
},
|
|
317
|
-
];
|
|
318
311
|
|
|
319
312
|
|
|
320
313
|
|
|
321
314
|
return (
|
|
322
|
-
<div className="
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
<div className="relative container mx-auto px-4 py-16">
|
|
330
|
-
<div className="flex flex-col gap-6">
|
|
331
|
-
<div className="flex items-center justify-between">
|
|
315
|
+
<div className="bg-white">
|
|
316
|
+
|
|
317
|
+
<div className="min-h-screen bg-white max-w-7xl mx-auto">
|
|
318
|
+
<div className="relative pb-20 pt-8">
|
|
319
|
+
<div className="container mx-auto px-4">
|
|
320
|
+
{/* Back Button */}
|
|
321
|
+
<div className="mb-6">
|
|
332
322
|
<Button
|
|
333
323
|
variant="ghost"
|
|
334
|
-
className="text-
|
|
324
|
+
className="text-slate-600 hover:text-slate-900 hover:bg-slate-100"
|
|
335
325
|
onClick={() => router.push(buildPath('/shop'))}
|
|
336
326
|
>
|
|
337
|
-
<ArrowLeft className="h-
|
|
338
|
-
|
|
327
|
+
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
328
|
+
Back to Shop
|
|
339
329
|
</Button>
|
|
340
|
-
<div className="hidden items-center gap-3 text-sm text-white/80 md:flex">
|
|
341
|
-
<Link href={buildPath('/')} className="transition hover:text-white">
|
|
342
|
-
Home
|
|
343
|
-
</Link>
|
|
344
|
-
<ChevronRight className="h-4 w-4" />
|
|
345
|
-
<Link href={buildPath('/shop')} className="transition hover:text-white">
|
|
346
|
-
Shop
|
|
347
|
-
</Link>
|
|
348
|
-
<ChevronRight className="h-4 w-4" />
|
|
349
|
-
<span className="truncate font-medium text-white">{product.name}</span>
|
|
350
|
-
</div>
|
|
351
330
|
</div>
|
|
352
|
-
<
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
331
|
+
<div className="grid gap-10 lg:grid-cols-[minmax(0,50fr)_minmax(0,50fr)]">
|
|
332
|
+
<div className="space-y-10">
|
|
333
|
+
<section className="rounded-3xl ">
|
|
334
|
+
<div className="space-y-6">
|
|
335
|
+
<motion.div
|
|
336
|
+
key={selectedVariant?.id || 'default'}
|
|
337
|
+
initial={{ opacity: 0.4, scale: 0.98 }}
|
|
338
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
339
|
+
transition={{ duration: 0.35 }}
|
|
340
|
+
className="relative overflow-hidden rounded-3xl bg-slate-100 h-[420px] md:h-[560px]"
|
|
341
|
+
>
|
|
342
|
+
{variantImages.length > 0 && variantImages[activeImageIndex] ? (
|
|
343
|
+
<Image
|
|
344
|
+
src={variantImages[activeImageIndex].src || variantImages[activeImageIndex].url || variantImages[activeImageIndex].file}
|
|
345
|
+
alt={currentVariant.name || product.name}
|
|
346
|
+
fill
|
|
347
|
+
priority
|
|
348
|
+
sizes="(max-width: 1024px) 100vw, 800px"
|
|
349
|
+
className="object-contain"
|
|
350
|
+
/>
|
|
351
|
+
) : product?.productMedia?.[0]?.file ? (
|
|
352
|
+
<Image
|
|
353
|
+
src={product.productMedia[0].file}
|
|
354
|
+
alt={product.name}
|
|
355
|
+
fill
|
|
356
|
+
priority
|
|
357
|
+
sizes="(max-width: 1024px) 100vw, 800px"
|
|
358
|
+
className="object-contain"
|
|
359
|
+
/>
|
|
360
|
+
) : null}
|
|
361
|
+
{discount > 0 && (
|
|
362
|
+
<Badge
|
|
363
|
+
variant="danger"
|
|
364
|
+
className="absolute left-6 top-6 shadow-lg shadow-red-500/20"
|
|
365
|
+
>
|
|
366
|
+
Save {discount}%
|
|
367
|
+
</Badge>
|
|
368
|
+
)}
|
|
369
|
+
{!variantInStock && (
|
|
370
|
+
<Badge
|
|
371
|
+
variant="secondary"
|
|
372
|
+
className="absolute right-6 top-6 bg-white/90 text-slate-700 shadow-lg border-slate-200"
|
|
373
|
+
>
|
|
374
|
+
Out of Stock
|
|
375
|
+
</Badge>
|
|
376
|
+
)}
|
|
377
|
+
</motion.div>
|
|
378
|
+
|
|
379
|
+
{/* Variant Photos Below Main Photo */}
|
|
380
|
+
{variantImages.length > 0 && (
|
|
381
|
+
<div className="grid grid-cols-8 gap-2">
|
|
382
|
+
{variantImages.map((image: { src: string; alt?: string }, index: number) => (
|
|
383
|
+
<button
|
|
384
|
+
key={image.src + index}
|
|
385
|
+
type="button"
|
|
386
|
+
onClick={() => setActiveImageIndex(index)}
|
|
387
|
+
className={`relative aspect-square overflow-hidden rounded-lg border-2 transition-all ${activeImageIndex === index
|
|
388
|
+
? 'border-primary-500 ring-2 ring-primary-200 ring-offset-2 shadow-md'
|
|
389
|
+
: 'border-slate-200 hover:border-primary-300'
|
|
390
|
+
}`}
|
|
391
|
+
>
|
|
392
|
+
<Image
|
|
393
|
+
src={image?.src}
|
|
394
|
+
alt={image.alt || `Product image ${index + 1}`}
|
|
395
|
+
className="h-full w-full object-cover object-center"
|
|
396
|
+
width={80}
|
|
397
|
+
height={80}
|
|
398
|
+
unoptimized={true}
|
|
399
|
+
/>
|
|
400
|
+
</button>
|
|
401
|
+
))}
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
</section>
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
375
410
|
</div>
|
|
376
|
-
</motion.div>
|
|
377
|
-
</div>
|
|
378
|
-
</div>
|
|
379
|
-
</section>
|
|
380
411
|
|
|
381
|
-
<div className="relative -mt-16 pb-20">
|
|
382
|
-
<div className="container mx-auto px-4">
|
|
383
|
-
<div className="grid gap-10 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
|
|
384
|
-
<div className="space-y-10">
|
|
385
|
-
<section className="rounded-3xl border border-white bg-white/70 p-6 shadow-xl shadow-primary-100/40 backdrop-blur">
|
|
386
|
-
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_220px]">
|
|
387
|
-
<motion.div
|
|
388
|
-
key={variantImages[activeImageIndex]}
|
|
389
|
-
initial={{ opacity: 0.4, scale: 0.98 }}
|
|
390
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
391
|
-
transition={{ duration: 0.35 }}
|
|
392
|
-
className="relative overflow-hidden rounded-3xl bg-slate-100 h-[420px] md:h-[560px]"
|
|
393
|
-
>
|
|
394
|
-
<Image
|
|
395
|
-
src={variantImages[activeImageIndex]}
|
|
396
|
-
alt={currentVariant.name || product.name}
|
|
397
|
-
fill
|
|
398
|
-
priority
|
|
399
|
-
sizes="(max-width: 1024px) 100vw, 800px"
|
|
400
|
-
className="object-contain"
|
|
401
|
-
/>
|
|
402
|
-
{discount > 0 && (
|
|
403
|
-
<Badge
|
|
404
|
-
variant="danger"
|
|
405
|
-
className="absolute left-6 top-6 shadow-lg shadow-red-500/20"
|
|
406
|
-
>
|
|
407
|
-
Save {discount}%
|
|
408
|
-
</Badge>
|
|
409
|
-
)}
|
|
410
|
-
{!variantInStock && (
|
|
411
|
-
<Badge
|
|
412
|
-
variant="secondary"
|
|
413
|
-
className="absolute right-6 top-6 bg-white/90 text-slate-700 shadow-lg border-slate-200"
|
|
414
|
-
>
|
|
415
|
-
Out of Stock
|
|
416
|
-
</Badge>
|
|
417
|
-
)}
|
|
418
|
-
</motion.div>
|
|
419
412
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
<
|
|
427
|
-
{
|
|
413
|
+
<aside className="space-y-6 lg:sticky lg:top-24">
|
|
414
|
+
<div className="">
|
|
415
|
+
{/* Category Path */}
|
|
416
|
+
{product.parentCategories && product.parentCategories.length > 0 && (
|
|
417
|
+
<div className="mb-4 text-sm text-slate-600">
|
|
418
|
+
{product.parentCategories.map((cat: Category, index: number) => (
|
|
419
|
+
<span key={cat.id || index}>
|
|
420
|
+
{cat.name}
|
|
421
|
+
{index < product.parentCategories.length - 1 && ' • '}
|
|
422
|
+
</span>
|
|
423
|
+
))}
|
|
424
|
+
{product.parentSubCategories && product.parentSubCategories.length > 0 && (
|
|
425
|
+
<>
|
|
426
|
+
{' • '}
|
|
427
|
+
{product.parentSubCategories.map((subCat: Category, index: number) => (
|
|
428
|
+
<span key={subCat.id || index}>
|
|
429
|
+
{subCat.name}
|
|
430
|
+
{index < product.parentSubCategories.length - 1 && ' • '}
|
|
431
|
+
</span>
|
|
432
|
+
))}
|
|
433
|
+
</>
|
|
434
|
+
)}
|
|
435
|
+
</div>
|
|
436
|
+
)}
|
|
437
|
+
|
|
438
|
+
{/* Product Name */}
|
|
439
|
+
<h1 className="text-2xl font-bold text-slate-900 mb-6">
|
|
440
|
+
{product.name}
|
|
441
|
+
</h1>
|
|
442
|
+
|
|
443
|
+
{/* Variant Selector with Images */}
|
|
444
|
+
{product?.productVariants && product.productVariants.length > 0 && (
|
|
445
|
+
<div className="mb-6">
|
|
446
|
+
<label className="block text-sm font-medium text-slate-700 mb-3">
|
|
447
|
+
Select Variant
|
|
448
|
+
</label>
|
|
449
|
+
<div className="space-y-3">
|
|
450
|
+
{product.productVariants.map((variant: ProductVariant) => {
|
|
451
|
+
const isSelected = selectedVariant?.id === variant.id;
|
|
452
|
+
const variantImage = variant.productMedia?.[0]?.file
|
|
453
|
+
|| product.productMedia?.[0]?.file
|
|
454
|
+
|| product.images?.[0]
|
|
455
|
+
|| '/placeholder-product.jpg';
|
|
456
|
+
return (
|
|
428
457
|
<button
|
|
429
458
|
key={variant.id}
|
|
430
459
|
type="button"
|
|
431
460
|
onClick={() => handleVariantSelect(variant)}
|
|
432
|
-
className={`
|
|
433
|
-
|
|
434
|
-
|
|
461
|
+
className={`flex w-full items-center gap-3 rounded-lg border-2 px-4 py-3 text-left transition ${isSelected
|
|
462
|
+
? 'border-primary-500 bg-primary-50'
|
|
463
|
+
: 'border-slate-200 bg-white hover:border-primary-300'
|
|
435
464
|
}`}
|
|
436
465
|
>
|
|
437
|
-
{
|
|
466
|
+
<div className={`relative h-12 w-12 flex-shrink-0 overflow-hidden rounded-full border-2 ${isSelected ? 'border-primary-500' : 'border-slate-200'
|
|
467
|
+
}`}>
|
|
468
|
+
<Image
|
|
469
|
+
src={variantImage}
|
|
470
|
+
alt={variant.name || 'Variant image'}
|
|
471
|
+
fill
|
|
472
|
+
className="object-cover"
|
|
473
|
+
sizes="48px"
|
|
474
|
+
/>
|
|
475
|
+
</div>
|
|
476
|
+
<div className="flex-1">
|
|
477
|
+
<p className={`text-sm font-medium ${isSelected ? 'text-primary-700' : 'text-slate-900'}`}>
|
|
478
|
+
{variant.name}
|
|
479
|
+
</p>
|
|
480
|
+
{variant.sku && (
|
|
481
|
+
<p className="text-xs text-slate-500 mt-0.5">SKU: {variant.sku}</p>
|
|
482
|
+
)}
|
|
483
|
+
</div>
|
|
484
|
+
{isSelected && (
|
|
485
|
+
<Check className="h-5 w-5 text-primary-600 flex-shrink-0" />
|
|
486
|
+
)}
|
|
438
487
|
</button>
|
|
439
|
-
)
|
|
440
|
-
|
|
488
|
+
);
|
|
489
|
+
})}
|
|
441
490
|
</div>
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
|
|
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>
|
|
442
508
|
)}
|
|
509
|
+
</div>
|
|
443
510
|
|
|
444
|
-
|
|
445
|
-
|
|
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
|
+
|
|
540
|
+
{/* Quantity Selector */}
|
|
541
|
+
<div className="mb-6">
|
|
542
|
+
<label className="block text-sm font-medium text-slate-700 mb-2">Quantity</label>
|
|
543
|
+
<div className="flex items-center gap-3">
|
|
544
|
+
<div className="flex items-center rounded-lg border border-slate-200 bg-white">
|
|
446
545
|
<button
|
|
447
|
-
key={image.src + index}
|
|
448
546
|
type="button"
|
|
449
|
-
onClick={() =>
|
|
450
|
-
className=
|
|
451
|
-
|
|
452
|
-
: 'border-transparent hover:border-primary-200'
|
|
453
|
-
}`}
|
|
547
|
+
onClick={() => setQuantity((current) => Math.max(1, current - 1))}
|
|
548
|
+
className="px-3 py-2 hover:bg-slate-50"
|
|
549
|
+
aria-label="Decrease quantity"
|
|
454
550
|
>
|
|
455
|
-
<
|
|
456
|
-
src={image.src}
|
|
457
|
-
alt={image.alt || `Product image ${index + 1}`}
|
|
458
|
-
className="h-full w-full object-cover object-center"
|
|
459
|
-
width={200}
|
|
460
|
-
height={200}
|
|
461
|
-
unoptimized={true}
|
|
462
|
-
/>
|
|
551
|
+
<Minus className="h-4 w-4 text-slate-600" />
|
|
463
552
|
</button>
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
</div>
|
|
467
|
-
</div>
|
|
468
|
-
</section>
|
|
469
|
-
|
|
470
|
-
<section className="grid gap-6 lg:grid-cols-3">
|
|
471
|
-
{highlightCards.map((card) => {
|
|
472
|
-
const Icon = card.icon;
|
|
473
|
-
return (
|
|
474
|
-
<div
|
|
475
|
-
key={card.title}
|
|
476
|
-
className="rounded-3xl border border-slate-100 bg-white p-6 shadow-sm transition hover:-translate-y-1 hover:shadow-lg"
|
|
477
|
-
>
|
|
478
|
-
<div className="flex items-center gap-3">
|
|
479
|
-
<span className="rounded-2xl bg-primary-50 p-3 text-primary-600">
|
|
480
|
-
<Icon className="h-5 w-5" />
|
|
553
|
+
<span className="w-12 text-center text-sm font-semibold text-slate-900">
|
|
554
|
+
{quantity}
|
|
481
555
|
</span>
|
|
482
|
-
<
|
|
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"
|
|
563
|
+
aria-label="Increase quantity"
|
|
564
|
+
>
|
|
565
|
+
<Plus className="h-4 w-4 text-slate-600" />
|
|
566
|
+
</button>
|
|
483
567
|
</div>
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
<section className="grid gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
|
|
491
|
-
<div className="rounded-3xl border border-slate-100 bg-white p-8 shadow-sm">
|
|
492
|
-
<div className="flex flex-wrap items-center gap-4 pb-6">
|
|
493
|
-
<div className="flex items-center gap-1 text-amber-500">
|
|
494
|
-
{Array.from({ length: 5 }).map((_, index) => (
|
|
495
|
-
<Star key={index} className="h-4 w-4 fill-current" />
|
|
496
|
-
))}
|
|
568
|
+
{selectedVariant && (
|
|
569
|
+
<span className="text-sm text-slate-500">
|
|
570
|
+
{selectedVariant.inventoryCount || product.inventoryCount || 0} available
|
|
571
|
+
</span>
|
|
572
|
+
)}
|
|
497
573
|
</div>
|
|
498
|
-
<span className="text-sm font-medium text-slate-500">
|
|
499
|
-
Rated 4.8 • Patients love the results
|
|
500
|
-
</span>
|
|
501
|
-
</div>
|
|
502
|
-
<div className="space-y-8">
|
|
503
|
-
{product.description && (
|
|
504
|
-
<div className="space-y-3">
|
|
505
|
-
<h3 className="text-lg font-semibold text-slate-900">Description</h3>
|
|
506
|
-
<p className="text-base leading-relaxed text-slate-600" dangerouslySetInnerHTML={{ __html: product.description }} />
|
|
507
|
-
</div>
|
|
508
|
-
)}
|
|
509
574
|
</div>
|
|
510
|
-
</div>
|
|
511
|
-
<div className="h-full rounded-3xl border border-slate-100 bg-gradient-to-br from-primary-50 via-white to-secondary-50 p-8 shadow-sm">
|
|
512
|
-
<h3 className="text-sm font-semibold uppercase tracking-[0.3em] text-primary-500">
|
|
513
|
-
Care tips
|
|
514
|
-
</h3>
|
|
515
|
-
<div className="mt-4 space-y-4 text-sm text-slate-600">
|
|
516
|
-
<p className="leading-relaxed">
|
|
517
|
-
Store in a cool, dry place away from direct sunlight. Check packaging for
|
|
518
|
-
allergen statements.
|
|
519
|
-
</p>
|
|
520
|
-
<p className="leading-relaxed">
|
|
521
|
-
Consult with your local pharmacist if you are combining with other treatments
|
|
522
|
-
or have chronic conditions.
|
|
523
|
-
</p>
|
|
524
|
-
<p className="rounded-2xl bg-white/60 p-4 leading-relaxed text-primary-700">
|
|
525
|
-
Questions? Our care team is on standby — reach us via chat for tailored
|
|
526
|
-
support before you checkout.
|
|
527
|
-
</p>
|
|
528
|
-
</div>
|
|
529
|
-
</div>
|
|
530
|
-
</section>
|
|
531
575
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
<p>
|
|
560
|
-
<span className="font-medium text-slate-700">SKU:</span> {variantSku}
|
|
561
|
-
</p>
|
|
562
|
-
<p>
|
|
563
|
-
<span className="font-medium text-slate-700">Status:</span>{' '}
|
|
564
|
-
<span className={variantInStock ? 'text-green-600' : 'text-amber-600'}>
|
|
565
|
-
{variantInStock ? 'In Stock' : 'Out of Stock'}
|
|
566
|
-
</span>
|
|
567
|
-
</p>
|
|
568
|
-
<p>
|
|
569
|
-
<span className="font-medium text-slate-700">Last updated:</span>{' '}
|
|
570
|
-
{lastUpdatedLabel}
|
|
571
|
-
</p>
|
|
572
|
-
<p>
|
|
573
|
-
<span className="font-medium text-slate-700">Ships from:</span> Local
|
|
574
|
-
pharmacy distribution center
|
|
575
|
-
</p>
|
|
576
|
-
</div>
|
|
576
|
+
{/* Action Buttons */}
|
|
577
|
+
<div className="flex flex-col gap-4">
|
|
578
|
+
<Button
|
|
579
|
+
size="md"
|
|
580
|
+
className="w-full bg-orange-500 hover:bg-orange-600 text-white py-2.5"
|
|
581
|
+
onClick={handleAddToCart}
|
|
582
|
+
isLoading={isAddingToCart}
|
|
583
|
+
disabled={!selectedVariant || selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK}
|
|
584
|
+
>
|
|
585
|
+
<ShoppingCart className="h-4 w-4" />
|
|
586
|
+
{!selectedVariant
|
|
587
|
+
? 'Select a variant'
|
|
588
|
+
: selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK
|
|
589
|
+
? 'Out of Stock'
|
|
590
|
+
: 'Add to Cart'}
|
|
591
|
+
</Button>
|
|
592
|
+
<Button
|
|
593
|
+
size="md"
|
|
594
|
+
variant="outline"
|
|
595
|
+
className="w-full py-2.5"
|
|
596
|
+
onClick={handleToggleFavorite}
|
|
597
|
+
>
|
|
598
|
+
<Heart
|
|
599
|
+
className={`h-4 w-4 mr-2 ${isFavorited ? 'fill-red-500 text-red-500' : 'text-slate-500'}`}
|
|
600
|
+
/>
|
|
601
|
+
{isFavorited ? 'Saved' : 'Save for later'}
|
|
602
|
+
</Button>
|
|
577
603
|
</div>
|
|
604
|
+
|
|
578
605
|
</div>
|
|
579
|
-
</section>
|
|
580
|
-
</div>
|
|
581
606
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
607
|
+
<div className="rounded-3xl border border-primary-100 bg-primary-50/70 p-6 text-sm text-primary-700 shadow-sm">
|
|
608
|
+
<p className="font-semibold uppercase tracking-[0.25em]">Need advice?</p>
|
|
609
|
+
<p className="mt-2 leading-relaxed">
|
|
610
|
+
Chat with a pharmacist in real time before completing your purchase. We will help
|
|
611
|
+
you choose supporting supplements and answer dosing questions.
|
|
587
612
|
</p>
|
|
588
|
-
{variantComparePrice && variantComparePrice > variantPrice && (
|
|
589
|
-
<p className="text-base text-slate-400 line-through">
|
|
590
|
-
{formatPrice(variantComparePrice)}
|
|
591
|
-
</p>
|
|
592
|
-
)}
|
|
593
|
-
{discount > 0 && (
|
|
594
|
-
<Badge variant="danger" size="sm">
|
|
595
|
-
-{discount}%
|
|
596
|
-
</Badge>
|
|
597
|
-
)}
|
|
598
613
|
</div>
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
614
|
+
</aside>
|
|
615
|
+
</div>
|
|
616
|
+
<section className="mt-10">
|
|
617
|
+
<div className="rounded-3xl border border-slate-100 bg-white p-8 shadow-sm">
|
|
618
|
+
<div className="flex flex-wrap items-center gap-4 pb-6">
|
|
619
|
+
<div className="flex items-center gap-1 text-amber-500">
|
|
620
|
+
{Array.from({ length: 5 }).map((_, index) => (
|
|
621
|
+
<Star key={index} className="h-4 w-4 fill-current" />
|
|
622
|
+
))}
|
|
604
623
|
</div>
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
>
|
|
615
|
-
<Minus className="h-4 w-4" />
|
|
616
|
-
</button>
|
|
617
|
-
<span className="w-12 text-center text-sm font-semibold text-slate-700">
|
|
618
|
-
{quantity}
|
|
619
|
-
</span>
|
|
620
|
-
<button
|
|
621
|
-
type="button"
|
|
622
|
-
onClick={() => setQuantity((current) => current + 1)}
|
|
623
|
-
className="rounded-r-full p-2 hover:bg-primary-100/60"
|
|
624
|
-
aria-label="Increase quantity"
|
|
625
|
-
>
|
|
626
|
-
<Plus className="h-4 w-4" />
|
|
627
|
-
</button>
|
|
624
|
+
<span className="text-sm font-medium text-slate-500">
|
|
625
|
+
Rated 4.8 • Patients love the results
|
|
626
|
+
</span>
|
|
627
|
+
</div>
|
|
628
|
+
<div className="space-y-8">
|
|
629
|
+
{product.description && (
|
|
630
|
+
<div className="space-y-3">
|
|
631
|
+
<h3 className="text-lg font-semibold text-slate-900">Description</h3>
|
|
632
|
+
<p className="text-base leading-relaxed text-slate-600" dangerouslySetInnerHTML={{ __html: product.description }} />
|
|
628
633
|
</div>
|
|
629
|
-
|
|
634
|
+
)}
|
|
630
635
|
</div>
|
|
636
|
+
</div>
|
|
637
|
+
</section>
|
|
631
638
|
|
|
632
|
-
{selectedVariant && (
|
|
633
|
-
<div className="mt-4 text-sm">
|
|
634
|
-
{selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK ||
|
|
635
|
-
selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.LOWSTOCK ? (
|
|
636
|
-
<div className="text-red-600 font-medium">Out of Stock</div>
|
|
637
|
-
) : <div className="text-green-600 font-medium">In Stock</div>
|
|
638
|
-
}
|
|
639
|
-
</div>
|
|
640
|
-
)}
|
|
641
|
-
<div className="mt-6 space-x-3">
|
|
642
|
-
<Button
|
|
643
|
-
size="lg"
|
|
644
|
-
className="w-full"
|
|
645
|
-
onClick={handleAddToCart}
|
|
646
|
-
isLoading={isAddingToCart}
|
|
647
|
-
disabled={!selectedVariant || selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK}
|
|
648
|
-
>
|
|
649
|
-
<ShoppingCart className="h-5 w-5" />
|
|
650
|
-
{!selectedVariant
|
|
651
|
-
? 'Select a variant'
|
|
652
|
-
: selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK
|
|
653
|
-
? 'Out of Stock'
|
|
654
|
-
: `Add to Cart`}
|
|
655
|
-
</Button>
|
|
656
|
-
<Button
|
|
657
|
-
size="lg"
|
|
658
|
-
variant="outline"
|
|
659
|
-
className="w-full"
|
|
660
|
-
onClick={handleToggleFavorite}
|
|
661
|
-
>
|
|
662
|
-
<Heart
|
|
663
|
-
className={`h-5 w-5 ${isFavorited ? 'fill-red-500 text-red-500' : 'text-slate-500'
|
|
664
|
-
}`}
|
|
665
|
-
/>
|
|
666
|
-
{isFavorited ? 'Saved' : 'Save for later'}
|
|
667
|
-
</Button>
|
|
668
|
-
</div>
|
|
669
639
|
|
|
670
|
-
</div>
|
|
671
640
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
|
677
665
|
</p>
|
|
678
666
|
</div>
|
|
679
|
-
</
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
667
|
+
</div>
|
|
668
|
+
{relatedProducts.length > 0 && (
|
|
669
|
+
<section className="mt-20">
|
|
670
|
+
<div className="flex items-center justify-between">
|
|
671
|
+
<div>
|
|
672
|
+
<h2 className="text-2xl font-semibold text-slate-900">You may also like</h2>
|
|
673
|
+
<p className="mt-1 text-sm text-slate-500">
|
|
674
|
+
Hand-picked recommendations that pair nicely with this product.
|
|
675
|
+
</p>
|
|
676
|
+
</div>
|
|
677
|
+
<Link href={buildPath('/shop')} className="hidden md:inline-flex">
|
|
678
|
+
<Button variant="ghost" className="text-primary-600">
|
|
679
|
+
View all products
|
|
680
|
+
</Button>
|
|
681
|
+
</Link>
|
|
690
682
|
</div>
|
|
691
|
-
<
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
683
|
+
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
|
684
|
+
{relatedProducts?.map((relatedProduct) => (
|
|
685
|
+
<ProductCard
|
|
686
|
+
key={relatedProduct.id || relatedProduct._id}
|
|
687
|
+
product={relatedProduct}
|
|
688
|
+
onClickProduct={(item) => {
|
|
689
|
+
router.push(buildPath(`/products/${item._id || item.id}`));
|
|
690
|
+
}}
|
|
691
|
+
/>
|
|
692
|
+
))}
|
|
693
|
+
</div>
|
|
694
|
+
</section>
|
|
695
|
+
)}
|
|
696
|
+
</div>
|
|
704
697
|
</div>
|
|
705
698
|
</div>
|
|
706
699
|
</div>
|