hey-pharmacist-ecommerce 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +157 -17
  2. package/dist/index.d.mts +3636 -316
  3. package/dist/index.d.ts +3636 -316
  4. package/dist/index.js +6802 -3866
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +6756 -3818
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +18 -15
  9. package/src/components/AddressFormModal.tsx +171 -0
  10. package/src/components/CartItem.tsx +17 -12
  11. package/src/components/FilterChips.tsx +195 -0
  12. package/src/components/Header.tsx +121 -71
  13. package/src/components/OrderCard.tsx +18 -25
  14. package/src/components/ProductCard.tsx +209 -72
  15. package/src/components/ui/Button.tsx +13 -5
  16. package/src/components/ui/Card.tsx +46 -0
  17. package/src/hooks/useAddresses.ts +83 -0
  18. package/src/hooks/useOrders.ts +37 -19
  19. package/src/hooks/useProducts.ts +55 -63
  20. package/src/hooks/useWishlistProducts.ts +75 -0
  21. package/src/index.ts +3 -19
  22. package/src/lib/Apis/api.ts +1 -0
  23. package/src/lib/Apis/apis/cart-api.ts +3 -3
  24. package/src/lib/Apis/apis/inventory-api.ts +0 -108
  25. package/src/lib/Apis/apis/stores-api.ts +70 -0
  26. package/src/lib/Apis/apis/wishlist-api.ts +447 -0
  27. package/src/lib/Apis/models/cart-item-populated.ts +0 -1
  28. package/src/lib/Apis/models/create-single-variant-product-dto.ts +3 -10
  29. package/src/lib/Apis/models/create-variant-dto.ts +26 -33
  30. package/src/lib/Apis/models/extended-product-dto.ts +20 -24
  31. package/src/lib/Apis/models/index.ts +2 -1
  32. package/src/lib/Apis/models/order-time-line-dto.ts +49 -0
  33. package/src/lib/Apis/models/order.ts +3 -8
  34. package/src/lib/Apis/models/populated-order.ts +3 -8
  35. package/src/lib/Apis/models/product-variant.ts +29 -0
  36. package/src/lib/Apis/models/update-product-variant-dto.ts +16 -23
  37. package/src/lib/Apis/models/wishlist.ts +51 -0
  38. package/src/lib/Apis/wrapper.ts +18 -7
  39. package/src/lib/api-adapter/index.ts +0 -12
  40. package/src/lib/types/index.ts +16 -61
  41. package/src/lib/utils/colors.ts +7 -4
  42. package/src/lib/utils/format.ts +1 -1
  43. package/src/lib/validations/address.ts +14 -0
  44. package/src/providers/AuthProvider.tsx +61 -31
  45. package/src/providers/CartProvider.tsx +18 -28
  46. package/src/providers/EcommerceProvider.tsx +7 -0
  47. package/src/providers/FavoritesProvider.tsx +86 -0
  48. package/src/providers/ThemeProvider.tsx +16 -1
  49. package/src/providers/WishlistProvider.tsx +174 -0
  50. package/src/screens/AddressesScreen.tsx +484 -0
  51. package/src/screens/CartScreen.tsx +120 -84
  52. package/src/screens/CategoriesScreen.tsx +120 -0
  53. package/src/screens/CheckoutScreen.tsx +919 -241
  54. package/src/screens/CurrentOrdersScreen.tsx +125 -61
  55. package/src/screens/HomeScreen.tsx +209 -0
  56. package/src/screens/LoginScreen.tsx +133 -88
  57. package/src/screens/NewAddressScreen.tsx +187 -0
  58. package/src/screens/OrdersScreen.tsx +162 -50
  59. package/src/screens/ProductDetailScreen.tsx +641 -190
  60. package/src/screens/ProfileScreen.tsx +192 -116
  61. package/src/screens/RegisterScreen.tsx +193 -144
  62. package/src/screens/SearchResultsScreen.tsx +165 -0
  63. package/src/screens/ShopScreen.tsx +1110 -146
  64. package/src/screens/WishlistScreen.tsx +428 -0
  65. package/src/lib/Apis/models/inventory-paginated-response.ts +0 -75
  66. package/src/lib/api/auth.ts +0 -81
  67. package/src/lib/api/cart.ts +0 -42
  68. package/src/lib/api/orders.ts +0 -53
  69. package/src/lib/api/products.ts +0 -51
  70. package/src/lib/api-adapter/auth-adapter.ts +0 -196
  71. package/src/lib/api-adapter/cart-adapter.ts +0 -193
  72. package/src/lib/api-adapter/mappers.ts +0 -152
  73. package/src/lib/api-adapter/orders-adapter.ts +0 -195
  74. package/src/lib/api-adapter/products-adapter.ts +0 -194
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hey-pharmacist-ecommerce",
3
- "version": "1.0.5",
4
- "description": "Multi-tenant e-commerce package for Next.js",
3
+ "version": "1.0.7",
4
+ "description": "Production-ready, multi-tenant ecommerce UI + API adapter for Next.js with auth, carts, checkout, orders, theming, and pharmacist-focused UX.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",
@@ -12,39 +12,42 @@
12
12
  "scripts": {
13
13
  "dev": "next dev",
14
14
  "build": "tsup",
15
+ "build:watch": "tsup --watch",
15
16
  "lint": "next lint",
16
- "type-check": "tsc --noEmit"
17
+ "type-check": "tsc --noEmit",
18
+ "prepublishOnly": "npm run build"
17
19
  },
