@nyris/nyris-webapp 0.3.89 → 0.3.90

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 (70) hide show
  1. package/build/_headers +2 -0
  2. package/build/asset-manifest.json +6 -6
  3. package/build/index.html +1 -1
  4. package/build/js/settings.example.js +17 -0
  5. package/build/static/css/main.734b52e1.css +4 -0
  6. package/build/static/css/main.734b52e1.css.map +1 -0
  7. package/build/static/js/main.cede3ae1.js +3 -0
  8. package/build/static/js/{main.ca8b95bc.js.map → main.cede3ae1.js.map} +1 -1
  9. package/package.json +3 -3
  10. package/public/_headers +2 -0
  11. package/public/index.html +1 -1
  12. package/public/js/settings.example.js +17 -0
  13. package/src/App.tsx +5 -3
  14. package/src/components/Cart.tsx +321 -0
  15. package/src/components/CustomCameraDrawer.tsx +4 -22
  16. package/src/components/DragDropFile.tsx +57 -38
  17. package/src/components/ExperienceVisualSearch/ExperienceVisualSearch.tsx +6 -1
  18. package/src/components/ExperienceVisualSearch/ExperienceVisualSearchTrigger.tsx +2 -2
  19. package/src/components/GroundingSpecs.tsx +47 -0
  20. package/src/components/Header.tsx +94 -93
  21. package/src/components/HitsPerPage.tsx +4 -2
  22. package/src/components/ImagePreview.tsx +64 -31
  23. package/src/components/ImageUpload.tsx +247 -0
  24. package/src/components/ItemSpecification.tsx +164 -0
  25. package/src/components/MatchNotificationBanner.tsx +165 -0
  26. package/src/components/PostFilter/PostFilter.tsx +22 -1
  27. package/src/components/PostFilter/PostFilterComponent.tsx +59 -26
  28. package/src/components/PostFilter/PostFilterFindApi.tsx +242 -0
  29. package/src/components/PoweredBy.tsx +16 -0
  30. package/src/components/PreFilter/PreFilter.tsx +77 -54
  31. package/src/components/Product/Product.tsx +186 -28
  32. package/src/components/Product/ProductAttribute.tsx +2 -2
  33. package/src/components/Product/ProductDetailView.tsx +123 -18
  34. package/src/components/Product/ProductDetailViewModal.tsx +3 -0
  35. package/src/components/Product/ProductList.tsx +78 -8
  36. package/src/components/SidePanel.tsx +212 -120
  37. package/src/components/TextSearch.tsx +82 -203
  38. package/src/components/Toaster.tsx +34 -15
  39. package/src/helpers/ToastHelper.ts +6 -2
  40. package/src/hooks/useCadSearch.ts +5 -0
  41. package/src/hooks/useImageSearch.ts +102 -13
  42. package/src/index.css +59 -0
  43. package/src/layouts/AppLayout.tsx +16 -14
  44. package/src/pages/Home.tsx +61 -13
  45. package/src/pages/Result.tsx +287 -295
  46. package/src/services/vizo.ts +161 -0
  47. package/src/stores/request/Misc/misc.initialstate.ts +1 -0
  48. package/src/stores/request/Misc/misc.slice.ts +1 -0
  49. package/src/stores/request/filter/filter.initialState.ts +3 -0
  50. package/src/stores/request/filter/filter.slice.ts +23 -0
  51. package/src/stores/result/prodcuts/products.initialState.ts +4 -0
  52. package/src/stores/result/prodcuts/products.slice.ts +15 -0
  53. package/src/stores/types.ts +27 -1
  54. package/src/stores/ui/loading/loading.initialState.ts +1 -0
  55. package/src/stores/ui/loading/loading.slice.ts +4 -0
  56. package/src/stores/ui/sidePanel/sidePanel.initialState.ts +5 -0
  57. package/src/stores/ui/sidePanel/sidePanel.slice.ts +11 -0
  58. package/src/stores/ui/uiStore.ts +4 -1
  59. package/src/styles/Cart.scss +210 -0
  60. package/src/styles/common.scss +10 -0
  61. package/src/translations.ts +4 -4
  62. package/src/types.ts +11 -3
  63. package/src/utils/prepareImageList.ts +6 -5
  64. package/src/utils/textSearchFilter.ts +203 -0
  65. package/tailwind.config.js +1 -0
  66. package/build/static/css/main.ba1c7479.css +0 -4
  67. package/build/static/css/main.ba1c7479.css.map +0 -1
  68. package/build/static/js/main.ca8b95bc.js +0 -3
  69. package/src/components/Footer.tsx +0 -21
  70. /package/build/static/js/{main.ca8b95bc.js.LICENSE.txt → main.cede3ae1.js.LICENSE.txt} +0 -0
