musora-content-services 2.28.6 → 2.30.3

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 (158) 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/conventional-commits.yaml +0 -0
  5. package/.github/workflows/docs.js.yml +0 -0
  6. package/.github/workflows/node.js.yml +0 -0
  7. package/.prettierignore +0 -0
  8. package/.prettierrc +0 -0
  9. package/CHANGELOG.md +49 -0
  10. package/README.md +0 -0
  11. package/babel.config.cjs +0 -0
  12. package/docs/ContentOrganization.html +0 -0
  13. package/docs/Gamification.html +0 -0
  14. package/docs/UserManagementSystem.html +0 -0
  15. package/docs/api_types.js.html +0 -0
  16. package/docs/config.js.html +0 -0
  17. package/docs/content-org_content-org.js.html +0 -0
  18. package/docs/content-org_playlists-types.js.html +0 -0
  19. package/docs/content-org_playlists.js.html +0 -0
  20. package/docs/content.js.html +0 -0
  21. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  22. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  23. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  24. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  25. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  26. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  27. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  28. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  29. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  30. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
  31. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  32. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  33. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  34. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  35. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
  36. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  37. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  38. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  39. package/docs/gamification_awards.js.html +0 -0
  40. package/docs/gamification_gamification.js.html +0 -0
  41. package/docs/gamification_types.js.html +0 -0
  42. package/docs/global.html +0 -0
  43. package/docs/index.html +0 -0
  44. package/docs/module-Awards.html +0 -0
  45. package/docs/module-Config.html +0 -0
  46. package/docs/module-Content-Services-V2.html +0 -0
  47. package/docs/module-Interests.html +0 -0
  48. package/docs/module-Permissions.html +0 -0
  49. package/docs/module-Playlists.html +0 -0
  50. package/docs/module-Railcontent-Services.html +0 -0
  51. package/docs/module-Sanity-Services.html +0 -0
  52. package/docs/module-Sessions.html +0 -0
  53. package/docs/module-UserActivity.html +0 -0
  54. package/docs/module-UserChat.html +0 -0
  55. package/docs/module-UserManagement.html +0 -0
  56. package/docs/module-UserNotifications.html +0 -0
  57. package/docs/module-UserProfile.html +0 -0
  58. package/docs/railcontent.js.html +0 -0
  59. package/docs/sanity.js.html +0 -0
  60. package/docs/scripts/collapse.js +0 -0
  61. package/docs/scripts/commonNav.js +0 -0
  62. package/docs/scripts/linenumber.js +0 -0
  63. package/docs/scripts/nav.js +0 -0
  64. package/docs/scripts/polyfill.js +0 -0
  65. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
  66. package/docs/scripts/prettify/lang-css.js +0 -0
  67. package/docs/scripts/prettify/prettify.js +0 -0
  68. package/docs/scripts/search.js +0 -0
  69. package/docs/styles/jsdoc.css +0 -0
  70. package/docs/styles/prettify.css +0 -0
  71. package/docs/userActivity.js.html +0 -0
  72. package/docs/user_chat.js.html +0 -0
  73. package/docs/user_interests.js.html +0 -0
  74. package/docs/user_management.js.html +0 -0
  75. package/docs/user_notifications.js.html +0 -0
  76. package/docs/user_permissions.js.html +0 -0
  77. package/docs/user_profile.js.html +0 -0
  78. package/docs/user_sessions.js.html +0 -0
  79. package/docs/user_types.js.html +0 -0
  80. package/docs/user_user-management-system.js.html +0 -0
  81. package/jest.config.js +0 -0
  82. package/jsdoc.json +0 -0
  83. package/link_mcs.sh +0 -0
  84. package/package.json +1 -1
  85. package/src/contentMetaData.js +0 -0
  86. package/src/contentTypeConfig.js +69 -50
  87. package/src/filterBuilder.js +0 -0
  88. package/src/index.d.ts +0 -0
  89. package/src/index.js +0 -0
  90. package/src/infrastructure/http/HttpClient.ts +0 -0
  91. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
  92. package/src/infrastructure/http/index.ts +0 -0
  93. package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
  94. package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
  95. package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
  96. package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
  97. package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
  98. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
  99. package/src/lib/httpHelper.js +0 -0
  100. package/src/lib/lastUpdated.js +0 -0
  101. package/src/services/api/types.js +0 -0
  102. package/src/services/config.js +0 -6
  103. package/src/services/content-org/content-org.js +0 -0
  104. package/src/services/content-org/guided-courses.ts +0 -0
  105. package/src/services/content-org/playlists-types.js +10 -0
  106. package/src/services/content-org/playlists.js +14 -14
  107. package/src/services/content.js +8 -9
  108. package/src/services/contentAggregator.js +45 -7
  109. package/src/services/contentLikes.js +0 -0
  110. package/src/services/contentProgress.js +122 -26
  111. package/src/services/dataContext.js +0 -0
  112. package/src/services/dateUtils.js +0 -0
  113. package/src/services/forum.js +0 -0
  114. package/src/services/gamification/awards.js +52 -33
  115. package/src/services/gamification/gamification.js +0 -0
  116. package/src/services/gamification/types.js +5 -23
  117. package/src/services/imageSRCBuilder.js +0 -0
  118. package/src/services/imageSRCVerify.js +0 -0
  119. package/src/services/railcontent.js +25 -36
  120. package/src/services/recommendations.js +20 -18
  121. package/src/services/sanity.js +27 -57
  122. package/src/services/types.js +0 -8
  123. package/src/services/user/account.ts +0 -0
  124. package/src/services/user/chat.js +0 -0
  125. package/src/services/user/interests.js +0 -0
  126. package/src/services/user/management.js +0 -0
  127. package/src/services/user/notifications.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 +340 -419
  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 -4
  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 +0 -0
  150. package/test/mockData/mockData_sanity_progress_content.json +0 -0
  151. package/test/mockData/mockData_user_practices.json +0 -0
  152. package/test/notifications.test.js +0 -0
  153. package/test/progressRows.test.js +0 -0
  154. package/test/sanityQueryService.test.js +0 -0
  155. package/test/streakMessage.test.js +0 -0
  156. package/test/user/permissions.test.js +0 -0
  157. package/test/userActivity.test.js +0 -0
  158. package/tools/generate-index.cjs +0 -0
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { globalConfig } from '../config.js'
5
5
  import { fetchHandler } from '../railcontent.js'
