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