hey-pharmacist-ecommerce 1.0.4 → 1.0.6

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 (74) hide show
  1. package/README.md +107 -1
  2. package/dist/index.d.mts +3636 -316
  3. package/dist/index.d.ts +3636 -316
  4. package/dist/index.js +6802 -3865
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +6756 -3817
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +17 -14
  9. package/src/components/AddressFormModal.tsx +171 -0
  10. package/src/components/CartItem.tsx +17 -12
  11. package/src/components/FilterChips.tsx +195 -0
  12. package/src/components/Header.tsx +121 -71
  13. package/src/components/OrderCard.tsx +18 -25
  14. package/src/components/ProductCard.tsx +209 -72
  15. package/src/components/ui/Button.tsx +13 -5
  16. package/src/components/ui/Card.tsx +46 -0
  17. package/src/hooks/useAddresses.ts +83 -0
  18. package/src/hooks/useOrders.ts +37 -19
  19. package/src/hooks/useProducts.ts +55 -63
  20. package/src/hooks/useWishlistProducts.ts +75 -0
  21. package/src/index.ts +3 -19
  22. package/src/lib/Apis/api.ts +1 -0
  23. package/src/lib/Apis/apis/cart-api.ts +3 -3
  24. package/src/lib/Apis/apis/inventory-api.ts +0 -108
  25. package/src/lib/Apis/apis/stores-api.ts +70 -0
  26. package/src/lib/Apis/apis/wishlist-api.ts +447 -0
  27. package/src/lib/Apis/models/cart-item-populated.ts +0 -1
  28. package/src/lib/Apis/models/create-single-variant-product-dto.ts +3 -10
  29. package/src/lib/Apis/models/create-variant-dto.ts +26 -33
  30. package/src/lib/Apis/models/extended-product-dto.ts +20 -24
  31. package/src/lib/Apis/models/index.ts +2 -1
  32. package/src/lib/Apis/models/order-time-line-dto.ts +49 -0
  33. package/src/lib/Apis/models/order.ts +3 -8
  34. package/src/lib/Apis/models/populated-order.ts +3 -8
  35. package/src/lib/Apis/models/product-variant.ts +29 -0
  36. package/src/lib/Apis/models/update-product-variant-dto.ts +16 -23
  37. package/src/lib/Apis/models/wishlist.ts +51 -0
  38. package/src/lib/Apis/wrapper.ts +18 -7
  39. package/src/lib/api-adapter/index.ts +0 -12
  40. package/src/lib/types/index.ts +16 -61
  41. package/src/lib/utils/colors.ts +7 -4
  42. package/src/lib/utils/format.ts +1 -1
  43. package/src/lib/validations/address.ts +14 -0
  44. package/src/providers/AuthProvider.tsx +61 -31
  45. package/src/providers/CartProvider.tsx +18 -28
  46. package/src/providers/EcommerceProvider.tsx +7 -0
  47. package/src/providers/FavoritesProvider.tsx +86 -0
  48. package/src/providers/ThemeProvider.tsx +16 -1
  49. package/src/providers/WishlistProvider.tsx +174 -0
  50. package/src/screens/AddressesScreen.tsx +484 -0
  51. package/src/screens/CartScreen.tsx +120 -84
  52. package/src/screens/CategoriesScreen.tsx +120 -0
  53. package/src/screens/CheckoutScreen.tsx +919 -241
  54. package/src/screens/CurrentOrdersScreen.tsx +125 -61
  55. package/src/screens/HomeScreen.tsx +209 -0
  56. package/src/screens/LoginScreen.tsx +133 -88
  57. package/src/screens/NewAddressScreen.tsx +187 -0
  58. package/src/screens/OrdersScreen.tsx +162 -50
  59. package/src/screens/ProductDetailScreen.tsx +641 -190
  60. package/src/screens/ProfileScreen.tsx +192 -116
  61. package/src/screens/RegisterScreen.tsx +193 -144
  62. package/src/screens/SearchResultsScreen.tsx +165 -0
  63. package/src/screens/ShopScreen.tsx +1110 -146
  64. package/src/screens/WishlistScreen.tsx +428 -0
  65. package/src/lib/Apis/models/inventory-paginated-response.ts +0 -75
  66. package/src/lib/api/auth.ts +0 -81
  67. package/src/lib/api/cart.ts +0 -42
  68. package/src/lib/api/orders.ts +0 -53
  69. package/src/lib/api/products.ts +0 -51
  70. package/src/lib/api-adapter/auth-adapter.ts +0 -196
  71. package/src/lib/api-adapter/cart-adapter.ts +0 -193
  72. package/src/lib/api-adapter/mappers.ts +0 -147
  73. package/src/lib/api-adapter/orders-adapter.ts +0 -195
  74. package/src/lib/api-adapter/products-adapter.ts +0 -194
@@ -1,255 +1,706 @@
1
1
  'use client';
2
2
 
3
- import React, { useState } from 'react';
3
+ import React, { useEffect, useState, useMemo } from 'react';
4
4
  import { motion } from 'framer-motion';
