musora-content-services 1.0.120 → 1.0.122

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 (59) hide show
  1. package/.github/workflows/node.js.yml +0 -0
  2. package/CHANGELOG.md +4 -0
  3. package/README.md +0 -0
  4. package/babel.config.js +0 -0
  5. package/docs/config.js.html +7 -3
  6. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  7. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  8. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  9. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  10. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  11. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  12. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  13. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  14. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  15. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
  16. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  17. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  18. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  19. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  20. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
  21. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  22. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  23. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  24. package/docs/index.html +1 -1
  25. package/docs/module-Config.html +28 -3
  26. package/docs/module-Railcontent-Services.html +7 -7
  27. package/docs/module-Sanity-Services.html +426 -48
  28. package/docs/railcontent.js.html +37 -47
  29. package/docs/sanity.js.html +69 -12
  30. package/docs/scripts/collapse.js +0 -0
  31. package/docs/scripts/commonNav.js +0 -0
  32. package/docs/scripts/linenumber.js +0 -0
  33. package/docs/scripts/nav.js +0 -0
  34. package/docs/scripts/polyfill.js +0 -0
  35. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
  36. package/docs/scripts/prettify/lang-css.js +0 -0
  37. package/docs/scripts/prettify/prettify.js +0 -0
  38. package/docs/scripts/search.js +0 -0
  39. package/docs/styles/jsdoc.css +0 -0
  40. package/docs/styles/prettify.css +0 -0
  41. package/jest.config.js +0 -0
  42. package/jsdoc.json +0 -0
  43. package/package.json +1 -1
  44. package/src/contentMetaData.js +11 -0
  45. package/src/contentTypeConfig.js +6 -2
  46. package/src/index.d.ts +11 -21
  47. package/src/index.js +11 -21
  48. package/src/services/config.js +6 -2
  49. package/src/services/contentLikes.js +38 -0
  50. package/src/services/dataContext.js +88 -0
  51. package/src/services/railcontent.js +36 -46
  52. package/src/services/sanity.js +64 -10
  53. package/test/contentLikes.test.js +86 -0
  54. package/test/localStorageMock.js +0 -0
  55. package/test/log.js +0 -0
  56. package/test/sanityQueryService.test.js +39 -0
  57. package/tools/generate-index.js +0 -0
  58. package/src/services/userContext.js +0 -105
  59. package/test/userContext.test.js +0 -86
package/src/index.js CHANGED
@@ -5,6 +5,12 @@ import {
5
5
  initializeService
6
6
  } from './services/config.js';
7
7
 
