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.
@@ -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
- <div className="space-y-8">
577
- <div className="space-y-8">
578
- <div className="flex items-start justify-between gap-3">
579
- <div>
580
- <h3 className="text-lg font-semibold text-gray-900">Refine results</h3>
581
- <p className="mt-1 text-sm text-gray-500">
582
- Filter by category, price, and availability to find the perfect fit faster.
583
- </p>
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
- {hasActiveFilters && (
675
+
676
+ {/* Category Section */}
677
+ <div className="space-y-3">
586
678
  <button
587
679
  type="button"
588
- onClick={handleClearFilters}
589
- className="text-sm font-semibold text-primary-600 hover:text-primary-700"
680
+ onClick={() => toggleFilterSection('category')}
681
+ className="flex w-full items-center justify-between text-sm font-medium text-slate-600"
590
682
  >
591
- Clear all
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
- <div className="space-y-6">
597
- <div className="space-y-3">
598
- <h4 className="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500">Categories</h4>
599
- <div className="space-y-2">
600
- {sortedCategories.length === 0 && (
601
- <span className="text-sm text-gray-500">No categories available yet.</span>
602
- )}
603
- {sortedCategories.map((category) => {
604
- const isCategoryActive = categoryFilter === category.id;
605
- const isExpanded = !!expandedCategories[category.id as string];
606
- return (
607
- <div key={category.id} className="rounded-xl border-gray-100 bg-white/50">
608
- <div
609
- role="button"
610
- tabIndex={0}
611
- onClick={() => {
612
- // Selecting a category also expands it for quick access to subcategories
613
- if (!isExpanded) toggleCategoryExpand(category.id ?? '');
614
- handleCategoryChange(category.id ?? '');
615
- }}
616
- className={`flex w-full items-center justify-between rounded-xl px-3 py-2 text-sm font-medium transition ${
617
- isCategoryActive
618
- ? 'text-primary-700 bg-primary-50'
619
- : 'text-gray-700 hover:text-primary-700 hover:bg-primary-50/50'
620
- }`}
621
- >
622
- <span className="flex items-center gap-2">
623
- <span className='font-medium'>{category.name}</span>
624
- {category.productCount > 0 && (
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={(e) => {
633
- e.preventDefault();
634
- e.stopPropagation();
635
- toggleCategoryExpand(category.id ?? '');
722
+ onClick={() => {
723
+ if (!isExpanded) toggleCategoryExpand(category.id ?? '');
724
+ handleCategoryChange(category.id ?? '');
636
725
  }}
637
- className="rounded-md p-1 hover:bg-gray-100"
638
- aria-label={isExpanded ? 'Collapse' : 'Expand'}
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
- <ChevronDown className={`h-4 w-4 transition-transform ${isExpanded ? 'rotate-180 text-primary-600' : 'rotate-0 text-gray-400'}`} />
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
- </div>
643
- {isExpanded && Array.isArray(category.categorySubCategories) && category.categorySubCategories.length > 0 && (
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-2 py-2 text-sm text-start transition ${
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-gray-600 hover:text-primary-700 '
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
- </div>
665
- )}
666
- </div>
667
- );
668
- })}
669
- </div>
769
+ )}
770
+ </div>
771
+ );
772
+ })}
773
+ </div>
774
+ )}
670
775
  </div>
671
- </div>
672
- </div>
673
776
 
674
- <div className="space-y-4">
675
- <h4 className="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500">
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={applyCustomPrice}
720
- disabled={!isCustomPriceDirty}
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
- Apply price range
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={handleToggleStock}
734
- className={`flex w-full items-center justify-between rounded-xl border px-4 py-3 text-sm font-medium transition ${
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>In stock only</span>
741
- <Sparkles className="h-4 w-4 text-primary-500" />
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={handleToggleNewArrivals}
746
- className={`mt-2 flex w-full items-center justify-between rounded-xl border px-4 py-3 text-sm font-medium transition ${
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>New arrivals (last 30 days)</span>
753
- <Sparkles className="h-4 w-4 text-secondary-500" />
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-slate-50">
762
- <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">
763
- <div
764
- className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(255,255,255,0.35),_transparent_60%)]"
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-3xl space-y-8 text-center md:mx-auto md:text-left"
947
+ className="max-w-4xl mx-auto space-y-6 text-center"
773
948
  >
