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
|
@@ -10,6 +10,7 @@ import { AnimatePresence, motion } from 'framer-motion';
|
|
|
10
10
|
import {
|
|
11
11
|
ArrowUpDown,
|
|
12
12
|
ChevronDown,
|
|
13
|
+
ChevronUp,
|
|
13
14
|
Clock,
|
|
14
15
|
LayoutGrid,
|
|
15
16
|
LayoutList,
|
|
@@ -20,12 +21,21 @@ import {
|
|
|
20
21
|
Sparkles,
|
|
21
22
|
TrendingUp,
|
|
22
23
|
X,
|
|
24
|
+
ShoppingBag,
|
|
25
|
+
Shirt,
|
|
26
|
+
Pill,
|
|
27
|
+
Box,
|
|
28
|
+
Heart,
|
|
29
|
+
Star,
|
|
30
|
+
Check,
|
|
31
|
+
Tag,
|
|
32
|
+
DollarSign,
|
|
23
33
|
} from 'lucide-react';
|
|
24
34
|
import Image from 'next/image';
|
|
25
35
|
import { useRouter } from 'next/navigation';
|
|
26
36
|
import { useBasePath } from '@/providers/BasePathProvider';
|
|
27
37
|
import { ProductCard } from '@/components/ProductCard';
|
|
28
|
-
import { ProductCardSkeleton } from '@/components/ui/Skeleton';
|
|
38
|
+
import { ProductCardSkeleton, Skeleton } from '@/components/ui/Skeleton';
|
|
29
39
|
import { EmptyState } from '@/components/EmptyState';
|
|
30
40
|
import { Button } from '@/components/ui/Button';
|
|
31
41
|
import { Input } from '@/components/ui/Input';
|
|
@@ -57,9 +67,12 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
57
67
|
max: '',
|
|
58
68
|
});
|
|
59
69
|
const [expandedCategories, setExpandedCategories] = useState<Record<string, boolean>>({});
|
|
70
|
+
const [expandedFilterSections, setExpandedFilterSections] = useState<Record<string, boolean>>({
|
|
71
|
+
category: true, // Category section starts expanded
|
|
72
|
+
});
|
|
60
73
|
|
|
61
74
|
const { products, isLoading, pagination } = useProducts(filters, page, 20);
|
|
62
|
-
const { categories } = useCategories();
|
|
75
|
+
const { categories, isLoading: isLoadingCategories } = useCategories();
|
|
63
76
|
|
|
64
77
|
const handleSearch = (e: React.FormEvent) => {
|
|
65
78
|
e.preventDefault();
|
|
@@ -205,8 +218,11 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
205
218
|
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
206
219
|
items = items.filter((p) => new Date(p.createdAt).getTime() >= thirtyDaysAgo);
|
|
207
220
|
}
|
|
221
|
+
if (filters.brand) {
|
|
222
|
+
items = items.filter((p) => p.brand && p.brand.trim().toLowerCase() === filters.brand!.toLowerCase());
|
|
223
|
+
}
|
|
208
224
|
return items;
|
|
209
|
-
}, [isLoading, products, filters.tags, filters.newArrivals]);
|
|
225
|
+
}, [isLoading, products, filters.tags, filters.newArrivals, filters.brand]);
|
|
210
226
|
|
|
211
227
|
const sortedProducts = useMemo(() => {
|
|
212
228
|
if (isLoading) {
|
|
@@ -231,6 +247,17 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
231
247
|
|
|
232
248
|
const displayedProducts = sortedProducts;
|
|
233
249
|
|
|
250
|
+
// Extract unique brands from products
|
|
251
|
+
const availableBrands = useMemo(() => {
|
|
252
|
+
const brandSet = new Set<string>();
|
|
253
|
+
products.forEach((product) => {
|
|
254
|
+
if (product.brand && product.brand.trim()) {
|
|
255
|
+
brandSet.add(product.brand.trim());
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
return Array.from(brandSet).sort();
|
|
259
|
+
}, [products]);
|
|
260
|
+
|
|
234
261
|
const quickSearches = useMemo(() => {
|
|
235
262
|
const counts = new Map<string, number>();
|
|
236
263
|
products.forEach((p) => {
|
|
@@ -304,6 +331,14 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
304
331
|
setPage(1);
|
|
305
332
|
}, []);
|
|
306
333
|
|
|
334
|
+
const handleClearCategory = useCallback(() => {
|
|
335
|
+
setFilters((current) => {
|
|
336
|
+
const { category, subCategory, ...rest } = current as any;
|
|
337
|
+
return rest;
|
|
338
|
+
});
|
|
339
|
+
setPage(1);
|
|
340
|
+
}, []);
|
|
341
|
+
|
|
307
342
|
const handleClearFilters = useCallback(() => {
|
|
308
343
|
setFilters({});
|
|
309
344
|
setSearchQuery('');
|
|
@@ -373,6 +408,25 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
373
408
|
setPage(1);
|
|
374
409
|
}, []);
|
|
375
410
|
|
|
411
|
+
const handleBrandChange = useCallback((brand: string) => {
|
|
412
|
+
setFilters((current) => {
|
|
413
|
+
if (current.brand === brand) {
|
|
414
|
+
const { brand: _, ...rest } = current;
|
|
415
|
+
return rest;
|
|
416
|
+
}
|
|
417
|
+
return { ...current, brand };
|
|
418
|
+
});
|
|
419
|
+
setPage(1);
|
|
420
|
+
}, []);
|
|
421
|
+
|
|
422
|
+
const handleRemoveBrand = useCallback(() => {
|
|
423
|
+
setFilters((current) => {
|
|
424
|
+
const { brand, ...rest } = current;
|
|
425
|
+
return rest;
|
|
426
|
+
});
|
|
427
|
+
setPage(1);
|
|
428
|
+
}, []);
|
|
429
|
+
|
|
376
430
|
const handlePriceRangeSelect = useCallback(
|
|
377
431
|
(value: string) => {
|
|
378
432
|
const range = priceRanges.find((item) => item.value === value);
|
|
@@ -466,6 +520,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
466
520
|
maxPrice,
|
|
467
521
|
tags,
|
|
468
522
|
newArrivals,
|
|
523
|
+
brand: brandFilter,
|
|
469
524
|
} = filters;
|
|
470
525
|
|
|
471
526
|
const activeFilterChips = useMemo(() => {
|
|
@@ -547,6 +602,14 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
547
602
|
});
|
|
548
603
|
}
|
|
549
604
|
|
|
605
|
+
if (brandFilter) {
|
|
606
|
+
chips.push({
|
|
607
|
+
key: 'brand',
|
|
608
|
+
label: `Brand: ${brandFilter}`,
|
|
609
|
+
onRemove: handleRemoveBrand,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
550
613
|
return chips;
|
|
551
614
|
}, [
|
|
552
615
|
categories,
|
|
@@ -558,12 +621,14 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
558
621
|
handleRemovePrice,
|
|
559
622
|
handleRemoveSearch,
|
|
560
623
|
handleRemoveTag,
|
|
624
|
+
handleRemoveBrand,
|
|
561
625
|
inStockOnly,
|
|
562
626
|
maxPrice,
|
|
563
627
|
minPrice,
|
|
564
628
|
searchFilter,
|
|
565
629
|
tags,
|
|
566
630
|
newArrivals,
|
|
631
|
+
brandFilter,
|
|
567
632
|
]);
|
|
568
633
|
|
|
569
634
|
const hasActiveFilters = activeFilterChips.length > 0;
|
|
@@ -571,327 +636,443 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
571
636
|
const isCustomPriceDirty =
|
|
572
637
|
customPrice.min.trim() !== '' || customPrice.max.trim() !== '';
|
|
573
638
|
|
|
639
|
+
const toggleFilterSection = useCallback((section: string) => {
|
|
640
|
+
setExpandedFilterSections((prev) => ({
|
|
641
|
+
...prev,
|
|
642
|
+
[section]: !prev[section],
|
|
643
|
+
}));
|
|
644
|
+
}, []);
|
|
645
|
+
|
|
646
|
+
const getCategoryIconForFilter = (categoryName: string) => {
|
|
647
|
+
const name = categoryName.toLowerCase();
|
|
648
|
+
if (name.includes('scrub') || name.includes('uniform')) return Shirt;
|
|
649
|
+
if (name.includes('vitamin') || name.includes('supplement')) return Pill;
|
|
650
|
+
if (name.includes('medicine') || name.includes('medication')) return Box;
|
|
651
|
+
if (name.includes('care') || name.includes('personal')) return Heart;
|
|
652
|
+
return Package;
|
|
653
|
+
};
|
|
654
|
+
|
|
574
655
|
const renderFiltersPanel = () => (
|
|
575
656
|
<>
|
|
576
|
-
<div className="space-y-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
657
|
+
<div className="space-y-6">
|
|
658
|
+
{/* Filters Title */}
|
|
659
|
+
<h3 className="text-lg font-bold text-slate-900">Filters</h3>
|
|
660
|
+
|
|
661
|
+
{/* Search Products */}
|
|
662
|
+
<div className="space-y-2">
|
|
663
|
+
<label className="text-sm font-medium text-slate-600">Search Products</label>
|
|
664
|
+
<div className="relative">
|
|
665
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
|
|
666
|
+
<input
|
|
667
|
+
type="text"
|
|
668
|
+
placeholder="Search..."
|
|
669
|
+
value={searchQuery}
|
|
670
|
+
onChange={handleInputChange}
|
|
671
|
+
className="w-full rounded-lg border border-slate-200 bg-white pl-10 pr-4 py-2 text-sm text-slate-900 placeholder-slate-400 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20"
|
|
672
|
+
/>
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
675
|
+
|
|
676
|
+
{/* Category Section */}
|
|
677
|
+
<div className="space-y-3">
|
|
678
|
+
<button
|
|
679
|
+
type="button"
|
|
680
|
+
onClick={() => toggleFilterSection('category')}
|
|
681
|
+
className="flex w-full items-center justify-between text-sm font-medium text-slate-600"
|
|
682
|
+
>
|
|
683
|
+
<span>Category</span>
|
|
684
|
+
{expandedFilterSections.category ? (
|
|
685
|
+
<ChevronUp className="h-4 w-4" />
|
|
686
|
+
) : (
|
|
687
|
+
<ChevronDown className="h-4 w-4" />
|
|
688
|
+
)}
|
|
689
|
+
</button>
|
|
690
|
+
|
|
691
|
+
{expandedFilterSections.category && (
|
|
692
|
+
<div className="space-y-2">
|
|
693
|
+
{/* All Products Option */}
|
|
586
694
|
<button
|
|
587
695
|
type="button"
|
|
588
|
-
onClick={
|
|
589
|
-
className=
|
|
696
|
+
onClick={handleClearCategory}
|
|
697
|
+
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition ${
|
|
698
|
+
!categoryFilter
|
|
699
|
+
? 'bg-primary-600 text-white'
|
|
700
|
+
: 'bg-white text-slate-700 border border-slate-200 hover:border-primary-300'
|
|
701
|
+
}`}
|
|
590
702
|
>
|
|
591
|
-
|
|
703
|
+
<div className={`flex h-6 w-6 items-center justify-center rounded ${!categoryFilter ? 'bg-white/20' : 'bg-slate-100'}`}>
|
|
704
|
+
{!categoryFilter ? (
|
|
705
|
+
<Check className="h-4 w-4 text-white" />
|
|
706
|
+
) : (
|
|
707
|
+
<ShoppingBag className="h-4 w-4 text-slate-600" />
|
|
708
|
+
)}
|
|
709
|
+
</div>
|
|
710
|
+
<span>All Products</span>
|
|
592
711
|
</button>
|
|
593
|
-
)}
|
|
594
|
-
</div>
|
|
595
712
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
className={`
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
>
|
|
621
|
-
<span className="flex items-center gap-2">
|
|
622
|
-
<span className='font-medium'>{category.name}</span>
|
|
623
|
-
{category.productCount > 0 && (
|
|
624
|
-
<span className={`ml-1 rounded-full bg-gray-100 px-2 py-0.5 text-xs ${isCategoryActive ? 'text-primary-700' : 'text-gray-500'}`}>
|
|
625
|
-
{category.productCount}
|
|
626
|
-
</span>
|
|
627
|
-
)}
|
|
628
|
-
</span>
|
|
713
|
+
{/* Category Options */}
|
|
714
|
+
{sortedCategories.map((category) => {
|
|
715
|
+
const isCategoryActive = categoryFilter === category.id;
|
|
716
|
+
const isExpanded = !!expandedCategories[category.id as string];
|
|
717
|
+
const Icon = getCategoryIconForFilter(category.name ?? '');
|
|
718
|
+
return (
|
|
719
|
+
<div key={category.id} className="space-y-1">
|
|
720
|
+
<button
|
|
721
|
+
type="button"
|
|
722
|
+
onClick={() => {
|
|
723
|
+
if (!isExpanded) toggleCategoryExpand(category.id ?? '');
|
|
724
|
+
handleCategoryChange(category.id ?? '');
|
|
725
|
+
}}
|
|
726
|
+
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition ${
|
|
727
|
+
isCategoryActive
|
|
728
|
+
? 'bg-primary-600 text-white'
|
|
729
|
+
: 'bg-white text-slate-700 border border-slate-200 hover:border-primary-300'
|
|
730
|
+
}`}
|
|
731
|
+
>
|
|
732
|
+
<div className={`flex h-6 w-6 items-center justify-center rounded ${isCategoryActive ? 'bg-white/20' : 'bg-slate-100'}`}>
|
|
733
|
+
<Icon className={`h-4 w-4 ${isCategoryActive ? 'text-white' : 'text-slate-600'}`} />
|
|
734
|
+
</div>
|
|
735
|
+
<span className="flex-1 text-left">{category.name}</span>
|
|
736
|
+
{Array.isArray(category.categorySubCategories) && category.categorySubCategories.length > 0 && (
|
|
629
737
|
<button
|
|
630
738
|
type="button"
|
|
631
739
|
onClick={(e) => {
|
|
632
|
-
e.preventDefault();
|
|
633
740
|
e.stopPropagation();
|
|
634
741
|
toggleCategoryExpand(category.id ?? '');
|
|
635
742
|
}}
|
|
636
|
-
className="
|
|
637
|
-
aria-label={isExpanded ? 'Collapse' : 'Expand'}
|
|
743
|
+
className="p-1"
|
|
638
744
|
>
|
|
639
|
-
<ChevronDown className={`h-4 w-4 transition-transform ${isExpanded ? 'rotate-180 text-
|
|
745
|
+
<ChevronDown className={`h-4 w-4 transition-transform ${isExpanded ? 'rotate-180' : ''} ${isCategoryActive ? 'text-white' : 'text-slate-400'}`} />
|
|
640
746
|
</button>
|
|
641
|
-
</div>
|
|
642
|
-
{isExpanded && Array.isArray(category.categorySubCategories) && category.categorySubCategories.length > 0 && (
|
|
643
|
-
<div className="mt-1 border-gray-100 px-2 pb-2 pl-4">
|
|
644
|
-
<div className="divide-y divide-gray-100">
|
|
645
|
-
{category.categorySubCategories.map((sub) => {
|
|
646
|
-
const isSubActive = subCategoryFilter === sub.id;
|
|
647
|
-
return (
|
|
648
|
-
<button
|
|
649
|
-
key={sub.id}
|
|
650
|
-
type="button"
|
|
651
|
-
onClick={() => handleSubCategoryChange(category.id ?? '', sub.id)}
|
|
652
|
-
className={`block w-full px-2 py-2 text-sm text-start transition ${isSubActive
|
|
653
|
-
? 'text-primary-700'
|
|
654
|
-
: 'text-gray-600 hover:text-primary-700 '
|
|
655
|
-
}`}
|
|
656
|
-
>
|
|
657
|
-
{sub.name}
|
|
658
|
-
</button>
|
|
659
|
-
);
|
|
660
|
-
})}
|
|
661
|
-
</div>
|
|
662
|
-
</div>
|
|
663
747
|
)}
|
|
664
|
-
</
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
748
|
+
</button>
|
|
749
|
+
{isExpanded && Array.isArray(category.categorySubCategories) && category.categorySubCategories.length > 0 && (
|
|
750
|
+
<div className="ml-9 space-y-1">
|
|
751
|
+
{category.categorySubCategories.map((sub) => {
|
|
752
|
+
const isSubActive = subCategoryFilter === sub.id;
|
|
753
|
+
return (
|
|
754
|
+
<button
|
|
755
|
+
key={sub.id}
|
|
756
|
+
type="button"
|
|
757
|
+
onClick={() => handleSubCategoryChange(category.id ?? '', sub.id)}
|
|
758
|
+
className={`block w-full rounded-lg px-3 py-2 text-sm text-left transition ${
|
|
759
|
+
isSubActive
|
|
760
|
+
? 'bg-primary-50 text-primary-700 font-medium'
|
|
761
|
+
: 'text-slate-600 hover:bg-slate-50'
|
|
762
|
+
}`}
|
|
763
|
+
>
|
|
764
|
+
{sub.name}
|
|
765
|
+
</button>
|
|
766
|
+
);
|
|
767
|
+
})}
|
|
768
|
+
</div>
|
|
769
|
+
)}
|
|
770
|
+
</div>
|
|
771
|
+
);
|
|
772
|
+
})}
|
|
668
773
|
</div>
|
|
669
|
-
|
|
774
|
+
)}
|
|
670
775
|
</div>
|
|
671
776
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
Price
|
|
675
|
-
</h4>
|
|
676
|
-
<div className="flex flex-wrap gap-2">
|
|
677
|
-
{priceRanges.map((range) => {
|
|
678
|
-
const isActive = selectedPriceRange === range.value;
|
|
679
|
-
return (
|
|
680
|
-
<button
|
|
681
|
-
type="button"
|
|
682
|
-
key={range.value}
|
|
683
|
-
onClick={() => handlePriceRangeSelect(range.value)}
|
|
684
|
-
className={`rounded-full border px-3 py-1.5 text-sm font-medium transition ${isActive
|
|
685
|
-
? 'border-secondary-600 bg-secondary-600 text-white shadow-lg shadow-secondary-500/30'
|
|
686
|
-
: 'border-gray-200 bg-white text-gray-600 hover:border-secondary-300 hover:text-secondary-600'
|
|
687
|
-
}`}
|
|
688
|
-
>
|
|
689
|
-
{range.label}
|
|
690
|
-
</button>
|
|
691
|
-
);
|
|
692
|
-
})}
|
|
693
|
-
</div>
|
|
694
|
-
<div className="grid grid-cols-2 gap-3">
|
|
695
|
-
<Input
|
|
696
|
-
type="number"
|
|
697
|
-
min="0"
|
|
698
|
-
placeholder="Custom min"
|
|
699
|
-
value={customPrice.min}
|
|
700
|
-
onChange={(event) =>
|
|
701
|
-
setCustomPrice((current) => ({ ...current, min: event.target.value }))
|
|
702
|
-
}
|
|
703
|
-
/>
|
|
704
|
-
<Input
|
|
705
|
-
type="number"
|
|
706
|
-
min="0"
|
|
707
|
-
placeholder="Custom max"
|
|
708
|
-
value={customPrice.max}
|
|
709
|
-
onChange={(event) =>
|
|
710
|
-
setCustomPrice((current) => ({ ...current, max: event.target.value }))
|
|
711
|
-
}
|
|
712
|
-
/>
|
|
713
|
-
</div>
|
|
777
|
+
{/* Brand Section */}
|
|
778
|
+
<div className="space-y-3">
|
|
714
779
|
<button
|
|
715
780
|
type="button"
|
|
716
|
-
onClick={
|
|
717
|
-
|
|
718
|
-
className="w-full rounded-xl border border-primary-500 bg-primary-500/10 px-4 py-2.5 text-sm font-semibold text-primary-700 transition hover:bg-primary-500/20 disabled:cursor-not-allowed disabled:border-gray-200 disabled:text-gray-400"
|
|
781
|
+
onClick={() => toggleFilterSection('brand')}
|
|
782
|
+
className="flex w-full items-center justify-between text-sm font-medium text-slate-600"
|
|
719
783
|
>
|
|
720
|
-
|
|
784
|
+
<span>Brand</span>
|
|
785
|
+
{expandedFilterSections.brand ? (
|
|
786
|
+
<ChevronUp className="h-4 w-4" />
|
|
787
|
+
) : (
|
|
788
|
+
<ChevronDown className="h-4 w-4" />
|
|
789
|
+
)}
|
|
721
790
|
</button>
|
|
791
|
+
{expandedFilterSections.brand && (
|
|
792
|
+
<div className="space-y-1.5 max-h-64 overflow-y-auto">
|
|
793
|
+
{availableBrands.length === 0 ? (
|
|
794
|
+
<div className="text-xs text-slate-500">No brands available</div>
|
|
795
|
+
) : (
|
|
796
|
+
availableBrands.map((brand) => {
|
|
797
|
+
const isSelected = brandFilter === brand;
|
|
798
|
+
return (
|
|
799
|
+
<button
|
|
800
|
+
key={brand}
|
|
801
|
+
type="button"
|
|
802
|
+
onClick={() => handleBrandChange(brand)}
|
|
803
|
+
className={`flex w-full items-center justify-between rounded-md border px-2.5 py-1.5 text-xs font-medium transition ${
|
|
804
|
+
isSelected
|
|
805
|
+
? 'border-primary-500 bg-primary-50 text-primary-700'
|
|
806
|
+
: 'border-slate-200 bg-white text-slate-600 hover:border-primary-300'
|
|
807
|
+
}`}
|
|
808
|
+
>
|
|
809
|
+
<span className="truncate">{brand}</span>
|
|
810
|
+
{isSelected && <Check className="h-3 w-3 text-primary-600 flex-shrink-0 ml-2" />}
|
|
811
|
+
</button>
|
|
812
|
+
);
|
|
813
|
+
})
|
|
814
|
+
)}
|
|
815
|
+
</div>
|
|
816
|
+
)}
|
|
722
817
|
</div>
|
|
723
818
|
|
|
819
|
+
{/* Availability Section */}
|
|
724
820
|
<div className="space-y-3">
|
|
725
|
-
<h4 className="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500">
|
|
726
|
-
Availability
|
|
727
|
-
</h4>
|
|
728
821
|
<button
|
|
729
822
|
type="button"
|
|
730
|
-
onClick={
|
|
731
|
-
className=
|
|
732
|
-
? 'border-primary-500 bg-primary-50 text-primary-700'
|
|
733
|
-
: 'border-gray-200 bg-white text-gray-600 hover:border-primary-300 hover:text-primary-600'
|
|
734
|
-
}`}
|
|
823
|
+
onClick={() => toggleFilterSection('availability')}
|
|
824
|
+
className="flex w-full items-center justify-between text-sm font-medium text-slate-600"
|
|
735
825
|
>
|
|
736
|
-
<span>
|
|
737
|
-
|
|
826
|
+
<span>Availability</span>
|
|
827
|
+
{expandedFilterSections.availability ? (
|
|
828
|
+
<ChevronUp className="h-4 w-4" />
|
|
829
|
+
) : (
|
|
830
|
+
<ChevronDown className="h-4 w-4" />
|
|
831
|
+
)}
|
|
738
832
|
</button>
|
|
833
|
+
{expandedFilterSections.availability && (
|
|
834
|
+
<div className="space-y-2">
|
|
835
|
+
<button
|
|
836
|
+
type="button"
|
|
837
|
+
onClick={handleToggleStock}
|
|
838
|
+
className={`flex w-full items-center justify-between rounded-lg border px-3 py-2.5 text-sm font-medium transition ${
|
|
839
|
+
inStockOnly
|
|
840
|
+
? 'border-primary-500 bg-primary-50 text-primary-700'
|
|
841
|
+
: 'border-slate-200 bg-white text-slate-600 hover:border-primary-300'
|
|
842
|
+
}`}
|
|
843
|
+
>
|
|
844
|
+
<span>In stock only</span>
|
|
845
|
+
{inStockOnly && <Check className="h-4 w-4 text-primary-600" />}
|
|
846
|
+
</button>
|
|
847
|
+
</div>
|
|
848
|
+
)}
|
|
849
|
+
</div>
|
|
850
|
+
|
|
851
|
+
{/* Price Range Section */}
|
|
852
|
+
<div className="space-y-3">
|
|
739
853
|
<button
|
|
740
854
|
type="button"
|
|
741
|
-
onClick={
|
|
742
|
-
className=
|
|
743
|
-
? 'border-secondary-500 bg-secondary-50 text-secondary-700'
|
|
744
|
-
: 'border-gray-200 bg-white text-gray-600 hover:border-secondary-300 hover:text-secondary-600'
|
|
745
|
-
}`}
|
|
855
|
+
onClick={() => toggleFilterSection('price')}
|
|
856
|
+
className="flex w-full items-center justify-between text-sm font-medium text-slate-600"
|
|
746
857
|
>
|
|
747
|
-
<span>
|
|
748
|
-
|
|
858
|
+
<span>Price Range</span>
|
|
859
|
+
{expandedFilterSections.price ? (
|
|
860
|
+
<ChevronUp className="h-4 w-4" />
|
|
861
|
+
) : (
|
|
862
|
+
<ChevronDown className="h-4 w-4" />
|
|
863
|
+
)}
|
|
749
864
|
</button>
|
|
865
|
+
{expandedFilterSections.price && (
|
|
866
|
+
<div className="space-y-3">
|
|
867
|
+
<div className="flex flex-wrap gap-2">
|
|
868
|
+
{priceRanges.map((range) => {
|
|
869
|
+
const isActive = selectedPriceRange === range.value;
|
|
870
|
+
return (
|
|
871
|
+
<button
|
|
872
|
+
type="button"
|
|
873
|
+
key={range.value}
|
|
874
|
+
onClick={() => handlePriceRangeSelect(range.value)}
|
|
875
|
+
className={`rounded-lg border px-3 py-1.5 text-sm font-medium transition ${
|
|
876
|
+
isActive
|
|
877
|
+
? 'border-primary-600 bg-primary-600 text-white'
|
|
878
|
+
: 'border-slate-200 bg-white text-slate-600 hover:border-primary-300'
|
|
879
|
+
}`}
|
|
880
|
+
>
|
|
881
|
+
{range.label}
|
|
882
|
+
</button>
|
|
883
|
+
);
|
|
884
|
+
})}
|
|
885
|
+
</div>
|
|
886
|
+
<div className="grid grid-cols-2 gap-2">
|
|
887
|
+
<Input
|
|
888
|
+
type="number"
|
|
889
|
+
min="0"
|
|
890
|
+
placeholder="Min"
|
|
891
|
+
value={customPrice.min}
|
|
892
|
+
onChange={(event) =>
|
|
893
|
+
setCustomPrice((current) => ({ ...current, min: event.target.value }))
|
|
894
|
+
}
|
|
895
|
+
className="text-sm"
|
|
896
|
+
/>
|
|
897
|
+
<Input
|
|
898
|
+
type="number"
|
|
899
|
+
min="0"
|
|
900
|
+
placeholder="Max"
|
|
901
|
+
value={customPrice.max}
|
|
902
|
+
onChange={(event) =>
|
|
903
|
+
setCustomPrice((current) => ({ ...current, max: event.target.value }))
|
|
904
|
+
}
|
|
905
|
+
className="text-sm"
|
|
906
|
+
/>
|
|
907
|
+
</div>
|
|
908
|
+
<button
|
|
909
|
+
type="button"
|
|
910
|
+
onClick={applyCustomPrice}
|
|
911
|
+
disabled={!isCustomPriceDirty}
|
|
912
|
+
className="w-full rounded-lg border border-primary-500 bg-primary-500/10 px-4 py-2 text-sm font-medium text-primary-700 transition hover:bg-primary-500/20 disabled:cursor-not-allowed disabled:border-slate-200 disabled:text-slate-400"
|
|
913
|
+
>
|
|
914
|
+
Apply
|
|
915
|
+
</button>
|
|
916
|
+
</div>
|
|
917
|
+
)}
|
|
750
918
|
</div>
|
|
919
|
+
|
|
751
920
|
</div>
|
|
752
921
|
</>
|
|
753
922
|
);
|
|
754
923
|
|
|
924
|
+
// Get first 5 categories for the category section (6 total with "All Products")
|
|
925
|
+
const displayCategories = useMemo(() => {
|
|
926
|
+
return topCategories.slice(0, 5);
|
|
927
|
+
}, [topCategories]);
|
|
928
|
+
|
|
929
|
+
// Map category names to icons
|
|
930
|
+
const getCategoryIcon = (categoryName: string) => {
|
|
931
|
+
const name = categoryName.toLowerCase();
|
|
932
|
+
if (name.includes('scrub') || name.includes('uniform')) return Shirt;
|
|
933
|
+
if (name.includes('vitamin') || name.includes('supplement')) return Pill;
|
|
934
|
+
if (name.includes('medicine') || name.includes('medication')) return Box;
|
|
935
|
+
if (name.includes('care') || name.includes('personal')) return Heart;
|
|
936
|
+
return Package;
|
|
937
|
+
};
|
|
938
|
+
|
|
755
939
|
return (
|
|
756
|
-
<div className="min-h-screen bg-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
aria-hidden="true"
|
|
761
|
-
/>
|
|
762
|
-
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_right,_rgba(94,234,212,0.35),_transparent_55%)] opacity-60" />
|
|
763
|
-
<div className="relative container mx-auto px-4 py-24">
|
|
940
|
+
<div className="min-h-screen bg-[#F9FAFB]">
|
|
941
|
+
{/* Header Section */}
|
|
942
|
+
<section className="relative overflow-hidden bg-[#E6EBF0] py-16 md:py-24">
|
|
943
|
+
<div className="container mx-auto px-4">
|
|
764
944
|
<motion.div
|
|
765
945
|
initial={{ opacity: 0, y: 24 }}
|
|
766
946
|
animate={{ opacity: 1, y: 0 }}
|
|
767
|
-
className="max-w-
|
|
947
|
+
className="max-w-4xl mx-auto space-y-6 text-center"
|
|
768
948
|
>
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
949
|
+
{/* Orange icon and badge */}
|
|
950
|
+
<div className="flex items-center justify-center gap-2 mb-4">
|
|
951
|
+
<Star className="h-5 w-5 text-orange-500 fill-orange-500" />
|
|
952
|
+
<span className="text-sm font-semibold uppercase tracking-wider text-orange-500">
|
|
953
|
+
COMPLETE PHARMACY SHOP
|
|
954
|
+
</span>
|
|
955
|
+
</div>
|
|
956
|
+
|
|
957
|
+
{/* Main Title */}
|
|
958
|
+
<h1 className="text-3xl md:text-4xl font-bold text-slate-900 leading-tight">
|
|
959
|
+
Medical Supplies & Wellness Products
|
|
775
960
|
</h1>
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
961
|
+
|
|
962
|
+
{/* Description */}
|
|
963
|
+
<p className="text-base md:text-lg text-slate-600 max-w-2xl mx-auto">
|
|
964
|
+
From professional scrubs to vitamins, medicines to personal care - everything you need for health and wellness.
|
|
779
965
|
</p>
|
|
966
|
+
|
|
967
|
+
{/* Search Bar */}
|
|
780
968
|
<form
|
|
781
969
|
onSubmit={handleSearch}
|
|
782
|
-
className="
|
|
970
|
+
className="max-w-2xl mx-auto mt-8"
|
|
783
971
|
>
|
|
784
972
|
<div className="relative w-full">
|
|
973
|
+
<Search className="absolute left-5 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400 pointer-events-none" />
|
|
785
974
|
<input
|
|
786
975
|
type="search"
|
|
787
|
-
placeholder="Search for products,
|
|
976
|
+
placeholder="Search for products, brands, or categories..."
|
|
788
977
|
value={searchQuery}
|
|
789
978
|
onChange={handleInputChange}
|
|
790
979
|
onKeyDown={handleKeyDown}
|
|
791
|
-
className="flex h-
|
|
980
|
+
className="flex h-16 w-full rounded-full border-0 bg-white px-5 pl-14 pr-5 text-base text-slate-900 placeholder-slate-400 shadow-lg focus:outline-none focus:ring-2 focus:ring-primary-500/30 disabled:opacity-50"
|
|
792
981
|
disabled={isSearching}
|
|
793
982
|
/>
|
|
794
|
-
<button
|
|
795
|
-
type="submit"
|
|
796
|
-
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-xl bg-white/20 p-3 text-white transition hover:bg-white/30 disabled:opacity-50"
|
|
797
|
-
disabled={!searchQuery.trim() || isSearching}
|
|
798
|
-
>
|
|
799
|
-
{isSearching ? (
|
|
800
|
-
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
|
801
|
-
) : (
|
|
802
|
-
<Search className="h-5 w-5" />
|
|
803
|
-
)}
|
|
804
|
-
</button>
|
|
805
983
|
</div>
|
|
806
984
|
</form>
|
|
807
985
|
</motion.div>
|
|
986
|
+
</div>
|
|
987
|
+
</section>
|
|
808
988
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
>
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
type="button"
|
|
824
|
-
onClick={() => handleQuickSearch(term)}
|
|
825
|
-
className="rounded-full border border-white/30 bg-white/10 px-4 py-2 text-sm font-medium text-white transition hover:border-white/50 hover:bg-white/20"
|
|
989
|
+
{/* Shop by Category Section */}
|
|
990
|
+
<section className="bg-white py-8 ">
|
|
991
|
+
<div className="container mx-auto px-4">
|
|
992
|
+
<h2 className="text-2xl md:text-3xl font-bold text-slate-900 mb-8">
|
|
993
|
+
Shop by Category
|
|
994
|
+
</h2>
|
|
995
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-6">
|
|
996
|
+
{isLoadingCategories ? (
|
|
997
|
+
// Skeleton loaders
|
|
998
|
+
<>
|
|
999
|
+
{Array.from({ length: 6 }).map((_, index) => (
|
|
1000
|
+
<div
|
|
1001
|
+
key={index}
|
|
1002
|
+
className="rounded-xl border border-slate-200 bg-white p-8"
|
|
826
1003
|
>
|
|
827
|
-
|
|
828
|
-
|
|
1004
|
+
<Skeleton className="h-10 w-10 mb-4 rounded-lg" />
|
|
1005
|
+
<Skeleton className="h-6 w-3/4 mb-2 rounded" />
|
|
1006
|
+
<Skeleton className="h-4 w-1/2 rounded" />
|
|
1007
|
+
</div>
|
|
829
1008
|
))}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
<
|
|
835
|
-
Trending categories
|
|
836
|
-
</p>
|
|
837
|
-
<div className="flex flex-wrap justify-start gap-2 md:justify-end">
|
|
838
|
-
{topCategories.map((category) => (
|
|
839
|
-
<button
|
|
840
|
-
key={category.id}
|
|
841
|
-
type="button"
|
|
842
|
-
onClick={() => handleCategoryChange(category.id ?? '')}
|
|
843
|
-
className="rounded-full bg-white/15 px-3 py-1.5 text-sm font-medium text-white transition hover:bg-white/25"
|
|
844
|
-
>
|
|
845
|
-
{category.name}
|
|
846
|
-
</button>
|
|
847
|
-
))}
|
|
848
|
-
</div>
|
|
849
|
-
</div>
|
|
850
|
-
)}
|
|
851
|
-
</motion.div>
|
|
852
|
-
|
|
853
|
-
<div className="mt-10 grid gap-4 md:grid-cols-3">
|
|
854
|
-
{insightCards.map((card, index) => {
|
|
855
|
-
const Icon = card.icon;
|
|
856
|
-
return (
|
|
857
|
-
<motion.div
|
|
858
|
-
key={card.id}
|
|
1009
|
+
</>
|
|
1010
|
+
) : (
|
|
1011
|
+
<>
|
|
1012
|
+
{/* All Products Card */}
|
|
1013
|
+
<motion.button
|
|
859
1014
|
initial={{ opacity: 0, y: 20 }}
|
|
860
1015
|
animate={{ opacity: 1, y: 0 }}
|
|
861
|
-
|
|
862
|
-
className={`rounded-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
role={card.id === 'new' ? 'button' : undefined}
|
|
868
|
-
aria-pressed={card.id === 'new' ? (newArrivals ? 'true' : 'false') : undefined}
|
|
869
|
-
title={card.id === 'new' ? (newArrivals ? 'Filter active: showing products from the last 30 days' : 'Click to filter products from the last 30 days') : undefined}
|
|
1016
|
+
onClick={handleClearCategory}
|
|
1017
|
+
className={`group relative overflow-hidden rounded-xl p-8 text-left transition ${
|
|
1018
|
+
!categoryFilter
|
|
1019
|
+
? 'bg-gradient-to-b from-primary-500 to-primary-300 text-white shadow-lg scale-105'
|
|
1020
|
+
: 'border border-slate-200 bg-white hover:border-primary-300 hover:shadow-md'
|
|
1021
|
+
}`}
|
|
870
1022
|
>
|
|
871
|
-
<
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1023
|
+
<ShoppingBag className={`h-10 w-10 mb-4 ${!categoryFilter ? 'text-white' : 'text-primary-500'}`} />
|
|
1024
|
+
<h3 className={`text-xl font-bold mb-2 ${!categoryFilter ? 'text-white' : 'text-slate-900 group-hover:text-primary-600'}`}>
|
|
1025
|
+
All Products
|
|
1026
|
+
</h3>
|
|
1027
|
+
<p className={`text-sm ${!categoryFilter ? 'text-white/90' : 'text-slate-500'}`}>
|
|
1028
|
+
Browse everything
|
|
1029
|
+
</p>
|
|
1030
|
+
</motion.button>
|
|
1031
|
+
|
|
1032
|
+
{/* Category Cards */}
|
|
1033
|
+
{displayCategories.map((category, index) => {
|
|
1034
|
+
const Icon = getCategoryIcon(category.name ?? '');
|
|
1035
|
+
const isSelected = categoryFilter === category.id;
|
|
1036
|
+
return (
|
|
1037
|
+
<motion.button
|
|
1038
|
+
key={category.id}
|
|
1039
|
+
initial={{ opacity: 0, y: 20 }}
|
|
1040
|
+
animate={{ opacity: 1, y: 0 }}
|
|
1041
|
+
transition={{ delay: index * 0.1 }}
|
|
1042
|
+
onClick={() => handleCategoryChange(category.id ?? '')}
|
|
1043
|
+
className={`group rounded-xl p-8 text-left transition ${
|
|
1044
|
+
isSelected
|
|
1045
|
+
? 'bg-gradient-to-b from-primary-500 to-primary-300 text-white shadow-lg'
|
|
1046
|
+
: 'border border-slate-200 bg-white hover:border-primary-300 hover:shadow-md'
|
|
1047
|
+
}`}
|
|
1048
|
+
>
|
|
1049
|
+
<Icon className={`h-10 w-10 mb-4 ${isSelected ? 'text-white' : 'text-primary-500'}`} />
|
|
1050
|
+
<h3 className={`text-xl font-bold mb-2 transition-colors ${
|
|
1051
|
+
isSelected ? 'text-white' : 'text-slate-900 group-hover:text-primary-600'
|
|
1052
|
+
}`}>
|
|
1053
|
+
{category.name}
|
|
1054
|
+
</h3>
|
|
1055
|
+
<p className={`text-sm ${isSelected ? 'text-white/90' : 'text-slate-500'}`}>
|
|
1056
|
+
{category.name?.toLowerCase().includes('scrub') ? 'Professional uniforms' :
|
|
1057
|
+
category.name?.toLowerCase().includes('vitamin') ? 'Health supplements' :
|
|
1058
|
+
category.name?.toLowerCase().includes('medicine') ? 'OTC medications' :
|
|
1059
|
+
category.name?.toLowerCase().includes('care') ? 'Daily essentials' :
|
|
1060
|
+
'Shop now'}
|
|
875
1061
|
</p>
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
</div>
|
|
882
|
-
<p className="mt-3 text-sm text-white/70">{card.helper}</p>
|
|
883
|
-
</motion.div>
|
|
884
|
-
);
|
|
885
|
-
})}
|
|
1062
|
+
</motion.button>
|
|
1063
|
+
);
|
|
1064
|
+
})}
|
|
1065
|
+
</>
|
|
1066
|
+
)}
|
|
886
1067
|
</div>
|
|
887
1068
|
</div>
|
|
888
1069
|
</section>
|
|
889
1070
|
|
|
890
|
-
<div className="relative z-10
|
|
1071
|
+
<div className="relative z-10 pb-16 mt-8">
|
|
891
1072
|
<div className="container mx-auto px-4">
|
|
892
1073
|
<div className="flex flex-col gap-8 lg:flex-row">
|
|
893
1074
|
<aside className="hidden w-72 flex-shrink-0 lg:block">
|
|
894
|
-
<div className="sticky top-24 rounded-
|
|
1075
|
+
<div className="sticky top-24 rounded-lg bg-white p-6">
|
|
895
1076
|
{renderFiltersPanel()}
|
|
896
1077
|
</div>
|
|
897
1078
|
</aside>
|
|
@@ -900,10 +1081,11 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
900
1081
|
<div className="rounded-3xl border border-gray-100 bg-white p-6 shadow-sm">
|
|
901
1082
|
<div className="flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
|
|
902
1083
|
<div>
|
|
903
|
-
<h2 className="text-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1084
|
+
<h2 className="text-base font-medium text-gray-700">
|
|
1085
|
+
{isLoading
|
|
1086
|
+
? 'Loading products...'
|
|
1087
|
+
: `Showing ${displayedProducts.length} of ${pagination.total || displayedProducts.length} products`}
|
|
1088
|
+
</h2>
|
|
907
1089
|
</div>
|
|
908
1090
|
<div className="flex flex-col gap-3 md:flex-row md:items-center">
|
|
909
1091
|
<div className="relative">
|
|
@@ -991,29 +1173,18 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
991
1173
|
)}
|
|
992
1174
|
</div>
|
|
993
1175
|
|
|
994
|
-
<div
|
|
995
|
-
<div className="flex flex-col gap-3 text-sm text-gray-600 md:flex-row md:items-center md:justify-between">
|
|
996
|
-
<span>
|
|
997
|
-
{isLoading
|
|
998
|
-
? 'Loading products...'
|
|
999
|
-
: `Showing ${displayedProducts.length} of ${pagination.total || displayedProducts.length} products`}
|
|
1000
|
-
</span>
|
|
1001
|
-
<span className="inline-flex items-center gap-2 text-gray-400">
|
|
1002
|
-
<Clock className="h-4 w-4" />
|
|
1003
|
-
Updated a moment ago
|
|
1004
|
-
</span>
|
|
1005
|
-
</div>
|
|
1176
|
+
<div >
|
|
1006
1177
|
|
|
1007
1178
|
<div className="mt-6">
|
|
1008
1179
|
{isLoading ? (
|
|
1009
|
-
<div className="grid grid-cols-1 gap-
|
|
1180
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
|
1010
1181
|
{Array.from({ length: 6 }).map((_, index) => (
|
|
1011
1182
|
<ProductCardSkeleton key={index} />
|
|
1012
1183
|
))}
|
|
1013
1184
|
</div>
|
|
1014
1185
|
) : displayedProducts.length > 0 ? (
|
|
1015
1186
|
viewMode === 'grid' ? (
|
|
1016
|
-
<div className="grid grid-cols-1 gap-
|
|
1187
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
|
1017
1188
|
{displayedProducts.map((product) => (
|
|
1018
1189
|
<div key={product.id} className="h-full">
|
|
1019
1190
|
<ProductCard
|