musora-content-services 2.7.0 → 2.7.2

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 (157) hide show
  1. package/.coderabbit.yaml +0 -0
  2. package/.editorconfig +0 -0
  3. package/.github/pull_request_template.md +0 -0
  4. package/.github/workflows/node.js.yml +0 -0
  5. package/.prettierignore +0 -0
  6. package/.prettierrc +0 -0
  7. package/.yarnrc.yml +1 -0
  8. package/CHANGELOG.md +4 -0
  9. package/README.md +0 -0
  10. package/babel.config.cjs +0 -0
  11. package/docs/Content-Organization.html +0 -0
  12. package/docs/ContentOrganization.html +2 -2
  13. package/docs/Gamification.html +2 -2
  14. package/docs/UserManagement.html +0 -0
  15. package/docs/UserManagementSystem.html +2 -2
  16. package/docs/api_types.js.html +2 -2
  17. package/docs/config.js.html +2 -2
  18. package/docs/content-org_content-org.js.html +2 -2
  19. package/docs/content-org_playlists-types.js.html +3 -5
  20. package/docs/content-org_playlists.js.html +23 -9
  21. package/docs/content.js.html +9 -2
  22. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  23. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  24. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  25. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  26. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  27. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  28. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  29. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  30. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  31. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
  32. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  33. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  34. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  35. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  36. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
  37. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  38. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  39. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  40. package/docs/gamification_awards.js.html +2 -2
  41. package/docs/gamification_gamification.js.html +2 -2
  42. package/docs/gamification_types.js.html +2 -2
  43. package/docs/global.html +5 -53
  44. package/docs/global.html#User +0 -0
  45. package/docs/index.html +2 -2
  46. package/docs/module-Awards.html +2 -2
  47. package/docs/module-Config.html +2 -2
  48. package/docs/module-Content-Services-V2.html +8 -8
  49. package/docs/module-Content-Services.html +763 -0
  50. package/docs/module-Interests.html +2 -2
  51. package/docs/module-Permissions.html +2 -2
  52. package/docs/module-Playlists.html +21 -20
  53. package/docs/module-Railcontent-Services.html +2 -2
  54. package/docs/module-Sanity-Services.html +30 -30
  55. package/docs/module-Session-Management.html +0 -0
  56. package/docs/module-Sessions.html +2 -2
  57. package/docs/module-User-Activity.html +737 -58
  58. package/docs/module-User-Management.html +0 -0
  59. package/docs/module-User-Permissions.html +0 -0
  60. package/docs/module-UserManagement.html +2 -2
  61. package/docs/module-UserProfile.html +2 -2
  62. package/docs/railcontent.js.html +2 -2
  63. package/docs/sanity.js.html +24 -6
  64. package/docs/scripts/collapse.js +0 -0
  65. package/docs/scripts/commonNav.js +0 -0
  66. package/docs/scripts/linenumber.js +0 -0
  67. package/docs/scripts/nav.js +0 -0
  68. package/docs/scripts/polyfill.js +0 -0
  69. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
  70. package/docs/scripts/prettify/lang-css.js +0 -0
  71. package/docs/scripts/prettify/prettify.js +0 -0
  72. package/docs/scripts/search.js +0 -0
  73. package/docs/styles/jsdoc.css +0 -0
  74. package/docs/styles/prettify.css +0 -0
  75. package/docs/types.js.html +0 -0
  76. package/docs/userActivity.js.html +360 -12
  77. package/docs/user_interests.js.html +2 -2
  78. package/docs/user_management.js.html +2 -2
  79. package/docs/user_permissions.js.html +2 -2
  80. package/docs/user_profile.js.html +4 -4
  81. package/docs/user_sessions.js.html +2 -2
  82. package/docs/user_types.js.html +2 -2
  83. package/docs/user_user-management-system.js.html +2 -2
  84. package/docs/user_user-management.js.html +0 -0
  85. package/jest.config.js +0 -0
  86. package/jsdoc.json +0 -0
  87. package/link_mcs.sh +0 -0
  88. package/package.json +1 -1
  89. package/src/contentMetaData.js +13 -12
  90. package/src/contentTypeConfig.js +18 -2
  91. package/src/filterBuilder.js +0 -0
  92. package/src/index.d.ts +10 -2
  93. package/src/index.js +10 -2
  94. package/src/infrastructure/http/HttpClient.ts +0 -0
  95. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
  96. package/src/infrastructure/http/index.ts +0 -0
  97. package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
  98. package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
  99. package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
  100. package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
  101. package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
  102. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
  103. package/src/lib/httpHelper.js +0 -0
  104. package/src/lib/lastUpdated.js +0 -0
  105. package/src/services/api/types.js +0 -0
  106. package/src/services/config.js +0 -0
  107. package/src/services/content-org/content-org.js +0 -0
  108. package/src/services/content-org/playlists-types.js +0 -0
  109. package/src/services/content-org/playlists.js +0 -0
  110. package/src/services/content.js +7 -0
  111. package/src/services/contentAggregator.js +0 -0
  112. package/src/services/contentLikes.js +0 -0
  113. package/src/services/contentProgress.js +49 -20
  114. package/src/services/dataContext.js +0 -0
  115. package/src/services/dateUtils.js +0 -0
  116. package/src/services/forum.js +0 -0
  117. package/src/services/gamification/awards.js +0 -0
  118. package/src/services/gamification/gamification.js +0 -0
  119. package/src/services/gamification/types.js +0 -0
  120. package/src/services/imageSRCBuilder.js +0 -0
  121. package/src/services/imageSRCVerify.js +0 -0
  122. package/src/services/railcontent.js +0 -0
  123. package/src/services/recommendations.js +0 -0
  124. package/src/services/sanity.js +22 -4
  125. package/src/services/types.js +0 -0
  126. package/src/services/user/interests.js +0 -0
  127. package/src/services/user/management.js +0 -0
  128. package/src/services/user/permissions.js +0 -0
  129. package/src/services/user/profile.js +0 -0
  130. package/src/services/user/sessions.js +0 -0
  131. package/src/services/user/types.js +0 -0
  132. package/src/services/user/user-management-system.js +0 -0
  133. package/src/services/userActivity.js +348 -10
  134. package/test/HttpClient.test.js +0 -0
  135. package/test/content.test.js +0 -0
  136. package/test/contentLikes.test.js +0 -0
  137. package/test/contentProgress.test.js +0 -0
  138. package/test/dataContext.test.js +0 -0
  139. package/test/forum.test.js +0 -0
  140. package/test/imageSRCBuilder.test.js +0 -0
  141. package/test/imageSRCVerify.test.js +0 -0
  142. package/test/initializeTests.js +0 -0
  143. package/test/lib/lastUpdated.test.js +0 -0
  144. package/test/live/contentProgressLive.test.js +0 -0
  145. package/test/live/railcontentLive.test.js +0 -0
  146. package/test/localStorageMock.js +0 -0
  147. package/test/log.js +0 -0
  148. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  149. package/test/mockData/mockData_progress_content.json +182 -0
  150. package/test/mockData/mockData_sanity_progress_content.json +1451 -0
  151. package/test/mockData/mockData_user_practices.json +0 -0
  152. package/test/progressRows.test.js +215 -0
  153. package/test/sanityQueryService.test.js +1 -1
  154. package/test/streakMessage.test.js +0 -0
  155. package/test/user/permissions.test.js +0 -0
  156. package/test/userActivity.test.js +0 -0
  157. package/tools/generate-index.cjs +0 -0
