@akinon/pz-similar-products 1.92.0-rc.31 → 1.92.0-rc.33
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 +12 -0
- package/README.md +202 -0
- package/package.json +1 -1
- package/src/data/endpoints.ts +14 -5
- 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 +9 -1
- package/src/views/image-search-button.tsx +20 -18
- package/src/views/main.tsx +82 -5
- package/src/views/product-image-search-feature.tsx +8 -3
- package/src/views/search-button.tsx +12 -2
- package/src/views/search-modal.tsx +169 -10
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
|
))}
|
|
@@ -10,12 +10,14 @@ interface HeaderImageSearchFeatureProps {
|
|
|
10
10
|
className?: string;
|
|
11
11
|
isEnabled?: boolean;
|
|
12
12
|
settings?: any;
|
|
13
|
+
enableTextSearch?: boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function HeaderImageSearchFeature({
|
|
16
17
|
className,
|
|
17
18
|
isEnabled: isEnabledProp,
|
|
18
|
-
settings
|
|
19
|
+
settings: userSettings,
|
|
20
|
+
enableTextSearch = false
|
|
19
21
|
}: HeaderImageSearchFeatureProps) {
|
|
20
22
|
const { isEnabled: hookIsEnabled, isLoading } = useImageSearchFeature();
|
|
21
23
|
|
|
@@ -29,6 +31,11 @@ export function HeaderImageSearchFeature({
|
|
|
29
31
|
const [isImageSearchModalOpen, setIsImageSearchModalOpen] = useState(false);
|
|
30
32
|
const [isResultsModalOpen, setIsResultsModalOpen] = useState(false);
|
|
31
33
|
const [uploadedImageFile, setUploadedImageFile] = useState<File | null>(null);
|
|
34
|
+
|
|
35
|
+
const settings = {
|
|
36
|
+
...userSettings,
|
|
37
|
+
enableTextSearch
|
|
38
|
+
};
|
|
32
39
|
|
|
33
40
|
if (isLoading || !finalIsEnabled) {
|
|
34
41
|
return null;
|
|
@@ -60,6 +67,7 @@ export function HeaderImageSearchFeature({
|
|
|
60
67
|
<ImageSearchButton
|
|
61
68
|
onClick={handleOpenImageSearch}
|
|
62
69
|
className={className}
|
|
70
|
+
settings={settings}
|
|
63
71
|
/>
|
|
64
72
|
<SimilarProductsPlugin
|
|
65
73
|
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,
|
|
@@ -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
|
|
@@ -13,6 +13,7 @@ interface ProductImageSearchFeatureProps {
|
|
|
13
13
|
settings?: any;
|
|
14
14
|
className?: string;
|
|
15
15
|
isEnabled?: boolean;
|
|
16
|
+
enableTextSearch?: boolean;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function ProductImageSearchFeature({
|
|
@@ -20,9 +21,13 @@ export function ProductImageSearchFeature({
|
|
|
20
21
|
activeIndex = 0,
|
|
21
22
|
settings: userSettings,
|
|
22
23
|
className = 'absolute top-6 left-6 z-[20]',
|
|
23
|
-
isEnabled: isEnabledProp
|
|
24
|
+
isEnabled: isEnabledProp,
|
|
25
|
+
enableTextSearch = false
|
|
24
26
|
}: ProductImageSearchFeatureProps) {
|
|
25
|
-
const settings = mergeSettings(
|
|
27
|
+
const settings = mergeSettings({
|
|
28
|
+
...userSettings,
|
|
29
|
+
enableTextSearch
|
|
30
|
+
});
|
|
26
31
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
27
32
|
const { isEnabled: hookIsEnabled } = useImageSearchFeature();
|
|
28
33
|
|
|
@@ -41,7 +46,7 @@ export function ProductImageSearchFeature({
|
|
|
41
46
|
<>
|
|
42
47
|
{finalIsEnabled && (
|
|
43
48
|
<>
|
|
44
|
-
<SimilarProductsButton onClick={handleClick} className={className} />
|
|
49
|
+
<SimilarProductsButton onClick={handleClick} className={className} settings={settings} />
|
|
45
50
|
|
|
46
51
|
<SimilarProductsPlugin
|
|
47
52
|
product={product}
|
|
@@ -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>
|