@akinon/pz-similar-products 1.92.0-rc.16

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.
@@ -0,0 +1,939 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { Product } from '@akinon/next/types';
3
+ import {
4
+ useLazyGetSimilarProductsByUrlQuery,
5
+ useGetSimilarProductsByImageMutation,
6
+ useLazyGetSimilarProductsListQuery,
7
+ type SimilarProductsListResponse
8
+ } from '../data/endpoints';
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 '../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 [isCropProcessing, setIsCropProcessing] = useState(false);
34
+ const [loadedPages, setLoadedPages] = useState<Set<number>>(new Set([1]));
35
+ const [allLoadedProducts, setAllLoadedProducts] = useState<Product[]>([]);
36
+
37
+ const [fetchSimilarProductsByUrl, { isLoading: isUrlSearchLoading }] =
38
+ useLazyGetSimilarProductsByUrlQuery();
39
+
40
+ const [getSimilarProductsByImage, { isLoading: isImageSearchLoading }] =
41
+ useGetSimilarProductsByImageMutation();
42
+
43
+ const [fetchProductsList, { isLoading: isProductsListLoading }] =
44
+ useLazyGetSimilarProductsListQuery();
45
+
46
+ const isLoading =
47
+ isUrlSearchLoading ||
48
+ isImageSearchLoading ||
49
+ isProductsListLoading ||
50
+ isCropProcessing;
51
+
52
+ const createEmptySearchResults = useCallback(
53
+ (): SearchResults => ({
54
+ products: [],
55
+ facets: [],
56
+ sorters: [],
57
+ search_text: null,
58
+ pagination: {
59
+ current_page: 1,
60
+ num_pages: 1,
61
+ page_size: 1,
62
+ total_count: 0
63
+ }
64
+ }),
65
+ []
66
+ );
67
+
68
+ const updateResultsAndKey = useCallback(
69
+ (results: SearchResults, isLoadMore: boolean = false) => {
70
+ if (isLoadMore) {
71
+ setAllLoadedProducts((prev) => [...prev, ...results.products]);
72
+ setSearchResults((prevResults) => ({
73
+ ...results,
74
+ products: [...(prevResults?.products || []), ...results.products]
75
+ }));
76
+ } else {
77
+ setAllLoadedProducts(results.products);
78
+ setSearchResults(results);
79
+ setLoadedPages(new Set([1]));
80
+ }
81
+ setResultsKey((prevKey) => prevKey + 1);
82
+ },
83
+ []
84
+ );
85
+
86
+ const buildSearchParams = useCallback(
87
+ (facets?: Facet[], sortValue?: string, page?: number): SearchParams => {
88
+ const searchParams: SearchParams = {};
89
+
90
+ if (page) {
91
+ searchParams.page = String(page);
92
+ }
93
+
94
+ // Test için farklı parametre isimlerini deniyoruz
95
+ searchParams.page_size = '1';
96
+ searchParams.pageSize = '1';
97
+ searchParams.limit = '1';
98
+ searchParams.per_page = '1';
99
+ searchParams.size = '1';
100
+ console.log('🔍 API çağrısı searchParams:', searchParams);
101
+
102
+ if (facets) {
103
+ facets.forEach((facet) => {
104
+ if (String(facet.key) === 'category_ids') return;
105
+
106
+ const selectedChoices =
107
+ facet.data?.choices?.filter((choice) => choice.is_selected) || [];
108
+
109
+ if (selectedChoices.length > 0) {
110
+ const values = selectedChoices.map((choice) =>
111
+ String(choice.value)
112
+ );
113
+ const searchKey = String(facet.search_key || facet.key);
114
+ searchParams[searchKey] = values.join(',');
115
+ }
116
+ });
117
+ }
118
+
119
+ if (sortValue) {
120
+ searchParams.sorter = sortValue;
121
+ }
122
+
123
+ return searchParams;
124
+ },
125
+ []
126
+ );
127
+
128
+ const preserveFacetState = useCallback(
129
+ (responseFacets: Facet[], existingFacets: Facet[]) => {
130
+ return (
131
+ responseFacets?.map((responseFacet) => {
132
+ const existingFacet = existingFacets?.find(
133
+ (f) => String(f.key) === String(responseFacet.key)
134
+ );
135
+ if (existingFacet && responseFacet.data?.choices) {
136
+ return {
137
+ ...responseFacet,
138
+ data: {
139
+ ...responseFacet.data,
140
+ choices: responseFacet.data.choices.map((choice) => {
141
+ const existingChoice = existingFacet.data?.choices?.find(
142
+ (c) => String(c.value) === String(choice.value)
143
+ );
144
+ return {
145
+ ...choice,
146
+ is_selected: existingChoice?.is_selected || false
147
+ };
148
+ })
149
+ }
150
+ };
151
+ }
152
+ return responseFacet;
153
+ }) ||
154
+ existingFacets ||
155
+ []
156
+ );
157
+ },
158
+ []
159
+ );
160
+
161
+ const updateSorterState = useCallback(
162
+ (sorters: SortOption[], selectedValue: string) => {
163
+ return (
164
+ sorters?.map((sorter) => ({
165
+ ...sorter,
166
+ is_selected: String(sorter.value) === String(selectedValue)
167
+ })) || []
168
+ );
169
+ },
170
+ []
171
+ );
172
+
173
+ const sortProducts = useCallback(
174
+ (products: SearchResults['products'], sortValue: string) => {
175
+ if (!products || products.length === 0) return products;
176
+
177
+ const sortedProducts = [...products];
178
+
179
+ switch (sortValue) {
180
+ case 'price':
181
+ return sortedProducts.sort((a, b) => {
182
+ const priceA = parseFloat(String(a.price || '0'));
183
+ const priceB = parseFloat(String(b.price || '0'));
184
+ return isNaN(priceA) || isNaN(priceB) ? 0 : priceA - priceB;
185
+ });
186
+ case '-price':
187
+ return sortedProducts.sort((a, b) => {
188
+ const priceA = parseFloat(String(a.price || '0'));
189
+ const priceB = parseFloat(String(b.price || '0'));
190
+ return isNaN(priceA) || isNaN(priceB) ? 0 : priceB - priceA;
191
+ });
192
+ case 'newcomers':
193
+ return sortedProducts.sort((a, b) => {
194
+ const pkA = parseInt(String(a.pk || '0'), 10);
195
+ const pkB = parseInt(String(b.pk || '0'), 10);
196
+ return isNaN(pkA) || isNaN(pkB) ? 0 : pkB - pkA;
197
+ });
198
+ case 'default':
199
+ default:
200
+ return products;
201
+ }
202
+ },
203
+ []
204
+ );
205
+
206
+ const createSearchFilter = useCallback(
207
+ (filterData: number[] | Record<string, unknown> | unknown) => {
208
+ let filterObject: Record<string, unknown>;
209
+
210
+ if (Array.isArray(filterData)) {
211
+ filterObject = {
212
+ 'products.pk': filterData,
213
+ default_sorting_deactivated: true
214
+ };
215
+ } else if (typeof filterData === 'object' && filterData !== null) {
216
+ filterObject = {
217
+ ...(filterData as Record<string, unknown>),
218
+ default_sorting_deactivated: true
219
+ };
220
+ } else {
221
+ filterObject = {
222
+ default_sorting_deactivated: true
223
+ };
224
+ }
225
+
226
+ const jsonString = JSON.stringify(filterObject);
227
+ return btoa(jsonString);
228
+ },
229
+ []
230
+ );
231
+
232
+ const executeProductSearch = useCallback(
233
+ async (
234
+ productIds: number[],
235
+ searchParams: SearchParams = {},
236
+ currentSortValue?: string
237
+ ): Promise<SearchResults | null> => {
238
+ try {
239
+ const base64Filter = createSearchFilter(productIds);
240
+ const result = await fetchProductsList({
241
+ filter: base64Filter,
242
+ searchParams,
243
+ isExclude: false
244
+ }).unwrap();
245
+
246
+ const sortedProducts = currentSortValue
247
+ ? sortProducts(result.products, currentSortValue)
248
+ : result.products;
249
+
250
+ return {
251
+ ...result,
252
+ products: sortedProducts,
253
+ facets: preserveFacetState(
254
+ result.facets,
255
+ searchResults?.facets || []
256
+ ),
257
+ sorters: updateSorterState(
258
+ result.sorters,
259
+ currentSortValue ||
260
+ searchResults?.sorters?.find((s) => s.is_selected)?.value ||
261
+ 'default'
262
+ )
263
+ };
264
+ } catch (error) {
265
+ console.error('Product search failed:', error);
266
+ return createEmptySearchResults();
267
+ }
268
+ },
269
+ [
270
+ createSearchFilter,
271
+ fetchProductsList,
272
+ sortProducts,
273
+ preserveFacetState,
274
+ updateSorterState,
275
+ searchResults,
276
+ createEmptySearchResults
277
+ ]
278
+ );
279
+
280
+ const extractProductIds = useCallback((data: unknown): number[] => {
281
+ if (!data) return [];
282
+
283
+ if (data && typeof data === 'object') {
284
+ const obj = data as Record<string, unknown>;
285
+
286
+ const possibleArrayKeys = [
287
+ 'product_ids',
288
+ 'productIds',
289
+ 'products',
290
+ 'results'
291
+ ];
292
+
293
+ for (const key of possibleArrayKeys) {
294
+ if (obj[key] && Array.isArray(obj[key])) {
295
+ const arr = obj[key] as unknown[];
296
+ const filtered = arr
297
+ .map((item) => {
298
+ if (typeof item === 'number') return item;
299
+ if (typeof item === 'string') {
300
+ const parsed = parseInt(item, 10);
301
+ return isNaN(parsed) ? null : parsed;
302
+ }
303
+ return null;
304
+ })
305
+ .filter((id): id is number => id !== null && id > 0);
306
+
307
+ if (filtered.length > 0) return filtered;
308
+ }
309
+ }
310
+
311
+ if (obj.similar_products && Array.isArray(obj.similar_products)) {
312
+ return obj.similar_products
313
+ .map((item) => {
314
+ if (item && typeof item === 'object' && 'product_id' in item) {
315
+ const productId = (item as any).product_id;
316
+ if (typeof productId === 'number') return productId;
317
+ if (typeof productId === 'string') {
318
+ const parsed = parseInt(productId, 10);
319
+ return isNaN(parsed) ? null : parsed;
320
+ }
321
+ }
322
+ return null;
323
+ })
324
+ .filter((id): id is number => id !== null && id > 0);
325
+ }
326
+ }
327
+
328
+ if (Array.isArray(data)) {
329
+ return data
330
+ .map((item) => {
331
+ if (typeof item === 'number') return item;
332
+ if (typeof item === 'string') {
333
+ const parsed = parseInt(item, 10);
334
+ return isNaN(parsed) ? null : parsed;
335
+ }
336
+ return null;
337
+ })
338
+ .filter((id): id is number => id !== null && id > 0);
339
+ }
340
+
341
+ return [];
342
+ }, []);
343
+
344
+ const handleSearchResults = useCallback(
345
+ async (data: unknown) => {
346
+ const productIds = extractProductIds(data);
347
+
348
+ if (productIds.length > 0) {
349
+ const result = await executeProductSearch(productIds);
350
+ if (result) {
351
+ const resultWithCorrectSorterState = {
352
+ ...result,
353
+ sorters: updateSorterState(result.sorters, 'default')
354
+ };
355
+ updateResultsAndKey(resultWithCorrectSorterState);
356
+ }
357
+ } else {
358
+ updateResultsAndKey(createEmptySearchResults());
359
+ }
360
+
361
+ setLastProductIds(productIds);
362
+ },
363
+ [
364
+ extractProductIds,
365
+ executeProductSearch,
366
+ updateSorterState,
367
+ updateResultsAndKey,
368
+ createEmptySearchResults
369
+ ]
370
+ );
371
+
372
+ const validateImageFileWithTranslation = useCallback(
373
+ async (file: File): Promise<ImageValidationResult> => {
374
+ try {
375
+ const result = await validateImageFile(file, 5);
376
+
377
+ if (!result.isValid) {
378
+ if (result.error?.includes('size')) {
379
+ return {
380
+ isValid: false,
381
+ error: t('common.similar_products.errors.file_size_too_large')
382
+ };
383
+ }
384
+ if (result.error?.includes('not a valid image')) {
385
+ return {
386
+ isValid: false,
387
+ error: t('common.similar_products.errors.invalid_image_file')
388
+ };
389
+ }
390
+ if (result.error?.includes('format')) {
391
+ return {
392
+ isValid: false,
393
+ error: t('common.similar_products.errors.invalid_image_format')
394
+ };
395
+ }
396
+ }
397
+
398
+ return result;
399
+ } catch (error) {
400
+ return {
401
+ isValid: false,
402
+ error: t('common.similar_products.errors.processing_error')
403
+ };
404
+ }
405
+ },
406
+ [t]
407
+ );
408
+
409
+ const handleImageSearch = useCallback(
410
+ async (
411
+ searchFn: (image: string) => Promise<unknown>,
412
+ imageData: string,
413
+ errorPrefix: string
414
+ ) => {
415
+ setFileError('');
416
+ try {
417
+ const result = await searchFn(imageData);
418
+ await handleSearchResults(result);
419
+ return result;
420
+ } catch (error) {
421
+ console.error(`${errorPrefix} failed:`, error);
422
+ setFileError(t('common.similar_products.errors.search_failed'));
423
+ updateResultsAndKey(createEmptySearchResults());
424
+ return null;
425
+ }
426
+ },
427
+ [
428
+ handleSearchResults,
429
+ updateResultsAndKey,
430
+ createEmptySearchResults,
431
+ t,
432
+ setFileError
433
+ ]
434
+ );
435
+
436
+ const fetchSimilarProductsByImageUrl = useCallback(
437
+ async (imageUrl: string) => {
438
+ setFileError('');
439
+ try {
440
+ const productPk = product?.pk;
441
+ const excludedIds = productPk ? [productPk] : undefined;
442
+
443
+ const result = await fetchSimilarProductsByUrl({
444
+ url: imageUrl,
445
+ limit: 20,
446
+ excluded_product_ids: excludedIds
447
+ }).unwrap();
448
+
449
+ await handleSearchResults(result);
450
+ return result;
451
+ } catch (error) {
452
+ console.error('Image search request failed:', error);
453
+ setFileError(t('common.similar_products.errors.connection_failed'));
454
+ updateResultsAndKey(createEmptySearchResults());
455
+ return null;
456
+ }
457
+ },
458
+ [
459
+ fetchSimilarProductsByUrl,
460
+ product?.pk,
461
+ handleSearchResults,
462
+ updateResultsAndKey,
463
+ createEmptySearchResults,
464
+ t,
465
+ setFileError
466
+ ]
467
+ );
468
+
469
+ useEffect(() => {
470
+ if (product?.productimage_set?.length > 0) {
471
+ const initialImageUrl = product.productimage_set[0].image;
472
+ setCurrentImageUrl(initialImageUrl);
473
+ }
474
+ }, [product]);
475
+
476
+ const fetchSimilarProductsByBase64 = useCallback(
477
+ async (base64Image: string) => {
478
+ const base64Data = base64Image.startsWith('data:')
479
+ ? base64Image.split(',')[1]
480
+ : base64Image;
481
+
482
+ return handleImageSearch(
483
+ async () =>
484
+ getSimilarProductsByImage({
485
+ image: base64Data,
486
+ limit: 20
487
+ }).unwrap(),
488
+ base64Image,
489
+ 'Image search'
490
+ );
491
+ },
492
+ [getSimilarProductsByImage, handleImageSearch]
493
+ );
494
+
495
+ const fetchSimilarProductsByImageCrop = useCallback(
496
+ async (dataString: string) => {
497
+ setFileError('');
498
+ if (dataString.startsWith('data:application/x-cors-fallback;base64,')) {
499
+ try {
500
+ const fallbackDataEncoded = dataString.replace(
501
+ 'data:application/x-cors-fallback;base64,',
502
+ ''
503
+ );
504
+ const fallbackData = JSON.parse(atob(fallbackDataEncoded));
505
+
506
+ try {
507
+ const proxyResponse = await fetch('/api/image-proxy', {
508
+ method: 'POST',
509
+ headers: { 'Content-Type': 'application/json' },
510
+ body: JSON.stringify({ imageUrl: fallbackData.originalUrl })
511
+ });
512
+
513
+ if (proxyResponse.ok) {
514
+ const proxyResult = await proxyResponse.json();
515
+ if (proxyResult.success && proxyResult.base64Image) {
516
+ return new Promise((resolve) => {
517
+ const img = new Image();
518
+ img.onload = () => {
519
+ try {
520
+ const canvas = document.createElement('canvas');
521
+ const ctx = canvas.getContext('2d');
522
+
523
+ if (!ctx) {
524
+ throw new Error('No canvas context');
525
+ }
526
+
527
+ const cropX = fallbackData.crop.x * fallbackData.scale.x;
528
+ const cropY = fallbackData.crop.y * fallbackData.scale.y;
529
+ const cropWidth =
530
+ fallbackData.crop.width * fallbackData.scale.x;
531
+ const cropHeight =
532
+ fallbackData.crop.height * fallbackData.scale.y;
533
+
534
+ canvas.width = cropWidth;
535
+ canvas.height = cropHeight;
536
+
537
+ ctx.drawImage(
538
+ img,
539
+ cropX,
540
+ cropY,
541
+ cropWidth,
542
+ cropHeight,
543
+ 0,
544
+ 0,
545
+ cropWidth,
546
+ cropHeight
547
+ );
548
+
549
+ const croppedBase64 = canvas.toDataURL('image/jpeg');
550
+
551
+ const base64Data = croppedBase64.startsWith('data:')
552
+ ? croppedBase64.split(',')[1]
553
+ : croppedBase64;
554
+
555
+ const result = handleImageSearch(
556
+ async () =>
557
+ getSimilarProductsByImage({
558
+ image: base64Data,
559
+ limit: 20,
560
+ excluded_product_ids: product?.pk
561
+ ? [product.pk]
562
+ : undefined
563
+ }).unwrap(),
564
+ croppedBase64,
565
+ 'Proxy crop search'
566
+ );
567
+ resolve(result);
568
+ } catch (cropError) {
569
+ console.error('Client-side crop failed:', cropError);
570
+ const result = fetchSimilarProductsByImageUrl(
571
+ fallbackData.originalUrl
572
+ );
573
+ resolve(result);
574
+ }
575
+ };
576
+
577
+ img.onerror = () => {
578
+ console.error('Failed to load proxy image');
579
+ const result = fetchSimilarProductsByImageUrl(
580
+ fallbackData.originalUrl
581
+ );
582
+ resolve(result);
583
+ };
584
+
585
+ img.src = proxyResult.base64Image;
586
+ });
587
+ }
588
+ }
589
+ } catch (proxyError) {
590
+ console.warn('Proxy method failed:', proxyError);
591
+ }
592
+
593
+ return fetchSimilarProductsByImageUrl(fallbackData.originalUrl);
594
+ } catch (error) {
595
+ console.error('Failed to parse CORS fallback data:', error);
596
+ setFileError(
597
+ t('common.similar_products.errors.crop_processing_error')
598
+ );
599
+ return null;
600
+ }
601
+ }
602
+
603
+ const base64Data = dataString.startsWith('data:')
604
+ ? dataString.split(',')[1]
605
+ : dataString;
606
+
607
+ return handleImageSearch(
608
+ async () =>
609
+ getSimilarProductsByImage({
610
+ image: base64Data,
611
+ limit: 20,
612
+ excluded_product_ids: product?.pk ? [product.pk] : undefined
613
+ }).unwrap(),
614
+ dataString,
615
+ 'Image crop search'
616
+ );
617
+ },
618
+ [
619
+ getSimilarProductsByImage,
620
+ handleImageSearch,
621
+ product,
622
+ fetchSimilarProductsByImageUrl,
623
+ setFileError,
624
+ t
625
+ ]
626
+ );
627
+
628
+ const handleFileUpload = async (
629
+ event: React.ChangeEvent<HTMLInputElement>
630
+ ) => {
631
+ setFileError('');
632
+ const file = event.target.files?.[0];
633
+ if (!file) return;
634
+
635
+ try {
636
+ const validation = await validateImageFileWithTranslation(file);
637
+ if (!validation.isValid) {
638
+ setFileError(validation.error!);
639
+ return;
640
+ }
641
+
642
+ const reader = new FileReader();
643
+ reader.onload = async (e) => {
644
+ const dataUrl = e.target?.result as string;
645
+ setCurrentImageUrl(dataUrl);
646
+ setHasUploadedImage(true);
647
+
648
+ const metadataValidation = await validateImageFromDataUrl(dataUrl);
649
+ if (!metadataValidation.isValid) {
650
+ setFileError(
651
+ t('common.similar_products.errors.invalid_image_properties')
652
+ );
653
+ return;
654
+ }
655
+ fetchSimilarProductsByBase64(dataUrl);
656
+ };
657
+
658
+ reader.onerror = () =>
659
+ setFileError(t('common.similar_products.errors.file_read_error'));
660
+ reader.readAsDataURL(file);
661
+ } catch (error) {
662
+ setFileError(t('common.similar_products.errors.processing_error'));
663
+ }
664
+ };
665
+
666
+ const handleImageCrop = async (base64Image: string) => {
667
+ if (isCropProcessing || isImageSearchLoading || isProductsListLoading) {
668
+ return;
669
+ }
670
+
671
+ setIsCropProcessing(true);
672
+ setFileError('');
673
+
674
+ try {
675
+ setCurrentImageUrl(base64Image);
676
+
677
+ const metadataValidation = await validateImageFromDataUrl(base64Image);
678
+ if (!metadataValidation.isValid) {
679
+ setFileError(
680
+ t('common.similar_products.errors.invalid_image_properties')
681
+ );
682
+ return;
683
+ }
684
+
685
+ if (hasUploadedImage) {
686
+ await fetchSimilarProductsByBase64(base64Image);
687
+ } else {
688
+ await fetchSimilarProductsByImageCrop(base64Image);
689
+ }
690
+ } catch (error) {
691
+ setFileError(t('common.similar_products.errors.crop_processing_error'));
692
+ } finally {
693
+ setIsCropProcessing(false);
694
+ }
695
+ };
696
+
697
+ const handleSortChange = async (sortValue: string) => {
698
+ if (lastProductIds.length === 0) return;
699
+
700
+ if (searchResults?.sorters) {
701
+ const updatedSearchResults = {
702
+ ...searchResults,
703
+ sorters: updateSorterState(searchResults.sorters, sortValue)
704
+ };
705
+ updateResultsAndKey(updatedSearchResults);
706
+ }
707
+
708
+ const searchParams = buildSearchParams(searchResults?.facets, sortValue);
709
+ const result = await executeProductSearch(
710
+ lastProductIds,
711
+ searchParams,
712
+ sortValue
713
+ );
714
+
715
+ if (result) {
716
+ updateResultsAndKey(result);
717
+ }
718
+ };
719
+
720
+ const handlePageChange = async (page: number, sortValue?: string) => {
721
+ if (lastProductIds.length === 0) return;
722
+
723
+ const currentSort =
724
+ sortValue || searchResults?.sorters?.find((s) => s.is_selected)?.value;
725
+ const searchParams = buildSearchParams(
726
+ searchResults?.facets,
727
+ currentSort,
728
+ page
729
+ );
730
+ const result = await executeProductSearch(
731
+ lastProductIds,
732
+ searchParams,
733
+ currentSort
734
+ );
735
+
736
+ if (result) {
737
+ updateResultsAndKey(result);
738
+ }
739
+ };
740
+
741
+ const updateFacetSelection = useCallback(
742
+ (facetKey: string, choiceValue: string | number, isSelected: boolean) => {
743
+ if (!searchResults || String(facetKey) === 'category_ids') return null;
744
+
745
+ return {
746
+ ...searchResults,
747
+ facets:
748
+ searchResults.facets?.map((facet) => {
749
+ if (String(facet.key) === String(facetKey)) {
750
+ return {
751
+ ...facet,
752
+ data: {
753
+ ...facet.data,
754
+ choices:
755
+ facet.data?.choices?.map((choice) => {
756
+ if (String(choice.value) === String(choiceValue)) {
757
+ return { ...choice, is_selected: isSelected };
758
+ }
759
+ return choice;
760
+ }) || []
761
+ }
762
+ };
763
+ }
764
+ return facet;
765
+ }) || []
766
+ };
767
+ },
768
+ [searchResults]
769
+ );
770
+
771
+ const handleFacetChange = async (
772
+ facetKey: string,
773
+ choiceValue: string | number
774
+ ) => {
775
+ if (
776
+ !searchResults ||
777
+ lastProductIds.length === 0 ||
778
+ String(facetKey) === 'category_ids'
779
+ )
780
+ return;
781
+
782
+ const targetFacet = searchResults.facets?.find(
783
+ (f) => String(f.key) === String(facetKey)
784
+ );
785
+ const targetChoice = targetFacet?.data?.choices?.find(
786
+ (c) => String(c.value) === String(choiceValue)
787
+ );
788
+
789
+ const updatedSearchResults = updateFacetSelection(
790
+ facetKey,
791
+ choiceValue,
792
+ !(targetChoice?.is_selected || false)
793
+ );
794
+
795
+ if (!updatedSearchResults) return;
796
+
797
+ const resultsWithResetSorter = {
798
+ ...updatedSearchResults,
799
+ sorters:
800
+ updatedSearchResults.sorters?.map((sorter) => ({
801
+ ...sorter,
802
+ is_selected: sorter.value === 'default'
803
+ })) || []
804
+ };
805
+
806
+ updateResultsAndKey(resultsWithResetSorter);
807
+
808
+ const searchParams = buildSearchParams(resultsWithResetSorter.facets);
809
+ const result = await executeProductSearch(lastProductIds, searchParams);
810
+
811
+ if (result) {
812
+ const finalResult = {
813
+ ...result,
814
+ facets: preserveFacetState(
815
+ result.facets,
816
+ resultsWithResetSorter.facets
817
+ ),
818
+ sorters: resultsWithResetSorter.sorters // Reset edilmiş sorter'ları kullan
819
+ };
820
+ updateResultsAndKey(finalResult);
821
+ }
822
+ };
823
+
824
+ const removeFacetFilter = async (
825
+ facetKey: string,
826
+ choiceValue: string | number
827
+ ) => {
828
+ if (
829
+ !searchResults ||
830
+ lastProductIds.length === 0 ||
831
+ String(facetKey) === 'category_ids'
832
+ )
833
+ return;
834
+
835
+ const updatedSearchResults = updateFacetSelection(
836
+ facetKey,
837
+ choiceValue,
838
+ false
839
+ );
840
+ if (!updatedSearchResults) return;
841
+
842
+ const resultsWithResetSorter = {
843
+ ...updatedSearchResults,
844
+ sorters:
845
+ updatedSearchResults.sorters?.map((sorter) => ({
846
+ ...sorter,
847
+ is_selected: sorter.value === 'default'
848
+ })) || []
849
+ };
850
+
851
+ const searchParams = buildSearchParams(resultsWithResetSorter.facets);
852
+ const result = await executeProductSearch(lastProductIds, searchParams);
853
+
854
+ if (result) {
855
+ const finalResult = {
856
+ ...result,
857
+ facets: preserveFacetState(
858
+ result.facets,
859
+ resultsWithResetSorter.facets
860
+ ),
861
+ sorters: resultsWithResetSorter.sorters
862
+ };
863
+ updateResultsAndKey(finalResult);
864
+ }
865
+ };
866
+
867
+ const clearError = useCallback(() => {
868
+ setFileError('');
869
+ }, []);
870
+
871
+ const clearFileInput = useCallback(
872
+ (fileInputRef: React.RefObject<HTMLInputElement>) => {
873
+ if (fileInputRef.current) {
874
+ fileInputRef.current.value = '';
875
+ }
876
+ },
877
+ []
878
+ );
879
+
880
+ const handleLoadMore = async () => {
881
+ if (!searchResults?.pagination || isLoading) return;
882
+
883
+ const nextPage = searchResults.pagination.current_page + 1;
884
+ if (nextPage > searchResults.pagination.num_pages) return;
885
+
886
+ const currentSortValue =
887
+ searchResults.sorters?.find((s) => s.is_selected)?.value || 'default';
888
+ const searchParams = buildSearchParams(
889
+ searchResults.facets,
890
+ currentSortValue,
891
+ nextPage
892
+ );
893
+ const result = await executeProductSearch(
894
+ lastProductIds,
895
+ searchParams,
896
+ currentSortValue
897
+ );
898
+
899
+ if (result) {
900
+ setLoadedPages((prev) => new Set([...Array.from(prev), nextPage]));
901
+
902
+ updateResultsAndKey(
903
+ {
904
+ ...result,
905
+ pagination: {
906
+ ...result.pagination,
907
+ current_page: nextPage
908
+ }
909
+ },
910
+ true
911
+ );
912
+ }
913
+ };
914
+
915
+ return {
916
+ currentImageUrl,
917
+ setCurrentImageUrl,
918
+ isLoading,
919
+ fileError,
920
+ searchResults,
921
+ resultsKey,
922
+ hasUploadedImage,
923
+ setHasUploadedImage,
924
+ handleFileUpload,
925
+ handleImageCrop,
926
+ fetchSimilarProductsByImageUrl,
927
+ fetchSimilarProductsByBase64,
928
+ fetchSimilarProductsByImageCrop,
929
+ handleSortChange,
930
+ handlePageChange,
931
+ handleFacetChange,
932
+ removeFacetFilter,
933
+ handleLoadMore,
934
+ loadedPages: Array.from(loadedPages),
935
+ allLoadedProducts,
936
+ clearError,
937
+ clearFileInput
938
+ };
939
+ }