@@ -0,0 +1,161 @@
1
+ import axios from 'axios';
2
+ // Utility: Convert HTMLCanvasElement to Blob
3
+ export function canvasToBlob(
4
+ canvas: HTMLCanvasElement,
5
+ type = 'image/jpeg',
6
+ quality = 0.95,
7
+ ): Promise<Blob> {
8
+ return new Promise((resolve, reject) => {
9
+ canvas.toBlob(
10
+ blob => {
11
+ if (blob) {
12
+ resolve(blob);
13
+ } else {
14
+ reject(new Error('Canvas toBlob failed'));
15
+ }
16
+ },
17
+ type,
18
+ quality,
19
+ );
20
+ });
21
+ }
22
+
23
+ function buildProductEntry(p: any, index: number): string {
24
+ const title = p.Bezeichnung || p.title || p.name || '';
25
+ const desc =
26
+ p.VK_Text_Englisch ||
27
+ p.VK_Text_Deutsch ||
28
+ p.description ||
29
+ p.description_short ||
30
+ '';
31
+ const sku = p.sku || '';
32
+ const brand = p.brand || '';
33
+
34
+ // Include customIds metadata
35
+ const customIds = p.customIds || {};
36
+ const mpn = customIds.mpn || customIds.mpn_original || p.mpn || '';
37
+ const normalizedPN =
38
+ customIds['normalized PN'] || customIds['normalized_PN'] || '';
39
+ const normalizedModel =
40
+ customIds['normalized model'] || customIds['normalized_model'] || '';
41
+ const gtin = customIds.gtin || '';
42
+
43
+ const idParts: string[] = [];
44
+ if (mpn) idParts.push(`MPN: ${mpn}`);
45
+ if (normalizedPN && normalizedPN !== mpn)
46
+ idParts.push(`NormalizedPN: ${normalizedPN}`);
47
+ if (normalizedModel && normalizedModel !== normalizedPN)
48
+ idParts.push(`Model: ${normalizedModel}`);
49
+ if (gtin) idParts.push(`GTIN: ${gtin}`);
50
+
51
+ const idStr = idParts.length > 0 ? ` | IDs: ${idParts.join(', ')}` : '';
52
+
53
+ return `[${index}] SKU: ${sku} | Brand: ${brand} | ${title} | ${desc}${idStr}`;
54
+ }
55
+
56
+ /**
57
+ * Calls the nyris grounded search API with a File or HTMLCanvasElement.
58
+ * @param imageInput File or HTMLCanvasElement
59
+ * @param apiKey nyris API key
60
+ */
61
+ export async function groundedSearch(
62
+ imageInput: File | HTMLCanvasElement,
63
+ apiKey: string,
64
+ ): Promise<any> {
65
+ const formData = new FormData();
66
+ let imageBlobOrFile: File | Blob;
67
+
68
+ if (imageInput instanceof HTMLCanvasElement) {
69
+ imageBlobOrFile = await canvasToBlob(imageInput);
70
+ formData.append('image', imageBlobOrFile, 'canvas.jpg');
71
+ } else {
72
+ formData.append('image', imageInput);
73
+ }
74
+
75
+ try {
76
+ const response = await axios.post(
77
+ 'https://api.nyris.io/api/ai-analysis/grounded-search',
78
+ formData,
79
+ {
80
+ headers: {
81
+ 'x-api-key': window.settings.aiApiKey || '',
82
+ // Let axios set Content-Type with boundary automatically
83
+ },
84
+ },
85
+ );
86
+ return response.data;
87
+ } catch (error: any) {
88
+ if (error.response) {
89
+ throw new Error(`API error: ${error.response.statusText}`);
90
+ } else {
91
+ throw new Error(`API error: ${error.message}`);
92
+ }
93
+ }
94
+ }
95
+
96
+ export async function groundedSearchFilter({
97
+ products,
98
+ groundingSummary,
99
+ apiKey,
100
+ }: {
101
+ products: any[];
102
+ groundingSummary: string;
103
+ apiKey: string;
104
+ }): Promise<any> {
105
+ const productList = products
106
+ .map((p, i) => buildProductEntry(p, i))
107
+ .join('\n');
108
+
109
+ try {
110
+ const response = await axios.post(
111
+ 'https://api.nyris.io/api/ai-analysis/filtering-items',
112
+ {
113
+ itemsDataSummary: productList,
114
+ groundingSummary,
115
+ },
116
+ {
117
+ headers: {
118
+ 'x-api-key': apiKey || window.settings.aiApiKey || '',
119
+ 'Content-Type': 'application/json',
120
+ },
121
+ },
122
+ );
123
+ return response.data;
124
+ } catch (error: any) {
125
+ if (error.response) {
126
+ throw new Error(`API error: ${error.response.statusText}`);
127
+ } else {
128
+ throw new Error(`API error: ${error.message}`);
129
+ }
130
+ }
131
+ }
132
+
133
+ export async function groundingSearchQuery({
134
+ groundingSummary,
135
+ apiKey,
136
+ }: {
137
+ groundingSummary: string;
138
+ apiKey: string;
139
+ }): Promise<any> {
140
+ try {
141
+ const response = await axios.post(
142
+ 'https://api.nyris.io/api/ai-analysis/search-query',
143
+ {
144
+ groundingSummary,
145
+ },
146
+ {
147
+ headers: {
148
+ 'x-api-key': apiKey || window.settings.aiApiKey || '',
149
+ 'Content-Type': 'application/json',
150
+ },
151
+ },
152
+ );
153
+ return response.data;
154
+ } catch (error: any) {
155
+ if (error.response) {
156
+ throw new Error(`API error: ${error.response.statusText}`);
157
+ } else {
158
+ throw new Error(`API error: ${error.message}`);
159
+ }
160
+ }
161
+ }
@@ -2,4 +2,5 @@ import { MiscState } from 'stores/types';
2
2
 
