hey-pharmacist-ecommerce 1.1.11 → 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,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-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>
584
- </div>
585
- {hasActiveFilters && (
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={handleClearFilters}
589
- className="text-sm font-semibold text-primary-600 hover:text-primary-700"
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
- Clear all
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
- <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 ${isCategoryActive
617
- ? 'text-primary-700 bg-primary-50'
618
- : 'text-gray-700 hover:text-primary-700 hover:bg-primary-50/50'
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="rounded-md p-1 hover:bg-gray-100"
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-primary-600' : 'rotate-0 text-gray-400'}`} />
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
- </div>
665
- );
666
- })}
667
- </div>
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
- </div>
774
+ )}
670
775
  </div>
671
776
 
672
- <div className="space-y-4">
673
- <h4 className="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500">
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={applyCustomPrice}
717
- disabled={!isCustomPriceDirty}
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
- 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
+ )}
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={handleToggleStock}
731
- className={`flex w-full items-center justify-between rounded-xl border px-4 py-3 text-sm font-medium transition ${inStockOnly
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>In stock only</span>
737
- <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
+ )}
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={handleToggleNewArrivals}
742
- className={`mt-2 flex w-full items-center justify-between rounded-xl border px-4 py-3 text-sm font-medium transition ${newArrivals
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>New arrivals (last 30 days)</span>
748
- <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
+ )}
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-slate-50">
757
- <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">
758
- <div
759
- className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(255,255,255,0.35),_transparent_60%)]"
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-3xl space-y-8 text-center md:mx-auto md:text-left"
947
+ className="max-w-4xl mx-auto space-y-6 text-center"
768
948
  >
769
- <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">
770
- <Sparkles className="h-4 w-4" />
771
- Wellness products, curated for you
772
- </span>
773
- <h1 className="text-4xl font-bold leading-tight md:text-6xl">
774
- 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
775
960
  </h1>
776
- <p className="text-lg text-white/80 md:text-xl">
777
- Explore a modern storefront with real-time inventory, smart filters, and rich
778
- 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.
779
965
  </p>
966
+
967
+ {/* Search Bar */}
780
968
  <form
781
969
  onSubmit={handleSearch}
782
- className="mx-auto max-w-2xl md:mx-0"
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, categories, or symptoms..."
976
+ placeholder="Search for products, brands, or categories..."
788
977
  value={searchQuery}
789
978
  onChange={handleInputChange}
790
979
  onKeyDown={handleKeyDown}
791
- 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"
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
- <motion.div
810
- initial={{ opacity: 0, y: 24 }}
811
- animate={{ opacity: 1, y: 0 }}
812
- transition={{ delay: 0.15 }}
813
- 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"
814
- >
815
- <div className="space-y-3">
816
- <p className="text-sm font-semibold uppercase tracking-[0.3em] text-white/60">
817
- Explore popular searches
818
- </p>
819
- <div className="flex flex-wrap gap-2">
820
- {quickSearches.map((term) => (
821
- <button
822
- key={term}
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
- {term}
828
- </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>
829
1008
  ))}
830
- </div>
831
- </div>
832
- {topCategories.length > 0 && (
833
- <div className="space-y-3 md:text-right">
834
- <p className="text-sm font-semibold uppercase tracking-[0.3em] text-white/60">
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
- transition={{ delay: 0.2 + index * 0.05 }}
862
- className={`rounded-2xl border p-5 backdrop-blur ${card.id === 'new' && newArrivals
863
- ? 'border-white/40 bg-white/25 ring-2 ring-white/30'
864
- : 'border-white/20 bg-white/15'
865
- } ${card.id === 'new' ? 'cursor-pointer hover:bg-white/20' : ''}`}
866
- onClick={card.id === 'new' ? handleToggleNewArrivals : undefined}
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
- <div className="flex items-center justify-between">
872
- <div>
873
- <p className="text-sm font-semibold uppercase tracking-[0.3em] text-white/60">
874
- {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'}
875
1061
  </p>
876
- <p className="mt-2 text-3xl font-semibold text-white">{card.value}</p>
877
- </div>
878
- <span className="rounded-full bg-white/20 p-3 text-white">
879
- <Icon className="h-5 w-5" />
880
- </span>
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 -mt-12 pb-16">
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-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">
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-2xl font-bold text-gray-900">All products</h2>
904
- <p className="mt-1 text-sm text-gray-500">
905
- Browse a pharmacy-grade catalogue with smart merchandising and modern UI.
906
- </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>
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 className="rounded-3xl border border-gray-100 bg-white p-6 shadow-sm">
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-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">
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-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">
1017
1188
  {displayedProducts.map((product) => (
1018
1189
  <div key={product.id} className="h-full">
1019
1190
  <ProductCard