@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.
- package/build/asset-manifest.json +6 -6
- package/build/index.html +1 -1
- package/build/js/settings.example.js +6 -1
- package/build/static/css/main.7c7f5447.css +4 -0
- package/build/static/css/main.7c7f5447.css.map +1 -0
- package/build/static/js/main.9e7fb38a.js +3 -0
- package/build/static/js/{main.b80e9533.js.map → main.9e7fb38a.js.map} +1 -1
- package/package.json +3 -3
- package/public/js/settings.example.js +6 -1
- package/src/components/Cart.tsx +200 -85
- package/src/components/Header.tsx +1 -1
- package/src/components/Inquiry/InquiryModal.tsx +2 -2
- package/src/components/PostFilter/PostFilter.tsx +36 -3
- package/src/components/PostFilter/PostFilterFindApi.tsx +157 -65
- package/src/components/PreFilter/PreFilter.tsx +39 -5
- package/src/components/Product/Product.tsx +2 -2
- package/src/components/Product/ProductDetailView.tsx +2 -2
- package/src/components/Product/ProductList.tsx +4 -0
- package/src/components/ScopeMissing.tsx +50 -0
- package/src/components/SmartFilter.tsx +59 -69
- package/src/components/TextSearch.tsx +3 -1
- package/src/hooks/useEffectiveGroundingResults.ts +6 -1
- package/src/hooks/useImageSearch.ts +49 -6
- package/src/hooks/useResultEvaluator.ts +8 -8
- package/src/layouts/AppLayout.tsx +27 -6
- package/src/pages/Result.tsx +2 -1
- package/src/routes/ProctedRoute.tsx +52 -9
- package/src/services/filter.ts +13 -0
- package/src/services/visualSearch.ts +10 -1
- package/src/stores/result/prodcuts/products.initialState.ts +2 -0
- package/src/stores/result/prodcuts/products.slice.ts +8 -0
- package/src/stores/scope/scopeStore.ts +35 -0
- package/src/stores/types.ts +9 -0
- package/src/styles/Cart.scss +22 -0
- package/src/types.ts +6 -1
- package/build/static/css/main.5fff4ca0.css +0 -4
- package/build/static/css/main.5fff4ca0.css.map +0 -1
- package/build/static/js/main.b80e9533.js +0 -3
- /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.
|
|
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.
|
|
9
|
-
"@nyris/nyris-react-components": "^0.3.
|
|
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:
|
|
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: '',
|
package/src/components/Cart.tsx
CHANGED
|
@@ -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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
'
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
337
|
+
if (!/^[0-9]+$/.test(rawValue)) return;
|
|
272
338
|
|
|
273
|
-
|
|
274
|
-
|
|
339
|
+
const newValue = Number(rawValue);
|
|
340
|
+
if (newValue < 1) return;
|
|
275
341
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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-
|
|
297
|
-
onClick={() =>
|
|
369
|
+
className="h-7 w-7 rounded bg-[#FFE5EF4D] p-1.5"
|
|
370
|
+
onClick={() => removePartBySku(part.sku)}
|
|
298
371
|
>
|
|
299
|
-
<Icon name="
|
|
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
|
-
|
|
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>
|
|
@@ -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)
|
|
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 {
|
|
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 =
|
|
73
|
+
const isVisible =
|
|
74
|
+
(!scopedAllowedValues || scopedAllowedValues.size > 0) &&
|
|
75
|
+
(hasAvailableValues || hasSelection);
|
|
43
76
|
|
|
44
77
|
useEffect(() => {
|
|
45
78
|
onVisibilityChange?.(attribute, isVisible);
|