musora-content-services 2.7.3 → 2.8.1

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/docs/ContentOrganization.html +2 -2
  3. package/docs/Gamification.html +2 -2
  4. package/docs/UserManagementSystem.html +2 -2
  5. package/docs/api_types.js.html +2 -2
  6. package/docs/config.js.html +2 -2
  7. package/docs/content-org_content-org.js.html +2 -2
  8. package/docs/content-org_playlists-types.js.html +2 -2
  9. package/docs/content-org_playlists.js.html +2 -4
  10. package/docs/content.js.html +2 -2
  11. package/docs/gamification_awards.js.html +2 -2
  12. package/docs/gamification_gamification.js.html +2 -2
  13. package/docs/gamification_types.js.html +2 -2
  14. package/docs/global.html +2 -2
  15. package/docs/index.html +2 -2
  16. package/docs/module-Awards.html +2 -2
  17. package/docs/module-Config.html +2 -2
  18. package/docs/module-Content-Services-V2.html +2 -2
  19. package/docs/module-Interests.html +2 -2
  20. package/docs/module-Permissions.html +2 -2
  21. package/docs/module-Playlists.html +13 -13
  22. package/docs/module-Railcontent-Services.html +2 -2
  23. package/docs/module-Sanity-Services.html +2 -2
  24. package/docs/module-Sessions.html +2 -2
  25. package/docs/module-User-Activity.html +7 -7
  26. package/docs/module-UserManagement.html +448 -23
  27. package/docs/module-UserProfile.html +2 -2
  28. package/docs/railcontent.js.html +2 -2
  29. package/docs/sanity.js.html +2 -2
  30. package/docs/userActivity.js.html +2 -12
  31. package/docs/user_interests.js.html +2 -2
  32. package/docs/user_management.js.html +85 -12
  33. package/docs/user_permissions.js.html +2 -2
  34. package/docs/user_profile.js.html +2 -2
  35. package/docs/user_sessions.js.html +2 -2
  36. package/docs/user_types.js.html +2 -2
  37. package/docs/user_user-management-system.js.html +2 -2
  38. package/link_mcs.sh +0 -0
  39. package/package.json +1 -1
  40. package/src/contentMetaData.js +2 -2
  41. package/src/contentTypeConfig.js +10 -5
  42. package/src/index.d.ts +9 -1
  43. package/src/index.js +9 -1
  44. package/src/lib/httpHelper.js +60 -20
  45. package/src/services/dateUtils.js +29 -0
  46. package/src/services/user/management.js +83 -10
  47. package/src/services/userActivity.js +194 -43
  48. package/test/userActivity.test.js +0 -0
@@ -8,14 +8,14 @@ import {
8
8
  fetchUserPracticeMeta,
9
9
  fetchUserPracticeNotes,
10
10
  fetchHandler,
11
- fetchRecentUserActivities,
11
+ fetchRecentUserActivities, fetchChallengeLessonData
12
12
  } from './railcontent'
13
13
  import { DataContext, UserActivityVersionKey } from './dataContext.js'
14
14
  import { fetchByRailContentIds, fetchShows } from './sanity'
15
- import {fetchUserPlaylists} from "./content-org/playlists";
16
- import { convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay } from './dateUtils.js'
15
+ import {fetchPlaylist, fetchUserPlaylists} from "./content-org/playlists";
16
+ import {convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay, getTimeRemainingUntilLocal} from './dateUtils.js'
17
17
  import { globalConfig } from './config'
18
- import {collectionLessonTypes, lessonTypesMapping, progressTypesMapping} from "../contentTypeConfig";
18
+ import {collectionLessonTypes, lessonTypesMapping, progressTypesMapping, showsLessonTypes, songs} from "../contentTypeConfig";
19
19
  import {getAllStartedOrCompleted, getProgressStateByIds} from "./contentProgress";
20
20
  import {TabResponseType} from "../contentMetaData";
21
21
 
