@nyris/nyris-webapp 0.3.98 → 0.4.0

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 (97) hide show
  1. package/build/asset-manifest.json +7 -6
  2. package/build/index.html +1 -1
  3. package/build/js/settings.example.js +42 -0
  4. package/build/static/css/main.a7ff7a7f.css +4 -0
  5. package/build/static/css/main.a7ff7a7f.css.map +1 -0
  6. package/build/static/js/main.916c157c.js +3 -0
  7. package/build/static/js/{main.93fdbd6e.js.map → main.916c157c.js.map} +1 -1
  8. package/build/static/media/plate_info.6beaea36d35053be3a45.png +0 -0
  9. package/package.json +3 -3
  10. package/public/js/settings.example.js +42 -0
  11. package/src/common/assets/images/plate_info.png +0 -0
  12. package/src/components/Cart.tsx +41 -0
  13. package/src/components/ChatAssistant/ChatAssistant.test.tsx +213 -0
  14. package/src/components/ChatAssistant/ChatAssistant.tsx +18 -1
  15. package/src/components/ChatAssistant/MobileChatAssistant.tsx +18 -1
  16. package/src/components/ChatAssistant/OptionChip.tsx +23 -1
  17. package/src/components/ChatAssistant/useChatAssistantLogic.ts +35 -14
  18. package/src/components/GoBackButton.tsx +29 -2
  19. package/src/components/Header.tsx +3 -0
  20. package/src/components/HitsPerPage.tsx +55 -18
  21. package/src/components/ImagePreview.tsx +31 -1
  22. package/src/components/ImageUpload.tsx +12 -1
  23. package/src/components/Inquiry/InquiryModal.tsx +61 -26
  24. package/src/components/Pagination.tsx +33 -6
  25. package/src/components/PostFilter/PostFilter.tsx +8 -1
  26. package/src/components/PostFilter/PostFilterFindApi.test.tsx +249 -0
  27. package/src/components/PostFilter/PostFilterFindApi.tsx +128 -52
  28. package/src/components/PreFilter/PreFilter.tsx +10 -2
  29. package/src/components/Product/Product.tsx +60 -10
  30. package/src/components/Product/ProductDetailView.tsx +23 -1
  31. package/src/components/Product/ProductDetailViewModal.tsx +5 -1
  32. package/src/components/Product/ProductList.tsx +51 -18
  33. package/src/components/Rfq/RfqModal.tsx +3 -0
  34. package/src/components/ScopeSelect/ScopeSelectForm.tsx +514 -0
  35. package/src/components/ScopeSelect/ScopeSelectImageActions.tsx +62 -0
  36. package/src/components/ScopeSelect/ScopeSelectImagePreview.tsx +141 -0
  37. package/src/components/ScopeSelect/ScopeSelectInfo.tsx +68 -0
  38. package/src/components/ScopeSelect/ScopeSelectInput.tsx +123 -0
  39. package/src/components/ScopeSelect/ScopeSelectModal.tsx +310 -0
  40. package/src/components/ScopeSelect/identifyTypePlateImage.test.ts +129 -0
  41. package/src/components/ScopeSelect/identifyTypePlateImage.ts +83 -0
  42. package/src/components/ScopeSelect/parseScopeIdResponse.test.ts +103 -0
  43. package/src/components/ScopeSelect/parseScopeIdResponse.ts +72 -0
  44. package/src/components/ScopeSelect/scopeSelectHelpers.ts +54 -0
  45. package/src/components/SidePanel.tsx +33 -0
  46. package/src/components/TextSearch.tsx +77 -11
  47. package/src/components/Tooltip/TooltipComponent.tsx +12 -4
  48. package/src/components/TypePlateUpload.tsx +132 -0
  49. package/src/hooks/chatProductMetadata.test.ts +307 -0
  50. package/src/hooks/useBadResultsRecovery.ts +26 -49
  51. package/src/hooks/useEffectiveGroundingResults.ts +8 -6
  52. package/src/hooks/useGoodResultsChat.ts +28 -39
  53. package/src/hooks/useImageSearch.ts +290 -12
  54. package/src/hooks/useMachinePartsListingPagination.ts +95 -0
  55. package/src/hooks/useResultEvaluator.ts +63 -73
  56. package/src/index.css +1 -1
  57. package/src/layouts/AppLayout.tsx +43 -3
  58. package/src/pages/Home.tsx +60 -14
  59. package/src/pages/Result.tsx +194 -14
  60. package/src/services/Feedback.ts +225 -34
  61. package/src/services/feedbackSubtypes.ts +63 -0
  62. package/src/services/filter.ts +1 -1
  63. package/src/services/initialMachinePartsText.payload.test.ts +61 -0
  64. package/src/services/initialMachinePartsText.test.ts +33 -0
  65. package/src/services/initialMachinePartsText.ts +11 -0
  66. package/src/services/machinePartsPage.test.ts +74 -0
  67. package/src/services/machinePartsPage.ts +120 -0
  68. package/src/services/normalizeChatResponse.ts +26 -0
  69. package/src/services/scopeLookup.test.ts +67 -0
  70. package/src/services/scopeLookup.ts +39 -0
  71. package/src/services/visualSearch.listingOptions.test.ts +79 -0
  72. package/src/services/visualSearch.ts +71 -3
  73. package/src/services/vizo.ts +4 -3
  74. package/src/stores/chat/conversationStore.test.ts +133 -0
  75. package/src/stores/chat/conversationStore.ts +55 -14
  76. package/src/stores/machinePartsListing/machinePartsListingStore.ts +35 -0
  77. package/src/stores/request/filter/filter.initialState.ts +1 -0
  78. package/src/stores/request/filter/filter.slice.ts +2 -0
  79. package/src/stores/result/prodcuts/products.initialState.ts +1 -0
  80. package/src/stores/result/prodcuts/products.slice.ts +4 -0
  81. package/src/stores/scope/scopeStore.ts +67 -4
  82. package/src/stores/types.ts +19 -14
  83. package/src/translations.ts +213 -0
  84. package/src/types.ts +41 -0
  85. package/src/utils/chatProductMetadata.test.ts +130 -0
  86. package/src/utils/chatProductMetadata.ts +93 -0
  87. package/src/utils/compressImage.ts +3 -3
  88. package/src/utils/fetchProductImage.ts +1 -0
  89. package/src/utils/normalizeChatAssistantResponse.ts +227 -0
  90. package/src/utils/productCardImageUrl.test.ts +41 -0
  91. package/src/utils/productCardImageUrl.ts +49 -0
  92. package/src/utils/textSearchFilter.test.ts +41 -0
  93. package/src/utils/textSearchFilter.ts +13 -6
  94. package/build/static/css/main.81216c5d.css +0 -4
  95. package/build/static/css/main.81216c5d.css.map +0 -1
  96. package/build/static/js/main.93fdbd6e.js +0 -3
  97. /package/build/static/js/{main.93fdbd6e.js.LICENSE.txt → main.916c157c.js.LICENSE.txt} +0 -0
