@nyris/nyris-webapp 0.3.95 → 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 (39) 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.b80e9533.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 +157 -65
  15. package/src/components/PreFilter/PreFilter.tsx +39 -5
  16. package/src/components/Product/Product.tsx +2 -2
  17. package/src/components/Product/ProductDetailView.tsx +2 -2
  18. package/src/components/Product/ProductList.tsx +4 -0
  19. package/src/components/ScopeMissing.tsx +50 -0
  20. package/src/components/SmartFilter.tsx +59 -69
  21. package/src/components/TextSearch.tsx +3 -1
  22. package/src/hooks/useEffectiveGroundingResults.ts +6 -1
  23. package/src/hooks/useImageSearch.ts +49 -6
  24. package/src/hooks/useResultEvaluator.ts +8 -8
  25. package/src/layouts/AppLayout.tsx +27 -6
  26. package/src/pages/Result.tsx +2 -1
  27. package/src/routes/ProctedRoute.tsx +52 -9
  28. package/src/services/filter.ts +13 -0
  29. package/src/services/visualSearch.ts +10 -1
  30. package/src/stores/result/prodcuts/products.initialState.ts +2 -0
  31. package/src/stores/result/prodcuts/products.slice.ts +8 -0
  32. package/src/stores/scope/scopeStore.ts +35 -0
  33. package/src/stores/types.ts +9 -0
  34. package/src/styles/Cart.scss +22 -0
  35. package/src/types.ts +6 -1
  36. package/build/static/css/main.5fff4ca0.css +0 -4
  37. package/build/static/css/main.5fff4ca0.css.map +0 -1
  38. package/build/static/js/main.b80e9533.js +0 -3
  39. /package/build/static/js/{main.b80e9533.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.95",
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.95",
9
- "@nyris/nyris-react-components": "^0.3.95",
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);