6
+ import { getNavigateToForPlaylists } from '../contentAggregator.js'
6
7
  import './playlists-types.js'
7
8
 
8
9
  /**
@@ -42,7 +43,7 @@ export async function fetchUserPlaylists(
42
43
  const content = content_id ? `&content_id=${content_id}` : ''
43
44
  const brandString = brand ? `&brand=${brand}` : ''
44
45
  const url = `${BASE_PATH}/v1/user/playlists${pageString}${brandString}${limitString}${sortString}${content}`
45
- return await fetchHandler(url)
46
+ return await getNavigateToForPlaylists(await fetchHandler(url), {dataField: 'data'})
46
47
  }
47
48
 
48
49
  /**
@@ -224,12 +225,13 @@ export async function togglePlaylistPrivate(playlistId, is_private)
224
225
  * Updates a playlists values
225
226
  *
226
227
  * @param {string|number} playlistId
227
- * @param {Object} updateData - An object containing fields to update on the playlist:
228
+ * @param {UpdatePlaylistDTO} updateData - An object containing fields to update on the playlist:
228
229
  * - `name` (string): The name of the new playlist (required, max 255 characters).
229
230
  * - `description` (string): A description of the playlist (optional, max 1000 characters).
230
- * - `category` (string): The category of the playlist.
231
- * + * - `deleted_items` (array): List of playlist item IDs to delete.
232
- * + * - `item_order` (array): Updated order of playlist items (ids, not railcontent_ids).
231
+ * - `category` (string): The category of the playlist (optional).
232
+ * - `is_private` (boolean): Whether the playlist is private (optional, defaults to false).
233
+ * - `deleted_items` (array): List of playlist item IDs to delete (optional).
234
+ * - `item_order` (array): Updated order of playlist items (ids, not railcontent_ids) (optional).
233
235
  *
234
236
  * @returns {Promise<object>} - A promise that resolves to the created playlist data and lessons if successful, or an error response if validation fails.
235
237
  *
@@ -242,16 +244,14 @@ export async function togglePlaylistPrivate(playlistId, is_private)
242
244
  * .then(response => console.log(response.playlist); console.log(response.lessons))
243
245
  * .catch(error => console.error('Error updating playlist:', error));
244
246
  */
