@akinon/pz-similar-products 1.115.0-rc.23 → 1.115.0
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 +30 -34
- 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,47 +1,43 @@
|
|
|
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
|
-
import {
|
|
6
|
+
import { twMerge } from 'tailwind-merge';
|
|
7
7
|
|
|
8
8
|
interface ImageSearchButtonProps {
|
|
9
9
|
onClick: () => void;
|
|
10
10
|
className?: string;
|
|
11
|
+
settings?: any;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function ImageSearchButton({
|
|
14
15
|
onClick,
|
|
15
|
-
className
|
|
16
|
+
className,
|
|
17
|
+
settings
|
|
16
18
|
}: ImageSearchButtonProps) {
|
|
17
19
|
const { t } = useLocalization();
|
|
18
|
-
const { isEnabled, isLoading } = useImageSearchFeature();
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const imageSearchButtonIconName =
|
|
22
|
+
settings?.iconNames?.imageSearchButton || 'search';
|
|
23
|
+
const imageSearchButtonIconClassName = twMerge(
|
|
24
|
+
'text-black',
|
|
25
|
+
settings?.customStyles?.imageSearchButtonIcon
|
|
26
|
+
);
|
|
23
27
|
|
|
24
28
|
return (
|
|
25
29
|
<Button
|
|
26
30
|
onClick={onClick}
|
|
27
|
-
className={`flex items-center justify-center mr-2 text-gray-500 focus:outline-none border-none bg-transparent ${
|
|
31
|
+
className={`flex items-center justify-center mr-2 text-gray-500 focus:outline-none border-none bg-transparent ${
|
|
32
|
+
className || ''
|
|
33
|
+
}`}
|
|
28
34
|
title={t('common.search.image_search.title')}
|
|
29
35
|
>
|
|
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>
|
|
36
|
+
<Icon
|
|
37
|
+
name={imageSearchButtonIconName}
|
|
38
|
+
size={20}
|
|
39
|
+
className={imageSearchButtonIconClassName}
|
|
40
|
+
/>
|
|
45
41
|
</Button>
|
|
46
42
|
);
|
|
47
43
|
}
|
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,
|
|
@@ -57,9 +84,47 @@ export function SimilarProductsPlugin({
|
|
|
57
84
|
fetchSimilarProductsByImageUrl,
|
|
58
85
|
fetchSimilarProductsByImageCrop,
|
|
59
86
|
clearError,
|
|
60
|
-
clearResults
|
|
87
|
+
clearResults,
|
|
88
|
+
handleTextSearch,
|
|
89
|
+
handleClearText,
|
|
90
|
+
resetCropState
|
|
61
91
|
} = useSimilarProducts(product);
|
|
62
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
|
+
|
|
63
128
|
const {
|
|
64
129
|
isCropping,
|
|
65
130
|
crop,
|
|
@@ -74,7 +139,7 @@ export function SimilarProductsPlugin({
|
|
|
74
139
|
resetCrop
|
|
75
140
|
} = useImageCropper(
|
|
76
141
|
(loading) => {},
|
|
77
|
-
|
|
142
|
+
cropProcessImageFunction,
|
|
78
143
|
clearError
|
|
79
144
|
);
|
|
80
145
|
|
|
@@ -85,8 +150,12 @@ export function SimilarProductsPlugin({
|
|
|
85
150
|
product.productimage_set[0].image;
|
|
86
151
|
setCurrentImageUrl(originalImageUrl);
|
|
87
152
|
setHasUploadedImage(false);
|
|
153
|
+
hasUploadedImageRef.current = false;
|
|
154
|
+
setUploadFlag(false);
|
|
155
|
+
setSearchText('');
|
|
88
156
|
resetCrop();
|
|
89
|
-
|
|
157
|
+
resetCropState();
|
|
158
|
+
fetchSimilarProductsByImageUrl(originalImageUrl, '');
|
|
90
159
|
}
|
|
91
160
|
};
|
|
92
161
|
|
|
@@ -108,6 +177,8 @@ export function SimilarProductsPlugin({
|
|
|
108
177
|
|
|
109
178
|
if (!isOpen) {
|
|
110
179
|
setHasInitialSearchDone(false);
|
|
180
|
+
hasUploadedImageRef.current = false;
|
|
181
|
+
setUploadFlag(false);
|
|
111
182
|
clearError();
|
|
112
183
|
clearResults();
|
|
113
184
|
}
|
|
@@ -129,7 +200,9 @@ export function SimilarProductsPlugin({
|
|
|
129
200
|
if (result) {
|
|
130
201
|
setCurrentImageUrl(result);
|
|
131
202
|
setHasUploadedImage(true);
|
|
132
|
-
|
|
203
|
+
hasUploadedImageRef.current = true;
|
|
204
|
+
setUploadFlag(true);
|
|
205
|
+
fetchSimilarProductsByImageCrop(result, false);
|
|
133
206
|
}
|
|
134
207
|
};
|
|
135
208
|
reader.readAsDataURL(uploadedImageFile);
|
|
@@ -193,6 +266,10 @@ export function SimilarProductsPlugin({
|
|
|
193
266
|
settings={settings}
|
|
194
267
|
className={settings.customStyles?.modal}
|
|
195
268
|
showResetButton={showResetButton}
|
|
269
|
+
searchText={searchText}
|
|
270
|
+
setSearchText={setSearchText}
|
|
271
|
+
handleTextSearch={handleTextSearch}
|
|
272
|
+
handleClearText={handleClearText}
|
|
196
273
|
/>
|
|
197
274
|
|
|
198
275
|
<ImageSearchModalComponent
|
|
@@ -5,7 +5,6 @@ import { Product } from '@akinon/next/types';
|
|
|
5
5
|
import { SimilarProductsButton } from './search-button';
|
|
6
6
|
import { SimilarProductsPlugin } from './main';
|
|
7
7
|
import { mergeSettings } from '../utils';
|
|
8
|
-
import { useImageSearchFeature } from '../hooks/use-image-search-feature';
|
|
9
8
|
|
|
10
9
|
interface ProductImageSearchFeatureProps {
|
|
11
10
|
product: Product;
|
|
@@ -13,6 +12,7 @@ interface ProductImageSearchFeatureProps {
|
|
|
13
12
|
settings?: any;
|
|
14
13
|
className?: string;
|
|
15
14
|
isEnabled?: boolean;
|
|
15
|
+
enableTextSearch?: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function ProductImageSearchFeature({
|
|
@@ -20,18 +20,18 @@ export function ProductImageSearchFeature({
|
|
|
20
20
|
activeIndex = 0,
|
|
21
21
|
settings: userSettings,
|
|
22
22
|
className = 'absolute top-6 left-6 z-[20]',
|
|
23
|
-
isEnabled
|
|
23
|
+
isEnabled = false,
|
|
24
|
+
enableTextSearch = false
|
|
24
25
|
}: ProductImageSearchFeatureProps) {
|
|
25
|
-
const settings = mergeSettings(
|
|
26
|
+
const settings = mergeSettings({
|
|
27
|
+
...userSettings,
|
|
28
|
+
enableTextSearch
|
|
29
|
+
});
|
|
26
30
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
27
|
-
const { isEnabled: hookIsEnabled } = useImageSearchFeature();
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
: isEnabledProp !== undefined
|
|
33
|
-
? isEnabledProp
|
|
34
|
-
: hookIsEnabled;
|
|
32
|
+
if (!isEnabled) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
35
|
|
|
36
36
|
const handleClick = () => {
|
|
37
37
|
setIsModalOpen(true);
|
|
@@ -39,19 +39,19 @@ export function ProductImageSearchFeature({
|
|
|
39
39
|
|
|
40
40
|
return (
|
|
41
41
|
<>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
<SimilarProductsButton
|
|
43
|
+
onClick={handleClick}
|
|
44
|
+
className={className}
|
|
45
|
+
settings={settings}
|
|
46
|
+
/>
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
</>
|
|
54
|
-
)}
|
|
48
|
+
<SimilarProductsPlugin
|
|
49
|
+
product={product}
|
|
50
|
+
isOpen={isModalOpen}
|
|
51
|
+
onClose={() => setIsModalOpen(false)}
|
|
52
|
+
activeIndex={activeIndex}
|
|
53
|
+
settings={settings}
|
|
54
|
+
/>
|
|
55
55
|
</>
|
|
56
56
|
);
|
|
57
57
|
}
|
|
@@ -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>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import { Modal, Button, Icon, Select } from '@akinon/next/components';
|
|
4
|
+
import { Modal, Button, Icon, Select, Input } from '@akinon/next/components';
|
|
5
5
|
import { useLocalization } from '@akinon/next/hooks';
|
|
6
6
|
import { SimilarProductsFilterSidebar } from './filters';
|
|
7
7
|
import { SimilarProductsResultsGrid } from './results';
|
|
@@ -43,7 +43,11 @@ export function SimilarProductsModal({
|
|
|
43
43
|
fileError,
|
|
44
44
|
showResetButton = true,
|
|
45
45
|
settings,
|
|
46
|
-
className
|
|
46
|
+
className,
|
|
47
|
+
searchText,
|
|
48
|
+
setSearchText,
|
|
49
|
+
handleTextSearch,
|
|
50
|
+
handleClearText
|
|
47
51
|
}: SimilarProductsModalProps) {
|
|
48
52
|
const { t } = useLocalization();
|
|
49
53
|
|
|
@@ -164,6 +168,24 @@ export function SimilarProductsModal({
|
|
|
164
168
|
})) || []
|
|
165
169
|
) || [];
|
|
166
170
|
|
|
171
|
+
const modalCloseIconName = settings?.iconNames?.modalClose || 'close';
|
|
172
|
+
const filterIconName = settings?.iconNames?.filter || 'filter';
|
|
173
|
+
const modalSearchIconName = settings?.iconNames?.modalSearch || 'search';
|
|
174
|
+
const modalSearchClearIconName =
|
|
175
|
+
settings?.iconNames?.modalSearchClear || 'close';
|
|
176
|
+
|
|
177
|
+
const modalCloseIconClassName = twMerge(
|
|
178
|
+
'',
|
|
179
|
+
settings?.customStyles?.modalCloseIcon
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const filterIconClassName = twMerge('', settings?.customStyles?.filterIcon);
|
|
183
|
+
|
|
184
|
+
const modalSearchIconClassName = twMerge(
|
|
185
|
+
'text-gray-500',
|
|
186
|
+
settings?.customStyles?.modalSearchIcon
|
|
187
|
+
);
|
|
188
|
+
|
|
167
189
|
const renderHeader = () => {
|
|
168
190
|
if (settings?.customRenderers?.render?.modal?.renderHeader) {
|
|
169
191
|
return settings.customRenderers.render.modal.renderHeader({
|
|
@@ -246,7 +268,11 @@ export function SimilarProductsModal({
|
|
|
246
268
|
disabled={isLoading}
|
|
247
269
|
className={buttonClassName}
|
|
248
270
|
>
|
|
249
|
-
<Icon
|
|
271
|
+
<Icon
|
|
272
|
+
name={modalCloseIconName}
|
|
273
|
+
size={12}
|
|
274
|
+
className={modalCloseIconClassName}
|
|
275
|
+
/>
|
|
250
276
|
</Button>
|
|
251
277
|
</div>
|
|
252
278
|
);
|
|
@@ -266,7 +292,9 @@ export function SimilarProductsModal({
|
|
|
266
292
|
'',
|
|
267
293
|
onSortChange: handleSortChange,
|
|
268
294
|
onFilterMenuToggle: () => setIsFilterMenuOpen(true),
|
|
269
|
-
isLoading
|
|
295
|
+
isLoading,
|
|
296
|
+
searchText,
|
|
297
|
+
setSearchText
|
|
270
298
|
});
|
|
271
299
|
}
|
|
272
300
|
|
|
@@ -281,15 +309,30 @@ export function SimilarProductsModal({
|
|
|
281
309
|
);
|
|
282
310
|
|
|
283
311
|
const filterToggleClassName = twMerge(
|
|
284
|
-
'md:hidden text-xs',
|
|
312
|
+
'md:hidden text-xs px-3 py-2',
|
|
285
313
|
settings?.customStyles?.filterToggleButton
|
|
286
314
|
);
|
|
287
315
|
|
|
288
316
|
const sortDropdownClassName = twMerge(
|
|
289
|
-
'h-10 px-4 text-
|
|
317
|
+
'h-10 px-3 md:px-4 text-sm md:text-xs bg-gray-200 hover:bg-gray-400 transition-colors duration-200 border-gray-300 focus:border-primary focus:ring-1 focus:ring-primary w-full md:w-40 min-w-[120px]',
|
|
290
318
|
settings?.customStyles?.sortDropdown
|
|
291
319
|
);
|
|
292
320
|
|
|
321
|
+
const modalSearchInputClassName = twMerge(
|
|
322
|
+
'h-10 px-3 md:px-4 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent w-48 md:w-60 min-w-[180px]',
|
|
323
|
+
settings?.customStyles?.modalSearchInput
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const modalSearchContainerClassName = twMerge(
|
|
327
|
+
'relative flex items-center',
|
|
328
|
+
settings?.customStyles?.modalSearchContainer
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const modalSearchButtonClassName = twMerge(
|
|
332
|
+
'absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors',
|
|
333
|
+
settings?.customStyles?.modalSearchButton
|
|
334
|
+
);
|
|
335
|
+
|
|
293
336
|
const renderItemCount = () => {
|
|
294
337
|
if (settings?.customRenderers?.render?.modal?.renderItemCount) {
|
|
295
338
|
return settings.customRenderers.render.modal.renderItemCount({
|
|
@@ -299,7 +342,9 @@ export function SimilarProductsModal({
|
|
|
299
342
|
}
|
|
300
343
|
return (
|
|
301
344
|
<div className={itemCountClassName}>
|
|
302
|
-
|
|
345
|
+
<span className="hidden md:inline">
|
|
346
|
+
{searchResults?.pagination?.total_count || 0} items
|
|
347
|
+
</span>
|
|
303
348
|
</div>
|
|
304
349
|
);
|
|
305
350
|
};
|
|
@@ -319,7 +364,11 @@ export function SimilarProductsModal({
|
|
|
319
364
|
onClick={() => setIsFilterMenuOpen(true)}
|
|
320
365
|
data-testid="similar-products-filter"
|
|
321
366
|
>
|
|
322
|
-
<Icon
|
|
367
|
+
<Icon
|
|
368
|
+
name={filterIconName}
|
|
369
|
+
size={14}
|
|
370
|
+
className={filterIconClassName}
|
|
371
|
+
/>
|
|
323
372
|
{t('common.product.filters')}
|
|
324
373
|
</Button>
|
|
325
374
|
);
|
|
@@ -355,26 +404,134 @@ export function SimilarProductsModal({
|
|
|
355
404
|
);
|
|
356
405
|
};
|
|
357
406
|
|
|
407
|
+
const renderModalSearchInput = () => {
|
|
408
|
+
if (
|
|
409
|
+
!settings?.enableTextSearch ||
|
|
410
|
+
searchText === undefined ||
|
|
411
|
+
!setSearchText ||
|
|
412
|
+
!searchResults?.products?.length ||
|
|
413
|
+
!searchResults?.sorters?.length
|
|
414
|
+
) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (settings?.customRenderers?.render?.modal?.renderModalSearchInput) {
|
|
419
|
+
return settings.customRenderers.render.modal.renderModalSearchInput({
|
|
420
|
+
searchText,
|
|
421
|
+
setSearchText,
|
|
422
|
+
isLoading,
|
|
423
|
+
placeholder: t('common.search.placeholder'),
|
|
424
|
+
onSearch: handleTextSearch
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<div className={modalSearchContainerClassName}>
|
|
430
|
+
<Input
|
|
431
|
+
type="text"
|
|
432
|
+
value={searchText}
|
|
433
|
+
onChange={(e) => setSearchText(e.currentTarget.value)}
|
|
434
|
+
onKeyDown={(e) => {
|
|
435
|
+
if (e.key === 'Enter' && handleTextSearch) {
|
|
436
|
+
e.preventDefault();
|
|
437
|
+
handleTextSearch();
|
|
438
|
+
}
|
|
439
|
+
}}
|
|
440
|
+
placeholder={t('common.search.placeholder')}
|
|
441
|
+
className={twMerge(modalSearchInputClassName, 'pr-20')}
|
|
442
|
+
disabled={isLoading}
|
|
443
|
+
/>
|
|
444
|
+
{searchText && (
|
|
445
|
+
<button
|
|
446
|
+
onClick={() => {
|
|
447
|
+
if (handleClearText) {
|
|
448
|
+
handleClearText();
|
|
449
|
+
}
|
|
450
|
+
}}
|
|
451
|
+
disabled={isLoading}
|
|
452
|
+
className={twMerge(
|
|
453
|
+
'absolute right-12 top-1/2 -translate-y-1/2 p-2 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors',
|
|
454
|
+
settings?.customStyles?.modalSearchClearButton
|
|
455
|
+
)}
|
|
456
|
+
>
|
|
457
|
+
<Icon
|
|
458
|
+
name={modalSearchClearIconName}
|
|
459
|
+
size={16}
|
|
460
|
+
className={twMerge(
|
|
461
|
+
'text-gray-400 hover:text-gray-600',
|
|
462
|
+
settings?.customStyles?.modalSearchClearIcon
|
|
463
|
+
)}
|
|
464
|
+
/>
|
|
465
|
+
</button>
|
|
466
|
+
)}
|
|
467
|
+
<button
|
|
468
|
+
onClick={() => {
|
|
469
|
+
if (handleTextSearch) {
|
|
470
|
+
handleTextSearch();
|
|
471
|
+
}
|
|
472
|
+
}}
|
|
473
|
+
disabled={isLoading}
|
|
474
|
+
className={modalSearchButtonClassName}
|
|
475
|
+
>
|
|
476
|
+
{settings?.customRenderers?.render?.modal?.renderSearchIcon ? (
|
|
477
|
+
settings.customRenderers.render.modal.renderSearchIcon({
|
|
478
|
+
disabled: isLoading,
|
|
479
|
+
onClick: handleTextSearch
|
|
480
|
+
})
|
|
481
|
+
) : (
|
|
482
|
+
<Icon
|
|
483
|
+
name={modalSearchIconName}
|
|
484
|
+
size={18}
|
|
485
|
+
className={modalSearchIconClassName}
|
|
486
|
+
/>
|
|
487
|
+
)}
|
|
488
|
+
</button>
|
|
489
|
+
</div>
|
|
490
|
+
);
|
|
491
|
+
};
|
|
492
|
+
|
|
358
493
|
return (
|
|
359
494
|
<div className={containerClassName}>
|
|
360
495
|
<div
|
|
361
496
|
className={twMerge(
|
|
362
|
-
'
|
|
497
|
+
'p-3 md:p-4',
|
|
498
|
+
settings?.enableTextSearch &&
|
|
499
|
+
searchText !== undefined &&
|
|
500
|
+
setSearchText
|
|
501
|
+
? 'flex gap-3 md:grid md:grid-cols-3 md:gap-4 md:items-center'
|
|
502
|
+
: 'flex items-center justify-between',
|
|
363
503
|
settings?.customStyles?.controlsInner
|
|
364
504
|
)}
|
|
365
505
|
>
|
|
366
506
|
<div
|
|
367
507
|
className={twMerge(
|
|
368
|
-
'flex items-center gap-3',
|
|
508
|
+
'flex items-center gap-2 md:gap-3',
|
|
369
509
|
settings?.customStyles?.controlsLeft
|
|
370
510
|
)}
|
|
371
511
|
>
|
|
372
512
|
{renderItemCount()}
|
|
373
513
|
{renderFilterToggle()}
|
|
374
514
|
</div>
|
|
515
|
+
{settings?.enableTextSearch &&
|
|
516
|
+
searchText !== undefined &&
|
|
517
|
+
setSearchText && (
|
|
518
|
+
<div
|
|
519
|
+
className={twMerge(
|
|
520
|
+
'flex items-center justify-center w-full',
|
|
521
|
+
settings?.customStyles?.controlsCenter
|
|
522
|
+
)}
|
|
523
|
+
>
|
|
524
|
+
{renderModalSearchInput()}
|
|
525
|
+
</div>
|
|
526
|
+
)}
|
|
375
527
|
<div
|
|
376
528
|
className={twMerge(
|
|
377
529
|
'relative',
|
|
530
|
+
settings?.enableTextSearch &&
|
|
531
|
+
searchText !== undefined &&
|
|
532
|
+
setSearchText
|
|
533
|
+
? 'flex justify-end md:mt-0'
|
|
534
|
+
: '',
|
|
378
535
|
settings?.customStyles?.controlsRight
|
|
379
536
|
)}
|
|
380
537
|
>
|
|
@@ -410,6 +567,8 @@ export function SimilarProductsModal({
|
|
|
410
567
|
isLoading={isLoading}
|
|
411
568
|
handleFacetChange={handleFacetChange}
|
|
412
569
|
removeFacetFilter={removeFacetFilter}
|
|
570
|
+
searchText={searchText}
|
|
571
|
+
setSearchText={setSearchText}
|
|
413
572
|
currentImageUrl={currentImageUrl}
|
|
414
573
|
isCropping={isCropping}
|
|
415
574
|
imageRef={imageRef}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
const IMAGE_SEARCH_STORAGE_KEY = 'enable_image_search';
|
|
4
|
-
|
|
5
|
-
export function useImageSearchFeature() {
|
|
6
|
-
const [isEnabled, setIsEnabled] = useState(false);
|
|
7
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
8
|
-
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
const envEnabled = process.env.NEXT_PUBLIC_ENABLE_IMAGE_SEARCH === 'true';
|
|
11
|
-
|
|
12
|
-
if (envEnabled) {
|
|
13
|
-
setIsEnabled(true);
|
|
14
|
-
setIsLoading(false);
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const localStorageValue = localStorage.getItem(IMAGE_SEARCH_STORAGE_KEY);
|
|
20
|
-
setIsEnabled(localStorageValue === 'true');
|
|
21
|
-
} catch (error) {
|
|
22
|
-
setIsEnabled(false);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
setIsLoading(false);
|
|
26
|
-
}, []);
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
isEnabled,
|
|
30
|
-
isLoading
|
|
31
|
-
};
|
|
32
|
-
}
|