5
- import { ShoppingCart, Heart, Minus, Plus, Star, Check, Truck, Shield } from 'lucide-react';
5
+ import {
6
+ ArrowLeft,
7
+ Award,
8
+ Check,
9
+ ChevronRight,
10
+ Heart,
11
+ Minus,
12
+ Package,
13
+ Plus,
14
+ Shield,
15
+ ShieldCheck,
16
+ ShoppingCart,
17
+ Sparkles,
18
+ Star,
19
+ Truck,
20
+ } from 'lucide-react';
21
+ import Link from 'next/link';
22
+ import Image from 'next/image';
6
23
  import { Button } from '@/components/ui/Button';
7
24
  import { Badge } from '@/components/ui/Badge';
8
25
  import { ProductCard } from '@/components/ProductCard';
9
26
  import { useProduct } from '@/hooks/useProducts';
10
27
  import { useCart } from '@/providers/CartProvider';
11
- import { formatPrice } from '@/lib/utils/format';
12
- import { productsApi } from '@/lib/api/products';
13
- import Image from 'next/image';
28
+ import { formatDate, formatPrice } from '@/lib/utils/format';
14
29
  import { toast } from 'sonner';
30
+ import { useRouter } from 'next/navigation';
31
+ import { ProductVariantsApi } from '@/lib/Apis/apis/product-variants-api';
32
+ import { AXIOS_CONFIG } from '@/lib/Apis/wrapper';
33
+ import { useWishlist } from '@/providers/WishlistProvider';
34
+ import { ProductsApi, ProductVariant, ProductVariantInventoryStatusEnum } from '@/lib/Apis';
35
+
36
+ const safeFormatDate = (date?: Date | string, format: 'long' | 'short' = 'long'): string => {
37
+ if (!date) return 'N/A';
38
+ try {
39
+ const dateObj = typeof date === 'string' ? new Date(date) : date;
40
+ return formatDate(dateObj, format);
41
+ } catch (error) {
42
+ console.error('Error formatting date:', error);
43
+ return 'N/A';
44
+ }
45
+ };
15
46
 
16
47
  interface ProductDetailScreenProps {
17
48
  productId: string;
18
49
  }
19
50
 
20
51
  export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
21
- const { product, isLoading } = useProduct(productId);
52
+ // Hooks and state at the top level
53
+ const router = useRouter();
54
+ const { product: productData, isLoading } = useProduct(productId);
22
55
  const { addToCart } = useCart();
23
- const [selectedImage, setSelectedImage] = useState(0);
56
+
57
+ // State declarations
58
+ const [selectedVariant, setSelectedVariant] = useState<ProductVariant | null>(null);
24
59
  const [quantity, setQuantity] = useState(1);
25
60
  const [isAddingToCart, setIsAddingToCart] = useState(false);
26
61
  const [isFavorited, setIsFavorited] = useState(false);
27
62
  const [relatedProducts, setRelatedProducts] = useState<any[]>([]);
63
+ const [initialProductData, setInitialProductData] = useState<any>(null);
64
+ const [activeImageIndex, setActiveImageIndex] = useState(0);
28
65
 
