@akinon/next 1.92.0-rc.9 → 1.92.0-snapshot-ZERO-3449-20250618101111

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 (42) hide show
  1. package/CHANGELOG.md +33 -1180
  2. package/api/similar-product-list.ts +63 -0
  3. package/api/similar-products.ts +109 -0
  4. package/components/accordion.tsx +5 -20
  5. package/components/file-input.tsx +3 -65
  6. package/components/input.tsx +0 -2
  7. package/components/link.tsx +12 -16
  8. package/components/modal.tsx +16 -32
  9. package/components/plugin-module.tsx +3 -13
  10. package/components/selected-payment-option-view.tsx +0 -11
  11. package/data/client/similar-products.ts +122 -0
  12. package/data/urls.ts +5 -1
  13. package/hocs/server/with-segment-defaults.tsx +2 -5
  14. package/hooks/index.ts +2 -0
  15. package/hooks/use-image-cropper.ts +160 -0
  16. package/hooks/use-similar-products.ts +720 -0
  17. package/instrumentation/node.ts +13 -15
  18. package/lib/cache.ts +0 -2
  19. package/middlewares/complete-gpay.ts +1 -2
  20. package/middlewares/complete-masterpass.ts +1 -2
  21. package/middlewares/default.ts +184 -196
  22. package/middlewares/index.ts +1 -3
  23. package/middlewares/redirection-payment.ts +1 -2
  24. package/middlewares/saved-card-redirection.ts +1 -2
  25. package/middlewares/three-d-redirection.ts +1 -2
  26. package/middlewares/url-redirection.ts +14 -8
  27. package/package.json +3 -3
  28. package/plugins.d.ts +0 -2
  29. package/plugins.js +1 -3
  30. package/redux/middlewares/checkout.ts +2 -15
  31. package/redux/reducers/checkout.ts +1 -9
  32. package/sentry/index.ts +17 -54
  33. package/types/commerce/order.ts +0 -1
  34. package/types/index.ts +73 -26
  35. package/utils/app-fetch.ts +2 -2
  36. package/utils/image-validation.ts +303 -0
  37. package/utils/redirect.ts +3 -5
  38. package/with-pz-config.js +5 -1
  39. package/data/server/basket.ts +0 -72
  40. package/hooks/use-loyalty-availability.ts +0 -21
  41. package/middlewares/wallet-complete-redirection.ts +0 -179
  42. package/utils/redirect-ignore.ts +0 -35
