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.
- package/.coderabbit.yaml +0 -0
- package/.editorconfig +0 -0
- package/.github/pull_request_template.md +0 -0
- package/.github/workflows/conventional-commits.yaml +0 -0
- package/.github/workflows/docs.js.yml +0 -0
- package/.github/workflows/node.js.yml +0 -0
- package/.prettierignore +0 -0
- package/.prettierrc +0 -0
- package/CHANGELOG.md +49 -0
- package/README.md +0 -0
- package/babel.config.cjs +0 -0
- package/docs/ContentOrganization.html +0 -0
- package/docs/Gamification.html +0 -0
- package/docs/UserManagementSystem.html +0 -0
- package/docs/api_types.js.html +0 -0
- package/docs/config.js.html +0 -0
- package/docs/content-org_content-org.js.html +0 -0
- package/docs/content-org_playlists-types.js.html +0 -0
- package/docs/content-org_playlists.js.html +0 -0
- package/docs/content.js.html +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/gamification_awards.js.html +0 -0
- package/docs/gamification_gamification.js.html +0 -0
- package/docs/gamification_types.js.html +0 -0
- package/docs/global.html +0 -0
- package/docs/index.html +0 -0
- package/docs/module-Awards.html +0 -0
- package/docs/module-Config.html +0 -0
- package/docs/module-Content-Services-V2.html +0 -0
- package/docs/module-Interests.html +0 -0
- package/docs/module-Permissions.html +0 -0
- package/docs/module-Playlists.html +0 -0
- package/docs/module-Railcontent-Services.html +0 -0
- package/docs/module-Sanity-Services.html +0 -0
- package/docs/module-Sessions.html +0 -0
- package/docs/module-UserActivity.html +0 -0
- package/docs/module-UserChat.html +0 -0
- package/docs/module-UserManagement.html +0 -0
- package/docs/module-UserNotifications.html +0 -0
- package/docs/module-UserProfile.html +0 -0
- package/docs/railcontent.js.html +0 -0
- package/docs/sanity.js.html +0 -0
- package/docs/scripts/collapse.js +0 -0
- package/docs/scripts/commonNav.js +0 -0
- package/docs/scripts/linenumber.js +0 -0
- package/docs/scripts/nav.js +0 -0
- package/docs/scripts/polyfill.js +0 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
- package/docs/scripts/prettify/lang-css.js +0 -0
- package/docs/scripts/prettify/prettify.js +0 -0
- package/docs/scripts/search.js +0 -0
- package/docs/styles/jsdoc.css +0 -0
- package/docs/styles/prettify.css +0 -0
- package/docs/userActivity.js.html +0 -0
- package/docs/user_chat.js.html +0 -0
- package/docs/user_interests.js.html +0 -0
- package/docs/user_management.js.html +0 -0
- package/docs/user_notifications.js.html +0 -0
- package/docs/user_permissions.js.html +0 -0
- package/docs/user_profile.js.html +0 -0
- package/docs/user_sessions.js.html +0 -0
- package/docs/user_types.js.html +0 -0
- package/docs/user_user-management-system.js.html +0 -0
- package/jest.config.js +0 -0
- package/jsdoc.json +0 -0
- package/link_mcs.sh +0 -0
- package/package.json +1 -1
- package/src/contentMetaData.js +0 -0
- package/src/contentTypeConfig.js +69 -50
- package/src/filterBuilder.js +0 -0
- package/src/index.d.ts +0 -0
- package/src/index.js +0 -0
- package/src/infrastructure/http/HttpClient.ts +0 -0
- package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
- package/src/infrastructure/http/index.ts +0 -0
- package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
- package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
- package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
- package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
- package/src/lib/httpHelper.js +0 -0
- package/src/lib/lastUpdated.js +0 -0
- package/src/services/api/types.js +0 -0
- package/src/services/config.js +0 -6
- package/src/services/content-org/content-org.js +0 -0
- package/src/services/content-org/guided-courses.ts +0 -0
- package/src/services/content-org/playlists-types.js +10 -0
- package/src/services/content-org/playlists.js +14 -14
- package/src/services/content.js +8 -9
- package/src/services/contentAggregator.js +45 -7
- package/src/services/contentLikes.js +0 -0
- package/src/services/contentProgress.js +122 -26
- package/src/services/dataContext.js +0 -0
- package/src/services/dateUtils.js +0 -0
- package/src/services/forum.js +0 -0
- package/src/services/gamification/awards.js +52 -33
- package/src/services/gamification/gamification.js +0 -0
- package/src/services/gamification/types.js +5 -23
- package/src/services/imageSRCBuilder.js +0 -0
- package/src/services/imageSRCVerify.js +0 -0
- package/src/services/railcontent.js +25 -36
- package/src/services/recommendations.js +20 -18
- package/src/services/sanity.js +27 -57
- package/src/services/types.js +0 -8
- package/src/services/user/account.ts +0 -0
- package/src/services/user/chat.js +0 -0
- package/src/services/user/interests.js +0 -0
- package/src/services/user/management.js +0 -0
- package/src/services/user/notifications.js +0 -0
- package/src/services/user/permissions.js +0 -0
- package/src/services/user/profile.js +0 -0
- package/src/services/user/sessions.js +0 -0
- package/src/services/user/types.js +0 -0
- package/src/services/user/user-management-system.js +0 -0
- package/src/services/userActivity.js +340 -419
- package/test/HttpClient.test.js +0 -0
- package/test/content.test.js +0 -0
- package/test/contentLikes.test.js +0 -0
- package/test/contentProgress.test.js +0 -0
- package/test/dataContext.test.js +0 -0
- package/test/forum.test.js +0 -0
- package/test/imageSRCBuilder.test.js +0 -0
- package/test/imageSRCVerify.test.js +0 -0
- package/test/initializeTests.js +0 -4
- package/test/lib/lastUpdated.test.js +0 -0
- package/test/live/contentProgressLive.test.js +0 -0
- package/test/live/railcontentLive.test.js +0 -0
- package/test/localStorageMock.js +0 -0
- package/test/log.js +0 -0
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
- package/test/mockData/mockData_progress_content.json +0 -0
- package/test/mockData/mockData_sanity_progress_content.json +0 -0
- package/test/mockData/mockData_user_practices.json +0 -0
- package/test/notifications.test.js +0 -0
- package/test/progressRows.test.js +0 -0
- package/test/sanityQueryService.test.js +0 -0
- package/test/streakMessage.test.js +0 -0
- package/test/user/permissions.test.js +0 -0
- package/test/userActivity.test.js +0 -0
- 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 {
|
|
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
|
-
*
|
|
232
|
-
*
|
|
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
|
|
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
|
|
253
|
-
...
|
|
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
|
|
347
|
+
return await getNavigateToForPlaylists(await fetchHandler(url))
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
/**
|
package/src/services/content.js
CHANGED
|
@@ -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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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 [
|
|
85
|
-
addProgressPercentage ?
|
|
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:
|
|
96
|
-
...(addProgressStatus ? { progressStatus:
|
|
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
|
|
10
|
-
import {getNextLessonLessonParentTypes} from
|
|
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(
|
|
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(
|
|
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(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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({
|
|
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 =
|
|
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(
|
|
375
|
-
|
|
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
|
-
|
|
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
|
package/src/services/forum.js
CHANGED
|
File without changes
|