29
- React.useEffect(() => {
30
- if (product) {
31
- productsApi.getRelatedProducts(product.id).then((res) => {
32
- if (res.success) {
33
- setRelatedProducts(res.data);
34
- }
35
- });
66
+
67
+ useEffect(() => {
68
+ setActiveImageIndex(0);
69
+ }, [selectedVariant]);
70
+
71
+ const product = useMemo(() => {
72
+ if (initialProductData && !productData) {
73
+ return initialProductData;
74
+ }
75
+
76
+ if (!productData) return null;
77
+
78
+ if (productData.productVariants?.length && selectedVariant) {
79
+ return {
80
+ ...productData,
81
+ price: selectedVariant.finalPrice,
82
+ inStock: selectedVariant.isActive,
83
+ sku: selectedVariant.sku || productData.sku,
84
+ variantId: selectedVariant.id,
85
+ variantName: selectedVariant.name
86
+ };
87
+ }
88
+
89
+ return productData;
90
+ }, [productData, selectedVariant, initialProductData]);
91
+
92
+ const getVariantImages = () => {
93
+ if (selectedVariant?.productMedia?.length) {
94
+ return selectedVariant.productMedia.map((media: any) => ({
95
+ src: media.file,
96
+ width: 800,
97
+ height: 1000,
98
+ alt: product?.name || 'Product image',
99
+ ...media,
100
+ url: media.file,
101
+ }));
102
+ }
103
+
104
+ if (product?.images?.length) {
105
+ return product.images.map((image: string) => ({
106
+ src: image,
107
+ width: 800,
108
+ height: 1000,
109
+ alt: product?.name || 'Product image',
110
+ url: image,
111
+ file: image,
112
+ type: 'image' as const
113
+ }));
114
+ }
115
+
116
+ return [];
117
+ };
118
+
119
+ const currentVariant = selectedVariant || product?.productVariants?.[0] || product;
120
+ const variantImages = getVariantImages();
121
+
122
+ // Use only the selected variant's prices for discount calculation
123
+ const variantPrice = selectedVariant?.finalPrice || currentVariant?.finalPrice || product?.price || 0;
124
+ const variantComparePrice = selectedVariant?.retailPrice ||
125
+ (currentVariant && 'retailPrice' in currentVariant ? currentVariant.retailPrice : null) ||
126
+ product?.compareAtPrice;
127
+
128
+ // Only show discount if we have both prices and the compare price is higher
129
+ const discount = variantComparePrice && variantPrice && variantComparePrice > variantPrice
130
+ ? Math.round(((variantComparePrice - variantPrice) / variantComparePrice) * 100)
131
+ : 0;
132
+ const lastUpdatedLabel = safeFormatDate(
133
+ (currentVariant && 'updatedAt' in currentVariant && currentVariant.updatedAt)
134
+ ? currentVariant.updatedAt
135
+ : product?.updatedAt
136
+ );
137
+ const variantSku = currentVariant?.sku || product?.sku || 'N/A';
138
+ // const variantInStock = currentVariant && 'inStock' in currentVariant
139
+ // ? currentVariant.inStock
140
+ // : product?.inStock;
141
+
142
+ const variantInStock = currentVariant?.inventoryCount > 0;
143
+
144
+
145
+ useEffect(() => {
146
+ if (typeof window === 'undefined') return;
147
+
148
+ const searchParams = new URLSearchParams(window.location.search);
149
+ const productParam = searchParams.get('product');
150
+
151
+ if (productParam) {
152
+ try {
153
+ const decodedProduct = JSON.parse(decodeURIComponent(productParam));
154
+ setInitialProductData(decodedProduct);
155
+ } catch (error) {
156
+ console.error('Failed to parse product data from URL', error);
157
+ }
158
+ }
159
+ }, []);
160
+
161
+
162
+
163
+ useEffect(() => {
164
+ if (product?.productVariants?.length > 0) {
165
+ const newVariant = product?.productVariants?.[0];
166
+ setSelectedVariant(newVariant);
36
167
  }
37
- }, [product]);
168
+ }, [product?.productVariants]);
169
+
170
+ useEffect(() => {
171
+ if (!product?.id) return;
172
+
173
+ let isMounted = true;
174
+ const fetchRelated = async () => {
175
+ try {
176
+ const response = await new ProductsApi(AXIOS_CONFIG).getRelatedProducts(product.id, {
177
+ params: {
178
+ limit: 4
179
+ }
180
+ });
181
+ setRelatedProducts(response.data || []);
182
+ } catch (error: any) {
183
+ console.error('Failed to fetch related products', error);
184
+ }
185
+ };
186
+ fetchRelated();
187
+ return () => {
188
+ isMounted = false;
189
+ };
190
+ }, [product?.id]);
191
+
192
+
193
+ const handleVariantSelect = async (variant: ProductVariant) => {
194
+ setSelectedVariant(variant);
195
+ };
38
196
 
39
197
  const handleAddToCart = async () => {
40
- if (!product) return;
198
+ if (!product || !selectedVariant) return;
199
+
41
200
  setIsAddingToCart(true);
42
201
  try {
43
- await addToCart(product.id, quantity);
202
+ await addToCart(
203
+ product.id,
204
+ quantity,
205
+ selectedVariant.id
206
+ );
207
+ toast.success('Added to cart', {
208
+ description: `${quantity} × ${product.name}${selectedVariant.name ? ` (${selectedVariant.name})` : ''}`,
209
+ });
210
+ } catch (error) {
211
+ console.error('Failed to add to cart', error);
212
+ toast.error('Could not add to cart. Please try again.');
44
213
  } finally {
45
214
  setIsAddingToCart(false);
46
215
  }
47
216
  };
48
217
 
218
+ const { addToWishlist, removeFromWishlist, isInWishlist } = useWishlist();
219
+
220
+ // Initialize isFavorited based on whether the product is in the wishlist
221
+ useEffect(() => {
222
+ if (product) {
223
+ setIsFavorited(isInWishlist(product.id));
224
+ }
225
+ }, [product, isInWishlist]);
226
+
227
+ const handleToggleFavorite = async () => {
228
+ if (!product) return;
229
+
230
+ try {
231
+ if (isFavorited) {
232
+ await removeFromWishlist(product.id);
233
+ toast.info('Removed from saved items');
234
+ } else {
235
+ await addToWishlist(product);
236
+ toast.success('Saved to your favorites');
237
+ }
238
+ // Update the local state after successful API call
239
+ setIsFavorited(!isFavorited);
240
+ } catch (error) {
241
+ console.error('Error updating wishlist:', error);
242
+ toast.error('Failed to update wishlist');
243
+ }
244
+ };
245
+
49
246
  if (isLoading) {
50
- return <div className="container mx-auto px-4 py-12">Loading...</div>;
247
+ return (
248
+ <div className="min-h-screen bg-slate-50">
249
+ <div className="container mx-auto px-4 py-16">
250
+ <div className="grid gap-10 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
251
+ <div className="space-y-6">
252
+ <div className="h-[520px] animate-pulse rounded-3xl bg-slate-200" />
253
+ <div className="grid grid-cols-3 gap-4">
254
+ {Array.from({ length: 3 }).map((_, index) => (
255
+ <div key={index} className="h-32 animate-pulse rounded-2xl bg-slate-200" />
256
+ ))}
257
+ </div>
258
+ </div>
259
+ <div className="space-y-4 rounded-3xl bg-white p-6 shadow-sm">
260
+ <div className="h-8 w-32 animate-pulse rounded-full bg-slate-200" />
261
+ <div className="h-10 w-48 animate-pulse rounded-full bg-slate-200" />
262
+ <div className="h-6 w-full animate-pulse rounded-full bg-slate-200" />
263
+ <div className="h-12 w-full animate-pulse rounded-2xl bg-slate-200" />
264
+ <div className="h-12 w-full animate-pulse rounded-2xl bg-slate-200" />
265
+ </div>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ );
51
270
  }
52
271
 
53
272
  if (!product) {
54
- return <div className="container mx-auto px-4 py-12">Product not found</div>;
273
+ return (
274
+ <div className="min-h-screen bg-slate-50">
275
+ <div className="container mx-auto px-4 py-16">
276
+ <div className="rounded-3xl bg-white p-10 text-center shadow-sm">
277
+ <Sparkles className="mx-auto h-10 w-10 text-primary-500" />
278
+ <h1 className="mt-6 text-2xl font-semibold text-gray-900">Product not found</h1>
279
+ <p className="mt-2 text-gray-600">
280
+ It may have been removed or is temporarily unavailable. Discover other pharmacy
281
+ essentials in our catalogue.
282
+ </p>
283
+ <div className="mt-6">
284
+ <Link href="/shop" className="inline-block">
285
+ <Button>Browse products</Button>
286
+ </Link>
287
+ </div>
288
+ </div>
289
+ </div>
290
+ </div>
291
+ );
55
292
  }
56
293
 
57
- const discount = product.compareAtPrice
58
- ? Math.round(((product.compareAtPrice - product.price) / product.compareAtPrice) * 100)
59
- : 0;
294
+ const benefitPills =
295
+ product.tags && product.tags.length > 0
296
+ ? product.tags.slice(0, 6)
297
+ : ['Pharmacist approved', 'Gentle on daily routines', 'Backed by real customers'];
298
+
299
+ const highlightCards = [
300
+ {
301
+ icon: ShieldCheck,
302
+ title: 'Pharmacy grade assurance',
303
+ description: 'Sourced from trusted suppliers and reviewed by licensed professionals.',
304
+ },
305
+ {
306
+ icon: Truck,
307
+ title: 'Fast, cold-chain ready shipping',
308
+ description: 'Carefully packed and dispatched within 24 hours on business days.',
309
+ },
310
+ {
311
+ icon: Award,
312
+ title: 'Loved by patients',
313
+ description: 'Average rating 4.8/5 with over 120 verified customer experiences.',
314
+ },
315
+ ];
60
316
 
61
- return (
62
- <div className="min-h-screen bg-gray-50 py-12">
63
- <div className="container mx-auto px-4">
64
- {/* Breadcrumbs */}
65
- <div className="flex items-center gap-2 text-sm text-gray-600 mb-8">
66
- <a href="/" className="hover:text-primary-600">Home</a>
67
- <span>/</span>
68
- <a href="/shop" className="hover:text-primary-600">Shop</a>
69
- <span>/</span>
70
- <span className="text-gray-900">{product.name}</span>
71
- </div>
72
317
 
73
- {/* Product Content */}
74
- <div className="grid lg:grid-cols-2 gap-12 mb-16">
75
- {/* Images */}
76
- <div>
318
+
319
+ return (
320
+ <div className="min-h-screen bg-slate-50">
321
+ <section className="relative overflow-hidden bg-gradient-to-br from-[rgb(var(--header-from))] via-[rgb(var(--header-via))] to-[rgb(var(--header-to))] text-white mb-8">
322
+ <div
323
+ className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(255,255,255,0.35),_transparent_60%)]"
324
+ aria-hidden="true"
325
+ />
326
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_right,_rgba(255,255,255,0.25),_transparent_55%)] opacity-70" />
327
+ <div className="relative container mx-auto px-4 py-16">
328
+ <div className="flex flex-col gap-6">
329
+ <div className="flex items-center justify-between">
330
+ <Button
331
+ variant="ghost"
332
+ className="text-white hover:bg-white/10"
333
+ onClick={() => router.push('/shop')}
334
+ >
335
+ <ArrowLeft className="h-5 w-5" />
336
+ Continue shopping
337
+ </Button>
338
+ <div className="hidden items-center gap-3 text-sm text-white/80 md:flex">
339
+ <Link href="/" className="transition hover:text-white">
340
+ Home
341
+ </Link>
342
+ <ChevronRight className="h-4 w-4" />
343
+ <Link href="/shop" className="transition hover:text-white">
344
+ Shop
345
+ </Link>
346
+ <ChevronRight className="h-4 w-4" />
347
+ <span className="truncate font-medium text-white">{product.name}</span>
348
+ </div>
349
+ </div>
77
350
  <motion.div