@@ -830,7 +830,7 @@ async function formatPracticeMeta(practices) {
830
830
  title: practice.content_id ? content.title : practice.title,
831
831
  category_id: practice.category_id,
832
832
  instrument_id: practice.instrument_id,
833
- content_type: getFormattedType(content.type || ''),
833
+ content_type: getFormattedType(content.type || '', content.brand),
834
834
  content_id: practice.content_id || null,
835
835
  content_brand: content.brand || null,
836
836
  created_at: convertToTimeZone(utcDate, userTimeZone),
@@ -911,6 +911,7 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
911
911
  const playlistEngagedOnContents = eligiblePlaylistItems.map(item => item.last_engaged_on);
912
912
  const playlistsContents = await fetchByRailContentIds(playlistEngagedOnContents, 'progress-tracker');
913
913
  const excludedParents = new Set();
914
+ const existingShows = new Set();
914
915
  for (const item of playlistsContents) {
915
916
  const contentId = item.id ?? item.railcontent_id;
916
917
  excludedParents.add(contentId)
@@ -960,20 +961,44 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
960
961
  }
961
962
  // Handle standalone parents
962
963
  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
- });
964
+ if(!existingShows.has(content.type)){
965
+ progressMap.set(id, {
966
+ id,
967
+ raw: content,
968
+ state: progress.status,
969
+ percent: progress.progress,
970
+ progressTimestamp: progress.last_update * 1000
971
+ });
972
+ }
973
+ if(showsLessonTypes.includes(content.type)) {
974
+ existingShows.add(content.type)
975
+ }
970
976
  }
971
977
  }
972
- const progressList = Array.from(progressMap.values());
978
+ const pinnedItem = await extractPinnedItem({
979
+ brand,
980
+ progressMap,
981
+ playlistItems: eligiblePlaylistItems,
982
+ })
983
+ const progressList = Array.from(progressMap.values())
984
+ if (pinnedItem) {
985
+ pinnedItem.pinned = true
986
+ }
987
+
988
+ const pinnedId = pinnedItem?.id
989
+ const filteredProgressList = pinnedId
990
+ ? progressList.filter(item => item.id !== pinnedId)
991
+ : progressList;
992
+ const filteredPlaylists = pinnedId
993
+ ? eligiblePlaylistItems.filter(item => item.id !== pinnedId)
994
+ : eligiblePlaylistItems;
995
+ const combinedBase = [...filteredProgressList, ...filteredPlaylists]
996
+ const combined = pinnedItem ? [pinnedItem, ...combinedBase] : combinedBase
997
+
998
+ const finalCombined = mergeAndSortItems(combined, limit)
973
999
 
