@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
@@ -0,0 +1,50 @@
1
+ const ScopeMissing = () => {
2
+ const { supportEmail } = window.settings.auth0;
3
+
4
+ return (
5
+ <div className="pl-8 desktop:pl-16 pt-6 desktop:pt-8 flex flex-col gap-4 bg-white h-full">
6
+ <div
7
+ style={{
8
+ fontSize: '20px',
9
+ fontWeight: 'bold',
10
+ color: '#2B2C46',
11
+ }}
12
+ >
13
+ Access Denied
14
+ </div>
15
+
16
+ <div
17
+ style={{
18
+ width: '200px',
19
+ height: '4px',
20
+ borderRadius: '4px',
21
+ }}
22
+ className="bg-red-500"
23
+ />
24
+
25
+ <p style={{ color: '#2B2C46', fontSize: '13px' }}>
26
+ You do not have access to the {window.settings.visualSearchFilterKey}.
27
+ If you believe this is an error, please contact support.
28
+ </p>
29
+ <a
30
+ className="contact-support"
31
+ style={{
32
+ backgroundColor: '#2B2C46',
33
+ padding: '8px 16px 8px 16px',
34
+ color: '#fff',
35
+ border: 'none',
36
+ cursor: 'pointer',
37
+ fontSize: '14px',
38
+ width: 'fit-content',
39
+ }}
40
+ href={`mailto:${
41
+ supportEmail || 'support@nyris.io'
42
+ }?subject=Access Denied&body=`}
43
+ >
44
+ Contact Support
45
+ </a>
46
+ </div>
47
+ );
48
+ };
49
+
50
+ export default ScopeMissing;
@@ -1,5 +1,4 @@
1
1
  import React, { useMemo } from 'react';
2
- import ItemSpecification from './ItemSpecification';
3
2
  import { Icon } from '@nyris/nyris-react-components';
4
3
  import useRequestStore from 'stores/request/requestStore';
5
4
  import useResultStore from 'stores/result/resultStore';