3
3
  export const initialState: MiscState = {
4
4
  metaFilter: '',
5
+ groundingQuery: '',
5
6
  };
@@ -5,6 +5,7 @@ import { initialState } from './misc.initialstate';
5
5
  const miscSlice: StateCreator<MiscState & MiscAction> = set => ({
6
6
  ...initialState,
7
7
  setMetaFilter: filter => set(state => ({ metaFilter: filter })),
8
+ setGroundingQuery: query => set(state => ({ groundingQuery: query })),
8
9
  });
9
10
 
10
11
  export default miscSlice;
@@ -3,6 +3,9 @@ import { FilterState } from 'stores/types';
3
3
  export const initialState: FilterState = {
4
4
  algoliaFilter: '',
5
5
  preFilter: {},
6
+ preFilterList: [],
6
7
  firstSearchPreFilter: {},
7
8
  specificationFilter: {},
9
+ postFilterSelections: {},
10
+ preFilterLoading: false,
8
11
  };
@@ -10,6 +10,29 @@ const filterSlice: StateCreator<FilterState & FilterAction> = set => ({
10
10
  set(state => ({ firstSearchPreFilter: filter })),
11
11
  setSpecificationFilter: filter =>
12
12
  set(state => ({ specificationFilter: filter })),
13
+ setPreFilterList: filter => set(state => ({ preFilterList: filter })),
14
+ setPreFilterLoading: isLoading =>
15
+ set(state => ({ preFilterLoading: isLoading })),
16
+ setPostFilterSelections: filters =>
17
+ set(state => ({ postFilterSelections: filters })),
18
+ togglePostFilterSelection: (attribute, value) =>
19
+ set(state => {
20
+ const current = state.postFilterSelections?.[attribute] || [];
21
+ const exists = current.includes(value);
22
+ const nextValues = exists
23
+ ? current.filter(item => item !== value)
24
+ : [...current, value];
25
+ const nextSelections = { ...state.postFilterSelections };
26
+
27
+ if (nextValues.length > 0) {
28
+ nextSelections[attribute] = nextValues;
29
+ } else {
30
+ delete nextSelections[attribute];
31
+ }
32
+
33
+ return { postFilterSelections: nextSelections };
34
+ }),
35
+ clearPostFilterSelections: () => set(state => ({ postFilterSelections: {} })),
13
36
  });
14
37
 
15
38
  export default filterSlice;
@@ -15,4 +15,8 @@ export const initialState: ProductsState = {
15
15
  optimizedSearchQuery: '',
16
16
  specification: {},
17
17
  },
