@nyris/nyris-webapp 0.3.96 → 0.3.97

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 (34) hide show
  1. package/build/asset-manifest.json +6 -6
  2. package/build/index.html +1 -1
  3. package/build/js/settings.example.js +6 -1
  4. package/build/static/css/main.7c7f5447.css +4 -0
  5. package/build/static/css/main.7c7f5447.css.map +1 -0
  6. package/build/static/js/main.9e7fb38a.js +3 -0
  7. package/build/static/js/{main.fd12cce3.js.map → main.9e7fb38a.js.map} +1 -1
  8. package/package.json +3 -3
  9. package/public/js/settings.example.js +6 -1
  10. package/src/components/Cart.tsx +200 -85
  11. package/src/components/Header.tsx +1 -1
  12. package/src/components/Inquiry/InquiryModal.tsx +2 -2
  13. package/src/components/PostFilter/PostFilter.tsx +36 -3
  14. package/src/components/PostFilter/PostFilterFindApi.tsx +41 -3
  15. package/src/components/PreFilter/PreFilter.tsx +30 -2
  16. package/src/components/Product/Product.tsx +2 -2
  17. package/src/components/Product/ProductDetailView.tsx +2 -2
  18. package/src/components/ScopeMissing.tsx +50 -0
  19. package/src/components/SmartFilter.tsx +59 -69
  20. package/src/components/TextSearch.tsx +3 -1
  21. package/src/hooks/useImageSearch.ts +9 -1
  22. package/src/hooks/useResultEvaluator.ts +8 -8
  23. package/src/layouts/AppLayout.tsx +27 -6
  24. package/src/pages/Result.tsx +2 -1
  25. package/src/routes/ProctedRoute.tsx +52 -9
  26. package/src/services/filter.ts +13 -0
  27. package/src/services/visualSearch.ts +10 -1
  28. package/src/stores/scope/scopeStore.ts +35 -0
  29. package/src/styles/Cart.scss +22 -0
  30. package/src/types.ts +6 -1
  31. package/build/static/css/main.5fff4ca0.css +0 -4
  32. package/build/static/css/main.5fff4ca0.css.map +0 -1
  33. package/build/static/js/main.fd12cce3.js +0 -3
  34. /package/build/static/js/{main.fd12cce3.js.LICENSE.txt → main.9e7fb38a.js.LICENSE.txt} +0 -0
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nyris/nyris-webapp",
3
- "version": "0.3.96",
3
+ "version": "0.3.97",
4
4
  "dependencies": {
5
5
  "@auth0/auth0-react": "^2.2.4",
6
6
  "@emailjs/browser": "^4.3.3",
7
7
  "@google/genai": "^1.46.0",
8
- "@nyris/nyris-api": "^0.3.96",
9
- "@nyris/nyris-react-components": "^0.3.96",
8
+ "@nyris/nyris-api": "^0.3.97",
9
+ "@nyris/nyris-react-components": "^0.3.97",
10
10
  "@radix-ui/react-accordion": "^1.2.2",
11
11
  "@radix-ui/react-dialog": "^1.1.4",
12
12
  "@radix-ui/react-popover": "^1.1.4",
@@ -97,7 +97,11 @@ var settings = {
97
97
  },
98
98
  ],
99
99
  // features
100
- cart: false,
100
+ cart: {
101
+ enabled: true,
102
+ buttonText: 'Add to cart',
103
+ emailTemplateId: 'template_h3rt2pi',
104
+ },
101
105
  showPoweredByNyris: '',
102
106
  postFilterOption: '',
103
107
  preFilterOption: '',
@@ -115,6 +119,7 @@ var settings = {
115
119
  enabled: '',
116
120
  emailTemplateId: '',
117
121
  },