@@ -40,6 +40,16 @@ export async function getResumeTimeSecondsByIds(contentIds) {
40
40
  return getByIds(contentIds, DATA_KEY_RESUME_TIME, 0)
41
41
  }
42
42
 
43
+ export async function getProgressDateByIds(contentIds) {
44
+ let data = await dataContext.getData()
45
+ let progress = {}
46
+ contentIds?.forEach((id) => (progress[id] = {
47
+ 'last_update': data[id]?.[DATA_KEY_LAST_UPDATED_TIME] ?? 0,
48
+ 'progress': data[id]?.[DATA_KEY_PROGRESS] ?? 0,
49
+ 'status': data[id]?.[DATA_KEY_STATUS] ?? ''}))
50
+ return progress
51
+ }
52
+
43
53
  async function getById(contentId, dataKey, defaultValue) {
44
54
  let data = await dataContext.getData()
45
55
  return data[contentId]?.[dataKey] ?? defaultValue
@@ -96,29 +106,49 @@ export async function getAllCompleted(limit = null) {
96
106
  return ids
97
107
  }
98
108
 
99
- export async function getAllStartedOrCompleted(limit = null) {
109
+ export async function getAllStartedOrCompleted({ limit = null, onlyIds = true, brand = null, excludedIds = [] } = {}) {
100
110
  const data = await dataContext.getData()
101
- let ids = Object.keys(data)
102
- .filter(function (key) {
103
- return (
104
- data[parseInt(key)][DATA_KEY_STATUS] === STATE_STARTED ||
105
- data[parseInt(key)][DATA_KEY_STATUS] === STATE_COMPLETED
106
- )
107
- })
108
- .map(function (key) {
109
- return parseInt(key)
111
+ const oneMonthAgoInSeconds = Math.floor(Date.now() / 1000) - 60 * 24 * 60 * 60 // 60 days in seconds
112
+
113
+ const excludedSet = new Set(excludedIds.map(id => parseInt(id))) // ensure IDs are numbers
114
+
115
+ let filtered = Object.entries(data)
116
+ .filter(([key, item]) => {
117
+ const id = parseInt(key)
118
+ const isRelevantStatus =
119
+ item[DATA_KEY_STATUS] === STATE_STARTED || item[DATA_KEY_STATUS] === STATE_COMPLETED
120
+ const isRecent = item[DATA_KEY_LAST_UPDATED_TIME] >= oneMonthAgoInSeconds
121
+ const isCorrectBrand = !brand || item.b === brand
122
+ const isNotExcluded = !excludedSet.has(id)
123
+ return isRelevantStatus && isRecent && isCorrectBrand && isNotExcluded
110
124
  })
111
- .sort(function (a, b) {
112
- let v1 = data[a][DATA_KEY_LAST_UPDATED_TIME]
113
- let v2 = data[b][DATA_KEY_LAST_UPDATED_TIME]
125
+ .sort(([, a], [, b]) => {
126
+ const v1 = a[DATA_KEY_LAST_UPDATED_TIME]
127
+ const v2 = b[DATA_KEY_LAST_UPDATED_TIME]
114
128
  if (v1 > v2) return -1
115
129
  else if (v1 < v2) return 1
116
130
  return 0
117
131
  })
132
+
118
133
  if (limit) {
119
- ids = ids.slice(0, limit)
134
+ filtered = filtered.slice(0, limit)
135
+ }
136
+
137
+ if (onlyIds) {
138
+ return filtered.map(([key]) => parseInt(key))
139
+ } else {
140
+ const progress = {}
141
+ filtered.forEach(([key, item]) => {
142
+ const id = parseInt(key)
143
+ progress[id] = {
144
+ last_update: item?.[DATA_KEY_LAST_UPDATED_TIME] ?? 0,
145
+ progress: item?.[DATA_KEY_PROGRESS] ?? 0,
146
+ status: item?.[DATA_KEY_STATUS] ?? '',
147
+ brand: item?.b ?? '',
148
+ }
149
+ })
150
+ return progress
120
151
  }
121
- return ids
122
152
  }
123
153
 
124
154
  export async function assignmentStatusCompleted(assignmentId, parentContentId) {
@@ -252,12 +282,11 @@ export async function recordWatchSession(
252
282
  sessionId = uuidv4()
253
283
  }
254
284
 
255
- await recordUserPractice({ content_id: contentId, duration_seconds: secondsPlayed })
256
- try {
257
- await recordUserPractice({ content_id: contentId, duration_seconds: secondsPlayed })
258
- } catch (error) {
285
+ try {
286
+ await recordUserPractice({ content_id: contentId, duration_seconds: Math.ceil(secondsPlayed) })
287
+ } catch (error) {
259
288
  console.error('Failed to record user practice:', error)
260
- }
289
+ }
261
290
 
262
291
  await dataContext.update(
263
292
  async function (localContext) {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -512,15 +512,18 @@ export async function fetchByRailContentId(id, contentType) {
512
512
  * .then(contents => console.log(contents))
513
513
  * .catch(error => console.error(error));
514
514
  */
515
- export async function fetchByRailContentIds(ids, contentType = undefined) {
515
+ export async function fetchByRailContentIds(ids, contentType = undefined, brand= undefined) {
516
516
  if (!ids) {
517
517
  return []
518
518
  }
519
519
  const idsString = ids.join(',')
520
+ const brandFilter = brand ? ` && brand == "${brand}"` : ''
521
+ const query = `*[
522
+ railcontent_id in [${idsString}]${brandFilter}
523
+ ]{
524
+ ${getFieldsForContentType(contentType)}
525
+ }`
520
526
 
521
- const query = `*[railcontent_id in [${idsString}]]{
522
- ${getFieldsForContentType(contentType)}
523
- }`
524
527
  const results = await fetchSanity(query, true)
525
528
 
526
529
  const sortFuction = function compare(a, b) {
@@ -2355,3 +2358,18 @@ export async function fetchScheduledAndNewReleases(
2355
2358
 
2356
2359
  return fetchSanity(query, true)
2357
2360
  }
2361
+
2362
+ export async function fetchShows(
2363
+ brand,
2364
+ type,
2365
+ sort = 'sort'
2366
+ ) {
2367
+ const sortOrder = getSortOrder(sort, brand)
2368
+ const filter = `_type == '${type}' && brand == '${brand}'`
2369
+ const filterParams = {}
2370
+
2371
+ const query = await buildQuery(filter, filterParams, getFieldsForContentType(type), {
2372
+ sortOrder: sortOrder
2373
+ })
2374
+ return fetchSanity(query, true)
2375
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -11,10 +11,13 @@ import {
11
11
  fetchRecentUserActivities,
12
12
  } from './railcontent'
13
13
  import { DataContext, UserActivityVersionKey } from './dataContext.js'
14
- import { fetchByRailContentIds } from './sanity'
15
- import { lessonTypesMapping } from '../contentTypeConfig'
14
+ import { fetchByRailContentIds, fetchShows } from './sanity'
15
+ import {fetchUserPlaylists} from "./content-org/playlists";
16
16
  import { convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay } from './dateUtils.js'
17
17
  import { globalConfig } from './config'
18
+ import {collectionLessonTypes, lessonTypesMapping, progressTypesMapping} from "../contentTypeConfig";
19
+ import {getAllStartedOrCompleted, getProgressStateByIds} from "./contentProgress";
20
+ import {TabResponseType} from "../contentMetaData";
18
21
 
19
22
  const DATA_KEY_PRACTICES = 'practices'
20
23
  const DATA_KEY_LAST_UPDATED_TIME = 'u'
@@ -835,14 +838,6 @@ async function formatPracticeMeta(practices) {
835
838
  })
836
839
  }
837
840
 
838
- export function getFormattedType(type) {
839
- for (const [key, values] of Object.entries(lessonTypesMapping)) {
840
- if (values.includes(type)) {
841
- return key.replace(/\b\w/g, (char) => char.toUpperCase())
842
- }
843
- }
844
- return null
845
- }
846
841
 
847
842
  /**
848
843
  * Records a new user activity in the system.
@@ -887,3 +882,346 @@ export async function deleteUserActivity(id) {
887
882
  const url = `/api/user-management-system/v1/activities/${id}`
888
883
  return await fetchHandler(url, 'DELETE')
889
884
  }
885
+ /**
886
+ * Fetches and combines recent user progress rows and playlists, excluding certain types and parents.
887
+ *
888
+ * @param {Object} [options={}] - Options for fetching progress rows.
889
+ * @param {string|null} [options.brand=null] - The brand context for progress data.
890
+ * @param {number} [options.limit=8] - Maximum number of progress rows to return.
891
+ * @returns {Promise<Object>} - A promise that resolves to an object containing progress rows formatted for UI.
892
+ *
893
+ * @example
894
+ * getProgressRows({ brand: 'drumeo', limit: 10 })
895
+ * .then(data => console.log(data))
896
+ * .catch(error => console.error(error));
897
+ */
898
+ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
899
+ const excludedTypes = new Set([
900
+ 'pack-bundle',
901
+ 'learning-path-course',
902
+ 'learning-path-level'
903
+ ]);
904
+
905
+ const recentPlaylists = await fetchUserPlaylists(brand, {
906
+ sort: '-last_progress',
907
+ limit: limit,
908
+ });
909
+ const playlists = recentPlaylists?.data || [];
910
+ const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists);
911
+ const playlistEngagedOnContents = eligiblePlaylistItems.map(item => item.last_engaged_on);
912
+ const playlistsContents = await fetchByRailContentIds(playlistEngagedOnContents, 'progress-tracker');
913
+ const excludedParents = new Set();
914
+ for (const item of playlistsContents) {
915
+ const contentId = item.id ?? item.railcontent_id;
916
+ excludedParents.add(contentId)
917
+ const parentIds = item.parent_content_data || [];
918
+ parentIds.forEach(id => excludedParents.add(id));
919
+ }
920
+
921
+ const progressContents = await getAllStartedOrCompleted({onlyIds: false, brand: brand, excludedIds: Array.from(excludedParents) });
922
+ const contents = await fetchByRailContentIds(Object.keys(progressContents), 'progress-tracker', brand);
923
+ const contentsMap = {};
924
+ contents.forEach(content => {
925
+ contentsMap[content.railcontent_id] = content;
926
+ });
927
+ const childToParentMap = {};
928
+ Object.values(contentsMap).forEach(content => {
929
+ if (Array.isArray(content.parent_content_data) && content.parent_content_data.length > 0) {
930
+ childToParentMap[content.id] = content.parent_content_data[content.parent_content_data.length - 1];
931
+ }
932
+ });
933
+ const progressMap = new Map();
934
+ for (const [idStr, progress] of Object.entries(progressContents)) {
935
+ const id = parseInt(idStr);
936
+ const content = contentsMap[id];
937
+ if (!content || excludedTypes.has(content.type)) continue;
938
+ const parentId = childToParentMap[id];
939
+ // Handle children with parents
940
+ if (parentId) {
941
+ const parentContent = contentsMap[parentId];
942
+ if (!parentContent || excludedTypes.has(parentContent.type)) continue;
943
+ const existing = progressMap.get(parentId);
944
+ if (existing) {
945
+ // If childIndex isn't already set, set it
946
+ if (existing.childIndex === undefined) {
947
+ existing.childIndex = id;
948
+ }
949
+ } else {
950
+ progressMap.set(parentId, {
951
+ id: parentId,
952
+ raw: parentContent,
953
+ state: progress.status,
954
+ percent: progress.progress,
955
+ progressTimestamp: progress.last_update * 1000,
956
+ childIndex: id
957
+ });
958
+ }
959
+ continue;
960
+ }
961
+ // Handle standalone parents
962
+ if (!progressMap.has(id)) {
963
+ progressMap.set(id, {
964
+ id,
965
+ raw: content,
966
+ state: progress.status,
967
+ percent: progress.progress,
968
+ progressTimestamp: progress.last_update * 1000
969
+ });
970
+ }
971
+ }
972
+ const progressList = Array.from(progressMap.values());
973
+
974
+ const combined = mergeAndSortItems([...progressList, ...eligiblePlaylistItems], limit);
975
+ const results = await Promise.all(
976
+ combined.slice(0, limit).map(item =>
977
+ item.type === 'playlist'
978
+ ? processPlaylistItem(item)
979
+ : processContentItem(item)
980
+ )
981
+ );
982
+
983
+ return {
984
+ type: TabResponseType.PROGRESS_ROWS,
985
+ data: results
986
+ };
987
+ }
988
+
989
+ async function processContentItem(item) {
990
+ let data = item.raw;
991
+ const contentType = getFormattedType(data.type);
992
+ const status = item.state;
993
+
994
+ let ctaText = 'Continue';
995
+ if (contentType === 'transcription' || contentType === 'play-along' || contentType === 'jam-track') ctaText = 'Replay Song';
996
+ if (contentType === 'lesson') ctaText = status === 'completed' ? 'Revisit Lesson' : 'Continue';
997
+ if ((contentType === 'pack' || contentType === 'song tutorial' || collectionLessonTypes.includes(contentType)) && status === 'completed') {
998
+ ctaText = 'View Lessons';
999
+ }
1000
+
1001
+ if (data.lesson_count > 0) {
1002
+ if (item.childIndex) {
1003
+ let nextId = item.childIndex
1004
+ const lessonIds = extractLessonIds(item);
1005
+ const progressOnItems = await getProgressStateByIds(lessonIds);
1006
+ const completedCount = Object.values(progressOnItems).filter(value => value === 'completed').length;
1007
+ const nextByProgress = findIncompleteLesson(progressOnItems, item.childIndex, item.raw.type);
1008
+ nextId = nextByProgress ? nextByProgress : nextId;
1009
+
1010
+ const nestedLessons = data.lessons
1011
+ .filter(item => Array.isArray(item.lessons))
1012
+ .flatMap(parent =>
1013
+ parent.lessons.map(lesson => ({
1014
+ ...lesson,
1015
+ parent: {
1016
+ id: parent.id,
1017
+ slug: parent.slug,
1018
+ title: parent.title,
1019
+ type: parent.type
1020
+ }
1021
+ }))
1022
+ );
1023
+
1024
+ const lessons = (nestedLessons.length === 0) ? data.lessons : nestedLessons;
1025
+ const nextLesson = lessons.find(lesson => lesson.id === nextId);
1026
+ data.first_incomplete_child = nextLesson?.parent ?? nextLesson;
1027
+ data.second_incomplete_child = (nextLesson?.parent) ? nextLesson : null;
1028
+ data.completed_children = completedCount;
1029
+ }
1030
+ }
1031
+
1032
+ if(contentType == 'show' && status == 'completed'){
1033
+ const shows = await fetchShows(data.brand, data.type)
1034
+ const showIds = shows.map(item => item.id);
1035
+ const progressOnItems = await getProgressStateByIds(showIds);
1036
+ const nextByProgress = findIncompleteLesson(progressOnItems, data.id, data.type);
1037
+ data = shows.find(lesson => lesson.id === nextByProgress);
1038
+ }
1039
+
1040
+ return {
1041
+ id: item.id,
1042
+ progressType: 'content',
1043
+ header: contentType,
1044
+ body: {
1045
+ progressPercent: item.percent,
1046
+ thumbnail: data.thumbnail,
1047
+ title: data.title,
1048
+ subtitle: !data.child_count || data.lesson_count === 1
1049
+ ? `${data.difficulty_string} • ${data.artist_name}`
1050
+ : `${data.completed_children} of ${data.lesson_count ?? data.child_count} Lessons Complete`
1051
+ },
1052
+ cta: {
1053
+ text: ctaText,
1054
+ action: {
1055
+ type: data.type,
1056
+ brand: data.brand,
1057
+ id: data.id,
1058
+ slug: data.slug,
1059
+ child: data.first_incomplete_child
1060
+ ? {
1061
+ id: data.first_incomplete_child.id,
1062
+ type: data.first_incomplete_child.type,
1063
+ brand: data.first_incomplete_child.brand,
1064
+ slug: data.first_incomplete_child.slug,
1065
+ child: data.second_incomplete_child
1066
+ ? {
1067
+ id: data.second_incomplete_child.id,
1068
+ type: data.second_incomplete_child.type,
1069
+ brand: data.second_incomplete_child.brand,
1070
+ slug: data.second_incomplete_child.slug
1071
+ }
1072
+ : null
1073
+ }
1074
+ : null
1075
+ }
1076
+ },
1077
+ progressTimestamp: item.progressTimestamp
1078
+ };
1079
+ }
1080
+
1081
+ async function processPlaylistItem(item) {
1082
+ const playlist = item.raw;
1083
+ const progressOnItems = await getProgressStateByIds(playlist.items.map(a => a.content_id));
1084
+ const allItemsCompleted = item.raw.items.every(i => {
1085
+ const itemId = i.content_id;
1086
+ const progress = progressOnItems[itemId];
1087
+ return progress && progress === 'completed';
1088
+ });
1089
+ let nextItem = playlist.items[0] ?? null;
1090
+ if (!allItemsCompleted) {
1091
+ const lastItemProgress = progressOnItems[playlist.last_engaged_on];
1092
+ const index = playlist.items.findIndex(i => i.content_id === playlist.last_engaged_on);
1093
+ if (lastItemProgress === 'completed') {
1094
+ nextItem = playlist.items[index + 1] ?? nextItem;
1095
+ } else {
1096
+ nextItem = playlist.items[index] ?? nextItem;
1097
+ }
1098
+ }
1099
+
1100
+ return {
1101
+ id: playlist.id,
1102
+ progressType: 'playlist',
1103
+ header: 'playlist',
1104
+ body: {
1105
+ first_items_thumbnail_url: playlist.first_items_thumbnail_url,
1106
+ title: playlist.name,
1107
+ subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`
1108
+ },
1109
+ progressTimestamp: item.progressTimestamp,
1110
+ cta: {
1111
+ text: 'Continue',
1112
+ action: {
1113
+ brand: playlist.brand,
1114
+ id: playlist.id,
1115
+ itemId: nextItem.id,
1116
+ type: 'playlists',
1117
+ }
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ const getFormattedType = type => {
1123
+ for (const [key, values] of Object.entries(progressTypesMapping)) {
1124
+ if (values.includes(type)) {
1125
+ return key;
1126
+ }
1127
+ }
1128
+ return null;
1129
+ };
1130
+
1131
+ function extractLessonIds(data) {
1132
+ const ids = [];
1133
+ function traverse(lessons) {
1134
+ for (const item of lessons) {
1135
+ if (item.id) {
1136
+ ids.push(item.id);
1137
+ }
1138
+ if (item.lessons) {
1139
+ traverse(item.lessons); // Recursively handle nested lessons
1140
+ }
1141
+ }
1142
+ }
1143
+ if (data.raw && Array.isArray(data.raw.lessons)) {
1144
+ traverse(data.raw.lessons);
1145
+ }
1146
+
1147
+ return ids;
1148
+ }
1149
+
1150
+
1151
+ async function getEligiblePlaylistItems(playlists) {
1152
+ const eligible = playlists.filter(p => p.last_progress && p.last_engaged_on);
1153
+ return Promise.all(
1154
+ eligible.map(async p => {
1155
+ const utcDate = new Date(p.last_progress.replace(' ', 'T') + 'Z');
1156
+ const timestamp = utcDate.getTime();
1157
+ return {
1158
+ type: 'playlist',
1159
+ progressTimestamp: timestamp,
1160
+ last_engaged_on: p.last_engaged_on,
1161
+ raw: p
1162
+ };
1163
+ })
1164
+ );
1165
+ }
1166
+
1167
+ function mergeAndSortItems(items, limit) {
1168
+ return items
1169
+ .filter(item => typeof item.progressTimestamp === 'number' && item.progressTimestamp > 0)
1170
+ .sort((a, b) => b.progressTimestamp - a.progressTimestamp)
1171
+ .slice(0, limit + 5);
1172
+ }
1173
+
1174
+ function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
1175
+ const ids = Object.keys(progressOnItems).map(Number);
1176
+ if (contentType === 'challenge') {
1177
+ // Return first incomplete lesson
1178
+ return ids.find(id => progressOnItems[id] !== 'completed') || null;
1179
+ }
1180
+
1181
+ // For other types, find next incomplete after current
1182
+ const currentIndex = ids.indexOf(Number(currentContentId));
1183
+ if (currentIndex === -1) return null;
1184
+
1185
+ for (let i = currentIndex + 1; i < ids.length; i++) {
1186
+ const id = ids[i];
1187
+ if (progressOnItems[id] !== 'completed') {
1188
+ return id;
1189
+ }
1190
+ }
1191
+
1192
+ return ids[0];
1193
+ }
1194
+
1195
+ /**
1196
+ * Pins a specific progress row for a user, scoped by brand.
1197
+ *
1198
+ * @param {string} brand - The brand context for the pin action.
1199
+ * @param {number|string} id - The ID of the progress item to pin.
1200
+ * @param {string} progressType - The type of progress (e.g., 'content', 'playlist').
1201
+ * @returns {Promise<Object>} - A promise resolving to the response from the pin API.
1202
+ *
1203
+ * @example
1204
+ * pinProgressRow('drumeo', 12345, 'content')
1205
+ * .then(response => console.log(response))
1206
+ * .catch(error => console.error(error));
1207
+ */
1208
+ export async function pinProgressRow(brand, id, progressType) {
1209
+ const url = `/api/user-management-system/v1/progress/pin?brand=${brand}&id=${id}&progressType=${progressType}`;
1210
+ return await fetchHandler(url, 'PUT', null)
1211
+ }
1212
+ /**
1213
+ * Unpins the current pinned progress row for a user, scoped by brand.
1214
+ *
1215
+ * @param {string} brand - The brand context for the unpin action.
1216
+ * @returns {Promise<Object>} - A promise resolving to the response from the unpin API.
1217
+ *
1218
+ * @example
1219
+ * unpinProgressRow('drumeo')
1220
+ * .then(response => console.log(response))
1221
+ * .catch(error => console.error(error));
1222
+ */
1223
+ export async function unpinProgressRow(brand) {
1224
+ const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}`;
1225
+ return await fetchHandler(url, 'PUT', null)
1226
+ }
1227
+
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/test/log.js CHANGED
File without changes