18
+
19
+ googleGroundingResponse: null,
20
+ groundingFilterResult: [],
21
+ showingGroundingFilterResult: false,
18
22
  };
@@ -26,6 +26,21 @@ const productsSlice: StateCreator<ProductsState & ProductsAction> = set => ({
26
26
  ...analysis,
27
27
  },
28
28
  })),
29
+ setGoogleGroundingResponse: result =>
30
+ set(state => ({
31
+ googleGroundingResponse: result,
32
+ })),
33
+
34
+ setGroundingFilterResult: result =>
35
+ set(state => ({
36
+ groundingFilterResult: result,
37
+ })),
38
+ setShowingGroundingFilterResult: (val?: boolean) =>
39
+ set(state =>
40
+ val === undefined
41
+ ? { showingGroundingFilterResult: !state.showingGroundingFilterResult }
42
+ : { showingGroundingFilterResult: val },
43
+ ),
29
44
  });
30
45
 
31
46
  export default productsSlice;
@@ -1,4 +1,4 @@
1
- import { RectCoords, Region } from '@nyris/nyris-api';
1
+ import { Filter, RectCoords, Region } from '@nyris/nyris-api';
2
2
 
3
3
  export interface RequestImageState {
4
4
  requestImages: HTMLCanvasElement[];
@@ -30,12 +30,14 @@ export interface LoadingState {
30
30
  isFindApiLoading: boolean;
31
31
  isAlgoliaLoading: boolean;
32
32
  isCadenasLoaded: boolean;
33
+ isGoogleGroundingLoading: boolean;
33
34
  }
34
35
 
35
36
  export interface LoadingAction {
36
37
  setIsFindApiLoading: (isLoading: boolean) => void;
37
38
  setIsAlgoliaLoading: (isLoading: boolean) => void;
38
39
  setIsCadenasLoaded: (isLoading: boolean) => void;
40
+ setIsGoogleGroundingLoading: (isLoading: boolean) => void;
39
41
  }
40
42
 
41
43
  export interface ProductsState {
@@ -54,6 +56,9 @@ export interface ProductsState {
54
56
  };
55
57
 
56
58
  specificationFilteredProducts: any[];
59
+ googleGroundingResponse: Record<string, any> | null;
60
+ groundingFilterResult: any[];
61
+ showingGroundingFilterResult: boolean;
57
62
  }
58
63
 
59
64
  export interface ProductsAction {
@@ -67,6 +72,9 @@ export interface ProductsAction {
67
72
  specification: Record<string, string>;
68
73
  }) => void;
69
74
  setSpecificationFilteredProducts: (products: any[]) => void;
75
+ setGoogleGroundingResponse: (result: any | null) => void;
76
+ setGroundingFilterResult: (result: any[]) => void;
77
+ setShowingGroundingFilterResult: (val?: boolean) => void;
70
78
  }
71
79
 
