@akinon/pz-similar-products 1.113.0-rc.19 → 1.113.0-snapshot-ZERO-3878-20251203130841
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 +37 -23
- package/README.md +202 -0
- package/package.json +1 -1
- package/src/data/endpoints.ts +14 -5
- package/src/hooks/index.ts +0 -1
- package/src/hooks/use-similar-products.ts +148 -23
- package/src/types/index.ts +51 -0
- package/src/utils/index.ts +38 -3
- package/src/views/filters.tsx +27 -3
- package/src/views/header-image-search-feature.tsx +28 -34
- package/src/views/image-search-button.tsx +19 -23
- package/src/views/main.tsx +82 -5
- package/src/views/product-image-search-feature.tsx +22 -22
- package/src/views/search-button.tsx +12 -2
- package/src/views/search-modal.tsx +169 -10
- package/src/hooks/use-image-search-feature.ts +0 -32
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
2
|
import { Product } from '@akinon/next/types';
|
|
3
3
|
import {
|
|
4
4
|
useLazyGetSimilarProductsByUrlQuery,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
validateImageFromDataUrl,
|
|
14
14
|
type ImageValidationResult
|
|
15
15
|
} from '../utils/image-validation';
|
|
16
|
+
import { debounce } from '../utils';
|
|
16
17
|
|
|
17
18
|
type SearchResults = SimilarProductsListResponse;
|
|
18
19
|
|
|
@@ -24,6 +25,7 @@ export function useSimilarProducts(product: Product) {
|
|
|
24
25
|
const { t } = useLocalization();
|
|
25
26
|
const [currentImageUrl, setCurrentImageUrl] = useState('');
|
|
26
27
|
const [fileError, setFileError] = useState('');
|
|
28
|
+
const [searchText, setSearchText] = useState('');
|
|
27
29
|
const [searchResults, setSearchResults] = useState<SearchResults | null>(
|
|
28
30
|
null
|
|
29
31
|
);
|
|
@@ -33,6 +35,16 @@ export function useSimilarProducts(product: Product) {
|
|
|
33
35
|
const [isCropProcessing, setIsCropProcessing] = useState(false);
|
|
34
36
|
const [loadedPages, setLoadedPages] = useState<Set<number>>(new Set([1]));
|
|
35
37
|
const [allLoadedProducts, setAllLoadedProducts] = useState<Product[]>([]);
|
|
38
|
+
const [currentImageBase64, setCurrentImageBase64] = useState<string>('');
|
|
39
|
+
const [hasCroppedImage, setHasCroppedImage] = useState(false);
|
|
40
|
+
|
|
41
|
+
const searchTextRef = useRef<string>('');
|
|
42
|
+
const hasUploadedImageRef = useRef<boolean>(false);
|
|
43
|
+
|
|
44
|
+
const setSearchTextWithRef = useCallback((text: string) => {
|
|
45
|
+
setSearchText(text);
|
|
46
|
+
searchTextRef.current = text;
|
|
47
|
+
}, []);
|
|
36
48
|
|
|
37
49
|
const [fetchSimilarProductsByUrl, { isLoading: isUrlSearchLoading }] =
|
|
38
50
|
useLazyGetSimilarProductsByUrlQuery();
|
|
@@ -426,17 +438,24 @@ export function useSimilarProducts(product: Product) {
|
|
|
426
438
|
);
|
|
427
439
|
|
|
428
440
|
const fetchSimilarProductsByImageUrl = useCallback(
|
|
429
|
-
async (imageUrl: string) => {
|
|
441
|
+
async (imageUrl: string, overrideText?: string) => {
|
|
430
442
|
setFileError('');
|
|
443
|
+
|
|
431
444
|
try {
|
|
432
445
|
const productPk = product?.pk;
|
|
433
446
|
const excludedIds = productPk ? [productPk] : undefined;
|
|
434
447
|
|
|
435
|
-
const
|
|
448
|
+
const textToUse =
|
|
449
|
+
overrideText !== undefined ? overrideText : searchTextRef.current;
|
|
450
|
+
|
|
451
|
+
const requestParams = {
|
|
436
452
|
url: imageUrl,
|
|
437
453
|
limit: 20,
|
|
438
|
-
excluded_product_ids: excludedIds
|
|
439
|
-
|
|
454
|
+
excluded_product_ids: excludedIds,
|
|
455
|
+
text: textToUse || undefined
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const result = await fetchSimilarProductsByUrl(requestParams).unwrap();
|
|
440
459
|
|
|
441
460
|
await handleSearchResults(result);
|
|
442
461
|
return result;
|
|
@@ -450,6 +469,8 @@ export function useSimilarProducts(product: Product) {
|
|
|
450
469
|
[
|
|
451
470
|
fetchSimilarProductsByUrl,
|
|
452
471
|
product?.pk,
|
|
472
|
+
searchText,
|
|
473
|
+
searchTextRef,
|
|
453
474
|
handleSearchResults,
|
|
454
475
|
updateResultsAndKey,
|
|
455
476
|
createEmptySearchResults,
|
|
@@ -462,31 +483,62 @@ export function useSimilarProducts(product: Product) {
|
|
|
462
483
|
if (product?.productimage_set?.length > 0) {
|
|
463
484
|
const initialImageUrl = product.productimage_set[0].image;
|
|
464
485
|
setCurrentImageUrl(initialImageUrl);
|
|
486
|
+
setHasCroppedImage(false);
|
|
487
|
+
setCurrentImageBase64('');
|
|
488
|
+
setHasUploadedImage(false);
|
|
489
|
+
hasUploadedImageRef.current = false;
|
|
465
490
|
}
|
|
466
491
|
}, [product]);
|
|
467
492
|
|
|
468
493
|
const fetchSimilarProductsByBase64 = useCallback(
|
|
469
|
-
async (
|
|
494
|
+
async (
|
|
495
|
+
base64Image: string,
|
|
496
|
+
overrideText?: string,
|
|
497
|
+
forceExclude?: boolean
|
|
498
|
+
) => {
|
|
470
499
|
const base64Data = base64Image.startsWith('data:')
|
|
471
500
|
? base64Image.split(',')[1]
|
|
472
501
|
: base64Image;
|
|
473
502
|
|
|
503
|
+
const textToUse =
|
|
504
|
+
overrideText !== undefined ? overrideText : searchTextRef.current;
|
|
505
|
+
|
|
506
|
+
const shouldExclude =
|
|
507
|
+
forceExclude !== undefined ? forceExclude : !hasUploadedImage;
|
|
508
|
+
|
|
509
|
+
const requestData = {
|
|
510
|
+
image: base64Data,
|
|
511
|
+
limit: 20,
|
|
512
|
+
excluded_product_ids: shouldExclude
|
|
513
|
+
? product?.pk
|
|
514
|
+
? [product.pk]
|
|
515
|
+
: undefined
|
|
516
|
+
: undefined,
|
|
517
|
+
text: textToUse || undefined
|
|
518
|
+
};
|
|
519
|
+
|
|
474
520
|
return handleImageSearch(
|
|
475
|
-
async () =>
|
|
476
|
-
getSimilarProductsByImage({
|
|
477
|
-
image: base64Data,
|
|
478
|
-
limit: 20
|
|
479
|
-
}).unwrap(),
|
|
521
|
+
async () => getSimilarProductsByImage(requestData).unwrap(),
|
|
480
522
|
base64Image,
|
|
481
523
|
'Image search'
|
|
482
524
|
);
|
|
483
525
|
},
|
|
484
|
-
[
|
|
526
|
+
[
|
|
527
|
+
getSimilarProductsByImage,
|
|
528
|
+
searchText,
|
|
529
|
+
searchTextRef,
|
|
530
|
+
handleImageSearch,
|
|
531
|
+
hasUploadedImage,
|
|
532
|
+
product?.pk
|
|
533
|
+
]
|
|
485
534
|
);
|
|
486
535
|
|
|
487
536
|
const fetchSimilarProductsByImageCrop = useCallback(
|
|
488
|
-
async (dataString: string) => {
|
|
537
|
+
async (dataString: string, excludeCurrentProduct: boolean = true) => {
|
|
489
538
|
setFileError('');
|
|
539
|
+
|
|
540
|
+
setCurrentImageBase64(dataString);
|
|
541
|
+
setHasCroppedImage(true);
|
|
490
542
|
if (dataString.startsWith('data:application/x-cors-fallback;base64,')) {
|
|
491
543
|
try {
|
|
492
544
|
const fallbackDataEncoded = dataString.replace(
|
|
@@ -549,9 +601,12 @@ export function useSimilarProducts(product: Product) {
|
|
|
549
601
|
getSimilarProductsByImage({
|
|
550
602
|
image: base64Data,
|
|
551
603
|
limit: 20,
|
|
552
|
-
excluded_product_ids:
|
|
553
|
-
?
|
|
554
|
-
|
|
604
|
+
excluded_product_ids: excludeCurrentProduct
|
|
605
|
+
? product?.pk
|
|
606
|
+
? [product.pk]
|
|
607
|
+
: undefined
|
|
608
|
+
: undefined,
|
|
609
|
+
text: searchTextRef.current || undefined
|
|
555
610
|
}).unwrap(),
|
|
556
611
|
croppedBase64,
|
|
557
612
|
'Proxy crop search'
|
|
@@ -601,7 +656,12 @@ export function useSimilarProducts(product: Product) {
|
|
|
601
656
|
getSimilarProductsByImage({
|
|
602
657
|
image: base64Data,
|
|
603
658
|
limit: 20,
|
|
604
|
-
excluded_product_ids:
|
|
659
|
+
excluded_product_ids: excludeCurrentProduct
|
|
660
|
+
? product?.pk
|
|
661
|
+
? [product.pk]
|
|
662
|
+
: undefined
|
|
663
|
+
: undefined,
|
|
664
|
+
text: searchTextRef.current || undefined
|
|
605
665
|
}).unwrap(),
|
|
606
666
|
dataString,
|
|
607
667
|
'Image crop search'
|
|
@@ -609,11 +669,14 @@ export function useSimilarProducts(product: Product) {
|
|
|
609
669
|
},
|
|
610
670
|
[
|
|
611
671
|
getSimilarProductsByImage,
|
|
672
|
+
searchText,
|
|
673
|
+
searchTextRef,
|
|
612
674
|
handleImageSearch,
|
|
613
675
|
product,
|
|
614
676
|
fetchSimilarProductsByImageUrl,
|
|
615
677
|
setFileError,
|
|
616
|
-
t
|
|
678
|
+
t,
|
|
679
|
+
hasUploadedImage
|
|
617
680
|
]
|
|
618
681
|
);
|
|
619
682
|
|
|
@@ -624,6 +687,8 @@ export function useSimilarProducts(product: Product) {
|
|
|
624
687
|
const file = event.target.files?.[0];
|
|
625
688
|
if (!file) return;
|
|
626
689
|
|
|
690
|
+
setSearchTextWithRef('');
|
|
691
|
+
|
|
627
692
|
try {
|
|
628
693
|
let processedFile = file;
|
|
629
694
|
|
|
@@ -650,6 +715,9 @@ export function useSimilarProducts(product: Product) {
|
|
|
650
715
|
const dataUrl = e.target?.result as string;
|
|
651
716
|
setCurrentImageUrl(dataUrl);
|
|
652
717
|
setHasUploadedImage(true);
|
|
718
|
+
hasUploadedImageRef.current = true;
|
|
719
|
+
setHasCroppedImage(false);
|
|
720
|
+
setCurrentImageBase64('');
|
|
653
721
|
|
|
654
722
|
const metadataValidation = await validateImageFromDataUrl(dataUrl);
|
|
655
723
|
if (!metadataValidation.isValid) {
|
|
@@ -658,7 +726,7 @@ export function useSimilarProducts(product: Product) {
|
|
|
658
726
|
);
|
|
659
727
|
return;
|
|
660
728
|
}
|
|
661
|
-
fetchSimilarProductsByBase64(dataUrl);
|
|
729
|
+
fetchSimilarProductsByBase64(dataUrl, undefined, false);
|
|
662
730
|
};
|
|
663
731
|
|
|
664
732
|
reader.onerror = () =>
|
|
@@ -679,6 +747,8 @@ export function useSimilarProducts(product: Product) {
|
|
|
679
747
|
|
|
680
748
|
try {
|
|
681
749
|
setCurrentImageUrl(base64Image);
|
|
750
|
+
setCurrentImageBase64(base64Image);
|
|
751
|
+
setHasCroppedImage(true);
|
|
682
752
|
|
|
683
753
|
const metadataValidation = await validateImageFromDataUrl(base64Image);
|
|
684
754
|
if (!metadataValidation.isValid) {
|
|
@@ -688,10 +758,14 @@ export function useSimilarProducts(product: Product) {
|
|
|
688
758
|
return;
|
|
689
759
|
}
|
|
690
760
|
|
|
691
|
-
if (
|
|
692
|
-
await fetchSimilarProductsByBase64(
|
|
761
|
+
if (hasUploadedImageRef.current) {
|
|
762
|
+
await fetchSimilarProductsByBase64(
|
|
763
|
+
base64Image,
|
|
764
|
+
searchTextRef.current,
|
|
765
|
+
false
|
|
766
|
+
);
|
|
693
767
|
} else {
|
|
694
|
-
await fetchSimilarProductsByImageCrop(base64Image);
|
|
768
|
+
await fetchSimilarProductsByImageCrop(base64Image, true);
|
|
695
769
|
}
|
|
696
770
|
} catch (error) {
|
|
697
771
|
setFileError(t('common.similar_products.errors.crop_processing_error'));
|
|
@@ -882,6 +956,11 @@ export function useSimilarProducts(product: Product) {
|
|
|
882
956
|
setLoadedPages(new Set([1]));
|
|
883
957
|
}, []);
|
|
884
958
|
|
|
959
|
+
const resetCropState = useCallback(() => {
|
|
960
|
+
setHasCroppedImage(false);
|
|
961
|
+
setCurrentImageBase64('');
|
|
962
|
+
}, []);
|
|
963
|
+
|
|
885
964
|
const clearFileInput = useCallback(
|
|
886
965
|
(fileInputRef: React.RefObject<HTMLInputElement>) => {
|
|
887
966
|
if (fileInputRef.current) {
|
|
@@ -891,6 +970,47 @@ export function useSimilarProducts(product: Product) {
|
|
|
891
970
|
[]
|
|
892
971
|
);
|
|
893
972
|
|
|
973
|
+
const handleTextSearch = useCallback(async () => {
|
|
974
|
+
const textToUse = searchTextRef.current || searchText;
|
|
975
|
+
|
|
976
|
+
if (hasCroppedImage && currentImageBase64) {
|
|
977
|
+
await fetchSimilarProductsByBase64(currentImageBase64, textToUse, !hasUploadedImage);
|
|
978
|
+
} else if (hasUploadedImage && currentImageUrl) {
|
|
979
|
+
await fetchSimilarProductsByBase64(currentImageUrl, textToUse, false);
|
|
980
|
+
} else if (currentImageUrl) {
|
|
981
|
+
await fetchSimilarProductsByImageUrl(currentImageUrl, textToUse);
|
|
982
|
+
}
|
|
983
|
+
}, [
|
|
984
|
+
searchText,
|
|
985
|
+
searchTextRef,
|
|
986
|
+
currentImageUrl,
|
|
987
|
+
currentImageBase64,
|
|
988
|
+
hasCroppedImage,
|
|
989
|
+
hasUploadedImage,
|
|
990
|
+
fetchSimilarProductsByImageUrl,
|
|
991
|
+
fetchSimilarProductsByBase64
|
|
992
|
+
]);
|
|
993
|
+
|
|
994
|
+
const handleClearText = useCallback(async () => {
|
|
995
|
+
setSearchTextWithRef('');
|
|
996
|
+
|
|
997
|
+
if (hasCroppedImage && currentImageBase64) {
|
|
998
|
+
await fetchSimilarProductsByBase64(currentImageBase64, '', !hasUploadedImage);
|
|
999
|
+
} else if (hasUploadedImage && currentImageUrl) {
|
|
1000
|
+
await fetchSimilarProductsByBase64(currentImageUrl, '', false);
|
|
1001
|
+
} else if (currentImageUrl) {
|
|
1002
|
+
await fetchSimilarProductsByImageUrl(currentImageUrl, '');
|
|
1003
|
+
}
|
|
1004
|
+
}, [
|
|
1005
|
+
hasCroppedImage,
|
|
1006
|
+
currentImageBase64,
|
|
1007
|
+
currentImageUrl,
|
|
1008
|
+
hasUploadedImage,
|
|
1009
|
+
fetchSimilarProductsByBase64,
|
|
1010
|
+
fetchSimilarProductsByImageUrl,
|
|
1011
|
+
setSearchTextWithRef
|
|
1012
|
+
]);
|
|
1013
|
+
|
|
894
1014
|
const handleLoadMore = async () => {
|
|
895
1015
|
if (!searchResults?.pagination || isLoading) return;
|
|
896
1016
|
|
|
@@ -931,6 +1051,8 @@ export function useSimilarProducts(product: Product) {
|
|
|
931
1051
|
setCurrentImageUrl,
|
|
932
1052
|
isLoading,
|
|
933
1053
|
fileError,
|
|
1054
|
+
searchText,
|
|
1055
|
+
setSearchText: setSearchTextWithRef,
|
|
934
1056
|
searchResults,
|
|
935
1057
|
resultsKey,
|
|
936
1058
|
hasUploadedImage,
|
|
@@ -949,6 +1071,9 @@ export function useSimilarProducts(product: Product) {
|
|
|
949
1071
|
allLoadedProducts,
|
|
950
1072
|
clearError,
|
|
951
1073
|
clearResults,
|
|
952
|
-
clearFileInput
|
|
1074
|
+
clearFileInput,
|
|
1075
|
+
handleTextSearch,
|
|
1076
|
+
handleClearText,
|
|
1077
|
+
resetCropState
|
|
953
1078
|
};
|
|
954
1079
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -58,6 +58,10 @@ export interface SimilarProductsModalProps {
|
|
|
58
58
|
showResetButton?: boolean;
|
|
59
59
|
settings?: any;
|
|
60
60
|
className?: string;
|
|
61
|
+
searchText?: string;
|
|
62
|
+
setSearchText?: (text: string) => void;
|
|
63
|
+
handleTextSearch?: () => void;
|
|
64
|
+
handleClearText?: () => void;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
export interface FilterSidebarProps {
|
|
@@ -67,6 +71,8 @@ export interface FilterSidebarProps {
|
|
|
67
71
|
isLoading: boolean;
|
|
68
72
|
handleFacetChange: (facetKey: string, choiceValue: string | number) => void;
|
|
69
73
|
removeFacetFilter: (facetKey: string, choiceValue: string | number) => void;
|
|
74
|
+
searchText?: string;
|
|
75
|
+
setSearchText?: (text: string) => void;
|
|
70
76
|
currentImageUrl: string;
|
|
71
77
|
isCropping: boolean;
|
|
72
78
|
imageRef: React.RefObject<HTMLImageElement>;
|
|
@@ -126,6 +132,8 @@ export interface CustomRendererProps {
|
|
|
126
132
|
onSortChange: (value: string) => void;
|
|
127
133
|
onFilterMenuToggle: () => void;
|
|
128
134
|
isLoading: boolean;
|
|
135
|
+
searchText?: string;
|
|
136
|
+
setSearchText?: (text: string) => void;
|
|
129
137
|
}) => React.ReactNode;
|
|
130
138
|
renderSortDropdown?: (props: {
|
|
131
139
|
sorters: SortOption[];
|
|
@@ -141,6 +149,17 @@ export interface CustomRendererProps {
|
|
|
141
149
|
onClick: () => void;
|
|
142
150
|
isLoading: boolean;
|
|
143
151
|
}) => React.ReactNode;
|
|
152
|
+
renderModalSearchInput?: (props: {
|
|
153
|
+
searchText: string;
|
|
154
|
+
setSearchText: (text: string) => void;
|
|
155
|
+
isLoading: boolean;
|
|
156
|
+
placeholder: string;
|
|
157
|
+
onSearch?: () => void;
|
|
158
|
+
}) => React.ReactNode;
|
|
159
|
+
renderSearchIcon?: (props: {
|
|
160
|
+
disabled: boolean;
|
|
161
|
+
onClick?: () => void;
|
|
162
|
+
}) => React.ReactNode;
|
|
144
163
|
renderEmptyState?: () => React.ReactNode;
|
|
145
164
|
};
|
|
146
165
|
filterSidebar?: {
|
|
@@ -219,6 +238,12 @@ export interface CustomRendererProps {
|
|
|
219
238
|
onClearAll: () => void;
|
|
220
239
|
isLoading: boolean;
|
|
221
240
|
}) => React.ReactNode;
|
|
241
|
+
renderTextSearch?: (props: {
|
|
242
|
+
searchText: string;
|
|
243
|
+
setSearchText: (text: string) => void;
|
|
244
|
+
isLoading: boolean;
|
|
245
|
+
placeholder: string;
|
|
246
|
+
}) => React.ReactNode;
|
|
222
247
|
};
|
|
223
248
|
resultsGrid?: {
|
|
224
249
|
renderGrid?: (props: ResultsGridProps) => React.ReactNode;
|
|
@@ -309,6 +334,7 @@ export interface SimilarProductsSettings {
|
|
|
309
334
|
resultsPerPage?: number;
|
|
310
335
|
enableCropping?: boolean;
|
|
311
336
|
enableFileUpload?: boolean;
|
|
337
|
+
enableTextSearch?: boolean;
|
|
312
338
|
paginationType?: 'pagination' | 'load-more' | 'infinite-scroll';
|
|
313
339
|
loadMoreText?: string;
|
|
314
340
|
loadMoreStyle?: 'button' | 'auto';
|
|
@@ -329,10 +355,22 @@ export interface SimilarProductsSettings {
|
|
|
329
355
|
controlsContainer?: string;
|
|
330
356
|
controlsInner?: string;
|
|
331
357
|
controlsLeft?: string;
|
|
358
|
+
controlsCenter?: string;
|
|
332
359
|
controlsRight?: string;
|
|
333
360
|
itemCount?: string;
|
|
334
361
|
sortDropdown?: string;
|
|
335
362
|
filterToggleButton?: string;
|
|
363
|
+
modalSearchInput?: string;
|
|
364
|
+
modalSearchContainer?: string;
|
|
365
|
+
modalSearchButton?: string;
|
|
366
|
+
modalSearchIcon?: string;
|
|
367
|
+
modalSearchClearButton?: string;
|
|
368
|
+
modalSearchClearIcon?: string;
|
|
369
|
+
searchButtonIcon?: string;
|
|
370
|
+
modalCloseIcon?: string;
|
|
371
|
+
filterIcon?: string;
|
|
372
|
+
filterRemoveIcon?: string;
|
|
373
|
+
imageSearchButtonIcon?: string;
|
|
336
374
|
|
|
337
375
|
filterSidebarMobileHeader?: string;
|
|
338
376
|
filterSidebarMobileTitle?: string;
|
|
@@ -414,6 +452,19 @@ export interface SimilarProductsSettings {
|
|
|
414
452
|
mobileActiveFilterTag?: string;
|
|
415
453
|
mobileClearAllButton?: string;
|
|
416
454
|
filterSidebarMobileOverlay?: string;
|
|
455
|
+
|
|
456
|
+
textSearchContainer?: string;
|
|
457
|
+
textSearchLabel?: string;
|
|
458
|
+
textSearchInput?: string;
|
|
459
|
+
};
|
|
460
|
+
iconNames?: {
|
|
461
|
+
searchButton?: string;
|
|
462
|
+
modalClose?: string;
|
|
463
|
+
filter?: string;
|
|
464
|
+
filterRemove?: string;
|
|
465
|
+
modalSearch?: string;
|
|
466
|
+
modalSearchClear?: string;
|
|
467
|
+
imageSearchButton?: string;
|
|
417
468
|
};
|
|
418
469
|
customRenderers?: {
|
|
419
470
|
Modal?: React.ComponentType<SimilarProductsModalProps>;
|
package/src/utils/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export const defaultSettings: Required<SimilarProductsSettings> = {
|
|
|
7
7
|
resultsPerPage: 20,
|
|
8
8
|
enableCropping: true,
|
|
9
9
|
enableFileUpload: true,
|
|
10
|
+
enableTextSearch: false,
|
|
10
11
|
customStyles: {
|
|
11
12
|
modal: '',
|
|
12
13
|
filterSidebar: '',
|
|
@@ -15,7 +16,31 @@ export const defaultSettings: Required<SimilarProductsSettings> = {
|
|
|
15
16
|
productItem: '',
|
|
16
17
|
pagination: '',
|
|
17
18
|
filterGroup: '',
|
|
18
|
-
imageSection: ''
|
|
19
|
+
imageSection: '',
|
|
20
|
+
textSearchContainer: '',
|
|
21
|
+
textSearchLabel: '',
|
|
22
|
+
textSearchInput: '',
|
|
23
|
+
controlsCenter: '',
|
|
24
|
+
modalSearchInput: '',
|
|
25
|
+
modalSearchContainer: '',
|
|
26
|
+
modalSearchButton: '',
|
|
27
|
+
modalSearchIcon: '',
|
|
28
|
+
modalSearchClearButton: '',
|
|
29
|
+
modalSearchClearIcon: '',
|
|
30
|
+
searchButtonIcon: '',
|
|
31
|
+
modalCloseIcon: '',
|
|
32
|
+
filterIcon: '',
|
|
33
|
+
filterRemoveIcon: '',
|
|
34
|
+
imageSearchButtonIcon: ''
|
|
35
|
+
},
|
|
36
|
+
iconNames: {
|
|
37
|
+
searchButton: 'search',
|
|
38
|
+
modalClose: 'close',
|
|
39
|
+
filter: 'filter',
|
|
40
|
+
filterRemove: 'close',
|
|
41
|
+
modalSearch: 'search',
|
|
42
|
+
modalSearchClear: 'close',
|
|
43
|
+
imageSearchButton: 'search'
|
|
19
44
|
},
|
|
20
45
|
customRenderers: {
|
|
21
46
|
render: {}
|
|
@@ -43,6 +68,10 @@ export function mergeSettings(
|
|
|
43
68
|
...defaultSettings.customStyles,
|
|
44
69
|
...userSettings.customStyles
|
|
45
70
|
},
|
|
71
|
+
iconNames: {
|
|
72
|
+
...defaultSettings.iconNames,
|
|
73
|
+
...userSettings.iconNames
|
|
74
|
+
},
|
|
46
75
|
customRenderers: {
|
|
47
76
|
...defaultSettings.customRenderers,
|
|
48
77
|
...userSettings.customRenderers,
|
|
@@ -100,13 +129,19 @@ export function validateImageFile(
|
|
|
100
129
|
export function debounce<T extends (...args: any[]) => any>(
|
|
101
130
|
func: T,
|
|
102
131
|
wait: number
|
|
103
|
-
): (...args: Parameters<T>) => void {
|
|
132
|
+
): ((...args: Parameters<T>) => void) & { cancel: () => void } {
|
|
104
133
|
let timeout: NodeJS.Timeout;
|
|
105
134
|
|
|
106
|
-
|
|
135
|
+
const debounced = (...args: Parameters<T>) => {
|
|
107
136
|
clearTimeout(timeout);
|
|
108
137
|
timeout = setTimeout(() => func(...args), wait);
|
|
109
138
|
};
|
|
139
|
+
|
|
140
|
+
debounced.cancel = () => {
|
|
141
|
+
clearTimeout(timeout);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return debounced as ((...args: Parameters<T>) => void) & { cancel: () => void };
|
|
110
145
|
}
|
|
111
146
|
|
|
112
147
|
export function dataURLToBlob(dataURL: string): Blob {
|
package/src/views/filters.tsx
CHANGED
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
Button,
|
|
6
6
|
Icon,
|
|
7
7
|
Accordion,
|
|
8
|
-
LoaderSpinner
|
|
8
|
+
LoaderSpinner,
|
|
9
|
+
Input
|
|
9
10
|
} from '@akinon/next/components';
|
|
10
11
|
import { useLocalization } from '@akinon/next/hooks';
|
|
11
12
|
import { FilterSidebarProps } from '../types';
|
|
@@ -55,6 +56,8 @@ export function SimilarProductsFilterSidebar({
|
|
|
55
56
|
isLoading,
|
|
56
57
|
handleFacetChange,
|
|
57
58
|
removeFacetFilter,
|
|
59
|
+
searchText,
|
|
60
|
+
setSearchText,
|
|
58
61
|
currentImageUrl,
|
|
59
62
|
isCropping,
|
|
60
63
|
imageRef,
|
|
@@ -166,6 +169,19 @@ export function SimilarProductsFilterSidebar({
|
|
|
166
169
|
className
|
|
167
170
|
);
|
|
168
171
|
|
|
172
|
+
const modalCloseIconClassName = twMerge(
|
|
173
|
+
'',
|
|
174
|
+
settings?.customStyles?.modalCloseIcon
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const filterRemoveIconClassName = twMerge(
|
|
178
|
+
'',
|
|
179
|
+
settings?.customStyles?.filterRemoveIcon
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const modalCloseIconName = settings?.iconNames?.modalClose || 'close';
|
|
183
|
+
const filterRemoveIconName = settings?.iconNames?.filterRemove || 'close';
|
|
184
|
+
|
|
169
185
|
return (
|
|
170
186
|
<>
|
|
171
187
|
{isFilterMenuOpen && (
|
|
@@ -222,7 +238,11 @@ export function SimilarProductsFilterSidebar({
|
|
|
222
238
|
settings?.customStyles?.filterSidebarMobileCloseButton
|
|
223
239
|
)}
|
|
224
240
|
>
|
|
225
|
-
<Icon
|
|
241
|
+
<Icon
|
|
242
|
+
name={modalCloseIconName}
|
|
243
|
+
size={16}
|
|
244
|
+
className={modalCloseIconClassName}
|
|
245
|
+
/>
|
|
226
246
|
</Button>
|
|
227
247
|
</div>
|
|
228
248
|
<div
|
|
@@ -928,7 +948,11 @@ export function SimilarProductsFilterSidebar({
|
|
|
928
948
|
disabled={isLoading}
|
|
929
949
|
className="hover:bg-gray-200 rounded-full p-0 w-5 h-5 disabled:opacity-50 disabled:cursor-not-allowed ml-1"
|
|
930
950
|
>
|
|
931
|
-
<Icon
|
|
951
|
+
<Icon
|
|
952
|
+
name={filterRemoveIconName}
|
|
953
|
+
size={10}
|
|
954
|
+
className={filterRemoveIconClassName}
|
|
955
|
+
/>
|
|
932
956
|
</Button>
|
|
933
957
|
</div>
|
|
934
958
|
))}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
4
|
import { Product } from '@akinon/next/types';
|
|
5
|
-
import { useImageSearchFeature } from '../hooks/use-image-search-feature';
|
|
6
5
|
import { ImageSearchButton } from './image-search-button';
|
|
7
6
|
import { SimilarProductsPlugin } from './main';
|
|
8
7
|
|
|
@@ -10,27 +9,25 @@ interface HeaderImageSearchFeatureProps {
|
|
|
10
9
|
className?: string;
|
|
11
10
|
isEnabled?: boolean;
|
|
12
11
|
settings?: any;
|
|
12
|
+
enableTextSearch?: boolean;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function HeaderImageSearchFeature({
|
|
16
16
|
className,
|
|
17
|
-
isEnabled
|
|
18
|
-
settings
|
|
17
|
+
isEnabled = false,
|
|
18
|
+
settings: userSettings,
|
|
19
|
+
enableTextSearch = false
|
|
19
20
|
}: HeaderImageSearchFeatureProps) {
|
|
20
|
-
const { isEnabled: hookIsEnabled, isLoading } = useImageSearchFeature();
|
|
21
|
-
|
|
22
|
-
const envEnabled = process.env.NEXT_PUBLIC_ENABLE_IMAGE_SEARCH === 'true';
|
|
23
|
-
const finalIsEnabled = envEnabled
|
|
24
|
-
? true
|
|
25
|
-
: isEnabledProp !== undefined
|
|
26
|
-
? isEnabledProp
|
|
27
|
-
: hookIsEnabled;
|
|
28
|
-
|
|
29
21
|
const [isImageSearchModalOpen, setIsImageSearchModalOpen] = useState(false);
|
|
30
22
|
const [isResultsModalOpen, setIsResultsModalOpen] = useState(false);
|
|
31
23
|
const [uploadedImageFile, setUploadedImageFile] = useState<File | null>(null);
|
|
32
24
|
|
|
33
|
-
|
|
25
|
+
const settings = {
|
|
26
|
+
...userSettings,
|
|
27
|
+
enableTextSearch
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (!isEnabled) {
|
|
34
31
|
return null;
|
|
35
32
|
}
|
|
36
33
|
|
|
@@ -55,26 +52,23 @@ export function HeaderImageSearchFeature({
|
|
|
55
52
|
|
|
56
53
|
return (
|
|
57
54
|
<>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
/>
|
|
76
|
-
</>
|
|
77
|
-
)}
|
|
55
|
+
<ImageSearchButton
|
|
56
|
+
onClick={handleOpenImageSearch}
|
|
57
|
+
className={className}
|
|
58
|
+
settings={settings}
|
|
59
|
+
/>
|
|
60
|
+
<SimilarProductsPlugin
|
|
61
|
+
product={{} as Product}
|
|
62
|
+
isOpen={isResultsModalOpen}
|
|
63
|
+
onClose={handleResultsModalClose}
|
|
64
|
+
activeIndex={0}
|
|
65
|
+
showImageSearchModal={isImageSearchModalOpen}
|
|
66
|
+
onImageSearchModalClose={handleImageSearchModalClose}
|
|
67
|
+
uploadedImageFile={uploadedImageFile}
|
|
68
|
+
onImageUpload={handleImageUpload}
|
|
69
|
+
showResetButton={false}
|
|
70
|
+
settings={settings}
|
|
71
|
+
/>
|
|
78
72
|
</>
|
|
79
73
|
);
|
|
80
74
|
}
|