78
- initial={{ opacity: 0, y: 20 }}
351
+ initial={{ opacity: 0, y: 24 }}
79
352
  animate={{ opacity: 1, y: 0 }}
80
- className="bg-white rounded-2xl overflow-hidden shadow-sm mb-4"
353
+ className="max-w-3xl space-y-4"
81
354
  >
82
- <div className="relative aspect-square">
83
- <Image
84
- src={product.images[selectedImage] || '/placeholder-product.jpg'}
85
- alt={product.name}
86
- fill
87
- className="object-cover"
88
- />
89
- {discount > 0 && (
90
- <Badge variant="danger" className="absolute top-4 left-4">
91
- -{discount}%
92
- </Badge>
93
- )}
355
+ {product.category && (
356
+ <Badge
357
+ variant="secondary"
358
+ className="bg-white/15 text-white backdrop-blur-sm"
359
+ >
360
+ {product.category}
361
+ </Badge>
362
+ )}
363
+ <h1 className="text-4xl font-bold leading-tight md:text-5xl">{product.name}</h1>
364
+ <div className="flex flex-wrap items-center gap-3 text-sm text-white/80">
365
+ <span className="inline-flex items-center gap-2 rounded-full bg-white/10 px-3 py-1 font-medium text-white">
366
+ <Sparkles className="h-4 w-4" />
367
+ Ready to ship today
368
+ </span>
369
+ <span className="inline-flex items-center gap-2 rounded-full bg-white/10 px-3 py-1 font-medium text-white">
370
+ <Shield className="h-4 w-4" />
371
+ 30-day happiness guarantee
372
+ </span>
94
373
  </div>
