musora-content-services 2.30.4 → 2.30.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [2.30.5](https://github.com/railroadmedia/musora-content-services/compare/v2.30.4...v2.30.5) (2025-08-06)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **MU2-918:** practice meta data for manual only sessions ([#402](https://github.com/railroadmedia/musora-content-services/issues/402)) ([a911529](https://github.com/railroadmedia/musora-content-services/commit/a9115297f00b6aed5f0ad73714cb860cfd71f331))
11
+ * **MU2-918:** safer .find use ([#405](https://github.com/railroadmedia/musora-content-services/issues/405)) ([ce81469](https://github.com/railroadmedia/musora-content-services/commit/ce814692a7aa46ee814c28ecae1ec404a0c09046))
12
+
5
13
  ### [2.30.4](https://github.com/railroadmedia/musora-content-services/compare/v2.30.3...v2.30.4) (2025-08-05)
6
14
 
7
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.30.4",
3
+ "version": "2.30.5",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -35,7 +35,7 @@ import { TabResponseType } from '../contentMetaData'
35
35
  import dayjs from 'dayjs'
36
36
  import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
37
37
  import weekOfYear from 'dayjs/plugin/weekOfYear'
38
- import {addContextToContent} from "./contentAggregator.js";
38
+ import { addContextToContent } from './contentAggregator.js'
39
39
 
40
40
  const DATA_KEY_PRACTICES = 'practices'
41
41
  const DATA_KEY_LAST_UPDATED_TIME = 'u'
@@ -585,10 +585,10 @@ export async function getRecentActivity({ page = 1, limit = 5, tabName = null }
585
585
  const contentIds = recentActivityData.data.map((p) => p.contentId).filter((id) => id !== null)
586
586
  const contents = await addContextToContent(fetchByRailContentIds, contentIds, {
587
587
  addNavigateTo: true,
588
- addNextLesson: true
588
+ addNextLesson: true,
589
589
  })
590
590
  recentActivityData.data = recentActivityData.data.map((practice) => {
591
- const content = contents.find((c) => c.id === practice.contentId) || {}
591
+ const content = contents?.find((c) => c.id === practice.contentId) || {}
592
592
  return {
593
593
  ...practice,
594
594
  parent_id: content.parent_id || null,
@@ -853,14 +853,15 @@ export async function calculateLongestStreaks(userId = globalConfig.sessionConfi
853
853
  }
854
854
  }
855
855
 
856
- async function formatPracticeMeta(practices) {
856
+ async function formatPracticeMeta(practices = []) {
857
857
  const contentIds = practices.map((p) => p.content_id).filter((id) => id !== null)
858
858
  const contents = await addContextToContent(fetchByRailContentIds, contentIds, {
859
859
  addNavigateTo: true,
860
- addNextLesson: true
860
+ addNextLesson: true,
861
861
  })
862
+
862
863
  return practices.map((practice) => {
863
- const content = contents.find((c) => c.id === practice.content_id) || {}
864
+ const content = contents && contents.length > 0 ? contents.find((c) => c.id === practice.content_id) : {}
864
865
 
865
866
  return {
866
867
  id: practice.id,
@@ -869,18 +870,18 @@ async function formatPracticeMeta(practices) {
869
870
  thumbnail_url: practice.content_id ? content.thumbnail : practice.thumbnail_url || '',
870
871
  duration: practice.duration_seconds || 0,
871
872
  duration_seconds: practice.duration_seconds || 0,
872
- content_url: content.url || null,
873
+ content_url: content?.url || null,
873
874
  title: practice.content_id ? content.title : practice.title,
874
875
  category_id: practice.category_id,
875
876
  instrument_id: practice.instrument_id,
876
- content_type: getFormattedType(content.type || '', content.brand),
877
+ content_type: getFormattedType(content?.type || '', content?.brand || null),
877
878
  content_id: practice.content_id || null,
878
- content_brand: content.brand || null,
879
+ content_brand: content?.brand || null,
879
880
  created_at: dayjs(practice.created_at),
880
- sanity_type: content.type || null,
881
- content_slug: content.slug || null,
882
- parent_id: content.parent_id || null,
883
- navigateTo: content.navigateTo || null,
881
+ sanity_type: content?.type || null,
882
+ content_slug: content?.slug || null,
883
+ parent_id: content?.parent_id || null,
884
+ navigateTo: content?.navigateTo || null,
884
885
  }
885
886
  })
886
887
  }
@@ -929,24 +930,31 @@ export async function deleteUserActivity(id) {
929
930
  return await fetchHandler(url, 'DELETE')
930
931
  }
931
932
 
932
- async function extractPinnedItemsAndSortAllItems(userPinnedItem, contentsMap, eligiblePlaylistItems, pinnedGuidedCourse, limit) {
933
+ async function extractPinnedItemsAndSortAllItems(
934
+ userPinnedItem,
935
+ contentsMap,
936
+ eligiblePlaylistItems,
937
+ pinnedGuidedCourse,
938
+ limit
939
+ ) {
933
940
  let pinnedItem = await popPinnedItemFromContentsOrPlaylistMap(
934
941
  userPinnedItem,
935
942
  contentsMap,
936
- eligiblePlaylistItems,
943
+ eligiblePlaylistItems
937
944
  )
938
945
 
939
946
  const guidedCourseID = pinnedGuidedCourse?.content_id
940
- let combined = [];
947
+ let combined = []
941
948
  if (pinnedGuidedCourse) {
942
- const guidedCourseContent = contentsMap.get(guidedCourseID) ?? await addContextToContent(fetchByRailContentId, guidedCourseID, 'guided-course',
943
- {
949
+ const guidedCourseContent =
950
+ contentsMap.get(guidedCourseID) ??
951
+ (await addContextToContent(fetchByRailContentId, guidedCourseID, 'guided-course', {
944
952
  addNextLesson: true,
945
953
  addNavigateTo: true,
946
954
  addProgressStatus: true,
947
955
  addProgressPercentage: true,
948
956
  addProgressTimestamp: true,
949
- })
957
+ }))
950
958
  contentsMap = popContentAndRemoveChildrenFromContentsMap(guidedCourseContent, contentsMap)
951
959
  guidedCourseContent.pinned = true
952
960
  combined.push(guidedCourseContent)
@@ -967,23 +975,26 @@ function generateContentsMap(contents, playlistsContents) {
967
975
  'learning-path-course',
968
976
  'learning-path-level',
969
977
  'guided-course-part',
970
- ]);
971
- const existingShows = new Set();
972
- const contentsMap = new Map();
973
- const childToParentMap = {};
974
- contents.forEach(content => {
978
+ ])
979
+ const existingShows = new Set()
980
+ const contentsMap = new Map()
981
+ const childToParentMap = {}
982
+ contents.forEach((content) => {
975
983
  if (Array.isArray(content.parent_content_data) && content.parent_content_data.length > 0) {
976
- childToParentMap[content.id] = content.parent_content_data[content.parent_content_data.length - 1];
984
+ childToParentMap[content.id] =
985
+ content.parent_content_data[content.parent_content_data.length - 1]
977
986
  }
978
- });
987
+ })
979
988
 
980
- const allRecentTypeSet = new Set(
981
- Object.values(recentTypes).flat()
982
- )
983
- contents.forEach(content => {
989
+ const allRecentTypeSet = new Set(Object.values(recentTypes).flat())
990
+ contents.forEach((content) => {
984
991
  const id = content.id
985
992
  const type = content.type
986
- if (excludedTypes.has(type) || (!allRecentTypeSet.has(type) && !showsLessonTypes.includes(type)) ) return;
993
+ if (
994
+ excludedTypes.has(type) ||
995
+ (!allRecentTypeSet.has(type) && !showsLessonTypes.includes(type))
996
+ )
997
+ return
987
998
  if (!childToParentMap[id]) {
988
999
  // Shows don't have a parent to link them, but need to be handled as if they're a set of children
989
1000
  if (!existingShows.has(type)) {
@@ -1001,11 +1012,11 @@ function generateContentsMap(contents, playlistsContents) {
1001
1012
  for (const item of playlistsContents) {
1002
1013
  const contentId = item.id
1003
1014
  contentsMap.delete(contentId)
1004
- const parentIds = item.parent_content_data || [];
1005
- parentIds.forEach(id => contentsMap.delete(id));
1015
+ const parentIds = item.parent_content_data || []
1016
+ parentIds.forEach((id) => contentsMap.delete(id))
1006
1017
  }
1007
1018
  }
1008
- return contentsMap;
1019
+ return contentsMap
1009
1020
  }
1010
1021
 
1011
1022
  /**
@@ -1022,19 +1033,21 @@ function generateContentsMap(contents, playlistsContents) {
1022
1033
  * .catch(error => console.error(error));
1023
1034
  */
1024
1035
  export async function getProgressRows({ brand = null, limit = 8 } = {}) {
1025
-
1026
1036
  // TODO slice progress to a reasonable number, say 100
1027
- const [recentPlaylists, progressContents, allPinnedGuidedCourse, userPinnedItem ] = await Promise.all([
1028
- fetchUserPlaylists(brand, { sort: '-last_progress', limit: limit}),
1029
- getAllStartedOrCompleted({onlyIds: false, brand: brand }),
1030
- pinnedGuidedCourses(brand),
1031
- getUserPinnedItem(brand),
1032
- ])
1037
+ const [recentPlaylists, progressContents, allPinnedGuidedCourse, userPinnedItem] =
1038
+ await Promise.all([
1039
+ fetchUserPlaylists(brand, { sort: '-last_progress', limit: limit }),
1040
+ getAllStartedOrCompleted({ onlyIds: false, brand: brand }),
1041
+ pinnedGuidedCourses(brand),
1042
+ getUserPinnedItem(brand),
1043
+ ])
1033
1044
  let pinnedGuidedCourse = allPinnedGuidedCourse?.[0] ?? null
1034
1045
 
1035
- const playlists = recentPlaylists?.data || [];
1036
- const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists);
1037
- const playlistEngagedOnContents = eligiblePlaylistItems.map(item => item.playlist.last_engaged_on);
1046
+ const playlists = recentPlaylists?.data || []
1047
+ const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists)
1048
+ const playlistEngagedOnContents = eligiblePlaylistItems.map(
1049
+ (item) => item.playlist.last_engaged_on
1050
+ )
1038
1051
 
1039
1052
  const nonPlaylistContentIds = Object.keys(progressContents)
1040
1053
  if (pinnedGuidedCourse) {
@@ -1043,7 +1056,7 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
1043
1056
  if (userPinnedItem?.progressType === 'content') {
1044
1057
  nonPlaylistContentIds.push(userPinnedItem.id)
1045
1058
  }
1046
- const [ playlistsContents, contents ] = await Promise.all([
1059
+ const [playlistsContents, contents] = await Promise.all([
1047
1060
  addContextToContent(fetchByRailContentIds, playlistEngagedOnContents, 'progress-tracker', {
1048
1061
  addNextLesson: true,
1049
1062
  addNavigateTo: true,
@@ -1057,17 +1070,23 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
1057
1070
  addProgressStatus: true,
1058
1071
  addProgressPercentage: true,
1059
1072
  addProgressTimestamp: true,
1060
- })
1061
- ]);
1062
- const contentsMap = generateContentsMap(contents, playlistsContents);
1063
- let combined = await extractPinnedItemsAndSortAllItems(userPinnedItem, contentsMap, eligiblePlaylistItems, pinnedGuidedCourse, limit);
1073
+ }),
1074
+ ])
1075
+ const contentsMap = generateContentsMap(contents, playlistsContents)
1076
+ let combined = await extractPinnedItemsAndSortAllItems(
1077
+ userPinnedItem,
1078
+ contentsMap,
1079
+ eligiblePlaylistItems,
1080
+ pinnedGuidedCourse,
1081
+ limit
1082
+ )
1064
1083
  const results = await Promise.all(
1065
- combined.slice(0, limit).map(item =>
1066
- item.type === 'playlist'
1067
- ? processPlaylistItem(item)
1068
- : processContentItem(item)
1069
- )
1070
- );
1084
+ combined
1085
+ .slice(0, limit)
1086
+ .map((item) =>
1087
+ item.type === 'playlist' ? processPlaylistItem(item) : processContentItem(item)
1088
+ )
1089
+ )
1071
1090
  console.log('HomePageProgressRows results: remove before merge', results)
1072
1091
  return {
1073
1092
  type: TabResponseType.PROGRESS_ROWS,
@@ -1084,118 +1103,132 @@ async function getUserPinnedItem(brand) {
1084
1103
  }
1085
1104
 
1086
1105
  async function processContentItem(content) {
1087
- const contentType = getFormattedType(content.type, content.brand);
1106
+ const contentType = getFormattedType(content.type, content.brand)
1088
1107
  const isLive = content.isLive ?? false
1089
1108
  let ctaText = getDefaultCTATextForContent(content, contentType)
1090
1109
 
1091
1110
  content.completed_children = await getCompletedChildren(content, contentType)
1092
1111
 
1093
1112
  if (content.type === 'guided-course') {
1094
- const nextLessonPublishedOn = content.children.find(child => child.id === content.navigateTo.id)?.published_on
1113
+ const nextLessonPublishedOn = content.children.find(
1114
+ (child) => child.id === content.navigateTo.id
1115
+ )?.published_on
1095
1116
  let isLocked = new Date(nextLessonPublishedOn) > new Date()
1096
1117
  if (isLocked) {
1097
1118
  content.is_locked = true
1098
- const timeRemaining = getTimeRemainingUntilLocal(nextLessonPublishedOn, {withTotalSeconds: true})
1119
+ const timeRemaining = getTimeRemainingUntilLocal(nextLessonPublishedOn, {
1120
+ withTotalSeconds: true,
1121
+ })
1099
1122
  content.time_remaining_seconds = timeRemaining.totalSeconds
1100
1123
  ctaText = 'Next lesson in ' + timeRemaining.formatted
1101
- } else if (!content.progressStatus || content.progressStatus === 'not-started' ) {
1102
- ctaText = "Start Course"
1124
+ } else if (!content.progressStatus || content.progressStatus === 'not-started') {
1125
+ ctaText = 'Start Course'
1103
1126
  }
1104
1127
  }
1105
1128
 
1106
- if (contentType === 'show'){
1129
+ if (contentType === 'show') {
1107
1130
  const shows = await fetchShows(content.brand, content.type)
1108
- const showIds = shows.map(item => item.id);
1109
- const progressOnItems = await getProgressStateByIds(showIds);
1131
+ const showIds = shows.map((item) => item.id)
1132
+ const progressOnItems = await getProgressStateByIds(showIds)
1110
1133
  const completedShows = content.completed_children
1111
1134
  const progressTimestamp = content.progressTimestamp
1112
1135
  const wasPinned = content.pinned ?? false
1113
1136
  if (content.progressStatus === 'completed') {
1114
1137
  // this could be handled more gracefully if their was a parent content type for shows
1115
- const nextByProgress = findIncompleteLesson(progressOnItems, content.id, content.type);
1116
- content = shows.find(lesson => lesson.id === nextByProgress);
1138
+ const nextByProgress = findIncompleteLesson(progressOnItems, content.id, content.type)
1139
+ content = shows.find((lesson) => lesson.id === nextByProgress)
1117
1140
  content.completed_children = completedShows
1118
1141
  content.progressTimestamp = progressTimestamp
1119
1142
  content.pinned = wasPinned
1120
1143
  }
1121
- content.child_count = shows.length;
1122
- content.progressPercentage = Math.round((completedShows / shows.length) * 100);
1144
+ content.child_count = shows.length
1145
+ content.progressPercentage = Math.round((completedShows / shows.length) * 100)
1123
1146
  if (completedShows === shows.length) {
1124
- ctaText = 'Revisit Show';
1147
+ ctaText = 'Revisit Show'
1125
1148
  }
1126
1149
  }
1127
1150
 
1128
1151
  return {
1129
- id: content.id,
1130
- progressType: 'content',
1131
- header: contentType,
1132
- pinned: content.pinned ?? false,
1133
- content: content,
1134
- body: {
1135
- progressPercent: isLive ? undefined: content.progressPercentage,
1136
- thumbnail: content.thumbnail,
1137
- title: content.title,
1138
- isLive: isLive,
1139
- badge: content.badge ?? null,
1140
- isLocked: content.is_locked ?? false,
1141
- subtitle: !content.child_count || content.lesson_count === 1
1142
- ? (contentType === 'lesson' && isLive === false) ? `${content.progressPercentage}% Complete`: `${content.difficulty_string} ${content.artist_name}`
1143
- : `${content.completed_children} of ${content.lesson_count ?? content.child_count} Lessons Complete`
1152
+ id: content.id,
1153
+ progressType: 'content',
1154
+ header: contentType,
1155
+ pinned: content.pinned ?? false,
1156
+ content: content,
1157
+ body: {
1158
+ progressPercent: isLive ? undefined : content.progressPercentage,
1159
+ thumbnail: content.thumbnail,
1160
+ title: content.title,
1161
+ isLive: isLive,
1162
+ badge: content.badge ?? null,
1163
+ isLocked: content.is_locked ?? false,
1164
+ subtitle:
1165
+ !content.child_count || content.lesson_count === 1
1166
+ ? contentType === 'lesson' && isLive === false
1167
+ ? `${content.progressPercentage}% Complete`
1168
+ : `${content.difficulty_string} • ${content.artist_name}`
1169
+ : `${content.completed_children} of ${content.lesson_count ?? content.child_count} Lessons Complete`,
1144
1170
  },
1145
- cta: {
1146
- text: ctaText,
1171
+ cta: {
1172
+ text: ctaText,
1147
1173
  timeRemainingToUnlockSeconds: content.time_remaining_seconds ?? null,
1148
1174
  action: {
1149
- type: content.type,
1175
+ type: content.type,
1150
1176
  brand: content.brand,
1151
- id: content.id,
1152
- slug: content.slug,
1177
+ id: content.id,
1178
+ slug: content.slug,
1153
1179
  child: content.navigateTo,
1154
- }
1180
+ },
1155
1181
  },
1156
1182
  // *1000 is to match playlists which are saved in millisecond accuracy
1157
- progressTimestamp: content.progressTimestamp * 1000
1158
- };
1183
+ progressTimestamp: content.progressTimestamp * 1000,
1184
+ }
1159
1185
  }
1160
1186
 
1161
- function getDefaultCTATextForContent(content, contentType)
1162
- {
1163
- let ctaText = 'Continue';
1164
- if (content.progressStatus === 'completed')
1165
- {
1166
- if (contentType === songs[content.brand] || contentType === 'play along' || contentType === 'jam track') ctaText = 'Replay Song';
1167
- if (contentType === 'lesson') ctaText = 'Revisit Lesson';
1168
- if (contentType === 'song tutorial' || collectionLessonTypes.includes(contentType)) ctaText = 'Revisit Lessons' ;
1187
+ function getDefaultCTATextForContent(content, contentType) {
1188
+ let ctaText = 'Continue'
1189
+ if (content.progressStatus === 'completed') {
1190
+ if (
1191
+ contentType === songs[content.brand] ||
1192
+ contentType === 'play along' ||
1193
+ contentType === 'jam track'
1194
+ )
1195
+ ctaText = 'Replay Song'
1196
+ if (contentType === 'lesson') ctaText = 'Revisit Lesson'
1197
+ if (contentType === 'song tutorial' || collectionLessonTypes.includes(contentType))
1198
+ ctaText = 'Revisit Lessons'
1169
1199
  if (contentType === 'pack') ctaText = 'View Lessons'
1170
1200
  }
1171
1201
  return ctaText
1172
1202
  }
1173
1203
 
1174
- async function getCompletedChildren(content, contentType)
1175
- {
1204
+ async function getCompletedChildren(content, contentType) {
1176
1205
  let completedChildren = null
1177
1206
  if (contentType === 'show') {
1178
1207
  const shows = await addContextToContent(fetchShows, content.brand, content.type, {
1179
1208
  addProgressStatus: true,
1180
1209
  })
1181
- completedChildren = Object.values(shows).filter(show => show.progressStatus === 'completed').length;
1210
+ completedChildren = Object.values(shows).filter(
1211
+ (show) => show.progressStatus === 'completed'
1212
+ ).length
1182
1213
  } else if (content.lesson_count > 0) {
1183
- const lessonIds = getLeafNodes(content);
1184
- const progressOnItems = await getProgressStateByIds(lessonIds);
1185
- completedChildren = Object.values(progressOnItems).filter(value => value === 'completed').length;
1214
+ const lessonIds = getLeafNodes(content)
1215
+ const progressOnItems = await getProgressStateByIds(lessonIds)
1216
+ completedChildren = Object.values(progressOnItems).filter(
1217
+ (value) => value === 'completed'
1218
+ ).length
1186
1219
  }
1187
1220
  return completedChildren
1188
1221
  }
1189
1222
 
1190
1223
  async function processPlaylistItem(item) {
1191
- const playlist = item.playlist;
1224
+ const playlist = item.playlist
1192
1225
  return {
1193
- id: playlist.id,
1194
- progressType: 'playlist',
1195
- header: 'playlist',
1196
- pinned: item.pinned ?? false,
1197
- playlist: playlist,
1198
- body: {
1226
+ id: playlist.id,
1227
+ progressType: 'playlist',
1228
+ header: 'playlist',
1229
+ pinned: item.pinned ?? false,
1230
+ playlist: playlist,
1231
+ body: {
1199
1232
  first_items_thumbnail_url: playlist.first_items_thumbnail_url,
1200
1233
  title: playlist.name,
1201
1234
  subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`,
@@ -1205,14 +1238,14 @@ async function processPlaylistItem(item) {
1205
1238
  cta: {
1206
1239
  text: 'Continue',
1207
1240
  action: {
1208
- brand: playlist.brand,
1241
+ brand: playlist.brand,
1209
1242
  item_id: playlist.navigateTo.id ?? null,
1210
1243
  content_id: playlist.navigateTo.content_id ?? null,
1211
- type: 'playlists',
1244
+ type: 'playlists',
1212
1245
  // TODO depreciated, maintained to avoid breaking changes
1213
- id: playlist.id,
1214
- }
1215
- }
1246
+ id: playlist.id,
1247
+ },
1248
+ },
1216
1249
  }
1217
1250
  }
1218
1251
 
@@ -1227,18 +1260,18 @@ const getFormattedType = (type, brand) => {
1227
1260
  }
1228
1261
 
1229
1262
  function getLeafNodes(content) {
1230
- const ids = [];
1263
+ const ids = []
1231
1264
  function traverse(children) {
1232
1265
  for (const item of children) {
1233
1266
  if (item.children) {
1234
- traverse(item.children); // Recursively handle nested lessons
1267
+ traverse(item.children) // Recursively handle nested lessons
1235
1268
  } else if (item.id) {
1236
- ids.push(item.id);
1269
+ ids.push(item.id)
1237
1270
  }
1238
1271
  }
1239
1272
  }
1240
1273
  if (content && Array.isArray(content.children)) {
1241
- traverse(content.children);
1274
+ traverse(content.children)
1242
1275
  }
1243
1276
  return ids
1244
1277
  }
@@ -1255,7 +1288,7 @@ async function getEligiblePlaylistItems(playlists) {
1255
1288
  progressTimestamp: timestamp / 1000,
1256
1289
  playlist: p,
1257
1290
  id: p.id,
1258
- };
1291
+ }
1259
1292
  })
1260
1293
  )
1261
1294
  }
@@ -1265,7 +1298,7 @@ function mergeAndSortItems(items, limit) {
1265
1298
  const deduped = []
1266
1299
 
1267
1300
  for (const item of items) {
1268
- const key = `${item.id}-${item.type}`;
1301
+ const key = `${item.id}-${item.type}`
1269
1302
  if (!seen.has(key)) {
1270
1303
  seen.add(key)
1271
1304
  deduped.push(item)
@@ -1273,12 +1306,12 @@ function mergeAndSortItems(items, limit) {
1273
1306
  }
1274
1307
 
1275
1308
  return deduped
1276
- .filter(item => typeof item.progressTimestamp === 'number' && item.progressTimestamp >= 0)
1309
+ .filter((item) => typeof item.progressTimestamp === 'number' && item.progressTimestamp >= 0)
1277
1310
  .sort((a, b) => {
1278
- if (a.pinned && !b.pinned) return -1;
1279
- if (!a.pinned && b.pinned) return 1;
1311
+ if (a.pinned && !b.pinned) return -1
1312
+ if (!a.pinned && b.pinned) return 1
1280
1313
  // TODO pinned guided course should always be before user pinned item
1281
- return b.progressTimestamp - a.progressTimestamp;
1314
+ return b.progressTimestamp - a.progressTimestamp
1282
1315
  })
1283
1316
  .slice(0, limit + 5)
1284
1317
  }
@@ -1306,39 +1339,36 @@ export function findIncompleteLesson(progressOnItems, currentContentId, contentT
1306
1339
 
1307
1340
  async function popPinnedItemFromContentsOrPlaylistMap(pinned, contentsMap, playlistItems) {
1308
1341
  if (!pinned) return null
1309
- const {id, progressType, pinnedAt} = pinned
1342
+ const { id, progressType, pinnedAt } = pinned
1310
1343
  let item = null
1311
1344
  if (progressType === 'content') {
1312
1345
  const pinnedId = parseInt(id)
1313
1346
  if (contentsMap.has(pinnedId)) {
1314
1347
  item = contentsMap.get(pinnedId)
1315
1348
  contentsMap.delete(pinnedId)
1316
-
1317
1349
  } else {
1318
1350
  // we use fetchByRailContentIds so that we don't have the _type restriction in the query
1319
1351
  let data = await fetchByRailContentIds([id], 'progress-tracker')
1320
- item = await addContextToContent(() => data[0] ?? null,
1321
- {
1322
- addNextLesson: true,
1323
- addNavigateTo: true,
1324
- addProgressStatus: true,
1325
- addProgressPercentage: true,
1326
- addProgressTimestamp: true
1327
- }
1328
- )
1352
+ item = await addContextToContent(() => data[0] ?? null, {
1353
+ addNextLesson: true,
1354
+ addNavigateTo: true,
1355
+ addProgressStatus: true,
1356
+ addProgressPercentage: true,
1357
+ addProgressTimestamp: true,
1358
+ })
1329
1359
  }
1330
1360
  }
1331
1361
  if (progressType === 'playlist') {
1332
- const pinnedPlaylist = playlistItems.find(p => p.playlist.id === id)
1362
+ const pinnedPlaylist = playlistItems.find((p) => p.playlist.id === id)
1333
1363
  if (pinnedPlaylist) {
1334
- playlistItems = playlistItems.filter(p => p.playlist.id !== id)
1364
+ playlistItems = playlistItems.filter((p) => p.playlist.id !== id)
1335
1365
  item = pinnedPlaylist
1336
1366
  } else {
1337
1367
  const playlist = await fetchPlaylist(id)
1338
1368
  item = {
1339
- id: id,
1340
- playlist: playlist,
1341
- type: 'playlist',
1369
+ id: id,
1370
+ playlist: playlist,
1371
+ type: 'playlist',
1342
1372
  progressTimestamp: new Date(pinnedAt).getTime(),
1343
1373
  }
1344
1374
  }
@@ -1347,11 +1377,11 @@ async function popPinnedItemFromContentsOrPlaylistMap(pinned, contentsMap, playl
1347
1377
  }
1348
1378
 
1349
1379
  function popContentAndRemoveChildrenFromContentsMap(content, contentsMap) {
1350
- const children = content.children.map(child => child.id)
1380
+ const children = content.children.map((child) => child.id)
1351
1381
  if (contentsMap.has(content.id)) {
1352
1382
  contentsMap.delete(content.id)
1353
1383
  }
1354
- children.forEach(child => {
1384
+ children.forEach((child) => {
1355
1385
  if (contentsMap.has(child)) {
1356
1386
  contentsMap.delete(child)
1357
1387
  }