@nyris/nyris-webapp 0.3.88 → 0.3.90
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/build/_headers +2 -0
- package/build/asset-manifest.json +6 -6
- package/build/index.html +1 -1
- package/build/js/settings.example.js +17 -0
- package/build/static/css/main.734b52e1.css +4 -0
- package/build/static/css/main.734b52e1.css.map +1 -0
- package/build/static/js/main.cede3ae1.js +3 -0
- package/build/static/js/{main.e861b336.js.map → main.cede3ae1.js.map} +1 -1
- package/package.json +3 -3
- package/public/_headers +2 -0
- package/public/index.html +1 -1
- package/public/js/settings.example.js +17 -0
- package/src/App.tsx +5 -3
- package/src/components/Cart.tsx +321 -0
- package/src/components/CustomCameraDrawer.tsx +4 -22
- package/src/components/DragDropFile.tsx +57 -38
- package/src/components/ExperienceVisualSearch/ExperienceVisualSearch.tsx +6 -1
- package/src/components/ExperienceVisualSearch/ExperienceVisualSearchTrigger.tsx +2 -2
- package/src/components/GroundingSpecs.tsx +47 -0
- package/src/components/Header.tsx +94 -93
- package/src/components/HitsPerPage.tsx +4 -2
- package/src/components/ImagePreview.tsx +64 -31
- package/src/components/ImageUpload.tsx +247 -0
- package/src/components/ItemSpecification.tsx +164 -0
- package/src/components/MatchNotificationBanner.tsx +165 -0
- package/src/components/PostFilter/PostFilter.tsx +22 -1
- package/src/components/PostFilter/PostFilterComponent.tsx +59 -26
- package/src/components/PostFilter/PostFilterFindApi.tsx +242 -0
- package/src/components/PoweredBy.tsx +16 -0
- package/src/components/PreFilter/PreFilter.tsx +77 -54
- package/src/components/Product/Product.tsx +186 -28
- package/src/components/Product/ProductAttribute.tsx +2 -2
- package/src/components/Product/ProductDetailView.tsx +123 -18
- package/src/components/Product/ProductDetailViewModal.tsx +3 -0
- package/src/components/Product/ProductList.tsx +78 -8
- package/src/components/SidePanel.tsx +212 -120
- package/src/components/TextSearch.tsx +82 -203
- package/src/components/Toaster.tsx +34 -15
- package/src/helpers/ToastHelper.ts +6 -2
- package/src/hooks/useCadSearch.ts +5 -0
- package/src/hooks/useImageSearch.ts +102 -13
- package/src/index.css +59 -0
- package/src/layouts/AppLayout.tsx +16 -14
- package/src/pages/Home.tsx +61 -13
- package/src/pages/Result.tsx +287 -295
- package/src/services/vizo.ts +161 -0
- package/src/stores/request/Misc/misc.initialstate.ts +1 -0
- package/src/stores/request/Misc/misc.slice.ts +1 -0
- package/src/stores/request/filter/filter.initialState.ts +3 -0
- package/src/stores/request/filter/filter.slice.ts +23 -0
- package/src/stores/result/prodcuts/products.initialState.ts +4 -0
- package/src/stores/result/prodcuts/products.slice.ts +15 -0
- package/src/stores/types.ts +27 -1
- package/src/stores/ui/loading/loading.initialState.ts +1 -0
- package/src/stores/ui/loading/loading.slice.ts +4 -0
- package/src/stores/ui/sidePanel/sidePanel.initialState.ts +5 -0
- package/src/stores/ui/sidePanel/sidePanel.slice.ts +11 -0
- package/src/stores/ui/uiStore.ts +4 -1
- package/src/styles/Cart.scss +210 -0
- package/src/styles/common.scss +10 -0
- package/src/translations.ts +4 -4
- package/src/types.ts +11 -3
- package/src/utils/prepareImageList.ts +6 -5
- package/src/utils/textSearchFilter.ts +203 -0
- package/tailwind.config.js +1 -0
- package/build/static/css/main.ba1c7479.css +0 -4
- package/build/static/css/main.ba1c7479.css.map +0 -1
- package/build/static/js/main.e861b336.js +0 -3
- package/src/components/Footer.tsx +0 -21
- /package/build/static/js/{main.e861b336.js.LICENSE.txt → main.cede3ae1.js.LICENSE.txt} +0 -0
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
AccordionItem,
|
|
4
4
|
AccordionTrigger,
|
|
5
5
|
} from 'components/Accordion';
|
|
6
|
-
import React, { useState } from 'react';
|
|
6
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
8
|
import { twMerge } from 'tailwind-merge';
|
|
9
9
|
import PostFilter from './PostFilter';
|
|
10
|
-
import
|
|
10
|
+
import PostFilterFindApi from './PostFilterFindApi';
|
|
11
11
|
|
|
12
12
|
function PostFilterComponent({ className }: { className?: string }) {
|
|
13
13
|
const [accordionValues, setAccordionValues] = useState([
|
|
@@ -17,25 +17,45 @@ function PostFilterComponent({ className }: { className?: string }) {
|
|
|
17
17
|
'expand',
|
|
18
18
|
]);
|
|
19
19
|
|
|
20
|
-
const isExpanded =
|
|
20
|
+
const isExpanded = useMemo(
|
|
21
21
|
() => accordionValues.includes('expand'),
|
|
22
22
|
[accordionValues],
|
|
23
23
|
);
|
|
24
24
|
|
|
25
25
|
const { t } = useTranslation();
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
|
|
27
|
+
const [visibleSections, setVisibleSections] = useState<
|
|
28
|
+
Record<string, boolean>
|
|
29
|
+
>({});
|
|
30
|
+
|
|
31
|
+
const handleVisibilityChange = useCallback(
|
|
32
|
+
(attribute: string, isVisible: boolean) => {
|
|
33
|
+
setVisibleSections(prev =>
|
|
34
|
+
prev[attribute] === isVisible
|
|
35
|
+
? prev
|
|
36
|
+
: { ...prev, [attribute]: isVisible },
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
[],
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const hasVisibleSections = useMemo(
|
|
43
|
+
() => Object.values(visibleSections).some(Boolean),
|
|
44
|
+
[visibleSections],
|
|
29
45
|
);
|
|
30
46
|
|
|
31
|
-
|
|
47
|
+
const visibleAttributes = useMemo(() => {
|
|
48
|
+
return window.settings.refinements
|
|
49
|
+
.map((refinement: { attribute: string }) => refinement.attribute)
|
|
50
|
+
.filter((attribute: string | number) => visibleSections[attribute]);
|
|
51
|
+
}, [visibleSections]);
|
|
32
52
|
|
|
33
53
|
return (
|
|
34
54
|
<div
|
|
35
55
|
className={twMerge([
|
|
36
56
|
'mt-4',
|
|
37
57
|
'w-full',
|
|
38
|
-
'px-
|
|
58
|
+
'px-4',
|
|
39
59
|
'flex',
|
|
40
60
|
'flex-col',
|
|
41
61
|
'gap-4',
|
|
@@ -81,31 +101,44 @@ function PostFilterComponent({ className }: { className?: string }) {
|
|
|
81
101
|
}}
|
|
82
102
|
value={accordionValues}
|
|
83
103
|
>
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
104
|
+
{hasVisibleSections && (
|
|
105
|
+
<AccordionItem value={'expand'}>
|
|
106
|
+
<AccordionTrigger className="text-xs font-normal w-full h-8 items-center justify-end gap-2 border-b border-solid border-[#e0e0e0]">
|
|
107
|
+
{isExpanded ? t('Collapse all') : t('Expand all')}
|
|
108
|
+
</AccordionTrigger>
|
|
109
|
+
</AccordionItem>
|
|
110
|
+
)}
|
|
89
111
|
{window.settings.refinements.map(
|
|
90
112
|
(
|
|
91
113
|
refinement: { attribute: string; header: any; searchable: any },
|
|
92
114
|
index: React.Key | null | undefined,
|
|
93
115
|
) => {
|
|
116
|
+
const isLastVisible =
|
|
117
|
+
visibleAttributes[visibleAttributes.length - 1] ===
|
|
118
|
+
refinement.attribute;
|
|
119
|
+
|
|
94
120
|
return (
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
121
|
+
<React.Fragment key={index}>
|
|
122
|
+
{window.settings.algolia.enabled && (
|
|
123
|
+
<PostFilter
|
|
124
|
+
attribute={refinement.attribute}
|
|
125
|
+
label={refinement.header}
|
|
126
|
+
searchable={refinement.searchable}
|
|
127
|
+
onVisibilityChange={handleVisibilityChange}
|
|
128
|
+
isLastVisible={isLastVisible}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{!window.settings.algolia.enabled && (
|
|
133
|
+
<PostFilterFindApi
|
|
134
|
+
attribute={refinement.attribute}
|
|
135
|
+
label={refinement.header}
|
|
136
|
+
searchable={false}
|
|
137
|
+
onVisibilityChange={handleVisibilityChange}
|
|
138
|
+
isLastVisible={isLastVisible}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
</React.Fragment>
|
|
109
142
|
);
|
|
110
143
|
},
|
|
111
144
|
)}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
AccordionContent,
|
|
4
|
+
AccordionItem,
|
|
5
|
+
AccordionTrigger,
|
|
6
|
+
} from 'components/Accordion';
|
|
7
|
+
import { Icon } from '@nyris/nyris-react-components';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import useResultStore from 'stores/result/resultStore';
|
|
10
|
+
import useRequestStore from 'stores/request/requestStore';
|
|
11
|
+
import { filterProductsByText } from 'utils/textSearchFilter';
|
|
12
|
+
|
|
13
|
+
function PostFilterFindApi({
|
|
14
|
+
attribute,
|
|
15
|
+
label,
|
|
16
|
+
searchable,
|
|
17
|
+
onVisibilityChange,
|
|
18
|
+
isLastVisible,
|
|
19
|
+
}: {
|
|
20
|
+
attribute: string;
|
|
21
|
+
label: string;
|
|
22
|
+
searchable: boolean;
|
|
23
|
+
onVisibilityChange?: (attribute: string, isVisible: boolean) => void;
|
|
24
|
+
isLastVisible?: boolean;
|
|
25
|
+
}) {
|
|
26
|
+
const [itemsLimit, setItemsLimit] = useState(10);
|
|
27
|
+
const [searchInput, setSearchInput] = useState<string>('');
|
|
28
|
+
|
|
29
|
+
const productsFromFindApi = useResultStore(
|
|
30
|
+
state => state.productsFromFindApi,
|
|
31
|
+
);
|
|
32
|
+
const groundingFilterResult = useResultStore(
|
|
33
|
+
state => state.groundingFilterResult,
|
|
34
|
+
);
|
|
35
|
+
const showingGroundingFilterResult = useResultStore(
|
|
36
|
+
state => state.showingGroundingFilterResult,
|
|
37
|
+
);
|
|
38
|
+
const query = useRequestStore(state => state.query);
|
|
39
|
+
const requestImages = useRequestStore(state => state.requestImages);
|
|
40
|
+
const postFilterSelections = useRequestStore(
|
|
41
|
+
state => state.postFilterSelections,
|
|
42
|
+
);
|
|
43
|
+
const togglePostFilterSelection = useRequestStore(
|
|
44
|
+
state => state.togglePostFilterSelection,
|
|
45
|
+
);
|
|
46
|
+
const { t } = useTranslation();
|
|
47
|
+
|
|
48
|
+
const productsForFilters = useMemo(() => {
|
|
49
|
+
let baseProducts = productsFromFindApi;
|
|
50
|
+
|
|
51
|
+
if (requestImages?.length && query?.trim()) {
|
|
52
|
+
baseProducts = filterProductsByText(query, productsFromFindApi);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (showingGroundingFilterResult && groundingFilterResult?.length > 0) {
|
|
56
|
+
const grounded = baseProducts.filter((product: any) =>
|
|
57
|
+
groundingFilterResult.some((result: any) => product.sku === result.sku),
|
|
58
|
+
);
|
|
59
|
+
if (grounded.length > 0) {
|
|
60
|
+
return grounded;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return baseProducts;
|
|
65
|
+
}, [
|
|
66
|
+
productsFromFindApi,
|
|
67
|
+
query,
|
|
68
|
+
requestImages,
|
|
69
|
+
showingGroundingFilterResult,
|
|
70
|
+
groundingFilterResult,
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
const filteredProductsForCounts = useMemo(() => {
|
|
74
|
+
const selections = postFilterSelections || {};
|
|
75
|
+
const activeSelections = Object.entries(selections).filter(
|
|
76
|
+
([attr, values]) => attr !== attribute && Array.isArray(values),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (!activeSelections.length) {
|
|
80
|
+
return productsForFilters;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return productsForFilters.filter((product: any) =>
|
|
84
|
+
activeSelections.every(([attr, values]) => {
|
|
85
|
+
const filterValues = product.filters?.[attr];
|
|
86
|
+
if (!Array.isArray(filterValues) || filterValues.length === 0) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return values.some(value => filterValues.includes(value));
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}, [attribute, postFilterSelections, productsForFilters]);
|
|
93
|
+
console.log({ filteredProductsForCounts });
|
|
94
|
+
|
|
95
|
+
const valueCounts = useMemo(() => {
|
|
96
|
+
const counts: Record<string, number> = {};
|
|
97
|
+
filteredProductsForCounts?.forEach((product: any) => {
|
|
98
|
+
const filterValues = product.filters?.[attribute];
|
|
99
|
+
if (Array.isArray(filterValues)) {
|
|
100
|
+
filterValues.forEach((val: string) => {
|
|
101
|
+
if (val) counts[val] = (counts[val] || 0) + 1;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return counts;
|
|
107
|
+
}, [attribute, filteredProductsForCounts]);
|
|
108
|
+
|
|
109
|
+
const items = useMemo(() => {
|
|
110
|
+
const selectedValues = postFilterSelections?.[attribute] || [];
|
|
111
|
+
const allValues = new Set<string>([
|
|
112
|
+
...Object.keys(valueCounts),
|
|
113
|
+
...selectedValues,
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
return Array.from(allValues)
|
|
117
|
+
.map(label => ({
|
|
118
|
+
label,
|
|
119
|
+
value: label,
|
|
120
|
+
count: valueCounts[label] || 0,
|
|
121
|
+
isRefined: selectedValues.includes(label),
|
|
122
|
+
}))
|
|
123
|
+
.filter(
|
|
124
|
+
item =>
|
|
125
|
+
!searchInput ||
|
|
126
|
+
item.label.toLowerCase().includes(searchInput.toLowerCase()),
|
|
127
|
+
)
|
|
128
|
+
.sort((a, b) => b.count - a.count || a.label.localeCompare(b.label))
|
|
129
|
+
.slice(0, itemsLimit);
|
|
130
|
+
}, [attribute, itemsLimit, postFilterSelections, searchInput, valueCounts]);
|
|
131
|
+
|
|
132
|
+
const selectedValues = postFilterSelections?.[attribute] || [];
|
|
133
|
+
const hasSelection = selectedValues.length > 0;
|
|
134
|
+
const hasAvailableValues = Object.keys(valueCounts).length > 0;
|
|
135
|
+
const hasSearch = searchable && searchInput.length > 0;
|
|
136
|
+
const isVisible = hasAvailableValues || hasSelection || hasSearch;
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
onVisibilityChange?.(attribute, isVisible);
|
|
140
|
+
}, [attribute, isVisible, onVisibilityChange]);
|
|
141
|
+
|
|
142
|
+
if (!isVisible) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const onShowMore = () => {
|
|
147
|
+
setItemsLimit(prev => prev + 10);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
<AccordionItem
|
|
153
|
+
value={attribute}
|
|
154
|
+
className={
|
|
155
|
+
isLastVisible ? 'py-4' : 'border-b border-solid border-[#e0e0e0] py-4'
|
|
156
|
+
}
|
|
157
|
+
>
|
|
158
|
+
<AccordionTrigger className="text-sm font-semibold w-full h-8 items-center">
|
|
159
|
+
{label}
|
|
160
|
+
</AccordionTrigger>
|
|
161
|
+
{searchable && (
|
|
162
|
+
<div
|
|
163
|
+
style={{
|
|
164
|
+
position: 'relative',
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
<input
|
|
168
|
+
name="postfilter-search"
|
|
169
|
+
type="text"
|
|
170
|
+
autoComplete="off"
|
|
171
|
+
autoCorrect="off"
|
|
172
|
+
autoCapitalize="off"
|
|
173
|
+
spellCheck={false}
|
|
174
|
+
maxLength={512}
|
|
175
|
+
value={searchInput}
|
|
176
|
+
onChange={event => setSearchInput(event.currentTarget.value)}
|
|
177
|
+
className="w-full h-8 rounded-2xl bg-[#F3F3F5] pl-8 pr-2 outline-none"
|
|
178
|
+
style={{
|
|
179
|
+
fontSize: 14,
|
|
180
|
+
}}
|
|
181
|
+
placeholder={`${t('Search')} ${label}`}
|
|
182
|
+
/>
|
|
183
|
+
{searchInput && (
|
|
184
|
+
<Icon
|
|
185
|
+
name="close"
|
|
186
|
+
className="absolute top-2.5 right-3 hover:cursor-pointer w-3 h-3"
|
|
187
|
+
onClick={() => setSearchInput('')}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
<Icon name="search" className="absolute top-2 left-2" />
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{!items.length && (
|
|
195
|
+
<div
|
|
196
|
+
style={{
|
|
197
|
+
fontSize: 14,
|
|
198
|
+
paddingTop: 16,
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
{t('No filters found')}
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
<AccordionContent>
|
|
205
|
+
<div className="flex flex-col gap-4 mt-4 ml-2">
|
|
206
|
+
{items.map(item => (
|
|
207
|
+
<div key={item.label}>
|
|
208
|
+
<label className="flex items-center w-fit cursor-pointer">
|
|
209
|
+
<input
|
|
210
|
+
type="checkbox"
|
|
211
|
+
checked={item.isRefined}
|
|
212
|
+
onChange={() =>
|
|
213
|
+
togglePostFilterSelection(attribute, item.value)
|
|
214
|
+
}
|
|
215
|
+
className="cursor-pointer"
|
|
216
|
+
/>
|
|
217
|
+
<span className="text-xs font-normal pl-2 pr-1">
|
|
218
|
+
{item.label}
|
|
219
|
+
</span>
|
|
220
|
+
<span className="text-xs font-normal">({item.count})</span>
|
|
221
|
+
</label>
|
|
222
|
+
</div>
|
|
223
|
+
))}
|
|
224
|
+
{items.length === itemsLimit && (
|
|
225
|
+
<button
|
|
226
|
+
className="hover:bg-[#E9E9EC] rounded-[4px] p-2"
|
|
227
|
+
style={{
|
|
228
|
+
fontSize: 14,
|
|
229
|
+
}}
|
|
230
|
+
onClick={onShowMore}
|
|
231
|
+
>
|
|
232
|
+
{t('Load More')}
|
|
233
|
+
</button>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
</AccordionContent>
|
|
237
|
+
</AccordionItem>
|
|
238
|
+
</>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export default PostFilterFindApi;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Icon } from '@nyris/nyris-react-components';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export default function PoweredBy({ className }: { className?: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<div className={twMerge(className)}>
|
|
7
|
+
<Icon
|
|
8
|
+
className="fill-black group-hover:fill-[url(#powered_by_nyris_colored_svg__gradient)] hover:fill-[url(#powered_by_nyris_colored_svg__gradient)] cursor-pointer w-[110px] h-5"
|
|
9
|
+
name="powered_by_nyris"
|
|
10
|
+
onClick={() => {
|
|
11
|
+
window.open('https://www.nyris.io', '_blank');
|
|
12
|
+
}}
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { searchFilters } from 'services/filter';
|
|
3
3
|
|
|
4
4
|
import { isEmpty, pickBy } from 'lodash';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
@@ -9,9 +9,8 @@ import useRequestStore from 'stores/request/requestStore';
|
|
|
9
9
|
import { truncateString } from 'utils/truncateString';
|
|
10
10
|
import { twMerge } from 'tailwind-merge';
|
|
11
11
|
import Tooltip from 'components/Tooltip/TooltipComponent';
|
|
12
|
-
import { Skeleton } from 'components/Skeleton';
|
|
13
12
|
import { useNavigate } from 'react-router';
|
|
14
|
-
import useResultStore from
|
|
13
|
+
import useResultStore from '../../stores/result/resultStore';
|
|
15
14
|
|
|
16
15
|
interface Props {
|
|
17
16
|
handleClose?: any;
|
|
@@ -25,15 +24,18 @@ const PreFilterComponent = (props: Props) => {
|
|
|
25
24
|
|
|
26
25
|
const [searchKey, setSearchKey] = useState<string>('');
|
|
27
26
|
|
|
28
|
-
const [isLoading, setLoading] = useState<boolean>(
|
|
27
|
+
const [isLoading, setLoading] = useState<boolean>(true);
|
|
29
28
|
const [columns, setColumns] = useState<number>(0);
|
|
29
|
+
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
30
|
+
const debounceDelayMs = 300;
|
|
30
31
|
|
|
31
32
|
const { singleImageSearch } = useImageSearch();
|
|
32
33
|
|
|
33
34
|
const requestImages = useRequestStore(state => state.requestImages);
|
|
34
35
|
const imageRegions = useRequestStore(state => state.regions);
|
|
35
36
|
const keyFilterState = useRequestStore(state => state.preFilter);
|
|
36
|
-
|
|
37
|
+
const preFilterList = useRequestStore(state => state.preFilterList);
|
|
38
|
+
const preFilterLoading = useRequestStore(state => state.preFilterLoading);
|
|
37
39
|
const setPreFilter = useRequestStore(state => state.setPreFilter);
|
|
38
40
|
const setAlgoliaFilter = useRequestStore(state => state.setAlgoliaFilter);
|
|
39
41
|
const specification = useRequestStore(state => state.specifications);
|
|
@@ -56,45 +58,48 @@ const PreFilterComponent = (props: Props) => {
|
|
|
56
58
|
[keyFilter],
|
|
57
59
|
);
|
|
58
60
|
|
|
61
|
+
const isLoadingState = isLoading || preFilterLoading;
|
|
62
|
+
|
|
59
63
|
useEffect(() => {
|
|
64
|
+
if (searchKey && searchKey.trim()) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
setLoading(true);
|
|
60
68
|
getDataFilterDesktop();
|
|
61
69
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
70
|
+
}, [preFilterList, searchKey]);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
return () => {
|
|
74
|
+
if (debounceTimerRef.current) {
|
|
75
|
+
clearTimeout(debounceTimerRef.current);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
62
78
|
}, []);
|
|
63
79
|
|
|
64
|
-
const getDataFilterDesktop =
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const arrResult =
|
|
69
|
-
res.find(value => value.key === settings.visualSearchFilterKey)
|
|
70
|
-
?.values || [];
|
|
80
|
+
const getDataFilterDesktop = () => {
|
|
81
|
+
const arrResult =
|
|
82
|
+
preFilterList.find(value => value.key === settings.visualSearchFilterKey)
|
|
83
|
+
?.values || [];
|
|
71
84
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
})
|
|
83
|
-
.catch((e: any) => {
|
|
84
|
-
console.log('err getDataFilterDesktop', e);
|
|
85
|
-
})
|
|
86
|
-
.finally(() => {
|
|
87
|
-
setLoading(false);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
return dataResultFilter;
|
|
85
|
+
const newResult = arrResult.sort().reduce((a: any, c: any) => {
|
|
86
|
+
if (!c[0]) return a;
|
|
87
|
+
let k = c[0]?.toLocaleUpperCase();
|
|
88
|
+
if (a[k]) a[k].push(c);
|
|
89
|
+
else a[k] = [c];
|
|
90
|
+
return a;
|
|
91
|
+
}, {});
|
|
92
|
+
setResultFilter(newResult);
|
|
93
|
+
setColumns(Object.keys(newResult).length);
|
|
94
|
+
setLoading(false);
|
|
91
95
|
};
|
|
92
96
|
|
|
93
97
|
const filterSearchHandler = async (value: any) => {
|
|
94
|
-
if (!value) {
|
|
98
|
+
if (!value || !String(value).trim()) {
|
|
95
99
|
getDataFilterDesktop();
|
|
96
100
|
return;
|
|
97
101
|
}
|
|
102
|
+
setLoading(true);
|
|
98
103
|
const data = await searchFilters(
|
|
99
104
|
settings.visualSearchFilterKey,
|
|
100
105
|
encodeURIComponent(value),
|
|
@@ -110,14 +115,25 @@ const PreFilterComponent = (props: Props) => {
|
|
|
110
115
|
setResultFilter({});
|
|
111
116
|
setColumns(4);
|
|
112
117
|
}
|
|
118
|
+
setLoading(false);
|
|
113
119
|
return;
|
|
114
120
|
})
|
|
115
121
|
.catch((e: any) => {
|
|
116
122
|
console.log('err filterSearchHandler', e);
|
|
123
|
+
setLoading(false);
|
|
117
124
|
});
|
|
118
125
|
return data;
|
|
119
126
|
};
|
|
120
127
|
|
|
128
|
+
const debouncedFilterSearchHandler = (value: any) => {
|
|
129
|
+
if (debounceTimerRef.current) {
|
|
130
|
+
clearTimeout(debounceTimerRef.current);
|
|
131
|
+
}
|
|
132
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
133
|
+
filterSearchHandler(value);
|
|
134
|
+
}, debounceDelayMs);
|
|
135
|
+
};
|
|
136
|
+
|
|
121
137
|
const onHandlerSubmitData = () => {
|
|
122
138
|
const preFilter = pickBy(keyFilter, value => !!value);
|
|
123
139
|
setPreFilter(preFilter);
|
|
@@ -131,8 +147,11 @@ const PreFilterComponent = (props: Props) => {
|
|
|
131
147
|
: '';
|
|
132
148
|
setAlgoliaFilter(filter);
|
|
133
149
|
|
|
134
|
-
if (
|
|
135
|
-
|
|
150
|
+
if (
|
|
151
|
+
preFilterValues?.length &&
|
|
152
|
+
preFilterValues[0] !== specification?.prefilter_value
|
|
153
|
+
) {
|
|
154
|
+
setSpecifications({ prefilter_value: preFilterValues?.join(', ') || '' });
|
|
136
155
|
}
|
|
137
156
|
if (specification?.is_nameplate) {
|
|
138
157
|
setImageAnalysis({});
|
|
@@ -181,7 +200,7 @@ const PreFilterComponent = (props: Props) => {
|
|
|
181
200
|
className="border-none bg-transparent outline-none w-full desktop:w-[500px] h-full"
|
|
182
201
|
placeholder={t('Search')}
|
|
183
202
|
onChange={(e: any) => {
|
|
184
|
-
|
|
203
|
+
debouncedFilterSearchHandler(e.target.value);
|
|
185
204
|
setSearchKey(e.target.value);
|
|
186
205
|
}}
|
|
187
206
|
value={searchKey}
|
|
@@ -191,7 +210,7 @@ const PreFilterComponent = (props: Props) => {
|
|
|
191
210
|
className="w-10 h-10 rounded-[50%] flex justify-center items-center cursor-pointer"
|
|
192
211
|
onClick={() => {
|
|
193
212
|
setSearchKey('');
|
|
194
|
-
|
|
213
|
+
debouncedFilterSearchHandler('');
|
|
195
214
|
}}
|
|
196
215
|
>
|
|
197
216
|
<Icon name="close" className="w-4 h-4 text-primary" />
|
|
@@ -299,25 +318,29 @@ const PreFilterComponent = (props: Props) => {
|
|
|
299
318
|
);
|
|
300
319
|
})}
|
|
301
320
|
|
|
302
|
-
{
|
|
303
|
-
<div className=
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
321
|
+
{isLoadingState && (
|
|
322
|
+
<div className="w-full flex items-center justify-center py-10">
|
|
323
|
+
<svg
|
|
324
|
+
width={48}
|
|
325
|
+
height={48}
|
|
326
|
+
viewBox="0 0 50 50"
|
|
327
|
+
fill="none"
|
|
328
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
329
|
+
className="loading-spinner"
|
|
330
|
+
>
|
|
331
|
+
<circle
|
|
332
|
+
cx="25"
|
|
333
|
+
cy="25"
|
|
334
|
+
r="20"
|
|
335
|
+
stroke="#3E36DC"
|
|
336
|
+
strokeWidth="4"
|
|
337
|
+
strokeLinecap="round"
|
|
338
|
+
strokeDasharray="90 30"
|
|
339
|
+
/>
|
|
340
|
+
</svg>
|
|
318
341
|
</div>
|
|
319
342
|
)}
|
|
320
|
-
{isEmpty(resultFilter) && !
|
|
343
|
+
{isEmpty(resultFilter) && !isLoadingState && (
|
|
321
344
|
<div>{t('No result found')}</div>
|
|
322
345
|
)}
|
|
323
346
|
</div>
|