95
374
  </motion.div>
96
-
97
- {/* Thumbnail Images */}
98
- {product.images.length > 1 && (
99
- <div className="grid grid-cols-4 gap-4">
100
- {product.images.map((image, index) => (
101
- <button
102
- key={index}
103
- onClick={() => setSelectedImage(index)}
104
- className={`relative aspect-square rounded-lg overflow-hidden border-2 transition-all ${
105
- selectedImage === index
106
- ? 'border-primary-600 ring-2 ring-primary-600/20'
107
- : 'border-gray-200 hover:border-gray-300'
108
- }`}
109
- >
110
- <Image src={image} alt="" fill className="object-cover" />
111
- </button>
112
- ))}
113
- </div>
114
- )}
115
375
  </div>
376
+ </div>
377
+ </section>
116
378
 
117
- {/* Product Info */}
118
- <div>
119
- <motion.div
120
- initial={{ opacity: 0, y: 20 }}
121
- animate={{ opacity: 1, y: 0 }}
122
- transition={{ delay: 0.1 }}
123
- >
124
- {/* Category */}
125
- <p className="text-sm text-gray-500 uppercase tracking-wider mb-2">
126
- {product.category}
127
- </p>
128
-
129
- {/* Title */}
130
- <h1 className="text-4xl font-bold text-gray-900 mb-4">{product.name}</h1>
131
-
132
- {/* Rating */}
133
- <div className="flex items-center gap-2 mb-6">
134
- <div className="flex items-center">
135
- {[...Array(5)].map((_, i) => (
136
- <Star
137
- key={i}
138
- className="w-5 h-5 fill-yellow-400 text-yellow-400"
379
+ <div className="relative -mt-16 pb-20">
380
+ <div className="container mx-auto px-4">
381
+ <div className="grid gap-10 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
382
+ <div className="space-y-10">
383
+ <section className="rounded-3xl border border-white bg-white/70 p-6 shadow-xl shadow-primary-100/40 backdrop-blur">
384
+ <div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_220px]">
385
+ <motion.div
386
+ key={variantImages[activeImageIndex]}
387
+ initial={{ opacity: 0.4, scale: 0.98 }}
388
+ animate={{ opacity: 1, scale: 1 }}
389
+ transition={{ duration: 0.35 }}
390
+ className="relative overflow-hidden rounded-3xl bg-slate-100 h-[420px] md:h-[560px]"
391
+ >
392
+ <Image
393
+ src={variantImages[activeImageIndex]}
394
+ alt={currentVariant.name || product.name}
395
+ fill
396
+ priority
397
+ sizes="(max-width: 1024px) 100vw, 800px"
398
+ className="object-contain"
139
399
  />
140
- ))}
141
- </div>
142
- <span className="text-gray-600">(4.8) • 127 reviews</span>
143
- </div>
400
+ {discount > 0 && (
401
+ <Badge
402
+ variant="danger"
403
+ className="absolute left-6 top-6 shadow-lg shadow-red-500/20"
404
+ >
405
+ Save {discount}%
406
+ </Badge>
407
+ )}
408
+ {!variantInStock && (
409
+ <Badge
410
+ variant="secondary"
411
+ className="absolute right-6 top-6 bg-white/90 text-slate-700 shadow-lg border-slate-200"
412
+ >
413
+ Out of Stock
414
+ </Badge>
415
+ )}
416
+ </motion.div>
144
417
 
