@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.
- package/CHANGELOG.md +33 -1180
- package/api/similar-product-list.ts +63 -0
- package/api/similar-products.ts +109 -0
- package/components/accordion.tsx +5 -20
- package/components/file-input.tsx +3 -65
- package/components/input.tsx +0 -2
- package/components/link.tsx +12 -16
- package/components/modal.tsx +16 -32
- package/components/plugin-module.tsx +3 -13
- package/components/selected-payment-option-view.tsx +0 -11
- package/data/client/similar-products.ts +122 -0
- package/data/urls.ts +5 -1
- package/hocs/server/with-segment-defaults.tsx +2 -5
- package/hooks/index.ts +2 -0
- package/hooks/use-image-cropper.ts +160 -0
- package/hooks/use-similar-products.ts +720 -0
- package/instrumentation/node.ts +13 -15
- package/lib/cache.ts +0 -2
- package/middlewares/complete-gpay.ts +1 -2
- package/middlewares/complete-masterpass.ts +1 -2
- package/middlewares/default.ts +184 -196
- package/middlewares/index.ts +1 -3
- package/middlewares/redirection-payment.ts +1 -2
- package/middlewares/saved-card-redirection.ts +1 -2
- package/middlewares/three-d-redirection.ts +1 -2
- package/middlewares/url-redirection.ts +14 -8
- package/package.json +3 -3
- package/plugins.d.ts +0 -2
- package/plugins.js +1 -3
- package/redux/middlewares/checkout.ts +2 -15
- package/redux/reducers/checkout.ts +1 -9
- package/sentry/index.ts +17 -54
- package/types/commerce/order.ts +0 -1
- package/types/index.ts +73 -26
- package/utils/app-fetch.ts +2 -2
- package/utils/image-validation.ts +303 -0
- package/utils/redirect.ts +3 -5
- package/with-pz-config.js +5 -1
- package/data/server/basket.ts +0 -72
- package/hooks/use-loyalty-availability.ts +0 -21
- package/middlewares/wallet-complete-redirection.ts +0 -179
- 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
|
+
}
|