@@ -19,8 +18,12 @@ function SmartFilter({
19
18
  const isSmartFiltersEnabled = !!settings.useSmartFilters;
20
19
  const smartFilters = useResultStore(state => state.smartFilters);
21
20
  const setSmartFilters = useResultStore(state => state.setSmartFilters);
22
- const productsFromFindApi = useResultStore(state => state.productsFromFindApi);
23
- const productsFromAlgolia = useResultStore(state => state.productsFromAlgolia);
21
+ const productsFromFindApi = useResultStore(
22
+ state => state.productsFromFindApi,
23
+ );
24
+ const productsFromAlgolia = useResultStore(
25
+ state => state.productsFromAlgolia,
26
+ );
24
27
  const firstSearchResults = useResultStore(state => state.firstSearchResults);
25
28
  const setFindApiProducts = useResultStore(state => state.setFindApiProducts);
26
29
  const setAlgoliaProducts = useResultStore(state => state.setAlgoliaProducts);
@@ -194,7 +197,10 @@ function SmartFilter({
194
197
  return;
195
198
  }
196
199
 
197
- if (rerankedSkus.length === 0 || !applySkuOrderingToCurrentResults(rerankedSkus)) {
200
+ if (
201
+ rerankedSkus.length === 0 ||
202
+ !applySkuOrderingToCurrentResults(rerankedSkus)
203
+ ) {
198
204
  setSmartFilters({
199
205
  ...smartFilters,
200
206
  selectedFilterKey: filter.key,
@@ -214,7 +220,9 @@ function SmartFilter({
214
220
  errorMessage: null,
215
221
  });
216
222
  } catch (error: any) {
217
- const timeout = String(error?.message || '').includes('SMART_FILTER_TIMEOUT');
223
+ const timeout = String(error?.message || '').includes(
224
+ 'SMART_FILTER_TIMEOUT',
225
+ );
218
226
  setSmartFilters({
219
227
  ...smartFilters,
220
228
  selectedFilterKey: filter.key,
@@ -241,25 +249,7 @@ function SmartFilter({
241
249
  </div>
242
250
  <Icon name="smart_filter_indication" className="mt-1" />
243
251
  </div>
244
- <div className={`flex flex-wrap mx-4 gap-2 ${imageAnalysis?.specification ? 'mt-4' : ''}`}>
245
- {Object.keys(imageAnalysis?.specification || {})
246
- .filter(key => key !== 'is_nameplate' && key !== 'prefilter_value')
247
- .map(key => {
248
- const value = imageAnalysis?.specification[key];
249
- if (!value) {
250
- return null;
251
- }
252
- return (
253
- <ItemSpecification
254
- key={key}
255
- attr={key}
256
- value={value}
257
- specificationFilter={specificationFilter}
258
- imageAnalysis={imageAnalysis}
259
- />
260
- );
261
- })}
262
- </div>
252
+
263
253
  {smartFilters.status === 'loading' && (
264
254
  <div className="ml-[30px] mt-3 text-xs text-[#3E36DC]">
265
255
  Loading smart filters...
@@ -270,11 +260,12 @@ function SmartFilter({
270
260
  {smartFilters.errorMessage || 'Could not generate smart filters.'}
271
261
  </div>
272
262
  )}
273
- {smartFilters.status === 'success' && smartFilters.filters.length === 0 && (
274
- <div className="ml-[30px] mt-3 text-xs text-[#767A9F]">
275
- No smart filters available for this image.
276
- </div>
277
- )}
263
+ {smartFilters.status === 'success' &&
264
+ smartFilters.filters.length === 0 && (
265
+ <div className="ml-[30px] mt-3 text-xs text-[#767A9F]">
266
+ No smart filters available for this image.
267
+ </div>
268
+ )}
278
269
  <div
279
270
  className={
280
271
  variant === 'results'
@@ -298,47 +289,46 @@ function SmartFilter({
298
289
  ? 'flex flex-row flex-nowrap gap-2 pb-1'
299
290
  : 'flex flex-row flex-wrap gap-2 pb-1'
300
291
  }
301
- style={variant === 'results' ? { width: 'max-content' } : { width: 'auto' }}
292
+ style={
293
+ variant === 'results' ? { width: 'max-content' } : { width: 'auto' }
294
+ }
302
295
  >
303
- {smartFilters.filters.map((filter, index) => {
304
- const isSelected =
305
- smartFilters.selectedFilterKey === filter.key &&
306
- smartFilters.selectedFilterValue === filter.value;
307
- const hasSelection =
308
- !!smartFilters.selectedFilterKey &&
309
- !!smartFilters.selectedFilterValue;
310
- const isRerankLoading = smartFilters.rerankStatus === 'loading';
311
- const isDisabled = !isSelected && (hasSelection || isRerankLoading);
312
- return (
313
- <button
314
- key={`${filter.key}-${filter.value}-${index}`}
315
- type="button"
316
- disabled={isDisabled}
317
- aria-pressed={isSelected}
318
- style={variant === 'results' ? { flex: '0 0 auto' } : undefined}
319
- className={`px-3.5 py-2 rounded-lg border text-sm inline-flex items-center gap-1.5 ${
320
- isSelected
321
- ? 'bg-[#545987] text-white border-[#545987]'
322
- : 'bg-[#E7E8F1] text-[#767A9F] border-transparent'
323
- } ${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
324
- onClick={() => {
325
- if (isRerankLoading) return;
326
- void onFilterClick({
327
- key: filter.key,
328
- value: filter.value,
329
- });
330
- }}
331
- >
332
- <span>{filter.value}</span>
333
- {isSelected && (
334
- <Icon
335
- name="close"
336
- className="w-3 h-3 text-white"
337
- />
338
- )}
339
- </button>
340
- );
341
- })}
296
+ {smartFilters.filters.map((filter, index) => {
297
+ const isSelected =
298
+ smartFilters.selectedFilterKey === filter.key &&
299
+ smartFilters.selectedFilterValue === filter.value;
300
+ const hasSelection =
301
+ !!smartFilters.selectedFilterKey &&
302
+ !!smartFilters.selectedFilterValue;
303
+ const isRerankLoading = smartFilters.rerankStatus === 'loading';
304
+ const isDisabled = !isSelected && (hasSelection || isRerankLoading);
305
+ return (
306
+ <button
307
+ key={`${filter.key}-${filter.value}-${index}`}
308
+ type="button"
309
+ disabled={isDisabled}
310
+ aria-pressed={isSelected}
311
+ style={variant === 'results' ? { flex: '0 0 auto' } : undefined}
312
+ className={`px-3.5 py-2 rounded-lg border text-sm inline-flex items-center gap-1.5 ${
313
+ isSelected
314
+ ? 'bg-[#545987] text-white border-[#545987]'
315
+ : 'bg-[#E7E8F1] text-[#767A9F] border-transparent'
316
+ } ${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
317
+ onClick={() => {
318
+ if (isRerankLoading) return;
319
+ void onFilterClick({
320
+ key: filter.key,
321
+ value: filter.value,
322
+ });
323
+ }}
324
+ >
325
+ <span>{filter.value}</span>
326
+ {isSelected && (
327
+ <Icon name="close" className="w-3 h-3 text-white" />
328
+ )}
329
+ </button>
330
+ );
331
+ })}
342
332
  </div>
343
333
  </div>
344
334
  </div>
@@ -204,7 +204,9 @@ function TextSearch({
204
204
  !isEmpty(preFilter)
205
205
  ? 'fill-theme-primary'
206
206
  : 'fill-[#2B2C46]',
207
- `desktop:fill-[#3B3E5F]`,
207
+ !isEmpty(preFilter)
208
+ ? 'desktop:fill-white'
209
+ : 'desktop:fill-[#2B2C46]',
208
210
  )}
209
211
  />
210
212
  </div>
@@ -20,8 +20,11 @@ import { useGroundedSearch } from './useGroundedSearch';
20
20
  import { generateSmartFilters } from '../services/vizo';
21
21
  import { cropImage } from 'utils/cropImageToBase64';
22
22
  import { invalidatePostFilterRequests } from 'components/PostFilter/PostFilterFindApi';
23
+ import { useAuth0 } from '@auth0/auth0-react';
23
24
 
24
25
  export const useImageSearch = () => {
26
+ const { user } = useAuth0();
27
+
25
28
  const setRegions = useRequestStore(state => state.setRegions);
26
29
  const query = useRequestStore(state => state.query);
27
30
 
@@ -69,7 +72,9 @@ export const useImageSearch = () => {
69
72
  state => state.setGroundingFilterResult,
70
73
  );
71
74
  const setSmartFilters = useResultStore(state => state.setSmartFilters);
72
- const setPostFilterFacets = useResultStore(state => state.setPostFilterFacets);
75
+ const setPostFilterFacets = useResultStore(
76
+ state => state.setPostFilterFacets,
77
+ );
73
78
  const setInitialFindSearchSnapshot = useResultStore(
74
79
  state => state.setInitialFindSearchSnapshot,
75
80
  );
@@ -208,6 +213,7 @@ export const useImageSearch = () => {
208
213
  ];
209
214
 
210
215
  let smartFilterPromise: Promise<void> | null = null;
216
+
211
217
  if (isSmartFiltersEnabled && requestImage && (newSearch || imageRegion)) {
212
218
  setSmartFilters({
213
219
  status: 'loading',
@@ -324,6 +330,7 @@ export const useImageSearch = () => {
324
330
  ? query
325
331
  : text
326
332
  : undefined,
333
+ scopeIds: window.settings?.scopeFilter ? user?.email : undefined,
327
334
  });
328
335
  useResultStore.getState().resetGrounding();
329
336
 
@@ -421,6 +428,7 @@ export const useImageSearch = () => {
421
428
  setSmartFilters,
422
429
  setPostFilterFacets,
423
430
  setInitialFindSearchSnapshot,
431
+ user,
424
432
  ],
425
433
  );
426
434
 
@@ -339,14 +339,14 @@ Evaluate if these results match the image. If they do (same type of product), ro
339
339
 
340
340
  // Enforce ≤5 rule: if initial results are ≤5, strip filter options, keep only text_search_prompt
341
341
  if (activeProducts.length <= 5 && activeProducts.length > 0) {
342
- mappedOptions = [
343
- {
344
- label: "Describe what you're looking for",
345
- action_type: 'text_search_prompt' as const,
346
- key: 'text_search',
347
- value: 'text_search',
348
- },
349
- ];
342
+ // mappedOptions = [
343
+ // {
344
+ // label: "Describe what you're looking for",
345
+ // action_type: 'text_search_prompt' as const,
346
+ // key: 'text_search',
347
+ // value: 'text_search',
348
+ // },
349
+ // ];
350
350
 
351
351
  if (
352
352
  window.settings.vizo?.findRelatedParts &&
@@ -22,6 +22,7 @@ import { getFilters } from 'services/filter';
22
22
  import { useUploadSearch } from 'hooks/useUploadSearch';
23
23
  import { ToastHelper } from 'helpers/ToastHelper';
24
24
  import { useTranslation } from 'react-i18next';
25
+ import useScopeStore from 'stores/scope/scopeStore';
25
26
 
26
27
  i18n.use(initReactI18next).init({
27
28
  resources: translations,
@@ -49,6 +50,7 @@ function AppLayout(): JSX.Element {
49
50
 
50
51
  const algoliaFilter = useRequestStore(state => state.algoliaFilter);
51
52
  const metaFilter = useRequestStore(state => state.metaFilter);
53
+ const scopeItemKeys = useScopeStore(state => state.filterAttributes);
52
54
 
53
55
  const setAlgoliaProducts = useResultStore(state => state.setAlgoliaProducts);
54
56
  const setIsAlgoliaLoading = useUiStore(state => state.setIsAlgoliaLoading);
@@ -88,7 +90,7 @@ function AppLayout(): JSX.Element {
88
90
  useEffect(() => {
89
91
  setAlgoliaProducts(items);
90
92
  // eslint-disable-next-line react-hooks/exhaustive-deps
91
- }, [items]);
93
+ }, [items, scopeItemKeys]);
92
94
 
93
95
  useEffect(() => {
94
96
  if (status === 'loading' || status === 'stalled') {
@@ -145,10 +147,10 @@ function AppLayout(): JSX.Element {
145
147
  }, [settings, setPreFilterList, setPreFilterLoading]);
146
148
 
147
149
  useEffect(() => {
148
- if (showPreFilter) {
150
+ if (showPreFilter && !settings.scopeFilter) {
149
151
  getPreFilters();
150
152
  }
151
- }, [getPreFilters, showPreFilter]);
153
+ }, [getPreFilters, showPreFilter, settings.scopeFilter]);
152
154
 
153
155
  const getImageFromClipboard = useCallback(
154
156
  (event: ClipboardEvent): File | null => {
@@ -213,6 +215,25 @@ function AppLayout(): JSX.Element {
213
215
  };
214
216
  }, [handleClipboardPaste, showLayout]);
215
217
 
218
+ const applyScopeFilter = (filters: string | undefined) => {
219
+ const scopeFilterClause =
220
+ window.settings.scopeFilter && scopeItemKeys.length
221
+ ? `(${scopeItemKeys[0].values.map(key => `${window.settings.alogoliaFilterField}:'${key}'`).join(' OR ')})`
222
+ : '';
223
+
224
+ if (!window.settings.scopeFilter || !window.settings.auth0?.enabled) {
225
+ return filters;
226
+ }
227
+
228
+ if (!scopeFilterClause) {
229
+ return `sku:NOT_FOUND`;
230
+ }
231
+
232
+ return filters
233
+ ? `${scopeFilterClause} AND (${filters})`
234
+ : scopeFilterClause;
235
+ };
236
+
216
237
  if (!showLayout) {
217
238
  return <Outlet />;
218
239
  }
@@ -226,7 +247,7 @@ function AppLayout(): JSX.Element {
226
247
  {window.settings?.algolia.enabled && (
227
248
  <Configure
228
249
  query={query}
229
- filters={
250
+ filters={applyScopeFilter(
230
251
  specifications && !requestImages.length
231
252
  ? `${alogoliaFilterField}:'${specifications.prefilter_value}'`
232
253
  : !query && !algoliaFilter.includes('score=1')
@@ -237,8 +258,8 @@ function AppLayout(): JSX.Element {
237
258
  algoliaFilter ? 'AND ' : ''
238
259
  }${alogoliaFilterField}:'${metaFilter}'`
239
260
  : ''
240
- }`
241
- }
261
+ }`,
262
+ )}
242
263
  // facets={['brand', 'keyword_0']}
243
264
  hitsPerPage={20}
244
265
  />
@@ -322,7 +322,7 @@ function Results() {
322
322
  >
323
323
  {t('Showing results for machine', {
324
324
  prefilter_title:
325
- window.settings.preFilterTitle?.toLocaleLowerCase(),
325
+ window.settings.visualSearchFilterKey,
326
326
  prefilter_value: specifications.prefilter_value,
327
327
  })}
328
328
  </div>
@@ -570,6 +570,7 @@ function Results() {
570
570
  onClose={() => {
571
571
  setOpenModalCamera(s => !s);
572
572
  }}
573
+ newSearch={true}
573
574
  />
574
575
  {isAiModeOpen && (
575
576
  <div className="desktop:hidden">
@@ -1,15 +1,25 @@
1
1
  import { useAuth0 } from '@auth0/auth0-react';
2
2
  import EmailVerification from 'components/EmailVerification';
3
+ import ScopeMissing from 'components/ScopeMissing';
3
4
  import { useEffect, ReactElement } from 'react';
5
+ import { getScopes } from 'services/filter';
6
+ import useScopeStore from 'stores/scope/scopeStore';
4
7
 
5
8
  export default function ProtectedRoute({
6
9
  children,
7
10
  }: {
8
11
  children: ReactElement;
9
12
  }) {
10
- const { auth0 } = window.settings;
13
+ const { auth0, scopeFilter } = window.settings;
11
14
 
12
15
  const { user, isAuthenticated, isLoading, loginWithRedirect } = useAuth0();
16
+ const setScope = useScopeStore(state => state.setScope);
17
+ const filterAttributes = useScopeStore(state => state.filterAttributes);
18
+
19
+ const setScopeLoading = useScopeStore(state => state.setScopeLoading);
20
+ const scopeIsLoading = useScopeStore(state => state.isLoading);
21
+ const hasFetchedScope = useScopeStore(state => state.hasFetched);
22
+ const setScopeFetched = useScopeStore(state => state.setScopeFetched);
13
23
 
14
24
  useEffect(() => {
15
25
  if (!isLoading && !user && !isAuthenticated && auth0.enabled) {
@@ -17,21 +27,54 @@ export default function ProtectedRoute({
17
27
  }
18
28
  }, [loginWithRedirect, isLoading, user, isAuthenticated, auth0]);
19
29
 
30
+ useEffect(() => {
31
+ if (!scopeFilter || !isAuthenticated || !user?.email || hasFetchedScope) {
32
+ return;
33
+ }
34
+ setScopeFetched(true);
35
+ setScopeLoading(true);
36
+ getScopes(user.email, window.settings)
37
+ .then(response => {
38
+ setScope(response.itemKeys || [], response.filterAttributes || []);
39
+ })
40
+ .catch((e: any) => {
41
+ console.log('err getScopes', e);
42
+ })
43
+ .finally(() => {
44
+ setScopeLoading(false);
45
+ });
46
+ }, [
47
+ scopeFilter,
48
+ isAuthenticated,
49
+ user,
50
+ hasFetchedScope,
51
+ setScope,
52
+ setScopeLoading,
53
+ setScopeFetched,
54
+ ]);
55
+
20
56
  if (!auth0.enabled) {
21
57
  return children;
22
58
  }
23
59
 
24
- // if (isLoading) {
25
- // return <div>Loading...</div>;
26
- // }
27
-
28
- if (!isLoading && isAuthenticated && user?.email_verified) {
29
- return children;
60
+ if (isLoading || !isAuthenticated) {
61
+ return <div></div>;
30
62
  }
31
63
 
32
- if (!isLoading && isAuthenticated && !user?.email_verified) {
64
+ if (!user?.email_verified) {
33
65
  return <EmailVerification />;
34
66
  }
35
67
 
36
- return <div></div>;
68
+ if (scopeFilter) {
69
+ // wait for the scope fetch to finish before deciding access, otherwise ScopeMissing flashes early
70
+ if (scopeIsLoading || !hasFetchedScope) {
71
+ return <div></div>;
72
+ }
73
+
74
+ if (filterAttributes.length === 0) {
75
+ return <ScopeMissing />;
76
+ }
77
+ }
78
+
79
+ return children;
37
80
  }
@@ -22,3 +22,16 @@ export const searchFilters = async (
22
22
  const newValue = value ? value : '';
23
23
  return nyrisApi.searchFilters(key, newValue);
24
24
  };
25
+
26
+ export interface ScopeResponse {
27
+ itemKeys: string[];
28
+ filterAttributes: Filter[];
29
+ }
30
+
31
+ export const getScopes = async (
32
+ scopeIds: string,
33
+ settings: NyrisAPISettings,
34
+ ): Promise<ScopeResponse> => {
35
+ const nyrisApi = new NyrisAPI(settings);
36
+ return nyrisApi.getScopes(scopeIds);
37
+ };
@@ -58,12 +58,14 @@ export const find = async ({
58
58
  region,
59
59
  filters,
60
60
  text,
61
+ scopeIds,
61
62
  }: {
62
63
  image?: HTMLCanvasElement;
63
64
  settings: NyrisAPISettings;
64
65
  region?: RectCoords;
65
66
  filters?: Filter[];
66
67
  text?: string;
68
+ scopeIds?: string;
67
69
  }) => {
68
70
  const nyrisApi = new NyrisAPI(settings);
69
71
  let options: ImageSearchOptions = text ? { text } : {};
@@ -82,7 +84,14 @@ export const find = async ({
82
84
  if (region) {
83
85
  options = { ...options, cropRect: region };
84
86
  }
85
- return nyrisApi.find(options, image, filters);
87
+ return nyrisApi.find(
88
+ options,
89
+ image,
90
+ filters,
91
+ undefined,
92
+ undefined,
93
+ scopeIds ? [scopeIds] : [],
94
+ );
86
95
  };
87
96
 
88
97
  export const findCad = ({
@@ -0,0 +1,35 @@
1
+ import { create } from 'zustand';
2
+ import { Filter } from '@nyris/nyris-api';
3
+
4
+ interface ScopeState {
5
+ itemKeys: string[];
6
+ filterAttributes: Filter[];
7
+ isLoading: boolean;
8
+ hasFetched: boolean;
9
+ }
10
+
11
+ interface ScopeAction {
12
+ setScope: (itemKeys: string[], filterAttributes: Filter[]) => void;
13
+ setScopeLoading: (isLoading: boolean) => void;
14
+ setScopeFetched: (hasFetched: boolean) => void;
15
+ resetScope: () => void;
16
+ }
17
+
18
+ const initialState: ScopeState = {
19
+ itemKeys: [],
20
+ filterAttributes: [],
21
+ isLoading: false,
22
+ hasFetched: false,
23
+ };
24
+
25
+ const useScopeStore = create<ScopeState & ScopeAction>()(set => ({
26
+ ...initialState,
27
+
28
+ setScope: (itemKeys, filterAttributes) =>
29
+ set({ itemKeys, filterAttributes, hasFetched: true }),
30
+ setScopeLoading: isLoading => set({ isLoading }),
31
+ setScopeFetched: hasFetched => set({ hasFetched }),
32
+ resetScope: () => set(initialState),
33
+ }));
34
+
35
+ export default useScopeStore;
@@ -205,6 +205,28 @@
205
205
  margin-right: 8px;
206
206
  }
207
207
  }
208
+ .cart-email-button {
209
+ background-color: #fff;
210
+ color: #3e36dc;
211
+ border: 1px solid #3e36dc;
212
+ width: 100%;
213
+ height: 48px;
214
+ display: flex;
215
+ flex-direction: row;
216
+ align-items: center;
217
+ justify-content: center;
218
+ font-weight: 600;
219
+ font-size: 16px;
220
+ border-radius: 8px;
221
+ margin-top: 8px;
222
+ &:disabled {
223
+ opacity: 0.6;
224
+ cursor: not-allowed;
225
+ }
226
+ svg {
227
+ margin-right: 8px;
228
+ }
229
+ }
208
230
  }
209
231
  }
210
232
  }
package/src/types.ts CHANGED
@@ -79,7 +79,11 @@ export interface AppSettings extends NyrisAPISettings {
79
79
  brandName?: string;
80
80
  cadenas?: Cadenas;
81
81
  cadSearch?: boolean;
82
- cart?: boolean;
82
+ cart?: {
83
+ enabled: boolean;
84
+ buttonText: string;
85
+ emailTemplateId: string;
86
+ };
83
87
  clarityId?: string;
84
88
  CTAButton?: CTAButtonSettings;
85
89
  experienceVisualSearch?: boolean;
@@ -110,6 +114,7 @@ export interface AppSettings extends NyrisAPISettings {
110
114
  refinements?: any;
111
115
  regions: boolean;
112
116
  rfq?: Rfq;
117
+ scopeFilter?: boolean;
113
118
  secondaryCTAButton?: SecondaryCTAButton;
114
119
  secondaryCTAButtonText?: string;
115
120
  secondaryTitle: string;