@athoscommerce/snap-controller 1.0.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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/dist/cjs/Abstract/AbstractController.d.ts +42 -0
  4. package/dist/cjs/Abstract/AbstractController.d.ts.map +1 -0
  5. package/dist/cjs/Abstract/AbstractController.js +305 -0
  6. package/dist/cjs/Autocomplete/AutocompleteController.d.ts +59 -0
  7. package/dist/cjs/Autocomplete/AutocompleteController.d.ts.map +1 -0
  8. package/dist/cjs/Autocomplete/AutocompleteController.js +1091 -0
  9. package/dist/cjs/Finder/FinderController.d.ts +15 -0
  10. package/dist/cjs/Finder/FinderController.d.ts.map +1 -0
  11. package/dist/cjs/Finder/FinderController.js +336 -0
  12. package/dist/cjs/Recommendation/RecommendationController.d.ts +27 -0
  13. package/dist/cjs/Recommendation/RecommendationController.d.ts.map +1 -0
  14. package/dist/cjs/Recommendation/RecommendationController.js +447 -0
  15. package/dist/cjs/Search/SearchController.d.ts +41 -0
  16. package/dist/cjs/Search/SearchController.d.ts.map +1 -0
  17. package/dist/cjs/Search/SearchController.js +993 -0
  18. package/dist/cjs/index.d.ts +7 -0
  19. package/dist/cjs/index.d.ts.map +1 -0
  20. package/dist/cjs/index.js +29 -0
  21. package/dist/cjs/types.d.ts +87 -0
  22. package/dist/cjs/types.d.ts.map +1 -0
  23. package/dist/cjs/types.js +10 -0
  24. package/dist/cjs/utils/getParams.d.ts +3 -0
  25. package/dist/cjs/utils/getParams.d.ts.map +1 -0
  26. package/dist/cjs/utils/getParams.js +70 -0
  27. package/dist/cjs/utils/isClickWithinBannerLink.d.ts +2 -0
  28. package/dist/cjs/utils/isClickWithinBannerLink.d.ts.map +1 -0
  29. package/dist/cjs/utils/isClickWithinBannerLink.js +21 -0
  30. package/dist/cjs/utils/isClickWithinProductLink.d.ts +5 -0
  31. package/dist/cjs/utils/isClickWithinProductLink.d.ts.map +1 -0
  32. package/dist/cjs/utils/isClickWithinProductLink.js +25 -0
  33. package/dist/esm/Abstract/AbstractController.d.ts +42 -0
  34. package/dist/esm/Abstract/AbstractController.d.ts.map +1 -0
  35. package/dist/esm/Abstract/AbstractController.js +208 -0
  36. package/dist/esm/Autocomplete/AutocompleteController.d.ts +59 -0
  37. package/dist/esm/Autocomplete/AutocompleteController.d.ts.map +1 -0
  38. package/dist/esm/Autocomplete/AutocompleteController.js +882 -0
  39. package/dist/esm/Finder/FinderController.d.ts +15 -0
  40. package/dist/esm/Finder/FinderController.d.ts.map +1 -0
  41. package/dist/esm/Finder/FinderController.js +218 -0
  42. package/dist/esm/Recommendation/RecommendationController.d.ts +27 -0
  43. package/dist/esm/Recommendation/RecommendationController.d.ts.map +1 -0
  44. package/dist/esm/Recommendation/RecommendationController.js +342 -0
  45. package/dist/esm/Search/SearchController.d.ts +41 -0
  46. package/dist/esm/Search/SearchController.d.ts.map +1 -0
  47. package/dist/esm/Search/SearchController.js +795 -0
  48. package/dist/esm/index.d.ts +7 -0
  49. package/dist/esm/index.d.ts.map +1 -0
  50. package/dist/esm/index.js +6 -0
  51. package/dist/esm/types.d.ts +87 -0
  52. package/dist/esm/types.d.ts.map +1 -0
  53. package/dist/esm/types.js +7 -0
  54. package/dist/esm/utils/getParams.d.ts +3 -0
  55. package/dist/esm/utils/getParams.d.ts.map +1 -0
  56. package/dist/esm/utils/getParams.js +67 -0
  57. package/dist/esm/utils/isClickWithinBannerLink.d.ts +2 -0
  58. package/dist/esm/utils/isClickWithinBannerLink.d.ts.map +1 -0
  59. package/dist/esm/utils/isClickWithinBannerLink.js +17 -0
  60. package/dist/esm/utils/isClickWithinProductLink.d.ts +5 -0
  61. package/dist/esm/utils/isClickWithinProductLink.d.ts.map +1 -0
  62. package/dist/esm/utils/isClickWithinProductLink.js +19 -0
  63. package/package.json +41 -0
