musora-content-services 2.19.2 → 2.20.0
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 +8 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +31 -5
- package/src/index.d.ts +6 -0
- package/src/index.js +6 -0
- package/src/services/content-org/guided-courses.ts +2 -2
- package/src/services/contentAggregator.js +41 -11
- package/src/services/contentProgress.js +69 -3
- package/src/services/imageSRCVerify.js +0 -0
- package/src/services/sanity.js +6 -4
- package/src/services/userActivity.js +140 -54
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
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.20.0](https://github.com/railroadmedia/musora-content-services/compare/v2.19.2...v2.20.0) (2025-07-11)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **BEH-662:** add pinned guided course to homepage ([#332](https://github.com/railroadmedia/musora-content-services/issues/332)) ([6a06612](https://github.com/railroadmedia/musora-content-services/commit/6a066121684cbab1610119906568e4320c4df08c))
|
|
11
|
+
* **BEH-706:** add continue behaviour to lessons collection page and index page ([#326](https://github.com/railroadmedia/musora-content-services/issues/326)) ([6add054](https://github.com/railroadmedia/musora-content-services/commit/6add054841688ea0cd550f11ad21e047de63f235))
|
|
12
|
+
|
|
5
13
|
### [2.19.2](https://github.com/railroadmedia/musora-content-services/compare/v2.19.1...v2.19.2) (2025-07-09)
|
|
6
14
|
|
|
7
15
|
### [2.19.1](https://github.com/railroadmedia/musora-content-services/compare/v2.19.0...v2.19.1) (2025-07-09)
|
package/package.json
CHANGED
package/src/contentTypeConfig.js
CHANGED
|
@@ -180,6 +180,8 @@ export const lessonTypesMapping = {
|
|
|
180
180
|
'jam tracks': ['jam-track'],
|
|
181
181
|
};
|
|
182
182
|
|
|
183
|
+
export const getNextLessonLessonParentTypes = ['course', 'guided-course', 'pack-bundle'];
|
|
184
|
+
|
|
183
185
|
export const progressTypesMapping = {
|
|
184
186
|
'lesson': [...singleLessonTypes,...practiceAlongsLessonTypes, ...liveArchivesLessonTypes, ...performancesLessonTypes, ...studentArchivesLessonTypes, ...documentariesLessonTypes, 'live'],
|
|
185
187
|
'course': ['course'],
|
|
@@ -212,7 +214,27 @@ export const recentTypes = {
|
|
|
212
214
|
|
|
213
215
|
export let contentTypeConfig = {
|
|
214
216
|
'progress-tracker': {
|
|
215
|
-
fields: ['"parent_content_data": parent_content_data[].id',
|
|
217
|
+
fields: ['"parent_content_data": parent_content_data[].id',
|
|
218
|
+
'"badge" : badge.asset->url',
|
|
219
|
+
'"lessons": child[]->{' +
|
|
220
|
+
'"id": railcontent_id,' +
|
|
221
|
+
'"slug":slug.current,' +
|
|
222
|
+
'"brand":brand,' +
|
|
223
|
+
'"type": _type,' +
|
|
224
|
+
'"thumbnail": thumbnail.asset->url,' +
|
|
225
|
+
'published_on,' +
|
|
226
|
+
'"lessons": child[]->{' +
|
|
227
|
+
'"id":railcontent_id,' +
|
|
228
|
+
'"slug":slug.current,' +
|
|
229
|
+
'"type": _type,' +
|
|
230
|
+
'"brand":brand},' +
|
|
231
|
+
'"thumbnail": thumbnail.asset->url,' +
|
|
232
|
+
'published_on,' +
|
|
233
|
+
'}'
|
|
234
|
+
],
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
216
238
|
},
|
|
217
239
|
song: {
|
|
218
240
|
fields: ['album', 'soundslice', 'instrumentless', `"resources": ${resourcesField}`],
|
|
@@ -420,10 +442,14 @@ export let contentTypeConfig = {
|
|
|
420
442
|
'"logo_image_url": logo_image_url.asset->url',
|
|
421
443
|
'total_xp',
|
|
422
444
|
`"children": child[]->{
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
445
|
+
"description": ${descriptionField},
|
|
446
|
+
"lesson_count": child_count,
|
|
447
|
+
"children": child[]->{
|
|
448
|
+
"description": ${descriptionField},
|
|
449
|
+
${getFieldsForContentType()}
|
|
450
|
+
},
|
|
451
|
+
${getFieldsForContentType()}
|
|
452
|
+
}`,
|
|
427
453
|
`"resources": ${resourcesField}`,
|
|
428
454
|
'"thumbnail": thumbnail.asset->url',
|
|
429
455
|
'"light_mode_logo": light_mode_logo_url.asset->url',
|
package/src/index.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ import {
|
|
|
62
62
|
getAllCompleted,
|
|
63
63
|
getAllStarted,
|
|
64
64
|
getAllStartedOrCompleted,
|
|
65
|
+
getLastInteractedOf,
|
|
66
|
+
getNextLesson,
|
|
65
67
|
getProgressDateByIds,
|
|
66
68
|
getProgressPercentage,
|
|
67
69
|
getProgressPercentageByIds,
|
|
@@ -284,6 +286,7 @@ import {
|
|
|
284
286
|
createPracticeNotes,
|
|
285
287
|
deletePracticeSession,
|
|
286
288
|
deleteUserActivity,
|
|
289
|
+
findIncompleteLesson,
|
|
287
290
|
getPracticeNotes,
|
|
288
291
|
getPracticeSessions,
|
|
289
292
|
getProgressRows,
|
|
@@ -422,14 +425,17 @@ declare module 'musora-content-services' {
|
|
|
422
425
|
fetchUserPracticeMeta,
|
|
423
426
|
fetchUserPracticeNotes,
|
|
424
427
|
fetchUserPractices,
|
|
428
|
+
findIncompleteLesson,
|
|
425
429
|
getActiveDiscussions,
|
|
426
430
|
getAllCompleted,
|
|
427
431
|
getAllStarted,
|
|
428
432
|
getAllStartedOrCompleted,
|
|
429
433
|
getContentRows,
|
|
434
|
+
getLastInteractedOf,
|
|
430
435
|
getLessonContentRows,
|
|
431
436
|
getMonday,
|
|
432
437
|
getNewAndUpcoming,
|
|
438
|
+
getNextLesson,
|
|
433
439
|
getPracticeNotes,
|
|
434
440
|
getPracticeSessions,
|
|
435
441
|
getProgressDateByIds,
|
package/src/index.js
CHANGED
|
@@ -62,6 +62,8 @@ import {
|
|
|
62
62
|
getAllCompleted,
|
|
63
63
|
getAllStarted,
|
|
64
64
|
getAllStartedOrCompleted,
|
|
65
|
+
getLastInteractedOf,
|
|
66
|
+
getNextLesson,
|
|
65
67
|
getProgressDateByIds,
|
|
66
68
|
getProgressPercentage,
|
|
67
69
|
getProgressPercentageByIds,
|
|
@@ -284,6 +286,7 @@ import {
|
|
|
284
286
|
createPracticeNotes,
|
|
285
287
|
deletePracticeSession,
|
|
286
288
|
deleteUserActivity,
|
|
289
|
+
findIncompleteLesson,
|
|
287
290
|
getPracticeNotes,
|
|
288
291
|
getPracticeSessions,
|
|
289
292
|
getProgressRows,
|
|
@@ -421,14 +424,17 @@ export {
|
|
|
421
424
|
fetchUserPracticeMeta,
|
|
422
425
|
fetchUserPracticeNotes,
|
|
423
426
|
fetchUserPractices,
|
|
427
|
+
findIncompleteLesson,
|
|
424
428
|
getActiveDiscussions,
|
|
425
429
|
getAllCompleted,
|
|
426
430
|
getAllStarted,
|
|
427
431
|
getAllStartedOrCompleted,
|
|
428
432
|
getContentRows,
|
|
433
|
+
getLastInteractedOf,
|
|
429
434
|
getLessonContentRows,
|
|
430
435
|
getMonday,
|
|
431
436
|
getNewAndUpcoming,
|
|
437
|
+
getNextLesson,
|
|
432
438
|
getPracticeNotes,
|
|
433
439
|
getPracticeSessions,
|
|
434
440
|
getProgressDateByIds,
|
|
@@ -40,7 +40,7 @@ export async function guidedCourses() {
|
|
|
40
40
|
return await fetchHandler(url, 'GET')
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export async function pinnedGuidedCourses() {
|
|
44
|
-
const url: string = `${BASE_PATH}/v1/user/guided-courses/pinned`
|
|
43
|
+
export async function pinnedGuidedCourses(brand) {
|
|
44
|
+
const url: string = `${BASE_PATH}/v1/user/guided-courses/pinned?brand=${brand}`
|
|
45
45
|
return await fetchHandler(url, 'GET')
|
|
46
46
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
getLastInteractedOf,
|
|
3
|
+
getNextLesson,
|
|
4
|
+
getProgressPercentageByIds,
|
|
5
|
+
getProgressStateByIds,
|
|
6
|
+
getResumeTimeSecondsByIds
|
|
7
|
+
} from "./contentProgress"
|
|
8
|
+
import {isContentLikedByIds} from "./contentLikes"
|
|
9
|
+
import {fetchLastInteractedChild, fetchLikeCount} from "./railcontent"
|
|
5
10
|
|
|
6
11
|
|
|
7
12
|
export async function addContextToContent(dataPromise, ...dataArgs)
|
|
@@ -17,6 +22,8 @@ export async function addContextToContent(dataPromise, ...dataArgs)
|
|
|
17
22
|
addProgressStatus = false,
|
|
18
23
|
addResumeTimeSeconds = false,
|
|
19
24
|
addLastInteractedChild = false,
|
|
25
|
+
addNextLesson = false,
|
|
26
|
+
addLastInteractedParent = false,
|
|
20
27
|
} = options
|
|
21
28
|
|
|
22
29
|
const dataParam = lastArg === options ? dataArgs.slice(0, -1) : dataArgs
|
|
@@ -24,26 +31,43 @@ export async function addContextToContent(dataPromise, ...dataArgs)
|
|
|
24
31
|
const data = await dataPromise(...dataParam)
|
|
25
32
|
if(!data) return false
|
|
26
33
|
|
|
27
|
-
let
|
|
34
|
+
let items = []
|
|
35
|
+
let dataMap = []
|
|
28
36
|
|
|
29
37
|
if (dataField && data?.[dataField]) {
|
|
30
|
-
|
|
38
|
+
items = data[dataField];
|
|
31
39
|
} else if (Array.isArray(data)) {
|
|
32
|
-
|
|
40
|
+
items = data;
|
|
33
41
|
} else if (data?.id) {
|
|
34
|
-
|
|
42
|
+
items = [data]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ids = items.map(item => item?.id).filter(Boolean)
|
|
46
|
+
|
|
47
|
+
//create data structure for common use by functions
|
|
48
|
+
if (addNextLesson) {
|
|
49
|
+
items.forEach((item) => {
|
|
50
|
+
if (item?.id) {
|
|
51
|
+
dataMap.push({
|
|
52
|
+
'children': item.children?.map(child => child.id) ?? [],
|
|
53
|
+
'type': item.type,
|
|
54
|
+
'id': item.id,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
})
|
|
35
58
|
}
|
|
36
59
|
|
|
37
60
|
if(ids.length === 0) return false
|
|
38
61
|
|
|
39
|
-
const [progressPercentageData, progressStatusData, isLikedData, resumeTimeData, lastInteractedChildData] = await Promise.all([
|
|
62
|
+
const [progressPercentageData, progressStatusData, isLikedData, resumeTimeData, lastInteractedChildData, nextLessonData] = await Promise.all([
|
|
40
63
|
addProgressPercentage ? getProgressPercentageByIds(ids) : Promise.resolve(null),
|
|
41
64
|
addProgressStatus ? getProgressStateByIds(ids) : Promise.resolve(null),
|
|
42
65
|
addIsLiked ? isContentLikedByIds(ids) : Promise.resolve(null),
|
|
43
66
|
addResumeTimeSeconds ? getResumeTimeSecondsByIds(ids) : Promise.resolve(null),
|
|
44
67
|
addLastInteractedChild ? fetchLastInteractedChild(ids) : Promise.resolve(null),
|
|
68
|
+
(addNextLesson || addLastInteractedParent) ? getNextLesson(dataMap) : Promise.resolve(null),
|
|
45
69
|
])
|
|
46
|
-
|
|
70
|
+
|
|
47
71
|
const addContext = async (item) => ({
|
|
48
72
|
...item,
|
|
49
73
|
...(addProgressPercentage ? { progressPercentage: progressPercentageData?.[item.id] } : {}),
|
|
@@ -52,8 +76,14 @@ export async function addContextToContent(dataPromise, ...dataArgs)
|
|
|
52
76
|
...(addLikeCount && ids.length === 1 ? { likeCount: await fetchLikeCount(item.id) } : {}),
|
|
53
77
|
...(addResumeTimeSeconds ? { resumeTime: resumeTimeData?.[item.id] } : {}),
|
|
54
78
|
...(addLastInteractedChild ? { lastInteractedChild: lastInteractedChildData?.[item.id] } : {}),
|
|
79
|
+
...(addNextLesson ? { nextLesson: nextLessonData?.[item.id] } : {}),
|
|
55
80
|
})
|
|
56
|
-
|
|
81
|
+
|
|
82
|
+
if (addLastInteractedParent) {
|
|
83
|
+
const parentId = await getLastInteractedOf(ids);
|
|
84
|
+
data['nextLesson'] = nextLessonData[parentId];
|
|
85
|
+
}
|
|
86
|
+
|
|
57
87
|
if (dataField) {
|
|
58
88
|
data[dataField] = Array.isArray(data[dataField])
|
|
59
89
|
? await Promise.all(data[dataField].map(addContext))
|
|
@@ -5,8 +5,9 @@ import {
|
|
|
5
5
|
postRecordWatchSession,
|
|
6
6
|
} from './railcontent.js'
|
|
7
7
|
import { DataContext, ContentProgressVersionKey } from './dataContext.js'
|
|
8
|
-
import {
|
|
9
|
-
import {recordUserPractice} from "./userActivity";
|
|
8
|
+
import {fetchHierarchy} from './sanity.js'
|
|
9
|
+
import {recordUserPractice, findIncompleteLesson} from "./userActivity";
|
|
10
|
+
import {getNextLessonLessonParentTypes} from "../contentTypeConfig.js";
|
|
10
11
|
|
|
11
12
|
const STATE_STARTED = 'started'
|
|
12
13
|
const STATE_COMPLETED = 'completed'
|
|
@@ -14,10 +15,11 @@ const DATA_KEY_STATUS = 's'
|
|
|
14
15
|
const DATA_KEY_PROGRESS = 'p'
|
|
15
16
|
const DATA_KEY_RESUME_TIME = 't'
|
|
16
17
|
const DATA_KEY_LAST_UPDATED_TIME = 'u'
|
|
18
|
+
|
|
17
19
|
export let dataContext = new DataContext(ContentProgressVersionKey, fetchContentProgress)
|
|
18
20
|
|
|
19
21
|
export async function getProgressPercentage(contentId) {
|
|
20
|
-
return getById(
|
|
22
|
+
return getById(contentId, DATA_KEY_PROGRESS, 0)
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export async function getProgressPercentageByIds(contentIds) {
|
|
@@ -40,6 +42,70 @@ export async function getResumeTimeSecondsByIds(contentIds) {
|
|
|
40
42
|
return getByIds(contentIds, DATA_KEY_RESUME_TIME, 0)
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
export async function getNextLesson(dataMap)
|
|
46
|
+
{
|
|
47
|
+
let nextLessonData = {}
|
|
48
|
+
|
|
49
|
+
for (const content of dataMap) {
|
|
50
|
+
|
|
51
|
+
//only calculate nextLesson if needed, based on content type
|
|
52
|
+
if (!getNextLessonLessonParentTypes.includes(content.type)) {
|
|
53
|
+
nextLessonData[content.id] = null
|
|
54
|
+
|
|
55
|
+
} else {
|
|
56
|
+
//return first child if parent-content is complete or no progress
|
|
57
|
+
const contentState = await getProgressState(content.id)
|
|
58
|
+
if (contentState !== STATE_STARTED) {
|
|
59
|
+
nextLessonData[content.id] = content.children[0]
|
|
60
|
+
|
|
61
|
+
} else {
|
|
62
|
+
//if content in progress
|
|
63
|
+
|
|
64
|
+
const childrenStates = await getProgressStateByIds(content.children)
|
|
65
|
+
|
|
66
|
+
//calculate last_engaged
|
|
67
|
+
const lastInteracted = await getLastInteractedOf(content.children)
|
|
68
|
+
const lastInteractedStatus = childrenStates[lastInteracted]
|
|
69
|
+
|
|
70
|
+
//different nextLesson behaviour for different content types
|
|
71
|
+
if (content.type === 'course' || content.type === 'pack-bundle') {
|
|
72
|
+
if (lastInteractedStatus === STATE_STARTED) {
|
|
73
|
+
nextLessonData[content.id] = lastInteracted
|
|
74
|
+
} else {
|
|
75
|
+
nextLessonData[content.id] = findIncompleteLesson(childrenStates, lastInteracted, content.type)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
} else if (content.type === 'guided-course') {
|
|
79
|
+
nextLessonData[content.id] = findIncompleteLesson(childrenStates, lastInteracted, content.type)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return nextLessonData
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* filter through contents, only keeping the most recent
|
|
89
|
+
* @param {array} contentIds
|
|
90
|
+
* @returns {Promise<number>}
|
|
91
|
+
*/
|
|
92
|
+
export async function getLastInteractedOf(contentIds) {
|
|
93
|
+
const data = await getByIds(contentIds, DATA_KEY_LAST_UPDATED_TIME, 0)
|
|
94
|
+
const sorted = Object.keys(data)
|
|
95
|
+
.map(function (key) {
|
|
96
|
+
return parseInt(key)
|
|
97
|
+
})
|
|
98
|
+
.sort(function (a, b) {
|
|
99
|
+
let v1 = data[a]
|
|
100
|
+
let v2 = data[b]
|
|
101
|
+
if (v1 > v2) return -1
|
|
102
|
+
else if (v1 < v2) return 1
|
|
103
|
+
return 0
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return sorted[0]
|
|
107
|
+
}
|
|
108
|
+
|
|
43
109
|
export async function getProgressDateByIds(contentIds) {
|
|
44
110
|
let data = await dataContext.getData()
|
|
45
111
|
let progress = {}
|
|
File without changes
|
package/src/services/sanity.js
CHANGED
|
@@ -513,7 +513,7 @@ export async function fetchByRailContentId(id, contentType) {
|
|
|
513
513
|
* .catch(error => console.error(error));
|
|
514
514
|
*/
|
|
515
515
|
export async function fetchByRailContentIds(ids, contentType = undefined, brand = undefined) {
|
|
516
|
-
if (!ids) {
|
|
516
|
+
if (!ids?.length) {
|
|
517
517
|
return []
|
|
518
518
|
}
|
|
519
519
|
const idsString = ids.join(',')
|
|
@@ -2356,9 +2356,11 @@ export async function fetchTabData(
|
|
|
2356
2356
|
|
|
2357
2357
|
filter = `brand == "${brand}" ${includedFieldsFilter} ${progressFilter}`
|
|
2358
2358
|
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
2359
|
-
entityFieldsString =
|
|
2360
|
-
|
|
2361
|
-
|
|
2359
|
+
entityFieldsString =
|
|
2360
|
+
` ${fieldsString},
|
|
2361
|
+
'children': child[${childrenFilter}]->{'id': railcontent_id},
|
|
2362
|
+
'lesson_count': coalesce(count(child[${childrenFilter}]->), 0),
|
|
2363
|
+
'length_in_seconds': coalesce(
|
|
2362
2364
|
math::sum(
|
|
2363
2365
|
select(
|
|
2364
2366
|
child[${childrenFilter}]->length_in_seconds
|
|
@@ -8,16 +8,25 @@ import {
|
|
|
8
8
|
fetchUserPracticeMeta,
|
|
9
9
|
fetchUserPracticeNotes,
|
|
10
10
|
fetchHandler,
|
|
11
|
-
fetchRecentUserActivities,
|
|
11
|
+
fetchRecentUserActivities,
|
|
12
|
+
fetchChallengeLessonData,
|
|
13
|
+
fetchLastInteractedChild,
|
|
12
14
|
} from './railcontent'
|
|
13
15
|
import { DataContext, UserActivityVersionKey } from './dataContext.js'
|
|
14
16
|
import { fetchByRailContentIds, fetchShows } from './sanity'
|
|
15
|
-
import {fetchPlaylist, fetchUserPlaylists} from "./content-org/playlists"
|
|
17
|
+
import {fetchPlaylist, fetchUserPlaylists} from "./content-org/playlists"
|
|
18
|
+
import {pinnedGuidedCourses} from "./content-org/guided-courses"
|
|
16
19
|
import {convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay, getTimeRemainingUntilLocal} from './dateUtils.js'
|
|
17
20
|
import { globalConfig } from './config'
|
|
18
21
|
import {collectionLessonTypes, lessonTypesMapping, progressTypesMapping, showsLessonTypes, songs} from "../contentTypeConfig";
|
|
19
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
getAllStartedOrCompleted,
|
|
24
|
+
getProgressPercentageByIds,
|
|
25
|
+
getProgressStateByIds,
|
|
26
|
+
getResumeTimeSecondsByIds
|
|
27
|
+
} from "./contentProgress";
|
|
20
28
|
import {TabResponseType} from "../contentMetaData";
|
|
29
|
+
import {isContentLikedByIds} from "./contentLikes.js";
|
|
21
30
|
|
|
22
31
|
const DATA_KEY_PRACTICES = 'practices'
|
|
23
32
|
const DATA_KEY_LAST_UPDATED_TIME = 'u'
|
|
@@ -351,7 +360,7 @@ export async function removeUserPractice(id) {
|
|
|
351
360
|
Object.keys(localContext.data[DATA_KEY_PRACTICES]).forEach((date) => {
|
|
352
361
|
localContext.data[DATA_KEY_PRACTICES][date] = localContext.data[DATA_KEY_PRACTICES][
|
|
353
362
|
date
|
|
354
|
-
|
|
363
|
+
].filter((practice) => practice.id !== id)
|
|
355
364
|
})
|
|
356
365
|
}
|
|
357
366
|
},
|
|
@@ -911,26 +920,45 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
|
|
|
911
920
|
'learning-path-course',
|
|
912
921
|
'learning-path-level'
|
|
913
922
|
]);
|
|
923
|
+
// TODO slice progress to a reasonable number, say 100
|
|
924
|
+
const [recentPlaylists, progressContents, allPinnedGuidedCourse, userPinnedItem ] = await Promise.all([
|
|
925
|
+
fetchUserPlaylists(brand, { sort: '-last_progress', limit: limit}),
|
|
926
|
+
getAllStartedOrCompleted({onlyIds: false, brand: brand }),
|
|
927
|
+
pinnedGuidedCourses(brand),
|
|
928
|
+
getUserPinnedItem(brand),
|
|
929
|
+
])
|
|
930
|
+
|
|
931
|
+
let pinnedGuidedCourse = allPinnedGuidedCourse?.[0] ?? null
|
|
914
932
|
|
|
915
|
-
const recentPlaylists = await fetchUserPlaylists(brand, {
|
|
916
|
-
sort: '-last_progress',
|
|
917
|
-
limit: limit,
|
|
918
|
-
});
|
|
919
933
|
const playlists = recentPlaylists?.data || [];
|
|
920
934
|
const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists);
|
|
921
935
|
const playlistEngagedOnContents = eligiblePlaylistItems.map(item => item.last_engaged_on);
|
|
922
|
-
|
|
936
|
+
|
|
937
|
+
const nonPlaylistContentIds = Object.keys(progressContents)
|
|
938
|
+
if (pinnedGuidedCourse) {
|
|
939
|
+
nonPlaylistContentIds.push(pinnedGuidedCourse.content_id);
|
|
940
|
+
}
|
|
941
|
+
if (userPinnedItem?.progressType === 'content') {
|
|
942
|
+
nonPlaylistContentIds.push(userPinnedItem.id)
|
|
943
|
+
}
|
|
944
|
+
const [ playlistsContents, contents ] = await Promise.all([
|
|
945
|
+
fetchByRailContentIds(playlistEngagedOnContents, 'progress-tracker'),
|
|
946
|
+
fetchByRailContentIds(nonPlaylistContentIds, 'progress-tracker', brand),
|
|
947
|
+
]);
|
|
948
|
+
|
|
923
949
|
const excludedParents = new Set();
|
|
924
950
|
const existingShows = new Set();
|
|
951
|
+
// TODO this doesn't work for guided courses as the GC card takes precedence over the playlist card
|
|
952
|
+
// https://musora.atlassian.net/browse/BEH-812
|
|
925
953
|
for (const item of playlistsContents) {
|
|
954
|
+
|
|
926
955
|
const contentId = item.id ?? item.railcontent_id;
|
|
927
|
-
|
|
956
|
+
delete progressContents[contentId]
|
|
928
957
|
const parentIds = item.parent_content_data || [];
|
|
929
|
-
parentIds.forEach(id =>
|
|
958
|
+
parentIds.forEach(id => delete progressContents[id] );
|
|
930
959
|
}
|
|
931
960
|
|
|
932
|
-
|
|
933
|
-
const contents = await fetchByRailContentIds(Object.keys(progressContents), 'progress-tracker', brand);
|
|
961
|
+
|
|
934
962
|
const contentsMap = {};
|
|
935
963
|
contents.forEach(content => {
|
|
936
964
|
contentsMap[content.railcontent_id] = content;
|
|
@@ -985,28 +1013,38 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
|
|
|
985
1013
|
}
|
|
986
1014
|
}
|
|
987
1015
|
}
|
|
988
|
-
const pinnedItem = await extractPinnedItem(
|
|
989
|
-
|
|
1016
|
+
const pinnedItem = userPinnedItem ? await extractPinnedItem(
|
|
1017
|
+
userPinnedItem,
|
|
990
1018
|
progressMap,
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1019
|
+
eligiblePlaylistItems,
|
|
1020
|
+
) : null
|
|
1021
|
+
|
|
1022
|
+
const pinnedId = pinnedItem?.id
|
|
1023
|
+
const guidedCourseID = pinnedGuidedCourse?.content_id
|
|
1024
|
+
let combined = [];
|
|
1025
|
+
if (pinnedGuidedCourse) {
|
|
1026
|
+
const guidedCourseContent = contentsMap[guidedCourseID]
|
|
1027
|
+
if (guidedCourseContent) {
|
|
1028
|
+
const temp = await extractPinnedGuidedCourseItem(guidedCourseContent, progressMap)
|
|
1029
|
+
temp.pinned = true
|
|
1030
|
+
combined.push(temp)
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
994
1033
|
if (pinnedItem) {
|
|
995
1034
|
pinnedItem.pinned = true
|
|
1035
|
+
combined.push(pinnedItem)
|
|
996
1036
|
}
|
|
1037
|
+
const progressList = Array.from(progressMap.values())
|
|
997
1038
|
|
|
998
|
-
const pinnedId = pinnedItem?.id
|
|
999
1039
|
const filteredProgressList = pinnedId
|
|
1000
|
-
? progressList.filter(item => item.id
|
|
1040
|
+
? progressList.filter(item => !(item.id === pinnedId || item.id === guidedCourseID))
|
|
1001
1041
|
: progressList;
|
|
1002
1042
|
const filteredPlaylists = pinnedId
|
|
1003
|
-
? eligiblePlaylistItems.filter(item => item.id
|
|
1043
|
+
? (eligiblePlaylistItems.filter(item => !(item.id === pinnedId || item.id === guidedCourseID)))
|
|
1004
1044
|
: eligiblePlaylistItems;
|
|
1005
|
-
const combinedBase = [...filteredProgressList, ...filteredPlaylists]
|
|
1006
|
-
const combined = pinnedItem ? [pinnedItem, ...combinedBase] : combinedBase
|
|
1007
1045
|
|
|
1046
|
+
combined = [...combined, ...filteredProgressList, ...filteredPlaylists]
|
|
1008
1047
|
const finalCombined = mergeAndSortItems(combined, limit)
|
|
1009
|
-
|
|
1010
1048
|
const results = await Promise.all(
|
|
1011
1049
|
finalCombined.slice(0, limit).map(item =>
|
|
1012
1050
|
item.type === 'playlist'
|
|
@@ -1022,11 +1060,17 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
|
|
|
1022
1060
|
};
|
|
1023
1061
|
}
|
|
1024
1062
|
|
|
1063
|
+
async function getUserPinnedItem(brand) {
|
|
1064
|
+
const userRaw = await globalConfig.localStorage.getItem('user');
|
|
1065
|
+
const user = userRaw ? JSON.parse(userRaw) : {};
|
|
1066
|
+
user.brand_pinned_progress = user.brand_pinned_progress || {}
|
|
1067
|
+
return user.brand_pinned_progress[brand] ?? null
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1025
1070
|
async function processContentItem(item) {
|
|
1026
1071
|
let data = item.raw;
|
|
1027
1072
|
const contentType = getFormattedType(data.type, data.brand);
|
|
1028
1073
|
const status = item.state;
|
|
1029
|
-
|
|
1030
1074
|
let ctaText = 'Continue';
|
|
1031
1075
|
if (contentType === 'transcription' || contentType === 'play-along' || contentType === 'jam-track') ctaText = 'Replay Song';
|
|
1032
1076
|
if (contentType === 'lesson') ctaText = status === 'completed' ? 'Revisit Lesson' : 'Continue';
|
|
@@ -1064,6 +1108,29 @@ async function processContentItem(item) {
|
|
|
1064
1108
|
const nextLesson = lessons.find(lesson => lesson.id === nextId)
|
|
1065
1109
|
data.first_incomplete_child = nextLesson?.parent ?? nextLesson
|
|
1066
1110
|
data.second_incomplete_child = (nextLesson?.parent) ? nextLesson : null
|
|
1111
|
+
if(data.type === 'guided-course'){
|
|
1112
|
+
let isLocked = new Date(nextLesson.published_on) > new Date()
|
|
1113
|
+
data.thumbnail = nextLesson.thumbnail
|
|
1114
|
+
// USHP-4 completed
|
|
1115
|
+
if (status === 'completed') {
|
|
1116
|
+
// duplicated code to above, but here for clarity
|
|
1117
|
+
ctaText = 'Revisit Lessons'
|
|
1118
|
+
// USHP-1 if lesson locked show unlock in X time
|
|
1119
|
+
} else if (isLocked) {
|
|
1120
|
+
data.is_locked = true
|
|
1121
|
+
const timeRemaining = getTimeRemainingUntilLocal(nextLesson.published_on, {withTotalSeconds: true})
|
|
1122
|
+
data.time_remaining_seconds = timeRemaining.totalSeconds
|
|
1123
|
+
ctaText = 'Next lesson in ' + timeRemaining.formatted
|
|
1124
|
+
}
|
|
1125
|
+
// USHP-2 start course if not started
|
|
1126
|
+
else if (status === 'not-started') {
|
|
1127
|
+
ctaText = "Start Course"
|
|
1128
|
+
}
|
|
1129
|
+
// USHP-3 in progress for lesson
|
|
1130
|
+
else {
|
|
1131
|
+
ctaText = "Continue"
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1067
1134
|
}
|
|
1068
1135
|
}
|
|
1069
1136
|
|
|
@@ -1079,7 +1146,7 @@ async function processContentItem(item) {
|
|
|
1079
1146
|
data.completed_children = completedCount;
|
|
1080
1147
|
data.child_count = shows.length;
|
|
1081
1148
|
item.percent = Math.round((completedCount / shows.length) * 100);
|
|
1082
|
-
if(completedCount
|
|
1149
|
+
if(completedCount === shows.length) {
|
|
1083
1150
|
ctaText = 'Revisit Lessons';
|
|
1084
1151
|
}
|
|
1085
1152
|
}
|
|
@@ -1096,8 +1163,8 @@ async function processContentItem(item) {
|
|
|
1096
1163
|
badge: data.badge ?? null,
|
|
1097
1164
|
isLocked: data.is_locked ?? false,
|
|
1098
1165
|
subtitle: !data.child_count || data.lesson_count === 1
|
|
1099
|
-
|
|
1100
|
-
|
|
1166
|
+
? (contentType === 'lesson') ? `${item.percent}% Complete`: `${data.difficulty_string} • ${data.artist_name}`
|
|
1167
|
+
: `${data.completed_children} of ${data.lesson_count ?? data.child_count} Lessons Complete`
|
|
1101
1168
|
},
|
|
1102
1169
|
cta: {
|
|
1103
1170
|
text: ctaText,
|
|
@@ -1108,21 +1175,21 @@ async function processContentItem(item) {
|
|
|
1108
1175
|
id: data.id,
|
|
1109
1176
|
slug: data.slug,
|
|
1110
1177
|
child: data.first_incomplete_child
|
|
1111
|
-
|
|
1178
|
+
? {
|
|
1112
1179
|
id: data.first_incomplete_child.id,
|
|
1113
1180
|
type: data.first_incomplete_child.type,
|
|
1114
1181
|
brand: data.first_incomplete_child.brand,
|
|
1115
1182
|
slug: data.first_incomplete_child.slug,
|
|
1116
1183
|
child: data.second_incomplete_child
|
|
1117
|
-
|
|
1184
|
+
? {
|
|
1118
1185
|
id: data.second_incomplete_child.id,
|
|
1119
1186
|
type: data.second_incomplete_child.type,
|
|
1120
1187
|
brand: data.second_incomplete_child.brand,
|
|
1121
1188
|
slug: data.second_incomplete_child.slug
|
|
1122
1189
|
}
|
|
1123
|
-
|
|
1190
|
+
: null
|
|
1124
1191
|
}
|
|
1125
|
-
|
|
1192
|
+
: null
|
|
1126
1193
|
}
|
|
1127
1194
|
},
|
|
1128
1195
|
progressTimestamp: item.progressTimestamp
|
|
@@ -1234,12 +1301,13 @@ function mergeAndSortItems(items, limit) {
|
|
|
1234
1301
|
.sort((a, b) => {
|
|
1235
1302
|
if (a.pinned && !b.pinned) return -1;
|
|
1236
1303
|
if (!a.pinned && b.pinned) return 1;
|
|
1304
|
+
// TODO guided course should always be before user pinned item
|
|
1237
1305
|
return b.progressTimestamp - a.progressTimestamp;
|
|
1238
1306
|
})
|
|
1239
1307
|
.slice(0, limit + 5);
|
|
1240
1308
|
}
|
|
1241
1309
|
|
|
1242
|
-
function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
|
|
1310
|
+
export function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
|
|
1243
1311
|
const ids = Object.keys(progressOnItems).map(Number);
|
|
1244
1312
|
if (contentType === 'guided-course') {
|
|
1245
1313
|
// Return first incomplete lesson
|
|
@@ -1274,11 +1342,10 @@ function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
|
|
|
1274
1342
|
* .catch(error => console.error(error));
|
|
1275
1343
|
*/
|
|
1276
1344
|
export async function pinProgressRow(brand, id, progressType) {
|
|
1277
|
-
if (!(brand && id && progressType)) throw new Error(`undefined parameter progressType: ${progressType} brand: ${brand} or id: ${id}`)
|
|
1278
1345
|
const url = `/api/user-management-system/v1/progress/pin?brand=${brand}&id=${id}&progressType=${progressType}`;
|
|
1279
1346
|
const response = await fetchHandler(url, 'PUT', null)
|
|
1280
|
-
if (response && !response.error
|
|
1281
|
-
await
|
|
1347
|
+
if (response && !response.error) {
|
|
1348
|
+
await updatePinnedProgressRow(brand, {
|
|
1282
1349
|
id,
|
|
1283
1350
|
progressType,
|
|
1284
1351
|
pinnedAt: new Date().toISOString(),
|
|
@@ -1290,25 +1357,23 @@ export async function pinProgressRow(brand, id, progressType) {
|
|
|
1290
1357
|
* Unpins the current pinned progress row for a user, scoped by brand.
|
|
1291
1358
|
*
|
|
1292
1359
|
* @param {string} brand - The brand context for the unpin action.
|
|
1293
|
-
* @param {string} id - The content or playlist id to unpin.
|
|
1294
1360
|
* @returns {Promise<Object>} - A promise resolving to the response from the unpin API.
|
|
1295
1361
|
*
|
|
1296
1362
|
* @example
|
|
1297
|
-
* unpinProgressRow('drumeo'
|
|
1363
|
+
* unpinProgressRow('drumeo')
|
|
1298
1364
|
* .then(response => console.log(response))
|
|
1299
1365
|
* .catch(error => console.error(error));
|
|
1300
1366
|
*/
|
|
1301
|
-
export async function unpinProgressRow(brand
|
|
1302
|
-
|
|
1303
|
-
const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}&id=${id}`
|
|
1367
|
+
export async function unpinProgressRow(brand) {
|
|
1368
|
+
const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}`
|
|
1304
1369
|
const response = await fetchHandler(url, 'PUT', null)
|
|
1305
|
-
if (response && !response.error
|
|
1306
|
-
await
|
|
1370
|
+
if (response && !response.error) {
|
|
1371
|
+
await updatePinnedProgressRow(brand, null)
|
|
1307
1372
|
}
|
|
1308
1373
|
return response
|
|
1309
1374
|
}
|
|
1310
1375
|
|
|
1311
|
-
async function
|
|
1376
|
+
async function updatePinnedProgressRow(brand, pinnedData) {
|
|
1312
1377
|
const userRaw = await globalConfig.localStorage.getItem('user');
|
|
1313
1378
|
const user = userRaw ? JSON.parse(userRaw) : {};
|
|
1314
1379
|
user.brand_pinned_progress = user.brand_pinned_progress || {}
|
|
@@ -1316,14 +1381,7 @@ async function updateUserPinnedProgressRow(brand, pinnedData) {
|
|
|
1316
1381
|
await globalConfig.localStorage.setItem('user', JSON.stringify(user))
|
|
1317
1382
|
}
|
|
1318
1383
|
|
|
1319
|
-
async function extractPinnedItem(
|
|
1320
|
-
const userRaw = await globalConfig.localStorage.getItem('user');
|
|
1321
|
-
const user = userRaw ? JSON.parse(userRaw) : {};
|
|
1322
|
-
user.brand_pinned_progress = user.brand_pinned_progress || {}
|
|
1323
|
-
|
|
1324
|
-
const pinned = user.brand_pinned_progress[brand]
|
|
1325
|
-
if (!pinned) return null
|
|
1326
|
-
|
|
1384
|
+
async function extractPinnedItem(pinned, progressMap, playlistItems) {
|
|
1327
1385
|
const {id, progressType, pinnedAt} = pinned
|
|
1328
1386
|
|
|
1329
1387
|
if (progressType === 'content') {
|
|
@@ -1356,7 +1414,7 @@ async function extractPinnedItem({brand, progressMap, playlistItems}) {
|
|
|
1356
1414
|
raw: playlist,
|
|
1357
1415
|
progressTimestamp: new Date(pinnedAt).getTime(),
|
|
1358
1416
|
type: 'playlist',
|
|
1359
|
-
last_engaged_on: playlist.
|
|
1417
|
+
last_engaged_on: playlist.last_engaged_on,
|
|
1360
1418
|
}
|
|
1361
1419
|
}
|
|
1362
1420
|
}
|
|
@@ -1364,6 +1422,35 @@ async function extractPinnedItem({brand, progressMap, playlistItems}) {
|
|
|
1364
1422
|
return null
|
|
1365
1423
|
}
|
|
1366
1424
|
|
|
1425
|
+
async function extractPinnedGuidedCourseItem(guidedCourse, progressMap) {
|
|
1426
|
+
const children = guidedCourse.lessons.map(child => child.id)
|
|
1427
|
+
let existingGuidedCourseProgress = null
|
|
1428
|
+
if (progressMap.has(guidedCourse.id)) {
|
|
1429
|
+
existingGuidedCourseProgress = progressMap.get(guidedCourse.id)
|
|
1430
|
+
progressMap.delete(guidedCourse.id)
|
|
1431
|
+
}
|
|
1432
|
+
let lastChild = null
|
|
1433
|
+
children.forEach(child => {
|
|
1434
|
+
if (progressMap.has(child)) {
|
|
1435
|
+
let childProgress = progressMap.get(child)
|
|
1436
|
+
if (!lastChild && childProgress.state !== 'completed') {
|
|
1437
|
+
lastChild = childProgress
|
|
1438
|
+
lastChild.id = child
|
|
1439
|
+
}
|
|
1440
|
+
progressMap.delete(child)
|
|
1441
|
+
}
|
|
1442
|
+
})
|
|
1443
|
+
return existingGuidedCourseProgress ?? {
|
|
1444
|
+
id: guidedCourse.id,
|
|
1445
|
+
state: 'not-started',
|
|
1446
|
+
percent: 0,
|
|
1447
|
+
raw: guidedCourse,
|
|
1448
|
+
pinned: true,
|
|
1449
|
+
progressTimestamp: new Date().getTime(),
|
|
1450
|
+
childIndex: guidedCourse.id,
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1367
1454
|
function getFirstLeafLessonId(data) {
|
|
1368
1455
|
function findFirstLeaf(lessons) {
|
|
1369
1456
|
for (const item of lessons) {
|
|
@@ -1380,4 +1467,3 @@ function getFirstLeafLessonId(data) {
|
|
|
1380
1467
|
}
|
|
1381
1468
|
|
|
1382
1469
|
|
|
1383
|
-
|