122
+ scopeFilter: true,
118
123
  support: {
119
124
  enabled: '',
120
125
  emailInquiry: '',
@@ -4,6 +4,14 @@ import Tooltip from './Tooltip/TooltipComponent';
4
4
  import { twMerge } from 'tailwind-merge';
5
5
  import toast from 'react-hot-toast';
6
6
  import { Icon } from '@nyris/nyris-react-components';
7
+ import { Send } from 'lucide-react';
8
+ import emailjs from '@emailjs/browser';
9
+ import { useAuth0 } from '@auth0/auth0-react';
10
+ import NoImage from '../common/assets/images/no-image.svg';
11
+
12
+ const EMAILJS_SERVICE_ID = 'service_zfsxshi';
13
+ const EMAILJS_TEMPLATE_ID = window.settings.cart?.emailTemplateId || '';
14
+ const EMAILJS_PUBLIC_KEY = 'SMGihPnuEGcYLm0V4';
7
15
 
8
16
  const Cart = () => {
9
17
  const [isCartOpen, setIsCartOpen] = useState(false);
@@ -13,6 +21,13 @@ const Cart = () => {
13
21
  {},
14
22
  );
15
23
 
24
+ const { user } = useAuth0();
25
+ const auth0Email = window.settings.auth0?.enabled ? user?.email : undefined;
26
+
27
+ const [isSendingEmail, setIsSendingEmail] = useState(false);
28
+ const [isEmailInputVisible, setIsEmailInputVisible] = useState(false);
29
+ const [userEmail, setUserEmail] = useState('');
30
+
16
31
  const updateCount = () => {
17
32
  const cart = sessionStorage.getItem('cart');
18
33
  if (cart) {
@@ -111,6 +126,48 @@ const Cart = () => {
111
126
  toast.success('Your data has been downloaded.');
112
127
  };
113
128
 
129
+ const sendCSVByEmail = async (
130
+ e?: React.FormEvent,
131
+ emailOverride?: string,
132
+ ) => {
133
+ if (e) {
134
+ e.preventDefault();
135
+ }
136
+ const emailToSend = emailOverride ?? userEmail;
137
+ if (!emailToSend || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailToSend)) {
138
+ toast.error('Please enter a valid email address.');
139
+ return;
140
+ }
141
+
142
+ setIsSendingEmail(true);
143
+ try {
144
+ const csvContent = generateCSVContent();
145
+ // Use btoa to encode the CSV string to base64 directly as recommended by EmailJS
146
+ const base64Content = btoa(unescape(encodeURIComponent(csvContent)));
147
+
148
+ await emailjs.send(
149
+ EMAILJS_SERVICE_ID,
150
+ EMAILJS_TEMPLATE_ID,
151
+ {
152
+ email_id: emailToSend,
153
+ // machine_name: getMachineName(),
154
+ date: new Date().toLocaleDateString(),
155
+ item_count: amountOfParts,
156
+ content: base64Content,
157
+ },
158
+ EMAILJS_PUBLIC_KEY,
159
+ );
160
+
161
+ toast.success('Parts list has been sent by email.');
162
+ setIsEmailInputVisible(false);
163
+ setUserEmail('');
164
+ } catch (error) {
165
+ toast.error('Failed to send the parts list by email.');
166
+ } finally {
167
+ setIsSendingEmail(false);
168
+ }
169
+ };
170
+
114
171
  return (
115
172
  <>
116
173
  <Tooltip
@@ -211,103 +268,113 @@ const Cart = () => {
211
268
  ) : (
212
269
  ''
213
270
  )}
214
- {cartParts.map((part: any, i: number) => (
215
- <div
216
- key={part.sku + i}
217
- className={twMerge(
218
- 'flex h-fit w-full items-center justify-between rounded-lg border border-solid border-[#DDDEE7] bg-[#F3F4F8]',
219
- 'p-2',
220
- 'gap-2',
221
- )}
222
- >
223
- <div className="flex flex-1 items-center">
224
- <img
225
- src={
226
- !window.settings.algolia.enabled
227
- ? part['image']
228
- : part['image(main_similarity)']
229
- ? part['image(main_similarity)']
230
- : part['main_image_link']
231
- }
232
- key={part.sku + i}
233
- alt={`product ${i}`}
234
- className="mr-2 h-[50px] w-[50px] bg-white object-contain"
235
- />
236
- <div>
271
+ {cartParts.map((part: any, i: number) => {
272
+ const imageSrc = !window.settings.algolia.enabled
273
+ ? part['image']
274
+ : part['image(main_similarity)'] ||
275
+ part['main_image_link'];
276
+
277
+ return (
278
+ <div
279
+ key={part.sku + i}
280
+ className={twMerge(
281
+ 'flex h-fit w-full items-center justify-between rounded-lg border border-solid border-[#DDDEE7] bg-[#F3F4F8]',
282
+ 'p-2',
283
+ 'gap-2',
284
+ )}
285
+ >
286
+ <div className="flex flex-1 items-center">
287
+ {!imageSrc ? (
288
+ <img
289
+ src={NoImage}
290
+ alt="image_item"
291
+ className="mr-2 h-[50px] w-[50px] object-contain"
292
+ />
293
+ ) : (
294
+ <img
295
+ src={imageSrc}
296
+ key={part.sku + i}
297
+ alt={`product ${i}`}
298
+ className="mr-2 h-[50px] w-[50px] bg-white object-contain"
299
+ />
300
+ )}
301
+
237
302
  <div>
238
- <div className="max-w-[175px] break-words text-[9px] font-normal">
239
- {part.sku}
240
- </div>
241
- <div className="max-w-[175px] break-words text-xs font-semibold">
242
- {part.title}
303
+ <div>
304
+ <div className="max-w-[175px] break-words text-[9px] font-normal">
305
+ {part.sku}
306
+ </div>
307
+ <div className="max-w-[175px] break-words text-xs font-semibold">
308
+ {part.title}
309
+ </div>
243
310
  </div>
244
311
  </div>
245
312
  </div>
246
- </div>
247
- <div className="flex items-center">
248
- <div className="ml-auto flex items-center gap-4 pr-4">
249
- <button
250
- className="h-4 w-4 rounded-full bg-[#FAFAFA]"
251
- onClick={() => updateQuantity(part.sku, -1)}
252
- >
253
- <Icon name="minus" width="16" height="16" />
254
- </button>
255
- <input
256
- className="flex h-[22px] w-[22px] items-center justify-center rounded border border-solid border-[#DDDEE7] bg-white text-center"
257
- value={getQuantityDisplayValue(
258
- part.sku,
259
- part.quantity,
260
- )}
261
- onChange={e => {
262
- const rawValue = e.target.value;
263
- if (rawValue === '') {
264
- setQuantityEdits(prev => ({
265
- ...prev,
266
- [part.sku]: '',
267
- }));
268
- return;
269
- }
313
+ <div className="flex items-center">
314
+ <div className="ml-auto flex items-center gap-4 pr-4">
315
+ <button
316
+ className="h-4 w-4 rounded-full bg-[#FAFAFA]"
317
+ onClick={() => updateQuantity(part.sku, -1)}
318
+ >
319
+ <Icon name="minus" width="16" height="16" />
320
+ </button>
321
+ <input
322
+ className="flex h-[22px] w-[22px] items-center justify-center rounded border border-solid border-[#DDDEE7] bg-white text-center"
323
+ value={getQuantityDisplayValue(
324
+ part.sku,
325
+ part.quantity,
326
+ )}
327
+ onChange={e => {
328
+ const rawValue = e.target.value;
329
+ if (rawValue === '') {
330
+ setQuantityEdits(prev => ({
331
+ ...prev,
332
+ [part.sku]: '',
333
+ }));
334
+ return;
335
+ }
270
336
 
271
- if (!/^[0-9]+$/.test(rawValue)) return;
337
+ if (!/^[0-9]+$/.test(rawValue)) return;
272
338
 
273
- const newValue = Number(rawValue);
274
- if (newValue < 1) return;
339
+ const newValue = Number(rawValue);
340
+ if (newValue < 1) return;
275
341
 
276
- setQuantityEdits(prev => ({
277
- ...prev,
278
- [part.sku]: rawValue,
279
- }));
280
- updateQuantity(
281
- part.sku,
282
- newValue - (part.quantity || 0),
283
- );
284
- }}
285
- onBlur={() => {
286
- if (quantityEdits[part.sku] === '') {
287
- setQuantityEdits(prev => {
288
- const updated = { ...prev };
289
- delete updated[part.sku];
290
- return updated;
291
- });
292
- }
293
- }}
294
- />
342
+ setQuantityEdits(prev => ({
343
+ ...prev,
344
+ [part.sku]: rawValue,
345
+ }));
346
+ updateQuantity(
347
+ part.sku,
348
+ newValue - (part.quantity || 0),
349
+ );
350
+ }}
351
+ onBlur={() => {
352
+ if (quantityEdits[part.sku] === '') {
353
+ setQuantityEdits(prev => {
354
+ const updated = { ...prev };
355
+ delete updated[part.sku];
356
+ return updated;
357
+ });
358
+ }
359
+ }}
360
+ />
361
+ <button
362
+ className="h-4 w-4 rounded-full bg-[#FAFAFA] flex items-center justify-center"
363
+ onClick={() => updateQuantity(part.sku, 1)}
364
+ >
365
+ <Icon name="plus" width="10" height="10" />
366
+ </button>
367
+ </div>
295
368
  <button
296
- className="h-4 w-4 rounded-full bg-[#FAFAFA] flex items-center justify-center"
297
- onClick={() => updateQuantity(part.sku, 1)}
369
+ className="h-7 w-7 rounded bg-[#FFE5EF4D] p-1.5"
370
+ onClick={() => removePartBySku(part.sku)}
298
371
  >
299
- <Icon name="plus" width="10" height="10" />
372
+ <Icon name="trash" color="#E31B5D" />
300
373
  </button>
301
374
  </div>
302
- <button
303
- className="h-7 w-7 rounded bg-[#FFE5EF4D] p-1.5"
304
- onClick={() => removePartBySku(part.sku)}
305
- >
306
- <Icon name="trash" color="#E31B5D" />
307
- </button>
308
375
  </div>
309
- </div>
310
- ))}
376
+ );
377
+ })}
311
378
  {amountOfParts > 0 ? (
312
379
  <button
313
380
  className="flex h-12 w-full items-center justify-center rounded-lg bg-[#3E36DC] text-base font-semibold text-white"
@@ -320,6 +387,54 @@ const Cart = () => {
320
387
  ) : (
321
388
  ''
322
389
  )}
390
+ {amountOfParts > 0 &&
391
+ window.settings.cart?.emailTemplateId ? (
392
+ auth0Email ? (
393
+ <button
394
+ className="mt-2 flex h-12 w-full items-center justify-center gap-2 rounded-lg border border-solid border-[#3E36DC] bg-white text-base font-semibold text-[#3E36DC] disabled:cursor-not-allowed disabled:opacity-60"
395
+ type="button"
396
+ onClick={() => sendCSVByEmail(undefined, auth0Email)}
397
+ disabled={isSendingEmail}
398
+ >
399
+ <Icon name="email" />
400
+ Send by email
401
+ </button>
402
+ ) : isEmailInputVisible ? (
403
+ <form
404
+ className="cart-email-form flex items-center relative mt-2 h-12 w-full"
405
+ onSubmit={sendCSVByEmail}
406
+ >
407
+ <input
408
+ type="email"
409
+ className="w-full h-full px-4 border border-[#CACAD1] rounded-lg pr-12 focus:outline-none focus:border-[#3e36dc]"
410
+ placeholder="Enter your email"
411
+ value={userEmail}
412
+ onChange={e => setUserEmail(e.target.value)}
413
+ disabled={isSendingEmail}
414
+ autoFocus
415
+ />
416
+ <button
417
+ type="submit"
418
+ className="absolute right-2 h-8 w-8 flex items-center justify-center rounded disabled:opacity-50"
419
+ disabled={isSendingEmail || !userEmail}
420
+ >
421
+ <Send className="w-5 h-5 " />
422
+ </button>
423
+ </form>
424
+ ) : (
425
+ <button
426
+ className="mt-2 flex h-12 w-full items-center justify-center gap-2 rounded-lg border border-solid border-[#3E36DC] bg-white text-base font-semibold text-[#3E36DC] disabled:cursor-not-allowed disabled:opacity-60"
427
+ type="button"
428
+ onClick={() => setIsEmailInputVisible(true)}
429
+ disabled={isSendingEmail}
430
+ >
431
+ <Icon name="email" />
432
+ Send by email
433
+ </button>
434
+ )
435
+ ) : (
436
+ ''
437
+ )}
323
438
  </div>
324
439
  </div>
325
440
  </div>
@@ -164,7 +164,7 @@ function Header() {
164
164
  </div>
165
165
  </>
166
166
  )}
167
- {window.settings.cart && <Cart />}
167
+ {window.settings.cart?.enabled && <Cart />}
168
168
  </div>
169
169
 
170
170
  <LogoutModal
@@ -66,10 +66,10 @@ export default function InquiryModal({
66
66
  useEffect(() => {
67
67
  function omitKeys(obj: any, keys: string[]) {
68
68
  return Object.fromEntries(
69
- Object.entries(obj).filter(([key]) => !keys.includes(key)),
69
+ Object.entries(obj)?.filter(([key]) => !keys.includes(key)),
70
70
  );
71
71
  }
72
- const omittedSpecification = omitKeys(specifications, [
72
+ const omittedSpecification = omitKeys(specifications || {}, [
73
73
  'is_nameplate',
74
74
  'prefilter_value',
75
75
  ]);
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useEffect, useMemo, useState } from 'react';
2
2
  import {
3
3
  AccordionContent,
4
4
  AccordionItem,
@@ -7,6 +7,7 @@ import {
7
7
  import { useRefinementList } from 'react-instantsearch';
8
8
  import { Icon } from '@nyris/nyris-react-components';
9
9
  import { useTranslation } from 'react-i18next';
10
+ import useScopeStore from 'stores/scope/scopeStore';
10
11
 
11
12
  function PostFilter({
12
13
  attribute,
@@ -23,7 +24,11 @@ function PostFilter({
23
24
  }) {
24
25
  const [itemsLimit, setItemsLimit] = useState(10);
25
26
  const [searchInput, setSearchInput] = useState<string>('');
26
- const { items, refine, searchForItems } = useRefinementList({
27
+ const {
28
+ items: allItems,
29
+ refine,
30
+ searchForItems,
31
+ } = useRefinementList({
27
32
  attribute: attribute,
28
33
  operator: 'or',
29
34
  showMore: false,
@@ -32,6 +37,32 @@ function PostFilter({
32
37
 
33
38
  const { t } = useTranslation();
34
39
 
40
+ const scopeFilterEnabled = window.settings.scopeFilter;
41
+ const scopeFilterAttributes = useScopeStore(state => state.filterAttributes);
42
+
43
+ const scopedAllowedValues = useMemo(() => {
44
+ if (!scopeFilterEnabled || !window.settings.auth0.enabled) {
45
+ return null;
46
+ }
47
+ // todo: support multiple attributes in the future
48
+ const scopeAttribute = scopeFilterAttributes[0];
49
+ if (!scopeAttribute) {
50
+ return new Set<string>();
51
+ }
52
+ return new Set(
53
+ scopeAttribute.values.map(value => value.toLocaleLowerCase()),
54
+ );
55
+ }, [scopeFilterEnabled, scopeFilterAttributes]);
56
+
57
+ const items = useMemo(() => {
58
+ if (!scopedAllowedValues) {
59
+ return allItems;
60
+ }
61
+ return allItems.filter(item =>
62
+ scopedAllowedValues.has(item.value.toLocaleLowerCase()),
63
+ );
64
+ }, [allItems, scopedAllowedValues]);
65
+
35
66
  useEffect(() => {
36
67
  searchForItems(searchInput);
37
68
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -39,7 +70,9 @@ function PostFilter({
39
70
 
40
71
  const hasSelection = items.some(item => item.isRefined);
41
72
  const hasAvailableValues = items.length > 0 || searchInput.length > 0;
42
- const isVisible = hasAvailableValues || hasSelection;
73
+ const isVisible =
74
+ (!scopedAllowedValues || scopedAllowedValues.size > 0) &&
75
+ (hasAvailableValues || hasSelection);
43
76
 
44
77
  useEffect(() => {
45
78
  onVisibilityChange?.(attribute, isVisible);
@@ -10,7 +10,9 @@ import { isEmpty } from 'lodash';
10
10
  import useResultStore from 'stores/result/resultStore';
11
11
  import useRequestStore from 'stores/request/requestStore';
12
12
  import useUiStore from 'stores/ui/uiStore';
13
+ import useScopeStore from 'stores/scope/scopeStore';
13
14
  import { find } from 'services/visualSearch';
15
+ import { useAuth0 } from '@auth0/auth0-react';
14
16
 
15
17
  let postFilterRequestCounter = 0;
16
18
 
@@ -31,6 +33,8 @@ function PostFilterFindApi({
31
33
  onVisibilityChange?: (attribute: string, isVisible: boolean) => void;
32
34
  isLastVisible?: boolean;
33
35
  }) {
36
+ const { user } = useAuth0();
37
+
34
38
  const [itemsLimit, setItemsLimit] = useState(10);
35
39
  const [searchInput, setSearchInput] = useState<string>('');
36
40
 
@@ -52,6 +56,23 @@ function PostFilterFindApi({
52
56
  const { t } = useTranslation();
53
57
  const settings = window.settings;
54
58
 
59
+ const scopeFilterEnabled = settings.scopeFilter;
60
+ const scopeFilterAttributes = useScopeStore(state => state.filterAttributes);
61
+
62
+ const scopedAllowedValues = useMemo(() => {
63
+ if (!scopeFilterEnabled || !window.settings.auth0?.enabled) {
64
+ return null;
65
+ }
66
+ // todo: support multiple attributes in the future
67
+ const scopeAttribute = scopeFilterAttributes[0];
68
+ if (!scopeAttribute) {
69
+ return new Set<string>();
70
+ }
71
+ return new Set(
72
+ scopeAttribute.values.map(value => value.toLocaleLowerCase()),
73
+ );
74
+ }, [scopeFilterEnabled, scopeFilterAttributes]);
75
+
55
76
  const matchedFacet = useMemo(() => {
56
77
  const facets = Array.isArray(postFilterFacets) ? postFilterFacets : [];
57
78
  return facets.find(
@@ -96,6 +117,11 @@ function PostFilterFindApi({
96
117
  count: valueCounts[label] || 0,
97
118
  isRefined: selectedValues.includes(label),
98
119
  }))
120
+ .filter(
121
+ item =>
122
+ !scopedAllowedValues ||
123
+ scopedAllowedValues.has(item.value.toLocaleLowerCase()),
124
+ )
99
125
  .filter(
100
126
  item =>
101
127
  !searchInput ||
@@ -103,13 +129,22 @@ function PostFilterFindApi({
103
129
  )
104
130
  .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label))
105
131
  .slice(0, itemsLimit);
106
- }, [attribute, itemsLimit, postFilterSelections, searchInput, valueCounts]);
132
+ }, [
133
+ attribute,
134
+ itemsLimit,
135
+ postFilterSelections,
136
+ scopedAllowedValues,
137
+ searchInput,
138
+ valueCounts,
139
+ ]);
107
140
 
108
141
  const selectedValues = postFilterSelections?.[attribute] || [];
109
142
  const hasSelection = selectedValues.length > 0;
110
143
  const hasAvailableValues = Object.keys(valueCounts).length > 0;
111
144
  const hasSearch = searchable && searchInput.length > 0;
112
- const isVisible = hasAvailableValues || hasSelection || hasSearch;
145
+ const isVisible =
146
+ (!scopedAllowedValues || scopedAllowedValues.size > 0) &&
147
+ (hasAvailableValues || hasSelection || hasSearch);
113
148
 
114
149
  useEffect(() => {
115
150
  onVisibilityChange?.(attribute, isVisible);
@@ -180,6 +215,7 @@ function PostFilterFindApi({
180
215
  region: regions[0],
181
216
  filters,
182
217
  text: !requestImages[0] ? query : undefined,
218
+ scopeIds: window.settings?.scopeFilter ? user?.email : undefined,
183
219
  })
184
220
  .then(response => {
185
221
  if (requestId !== postFilterRequestCounter) {
@@ -264,7 +300,9 @@ function PostFilterFindApi({
264
300
  <input
265
301
  type="checkbox"
266
302
  checked={item.isRefined}
267
- onChange={() => handleFilterClick(item.value, item.isRefined)}
303
+ onChange={() =>
304
+ handleFilterClick(item.value, item.isRefined)
305
+ }
268
306
  className="cursor-pointer"
269
307
  />
270
308
  <span className="text-xs font-normal pl-2 pr-1">
@@ -10,6 +10,7 @@ import { truncateString } from 'utils/truncateString';
10
10
  import { twMerge } from 'tailwind-merge';
11
11
  import Tooltip from 'components/Tooltip/TooltipComponent';
12
12
  import useResultStore from '../../stores/result/resultStore';
13
+ import useScopeStore from 'stores/scope/scopeStore';
13
14
 
14
15
  interface Props {
15
16
  handleClose?: any;
@@ -42,6 +43,9 @@ const PreFilterComponent = (props: Props) => {
42
43
  const setImageAnalysis = useResultStore(state => state.setImageAnalysis);
43
44
  const regions = useRequestStore(state => state.regions);
44
45
 
46
+ const scopeFilterEnabled = !!settings.scopeFilter;
47
+ const scopeFilterAttributes = useScopeStore(state => state.filterAttributes);
48
+
45
49
  const [keyFilter, setKeyFilter] = useState<Record<string, boolean>>(
46
50
  keyFilterState || {},
47
51
  );
@@ -66,7 +70,7 @@ const PreFilterComponent = (props: Props) => {
66
70
  setLoading(true);
67
71
  getDataFilterDesktop();
68
72
  // eslint-disable-next-line react-hooks/exhaustive-deps
69
- }, [preFilterList, searchKey]);
73
+ }, [preFilterList, scopeFilterAttributes, searchKey]);
70
74
 
71
75
  useEffect(() => {
72
76
  return () => {
@@ -76,7 +80,11 @@ const PreFilterComponent = (props: Props) => {
76
80
  };
77
81
  }, []);
78
82
 
79
- const matchedPreFilter = preFilterList.find(
83
+ const activeFilterList = scopeFilterEnabled
84
+ ? scopeFilterAttributes
85
+ : preFilterList;
86
+
87
+ const matchedPreFilter = activeFilterList.find(
80
88
  value =>
81
89
  value.key?.toLocaleLowerCase() ===
82
90
  settings.visualSearchFilterKey?.toLocaleLowerCase(),
@@ -105,6 +113,26 @@ const PreFilterComponent = (props: Props) => {
105
113
  return;
106
114
  }
107
115
  setLoading(true);
116
+
117
+ if (scopeFilterEnabled) {
118
+ const arrResult = matchedPreFilter?.values || [];
119
+ const lowerValue = String(value).toLocaleLowerCase();
120
+ const res = arrResult.filter(item =>
121
+ String(item).toLocaleLowerCase().includes(lowerValue),
122
+ );
123
+ if (res.length > 0) {
124
+ setResultFilter({ [res[0][0].toLocaleUpperCase()]: res });
125
+ if (res.length <= 20) setColumns(1);
126
+ else if (res.length <= 40) setColumns(2);
127
+ else setColumns(4);
128
+ } else {
129
+ setResultFilter({});
130
+ setColumns(4);
131
+ }
132
+ setLoading(false);
133
+ return;
134
+ }
135
+
108
136
  const data = await searchFilters(
109
137
  preFilterApiKey,
110
138
  encodeURIComponent(value),
@@ -673,7 +673,7 @@ function Product(props: Props) {
673
673
  </div>
674
674
  )}
675
675
 
676
- {settings.cart && (
676
+ {settings.cart?.enabled && (
677
677
  <div
678
678
  style={{
679
679
  boxShadow: '-2px 2px 4px rgba(170, 171, 181, 0.5)',
@@ -717,7 +717,7 @@ function Product(props: Props) {
717
717
  paddingRight: '8px',
718
718
  }}
719
719
  >
720
- Add to cart
720
+ {settings.cart?.buttonText || 'Add to cart'}
721
721
  </div>
722
722
  <div style={{ width: '16px' }}>
723
723
  <Icon
@@ -456,7 +456,7 @@ function ProductDetailView(props: Props) {
456
456
  </div>
457
457
  )}
458
458
  </div>
459
- {settings.cart && (
459
+ {settings.cart?.enabled && (
460
460
  <div
461
461
  className="bg-[#2B2C46] rounded mt-2 flex justify-between items-center h-8 desktop:h-12 w-full btn-detail-item"
462
462
  style={{
@@ -472,7 +472,7 @@ function ProductDetailView(props: Props) {
472
472
  }}
473
473
  >
474
474
  <div className="text-f16 fw-600 max-line-1 text-white pr-1.5">
475
- Add to cart
475
+ {settings.cart?.buttonText || 'Add to cart'}
476
476
  </div>
477
477
  <div style={{ width: '16px' }}>
478
478
  <Icon