145
- {/* Price */}
146
- <div className="flex items-baseline gap-3 mb-6">
147
- <span className="text-4xl font-bold text-gray-900">
148
- {formatPrice(product.price)}
149
- </span>
150
- {product.compareAtPrice && (
151
- <span className="text-xl text-gray-500 line-through">
152
- {formatPrice(product.compareAtPrice)}
153
- </span>
154
- )}
155
- </div>
418
+ <div className="flex flex-col gap-4">
419
+ {product?.productVariants?.length > 0 && (
420
+ <div className="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm">
421
+ <p className="mb-2 text-sm font-semibold uppercase tracking-[0.25em] text-slate-400">
422
+ Variant
423
+ </p>
424
+ <div className="flex flex-wrap gap-2">
425
+ {product?.productVariants?.map((variant: ProductVariant, index: number) => (
426
+ <button
427
+ key={variant.id}
428
+ type="button"
429
+ onClick={() => handleVariantSelect(variant)}
430
+ className={`rounded-full px-4 py-2 text-sm font-medium transition ${selectedVariant?.id === variant.id
431
+ ? 'bg-primary-600 text-white'
432
+ : 'bg-slate-100 text-slate-700 hover:bg-slate-200'
433
+ }`}
434
+ >
435
+ {variant.name}
436
+ </button>
437
+ ))}
438
+ </div>
439
+ </div>
440
+ )}
156
441
 
157
- {/* Stock Status */}
158
- <div className="mb-6">
159
- {product.inStock ? (
160
- <div className="flex items-center gap-2 text-green-600">
161
- <Check className="w-5 h-5" />
162
- <span className="font-medium">In Stock</span>
163
- {product.stock && <span className="text-gray-500">({product.stock} available)</span>}
442
+ <div className="grid grid-cols-3 gap-3">
443
+ {variantImages.map((image: { src: string; alt?: string }, index: number) => (
444
+ <button
445
+ key={image.src + index}
446
+ type="button"
447
+ onClick={() => setActiveImageIndex(index)}
448
+ className={`relative aspect-square overflow-hidden rounded-2xl border transition ${activeImageIndex === index
449
+ ? 'border-primary-500 shadow-lg shadow-primary-200/60'
450
+ : 'border-transparent hover:border-primary-200'
451
+ }`}
452
+ >
453
+ <Image
454
+ src={image.src}
455
+ alt={image.alt || `Product image ${index + 1}`}
456
+ className="h-full w-full object-cover object-center"
457
+ width={200}
458
+ height={200}
459
+ unoptimized={true}
460
+ />
461
+ </button>
462
+ ))}
463
+ </div>
164
464
  </div>
165
- ) : (
166
- <div className="text-red-600 font-medium">Out of Stock</div>
167
- )}
168
- </div>
169
-
170
- {/* Description */}
171
- <div className="mb-8">
172
- <p className="text-gray-700 leading-relaxed">{product.description}</p>
173
- </div>
465
+ </div>
466
+ </section>
174
467
 
175
- {/* Quantity Selector */}
176
- {product.inStock && (
177
- <div className="mb-6">
178
- <label className="block text-sm font-medium text-gray-700 mb-2">
179
- Quantity
180
- </label>
181
- <div className="flex items-center border-2 border-gray-200 rounded-lg w-fit">
182
- <button
183
- onClick={() => setQuantity(Math.max(1, quantity - 1))}
184
- className="p-3 hover:bg-gray-100 transition-colors"
468
+ <section className="grid gap-6 lg:grid-cols-3">
469
+ {highlightCards.map((card) => {
470
+ const Icon = card.icon;
471
+ return (
472
+ <div
473
+ key={card.title}
474
+ className="rounded-3xl border border-slate-100 bg-white p-6 shadow-sm transition hover:-translate-y-1 hover:shadow-lg"
185
475
  >
186
- <Minus className="w-5 h-5" />
187
- </button>
188
- <span className="px-6 font-medium min-w-[4rem] text-center">
189
- {quantity}
476
+ <div className="flex items-center gap-3">
477
+ <span className="rounded-2xl bg-primary-50 p-3 text-primary-600">
478
+ <Icon className="h-5 w-5" />
479
+ </span>
480
+ <h3 className="text-base font-semibold text-slate-900">{card.title}</h3>
481
+ </div>
482
+ <p className="mt-3 text-sm leading-relaxed text-slate-600">{card.description}</p>
483
+ </div>
484
+ );
485
+ })}
486
+ </section>
487
+
488
+ <section className="grid gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
489
+ <div className="rounded-3xl border border-slate-100 bg-white p-8 shadow-sm">
490
+ <div className="flex flex-wrap items-center gap-4 pb-6">
491
+ <div className="flex items-center gap-1 text-amber-500">
492
+ {Array.from({ length: 5 }).map((_, index) => (
493
+ <Star key={index} className="h-4 w-4 fill-current" />
494
+ ))}
495
+ </div>
496
+ <span className="text-sm font-medium text-slate-500">
497
+ Rated 4.8 • Patients love the results
190
498
  </span>
191
- <button
192
- onClick={() => setQuantity(quantity + 1)}
193
- className="p-3 hover:bg-gray-100 transition-colors"
194
- >
195
- <Plus className="w-5 h-5" />
196
- </button>
499
+ </div>
500
+ <div className="space-y-8">
501
+ {product.description && (
502
+ <div className="space-y-3">
503
+ <h3 className="text-lg font-semibold text-slate-900">Description</h3>
504
+ <p className="text-base leading-relaxed text-slate-600" dangerouslySetInnerHTML={{ __html: product.description }} />
505
+ </div>
506
+ )}
197
507
  </div>
198
508
  </div>
199
- )}
509
+ <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">
510
+ <h3 className="text-sm font-semibold uppercase tracking-[0.3em] text-primary-500">
511
+ Care tips
512
+ </h3>
513
+ <div className="mt-4 space-y-4 text-sm text-slate-600">
514
+ <p className="leading-relaxed">
515
+ Store in a cool, dry place away from direct sunlight. Check packaging for
516
+ allergen statements.
517
+ </p>
518
+ <p className="leading-relaxed">
519
+ Consult with your local pharmacist if you are combining with other treatments
520
+ or have chronic conditions.
521
+ </p>
522
+ <p className="rounded-2xl bg-white/60 p-4 leading-relaxed text-primary-700">
523
+ Questions? Our care team is on standby — reach us via chat for tailored
524
+ support before you checkout.
525
+ </p>
526
+ </div>
527
+ </div>
528
+ </section>
200
529
 
