musora-content-services 2.19.2 → 2.21.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/.claude/settings.local.json +8 -0
- package/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +34 -8
- package/src/index.d.ts +8 -0
- package/src/index.js +8 -0
- package/src/services/content-org/guided-courses.ts +2 -2
- package/src/services/content.js +42 -25
- package/src/services/contentAggregator.js +58 -13
- package/src/services/contentProgress.js +69 -3
- package/src/services/recommendations.js +8 -5
- package/src/services/sanity.js +21 -4
- package/src/services/userActivity.js +140 -54
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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.21.0](https://github.com/railroadmedia/musora-content-services/compare/v2.20.0...v2.21.0) (2025-07-12)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **BEH-572:** ai recommender row categories ([#337](https://github.com/railroadmedia/musora-content-services/issues/337)) ([d1984d2](https://github.com/railroadmedia/musora-content-services/commit/d1984d28371fa2d4f22d8ddb50fdcfd952399b46))
|
|
11
|
+
|
|
12
|
+
## [2.20.0](https://github.com/railroadmedia/musora-content-services/compare/v2.19.2...v2.20.0) (2025-07-11)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **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))
|
|
18
|
+
* **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))
|
|
19
|
+
|
|
5
20
|
### [2.19.2](https://github.com/railroadmedia/musora-content-services/compare/v2.19.1...v2.19.2) (2025-07-09)
|
|
6
21
|
|
|
7
22
|
### [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
|
@@ -24,7 +24,6 @@ export const DEFAULT_FIELDS = [
|
|
|
24
24
|
"'url': web_url_path",
|
|
25
25
|
'published_on',
|
|
26
26
|
"'type': _type",
|
|
27
|
-
'progress_percent',
|
|
28
27
|
"'length_in_seconds' : coalesce(length_in_seconds, soundslice[0].soundslice_length_in_second)",
|
|
29
28
|
'brand',
|
|
30
29
|
"'genre': genre[]->name",
|
|
@@ -139,7 +138,7 @@ export const singleLessonTypes = ['quick-tips', 'rudiment', 'coach-lessons'];
|
|
|
139
138
|
export const practiceAlongsLessonTypes = ['workout', 'boot-camp','challenges'];
|
|
140
139
|
export const performancesLessonTypes = ['performance','solo','drum-fest-international-2022'];
|
|
141
140
|
export const documentariesLessonTypes = ['tama','sonor','history-of-electronic-drums','paiste-cymbals'];
|
|
142
|
-
export const liveArchivesLessonTypes = ['podcast', 'coach-stream', 'question-and-answer', 'live-streams'];
|
|
141
|
+
export const liveArchivesLessonTypes = ['podcast', 'coach-stream', 'question-and-answer', 'live-streams', 'live'];
|
|
143
142
|
export const studentArchivesLessonTypes = ['student-review', 'student-focus','student-collaboration'];
|
|
144
143
|
export const tutorialsLessonTypes = ['song-tutorial'];
|
|
145
144
|
export const transcriptionsLessonTypes = ['song'];
|
|
@@ -180,6 +179,8 @@ export const lessonTypesMapping = {
|
|
|
180
179
|
'jam tracks': ['jam-track'],
|
|
181
180
|
};
|
|
182
181
|
|
|
182
|
+
export const getNextLessonLessonParentTypes = ['course', 'guided-course', 'pack-bundle'];
|
|
183
|
+
|
|
183
184
|
export const progressTypesMapping = {
|
|
184
185
|
'lesson': [...singleLessonTypes,...practiceAlongsLessonTypes, ...liveArchivesLessonTypes, ...performancesLessonTypes, ...studentArchivesLessonTypes, ...documentariesLessonTypes, 'live'],
|
|
185
186
|
'course': ['course'],
|
|
@@ -207,12 +208,33 @@ export const filterTypes = {
|
|
|
207
208
|
export const recentTypes = {
|
|
208
209
|
lessons: [...individualLessonsTypes],
|
|
209
210
|
songs: [...tutorialsLessonTypes, ...transcriptionsLessonTypes, ...playAlongLessonTypes],
|
|
210
|
-
home: [...individualLessonsTypes, ...tutorialsLessonTypes, ...transcriptionsLessonTypes, ...playAlongLessonTypes
|
|
211
|
+
home: [...individualLessonsTypes, ...tutorialsLessonTypes, ...transcriptionsLessonTypes, ...playAlongLessonTypes,
|
|
212
|
+
'guided-course', 'learning-path', 'live']
|
|
211
213
|
}
|
|
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,
|
|
@@ -187,6 +189,7 @@ import {
|
|
|
187
189
|
fetchCoachLessons,
|
|
188
190
|
fetchComingSoon,
|
|
189
191
|
fetchCommentModContentData,
|
|
192
|
+
fetchContentRows,
|
|
190
193
|
fetchFoundation,
|
|
191
194
|
fetchGenreLessons,
|
|
192
195
|
fetchHierarchy,
|
|
@@ -284,6 +287,7 @@ import {
|
|
|
284
287
|
createPracticeNotes,
|
|
285
288
|
deletePracticeSession,
|
|
286
289
|
deleteUserActivity,
|
|
290
|
+
findIncompleteLesson,
|
|
287
291
|
getPracticeNotes,
|
|
288
292
|
getPracticeSessions,
|
|
289
293
|
getProgressRows,
|
|
@@ -361,6 +365,7 @@ declare module 'musora-content-services' {
|
|
|
361
365
|
fetchContentInProgress,
|
|
362
366
|
fetchContentPageUserData,
|
|
363
367
|
fetchContentProgress,
|
|
368
|
+
fetchContentRows,
|
|
364
369
|
fetchEnrollmentPageMetadata,
|
|
365
370
|
fetchFoundation,
|
|
366
371
|
fetchGenreLessons,
|
|
@@ -422,14 +427,17 @@ declare module 'musora-content-services' {
|
|
|
422
427
|
fetchUserPracticeMeta,
|
|
423
428
|
fetchUserPracticeNotes,
|
|
424
429
|
fetchUserPractices,
|
|
430
|
+
findIncompleteLesson,
|
|
425
431
|
getActiveDiscussions,
|
|
426
432
|
getAllCompleted,
|
|
427
433
|
getAllStarted,
|
|
428
434
|
getAllStartedOrCompleted,
|
|
429
435
|
getContentRows,
|
|
436
|
+
getLastInteractedOf,
|
|
430
437
|
getLessonContentRows,
|
|
431
438
|
getMonday,
|
|
432
439
|
getNewAndUpcoming,
|
|
440
|
+
getNextLesson,
|
|
433
441
|
getPracticeNotes,
|
|
434
442
|
getPracticeSessions,
|
|
435
443
|
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,
|
|
@@ -187,6 +189,7 @@ import {
|
|
|
187
189
|
fetchCoachLessons,
|
|
188
190
|
fetchComingSoon,
|
|
189
191
|
fetchCommentModContentData,
|
|
192
|
+
fetchContentRows,
|
|
190
193
|
fetchFoundation,
|
|
191
194
|
fetchGenreLessons,
|
|
192
195
|
fetchHierarchy,
|
|
@@ -284,6 +287,7 @@ import {
|
|
|
284
287
|
createPracticeNotes,
|
|
285
288
|
deletePracticeSession,
|
|
286
289
|
deleteUserActivity,
|
|
290
|
+
findIncompleteLesson,
|
|
287
291
|
getPracticeNotes,
|
|
288
292
|
getPracticeSessions,
|
|
289
293
|
getProgressRows,
|
|
@@ -360,6 +364,7 @@ export {
|
|
|
360
364
|
fetchContentInProgress,
|
|
361
365
|
fetchContentPageUserData,
|
|
362
366
|
fetchContentProgress,
|
|
367
|
+
fetchContentRows,
|
|
363
368
|
fetchEnrollmentPageMetadata,
|
|
364
369
|
fetchFoundation,
|
|
365
370
|
fetchGenreLessons,
|
|
@@ -421,14 +426,17 @@ export {
|
|
|
421
426
|
fetchUserPracticeMeta,
|
|
422
427
|
fetchUserPracticeNotes,
|
|
423
428
|
fetchUserPractices,
|
|
429
|
+
findIncompleteLesson,
|
|
424
430
|
getActiveDiscussions,
|
|
425
431
|
getAllCompleted,
|
|
426
432
|
getAllStarted,
|
|
427
433
|
getAllStartedOrCompleted,
|
|
428
434
|
getContentRows,
|
|
435
|
+
getLastInteractedOf,
|
|
429
436
|
getLessonContentRows,
|
|
430
437
|
getMonday,
|
|
431
438
|
getNewAndUpcoming,
|
|
439
|
+
getNextLesson,
|
|
432
440
|
getPracticeNotes,
|
|
433
441
|
getPracticeSessions,
|
|
434
442
|
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
|
}
|
package/src/services/content.js
CHANGED
|
@@ -11,11 +11,12 @@ import {
|
|
|
11
11
|
fetchUpcomingEvents,
|
|
12
12
|
fetchScheduledReleases,
|
|
13
13
|
fetchReturning,
|
|
14
|
-
fetchLeaving, fetchScheduledAndNewReleases
|
|
14
|
+
fetchLeaving, fetchScheduledAndNewReleases, fetchContentRows
|
|
15
15
|
} from './sanity.js'
|
|
16
16
|
import {TabResponseType, Tabs, capitalizeFirstLetter} from '../contentMetaData.js'
|
|
17
17
|
import {fetchHandler} from "./railcontent";
|
|
18
|
-
import {recommendations} from "./recommendations";
|
|
18
|
+
import {recommendations, rankCategories} from "./recommendations";
|
|
19
|
+
import {addContextToContent} from "./contentAggregator.js";
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
export async function getLessonContentRows (brand='drumeo', pageName = 'lessons') {
|
|
@@ -149,11 +150,11 @@ export async function getRecent(brand, pageName, tabName = 'all', {
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
/**
|
|
152
|
-
* Fetches content rows for a given brand and page with optional filtering by content row
|
|
153
|
+
* Fetches content rows for a given brand and page with optional filtering by content row slug.
|
|
153
154
|
*
|
|
154
155
|
* @param {string} brand - The brand for which to fetch content rows.
|
|
155
156
|
* @param {string} pageName - The page name (e.g., 'lessons', 'songs', 'challenges').
|
|
156
|
-
* @param {string}
|
|
157
|
+
* @param {string|null} contentRowSlug - The specific content row ID to fetch.
|
|
157
158
|
* @param {Object} params - Parameters for pagination.
|
|
158
159
|
* @param {number} [params.page=1] - The page number for pagination.
|
|
159
160
|
* @param {number} [params.limit=10] - The maximum number of content items per row.
|
|
@@ -168,32 +169,48 @@ export async function getRecent(brand, pageName, tabName = 'all', {
|
|
|
168
169
|
* .then(content => console.log(content))
|
|
169
170
|
* .catch(error => console.error(error));
|
|
170
171
|
*/
|
|
171
|
-
export async function getContentRows(brand, pageName,
|
|
172
|
+
export async function getContentRows(brand, pageName, contentRowSlug = null, {
|
|
172
173
|
page = 1,
|
|
173
|
-
limit = 10
|
|
174
|
+
limit = 10
|
|
174
175
|
} = {}) {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
176
|
+
const sanityData = await addContextToContent(fetchContentRows, brand, pageName, contentRowSlug, {
|
|
177
|
+
dataField: 'content',
|
|
178
|
+
iterateDataFieldOnEachArrayElement: true,
|
|
179
|
+
addProgressStatus: true,
|
|
180
|
+
addProgressPercentage: true,
|
|
181
|
+
addNextLesson: true
|
|
182
|
+
})
|
|
183
|
+
if (!sanityData) {
|
|
184
|
+
return []
|
|
185
|
+
}
|
|
186
|
+
let contentMap = {}
|
|
187
|
+
let recData = {}
|
|
188
|
+
let slugNameMap = {}
|
|
189
|
+
for (const category of sanityData) {
|
|
190
|
+
recData[category.slug] = category.content.map(item => item.railcontent_id)
|
|
191
|
+
for (const content of category.content) {
|
|
192
|
+
contentMap[content.railcontent_id] = content
|
|
193
|
+
}
|
|
194
|
+
slugNameMap[category.slug] = category.name
|
|
195
|
+
}
|
|
196
|
+
const start = (page - 1) * limit
|
|
197
|
+
const end = start + limit
|
|
198
|
+
const sortedData = await rankCategories(brand, recData)
|
|
199
|
+
let finalData = []
|
|
200
|
+
for (const category of sortedData) {
|
|
201
|
+
finalData.push( {
|
|
202
|
+
id: category.slug,
|
|
203
|
+
title: slugNameMap[category.slug],
|
|
204
|
+
items: category.items.slice(start, end).map(id => contentMap[id])})
|
|
205
|
+
}
|
|
187
206
|
|
|
188
|
-
|
|
189
|
-
|
|
207
|
+
return contentRowSlug ?
|
|
208
|
+
{
|
|
190
209
|
type: TabResponseType.CATALOG,
|
|
191
|
-
data:
|
|
210
|
+
data: finalData[0].items,
|
|
192
211
|
meta: {}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return results
|
|
212
|
+
}
|
|
213
|
+
: finalData
|
|
197
214
|
}
|
|
198
215
|
|
|
199
216
|
/**
|
|
@@ -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)
|
|
@@ -11,12 +16,15 @@ export async function addContextToContent(dataPromise, ...dataArgs)
|
|
|
11
16
|
|
|
12
17
|
const {
|
|
13
18
|
dataField = null,
|
|
19
|
+
iterateDataFieldOnEachArrayElement = false,
|
|
14
20
|
addProgressPercentage = false,
|
|
15
21
|
addIsLiked = false,
|
|
16
22
|
addLikeCount = false,
|
|
17
23
|
addProgressStatus = false,
|
|
18
24
|
addResumeTimeSeconds = false,
|
|
19
25
|
addLastInteractedChild = false,
|
|
26
|
+
addNextLesson = false,
|
|
27
|
+
addLastInteractedParent = false,
|
|
20
28
|
} = options
|
|
21
29
|
|
|
22
30
|
const dataParam = lastArg === options ? dataArgs.slice(0, -1) : dataArgs
|
|
@@ -24,26 +32,49 @@ export async function addContextToContent(dataPromise, ...dataArgs)
|
|
|
24
32
|
const data = await dataPromise(...dataParam)
|
|
25
33
|
if(!data) return false
|
|
26
34
|
|
|
27
|
-
let
|
|
35
|
+
let items = []
|
|
36
|
+
let dataMap = []
|
|
28
37
|
|
|
29
|
-
if (dataField && data?.[dataField]) {
|
|
30
|
-
|
|
38
|
+
if (dataField && (data?.[dataField] || iterateDataFieldOnEachArrayElement)) {
|
|
39
|
+
if (iterateDataFieldOnEachArrayElement && Array.isArray(data)) {
|
|
40
|
+
for(const parent of data) {
|
|
41
|
+
ids = [...ids, ...parent[dataField].map(item => item?.id).filter(Boolean)]
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
ids = data[dataField].map(item => item?.id).filter(Boolean);
|
|
45
|
+
}
|
|
31
46
|
} else if (Array.isArray(data)) {
|
|
32
|
-
|
|
47
|
+
items = data;
|
|
33
48
|
} else if (data?.id) {
|
|
34
|
-
|
|
49
|
+
items = [data]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ids = items.map(item => item?.id).filter(Boolean)
|
|
53
|
+
|
|
54
|
+
//create data structure for common use by functions
|
|
55
|
+
if (addNextLesson) {
|
|
56
|
+
items.forEach((item) => {
|
|
57
|
+
if (item?.id) {
|
|
58
|
+
dataMap.push({
|
|
59
|
+
'children': item.children?.map(child => child.id) ?? [],
|
|
60
|
+
'type': item.type,
|
|
61
|
+
'id': item.id,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
})
|
|
35
65
|
}
|
|
36
66
|
|
|
37
67
|
if(ids.length === 0) return false
|
|
38
68
|
|
|
39
|
-
const [progressPercentageData, progressStatusData, isLikedData, resumeTimeData, lastInteractedChildData] = await Promise.all([
|
|
69
|
+
const [progressPercentageData, progressStatusData, isLikedData, resumeTimeData, lastInteractedChildData, nextLessonData] = await Promise.all([
|
|
40
70
|
addProgressPercentage ? getProgressPercentageByIds(ids) : Promise.resolve(null),
|
|
41
71
|
addProgressStatus ? getProgressStateByIds(ids) : Promise.resolve(null),
|
|
42
72
|
addIsLiked ? isContentLikedByIds(ids) : Promise.resolve(null),
|
|
43
73
|
addResumeTimeSeconds ? getResumeTimeSecondsByIds(ids) : Promise.resolve(null),
|
|
44
74
|
addLastInteractedChild ? fetchLastInteractedChild(ids) : Promise.resolve(null),
|
|
75
|
+
(addNextLesson || addLastInteractedParent) ? getNextLesson(dataMap) : Promise.resolve(null),
|
|
45
76
|
])
|
|
46
|
-
|
|
77
|
+
|
|
47
78
|
const addContext = async (item) => ({
|
|
48
79
|
...item,
|
|
49
80
|
...(addProgressPercentage ? { progressPercentage: progressPercentageData?.[item.id] } : {}),
|
|
@@ -52,12 +83,26 @@ export async function addContextToContent(dataPromise, ...dataArgs)
|
|
|
52
83
|
...(addLikeCount && ids.length === 1 ? { likeCount: await fetchLikeCount(item.id) } : {}),
|
|
53
84
|
...(addResumeTimeSeconds ? { resumeTime: resumeTimeData?.[item.id] } : {}),
|
|
54
85
|
...(addLastInteractedChild ? { lastInteractedChild: lastInteractedChildData?.[item.id] } : {}),
|
|
86
|
+
...(addNextLesson ? { nextLesson: nextLessonData?.[item.id] } : {}),
|
|
55
87
|
})
|
|
56
|
-
|
|
88
|
+
|
|
89
|
+
if (addLastInteractedParent) {
|
|
90
|
+
const parentId = await getLastInteractedOf(ids);
|
|
91
|
+
data['nextLesson'] = nextLessonData[parentId];
|
|
92
|
+
}
|
|
93
|
+
|
|
57
94
|
if (dataField) {
|
|
58
|
-
|
|
95
|
+
if (iterateDataFieldOnEachArrayElement) {
|
|
96
|
+
for(let parent of data) {
|
|
97
|
+
parent[dataField] = Array.isArray(parent[dataField])
|
|
98
|
+
? await Promise.all(parent[dataField].map(addContext))
|
|
99
|
+
: await addContext(parent[dataField])
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
data[dataField] = Array.isArray(data[dataField])
|
|
59
103
|
? await Promise.all(data[dataField].map(addContext))
|
|
60
104
|
: await addContext(data[dataField])
|
|
105
|
+
}
|
|
61
106
|
return data
|
|
62
107
|
} else {
|
|
63
108
|
return Array.isArray(data)
|
|
@@ -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 = {}
|
|
@@ -80,11 +80,14 @@ export async function rankCategories(brand, categories) {
|
|
|
80
80
|
globalConfig.recommendationsConfig.token
|
|
81
81
|
)
|
|
82
82
|
const response = await httpClient.post(url, data)
|
|
83
|
-
let rankedCategories =
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
let rankedCategories = []
|
|
84
|
+
|
|
85
|
+
for (const rankedPlaylist of response['ranked_playlists']) {
|
|
86
|
+
rankedCategories.push({
|
|
87
|
+
'slug': rankedPlaylist.playlist_id,
|
|
88
|
+
'items': rankedPlaylist.ranked_items
|
|
89
|
+
})
|
|
90
|
+
}
|
|
88
91
|
return rankedCategories
|
|
89
92
|
} catch (error) {
|
|
90
93
|
console.error('Fetch error:', error)
|
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(',')
|
|
@@ -540,6 +540,21 @@ export async function fetchByRailContentIds(ids, contentType = undefined, brand
|
|
|
540
540
|
return sortedResults
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
+
export async function fetchContentRows(brand, pageName, contentRowSlug)
|
|
544
|
+
{
|
|
545
|
+
if (pageName === 'lessons') pageName = 'lesson'
|
|
546
|
+
if (pageName === 'songs') pageName = 'song'
|
|
547
|
+
const rowString = contentRowSlug ? ` && slug.current == "${contentRowSlug.toLowerCase()}"` : ''
|
|
548
|
+
return fetchSanity(`*[_type == 'recommended-content-row' && brand == '${brand}' && type == '${pageName}'${rowString}]{
|
|
549
|
+
brand,
|
|
550
|
+
name,
|
|
551
|
+
'slug': slug.current,
|
|
552
|
+
'content': content[]->{ ${getFieldsForContentType()} }
|
|
553
|
+
}`, true)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
|
|
543
558
|
/**
|
|
544
559
|
* Fetch all content for a specific brand and type with pagination, search, and grouping options.
|
|
545
560
|
* @param {string} brand - The brand for which to fetch content.
|
|
@@ -2356,9 +2371,11 @@ export async function fetchTabData(
|
|
|
2356
2371
|
|
|
2357
2372
|
filter = `brand == "${brand}" ${includedFieldsFilter} ${progressFilter}`
|
|
2358
2373
|
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
2359
|
-
entityFieldsString =
|
|
2360
|
-
|
|
2361
|
-
|
|
2374
|
+
entityFieldsString =
|
|
2375
|
+
` ${fieldsString},
|
|
2376
|
+
'children': child[${childrenFilter}]->{'id': railcontent_id},
|
|
2377
|
+
'lesson_count': coalesce(count(child[${childrenFilter}]->), 0),
|
|
2378
|
+
'length_in_seconds': coalesce(
|
|
2362
2379
|
math::sum(
|
|
2363
2380
|
select(
|
|
2364
2381
|
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
|
-
|