musora-content-services 2.30.4 → 2.30.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,54 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [2.30.9](https://github.com/railroadmedia/musora-content-services/compare/v2.30.5...v2.30.9) (2025-08-13)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **BEH-845:** enable soft deletes for user activities ([#384](https://github.com/railroadmedia/musora-content-services/issues/384)) ([9c146c4](https://github.com/railroadmedia/musora-content-services/commit/9c146c43536e517ca69d3f264b117850fec748ec))
11
+ * **MU-921:** add context to content empty arrays ([#410](https://github.com/railroadmedia/musora-content-services/issues/410)) ([0ca84ae](https://github.com/railroadmedia/musora-content-services/commit/0ca84aea741e605d17c0f45bbfb6cc8ea018e676))
12
+ * **recsys:** support navigateTo and permissions filtering for recomme… ([#403](https://github.com/railroadmedia/musora-content-services/issues/403)) ([864ec0f](https://github.com/railroadmedia/musora-content-services/commit/864ec0fc05704d9641a965bc4de63a97422668c4))
13
+ * **T3PS-454:** rank lessons by recommended ([#380](https://github.com/railroadmedia/musora-content-services/issues/380)) ([cc55bfd](https://github.com/railroadmedia/musora-content-services/commit/cc55bfd0fb19f1b42c42eb2ea6573817a8d2cc69))
14
+
15
+ ### [2.30.8](https://github.com/railroadmedia/musora-content-services/compare/v2.30.5...v2.30.8) (2025-08-13)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **BEH-845:** enable soft deletes for user activities ([#384](https://github.com/railroadmedia/musora-content-services/issues/384)) ([9c146c4](https://github.com/railroadmedia/musora-content-services/commit/9c146c43536e517ca69d3f264b117850fec748ec))
21
+ * **MU-921:** add context to content empty arrays ([#410](https://github.com/railroadmedia/musora-content-services/issues/410)) ([0ca84ae](https://github.com/railroadmedia/musora-content-services/commit/0ca84aea741e605d17c0f45bbfb6cc8ea018e676))
22
+ * **recsys:** support navigateTo and permissions filtering for recomme… ([#403](https://github.com/railroadmedia/musora-content-services/issues/403)) ([864ec0f](https://github.com/railroadmedia/musora-content-services/commit/864ec0fc05704d9641a965bc4de63a97422668c4))
23
+ * **T3PS-454:** rank lessons by recommended ([#380](https://github.com/railroadmedia/musora-content-services/issues/380)) ([cc55bfd](https://github.com/railroadmedia/musora-content-services/commit/cc55bfd0fb19f1b42c42eb2ea6573817a8d2cc69))
24
+
25
+ ### [2.30.7](https://github.com/railroadmedia/musora-content-services/compare/v2.30.5...v2.30.7) (2025-08-13)
26
+
27
+
28
+ ### Bug Fixes
29
+
30
+ * **BEH-845:** enable soft deletes for user activities ([#384](https://github.com/railroadmedia/musora-content-services/issues/384)) ([9c146c4](https://github.com/railroadmedia/musora-content-services/commit/9c146c43536e517ca69d3f264b117850fec748ec))
31
+ * **MU-921:** add context to content empty arrays ([#410](https://github.com/railroadmedia/musora-content-services/issues/410)) ([0ca84ae](https://github.com/railroadmedia/musora-content-services/commit/0ca84aea741e605d17c0f45bbfb6cc8ea018e676))
32
+ * **recsys:** support navigateTo and permissions filtering for recomme… ([#403](https://github.com/railroadmedia/musora-content-services/issues/403)) ([864ec0f](https://github.com/railroadmedia/musora-content-services/commit/864ec0fc05704d9641a965bc4de63a97422668c4))
33
+ * **T3PS-454:** rank lessons by recommended ([#380](https://github.com/railroadmedia/musora-content-services/issues/380)) ([cc55bfd](https://github.com/railroadmedia/musora-content-services/commit/cc55bfd0fb19f1b42c42eb2ea6573817a8d2cc69))
34
+
35
+ ### [2.30.6](https://github.com/railroadmedia/musora-content-services/compare/v2.30.5...v2.30.6) (2025-08-13)
36
+
37
+
38
+ ### Bug Fixes
39
+
40
+ * **BEH-845:** enable soft deletes for user activities ([#384](https://github.com/railroadmedia/musora-content-services/issues/384)) ([9c146c4](https://github.com/railroadmedia/musora-content-services/commit/9c146c43536e517ca69d3f264b117850fec748ec))
41
+ * **MU-921:** add context to content empty arrays ([#410](https://github.com/railroadmedia/musora-content-services/issues/410)) ([0ca84ae](https://github.com/railroadmedia/musora-content-services/commit/0ca84aea741e605d17c0f45bbfb6cc8ea018e676))
42
+ * **recsys:** support navigateTo and permissions filtering for recomme… ([#403](https://github.com/railroadmedia/musora-content-services/issues/403)) ([864ec0f](https://github.com/railroadmedia/musora-content-services/commit/864ec0fc05704d9641a965bc4de63a97422668c4))
43
+ * **T3PS-454:** rank lessons by recommended ([#380](https://github.com/railroadmedia/musora-content-services/issues/380)) ([cc55bfd](https://github.com/railroadmedia/musora-content-services/commit/cc55bfd0fb19f1b42c42eb2ea6573817a8d2cc69))
44
+
45
+ ### [2.30.5](https://github.com/railroadmedia/musora-content-services/compare/v2.30.4...v2.30.5) (2025-08-06)
46
+
47
+
48
+ ### Bug Fixes
49
+
50
+ * **MU2-918:** practice meta data for manual only sessions ([#402](https://github.com/railroadmedia/musora-content-services/issues/402)) ([a911529](https://github.com/railroadmedia/musora-content-services/commit/a9115297f00b6aed5f0ad73714cb860cfd71f331))
51
+ * **MU2-918:** safer .find use ([#405](https://github.com/railroadmedia/musora-content-services/issues/405)) ([ce81469](https://github.com/railroadmedia/musora-content-services/commit/ce814692a7aa46ee814c28ecae1ec404a0c09046))
52
+
5
53
  ### [2.30.4](https://github.com/railroadmedia/musora-content-services/compare/v2.30.3...v2.30.4) (2025-08-05)
6
54
 
7
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.30.4",
3
+ "version": "2.30.9",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -21,6 +21,7 @@ export class FilterBuilder {
21
21
  isSingle = false,
22
22
  allowsPullSongsContent = true,
23
23
  isChildrenFilter = false,
24
+ isParentFilter = false,
24
25
  } = {}
25
26
  ) {
26
27
  this.availableContentStatuses = availableContentStatuses
@@ -35,7 +36,17 @@ export class FilterBuilder {
35
36
  this.filter = filter
36
37
  // this.debug = process.env.DEBUG === 'true' || false;
37
38
  this.debug = false
38
- this.prefix = isChildrenFilter ? '@->' : ''
39
+ this.prefix = this.getPrefix(isParentFilter, isChildrenFilter)
40
+ }
41
+
42
+ getPrefix(isParentFilter, isChildrenFilter) {
43
+ if (isParentFilter) {
44
+ return '^.'
45
+ } else if (isChildrenFilter) {
46
+ return '@->'
47
+ } else {
48
+ return ''
49
+ }
39
50
  }
40
51
 
41
52
  static withOnlyFilterAvailableStatuses(filter, availableContentStatuses, bypassPermissions) {
package/src/index.d.ts CHANGED
@@ -44,7 +44,8 @@ import {
44
44
  } from './services/content.js';
45
45
 
46
46
  import {
47
- addContextToContent
47
+ addContextToContent,
48
+ getNavigateToForPlaylists
48
49
  } from './services/contentAggregator.js';
49
50
 
50
51
  import {
@@ -61,6 +62,7 @@ import {
61
62
  getAllStarted,
62
63
  getAllStartedOrCompleted,
63
64
  getLastInteractedOf,
65
+ getNavigateTo,
64
66
  getNextLesson,
65
67
  getProgressDateByIds,
66
68
  getProgressPercentage,
@@ -143,6 +145,7 @@ import {
143
145
  postContentComplete,
144
146
  postContentLiked,
145
147
  postContentReset,
148
+ postContentRestore,
146
149
  postContentUnliked,
147
150
  postPlaylistContentEngaged,
148
151
  postRecordWatchSession,
@@ -254,6 +257,7 @@ import {
254
257
  markNotificationAsRead,
255
258
  markNotificationAsUnread,
256
259
  pauseLiveEventPolling,
260
+ restoreNotification,
257
261
  startLiveEventPolling,
258
262
  updateNotificationSetting
259
263
  } from './services/user/notifications.js';
@@ -292,6 +296,7 @@ import {
292
296
  recordUserPractice,
293
297
  removeUserPractice,
294
298
  restorePracticeSession,
299
+ restoreUserActivity,
295
300
  restoreUserPractice,
296
301
  unpinProgressRow,
297
302
  updatePracticeNotes,
@@ -423,6 +428,8 @@ declare module 'musora-content-services' {
423
428
  getLastInteractedOf,
424
429
  getLessonContentRows,
425
430
  getMonday,
431
+ getNavigateTo,
432
+ getNavigateToForPlaylists,
426
433
  getNewAndUpcoming,
427
434
  getNextLesson,
428
435
  getPracticeNotes,
@@ -477,6 +484,7 @@ declare module 'musora-content-services' {
477
484
  postContentComplete,
478
485
  postContentLiked,
479
486
  postContentReset,
487
+ postContentRestore,
480
488
  postContentUnliked,
481
489
  postPlaylistContentEngaged,
482
490
  postRecordWatchSession,
@@ -496,7 +504,9 @@ declare module 'musora-content-services' {
496
504
  resetPassword,
497
505
  restoreComment,
498
506
  restoreItemFromPlaylist,
507
+ restoreNotification,
499
508
  restorePracticeSession,
509
+ restoreUserActivity,
500
510
  restoreUserPractice,
501
511
  sendAccountSetupEmail,
502
512
  sendPasswordResetEmail,
package/src/index.js CHANGED
@@ -44,7 +44,8 @@ import {
44
44
  } from './services/content.js';
45
45
 
46
46
  import {
47
- addContextToContent
47
+ addContextToContent,
48
+ getNavigateToForPlaylists
48
49
  } from './services/contentAggregator.js';
49
50
 
50
51
  import {
@@ -61,6 +62,7 @@ import {
61
62
  getAllStarted,
62
63
  getAllStartedOrCompleted,
63
64
  getLastInteractedOf,
65
+ getNavigateTo,
64
66
  getNextLesson,
65
67
  getProgressDateByIds,
66
68
  getProgressPercentage,
@@ -129,9 +131,9 @@ import {
129
131
  fetchNextContentDataForParent,
130
132
  fetchRecentUserActivities,
131
133
  fetchSongsInProgress,
134
+ fetchTopComment,
132
135
  fetchUserAward,
133
136
  fetchUserBadges,
134
- fetchTopComment,
135
137
  fetchUserLikes,
136
138
  fetchUserPermissionsData,
137
139
  fetchUserPracticeMeta,
@@ -143,6 +145,7 @@ import {
143
145
  postContentComplete,
144
146
  postContentLiked,
145
147
  postContentReset,
148
+ postContentRestore,
146
149
  postContentUnliked,
147
150
  postPlaylistContentEngaged,
148
151
  postRecordWatchSession,
@@ -254,6 +257,7 @@ import {
254
257
  markNotificationAsRead,
255
258
  markNotificationAsUnread,
256
259
  pauseLiveEventPolling,
260
+ restoreNotification,
257
261
  startLiveEventPolling,
258
262
  updateNotificationSetting
259
263
  } from './services/user/notifications.js';
@@ -292,6 +296,7 @@ import {
292
296
  recordUserPractice,
293
297
  removeUserPractice,
294
298
  restorePracticeSession,
299
+ restoreUserActivity,
295
300
  restoreUserPractice,
296
301
  unpinProgressRow,
297
302
  updatePracticeNotes,
@@ -422,6 +427,8 @@ export {
422
427
  getLastInteractedOf,
423
428
  getLessonContentRows,
424
429
  getMonday,
430
+ getNavigateTo,
431
+ getNavigateToForPlaylists,
425
432
  getNewAndUpcoming,
426
433
  getNextLesson,
427
434
  getPracticeNotes,
@@ -476,6 +483,7 @@ export {
476
483
  postContentComplete,
477
484
  postContentLiked,
478
485
  postContentReset,
486
+ postContentRestore,
479
487
  postContentUnliked,
480
488
  postPlaylistContentEngaged,
481
489
  postRecordWatchSession,
@@ -495,7 +503,9 @@ export {
495
503
  resetPassword,
496
504
  restoreComment,
497
505
  restoreItemFromPlaylist,
506
+ restoreNotification,
498
507
  restorePracticeSession,
508
+ restoreUserActivity,
499
509
  restoreUserPractice,
500
510
  sendAccountSetupEmail,
501
511
  sendPasswordResetEmail,
@@ -15,7 +15,7 @@ import {
15
15
  } from './sanity.js'
16
16
  import {TabResponseType, Tabs, capitalizeFirstLetter} from '../contentMetaData.js'
17
17
  import {fetchHandler} from "./railcontent";
18
- import {recommendations, rankCategories} from "./recommendations";
18
+ import {recommendations, rankCategories, rankItems} from "./recommendations";
19
19
  import {addContextToContent} from "./contentAggregator.js";
20
20
 
21
21
 
@@ -85,12 +85,22 @@ export async function getTabResults(brand, pageName, tabName, {
85
85
  addProgressStatus: true
86
86
  })
87
87
  } else {
88
- let temp = await fetchTabData(brand, pageName, { page, limit, sort, includedFields: mergedIncludedFields, progress: progressValue });
89
- results = await addContextToContent(() => temp.entity, {
90
- addNextLesson: true,
91
- addNavigateTo: true,
92
- addProgressPercentage: true,
93
- addProgressStatus: true
88
+ let temp = await fetchTabData(brand, pageName, { page, limit, sort, includedFields: mergedIncludedFields, progress: progressValue });
89
+
90
+ const [ranking, contextResults] = await Promise.all([
91
+ sort === 'recommended' ? rankItems(brand, temp.entity.map(e => e.railcontent_id)) : [],
92
+ addContextToContent(() => temp.entity, {
93
+ addNextLesson: true,
94
+ addNavigateTo: true,
95
+ addProgressPercentage: true,
96
+ addProgressStatus: true
97
+ })
98
+ ]);
99
+
100
+ results = ranking.length === 0 ? contextResults : contextResults.sort((a, b) => {
101
+ const indexA = ranking.indexOf(a.railcontent_id);
102
+ const indexB = ranking.indexOf(b.railcontent_id);
103
+ return (indexA === -1 ? Infinity : indexA) - (indexB === -1 ? Infinity : indexB);
94
104
  })
95
105
  }
96
106
 
@@ -392,7 +402,7 @@ export async function getRecommendedForYou(brand, rowId = null, {
392
402
  // Apply pagination before calling fetchByRailContentIds
393
403
  const startIndex = (page - 1) * limit;
394
404
  const paginatedData = data.slice(startIndex, startIndex + limit);
395
- const contents = await addContextToContent(fetchByRailContentIds, paginatedData, 'tab-data',
405
+ const contents = await addContextToContent(fetchByRailContentIds, paginatedData, 'tab-data', brand, true,
396
406
  {
397
407
  addNextLesson: true,
398
408
  addNavigateTo: true,
@@ -77,12 +77,13 @@ export async function addContextToContent(dataPromise, ...dataArgs)
77
77
  const dataParam = lastArg === options ? dataArgs.slice(0, -1) : dataArgs
78
78
 
79
79
  let data = await dataPromise(...dataParam)
80
- if(!data) return false
81
80
  const isDataAnArray = Array.isArray(data)
82
- const items = extractItemsFromData(data, dataField, isDataAnArray, dataField_includeParent)
83
- const ids = items.map(item => item?.id).filter(Boolean)
81
+ if(isDataAnArray && data.length === 0) return data
82
+ if(!data) return false
84
83
 
85
- if(ids.length === 0) return false
84
+ const items = extractItemsFromData(data, dataField, isDataAnArray, dataField_includeParent) ?? []
85
+ const ids = items.map(item => item?.id).filter(Boolean)
86
+ if(ids.length === 0) return data
86
87
 
87
88
  const [progressData, isLikedData, resumeTimeData, lastInteractedChildData, nextLessonData, navigateToData] = await Promise.all([
88
89
  addProgressPercentage || addProgressStatus || addProgressTimestamp ? getProgressDateByIds(ids) : Promise.resolve(null),
@@ -378,16 +378,36 @@ export async function fetchUserBadges(brand = null) {
378
378
  return await fetchHandler(url, 'get')
379
379
  }
380
380
 
381
+ /**
382
+ * complete a content's progress for a given user
383
+ * @param contentId
384
+ * @returns {Promise<any|string|null>}
385
+ */
381
386
  export async function postContentComplete(contentId) {
382
387
  let url = `/api/content/v1/user/progress/complete/${contentId}`
383
388
  return postDataHandler(url)
384
389
  }
385
390
 
391
+ /**
392
+ * resets the user's progress on a content
393
+ * @param contentId
394
+ * @returns {Promise<any|string|null>}
395
+ */
386
396
  export async function postContentReset(contentId) {
387
397
  let url = `/api/content/v1/user/progress/reset/${contentId}`
388
398
  return postDataHandler(url)
389
399
  }
390
400
 
401
+ /**
402
+ * restores the user's progress on a content
403
+ * @param contentId
404
+ * @returns {Promise<any|string|null>}
405
+ */
406
+ export async function postContentRestore(contentId) {
407
+ let url = `/api/content/v1/user/progress/restore/${contentId}`
408
+ return postDataHandler(url)
409
+ }
410
+
391
411
  /**
392
412
  * Set a user's StudentView Flag
393
413
  *
@@ -476,7 +476,7 @@ export async function fetchByRailContentId(id, contentType) {
476
476
  * .then(contents => console.log(contents))
477
477
  * .catch(error => console.error(error));
478
478
  */
479
- export async function fetchByRailContentIds(ids, contentType = undefined, brand = undefined) {
479
+ export async function fetchByRailContentIds(ids, contentType = undefined, brand = undefined, includePermissionsAndStatusFilter = false) {
480
480
  if (!ids?.length) {
481
481
  return []
482
482
  }
@@ -485,8 +485,10 @@ export async function fetchByRailContentIds(ids, contentType = undefined, brand
485
485
  const brandFilter = brand ? ` && brand == "${brand}"` : ''
486
486
  const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`, {pullFutureContent: true}).buildFilter()
487
487
  const fields = await getFieldsForContentTypeWithFilteredChildren(contentType, true)
488
+ const baseFilter = `railcontent_id in [${idsString}]${brandFilter}`
489
+ const finalFilter = includePermissionsAndStatusFilter ? await new FilterBuilder(baseFilter).buildFilter() : baseFilter
488
490
  const query = `*[
489
- railcontent_id in [${idsString}]${brandFilter}
491
+ ${finalFilter}
490
492
  ]{
491
493
  ${fields}
492
494
  'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
@@ -535,7 +537,9 @@ export async function fetchContentRows(brand, pageName, contentRowSlug)
535
537
  name,
536
538
  'slug': slug.current,
537
539
  'content': content[${childFilter}]->{
538
- 'children': child[${childFilter}]->{ 'id': railcontent_id, 'children': child[${childFilter}]->{'id': railcontent_id}, },
540
+ 'children': child[${childFilter}]->{ 'id': railcontent_id,
541
+ 'type': _type, brand, 'thumbnail': thumbnail.asset->url,
542
+ 'children': child[${childFilter}]->{'id': railcontent_id}, },
539
543
  ${getFieldsForContentType('tab-data')}
540
544
  'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
541
545
  },
@@ -1089,6 +1093,7 @@ export async function jumpToContinueContent(railcontentId) {
1089
1093
  /**
1090
1094
  * Fetch the page data for a specific lesson by Railcontent ID.
1091
1095
  * @param {string} railContentId - The Railcontent ID of the current lesson.
1096
+ * @parent {boolean} addParent - Whether to include parent content data in the response.
1092
1097
  * @returns {Promise<Object|null>} - The fetched page data or null if found.
1093
1098
  *
1094
1099
  * @example
@@ -1096,44 +1101,51 @@ export async function jumpToContinueContent(railcontentId) {
1096
1101
  * .then(data => console.log(data))
1097
1102
  * .catch(error => console.error(error));
1098
1103
  */
1099
- export async function fetchLessonContent(railContentId) {
1104
+ export async function fetchLessonContent(railContentId, { addParent = false } = {}) {
1100
1105
  const filterParams = { isSingle: true, pullFutureContent: true }
1101
1106
 
1107
+ const parentQuery = addParent
1108
+ ? `"parent_content_data": *[railcontent_id in [...(^.parent_content_data[].id)]]{
1109
+ "id": railcontent_id,
1110
+ title,
1111
+ slug,
1112
+ "type": _type,
1113
+ "logo" : logo_image_url.asset->url,
1114
+ "dark_mode_logo": dark_mode_logo_url.asset->url,
1115
+ "light_mode_logo": light_mode_logo_url.asset->url,
1116
+ "badge": badge[0]->badge.asset->url,
1117
+ },`
1118
+ : ''
1119
+
1102
1120
  const fields = `${getFieldsForContentType()}
1103
- "resources": ${resourcesField},
1104
- soundslice,
1105
- instrumentless,
1106
- soundslice_slug,
1107
- "description": ${descriptionField},
1108
- "chapters": ${chapterField},
1109
- "instructors":instructor[]->name,
1110
- "instructor": ${instructorField},
1111
- ${assignmentsField}
1112
- video,
1113
- length_in_seconds,
1114
- mp3_no_drums_no_click_url,
1115
- mp3_no_drums_yes_click_url,
1116
- mp3_yes_drums_no_click_url,
1117
- mp3_yes_drums_yes_click_url,
1118
- "permission_id": permission[]->railcontent_id,
1119
- "parent_content_data": parent_content_data[]{
1120
- "id": id,
1121
- "title": *[railcontent_id == ^.id][0].title,
1122
- "slug":*[railcontent_id == ^.id][0].slug,
1123
- "type": *[railcontent_id == ^.id][0]._type,
1124
- "logo" : *[railcontent_id == ^.id][0].logo_image_url.asset->url,
1125
- "dark_mode_logo": *[railcontent_id == ^.id][0].dark_mode_logo_url.asset->url,
1126
- "light_mode_logo": *[railcontent_id == ^.id][0].light_mode_logo_url.asset->url,
1127
- },
1128
- ...select(
1129
- defined(live_event_start_time) => {
1130
- "live_event_start_time": live_event_start_time,
1131
- "live_event_end_time": live_event_end_time,
1132
- "live_event_youtube_id": live_event_youtube_id,
1133
- "videoId": coalesce(live_event_youtube_id, video.external_id),
1134
- "live_event_is_global": live_global_event == true
1135
- }
1136
- )`
1121
+ "resources": ${resourcesField},
1122
+ soundslice,
1123
+ instrumentless,
1124
+ soundslice_slug,
1125
+ "description": ${descriptionField},
1126
+ "chapters": ${chapterField},
1127
+ "instructors":instructor[]->name,
1128
+ "instructor": ${instructorField},
1129
+ ${assignmentsField}
1130
+ video,
1131
+ length_in_seconds,
1132
+ mp3_no_drums_no_click_url,
1133
+ mp3_no_drums_yes_click_url,
1134
+ mp3_yes_drums_no_click_url,
1135
+ mp3_yes_drums_yes_click_url,
1136
+ "permission_id": permission[]->railcontent_id,
1137
+ ${parentQuery}
1138
+ ...select(
1139
+ defined(live_event_start_time) => {
1140
+ "live_event_start_time": live_event_start_time,
1141
+ "live_event_end_time": live_event_end_time,
1142
+ "live_event_youtube_id": live_event_youtube_id,
1143
+ "videoId": coalesce(live_event_youtube_id, video.external_id),
1144
+ "live_event_is_global": live_global_event == true
1145
+ }
1146
+ )
1147
+ `
1148
+
1137
1149
  const query = await buildQuery(`railcontent_id == ${railContentId}`, filterParams, fields, {
1138
1150
  isSingle: true,
1139
1151
  })
@@ -1166,7 +1178,7 @@ export async function fetchLessonContent(railContentId) {
1166
1178
  export async function fetchRelatedRecommendedContent(railContentId, brand, count = 10) {
1167
1179
  const recommendedItems = await fetchSimilarItems(railContentId, brand, count)
1168
1180
  if (recommendedItems && recommendedItems.length > 0) {
1169
- return fetchByRailContentIds(recommendedItems)
1181
+ return fetchByRailContentIds(recommendedItems, 'tab-data', brand, true)
1170
1182
  }
1171
1183
 
1172
1184
  return await fetchRelatedLessons(railContentId, brand).then((result) =>
@@ -1239,20 +1251,26 @@ async function fetchRelatedByLicense(railcontentId, brand, onlyUseSongTypes, cou
1239
1251
  * @param {string} brand - The current brand.
1240
1252
  * @returns {Promise<Array<Object>|null>} - The fetched related lessons data or null if not found.
1241
1253
  */
1242
- export async function fetchSiblingContent(railContentId, brand)
1254
+ export async function fetchSiblingContent(railContentId, brand= null)
1243
1255
  {
1244
1256
  const filterGetParent = await new FilterBuilder(`references(^._id) && _type == ^.parent_type`, {
1257
+ pullFutureContent: true
1258
+ }).buildFilter()
1259
+ const filterForParentList = await new FilterBuilder(`references(^._id) && _type == ^.parent_type`, {
1245
1260
  pullFutureContent: true,
1261
+ isParentFilter: true,
1246
1262
  }).buildFilter()
1263
+
1247
1264
  const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
1248
1265
 
1266
+ const brandString = brand ? ` && brand == "${brand}"` : ''
1249
1267
  const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, artist->, "permission_id": permission[]->railcontent_id, "genre": genre[]->name, "parent_id": parent_content_data[0].id`
1250
1268
 
1251
- const query = `*[railcontent_id == ${railContentId} && brand == "${brand}"]{
1269
+ const query = `*[railcontent_id == ${railContentId}${brandString}]{
1252
1270
  _type, parent_type, 'parent_id': parent_content_data[0].id, railcontent_id,
1253
1271
  'for-calculations': *[${filterGetParent}][0]{
1254
1272
  'siblings-list': child[]->railcontent_id,
1255
- 'parents-list': *[${filterGetParent}][0].child[]->railcontent_id
1273
+ 'parents-list': *[${filterForParentList}][0].child[]->railcontent_id
1256
1274
  },
1257
1275
  "related_lessons" : *[${filterGetParent}][0].child[${childrenFilter}]->{${queryFields}}
1258
1276
  }`
@@ -2157,7 +2175,6 @@ export async function fetchTabData(
2157
2175
  ) {
2158
2176
  const start = (page - 1) * limit
2159
2177
  const end = start + limit
2160
- let withoutPagination = false
2161
2178
  // Construct the included fields filter, replacing 'difficulty' with 'difficulty_string'
2162
2179
  const includedFieldsFilter =
2163
2180
  includedFields.length > 0 ? filtersToGroq(includedFields, [], pageName) : ''
@@ -2168,27 +2185,19 @@ export async function fetchTabData(
2168
2185
  case 'recent':
2169
2186
  progressIds = await getAllStartedOrCompleted({ brand, onlyIds: true });
2170
2187
  sortOrder = null;
2171
- withoutPagination = true;
2172
2188
  break;
2173
2189
  case 'incomplete':
2174
2190
  progressIds = await getAllStarted();
2175
2191
  sortOrder = null;
2176
- withoutPagination = true;
2177
2192
  break;
2178
2193
  case 'completed':
2179
2194
  progressIds = await getAllCompleted();
2180
2195
  sortOrder = null;
2181
- withoutPagination = true;
2182
2196
  break;
2183
2197
  }
2184
2198
 
2185
2199
  // limits the results to supplied progressIds for started & completed filters
2186
2200
  const progressFilter = await getProgressFilter(progress, progressIds)
2187
- if (sort === "recommended"){
2188
- progressIds = await recommendations(brand);
2189
- withoutPagination = true;
2190
- }
2191
-
2192
2201
  const fieldsString = getFieldsForContentType('tab-data');
2193
2202
  const now = getSanityDate(new Date())
2194
2203
 
@@ -2202,7 +2211,7 @@ export async function fetchTabData(
2202
2211
  const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`).buildFilter()
2203
2212
  entityFieldsString =
2204
2213
  ` ${fieldsString}
2205
- 'children': child[${childrenFilter}]->{'id': railcontent_id},
2214
+ 'children': child[${childrenFilter}]->{'id': railcontent_id, 'type': _type, brand, 'thumbnail': thumbnail.asset->url},
2206
2215
  'isLive': live_event_start_time <= "${now}" && live_event_end_time >= "${now}",
2207
2216
  'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
2208
2217
  'length_in_seconds': coalesce(
@@ -2217,13 +2226,12 @@ export async function fetchTabData(
2217
2226
  query = buildEntityAndTotalQuery(filterWithRestrictions, entityFieldsString, {
2218
2227
  sortOrder: sortOrder,
2219
2228
  start: start,
2220
- end: end,
2221
- withoutPagination: withoutPagination,
2229
+ end: end
2222
2230
  })
2223
2231
 
2224
2232
  let results = await fetchSanity(query, true);
2225
2233
 
2226
- if ((['recent', 'incomplete', 'completed'].includes(progress) || sort == "recommended" )&& results.entity.length > 1) {
2234
+ if (['recent', 'incomplete', 'completed'].includes(progress) && results.entity.length > 1) {
2227
2235
  const orderMap = new Map(progressIds.map((id, index) => [id, index]))
2228
2236
  results.entity = results.entity
2229
2237
  .sort((a, b) => {
@@ -114,11 +114,33 @@ export async function deleteNotification(notificationId) {
114
114
  if (!notificationId) {
115
115
  throw new Error('notificationId is required')
116
116
  }
117
-
118
117
  const url = `${baseUrl}/v1/${notificationId}`
119
118
  return fetchHandler(url, 'delete')
120
119
  }
121
120
 
121
+ /**
122
+ * Restores a specific notification.
123
+ *
124
+ * @param {number} notificationId - The ID of the notification to restore.
125
+ *
126
+ * @returns {Promise<any>} - A promise that resolves when the notification is successfully restored.
127
+ *
128
+ * @throws {Error} - Throws an error if notificationId is not provided.
129
+ *
130
+ * @example
131
+ * restoreNotification(123)
132
+ * .then(response => console.log(response))
133
+ * .catch(error => console.error(error));
134
+ */
135
+ export async function restoreNotification(notificationId) {
136
+ if (!notificationId) {
137
+ throw new Error('notificationId is required')
138
+ }
139
+
140
+ const url = `${baseUrl}/v1/${notificationId}`
141
+ return fetchHandler(url, 'put')
142
+ }
143
+
122
144
  /**
123
145
  * Fetches the count of unread notifications for the current user in a given brand context.
124
146
  *
@@ -11,7 +11,7 @@ import {
11
11
  fetchRecentUserActivities,
12
12
  } from './railcontent'
13
13
  import { DataContext, UserActivityVersionKey } from './dataContext.js'
14
- import { fetchByRailContentIds, fetchShows } from './sanity'
14
+ import { fetchByRailContentId, fetchByRailContentIds, fetchShows } from './sanity'
15
15
  import { fetchPlaylist, fetchUserPlaylists } from './content-org/playlists'
16
16
  import { pinnedGuidedCourses } from './content-org/guided-courses'
17
17
  import {
@@ -35,7 +35,7 @@ import { TabResponseType } from '../contentMetaData'
35
35
  import dayjs from 'dayjs'
36
36
  import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
37
37
  import weekOfYear from 'dayjs/plugin/weekOfYear'
38
- import {addContextToContent} from "./contentAggregator.js";
38
+ import { addContextToContent } from './contentAggregator.js'
39
39
 
40
40
  const DATA_KEY_PRACTICES = 'practices'
41
41
  const DATA_KEY_LAST_UPDATED_TIME = 'u'
@@ -585,10 +585,10 @@ export async function getRecentActivity({ page = 1, limit = 5, tabName = null }
585
585
  const contentIds = recentActivityData.data.map((p) => p.contentId).filter((id) => id !== null)
586
586
  const contents = await addContextToContent(fetchByRailContentIds, contentIds, {
587
587
  addNavigateTo: true,
588
- addNextLesson: true
588
+ addNextLesson: true,
589
589
  })
590
590
  recentActivityData.data = recentActivityData.data.map((practice) => {
591
- const content = contents.find((c) => c.id === practice.contentId) || {}
591
+ const content = contents?.find((c) => c.id === practice.contentId) || {}
592
592
  return {
593
593
  ...practice,
594
594
  parent_id: content.parent_id || null,
@@ -853,14 +853,15 @@ export async function calculateLongestStreaks(userId = globalConfig.sessionConfi
853
853
  }
854
854
  }
855
855
 
856
- async function formatPracticeMeta(practices) {
856
+ async function formatPracticeMeta(practices = []) {
857
857
  const contentIds = practices.map((p) => p.content_id).filter((id) => id !== null)
858
858
  const contents = await addContextToContent(fetchByRailContentIds, contentIds, {
859
859
  addNavigateTo: true,
860
- addNextLesson: true
860
+ addNextLesson: true,
861
861
  })
862
+
862
863
  return practices.map((practice) => {
863
- const content = contents.find((c) => c.id === practice.content_id) || {}
864
+ const content = contents && contents.length > 0 ? contents.find((c) => c.id === practice.content_id) : {}
864
865
 
865
866
  return {
866
867
  id: practice.id,
@@ -869,18 +870,18 @@ async function formatPracticeMeta(practices) {
869
870
  thumbnail_url: practice.content_id ? content.thumbnail : practice.thumbnail_url || '',
870
871
  duration: practice.duration_seconds || 0,
871
872
  duration_seconds: practice.duration_seconds || 0,
872
- content_url: content.url || null,
873
+ content_url: content?.url || null,
873
874
  title: practice.content_id ? content.title : practice.title,
874
875
  category_id: practice.category_id,
875
876
  instrument_id: practice.instrument_id,
876
- content_type: getFormattedType(content.type || '', content.brand),
877
+ content_type: getFormattedType(content?.type || '', content?.brand || null),
877
878
  content_id: practice.content_id || null,
878
- content_brand: content.brand || null,
879
+ content_brand: content?.brand || null,
879
880
  created_at: dayjs(practice.created_at),
880
- sanity_type: content.type || null,
881
- content_slug: content.slug || null,
882
- parent_id: content.parent_id || null,
883
- navigateTo: content.navigateTo || null,
881
+ sanity_type: content?.type || null,
882
+ content_slug: content?.slug || null,
883
+ parent_id: content?.parent_id || null,
884
+ navigateTo: content?.navigateTo || null,
884
885
  }
885
886
  })
886
887
  }
@@ -929,24 +930,47 @@ export async function deleteUserActivity(id) {
929
930
  return await fetchHandler(url, 'DELETE')
930
931
  }
931
932
 
932
- async function extractPinnedItemsAndSortAllItems(userPinnedItem, contentsMap, eligiblePlaylistItems, pinnedGuidedCourse, limit) {
933
+ /**
934
+ * Restores a specific user activity by its ID.
935
+ *
936
+ * @param {number|string} id - The ID of the user activity to restore.
937
+ * @returns {Promise<Object>} - A promise that resolves to the API response after restoration.
938
+ *
939
+ * @example
940
+ * restoreUserActivity(789)
941
+ * .then(response => console.log('Restored:', response))
942
+ * .catch(error => console.error(error));
943
+ */
944
+ export async function restoreUserActivity(id) {
945
+ const url = `/api/user-management-system/v1/activities/${id}`
946
+ return await fetchHandler(url, 'POST')
947
+ }
948
+
949
+ async function extractPinnedItemsAndSortAllItems(
950
+ userPinnedItem,
951
+ contentsMap,
952
+ eligiblePlaylistItems,
953
+ pinnedGuidedCourse,
954
+ limit
955
+ ) {
933
956
  let pinnedItem = await popPinnedItemFromContentsOrPlaylistMap(
934
957
  userPinnedItem,
935
958
  contentsMap,
936
- eligiblePlaylistItems,
959
+ eligiblePlaylistItems
937
960
  )
938
961
 
939
962
  const guidedCourseID = pinnedGuidedCourse?.content_id
940
- let combined = [];
963
+ let combined = []
941
964
  if (pinnedGuidedCourse) {
942
- const guidedCourseContent = contentsMap.get(guidedCourseID) ?? await addContextToContent(fetchByRailContentId, guidedCourseID, 'guided-course',
943
- {
965
+ const guidedCourseContent =
966
+ contentsMap.get(guidedCourseID) ??
967
+ (await addContextToContent(fetchByRailContentId, guidedCourseID, 'guided-course', {
944
968
  addNextLesson: true,
945
969
  addNavigateTo: true,
946
970
  addProgressStatus: true,
947
971
  addProgressPercentage: true,
948
972
  addProgressTimestamp: true,
949
- })
973
+ }))
950
974
  contentsMap = popContentAndRemoveChildrenFromContentsMap(guidedCourseContent, contentsMap)
951
975
  guidedCourseContent.pinned = true
952
976
  combined.push(guidedCourseContent)
@@ -967,23 +991,26 @@ function generateContentsMap(contents, playlistsContents) {
967
991
  'learning-path-course',
968
992
  'learning-path-level',
969
993
  'guided-course-part',
970
- ]);
971
- const existingShows = new Set();
972
- const contentsMap = new Map();
973
- const childToParentMap = {};
974
- contents.forEach(content => {
994
+ ])
995
+ const existingShows = new Set()
996
+ const contentsMap = new Map()
997
+ const childToParentMap = {}
998
+ contents.forEach((content) => {
975
999
  if (Array.isArray(content.parent_content_data) && content.parent_content_data.length > 0) {
976
- childToParentMap[content.id] = content.parent_content_data[content.parent_content_data.length - 1];
1000
+ childToParentMap[content.id] =
1001
+ content.parent_content_data[content.parent_content_data.length - 1]
977
1002
  }
978
- });
1003
+ })
979
1004
 
980
- const allRecentTypeSet = new Set(
981
- Object.values(recentTypes).flat()
982
- )
983
- contents.forEach(content => {
1005
+ const allRecentTypeSet = new Set(Object.values(recentTypes).flat())
1006
+ contents.forEach((content) => {
984
1007
  const id = content.id
985
1008
  const type = content.type
986
- if (excludedTypes.has(type) || (!allRecentTypeSet.has(type) && !showsLessonTypes.includes(type)) ) return;
1009
+ if (
1010
+ excludedTypes.has(type) ||
1011
+ (!allRecentTypeSet.has(type) && !showsLessonTypes.includes(type))
1012
+ )
1013
+ return
987
1014
  if (!childToParentMap[id]) {
988
1015
  // Shows don't have a parent to link them, but need to be handled as if they're a set of children
989
1016
  if (!existingShows.has(type)) {
@@ -1001,11 +1028,11 @@ function generateContentsMap(contents, playlistsContents) {
1001
1028
  for (const item of playlistsContents) {
1002
1029
  const contentId = item.id
1003
1030
  contentsMap.delete(contentId)
1004
- const parentIds = item.parent_content_data || [];
1005
- parentIds.forEach(id => contentsMap.delete(id));
1031
+ const parentIds = item.parent_content_data || []
1032
+ parentIds.forEach((id) => contentsMap.delete(id))
1006
1033
  }
1007
1034
  }
1008
- return contentsMap;
1035
+ return contentsMap
1009
1036
  }
1010
1037
 
1011
1038
  /**
@@ -1022,19 +1049,21 @@ function generateContentsMap(contents, playlistsContents) {
1022
1049
  * .catch(error => console.error(error));
1023
1050
  */
1024
1051
  export async function getProgressRows({ brand = null, limit = 8 } = {}) {
1025
-
1026
1052
  // TODO slice progress to a reasonable number, say 100
1027
- const [recentPlaylists, progressContents, allPinnedGuidedCourse, userPinnedItem ] = await Promise.all([
1028
- fetchUserPlaylists(brand, { sort: '-last_progress', limit: limit}),
1029
- getAllStartedOrCompleted({onlyIds: false, brand: brand }),
1030
- pinnedGuidedCourses(brand),
1031
- getUserPinnedItem(brand),
1032
- ])
1053
+ const [recentPlaylists, progressContents, allPinnedGuidedCourse, userPinnedItem] =
1054
+ await Promise.all([
1055
+ fetchUserPlaylists(brand, { sort: '-last_progress', limit: limit }),
1056
+ getAllStartedOrCompleted({ onlyIds: false, brand: brand }),
1057
+ pinnedGuidedCourses(brand),
1058
+ getUserPinnedItem(brand),
1059
+ ])
1033
1060
  let pinnedGuidedCourse = allPinnedGuidedCourse?.[0] ?? null
1034
1061
 
1035
- const playlists = recentPlaylists?.data || [];
1036
- const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists);
1037
- const playlistEngagedOnContents = eligiblePlaylistItems.map(item => item.playlist.last_engaged_on);
1062
+ const playlists = recentPlaylists?.data || []
1063
+ const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists)
1064
+ const playlistEngagedOnContents = eligiblePlaylistItems.map(
1065
+ (item) => item.playlist.last_engaged_on
1066
+ )
1038
1067
 
1039
1068
  const nonPlaylistContentIds = Object.keys(progressContents)
1040
1069
  if (pinnedGuidedCourse) {
@@ -1043,7 +1072,7 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
1043
1072
  if (userPinnedItem?.progressType === 'content') {
1044
1073
  nonPlaylistContentIds.push(userPinnedItem.id)
1045
1074
  }
1046
- const [ playlistsContents, contents ] = await Promise.all([
1075
+ const [playlistsContents, contents] = await Promise.all([
1047
1076
  addContextToContent(fetchByRailContentIds, playlistEngagedOnContents, 'progress-tracker', {
1048
1077
  addNextLesson: true,
1049
1078
  addNavigateTo: true,
@@ -1057,17 +1086,23 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
1057
1086
  addProgressStatus: true,
1058
1087
  addProgressPercentage: true,
1059
1088
  addProgressTimestamp: true,
1060
- })
1061
- ]);
1062
- const contentsMap = generateContentsMap(contents, playlistsContents);
1063
- let combined = await extractPinnedItemsAndSortAllItems(userPinnedItem, contentsMap, eligiblePlaylistItems, pinnedGuidedCourse, limit);
1089
+ }),
1090
+ ])
1091
+ const contentsMap = generateContentsMap(contents, playlistsContents)
1092
+ let combined = await extractPinnedItemsAndSortAllItems(
1093
+ userPinnedItem,
1094
+ contentsMap,
1095
+ eligiblePlaylistItems,
1096
+ pinnedGuidedCourse,
1097
+ limit
1098
+ )
1064
1099
  const results = await Promise.all(
1065
- combined.slice(0, limit).map(item =>
1066
- item.type === 'playlist'
1067
- ? processPlaylistItem(item)
1068
- : processContentItem(item)
1069
- )
1070
- );
1100
+ combined
1101
+ .slice(0, limit)
1102
+ .map((item) =>
1103
+ item.type === 'playlist' ? processPlaylistItem(item) : processContentItem(item)
1104
+ )
1105
+ )
1071
1106
  console.log('HomePageProgressRows results: remove before merge', results)
1072
1107
  return {
1073
1108
  type: TabResponseType.PROGRESS_ROWS,
@@ -1084,44 +1119,48 @@ async function getUserPinnedItem(brand) {
1084
1119
  }
1085
1120
 
1086
1121
  async function processContentItem(content) {
1087
- const contentType = getFormattedType(content.type, content.brand);
1122
+ const contentType = getFormattedType(content.type, content.brand)
1088
1123
  const isLive = content.isLive ?? false
1089
1124
  let ctaText = getDefaultCTATextForContent(content, contentType)
1090
1125
 
1091
1126
  content.completed_children = await getCompletedChildren(content, contentType)
1092
1127
 
1093
1128
  if (content.type === 'guided-course') {
1094
- const nextLessonPublishedOn = content.children.find(child => child.id === content.navigateTo.id)?.published_on
1129
+ const nextLessonPublishedOn = content.children.find(
1130
+ (child) => child.id === content.navigateTo.id
1131
+ )?.published_on
1095
1132
  let isLocked = new Date(nextLessonPublishedOn) > new Date()
1096
1133
  if (isLocked) {
1097
1134
  content.is_locked = true
1098
- const timeRemaining = getTimeRemainingUntilLocal(nextLessonPublishedOn, {withTotalSeconds: true})
1135
+ const timeRemaining = getTimeRemainingUntilLocal(nextLessonPublishedOn, {
1136
+ withTotalSeconds: true,
1137
+ })
1099
1138
  content.time_remaining_seconds = timeRemaining.totalSeconds
1100
1139
  ctaText = 'Next lesson in ' + timeRemaining.formatted
1101
- } else if (!content.progressStatus || content.progressStatus === 'not-started' ) {
1102
- ctaText = "Start Course"
1140
+ } else if (!content.progressStatus || content.progressStatus === 'not-started') {
1141
+ ctaText = 'Start Course'
1103
1142
  }
1104
1143
  }
1105
1144
 
1106
- if (contentType === 'show'){
1145
+ if (contentType === 'show') {
1107
1146
  const shows = await fetchShows(content.brand, content.type)
1108
- const showIds = shows.map(item => item.id);
1109
- const progressOnItems = await getProgressStateByIds(showIds);
1147
+ const showIds = shows.map((item) => item.id)
1148
+ const progressOnItems = await getProgressStateByIds(showIds)
1110
1149
  const completedShows = content.completed_children
1111
1150
  const progressTimestamp = content.progressTimestamp
1112
1151
  const wasPinned = content.pinned ?? false
1113
1152
  if (content.progressStatus === 'completed') {
1114
1153
  // this could be handled more gracefully if their was a parent content type for shows
1115
- const nextByProgress = findIncompleteLesson(progressOnItems, content.id, content.type);
1116
- content = shows.find(lesson => lesson.id === nextByProgress);
1154
+ const nextByProgress = findIncompleteLesson(progressOnItems, content.id, content.type)
1155
+ content = shows.find((lesson) => lesson.id === nextByProgress)
1117
1156
  content.completed_children = completedShows
1118
1157
  content.progressTimestamp = progressTimestamp
1119
1158
  content.pinned = wasPinned
1120
1159
  }
1121
- content.child_count = shows.length;
1122
- content.progressPercentage = Math.round((completedShows / shows.length) * 100);
1160
+ content.child_count = shows.length
1161
+ content.progressPercentage = Math.round((completedShows / shows.length) * 100)
1123
1162
  if (completedShows === shows.length) {
1124
- ctaText = 'Revisit Show';
1163
+ ctaText = 'Revisit Show'
1125
1164
  }
1126
1165
  }
1127
1166
 
@@ -1132,70 +1171,77 @@ async function processContentItem(content) {
1132
1171
  pinned: content.pinned ?? false,
1133
1172
  content: content,
1134
1173
  body: {
1135
- progressPercent: isLive ? undefined: content.progressPercentage,
1174
+ progressPercent: isLive ? undefined : content.progressPercentage,
1136
1175
  thumbnail: content.thumbnail,
1137
1176
  title: content.title,
1138
1177
  isLive: isLive,
1139
1178
  badge: content.badge ?? null,
1140
1179
  isLocked: content.is_locked ?? false,
1141
- subtitle: !content.child_count || content.lesson_count === 1
1142
- ? (contentType === 'lesson' && isLive === false) ? `${content.progressPercentage}% Complete`: `${content.difficulty_string} ${content.artist_name}`
1143
- : `${content.completed_children} of ${content.lesson_count ?? content.child_count} Lessons Complete`
1180
+ subtitle: collectionLessonTypes.includes(content.type) || content.lesson_count > 1
1181
+ ? `${content.completed_children} of ${content.lesson_count ?? content.child_count} Lessons Complete`
1182
+ : (contentType === 'lesson' && isLive === false) ? `${content.progressPercentage}% Complete`: `${content.difficulty_string} ${content.artist_name}`
1144
1183
  },
1145
- cta: {
1146
- text: ctaText,
1184
+ cta: {
1185
+ text: ctaText,
1147
1186
  timeRemainingToUnlockSeconds: content.time_remaining_seconds ?? null,
1148
1187
  action: {
1149
- type: content.type,
1188
+ type: content.type,
1150
1189
  brand: content.brand,
1151
- id: content.id,
1152
- slug: content.slug,
1190
+ id: content.id,
1191
+ slug: content.slug,
1153
1192
  child: content.navigateTo,
1154
- }
1193
+ },
1155
1194
  },
1156
1195
  // *1000 is to match playlists which are saved in millisecond accuracy
1157
- progressTimestamp: content.progressTimestamp * 1000
1158
- };
1196
+ progressTimestamp: content.progressTimestamp * 1000,
1197
+ }
1159
1198
  }
1160
1199
 
1161
- function getDefaultCTATextForContent(content, contentType)
1162
- {
1163
- let ctaText = 'Continue';
1164
- if (content.progressStatus === 'completed')
1165
- {
1166
- if (contentType === songs[content.brand] || contentType === 'play along' || contentType === 'jam track') ctaText = 'Replay Song';
1167
- if (contentType === 'lesson') ctaText = 'Revisit Lesson';
1168
- if (contentType === 'song tutorial' || collectionLessonTypes.includes(contentType)) ctaText = 'Revisit Lessons' ;
1200
+ function getDefaultCTATextForContent(content, contentType) {
1201
+ let ctaText = 'Continue'
1202
+ if (content.progressStatus === 'completed') {
1203
+ if (
1204
+ contentType === songs[content.brand] ||
1205
+ contentType === 'play along' ||
1206
+ contentType === 'jam track'
1207
+ )
1208
+ ctaText = 'Replay Song'
1209
+ if (contentType === 'lesson') ctaText = 'Revisit Lesson'
1210
+ if (contentType === 'song tutorial' || collectionLessonTypes.includes(contentType))
1211
+ ctaText = 'Revisit Lessons'
1169
1212
  if (contentType === 'pack') ctaText = 'View Lessons'
1170
1213
  }
1171
1214
  return ctaText
1172
1215
  }
1173
1216
 
1174
- async function getCompletedChildren(content, contentType)
1175
- {
1217
+ async function getCompletedChildren(content, contentType) {
1176
1218
  let completedChildren = null
1177
1219
  if (contentType === 'show') {
1178
1220
  const shows = await addContextToContent(fetchShows, content.brand, content.type, {
1179
1221
  addProgressStatus: true,
1180
1222
  })
1181
- completedChildren = Object.values(shows).filter(show => show.progressStatus === 'completed').length;
1223
+ completedChildren = Object.values(shows).filter(
1224
+ (show) => show.progressStatus === 'completed'
1225
+ ).length
1182
1226
  } else if (content.lesson_count > 0) {
1183
- const lessonIds = getLeafNodes(content);
1184
- const progressOnItems = await getProgressStateByIds(lessonIds);
1185
- completedChildren = Object.values(progressOnItems).filter(value => value === 'completed').length;
1227
+ const lessonIds = getLeafNodes(content)
1228
+ const progressOnItems = await getProgressStateByIds(lessonIds)
1229
+ completedChildren = Object.values(progressOnItems).filter(
1230
+ (value) => value === 'completed'
1231
+ ).length
1186
1232
  }
1187
1233
  return completedChildren
1188
1234
  }
1189
1235
 
1190
1236
  async function processPlaylistItem(item) {
1191
- const playlist = item.playlist;
1237
+ const playlist = item.playlist
1192
1238
  return {
1193
- id: playlist.id,
1194
- progressType: 'playlist',
1195
- header: 'playlist',
1196
- pinned: item.pinned ?? false,
1197
- playlist: playlist,
1198
- body: {
1239
+ id: playlist.id,
1240
+ progressType: 'playlist',
1241
+ header: 'playlist',
1242
+ pinned: item.pinned ?? false,
1243
+ playlist: playlist,
1244
+ body: {
1199
1245
  first_items_thumbnail_url: playlist.first_items_thumbnail_url,
1200
1246
  title: playlist.name,
1201
1247
  subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`,
@@ -1205,14 +1251,14 @@ async function processPlaylistItem(item) {
1205
1251
  cta: {
1206
1252
  text: 'Continue',
1207
1253
  action: {
1208
- brand: playlist.brand,
1254
+ brand: playlist.brand,
1209
1255
  item_id: playlist.navigateTo.id ?? null,
1210
1256
  content_id: playlist.navigateTo.content_id ?? null,
1211
- type: 'playlists',
1257
+ type: 'playlists',
1212
1258
  // TODO depreciated, maintained to avoid breaking changes
1213
- id: playlist.id,
1214
- }
1215
- }
1259
+ id: playlist.id,
1260
+ },
1261
+ },
1216
1262
  }
1217
1263
  }
1218
1264
 
@@ -1227,18 +1273,18 @@ const getFormattedType = (type, brand) => {
1227
1273
  }
1228
1274
 
1229
1275
  function getLeafNodes(content) {
1230
- const ids = [];
1276
+ const ids = []
1231
1277
  function traverse(children) {
1232
1278
  for (const item of children) {
1233
1279
  if (item.children) {
1234
- traverse(item.children); // Recursively handle nested lessons
1280
+ traverse(item.children) // Recursively handle nested lessons
1235
1281
  } else if (item.id) {
1236
- ids.push(item.id);
1282
+ ids.push(item.id)
1237
1283
  }
1238
1284
  }
1239
1285
  }
1240
1286
  if (content && Array.isArray(content.children)) {
1241
- traverse(content.children);
1287
+ traverse(content.children)
1242
1288
  }
1243
1289
  return ids
1244
1290
  }
@@ -1255,7 +1301,7 @@ async function getEligiblePlaylistItems(playlists) {
1255
1301
  progressTimestamp: timestamp / 1000,
1256
1302
  playlist: p,
1257
1303
  id: p.id,
1258
- };
1304
+ }
1259
1305
  })
1260
1306
  )
1261
1307
  }
@@ -1265,7 +1311,7 @@ function mergeAndSortItems(items, limit) {
1265
1311
  const deduped = []
1266
1312
 
1267
1313
  for (const item of items) {
1268
- const key = `${item.id}-${item.type}`;
1314
+ const key = `${item.id}-${item.type}`
1269
1315
  if (!seen.has(key)) {
1270
1316
  seen.add(key)
1271
1317
  deduped.push(item)
@@ -1273,12 +1319,12 @@ function mergeAndSortItems(items, limit) {
1273
1319
  }
1274
1320
 
1275
1321
  return deduped
1276
- .filter(item => typeof item.progressTimestamp === 'number' && item.progressTimestamp >= 0)
1322
+ .filter((item) => typeof item.progressTimestamp === 'number' && item.progressTimestamp >= 0)
1277
1323
  .sort((a, b) => {
1278
- if (a.pinned && !b.pinned) return -1;
1279
- if (!a.pinned && b.pinned) return 1;
1324
+ if (a.pinned && !b.pinned) return -1
1325
+ if (!a.pinned && b.pinned) return 1
1280
1326
  // TODO pinned guided course should always be before user pinned item
1281
- return b.progressTimestamp - a.progressTimestamp;
1327
+ return b.progressTimestamp - a.progressTimestamp
1282
1328
  })
1283
1329
  .slice(0, limit + 5)
1284
1330
  }
@@ -1306,39 +1352,36 @@ export function findIncompleteLesson(progressOnItems, currentContentId, contentT
1306
1352
 
1307
1353
  async function popPinnedItemFromContentsOrPlaylistMap(pinned, contentsMap, playlistItems) {
1308
1354
  if (!pinned) return null
1309
- const {id, progressType, pinnedAt} = pinned
1355
+ const { id, progressType, pinnedAt } = pinned
1310
1356
  let item = null
1311
1357
  if (progressType === 'content') {
1312
1358
  const pinnedId = parseInt(id)
1313
1359
  if (contentsMap.has(pinnedId)) {
1314
1360
  item = contentsMap.get(pinnedId)
1315
1361
  contentsMap.delete(pinnedId)
1316
-
1317
1362
  } else {
1318
1363
  // we use fetchByRailContentIds so that we don't have the _type restriction in the query
1319
1364
  let data = await fetchByRailContentIds([id], 'progress-tracker')
1320
- item = await addContextToContent(() => data[0] ?? null,
1321
- {
1322
- addNextLesson: true,
1323
- addNavigateTo: true,
1324
- addProgressStatus: true,
1325
- addProgressPercentage: true,
1326
- addProgressTimestamp: true
1327
- }
1328
- )
1365
+ item = await addContextToContent(() => data[0] ?? null, {
1366
+ addNextLesson: true,
1367
+ addNavigateTo: true,
1368
+ addProgressStatus: true,
1369
+ addProgressPercentage: true,
1370
+ addProgressTimestamp: true,
1371
+ })
1329
1372
  }
1330
1373
  }
1331
1374
  if (progressType === 'playlist') {
1332
- const pinnedPlaylist = playlistItems.find(p => p.playlist.id === id)
1375
+ const pinnedPlaylist = playlistItems.find((p) => p.playlist.id === id)
1333
1376
  if (pinnedPlaylist) {
1334
- playlistItems = playlistItems.filter(p => p.playlist.id !== id)
1377
+ playlistItems = playlistItems.filter((p) => p.playlist.id !== id)
1335
1378
  item = pinnedPlaylist
1336
1379
  } else {
1337
1380
  const playlist = await fetchPlaylist(id)
1338
1381
  item = {
1339
- id: id,
1340
- playlist: playlist,
1341
- type: 'playlist',
1382
+ id: id,
1383
+ playlist: playlist,
1384
+ type: 'playlist',
1342
1385
  progressTimestamp: new Date(pinnedAt).getTime(),
1343
1386
  }
1344
1387
  }
@@ -1347,11 +1390,11 @@ async function popPinnedItemFromContentsOrPlaylistMap(pinned, contentsMap, playl
1347
1390
  }
1348
1391
 
1349
1392
  function popContentAndRemoveChildrenFromContentsMap(content, contentsMap) {
1350
- const children = content.children.map(child => child.id)
1393
+ const children = content.children.map((child) => child.id)
1351
1394
  if (contentsMap.has(content.id)) {
1352
1395
  contentsMap.delete(content.id)
1353
1396
  }
1354
- children.forEach(child => {
1397
+ children.forEach((child) => {
1355
1398
  if (contentsMap.has(child)) {
1356
1399
  contentsMap.delete(child)
1357
1400
  }