201
- {/* Actions */}
202
- <div className="flex gap-4 mb-8">
203
- <Button
204
- size="lg"
205
- onClick={handleAddToCart}
206
- isLoading={isAddingToCart}
207
- disabled={!product.inStock}
208
- className="flex-1"
209
- >
210
- <ShoppingCart className="w-5 h-5" />
211
- Add to Cart
212
- </Button>
213
- <Button
214
- size="lg"
215
- variant="outline"
216
- onClick={() => setIsFavorited(!isFavorited)}
217
- className="px-6"
218
- >
219
- <Heart
220
- className={`w-5 h-5 ${isFavorited ? 'fill-red-500 text-red-500' : ''}`}
221
- />
222
- </Button>
223
- </div>
530
+ <section className="rounded-3xl border border-slate-100 bg-white p-8 shadow-sm">
531
+ <h2 className="text-xl font-semibold text-slate-900">Product insights</h2>
532
+ <div className="mt-6 grid gap-6 md:grid-cols-2">
533
+ <div className="rounded-2xl border border-slate-200 bg-slate-50/60 p-5">
534
+ <p className="text-xs font-semibold uppercase tracking-[0.3em] text-slate-500">
535
+ Availability
536
+ </p>
537
+ <div className="mt-3 flex items-center gap-2 text-sm text-slate-600">
538
+ <Check className="h-4 w-4 text-primary-500" />
539
+ {product.inStock ? 'Available for dispatch today' : 'Currently restocking'}
540
+ </div>
541
+ {product.stock !== undefined && (
542
+ <p className="mt-2 text-xs text-slate-500">
543
+ {product.stock > 0
544
+ ? `${product.stock} units remaining in inventory`
545
+ : 'Join the waitlist to be notified first'}
546
+ </p>
547
+ )}
548
+ </div>
549
+ <div className="rounded-2xl border border-slate-200 bg-slate-50/60 p-5">
550
+ <p className="text-xs font-semibold uppercase tracking-[0.3em] text-slate-500">
551
+ Product details
552
+ </p>
553
+ <div className="mt-3 space-y-2 text-sm text-slate-600">
554
+ <p>
555
+ <span className="font-medium text-slate-700">Variant:</span> {currentVariant.name}
556
+ </p>
557
+ <p>
558
+ <span className="font-medium text-slate-700">SKU:</span> {variantSku}
559
+ </p>
560
+ <p>
561
+ <span className="font-medium text-slate-700">Status:</span>{' '}
562
+ <span className={variantInStock ? 'text-green-600' : 'text-amber-600'}>
563
+ {variantInStock ? 'In Stock' : 'Out of Stock'}
564
+ </span>
565
+ </p>
566
+ <p>
567
+ <span className="font-medium text-slate-700">Last updated:</span>{' '}
568
+ {lastUpdatedLabel}
569
+ </p>
570
+ <p>
571
+ <span className="font-medium text-slate-700">Ships from:</span> Local
572
+ pharmacy distribution center
573
+ </p>
574
+ </div>
575
+ </div>
576
+ </div>
577
+ </section>
578
+ </div>
224
579
 
225
- {/* Features */}
226
- <div className="space-y-4 border-t border-gray-200 pt-6">
227
- <div className="flex items-center gap-3 text-gray-700">
228
- <Truck className="w-5 h-5 text-primary-600" />
229
- <span>Free shipping on orders over $50</span>
580
+ <aside className="space-y-6 lg:sticky lg:top-24">
581
+ <div className="rounded-3xl border border-slate-100 bg-white p-8 shadow-lg shadow-primary-100/40">
582
+ <div className="flex items-baseline gap-3">
583
+ <p className="text-3xl font-bold text-slate-900">
584
+ {selectedVariant ? formatPrice(selectedVariant.finalPrice) : formatPrice(product.price)}
585
+ </p>
586
+ {variantComparePrice && variantComparePrice > variantPrice && (
587
+ <p className="text-base text-slate-400 line-through">
588
+ {formatPrice(variantComparePrice)}
589
+ </p>
590
+ )}
591
+ {discount > 0 && (
592
+ <Badge variant="danger" size="sm">
593
+ -{discount}%
594
+ </Badge>
595
+ )}
230
596
  </div>
