hey-pharmacist-ecommerce 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -1
- package/dist/index.d.mts +3636 -316
- package/dist/index.d.ts +3636 -316
- package/dist/index.js +6802 -3866
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6756 -3818
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -14
- package/src/components/AddressFormModal.tsx +171 -0
- package/src/components/CartItem.tsx +17 -12
- package/src/components/FilterChips.tsx +195 -0
- package/src/components/Header.tsx +121 -71
- package/src/components/OrderCard.tsx +18 -25
- package/src/components/ProductCard.tsx +209 -72
- package/src/components/ui/Button.tsx +13 -5
- package/src/components/ui/Card.tsx +46 -0
- package/src/hooks/useAddresses.ts +83 -0
- package/src/hooks/useOrders.ts +37 -19
- package/src/hooks/useProducts.ts +55 -63
- package/src/hooks/useWishlistProducts.ts +75 -0
- package/src/index.ts +3 -19
- package/src/lib/Apis/api.ts +1 -0
- package/src/lib/Apis/apis/cart-api.ts +3 -3
- package/src/lib/Apis/apis/inventory-api.ts +0 -108
- package/src/lib/Apis/apis/stores-api.ts +70 -0
- package/src/lib/Apis/apis/wishlist-api.ts +447 -0
- package/src/lib/Apis/models/cart-item-populated.ts +0 -1
- package/src/lib/Apis/models/create-single-variant-product-dto.ts +3 -10
- package/src/lib/Apis/models/create-variant-dto.ts +26 -33
- package/src/lib/Apis/models/extended-product-dto.ts +20 -24
- package/src/lib/Apis/models/index.ts +2 -1
- package/src/lib/Apis/models/order-time-line-dto.ts +49 -0
- package/src/lib/Apis/models/order.ts +3 -8
- package/src/lib/Apis/models/populated-order.ts +3 -8
- package/src/lib/Apis/models/product-variant.ts +29 -0
- package/src/lib/Apis/models/update-product-variant-dto.ts +16 -23
- package/src/lib/Apis/models/wishlist.ts +51 -0
- package/src/lib/Apis/wrapper.ts +18 -7
- package/src/lib/api-adapter/index.ts +0 -12
- package/src/lib/types/index.ts +16 -61
- package/src/lib/utils/colors.ts +7 -4
- package/src/lib/utils/format.ts +1 -1
- package/src/lib/validations/address.ts +14 -0
- package/src/providers/AuthProvider.tsx +61 -31
- package/src/providers/CartProvider.tsx +18 -28
- package/src/providers/EcommerceProvider.tsx +7 -0
- package/src/providers/FavoritesProvider.tsx +86 -0
- package/src/providers/ThemeProvider.tsx +16 -1
- package/src/providers/WishlistProvider.tsx +174 -0
- package/src/screens/AddressesScreen.tsx +484 -0
- package/src/screens/CartScreen.tsx +120 -84
- package/src/screens/CategoriesScreen.tsx +120 -0
- package/src/screens/CheckoutScreen.tsx +919 -241
- package/src/screens/CurrentOrdersScreen.tsx +125 -61
- package/src/screens/HomeScreen.tsx +209 -0
- package/src/screens/LoginScreen.tsx +133 -88
- package/src/screens/NewAddressScreen.tsx +187 -0
- package/src/screens/OrdersScreen.tsx +162 -50
- package/src/screens/ProductDetailScreen.tsx +641 -190
- package/src/screens/ProfileScreen.tsx +192 -116
- package/src/screens/RegisterScreen.tsx +193 -144
- package/src/screens/SearchResultsScreen.tsx +165 -0
- package/src/screens/ShopScreen.tsx +1110 -146
- package/src/screens/WishlistScreen.tsx +428 -0
- package/src/lib/Apis/models/inventory-paginated-response.ts +0 -75
- package/src/lib/api/auth.ts +0 -81
- package/src/lib/api/cart.ts +0 -42
- package/src/lib/api/orders.ts +0 -53
- package/src/lib/api/products.ts +0 -51
- package/src/lib/api-adapter/auth-adapter.ts +0 -196
- package/src/lib/api-adapter/cart-adapter.ts +0 -193
- package/src/lib/api-adapter/mappers.ts +0 -152
- package/src/lib/api-adapter/orders-adapter.ts +0 -195
- package/src/lib/api-adapter/products-adapter.ts +0 -194
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import React, {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
10
|
+
import {
|
|
11
|
+
ArrowUpDown,
|
|
12
|
+
ChevronDown,
|
|
13
|
+
Clock,
|
|
14
|
+
LayoutGrid,
|
|
15
|
+
LayoutList,
|
|
16
|
+
Package,
|
|
17
|
+
Search,
|
|
18
|
+
ShieldCheck,
|
|
19
|
+
SlidersHorizontal,
|
|
20
|
+
Sparkles,
|
|
21
|
+
TrendingUp,
|
|
22
|
+
X,
|
|
23
|
+
} from 'lucide-react';
|
|
24
|
+
import Image from 'next/image';
|
|
25
|
+
import { useRouter } from 'next/navigation';
|
|
6
26
|
import { ProductCard } from '@/components/ProductCard';
|
|
7
27
|
import { ProductCardSkeleton } from '@/components/ui/Skeleton';
|
|
8
28
|
import { EmptyState } from '@/components/EmptyState';
|
|
@@ -10,224 +30,1168 @@ import { Button } from '@/components/ui/Button';
|
|
|
10
30
|
import { Input } from '@/components/ui/Input';
|
|
11
31
|
import { useProducts, useCategories } from '@/hooks/useProducts';
|
|
12
32
|
import { ProductFilters } from '@/lib/types';
|
|
13
|
-
import {
|
|
14
|
-
|
|
33
|
+
import { formatPrice } from '@/lib/utils/format';
|
|
34
|
+
|
|
35
|
+
type SortOption = 'featured' | 'price-low-high' | 'price-high-low' | 'newest';
|
|
36
|
+
type ViewMode = 'grid' | 'list';
|
|
15
37
|
|
|
16
|
-
|
|
38
|
+
interface ShopScreenProps {
|
|
39
|
+
initialFilters?: ProductFilters;
|
|
40
|
+
categoryName?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProps) {
|
|
17
44
|
const router = useRouter();
|
|
18
|
-
const [filters, setFilters] = useState<ProductFilters>(
|
|
45
|
+
const [filters, setFilters] = useState<ProductFilters>(initialFilters);
|
|
19
46
|
const [page, setPage] = useState(1);
|
|
20
47
|
const [showFilters, setShowFilters] = useState(false);
|
|
21
48
|
const [searchQuery, setSearchQuery] = useState('');
|
|
49
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
50
|
+
const [sortOption, setSortOption] = useState<SortOption>('featured');
|
|
51
|
+
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
|
52
|
+
const [selectedPriceRange, setSelectedPriceRange] = useState<string | null>(null);
|
|
53
|
+
const [customPrice, setCustomPrice] = useState<{ min: string; max: string }>({
|
|
54
|
+
min: '',
|
|
55
|
+
max: '',
|
|
56
|
+
});
|
|
57
|
+
const [expandedCategories, setExpandedCategories] = useState<Record<string, boolean>>({});
|
|
22
58
|
|
|
23
59
|
const { products, isLoading, pagination } = useProducts(filters, page, 20);
|
|
24
60
|
const { categories } = useCategories();
|
|
25
61
|
|
|
26
62
|
const handleSearch = (e: React.FormEvent) => {
|
|
27
63
|
e.preventDefault();
|
|
28
|
-
|
|
29
|
-
|
|
64
|
+
if (searchQuery.trim()) {
|
|
65
|
+
setIsSearching(true);
|
|
66
|
+
router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`);
|
|
67
|
+
}
|
|
30
68
|
};
|
|
31
69
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
70
|
+
// Handle search input changes
|
|
71
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
72
|
+
setSearchQuery(e.target.value);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Handle search when pressing Enter
|
|
76
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
77
|
+
if (e.key === 'Enter' && searchQuery.trim()) {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
setIsSearching(true);
|
|
80
|
+
router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`);
|
|
81
|
+
}
|
|
35
82
|
};
|
|
36
83
|
|
|
37
|
-
const
|
|
84
|
+
const priceRanges = useMemo(
|
|
85
|
+
() => [
|
|
86
|
+
{ label: `Under ${formatPrice(25)}`, value: 'under-25', min: undefined, max: 25 },
|
|
87
|
+
{ label: `${formatPrice(25)} - ${formatPrice(50)}`, value: '25-50', min: 25, max: 50 },
|
|
88
|
+
{ label: `${formatPrice(50)} - ${formatPrice(100)}`, value: '50-100', min: 50, max: 100 },
|
|
89
|
+
{ label: `Over ${formatPrice(100)}`, value: 'over-100', min: 100, max: undefined },
|
|
90
|
+
],
|
|
91
|
+
[]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
setCustomPrice({
|
|
96
|
+
min: filters.minPrice !== undefined ? String(filters.minPrice) : '',
|
|
97
|
+
max: filters.maxPrice !== undefined ? String(filters.maxPrice) : '',
|
|
98
|
+
});
|
|
99
|
+
}, [filters.minPrice, filters.maxPrice]);
|
|
100
|
+
|
|
101
|
+
// Auto-expand active category or parent of active subcategory
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const updates: Record<string, boolean> = {};
|
|
104
|
+
if (filters.category) updates[filters.category] = true;
|
|
105
|
+
if (filters.subCategory) {
|
|
106
|
+
const parent = categories.find((c) =>
|
|
107
|
+
c.categorySubCategories?.some((sc) => sc.id === filters.subCategory)
|
|
108
|
+
);
|
|
109
|
+
if (parent?.id) updates[parent.id] = true;
|
|
110
|
+
}
|
|
111
|
+
if (Object.keys(updates).length) {
|
|
112
|
+
setExpandedCategories((prev) => ({ ...prev, ...updates }));
|
|
113
|
+
}
|
|
114
|
+
}, [filters.category, filters.subCategory, categories]);
|
|
115
|
+
|
|
116
|
+
const toggleCategoryExpand = useCallback((id: string) => {
|
|
117
|
+
setExpandedCategories((prev) => ({ ...prev, [id]: !prev[id] }));
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
const sortedCategories = useMemo(
|
|
121
|
+
() => [...categories].sort((a, b) => a.name?.localeCompare(b.name ?? '') ?? 0),
|
|
122
|
+
[categories]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const topCategories = useMemo(
|
|
126
|
+
() =>
|
|
127
|
+
[...categories]
|
|
128
|
+
.sort((a, b) => (b.productCount ?? 0) - (a.productCount ?? 0))
|
|
129
|
+
.slice(0, 6),
|
|
130
|
+
[categories]
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const productInsights = useMemo(() => {
|
|
134
|
+
if (!products.length) {
|
|
135
|
+
return { newArrivals: 0, inStockCount: 0 };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const monthAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
139
|
+
let newArrivals = 0;
|
|
140
|
+
let inStockCount = 0;
|
|
141
|
+
|
|
142
|
+
products.forEach((product) => {
|
|
143
|
+
if (product.inventoryCount > 0) inStockCount += 1;
|
|
144
|
+
if (new Date(product.createdAt).getTime() >= monthAgo) newArrivals += 1;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return { newArrivals, inStockCount };
|
|
148
|
+
}, [products]);
|
|
149
|
+
const insightCards = useMemo(
|
|
150
|
+
() => [
|
|
151
|
+
{
|
|
152
|
+
id: 'new',
|
|
153
|
+
label: 'New arrivals',
|
|
154
|
+
value: productInsights.newArrivals
|
|
155
|
+
? productInsights.newArrivals.toLocaleString()
|
|
156
|
+
: isLoading
|
|
157
|
+
? '...'
|
|
158
|
+
: '0',
|
|
159
|
+
helper: filters.newArrivals ? 'Filter active: showing last 30 days' : 'Click to show last 30 days',
|
|
160
|
+
icon: Sparkles,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'stock',
|
|
164
|
+
label: 'Available now',
|
|
165
|
+
value: productInsights.inStockCount
|
|
166
|
+
? productInsights.inStockCount.toLocaleString()
|
|
167
|
+
: isLoading
|
|
168
|
+
? '...'
|
|
169
|
+
: '0',
|
|
170
|
+
helper: 'Ready to ship today',
|
|
171
|
+
icon: ShieldCheck,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'catalogue',
|
|
175
|
+
label: 'Total products',
|
|
176
|
+
value:
|
|
177
|
+
pagination.total || products.length
|
|
178
|
+
? (pagination.total || products.length).toLocaleString()
|
|
179
|
+
: isLoading
|
|
180
|
+
? '...'
|
|
181
|
+
: '0',
|
|
182
|
+
helper: 'Across all categories',
|
|
183
|
+
icon: TrendingUp,
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
[
|
|
187
|
+
isLoading,
|
|
188
|
+
pagination.total,
|
|
189
|
+
productInsights.inStockCount,
|
|
190
|
+
productInsights.newArrivals,
|
|
191
|
+
products.length,
|
|
192
|
+
filters.newArrivals,
|
|
193
|
+
]
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const filteredProducts = useMemo(() => {
|
|
197
|
+
if (isLoading) return products;
|
|
198
|
+
let items = [...products];
|
|
199
|
+
if (filters.tags?.length) {
|
|
200
|
+
items = items.filter((p) => Array.isArray(p.tags) && p.tags.some((t) => filters.tags!.includes(t)));
|
|
201
|
+
}
|
|
202
|
+
if (filters.newArrivals) {
|
|
203
|
+
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
204
|
+
items = items.filter((p) => new Date(p.createdAt).getTime() >= thirtyDaysAgo);
|
|
205
|
+
}
|
|
206
|
+
return items;
|
|
207
|
+
}, [isLoading, products, filters.tags, filters.newArrivals]);
|
|
208
|
+
|
|
209
|
+
const sortedProducts = useMemo(() => {
|
|
210
|
+
if (isLoading) {
|
|
211
|
+
return filteredProducts;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const items = [...filteredProducts];
|
|
215
|
+
|
|
216
|
+
switch (sortOption) {
|
|
217
|
+
case 'price-low-high':
|
|
218
|
+
return items.sort((a, b) => a.finalPrice - b.finalPrice);
|
|
219
|
+
case 'price-high-low':
|
|
220
|
+
return items.sort((a, b) => b.finalPrice - a.finalPrice);
|
|
221
|
+
case 'newest':
|
|
222
|
+
return items.sort(
|
|
223
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
224
|
+
);
|
|
225
|
+
default:
|
|
226
|
+
return items;
|
|
227
|
+
}
|
|
228
|
+
}, [isLoading, filteredProducts, sortOption]);
|
|
229
|
+
|
|
230
|
+
const displayedProducts = sortedProducts;
|
|
231
|
+
|
|
232
|
+
const quickSearches = useMemo(() => {
|
|
233
|
+
const counts = new Map<string, number>();
|
|
234
|
+
products.forEach((p) => {
|
|
235
|
+
(p.tags || []).forEach((t) => counts.set(t, (counts.get(t) || 0) + 1));
|
|
236
|
+
});
|
|
237
|
+
return Array.from(counts.entries())
|
|
238
|
+
.sort((a, b) => b[1] - a[1])
|
|
239
|
+
.slice(0, 4)
|
|
240
|
+
.map(([tag]) => tag);
|
|
241
|
+
}, [products]);
|
|
242
|
+
|
|
243
|
+
const handleQuickSearch = useCallback((term: string) => {
|
|
244
|
+
setSearchQuery('');
|
|
245
|
+
setFilters((current) => ({
|
|
246
|
+
...current,
|
|
247
|
+
search: undefined,
|
|
248
|
+
tags: [term],
|
|
249
|
+
}));
|
|
250
|
+
setPage(1);
|
|
251
|
+
}, []);
|
|
252
|
+
|
|
253
|
+
const handleCategoryChange = useCallback(
|
|
254
|
+
(categorySlug: string) => {
|
|
255
|
+
setFilters((current) => {
|
|
256
|
+
if (current.category === categorySlug) {
|
|
257
|
+
const { category, subCategory, ...rest } = current;
|
|
258
|
+
return rest;
|
|
259
|
+
}
|
|
260
|
+
const next = { ...current, category: categorySlug } as ProductFilters;
|
|
261
|
+
// clear subCategory when switching parent category
|
|
262
|
+
if (next.subCategory) {
|
|
263
|
+
delete (next as any).subCategory;
|
|
264
|
+
}
|
|
265
|
+
return next;
|
|
266
|
+
});
|
|
267
|
+
setPage(1);
|
|
268
|
+
},
|
|
269
|
+
[]
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const handleSubCategoryChange = useCallback((parentCategoryId: string, subCategoryId: string) => {
|
|
273
|
+
setFilters((current) => {
|
|
274
|
+
if (current.subCategory === subCategoryId) {
|
|
275
|
+
const { subCategory, ...rest } = current as any;
|
|
276
|
+
return rest as ProductFilters;
|
|
277
|
+
}
|
|
278
|
+
return { ...current, category: parentCategoryId, subCategory: subCategoryId } as ProductFilters;
|
|
279
|
+
});
|
|
280
|
+
setPage(1);
|
|
281
|
+
}, []);
|
|
282
|
+
|
|
283
|
+
const handleToggleStock = useCallback(() => {
|
|
284
|
+
setFilters((current) => {
|
|
285
|
+
if (current.inStock) {
|
|
286
|
+
const { inStock, ...rest } = current;
|
|
287
|
+
return rest;
|
|
288
|
+
}
|
|
289
|
+
return { ...current, inStock: true };
|
|
290
|
+
});
|
|
291
|
+
setPage(1);
|
|
292
|
+
}, []);
|
|
293
|
+
|
|
294
|
+
const handleToggleNewArrivals = useCallback(() => {
|
|
295
|
+
setFilters((current) => {
|
|
296
|
+
if (current.newArrivals) {
|
|
297
|
+
const { newArrivals, ...rest } = current;
|
|
298
|
+
return rest as typeof current;
|
|
299
|
+
}
|
|
300
|
+
return { ...current, newArrivals: true };
|
|
301
|
+
});
|
|
302
|
+
setPage(1);
|
|
303
|
+
}, []);
|
|
304
|
+
|
|
305
|
+
const handleClearFilters = useCallback(() => {
|
|
38
306
|
setFilters({});
|
|
39
307
|
setSearchQuery('');
|
|
308
|
+
setSelectedPriceRange(null);
|
|
309
|
+
setCustomPrice({ min: '', max: '' });
|
|
40
310
|
setPage(1);
|
|
41
|
-
};
|
|
311
|
+
}, []);
|
|
42
312
|
|
|
43
|
-
const
|
|
313
|
+
const handleRemoveCategory = useCallback(() => {
|
|
314
|
+
setFilters((current) => {
|
|
315
|
+
const { category, subCategory, ...rest } = current as any;
|
|
316
|
+
return rest;
|
|
317
|
+
});
|
|
318
|
+
setPage(1);
|
|
319
|
+
}, []);
|
|
320
|
+
|
|
321
|
+
const handleRemoveSubCategory = useCallback(() => {
|
|
322
|
+
setFilters((current) => {
|
|
323
|
+
const next = { ...current } as any;
|
|
324
|
+
delete next.subCategory;
|
|
325
|
+
return next as ProductFilters;
|
|
326
|
+
});
|
|
327
|
+
setPage(1);
|
|
328
|
+
}, []);
|
|
329
|
+
|
|
330
|
+
const handleRemoveSearch = useCallback(() => {
|
|
331
|
+
setFilters((current) => {
|
|
332
|
+
const { search, ...rest } = current;
|
|
333
|
+
return rest;
|
|
334
|
+
});
|
|
335
|
+
setSearchQuery('');
|
|
336
|
+
setPage(1);
|
|
337
|
+
}, []);
|
|
338
|
+
|
|
339
|
+
const handleRemoveInStock = useCallback(() => {
|
|
340
|
+
setFilters((current) => {
|
|
341
|
+
const { inStock, ...rest } = current;
|
|
342
|
+
return rest;
|
|
343
|
+
});
|
|
344
|
+
setPage(1);
|
|
345
|
+
}, []);
|
|
346
|
+
|
|
347
|
+
const handleRemovePrice = useCallback(() => {
|
|
348
|
+
setFilters((current) => {
|
|
349
|
+
const next = { ...current };
|
|
350
|
+
delete next.minPrice;
|
|
351
|
+
delete next.maxPrice;
|
|
352
|
+
return next;
|
|
353
|
+
});
|
|
354
|
+
setSelectedPriceRange(null);
|
|
355
|
+
setCustomPrice({ min: '', max: '' });
|
|
356
|
+
setPage(1);
|
|
357
|
+
}, []);
|
|
358
|
+
|
|
359
|
+
const handleRemoveTag = useCallback((tag: string) => {
|
|
360
|
+
setFilters((current) => {
|
|
361
|
+
if (!current.tags) return current;
|
|
362
|
+
const updated = current.tags.filter((item) => item !== tag);
|
|
363
|
+
const next = { ...current };
|
|
364
|
+
if (updated.length) {
|
|
365
|
+
next.tags = updated;
|
|
366
|
+
} else {
|
|
367
|
+
delete next.tags;
|
|
368
|
+
}
|
|
369
|
+
return next;
|
|
370
|
+
});
|
|
371
|
+
setPage(1);
|
|
372
|
+
}, []);
|
|
373
|
+
|
|
374
|
+
const handlePriceRangeSelect = useCallback(
|
|
375
|
+
(value: string) => {
|
|
376
|
+
const range = priceRanges.find((item) => item.value === value);
|
|
377
|
+
|
|
378
|
+
if (selectedPriceRange === value) {
|
|
379
|
+
setSelectedPriceRange(null);
|
|
380
|
+
setFilters((current) => {
|
|
381
|
+
const next = { ...current };
|
|
382
|
+
delete next.minPrice;
|
|
383
|
+
delete next.maxPrice;
|
|
384
|
+
return next;
|
|
385
|
+
});
|
|
386
|
+
setCustomPrice({ min: '', max: '' });
|
|
387
|
+
setPage(1);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!range) return;
|
|
392
|
+
|
|
393
|
+
setSelectedPriceRange(value);
|
|
394
|
+
setFilters((current) => {
|
|
395
|
+
const next = { ...current };
|
|
396
|
+
if (range.min !== undefined) {
|
|
397
|
+
next.minPrice = range.min;
|
|
398
|
+
} else {
|
|
399
|
+
delete next.minPrice;
|
|
400
|
+
}
|
|
401
|
+
if (range.max !== undefined) {
|
|
402
|
+
next.maxPrice = range.max;
|
|
403
|
+
} else {
|
|
404
|
+
delete next.maxPrice;
|
|
405
|
+
}
|
|
406
|
+
return next;
|
|
407
|
+
});
|
|
408
|
+
setCustomPrice({
|
|
409
|
+
min: range.min !== undefined ? String(range.min) : '',
|
|
410
|
+
max: range.max !== undefined ? String(range.max) : '',
|
|
411
|
+
});
|
|
412
|
+
setPage(1);
|
|
413
|
+
},
|
|
414
|
+
[priceRanges, selectedPriceRange]
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const applyCustomPrice = useCallback(() => {
|
|
418
|
+
const rawMin = customPrice.min.trim();
|
|
419
|
+
const rawMax = customPrice.max.trim();
|
|
420
|
+
|
|
421
|
+
const minValue = rawMin !== '' ? Number(rawMin) : undefined;
|
|
422
|
+
const maxValue = rawMax !== '' ? Number(rawMax) : undefined;
|
|
423
|
+
|
|
424
|
+
let normalizedMin = minValue;
|
|
425
|
+
let normalizedMax = maxValue;
|
|
426
|
+
|
|
427
|
+
if (
|
|
428
|
+
normalizedMin !== undefined &&
|
|
429
|
+
normalizedMax !== undefined &&
|
|
430
|
+
!Number.isNaN(normalizedMin) &&
|
|
431
|
+
!Number.isNaN(normalizedMax) &&
|
|
432
|
+
normalizedMin > normalizedMax
|
|
433
|
+
) {
|
|
434
|
+
[normalizedMin, normalizedMax] = [normalizedMax, normalizedMin];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
setSelectedPriceRange(null);
|
|
438
|
+
setFilters((current) => {
|
|
439
|
+
const next = { ...current };
|
|
440
|
+
|
|
441
|
+
if (normalizedMin !== undefined && !Number.isNaN(normalizedMin)) {
|
|
442
|
+
next.minPrice = normalizedMin;
|
|
443
|
+
} else {
|
|
444
|
+
delete next.minPrice;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (normalizedMax !== undefined && !Number.isNaN(normalizedMax)) {
|
|
448
|
+
next.maxPrice = normalizedMax;
|
|
449
|
+
} else {
|
|
450
|
+
delete next.maxPrice;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return next;
|
|
454
|
+
});
|
|
455
|
+
setPage(1);
|
|
456
|
+
}, [customPrice]);
|
|
457
|
+
|
|
458
|
+
const {
|
|
459
|
+
search: searchFilter,
|
|
460
|
+
category: categoryFilter,
|
|
461
|
+
subCategory: subCategoryFilter,
|
|
462
|
+
inStock: inStockOnly,
|
|
463
|
+
minPrice,
|
|
464
|
+
maxPrice,
|
|
465
|
+
tags,
|
|
466
|
+
newArrivals,
|
|
467
|
+
} = filters;
|
|
468
|
+
|
|
469
|
+
const activeFilterChips = useMemo(() => {
|
|
470
|
+
const chips: { key: string; label: string; onRemove: () => void }[] = [];
|
|
471
|
+
|
|
472
|
+
if (searchFilter) {
|
|
473
|
+
chips.push({
|
|
474
|
+
key: 'search',
|
|
475
|
+
label: `Search: "${searchFilter}"`,
|
|
476
|
+
onRemove: handleRemoveSearch,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (subCategoryFilter) {
|
|
481
|
+
let subName: string | undefined;
|
|
482
|
+
let parentName: string | undefined;
|
|
483
|
+
categories.forEach((cat) => {
|
|
484
|
+
const found = cat.categorySubCategories?.find((sc) => sc.id === subCategoryFilter);
|
|
485
|
+
if (found) {
|
|
486
|
+
subName = found.name;
|
|
487
|
+
parentName = cat.name;
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
chips.push({
|
|
491
|
+
key: 'subcategory',
|
|
492
|
+
label: `Subcategory: ${subName ?? subCategoryFilter}`,
|
|
493
|
+
onRemove: handleRemoveSubCategory,
|
|
494
|
+
});
|
|
495
|
+
if (categoryFilter) {
|
|
496
|
+
const catObj = categories.find((c) => c.id === categoryFilter);
|
|
497
|
+
chips.push({
|
|
498
|
+
key: 'category',
|
|
499
|
+
label: `Category: ${catObj?.name ?? parentName ?? categoryFilter}`,
|
|
500
|
+
onRemove: handleRemoveCategory,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
} else if (categoryFilter) {
|
|
504
|
+
const category = categories.find((cat) => cat.id === categoryFilter);
|
|
505
|
+
chips.push({
|
|
506
|
+
key: 'category',
|
|
507
|
+
label: `Category: ${category?.name ?? categoryFilter}`,
|
|
508
|
+
onRemove: handleRemoveCategory,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (inStockOnly) {
|
|
513
|
+
chips.push({
|
|
514
|
+
key: 'stock',
|
|
515
|
+
label: 'In stock',
|
|
516
|
+
onRemove: handleRemoveInStock,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (minPrice !== undefined || maxPrice !== undefined) {
|
|
521
|
+
const minLabel = minPrice !== undefined ? formatPrice(minPrice) : 'Any';
|
|
522
|
+
const maxLabel = maxPrice !== undefined ? formatPrice(maxPrice) : 'Any';
|
|
523
|
+
chips.push({
|
|
524
|
+
key: 'price',
|
|
525
|
+
label: `Price: ${minLabel} - ${maxLabel}`,
|
|
526
|
+
onRemove: handleRemovePrice,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (tags?.length) {
|
|
531
|
+
tags.forEach((tag) => {
|
|
532
|
+
chips.push({
|
|
533
|
+
key: `tag-${tag}`,
|
|
534
|
+
label: `Tag: ${tag}`,
|
|
535
|
+
onRemove: () => handleRemoveTag(tag),
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (newArrivals) {
|
|
541
|
+
chips.push({
|
|
542
|
+
key: 'new-arrivals',
|
|
543
|
+
label: 'New arrivals',
|
|
544
|
+
onRemove: handleToggleNewArrivals,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return chips;
|
|
549
|
+
}, [
|
|
550
|
+
categories,
|
|
551
|
+
categoryFilter,
|
|
552
|
+
subCategoryFilter,
|
|
553
|
+
handleRemoveCategory,
|
|
554
|
+
handleRemoveSubCategory,
|
|
555
|
+
handleRemoveInStock,
|
|
556
|
+
handleRemovePrice,
|
|
557
|
+
handleRemoveSearch,
|
|
558
|
+
handleRemoveTag,
|
|
559
|
+
inStockOnly,
|
|
560
|
+
maxPrice,
|
|
561
|
+
minPrice,
|
|
562
|
+
searchFilter,
|
|
563
|
+
tags,
|
|
564
|
+
newArrivals,
|
|
565
|
+
]);
|
|
566
|
+
|
|
567
|
+
const hasActiveFilters = activeFilterChips.length > 0;
|
|
568
|
+
|
|
569
|
+
const isCustomPriceDirty =
|
|
570
|
+
customPrice.min.trim() !== '' || customPrice.max.trim() !== '';
|
|
571
|
+
|
|
572
|
+
const renderFiltersPanel = () => (
|
|
573
|
+
<>
|
|
574
|
+
<div className="space-y-8">
|
|
575
|
+
<div className="space-y-8">
|
|
576
|
+
<div className="flex items-start justify-between gap-3">
|
|
577
|
+
<div>
|
|
578
|
+
<h3 className="text-lg font-semibold text-gray-900">Refine results</h3>
|
|
579
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
580
|
+
Filter by category, price, and availability to find the perfect fit faster.
|
|
581
|
+
</p>
|
|
582
|
+
</div>
|
|
583
|
+
{hasActiveFilters && (
|
|
584
|
+
<button
|
|
585
|
+
type="button"
|
|
586
|
+
onClick={handleClearFilters}
|
|
587
|
+
className="text-sm font-semibold text-primary-600 hover:text-primary-700"
|
|
588
|
+
>
|
|
589
|
+
Clear all
|
|
590
|
+
</button>
|
|
591
|
+
)}
|
|
592
|
+
</div>
|
|
593
|
+
|
|
594
|
+
<div className="space-y-6">
|
|
595
|
+
<div className="space-y-3">
|
|
596
|
+
<h4 className="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500">Categories</h4>
|
|
597
|
+
<div className="space-y-2">
|
|
598
|
+
{sortedCategories.length === 0 && (
|
|
599
|
+
<span className="text-sm text-gray-500">No categories available yet.</span>
|
|
600
|
+
)}
|
|
601
|
+
{sortedCategories.map((category) => {
|
|
602
|
+
const isCategoryActive = categoryFilter === category.id;
|
|
603
|
+
const isExpanded = !!expandedCategories[category.id as string];
|
|
604
|
+
return (
|
|
605
|
+
<div key={category.id} className="rounded-xl border-gray-100 bg-white/50">
|
|
606
|
+
<div
|
|
607
|
+
role="button"
|
|
608
|
+
tabIndex={0}
|
|
609
|
+
onClick={() => {
|
|
610
|
+
// Selecting a category also expands it for quick access to subcategories
|
|
611
|
+
if (!isExpanded) toggleCategoryExpand(category.id ?? '');
|
|
612
|
+
handleCategoryChange(category.id ?? '');
|
|
613
|
+
}}
|
|
614
|
+
className={`flex w-full items-center justify-between rounded-xl px-3 py-2 text-sm font-medium transition ${
|
|
615
|
+
isCategoryActive
|
|
616
|
+
? 'text-primary-700 bg-primary-50'
|
|
617
|
+
: 'text-gray-700 hover:text-primary-700 hover:bg-primary-50/50'
|
|
618
|
+
}`}
|
|
619
|
+
>
|
|
620
|
+
<span className="flex items-center gap-2">
|
|
621
|
+
<span className='font-medium'>{category.name}</span>
|
|
622
|
+
{category.productCount > 0 && (
|
|
623
|
+
<span className={`ml-1 rounded-full bg-gray-100 px-2 py-0.5 text-xs ${isCategoryActive ? 'text-primary-700' : 'text-gray-500'}`}>
|
|
624
|
+
{category.productCount}
|
|
625
|
+
</span>
|
|
626
|
+
)}
|
|
627
|
+
</span>
|
|
628
|
+
<button
|
|
629
|
+
type="button"
|
|
630
|
+
onClick={(e) => {
|
|
631
|
+
e.preventDefault();
|
|
632
|
+
e.stopPropagation();
|
|
633
|
+
toggleCategoryExpand(category.id ?? '');
|
|
634
|
+
}}
|
|
635
|
+
className="rounded-md p-1 hover:bg-gray-100"
|
|
636
|
+
aria-label={isExpanded ? 'Collapse' : 'Expand'}
|
|
637
|
+
>
|
|
638
|
+
<ChevronDown className={`h-4 w-4 transition-transform ${isExpanded ? 'rotate-180 text-primary-600' : 'rotate-0 text-gray-400'}`} />
|
|
639
|
+
</button>
|
|
640
|
+
</div>
|
|
641
|
+
{isExpanded && Array.isArray(category.categorySubCategories) && category.categorySubCategories.length > 0 && (
|
|
642
|
+
<div className="mt-1 border-gray-100 px-2 pb-2 pl-4">
|
|
643
|
+
<div className="divide-y divide-gray-100">
|
|
644
|
+
{category.categorySubCategories.map((sub) => {
|
|
645
|
+
const isSubActive = subCategoryFilter === sub.id;
|
|
646
|
+
return (
|
|
647
|
+
<button
|
|
648
|
+
key={sub.id}
|
|
649
|
+
type="button"
|
|
650
|
+
onClick={() => handleSubCategoryChange(category.id ?? '', sub.id)}
|
|
651
|
+
className={`block w-full px-2 py-2 text-sm text-start transition ${
|
|
652
|
+
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
|
+
)}
|
|
664
|
+
</div>
|
|
665
|
+
);
|
|
666
|
+
})}
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
|
|
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 ${
|
|
685
|
+
isActive
|
|
686
|
+
? 'border-secondary-600 bg-secondary-600 text-white shadow-lg shadow-secondary-500/30'
|
|
687
|
+
: 'border-gray-200 bg-white text-gray-600 hover:border-secondary-300 hover:text-secondary-600'
|
|
688
|
+
}`}
|
|
689
|
+
>
|
|
690
|
+
{range.label}
|
|
691
|
+
</button>
|
|
692
|
+
);
|
|
693
|
+
})}
|
|
694
|
+
</div>
|
|
695
|
+
<div className="grid grid-cols-2 gap-3">
|
|
696
|
+
<Input
|
|
697
|
+
type="number"
|
|
698
|
+
min="0"
|
|
699
|
+
placeholder="Custom min"
|
|
700
|
+
value={customPrice.min}
|
|
701
|
+
onChange={(event) =>
|
|
702
|
+
setCustomPrice((current) => ({ ...current, min: event.target.value }))
|
|
703
|
+
}
|
|
704
|
+
/>
|
|
705
|
+
<Input
|
|
706
|
+
type="number"
|
|
707
|
+
min="0"
|
|
708
|
+
placeholder="Custom max"
|
|
709
|
+
value={customPrice.max}
|
|
710
|
+
onChange={(event) =>
|
|
711
|
+
setCustomPrice((current) => ({ ...current, max: event.target.value }))
|
|
712
|
+
}
|
|
713
|
+
/>
|
|
714
|
+
</div>
|
|
715
|
+
<button
|
|
716
|
+
type="button"
|
|
717
|
+
onClick={applyCustomPrice}
|
|
718
|
+
disabled={!isCustomPriceDirty}
|
|
719
|
+
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"
|
|
720
|
+
>
|
|
721
|
+
Apply price range
|
|
722
|
+
</button>
|
|
723
|
+
</div>
|
|
724
|
+
|
|
725
|
+
<div className="space-y-3">
|
|
726
|
+
<h4 className="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500">
|
|
727
|
+
Availability
|
|
728
|
+
</h4>
|
|
729
|
+
<button
|
|
730
|
+
type="button"
|
|
731
|
+
onClick={handleToggleStock}
|
|
732
|
+
className={`flex w-full items-center justify-between rounded-xl border px-4 py-3 text-sm font-medium transition ${
|
|
733
|
+
inStockOnly
|
|
734
|
+
? 'border-primary-500 bg-primary-50 text-primary-700'
|
|
735
|
+
: 'border-gray-200 bg-white text-gray-600 hover:border-primary-300 hover:text-primary-600'
|
|
736
|
+
}`}
|
|
737
|
+
>
|
|
738
|
+
<span>In stock only</span>
|
|
739
|
+
<Sparkles className="h-4 w-4 text-primary-500" />
|
|
740
|
+
</button>
|
|
741
|
+
<button
|
|
742
|
+
type="button"
|
|
743
|
+
onClick={handleToggleNewArrivals}
|
|
744
|
+
className={`mt-2 flex w-full items-center justify-between rounded-xl border px-4 py-3 text-sm font-medium transition ${
|
|
745
|
+
newArrivals
|
|
746
|
+
? 'border-secondary-500 bg-secondary-50 text-secondary-700'
|
|
747
|
+
: 'border-gray-200 bg-white text-gray-600 hover:border-secondary-300 hover:text-secondary-600'
|
|
748
|
+
}`}
|
|
749
|
+
>
|
|
750
|
+
<span>New arrivals (last 30 days)</span>
|
|
751
|
+
<Sparkles className="h-4 w-4 text-secondary-500" />
|
|
752
|
+
</button>
|
|
753
|
+
</div>
|
|
754
|
+
</div>
|
|
755
|
+
</>
|
|
756
|
+
);
|
|
44
757
|
|
|
45
758
|
return (
|
|
46
|
-
<div className="min-h-screen bg-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
759
|
+
<div className="min-h-screen bg-slate-50">
|
|
760
|
+
<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">
|
|
761
|
+
<div
|
|
762
|
+
className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(255,255,255,0.35),_transparent_60%)]"
|
|
763
|
+
aria-hidden="true"
|
|
764
|
+
/>
|
|
765
|
+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_right,_rgba(94,234,212,0.35),_transparent_55%)] opacity-60" />
|
|
766
|
+
<div className="relative container mx-auto px-4 py-24">
|
|
50
767
|
<motion.div
|
|
51
|
-
initial={{ opacity: 0, y:
|
|
768
|
+
initial={{ opacity: 0, y: 24 }}
|
|
52
769
|
animate={{ opacity: 1, y: 0 }}
|
|
53
|
-
className="max-w-3xl mx-auto text-
|
|
770
|
+
className="max-w-3xl space-y-8 text-center md:mx-auto md:text-left"
|
|
54
771
|
>
|
|
55
|
-
<
|
|
56
|
-
|
|
772
|
+
<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">
|
|
773
|
+
<Sparkles className="h-4 w-4" />
|
|
774
|
+
Wellness products, curated for you
|
|
775
|
+
</span>
|
|
776
|
+
<h1 className="text-4xl font-bold leading-tight md:text-6xl">
|
|
777
|
+
Find pharmacy favorites crafted to keep your family thriving
|
|
57
778
|
</h1>
|
|
58
|
-
<p className="text-
|
|
59
|
-
|
|
779
|
+
<p className="text-lg text-white/80 md:text-xl">
|
|
780
|
+
Explore a modern storefront with real-time inventory, smart filters, and rich
|
|
781
|
+
product details designed to make healthier choices easier.
|
|
60
782
|
</p>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
783
|
+
<form
|
|
784
|
+
onSubmit={handleSearch}
|
|
785
|
+
className="mx-auto max-w-2xl md:mx-0"
|
|
786
|
+
>
|
|
787
|
+
<div className="relative w-full">
|
|
788
|
+
<input
|
|
66
789
|
type="search"
|
|
67
|
-
placeholder="Search for products..."
|
|
790
|
+
placeholder="Search for products, categories, or symptoms..."
|
|
68
791
|
value={searchQuery}
|
|
69
|
-
onChange={
|
|
70
|
-
|
|
792
|
+
onChange={handleInputChange}
|
|
793
|
+
onKeyDown={handleKeyDown}
|
|
794
|
+
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"
|
|
795
|
+
disabled={isSearching}
|
|
71
796
|
/>
|
|
72
797
|
<button
|
|
73
798
|
type="submit"
|
|
74
|
-
className="absolute right-2 top-1/2 -translate-y-1/2
|
|
799
|
+
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"
|
|
800
|
+
disabled={!searchQuery.trim() || isSearching}
|
|
75
801
|
>
|
|
76
|
-
|
|
802
|
+
{isSearching ? (
|
|
803
|
+
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
|
804
|
+
) : (
|
|
805
|
+
<Search className="h-5 w-5" />
|
|
806
|
+
)}
|
|
77
807
|
</button>
|
|
78
808
|
</div>
|
|
79
809
|
</form>
|
|
80
810
|
</motion.div>
|
|
81
|
-
</div>
|
|
82
|
-
</section>
|
|
83
811
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
812
|
+
<motion.div
|
|
813
|
+
initial={{ opacity: 0, y: 24 }}
|
|
814
|
+
animate={{ opacity: 1, y: 0 }}
|
|
815
|
+
transition={{ delay: 0.15 }}
|
|
816
|
+
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"
|
|
817
|
+
>
|
|
818
|
+
<div className="space-y-3">
|
|
819
|
+
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-white/60">
|
|
820
|
+
Explore popular searches
|
|
821
|
+
</p>
|
|
822
|
+
<div className="flex flex-wrap gap-2">
|
|
823
|
+
{quickSearches.map((term) => (
|
|
93
824
|
<button
|
|
94
|
-
|
|
95
|
-
|
|
825
|
+
key={term}
|
|
826
|
+
type="button"
|
|
827
|
+
onClick={() => handleQuickSearch(term)}
|
|
828
|
+
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"
|
|
96
829
|
>
|
|
97
|
-
|
|
830
|
+
{term}
|
|
98
831
|
</button>
|
|
99
|
-
)}
|
|
832
|
+
))}
|
|
100
833
|
</div>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<div className="space-y-
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
834
|
+
</div>
|
|
835
|
+
{topCategories.length > 0 && (
|
|
836
|
+
<div className="space-y-3 md:text-right">
|
|
837
|
+
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-white/60">
|
|
838
|
+
Trending categories
|
|
839
|
+
</p>
|
|
840
|
+
<div className="flex flex-wrap justify-start gap-2 md:justify-end">
|
|
841
|
+
{topCategories.map((category) => (
|
|
842
|
+
<button
|
|
108
843
|
key={category.id}
|
|
109
|
-
|
|
844
|
+
type="button"
|
|
845
|
+
onClick={() => handleCategoryChange(category.id ?? '')}
|
|
846
|
+
className="rounded-full bg-white/15 px-3 py-1.5 text-sm font-medium text-white transition hover:bg-white/25"
|
|
110
847
|
>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
checked={filters.category === category.slug}
|
|
114
|
-
onChange={() => handleCategoryChange(category.slug)}
|
|
115
|
-
className="w-5 h-5 text-primary-600 rounded"
|
|
116
|
-
/>
|
|
117
|
-
<span className="text-gray-700">{category.name}</span>
|
|
118
|
-
<span className="ml-auto text-sm text-gray-500">
|
|
119
|
-
({category.productCount})
|
|
120
|
-
</span>
|
|
121
|
-
</label>
|
|
848
|
+
{category.name}
|
|
849
|
+
</button>
|
|
122
850
|
))}
|
|
123
851
|
</div>
|
|
124
852
|
</div>
|
|
853
|
+
)}
|
|
854
|
+
</motion.div>
|
|
855
|
+
|
|
856
|
+
<div className="mt-10 grid gap-4 md:grid-cols-3">
|
|
857
|
+
{insightCards.map((card, index) => {
|
|
858
|
+
const Icon = card.icon;
|
|
859
|
+
return (
|
|
860
|
+
<motion.div
|
|
861
|
+
key={card.id}
|
|
862
|
+
initial={{ opacity: 0, y: 20 }}
|
|
863
|
+
animate={{ opacity: 1, y: 0 }}
|
|
864
|
+
transition={{ delay: 0.2 + index * 0.05 }}
|
|
865
|
+
className={`rounded-2xl border p-5 backdrop-blur ${
|
|
866
|
+
card.id === 'new' && newArrivals
|
|
867
|
+
? 'border-white/40 bg-white/25 ring-2 ring-white/30'
|
|
868
|
+
: 'border-white/20 bg-white/15'
|
|
869
|
+
} ${card.id === 'new' ? 'cursor-pointer hover:bg-white/20' : ''}`}
|
|
870
|
+
onClick={card.id === 'new' ? handleToggleNewArrivals : undefined}
|
|
871
|
+
role={card.id === 'new' ? 'button' : undefined}
|
|
872
|
+
aria-pressed={card.id === 'new' ? (newArrivals ? 'true' : 'false') : undefined}
|
|
873
|
+
title={card.id === 'new' ? (newArrivals ? 'Filter active: showing products from the last 30 days' : 'Click to filter products from the last 30 days') : undefined}
|
|
874
|
+
>
|
|
875
|
+
<div className="flex items-center justify-between">
|
|
876
|
+
<div>
|
|
877
|
+
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-white/60">
|
|
878
|
+
{card.label}
|
|
879
|
+
</p>
|
|
880
|
+
<p className="mt-2 text-3xl font-semibold text-white">{card.value}</p>
|
|
881
|
+
</div>
|
|
882
|
+
<span className="rounded-full bg-white/20 p-3 text-white">
|
|
883
|
+
<Icon className="h-5 w-5" />
|
|
884
|
+
</span>
|
|
885
|
+
</div>
|
|
886
|
+
<p className="mt-3 text-sm text-white/70">{card.helper}</p>
|
|
887
|
+
</motion.div>
|
|
888
|
+
);
|
|
889
|
+
})}
|
|
890
|
+
</div>
|
|
891
|
+
</div>
|
|
892
|
+
</section>
|
|
125
893
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
checked={filters.inStock === true}
|
|
133
|
-
onChange={(e) =>
|
|
134
|
-
setFilters({ ...filters, inStock: e.target.checked ? true : undefined })
|
|
135
|
-
}
|
|
136
|
-
className="w-5 h-5 text-primary-600 rounded"
|
|
137
|
-
/>
|
|
138
|
-
<span className="text-gray-700">In Stock Only</span>
|
|
139
|
-
</label>
|
|
894
|
+
<div className="relative z-10 -mt-12 pb-16">
|
|
895
|
+
<div className="container mx-auto px-4">
|
|
896
|
+
<div className="flex flex-col gap-8 lg:flex-row">
|
|
897
|
+
<aside className="hidden w-72 flex-shrink-0 lg:block">
|
|
898
|
+
<div className="sticky top-24 rounded-3xl border border-gray-100 bg-white p-6 shadow-xl shadow-gray-200/40">
|
|
899
|
+
{renderFiltersPanel()}
|
|
140
900
|
</div>
|
|
141
|
-
</
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
901
|
+
</aside>
|
|
902
|
+
|
|
903
|
+
<main className="flex-1 space-y-6">
|
|
904
|
+
<div className="rounded-3xl border border-gray-100 bg-white p-6 shadow-sm">
|
|
905
|
+
<div className="flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
|
|
906
|
+
<div>
|
|
907
|
+
<h2 className="text-2xl font-bold text-gray-900">All products</h2>
|
|
908
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
909
|
+
Browse a pharmacy-grade catalogue with smart merchandising and modern UI.
|
|
910
|
+
</p>
|
|
911
|
+
</div>
|
|
912
|
+
<div className="flex flex-col gap-3 md:flex-row md:items-center">
|
|
913
|
+
<div className="relative">
|
|
914
|
+
<ArrowUpDown className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
|
915
|
+
<select
|
|
916
|
+
value={sortOption}
|
|
917
|
+
onChange={(event) => {
|
|
918
|
+
setSortOption(event.target.value as SortOption);
|
|
919
|
+
}}
|
|
920
|
+
className="appearance-none rounded-xl border border-gray-200 bg-white py-2.5 pl-10 pr-9 text-sm font-medium text-gray-700 shadow-sm transition focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/30"
|
|
921
|
+
>
|
|
922
|
+
<option value="featured">Featured products</option>
|
|
923
|
+
<option value="price-low-high">Price: low to high</option>
|
|
924
|
+
<option value="price-high-low">Price: high to low</option>
|
|
925
|
+
<option value="newest">Newest arrivals</option>
|
|
926
|
+
</select>
|
|
927
|
+
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
|
928
|
+
</div>
|
|
929
|
+
<div className="flex items-center rounded-xl border border-gray-200 bg-white shadow-sm">
|
|
930
|
+
<button
|
|
931
|
+
type="button"
|
|
932
|
+
onClick={() => setViewMode('grid')}
|
|
933
|
+
className={`flex items-center gap-2 rounded-l-xl px-4 py-2 text-sm font-medium transition ${
|
|
934
|
+
viewMode === 'grid'
|
|
935
|
+
? 'bg-primary-50 text-primary-600'
|
|
936
|
+
: 'text-gray-500 hover:text-gray-700'
|
|
937
|
+
}`}
|
|
938
|
+
aria-pressed={viewMode === 'grid'}
|
|
939
|
+
>
|
|
940
|
+
<LayoutGrid className="h-4 w-4" />
|
|
941
|
+
Grid
|
|
942
|
+
</button>
|
|
943
|
+
<button
|
|
944
|
+
type="button"
|
|
945
|
+
onClick={() => setViewMode('list')}
|
|
946
|
+
className={`flex items-center gap-2 rounded-r-xl px-4 py-2 text-sm font-medium transition ${
|
|
947
|
+
viewMode === 'list'
|
|
948
|
+
? 'bg-primary-50 text-primary-600'
|
|
949
|
+
: 'text-gray-500 hover:text-gray-700'
|
|
950
|
+
}`}
|
|
951
|
+
aria-pressed={viewMode === 'list'}
|
|
952
|
+
>
|
|
953
|
+
<LayoutList className="h-4 w-4" />
|
|
954
|
+
List
|
|
955
|
+
</button>
|
|
956
|
+
</div>
|
|
957
|
+
</div>
|
|
958
|
+
</div>
|
|
959
|
+
|
|
960
|
+
<div className="mt-4 md:hidden">
|
|
961
|
+
<Button
|
|
962
|
+
variant="outline"
|
|
963
|
+
className="w-full"
|
|
964
|
+
onClick={() => setShowFilters(true)}
|
|
965
|
+
>
|
|
966
|
+
<SlidersHorizontal className="h-5 w-5" />
|
|
967
|
+
Filters
|
|
968
|
+
{hasActiveFilters && (
|
|
969
|
+
<span className="ml-2 rounded-full bg-primary-600 px-2 py-0.5 text-xs font-semibold text-white">
|
|
970
|
+
{activeFilterChips.length}
|
|
971
|
+
</span>
|
|
972
|
+
)}
|
|
973
|
+
</Button>
|
|
974
|
+
</div>
|
|
975
|
+
|
|
155
976
|
{hasActiveFilters && (
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
977
|
+
<div className="mt-6 flex flex-wrap items-center gap-2 border-t border-gray-100 pt-4">
|
|
978
|
+
{activeFilterChips.map((chip) => (
|
|
979
|
+
<button
|
|
980
|
+
key={chip.key}
|
|
981
|
+
type="button"
|
|
982
|
+
onClick={chip.onRemove}
|
|
983
|
+
className="group flex items-center gap-2 rounded-full bg-primary-50 px-3 py-1.5 text-sm font-medium text-primary-700 transition hover:bg-primary-100"
|
|
984
|
+
>
|
|
985
|
+
{chip.label}
|
|
986
|
+
<X className="h-4 w-4 text-primary-500 group-hover:text-primary-700" />
|
|
987
|
+
</button>
|
|
988
|
+
))}
|
|
989
|
+
<button
|
|
990
|
+
type="button"
|
|
991
|
+
onClick={handleClearFilters}
|
|
992
|
+
className="text-sm font-semibold text-gray-500 hover:text-gray-700"
|
|
993
|
+
>
|
|
994
|
+
Reset all
|
|
995
|
+
</button>
|
|
996
|
+
</div>
|
|
159
997
|
)}
|
|
160
|
-
</
|
|
161
|
-
</div>
|
|
998
|
+
</div>
|
|
162
999
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
1000
|
+
<div className="rounded-3xl border border-gray-100 bg-white p-6 shadow-sm">
|
|
1001
|
+
<div className="flex flex-col gap-3 text-sm text-gray-600 md:flex-row md:items-center md:justify-between">
|
|
1002
|
+
<span>
|
|
1003
|
+
{isLoading
|
|
1004
|
+
? 'Loading products...'
|
|
1005
|
+
: `Showing ${displayedProducts.length} of ${pagination.total || displayedProducts.length} products`}
|
|
1006
|
+
</span>
|
|
1007
|
+
<span className="inline-flex items-center gap-2 text-gray-400">
|
|
1008
|
+
<Clock className="h-4 w-4" />
|
|
1009
|
+
Updated a moment ago
|
|
1010
|
+
</span>
|
|
1011
|
+
</div>
|
|
175
1012
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
1013
|
+
<div className="mt-6">
|
|
1014
|
+
{isLoading ? (
|
|
1015
|
+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
|
1016
|
+
{Array.from({ length: 6 }).map((_, index) => (
|
|
1017
|
+
<ProductCardSkeleton key={index} />
|
|
1018
|
+
))}
|
|
1019
|
+
</div>
|
|
1020
|
+
) : displayedProducts.length > 0 ? (
|
|
1021
|
+
viewMode === 'grid' ? (
|
|
1022
|
+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
|
1023
|
+
{displayedProducts.map((product) => (
|
|
1024
|
+
<div key={product.id} className="h-full">
|
|
1025
|
+
<ProductCard
|
|
1026
|
+
product={product}
|
|
1027
|
+
onClickProduct={(item) => {
|
|
1028
|
+
const productData = encodeURIComponent(JSON.stringify(item));
|
|
1029
|
+
router.push(`/products/${item.id}?product=${productData}`);
|
|
1030
|
+
}}
|
|
1031
|
+
/>
|
|
1032
|
+
</div>
|
|
1033
|
+
))}
|
|
1034
|
+
</div>
|
|
1035
|
+
) : (
|
|
1036
|
+
<div className="space-y-4">
|
|
1037
|
+
{displayedProducts.map((product) => {
|
|
1038
|
+
const discount =
|
|
1039
|
+
product.priceBeforeDiscount && product.priceBeforeDiscount > product.finalPrice
|
|
1040
|
+
? Math.round(
|
|
1041
|
+
((product.priceBeforeDiscount - product.finalPrice) /
|
|
1042
|
+
product.priceBeforeDiscount) *
|
|
1043
|
+
100
|
|
1044
|
+
)
|
|
1045
|
+
: 0;
|
|
1046
|
+
|
|
1047
|
+
return (
|
|
1048
|
+
<motion.div
|
|
1049
|
+
key={product.id}
|
|
1050
|
+
whileHover={{ y: -4 }}
|
|
1051
|
+
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"
|
|
1052
|
+
onClick={() => router.push(`/products/${product.id}`)}
|
|
1053
|
+
>
|
|
1054
|
+
<div className="relative h-48 w-full overflow-hidden rounded-2xl bg-gray-100 md:h-40 md:w-40">
|
|
1055
|
+
<Image
|
|
1056
|
+
src={product.productMedia[0]?.file || '/placeholder-product.jpg'}
|
|
1057
|
+
alt={product.name}
|
|
1058
|
+
fill
|
|
1059
|
+
className="object-cover transition duration-500 group-hover:scale-105"
|
|
1060
|
+
/>
|
|
1061
|
+
</div>
|
|
1062
|
+
|
|
1063
|
+
<div className="flex-1 space-y-3">
|
|
1064
|
+
<div className="flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-wide text-primary-600">
|
|
1065
|
+
{product.parentCategories.length > 0 && (
|
|
1066
|
+
<span className="rounded-full bg-primary-50 px-3 py-1 text-primary-700">
|
|
1067
|
+
{product.parentCategories.map((category) => category?.name).join(', ')}
|
|
1068
|
+
</span>
|
|
1069
|
+
)}
|
|
1070
|
+
{product.tags?.slice(0, 3).map((tag) => (
|
|
1071
|
+
<span
|
|
1072
|
+
key={tag}
|
|
1073
|
+
className="rounded-full bg-slate-100 px-3 py-1 text-gray-600"
|
|
1074
|
+
>
|
|
1075
|
+
{tag}
|
|
1076
|
+
</span>
|
|
1077
|
+
))}
|
|
1078
|
+
</div>
|
|
1079
|
+
<h3 className="text-xl font-semibold text-gray-900">
|
|
1080
|
+
{product.name}
|
|
1081
|
+
</h3>
|
|
1082
|
+
{/* {product.description && (
|
|
1083
|
+
<p className="text-sm leading-relaxed text-gray-600 line-clamp-2">
|
|
1084
|
+
{product.description}
|
|
1085
|
+
</p>
|
|
1086
|
+
)} */}
|
|
1087
|
+
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-500">
|
|
1088
|
+
<span className="inline-flex items-center gap-2 font-medium text-primary-600">
|
|
1089
|
+
<ShieldCheck className="h-4 w-4" />
|
|
1090
|
+
{product.inventoryCount > 0 ? 'In stock & ready to ship' : 'Restocking soon'}
|
|
1091
|
+
</span>
|
|
1092
|
+
<span className="inline-flex items-center gap-2">
|
|
1093
|
+
<Clock className="h-4 w-4 text-primary-500" />
|
|
1094
|
+
Added {new Date(product.createdAt).toLocaleDateString()}
|
|
1095
|
+
</span>
|
|
1096
|
+
</div>
|
|
1097
|
+
</div>
|
|
1098
|
+
|
|
1099
|
+
<div className="flex w-full flex-col items-end gap-3 md:w-auto">
|
|
1100
|
+
<div className="text-right">
|
|
1101
|
+
<p className="text-3xl font-semibold text-gray-900">
|
|
1102
|
+
{formatPrice(product.finalPrice)}
|
|
1103
|
+
</p>
|
|
1104
|
+
{product.priceBeforeDiscount && (
|
|
1105
|
+
<p className="text-sm text-gray-400 line-through">
|
|
1106
|
+
{formatPrice(product.priceBeforeDiscount)}
|
|
1107
|
+
</p>
|
|
1108
|
+
)}
|
|
1109
|
+
</div>
|
|
1110
|
+
<Button
|
|
1111
|
+
size="sm"
|
|
1112
|
+
onClick={(event) => {
|
|
1113
|
+
event.stopPropagation();
|
|
1114
|
+
router.push(`/products/${product._id}`);
|
|
1115
|
+
}}
|
|
1116
|
+
>
|
|
1117
|
+
View product
|
|
1118
|
+
</Button>
|
|
1119
|
+
</div>
|
|
1120
|
+
</motion.div>
|
|
1121
|
+
);
|
|
1122
|
+
})}
|
|
1123
|
+
</div>
|
|
1124
|
+
)
|
|
1125
|
+
) : (
|
|
1126
|
+
<EmptyState
|
|
1127
|
+
icon={Package}
|
|
1128
|
+
title="No products found"
|
|
1129
|
+
description="Try adjusting your filters or search terms to uncover more results."
|
|
1130
|
+
actionLabel={hasActiveFilters ? 'Clear filters' : undefined}
|
|
1131
|
+
onAction={hasActiveFilters ? handleClearFilters : undefined}
|
|
191
1132
|
/>
|
|
192
|
-
)
|
|
1133
|
+
)}
|
|
193
1134
|
</div>
|
|
194
1135
|
|
|
195
|
-
{/* Pagination */}
|
|
196
1136
|
{pagination.totalPages > 1 && (
|
|
197
|
-
<div className="flex
|
|
1137
|
+
<div className="mt-10 flex flex-wrap items-center justify-center gap-3">
|
|
198
1138
|
<Button
|
|
199
1139
|
variant="outline"
|
|
200
|
-
onClick={() => setPage(
|
|
1140
|
+
onClick={() => setPage((current) => Math.max(1, current - 1))}
|
|
201
1141
|
disabled={page === 1}
|
|
202
1142
|
>
|
|
203
1143
|
Previous
|
|
204
1144
|
</Button>
|
|
205
|
-
<span className="
|
|
1145
|
+
<span className="text-sm font-semibold text-gray-600">
|
|
206
1146
|
Page {page} of {pagination.totalPages}
|
|
207
1147
|
</span>
|
|
208
1148
|
<Button
|
|
209
1149
|
variant="outline"
|
|
210
|
-
onClick={() =>
|
|
1150
|
+
onClick={() =>
|
|
1151
|
+
setPage((current) => Math.min(pagination.totalPages, current + 1))
|
|
1152
|
+
}
|
|
211
1153
|
disabled={page === pagination.totalPages}
|
|
212
1154
|
>
|
|
213
1155
|
Next
|
|
214
1156
|
</Button>
|
|
215
1157
|
</div>
|
|
216
1158
|
)}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
<EmptyState
|
|
220
|
-
icon={Package}
|
|
221
|
-
title="No products found"
|
|
222
|
-
description="Try adjusting your filters or search query"
|
|
223
|
-
actionLabel={hasActiveFilters ? 'Clear Filters' : undefined}
|
|
224
|
-
onAction={hasActiveFilters ? handleClearFilters : undefined}
|
|
225
|
-
/>
|
|
226
|
-
)}
|
|
1159
|
+
</div>
|
|
1160
|
+
</main>
|
|
227
1161
|
</div>
|
|
228
1162
|
</div>
|
|
229
1163
|
</div>
|
|
1164
|
+
|
|
1165
|
+
<AnimatePresence>
|
|
1166
|
+
{showFilters && (
|
|
1167
|
+
<motion.div
|
|
1168
|
+
initial={{ opacity: 0 }}
|
|
1169
|
+
animate={{ opacity: 1 }}
|
|
1170
|
+
exit={{ opacity: 0 }}
|
|
1171
|
+
className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm lg:hidden"
|
|
1172
|
+
>
|
|
1173
|
+
<motion.div
|
|
1174
|
+
initial={{ y: '100%' }}
|
|
1175
|
+
animate={{ y: 0 }}
|
|
1176
|
+
exit={{ y: '100%' }}
|
|
1177
|
+
transition={{ type: 'spring', stiffness: 260, damping: 26 }}
|
|
1178
|
+
className="absolute inset-x-0 bottom-0 max-h-[85vh] overflow-y-auto rounded-t-3xl bg-white p-6 shadow-2xl"
|
|
1179
|
+
>
|
|
1180
|
+
<div className="mb-6 flex items-center justify-between">
|
|
1181
|
+
<h3 className="text-lg font-semibold text-gray-900">Filters</h3>
|
|
1182
|
+
<button
|
|
1183
|
+
type="button"
|
|
1184
|
+
onClick={() => setShowFilters(false)}
|
|
1185
|
+
className="rounded-full border border-gray-200 p-2 text-gray-500 hover:text-gray-700"
|
|
1186
|
+
>
|
|
1187
|
+
<X className="h-4 w-4" />
|
|
1188
|
+
</button>
|
|
1189
|
+
</div>
|
|
1190
|
+
{renderFiltersPanel()}
|
|
1191
|
+
</motion.div>
|
|
1192
|
+
</motion.div>
|
|
1193
|
+
)}
|
|
1194
|
+
</AnimatePresence>
|
|
230
1195
|
</div>
|
|
231
1196
|
);
|
|
232
1197
|
}
|
|
233
|
-
|