8
+ import {
9
+ isContentLiked,
10
+ likeContent,
11
+ unlikeContent
12
+ } from './services/contentLikes.js';
13
+
8
14
  import {
9
15
  fetchAllCompletedStates,
10
16
  fetchCompletedContent,
@@ -12,10 +18,9 @@ import {
12
18
  fetchContentInProgress,
13
19
  fetchContentPageUserData,
14
20
  fetchHandler,
15
- fetchLikeContent,
16
21
  fetchSongsInProgress,
17
- fetchUnlikeContent,
18
- fetchUserContext,
22
+ fetchUserLikes,
23
+ fetchUserPermissions,
19
24
  fetchVimeoData
20
25
  } from './services/railcontent.js';
21
26
 
@@ -65,18 +70,7 @@ import {
65
70
  getSortOrder
66
71
  } from './services/sanity.js';
67
72
 
68
- import {
69
- clearCache,
70
- fetchContentData,
71
- init,
72
- likeContent,
73
- testClearLocal,
74
- unlikeContent,
75
- version
76
- } from './services/userContext.js';
77
-
78
73
  export {
79
- clearCache,
80
74
  fetchAll,
81
75
  fetchAllCompletedStates,
82
76
  fetchAllFilterOptions,
@@ -93,7 +87,6 @@ export {
93
87
  fetchCoachLessons,
94
88
  fetchCompletedContent,
95
89
  fetchCompletedState,
96
- fetchContentData,
97
90
  fetchContentInProgress,
98
91
  fetchContentPageUserData,
99
92
  fetchCourseOverview,
@@ -101,7 +94,6 @@ export {
101
94
  fetchGenreLessons,
102
95
  fetchHandler,
103
96
  fetchLessonContent,
104
- fetchLikeContent,
105
97
  fetchLiveEvent,
106
98
  fetchMetadata,
107
99
  fetchMethod,
@@ -126,17 +118,15 @@ export {
126
118
  fetchSongCount,
127
119
  fetchSongFilterOptions,
128
120
  fetchSongsInProgress,
129
- fetchUnlikeContent,
130
121
  fetchUpcomingEvents,
131
- fetchUserContext,
122
+ fetchUserLikes,
123
+ fetchUserPermissions,
132
124
  fetchVimeoData,
133
125
  fetchWorkouts,
134
126
  getSortOrder,
135
127
  globalConfig,
136
- init,
137
128
  initializeService,
129
+ isContentLiked,
138
130
  likeContent,
139
- testClearLocal,
140
131
  unlikeContent,
141
- version,
142
132
  };
@@ -4,7 +4,8 @@
4
4
 
5
5
  let globalConfig = {
6
6
  sanityConfig: {},
7
- railcontentConfig: {}
7
+ railcontentConfig: {},
8
+ localStorage: null
8
9
  };
9
10
 
10
11
  /**
@@ -24,6 +25,7 @@ let globalConfig = {
24
25
  * @param {string} config.railcontentConfig.token - The token for authenticating user-specific requests.
25
26
  * @param {string} config.railcontentConfig.userId - The user ID for fetching user-specific data.
26
27
  * @param {string} config.railcontentConfig.baseUrl - The url for the enviroment.
28
+ * @param {Object} config.localStorage - Cache to use for localStorage
27
29
 
28
30
  *
29
31
  * @example
@@ -41,12 +43,14 @@ let globalConfig = {
41
43
  * token: 'your-user-api-token',
42
44
  * userId: 'current-user-id',
43
45
  * baseUrl: 'https://web-staging-one.musora.com'
44
- * }
46
+ * },
47
+ * localStorage: localStorage
45
48
  * });
46
49
  */
47
50
  function initializeService(config) {
48
51
  globalConfig.sanityConfig = config.sanityConfig;
49
52
  globalConfig.railcontentConfig = config.railcontentConfig;
53
+ globalConfig.localStorage = config.localStorage;
50
54
  }
51
55
 
52
56
  // Export both the initialization function and the config object
@@ -0,0 +1,38 @@
1
+ import {fetchUserLikes, postContentLiked, postContentUnliked} from "./railcontent";
2
+ import {DataContext, ContentVersionKey} from "./dataContext";
3
+
4
+ export let dataContext = new DataContext(ContentVersionKey, fetchUserLikes);
5
+
6
+ export async function isContentLiked(contentId) {
7
+ let data = await dataContext.getData();
8
+ return data.includes(contentId);
9
+ }
10
+
11
+ export async function likeContent(contentId) {
12
+ await dataContext.update(
13
+ function (context) {
14
+ if (!context.data.includes(contentId)) {
15
+ context.data.push(contentId);
16
+ }
17
+ },
18
+ async function(){
19
+ return postContentLiked(contentId);
20
+ }
21
+ );
22
+ }
23
+
24
+ export async function unlikeContent(contentId) {
25
+ await dataContext.update(
26
+ function (context) {
27
+ if (context.data.includes(contentId)) {
28
+ const index = context.data.indexOf(contentId);
29
+ if (index > -1) { // only splice array when item is found
30
+ context.data.splice(index, 1); // 2nd parameter means remove one item only
31
+ }
32
+ }
33
+ },
34
+ async function(){
35
+ return postContentUnliked(contentId);
36
+ }
37
+ );
38
+ }
@@ -0,0 +1,88 @@
1
+ import {globalConfig} from "./config";
2
+
3
+ //These constants need to match MWP UserDataVersionKeyEnum enum
4
+ export const ContentVersionKey = 0;
5
+
6
+ let cache = null;
7
+
8
+ export class DataContext {
9
+ context = null;
10
+
11
+ constructor(dataVersionKey, fetchDataFunction) {
12
+ this.dataVersionKey = dataVersionKey;
13
+ this.fetchDataFunction = fetchDataFunction;
14
+ this.localStorageKey = `dataContext_${this.dataVersionKey}`;
15
+ this.localStorageLastUpdatedKey = `dataContext_${this.dataVersionKey}_lastUpdated`;
16
+ }
17
+
18
+ async getData() {
19
+ this.ensureLocalContextLoaded();
20
+ if (!this.context || this.shouldVerifyServerVerions()) {
21
+ let version = this.version();
22
+ let data = await this.fetchData(version);
23
+ if (data.version !== "No Change") {
24
+ this.context = data;
25
+ cache.setItem(this.localStorageKey, JSON.stringify(data));
26
+ }
27
+ cache.setItem(this.localStorageLastUpdatedKey, new Date().getTime());
28
+ }
29
+ return this.context.data;
30
+ }
31
+
32
+ async fetchData(version) {
33
+ return await this.fetchDataFunction(version);
34
+ }
35
+
36
+ ensureLocalContextLoaded() {
37
+ if (this.context) return;
38
+ this.verifyConfig();
39
+ let localData = cache.getItem(this.localStorageKey);
40
+ if (localData) {
41
+ this.context = JSON.parse(localData);
42
+ }
43
+ }
44
+
45
+ verifyConfig() {
46
+ if (!cache) {
47
+ cache = globalConfig.localStorage;
48
+ if (!cache) throw new Error('dataContext: LocalStorage cache not configured in musora content services initializeService.');
49
+ }
50
+ }
51
+
52
+ shouldVerifyServerVerions() {
53
+ let lastUpdated = cache.getItem(this.localStorageLastUpdatedKey);
54
+ if (!lastUpdated) return false;
55
+ const verifyServerTime = 10000; //10 s
56
+ return (new Date().getTime() - lastUpdated) > verifyServerTime;
57
+ }
58
+
59
+ clearCache() {
60
+ this.clearContext();
61
+ cache.removeItem(this.localStorageKey);
62
+ cache.removeItem(this.localStorageLastUpdatedKey);
63
+ }
64
+
65
+ clearContext() {
66
+ this.context = null;
67
+ }
68
+
69
+ async update(localUpdateFunction, serverUpdateFunction) {
70
+ this.ensureLocalContextLoaded();
71
+ if (this.context) {
72
+ localUpdateFunction(this.context);
73
+ this.context.version++;
74
+ let data = JSON.stringify(this.context);
75
+ cache.setItem(this.localStorageKey, data);
76
+ cache.setItem(this.localStorageLastUpdatedKey, new Date().getTime());
77
+ }
78
+ let response = await serverUpdateFunction();
79
+ if (response.version !== this.version()) {
80
+ this.clearCache();
81
+ }
82
+ }
83
+
84
+ version() {
85
+ return this.context?.version ?? -1;
86
+ }
87
+
88
+ }
@@ -2,7 +2,7 @@
2
2
  * @module Railcontent-Services
3
3
  */
4
4
 
5
- const { globalConfig } = require('./config');
5
+ const {globalConfig} = require('./config');
6
6
 
7
7
 
8
8
  /**
@@ -24,10 +24,11 @@ export async function fetchCompletedState(content_id) {
24
24
  };
25
25
 
26
26
  try {
27
- const response = await fetchAbsolute(url, { headers });
27
+ const response = await fetchAbsolute(url, {headers});
28
28
  const result = await response.json();
29
29
 
30
- if (result && result[content_id]) { return result[content_id]; // Return the correct object
30
+ if (result && result[content_id]) {
31
+ return result[content_id]; // Return the correct object
31
32
  } else {
32
33
  return null; // Handle unexpected structure
33
34
  }
@@ -56,7 +57,7 @@ export async function fetchVimeoData(vimeo_id) {
56
57
  };
57
58
 
58
59
  try {
59
- const response = await fetchAbsolute(url, { headers });
60
+ const response = await fetchAbsolute(url, {headers});
60
61
  const result = await response.json();
61
62
 
62
63
  if (result) {
@@ -91,9 +92,9 @@ export async function fetchAllCompletedStates(contentIds) {
91
92
  };
92
93
 
93
94
  try {
94
- const response = await fetchAbsolute(url, { headers });
95
+ const response = await fetchAbsolute(url, {headers});
95
96
  const result = await response.json();
96
- if(result){
97
+ if (result) {
97
98
  return result;
98
99
  } else {
99
100
  console.log('result not json');
@@ -123,9 +124,9 @@ export async function fetchSongsInProgress(brand) {
123
124
  };
124
125
 
125
126
  try {
126
- const response = await fetchAbsolute(url, { headers });
127
+ const response = await fetchAbsolute(url, {headers});
127
128
  const result = await response.json();
128
- if(result){
129
+ if (result) {
129
130
  //console.log('fetchSongsInProgress', result);
130
131
  return result;
131
132
  } else {
@@ -150,12 +151,12 @@ export async function fetchSongsInProgress(brand) {
150
151
  * .then(songs => console.log(songs))
151
152
  * .catch(error => console.error(error));
152
153
  */
153
- export async function fetchContentInProgress(type="all", brand, { page, limit } = {}) {
154
+ export async function fetchContentInProgress(type = "all", brand, {page, limit} = {}) {
154
155
  let url;
155
156
  const limitString = limit ? `&limit=${limit}` : '';
156
157
  const pageString = page ? `&page=${page}` : '';
157
158
 
158
- if(type === "all") {
159
+ if (type === "all") {
159
160
  url = `/content/in_progress/${globalConfig.railcontentConfig.userId}?brand=${brand}${limitString}${pageString}`;
160
161
  } else {
161
162
  url = `/content/in_progress/${globalConfig.railcontentConfig.userId}?content_type=${type}&brand=${brand}${limitString}${pageString}`;
@@ -165,9 +166,9 @@ export async function fetchContentInProgress(type="all", brand, { page, limit }
165
166
  'X-CSRF-TOKEN': globalConfig.railcontentConfig.token
166
167
  };
167
168
  try {
168
- const response = await fetchAbsolute(url, { headers });
169
+ const response = await fetchAbsolute(url, {headers});
169
170
  const result = await response.json();
170
- if(result){
171
+ if (result) {
171
172
  //console.log('contentInProgress', result);
172
173
  return result;
173
174
  } else {
@@ -192,12 +193,12 @@ export async function fetchContentInProgress(type="all", brand, { page, limit }
192
193
  * .then(songs => console.log(songs))
193
194
  * .catch(error => console.error(error));
194
195
  */
195
- export async function fetchCompletedContent(type="all", brand, { page, limit } = {}) {
196
+ export async function fetchCompletedContent(type = "all", brand, {page, limit} = {}) {
196
197
  let url;
197
198
  const limitString = limit ? `&limit=${limit}` : '';
198
199
  const pageString = page ? `&page=${page}` : '';
199
200
 
200
- if(type === "all") {
201
+ if (type === "all") {
201
202
  url = `/content/completed/${globalConfig.railcontentConfig.userId}?brand=${brand}${limitString}${pageString}`;
202
203
  } else {
203
204
  url = `/content/completed/${globalConfig.railcontentConfig.userId}?content_type=${type}&brand=${brand}${limitString}${pageString}`;
@@ -207,9 +208,9 @@ export async function fetchCompletedContent(type="all", brand, { page, limit } =
207
208
  'X-CSRF-TOKEN': globalConfig.railcontentConfig.token
208
209
  };
209
210
  try {
210
- const response = await fetchAbsolute(url, { headers });
211
+ const response = await fetchAbsolute(url, {headers});
211
212
  const result = await response.json();
212
- if(result){
213
+ if (result) {
213
214
  //console.log('completed content', result);
214
215
  return result;
215
216
  } else {
@@ -239,32 +240,11 @@ export async function fetchContentPageUserData(contentId) {
239
240
  'X-CSRF-TOKEN': globalConfig.railcontentConfig.token
240
241
  };
241
242
 
242
- try {
243
- const response = await fetchAbsolute(url, { headers });
244
- const result = await response.json();
245
- if(result){
246
- console.log('fetchContentPageUserData', result);
247
- return result;
248
- } else {
249
- console.log('result not json');
250
- }
251
- } catch (error) {
252
- console.error('Fetch error:', error);
253
- return null;
254
- }
255
- }
256
-
257
- export async function fetchUserContext() {
258
- let url = `/content/user_data_all`;
259
- const headers = {
260
- 'Content-Type': 'application/json',
261
- 'X-CSRF-TOKEN': globalConfig.railcontentConfig.token
262
- };
263
243
  try {
264
244
  const response = await fetchAbsolute(url, {headers});
265
245
  const result = await response.json();
266
246
  if (result) {
267
- console.log('fetchUserContext', result);
247
+ console.log('fetchContentPageUserData', result);
268
248
  return result;
269
249
  } else {
270
250
  console.log('result not json');
@@ -287,11 +267,16 @@ export async function fetchUserPermissions() {
287
267
  return fetchHandler(url, 'get') ?? [];
288
268
  }
289
269
 
290
- export async function fetchHandler(url, method = "get") {
291
- const headers = {
270
+ async function fetchDataHandler(url, dataVersion, method = "get") {
271
+ return fetchHandler(url, method, dataVersion);
272
+ }
273
+
274
+ export async function fetchHandler(url, method = "get", dataVersion = null) {
275
+ let headers = {
292
276
  'Content-Type': 'application/json',
293
- 'X-CSRF-TOKEN': globalConfig.railcontentConfig.token
277
+ 'X-CSRF-TOKEN': globalConfig.railcontentConfig.token,
294
278
  };
279
+ if (dataVersion) headers['Data-Version'] = dataVersion;
295
280
  try {
296
281
  const response = await fetchAbsolute(url, {method, headers});
297
282
  const result = await response.json();
@@ -306,18 +291,23 @@ export async function fetchHandler(url, method = "get") {
306
291
  return null;
307
292
  }
308
293
 
309
- export async function fetchLikeContent(contentId) {
310
- let url = `/content/${contentId}/like`;
294
+ export async function fetchUserLikes(currentVersion) {
295
+ let url = `/content/user/likes/all`;
296
+ return fetchDataHandler(url, currentVersion);
297
+ }
298
+
299
+ export async function postContentLiked(contentId) {
300
+ let url = `/content/user/likes/like/${contentId}`;
311
301
  return await fetchHandler(url, "post");
312
302
  }
313
303
 
314
- export async function fetchUnlikeContent(contentId) {
315
- let url = `/content/${contentId}/unlike`;
304
+ export async function postContentUnliked(contentId) {
305
+ let url = `/content/user/likes/unlike/${contentId}`;
316
306
  return await fetchHandler(url, "post");
317
307
  }
318
308
 
319
309
  function fetchAbsolute(url, params) {
320
- if(globalConfig.railcontentConfig.baseUrl) {
310
+ if (globalConfig.railcontentConfig.baseUrl) {
321
311
  if (url.startsWith('/')) {
322
312
  return fetch(globalConfig.railcontentConfig.baseUrl + url, params)
323
313
  }
@@ -13,6 +13,7 @@ import {
13
13
  getUpcomingEventsTypes,
14
14
  showsTypes,
15
15
  getNewReleasesTypes,
16
+ coachLessonsTypes
16
17
  } from "../contentTypeConfig";
17
18
 
18
19
  import {
@@ -576,6 +577,7 @@ export function getSortOrder(sort= '-published_on', groupBy)
576
577
  *
577
578
  * This function constructs a query to retrieve the total number of results and filter options such as difficulty, instrument type, and genre.
578
579
  * The filter options are dynamically generated based on the provided filters, style, artist, and content type.
580
+ * If a coachId is provided, the content type must be 'coach-lessons'.
579
581
  *
580
582
  * @param {string} brand - The brand for which to fetch the filter options.
581
583
  * @param {string[]} filters - Additional filters to apply to the query in the format of a key,value array. eg. ['difficulty,Intermediate', 'genre,rock']
@@ -584,13 +586,22 @@ export function getSortOrder(sort= '-published_on', groupBy)
584
586
  * @param {string} contentType - The content type to fetch (e.g., 'song', 'lesson').
585
587
  * @param {string} [term] - Optional search term to match against various fields such as title, album, artist name, and genre.
586
588
  * @param {Array<string>} [progressIds=undefined] - An array of railcontent IDs to filter the results by. Used for filtering by progress.
589
+ * @param {string} [coachId=undefined] - Optional coach ID to filter the results by a specific coach. If provided, contentType must be 'coach-lessons'.
587
590
  * @returns {Promise<Object|null>} - A promise that resolves to an object containing the total results and filter options, or null if the query fails.
588
591
  *
592
+ * @throws {Error} Will throw an error if coachId is provided but contentType is not 'coach-lessons'.
593
+ *
589
594
  * @example
590
595
  * // Example usage:
591
596
  * fetchAllFilterOptions('myBrand', '', 'Rock', 'John Doe', 'song', 'Love')
592
597
  * .then(options => console.log(options))
593
598
  * .catch(error => console.error(error));
599
+ *
600
+ * @example
601
+ * // Example usage with coachId:
602
+ * fetchAllFilterOptions('myBrand', '', 'Rock', 'John Doe', 'coach-lessons', 'Love', undefined, '123')
603
+ * .then(options => console.log(options))
604
+ * .catch(error => console.error(error));
594
605
  */
595
606
  export async function fetchAllFilterOptions(
596
607
  brand,
@@ -599,25 +610,48 @@ export async function fetchAllFilterOptions(
599
610
  artist,
600
611
  contentType,
601
612
  term,
602
- progressIds = undefined
613
+ progressIds = undefined,
614
+ coachId = undefined, // New parameter for coach ID
603
615
  ) {
616
+ if (coachId && contentType !== 'coach-lessons') {
617
+ throw new Error(`Invalid contentType: '${contentType}' for coachId. It must be 'coach-lessons'.`);
618
+ }
619
+
604
620
  filters = Array.isArray(filters) ? filters : [];
605
621
  const includedFieldsFilter = filters?.length > 0 ? filtersToGroq(filters) : undefined;
606
622
 
607
623
  const progressFilter = progressIds !== undefined ?
608
624
  `&& railcontent_id in [${progressIds.join(',')}]` : "";
609
625
 
610
- const commonFilter = `_type == '${contentType}' && brand == "${brand}"${style ? ` && '${style}' in genre[]->name` : ''}${artist ? ` && artist->name == '${artist}'` : ''} ${progressFilter} ${includedFieldsFilter ? includedFieldsFilter : ''}`;
626
+ // General common filter logic
627
+ let commonFilter;
628
+
629
+ if (coachId) {
630
+ // Coach-specific filtering
631
+ commonFilter = `brand == '${brand}' && references(*[_type=='instructor' && railcontent_id == ${coachId}]._id) ${includedFieldsFilter ? includedFieldsFilter : ''}`;
632
+ } else {
633
+ // Regular content filtering
634
+ commonFilter = `_type == '${contentType}' && brand == "${brand}"${style ? ` && '${style}' in genre[]->name` : ''}${artist ? ` && artist->name == '${artist}'` : ''} ${progressFilter} ${includedFieldsFilter ? includedFieldsFilter : ''}`;
635
+ }
636
+
637
+ // Determine metadata and allowable filters (handle coach lessons if coachId exists)
611
638
  const metaData = processMetadata(brand, contentType, true);
612
639
  const allowableFilters = metaData?.allowableFilters || [];
613
640
 
641
+ // Dynamic filter options construction
614
642
  const dynamicFilterOptions = allowableFilters.map(filter => {
615
- // Create a modified common filter for each allowable filter
616
643
  let includedFieldsFilterWithoutSelectedOption = filters?.length > 0 ? filtersToGroq(filters, filter) : undefined;
617
- const commonFilterWithoutSelectedOption = `_type == '${contentType}' && brand == "${brand}"${(style && filter !== "style") ? ` && '${style}' in genre[]->name` : ''}${(artist && filter !== "artist") ? ` && artist->name == '${artist}'` : ''} ${includedFieldsFilterWithoutSelectedOption ? includedFieldsFilterWithoutSelectedOption : ''}`;
644
+ let commonFilterWithoutSelectedOption;
645
+
646
+ if (coachId) {
647
+ commonFilterWithoutSelectedOption = `brand == '${brand}' && references(*[_type=='instructor' && railcontent_id == ${coachId}]._id) ${includedFieldsFilterWithoutSelectedOption ? includedFieldsFilterWithoutSelectedOption : ''}`;
648
+ } else {
649
+ // Regular content filter without the selected option
650
+ commonFilterWithoutSelectedOption = `_type == '${contentType}' && brand == "${brand}"${(style && filter !== "style") ? ` && '${style}' in genre[]->name` : ''}${(artist && filter !== "artist") ? ` && artist->name == '${artist}'` : ''} ${includedFieldsFilterWithoutSelectedOption ? includedFieldsFilterWithoutSelectedOption : ''}`;
651
+ }
618
652
 
619
653
  // Call getFilterOptions with the modified common filter
620
- return getFilterOptions(filter, commonFilterWithoutSelectedOption, contentType);
654
+ return getFilterOptions(filter, commonFilterWithoutSelectedOption, contentType, brand);
621
655
  }).join(' ');
622
656
 
623
657
  const query = `
@@ -937,7 +971,7 @@ export async function fetchRelatedLessons(railContentId, brand) {
937
971
  * Fetch related method lessons for a specific lesson by RailContent ID and type.
938
972
  * @param {string} railContentId - The RailContent ID of the current lesson.
939
973
  * @param {string} brand - The current brand.
940
- * @returns {Promise<Object>|null>} - The fetched related lessons
974
+ * @returns {Promise<Array<Object>|null>} - The fetched related lessons
941
975
  */
942
976
  export async function fetchRelatedMethodLessons(railContentId, brand) {
943
977
  const query = `*[railcontent_id == ${railContentId} && brand == "${brand}"]{
@@ -1082,8 +1116,15 @@ export async function fetchChallengeOverview(id) {
1082
1116
 
1083
1117
  /**
1084
1118
  * Fetch the data needed for the coach screen.
1119
+ * @param {string} brand - The brand for which to fetch coach lessons
1085
1120
  * @param {string} id - The Railcontent ID of the coach
1086
1121
  * @returns {Promise<Object|null>} - The lessons for the instructor or null if not found.
1122
+ * @param {Object} params - Parameters for pagination, filtering and sorting.
1123
+ * @param {string} [params.sortOrder="-published_on"] - The field to sort the lessons by.
1124
+ * @param {string} [params.searchTerm=""] - The search term to filter content by title.
1125
+ * @param {number} [params.page=1] - The page number for pagination.
1126
+ * @param {number} [params.limit=10] - The number of items per page.
1127
+ * @param {Array<string>} [params.includedFields=[]] - Additional filters to apply to the query in the format of a key,value array. eg. ['difficulty,Intermediate', 'genre,rock'].
1087
1128
  *
1088
1129
  * @example
1089
1130
  * fetchCoachLessons('coach123')
@@ -1095,12 +1136,17 @@ export async function fetchCoachLessons(brand, id, {
1095
1136
  searchTerm = '',
1096
1137
  page = 1,
1097
1138
  limit = 20,
1139
+ includedFields = [],
1098
1140
  } = {}) {
1099
1141
  const fieldsString = getFieldsForContentType();
1100
1142
  const start = (page - 1) * limit;
1101
1143
  const end = start + limit;
1102
1144
  const searchFilter = searchTerm ? `&& title match "${searchTerm}*"`: ''
1103
- const filter = `brand == '${brand}' ${searchFilter} && references(*[_type=='instructor' && railcontent_id == ${id}]._id)`;
1145
+ const includedFieldsFilter = includedFields.length > 0
1146
+ ? filtersToGroq(includedFields)
1147
+ : "";
1148
+ const filter = `brand == '${brand}' ${searchFilter} ${includedFieldsFilter} && references(*[_type=='instructor' && railcontent_id == ${id}]._id)`;
1149
+
1104
1150
  sortOrder = getSortOrder(sortOrder);
1105
1151
  const query = buildEntityAndTotalQuery(
1106
1152
  filter,
@@ -1374,7 +1420,7 @@ export async function fetchCatalogMetadata(contentType)
1374
1420
  * Fetch shows data for a brand.
1375
1421
  *
1376
1422
  * @param brand - The brand for which to fetch shows.
1377
- * @returns {Promise<[]>}
1423
+ * @returns {Promise<{name, description, type: *, thumbnailUrl}>}
1378
1424
  *
1379
1425
  * @example
1380
1426
  *
@@ -1508,8 +1554,10 @@ function buildEntityAndTotalQuery(
1508
1554
  }
1509
1555
 
1510
1556
 
1511
- function getFilterOptions(option, commonFilter,contentType){
1557
+ function getFilterOptions(option, commonFilter,contentType, brand){
1512
1558
  let filterGroq = '';
1559
+ const types = Array.from(new Set([...coachLessonsTypes,...showsTypes[brand]]));
1560
+
1513
1561
  switch (option) {
1514
1562
  case "difficulty":
1515
1563
  filterGroq = `
@@ -1521,6 +1569,12 @@ function getFilterOptions(option, commonFilter,contentType){
1521
1569
  {"type": "Expert", "count": count(*[${commonFilter} && difficulty_string == "Expert" ])}
1522
1570
  ][count > 0],`;
1523
1571
  break;
1572
+ case "type":
1573
+ const dynamicTypeOptions = types.map(filter => {
1574
+ return `{"type": "${filter}", "count": count(*[${commonFilter} && _type == "${filter}"])}`
1575
+ }).join(', ');
1576
+ filterGroq = `"type": [${dynamicTypeOptions}][count > 0],`;
1577
+ break;
1524
1578
  case "genre":
1525
1579
  case "essential":
1526
1580
  case "focus":
@@ -1529,7 +1583,7 @@ function getFilterOptions(option, commonFilter,contentType){
1529
1583
  case "lifestyle":
1530
1584
  case "creativity":
1531
1585
  filterGroq = `
1532
- "${option}": *[_type == '${option}' && '${contentType}' in filter_types] {
1586
+ "${option}": *[_type == '${option}' ${contentType ? ` && '${contentType}' in filter_types` : ''} ] {
1533
1587
  "type": name,
1534
1588
  "count": count(*[${commonFilter} && references(^._id)])
1535
1589
  }[count > 0],`;