231
- <div className="flex items-center gap-3 text-gray-700">
232
- <Shield className="w-5 h-5 text-primary-600" />
233
- <span>30-day money-back guarantee</span>
597
+
598
+ <div className="mt-6 space-y-4">
599
+ <div className="flex items-center gap-3 rounded-2xl bg-primary-50/80 px-4 py-3 text-sm font-medium text-primary-700">
600
+ <ShieldCheck className="h-4 w-4" />
601
+ Pharmacist verified product
602
+ </div>
603
+
604
+ <div className="flex items-center justify-between rounded-2xl border border-slate-200 px-4 py-3">
605
+ <span className="text-sm font-medium text-slate-600">Qty</span>
606
+ <div className="flex items-center rounded-full border border-slate-200 bg-slate-50">
607
+ <button
608
+ type="button"
609
+ onClick={() => setQuantity((current) => Math.max(1, current - 1))}
610
+ className="rounded-l-full p-2 hover:bg-primary-100/60"
611
+ aria-label="Decrease quantity"
612
+ >
613
+ <Minus className="h-4 w-4" />
614
+ </button>
615
+ <span className="w-12 text-center text-sm font-semibold text-slate-700">
616
+ {quantity}
617
+ </span>
618
+ <button
619
+ type="button"
620
+ onClick={() => setQuantity((current) => current + 1)}
621
+ className="rounded-r-full p-2 hover:bg-primary-100/60"
622
+ aria-label="Increase quantity"
623
+ >
624
+ <Plus className="h-4 w-4" />
625
+ </button>
626
+ </div>
627
+ </div>
234
628
  </div>
629
+
630
+ {selectedVariant && (
631
+ <div className="mt-4 text-sm">
632
+ {selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK ||
633
+ selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.LOWSTOCK ? (
634
+ <div className="text-red-600 font-medium">Out of Stock</div>
635
+ ) : <div className="text-green-600 font-medium">In Stock</div>
636
+ }
637
+ </div>
638
+ )}
639
+ <div className="mt-6 space-x-3">
640
+ <Button
641
+ size="lg"
642
+ className="w-full"
643
+ onClick={handleAddToCart}
644
+ isLoading={isAddingToCart}
645
+ disabled={!selectedVariant || selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK}
646
+ >
647
+ <ShoppingCart className="h-5 w-5" />
648
+ {!selectedVariant
649
+ ? 'Select a variant'
650
+ : selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK
651
+ ? 'Out of Stock'
652
+ : `Add to Cart`}
653
+ </Button>
654
+ <Button
655
+ size="lg"
656
+ variant="outline"
657
+ className="w-full"
658
+ onClick={handleToggleFavorite}
659
+ >
660
+ <Heart
661
+ className={`h-5 w-5 ${isFavorited ? 'fill-red-500 text-red-500' : 'text-slate-500'
662
+ }`}
663
+ />
664
+ {isFavorited ? 'Saved' : 'Save for later'}
665
+ </Button>
666
+ </div>
667
+
235
668
  </div>
236
- </motion.div>
237
- </div>
238
- </div>
239
669
 
240
- {/* Related Products */}
241
- {relatedProducts.length > 0 && (
242
- <div>
243
- <h2 className="text-3xl font-bold text-gray-900 mb-8">You May Also Like</h2>
244
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
245
- {relatedProducts.map((relatedProduct) => (
246
- <ProductCard key={relatedProduct.id} product={relatedProduct} />
247
- ))}
248
- </div>
670
+ <div className="rounded-3xl border border-primary-100 bg-primary-50/70 p-6 text-sm text-primary-700 shadow-sm">
671
+ <p className="font-semibold uppercase tracking-[0.25em]">Need advice?</p>
672
+ <p className="mt-2 leading-relaxed">
673
+ Chat with a pharmacist in real time before completing your purchase. We will help
674
+ you choose supporting supplements and answer dosing questions.
675
+ </p>
676
+ </div>
677
+ </aside>
249
678
  </div>
250
- )}
679
+
680
+ {relatedProducts.length > 0 && (
681
+ <section className="mt-20">
682
+ <div className="flex items-center justify-between">
683
+ <div>
684
+ <h2 className="text-2xl font-semibold text-slate-900">You may also like</h2>
685
+ <p className="mt-1 text-sm text-slate-500">
686
+ Hand-picked recommendations that pair nicely with this product.
687
+ </p>
688
+ </div>
689
+ <Link href="/shop" className="hidden md:inline-flex">
690
+ <Button variant="ghost" className="text-primary-600">
691
+ View all products
692
+ </Button>
693
+ </Link>
694
+ </div>
695
+ <div className="mt-6 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
696
+ {relatedProducts?.map((relatedProduct) => (
697
+ <ProductCard key={relatedProduct.id} product={relatedProduct} />
698
+ ))}
699
+ </div>
700
+ </section>
701
+ )}
702
+ </div>
251
703
  </div>
252
704
  </div>
253
705
  );
254
- }
255
-
706
+ }