hey-pharmacist-ecommerce 1.1.28 → 1.1.29
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/dist/index.d.mts +344 -640
- package/dist/index.d.ts +344 -640
- package/dist/index.js +1807 -838
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1807 -840
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/AccountOrdersTab.tsx +1 -1
- package/src/components/AccountSettingsTab.tsx +88 -6
- package/src/components/CartItem.tsx +1 -1
- package/src/components/Header.tsx +8 -2
- package/src/components/OrderCard.tsx +4 -4
- package/src/components/ProductCard.tsx +59 -42
- package/src/components/QuickViewModal.tsx +13 -13
- package/src/hooks/useAddresses.ts +4 -1
- package/src/hooks/usePaymentMethods.ts +26 -31
- package/src/hooks/useProducts.ts +63 -64
- package/src/hooks/useWishlistProducts.ts +4 -5
- package/src/index.ts +2 -0
- package/src/lib/Apis/api.ts +0 -1
- package/src/lib/Apis/apis/auth-api.ts +18 -29
- package/src/lib/Apis/apis/products-api.ts +845 -405
- package/src/lib/Apis/models/category-populated.ts +0 -12
- package/src/lib/Apis/models/category-sub-category-populated.ts +2 -2
- package/src/lib/Apis/models/category.ts +0 -18
- package/src/lib/Apis/models/{table-cell-dto.ts → change-password-dto.ts} +6 -6
- package/src/lib/Apis/models/create-product-dto.ts +30 -23
- package/src/lib/Apis/models/create-sub-category-dto.ts +6 -0
- package/src/lib/Apis/models/create-variant-dto.ts +29 -29
- package/src/lib/Apis/models/index.ts +5 -7
- package/src/lib/Apis/models/paginated-products-dto.ts +6 -6
- package/src/lib/Apis/models/product-summary.ts +69 -0
- package/src/lib/Apis/models/product-variant.ts +34 -65
- package/src/lib/Apis/models/product.ts +138 -0
- package/src/lib/Apis/models/products-insights-dto.ts +12 -0
- package/src/lib/Apis/models/single-product-media.ts +0 -12
- package/src/lib/Apis/models/sub-category.ts +6 -12
- package/src/lib/Apis/models/update-product-dto.ts +30 -19
- package/src/lib/Apis/models/update-sub-category-dto.ts +6 -0
- package/src/lib/Apis/models/{update-product-variant-dto.ts → update-variant-dto.ts} +51 -45
- package/src/lib/Apis/models/{shallow-parent-category-dto.ts → variant-id-inventory-body.ts} +5 -11
- package/src/lib/api-adapter/config.ts +53 -0
- package/src/lib/validations/address.ts +1 -1
- package/src/providers/FavoritesProvider.tsx +5 -5
- package/src/providers/WishlistProvider.tsx +4 -4
- package/src/screens/CartScreen.tsx +1 -1
- package/src/screens/ChangePasswordScreen.tsx +2 -6
- package/src/screens/CheckoutScreen.tsx +40 -11
- package/src/screens/ForgotPasswordScreen.tsx +153 -0
- package/src/screens/ProductDetailScreen.tsx +51 -60
- package/src/screens/RegisterScreen.tsx +31 -31
- package/src/screens/ResetPasswordScreen.tsx +202 -0
- package/src/screens/SearchResultsScreen.tsx +264 -26
- package/src/screens/ShopScreen.tsx +42 -45
- package/src/screens/WishlistScreen.tsx +35 -31
- package/src/lib/Apis/apis/product-variants-api.ts +0 -552
- package/src/lib/Apis/models/create-single-variant-product-dto.ts +0 -154
- package/src/lib/Apis/models/extended-product-dto.ts +0 -206
- package/src/lib/Apis/models/frequently-bought-product-dto.ts +0 -71
- package/src/lib/Apis/models/table-dto.ts +0 -34
|
@@ -1,87 +1,204 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
3
|
+
import { useEffect, useState, useCallback, useMemo } from 'react';
|
|
4
4
|
import { useSearchParams } from 'next/navigation';
|
|
5
5
|
import { ProductsApi } from '@/lib/Apis';
|
|
6
|
-
import {
|
|
6
|
+
import { Product } from '@/lib/Apis';
|
|
7
7
|
import { ProductCard } from '@/components/ProductCard';
|
|
8
8
|
import { Skeleton } from '@/components/ui/Skeleton';
|
|
9
9
|
import { Input } from '@/components/ui/Input';
|
|
10
|
-
import { Search, X } from 'lucide-react';
|
|
10
|
+
import { Search, X, TrendingUp, Clock } from 'lucide-react';
|
|
11
11
|
import Link from 'next/link';
|
|
12
12
|
import { AXIOS_CONFIG } from '@/lib/Apis/wrapper';
|
|
13
13
|
import { useWishlist } from '@/providers/WishlistProvider';
|
|
14
14
|
import { useRouter } from 'next/navigation';
|
|
15
15
|
import { useBasePath } from '@/providers/BasePathProvider';
|
|
16
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
16
17
|
|
|
17
18
|
export default function SearchPage() {
|
|
18
19
|
const router = useRouter();
|
|
19
20
|
const { buildPath } = useBasePath();
|
|
20
21
|
const searchParams = useSearchParams();
|
|
21
22
|
const searchQuery = searchParams.get('q') || '';
|
|
22
|
-
const [products, setProducts] = useState<
|
|
23
|
+
const [products, setProducts] = useState<Product[]>([]);
|
|
23
24
|
const [isLoading, setIsLoading] = useState(true);
|
|
24
25
|
const [searchInput, setSearchInput] = useState(searchQuery);
|
|
25
26
|
const [hasSearched, setHasSearched] = useState(false);
|
|
26
27
|
const { isInWishlist } = useWishlist();
|
|
28
|
+
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
29
|
+
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
30
|
+
const [recentSearches, setRecentSearches] = useState<string[]>([]);
|
|
31
|
+
|
|
32
|
+
// Load recent searches from localStorage
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const stored = localStorage.getItem('recent_searches');
|
|
35
|
+
if (stored) {
|
|
36
|
+
try {
|
|
37
|
+
setRecentSearches(JSON.parse(stored));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error('Failed to parse recent searches', e);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
// Save search query to recent searches
|
|
45
|
+
const saveRecentSearch = useCallback((query: string) => {
|
|
46
|
+
if (!query.trim()) return;
|
|
47
|
+
|
|
48
|
+
setRecentSearches((prev) => {
|
|
49
|
+
const updated = [query, ...prev.filter(s => s !== query)].slice(0, 5);
|
|
50
|
+
localStorage.setItem('recent_searches', JSON.stringify(updated));
|
|
51
|
+
return updated;
|
|
52
|
+
});
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
// Generate suggestions based on product data
|
|
56
|
+
const generateSuggestions = useMemo(() => {
|
|
57
|
+
if (!searchInput.trim() || searchInput.length < 2) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const query = searchInput.toLowerCase().trim();
|
|
62
|
+
const suggestionSet = new Set<string>();
|
|
63
|
+
|
|
64
|
+
// Extract suggestions from product names, brands, categories, and tags
|
|
65
|
+
products.forEach((product) => {
|
|
66
|
+
// Product name matches
|
|
67
|
+
if (product.name?.toLowerCase().includes(query)) {
|
|
68
|
+
suggestionSet.add(product.name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Brand matches
|
|
72
|
+
if (product.brand?.toLowerCase().includes(query)) {
|
|
73
|
+
suggestionSet.add(product.brand);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Tag matches
|
|
77
|
+
product.tags?.forEach((tag) => {
|
|
78
|
+
if (tag.toLowerCase().includes(query)) {
|
|
79
|
+
suggestionSet.add(tag);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return Array.from(suggestionSet).slice(0, 5);
|
|
85
|
+
}, [searchInput, products]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
setSuggestions(generateSuggestions);
|
|
89
|
+
}, [generateSuggestions]);
|
|
90
|
+
|
|
91
|
+
// Sanitize search input to handle special characters
|
|
92
|
+
const sanitizeSearchInput = useCallback((input: string): string => {
|
|
93
|
+
// Remove leading/trailing whitespace
|
|
94
|
+
let sanitized = input.trim();
|
|
95
|
+
|
|
96
|
+
// Replace multiple spaces with single space
|
|
97
|
+
sanitized = sanitized.replace(/\s+/g, ' ');
|
|
98
|
+
|
|
99
|
+
// Escape special regex characters for safe processing
|
|
100
|
+
// but keep the original input for search API
|
|
101
|
+
return sanitized;
|
|
102
|
+
}, []);
|
|
27
103
|
|
|
28
104
|
useEffect(() => {
|
|
29
105
|
const fetchSearchResults = async () => {
|
|
30
|
-
|
|
106
|
+
const sanitizedQuery = sanitizeSearchInput(searchQuery);
|
|
107
|
+
|
|
108
|
+
if (!sanitizedQuery) {
|
|
31
109
|
setProducts([]);
|
|
32
110
|
setIsLoading(false);
|
|
111
|
+
setHasSearched(false);
|
|
33
112
|
return;
|
|
34
113
|
}
|
|
35
114
|
|
|
36
115
|
try {
|
|
37
116
|
setIsLoading(true);
|
|
38
117
|
const api = new ProductsApi(AXIOS_CONFIG);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
118
|
+
|
|
119
|
+
// Search for products using getAllProducts
|
|
120
|
+
let response = await api.getAllProducts(
|
|
121
|
+
sanitizedQuery, // searchTerm
|
|
43
122
|
undefined, // maxPrice
|
|
44
123
|
undefined, // minPrice
|
|
45
124
|
undefined, // brandFilter
|
|
46
|
-
|
|
125
|
+
undefined, // availability
|
|
47
126
|
'relevance', // sort
|
|
48
|
-
|
|
127
|
+
undefined, // subCategoryId
|
|
128
|
+
undefined, // categoryId
|
|
49
129
|
true, // isActive
|
|
50
130
|
20, // limit
|
|
51
131
|
1 // page
|
|
52
132
|
);
|
|
53
133
|
|
|
134
|
+
console.log('Search API Response:', {
|
|
135
|
+
query: sanitizedQuery,
|
|
136
|
+
status: response.status,
|
|
137
|
+
dataLength: response.data?.data?.length,
|
|
138
|
+
totalItems: response.data?.totalItems
|
|
139
|
+
});
|
|
140
|
+
|
|
54
141
|
if (response.data?.data) {
|
|
55
|
-
const transformedProducts = response.data.data.map((item:
|
|
142
|
+
const transformedProducts = response.data.data.map((item: Product) => ({
|
|
56
143
|
...item,
|
|
57
144
|
id: item._id || '',
|
|
58
145
|
}));
|
|
59
146
|
|
|
60
147
|
setProducts(transformedProducts);
|
|
148
|
+
|
|
149
|
+
// Save to recent searches if results found
|
|
150
|
+
if (transformedProducts.length > 0) {
|
|
151
|
+
saveRecentSearch(sanitizedQuery);
|
|
152
|
+
}
|
|
61
153
|
} else {
|
|
62
154
|
setProducts([]);
|
|
63
155
|
}
|
|
64
156
|
setHasSearched(true);
|
|
65
157
|
} catch (error) {
|
|
66
158
|
console.error('Error fetching search results:', error);
|
|
159
|
+
if (error instanceof Error) {
|
|
160
|
+
console.error('Error details:', error.message);
|
|
161
|
+
}
|
|
67
162
|
setProducts([]);
|
|
163
|
+
setHasSearched(true);
|
|
68
164
|
} finally {
|
|
69
165
|
setIsLoading(false);
|
|
70
166
|
}
|
|
71
167
|
};
|
|
72
168
|
|
|
73
169
|
fetchSearchResults();
|
|
74
|
-
}, [searchQuery]);
|
|
170
|
+
}, [searchQuery, sanitizeSearchInput, saveRecentSearch]);
|
|
75
171
|
|
|
76
172
|
const handleSearch = (e: React.FormEvent) => {
|
|
77
173
|
e.preventDefault();
|
|
78
|
-
|
|
79
|
-
|
|
174
|
+
const sanitized = sanitizeSearchInput(searchInput);
|
|
175
|
+
if (sanitized) {
|
|
176
|
+
setShowSuggestions(false);
|
|
177
|
+
router.push(buildPath(`/search?q=${encodeURIComponent(sanitized)}`));
|
|
80
178
|
}
|
|
81
179
|
};
|
|
82
180
|
|
|
83
181
|
const clearSearch = () => {
|
|
84
182
|
setSearchInput('');
|
|
183
|
+
setShowSuggestions(false);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const handleSuggestionClick = (suggestion: string) => {
|
|
187
|
+
setSearchInput(suggestion);
|
|
188
|
+
setShowSuggestions(false);
|
|
189
|
+
router.push(buildPath(`/search?q=${encodeURIComponent(suggestion)}`));
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
193
|
+
const value = e.target.value;
|
|
194
|
+
setSearchInput(value);
|
|
195
|
+
setShowSuggestions(value.trim().length >= 2);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const handleInputFocus = () => {
|
|
199
|
+
if (searchInput.trim().length >= 2) {
|
|
200
|
+
setShowSuggestions(true);
|
|
201
|
+
}
|
|
85
202
|
};
|
|
86
203
|
|
|
87
204
|
return (
|
|
@@ -93,9 +210,12 @@ export default function SearchPage() {
|
|
|
93
210
|
<Input
|
|
94
211
|
type="text"
|
|
95
212
|
value={searchInput}
|
|
96
|
-
onChange={
|
|
213
|
+
onChange={handleInputChange}
|
|
214
|
+
onFocus={handleInputFocus}
|
|
215
|
+
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
|
|
97
216
|
placeholder="Search for products..."
|
|
98
217
|
className="pl-10 pr-10 py-6 text-base rounded-lg border-gray-300 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
218
|
+
autoComplete="off"
|
|
99
219
|
/>
|
|
100
220
|
{searchInput && (
|
|
101
221
|
<button
|
|
@@ -106,10 +226,70 @@ export default function SearchPage() {
|
|
|
106
226
|
<X className="h-5 w-5" />
|
|
107
227
|
</button>
|
|
108
228
|
)}
|
|
229
|
+
|
|
230
|
+
{/* Search Suggestions Dropdown */}
|
|
231
|
+
<AnimatePresence>
|
|
232
|
+
{showSuggestions && (suggestions.length > 0 || recentSearches.length > 0) && (
|
|
233
|
+
<motion.div
|
|
234
|
+
initial={{ opacity: 0, y: -10 }}
|
|
235
|
+
animate={{ opacity: 1, y: 0 }}
|
|
236
|
+
exit={{ opacity: 0, y: -10 }}
|
|
237
|
+
transition={{ duration: 0.2 }}
|
|
238
|
+
className="absolute z-50 w-full mt-2 bg-white rounded-lg shadow-lg border border-gray-200 overflow-hidden"
|
|
239
|
+
>
|
|
240
|
+
{/* Real-time Suggestions */}
|
|
241
|
+
{suggestions.length > 0 && (
|
|
242
|
+
<div className="p-2">
|
|
243
|
+
<div className="px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider flex items-center gap-2">
|
|
244
|
+
<TrendingUp className="h-3 w-3" />
|
|
245
|
+
Suggestions
|
|
246
|
+
</div>
|
|
247
|
+
{suggestions.map((suggestion, index) => (
|
|
248
|
+
<button
|
|
249
|
+
key={index}
|
|
250
|
+
type="button"
|
|
251
|
+
onClick={() => handleSuggestionClick(suggestion)}
|
|
252
|
+
className="w-full text-left px-3 py-2.5 hover:bg-gray-50 rounded-md transition-colors flex items-center gap-2 group"
|
|
253
|
+
>
|
|
254
|
+
<Search className="h-4 w-4 text-gray-400 group-hover:text-primary-500" />
|
|
255
|
+
<span className="text-sm text-gray-700 group-hover:text-gray-900">
|
|
256
|
+
{suggestion}
|
|
257
|
+
</span>
|
|
258
|
+
</button>
|
|
259
|
+
))}
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{/* Recent Searches */}
|
|
264
|
+
{recentSearches.length > 0 && !searchInput.trim() && (
|
|
265
|
+
<div className="p-2 border-t border-gray-100">
|
|
266
|
+
<div className="px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider flex items-center gap-2">
|
|
267
|
+
<Clock className="h-3 w-3" />
|
|
268
|
+
Recent Searches
|
|
269
|
+
</div>
|
|
270
|
+
{recentSearches.map((search, index) => (
|
|
271
|
+
<button
|
|
272
|
+
key={index}
|
|
273
|
+
type="button"
|
|
274
|
+
onClick={() => handleSuggestionClick(search)}
|
|
275
|
+
className="w-full text-left px-3 py-2.5 hover:bg-gray-50 rounded-md transition-colors flex items-center gap-2 group"
|
|
276
|
+
>
|
|
277
|
+
<Clock className="h-4 w-4 text-gray-400 group-hover:text-primary-500" />
|
|
278
|
+
<span className="text-sm text-gray-700 group-hover:text-gray-900">
|
|
279
|
+
{search}
|
|
280
|
+
</span>
|
|
281
|
+
</button>
|
|
282
|
+
))}
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
</motion.div>
|
|
286
|
+
)}
|
|
287
|
+
</AnimatePresence>
|
|
109
288
|
</div>
|
|
110
289
|
<button
|
|
111
290
|
type="submit"
|
|
112
|
-
className="mt-4 w-full bg-primary-600 hover:bg-primary-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
|
291
|
+
className="mt-4 w-full bg-primary-600 hover:bg-primary-700 text-white font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
292
|
+
disabled={!searchInput.trim()}
|
|
113
293
|
>
|
|
114
294
|
Search
|
|
115
295
|
</button>
|
|
@@ -144,21 +324,79 @@ export default function SearchPage() {
|
|
|
144
324
|
</div>
|
|
145
325
|
) : hasSearched ? (
|
|
146
326
|
<div className="text-center py-12">
|
|
147
|
-
<div className="
|
|
148
|
-
|
|
327
|
+
<div className="mb-6">
|
|
328
|
+
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
329
|
+
<Search className="h-8 w-8 text-gray-400" />
|
|
330
|
+
</div>
|
|
331
|
+
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
|
332
|
+
No products found
|
|
333
|
+
</h3>
|
|
334
|
+
<p className="text-gray-500 text-base mb-1">
|
|
335
|
+
We couldn't find any products matching "{searchQuery}"
|
|
336
|
+
</p>
|
|
337
|
+
<button
|
|
338
|
+
onClick={clearSearch}
|
|
339
|
+
className="px-6 py-2.5 mt-4 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors font-medium border border-gray-300 bg-white cursor-pointer"
|
|
340
|
+
>
|
|
341
|
+
Clear Search
|
|
342
|
+
</button>
|
|
149
343
|
</div>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
344
|
+
|
|
345
|
+
<div className="flex flex-col sm:flex-row gap-3 justify-center items-center">
|
|
346
|
+
|
|
347
|
+
<Link
|
|
348
|
+
href={buildPath('/shop')}
|
|
349
|
+
className="px-6 py-2.5 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors font-medium"
|
|
350
|
+
>
|
|
351
|
+
Browse All Products
|
|
154
352
|
</Link>
|
|
155
|
-
</
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
{recentSearches.length > 0 && (
|
|
356
|
+
<div className="mt-8 pt-8 border-t border-gray-200">
|
|
357
|
+
<p className="text-sm font-medium text-gray-700 mb-3">Recent Searches:</p>
|
|
358
|
+
<div className="flex flex-wrap gap-2 justify-center">
|
|
359
|
+
{recentSearches.map((search, index) => (
|
|
360
|
+
<button
|
|
361
|
+
key={index}
|
|
362
|
+
onClick={() => handleSuggestionClick(search)}
|
|
363
|
+
className="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-full text-sm transition-colors"
|
|
364
|
+
>
|
|
365
|
+
{search}
|
|
366
|
+
</button>
|
|
367
|
+
))}
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
156
371
|
</div>
|
|
157
372
|
) : (
|
|
158
373
|
<div className="text-center py-12">
|
|
159
|
-
<
|
|
160
|
-
|
|
374
|
+
<div className="w-16 h-16 bg-primary-50 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
375
|
+
<Search className="h-8 w-8 text-primary-500" />
|
|
376
|
+
</div>
|
|
377
|
+
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
|
378
|
+
Start Searching
|
|
379
|
+
</h3>
|
|
380
|
+
<p className="text-gray-500 mb-6">
|
|
381
|
+
Enter a search term above to find products
|
|
161
382
|
</p>
|
|
383
|
+
{recentSearches.length > 0 && (
|
|
384
|
+
<div className="max-w-md mx-auto">
|
|
385
|
+
<p className="text-sm font-medium text-gray-700 mb-3">Recent Searches:</p>
|
|
386
|
+
<div className="flex flex-wrap gap-2 justify-center">
|
|
387
|
+
{recentSearches.map((search, index) => (
|
|
388
|
+
<button
|
|
389
|
+
key={index}
|
|
390
|
+
onClick={() => handleSuggestionClick(search)}
|
|
391
|
+
className="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-full text-sm transition-colors flex items-center gap-2"
|
|
392
|
+
>
|
|
393
|
+
<Clock className="h-3 w-3" />
|
|
394
|
+
{search}
|
|
395
|
+
</button>
|
|
396
|
+
))}
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
)}
|
|
162
400
|
</div>
|
|
163
401
|
)}
|
|
164
402
|
</div>
|
|
@@ -6,11 +6,10 @@ import {
|
|
|
6
6
|
useMemo,
|
|
7
7
|
useState,
|
|
8
8
|
} from 'react';
|
|
9
|
-
import {
|
|
9
|
+
import { motion } from 'framer-motion';
|
|
10
10
|
import {
|
|
11
11
|
ArrowUpDown,
|
|
12
12
|
ChevronDown,
|
|
13
|
-
ChevronUp,
|
|
14
13
|
Clock,
|
|
15
14
|
LayoutGrid,
|
|
16
15
|
LayoutList,
|
|
@@ -74,12 +73,12 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
74
73
|
|
|
75
74
|
const { products, isLoading, pagination } = useProducts(filters, page, 20);
|
|
76
75
|
const { categories, isLoading: isLoadingCategories } = useCategories();
|
|
77
|
-
|
|
78
76
|
const handleSearch = (e: React.FormEvent) => {
|
|
79
77
|
e.preventDefault();
|
|
80
|
-
|
|
78
|
+
const sanitized = searchQuery.trim().replace(/\s+/g, ' ');
|
|
79
|
+
if (sanitized) {
|
|
81
80
|
setIsSearching(true);
|
|
82
|
-
router.push(buildPath(`/search?q=${encodeURIComponent(
|
|
81
|
+
router.push(buildPath(`/search?q=${encodeURIComponent(sanitized)}`));
|
|
83
82
|
}
|
|
84
83
|
};
|
|
85
84
|
|
|
@@ -90,10 +89,13 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
90
89
|
|
|
91
90
|
// Handle search when pressing Enter
|
|
92
91
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
93
|
-
if (e.key === 'Enter'
|
|
92
|
+
if (e.key === 'Enter') {
|
|
94
93
|
e.preventDefault();
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
const sanitized = searchQuery.trim().replace(/\s+/g, ' ');
|
|
95
|
+
if (sanitized) {
|
|
96
|
+
setIsSearching(true);
|
|
97
|
+
router.push(buildPath(`/search?q=${encodeURIComponent(sanitized)}`));
|
|
98
|
+
}
|
|
97
99
|
}
|
|
98
100
|
};
|
|
99
101
|
|
|
@@ -120,9 +122,10 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
120
122
|
if (filters.category) updates[filters.category] = true;
|
|
121
123
|
if (filters.subCategory) {
|
|
122
124
|
const parent = categories.find((c) =>
|
|
123
|
-
c.categorySubCategories?.some((sc) => sc.id === filters.subCategory)
|
|
125
|
+
c.categorySubCategories?.some((sc) => (sc._id ?? sc.id) === filters.subCategory)
|
|
124
126
|
);
|
|
125
|
-
|
|
127
|
+
const parentId = parent?._id ?? parent?.id;
|
|
128
|
+
if (parentId) updates[parentId] = true;
|
|
126
129
|
}
|
|
127
130
|
if (Object.keys(updates).length) {
|
|
128
131
|
setExpandedCategories((prev) => ({ ...prev, ...updates }));
|
|
@@ -156,7 +159,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
156
159
|
let inStockCount = 0;
|
|
157
160
|
|
|
158
161
|
products.forEach((product) => {
|
|
159
|
-
if (product.
|
|
162
|
+
if (product.summary?.totalInventory > 0) inStockCount += 1;
|
|
160
163
|
if (new Date(product.createdAt).getTime() >= monthAgo) newArrivals += 1;
|
|
161
164
|
});
|
|
162
165
|
|
|
@@ -213,7 +216,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
213
216
|
if (isLoading) return products;
|
|
214
217
|
let items = [...products];
|
|
215
218
|
if (filters.tags?.length) {
|
|
216
|
-
items = items.filter((p) => Array.isArray(p.tags) && p.tags.some((t) => filters.tags!.includes(t)));
|
|
219
|
+
items = items.filter((p) => Array.isArray(p.tags) && p.tags.some((t: string) => filters.tags!.includes(t)));
|
|
217
220
|
}
|
|
218
221
|
if (filters.newArrivals) {
|
|
219
222
|
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
@@ -234,9 +237,9 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
234
237
|
|
|
235
238
|
switch (sortOption) {
|
|
236
239
|
case 'price-low-high':
|
|
237
|
-
return items.sort((a, b) => a.finalPrice - b.finalPrice);
|
|
240
|
+
return items.sort((a, b) => (a.variants?.[0]?.finalPrice ?? 0) - (b.variants?.[0]?.finalPrice ?? 0));
|
|
238
241
|
case 'price-high-low':
|
|
239
|
-
return items.sort((a, b) => b.finalPrice - a.finalPrice);
|
|
242
|
+
return items.sort((a, b) => (b.variants?.[0]?.finalPrice ?? 0) - (a.variants?.[0]?.finalPrice ?? 0));
|
|
240
243
|
case 'newest':
|
|
241
244
|
return items.sort(
|
|
242
245
|
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
@@ -262,7 +265,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
262
265
|
const quickSearches = useMemo(() => {
|
|
263
266
|
const counts = new Map<string, number>();
|
|
264
267
|
products.forEach((p) => {
|
|
265
|
-
(p.tags || []).forEach((t) => counts.set(t, (counts.get(t) || 0) + 1));
|
|
268
|
+
(p.tags || []).forEach((t: string) => counts.set(t, (counts.get(t) || 0) + 1));
|
|
266
269
|
});
|
|
267
270
|
return Array.from(counts.entries())
|
|
268
271
|
.sort((a, b) => b[1] - a[1])
|
|
@@ -539,7 +542,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
539
542
|
let subName: string | undefined;
|
|
540
543
|
let parentName: string | undefined;
|
|
541
544
|
categories.forEach((cat) => {
|
|
542
|
-
const found = cat.categorySubCategories?.find((sc) => sc.id === subCategoryFilter);
|
|
545
|
+
const found = cat.categorySubCategories?.find((sc) => (sc._id ?? sc.id) === subCategoryFilter);
|
|
543
546
|
if (found) {
|
|
544
547
|
subName = found.name;
|
|
545
548
|
parentName = cat.name;
|
|
@@ -551,7 +554,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
551
554
|
onRemove: handleRemoveSubCategory,
|
|
552
555
|
});
|
|
553
556
|
if (categoryFilter) {
|
|
554
|
-
const catObj = categories.find((c) => c.id === categoryFilter);
|
|
557
|
+
const catObj = categories.find((c) => (c._id ?? c.id) === categoryFilter);
|
|
555
558
|
chips.push({
|
|
556
559
|
key: 'category',
|
|
557
560
|
label: `Category: ${catObj?.name ?? parentName ?? categoryFilter}`,
|
|
@@ -559,7 +562,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
559
562
|
});
|
|
560
563
|
}
|
|
561
564
|
} else if (categoryFilter) {
|
|
562
|
-
const category = categories.find((cat) => cat.id === categoryFilter);
|
|
565
|
+
const category = categories.find((cat) => (cat._id ?? cat.id) === categoryFilter);
|
|
563
566
|
chips.push({
|
|
564
567
|
key: 'category',
|
|
565
568
|
label: `Category: ${category?.name ?? categoryFilter}`,
|
|
@@ -697,15 +700,16 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
697
700
|
{expandedFilterSections.category && (
|
|
698
701
|
<div className="space-y-2">
|
|
699
702
|
{sortedCategories.map((category) => {
|
|
700
|
-
const
|
|
701
|
-
const
|
|
703
|
+
const categoryId = category._id ?? category.id ?? '';
|
|
704
|
+
const isCategoryActive = categoryFilter === categoryId;
|
|
705
|
+
const isExpanded = !!expandedCategories[categoryId];
|
|
702
706
|
const Icon = getCategoryIconForFilter(category.name ?? '');
|
|
703
707
|
return (
|
|
704
708
|
<button
|
|
705
|
-
key={
|
|
709
|
+
key={categoryId}
|
|
706
710
|
onClick={() => {
|
|
707
|
-
if (!isExpanded) toggleCategoryExpand(
|
|
708
|
-
handleCategoryChange(
|
|
711
|
+
if (!isExpanded) toggleCategoryExpand(categoryId);
|
|
712
|
+
handleCategoryChange(categoryId);
|
|
709
713
|
}}
|
|
710
714
|
className={`w-full text-left px-4 py-3 rounded-xl font-['Poppins',sans-serif] text-[13px] transition-all flex items-center gap-3 ${isCategoryActive
|
|
711
715
|
? 'bg-primary text-white shadow-lg'
|
|
@@ -955,15 +959,16 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
955
959
|
|
|
956
960
|
{displayCategories.map((category, index) => {
|
|
957
961
|
const Icon = getCategoryIcon(category.name ?? '');
|
|
958
|
-
const
|
|
962
|
+
const categoryId = category._id ?? category.id ?? '';
|
|
963
|
+
const isSelected = categoryFilter === categoryId;
|
|
959
964
|
|
|
960
965
|
return (
|
|
961
966
|
<motion.button
|
|
962
|
-
key={
|
|
967
|
+
key={categoryId}
|
|
963
968
|
initial={{ opacity: 0, y: 20 }}
|
|
964
969
|
animate={{ opacity: 1, y: 0 }}
|
|
965
970
|
transition={{ delay: index * 0.1 }}
|
|
966
|
-
onClick={() => handleCategoryChange(
|
|
971
|
+
onClick={() => handleCategoryChange(categoryId)}
|
|
967
972
|
className={`group relative overflow-hidden rounded-[24px] p-6 min-h-[180px] min-w-[170px] transition-all duration-300 ${isSelected ? 'bg-linear-to-br from-primary to-secondary text-white shadow-xl scale-105'
|
|
968
973
|
: 'bg-linear-to-br from-gray-50 to-white hover:shadow-lg border-2 border-gray-100 hover:border-primary'
|
|
969
974
|
}`}
|
|
@@ -1106,14 +1111,11 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
1106
1111
|
) : (
|
|
1107
1112
|
<div className="space-y-4">
|
|
1108
1113
|
{displayedProducts.map((product) => {
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
100
|
|
1115
|
-
)
|
|
1116
|
-
: 0;
|
|
1114
|
+
const firstVariant = product.variants?.[0];
|
|
1115
|
+
const displayFinalPrice = firstVariant?.finalPrice ?? 0;
|
|
1116
|
+
const displayRetailPrice = firstVariant?.retailPrice ?? 0;
|
|
1117
|
+
const displayIsDiscounted = firstVariant?.isDiscounted ?? false;
|
|
1118
|
+
const displayInventoryCount = firstVariant?.inventoryCount ?? 0;
|
|
1117
1119
|
|
|
1118
1120
|
return (
|
|
1119
1121
|
<motion.div
|
|
@@ -1124,7 +1126,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
1124
1126
|
>
|
|
1125
1127
|
<div className="relative h-48 w-full overflow-hidden rounded-2xl bg-gray-100 md:h-40 md:w-40">
|
|
1126
1128
|
<Image
|
|
1127
|
-
src={product.
|
|
1129
|
+
src={product.media?.[0]?.file || '/placeholder-product.jpg'}
|
|
1128
1130
|
alt={product.name}
|
|
1129
1131
|
fill
|
|
1130
1132
|
className="object-cover transition duration-500 group-hover:scale-105"
|
|
@@ -1133,12 +1135,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
1133
1135
|
|
|
1134
1136
|
<div className="flex-1 space-y-3">
|
|
1135
1137
|
<div className="flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-wide text-primary-600">
|
|
1136
|
-
{product.
|
|
1137
|
-
<span className="rounded-full bg-primary-50 px-3 py-1 text-primary-700">
|
|
1138
|
-
{product.parentCategories.map((category) => category?.name).join(', ')}
|
|
1139
|
-
</span>
|
|
1140
|
-
)}
|
|
1141
|
-
{product.tags?.slice(0, 3).map((tag) => (
|
|
1138
|
+
{product.tags?.slice(0, 3).map((tag: string) => (
|
|
1142
1139
|
<span
|
|
1143
1140
|
key={tag}
|
|
1144
1141
|
className="rounded-full bg-slate-100 px-3 py-1 text-gray-600"
|
|
@@ -1153,7 +1150,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
1153
1150
|
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-500">
|
|
1154
1151
|
<span className="inline-flex items-center gap-2 font-medium text-primary-600">
|
|
1155
1152
|
<ShieldCheck className="h-4 w-4" />
|
|
1156
|
-
{
|
|
1153
|
+
{displayInventoryCount > 0 ? 'In stock & ready to ship' : 'Restocking soon'}
|
|
1157
1154
|
</span>
|
|
1158
1155
|
<span className="inline-flex items-center gap-2">
|
|
1159
1156
|
<Clock className="h-4 w-4 text-primary-500" />
|
|
@@ -1165,11 +1162,11 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
1165
1162
|
<div className="flex w-full flex-col items-end gap-3 md:w-auto">
|
|
1166
1163
|
<div className="text-right">
|
|
1167
1164
|
<p className="text-3xl font-semibold text-gray-900">
|
|
1168
|
-
{formatPrice(
|
|
1165
|
+
{formatPrice(displayFinalPrice)}
|
|
1169
1166
|
</p>
|
|
1170
|
-
{
|
|
1167
|
+
{displayIsDiscounted && (
|
|
1171
1168
|
<p className="text-sm text-gray-400 line-through">
|
|
1172
|
-
{formatPrice(
|
|
1169
|
+
{formatPrice(displayRetailPrice)}
|
|
1173
1170
|
</p>
|
|
1174
1171
|
)}
|
|
1175
1172
|
</div>
|