@@ -0,0 +1,15 @@
1
+ import { AbstractController } from '../Abstract/AbstractController';
2
+ import { ControllerTypes } from '../types';
3
+ import type { FinderStore } from '@athoscommerce/snap-store-mobx';
4
+ import type { FinderControllerConfig, ControllerServices, ContextVariables } from '../types';
5
+ export declare class FinderController extends AbstractController {
6
+ type: ControllerTypes;
7
+ store: FinderStore;
8
+ config: FinderControllerConfig;
9
+ constructor(config: FinderControllerConfig, { client, store, urlManager, eventManager, profiler, logger, tracker }: ControllerServices, context?: ContextVariables);
10
+ get params(): Record<string, any>;
11
+ find: () => Promise<void>;
12
+ reset: () => void;
13
+ search: () => Promise<void>;
14
+ }
15
+ //# sourceMappingURL=FinderController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FinderController.d.ts","sourceRoot":"","sources":["../../../src/Finder/FinderController.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,KAAK,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAoB7F,qBAAa,gBAAiB,SAAQ,kBAAkB;IAChD,IAAI,kBAA0B;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,sBAAsB,CAAC;gBAGtC,MAAM,EAAE,sBAAsB,EAC9B,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAC1F,OAAO,CAAC,EAAE,gBAAgB;IAwB3B,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CA6BhC;IAED,IAAI,QAAa,OAAO,CAAC,IAAI,CAAC,CAkB5B;IAEF,KAAK,QAAO,IAAI,CAId;IAEF,MAAM,QAAa,OAAO,CAAC,IAAI,CAAC,CAoI9B;CACF"}
@@ -0,0 +1,218 @@
1
+ import deepmerge from 'deepmerge';
2
+ import { ErrorType } from '@athoscommerce/snap-store-mobx';
3
+ import { AbstractController } from '../Abstract/AbstractController';
4
+ import { getSearchParams } from '../utils/getParams';
5
+ import { ControllerTypes } from '../types';
6
+ const defaultConfig = {
7
+ id: 'finder',
8
+ beacon: {
9
+ enabled: true,
10
+ },
11
+ globals: {
12
+ pagination: {
13
+ pageSize: 0,
14
+ },
15
+ },
16
+ fields: [],
17
+ persist: {
18
+ enabled: false,
19
+ lockSelections: true,
20
+ expiration: 0,
21
+ },
22
+ };
23
+ export class FinderController extends AbstractController {
24
+ constructor(config, { client, store, urlManager, eventManager, profiler, logger, tracker }, context) {
25
+ super(config, { client, store, urlManager, eventManager, profiler, logger, tracker }, context);
26
+ this.type = ControllerTypes.finder;
27
+ this.find = async () => {
28
+ await this.store.save(); // save current selections to storage
29
+ try {
30
+ await this.eventManager.fire('beforeFind', {
31
+ controller: this,
32
+ });
33
+ // redirect to configured URL after middleware completes
34
+ window.location.href = this.urlManager.href;
35
+ }
36
+ catch (err) {
37
+ if (err?.message == 'cancelled') {
38
+ this.log.warn(`'beforeFind' middleware cancelled`);
39
+ }
40
+ else {
41
+ this.log.error(`error in 'beforeFind' middleware`);
42
+ this.log.error(err);
43
+ }
44
+ }
45
+ };
46
+ this.reset = () => {
47
+ this.store.reset();
48
+ this.urlManager.remove('filter').go();
49
+ this.store.setService('urlManager', this.urlManager);
50
+ };
51
+ this.search = async () => {
52
+ try {
53
+ if (!this.initialized) {
54
+ await this.init();
55
+ }
56
+ if (this.store.persisted) {
57
+ return;
58
+ }
59
+ const params = this.params;
60
+ this.store.loading = true;
61
+ try {
62
+ await this.eventManager.fire('beforeSearch', {
63
+ controller: this,
64
+ request: params,
65
+ });
66
+ }
67
+ catch (err) {
68
+ if (err?.message == 'cancelled') {
69
+ this.log.warn(`'beforeSearch' middleware cancelled`);
70
+ return;
71
+ }
72
+ else {
73
+ this.log.error(`error in 'beforeSearch' middleware`);
74
+ throw err;
75
+ }
76
+ }
77
+ const searchProfile = this.profiler.create({ type: 'event', name: 'search', context: params }).start();
78
+ const { meta, search } = await this.client.finder(params);
79
+ searchProfile.stop();
80
+ this.log.profile(searchProfile);
81
+ const afterSearchProfile = this.profiler.create({ type: 'event', name: 'afterSearch', context: params }).start();
82
+ try {
83
+ await this.eventManager.fire('afterSearch', {
84
+ controller: this,
85
+ request: params,
86
+ response: {
87
+ meta,
88
+ search,
89
+ },
90
+ });
91
+ }
92
+ catch (err) {
93
+ if (err?.message == 'cancelled') {
94
+ this.log.warn(`'afterSearch' middleware cancelled`);
95
+ afterSearchProfile.stop();
96
+ return;
97
+ }
98
+ else {
99
+ this.log.error(`error in 'afterSearch' middleware`);
100
+ throw err;
101
+ }
102
+ }
103
+ afterSearchProfile.stop();
104
+ this.log.profile(afterSearchProfile);
105
+ // update the store
106
+ this.store.update({ meta, search });
107
+ const afterStoreProfile = this.profiler.create({ type: 'event', name: 'afterStore', context: params }).start();
108
+ try {
109
+ await this.eventManager.fire('afterStore', {
110
+ controller: this,
111
+ request: params,
112
+ response: {
113
+ meta,
114
+ search,
115
+ },
116
+ });
117
+ }
118
+ catch (err) {
119
+ if (err?.message == 'cancelled') {
120
+ this.log.warn(`'afterStore' middleware cancelled`);
121
+ afterStoreProfile.stop();
122
+ return;
123
+ }
124
+ else {
125
+ this.log.error(`error in 'afterStore' middleware`);
126
+ throw err;
127
+ }
128
+ }
129
+ afterStoreProfile.stop();
130
+ this.log.profile(afterStoreProfile);
131
+ }
132
+ catch (err) {
133
+ if (err) {
134
+ if (err.err && err.fetchDetails) {
135
+ switch (err.fetchDetails.status) {
136
+ case 429: {
137
+ this.store.error = {
138
+ code: 429,
139
+ type: ErrorType.WARNING,
140
+ message: 'Too many requests try again later',
141
+ };
142
+ break;
143
+ }
144
+ case 500: {
145
+ this.store.error = {
146
+ code: 500,
147
+ type: ErrorType.ERROR,
148
+ message: 'Invalid Search Request or Service Unavailable',
149
+ };
150
+ break;
151
+ }
152
+ default: {
153
+ this.store.error = {
154
+ type: ErrorType.ERROR,
155
+ message: err.err.message,
156
+ };
157
+ break;
158
+ }
159
+ }
160
+ this.log.error(this.store.error);
161
+ this.handleError(err.err, err.fetchDetails);
162
+ }
163
+ else {
164
+ this.store.error = {
165
+ type: ErrorType.ERROR,
166
+ message: `Something went wrong... - ${err}`,
167
+ };
168
+ this.log.error(err);
169
+ this.handleError(err);
170
+ }
171
+ }
172
+ }
173
+ finally {
174
+ this.store.loading = false;
175
+ }
176
+ };
177
+ // deep merge config with defaults
178
+ this.config = deepmerge(defaultConfig, this.config);
179
+ this.store.setConfig(this.config);
180
+ // set the root URL on urlManager
181
+ if (this.config.url) {
182
+ this.urlManager = this.urlManager.withConfig((translatorConfig) => {
183
+ return {
184
+ ...translatorConfig,
185
+ urlRoot: this.config.url,
186
+ };
187
+ });
188
+ }
189
+ // attach config plugins and event middleware
190
+ this.use(this.config);
191
+ this.store.loadPersisted();
192
+ }
193
+ get params() {
194
+ const urlState = this.urlManager.state;
195
+ const { userId, sessionId, pageLoadId } = this.tracker.getContext();
196
+ const tracking = {};
197
+ if (userId) {
198
+ tracking.userId = userId;
199
+ }
200
+ if (sessionId) {
201
+ tracking.sessionId = sessionId;
202
+ }
203
+ if (pageLoadId) {
204
+ tracking.pageLoadId = pageLoadId;
205
+ }
206
+ tracking.domain = window.location.href;
207
+ // get only the finder fields and disable auto drill down
208
+ const defaultParams = {
209
+ facets: {
210
+ include: this.config.fields.map((fieldConfig) => fieldConfig.field),
211
+ autoDrillDown: false,
212
+ },
213
+ tracking: tracking,
214
+ };
215
+ const params = deepmerge({ ...getSearchParams(urlState) }, deepmerge(defaultParams, this.config.globals));
216
+ return params;
217
+ }
218
+ }
@@ -0,0 +1,27 @@
1
+ import { Product } from '@athoscommerce/snap-store-mobx';
2
+ import { AbstractController } from '../Abstract/AbstractController';
3
+ import { ControllerTypes } from '../types';
4
+ import type { Banner, RecommendationStore } from '@athoscommerce/snap-store-mobx';
5
+ import type { RecommendRequestModel } from '@athoscommerce/snap-client';
6
+ import type { RecommendationControllerConfig, ControllerServices, ContextVariables } from '../types';
7
+ type RecommendationTrackMethods = {
8
+ product: {
9
+ clickThrough: (e: MouseEvent, result: Product | Banner) => void;
10
+ click: (e: MouseEvent, result: Product | Banner) => void;
11
+ impression: (result: Product | Banner) => void;
12
+ addToCart: (result: Product) => void;
13
+ };
14
+ };
15
+ export declare class RecommendationController extends AbstractController {
16
+ type: ControllerTypes;
17
+ store: RecommendationStore;
18
+ config: RecommendationControllerConfig;
19
+ private events;
20
+ constructor(config: RecommendationControllerConfig, { client, store, urlManager, eventManager, profiler, logger, tracker }: ControllerServices, context?: ContextVariables);
21
+ track: RecommendationTrackMethods;
22
+ get params(): RecommendRequestModel;
23
+ search: () => Promise<void>;
24
+ addToCart: (_products: Product[] | Product) => Promise<void>;
25
+ }
26
+ export {};
27
+ //# sourceMappingURL=RecommendationController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecommendationController.d.ts","sourceRoot":"","sources":["../../../src/Recommendation/RecommendationController.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,OAAO,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAW3C,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAClF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,KAAK,EAAE,8BAA8B,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGrG,KAAK,0BAA0B,GAAG;IACjC,OAAO,EAAE;QACR,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QACzD,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QAC/C,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;KACrC,CAAC;CACF,CAAC;AAaF,qBAAa,wBAAyB,SAAQ,kBAAkB;IACxD,IAAI,kBAAkC;IACrC,KAAK,EAAE,mBAAmB,CAAC;IAC3B,MAAM,EAAE,8BAA8B,CAAC;IAE/C,OAAO,CAAC,MAAM,CAUP;gBAGN,MAAM,EAAE,8BAA8B,EACtC,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAC1F,OAAO,CAAC,EAAE,gBAAgB;IAwC3B,KAAK,EAAE,0BAA0B,CAyI/B;IAEF,IAAI,MAAM,IAAI,qBAAqB,CA4BlC;IAED,MAAM,QAAa,OAAO,CAAC,IAAI,CAAC,CAoI9B;IAEF,SAAS,cAAqB,OAAO,EAAE,GAAG,OAAO,KAAG,OAAO,CAAC,IAAI,CAAC,CAY/D;CACF"}
@@ -0,0 +1,342 @@
1
+ import deepmerge from 'deepmerge';
2
+ import { ErrorType } from '@athoscommerce/snap-store-mobx';
3
+ import { AbstractController } from '../Abstract/AbstractController';
4
+ import { ControllerTypes } from '../types';
5
+ import { CLICK_DUPLICATION_TIMEOUT, isClickWithinProductLink } from '../utils/isClickWithinProductLink';
6
+ const defaultConfig = {
7
+ id: 'recommend',
8
+ beacon: {
9
+ enabled: true,
10
+ },
11
+ tag: '',
12
+ batched: true,
13
+ realtime: false,
14
+ globals: {},
15
+ };
16
+ export class RecommendationController extends AbstractController {
17
+ constructor(config, { client, store, urlManager, eventManager, profiler, logger, tracker }, context) {
18
+ super(config, { client, store, urlManager, eventManager, profiler, logger, tracker }, context);
19
+ this.type = ControllerTypes.recommendation;
20
+ this.events = {};
21
+ this.track = {
22
+ product: {
23
+ clickThrough: (e, result) => {
24
+ if (!result) {
25
+ this.log.warn('No result provided to track.product.clickThrough');
26
+ return;
27
+ }
28
+ const responseId = result.responseId;
29
+ if (!this.events[responseId]) {
30
+ this.log.warn('No responseId found in controller, ensure correct controller is used');
31
+ return;
32
+ }
33
+ if (this.events[responseId]?.product[result.id]?.productClickThrough)
34
+ return;
35
+ const type = (['product', 'banner'].includes(result.type) ? result.type : 'product');
36
+ const beaconResult = {
37
+ type,
38
+ uid: result.id ? '' + result.id : '',
39
+ ...(type === 'product'
40
+ ? {
41
+ parentId: result.mappings.core?.parentId ? '' + result.mappings.core?.parentId : '',
42
+ sku: result.mappings.core?.sku ? '' + result.mappings.core?.sku : undefined,
43
+ }
44
+ : {}),
45
+ };
46
+ const data = {
47
+ tag: this.store.profile.tag,
48
+ responseId,
49
+ results: [beaconResult],
50
+ };
51
+ this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, product: result, trackEvent: data });
52
+ this.config.beacon?.enabled && this.tracker.events.recommendations.clickThrough({ data, siteId: this.config.globals?.siteId });
53
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
54
+ this.events[responseId].product[result.id].productClickThrough = true;
55
+ },
56
+ click: (e, result) => {
57
+ if (!result) {
58
+ this.log.warn('No result provided to track.product.click');
59
+ return;
60
+ }
61
+ const responseId = result.responseId;
62
+ if (!this.events[responseId]) {
63
+ this.log.warn('No responseId found in controller, ensure correct controller is used');
64
+ return;
65
+ }
66
+ if (result.type === 'banner') {
67
+ if (this.events[responseId]?.product[result.id]?.inlineBannerClickThrough) {
68
+ return;
69
+ }
70
+ this.track.product.clickThrough(e, result);
71
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
72
+ this.events[responseId].product[result.id].inlineBannerClickThrough = true;
73
+ setTimeout(() => {
74
+ this.events[responseId].product[result.id].inlineBannerClickThrough = false;
75
+ }, CLICK_DUPLICATION_TIMEOUT);
76
+ }
77
+ else if (isClickWithinProductLink(e, result)) {
78
+ if (this.events?.[responseId]?.product[result.id]?.productClickThrough) {
79
+ return;
80
+ }
81
+ this.track.product.clickThrough(e, result);
82
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
83
+ this.events[responseId].product[result.id].productClickThrough = true;
84
+ setTimeout(() => {
85
+ this.events[responseId].product[result.id].productClickThrough = false;
86
+ }, CLICK_DUPLICATION_TIMEOUT);
87
+ }
88
+ },
89
+ impression: (result) => {
90
+ if (!result) {
91
+ this.log.warn('No result provided to track.product.impression');
92
+ return;
93
+ }
94
+ const responseId = result.responseId;
95
+ if (!this.events[responseId]) {
96
+ this.log.warn('No responseId found in controller, ensure correct controller is used');
97
+ return;
98
+ }
99
+ else if (this.events[responseId]?.product[result.id]?.impression) {
100
+ return;
101
+ }
102
+ const type = (['product', 'banner'].includes(result.type) ? result.type : 'product');
103
+ const item = {
104
+ type,
105
+ uid: result.id ? '' + result.id : '',
106
+ ...(type === 'product'
107
+ ? {
108
+ parentId: result.mappings.core?.parentId ? '' + result.mappings.core?.parentId : '',
109
+ sku: result.mappings.core?.sku ? '' + result.mappings.core?.sku : undefined,
110
+ }
111
+ : {}),
112
+ };
113
+ const data = {
114
+ tag: this.store.profile.tag,
115
+ responseId,
116
+ results: [item],
117
+ banners: [],
118
+ };
119
+ this.eventManager.fire('track.product.impression', { controller: this, product: result, trackEvent: data });
120
+ this.config.beacon?.enabled && this.tracker.events.recommendations.impression({ data, siteId: this.config.globals?.siteId });
121
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
122
+ this.events[responseId].product[result.id].impression = true;
123
+ },
124
+ addToCart: (result) => {
125
+ if (!result) {
126
+ this.log.warn('No result provided to track.product.addToCart');
127
+ return;
128
+ }
129
+ const responseId = result.responseId;
130
+ if (!this.events[responseId]) {
131
+ this.log.warn('No responseId found in controller, ensure correct controller is used');
132
+ return;
133
+ }
134
+ const product = {
135
+ parentId: result.mappings.core?.parentId ? '' + result.mappings.core?.parentId : '',
136
+ uid: result.id,
137
+ sku: result.mappings.core?.sku,
138
+ qty: result.quantity || 1,
139
+ price: Number(result.mappings.core?.price),
140
+ };
141
+ const data = {
142
+ responseId,
143
+ tag: this.store.profile.tag,
144
+ results: [product],
145
+ };
146
+ this.eventManager.fire('track.product.addToCart', { controller: this, product: result, trackEvent: data });
147
+ this.config.beacon?.enabled && this.tracker.events.recommendations.addToCart({ data, siteId: this.config.globals?.siteId });
148
+ },
149
+ },
150
+ };
151
+ this.search = async () => {
152
+ try {
153
+ if (!this.initialized) {
154
+ await this.init();
155
+ }
156
+ const params = this.params;
157
+ this.store.loading = true;
158
+ try {
159
+ await this.eventManager.fire('beforeSearch', {
160
+ controller: this,
161
+ request: params,
162
+ });
163
+ }
164
+ catch (err) {
165
+ if (err?.message == 'cancelled') {
166
+ this.log.warn(`'beforeSearch' middleware cancelled`);
167
+ return;
168
+ }
169
+ else {
170
+ this.log.error(`error in 'beforeSearch' middleware`);
171
+ throw err;
172
+ }
173
+ }
174
+ const searchProfile = this.profiler.create({ type: 'event', name: 'search', context: params }).start();
175
+ const { meta, profile, results, responseId } = await this.client.recommend(params);
176
+ searchProfile.stop();
177
+ this.log.profile(searchProfile);
178
+ this.events[responseId] = this.events[responseId] || { product: {} };
179
+ const afterSearchProfile = this.profiler.create({ type: 'event', name: 'afterSearch', context: params }).start();
180
+ try {
181
+ await this.eventManager.fire('afterSearch', {
182
+ controller: this,
183
+ request: params,
184
+ response: { meta, profile, results },
185
+ });
186
+ }
187
+ catch (err) {
188
+ if (err?.message == 'cancelled') {
189
+ this.log.warn(`'afterSearch' middleware cancelled`);
190
+ afterSearchProfile.stop();
191
+ return;
192
+ }
193
+ else {
194
+ this.log.error(`error in 'afterSearch' middleware`);
195
+ throw err;
196
+ }
197
+ }
198
+ afterSearchProfile.stop();
199
+ this.log.profile(afterSearchProfile);
200
+ // update the store
201
+ this.store.update({
202
+ meta: meta,
203
+ profile: profile,
204
+ results: results,
205
+ responseId: responseId,
206
+ });
207
+ const data = { responseId, tag: this.store.profile.tag };
208
+ this.config.beacon?.enabled && this.tracker.events.recommendations.render({ data, siteId: this.config.globals?.siteId });
209
+ const afterStoreProfile = this.profiler.create({ type: 'event', name: 'afterStore', context: params }).start();
210
+ try {
211
+ await this.eventManager.fire('afterStore', {
212
+ controller: this,
213
+ request: params,
214
+ response: { meta, profile, results, responseId },
215
+ });
216
+ }
217
+ catch (err) {
218
+ if (err?.message == 'cancelled') {
219
+ this.log.warn(`'afterStore' middleware cancelled`);
220
+ afterStoreProfile.stop();
221
+ return;
222
+ }
223
+ else {
224
+ this.log.error(`error in 'afterStore' middleware`);
225
+ throw err;
226
+ }
227
+ }
228
+ afterStoreProfile.stop();
229
+ this.log.profile(afterStoreProfile);
230
+ }
231
+ catch (err) {
232
+ if (err) {
233
+ if (err.err && err.fetchDetails) {
234
+ switch (err.fetchDetails.status) {
235
+ case 429: {
236
+ this.store.error = {
237
+ code: 429,
238
+ type: ErrorType.WARNING,
239
+ message: 'Too many requests try again later',
240
+ };
241
+ break;
242
+ }
243
+ case 500: {
244
+ this.store.error = {
245
+ code: 500,
246
+ type: ErrorType.ERROR,
247
+ message: 'Invalid Search Request or Service Unavailable',
248
+ };
249
+ break;
250
+ }
251
+ default: {
252
+ this.store.error = {
253
+ type: ErrorType.ERROR,
254
+ message: err.err.message,
255
+ };
256
+ break;
257
+ }
258
+ }
259
+ this.log.error(this.store.error);
260
+ this.handleError(err.err, err.fetchDetails);
261
+ }
262
+ else {
263
+ this.store.error = {
264
+ type: ErrorType.ERROR,
265
+ message: `Something went wrong... - ${err}`,
266
+ };
267
+ this.log.error(err);
268
+ this.handleError(err);
269
+ }
270
+ }
271
+ }
272
+ finally {
273
+ this.store.loading = false;
274
+ }
275
+ };
276
+ this.addToCart = async (_products) => {
277
+ const products = typeof _products?.slice == 'function' ? _products.slice() : [_products];
278
+ if (!_products || products.length === 0) {
279
+ this.log.warn('No products provided to recommendation controller.addToCart');
280
+ return;
281
+ }
282
+ products.forEach((product) => {
283
+ this.track.product.addToCart(product);
284
+ });
285
+ if (products.length > 0) {
286
+ this.eventManager.fire('addToCart', { controller: this, products });
287
+ }
288
+ };
289
+ if (!config.tag) {
290
+ throw new Error(`Invalid config passed to RecommendationController. The "tag" attribute is required.`);
291
+ }
292
+ // attach to bfCache restore event and re-run search on the controller
293
+ // enabled by default
294
+ if (config.settings?.searchOnPageShow !== false) {
295
+ window.addEventListener('pageshow', (e) => {
296
+ if (e.persisted && !this.store.error && this.store.loaded && !this.store.loading) {
297
+ this.search();
298
+ }
299
+ });
300
+ }
301
+ // deep merge config with defaults
302
+ this.config = deepmerge(defaultConfig, this.config);
303
+ this.store.setConfig(this.config);
304
+ // add 'afterStore' middleware
305
+ // this.eventManager.on('afterStore', async (recommend: AfterStoreObj, next: Next): Promise<void | boolean> => {
306
+ // await next();
307
+ // // attach tracking events to cart store
308
+ // this.store.cart?.on('addItems', ({ items }: { items: Product[] }) => {
309
+ // // add to bundle
310
+ // });
311
+ // this.store.cart?.on('removeItems', ({ items }: { items: Product[] }) => {
312
+ // // remove from bundle
313
+ // });
314
+ // });
315
+ // attach config plugins and event middleware
316
+ this.use(this.config);
317
+ }
318
+ get params() {
319
+ const params = {
320
+ tag: this.config.tag,
321
+ batched: this.config.batched,
322
+ branch: this.config.branch || 'production',
323
+ batchId: this.config.batchId,
324
+ ...this.config.globals,
325
+ };
326
+ const { shopperId } = this.tracker.getContext();
327
+ if (shopperId) {
328
+ params.shopper = shopperId;
329
+ }
330
+ if (!params.siteId || params.siteId == this.tracker.getGlobals().siteId) {
331
+ const cart = this.tracker.cookies.cart.get();
332
+ const lastViewed = this.tracker.cookies.viewed.get();
333
+ if (cart?.length) {
334
+ params.cart = cart;
335
+ }
336
+ if (lastViewed?.length) {
337
+ params.lastViewed = lastViewed;
338
+ }
339
+ }
340
+ return params;
341
+ }
342
+ }
@@ -0,0 +1,41 @@
1
+ import { AbstractController } from '../Abstract/AbstractController';
2
+ import { StorageStore, MerchandisingContentBanner } from '@athoscommerce/snap-store-mobx';
3
+ import { ControllerTypes } from '../types';
4
+ import type { Product, Banner, SearchStore } from '@athoscommerce/snap-store-mobx';
5
+ import type { SearchControllerConfig, ControllerServices, ContextVariables } from '../types';
6
+ import { type SearchRequestModel } from '@athoscommerce/snapi-types';
7
+ type SearchTrackMethods = {
8
+ banner: {
9
+ click: (e: MouseEvent, merchandisingBanner: MerchandisingContentBanner) => void;
10
+ clickThrough: (e: MouseEvent, merchandisingBanner: MerchandisingContentBanner) => void;
11
+ impression: (merchandisingBanner: MerchandisingContentBanner) => void;
12
+ };
13
+ product: {
14
+ clickThrough: (e: MouseEvent, result: Product | Banner) => void;
15
+ click: (e: MouseEvent, result: Product | Banner) => void;
16
+ impression: (result: Product | Banner) => void;
17
+ addToCart: (results: Product) => void;
18
+ };
19
+ redirect: ({ redirectURL, responseId }: {
20
+ redirectURL: string;
21
+ responseId: string;
22
+ }) => void;
23
+ };
24
+ export declare class SearchController extends AbstractController {
25
+ type: ControllerTypes;
26
+ store: SearchStore;
27
+ config: SearchControllerConfig;
28
+ storage: StorageStore;
29
+ private previousResults;
30
+ private page;
31
+ private events;
32
+ constructor(config: SearchControllerConfig, { client, store, urlManager, eventManager, profiler, logger, tracker }: ControllerServices, context?: ContextVariables);
33
+ track: SearchTrackMethods;
34
+ get params(): SearchRequestModel;
35
+ search: () => Promise<void>;
36
+ addToCart: (_products: Product[] | Product) => Promise<void>;
37
+ }
38
+ export declare function getStorableRequestParams(request: SearchRequestModel): SearchRequestModel;
39
+ export declare function generateHrefSelector(element: HTMLElement, href: string, levels?: number): string | undefined;
40
+ export {};
41
+ //# sourceMappingURL=SearchController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SearchController.d.ts","sourceRoot":"","sources":["../../../src/Search/SearchController.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,YAAY,EAAa,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAErG,OAAO,EAAE,eAAe,EAAuB,MAAM,UAAU,CAAC;AAEhE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAiC,MAAM,gCAAgC,CAAC;AAClH,OAAO,KAAK,EACX,sBAAsB,EAGtB,kBAAkB,EAClB,gBAAgB,EAIhB,MAAM,UAAU,CAAC;AAElB,OAAO,EACN,KAAK,kBAAkB,EAYvB,MAAM,4BAA4B,CAAC;AAyCpC,KAAK,kBAAkB,GAAG;IACzB,MAAM,EAAE;QACP,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,mBAAmB,EAAE,0BAA0B,KAAK,IAAI,CAAC;QAChF,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,mBAAmB,EAAE,0BAA0B,KAAK,IAAI,CAAC;QACvF,UAAU,EAAE,CAAC,mBAAmB,EAAE,0BAA0B,KAAK,IAAI,CAAC;KACtE,CAAC;IACF,OAAO,EAAE;QACR,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QACzD,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QAC/C,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;KACtC,CAAC;IACF,QAAQ,EAAE,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC7F,CAAC;AAEF,qBAAa,gBAAiB,SAAQ,kBAAkB;IAChD,IAAI,kBAA0B;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,sBAAsB,CAAC;IACvC,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,CAAC,eAAe,CAAwC;IAC/D,OAAO,CAAC,IAAI,CAEV;IACF,OAAO,CAAC,MAAM,CAiBP;gBAGN,MAAM,EAAE,sBAAsB,EAC9B,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAC1F,OAAO,CAAC,EAAE,gBAAgB;IA2Q3B,KAAK,EAAE,kBAAkB,CA0PvB;IAEF,IAAI,MAAM,IAAI,kBAAkB,CA8C/B;IAED,MAAM,QAAa,OAAO,CAAC,IAAI,CAAC,CAiO9B;IAEF,SAAS,cAAqB,OAAO,EAAE,GAAG,OAAO,KAAG,OAAO,CAAC,IAAI,CAAC,CAY/D;CACF;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,kBAAkB,GAAG,kBAAkB,CAiBxF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,SAAI,GAAG,MAAM,GAAG,SAAS,CAuCvG"}