18
20
  "dependencies": {
21
+ "@hookform/resolvers": "^3.3.0",
22
+ "@tanstack/react-query": "^5.90.5",
19
23
  "axios": "^1.6.0",
20
- "framer-motion": "^10.16.0",
24
+ "cookies-next": "^4.0.0",
25
+ "framer-motion": "^10.12.18",
21
26
  "lucide-react": "^0.294.0",
22
- "zod": "^3.22.0",
23
- "@hookform/resolvers": "^3.3.0",
27
+ "react-hook-form": "^7.0.0",
24
28
  "sonner": "^1.2.0",
25
- "zustand": "^4.4.0",
26
- "cookies-next": "^4.0.0",
27
- "react-hook-form": "^7.48.0"
29
+ "zod": "^3.22.0",
30
+ "zustand": "^4.4.0"
28
31
  },
29
32
  "devDependencies": {
30
33
  "@types/node": "^20.0.0",
31
34
  "@types/react": "^18.2.0",
32
35
  "@types/react-dom": "^18.2.0",
33
- "typescript": "^5.3.0",
34
- "tailwindcss": "^3.3.0",
35
- "postcss": "^8.4.0",
36
36
  "autoprefixer": "^10.4.0",
37
- "tsup": "^8.0.0",
38
37
  "eslint": "^8.0.0",
39
38
  "eslint-config-next": "^14.0.0",
39
+ "next": "^14.0.0",
40
+ "postcss": "^8.4.0",
40
41
  "react": "^18.2.0",
41
42
  "react-dom": "^18.2.0",
42
- "next": "^14.0.0"
43
+ "tailwindcss": "^3.3.0",
44
+ "tsup": "^8.0.0",
45
+ "typescript": "^5.3.0"
43
46
  },
44
47
  "peerDependencies": {
48
+ "next": "^14.0.0",
45
49
  "react": "^18.0.0",
46
50
  "react-dom": "^18.0.0",
47
- "next": "^14.0.0",
48
51
  "react-hook-form": "^7.0.0"
49
52
  },
