@nyris/nyris-webapp 0.3.89 → 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.
Files changed (70) hide show
  1. package/build/_headers +2 -0
  2. package/build/asset-manifest.json +6 -6
  3. package/build/index.html +1 -1
  4. package/build/js/settings.example.js +17 -0
  5. package/build/static/css/main.734b52e1.css +4 -0
  6. package/build/static/css/main.734b52e1.css.map +1 -0
  7. package/build/static/js/main.cede3ae1.js +3 -0
  8. package/build/static/js/{main.ca8b95bc.js.map → main.cede3ae1.js.map} +1 -1
  9. package/package.json +3 -3
  10. package/public/_headers +2 -0
  11. package/public/index.html +1 -1
  12. package/public/js/settings.example.js +17 -0
  13. package/src/App.tsx +5 -3
  14. package/src/components/Cart.tsx +321 -0
  15. package/src/components/CustomCameraDrawer.tsx +4 -22
  16. package/src/components/DragDropFile.tsx +57 -38
  17. package/src/components/ExperienceVisualSearch/ExperienceVisualSearch.tsx +6 -1
  18. package/src/components/ExperienceVisualSearch/ExperienceVisualSearchTrigger.tsx +2 -2
  19. package/src/components/GroundingSpecs.tsx +47 -0
  20. package/src/components/Header.tsx +94 -93
  21. package/src/components/HitsPerPage.tsx +4 -2
  22. package/src/components/ImagePreview.tsx +64 -31
  23. package/src/components/ImageUpload.tsx +247 -0
  24. package/src/components/ItemSpecification.tsx +164 -0
  25. package/src/components/MatchNotificationBanner.tsx +165 -0
  26. package/src/components/PostFilter/PostFilter.tsx +22 -1
  27. package/src/components/PostFilter/PostFilterComponent.tsx +59 -26
  28. package/src/components/PostFilter/PostFilterFindApi.tsx +242 -0
  29. package/src/components/PoweredBy.tsx +16 -0
  30. package/src/components/PreFilter/PreFilter.tsx +77 -54
  31. package/src/components/Product/Product.tsx +186 -28
  32. package/src/components/Product/ProductAttribute.tsx +2 -2
  33. package/src/components/Product/ProductDetailView.tsx +123 -18
  34. package/src/components/Product/ProductDetailViewModal.tsx +3 -0
  35. package/src/components/Product/ProductList.tsx +78 -8
  36. package/src/components/SidePanel.tsx +212 -120
  37. package/src/components/TextSearch.tsx +82 -203
  38. package/src/components/Toaster.tsx +34 -15
  39. package/src/helpers/ToastHelper.ts +6 -2
  40. package/src/hooks/useCadSearch.ts +5 -0
  41. package/src/hooks/useImageSearch.ts +102 -13
  42. package/src/index.css +59 -0
  43. package/src/layouts/AppLayout.tsx +16 -14
  44. package/src/pages/Home.tsx +61 -13
  45. package/src/pages/Result.tsx +287 -295
  46. package/src/services/vizo.ts +161 -0
  47. package/src/stores/request/Misc/misc.initialstate.ts +1 -0
  48. package/src/stores/request/Misc/misc.slice.ts +1 -0
  49. package/src/stores/request/filter/filter.initialState.ts +3 -0
  50. package/src/stores/request/filter/filter.slice.ts +23 -0
  51. package/src/stores/result/prodcuts/products.initialState.ts +4 -0
  52. package/src/stores/result/prodcuts/products.slice.ts +15 -0
  53. package/src/stores/types.ts +27 -1
  54. package/src/stores/ui/loading/loading.initialState.ts +1 -0
  55. package/src/stores/ui/loading/loading.slice.ts +4 -0
  56. package/src/stores/ui/sidePanel/sidePanel.initialState.ts +5 -0
  57. package/src/stores/ui/sidePanel/sidePanel.slice.ts +11 -0
  58. package/src/stores/ui/uiStore.ts +4 -1
  59. package/src/styles/Cart.scss +210 -0
  60. package/src/styles/common.scss +10 -0
  61. package/src/translations.ts +4 -4
  62. package/src/types.ts +11 -3
  63. package/src/utils/prepareImageList.ts +6 -5
  64. package/src/utils/textSearchFilter.ts +203 -0
  65. package/tailwind.config.js +1 -0
  66. package/build/static/css/main.ba1c7479.css +0 -4
  67. package/build/static/css/main.ba1c7479.css.map +0 -1
  68. package/build/static/js/main.ca8b95bc.js +0 -3
  69. package/src/components/Footer.tsx +0 -21
  70. /package/build/static/js/{main.ca8b95bc.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 useResultStore from 'stores/result/resultStore';
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 = React.useMemo(
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 productsFromAlgolia = useResultStore(
28
- state => state.productsFromAlgolia,
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
- if (productsFromAlgolia.length === 0) return <></>;
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-6',
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
- <AccordionItem value={'expand'}>
85
- <AccordionTrigger className="text-xs font-normal w-full h-8 items-center justify-end gap-2 border-b border-solid border-[#e0e0e0]">
86
- {isExpanded ? t('Collapse all') : t('Expand all')}
87
- </AccordionTrigger>
88
- </AccordionItem>
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
- <div
96
- key={index}
97
- className={twMerge([
98
- index !== window.settings.refinements.length - 1 &&
99
- 'border-b border-solid border-[#e0e0e0]',
100
- 'py-4',
101
- ])}
102
- >
103
- <PostFilter
104
- attribute={refinement.attribute}
105
- label={refinement.header}
106
- searchable={refinement.searchable}
107
- />
108
- </div>
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 { getFilters, searchFilters } from 'services/filter';
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 "../../stores/result/resultStore";
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>(false);
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 = async () => {
65
- setLoading(true);
66
- const dataResultFilter = getFilters(1000, settings)
67
- .then(res => {
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
- const newResult = arrResult.sort().reduce((a: any, c: any) => {
73
- if (!c[0]) return a;
74
- let k = c[0]?.toLocaleUpperCase();
75
- if (a[k]) a[k].push(c);
76
- else a[k] = [c];
77
- return a;
78
- }, {});
79
- setResultFilter(newResult);
80
- setColumns(Object.keys(newResult).length);
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 (preFilterValues?.length && preFilterValues[0] !== specification?.prefilter_value) {
135
- setSpecifications({ prefilter_value: preFilterValues?.join(', ') || ''});
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
- filterSearchHandler(e.target.value);
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
- filterSearchHandler('');
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
- {isLoading && (
303
- <div className={`columns-1 desktop:columns-4`}>
304
- {Array(12)
305
- .fill('')
306
- .map((_, index) => {
307
- return (
308
- <div key={index} className="mb-4 flex flex-col gap-3 w-full">
309
- <Skeleton className="h-[20px] w-[60px]" />
310
- {Array(6)
311
- .fill('')
312
- .map((_, index) => (
313
- <Skeleton key={index} className="h-[20px] w-full" />
314
- ))}
315
- </div>
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) && !isLoading && (
343
+ {isEmpty(resultFilter) && !isLoadingState && (
321
344
  <div>{t('No result found')}</div>
322
345
  )}
323
346
  </div>