@@ -0,0 +1,720 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { Product } from '@akinon/next/types';
3
+ import {
4
+ useGetSimilarProductsByUrlQuery,
5
+ useGetSimilarProductsByImageMutation,
6
+ useLazyGetSimilarProductsListQuery,
7
+ type SimilarProductsListResponse
8
+ } from '@akinon/next/data/client/similar-products';
9
+ import { type Facet, type SortOption } from '@akinon/next/types/commerce';
10
+ import { useLocalization } from '@akinon/next/hooks';
11
+ import {
12
+ validateImageFile,
13
+ validateImageFromDataUrl,
14
+ type ImageValidationResult
15
+ } from '@akinon/next/utils/image-validation';
16
+
17
+ type SearchResults = SimilarProductsListResponse;
18
+
19
+ interface SearchParams {
20
+ [key: string]: string;
21
+ }
22
+
23
+ export function useSimilarProducts(product: Product) {
24
+ const { t } = useLocalization();
25
+ const [currentImageUrl, setCurrentImageUrl] = useState('');
26
+ const [fileError, setFileError] = useState('');
27
+ const [searchResults, setSearchResults] = useState<SearchResults | null>(
28
+ null
29
+ );
30
+ const [resultsKey, setResultsKey] = useState(0);
31
+ const [lastProductIds, setLastProductIds] = useState<number[]>([]);
32
+ const [hasUploadedImage, setHasUploadedImage] = useState(false);
33
+ const [shouldFetchByUrl, setShouldFetchByUrl] = useState(false);
34
+ const [urlToFetch, setUrlToFetch] = useState('');
35
+
36
+ const { data: urlSearchData, isLoading: isUrlSearchLoading } =
37
+ useGetSimilarProductsByUrlQuery(
38
+ {
39
+ url: urlToFetch,
40
+ limit: 20,
41
+ excluded_product_ids: product?.pk ? [product.pk] : undefined
42
+ },
43
+ { skip: !shouldFetchByUrl || !urlToFetch }
44
+ );
45
+
46
+ const [getSimilarProductsByImage, { isLoading: isImageSearchLoading }] =
47
+ useGetSimilarProductsByImageMutation();
48
+
49
+ const [fetchProductsList, { isLoading: isProductsListLoading }] =
50
+ useLazyGetSimilarProductsListQuery();
51
+
52
+ const isLoading =
53
+ isUrlSearchLoading || isImageSearchLoading || isProductsListLoading;
54
+
55
+ const createEmptySearchResults = useCallback(
56
+ (): SearchResults => ({
57
+ products: [],
58
+ facets: [],
59
+ sorters: [],
60
+ search_text: null,
61
+ pagination: {
62
+ current_page: 1,
63
+ num_pages: 1,
64
+ page_size: 20,
65
+ total_count: 0
66
+ }
67
+ }),
68
+ []
69
+ );
70
+
71
+ const updateResultsAndKey = useCallback((results: SearchResults) => {
72
+ setSearchResults(results);
73
+ setResultsKey((prevKey) => prevKey + 1);
74
+ }, []);
75
+
76
+ const buildSearchParams = useCallback(
77
+ (facets?: Facet[], sortValue?: string, page?: number): SearchParams => {
78
+ const searchParams: SearchParams = {};
79
+
80
+ if (page) {
81
+ searchParams.page = String(page);
82
+ }
83
+
84
+ if (facets) {
85
+ facets.forEach((facet) => {
86
+ if (String(facet.key) === 'category_ids') return;
87
+
88
+ const selectedChoices =
89
+ facet.data?.choices?.filter((choice) => choice.is_selected) || [];
90
+
91
+ if (selectedChoices.length > 0) {
92
+ const values = selectedChoices.map((choice) =>
93
+ String(choice.value)
94
+ );
95
+ const searchKey = String(facet.search_key || facet.key);
96
+ searchParams[searchKey] = values.join(',');
97
+ }
98
+ });
99
+ }
100
+
101
+ if (sortValue) {
102
+ searchParams.sorter = sortValue;
103
+ }
104
+
105
+ return searchParams;
106
+ },
107
+ []
108
+ );
109
+
110
+ const preserveFacetState = useCallback(
111
+ (responseFacets: Facet[], existingFacets: Facet[]) => {
112
+ return (
113
+ responseFacets?.map((responseFacet) => {
114
+ const existingFacet = existingFacets?.find(
115
+ (f) => String(f.key) === String(responseFacet.key)
116
+ );
117
+ if (existingFacet && responseFacet.data?.choices) {
118
+ return {
119
+ ...responseFacet,
120
+ data: {
121
+ ...responseFacet.data,
122
+ choices: responseFacet.data.choices.map((choice) => {
123
+ const existingChoice = existingFacet.data?.choices?.find(
124
+ (c) => String(c.value) === String(choice.value)
125
+ );
126
+ return {
127
+ ...choice,
128
+ is_selected: existingChoice?.is_selected || false
129
+ };
130
+ })
131
+ }
132
+ };
133
+ }
134
+ return responseFacet;
135
+ }) ||
136
+ existingFacets ||
137
+ []
138
+ );
139
+ },
140
+ []
141
+ );
142
+
143
+ const updateSorterState = useCallback(
144
+ (sorters: SortOption[], selectedValue: string) => {
145
+ return (
146
+ sorters?.map((sorter) => ({
147
+ ...sorter,
148
+ is_selected: String(sorter.value) === String(selectedValue)
149
+ })) || []
150
+ );
151
+ },
152
+ []
153
+ );
154
+
155
+ const sortProducts = useCallback(
156
+ (products: SearchResults['products'], sortValue: string) => {
157
+ if (!products || products.length === 0) return products;
158
+
159
+ const sortedProducts = [...products];
160
+
161
+ switch (sortValue) {
162
+ case 'price':
163
+ return sortedProducts.sort((a, b) => {
164
+ const priceA = parseFloat(String(a.price || '0'));
165
+ const priceB = parseFloat(String(b.price || '0'));
166
+ return isNaN(priceA) || isNaN(priceB) ? 0 : priceA - priceB;
167
+ });
168
+ case '-price':
169
+ return sortedProducts.sort((a, b) => {
170
+ const priceA = parseFloat(String(a.price || '0'));
171
+ const priceB = parseFloat(String(b.price || '0'));
172
+ return isNaN(priceA) || isNaN(priceB) ? 0 : priceB - priceA;
173
+ });
174
+ case 'newcomers':
175
+ return sortedProducts.sort((a, b) => {
176
+ const pkA = parseInt(String(a.pk || '0'), 10);
177
+ const pkB = parseInt(String(b.pk || '0'), 10);
178
+ return isNaN(pkA) || isNaN(pkB) ? 0 : pkB - pkA;
179
+ });
180
+ case 'default':
181
+ default:
182
+ return products;
183
+ }
184
+ },
185
+ []
186
+ );
187
+
188
+ const createSearchFilter = useCallback(
189
+ (filterData: number[] | Record<string, unknown> | unknown) => {
190
+ let filterObject: Record<string, unknown>;
191
+
192
+ if (Array.isArray(filterData)) {
193
+ filterObject = {
194
+ 'products.pk': filterData,
195
+ default_sorting_deactivated: true
196
+ };
197
+ } else if (typeof filterData === 'object' && filterData !== null) {
198
+ filterObject = {
199
+ ...(filterData as Record<string, unknown>),
200
+ default_sorting_deactivated: true
201
+ };
202
+ } else {
203
+ filterObject = {
204
+ default_sorting_deactivated: true
205
+ };
206
+ }
207
+
208
+ const jsonString = JSON.stringify(filterObject);
209
+ return btoa(jsonString);
210
+ },
211
+ []
212
+ );
213
+
214
+ const executeProductSearch = useCallback(
215
+ async (
216
+ productIds: number[],
217
+ searchParams: SearchParams = {},
218
+ currentSortValue?: string
219
+ ): Promise<SearchResults | null> => {
220
+ try {
221
+ const base64Filter = createSearchFilter(productIds);
222
+ const result = await fetchProductsList({
223
+ filter: base64Filter,
224
+ searchParams,
225
+ isExclude: false
226
+ }).unwrap();
227
+
228
+ const sortedProducts = currentSortValue
229
+ ? sortProducts(result.products, currentSortValue)
230
+ : result.products;
231
+
232
+ return {
233
+ ...result,
234
+ products: sortedProducts,
235
+ facets: preserveFacetState(
236
+ result.facets,
237
+ searchResults?.facets || []
238
+ ),
239
+ sorters: updateSorterState(
240
+ result.sorters,
241
+ currentSortValue ||
242
+ searchResults?.sorters?.find((s) => s.is_selected)?.value ||
243
+ 'default'
244
+ )
245
+ };
246
+ } catch (error) {
247
+ console.error('Product search failed:', error);
248
+ return createEmptySearchResults();
249
+ }
250
+ },
251
+ [
252
+ createSearchFilter,
253
+ fetchProductsList,
254
+ sortProducts,
255
+ preserveFacetState,
256
+ updateSorterState,
257
+ searchResults,
258
+ createEmptySearchResults
259
+ ]
260
+ );
261
+
262
+ const extractProductIds = useCallback((data: unknown): number[] => {
263
+ if (!data) return [];
264
+
265
+ if (data && typeof data === 'object') {
266
+ const obj = data as Record<string, unknown>;
267
+
268
+ const possibleArrayKeys = [
269
+ 'product_ids',
270
+ 'productIds',
271
+ 'products',
272
+ 'results'
273
+ ];
274
+
275
+ for (const key of possibleArrayKeys) {
276
+ if (obj[key] && Array.isArray(obj[key])) {
277
+ const arr = obj[key] as unknown[];
278
+ const filtered = arr
279
+ .map((item) => {
280
+ if (typeof item === 'number') return item;
281
+ if (typeof item === 'string') {
282
+ const parsed = parseInt(item, 10);
283
+ return isNaN(parsed) ? null : parsed;
284
+ }
285
+ return null;
286
+ })
287
+ .filter((id): id is number => id !== null && id > 0);
288
+
289
+ if (filtered.length > 0) return filtered;
290
+ }
291
+ }
292
+
293
+ if (obj.similar_products && Array.isArray(obj.similar_products)) {
294
+ return obj.similar_products
295
+ .map((item) => {
296
+ if (item && typeof item === 'object' && 'product_id' in item) {
297
+ const productId = (item as any).product_id;
298
+ if (typeof productId === 'number') return productId;
299
+ if (typeof productId === 'string') {
300
+ const parsed = parseInt(productId, 10);
301
+ return isNaN(parsed) ? null : parsed;
302
+ }
303
+ }
304
+ return null;
305
+ })
306
+ .filter((id): id is number => id !== null && id > 0);
307
+ }
308
+ }
309
+
310
+ if (Array.isArray(data)) {
311
+ return data
312
+ .map((item) => {
313
+ if (typeof item === 'number') return item;
314
+ if (typeof item === 'string') {
315
+ const parsed = parseInt(item, 10);
316
+ return isNaN(parsed) ? null : parsed;
317
+ }
318
+ return null;
319
+ })
320
+ .filter((id): id is number => id !== null && id > 0);
321
+ }
322
+
323
+ return [];
324
+ }, []);
325
+
326
+ const handleSearchResults = useCallback(
327
+ async (data: unknown) => {
328
+ const productIds = extractProductIds(data);
329
+
330
+ if (productIds.length > 0) {
331
+ const result = await executeProductSearch(productIds);
332
+ if (result) {
333
+ const resultWithCorrectSorterState = {
334
+ ...result,
335
+ sorters: updateSorterState(result.sorters, 'default')
336
+ };
337
+ updateResultsAndKey(resultWithCorrectSorterState);
338
+ }
339
+ } else {
340
+ updateResultsAndKey(createEmptySearchResults());
341
+ }
342
+
343
+ setLastProductIds(productIds);
344
+ },
345
+ [
346
+ extractProductIds,
347
+ executeProductSearch,
348
+ updateSorterState,
349
+ updateResultsAndKey,
350
+ createEmptySearchResults
351
+ ]
352
+ );
353
+
354
+ const validateImageFileWithTranslation = useCallback(
355
+ async (file: File): Promise<ImageValidationResult> => {
356
+ try {
357
+ const result = await validateImageFile(file, 5);
358
+
359
+ if (!result.isValid) {
360
+ if (result.error?.includes('size')) {
361
+ return {
362
+ isValid: false,
363
+ error: t('common.similar_products.errors.file_size_too_large')
364
+ };
365
+ }
366
+ if (result.error?.includes('not a valid image')) {
367
+ return {
368
+ isValid: false,
369
+ error: t('common.similar_products.errors.invalid_image_file')
370
+ };
371
+ }
372
+ if (result.error?.includes('format')) {
373
+ return {
374
+ isValid: false,
375
+ error: t('common.similar_products.errors.invalid_image_format')
376
+ };
377
+ }
378
+ }
379
+
380
+ return result;
381
+ } catch (error) {
382
+ return {
383
+ isValid: false,
384
+ error: t('common.similar_products.errors.processing_error')
385
+ };
386
+ }
387
+ },
388
+ [t]
389
+ );
390
+
391
+ const handleImageSearch = useCallback(
392
+ async (
393
+ searchFn: (image: string) => Promise<unknown>,
394
+ imageData: string,
395
+ errorPrefix: string
396
+ ) => {
397
+ try {
398
+ const result = await searchFn(imageData);
399
+ await handleSearchResults(result);
400
+ return result;
401
+ } catch (error) {
402
+ console.error(`${errorPrefix} failed:`, error);
403
+ setFileError(t('common.similar_products.errors.search_failed'));
404
+ updateResultsAndKey(createEmptySearchResults());
405
+ return null;
406
+ }
407
+ },
408
+ [handleSearchResults, updateResultsAndKey, createEmptySearchResults, t]
409
+ );
410
+
411
+ useEffect(() => {
412
+ if (product?.productimage_set?.length > 0) {
413
+ const initialImageUrl = product.productimage_set[0].image;
414
+ setCurrentImageUrl(initialImageUrl);
415
+ }
416
+ }, [product]);
417
+
418
+ useEffect(() => {
419
+ if (urlSearchData && shouldFetchByUrl) {
420
+ handleSearchResults(urlSearchData);
421
+ setShouldFetchByUrl(false);
422
+ setUrlToFetch('');
423
+ }
424
+ }, [urlSearchData, shouldFetchByUrl, handleSearchResults]);
425
+
426
+ const fetchSimilarProductsByImageUrl = useCallback(
427
+ async (imageUrl: string) => {
428
+ try {
429
+ setUrlToFetch(imageUrl);
430
+ setShouldFetchByUrl(true);
431
+ } catch (error) {
432
+ console.error('Image search request failed:', error);
433
+ setFileError(t('common.similar_products.errors.connection_failed'));
434
+ updateResultsAndKey(createEmptySearchResults());
435
+ return null;
436
+ }
437
+ },
438
+ [updateResultsAndKey, createEmptySearchResults, t]
439
+ );
440
+
441
+ const fetchSimilarProductsByBase64 = useCallback(
442
+ async (base64Image: string) => {
443
+ const base64Data = base64Image.startsWith('data:')
444
+ ? base64Image.split(',')[1]
445
+ : base64Image;
446
+
447
+ return handleImageSearch(
448
+ async () =>
449
+ getSimilarProductsByImage({
450
+ image: base64Data,
451
+ limit: 20
452
+ }).unwrap(),
453
+ base64Image,
454
+ 'Image search'
455
+ );
456
+ },
457
+ [getSimilarProductsByImage, handleImageSearch]
458
+ );
459
+
460
+ const fetchSimilarProductsByImageCrop = useCallback(
461
+ async (base64Image: string) => {
462
+ const base64Data = base64Image.startsWith('data:')
463
+ ? base64Image.split(',')[1]
464
+ : base64Image;
465
+
466
+ return handleImageSearch(
467
+ async () =>
468
+ getSimilarProductsByImage({
469
+ image: base64Data,
470
+ limit: 20,
471
+ excluded_product_ids: product?.pk ? [product.pk] : undefined
472
+ }).unwrap(),
473
+ base64Image,
474
+ 'Image crop search'
475
+ );
476
+ },
477
+ [getSimilarProductsByImage, handleImageSearch, product]
478
+ );
479
+
480
+ const handleFileUpload = async (
481
+ event: React.ChangeEvent<HTMLInputElement>
482
+ ) => {
483
+ setFileError('');
484
+ const file = event.target.files?.[0];
485
+ if (!file) return;
486
+
487
+ try {
488
+ const validation = await validateImageFileWithTranslation(file);
489
+ if (!validation.isValid) {
490
+ setFileError(validation.error!);
491
+ return;
492
+ }
493
+
494
+ const reader = new FileReader();
495
+ reader.onload = async (e) => {
496
+ const dataUrl = e.target?.result as string;
497
+ setCurrentImageUrl(dataUrl);
498
+ setHasUploadedImage(true);
499
+
500
+ const metadataValidation = await validateImageFromDataUrl(dataUrl);
501
+ if (!metadataValidation.isValid) {
502
+ setFileError(
503
+ t('common.similar_products.errors.invalid_image_properties')
504
+ );
505
+ return;
506
+ }
507
+ fetchSimilarProductsByBase64(dataUrl);
508
+ };
509
+
510
+ reader.onerror = () =>
511
+ setFileError(t('common.similar_products.errors.file_read_error'));
512
+ reader.readAsDataURL(file);
513
+ } catch (error) {
514
+ setFileError(t('common.similar_products.errors.processing_error'));
515
+ }
516
+ };
517
+
518
+ const handleImageCrop = async (base64Image: string) => {
519
+ setFileError('');
520
+
521
+ try {
522
+ setCurrentImageUrl(base64Image);
523
+
524
+ const metadataValidation = await validateImageFromDataUrl(base64Image);
525
+ if (!metadataValidation.isValid) {
526
+ setFileError(
527
+ t('common.similar_products.errors.invalid_image_properties')
528
+ );
529
+ return;
530
+ }
531
+
532
+ if (hasUploadedImage) {
533
+ await fetchSimilarProductsByBase64(base64Image);
534
+ } else {
535
+ await fetchSimilarProductsByImageCrop(base64Image);
536
+ }
537
+ } catch (error) {
538
+ setFileError(t('common.similar_products.errors.crop_processing_error'));
539
+ }
540
+ };
541
+
542
+ const handleSortChange = async (sortValue: string) => {
543
+ if (lastProductIds.length === 0) return;
544
+
545
+ if (searchResults?.sorters) {
546
+ const updatedSearchResults = {
547
+ ...searchResults,
548
+ sorters: updateSorterState(searchResults.sorters, sortValue)
549
+ };
550
+ updateResultsAndKey(updatedSearchResults);
551
+ }
552
+
553
+ const searchParams = buildSearchParams(searchResults?.facets, sortValue);
554
+ const result = await executeProductSearch(
555
+ lastProductIds,
556
+ searchParams,
557
+ sortValue
558
+ );
559
+
560
+ if (result) {
561
+ updateResultsAndKey(result);
562
+ }
563
+ };
564
+
565
+ const handlePageChange = async (page: number, sortValue?: string) => {
566
+ if (lastProductIds.length === 0) return;
567
+
568
+ const currentSort =
569
+ sortValue || searchResults?.sorters?.find((s) => s.is_selected)?.value;
570
+ const searchParams = buildSearchParams(
571
+ searchResults?.facets,
572
+ currentSort,
573
+ page
574
+ );
575
+ const result = await executeProductSearch(
576
+ lastProductIds,
577
+ searchParams,
578
+ currentSort
579
+ );
580
+
581
+ if (result) {
582
+ updateResultsAndKey(result);
583
+ }
584
+ };
585
+
586
+ const updateFacetSelection = useCallback(
587
+ (facetKey: string, choiceValue: string | number, isSelected: boolean) => {
588
+ if (!searchResults || String(facetKey) === 'category_ids') return null;
589
+
590
+ return {
591
+ ...searchResults,
592
+ facets:
593
+ searchResults.facets?.map((facet) => {
594
+ if (String(facet.key) === String(facetKey)) {
595
+ return {
596
+ ...facet,
597
+ data: {
598
+ ...facet.data,
599
+ choices:
600
+ facet.data?.choices?.map((choice) => {
601
+ if (String(choice.value) === String(choiceValue)) {
602
+ return { ...choice, is_selected: isSelected };
603
+ }
604
+ return choice;
605
+ }) || []
606
+ }
607
+ };
608
+ }
609
+ return facet;
610
+ }) || []
611
+ };
612
+ },
613
+ [searchResults]
614
+ );
615
+
616
+ const handleFacetChange = async (
617
+ facetKey: string,
618
+ choiceValue: string | number
619
+ ) => {
620
+ if (
621
+ !searchResults ||
622
+ lastProductIds.length === 0 ||
623
+ String(facetKey) === 'category_ids'
624
+ )
625
+ return;
626
+
627
+ const targetFacet = searchResults.facets?.find(
628
+ (f) => String(f.key) === String(facetKey)
629
+ );
630
+ const targetChoice = targetFacet?.data?.choices?.find(
631
+ (c) => String(c.value) === String(choiceValue)
632
+ );
633
+
634
+ const updatedSearchResults = updateFacetSelection(
635
+ facetKey,
636
+ choiceValue,
637
+ !(targetChoice?.is_selected || false)
638
+ );
639
+
640
+ if (!updatedSearchResults) return;
641
+
642
+ updateResultsAndKey(updatedSearchResults);
643
+
644
+ const searchParams = buildSearchParams(updatedSearchResults.facets);
645
+ const result = await executeProductSearch(lastProductIds, searchParams);
646
+
647
+ if (result) {
648
+ const finalResult = {
649
+ ...result,
650
+ facets: preserveFacetState(result.facets, updatedSearchResults.facets)
651
+ };
652
+ updateResultsAndKey(finalResult);
653
+ }
654
+ };
655
+
656
+ const removeFacetFilter = async (
657
+ facetKey: string,
658
+ choiceValue: string | number
659
+ ) => {
660
+ if (
661
+ !searchResults ||
662
+ lastProductIds.length === 0 ||
663
+ String(facetKey) === 'category_ids'
664
+ )
665
+ return;
666
+
667
+ const updatedSearchResults = updateFacetSelection(
668
+ facetKey,
669
+ choiceValue,
670
+ false
671
+ );
672
+ if (!updatedSearchResults) return;
673
+
674
+ const searchParams = buildSearchParams(updatedSearchResults.facets);
675
+ const result = await executeProductSearch(lastProductIds, searchParams);
676
+
677
+ if (result) {
678
+ const finalResult = {
679
+ ...result,
680
+ facets: preserveFacetState(result.facets, updatedSearchResults.facets)
681
+ };
682
+ updateResultsAndKey(finalResult);
683
+ }
684
+ };
685
+
686
+ const clearError = useCallback(() => {
687
+ setFileError('');
688
+ }, []);
689
+
690
+ const clearFileInput = useCallback(
691
+ (fileInputRef: React.RefObject<HTMLInputElement>) => {
692
+ if (fileInputRef.current) {
693
+ fileInputRef.current.value = '';
694
+ }
695
+ },
696
+ []
697
+ );
698
+
699
+ return {
700
+ currentImageUrl,
701
+ setCurrentImageUrl,
702
+ isLoading,
703
+ fileError,
704
+ searchResults,
705
+ resultsKey,
706
+ hasUploadedImage,
707
+ setHasUploadedImage,
708
+ handleFileUpload,
709
+ handleImageCrop,
710
+ fetchSimilarProductsByImageUrl,
711
+ fetchSimilarProductsByBase64,
712
+ fetchSimilarProductsByImageCrop,
713
+ handleSortChange,
714
+ handlePageChange,
715
+ handleFacetChange,
716
+ removeFacetFilter,
717
+ clearError,
718
+ clearFileInput
719
+ };
720
+ }