@akinon/pz-similar-products 1.92.0-rc.37 → 1.92.0-rc.39
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 +8 -0
- package/README.md +39 -23
- package/package.json +1 -1
- package/src/data/endpoints.ts +25 -6
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-similar-products.ts +20 -6
- package/src/hooks/use-text-search-feature.ts +41 -0
- package/src/views/header-image-search-feature.tsx +6 -2
- package/src/views/product-image-search-feature.tsx +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @akinon/pz-similar-products
|
|
2
2
|
|
|
3
|
+
## 1.92.0-rc.39
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 174bf3a: ZERO-3533: Refactor similar products API endpoints to support array search parameters and improve query handling
|
|
8
|
+
|
|
9
|
+
## 1.92.0-rc.38
|
|
10
|
+
|
|
3
11
|
## 1.92.0-rc.37
|
|
4
12
|
|
|
5
13
|
## 1.92.0-rc.36
|
package/README.md
CHANGED
|
@@ -92,13 +92,13 @@ Change which icons are displayed by specifying different icon names:
|
|
|
92
92
|
```tsx
|
|
93
93
|
const iconCustomization = {
|
|
94
94
|
iconNames: {
|
|
95
|
-
searchButton: 'magnifier',
|
|
96
|
-
modalClose: 'x',
|
|
97
|
-
filter: 'funnel',
|
|
98
|
-
filterRemove: 'trash',
|
|
95
|
+
searchButton: 'magnifier', // Search button (default: 'search')
|
|
96
|
+
modalClose: 'x', // Modal close buttons (default: 'close')
|
|
97
|
+
filter: 'funnel', // Filter button (default: 'filter')
|
|
98
|
+
filterRemove: 'trash', // Filter tag remove buttons (default: 'close')
|
|
99
99
|
modalSearch: 'search-outline', // Modal search button (default: 'search')
|
|
100
|
-
modalSearchClear: 'clear',
|
|
101
|
-
imageSearchButton: 'upload'
|
|
100
|
+
modalSearchClear: 'clear', // Search input clear button (default: 'close')
|
|
101
|
+
imageSearchButton: 'upload' // Image search button (default: 'search')
|
|
102
102
|
}
|
|
103
103
|
};
|
|
104
104
|
```
|
|
@@ -133,13 +133,15 @@ const completeIconSettings = {
|
|
|
133
133
|
modalSearchClear: 'clear-alt',
|
|
134
134
|
imageSearchButton: 'upload-cloud'
|
|
135
135
|
},
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
// Style the icons
|
|
138
138
|
customStyles: {
|
|
139
|
-
searchButtonIcon:
|
|
139
|
+
searchButtonIcon:
|
|
140
|
+
'fill-indigo-500 hover:fill-indigo-600 transition-all duration-200',
|
|
140
141
|
modalCloseIcon: 'text-gray-500 hover:text-gray-700 w-5 h-5',
|
|
141
142
|
filterIcon: 'text-blue-500 hover:text-blue-600 w-4 h-4',
|
|
142
|
-
filterRemoveIcon:
|
|
143
|
+
filterRemoveIcon:
|
|
144
|
+
'text-red-400 hover:text-red-600 w-3 h-3 transition-colors',
|
|
143
145
|
modalSearchIcon: 'text-green-500 hover:text-green-600',
|
|
144
146
|
modalSearchClearIcon: 'text-gray-400 hover:text-gray-600',
|
|
145
147
|
imageSearchButtonIcon: 'text-indigo-500 hover:text-indigo-600 w-5 h-5'
|
|
@@ -154,7 +156,7 @@ const completeIconSettings = {
|
|
|
154
156
|
activeIndex,
|
|
155
157
|
settings: completeIconSettings
|
|
156
158
|
}}
|
|
157
|
-
|
|
159
|
+
/>;
|
|
158
160
|
```
|
|
159
161
|
|
|
160
162
|
### Available Icon Customization Points
|
|
@@ -288,7 +290,7 @@ interface SimilarProductsSettings {
|
|
|
288
290
|
itemCount?: string;
|
|
289
291
|
sortDropdown?: string;
|
|
290
292
|
filterToggleButton?: string;
|
|
291
|
-
|
|
293
|
+
|
|
292
294
|
// Text search input
|
|
293
295
|
modalSearchInput?: string;
|
|
294
296
|
modalSearchContainer?: string;
|
|
@@ -296,7 +298,7 @@ interface SimilarProductsSettings {
|
|
|
296
298
|
modalSearchIcon?: string;
|
|
297
299
|
modalSearchClearButton?: string;
|
|
298
300
|
modalSearchClearIcon?: string;
|
|
299
|
-
|
|
301
|
+
|
|
300
302
|
// Icon customization
|
|
301
303
|
searchButtonIcon?: string; // Search button icon styling
|
|
302
304
|
modalCloseIcon?: string; // Modal close button icons styling
|
|
@@ -476,13 +478,16 @@ The plugin features a built-in text search functionality that allows users to se
|
|
|
476
478
|
```tsx
|
|
477
479
|
const textSearchSettings = {
|
|
478
480
|
enableTextSearch: true, // Enable text search input
|
|
479
|
-
|
|
481
|
+
|
|
480
482
|
customStyles: {
|
|
481
483
|
modalSearchContainer: 'relative flex items-center',
|
|
482
|
-
modalSearchInput:
|
|
483
|
-
|
|
484
|
+
modalSearchInput:
|
|
485
|
+
'h-10 px-4 pr-20 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500',
|
|
486
|
+
modalSearchButton:
|
|
487
|
+
'absolute right-2 top-1/2 -translate-y-1/2 p-2 hover:bg-gray-100 rounded-md',
|
|
484
488
|
modalSearchIcon: 'text-gray-500',
|
|
485
|
-
modalSearchClearButton:
|
|
489
|
+
modalSearchClearButton:
|
|
490
|
+
'absolute right-12 top-1/2 -translate-y-1/2 p-2 hover:bg-gray-100 rounded-md',
|
|
486
491
|
modalSearchClearIcon: 'text-gray-400 hover:text-gray-600'
|
|
487
492
|
}
|
|
488
493
|
};
|
|
@@ -493,22 +498,33 @@ const textSearchSettings = {
|
|
|
493
498
|
```tsx
|
|
494
499
|
const customIconSettings = {
|
|
495
500
|
enableTextSearch: true,
|
|
496
|
-
|
|
501
|
+
|
|
497
502
|
customRenderers: {
|
|
498
503
|
render: {
|
|
499
504
|
modal: {
|
|
500
505
|
renderSearchIcon: ({ disabled, onClick }) => (
|
|
501
|
-
<svg
|
|
502
|
-
className="w-5 h-5 text-blue-500 cursor-pointer hover:text-blue-700"
|
|
506
|
+
<svg
|
|
507
|
+
className="w-5 h-5 text-blue-500 cursor-pointer hover:text-blue-700"
|
|
503
508
|
onClick={onClick}
|
|
504
|
-
fill="none"
|
|
505
|
-
stroke="currentColor"
|
|
509
|
+
fill="none"
|
|
510
|
+
stroke="currentColor"
|
|
506
511
|
viewBox="0 0 24 24"
|
|
507
512
|
>
|
|
508
|
-
<path
|
|
513
|
+
<path
|
|
514
|
+
strokeLinecap="round"
|
|
515
|
+
strokeLinejoin="round"
|
|
516
|
+
strokeWidth={2}
|
|
517
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
518
|
+
/>
|
|
509
519
|
</svg>
|
|
510
520
|
),
|
|
511
|
-
renderModalSearchInput: ({
|
|
521
|
+
renderModalSearchInput: ({
|
|
522
|
+
searchText,
|
|
523
|
+
setSearchText,
|
|
524
|
+
isLoading,
|
|
525
|
+
placeholder,
|
|
526
|
+
onSearch
|
|
527
|
+
}) => (
|
|
512
528
|
<div className="relative">
|
|
513
529
|
<input
|
|
514
530
|
type="text"
|
package/package.json
CHANGED
package/src/data/endpoints.ts
CHANGED
|
@@ -27,7 +27,12 @@ export const similarProductsApi = api.injectEndpoints({
|
|
|
27
27
|
endpoints: (build) => ({
|
|
28
28
|
getSimilarProductsByUrl: build.query<
|
|
29
29
|
SimilarProductsResponse,
|
|
30
|
-
{
|
|
30
|
+
{
|
|
31
|
+
url: string;
|
|
32
|
+
limit?: number;
|
|
33
|
+
excluded_product_ids?: number[];
|
|
34
|
+
text?: string;
|
|
35
|
+
}
|
|
31
36
|
>({
|
|
32
37
|
query: ({ url, limit = 20, excluded_product_ids, text }) => {
|
|
33
38
|
const params = new URLSearchParams();
|
|
@@ -54,7 +59,12 @@ export const similarProductsApi = api.injectEndpoints({
|
|
|
54
59
|
|
|
55
60
|
getSimilarProductsByImage: build.mutation<
|
|
56
61
|
SimilarProductsResponse,
|
|
57
|
-
{
|
|
62
|
+
{
|
|
63
|
+
image: string;
|
|
64
|
+
limit?: number;
|
|
65
|
+
excluded_product_ids?: number[];
|
|
66
|
+
text?: string;
|
|
67
|
+
}
|
|
58
68
|
>({
|
|
59
69
|
query: ({ image, limit = 20, excluded_product_ids, text }) => {
|
|
60
70
|
const params = new URLSearchParams();
|
|
@@ -81,12 +91,21 @@ export const similarProductsApi = api.injectEndpoints({
|
|
|
81
91
|
SimilarProductsListResponse,
|
|
82
92
|
{
|
|
83
93
|
filter: string;
|
|
84
|
-
searchParams?: Record<string, string>;
|
|
94
|
+
searchParams?: Record<string, string | string[]>;
|
|
85
95
|
isExclude?: boolean;
|
|
86
96
|
}
|
|
87
97
|
>({
|
|
88
98
|
query: ({ filter, searchParams = {}, isExclude = false }) => {
|
|
89
|
-
const params = new URLSearchParams(
|
|
99
|
+
const params = new URLSearchParams();
|
|
100
|
+
|
|
101
|
+
Object.entries(searchParams).forEach(([key, value]) => {
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
value.forEach((v) => params.append(key, v));
|
|
104
|
+
} else {
|
|
105
|
+
params.append(key, value);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
90
109
|
const queryString = params.toString();
|
|
91
110
|
|
|
92
111
|
const headerName = isExclude
|
|
@@ -107,7 +126,7 @@ export const similarProductsApi = api.injectEndpoints({
|
|
|
107
126
|
.reduce((acc, key) => {
|
|
108
127
|
acc[key] = searchParams[key];
|
|
109
128
|
return acc;
|
|
110
|
-
}, {} as Record<string, string>);
|
|
129
|
+
}, {} as Record<string, string | string[]>);
|
|
111
130
|
|
|
112
131
|
return JSON.stringify({
|
|
113
132
|
filter,
|
|
@@ -128,4 +147,4 @@ export const {
|
|
|
128
147
|
useLazyGetSimilarProductsListQuery
|
|
129
148
|
} = similarProductsApi;
|
|
130
149
|
|
|
131
|
-
export type { Product, Facet, FacetChoice, SortOption, Pagination };
|
|
150
|
+
export type { Product, Facet, FacetChoice, SortOption, Pagination };
|
package/src/hooks/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { debounce } from '../utils';
|
|
|
18
18
|
type SearchResults = SimilarProductsListResponse;
|
|
19
19
|
|
|
20
20
|
interface SearchParams {
|
|
21
|
-
[key: string]: string;
|
|
21
|
+
[key: string]: string | string[];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export function useSimilarProducts(product: Product) {
|
|
@@ -115,7 +115,7 @@ export function useSimilarProducts(product: Product) {
|
|
|
115
115
|
String(choice.value)
|
|
116
116
|
);
|
|
117
117
|
const searchKey = String(facet.search_key || facet.key);
|
|
118
|
-
searchParams[searchKey] = values.
|
|
118
|
+
searchParams[searchKey] = values.length === 1 ? values[0] : values;
|
|
119
119
|
}
|
|
120
120
|
});
|
|
121
121
|
}
|
|
@@ -350,7 +350,9 @@ export function useSimilarProducts(product: Product) {
|
|
|
350
350
|
const productIds = extractProductIds(data);
|
|
351
351
|
|
|
352
352
|
if (productIds.length > 0) {
|
|
353
|
-
const
|
|
353
|
+
const searchParams = buildSearchParams(searchResults?.facets);
|
|
354
|
+
|
|
355
|
+
const result = await executeProductSearch(productIds, searchParams);
|
|
354
356
|
if (result) {
|
|
355
357
|
const resultWithCorrectSorterState = {
|
|
356
358
|
...result,
|
|
@@ -369,7 +371,11 @@ export function useSimilarProducts(product: Product) {
|
|
|
369
371
|
executeProductSearch,
|
|
370
372
|
updateSorterState,
|
|
371
373
|
updateResultsAndKey,
|
|
372
|
-
createEmptySearchResults
|
|
374
|
+
createEmptySearchResults,
|
|
375
|
+
searchText,
|
|
376
|
+
searchTextRef,
|
|
377
|
+
buildSearchParams,
|
|
378
|
+
searchResults
|
|
373
379
|
]
|
|
374
380
|
);
|
|
375
381
|
|
|
@@ -974,7 +980,11 @@ export function useSimilarProducts(product: Product) {
|
|
|
974
980
|
const textToUse = searchTextRef.current || searchText;
|
|
975
981
|
|
|
976
982
|
if (hasCroppedImage && currentImageBase64) {
|
|
977
|
-
await fetchSimilarProductsByBase64(
|
|
983
|
+
await fetchSimilarProductsByBase64(
|
|
984
|
+
currentImageBase64,
|
|
985
|
+
textToUse,
|
|
986
|
+
!hasUploadedImage
|
|
987
|
+
);
|
|
978
988
|
} else if (hasUploadedImage && currentImageUrl) {
|
|
979
989
|
await fetchSimilarProductsByBase64(currentImageUrl, textToUse, false);
|
|
980
990
|
} else if (currentImageUrl) {
|
|
@@ -995,7 +1005,11 @@ export function useSimilarProducts(product: Product) {
|
|
|
995
1005
|
setSearchTextWithRef('');
|
|
996
1006
|
|
|
997
1007
|
if (hasCroppedImage && currentImageBase64) {
|
|
998
|
-
await fetchSimilarProductsByBase64(
|
|
1008
|
+
await fetchSimilarProductsByBase64(
|
|
1009
|
+
currentImageBase64,
|
|
1010
|
+
'',
|
|
1011
|
+
!hasUploadedImage
|
|
1012
|
+
);
|
|
999
1013
|
} else if (hasUploadedImage && currentImageUrl) {
|
|
1000
1014
|
await fetchSimilarProductsByBase64(currentImageUrl, '', false);
|
|
1001
1015
|
} else if (currentImageUrl) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
const TEXT_SEARCH_STORAGE_KEY = 'enable_image_search_text';
|
|
4
|
+
|
|
5
|
+
interface UseTextSearchFeatureProps {
|
|
6
|
+
enableTextSearch?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function useTextSearchFeature(props?: UseTextSearchFeatureProps) {
|
|
10
|
+
const [isEnabled, setIsEnabled] = useState(false);
|
|
11
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const envEnabled = process.env.NEXT_PUBLIC_ENABLE_TEXT_SEARCH === 'true';
|
|
15
|
+
|
|
16
|
+
if (envEnabled) {
|
|
17
|
+
setIsEnabled(true);
|
|
18
|
+
setIsLoading(false);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const localStorageValue = localStorage.getItem(TEXT_SEARCH_STORAGE_KEY);
|
|
24
|
+
|
|
25
|
+
if (localStorageValue === 'true') {
|
|
26
|
+
setIsEnabled(true);
|
|
27
|
+
} else {
|
|
28
|
+
setIsEnabled(!!props?.enableTextSearch);
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
setIsEnabled(false);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setIsLoading(false);
|
|
35
|
+
}, [props?.enableTextSearch]);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
isEnabled,
|
|
39
|
+
isLoading
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -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
|
|
|
@@ -20,6 +21,9 @@ export function HeaderImageSearchFeature({
|
|
|
20
21
|
enableTextSearch = false
|
|
21
22
|
}: HeaderImageSearchFeatureProps) {
|
|
22
23
|
const { isEnabled: hookIsEnabled, isLoading } = useImageSearchFeature();
|
|
24
|
+
const { isEnabled: textSearchEnabled } = useTextSearchFeature({
|
|
25
|
+
enableTextSearch
|
|
26
|
+
});
|
|
23
27
|
|
|
24
28
|
const envEnabled = process.env.NEXT_PUBLIC_ENABLE_IMAGE_SEARCH === 'true';
|
|
25
29
|
const finalIsEnabled = envEnabled
|
|
@@ -31,10 +35,10 @@ export function HeaderImageSearchFeature({
|
|
|
31
35
|
const [isImageSearchModalOpen, setIsImageSearchModalOpen] = useState(false);
|
|
32
36
|
const [isResultsModalOpen, setIsResultsModalOpen] = useState(false);
|
|
33
37
|
const [uploadedImageFile, setUploadedImageFile] = useState<File | null>(null);
|
|
34
|
-
|
|
38
|
+
|
|
35
39
|
const settings = {
|
|
36
40
|
...userSettings,
|
|
37
|
-
enableTextSearch
|
|
41
|
+
enableTextSearch: textSearchEnabled
|
|
38
42
|
};
|
|
39
43
|
|
|
40
44
|
if (isLoading || !finalIsEnabled) {
|
|
@@ -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;
|
|
@@ -24,12 +25,16 @@ export function ProductImageSearchFeature({
|
|
|
24
25
|
isEnabled: isEnabledProp,
|
|
25
26
|
enableTextSearch = false
|
|
26
27
|
}: ProductImageSearchFeatureProps) {
|
|
28
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
29
|
+
const { isEnabled: hookIsEnabled } = useImageSearchFeature();
|
|
30
|
+
const { isEnabled: textSearchEnabled } = useTextSearchFeature({
|
|
31
|
+
enableTextSearch
|
|
32
|
+
});
|
|
33
|
+
|
|
27
34
|
const settings = mergeSettings({
|
|
28
35
|
...userSettings,
|
|
29
|
-
enableTextSearch
|
|
36
|
+
enableTextSearch: textSearchEnabled
|
|
30
37
|
});
|
|
31
|
-
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
32
|
-
const { isEnabled: hookIsEnabled } = useImageSearchFeature();
|
|
33
38
|
|
|
34
39
|
const envEnabled = process.env.NEXT_PUBLIC_ENABLE_IMAGE_SEARCH === 'true';
|
|
35
40
|
const finalIsEnabled = envEnabled
|
|
@@ -46,7 +51,11 @@ export function ProductImageSearchFeature({
|
|
|
46
51
|
<>
|
|
47
52
|
{finalIsEnabled && (
|
|
48
53
|
<>
|
|
49
|
-
<SimilarProductsButton
|
|
54
|
+
<SimilarProductsButton
|
|
55
|
+
onClick={handleClick}
|
|
56
|
+
className={className}
|
|
57
|
+
settings={settings}
|
|
58
|
+
/>
|
|
50
59
|
|
|
51
60
|
<SimilarProductsPlugin
|
|
52
61
|
product={product}
|