245
- export async function updatePlaylist(playlistId, {
246
- name = null, description = null, is_private = null, brand = null, category = null, deleted_items = null, item_order = null
247
- })
247
+ export async function updatePlaylist(playlistId, updateData)
248
248
  {
249
- const data = {
249
+ const { name, description, category, is_private, item_order, deleted_items } = updateData;
250
+ let data = {
250
251
  ...name && { name },
251
- ...description && { description },
252
- ...is_private !== null && { private: is_private},
253
- ...brand && { brand },
254
- ...category && { category},
252
+ ...'description' in updateData && { description },
253
+ ...'is_private' in updateData && { private: is_private || false },
254
+ ...'category' in updateData && { category },
255
255
  ...deleted_items && { deleted_items },
256
256
  ...item_order && { item_order },
257
257
  }
@@ -344,7 +344,7 @@ export async function duplicatePlaylist(playlistId, playlistData) {
344
344
  */
345
345
  export async function fetchPlaylist(playlistId) {
346
346
  const url = `${BASE_PATH}/v1/user/playlists/${playlistId}`
347
- return await fetchHandler(url, 'GET')
347
+ return await getNavigateToForPlaylists(await fetchHandler(url))
348
348
  }
349
349
 
350
350
  /**
@@ -80,6 +80,7 @@ export async function getTabResults(brand, pageName, tabName, {
80
80
  results = await addContextToContent(getLessonContentRows, brand, pageName, {
81
81
  dataField: 'items',
82
82
  addNextLesson: true,
83
+ addNavigateTo: true,
83
84
  addProgressPercentage: true,
84
85
  addProgressStatus: true
85
86
  })
@@ -87,6 +88,7 @@ export async function getTabResults(brand, pageName, tabName, {
87
88
  let temp = await fetchTabData(brand, pageName, { page, limit, sort, includedFields: mergedIncludedFields, progress: progressValue });
88
89
  results = await addContextToContent(() => temp.entity, {
89
90
  addNextLesson: true,
91
+ addNavigateTo: true,
90
92
  addProgressPercentage: true,
91
93
  addProgressStatus: true
92
94
  })
@@ -382,7 +384,7 @@ export async function getRecommendedForYou(brand, rowId = null, {
382
384
  limit = 10,
383
385
  } = {}) {
384
386
  const requiredItems = page * limit;
385
- const data = await recommendations(brand, {limit: requiredItems});
387
+ const data = await recommendations( brand, {limit: requiredItems})
386
388
  if (!data || !data.length) {
387
389
  return { id: 'recommended', title: 'Recommended For You', items: [] };
388
390
  }
@@ -390,14 +392,11 @@ export async function getRecommendedForYou(brand, rowId = null, {
390
392
  // Apply pagination before calling fetchByRailContentIds
391
393
  const startIndex = (page - 1) * limit;
392
394
  const paginatedData = data.slice(startIndex, startIndex + limit);
393
-
394
- const contents = await fetchByRailContentIds(paginatedData);
395
- const result = {
396
- id: 'recommended',
397
- title: 'Recommended For You',
398
- items: contents
399
- };
400
-
395
+ const contents = await addContextToContent(fetchByRailContentIds, paginatedData, 'tab-data',
396
+ {
397
+ addNextLesson: true,
398
+ addNavigateTo: true,
399
+ })
401
400
  if (rowId) {
402
401
  return {
403
402
  type: TabResponseType.CATALOG,
@@ -1,6 +1,6 @@
1
1
  import {
2
- getLastInteractedOf,
3
- getNextLesson,
2
+ getLastInteractedOf, getNavigateTo,
3
+ getNextLesson, getProgressDateByIds,
4
4
  getProgressPercentageByIds,
5
5
  getProgressStateByIds,
6
6
  getResumeTimeSecondsByIds
@@ -28,6 +28,7 @@ import {fetchLastInteractedChild, fetchLikeCount} from "./railcontent"
28
28
  * @param options.addIsLiked - add isLikedField
29
29
  * @param options.addLikeCount - add likeCount field
30
30
  * @param options.addProgressStatus - add progressStatus field
31
+ * @param options.addProgressTimestamp - add progressTimestamp field
31
32
  * @param options.addResumeTimeSeconds - add resumeTimeSeconds field
32
33
  * @param options.addLastInteractedChild - add lastInteractedChild field. This may be different from nextLesson
33
34
  * @param options.addNextLesson - add nextLesson field. For collection type content. each collection has different logic for calculating this data
@@ -66,9 +67,11 @@ export async function addContextToContent(dataPromise, ...dataArgs)
66
67
  addIsLiked = false,
67
68
  addLikeCount = false,
68
69
  addProgressStatus = false,
70
+ addProgressTimestamp = false,
69
71
  addResumeTimeSeconds = false,
70
72
  addLastInteractedChild = false,
71
73
  addNextLesson = false,
74
+ addNavigateTo = false,
72
75
  } = options
73
76
 
74
77
  const dataParam = lastArg === options ? dataArgs.slice(0, -1) : dataArgs
@@ -81,29 +84,63 @@ export async function addContextToContent(dataPromise, ...dataArgs)
81
84
 
82
85
  if(ids.length === 0) return false
83
86
 
84
- const [progressPercentageData, progressStatusData, isLikedData, resumeTimeData, lastInteractedChildData, nextLessonData] = await Promise.all([
85
- addProgressPercentage ? getProgressPercentageByIds(ids) : Promise.resolve(null),
86
- addProgressStatus ? getProgressStateByIds(ids) : Promise.resolve(null),
87
+ const [progressData, isLikedData, resumeTimeData, lastInteractedChildData, nextLessonData, navigateToData] = await Promise.all([
88
+ addProgressPercentage || addProgressStatus || addProgressTimestamp ? getProgressDateByIds(ids) : Promise.resolve(null),
87
89
  addIsLiked ? isContentLikedByIds(ids) : Promise.resolve(null),
88
90
  addResumeTimeSeconds ? getResumeTimeSecondsByIds(ids) : Promise.resolve(null),
89
91
  addLastInteractedChild ? fetchLastInteractedChild(ids) : Promise.resolve(null),
90
92
  addNextLesson ? getNextLesson(items) : Promise.resolve(null),
93
+ addNavigateTo ? getNavigateTo(items) : Promise.resolve(null),
91
94
  ])
95
+ if (addNextLesson) console.log('AddNextLesson is depreciated in favour of addNavigateTo')
92
96
 
93
97
  const addContext = async (item) => ({
94
98
  ...item,
95
- ...(addProgressPercentage ? { progressPercentage: progressPercentageData?.[item.id] } : {}),
96
- ...(addProgressStatus ? { progressStatus: progressStatusData?.[item.id] } : {}),
99
+ ...(addProgressPercentage ? { progressPercentage: progressData?.[item.id]['progress'] } : {}),
100
+ ...(addProgressStatus ? { progressStatus: progressData?.[item.id]['status'] } : {}),
101
+ ...(addProgressTimestamp ? { progressTimestamp: progressData?.[item.id]['last_update'] } : {}),
97
102
  ...(addIsLiked ? { isLiked: isLikedData?.[item.id] } : {}),
98
103
  ...(addLikeCount && ids.length === 1 ? { likeCount: await fetchLikeCount(item.id) } : {}),
99
104
  ...(addResumeTimeSeconds ? { resumeTime: resumeTimeData?.[item.id] } : {}),
100
105
  ...(addLastInteractedChild ? { lastInteractedChild: lastInteractedChildData?.[item.id] } : {}),
101
106
  ...(addNextLesson ? { nextLesson: nextLessonData?.[item.id] } : {}),
107
+ ...(addNavigateTo ? { navigateTo: navigateToData?.[item.id] } : {}),
102
108
  })
103
109
 
104
110
  return await processItems(data, addContext, dataField, isDataAnArray, dataField_includeParent)
105
111
  }
106
112
 
113
+ export async function getNavigateToForPlaylists(data, {dataField = null} = {} )
114
+ {
115
+ let playlists = extractItemsFromData(data, dataField, false, false)
116
+ let allIds = []
117
+ playlists.forEach((playlist) => allIds = [...allIds, ...playlist.items.map(a => a.content_id)])
118
+ const progressOnItems = await getProgressStateByIds(allIds);
119
+ const addContext = async (playlist) => {
120
+ const allItemsCompleted = playlist.items.every(i => {
121
+ const itemId = i.content_id;
122
+ const progress = progressOnItems[itemId];
123
+ return progress && progress === 'completed';
124
+ });
125
+ let nextItem = playlist.items[0] ?? null;
126
+ if (!allItemsCompleted) {
127
+ const lastItemProgress = progressOnItems[playlist.last_engaged_on];
128
+ const index = playlist.items.findIndex(i => i.content_id === playlist.last_engaged_on);
129
+ if (lastItemProgress === 'completed') {
130
+ nextItem = playlist.items[index + 1] ?? nextItem;
131
+ } else {
132
+ nextItem = playlist.items[index] ?? nextItem;
133
+ }
134
+ }
135
+ playlist.navigateTo = {
136
+ ...nextItem,
137
+ playlist_id: playlist.id,
138
+ }
139
+ return playlist
140
+ }
141
+ return await processItems(data, addContext, dataField, false, false,)
142
+ }
143
+
107
144
  function extractItemsFromData(data, dataField, isParentArray, includeParent)
108
145
  {
109
146
  let items = []
@@ -159,3 +196,4 @@ async function processItems(data, addContext, dataField, isParentArray, includeP
159
196
  }
160
197
  }
161
198
 
199
+
File without changes
@@ -5,9 +5,9 @@ import {
5
5
  postRecordWatchSession,
6
6
  } from './railcontent.js'
7
7
  import { DataContext, ContentProgressVersionKey } from './dataContext.js'
8
- import {fetchHierarchy} from './sanity.js'
9
- import {recordUserPractice, findIncompleteLesson} from "./userActivity";
10
- import {getNextLessonLessonParentTypes} from "../contentTypeConfig.js";
8
+ import { fetchHierarchy } from './sanity.js'
9
+ import { recordUserPractice, findIncompleteLesson } from './userActivity'
10
+ import { getNextLessonLessonParentTypes } from '../contentTypeConfig.js'
11
11
 
12
12
  const STATE_STARTED = 'started'
13
13
  const STATE_COMPLETED = 'completed'
@@ -45,22 +45,19 @@ export async function getResumeTimeSecondsByIds(contentIds) {
45
45
  return getByIds(contentIds, DATA_KEY_RESUME_TIME, 0)
46
46
  }
47
47
 
48
- export async function getNextLesson(data)
49
- {
48
+ export async function getNextLesson(data) {
50
49
  let nextLessonData = {}
51
50
 
52
51
  for (const content of data) {
53
- const children = content.children?.map(child => child.id) ?? []
52
+ const children = content.children?.map((child) => child.id) ?? []
54
53
  //only calculate nextLesson if needed, based on content type
55
54
  if (!getNextLessonLessonParentTypes.includes(content.type)) {
56
55
  nextLessonData[content.id] = null
57
-
58
56
  } else {
59
57
  //return first child if parent-content is complete or no progress
60
58
  const contentState = await getProgressState(content.id)
61
59
  if (contentState !== STATE_STARTED) {
62
60
  nextLessonData[content.id] = children[0]
63
-
64
61
  } else {
65
62
  const childrenStates = await getProgressStateByIds(children)
66
63
 
@@ -73,16 +70,23 @@ export async function getNextLesson(data)
73
70
  if (lastInteractedStatus === STATE_STARTED) {
74
71
  nextLessonData[content.id] = lastInteracted
75
72
  } else {
76
- nextLessonData[content.id] = findIncompleteLesson(childrenStates, lastInteracted, content.type)
73
+ nextLessonData[content.id] = findIncompleteLesson(
74
+ childrenStates,
75
+ lastInteracted,
76
+ content.type
77
+ )
77
78
  }
78
-
79
79
  } else if (content.type === 'guided-course' || content.type === 'song-tutorial') {
80
- nextLessonData[content.id] = findIncompleteLesson(childrenStates, lastInteracted, content.type)
80
+ nextLessonData[content.id] = findIncompleteLesson(
81
+ childrenStates,
82
+ lastInteracted,
83
+ content.type
84
+ )
81
85
  } else if (content.type === 'pack') {
82
86
  const packBundles = content.children ?? []
83
87
  const packBundleProgressData = await getNextLesson(packBundles)
84
- const parentId = await getLastInteractedOf(packBundles.map(bundle => bundle.id));
85
- nextLessonData[content.id] = packBundleProgressData[parentId];
88
+ const parentId = await getLastInteractedOf(packBundles.map((bundle) => bundle.id))
89
+ nextLessonData[content.id] = packBundleProgressData[parentId]
86
90
  }
87
91
  }
88
92
  }
@@ -90,6 +94,82 @@ export async function getNextLesson(data)
90
94
  return nextLessonData
91
95
  }
92
96
 
97
+ export async function getNavigateTo(data) {
98
+ let navigateToData = {}
99
+ const twoDepthContentTypes = ['pack'] //TODO add method when we know what it's called
100
+ //TODO add parent hierarchy upwards as well
101
+ // data structure is the same but instead of child{} we use parent{}
102
+ for (const content of data) {
103
+ //only calculate nextLesson if needed, based on content type
104
+ if (!getNextLessonLessonParentTypes.includes(content.type) || !content.children) {
105
+ navigateToData[content.id] = null
106
+ } else {
107
+ const children = new Map()
108
+ const childrenIds = []
109
+ content.children.forEach((child) => {
110
+ childrenIds.push(child.id)
111
+ children.set(child.id, child)
112
+ })
113
+ // return first child (or grand child) if parent-content is complete or no progress
114
+ const contentState = await getProgressState(content.id)
115
+ if (contentState !== STATE_STARTED) {
116
+ const firstChild = content.children[0]
117
+ let lastInteractedChildNavToData =
118
+ (await getNavigateTo([firstChild])[firstChild.id]) ?? null
119
+ navigateToData[content.id] = buildNavigateTo(
120
+ content.children[0],
121
+ lastInteractedChildNavToData
122
+ )
123
+ } else {
124
+ const childrenStates = await getProgressStateByIds(childrenIds)
125
+ const lastInteracted = await getLastInteractedOf(childrenIds)
126
+ const lastInteractedStatus = childrenStates[lastInteracted]
127
+
128
+ if (content.type === 'course' || content.type === 'pack-bundle') {
129
+ if (lastInteractedStatus === STATE_STARTED) {
130
+ navigateToData[content.id] = buildNavigateTo(children.get(lastInteracted))
131
+ } else {
132
+ let incompleteChild = findIncompleteLesson(childrenStates, lastInteracted, content.type)
133
+ navigateToData[content.id] = buildNavigateTo(children.get(incompleteChild))
134
+ }
135
+ } else if (content.type === 'guided-course' || content.type === 'song-tutorial') {
136
+ let incompleteChild = findIncompleteLesson(childrenStates, lastInteracted, content.type)
137
+ navigateToData[content.id] = buildNavigateTo(children.get(incompleteChild))
138
+ } else if (twoDepthContentTypes.includes(content.type)) {
139
+ const firstChildren = content.children ?? []
140
+ const lastInteractedChildId = await getLastInteractedOf(
141
+ firstChildren.map((child) => child.id)
142
+ )
143
+ if (childrenStates[lastInteractedChildId] === STATE_COMPLETED) {
144
+ // TODO: packs have an extra situation where we need to jump to the next course if all lessons in the last engaged course are completed
145
+ }
146
+ let lastInteractedChildNavToData = await getNavigateTo(firstChildren)
147
+ lastInteractedChildNavToData = lastInteractedChildNavToData[lastInteractedChildId]
148
+ navigateToData[content.id] = buildNavigateTo(
149
+ children.get(lastInteractedChildId),
150
+ lastInteractedChildNavToData
151
+ )
152
+ }
153
+ }
154
+ }
155
+ }
156
+ return navigateToData
157
+ }
158
+
159
+ function buildNavigateTo(content, child = null) {
160
+ if (!content) {
161
+ return null
162
+ }
163
+
164
+ return {
165
+ brand: content.brand ?? '',
166
+ thumbnail: content.thumbnail ?? '',
167
+ id: content.id ?? null,
168
+ type: content.type ?? '',
169
+ child: child,
170
+ }
171
+ }
172
+
93
173
  /**
94
174
  * filter through contents, only keeping the most recent
95
175
  * @param {array} contentIds
@@ -115,10 +195,14 @@ export async function getLastInteractedOf(contentIds) {
115
195
  export async function getProgressDateByIds(contentIds) {
116
196
  let data = await dataContext.getData()
117
197
  let progress = {}
118
- contentIds?.forEach((id) => (progress[id] = {
119
- 'last_update': data[id]?.[DATA_KEY_LAST_UPDATED_TIME] ?? 0,
120
- 'progress': data[id]?.[DATA_KEY_PROGRESS] ?? 0,
121
- 'status': data[id]?.[DATA_KEY_STATUS] ?? ''}))
198
+ contentIds?.forEach(
199
+ (id) =>
200
+ (progress[id] = {
201
+ last_update: data[id]?.[DATA_KEY_LAST_UPDATED_TIME] ?? 0,
202
+ progress: data[id]?.[DATA_KEY_PROGRESS] ?? 0,
203
+ status: data[id]?.[DATA_KEY_STATUS] ?? '',
204
+ })
205
+ )
122
206
  return progress
123
207
  }
124
208
 
@@ -178,11 +262,16 @@ export async function getAllCompleted(limit = null) {
178
262
  return ids
179
263
  }
180
264
 
181
- export async function getAllStartedOrCompleted({ limit = null, onlyIds = true, brand = null, excludedIds = [] } = {}) {
265
+ export async function getAllStartedOrCompleted({
266
+ limit = null,
267
+ onlyIds = true,
268
+ brand = null,
269
+ excludedIds = [],
270
+ } = {}) {
182
271
  const data = await dataContext.getData()
183
272
  const oneMonthAgoInSeconds = Math.floor(Date.now() / 1000) - 60 * 24 * 60 * 60 // 60 days in seconds
184
273
 
185
- const excludedSet = new Set(excludedIds.map(id => parseInt(id))) // ensure IDs are numbers
274
+ const excludedSet = new Set(excludedIds.map((id) => parseInt(id))) // ensure IDs are numbers
186
275
 
187
276
  let filtered = Object.entries(data)
188
277
  .filter(([key, item]) => {
@@ -240,13 +329,14 @@ export async function getAllStartedOrCompleted({ limit = null, onlyIds = true, b
240
329
  * const progressMap = await getStartedOrCompletedProgressOnly({ brand: 'drumeo' });
241
330
  * console.log(progressMap[123]); // => 52
242
331
  */
243
- export async function getStartedOrCompletedProgressOnly({ brand = null} = {}) {
332
+ export async function getStartedOrCompletedProgressOnly({ brand = null } = {}) {
244
333
  const data = await dataContext.getData()
245
334
  const result = {}
246
335
 
247
336
  Object.entries(data).forEach(([key, item]) => {
248
337
  const id = parseInt(key)
249
- const isRelevantStatus = item[DATA_KEY_STATUS] === STATE_STARTED || item[DATA_KEY_STATUS] === STATE_COMPLETED
338
+ const isRelevantStatus =
339
+ item[DATA_KEY_STATUS] === STATE_STARTED || item[DATA_KEY_STATUS] === STATE_COMPLETED
250
340
  const isCorrectBrand = !brand || item.b === brand
251
341
 
252
342
  if (isRelevantStatus && isCorrectBrand) {
@@ -360,7 +450,7 @@ export async function recordWatchSession(
360
450
  secondsPlayed,
361
451
  sessionId = null,
362
452
  instrumentId = null,
363
- categoryId = null,
453
+ categoryId = null
364
454
  ) {
365
455
  let mediaTypeId = getMediaTypeId(mediaType, mediaCategory)
366
456
  let updateLocalProgress = mediaTypeId === 1 || mediaTypeId === 2 //only update for video playback
@@ -371,10 +461,16 @@ export async function recordWatchSession(
371
461
  try {
372
462
  //TODO: Good enough for Alpha, Refine in reliability improvements
373
463
  sessionData[sessionId] = sessionData[sessionId] || {}
374
- const secondsSinceLastUpdate = Math.ceil(secondsPlayed - (sessionData[sessionId][contentId] ?? 0))
375
- await recordUserPractice({ content_id: contentId, duration_seconds: secondsSinceLastUpdate, instrument_id: instrumentId })
464
+ const secondsSinceLastUpdate = Math.ceil(
465
+ secondsPlayed - (sessionData[sessionId][contentId] ?? 0)
466
+ )
467
+ await recordUserPractice({
468
+ content_id: contentId,
469
+ duration_seconds: secondsSinceLastUpdate,
470
+ instrument_id: instrumentId,
471
+ })
376
472
  } catch (error) {
377
- console.error('Failed to record user practice:', error)
473
+ console.error('Failed to record user practice:', error)
378
474
  }
379
475
  sessionData[sessionId][contentId] = secondsPlayed
380
476
 
@@ -439,7 +535,7 @@ function bubbleProgress(hierarchy, contentId, localContext) {
439
535
  return localContext.data[childId]?.[DATA_KEY_PROGRESS] ?? 0
440
536
  })
441
537
  let progress = Math.round(childProgress.reduce((a, b) => a + b, 0) / childProgress.length)
442
- const brand =localContext.data[contentId]?.[DATA_KEY_BRAND] ?? null
538
+ const brand = localContext.data[contentId]?.[DATA_KEY_BRAND] ?? null
443
539
  data[DATA_KEY_PROGRESS] = progress
444
540
  data[DATA_KEY_STATUS] = progress === 100 ? STATE_COMPLETED : STATE_STARTED
445
541
  data[DATA_KEY_LAST_UPDATED_TIME] = Math.round(new Date().getTime() / 1000)
File without changes
File without changes
File without changes