hey-pharmacist-ecommerce 1.0.4 → 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.
Files changed (74) hide show
  1. package/README.md +107 -1
  2. package/dist/index.d.mts +3636 -316
  3. package/dist/index.d.ts +3636 -316
  4. package/dist/index.js +6802 -3865
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +6756 -3817
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +17 -14
  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 -147
  73. package/src/lib/api-adapter/orders-adapter.ts +0 -195
  74. package/src/lib/api-adapter/products-adapter.ts +0 -194
@@ -1,8 +1,28 @@
1
1
  'use client';
2
2
 
3
- import React, { useState } from 'react';
4
- import { motion } from 'framer-motion';
5
- import { Search, SlidersHorizontal, X } from 'lucide-react';
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 { Package } from 'lucide-react';
14
- import { useRouter } from 'next/navigation';
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
- export function ShopScreen() {
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
- setFilters({ ...filters, search: searchQuery });
29
- setPage(1);
64
+ if (searchQuery.trim()) {
65
+ setIsSearching(true);
66
+ router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`);
67
+ }
30
68
  };
31
69
 
32
- const handleCategoryChange = (category: string) => {
33
- setFilters({ ...filters, category: category === filters.category ? undefined : category });
34
- setPage(1);
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 handleClearFilters = () => {
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 hasActiveFilters = Object.keys(filters).length > 0;
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-gray-50">
47
- {/* Hero Section */}
48
- <section className="bg-gradient-to-br from-primary-600 via-primary-700 to-secondary-600 text-white py-20">
49
- <div className="container mx-auto px-4">
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: 20 }}
768
+ initial={{ opacity: 0, y: 24 }}
52
769
  animate={{ opacity: 1, y: 0 }}
53
- className="max-w-3xl mx-auto text-center"
770
+ className="max-w-3xl space-y-8 text-center md:mx-auto md:text-left"
54
771
  >
55
- <h1 className="text-5xl md:text-6xl font-bold mb-6">
56
- Discover Amazing Products
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-xl text-primary-100 mb-8">
59
- Browse our curated collection of quality products at great prices
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
- {/* Search Bar */}
63
- <form onSubmit={handleSearch} className="max-w-2xl mx-auto">
64
- <div className="relative">
65
- <Input
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={(e) => setSearchQuery(e.target.value)}
70
- className="pr-12 text-lg"
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 p-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
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
- <Search className="w-5 h-5" />
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
- {/* Main Content */}
85
- <div className="container mx-auto px-4 py-12">
86
- <div className="flex gap-8">
87
- {/* Sidebar Filters - Desktop */}
88
- <aside className="hidden lg:block w-64 flex-shrink-0">
89
- <div className="bg-white rounded-2xl p-6 shadow-sm sticky top-24">
90
- <div className="flex items-center justify-between mb-6">
91
- <h3 className="text-lg font-bold text-gray-900">Filters</h3>
92
- {hasActiveFilters && (
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
- onClick={handleClearFilters}
95
- className="text-sm text-primary-600 hover:text-primary-700 font-medium"
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
- Clear All
830
+ {term}
98
831
  </button>
99
- )}
832
+ ))}
100
833
  </div>
101
-
102
- {/* Categories */}
103
- <div className="space-y-4">
104
- <h4 className="font-semibold text-gray-900">Categories</h4>
105
- <div className="space-y-2">
106
- {categories.map((category) => (
107
- <label
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
- className="flex items-center gap-3 cursor-pointer hover:bg-gray-50 p-2 rounded-lg transition-colors"
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
- <input
112
- type="checkbox"
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
- {/* Stock Status */}
127
- <div className="space-y-4 mt-8">
128
- <h4 className="font-semibold text-gray-900">Availability</h4>
129
- <label className="flex items-center gap-3 cursor-pointer hover:bg-gray-50 p-2 rounded-lg transition-colors">
130
- <input
131
- type="checkbox"
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
- </div>
142
- </aside>
143
-
144
- {/* Products Grid */}
145
- <div className="flex-1">
146
- {/* Mobile Filter Button */}
147
- <div className="lg:hidden mb-6">
148
- <Button
149
- variant="outline"
150
- onClick={() => setShowFilters(!showFilters)}
151
- className="w-full"
152
- >
153
- <SlidersHorizontal className="w-5 h-5" />
154
- Filters
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
- <span className="ml-2 px-2 py-0.5 bg-primary-600 text-white text-xs rounded-full">
157
- Active
158
- </span>
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
- </Button>
161
- </div>
998
+ </div>
162
999
 
163
- {/* Results Header */}
164
- <div className="flex items-center justify-between mb-6">
165
- <p className="text-gray-600">
166
- {isLoading ? (
167
- 'Loading...'
168
- ) : (
169
- <>
170
- Showing {products.length} of {pagination.total} products
171
- </>
172
- )}
173
- </p>
174
- </div>
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
- {/* Products Grid */}
177
- {isLoading ? (
178
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
179
- {Array.from({ length: 6 }).map((_, i) => (
180
- <ProductCardSkeleton key={i} />
181
- ))}
182
- </div>
183
- ) : products.length > 0 ? (
184
- <>
185
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
186
- {products.map((product) => (
187
- <ProductCard
188
- key={product.id}
189
- product={product}
190
- onClickProduct={(p) => router.push(`/products/${p.id}`)}
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 justify-center items-center gap-2 mt-12">
1137
+ <div className="mt-10 flex flex-wrap items-center justify-center gap-3">
198
1138
  <Button
199
1139
  variant="outline"
200
- onClick={() => setPage(page - 1)}
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="px-4 text-gray-700">
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={() => setPage(page + 1)}
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
-