@@ -7,8 +7,11 @@ import useResultStore from 'stores/result/resultStore';
7
7
  import useRequestStore from 'stores/request/requestStore';
8
8
  import { twMerge } from 'tailwind-merge';
9
9
  import useUiStore from 'stores/ui/uiStore';
10
+ import useScopeStore from 'stores/scope/scopeStore';
11
+ import { useImageSearch } from 'hooks/useImageSearch';
10
12
 
11
13
  export const GoBackButton = ({ className }: { className?: string }) => {
14
+ const { singleImageSearch } = useImageSearch();
12
15
  const firstSearchResults = useResultStore(state => state.firstSearchResults);
13
16
  const setFindApiProducts = useResultStore(state => state.setFindApiProducts);
14
17
  const setImageAnalysis = useResultStore(state => state.setImageAnalysis);
@@ -27,12 +30,35 @@ export const GoBackButton = ({ className }: { className?: string }) => {
27
30
  const setAlgoliaFilter = useRequestStore(state => state.setAlgoliaFilter);
28
31
 
29
32
  const isFindApiLoading = useUiStore(state => state.isFindApiLoading);
33
+ const scopeId = useScopeStore(state => state.scopeId);
34
+ const scopeItemKeyCount = useScopeStore(state => state.itemKeys.length);
35
+ const machineScopeActive =
36
+ Boolean(window.settings.scopes?.scopeID) &&
37
+ Boolean(scopeId) &&
38
+ scopeItemKeyCount > 0;
30
39
 
31
40
  const { refine: clearPostFilters } = useClearRefinements();
32
41
 
33
42
  const { t } = useTranslation();
34
43
 
35
44
  const onGoBack = () => {
45
+ // The saved snapshot may predate the machine scope, so search the original
46
+ // image again; find() applies the active scope.
47
+ if (machineScopeActive && firstSearchImage) {
48
+ setPreFilter(firstSearchPreFilter);
49
+ resetRegions();
50
+ clearPostFilters();
51
+ void singleImageSearch({
52
+ image: firstSearchImage,
53
+ settings: window.settings,
54
+ showFeedback: true,
55
+ newSearch: false,
56
+ compress: false,
57
+ clearPostFilter: true,
58
+ });
59
+ return;
60
+ }
61
+
36
62
  setFindApiProducts(firstSearchResults);
37
63
  if (firstSearchImage) {
38
64
  setRequestImages([firstSearchImage]);
@@ -58,9 +84,10 @@ export const GoBackButton = ({ className }: { className?: string }) => {
58
84
  };
59
85
 
60
86
  if (
61
- firstSearchResults.length === 0 ||
87
+ !firstSearchImage ||
62
88
  requestImages[0] === firstSearchImage ||
63
- isFindApiLoading
89
+ isFindApiLoading ||
90
+ (!machineScopeActive && firstSearchResults.length === 0)
64
91
  )
65
92
  return <></>;
66
93
 
@@ -12,6 +12,7 @@ import LogoutModal from './LogoutModal';
12
12
  import useResultStore from 'stores/result/resultStore';
13
13
  import Cart from './Cart';
14
14
  import useUiStore from 'stores/ui/uiStore';
15
+ import useScopeStore from 'stores/scope/scopeStore';
15
16
 
16
17
  function Header() {
17
18
  const { theme, auth0 } = window.settings;
@@ -20,6 +21,7 @@ function Header() {
20
21
 
21
22
  const reset = useRequestStore(state => state.reset);
22
23
  const resetResultStore = useResultStore(state => state.reset);
24
+ const resetScope = useScopeStore(state => state.resetScope);
23
25
  const setSpecifications = useRequestStore(state => state.setSpecifications);
24
26
  const showSidePanel = useUiStore(state => state.showSidePanel);
25
27
  const setShowSidePanel = useUiStore(state => state.setShowSidePanel);
@@ -84,6 +86,7 @@ function Header() {
84
86
  onClick={() => {
85
87
  reset();
86
88
  resetResultStore();
89
+ resetScope();
87
90
  setSpecifications(null);
88
91
  }}
89
92
  className="pl-6"
@@ -2,13 +2,31 @@ import { useTranslation } from 'react-i18next';
2
2
  import { useHitsPerPage, UseHitsPerPageProps } from 'react-instantsearch';
3
3
  import PoweredBy from './PoweredBy';
4
4
  import { twMerge } from 'tailwind-merge';
5
+ import useMachinePartsListingStore from 'stores/machinePartsListing/machinePartsListingStore';
5
6
 
6
- export const HitsPerPage = (props: UseHitsPerPageProps) => {
7
+ interface Props extends UseHitsPerPageProps {
8
+ onFindApiPageSizeChange?: (pageSize: number) => void;
9
+ useFindApiPageSize?: boolean;
10
+ showPageSize?: boolean;
11
+ }
12
+
13
+ export const HitsPerPage = ({
14
+ onFindApiPageSizeChange,
15
+ useFindApiPageSize = false,
16
+ showPageSize = true,
17
+ ...props
18
+ }: Props) => {
7
19
  const { items, refine } = useHitsPerPage(props);
8
20
  const { value: currentValue } =
9
21
  items.find(({ isRefined }) => isRefined)! || {};
10
-
22
+ const pageSize = useMachinePartsListingStore(state => state.pageSize);
11
23
  const { t } = useTranslation();
24
+ const isAlgoliaEnabled = window.settings.algolia.enabled;
25
+ const controlAlgoliaPageSize = isAlgoliaEnabled && !useFindApiPageSize;
26
+ const options = controlAlgoliaPageSize ? items : props.items || items;
27
+ const selectedValue = controlAlgoliaPageSize
28
+ ? String(currentValue)
29
+ : String(pageSize);
12
30
 
13
31
  return (
14
32
  <>
@@ -18,25 +36,44 @@ export const HitsPerPage = (props: UseHitsPerPageProps) => {
18
36
  'shadow-ds-1',
19
37
  )}
20
38
  >
21
- <div className="self-stretch pl-4 pr-6 border-r border-solid border-[#DDDEE7] justify-center items-center gap-1 flex">
22
- <div className="text-[#3B3E5F] text-[13px] font-normal font-['Source Sans 3'] leading-none tracking-tight">
23
- {t('Items per page')}:
39
+ {showPageSize && (
40
+ <div className="self-stretch pl-4 pr-6 border-r border-solid border-[#DDDEE7] justify-center items-center gap-1 flex">
41
+ <div className="text-[#3B3E5F] text-[13px] font-normal font-['Source Sans 3'] leading-none tracking-tight">
42
+ {t('Items per page')}:
43
+ </div>
44
+ <select
45
+ className="bg-transparent text-[13px] outline-none"
46
+ onChange={event => {
47
+ const nextPageSize = Number(event.target.value);
48
+ if (controlAlgoliaPageSize) {
49
+ refine(nextPageSize);
50
+ return;
51
+ }
52
+ onFindApiPageSizeChange?.(nextPageSize);
53
+ }}
54
+ value={selectedValue}
55
+ >
56
+ {options.map(item => (
57
+ <option key={item.value} value={item.value}>
58
+ {item.label}
59
+ </option>
60
+ ))}
61
+ </select>
24
62
  </div>
25
- <select
26
- className="bg-transparent text-[13px] outline-none"
27
- onChange={event => {
28
- refine(Number(event.target.value));
63
+ )}
64
+ {!isAlgoliaEnabled || !showPageSize ? (
65
+ <div
66
+ style={{
67
+ display: 'flex',
68
+ justifyContent: 'end',
69
+ marginLeft: 'auto',
29
70
  }}
30
- value={String(currentValue)}
31
71
  >
32
- {items.map(item => (
33
- <option key={item.value} value={item.value}>
34
- {item.label}
35
- </option>
36
- ))}
37
- </select>
38
- </div>
39
- {window.settings.showPoweredByNyris && <PoweredBy />}
72
+ {window.settings.showPoweredByNyris && <PoweredBy />}
73
+ </div>
74
+ ) : (
75
+ window.settings.showPoweredByNyris && <PoweredBy />
76
+ )}
40
77
  </div>
41
78
  </>
42
79
  );
@@ -12,6 +12,8 @@ import { DEFAULT_REGION } from '../constants';
12
12
  import { useImageSearch } from 'hooks/useImageSearch';
13
13
  import useRequestStore from 'stores/request/requestStore';
14
14
  import useResultStore from 'stores/result/resultStore';
15
+ import { hasActiveMachineScope } from 'stores/scope/scopeStore';
16
+ import { firstMachinePartsListingText } from 'stores/machinePartsListing/machinePartsListingStore';
15
17
 
16
18
  function ImagePreviewComponent({
17
19
  showAdjustInfo = false,
@@ -52,7 +54,7 @@ function ImagePreviewComponent({
52
54
  const detectedRegions = useResultStore(state => state.detectedRegions);
53
55
  const resetResultStore = useResultStore(state => state.reset);
54
56
 
55
- const { singleImageSearch } = useImageSearch();
57
+ const { singleImageSearch, searchMachinePartsListing } = useImageSearch();
56
58
 
57
59
  const currentIndex = requestImages.length - 1;
58
60
 
@@ -86,6 +88,33 @@ function ImagePreviewComponent({
86
88
  }, []);
87
89
 
88
90
  const onImageRemove = () => {
91
+ if (hasActiveMachineScope()) {
92
+ const keyword = (useRequestStore.getState().query || '').trim();
93
+ resetRegions();
94
+ setRequestImages([]);
95
+ if (keyword) {
96
+ void singleImageSearch({
97
+ text: keyword,
98
+ settings,
99
+ showFeedback: false,
100
+ newSearch: false,
101
+ });
102
+ } else {
103
+ const text = firstMachinePartsListingText();
104
+ useRequestStore.getState().setQuery('');
105
+ useRequestStore.getState().setValueInput('');
106
+ if (text) {
107
+ void searchMachinePartsListing({
108
+ text,
109
+ showFeedback: false,
110
+ clearPostFilters: true,
111
+ });
112
+ }
113
+ }
114
+ navigate('/result');
115
+ return;
116
+ }
117
+
89
118
  resetRegions();
90
119
  setRequestImages([]);
91
120
 
@@ -106,6 +135,7 @@ function ImagePreviewComponent({
106
135
  imageRegion: r,
107
136
  showFeedback: true,
108
137
  compress: false,
138
+ regionSelectedByUser: true,
109
139
  }).then((res: any) => {
110
140
  const specificationPrefilter =
111
141
  res?.image_analysis?.specification?.prefilter_value || null;
@@ -12,9 +12,11 @@ import { useUploadSearch } from 'hooks/useUploadSearch';
12
12
  function ImageUpload({
13
13
  onCameraClick,
14
14
  disableDisclaimer,
15
+ guideOpen,
15
16
  }: {
16
17
  onCameraClick?: () => void;
17
18
  disableDisclaimer?: boolean;
19
+ guideOpen?: boolean;
18
20
  }) {
19
21
  const settings = window.settings;
20
22
  const user = useAuth0().user;
@@ -91,7 +93,16 @@ function ImageUpload({
91
93
  e.target.value = '';
92
94
  }}
93
95
  />
94
- <Tooltip content={t('Search with an image')}>
96
+ <Tooltip
97
+ content={
98
+ guideOpen
99
+ ? t('You can now upload an image to search.')
100
+ : t('Search with an image')
101
+ }
102
+ open={guideOpen ? true : undefined}
103
+ side="top"
104
+ sideOffset={10}
105
+ >
95
106
  <label
96
107
  className={twMerge(
97
108
  'min-w-10 min-h-10 flex justify-center items-center cursor-pointer rounded-full bg-gray-100 desktop:bg-transparent hover:bg-gray-100',
@@ -8,17 +8,20 @@ import { Icon } from '@nyris/nyris-react-components';
8
8
  import useRequestStore from 'stores/request/requestStore';
9
9
  import { getCroppedCanvas } from 'utils/misc';
10
10
  import { ToastHelper } from 'helpers/ToastHelper';
11
- import { Dialog, DialogContent } from 'components/Modal/Dialog';
11
+ import { Dialog, DialogContent, DialogTitle } from 'components/Modal/Dialog';
12
12
  import { AutosizeTextarea } from 'components/AutosizeTextArea';
13
13
  import Tooltip from 'components/Tooltip/TooltipComponent';
14
14
 
15
15
  import { useTranslation } from 'react-i18next';
16
16
  import { createImage } from '../../services/visualSearch';
17
+ import { trackClick } from 'services/Feedback';
18
+ import { CLICK_SUBTYPE } from 'services/feedbackSubtypes';
17
19
  interface Props {
18
20
  requestImage: any;
19
21
  selectedRegion: any;
20
22
  setIsInquiryModalOpen: any;
21
23
  isInquiryModalOpen?: any;
24
+ allowEmailFallback?: boolean;
22
25
  }
23
26
  // eslint-disable-next-line
24
27
  const emailRegex = /.+\@.+\..+$/;
@@ -42,6 +45,7 @@ export default function InquiryModal({
42
45
  selectedRegion,
43
46
  setIsInquiryModalOpen,
44
47
  isInquiryModalOpen,
48
+ allowEmailFallback = false,
45
49
  }: Props) {
46
50
  const settings = window.settings;
47
51
  const preFilter = useRequestStore(state => state.preFilter);
@@ -54,6 +58,27 @@ export default function InquiryModal({
54
58
  const [emailValid, setEmailValid] = useState<boolean | undefined>(undefined);
55
59
 
56
60
  const [information, setInformation] = useState('');
61
+ const useEmailClient =
62
+ allowEmailFallback && !settings.support?.emailTemplateId;
63
+ // encodeURIComponent, not URLSearchParams: mailto (RFC 6068) needs spaces as %20, not +
64
+ const supportEmailHref = `mailto:support@nyris.io?subject=${encodeURIComponent(
65
+ 'Vizo support request',
66
+ )}&body=${encodeURIComponent(`Email: ${email.trim()}\n\n${information}`)}`;
67
+ const submitStyle: React.CSSProperties = {
68
+ height: '66px',
69
+ display: 'flex',
70
+ alignItems: 'center',
71
+ width: '50%',
72
+ backgroundColor: emailValid
73
+ ? settings.theme?.primaryColor || '#0F766E'
74
+ : '#E9E9EC',
75
+ color: emailValid ? '#fff' : '#AAABB5',
76
+ fontSize: '14px',
77
+ paddingLeft: '16px',
78
+ border: 'none',
79
+ cursor: emailValid ? 'pointer' : 'default',
80
+ textDecoration: 'none',
81
+ };
57
82
 
58
83
  const setFormattedContent = React.useCallback(
59
84
  (text: string) => {
@@ -123,6 +148,7 @@ export default function InquiryModal({
123
148
  try {
124
149
  await emailjs.send(serviceId, templateId, body);
125
150
  ToastHelper.success(t('Request sent successfully'));
151
+ trackClick(CLICK_SUBTYPE.RAISE_INQUIRY);
126
152
  } catch (error) {
127
153
  toast(
128
154
  () => {
@@ -186,7 +212,8 @@ export default function InquiryModal({
186
212
  >
187
213
  <DialogContent
188
214
  closeButton={false}
189
- className="w-[360px] desktop:w-[378px] flex flex-col bg-white p-0"
215
+ aria-describedby={undefined}
216
+ className="w-[360px] desktop:w-[378px] max-w-[calc(100vw-32px)] max-h-[calc(100dvh-32px)] overflow-y-auto flex flex-col bg-white p-0"
190
217
  >
191
218
  <div>
192
219
  <div
@@ -197,7 +224,8 @@ export default function InquiryModal({
197
224
  justifyContent: 'space-between',
198
225
  }}
199
226
  >
200
- <p
227
+ <DialogTitle
228
+ className="text-sm leading-normal tracking-normal"
201
229
  style={{
202
230
  color: '#2B2C46',
203
231
  fontWeight: 'bold',
@@ -206,8 +234,10 @@ export default function InquiryModal({
206
234
  {requestImage
207
235
  ? t('Submit your image for inquiry')
208
236
  : t('Submit your inquiry')}
209
- </p>
210
- <div
237
+ </DialogTitle>
238
+ <button
239
+ type="button"
240
+ aria-label={t('Close')}
211
241
  onClick={() => setIsInquiryModalOpen(false)}
212
242
  style={{ display: 'flex', padding: '1px' }}
213
243
  >
@@ -219,7 +249,7 @@ export default function InquiryModal({
219
249
  cursor: 'pointer',
220
250
  }}
221
251
  />
222
- </div>
252
+ </button>
223
253
  </div>
224
254
  {requestImage && (
225
255
  <div
@@ -378,26 +408,31 @@ export default function InquiryModal({
378
408
  >
379
409
  {t('Cancel')}
380
410
  </button>
381
- <button
382
- style={{
383
- height: '66px',
384
- display: 'flex',
385
- alignItems: 'center',
386
- width: '50%',
387
- backgroundColor: emailValid
388
- ? settings.theme?.primaryColor
389
- : '#E9E9EC',
390
- color: emailValid ? '#fff' : '#AAABB5',
391
- fontSize: '14px',
392
- paddingLeft: '16px',
393
- border: 'none',
394
- cursor: emailValid ? 'pointer' : 'normal',
395
- }}
396
- disabled={!emailValid}
397
- onClick={handleInquiry}
398
- >
399
- {t('Send')}
400
- </button>
411
+ {useEmailClient ? (
412
+ <a
413
+ style={submitStyle}
414
+ href={emailValid ? supportEmailHref : undefined}
415
+ aria-disabled={!emailValid}
416
+ tabIndex={emailValid ? 0 : -1}
417
+ onClick={event => {
418
+ if (!emailValid) {
419
+ event.preventDefault();
420
+ return;
421
+ }
422
+ trackClick(CLICK_SUBTYPE.RAISE_INQUIRY);
423
+ }}
424
+ >
425
+ {t('Open email')}
426
+ </a>
427
+ ) : (
428
+ <button
429
+ style={submitStyle}
430
+ disabled={!emailValid}
431
+ onClick={handleInquiry}
432
+ >
433
+ {t('Send')}
434
+ </button>
435
+ )}
401
436
  </div>
402
437
  </div>
403
438
  </DialogContent>
@@ -1,19 +1,46 @@
1
1
  import { usePagination } from 'react-instantsearch';
2
2
  import { Icon } from '@nyris/nyris-react-components';
3
3
  import { twMerge } from 'tailwind-merge';
4
+ import { visibleMachinePartsPageIndexes } from 'services/machinePartsPage';
4
5
 
5
6
  export const Pagination = ({
6
7
  isLoading,
7
8
  className,
9
+ pageCount,
10
+ pageIndex = 0,
11
+ onPageChange,
8
12
  }: {
9
13
  isLoading: boolean;
10
14
  className?: string;
15
+ pageCount?: number;
16
+ pageIndex?: number;
17
+ onPageChange?: (pageIndex: number) => void;
11
18
  }) => {
12
- const { pages, currentRefinement, isFirstPage, isLastPage, refine } =
13
- usePagination();
19
+ const algoliaPagination = usePagination();
20
+ const isControlled = onPageChange !== undefined;
21
+ const currentRefinement = isControlled
22
+ ? pageIndex
23
+ : algoliaPagination.currentRefinement;
24
+ const isFirstPage = isControlled
25
+ ? currentRefinement <= 0
26
+ : algoliaPagination.isFirstPage;
27
+ const isLastPage = isControlled
28
+ ? currentRefinement >= (pageCount ?? 1) - 1
29
+ : algoliaPagination.isLastPage;
30
+ const pages = isControlled
31
+ ? visibleMachinePartsPageIndexes(currentRefinement, pageCount ?? 0)
32
+ : algoliaPagination.pages;
14
33
  const previousPageIndex = currentRefinement - 1;
15
34
  const nextPageIndex = currentRefinement + 1;
16
35
 
36
+ const selectPage = (page: number) => {
37
+ if (onPageChange) {
38
+ onPageChange(page);
39
+ return;
40
+ }
41
+ algoliaPagination.refine(page);
42
+ };
43
+
17
44
  return (
18
45
  <div
19
46
  className={twMerge(
@@ -22,13 +49,14 @@ export const Pagination = ({
22
49
  }`,
23
50
  className,
24
51
  )}
52
+ data-page-count={isControlled ? pageCount : undefined}
25
53
  >
26
54
  <div className="w-12 h-12 p-3.5 justify-center items-center flex">
27
55
  <div
28
56
  className="w-5 h-5 relative flex-col justify-start items-start flex cursor-pointer"
29
57
  onClick={() => {
30
58
  if (isFirstPage) return;
31
- refine(previousPageIndex);
59
+ selectPage(previousPageIndex);
32
60
  }}
33
61
  >
34
62
  <Icon name="caret_left" className="w-5 h-5" />
@@ -43,8 +71,7 @@ export const Pagination = ({
43
71
  className="w-12 px-4 pt-[15px] flex-col justify-end items-center gap-[11px] inline-flex cursor-pointer"
44
72
  onClick={event => {
45
73
  event.preventDefault();
46
-
47
- refine(page);
74
+ selectPage(page);
48
75
  }}
49
76
  >
50
77
  <div className="text-center text-sm font-normal leading-[18px] tracking-tight">
@@ -62,7 +89,7 @@ export const Pagination = ({
62
89
  className="w-5 h-5 relative flex-col justify-start items-start flex cursor-pointer"
63
90
  onClick={() => {
64
91
  if (isLastPage) return;
65
- refine(nextPageIndex);
92
+ selectPage(nextPageIndex);
66
93
  }}
67
94
  >
68
95
  <Icon name="caret_right" className="w-5 h-5" />
@@ -8,6 +8,8 @@ import { useRefinementList } from 'react-instantsearch';
8
8
  import { Icon } from '@nyris/nyris-react-components';
9
9
  import { useTranslation } from 'react-i18next';
10
10
  import useScopeStore from 'stores/scope/scopeStore';
11
+ import { trackClick } from 'services/Feedback';
12
+ import { CLICK_SUBTYPE } from 'services/feedbackSubtypes';
11
13
 
12
14
  function PostFilter({
13
15
  attribute,
@@ -145,7 +147,12 @@ function PostFilter({
145
147
  <input
146
148
  type="checkbox"
147
149
  checked={item.isRefined}
148
- onChange={() => refine(item.value)}
150
+ onChange={() => {
151
+ if (!item.isRefined) {
152
+ trackClick(CLICK_SUBTYPE.FILTER_APPLIED);
153
+ }
154
+ refine(item.value);
155
+ }}
149
156
  className="cursor-pointer"
150
157
  />
151
158
  <span className="text-xs font-normal pl-2 pr-1">