musora-content-services 2.7.0 → 2.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.coderabbit.yaml +0 -0
- package/.editorconfig +0 -0
- package/.github/pull_request_template.md +0 -0
- package/.github/workflows/node.js.yml +0 -0
- package/.prettierignore +0 -0
- package/.prettierrc +0 -0
- package/.yarnrc.yml +1 -0
- package/CHANGELOG.md +4 -0
- package/README.md +0 -0
- package/babel.config.cjs +0 -0
- package/docs/Content-Organization.html +0 -0
- package/docs/ContentOrganization.html +2 -2
- package/docs/Gamification.html +2 -2
- package/docs/UserManagement.html +0 -0
- package/docs/UserManagementSystem.html +2 -2
- package/docs/api_types.js.html +2 -2
- package/docs/config.js.html +2 -2
- package/docs/content-org_content-org.js.html +2 -2
- package/docs/content-org_playlists-types.js.html +3 -5
- package/docs/content-org_playlists.js.html +23 -9
- package/docs/content.js.html +9 -2
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/gamification_awards.js.html +2 -2
- package/docs/gamification_gamification.js.html +2 -2
- package/docs/gamification_types.js.html +2 -2
- package/docs/global.html +5 -53
- package/docs/global.html#User +0 -0
- package/docs/index.html +2 -2
- package/docs/module-Awards.html +2 -2
- package/docs/module-Config.html +2 -2
- package/docs/module-Content-Services-V2.html +8 -8
- package/docs/module-Content-Services.html +763 -0
- package/docs/module-Interests.html +2 -2
- package/docs/module-Permissions.html +2 -2
- package/docs/module-Playlists.html +21 -20
- package/docs/module-Railcontent-Services.html +2 -2
- package/docs/module-Sanity-Services.html +30 -30
- package/docs/module-Session-Management.html +0 -0
- package/docs/module-Sessions.html +2 -2
- package/docs/module-User-Activity.html +737 -58
- package/docs/module-User-Management.html +0 -0
- package/docs/module-User-Permissions.html +0 -0
- package/docs/module-UserManagement.html +2 -2
- package/docs/module-UserProfile.html +2 -2
- package/docs/railcontent.js.html +2 -2
- package/docs/sanity.js.html +24 -6
- package/docs/scripts/collapse.js +0 -0
- package/docs/scripts/commonNav.js +0 -0
- package/docs/scripts/linenumber.js +0 -0
- package/docs/scripts/nav.js +0 -0
- package/docs/scripts/polyfill.js +0 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
- package/docs/scripts/prettify/lang-css.js +0 -0
- package/docs/scripts/prettify/prettify.js +0 -0
- package/docs/scripts/search.js +0 -0
- package/docs/styles/jsdoc.css +0 -0
- package/docs/styles/prettify.css +0 -0
- package/docs/types.js.html +0 -0
- package/docs/userActivity.js.html +360 -12
- package/docs/user_interests.js.html +2 -2
- package/docs/user_management.js.html +2 -2
- package/docs/user_permissions.js.html +2 -2
- package/docs/user_profile.js.html +4 -4
- package/docs/user_sessions.js.html +2 -2
- package/docs/user_types.js.html +2 -2
- package/docs/user_user-management-system.js.html +2 -2
- package/docs/user_user-management.js.html +0 -0
- package/jest.config.js +0 -0
- package/jsdoc.json +0 -0
- package/link_mcs.sh +0 -0
- package/package.json +1 -1
- package/src/contentMetaData.js +13 -12
- package/src/contentTypeConfig.js +18 -2
- package/src/filterBuilder.js +0 -0
- package/src/index.d.ts +10 -2
- package/src/index.js +10 -2
- package/src/infrastructure/http/HttpClient.ts +0 -0
- package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
- package/src/infrastructure/http/index.ts +0 -0
- package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
- package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
- package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
- package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
- package/src/lib/httpHelper.js +0 -0
- package/src/lib/lastUpdated.js +0 -0
- package/src/services/api/types.js +0 -0
- package/src/services/config.js +0 -0
- package/src/services/content-org/content-org.js +0 -0
- package/src/services/content-org/playlists-types.js +0 -0
- package/src/services/content-org/playlists.js +0 -0
- package/src/services/content.js +7 -0
- package/src/services/contentAggregator.js +0 -0
- package/src/services/contentLikes.js +0 -0
- package/src/services/contentProgress.js +49 -20
- package/src/services/dataContext.js +0 -0
- package/src/services/dateUtils.js +0 -0
- package/src/services/forum.js +0 -0
- package/src/services/gamification/awards.js +0 -0
- package/src/services/gamification/gamification.js +0 -0
- package/src/services/gamification/types.js +0 -0
- package/src/services/imageSRCBuilder.js +0 -0
- package/src/services/imageSRCVerify.js +0 -0
- package/src/services/railcontent.js +0 -0
- package/src/services/recommendations.js +0 -0
- package/src/services/sanity.js +22 -4
- package/src/services/types.js +0 -0
- package/src/services/user/interests.js +0 -0
- package/src/services/user/management.js +0 -0
- package/src/services/user/permissions.js +0 -0
- package/src/services/user/profile.js +0 -0
- package/src/services/user/sessions.js +0 -0
- package/src/services/user/types.js +0 -0
- package/src/services/user/user-management-system.js +0 -0
- package/src/services/userActivity.js +348 -10
- package/test/HttpClient.test.js +0 -0
- package/test/content.test.js +0 -0
- package/test/contentLikes.test.js +0 -0
- package/test/contentProgress.test.js +0 -0
- package/test/dataContext.test.js +0 -0
- package/test/forum.test.js +0 -0
- package/test/imageSRCBuilder.test.js +0 -0
- package/test/imageSRCVerify.test.js +0 -0
- package/test/initializeTests.js +0 -0
- package/test/lib/lastUpdated.test.js +0 -0
- package/test/live/contentProgressLive.test.js +0 -0
- package/test/live/railcontentLive.test.js +0 -0
- package/test/localStorageMock.js +0 -0
- package/test/log.js +0 -0
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
- package/test/mockData/mockData_progress_content.json +182 -0
- package/test/mockData/mockData_sanity_progress_content.json +1451 -0
- package/test/mockData/mockData_user_practices.json +0 -0
- package/test/progressRows.test.js +215 -0
- package/test/sanityQueryService.test.js +1 -1
- package/test/streakMessage.test.js +0 -0
- package/test/user/permissions.test.js +0 -0
- package/test/userActivity.test.js +0 -0
- package/tools/generate-index.cjs +0 -0
|
@@ -40,6 +40,16 @@ export async function getResumeTimeSecondsByIds(contentIds) {
|
|
|
40
40
|
return getByIds(contentIds, DATA_KEY_RESUME_TIME, 0)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export async function getProgressDateByIds(contentIds) {
|
|
44
|
+
let data = await dataContext.getData()
|
|
45
|
+
let progress = {}
|
|
46
|
+
contentIds?.forEach((id) => (progress[id] = {
|
|
47
|
+
'last_update': data[id]?.[DATA_KEY_LAST_UPDATED_TIME] ?? 0,
|
|
48
|
+
'progress': data[id]?.[DATA_KEY_PROGRESS] ?? 0,
|
|
49
|
+
'status': data[id]?.[DATA_KEY_STATUS] ?? ''}))
|
|
50
|
+
return progress
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
async function getById(contentId, dataKey, defaultValue) {
|
|
44
54
|
let data = await dataContext.getData()
|
|
45
55
|
return data[contentId]?.[dataKey] ?? defaultValue
|
|
@@ -96,29 +106,49 @@ export async function getAllCompleted(limit = null) {
|
|
|
96
106
|
return ids
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
export async function getAllStartedOrCompleted(limit = null) {
|
|
109
|
+
export async function getAllStartedOrCompleted({ limit = null, onlyIds = true, brand = null, excludedIds = [] } = {}) {
|
|
100
110
|
const data = await dataContext.getData()
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
const oneMonthAgoInSeconds = Math.floor(Date.now() / 1000) - 60 * 24 * 60 * 60 // 60 days in seconds
|
|
112
|
+
|
|
113
|
+
const excludedSet = new Set(excludedIds.map(id => parseInt(id))) // ensure IDs are numbers
|
|
114
|
+
|
|
115
|
+
let filtered = Object.entries(data)
|
|
116
|
+
.filter(([key, item]) => {
|
|
117
|
+
const id = parseInt(key)
|
|
118
|
+
const isRelevantStatus =
|
|
119
|
+
item[DATA_KEY_STATUS] === STATE_STARTED || item[DATA_KEY_STATUS] === STATE_COMPLETED
|
|
120
|
+
const isRecent = item[DATA_KEY_LAST_UPDATED_TIME] >= oneMonthAgoInSeconds
|
|
121
|
+
const isCorrectBrand = !brand || item.b === brand
|
|
122
|
+
const isNotExcluded = !excludedSet.has(id)
|
|
123
|
+
return isRelevantStatus && isRecent && isCorrectBrand && isNotExcluded
|
|
110
124
|
})
|
|
111
|
-
.sort(
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
.sort(([, a], [, b]) => {
|
|
126
|
+
const v1 = a[DATA_KEY_LAST_UPDATED_TIME]
|
|
127
|
+
const v2 = b[DATA_KEY_LAST_UPDATED_TIME]
|
|
114
128
|
if (v1 > v2) return -1
|
|
115
129
|
else if (v1 < v2) return 1
|
|
116
130
|
return 0
|
|
117
131
|
})
|
|
132
|
+
|
|
118
133
|
if (limit) {
|
|
119
|
-
|
|
134
|
+
filtered = filtered.slice(0, limit)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (onlyIds) {
|
|
138
|
+
return filtered.map(([key]) => parseInt(key))
|
|
139
|
+
} else {
|
|
140
|
+
const progress = {}
|
|
141
|
+
filtered.forEach(([key, item]) => {
|
|
142
|
+
const id = parseInt(key)
|
|
143
|
+
progress[id] = {
|
|
144
|
+
last_update: item?.[DATA_KEY_LAST_UPDATED_TIME] ?? 0,
|
|
145
|
+
progress: item?.[DATA_KEY_PROGRESS] ?? 0,
|
|
146
|
+
status: item?.[DATA_KEY_STATUS] ?? '',
|
|
147
|
+
brand: item?.b ?? '',
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
return progress
|
|
120
151
|
}
|
|
121
|
-
return ids
|
|
122
152
|
}
|
|
123
153
|
|
|
124
154
|
export async function assignmentStatusCompleted(assignmentId, parentContentId) {
|
|
@@ -252,12 +282,11 @@ export async function recordWatchSession(
|
|
|
252
282
|
sessionId = uuidv4()
|
|
253
283
|
}
|
|
254
284
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
} catch (error) {
|
|
285
|
+
try {
|
|
286
|
+
await recordUserPractice({ content_id: contentId, duration_seconds: Math.ceil(secondsPlayed) })
|
|
287
|
+
} catch (error) {
|
|
259
288
|
console.error('Failed to record user practice:', error)
|
|
260
|
-
|
|
289
|
+
}
|
|
261
290
|
|
|
262
291
|
await dataContext.update(
|
|
263
292
|
async function (localContext) {
|
|
File without changes
|
|
File without changes
|
package/src/services/forum.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/services/sanity.js
CHANGED
|
@@ -512,15 +512,18 @@ export async function fetchByRailContentId(id, contentType) {
|
|
|
512
512
|
* .then(contents => console.log(contents))
|
|
513
513
|
* .catch(error => console.error(error));
|
|
514
514
|
*/
|
|
515
|
-
export async function fetchByRailContentIds(ids, contentType = undefined) {
|
|
515
|
+
export async function fetchByRailContentIds(ids, contentType = undefined, brand= undefined) {
|
|
516
516
|
if (!ids) {
|
|
517
517
|
return []
|
|
518
518
|
}
|
|
519
519
|
const idsString = ids.join(',')
|
|
520
|
+
const brandFilter = brand ? ` && brand == "${brand}"` : ''
|
|
521
|
+
const query = `*[
|
|
522
|
+
railcontent_id in [${idsString}]${brandFilter}
|
|
523
|
+
]{
|
|
524
|
+
${getFieldsForContentType(contentType)}
|
|
525
|
+
}`
|
|
520
526
|
|
|
521
|
-
const query = `*[railcontent_id in [${idsString}]]{
|
|
522
|
-
${getFieldsForContentType(contentType)}
|
|
523
|
-
}`
|
|
524
527
|
const results = await fetchSanity(query, true)
|
|
525
528
|
|
|
526
529
|
const sortFuction = function compare(a, b) {
|
|
@@ -2355,3 +2358,18 @@ export async function fetchScheduledAndNewReleases(
|
|
|
2355
2358
|
|
|
2356
2359
|
return fetchSanity(query, true)
|
|
2357
2360
|
}
|
|
2361
|
+
|
|
2362
|
+
export async function fetchShows(
|
|
2363
|
+
brand,
|
|
2364
|
+
type,
|
|
2365
|
+
sort = 'sort'
|
|
2366
|
+
) {
|
|
2367
|
+
const sortOrder = getSortOrder(sort, brand)
|
|
2368
|
+
const filter = `_type == '${type}' && brand == '${brand}'`
|
|
2369
|
+
const filterParams = {}
|
|
2370
|
+
|
|
2371
|
+
const query = await buildQuery(filter, filterParams, getFieldsForContentType(type), {
|
|
2372
|
+
sortOrder: sortOrder
|
|
2373
|
+
})
|
|
2374
|
+
return fetchSanity(query, true)
|
|
2375
|
+
}
|
package/src/services/types.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -11,10 +11,13 @@ import {
|
|
|
11
11
|
fetchRecentUserActivities,
|
|
12
12
|
} from './railcontent'
|
|
13
13
|
import { DataContext, UserActivityVersionKey } from './dataContext.js'
|
|
14
|
-
import { fetchByRailContentIds } from './sanity'
|
|
15
|
-
import {
|
|
14
|
+
import { fetchByRailContentIds, fetchShows } from './sanity'
|
|
15
|
+
import {fetchUserPlaylists} from "./content-org/playlists";
|
|
16
16
|
import { convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay } from './dateUtils.js'
|
|
17
17
|
import { globalConfig } from './config'
|
|
18
|
+
import {collectionLessonTypes, lessonTypesMapping, progressTypesMapping} from "../contentTypeConfig";
|
|
19
|
+
import {getAllStartedOrCompleted, getProgressStateByIds} from "./contentProgress";
|
|
20
|
+
import {TabResponseType} from "../contentMetaData";
|
|
18
21
|
|
|
19
22
|
const DATA_KEY_PRACTICES = 'practices'
|
|
20
23
|
const DATA_KEY_LAST_UPDATED_TIME = 'u'
|
|
@@ -835,14 +838,6 @@ async function formatPracticeMeta(practices) {
|
|
|
835
838
|
})
|
|
836
839
|
}
|
|
837
840
|
|
|
838
|
-
export function getFormattedType(type) {
|
|
839
|
-
for (const [key, values] of Object.entries(lessonTypesMapping)) {
|
|
840
|
-
if (values.includes(type)) {
|
|
841
|
-
return key.replace(/\b\w/g, (char) => char.toUpperCase())
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
return null
|
|
845
|
-
}
|
|
846
841
|
|
|
847
842
|
/**
|
|
848
843
|
* Records a new user activity in the system.
|
|
@@ -887,3 +882,346 @@ export async function deleteUserActivity(id) {
|
|
|
887
882
|
const url = `/api/user-management-system/v1/activities/${id}`
|
|
888
883
|
return await fetchHandler(url, 'DELETE')
|
|
889
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* Fetches and combines recent user progress rows and playlists, excluding certain types and parents.
|
|
887
|
+
*
|
|
888
|
+
* @param {Object} [options={}] - Options for fetching progress rows.
|
|
889
|
+
* @param {string|null} [options.brand=null] - The brand context for progress data.
|
|
890
|
+
* @param {number} [options.limit=8] - Maximum number of progress rows to return.
|
|
891
|
+
* @returns {Promise<Object>} - A promise that resolves to an object containing progress rows formatted for UI.
|
|
892
|
+
*
|
|
893
|
+
* @example
|
|
894
|
+
* getProgressRows({ brand: 'drumeo', limit: 10 })
|
|
895
|
+
* .then(data => console.log(data))
|
|
896
|
+
* .catch(error => console.error(error));
|
|
897
|
+
*/
|
|
898
|
+
export async function getProgressRows({ brand = null, limit = 8 } = {}) {
|
|
899
|
+
const excludedTypes = new Set([
|
|
900
|
+
'pack-bundle',
|
|
901
|
+
'learning-path-course',
|
|
902
|
+
'learning-path-level'
|
|
903
|
+
]);
|
|
904
|
+
|
|
905
|
+
const recentPlaylists = await fetchUserPlaylists(brand, {
|
|
906
|
+
sort: '-last_progress',
|
|
907
|
+
limit: limit,
|
|
908
|
+
});
|
|
909
|
+
const playlists = recentPlaylists?.data || [];
|
|
910
|
+
const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists);
|
|
911
|
+
const playlistEngagedOnContents = eligiblePlaylistItems.map(item => item.last_engaged_on);
|
|
912
|
+
const playlistsContents = await fetchByRailContentIds(playlistEngagedOnContents, 'progress-tracker');
|
|
913
|
+
const excludedParents = new Set();
|
|
914
|
+
for (const item of playlistsContents) {
|
|
915
|
+
const contentId = item.id ?? item.railcontent_id;
|
|
916
|
+
excludedParents.add(contentId)
|
|
917
|
+
const parentIds = item.parent_content_data || [];
|
|
918
|
+
parentIds.forEach(id => excludedParents.add(id));
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const progressContents = await getAllStartedOrCompleted({onlyIds: false, brand: brand, excludedIds: Array.from(excludedParents) });
|
|
922
|
+
const contents = await fetchByRailContentIds(Object.keys(progressContents), 'progress-tracker', brand);
|
|
923
|
+
const contentsMap = {};
|
|
924
|
+
contents.forEach(content => {
|
|
925
|
+
contentsMap[content.railcontent_id] = content;
|
|
926
|
+
});
|
|
927
|
+
const childToParentMap = {};
|
|
928
|
+
Object.values(contentsMap).forEach(content => {
|
|
929
|
+
if (Array.isArray(content.parent_content_data) && content.parent_content_data.length > 0) {
|
|
930
|
+
childToParentMap[content.id] = content.parent_content_data[content.parent_content_data.length - 1];
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
const progressMap = new Map();
|
|
934
|
+
for (const [idStr, progress] of Object.entries(progressContents)) {
|
|
935
|
+
const id = parseInt(idStr);
|
|
936
|
+
const content = contentsMap[id];
|
|
937
|
+
if (!content || excludedTypes.has(content.type)) continue;
|
|
938
|
+
const parentId = childToParentMap[id];
|
|
939
|
+
// Handle children with parents
|
|
940
|
+
if (parentId) {
|
|
941
|
+
const parentContent = contentsMap[parentId];
|
|
942
|
+
if (!parentContent || excludedTypes.has(parentContent.type)) continue;
|
|
943
|
+
const existing = progressMap.get(parentId);
|
|
944
|
+
if (existing) {
|
|
945
|
+
// If childIndex isn't already set, set it
|
|
946
|
+
if (existing.childIndex === undefined) {
|
|
947
|
+
existing.childIndex = id;
|
|
948
|
+
}
|
|
949
|
+
} else {
|
|
950
|
+
progressMap.set(parentId, {
|
|
951
|
+
id: parentId,
|
|
952
|
+
raw: parentContent,
|
|
953
|
+
state: progress.status,
|
|
954
|
+
percent: progress.progress,
|
|
955
|
+
progressTimestamp: progress.last_update * 1000,
|
|
956
|
+
childIndex: id
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
// Handle standalone parents
|
|
962
|
+
if (!progressMap.has(id)) {
|
|
963
|
+
progressMap.set(id, {
|
|
964
|
+
id,
|
|
965
|
+
raw: content,
|
|
966
|
+
state: progress.status,
|
|
967
|
+
percent: progress.progress,
|
|
968
|
+
progressTimestamp: progress.last_update * 1000
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const progressList = Array.from(progressMap.values());
|
|
973
|
+
|
|
974
|
+
const combined = mergeAndSortItems([...progressList, ...eligiblePlaylistItems], limit);
|
|
975
|
+
const results = await Promise.all(
|
|
976
|
+
combined.slice(0, limit).map(item =>
|
|
977
|
+
item.type === 'playlist'
|
|
978
|
+
? processPlaylistItem(item)
|
|
979
|
+
: processContentItem(item)
|
|
980
|
+
)
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
return {
|
|
984
|
+
type: TabResponseType.PROGRESS_ROWS,
|
|
985
|
+
data: results
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async function processContentItem(item) {
|
|
990
|
+
let data = item.raw;
|
|
991
|
+
const contentType = getFormattedType(data.type);
|
|
992
|
+
const status = item.state;
|
|
993
|
+
|
|
994
|
+
let ctaText = 'Continue';
|
|
995
|
+
if (contentType === 'transcription' || contentType === 'play-along' || contentType === 'jam-track') ctaText = 'Replay Song';
|
|
996
|
+
if (contentType === 'lesson') ctaText = status === 'completed' ? 'Revisit Lesson' : 'Continue';
|
|
997
|
+
if ((contentType === 'pack' || contentType === 'song tutorial' || collectionLessonTypes.includes(contentType)) && status === 'completed') {
|
|
998
|
+
ctaText = 'View Lessons';
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (data.lesson_count > 0) {
|
|
1002
|
+
if (item.childIndex) {
|
|
1003
|
+
let nextId = item.childIndex
|
|
1004
|
+
const lessonIds = extractLessonIds(item);
|
|
1005
|
+
const progressOnItems = await getProgressStateByIds(lessonIds);
|
|
1006
|
+
const completedCount = Object.values(progressOnItems).filter(value => value === 'completed').length;
|
|
1007
|
+
const nextByProgress = findIncompleteLesson(progressOnItems, item.childIndex, item.raw.type);
|
|
1008
|
+
nextId = nextByProgress ? nextByProgress : nextId;
|
|
1009
|
+
|
|
1010
|
+
const nestedLessons = data.lessons
|
|
1011
|
+
.filter(item => Array.isArray(item.lessons))
|
|
1012
|
+
.flatMap(parent =>
|
|
1013
|
+
parent.lessons.map(lesson => ({
|
|
1014
|
+
...lesson,
|
|
1015
|
+
parent: {
|
|
1016
|
+
id: parent.id,
|
|
1017
|
+
slug: parent.slug,
|
|
1018
|
+
title: parent.title,
|
|
1019
|
+
type: parent.type
|
|
1020
|
+
}
|
|
1021
|
+
}))
|
|
1022
|
+
);
|
|
1023
|
+
|
|
1024
|
+
const lessons = (nestedLessons.length === 0) ? data.lessons : nestedLessons;
|
|
1025
|
+
const nextLesson = lessons.find(lesson => lesson.id === nextId);
|
|
1026
|
+
data.first_incomplete_child = nextLesson?.parent ?? nextLesson;
|
|
1027
|
+
data.second_incomplete_child = (nextLesson?.parent) ? nextLesson : null;
|
|
1028
|
+
data.completed_children = completedCount;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if(contentType == 'show' && status == 'completed'){
|
|
1033
|
+
const shows = await fetchShows(data.brand, data.type)
|
|
1034
|
+
const showIds = shows.map(item => item.id);
|
|
1035
|
+
const progressOnItems = await getProgressStateByIds(showIds);
|
|
1036
|
+
const nextByProgress = findIncompleteLesson(progressOnItems, data.id, data.type);
|
|
1037
|
+
data = shows.find(lesson => lesson.id === nextByProgress);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
return {
|
|
1041
|
+
id: item.id,
|
|
1042
|
+
progressType: 'content',
|
|
1043
|
+
header: contentType,
|
|
1044
|
+
body: {
|
|
1045
|
+
progressPercent: item.percent,
|
|
1046
|
+
thumbnail: data.thumbnail,
|
|
1047
|
+
title: data.title,
|
|
1048
|
+
subtitle: !data.child_count || data.lesson_count === 1
|
|
1049
|
+
? `${data.difficulty_string} • ${data.artist_name}`
|
|
1050
|
+
: `${data.completed_children} of ${data.lesson_count ?? data.child_count} Lessons Complete`
|
|
1051
|
+
},
|
|
1052
|
+
cta: {
|
|
1053
|
+
text: ctaText,
|
|
1054
|
+
action: {
|
|
1055
|
+
type: data.type,
|
|
1056
|
+
brand: data.brand,
|
|
1057
|
+
id: data.id,
|
|
1058
|
+
slug: data.slug,
|
|
1059
|
+
child: data.first_incomplete_child
|
|
1060
|
+
? {
|
|
1061
|
+
id: data.first_incomplete_child.id,
|
|
1062
|
+
type: data.first_incomplete_child.type,
|
|
1063
|
+
brand: data.first_incomplete_child.brand,
|
|
1064
|
+
slug: data.first_incomplete_child.slug,
|
|
1065
|
+
child: data.second_incomplete_child
|
|
1066
|
+
? {
|
|
1067
|
+
id: data.second_incomplete_child.id,
|
|
1068
|
+
type: data.second_incomplete_child.type,
|
|
1069
|
+
brand: data.second_incomplete_child.brand,
|
|
1070
|
+
slug: data.second_incomplete_child.slug
|
|
1071
|
+
}
|
|
1072
|
+
: null
|
|
1073
|
+
}
|
|
1074
|
+
: null
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
progressTimestamp: item.progressTimestamp
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
async function processPlaylistItem(item) {
|
|
1082
|
+
const playlist = item.raw;
|
|
1083
|
+
const progressOnItems = await getProgressStateByIds(playlist.items.map(a => a.content_id));
|
|
1084
|
+
const allItemsCompleted = item.raw.items.every(i => {
|
|
1085
|
+
const itemId = i.content_id;
|
|
1086
|
+
const progress = progressOnItems[itemId];
|
|
1087
|
+
return progress && progress === 'completed';
|
|
1088
|
+
});
|
|
1089
|
+
let nextItem = playlist.items[0] ?? null;
|
|
1090
|
+
if (!allItemsCompleted) {
|
|
1091
|
+
const lastItemProgress = progressOnItems[playlist.last_engaged_on];
|
|
1092
|
+
const index = playlist.items.findIndex(i => i.content_id === playlist.last_engaged_on);
|
|
1093
|
+
if (lastItemProgress === 'completed') {
|
|
1094
|
+
nextItem = playlist.items[index + 1] ?? nextItem;
|
|
1095
|
+
} else {
|
|
1096
|
+
nextItem = playlist.items[index] ?? nextItem;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return {
|
|
1101
|
+
id: playlist.id,
|
|
1102
|
+
progressType: 'playlist',
|
|
1103
|
+
header: 'playlist',
|
|
1104
|
+
body: {
|
|
1105
|
+
first_items_thumbnail_url: playlist.first_items_thumbnail_url,
|
|
1106
|
+
title: playlist.name,
|
|
1107
|
+
subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`
|
|
1108
|
+
},
|
|
1109
|
+
progressTimestamp: item.progressTimestamp,
|
|
1110
|
+
cta: {
|
|
1111
|
+
text: 'Continue',
|
|
1112
|
+
action: {
|
|
1113
|
+
brand: playlist.brand,
|
|
1114
|
+
id: playlist.id,
|
|
1115
|
+
itemId: nextItem.id,
|
|
1116
|
+
type: 'playlists',
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const getFormattedType = type => {
|
|
1123
|
+
for (const [key, values] of Object.entries(progressTypesMapping)) {
|
|
1124
|
+
if (values.includes(type)) {
|
|
1125
|
+
return key;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return null;
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
function extractLessonIds(data) {
|
|
1132
|
+
const ids = [];
|
|
1133
|
+
function traverse(lessons) {
|
|
1134
|
+
for (const item of lessons) {
|
|
1135
|
+
if (item.id) {
|
|
1136
|
+
ids.push(item.id);
|
|
1137
|
+
}
|
|
1138
|
+
if (item.lessons) {
|
|
1139
|
+
traverse(item.lessons); // Recursively handle nested lessons
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (data.raw && Array.isArray(data.raw.lessons)) {
|
|
1144
|
+
traverse(data.raw.lessons);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return ids;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
async function getEligiblePlaylistItems(playlists) {
|
|
1152
|
+
const eligible = playlists.filter(p => p.last_progress && p.last_engaged_on);
|
|
1153
|
+
return Promise.all(
|
|
1154
|
+
eligible.map(async p => {
|
|
1155
|
+
const utcDate = new Date(p.last_progress.replace(' ', 'T') + 'Z');
|
|
1156
|
+
const timestamp = utcDate.getTime();
|
|
1157
|
+
return {
|
|
1158
|
+
type: 'playlist',
|
|
1159
|
+
progressTimestamp: timestamp,
|
|
1160
|
+
last_engaged_on: p.last_engaged_on,
|
|
1161
|
+
raw: p
|
|
1162
|
+
};
|
|
1163
|
+
})
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function mergeAndSortItems(items, limit) {
|
|
1168
|
+
return items
|
|
1169
|
+
.filter(item => typeof item.progressTimestamp === 'number' && item.progressTimestamp > 0)
|
|
1170
|
+
.sort((a, b) => b.progressTimestamp - a.progressTimestamp)
|
|
1171
|
+
.slice(0, limit + 5);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
|
|
1175
|
+
const ids = Object.keys(progressOnItems).map(Number);
|
|
1176
|
+
if (contentType === 'challenge') {
|
|
1177
|
+
// Return first incomplete lesson
|
|
1178
|
+
return ids.find(id => progressOnItems[id] !== 'completed') || null;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// For other types, find next incomplete after current
|
|
1182
|
+
const currentIndex = ids.indexOf(Number(currentContentId));
|
|
1183
|
+
if (currentIndex === -1) return null;
|
|
1184
|
+
|
|
1185
|
+
for (let i = currentIndex + 1; i < ids.length; i++) {
|
|
1186
|
+
const id = ids[i];
|
|
1187
|
+
if (progressOnItems[id] !== 'completed') {
|
|
1188
|
+
return id;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
return ids[0];
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Pins a specific progress row for a user, scoped by brand.
|
|
1197
|
+
*
|
|
1198
|
+
* @param {string} brand - The brand context for the pin action.
|
|
1199
|
+
* @param {number|string} id - The ID of the progress item to pin.
|
|
1200
|
+
* @param {string} progressType - The type of progress (e.g., 'content', 'playlist').
|
|
1201
|
+
* @returns {Promise<Object>} - A promise resolving to the response from the pin API.
|
|
1202
|
+
*
|
|
1203
|
+
* @example
|
|
1204
|
+
* pinProgressRow('drumeo', 12345, 'content')
|
|
1205
|
+
* .then(response => console.log(response))
|
|
1206
|
+
* .catch(error => console.error(error));
|
|
1207
|
+
*/
|
|
1208
|
+
export async function pinProgressRow(brand, id, progressType) {
|
|
1209
|
+
const url = `/api/user-management-system/v1/progress/pin?brand=${brand}&id=${id}&progressType=${progressType}`;
|
|
1210
|
+
return await fetchHandler(url, 'PUT', null)
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Unpins the current pinned progress row for a user, scoped by brand.
|
|
1214
|
+
*
|
|
1215
|
+
* @param {string} brand - The brand context for the unpin action.
|
|
1216
|
+
* @returns {Promise<Object>} - A promise resolving to the response from the unpin API.
|
|
1217
|
+
*
|
|
1218
|
+
* @example
|
|
1219
|
+
* unpinProgressRow('drumeo')
|
|
1220
|
+
* .then(response => console.log(response))
|
|
1221
|
+
* .catch(error => console.error(error));
|
|
1222
|
+
*/
|
|
1223
|
+
export async function unpinProgressRow(brand) {
|
|
1224
|
+
const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}`;
|
|
1225
|
+
return await fetchHandler(url, 'PUT', null)
|
|
1226
|
+
}
|
|
1227
|
+
|
package/test/HttpClient.test.js
CHANGED
|
File without changes
|
package/test/content.test.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/dataContext.test.js
CHANGED
|
File without changes
|
package/test/forum.test.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/initializeTests.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/localStorageMock.js
CHANGED
|
File without changes
|
package/test/log.js
CHANGED
|
File without changes
|
|
File without changes
|