774
- <span className="inline-flex items-center gap-2 rounded-full bg-white/10 px-4 py-2 text-sm font-semibold tracking-wide text-white/80 backdrop-blur">
775
- <Sparkles className="h-4 w-4" />
776
- Wellness products, curated for you
777
- </span>
778
- <h1 className="text-4xl font-bold leading-tight md:text-6xl">
779
- Find pharmacy favorites crafted to keep your family thriving
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
- <p className="text-lg text-white/80 md:text-xl">
782
- Explore a modern storefront with real-time inventory, smart filters, and rich
783
- product details designed to make healthier choices easier.
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="mx-auto max-w-2xl md:mx-0"
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, categories, or symptoms..."
976
+ placeholder="Search for products, brands, or categories..."
793
977
  value={searchQuery}
794
978
  onChange={handleInputChange}
795
979
  onKeyDown={handleKeyDown}
796
- className="flex h-12 w-full rounded-xl border border-white/20 bg-white/10 px-4 py-2 pr-14 text-lg text-white placeholder-white/60 shadow-2xl shadow-primary-900/20 backdrop-blur focus:border-white/30 focus:outline-none focus:ring-2 focus:ring-white/20 disabled:opacity-50"
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
- <motion.div
815
- initial={{ opacity: 0, y: 24 }}
816
- animate={{ opacity: 1, y: 0 }}
817
- transition={{ delay: 0.15 }}
818
- className="mt-12 flex flex-col gap-6 rounded-3xl border border-white/20 bg-white/10 p-6 backdrop-blur md:flex-row md:items-center md:justify-between"
819
- >
820
- <div className="space-y-3">
821
- <p className="text-sm font-semibold uppercase tracking-[0.3em] text-white/60">
822
- Explore popular searches
823
- </p>
824
- <div className="flex flex-wrap gap-2">
825
- {quickSearches.map((term) => (
826
- <button
827
- key={term}
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
- {term}
833
- </button>
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
- </div>
836
- </div>
837
- {topCategories.length > 0 && (
838
- <div className="space-y-3 md:text-right">
839
- <p className="text-sm font-semibold uppercase tracking-[0.3em] text-white/60">
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
- transition={{ delay: 0.2 + index * 0.05 }}
867
- className={`rounded-2xl border p-5 backdrop-blur ${
868
- card.id === 'new' && newArrivals
869
- ? 'border-white/40 bg-white/25 ring-2 ring-white/30'
870
- : 'border-white/20 bg-white/15'
871
- } ${card.id === 'new' ? 'cursor-pointer hover:bg-white/20' : ''}`}
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
- <div className="flex items-center justify-between">
878
- <div>
879
- <p className="text-sm font-semibold uppercase tracking-[0.3em] text-white/60">
880
- {card.label}
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
- <p className="mt-2 text-3xl font-semibold text-white">{card.value}</p>
883
- </div>
884
- <span className="rounded-full bg-white/20 p-3 text-white">
885
- <Icon className="h-5 w-5" />
886
- </span>
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 -mt-12 pb-16">
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-3xl border border-gray-100 bg-white p-6 shadow-xl shadow-gray-200/40">
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-2xl font-bold text-gray-900">All products</h2>
910
- <p className="mt-1 text-sm text-gray-500">
911
- Browse a pharmacy-grade catalogue with smart merchandising and modern UI.
912
- </p>
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 className="rounded-3xl border border-gray-100 bg-white p-6 shadow-sm">
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-6 sm:grid-cols-2 xl:grid-cols-3">
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-6 sm:grid-cols-2 xl:grid-cols-3">
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.id}`));
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
- ((product.priceBeforeDiscount - product.finalPrice) /
1043
- product.priceBeforeDiscount) *
1044
- 100
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.id}`))}
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