72
80
  export interface DetectedRegionsState {
@@ -79,24 +87,34 @@ export interface DetectedRegionsAction {
79
87
 
80
88
  export interface FilterState {
81
89
  preFilter: Record<string, boolean>;
90
+ preFilterList: Filter[];
82
91
  algoliaFilter: string;
83
92
  firstSearchPreFilter: Record<string, boolean>;
84
93
  specificationFilter: Record<string, string>;
94
+ postFilterSelections: Record<string, string[]>;
95
+ preFilterLoading: boolean;
85
96
  }
86
97
 
87
98
  export interface FilterAction {
88
99
  setPreFilter: (query: Record<string, boolean>) => void;
100
+ setPreFilterList: (any: Filter[]) => void;
89
101
  setFirstSearchPreFilter: (query: Record<string, boolean>) => void;
90
102
  setAlgoliaFilter: (query: string) => void;
91
103
  setSpecificationFilter: (query: Record<string, string>) => void;
104
+ setPostFilterSelections: (filters: Record<string, string[]>) => void;
105
+ togglePostFilterSelection: (attribute: string, value: string) => void;
106
+ clearPostFilterSelections: () => void;
107
+ setPreFilterLoading: (isLoading: boolean) => void;
92
108
  }
93
109
 
94
110
  export interface MiscState {
95
111
  metaFilter: string;
112
+ groundingQuery: string;
96
113
  }
97
114
 
98
115
  export interface MiscAction {
99
116
  setMetaFilter: (filter: string) => void;
117
+ setGroundingQuery: (query: string) => void;
100
118
  }
101
119
 
102
120
  export interface FeedbackState {
@@ -107,6 +125,14 @@ export interface FeedbackAction {
107
125
  setShowFeedback: (show: boolean) => void;
108
126
  }
109
127
 
128
+ export interface SidePanelState {
129
+ showSidePanel: boolean;
130
+ }
131
+
132
+ export interface SidePanelAction {
133
+ setShowSidePanel: (show: boolean) => void;
134
+ }
135
+
110
136
  export interface SessionState {
111
137
  requestId: string;
112
138
  sessionId: string;
@@ -4,4 +4,5 @@ export const initialState: LoadingState = {
4
4
  isAlgoliaLoading: false,
5
5
  isFindApiLoading: false,
6
6
  isCadenasLoaded: false,
7
+ isGoogleGroundingLoading: false,
7
8
  };
@@ -10,6 +10,10 @@ const loadingSlice: StateCreator<LoadingState & LoadingAction> = set => ({
10
10
  set(state => ({ isFindApiLoading: isLoading })),
11
11
  setIsCadenasLoaded: (isLoading: boolean) =>
12
12
  set(state => ({ isCadenasLoaded: isLoading })),
13
+ setIsGoogleGroundingLoading: isLoading =>
14
+ set(state => ({
15
+ isGoogleGroundingLoading: isLoading,
16
+ })),
13
17
  });
14
18
 
15
19
  export default loadingSlice;
@@ -0,0 +1,5 @@
1
+ import { SidePanelState } from 'stores/types';
2
+
3
+ export const initialState: SidePanelState = {
4
+ showSidePanel: true,
5
+ };
@@ -0,0 +1,11 @@
1
+ import { StateCreator } from 'zustand';
2
+ import { SidePanelAction, SidePanelState } from 'stores/types';
3
+
4
+ import { initialState } from './sidePanel.initialState';
5
+
6
+ const sidePanelSlice: StateCreator<SidePanelState & SidePanelAction> = set => ({
7
+ ...initialState,
8
+ setShowSidePanel: (showSidePanel: boolean) => set(() => ({ showSidePanel })),
9
+ });
10
+
11
+ export default sidePanelSlice;
@@ -1,12 +1,15 @@
1
1
  import { create } from 'zustand';
2
2
  import loadingSlice from './loading/loading.slice';
3
3
  import feedbackSlice from './feedback/feedback.slice';
4
+ import sidePanelSlice from './sidePanel/sidePanel.slice';
4
5
 
5
6
  type UiStore = ReturnType<typeof loadingSlice> &
6
- ReturnType<typeof feedbackSlice>;
7
+ ReturnType<typeof feedbackSlice> &
8
+ ReturnType<typeof sidePanelSlice>;
7
9
 
8
10
  const useUiStore = create<UiStore>()((...a) => ({
9
11
  ...loadingSlice(...a),
10
12
  ...feedbackSlice(...a),
13
+ ...sidePanelSlice(...a),
11
14
  }));
12
15
  export default useUiStore;
@@ -0,0 +1,210 @@
1
+ .cart-button {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ position: relative;
6
+ &.active {
7
+ background-color: #2b2c46;
8
+ }
9
+ &.disabled {
10
+ pointer-events: none;
11
+ cursor: default;
12
+ background-color: #801e1e;
13
+ }
14
+ .cart-amount {
15
+ position: absolute;
16
+ width: 16px;
17
+ height: 16px;
18
+ display: flex;
19
+ justify-content: center;
20
+ align-items: center;
21
+ border: 2px solid #fff;
22
+ background-color: #009ea6;
23
+ border-radius: 50%;
24
+ color: #fff;
25
+ font-size: 10px;
26
+ top: -6px;
27
+ right: -4px;
28
+ }
29
+ }
30
+
31
+ .cart {
32
+ position: absolute;
33
+ top: 0;
34
+ right: 0;
35
+ bottom: 0;
36
+ left: 0;
37
+ z-index: 51;
38
+ &-background {
39
+ top: 0;
40
+ right: 0;
41
+ bottom: 0;
42
+ left: 0;
43
+ position: absolute;
44
+ background-color: rgba(0, 0, 0, 0.2);
45
+ }
46
+ &-main {
47
+ height: 100vh;
48
+ width: 410px;
49
+ max-width: 100vw;
50
+ position: absolute;
51
+ right: 0;
52
+ background-color: #fff;
53
+ display: flex;
54
+ flex-direction: column;
55
+ @media (max-width: 410px) {
56
+ width: 100vw;
57
+ min-width: 0;
58
+ }
59
+ &-header {
60
+ height: 52px;
61
+ display: flex;
62
+ flex-direction: row;
63
+ justify-content: space-between;
64
+ align-items: center;
65
+ padding: 8px 16px;
66
+ &-title {
67
+ font-weight: 600;
68
+ font-size: 24px;
69
+ }
70
+ &-buttons {
71
+ display: flex;
72
+ flex-direction: row;
73
+ align-items: center;
74
+ .clear-all {
75
+ border: 1px solid #e31b5d;
76
+ padding: 4px 8px;
77
+ font-weight: 400;
78
+ font-size: 9px;
79
+ color: #e31b5d;
80
+ border-radius: 4px;
81
+ display: flex;
82
+ flex-direction: row;
83
+ align-items: center;
84
+ margin-right: 16px;
85
+ }
86
+ }
87
+ }
88
+ &-body {
89
+ flex: 1;
90
+ background-color: #f3f3f5;
91
+ padding: 16px;
92
+ overflow-x: auto;
93
+ &-category {
94
+ font-family: 'Fira Sans', sans-serif;
95
+ font-weight: 600;
96
+ font-size: 16px;
97
+ margin-bottom: 16px;
98
+ }
99
+ &-parts {
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: 8px;
103
+ margin-bottom: 16px;
104
+ &-item {
105
+ width: 100%;
106
+ height: 66px;
107
+ padding: 8px;
108
+ border: 1px #e0e0e0 solid;
109
+ border-radius: 8px;
110
+ background-color: #fafafa;
111
+ display: flex;
112
+ flex-direction: row;
113
+ align-items: center;
114
+ justify-content: space-between;
115
+ &-column {
116
+ flex: 1;
117
+ display: flex;
118
+ flex-direction: row;
119
+ align-items: center;
120
+ }
121
+ img {
122
+ width: 50px;
123
+ height: 50px;
124
+ object-fit: cover;
125
+ background-color: #fff;
126
+ margin-right: 8px;
127
+ }
128
+ &-sku {
129
+ font-weight: 400;
130
+ font-size: 9px;
131
+ max-width: 150px;
132
+ word-break: break-all;
133
+ }
134
+ &-title {
135
+ font-weight: 600;
136
+ font-size: 12px;
137
+ max-width: 150px;
138
+ word-break: break-all;
139
+ }
140
+ &-quantity {
141
+ display: flex;
142
+ flex-direction: row;
143
+ align-items: center;
144
+ gap: 16px;
145
+ padding-right: 16px;
146
+ margin-left: auto;
147
+ button {
148
+ width: 16px;
149
+ height: 16px;
150
+ border-radius: 50%;
151
+ background-color: #f3f3f5;
152
+ }
153
+ &-number {
154
+ min-width: 22px;
155
+ height: 22px;
156
+ background-color: #fff;
157
+ border-radius: 4px;
158
+ border: 1px solid #f3f3f5;
159
+ display: flex;
160
+ align-items: center;
161
+ justify-content: center;
162
+ }
163
+ }
164
+ &-remove {
165
+ width: 28px;
166
+ height: 28px;
167
+ background-color: #ffe5ef4d;
168
+ padding: 6px;
169
+ border-radius: 4px;
170
+ }
171
+ }
172
+ }
173
+ &-empty {
174
+ width: 100%;
175
+ height: 100%;
176
+ display: flex;
177
+ flex-direction: column;
178
+ align-items: center;
179
+ justify-content: center;
180
+ &-title {
181
+ font-weight: 600;
182
+ font-size: 16px;
183
+ line-height: 16px;
184
+ letter-spacing: 0.16px;
185
+ }
186
+ &-text {
187
+ font-weight: 400;
188
+ line-height: 16px;
189
+ letter-spacing: 0.16px;
190
+ }
191
+ }
192
+ .cart-download-button {
193
+ background-color: #3e36dc;
194
+ color: #fff;
195
+ width: 100%;
196
+ height: 48px;
197
+ display: flex;
198
+ flex-direction: row;
199
+ align-items: center;
200
+ justify-content: center;
201
+ font-weight: 600;
202
+ font-size: 16px;
203
+ border-radius: 8px;
204
+ svg {
205
+ margin-right: 8px;
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }
@@ -12,6 +12,16 @@
12
12
  -webkit-box-orient: vertical;
13
13
  }
14
14
 
15
+ .max-line-2 {
16
+ word-break: break-word;
17
+ overflow: hidden;
18
+ text-overflow: ellipsis;
19
+ display: -webkit-box;
20
+ max-height: 66px; /* fallback */
21
+ -webkit-line-clamp: 2; /* number of lines to show */
22
+ -webkit-box-orient: vertical;
23
+ }
24
+
15
25
  .box-wrap-loading {
16
26
  position: fixed;
17
27
  background: #00000075;
@@ -71,7 +71,7 @@ export const translations = {
71
71
  'Labels': 'Labels',
72
72
  'Barcodes': 'Barcodes',
73
73
  'Nameplate not matching': 'Nameplate detected, but {{prefilter_value}} was not found in the list. You can select the correct {{preFilterTitle}} to continue.',
74
- 'Extracted details from the nameplate could not be matched': 'Extracted details from the nameplate could not be matched to a known {{preFilterTitle}}. Select a different machine, send details to support via the Inquiry button, or continue searching.',
74
+ 'Extracted details from the nameplate could not be matched': 'The extracted details could not be matched to a known {{preFilterTitle}}. Please select a different {{preFilterTitle}} and continue searching.',
75
75
  'Showing results for machine': `Showing results for {{prefilter_title}} '{{prefilter_value}}'`,
76
76
  'No matches found for': `No matches found for {{prefilter_title}} '{{prefilter_value}}'. Showing all items instead.`,
77
77
  },
@@ -150,7 +150,7 @@ export const translations = {
150
150
  'Labels': 'Etiketten',
151
151
  'Barcodes': 'Barcodes',
152
152
  'Nameplate not matching': 'Typenschild erkannt, aber {{prefilter_value}} wurde nicht in der Liste gefunden. Sie können die richtigen {{preFilterTitle}}, um fortzufahren.',
153
- 'Extracted details from the nameplate could not be matched': 'Die vom Typenschild extrahierten Angaben konnten keiner bekannten {{preFilterTitle}} zugeordnet werden. Wählen Sie einen anderen Maschine aus, senden Sie die Angaben über die Schaltfläche „Anfrage” an den Support oder setzen Sie die Suche fort.',
153
+ 'Extracted details from the nameplate could not be matched': 'Die extrahierten Details konnten keinem bekannten {{preFilterTitle}} zugeordnet werden. Bitte wählen Sie einen anderen {{preFilterTitle}} aus und setzen Sie die Suche fort.',
154
154
  'Showing results for machine': `Ergebnisse für {{prefilter_title}} '{{prefilter_value}}' anzeigen`,
155
155
  'No matches found for': `Keine Übereinstimmungen gefunden für {{prefilter_title}} '{{prefilter_value}}'. Alle Artikel anzeigen stattdessen.`,
156
156
  },
@@ -229,8 +229,8 @@ export const translations = {
229
229
  'Parts': 'Peças',
230
230
  'Labels': 'Etiquetas',
231
231
  'Barcodes': 'Códigos de barras',
232
- 'Nameplate not matching': 'Placa de identificação detetada, mas {{prefilter_value}} não foi encontrado na lista. Pode selecionar os {{preFilterTitle}} corretos para continuar.',
233
- 'Extracted details from the nameplate could not be matched': 'Os detalhes extraídos da placa de identificação não puderam ser correspondidos a uma {{preFilterTitle}} conhecida. Selecione um máquina diferente, envie os detalhes para o suporte através do botão Consulta ou continue a pesquisa.',
232
+ 'Nameplate not matching': 'Placa de identificação detetada, mas {{prefilter_value}} não foi encontrado na lista. Pode selecionar os {{preFilterTitle}} corretos para continuar.',
233
+ 'Extracted details from the nameplate could not be matched': 'Os detalhes extraídos não puderam ser correspondidos a um {{preFilterTitle}} conhecido. Selecione um {{preFilterTitle}} diferente e continue a pesquisa.',
234
234
  'Showing results for machine': `Mostrando resultados para {{prefilter_title}} '{{prefilter_value}}'`,
235
235
  'No matches found for': `Não foram encontrados resultados para {{prefilter_title}} '{{prefilter_value}}'. Mostrando todos os itens em vez disso.`,
236
236
  },