974
- const combined = mergeAndSortItems([...progressList, ...eligiblePlaylistItems], limit);
975
1000
  const results = await Promise.all(
976
- combined.slice(0, limit).map(item =>
1001
+ finalCombined.slice(0, limit).map(item =>
977
1002
  item.type === 'playlist'
978
1003
  ? processPlaylistItem(item)
979
1004
  : processContentItem(item)
@@ -988,24 +1013,27 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
988
1013
 
989
1014
  async function processContentItem(item) {
990
1015
  let data = item.raw;
991
- const contentType = getFormattedType(data.type);
1016
+ const contentType = getFormattedType(data.type, data.brand);
992
1017
  const status = item.state;
993
1018
 
994
1019
  let ctaText = 'Continue';
995
1020
  if (contentType === 'transcription' || contentType === 'play-along' || contentType === 'jam-track') ctaText = 'Replay Song';
996
1021
  if (contentType === 'lesson') ctaText = status === 'completed' ? 'Revisit Lesson' : 'Continue';
997
- if ((contentType === 'pack' || contentType === 'song tutorial' || collectionLessonTypes.includes(contentType)) && status === 'completed') {
1022
+ if ((contentType === 'guided course' || contentType === 'song tutorial' || collectionLessonTypes.includes(contentType)) && status === 'completed') ctaText = 'Revisit Lessons' ;
1023
+ if (contentType === 'pack' && status === 'completed') {
998
1024
  ctaText = 'View Lessons';
999
1025
  }
1000
1026
 
1001
1027
  if (data.lesson_count > 0) {
1028
+ const lessonIds = extractLessonIds(item);
1029
+ const progressOnItems = await getProgressStateByIds(lessonIds);
1030
+ let completedCount = Object.values(progressOnItems).filter(value => value === 'completed').length;
1031
+ data.completed_children = completedCount;
1032
+
1002
1033
  if (item.childIndex) {
1003
1034
  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;
1035
+ const nextByProgress = findIncompleteLesson(progressOnItems, item.childIndex, item.raw.type)
1036
+ nextId = nextByProgress ? nextByProgress : nextId
1009
1037
 
1010
1038
  const nestedLessons = data.lessons
1011
1039
  .filter(item => Array.isArray(item.lessons))
@@ -1021,36 +1049,57 @@ async function processContentItem(item) {
1021
1049
  }))
1022
1050
  );
1023
1051
 
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;
1052
+ const lessons = (nestedLessons.length === 0) ? data.lessons : nestedLessons
1053
+ const nextLesson = lessons.find(lesson => lesson.id === nextId)
1054
+ data.first_incomplete_child = nextLesson?.parent ?? nextLesson
1055
+ data.second_incomplete_child = (nextLesson?.parent) ? nextLesson : null
1056
+ if(data.type === 'challenge' && nextByProgress !== undefined ){
1057
+ const challenge = await fetchChallengeLessonData(nextByProgress)
1058
+ if(challenge.lesson.is_locked) {
1059
+ const timeRemaining = getTimeRemainingUntilLocal(challenge.lesson.unlock_date, {withTotalSeconds:true})
1060
+ data.is_locked = true
1061
+ data.time_remaining_seconds = timeRemaining.totalSeconds
1062
+ ctaText = 'Next lesson in ' + timeRemaining.formatted
1063
+ }
1064
+ }
1029
1065
  }
1030
1066
  }
1031
1067
 
1032
- if(contentType == 'show' && status == 'completed'){
1068
+ if(contentType == 'show'){
1033
1069
  const shows = await fetchShows(data.brand, data.type)
1034
1070
  const showIds = shows.map(item => item.id);
1035
1071
  const progressOnItems = await getProgressStateByIds(showIds);
1036
- const nextByProgress = findIncompleteLesson(progressOnItems, data.id, data.type);
1037
- data = shows.find(lesson => lesson.id === nextByProgress);
1072
+ const completedCount = Object.values(progressOnItems).filter(value => value === 'completed').length;
1073
+ if(status == 'completed') {
1074
+ const nextByProgress = findIncompleteLesson(progressOnItems, data.id, data.type);
1075
+ data = shows.find(lesson => lesson.id === nextByProgress);
1076
+ }
1077
+ data.completed_children = completedCount;
1078
+ data.child_count = shows.length;
1079
+ item.percent = Math.round((completedCount / shows.length) * 100);
1080
+ if(completedCount == shows.length) {
1081
+ ctaText = 'Revisit Lessons';
1082
+ }
1038
1083
  }
1039
1084
 
1040
1085
  return {
1041
1086
  id: item.id,
1042
1087
  progressType: 'content',
1043
1088
  header: contentType,
1089
+ pinned: item.pinned ?? false,
1044
1090
  body: {
1045
1091
  progressPercent: item.percent,
1046
1092
  thumbnail: data.thumbnail,
1047
1093
  title: data.title,
1094
+ badge: data.badge ?? null,
1095
+ isLocked: data.is_locked ?? false,
1048
1096
  subtitle: !data.child_count || data.lesson_count === 1
1049
- ? `${data.difficulty_string} • ${data.artist_name}`
1097
+ ? (contentType === 'lesson') ? `${item.percent}% Complete`: `${data.difficulty_string} • ${data.artist_name}`
1050
1098
  : `${data.completed_children} of ${data.lesson_count ?? data.child_count} Lessons Complete`
1051
1099
  },
1052
1100
  cta: {
1053
1101
  text: ctaText,
1102
+ timeRemainingToUnlockSeconds: data.time_remaining_seconds ?? null,
1054
1103
  action: {
1055
1104
  type: data.type,
1056
1105
  brand: data.brand,
@@ -1101,10 +1150,12 @@ async function processPlaylistItem(item) {
1101
1150
  id: playlist.id,
1102
1151
  progressType: 'playlist',
1103
1152
  header: 'playlist',
1153
+ pinned: item.pinned ?? false,
1104
1154
  body: {
1105
1155
  first_items_thumbnail_url: playlist.first_items_thumbnail_url,
1106
1156
  title: playlist.name,
1107
- subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`
1157
+ subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`,
1158
+ total_items: playlist.total_items,
1108
1159
  },
1109
1160
  progressTimestamp: item.progressTimestamp,
1110
1161
  cta: {
@@ -1119,12 +1170,13 @@ async function processPlaylistItem(item) {
1119
1170
  }
1120
1171
  }
1121
1172
 
1122
- const getFormattedType = type => {
1173
+ const getFormattedType = (type, brand) => {
1123
1174
  for (const [key, values] of Object.entries(progressTypesMapping)) {
1124
1175
  if (values.includes(type)) {
1125
- return key;
1176
+ return key === 'songs' ? songs[brand] : key;
1126
1177
  }
1127
1178
  }
1179
+
1128
1180
  return null;
1129
1181
  };
1130
1182
 
@@ -1132,11 +1184,10 @@ function extractLessonIds(data) {
1132
1184
  const ids = [];
1133
1185
  function traverse(lessons) {
1134
1186
  for (const item of lessons) {
1135
- if (item.id) {
1136
- ids.push(item.id);
1137
- }
1138
1187
  if (item.lessons) {
1139
1188
  traverse(item.lessons); // Recursively handle nested lessons
1189
+ }else if (item.id) {
1190
+ ids.push(item.id);
1140
1191
  }
1141
1192
  }
1142
1193
  }
@@ -1165,9 +1216,24 @@ async function getEligiblePlaylistItems(playlists) {
1165
1216
  }
1166
1217
 
1167
1218
  function mergeAndSortItems(items, limit) {
1168
- return items
1219
+ const seen = new Set();
1220
+ const deduped = [];
1221
+
1222
+ for (const item of items) {
1223
+ const key = `${item.id}-${item.type || item.raw?.type}`;
1224
+ if (!seen.has(key)) {
1225
+ seen.add(key);
1226
+ deduped.push(item);
1227
+ }
1228
+ }
1229
+
1230
+ return deduped
1169
1231
  .filter(item => typeof item.progressTimestamp === 'number' && item.progressTimestamp > 0)
1170
- .sort((a, b) => b.progressTimestamp - a.progressTimestamp)
1232
+ .sort((a, b) => {
1233
+ if (a.pinned && !b.pinned) return -1;
1234
+ if (!a.pinned && b.pinned) return 1;
1235
+ return b.progressTimestamp - a.progressTimestamp;
1236
+ })
1171
1237
  .slice(0, limit + 5);
1172
1238
  }
1173
1239
 
@@ -1175,7 +1241,7 @@ function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
1175
1241
  const ids = Object.keys(progressOnItems).map(Number);
1176
1242
  if (contentType === 'challenge') {
1177
1243
  // Return first incomplete lesson
1178
- return ids.find(id => progressOnItems[id] !== 'completed') || null;
1244
+ return ids.find(id => progressOnItems[id] !== 'completed') || ids.at(0);
1179
1245
  }
1180
1246
 
1181
1247
  // For other types, find next incomplete after current
@@ -1207,7 +1273,15 @@ function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
1207
1273
  */
1208
1274
  export async function pinProgressRow(brand, id, progressType) {
1209
1275
  const url = `/api/user-management-system/v1/progress/pin?brand=${brand}&id=${id}&progressType=${progressType}`;
1210
- return await fetchHandler(url, 'PUT', null)
1276
+ const response = await fetchHandler(url, 'PUT', null)
1277
+ if (response && !response.error) {
1278
+ await updatePinnedProgressRow(brand, {
1279
+ id,
1280
+ progressType,
1281
+ pinnedAt: new Date().toISOString(),
1282
+ });
1283
+ }
1284
+ return response;
1211
1285
  }
1212
1286
  /**
1213
1287
  * Unpins the current pinned progress row for a user, scoped by brand.
@@ -1221,7 +1295,84 @@ export async function pinProgressRow(brand, id, progressType) {
1221
1295
  * .catch(error => console.error(error));
1222
1296
  */
1223
1297
  export async function unpinProgressRow(brand) {
1224
- const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}`;
1225
- return await fetchHandler(url, 'PUT', null)
1298
+ const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}`
1299
+ const response = await fetchHandler(url, 'PUT', null)
1300
+ if (response && !response.error) {
1301
+ await updatePinnedProgressRow(brand, null)
1302
+ }
1303
+ return response
1226
1304
  }
1227
1305
 
1306
+ async function updatePinnedProgressRow(brand, pinnedData) {
1307
+ const userRaw = await globalConfig.localStorage.getItem('user');
1308
+ const user = userRaw ? JSON.parse(userRaw) : {};
1309
+ user.brand_pinned_progress = user.brand_pinned_progress || {}
1310
+ user.brand_pinned_progress[brand] = pinnedData
1311
+ await globalConfig.localStorage.setItem('user', JSON.stringify(user))
1312
+ }
1313
+
1314
+ async function extractPinnedItem({brand, progressMap, playlistItems}) {
1315
+ const userRaw = await globalConfig.localStorage.getItem('user');
1316
+ const user = userRaw ? JSON.parse(userRaw) : {};
1317
+ user.brand_pinned_progress = user.brand_pinned_progress || {}
1318
+
1319
+ const pinned = user.brand_pinned_progress[brand]
1320
+ if (!pinned) return null
1321
+
1322
+ const {id, progressType, pinnedAt} = pinned
1323
+
1324
+ if (progressType === 'content') {
1325
+ const pinnedId = parseInt(id)
1326
+ if (progressMap.has(pinnedId)) {
1327
+ const item = progressMap.get(pinnedId)
1328
+ progressMap.delete(pinnedId)
1329
+ return item
1330
+ } else {
1331
+ const content = await fetchByRailContentIds([`${pinnedId}`], 'progress-tracker')
1332
+ const firstLessonId = getFirstLeafLessonId(content[0])
1333
+ return {
1334
+ id: pinnedId,
1335
+ state: 'started',
1336
+ percent: 0,
1337
+ raw: content[0],
1338
+ progressTimestamp: new Date(pinnedAt).getTime(),
1339
+ childIndex: firstLessonId
1340
+ }
1341
+ }
1342
+ }
1343
+ if (progressType === 'playlist') {
1344
+ const pinnedPlaylist = playlistItems.find(p => p.raw.id === id)
1345
+ if (pinnedPlaylist) {
1346
+ return pinnedPlaylist
1347
+ }else{
1348
+ const playlist = await fetchPlaylist(id)
1349
+ return {
1350
+ id: id,
1351
+ raw: playlist,
1352
+ progressTimestamp: new Date(pinnedAt).getTime(),
1353
+ type: 'playlist',
1354
+ last_engaged_on: playlist.items[0],
1355
+ }
1356
+ }
1357
+ }
1358
+
1359
+ return null
1360
+ }
1361
+
1362
+ function getFirstLeafLessonId(data) {
1363
+ function findFirstLeaf(lessons) {
1364
+ for (const item of lessons) {
1365
+ if (!item.lessons || item.lessons.length === 0) {
1366
+ return item.id || null
1367
+ }
1368
+ const found = findFirstLeaf(item.lessons)
1369
+ if (found) return found
1370
+ }
1371
+ return null
1372
+ }
1373
+
1374
+ return data.lessons ? findFirstLeaf(data.lessons) : null
1375
+ }
1376
+
1377
+
1378
+
File without changes