hey-pharmacist-ecommerce 1.1.10 → 1.1.12
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 +550 -398
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +551 -399
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/CartItem.tsx +63 -42
- package/src/components/ProductCard.tsx +131 -55
- 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/ProductDetailScreen.tsx +355 -362
- package/src/screens/RegisterScreen.tsx +1 -1
- package/src/screens/ShopScreen.tsx +438 -275
- 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,78 +636,118 @@ 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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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>
|
|
584
674
|
</div>
|
|
585
|
-
|
|
675
|
+
|
|
676
|
+
{/* Category Section */}
|
|
677
|
+
<div className="space-y-3">
|
|
586
678
|
<button
|
|
587
679
|
type="button"
|
|
588
|
-
onClick={
|
|
589
|
-
className="text-sm font-
|
|
680
|
+
onClick={() => toggleFilterSection('category')}
|
|
681
|
+
className="flex w-full items-center justify-between text-sm font-medium text-slate-600"
|
|
590
682
|
>
|
|
591
|
-
|
|
683
|
+
<span>Category</span>
|
|
684
|
+
{expandedFilterSections.category ? (
|
|
685
|
+
<ChevronUp className="h-4 w-4" />
|
|
686
|
+
) : (
|
|
687
|
+
<ChevronDown className="h-4 w-4" />
|
|
688
|
+
)}
|
|
592
689
|
</button>
|
|
593
|
-
)}
|
|
594
|
-
</div>
|
|
595
690
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
<span className={`ml-1 rounded-full bg-gray-100 px-2 py-0.5 text-xs ${isCategoryActive ? 'text-primary-700' : 'text-gray-500'}`}>
|
|
626
|
-
{category.productCount}
|
|
627
|
-
</span>
|
|
628
|
-
)}
|
|
629
|
-
</span>
|
|
691
|
+
{expandedFilterSections.category && (
|
|
692
|
+
<div className="space-y-2">
|
|
693
|
+
{/* All Products Option */}
|
|
694
|
+
<button
|
|
695
|
+
type="button"
|
|
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
|
+
}`}
|
|
702
|
+
>
|
|
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>
|
|
711
|
+
</button>
|
|
712
|
+
|
|
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">
|
|
630
720
|
<button
|
|
631
721
|
type="button"
|
|
632
|
-
onClick={(
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
toggleCategoryExpand(category.id ?? '');
|
|
722
|
+
onClick={() => {
|
|
723
|
+
if (!isExpanded) toggleCategoryExpand(category.id ?? '');
|
|
724
|
+
handleCategoryChange(category.id ?? '');
|
|
636
725
|
}}
|
|
637
|
-
className=
|
|
638
|
-
|
|
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
|
+
}`}
|
|
639
731
|
>
|
|
640
|
-
<
|
|
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 && (
|
|
737
|
+
<button
|
|
738
|
+
type="button"
|
|
739
|
+
onClick={(e) => {
|
|
740
|
+
e.stopPropagation();
|
|
741
|
+
toggleCategoryExpand(category.id ?? '');
|
|
742
|
+
}}
|
|
743
|
+
className="p-1"
|
|
744
|
+
>
|
|
745
|
+
<ChevronDown className={`h-4 w-4 transition-transform ${isExpanded ? 'rotate-180' : ''} ${isCategoryActive ? 'text-white' : 'text-slate-400'}`} />
|
|
746
|
+
</button>
|
|
747
|
+
)}
|
|
641
748
|
</button>
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
<div className="mt-1 border-gray-100 px-2 pb-2 pl-4">
|
|
645
|
-
<div className="divide-y divide-gray-100">
|
|
749
|
+
{isExpanded && Array.isArray(category.categorySubCategories) && category.categorySubCategories.length > 0 && (
|
|
750
|
+
<div className="ml-9 space-y-1">
|
|
646
751
|
{category.categorySubCategories.map((sub) => {
|
|
647
752
|
const isSubActive = subCategoryFilter === sub.id;
|
|
648
753
|
return (
|
|
@@ -650,10 +755,10 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
650
755
|
key={sub.id}
|
|
651
756
|
type="button"
|
|
652
757
|
onClick={() => handleSubCategoryChange(category.id ?? '', sub.id)}
|
|
653
|
-
className={`block w-full px-
|
|
758
|
+
className={`block w-full rounded-lg px-3 py-2 text-sm text-left transition ${
|
|
654
759
|
isSubActive
|
|
655
|
-
? 'text-primary-700'
|
|
656
|
-
: 'text-
|
|
760
|
+
? 'bg-primary-50 text-primary-700 font-medium'
|
|
761
|
+
: 'text-slate-600 hover:bg-slate-50'
|
|
657
762
|
}`}
|
|
658
763
|
>
|
|
659
764
|
{sub.name}
|
|
@@ -661,243 +766,313 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
661
766
|
);
|
|
662
767
|
})}
|
|
663
768
|
</div>
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
)
|
|
668
|
-
|
|
669
|
-
|
|
769
|
+
)}
|
|
770
|
+
</div>
|
|
771
|
+
);
|
|
772
|
+
})}
|
|
773
|
+
</div>
|
|
774
|
+
)}
|
|
670
775
|
</div>
|
|
671
|
-
</div>
|
|
672
|
-
</div>
|
|
673
776
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
Price
|
|
677
|
-
</h4>
|
|
678
|
-
<div className="flex flex-wrap gap-2">
|
|
679
|
-
{priceRanges.map((range) => {
|
|
680
|
-
const isActive = selectedPriceRange === range.value;
|
|
681
|
-
return (
|
|
682
|
-
<button
|
|
683
|
-
type="button"
|
|
684
|
-
key={range.value}
|
|
685
|
-
onClick={() => handlePriceRangeSelect(range.value)}
|
|
686
|
-
className={`rounded-full border px-3 py-1.5 text-sm font-medium transition ${
|
|
687
|
-
isActive
|
|
688
|
-
? 'border-secondary-600 bg-secondary-600 text-white shadow-lg shadow-secondary-500/30'
|
|
689
|
-
: 'border-gray-200 bg-white text-gray-600 hover:border-secondary-300 hover:text-secondary-600'
|
|
690
|
-
}`}
|
|
691
|
-
>
|
|
692
|
-
{range.label}
|
|
693
|
-
</button>
|
|
694
|
-
);
|
|
695
|
-
})}
|
|
696
|
-
</div>
|
|
697
|
-
<div className="grid grid-cols-2 gap-3">
|
|
698
|
-
<Input
|
|
699
|
-
type="number"
|
|
700
|
-
min="0"
|
|
701
|
-
placeholder="Custom min"
|
|
702
|
-
value={customPrice.min}
|
|
703
|
-
onChange={(event) =>
|
|
704
|
-
setCustomPrice((current) => ({ ...current, min: event.target.value }))
|
|
705
|
-
}
|
|
706
|
-
/>
|
|
707
|
-
<Input
|
|
708
|
-
type="number"
|
|
709
|
-
min="0"
|
|
710
|
-
placeholder="Custom max"
|
|
711
|
-
value={customPrice.max}
|
|
712
|
-
onChange={(event) =>
|
|
713
|
-
setCustomPrice((current) => ({ ...current, max: event.target.value }))
|
|
714
|
-
}
|
|
715
|
-
/>
|
|
716
|
-
</div>
|
|
777
|
+
{/* Brand Section */}
|
|
778
|
+
<div className="space-y-3">
|
|
717
779
|
<button
|
|
718
780
|
type="button"
|
|
719
|
-
onClick={
|
|
720
|
-
|
|
721
|
-
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"
|
|
722
783
|
>
|
|
723
|
-
|
|
784
|
+
<span>Brand</span>
|
|
785
|
+
{expandedFilterSections.brand ? (
|
|
786
|
+
<ChevronUp className="h-4 w-4" />
|
|
787
|
+
) : (
|
|
788
|
+
<ChevronDown className="h-4 w-4" />
|
|
789
|
+
)}
|
|
724
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
|
+
)}
|
|
725
817
|
</div>
|
|
726
818
|
|
|
819
|
+
{/* Availability Section */}
|
|
727
820
|
<div className="space-y-3">
|
|
728
|
-
<h4 className="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500">
|
|
729
|
-
Availability
|
|
730
|
-
</h4>
|
|
731
821
|
<button
|
|
732
822
|
type="button"
|
|
733
|
-
onClick={
|
|
734
|
-
className=
|
|
735
|
-
inStockOnly
|
|
736
|
-
? 'border-primary-500 bg-primary-50 text-primary-700'
|
|
737
|
-
: 'border-gray-200 bg-white text-gray-600 hover:border-primary-300 hover:text-primary-600'
|
|
738
|
-
}`}
|
|
823
|
+
onClick={() => toggleFilterSection('availability')}
|
|
824
|
+
className="flex w-full items-center justify-between text-sm font-medium text-slate-600"
|
|
739
825
|
>
|
|
740
|
-
<span>
|
|
741
|
-
|
|
826
|
+
<span>Availability</span>
|
|
827
|
+
{expandedFilterSections.availability ? (
|
|
828
|
+
<ChevronUp className="h-4 w-4" />
|
|
829
|
+
) : (
|
|
830
|
+
<ChevronDown className="h-4 w-4" />
|
|
831
|
+
)}
|
|
742
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">
|
|
743
853
|
<button
|
|
744
854
|
type="button"
|
|
745
|
-
onClick={
|
|
746
|
-
className=
|
|
747
|
-
newArrivals
|
|
748
|
-
? 'border-secondary-500 bg-secondary-50 text-secondary-700'
|
|
749
|
-
: 'border-gray-200 bg-white text-gray-600 hover:border-secondary-300 hover:text-secondary-600'
|
|
750
|
-
}`}
|
|
855
|
+
onClick={() => toggleFilterSection('price')}
|
|
856
|
+
className="flex w-full items-center justify-between text-sm font-medium text-slate-600"
|
|
751
857
|
>
|
|
752
|
-
<span>
|
|
753
|
-
|
|
858
|
+
<span>Price Range</span>
|
|
859
|
+
{expandedFilterSections.price ? (
|
|
860
|
+
<ChevronUp className="h-4 w-4" />
|
|
861
|
+
) : (
|
|
862
|
+
<ChevronDown className="h-4 w-4" />
|
|
863
|
+
)}
|
|
754
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
|
+
)}
|
|
755
918
|
</div>
|
|
919
|
+
|
|
756
920
|
</div>
|
|
757
921
|
</>
|
|
758
922
|
);
|
|
759
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
|
+
|
|
760
939
|
return (
|
|
761
|
-
<div className="min-h-screen bg-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
aria-hidden="true"
|
|
766
|
-
/>
|
|
767
|
-
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_right,_rgba(94,234,212,0.35),_transparent_55%)] opacity-60" />
|
|
768
|
-
<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">
|
|
769
944
|
<motion.div
|
|
770
945
|
initial={{ opacity: 0, y: 24 }}
|
|
771
946
|
animate={{ opacity: 1, y: 0 }}
|
|
772
|
-
className="max-w-
|
|
947
|
+
className="max-w-4xl mx-auto space-y-6 text-center"
|
|
773
948
|
>
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
|
780
960
|
</h1>
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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.
|
|
784
965
|
</p>
|
|
966
|
+
|
|
967
|
+
{/* Search Bar */}
|
|
785
968
|
<form
|
|
786
969
|
onSubmit={handleSearch}
|
|
787
|
-
className="
|
|
970
|
+
className="max-w-2xl mx-auto mt-8"
|
|
788
971
|
>
|
|
789
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" />
|
|
790
974
|
<input
|
|
791
975
|
type="search"
|
|
792
|
-
placeholder="Search for products,
|
|
976
|
+
placeholder="Search for products, brands, or categories..."
|
|
793
977
|
value={searchQuery}
|
|
794
978
|
onChange={handleInputChange}
|
|
795
979
|
onKeyDown={handleKeyDown}
|
|
796
|
-
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"
|
|
797
981
|
disabled={isSearching}
|
|
798
982
|
/>
|
|
799
|
-
<button
|
|
800
|
-
type="submit"
|
|
801
|
-
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"
|
|
802
|
-
disabled={!searchQuery.trim() || isSearching}
|
|
803
|
-
>
|
|
804
|
-
{isSearching ? (
|
|
805
|
-
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
|
806
|
-
) : (
|
|
807
|
-
<Search className="h-5 w-5" />
|
|
808
|
-
)}
|
|
809
|
-
</button>
|
|
810
983
|
</div>
|
|
811
984
|
</form>
|
|
812
985
|
</motion.div>
|
|
986
|
+
</div>
|
|
987
|
+
</section>
|
|
813
988
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
>
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
type="button"
|
|
829
|
-
onClick={() => handleQuickSearch(term)}
|
|
830
|
-
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"
|
|
831
1003
|
>
|
|
832
|
-
|
|
833
|
-
|
|
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>
|
|
834
1008
|
))}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
<
|
|
840
|
-
Trending categories
|
|
841
|
-
</p>
|
|
842
|
-
<div className="flex flex-wrap justify-start gap-2 md:justify-end">
|
|
843
|
-
{topCategories.map((category) => (
|
|
844
|
-
<button
|
|
845
|
-
key={category.id}
|
|
846
|
-
type="button"
|
|
847
|
-
onClick={() => handleCategoryChange(category.id ?? '')}
|
|
848
|
-
className="rounded-full bg-white/15 px-3 py-1.5 text-sm font-medium text-white transition hover:bg-white/25"
|
|
849
|
-
>
|
|
850
|
-
{category.name}
|
|
851
|
-
</button>
|
|
852
|
-
))}
|
|
853
|
-
</div>
|
|
854
|
-
</div>
|
|
855
|
-
)}
|
|
856
|
-
</motion.div>
|
|
857
|
-
|
|
858
|
-
<div className="mt-10 grid gap-4 md:grid-cols-3">
|
|
859
|
-
{insightCards.map((card, index) => {
|
|
860
|
-
const Icon = card.icon;
|
|
861
|
-
return (
|
|
862
|
-
<motion.div
|
|
863
|
-
key={card.id}
|
|
1009
|
+
</>
|
|
1010
|
+
) : (
|
|
1011
|
+
<>
|
|
1012
|
+
{/* All Products Card */}
|
|
1013
|
+
<motion.button
|
|
864
1014
|
initial={{ opacity: 0, y: 20 }}
|
|
865
1015
|
animate={{ opacity: 1, y: 0 }}
|
|
866
|
-
|
|
867
|
-
className={`rounded-
|
|
868
|
-
|
|
869
|
-
? '
|
|
870
|
-
: 'border-
|
|
871
|
-
}
|
|
872
|
-
onClick={card.id === 'new' ? handleToggleNewArrivals : undefined}
|
|
873
|
-
role={card.id === 'new' ? 'button' : undefined}
|
|
874
|
-
aria-pressed={card.id === 'new' ? (newArrivals ? 'true' : 'false') : undefined}
|
|
875
|
-
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
|
+
}`}
|
|
876
1022
|
>
|
|
877
|
-
<
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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'}
|
|
881
1061
|
</p>
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
</div>
|
|
888
|
-
<p className="mt-3 text-sm text-white/70">{card.helper}</p>
|
|
889
|
-
</motion.div>
|
|
890
|
-
);
|
|
891
|
-
})}
|
|
1062
|
+
</motion.button>
|
|
1063
|
+
);
|
|
1064
|
+
})}
|
|
1065
|
+
</>
|
|
1066
|
+
)}
|
|
892
1067
|
</div>
|
|
893
1068
|
</div>
|
|
894
1069
|
</section>
|
|
895
1070
|
|
|
896
|
-
<div className="relative z-10
|
|
1071
|
+
<div className="relative z-10 pb-16 mt-8">
|
|
897
1072
|
<div className="container mx-auto px-4">
|
|
898
1073
|
<div className="flex flex-col gap-8 lg:flex-row">
|
|
899
1074
|
<aside className="hidden w-72 flex-shrink-0 lg:block">
|
|
900
|
-
<div className="sticky top-24 rounded-
|
|
1075
|
+
<div className="sticky top-24 rounded-lg bg-white p-6">
|
|
901
1076
|
{renderFiltersPanel()}
|
|
902
1077
|
</div>
|
|
903
1078
|
</aside>
|
|
@@ -906,10 +1081,11 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
906
1081
|
<div className="rounded-3xl border border-gray-100 bg-white p-6 shadow-sm">
|
|
907
1082
|
<div className="flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
|
|
908
1083
|
<div>
|
|
909
|
-
<h2 className="text-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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>
|
|
913
1089
|
</div>
|
|
914
1090
|
<div className="flex flex-col gap-3 md:flex-row md:items-center">
|
|
915
1091
|
<div className="relative">
|
|
@@ -932,11 +1108,10 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
932
1108
|
<button
|
|
933
1109
|
type="button"
|
|
934
1110
|
onClick={() => setViewMode('grid')}
|
|
935
|
-
className={`flex items-center gap-2 rounded-l-xl px-4 py-2 text-sm font-medium transition ${
|
|
936
|
-
viewMode === 'grid'
|
|
1111
|
+
className={`flex items-center gap-2 rounded-l-xl px-4 py-2 text-sm font-medium transition ${viewMode === 'grid'
|
|
937
1112
|
? 'bg-primary-50 text-primary-600'
|
|
938
1113
|
: 'text-gray-500 hover:text-gray-700'
|
|
939
|
-
|
|
1114
|
+
}`}
|
|
940
1115
|
aria-pressed={viewMode === 'grid'}
|
|
941
1116
|
>
|
|
942
1117
|
<LayoutGrid className="h-4 w-4" />
|
|
@@ -945,11 +1120,10 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
945
1120
|
<button
|
|
946
1121
|
type="button"
|
|
947
1122
|
onClick={() => setViewMode('list')}
|
|
948
|
-
className={`flex items-center gap-2 rounded-r-xl px-4 py-2 text-sm font-medium transition ${
|
|
949
|
-
viewMode === 'list'
|
|
1123
|
+
className={`flex items-center gap-2 rounded-r-xl px-4 py-2 text-sm font-medium transition ${viewMode === 'list'
|
|
950
1124
|
? 'bg-primary-50 text-primary-600'
|
|
951
1125
|
: 'text-gray-500 hover:text-gray-700'
|
|
952
|
-
|
|
1126
|
+
}`}
|
|
953
1127
|
aria-pressed={viewMode === 'list'}
|
|
954
1128
|
>
|
|
955
1129
|
<LayoutList className="h-4 w-4" />
|
|
@@ -999,35 +1173,24 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
999
1173
|
)}
|
|
1000
1174
|
</div>
|
|
1001
1175
|
|
|
1002
|
-
<div
|
|
1003
|
-
<div className="flex flex-col gap-3 text-sm text-gray-600 md:flex-row md:items-center md:justify-between">
|
|
1004
|
-
<span>
|
|
1005
|
-
{isLoading
|
|
1006
|
-
? 'Loading products...'
|
|
1007
|
-
: `Showing ${displayedProducts.length} of ${pagination.total || displayedProducts.length} products`}
|
|
1008
|
-
</span>
|
|
1009
|
-
<span className="inline-flex items-center gap-2 text-gray-400">
|
|
1010
|
-
<Clock className="h-4 w-4" />
|
|
1011
|
-
Updated a moment ago
|
|
1012
|
-
</span>
|
|
1013
|
-
</div>
|
|
1176
|
+
<div >
|
|
1014
1177
|
|
|
1015
1178
|
<div className="mt-6">
|
|
1016
1179
|
{isLoading ? (
|
|
1017
|
-
<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">
|
|
1018
1181
|
{Array.from({ length: 6 }).map((_, index) => (
|
|
1019
1182
|
<ProductCardSkeleton key={index} />
|
|
1020
1183
|
))}
|
|
1021
1184
|
</div>
|
|
1022
1185
|
) : displayedProducts.length > 0 ? (
|
|
1023
1186
|
viewMode === 'grid' ? (
|
|
1024
|
-
<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">
|
|
1025
1188
|
{displayedProducts.map((product) => (
|
|
1026
1189
|
<div key={product.id} className="h-full">
|
|
1027
1190
|
<ProductCard
|
|
1028
1191
|
product={product}
|
|
1029
1192
|
onClickProduct={(item) => {
|
|
1030
|
-
router.push(buildPath(`/products/${item.
|
|
1193
|
+
router.push(buildPath(`/products/${item._id}`));
|
|
1031
1194
|
}}
|
|
1032
1195
|
/>
|
|
1033
1196
|
</div>
|
|
@@ -1039,10 +1202,10 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
1039
1202
|
const discount =
|
|
1040
1203
|
product.priceBeforeDiscount && product.priceBeforeDiscount > product.finalPrice
|
|
1041
1204
|
? Math.round(
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1205
|
+
((product.priceBeforeDiscount - product.finalPrice) /
|
|
1206
|
+
product.priceBeforeDiscount) *
|
|
1207
|
+
100
|
|
1208
|
+
)
|
|
1046
1209
|
: 0;
|
|
1047
1210
|
|
|
1048
1211
|
return (
|
|
@@ -1050,7 +1213,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
1050
1213
|
key={product.id}
|
|
1051
1214
|
whileHover={{ y: -4 }}
|
|
1052
1215
|
className="group flex cursor-pointer flex-col gap-6 rounded-2xl border border-gray-100 bg-white p-5 shadow-sm transition hover:shadow-xl md:flex-row md:items-start"
|
|
1053
|
-
onClick={() => router.push(buildPath(`/products/${product.
|
|
1216
|
+
onClick={() => router.push(buildPath(`/products/${product._id}`))}
|
|
1054
1217
|
>
|
|
1055
1218
|
<div className="relative h-48 w-full overflow-hidden rounded-2xl bg-gray-100 md:h-40 md:w-40">
|
|
1056
1219
|
<Image
|