@akinon/pz-similar-products 1.92.0-snapshot-ZERO-3457-20250627111231 → 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 +248 -3
- 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 +66 -0
- package/src/utils/index.ts +38 -3
- package/src/views/filters.tsx +713 -645
- package/src/views/header-image-search-feature.tsx +13 -1
- package/src/views/image-search-button.tsx +20 -18
- package/src/views/image-search.tsx +98 -86
- 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
|
@@ -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
|
}
|
|
@@ -79,114 +79,126 @@ export function ImageSearchModal({
|
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
return (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
title={t('common.search.image_search.title')}
|
|
85
|
-
open={isOpen}
|
|
86
|
-
setOpen={setIsOpen}
|
|
87
|
-
className={`w-full max-w-2xl max-h-[90vh] overflow-auto ${
|
|
88
|
-
className || ''
|
|
89
|
-
}`}
|
|
90
|
-
>
|
|
91
|
-
<div
|
|
92
|
-
className={twMerge(
|
|
93
|
-
'grid grid-cols-1 md:grid-cols-2 gap-4',
|
|
94
|
-
settings?.customStyles?.imageSearchContent
|
|
95
|
-
)}
|
|
96
|
-
>
|
|
82
|
+
<>
|
|
83
|
+
{isOpen && (
|
|
97
84
|
<div
|
|
98
85
|
className={twMerge(
|
|
99
|
-
'
|
|
100
|
-
settings?.customStyles?.
|
|
86
|
+
'fixed inset-0 bg-black/50 z-[60]',
|
|
87
|
+
settings?.customStyles?.imageSearchModalOverlay
|
|
101
88
|
)}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)}
|
|
116
|
-
>
|
|
117
|
-
<Input
|
|
118
|
-
type="file"
|
|
119
|
-
ref={fileInputRef}
|
|
120
|
-
onChange={handleFileSelect}
|
|
121
|
-
accept="image/*"
|
|
122
|
-
className="hidden"
|
|
123
|
-
/>
|
|
124
|
-
<Button
|
|
125
|
-
className={twMerge(
|
|
126
|
-
'w-full py-4 px-6 border-2 border-primary text-white hover:bg-primary hover:text-white transition-colors rounded-md',
|
|
127
|
-
settings?.customStyles?.imageSearchUploadButton
|
|
128
|
-
)}
|
|
129
|
-
onClick={() => fileInputRef.current?.click()}
|
|
130
|
-
>
|
|
131
|
-
{t('common.search.image_search.select_image')}
|
|
132
|
-
</Button>
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
89
|
+
onClick={() => setIsOpen(false)}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
<Modal
|
|
94
|
+
portalId="image-search-modal"
|
|
95
|
+
title={t('common.search.image_search.title')}
|
|
96
|
+
open={isOpen}
|
|
97
|
+
setOpen={setIsOpen}
|
|
98
|
+
className={`w-full max-w-2xl max-h-[90vh] overflow-auto ${
|
|
99
|
+
className || ''
|
|
100
|
+
}`}
|
|
101
|
+
>
|
|
135
102
|
<div
|
|
136
103
|
className={twMerge(
|
|
137
|
-
'
|
|
138
|
-
settings?.customStyles?.
|
|
104
|
+
'grid grid-cols-1 md:grid-cols-2 gap-4',
|
|
105
|
+
settings?.customStyles?.imageSearchContent
|
|
139
106
|
)}
|
|
140
107
|
>
|
|
141
|
-
<
|
|
142
|
-
className={twMerge(
|
|
143
|
-
'mb-2 font-medium',
|
|
144
|
-
settings?.customStyles?.imageSearchTipsTitle
|
|
145
|
-
)}
|
|
146
|
-
>
|
|
147
|
-
{t('common.search.image_search.best_results')}
|
|
148
|
-
</p>
|
|
149
|
-
<ul
|
|
108
|
+
<div
|
|
150
109
|
className={twMerge(
|
|
151
|
-
'
|
|
152
|
-
settings?.customStyles?.
|
|
110
|
+
'md:col-span-1 p-4',
|
|
111
|
+
settings?.customStyles?.imageSearchUploadSection
|
|
153
112
|
)}
|
|
154
113
|
>
|
|
155
|
-
<
|
|
114
|
+
<p
|
|
156
115
|
className={twMerge(
|
|
157
|
-
'mb-
|
|
158
|
-
settings?.customStyles?.
|
|
116
|
+
'mb-4 text-lg font-medium',
|
|
117
|
+
settings?.customStyles?.imageSearchUploadTitle
|
|
159
118
|
)}
|
|
160
119
|
>
|
|
161
|
-
{t('common.search.image_search.
|
|
162
|
-
</
|
|
163
|
-
<
|
|
120
|
+
{t('common.search.image_search.upload_image')}
|
|
121
|
+
</p>
|
|
122
|
+
<div
|
|
164
123
|
className={twMerge(
|
|
165
|
-
'
|
|
166
|
-
settings?.customStyles?.
|
|
124
|
+
'border-2 border-dashed border-gray-300 rounded-lg p-8 text-center',
|
|
125
|
+
settings?.customStyles?.imageSearchUploadArea
|
|
167
126
|
)}
|
|
168
127
|
>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
128
|
+
<Input
|
|
129
|
+
type="file"
|
|
130
|
+
ref={fileInputRef}
|
|
131
|
+
onChange={handleFileSelect}
|
|
132
|
+
accept="image/*"
|
|
133
|
+
className="hidden"
|
|
134
|
+
/>
|
|
135
|
+
<Button
|
|
136
|
+
className={twMerge(
|
|
137
|
+
'w-full py-4 px-6 border-2 border-primary text-white hover:bg-primary hover:text-white transition-colors rounded-md',
|
|
138
|
+
settings?.customStyles?.imageSearchUploadButton
|
|
139
|
+
)}
|
|
140
|
+
onClick={() => fileInputRef.current?.click()}
|
|
141
|
+
>
|
|
142
|
+
{t('common.search.image_search.select_image')}
|
|
143
|
+
</Button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div
|
|
147
|
+
className={twMerge(
|
|
148
|
+
'md:col-span-1 bg-gray-50 p-4 rounded-md h-fit my-4',
|
|
149
|
+
settings?.customStyles?.imageSearchTipsSection
|
|
150
|
+
)}
|
|
151
|
+
>
|
|
152
|
+
<p
|
|
172
153
|
className={twMerge(
|
|
173
|
-
'mb-2',
|
|
174
|
-
settings?.customStyles?.
|
|
154
|
+
'mb-2 font-medium',
|
|
155
|
+
settings?.customStyles?.imageSearchTipsTitle
|
|
175
156
|
)}
|
|
176
157
|
>
|
|
177
|
-
{t('common.search.image_search.
|
|
178
|
-
</
|
|
179
|
-
<
|
|
158
|
+
{t('common.search.image_search.best_results')}
|
|
159
|
+
</p>
|
|
160
|
+
<ul
|
|
180
161
|
className={twMerge(
|
|
181
|
-
'
|
|
182
|
-
settings?.customStyles?.
|
|
162
|
+
'text-left list-disc pl-5 text-sm text-gray-600',
|
|
163
|
+
settings?.customStyles?.imageSearchTipsList
|
|
183
164
|
)}
|
|
184
165
|
>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
166
|
+
<li
|
|
167
|
+
className={twMerge(
|
|
168
|
+
'mb-2',
|
|
169
|
+
settings?.customStyles?.imageSearchTipsItem
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
{t('common.search.image_search.tip_1')}
|
|
173
|
+
</li>
|
|
174
|
+
<li
|
|
175
|
+
className={twMerge(
|
|
176
|
+
'mb-2',
|
|
177
|
+
settings?.customStyles?.imageSearchTipsItem
|
|
178
|
+
)}
|
|
179
|
+
>
|
|
180
|
+
{t('common.search.image_search.tip_2')}
|
|
181
|
+
</li>
|
|
182
|
+
<li
|
|
183
|
+
className={twMerge(
|
|
184
|
+
'mb-2',
|
|
185
|
+
settings?.customStyles?.imageSearchTipsItem
|
|
186
|
+
)}
|
|
187
|
+
>
|
|
188
|
+
{t('common.search.image_search.tip_3')}
|
|
189
|
+
</li>
|
|
190
|
+
<li
|
|
191
|
+
className={twMerge(
|
|
192
|
+
'mb-2',
|
|
193
|
+
settings?.customStyles?.imageSearchTipsItem
|
|
194
|
+
)}
|
|
195
|
+
>
|
|
196
|
+
{t('common.search.image_search.tip_4')}
|
|
197
|
+
</li>
|
|
198
|
+
</ul>
|
|
199
|
+
</div>
|
|
188
200
|
</div>
|
|
189
|
-
</
|
|
190
|
-
|
|
201
|
+
</Modal>
|
|
202
|
+
</>
|
|
191
203
|
);
|
|
192
204
|
}
|
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>
|