@akinon/pz-similar-products 1.92.0-snapshot-ZERO-3457-20250627121541 → 1.93.0-rc.46
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 +112 -2
- package/README.md +243 -1
- package/package.json +1 -1
- package/src/data/endpoints.ts +37 -9
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-similar-products.ts +175 -27
- package/src/hooks/use-text-search-feature.ts +41 -0
- package/src/types/index.ts +64 -0
- package/src/utils/index.ts +38 -3
- package/src/views/filters.tsx +59 -9
- package/src/views/header-image-search-feature.tsx +13 -1
- package/src/views/image-search-button.tsx +20 -18
- package/src/views/main.tsx +92 -7
- package/src/views/product-image-search-feature.tsx +17 -3
- package/src/views/results.tsx +7 -5
- package/src/views/search-button.tsx +12 -2
- package/src/views/search-modal.tsx +173 -10
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
|
|
@@ -445,7 +465,10 @@ export function SimilarProductsFilterSidebar({
|
|
|
445
465
|
onDragEnd={() => {}}
|
|
446
466
|
ruleOfThirds={false}
|
|
447
467
|
aspect={settings?.cropAspectRatio}
|
|
448
|
-
className=
|
|
468
|
+
className={twMerge(
|
|
469
|
+
'slider-crop',
|
|
470
|
+
settings?.customStyles?.cropComponent
|
|
471
|
+
)}
|
|
449
472
|
disabled={isLoading}
|
|
450
473
|
keepSelection={true}
|
|
451
474
|
>
|
|
@@ -453,22 +476,45 @@ export function SimilarProductsFilterSidebar({
|
|
|
453
476
|
ref={imageRef}
|
|
454
477
|
src={currentImageUrl || ''}
|
|
455
478
|
alt={product?.name || 'Product image'}
|
|
456
|
-
className=
|
|
479
|
+
className={twMerge(
|
|
480
|
+
'max-w-full max-h-[200px] md:max-h-[280px]',
|
|
481
|
+
settings?.customStyles?.cropImage,
|
|
482
|
+
settings?.customStyles?.cropImageActive
|
|
483
|
+
)}
|
|
457
484
|
style={{ transform: `scale(1) rotate(0deg)` }}
|
|
458
485
|
/>
|
|
459
486
|
</ReactCrop>
|
|
460
487
|
) : (
|
|
461
|
-
<div
|
|
488
|
+
<div
|
|
489
|
+
className={twMerge(
|
|
490
|
+
'relative w-full h-full flex items-center justify-center',
|
|
491
|
+
settings?.customStyles?.cropImageContainer
|
|
492
|
+
)}
|
|
493
|
+
>
|
|
462
494
|
<img
|
|
463
495
|
ref={imageRef}
|
|
464
496
|
src={currentImageUrl || ''}
|
|
465
497
|
alt={product?.name || 'Product image'}
|
|
466
|
-
className=
|
|
498
|
+
className={twMerge(
|
|
499
|
+
'max-w-full max-h-[200px] md:max-h-[280px] object-contain',
|
|
500
|
+
settings?.customStyles?.cropImage,
|
|
501
|
+
settings?.customStyles?.cropImageNonCropping
|
|
502
|
+
)}
|
|
467
503
|
/>
|
|
468
504
|
{!isCropping && completedCrop && (
|
|
469
|
-
<div
|
|
505
|
+
<div
|
|
506
|
+
className={twMerge(
|
|
507
|
+
'hidden md:block absolute inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-in-out',
|
|
508
|
+
settings?.customStyles?.cropOverlay,
|
|
509
|
+
settings?.customStyles?.cropOverlayBackground
|
|
510
|
+
)}
|
|
511
|
+
>
|
|
470
512
|
<div
|
|
471
|
-
className=
|
|
513
|
+
className={twMerge(
|
|
514
|
+
'absolute transition-all duration-300 ease-in-out',
|
|
515
|
+
settings?.customStyles?.cropSelection,
|
|
516
|
+
settings?.customStyles?.cropSelectionHighlight
|
|
517
|
+
)}
|
|
472
518
|
style={{
|
|
473
519
|
width: `${completedCrop.width}px`,
|
|
474
520
|
height: `${completedCrop.height}px`,
|
|
@@ -902,7 +948,11 @@ export function SimilarProductsFilterSidebar({
|
|
|
902
948
|
disabled={isLoading}
|
|
903
949
|
className="hover:bg-gray-200 rounded-full p-0 w-5 h-5 disabled:opacity-50 disabled:cursor-not-allowed ml-1"
|
|
904
950
|
>
|
|
905
|
-
<Icon
|
|
951
|
+
<Icon
|
|
952
|
+
name={filterRemoveIconName}
|
|
953
|
+
size={10}
|
|
954
|
+
className={filterRemoveIconClassName}
|
|
955
|
+
/>
|
|
906
956
|
</Button>
|
|
907
957
|
</div>
|
|
908
958
|
))}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React, { useState, useRef } from 'react';
|
|
4
4
|
import { Product } from '@akinon/next/types';
|
|
5
5
|
import { useImageSearchFeature } from '../hooks/use-image-search-feature';
|
|
6
|
+
import { useTextSearchFeature } from '../hooks/use-text-search-feature';
|
|
6
7
|
import { ImageSearchButton } from './image-search-button';
|
|
7
8
|
import { SimilarProductsPlugin } from './main';
|
|
8
9
|
|
|
@@ -10,14 +11,19 @@ interface HeaderImageSearchFeatureProps {
|
|
|
10
11
|
className?: string;
|
|
11
12
|
isEnabled?: boolean;
|
|
12
13
|
settings?: any;
|
|
14
|
+
enableTextSearch?: boolean;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export function HeaderImageSearchFeature({
|
|
16
18
|
className,
|
|
17
19
|
isEnabled: isEnabledProp,
|
|
18
|
-
settings
|
|
20
|
+
settings: userSettings,
|
|
21
|
+
enableTextSearch = false
|
|
19
22
|
}: HeaderImageSearchFeatureProps) {
|
|
20
23
|
const { isEnabled: hookIsEnabled, isLoading } = useImageSearchFeature();
|
|
24
|
+
const { isEnabled: textSearchEnabled } = useTextSearchFeature({
|
|
25
|
+
enableTextSearch
|
|
26
|
+
});
|
|
21
27
|
|
|
22
28
|
const envEnabled = process.env.NEXT_PUBLIC_ENABLE_IMAGE_SEARCH === 'true';
|
|
23
29
|
const finalIsEnabled = envEnabled
|
|
@@ -30,6 +36,11 @@ export function HeaderImageSearchFeature({
|
|
|
30
36
|
const [isResultsModalOpen, setIsResultsModalOpen] = useState(false);
|
|
31
37
|
const [uploadedImageFile, setUploadedImageFile] = useState<File | null>(null);
|
|
32
38
|
|
|
39
|
+
const settings = {
|
|
40
|
+
...userSettings,
|
|
41
|
+
enableTextSearch: textSearchEnabled
|
|
42
|
+
};
|
|
43
|
+
|
|
33
44
|
if (isLoading || !finalIsEnabled) {
|
|
34
45
|
return null;
|
|
35
46
|
}
|
|
@@ -60,6 +71,7 @@ export function HeaderImageSearchFeature({
|
|
|
60
71
|
<ImageSearchButton
|
|
61
72
|
onClick={handleOpenImageSearch}
|
|
62
73
|
className={className}
|
|
74
|
+
settings={settings}
|
|
63
75
|
/>
|
|
64
76
|
<SimilarProductsPlugin
|
|
65
77
|
product={{} as Product}
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import { Button } from '@akinon/next/components';
|
|
4
|
+
import { Button, Icon } from '@akinon/next/components';
|
|
5
5
|
import { useLocalization } from '@akinon/next/hooks';
|
|
6
6
|
import { useImageSearchFeature } from '../hooks/use-image-search-feature';
|
|
7
|
+
import { twMerge } from 'tailwind-merge';
|
|
7
8
|
|
|
8
9
|
interface ImageSearchButtonProps {
|
|
9
10
|
onClick: () => void;
|
|
10
11
|
className?: string;
|
|
12
|
+
settings?: any;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export function ImageSearchButton({
|
|
14
16
|
onClick,
|
|
15
|
-
className
|
|
17
|
+
className,
|
|
18
|
+
settings
|
|
16
19
|
}: ImageSearchButtonProps) {
|
|
17
20
|
const { t } = useLocalization();
|
|
18
21
|
const { isEnabled, isLoading } = useImageSearchFeature();
|
|
19
22
|
|
|
23
|
+
const imageSearchButtonIconName =
|
|
24
|
+
settings?.iconNames?.imageSearchButton || 'search';
|
|
25
|
+
const imageSearchButtonIconClassName = twMerge(
|
|
26
|
+
'text-black',
|
|
27
|
+
settings?.customStyles?.imageSearchButtonIcon
|
|
28
|
+
);
|
|
29
|
+
|
|
20
30
|
if (isLoading || !isEnabled) {
|
|
21
31
|
return null;
|
|
22
32
|
}
|
|
@@ -24,24 +34,16 @@ export function ImageSearchButton({
|
|
|
24
34
|
return (
|
|
25
35
|
<Button
|
|
26
36
|
onClick={onClick}
|
|
27
|
-
className={`flex items-center justify-center mr-2 text-gray-500 focus:outline-none border-none bg-transparent ${
|
|
37
|
+
className={`flex items-center justify-center mr-2 text-gray-500 focus:outline-none border-none bg-transparent ${
|
|
38
|
+
className || ''
|
|
39
|
+
}`}
|
|
28
40
|
title={t('common.search.image_search.title')}
|
|
29
41
|
>
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
fill="none"
|
|
36
|
-
stroke="currentColor"
|
|
37
|
-
strokeWidth="2"
|
|
38
|
-
strokeLinecap="round"
|
|
39
|
-
strokeLinejoin="round"
|
|
40
|
-
className="text-gray-500"
|
|
41
|
-
>
|
|
42
|
-
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
|
|
43
|
-
<circle cx="12" cy="13" r="4"></circle>
|
|
44
|
-
</svg>
|
|
42
|
+
<Icon
|
|
43
|
+
name={imageSearchButtonIconName}
|
|
44
|
+
size={20}
|
|
45
|
+
className={imageSearchButtonIconClassName}
|
|
46
|
+
/>
|
|
45
47
|
</Button>
|
|
46
48
|
);
|
|
47
49
|
}
|
package/src/views/main.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState, useRef, useEffect } from 'react';
|
|
3
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
4
4
|
import { Product } from '@akinon/next/types';
|
|
5
5
|
import { useImageCropper } from '../hooks/use-image-cropper';
|
|
6
6
|
import { SimilarProductsModal } from './search-modal';
|
|
@@ -37,12 +37,39 @@ export function SimilarProductsPlugin({
|
|
|
37
37
|
const [isFilterMenuOpen, setIsFilterMenuOpen] = useState(false);
|
|
38
38
|
|
|
39
39
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
40
|
+
const hasUploadedImageRef = useRef<boolean>(false);
|
|
41
|
+
const [isImageUploadedViaFileInput, setIsImageUploadedViaFileInput] = useState(false);
|
|
42
|
+
|
|
43
|
+
const UPLOAD_FLAG_KEY = `similar-products-upload-${product.pk}`;
|
|
44
|
+
|
|
45
|
+
const setUploadFlag = (value: boolean) => {
|
|
46
|
+
try {
|
|
47
|
+
if (value) {
|
|
48
|
+
sessionStorage.setItem(UPLOAD_FLAG_KEY, 'true');
|
|
49
|
+
} else {
|
|
50
|
+
sessionStorage.removeItem(UPLOAD_FLAG_KEY);
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Session storage error:', error);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const getUploadFlag = () => {
|
|
58
|
+
try {
|
|
59
|
+
return sessionStorage.getItem(UPLOAD_FLAG_KEY) === 'true';
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('Session storage error:', error);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
40
65
|
|
|
41
66
|
const {
|
|
42
67
|
currentImageUrl,
|
|
43
68
|
setCurrentImageUrl,
|
|
44
69
|
isLoading,
|
|
45
70
|
fileError,
|
|
71
|
+
searchText,
|
|
72
|
+
setSearchText,
|
|
46
73
|
searchResults,
|
|
47
74
|
resultsKey,
|
|
48
75
|
hasUploadedImage,
|
|
@@ -56,9 +83,48 @@ export function SimilarProductsPlugin({
|
|
|
56
83
|
loadedPages,
|
|
57
84
|
fetchSimilarProductsByImageUrl,
|
|
58
85
|
fetchSimilarProductsByImageCrop,
|
|
59
|
-
clearError
|
|
86
|
+
clearError,
|
|
87
|
+
clearResults,
|
|
88
|
+
handleTextSearch,
|
|
89
|
+
handleClearText,
|
|
90
|
+
resetCropState
|
|
60
91
|
} = useSimilarProducts(product);
|
|
61
92
|
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (hasUploadedImage) {
|
|
95
|
+
setIsImageUploadedViaFileInput(true);
|
|
96
|
+
setUploadFlag(true);
|
|
97
|
+
} else {
|
|
98
|
+
setIsImageUploadedViaFileInput(false);
|
|
99
|
+
setUploadFlag(false);
|
|
100
|
+
}
|
|
101
|
+
}, [hasUploadedImage]);
|
|
102
|
+
|
|
103
|
+
const cropProcessImageFunction = useCallback(
|
|
104
|
+
(base64Image: string) => {
|
|
105
|
+
let shouldExclude = true;
|
|
106
|
+
|
|
107
|
+
if (base64Image.startsWith('data:application/x-cors-fallback;base64,')) {
|
|
108
|
+
try {
|
|
109
|
+
const fallbackDataEncoded = base64Image.replace('data:application/x-cors-fallback;base64,', '');
|
|
110
|
+
const fallbackData = JSON.parse(atob(fallbackDataEncoded));
|
|
111
|
+
|
|
112
|
+
if (fallbackData.originalUrl && fallbackData.originalUrl.startsWith('data:')) {
|
|
113
|
+
shouldExclude = false;
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('CORS fallback parse error:', error);
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
const isUploadedFromStorage = getUploadFlag();
|
|
120
|
+
shouldExclude = !isUploadedFromStorage;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fetchSimilarProductsByImageCrop(base64Image, shouldExclude);
|
|
124
|
+
},
|
|
125
|
+
[fetchSimilarProductsByImageCrop, hasUploadedImage, currentImageUrl, uploadedImageFile, isImageUploadedViaFileInput]
|
|
126
|
+
);
|
|
127
|
+
|
|
62
128
|
const {
|
|
63
129
|
isCropping,
|
|
64
130
|
crop,
|
|
@@ -73,7 +139,7 @@ export function SimilarProductsPlugin({
|
|
|
73
139
|
resetCrop
|
|
74
140
|
} = useImageCropper(
|
|
75
141
|
(loading) => {},
|
|
76
|
-
|
|
142
|
+
cropProcessImageFunction,
|
|
77
143
|
clearError
|
|
78
144
|
);
|
|
79
145
|
|
|
@@ -84,8 +150,12 @@ export function SimilarProductsPlugin({
|
|
|
84
150
|
product.productimage_set[0].image;
|
|
85
151
|
setCurrentImageUrl(originalImageUrl);
|
|
86
152
|
setHasUploadedImage(false);
|
|
153
|
+
hasUploadedImageRef.current = false;
|
|
154
|
+
setUploadFlag(false);
|
|
155
|
+
setSearchText('');
|
|
87
156
|
resetCrop();
|
|
88
|
-
|
|
157
|
+
resetCropState();
|
|
158
|
+
fetchSimilarProductsByImageUrl(originalImageUrl, '');
|
|
89
159
|
}
|
|
90
160
|
};
|
|
91
161
|
|
|
@@ -107,7 +177,10 @@ export function SimilarProductsPlugin({
|
|
|
107
177
|
|
|
108
178
|
if (!isOpen) {
|
|
109
179
|
setHasInitialSearchDone(false);
|
|
180
|
+
hasUploadedImageRef.current = false;
|
|
181
|
+
setUploadFlag(false);
|
|
110
182
|
clearError();
|
|
183
|
+
clearResults();
|
|
111
184
|
}
|
|
112
185
|
}, [
|
|
113
186
|
isOpen,
|
|
@@ -115,7 +188,8 @@ export function SimilarProductsPlugin({
|
|
|
115
188
|
activeIndex,
|
|
116
189
|
hasUploadedImage,
|
|
117
190
|
hasInitialSearchDone,
|
|
118
|
-
clearError
|
|
191
|
+
clearError,
|
|
192
|
+
clearResults
|
|
119
193
|
]);
|
|
120
194
|
|
|
121
195
|
useEffect(() => {
|
|
@@ -126,7 +200,9 @@ export function SimilarProductsPlugin({
|
|
|
126
200
|
if (result) {
|
|
127
201
|
setCurrentImageUrl(result);
|
|
128
202
|
setHasUploadedImage(true);
|
|
129
|
-
|
|
203
|
+
hasUploadedImageRef.current = true;
|
|
204
|
+
setUploadFlag(true);
|
|
205
|
+
fetchSimilarProductsByImageCrop(result, false);
|
|
130
206
|
}
|
|
131
207
|
};
|
|
132
208
|
reader.readAsDataURL(uploadedImageFile);
|
|
@@ -142,6 +218,11 @@ export function SimilarProductsPlugin({
|
|
|
142
218
|
}
|
|
143
219
|
};
|
|
144
220
|
|
|
221
|
+
const handleModalClose = () => {
|
|
222
|
+
resetCrop();
|
|
223
|
+
onClose();
|
|
224
|
+
};
|
|
225
|
+
|
|
145
226
|
const ModalComponent =
|
|
146
227
|
settings.customRenderers?.Modal || SimilarProductsModal;
|
|
147
228
|
const ImageSearchModalComponent =
|
|
@@ -151,7 +232,7 @@ export function SimilarProductsPlugin({
|
|
|
151
232
|
<>
|
|
152
233
|
<ModalComponent
|
|
153
234
|
isOpen={isOpen}
|
|
154
|
-
onClose={
|
|
235
|
+
onClose={handleModalClose}
|
|
155
236
|
searchResults={searchResults}
|
|
156
237
|
resultsKey={resultsKey}
|
|
157
238
|
isLoading={isLoading}
|
|
@@ -185,6 +266,10 @@ export function SimilarProductsPlugin({
|
|
|
185
266
|
settings={settings}
|
|
186
267
|
className={settings.customStyles?.modal}
|
|
187
268
|
showResetButton={showResetButton}
|
|
269
|
+
searchText={searchText}
|
|
270
|
+
setSearchText={setSearchText}
|
|
271
|
+
handleTextSearch={handleTextSearch}
|
|
272
|
+
handleClearText={handleClearText}
|
|
188
273
|
/>
|
|
189
274
|
|
|
190
275
|
<ImageSearchModalComponent
|
|
@@ -6,6 +6,7 @@ import { SimilarProductsButton } from './search-button';
|
|
|
6
6
|
import { SimilarProductsPlugin } from './main';
|
|
7
7
|
import { mergeSettings } from '../utils';
|
|
8
8
|
import { useImageSearchFeature } from '../hooks/use-image-search-feature';
|
|
9
|
+
import { useTextSearchFeature } from '../hooks/use-text-search-feature';
|
|
9
10
|
|
|
10
11
|
interface ProductImageSearchFeatureProps {
|
|
11
12
|
product: Product;
|
|
@@ -13,6 +14,7 @@ interface ProductImageSearchFeatureProps {
|
|
|
13
14
|
settings?: any;
|
|
14
15
|
className?: string;
|
|
15
16
|
isEnabled?: boolean;
|
|
17
|
+
enableTextSearch?: boolean;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export function ProductImageSearchFeature({
|
|
@@ -20,11 +22,19 @@ export function ProductImageSearchFeature({
|
|
|
20
22
|
activeIndex = 0,
|
|
21
23
|
settings: userSettings,
|
|
22
24
|
className = 'absolute top-6 left-6 z-[20]',
|
|
23
|
-
isEnabled: isEnabledProp
|
|
25
|
+
isEnabled: isEnabledProp,
|
|
26
|
+
enableTextSearch = false
|
|
24
27
|
}: ProductImageSearchFeatureProps) {
|
|
25
|
-
const settings = mergeSettings(userSettings);
|
|
26
28
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
27
29
|
const { isEnabled: hookIsEnabled } = useImageSearchFeature();
|
|
30
|
+
const { isEnabled: textSearchEnabled } = useTextSearchFeature({
|
|
31
|
+
enableTextSearch
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const settings = mergeSettings({
|
|
35
|
+
...userSettings,
|
|
36
|
+
enableTextSearch: textSearchEnabled
|
|
37
|
+
});
|
|
28
38
|
|
|
29
39
|
const envEnabled = process.env.NEXT_PUBLIC_ENABLE_IMAGE_SEARCH === 'true';
|
|
30
40
|
const finalIsEnabled = envEnabled
|
|
@@ -41,7 +51,11 @@ export function ProductImageSearchFeature({
|
|
|
41
51
|
<>
|
|
42
52
|
{finalIsEnabled && (
|
|
43
53
|
<>
|
|
44
|
-
<SimilarProductsButton
|
|
54
|
+
<SimilarProductsButton
|
|
55
|
+
onClick={handleClick}
|
|
56
|
+
className={className}
|
|
57
|
+
settings={settings}
|
|
58
|
+
/>
|
|
45
59
|
|
|
46
60
|
<SimilarProductsPlugin
|
|
47
61
|
product={product}
|
package/src/views/results.tsx
CHANGED
|
@@ -526,10 +526,14 @@ export function SimilarProductsResultsGrid({
|
|
|
526
526
|
);
|
|
527
527
|
};
|
|
528
528
|
|
|
529
|
-
if (isLoading && (!searchResults || !searchResults.products)) {
|
|
529
|
+
if (isLoading && (!searchResults || !searchResults.products?.length)) {
|
|
530
530
|
return renderLoadingState();
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
+
if (!isLoading && searchResults && searchResults.products?.length === 0) {
|
|
534
|
+
return renderEmptyState();
|
|
535
|
+
}
|
|
536
|
+
|
|
533
537
|
const renderLoadingOverlay = () => {
|
|
534
538
|
if (settings?.customRenderers?.render?.resultsGrid?.renderLoadingOverlay) {
|
|
535
539
|
return settings.customRenderers.render.resultsGrid.renderLoadingOverlay();
|
|
@@ -585,16 +589,14 @@ export function SimilarProductsResultsGrid({
|
|
|
585
589
|
<div className={gridClassName}>
|
|
586
590
|
{isLoading &&
|
|
587
591
|
searchResults &&
|
|
588
|
-
searchResults.products &&
|
|
592
|
+
searchResults.products?.length > 0 &&
|
|
589
593
|
renderLoadingOverlay()}
|
|
590
594
|
|
|
591
|
-
{searchResults && searchResults.products?.length > 0
|
|
595
|
+
{searchResults && searchResults.products?.length > 0 && (
|
|
592
596
|
<div className={resultsContainerClassName}>
|
|
593
597
|
{renderGridContainer()}
|
|
594
598
|
{renderPagination()}
|
|
595
599
|
</div>
|
|
596
|
-
) : (
|
|
597
|
-
renderEmptyState()
|
|
598
600
|
)}
|
|
599
601
|
</div>
|
|
600
602
|
);
|
|
@@ -3,22 +3,32 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { Icon } from '@akinon/next/components';
|
|
5
5
|
import { useLocalization } from '@akinon/next/hooks';
|
|
6
|
+
import { twMerge } from 'tailwind-merge';
|
|
6
7
|
|
|
7
8
|
interface SimilarProductsButtonProps {
|
|
8
9
|
onClick: () => void;
|
|
9
10
|
className?: string;
|
|
10
11
|
isLoading?: boolean;
|
|
11
12
|
disabled?: boolean;
|
|
13
|
+
settings?: any;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export function SimilarProductsButton({
|
|
15
17
|
onClick,
|
|
16
18
|
className = '',
|
|
17
19
|
isLoading = false,
|
|
18
|
-
disabled = false
|
|
20
|
+
disabled = false,
|
|
21
|
+
settings
|
|
19
22
|
}: SimilarProductsButtonProps) {
|
|
20
23
|
const { t } = useLocalization();
|
|
21
24
|
|
|
25
|
+
const searchButtonIconClassName = twMerge(
|
|
26
|
+
'fill-black',
|
|
27
|
+
settings?.customStyles?.searchButtonIcon
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const searchButtonIconName = settings?.iconNames?.searchButton || 'search';
|
|
31
|
+
|
|
22
32
|
return (
|
|
23
33
|
<button
|
|
24
34
|
onClick={onClick}
|
|
@@ -28,7 +38,7 @@ export function SimilarProductsButton({
|
|
|
28
38
|
} ${className}`}
|
|
29
39
|
>
|
|
30
40
|
<div className="flex items-center gap-2">
|
|
31
|
-
<Icon name=
|
|
41
|
+
<Icon name={searchButtonIconName} size={16} className={searchButtonIconClassName} />
|
|
32
42
|
<span className="text-xs font-medium text-black uppercase">
|
|
33
43
|
{t('common.product.view_similar_styles')}
|
|
34
44
|
</span>
|