50
53
  "keywords": [
@@ -0,0 +1,171 @@
1
+ import { useState } from 'react';
2
+ import { useForm } from 'react-hook-form';
3
+ import { zodResolver } from '@hookform/resolvers/zod';
4
+ import { Button } from '@/components/ui/Button';
5
+ import { Input } from '@/components/ui/Input';
6
+ import { Modal } from '@/components/ui/Modal';
7
+ import { addressSchema, type AddressFormData } from '@/lib/validations/address';
8
+ import { AddressesApi } from '@/lib/Apis/apis/addresses-api';
9
+ import { Address } from '@/lib/Apis';
10
+ import { AXIOS_CONFIG } from '@/lib/Apis/wrapper';
11
+ import { toast } from 'sonner';
12
+
13
+ interface AddressFormModalProps {
14
+ isOpen: boolean;
15
+ onClose: () => void;
16
+ onAddressAdded?: (address: any) => void;
17
+ onAddressUpdated?: (address: any) => void;
18
+ initialAddress?: Address | null;
19
+ }
20
+
21
+ export function AddressFormModal({ isOpen, onClose, onAddressAdded, onAddressUpdated, initialAddress }: AddressFormModalProps) {
22
+ const [isSubmitting, setIsSubmitting] = useState(false);
23
+
24
+ const {
25
+ register,
26
+ handleSubmit,
27
+ reset,
28
+ formState: { errors }
29
+ } = useForm<AddressFormData>({
30
+ resolver: zodResolver(addressSchema),
31
+ defaultValues: {
32
+ name: initialAddress?.name || '',
33
+ phone: initialAddress?.phone || '',
34
+ street1: initialAddress?.street1 || '',
35
+ street2: initialAddress?.street2 || '',
36
+ city: initialAddress?.city || '',
37
+ state: initialAddress?.state || '',
38
+ zip: initialAddress?.zip || '',
39
+ country: initialAddress?.country || 'United States',
40
+ }
41
+ });
42
+
43
+ // Reset form when switching between create/edit
44
+ // eslint-disable-next-line react-hooks/rules-of-hooks
45
+ useState(() => {
46
+ // Using useState initializer to avoid extra render; react-hook-form defaultValues handle initial load
47
+ });
48
+
49
+ const onSubmit = async (data: AddressFormData) => {
50
+ setIsSubmitting(true);
51
+ try {
52
+ const api = new AddressesApi(AXIOS_CONFIG);
53
+ if (initialAddress?.id) {
54
+ const response = await api.updateUserAddress({
55
+ name: data.name,
56
+ street1: data.street1,
57
+ street2: data.street2,
58
+ city: data.city,
59
+ state: data.state,
60
+ zip: data.zip,
61
+ country: data.country,
62
+ phone: data.phone,
63
+ }, initialAddress.id);
64
+ toast.success('Address updated successfully');
65
+ reset();
66
+ onClose();
67
+ if (onAddressUpdated) onAddressUpdated(response.data);
68
+ } else {
69
+ const response = await api.createAddressForUser({
70
+ name: data.name,
71
+ street1: data.street1,
72
+ street2: data.street2,
73
+ city: data.city,
74
+ state: data.state,
75
+ zip: data.zip,
76
+ country: data.country,
77
+ phone: data.phone,
78
+ });
79
+ if (response.status === 201) {
80
+ toast.success('Address added successfully');
81
+ reset();
82
+ onClose();
83
+ if (onAddressAdded) onAddressAdded(response.data);
84
+ } else {
85
+ toast.error('Failed to add address');
86
+ }
87
+ }
88
+ } catch (error) {
89
+ toast.error('Failed to add address');
90
+ } finally {
91
+ setIsSubmitting(false);
92
+ }
93
+ };
94
+
95
+ return (
96
+ <Modal
97
+ isOpen={isOpen}
98
+ onClose={onClose}
99
+ title={initialAddress ? 'Edit Address' : 'Add New Address'}
100
+ size="lg"
101
+ >
102
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
103
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
104
+ <Input
105
+ label="Full name"
106
+ placeholder="John Doe"
107
+ {...register('name')}
108
+ error={errors.name?.message}
109
+ />
110
+ <Input
111
+ label="Phone number"
112
+ placeholder="+1 (555) 123-4567"
113
+ {...register('phone')}
114
+ error={errors.phone?.message}
115
+ />
116
+ <div className="md:col-span-2">
117
+ <Input
118
+ label="Address line 1"
119
+ placeholder="123 Main St"
120
+ {...register('street1')}
121
+ error={errors.street1?.message}
122
+ />
123
+ </div>
124
+ <div className="md:col-span-2">
125
+ <Input
126
+ label="Address line 2 (optional)"
127
+ placeholder="Apt 4B"
128
+ {...register('street2')}
129
+ />
130
+ </div>
131
+ <Input
132
+ label="City"
133
+ placeholder="New York"
134
+ {...register('city')}
135
+ error={errors.city?.message}
136
+ />
137
+ <Input
138
+ label="State"
139
+ placeholder="NY"
140
+ {...register('state')}
141
+ error={errors.state?.message}
142
+ />
143
+ <Input
144
+ label="ZIP code"
145
+ placeholder="10001"
146
+ {...register('zip')}
147
+ error={errors.zip?.message}
148
+ />
149
+ <Input
150
+ label="Country"
151
+ placeholder="United States"
152
+ {...register('country')}
153
+ error={errors.country?.message}
154
+ />
155
+ </div>
156
+ <div className="flex justify-end gap-4">
157
+ <Button
158
+ type="button"
159
+ variant="outline"
160
+ onClick={onClose}
161
+ >
162
+ Cancel
163
+ </Button>
164
+ <Button type="submit" disabled={isSubmitting}>
165
+ {isSubmitting ? 'Adding Address...' : 'Add Address'}
166
+ </Button>
167
+ </div>
168
+ </form>
169
+ </Modal>
170
+ );
171
+ }
@@ -3,13 +3,14 @@
3
3
  import React, { useState } from 'react';
4
4
  import { motion } from 'framer-motion';
5
5
  import { Minus, Plus, Trash2 } from 'lucide-react';
6
- import { CartItem as CartItemType } from '@/lib/types';
7
6
  import { formatPrice } from '@/lib/utils/format';
8
7
  import { useCart } from '@/providers/CartProvider';
9
8
  import Image from 'next/image';
9
+ import { CartItemPopulated } from '@/lib/Apis';
10
+
10
11
 
11
12
  interface CartItemProps {
12
- item: CartItemType;
13
+ item: CartItemPopulated;
13
14
  }
14
15
 
15
16
  export function CartItem({ item }: CartItemProps) {
@@ -20,17 +21,17 @@ export function CartItem({ item }: CartItemProps) {
20
21
  if (newQuantity < 1) return;
21
22
  setIsUpdating(true);
22
23
  try {
23
- await updateQuantity(item.productId, newQuantity);
24
+ await updateQuantity(item.productVariantId, newQuantity);
24
25
  } finally {
25
26
  setIsUpdating(false);
26
27
  }
27
28
  };
28
29
 
29
30
  const handleRemove = async () => {
30
- await removeFromCart(item.productId);
31
+ await removeFromCart(item.productVariantId);
31
32
  };
32
33
 
33
- const itemTotal = item.product.price * item.quantity;
34
+ const itemTotal = item.productVariantData.finalPrice * item.quantity;
34
35
 
35
36
  return (
36
37
  <motion.div
@@ -43,8 +44,8 @@ export function CartItem({ item }: CartItemProps) {
43
44
  {/* Product Image */}
44
45
  <div className="relative w-24 h-24 rounded-lg overflow-hidden flex-shrink-0 bg-gray-100">
45
46
  <Image
46
- src={item.product.images[0] || '/placeholder-product.jpg'}
47
- alt={item.product.name}
47
+ src={item.productVariantData.productMedia[0]?.file || '/placeholder-product.jpg'}
48
+ alt={item.productVariantData.name}
48
49
  fill
49
50
  className="object-cover"
50
51
  />
@@ -53,10 +54,10 @@ export function CartItem({ item }: CartItemProps) {
53
54
  {/* Product Info */}
54
55
  <div className="flex-1 min-w-0">
55
56
  <h3 className="text-lg font-semibold text-gray-900 truncate">
56
- {item.product.name}
57
+ {item.productVariantData.name}
57
58
  </h3>
58
59
  <p className="text-sm text-gray-500 mt-1">
59
- {formatPrice(item.product.price)} each
60
+ {formatPrice(item.productVariantData.finalPrice)} each
60
61
  </p>
61
62
 
62
63
  {/* Quantity Controls */}
@@ -64,17 +65,21 @@ export function CartItem({ item }: CartItemProps) {
64
65
  <div className="flex items-center border-2 border-gray-200 rounded-lg">
65
66
  <button
66
67
  onClick={() => handleUpdateQuantity(item.quantity - 1)}
67
- disabled={isUpdating || item.quantity <= 1}
68
+ disabled={isUpdating || item.quantity <= 0}
68
69
  className="p-2 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
69
70
  >
70
71
  <Minus className="w-4 h-4" />
71
72
  </button>
72
73
  <span className="px-4 font-medium min-w-[3rem] text-center">
73
- {item.quantity}
74
+ {isUpdating ? (
75
+ <span className="inline-block h-4 w-4 align-middle animate-spin rounded-full border-2 border-gray-300 border-t-gray-600" />
76
+ ) : (
77
+ item.quantity
78
+ )}
74
79
  </span>
75
80
  <button
76
81
  onClick={() => handleUpdateQuantity(item.quantity + 1)}
77
- disabled={isUpdating}
82
+ disabled={isUpdating || item.quantity >= item.productVariantData.inventoryCount}
78
83
  className="p-2 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
79
84
  >
80
85
  <Plus className="w-4 h-4" />
@@ -0,0 +1,195 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { AnimatePresence, motion } from 'framer-motion';
3
+ import { Check, Search } from 'lucide-react';
4
+
5
+ interface FilterChipsProps {
6
+ label: string;
7
+ icon: React.ComponentType<{ className?: string }>;
8
+ filters: string[];
9
+ selected: string;
10
+ onSelect: (value: string) => void;
11
+ maxVisible?: number;
12
+ variant?: 'primary' | 'secondary';
13
+ }
14
+
15
+ export function FilterChips({
16
+ label,
17
+ icon: Icon,
18
+ filters,
19
+ selected,
20
+ onSelect,
21
+ maxVisible = 4,
22
+ variant = 'primary',
23
+ }: FilterChipsProps) {
24
+ const [isOverflowOpen, setIsOverflowOpen] = useState(false);
25
+ const [filterSearchTerm, setFilterSearchTerm] = useState('');
26
+ const overflowMenuRef = useRef<HTMLDivElement | null>(null);
27
+
28
+ const color = variant === 'primary' ? 'primary' : 'secondary';
29
+
30
+ const { visibleFilters, overflowFilters } = useMemo(() => {
31
+ const basePrimary = filters.slice(0, maxVisible);
32
+
33
+ if (basePrimary.includes(selected)) {
34
+ return {
35
+ visibleFilters: basePrimary,
36
+ overflowFilters: filters.filter((filter) => !basePrimary.includes(filter)),
37
+ };
38
+ }
39
+
40
+ const adjustedPrimary = [
41
+ ...basePrimary.slice(0, maxVisible - 1),
42
+ selected,
43
+ ];
44
+
45
+ const uniquePrimary = adjustedPrimary.filter(
46
+ (filter, index, self) => self.indexOf(filter) === index
47
+ );
48
+
49
+ return {
50
+ visibleFilters: uniquePrimary,
51
+ overflowFilters: filters.filter((filter) => !uniquePrimary.includes(filter)),
52
+ };
53
+ }, [filters, maxVisible, selected]);
54
+
55
+ const filteredOverflowFilters = useMemo(() => {
56
+ if (!filterSearchTerm.trim()) return overflowFilters;
57
+ return overflowFilters.filter((filter) =>
58
+ filter.toLowerCase().includes(filterSearchTerm.toLowerCase())
59
+ );
60
+ }, [filterSearchTerm, overflowFilters]);
61
+
62
+ useEffect(() => {
63
+ if (!isOverflowOpen) {
64
+ setFilterSearchTerm('');
65
+ }
66
+ }, [isOverflowOpen]);
67
+
68
+ useEffect(() => {
69
+ function handleClickOutside(event: MouseEvent) {
70
+ if (overflowMenuRef.current && !overflowMenuRef.current.contains(event.target as Node)) {
71
+ setIsOverflowOpen(false);
72
+ }
73
+ }
74
+
75
+ if (isOverflowOpen) {
76
+ document.addEventListener('mousedown', handleClickOutside);
77
+ }
78
+
79
+ return () => {
80
+ document.removeEventListener('mousedown', handleClickOutside);
81
+ };
82
+ }, [isOverflowOpen]);
83
+
84
+ return (
85
+ <div className="flex flex-col gap-3 md:flex-row md:items-center md:gap-4">
86
+ <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-slate-600">
87
+ <Icon className="h-4 w-4" />
88
+ {label}
89
+ </span>
90
+ <div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-3">
91
+ <div className="flex flex-wrap gap-2">
92
+ {visibleFilters.map((filter) => (
93
+ <button
94
+ key={filter}
95
+ type="button"
96
+ onClick={() => onSelect(filter)}
97
+ className={`rounded-full border px-3 py-1 text-sm font-medium transition ${
98
+ selected === filter
99
+ ? `border-${color}-600 bg-${color}-600 text-white shadow-lg shadow-${color}-500/30`
100
+ : `border-slate-200 bg-slate-50 text-slate-600 hover:border-${color}-300 hover:text-${color}-600`
101
+ }`}
102
+ >
103
+ {filter}
104
+ </button>
105
+ ))}
106
+ </div>
107
+
108
+ {overflowFilters.length > 0 && (
109
+ <div className="relative" ref={overflowMenuRef}>
110
+ <button
111
+ type="button"
112
+ onClick={() => setIsOverflowOpen((prev) => !prev)}
113
+ className={`flex items-center gap-2 rounded-full border px-3 py-1 text-sm font-medium transition ${
114
+ overflowFilters.includes(selected)
115
+ ? `border-${color}-600 bg-${color}-50 text-${color}-700 shadow-lg shadow-${color}-500/20`
116
+ : 'border-slate-200 bg-white text-slate-600 hover:border-slate-300'
117
+ }`}
118
+ >
119
+ <span>{overflowFilters.includes(selected) ? selected : 'More'}</span>
120
+ <span className={`inline-flex h-5 min-w-[1.5rem] items-center justify-center rounded-full bg-${color}-100 px-1 text-xs font-semibold text-${color}-600`}>
121
+ {overflowFilters.length}
122
+ </span>
123
+ </button>
124
+
125
+ <AnimatePresence>
126
+ {isOverflowOpen && (
127
+ <motion.div
128
+ initial={{ opacity: 0, y: 8 }}
129
+ animate={{ opacity: 1, y: 0 }}
130
+ exit={{ opacity: 0, y: 8 }}
131
+ transition={{ duration: 0.15 }}
132
+ className="absolute right-0 z-50 mt-2 w-64 rounded-2xl border border-slate-100 bg-white shadow-xl shadow-primary-50"
133
+ >
134
+ <div className="border-b border-slate-100 px-4 py-3">
135
+ <div className="relative">
136
+ <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
137
+ <input
138
+ type="text"
139
+ placeholder={`Search ${label.toLowerCase()}`}
140
+ value={filterSearchTerm}
141
+ onChange={(event) => setFilterSearchTerm(event.target.value)}
142
+ className="w-full rounded-full border border-slate-200 bg-slate-50 py-2 pl-9 pr-3 text-sm text-slate-600 outline-none transition focus:border-primary-300 focus:bg-white focus:ring-2 focus:ring-primary-200"
143
+ />
144
+ </div>
145
+ </div>
146
+ <div className="max-h-60 overflow-y-auto px-2 py-2">
147
+ {filteredOverflowFilters.length > 0 ? (
148
+ filteredOverflowFilters.map((filter) => (
149
+ <button
150
+ key={filter}
151
+ type="button"
152
+ onClick={() => {
153
+ onSelect(filter);
154
+ setIsOverflowOpen(false);
155
+ }}
156
+ className={`flex w-full items-center justify-between rounded-xl px-3 py-2 text-sm font-medium transition ${
157
+ selected === filter
158
+ ? `bg-${color}-600 text-white shadow-lg shadow-${color}-500/30`
159
+ : 'text-slate-600 hover:bg-slate-100'
160
+ }`}
161
+ >
162
+ <span>{filter}</span>
163
+ {selected === filter && <Check className="h-4 w-4" />}
164
+ </button>
165
+ ))
166
+ ) : (
167
+ <p className="px-3 py-4 text-sm text-slate-500">No items found.</p>
168
+ )}
169
+ </div>
170
+ <div className="flex items-center justify-between gap-2 border-t border-slate-100 px-4 py-3">
171
+ <span className="text-xs font-semibold uppercase tracking-wide text-slate-400">
172
+ Quick actions
173
+ </span>
174
+ <button
175
+ type="button"
176
+ onClick={() => {
177
+ onSelect('All');
178
+ setIsOverflowOpen(false);
179
+ }}
180
+ className="text-xs font-semibold uppercase tracking-wide text-primary-600 hover:text-primary-700"
181
+ >
182
+ Reset to All
183
+ </button>
184
+ </div>
185
+ </motion.div>
186
+ )}
187
+ </AnimatePresence>
188
+ </div>
189
+ )}
190
+ </div>
191
+ </div>
192
+ );
193
+ }
194
+
195
+