musora-content-services 1.6.5 → 1.6.6

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 (84) hide show
  1. package/.github/workflows/conventional-commits.yaml +16 -0
  2. package/.github/workflows/docs.js.yml +58 -0
  3. package/CHANGELOG.md +2 -0
  4. package/README.md +1 -2
  5. package/docs/v2/Content-Organization.html +245 -0
  6. package/docs/v2/ContentOrganization.html +245 -0
  7. package/docs/v2/Gamification.html +245 -0
  8. package/docs/v2/UserManagement.html +269 -0
  9. package/docs/v2/UserManagementSystem.html +317 -0
  10. package/docs/v2/api_types.js.html +97 -0
  11. package/docs/v2/config.js.html +143 -0
  12. package/docs/v2/content-org_content-org.js.html +76 -0
  13. package/docs/v2/content-org_playlists-types.js.html +116 -0
  14. package/docs/v2/content-org_playlists.js.html +418 -0
  15. package/docs/v2/content.js.html +466 -0
  16. package/docs/v2/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  17. package/docs/v2/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  18. package/docs/v2/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  19. package/docs/v2/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  20. package/docs/v2/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  21. package/docs/v2/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  22. package/docs/v2/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  23. package/docs/v2/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  24. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  25. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  26. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  27. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  28. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  29. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  30. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  31. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  32. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  33. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  34. package/docs/v2/gamification_awards.js.html +664 -0
  35. package/docs/v2/gamification_gamification.js.html +76 -0
  36. package/docs/v2/gamification_types.js.html +98 -0
  37. package/docs/v2/global.html +5812 -0
  38. package/docs/v2/global.html#User +293 -0
  39. package/docs/v2/index.html +168 -0
  40. package/docs/v2/module-Awards.html +354 -0
  41. package/docs/v2/module-Config.html +434 -0
  42. package/docs/v2/module-Content-Services-V2.html +2434 -0
  43. package/docs/v2/module-Interests.html +1066 -0
  44. package/docs/v2/module-Notifications.html +1183 -0
  45. package/docs/v2/module-Permissions.html +406 -0
  46. package/docs/v2/module-Playlists.html +2862 -0
  47. package/docs/v2/module-Railcontent-Services.html +7954 -0
  48. package/docs/v2/module-Sanity-Services.html +9608 -0
  49. package/docs/v2/module-Session-Management.html +575 -0
  50. package/docs/v2/module-Sessions.html +575 -0
  51. package/docs/v2/module-User-Activity.html +4410 -0
  52. package/docs/v2/module-User-Management.html +490 -0
  53. package/docs/v2/module-User-Permissions.html +406 -0
  54. package/docs/v2/module-UserActivity.html +4410 -0
  55. package/docs/v2/module-UserManagement.html +915 -0
  56. package/docs/v2/module-UserNotifications.html +1223 -0
  57. package/docs/v2/module-UserProfile.html +266 -0
  58. package/docs/v2/railcontent.js.html +984 -0
  59. package/docs/v2/sanity.js.html +2459 -0
  60. package/docs/v2/scripts/collapse.js +39 -0
  61. package/docs/v2/scripts/commonNav.js +28 -0
  62. package/docs/v2/scripts/linenumber.js +25 -0
  63. package/docs/v2/scripts/nav.js +12 -0
  64. package/docs/v2/scripts/polyfill.js +4 -0
  65. package/docs/v2/scripts/prettify/Apache-License-2.0.txt +202 -0
  66. package/docs/v2/scripts/prettify/lang-css.js +2 -0
  67. package/docs/v2/scripts/prettify/prettify.js +28 -0
  68. package/docs/v2/scripts/search.js +99 -0
  69. package/docs/v2/styles/jsdoc.css +776 -0
  70. package/docs/v2/styles/prettify.css +80 -0
  71. package/docs/v2/types.js.html +122 -0
  72. package/docs/v2/userActivity.js.html +1451 -0
  73. package/docs/v2/user_interests.js.html +150 -0
  74. package/docs/v2/user_management.js.html +178 -0
  75. package/docs/v2/user_notifications.js.html +192 -0
  76. package/docs/v2/user_permissions.js.html +110 -0
  77. package/docs/v2/user_profile.js.html +105 -0
  78. package/docs/v2/user_sessions.js.html +139 -0
  79. package/docs/v2/user_types.js.html +208 -0
  80. package/docs/v2/user_user-management-system.js.html +79 -0
  81. package/docs/v2/user_user-management.js.html +78 -0
  82. package/package.json +1 -1
  83. package/src/contentTypeConfig.js +27 -12
  84. package/src/services/sanity.js +120 -81
@@ -0,0 +1,1451 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>userActivity.js - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-Awards.html">Awards</a><ul class='methods'><li data-type='method'><a href="module-Awards.html#.fetchAwardsForUser">fetchAwardsForUser</a></li></ul></li><li><a href="module-Config.html">Config</a><ul class='methods'><li data-type='method'><a href="module-Config.html#.initializeService">initializeService</a></li></ul></li><li><a href="module-Content-Services-V2.html">Content-Services-V2</a><ul class='methods'><li data-type='method'><a href="module-Content-Services-V2.html#.getContentRows">getContentRows</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getNewAndUpcoming">getNewAndUpcoming</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getRecent">getRecent</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getRecommendedForYou">getRecommendedForYou</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getScheduleContentRows">getScheduleContentRows</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getTabResults">getTabResults</a></li></ul></li><li><a href="module-Interests.html">Interests</a><ul class='methods'><li data-type='method'><a href="module-Interests.html#.fetchInterests">fetchInterests</a></li><li data-type='method'><a href="module-Interests.html#.fetchUninterests">fetchUninterests</a></li><li data-type='method'><a href="module-Interests.html#.markContentAsInterested">markContentAsInterested</a></li><li data-type='method'><a href="module-Interests.html#.markContentAsNotInterested">markContentAsNotInterested</a></li><li data-type='method'><a href="module-Interests.html#.removeContentAsInterested">removeContentAsInterested</a></li><li data-type='method'><a href="module-Interests.html#.removeContentAsNotInterested">removeContentAsNotInterested</a></li></ul></li><li><a href="module-Permissions.html">Permissions</a><ul class='methods'><li data-type='method'><a href="module-Permissions.html#.fetchUserPermissions">fetchUserPermissions</a></li><li data-type='method'><a href="module-Permissions.html#.reset">reset</a></li></ul></li><li><a href="module-Playlists.html">Playlists</a><ul class='methods'><li data-type='method'><a href="module-Playlists.html#.addItemToPlaylist">addItemToPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.createPlaylist">createPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.deletePlaylist">deletePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.duplicatePlaylist">duplicatePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.fetchPlaylist">fetchPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.fetchPlaylistItems">fetchPlaylistItems</a></li><li data-type='method'><a href="module-Playlists.html#.fetchUserPlaylists">fetchUserPlaylists</a></li><li data-type='method'><a href="module-Playlists.html#.togglePlaylistPrivate">togglePlaylistPrivate</a></li><li data-type='method'><a href="module-Playlists.html#.undeletePlaylist">undeletePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.updatePlaylist">updatePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~likePlaylist">likePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~reportPlaylist">reportPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~togglePlaylistPrivate">togglePlaylistPrivate</a></li><li data-type='method'><a href="module-Playlists.html#~unlikePlaylist">unlikePlaylist</a></li></ul></li><li><a href="module-Railcontent-Services.html">Railcontent-Services</a><ul class='methods'><li data-type='method'><a href="module-Railcontent-Services.html#.assignModeratorToComment">assignModeratorToComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.closeComment">closeComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.createComment">createComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.deleteComment">deleteComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.editComment">editComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchAllCompletedStates">fetchAllCompletedStates</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCarouselCardData">fetchCarouselCardData</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchChallengeIndexMetadata">fetchChallengeIndexMetadata</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchChallengeLessonData">fetchChallengeLessonData</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchChallengeMetadata">fetchChallengeMetadata</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchChallengeUserActiveChallenges">fetchChallengeUserActiveChallenges</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCommentRelies">fetchCommentRelies</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchComments">fetchComments</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCompletedChallenges">fetchCompletedChallenges</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCompletedContent">fetchCompletedContent</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCompletedState">fetchCompletedState</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchContentInProgress">fetchContentInProgress</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchContentPageUserData">fetchContentPageUserData</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchNextContentDataForParent">fetchNextContentDataForParent</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchOwnedChallenges">fetchOwnedChallenges</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchRecentUserActivities">fetchRecentUserActivities</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchSongsInProgress">fetchSongsInProgress</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchTopComment">fetchTopComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchUserAward">fetchUserAward</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchUserBadges">fetchUserBadges</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchUserChallengeProgress">fetchUserChallengeProgress</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchUserPracticeNotes">fetchUserPracticeNotes</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.likeComment">likeComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.openComment">openComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesCommunityNotification">postChallengesCommunityNotification</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesEnroll">postChallengesEnroll</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesEnrollmentNotification">postChallengesEnrollmentNotification</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesHideCompletedBanner">postChallengesHideCompletedBanner</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesLeave">postChallengesLeave</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesSetStartDate">postChallengesSetStartDate</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesSoloNotification">postChallengesSoloNotification</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesUnlock">postChallengesUnlock</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.replyToComment">replyToComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.reportComment">reportComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.setStudentViewForUser">setStudentViewForUser</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.unassignModeratorToComment">unassignModeratorToComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.unlikeComment">unlikeComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#~fetchLastInteractedChild">fetchLastInteractedChild</a></li></ul></li><li><a href="module-Sanity-Services.html">Sanity-Services</a><ul class='methods'><li data-type='method'><a href="module-Sanity-Services.html#.fetchAll">fetchAll</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchAllFilterOptions">fetchAllFilterOptions</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchAllPacks">fetchAllPacks</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchArtistLessons">fetchArtistLessons</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchArtists">fetchArtists</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchByRailContentId">fetchByRailContentId</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchByRailContentIds">fetchByRailContentIds</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchByReference">fetchByReference</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchCoachLessons">fetchCoachLessons</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchComingSoon">fetchComingSoon</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchCommentModContentData">fetchCommentModContentData</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchFoundation">fetchFoundation</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchGenreLessons">fetchGenreLessons</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchLeaving">fetchLeaving</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchLessonContent">fetchLessonContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchLessonsFeaturingThisContent">fetchLessonsFeaturingThisContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMetadata">fetchMetadata</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethod">fetchMethod</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodChildren">fetchMethodChildren</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodChildrenIds">fetchMethodChildrenIds</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodPreviousNextLesson">fetchMethodPreviousNextLesson</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchNewReleases">fetchNewReleases</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchNextPreviousLesson">fetchNextPreviousLesson</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchOtherSongVersions">fetchOtherSongVersions</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchPackAll">fetchPackAll</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchPackData">fetchPackData</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchParentForDownload">fetchParentForDownload</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchRelatedLessons">fetchRelatedLessons</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchRelatedRecommendedContent">fetchRelatedRecommendedContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchRelatedSongs">fetchRelatedSongs</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchReturning">fetchReturning</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchSanity">fetchSanity</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchScheduledReleases">fetchScheduledReleases</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchShowsData">fetchShowsData</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchSongArtistCount">fetchSongArtistCount</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchSongById">fetchSongById</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchUpcomingEvents">fetchUpcomingEvents</a></li><li data-type='method'><a href="module-Sanity-Services.html#.jumpToContinueContent">jumpToContinueContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#~fetchRelatedByLicense">fetchRelatedByLicense</a></li><li data-type='method'><a href="module-Sanity-Services.html#~getNextAndPreviousQuarterDates">getNextAndPreviousQuarterDates</a></li><li data-type='method'><a href="module-Sanity-Services.html#~getQueryFromPage">getQueryFromPage</a></li><li data-type='method'><a href="module-Sanity-Services.html#~handleCustomFetchAll">handleCustomFetchAll</a></li></ul></li><li><a href="module-Sessions.html">Sessions</a><ul class='methods'><li data-type='method'><a href="module-Sessions.html#.login">login</a></li><li data-type='method'><a href="module-Sessions.html#.logout">logout</a></li></ul></li><li><a href="module-UserActivity.html">UserActivity</a><ul class='methods'><li data-type='method'><a href="module-UserActivity.html#.calculateLongestStreaks">calculateLongestStreaks</a></li><li data-type='method'><a href="module-UserActivity.html#.createPracticeNotes">createPracticeNotes</a></li><li data-type='method'><a href="module-UserActivity.html#.deletePracticeSession">deletePracticeSession</a></li><li data-type='method'><a href="module-UserActivity.html#.deleteUserActivity">deleteUserActivity</a></li><li data-type='method'><a href="module-UserActivity.html#.getPracticeNotes">getPracticeNotes</a></li><li data-type='method'><a href="module-UserActivity.html#.getPracticeSessions">getPracticeSessions</a></li><li data-type='method'><a href="module-UserActivity.html#.getProgressRows">getProgressRows</a></li><li data-type='method'><a href="module-UserActivity.html#.getRecentActivity">getRecentActivity</a></li><li data-type='method'><a href="module-UserActivity.html#.getUserMonthlyStats">getUserMonthlyStats</a></li><li data-type='method'><a href="module-UserActivity.html#.getUserWeeklyStats">getUserWeeklyStats</a></li><li data-type='method'><a href="module-UserActivity.html#.pinProgressRow">pinProgressRow</a></li><li data-type='method'><a href="module-UserActivity.html#.recordUserActivity">recordUserActivity</a></li><li data-type='method'><a href="module-UserActivity.html#.recordUserPractice">recordUserPractice</a></li><li data-type='method'><a href="module-UserActivity.html#.removeUserPractice">removeUserPractice</a></li><li data-type='method'><a href="module-UserActivity.html#.restorePracticeSession">restorePracticeSession</a></li><li data-type='method'><a href="module-UserActivity.html#.restoreUserPractice">restoreUserPractice</a></li><li data-type='method'><a href="module-UserActivity.html#.unpinProgressRow">unpinProgressRow</a></li><li data-type='method'><a href="module-UserActivity.html#.updatePracticeNotes">updatePracticeNotes</a></li><li data-type='method'><a href="module-UserActivity.html#.updateUserPractice">updateUserPractice</a></li></ul></li><li><a href="module-UserChat.html">UserChat</a><ul class='methods'><li data-type='method'><a href="module-UserChat.html#.fetchChatSettings">fetchChatSettings</a></li></ul></li><li><a href="module-UserManagement.html">UserManagement</a><ul class='methods'><li data-type='method'><a href="module-UserManagement.html#.blockUser">blockUser</a></li><li data-type='method'><a href="module-UserManagement.html#.deletePicture">deletePicture</a></li><li data-type='method'><a href="module-UserManagement.html#.unblockUser">unblockUser</a></li><li data-type='method'><a href="module-UserManagement.html#.uploadPicture">uploadPicture</a></li><li data-type='method'><a href="module-UserManagement.html#.uploadPictureFromS3">uploadPictureFromS3</a></li></ul></li><li><a href="module-UserNotifications.html">UserNotifications</a><ul class='methods'><li data-type='method'><a href="module-UserNotifications.html#.deleteNotification">deleteNotification</a></li><li data-type='method'><a href="module-UserNotifications.html#.fetchNotifications">fetchNotifications</a></li><li data-type='method'><a href="module-UserNotifications.html#.markAllNotificationsAsRead">markAllNotificationsAsRead</a></li><li data-type='method'><a href="module-UserNotifications.html#.markNotificationAsRead">markNotificationAsRead</a></li><li data-type='method'><a href="module-UserNotifications.html#.markNotificationAsUnread">markNotificationAsUnread</a></li></ul></li><li><a href="module-UserProfile.html">UserProfile</a><ul class='methods'><li data-type='method'><a href="module-UserProfile.html#.otherStats">otherStats</a></li></ul></li></ul><h3>Namespaces</h3><ul><li><a href="ContentOrganization.html">ContentOrganization</a></li><li><a href="Gamification.html">Gamification</a></li><li><a href="UserManagementSystem.html">UserManagementSystem</a></li></ul><h3><a href="global.html">Global</a></h3>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">userActivity.js</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>/**
49
+ * @module UserActivity
50
+ */
51
+
52
+ import {
53
+ fetchUserPractices,
54
+ logUserPractice,
55
+ fetchUserPracticeMeta,
56
+ fetchUserPracticeNotes,
57
+ fetchHandler,
58
+ fetchRecentUserActivities, fetchChallengeLessonData
59
+ } from './railcontent'
60
+ import { DataContext, UserActivityVersionKey } from './dataContext.js'
61
+ import { fetchByRailContentIds, fetchShows } from './sanity'
62
+ import {fetchPlaylist, fetchUserPlaylists} from "./content-org/playlists";
63
+ import {convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay, getTimeRemainingUntilLocal} from './dateUtils.js'
64
+ import { globalConfig } from './config'
65
+ import {collectionLessonTypes, lessonTypesMapping, progressTypesMapping, showsLessonTypes, songs} from "../contentTypeConfig";
66
+ import {getAllStartedOrCompleted, getProgressStateByIds} from "./contentProgress";
67
+ import {TabResponseType} from "../contentMetaData";
68
+
69
+ const DATA_KEY_PRACTICES = 'practices'
70
+ const DATA_KEY_LAST_UPDATED_TIME = 'u'
71
+
72
+ const DAYS = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
73
+
74
+ const streakMessages = {
75
+ startStreak: 'Start your streak by taking any lesson!',
76
+ restartStreak: 'Restart your streak by taking any lesson!',
77
+
78
+ // Messages when last active day is today
79
+ dailyStreak: (streak) =>
80
+ `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak! Way to keep it going!`,
81
+ dailyStreakShort: (streak) =>
82
+ `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak!`,
83
+ weeklyStreak: (streak) =>
84
+ `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep up the momentum!`,
85
+ greatJobWeeklyStreak: (streak) =>
86
+ `Great job! You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep it going!`,
87
+
88
+ // Messages when last active day is NOT today
89
+ dailyStreakReminder: (streak) =>
90
+ `You have ${getIndefiniteArticle(streak)} ${streak} day streak! Keep it going with any lesson or song!`,
91
+ weeklyStreakKeepUp: (streak) =>
92
+ `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep up the momentum!`,
93
+ weeklyStreakReminder: (streak) =>
94
+ `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep it going with any lesson or song!`,
95
+ }
96
+
97
+ function getIndefiniteArticle(streak) {
98
+ return streak === 8 || (streak >= 80 &amp;&amp; streak &lt;= 89) || (streak >= 800 &amp;&amp; streak &lt;= 899)
99
+ ? 'an'
100
+ : 'a'
101
+ }
102
+
103
+ export async function getUserPractices(userId = globalConfig.sessionConfig.userId) {
104
+ if (userId !== globalConfig.sessionConfig.userId) {
105
+ let data = await fetchUserPractices({ userId })
106
+ return data?.['data']?.[DATA_KEY_PRACTICES] ?? {}
107
+ } else {
108
+ let data = await userActivityContext.getData()
109
+ return data?.[DATA_KEY_PRACTICES] ?? {}
110
+ }
111
+ }
112
+
113
+ export let userActivityContext = new DataContext(UserActivityVersionKey, fetchUserPractices)
114
+
115
+ /**
116
+ * Retrieves user activity statistics for the current week, including daily activity and streak messages.
117
+ *
118
+ * @returns {Promise&lt;Object>} - A promise that resolves to an object containing weekly user activity statistics.
119
+ *
120
+ * @example
121
+ * // Retrieve user activity statistics for the current week
122
+ * getUserWeeklyStats()
123
+ * .then(stats => console.log(stats))
124
+ * .catch(error => console.error(error));
125
+ */
126
+ export async function getUserWeeklyStats() {
127
+ let data = await userActivityContext.getData()
128
+ let practices = data?.[DATA_KEY_PRACTICES] ?? {}
129
+ let sortedPracticeDays = Object.keys(practices)
130
+ .map((date) => new Date(date))
131
+ .sort((a, b) => b - a)
132
+
133
+ let today = new Date()
134
+ today.setHours(0, 0, 0, 0)
135
+ let startOfWeek = getMonday(today) // Get last Monday
136
+ let dailyStats = []
137
+
138
+ for (let i = 0; i &lt; 7; i++) {
139
+ let day = new Date(startOfWeek)
140
+ day.setDate(startOfWeek.getDate() + i)
141
+ let hasPractice = sortedPracticeDays.some((practiceDate) => isSameDate(practiceDate, day))
142
+ let isActive = isSameDate(today, day)
143
+ let type = hasPractice ? 'tracked' : isActive ? 'active' : 'none'
144
+ dailyStats.push({ key: i, label: DAYS[i], isActive, inStreak: hasPractice, type })
145
+ }
146
+
147
+ let { streakMessage } = getStreaksAndMessage(practices)
148
+
149
+ return { data: { dailyActiveStats: dailyStats, streakMessage, practices } }
150
+ }
151
+
152
+ /**
153
+ * Retrieves user activity statistics for a specified month and user, including daily and weekly activity data.
154
+ * If no parameters are provided, defaults to the current year, current month, and the logged-in user.
155
+ *
156
+ * @param {Object} [params={}] - Parameters for fetching user statistics.
157
+ * @param {number} [params.year=new Date().getFullYear()] - The year for which to retrieve the statistics.
158
+ * @param {number} [params.month=new Date().getMonth()] - The 0-based month index (0 = January).
159
+ * @param {number} [params.day=1] - The starting day (not used for grid calc, but kept for flexibility).
160
+ * @param {number} [params.userId=globalConfig.sessionConfig.userId] - The user ID for whom to retrieve stats.
161
+ *
162
+ * @returns {Promise&lt;Object>} A promise that resolves to an object containing:
163
+ * - `dailyActiveStats`: Array of daily activity data for the calendar grid.
164
+ * - `weeklyActiveStats`: Array of weekly streak summaries.
165
+ * - `practiceDuration`: Total number of seconds practiced in the month.
166
+ * - `currentDailyStreak`: Count of consecutive active days.
167
+ * - `currentWeeklyStreak`: Count of consecutive active weeks.
168
+ * - `daysPracticed`: Total number of active days in the month.
169
+ *
170
+ * @example
171
+ * // Get stats for current user and month
172
+ * getUserMonthlyStats().then(console.log);
173
+ *
174
+ * @example
175
+ * // Get stats for March 2024
176
+ * getUserMonthlyStats({ year: 2024, month: 2 }).then(console.log);
177
+ *
178
+ * @example
179
+ * // Get stats for another user
180
+ * getUserMonthlyStats({ userId: 123 }).then(console.log);
181
+ */
182
+ export async function getUserMonthlyStats(params = {}) {
183
+ const now = new Date()
184
+ const {
185
+ year = now.getFullYear(),
186
+ month = now.getMonth(),
187
+ day = 1,
188
+ userId = globalConfig.sessionConfig.userId,
189
+ } = params
190
+ let practices = await getUserPractices(userId)
191
+
192
+ // Get the first day of the specified month and the number of days in that month
193
+ let firstDayOfMonth = new Date(year, month, 1)
194
+ let today = new Date()
195
+ today.setHours(0, 0, 0, 0)
196
+
197
+ let startOfGrid = getMonday(firstDayOfMonth)
198
+
199
+ let previousWeekStart = new Date(startOfGrid)
200
+ previousWeekStart.setDate(previousWeekStart.getDate() - 7)
201
+
202
+ let previousWeekEnd = new Date(startOfGrid)
203
+ previousWeekEnd.setDate(previousWeekEnd.getDate() - 1)
204
+
205
+ let hadStreakBeforeMonth = false
206
+ for (let d = new Date(previousWeekStart); d &lt;= previousWeekEnd; d.setDate(d.getDate() + 1)) {
207
+ let dayKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
208
+ if (practices[dayKey]) {
209
+ hadStreakBeforeMonth = true
210
+ break
211
+ }
212
+ }
213
+
214
+ let endOfMonth = new Date(year, month + 1, 0)
215
+ while (endOfMonth.getDay() !== 0) {
216
+ endOfMonth.setDate(endOfMonth.getDate() + 1)
217
+ }
218
+
219
+ let daysInMonth = Math.ceil((endOfMonth - startOfGrid) / (1000 * 60 * 60 * 24)) + 1
220
+
221
+ let dailyStats = []
222
+ let practiceDuration = 0
223
+ let daysPracticed = 0
224
+ let weeklyStats = {}
225
+
226
+ for (let i = 0; i &lt; daysInMonth; i++) {
227
+ let day = new Date(startOfGrid)
228
+ day.setDate(startOfGrid.getDate() + i)
229
+ let dayKey = `${day.getFullYear()}-${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')}`
230
+
231
+ // Check if the user has activity for the day
232
+ let dayActivity = practices[dayKey] ?? null
233
+ let weekKey = getWeekNumber(day)
234
+
235
+ if (!weeklyStats[weekKey]) {
236
+ weeklyStats[weekKey] = { key: weekKey, inStreak: false }
237
+ }
238
+
239
+ if (dayActivity !== null) {
240
+ practiceDuration += dayActivity.reduce((sum, entry) => sum + entry.duration_seconds, 0)
241
+ daysPracticed++
242
+ }
243
+
244
+ let isActive = isSameDate(today, day)
245
+ let type = dayActivity !== null ? 'tracked' : isActive ? 'active' : 'none'
246
+ let isInStreak = dayActivity !== null
247
+ if (isInStreak) {
248
+ weeklyStats[weekKey].inStreak = true
249
+ }
250
+
251
+ dailyStats.push({
252
+ key: i,
253
+ label: dayKey,
254
+ isActive,
255
+ inStreak: dayActivity !== null,
256
+ type,
257
+ })
258
+ }
259
+
260
+ if (hadStreakBeforeMonth) {
261
+ const firstWeekKey = getWeekNumber(startOfGrid)
262
+ if (weeklyStats[firstWeekKey]) {
263
+ weeklyStats[firstWeekKey].continueStreak = true
264
+ }
265
+ }
266
+
267
+ let filteredPractices = Object.keys(practices)
268
+ .filter((date) => new Date(date) &lt;= endOfMonth)
269
+ .reduce((obj, key) => {
270
+ obj[key] = practices[key]
271
+ return obj
272
+ }, {})
273
+
274
+ let { currentDailyStreak, currentWeeklyStreak } = calculateStreaks(filteredPractices)
275
+
276
+ return {
277
+ data: {
278
+ dailyActiveStats: dailyStats,
279
+ weeklyActiveStats: Object.values(weeklyStats),
280
+ practiceDuration,
281
+ currentDailyStreak,
282
+ currentWeeklyStreak,
283
+ daysPracticed,
284
+ },
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Records user practice data and updates both the remote and local activity context.
290
+ *
291
+ * @param {Object} practiceDetails - The details of the practice session.
292
+ * @param {number} practiceDetails.duration_seconds - The duration of the practice session in seconds.
293
+ * @param {boolean} [practiceDetails.auto=true] - Whether the session was automatically logged.
294
+ * @param {number} [practiceDetails.content_id] - The ID of the practiced content (if available).
295
+ * @param {number} [practiceDetails.category_id] - The ID of the associated category (if available).
296
+ * @param {string} [practiceDetails.title] - The title of the practice session (max 64 characters).
297
+ * @param {string} [practiceDetails.thumbnail_url] - The URL of the session's thumbnail (max 255 characters).
298
+ * @returns {Promise&lt;Object>} - A promise that resolves to the response from logging the user practice.
299
+ *
300
+ * @example
301
+ * // Record an auto practice session with content ID
302
+ * recordUserPractice({ content_id: 123, duration_seconds: 300 })
303
+ * .then(response => console.log(response))
304
+ * .catch(error => console.error(error));
305
+ *
306
+ * @example
307
+ * // Record a custom practice session with additional details
308
+ * recordUserPractice({
309
+ * duration_seconds: 600,
310
+ * auto: false,
311
+ * category_id: 5,
312
+ * title: "Guitar Warm-up",
313
+ * thumbnail_url: "https://example.com/thumbnail.jpg"
314
+ * })
315
+ * .then(response => console.log(response))
316
+ * .catch(error => console.error(error));
317
+ */
318
+ export async function recordUserPractice(practiceDetails) {
319
+ practiceDetails.auto = 0
320
+ if (practiceDetails.content_id) {
321
+ practiceDetails.auto = 1
322
+ }
323
+
324
+ await userActivityContext.update(
325
+ async function (localContext) {
326
+ let userData = localContext.data ?? { [DATA_KEY_PRACTICES]: {} }
327
+ localContext.data = userData
328
+ },
329
+ async function () {
330
+ const response = await logUserPractice(practiceDetails)
331
+ if (response) {
332
+ await userActivityContext.updateLocal(async function (localContext) {
333
+ const newPractices = response.data ?? []
334
+ newPractices.forEach((newPractice) => {
335
+ const { date } = newPractice
336
+ if (!localContext.data[DATA_KEY_PRACTICES][date]) {
337
+ localContext.data[DATA_KEY_PRACTICES][date] = []
338
+ }
339
+ localContext.data[DATA_KEY_PRACTICES][date][DATA_KEY_LAST_UPDATED_TIME] = Math.round(
340
+ new Date().getTime() / 1000
341
+ )
342
+ localContext.data[DATA_KEY_PRACTICES][date].push({
343
+ id: newPractice.id,
344
+ duration_seconds: newPractice.duration_seconds, // Add the new practice for this date
345
+ })
346
+ })
347
+ })
348
+ }
349
+ return response
350
+ }
351
+ )
352
+ }
353
+ /**
354
+ * Updates a user's practice session with new details and syncs the changes remotely.
355
+ *
356
+ * @param {number} id - The unique identifier of the practice session to update.
357
+ * @param {Object} practiceDetails - The updated details of the practice session.
358
+ * @param {number} [practiceDetails.duration_seconds] - The duration of the practice session in seconds.
359
+ * @param {number} [practiceDetails.category_id] - The ID of the associated category (if available).
360
+ * @param {string} [practiceDetails.title] - The title of the practice session (max 64 characters).
361
+ * @param {string} [practiceDetails.thumbnail_url] - The URL of the session's thumbnail (max 255 characters).
362
+ * @returns {Promise&lt;Object>} - A promise that resolves to the response from updating the user practice.
363
+ *
364
+ * @example
365
+ * // Update a practice session's duration
366
+ * updateUserPractice(123, { duration_seconds: 600 })
367
+ * .then(response => console.log(response))
368
+ * .catch(error => console.error(error));
369
+ *
370
+ * @example
371
+ * // Change a practice session to manual and update its category
372
+ * updateUserPractice(456, { auto: false, category_id: 8 })
373
+ * .then(response => console.log(response))
374
+ * .catch(error => console.error(error));
375
+ */
376
+ export async function updateUserPractice(id, practiceDetails) {
377
+ const url = `/api/user/practices/v1/practices/${id}`
378
+ return await fetchHandler(url, 'PUT', null, practiceDetails)
379
+ }
380
+
381
+ /**
382
+ * Removes a user's practice session by ID, updating both the local and remote activity context.
383
+ *
384
+ * @param {number} id - The unique identifier of the practice session to be removed.
385
+ * @returns {Promise&lt;void>} - A promise that resolves once the practice session is removed.
386
+ *
387
+ * @example
388
+ * // Remove a practice session with ID 123
389
+ * removeUserPractice(123)
390
+ * .then(() => console.log("Practice session removed successfully"))
391
+ * .catch(error => console.error(error));
392
+ */
393
+ export async function removeUserPractice(id) {
394
+ let url = `/api/user/practices/v1/practices${buildQueryString([id])}`
395
+ await userActivityContext.update(
396
+ async function (localContext) {
397
+ if (localContext.data?.[DATA_KEY_PRACTICES]) {
398
+ Object.keys(localContext.data[DATA_KEY_PRACTICES]).forEach((date) => {
399
+ localContext.data[DATA_KEY_PRACTICES][date] = localContext.data[DATA_KEY_PRACTICES][
400
+ date
401
+ ].filter((practice) => practice.id !== id)
402
+ })
403
+ }
404
+ },
405
+ async function () {
406
+ return await fetchHandler(url, 'delete')
407
+ }
408
+ )
409
+ }
410
+
411
+ /**
412
+ * Restores a previously deleted user's practice session by ID, updating both the local and remote activity context.
413
+ *
414
+ * @param {number} id - The unique identifier of the practice session to be restored.
415
+ * @returns {Promise&lt;Object>} - A promise that resolves to the response containing the restored practice session data.
416
+ *
417
+ * @example
418
+ * // Restore a deleted practice session with ID 123
419
+ * restoreUserPractice(123)
420
+ * .then(response => console.log("Practice session restored:", response))
421
+ * .catch(error => console.error(error));
422
+ */
423
+ export async function restoreUserPractice(id) {
424
+ let url = `/api/user/practices/v1/practices/restore${buildQueryString([id])}`
425
+ const response = await fetchHandler(url, 'put')
426
+ if (response?.data) {
427
+ await userActivityContext.updateLocal(async function (localContext) {
428
+ const restoredPractice = response.data
429
+ const { date } = restoredPractice
430
+ if (!localContext.data[DATA_KEY_PRACTICES][date]) {
431
+ localContext.data[DATA_KEY_PRACTICES][date] = []
432
+ }
433
+ localContext.data[DATA_KEY_PRACTICES][date].push({
434
+ id: restoredPractice.id,
435
+ duration_seconds: restoredPractice.duration_seconds,
436
+ })
437
+ })
438
+ }
439
+ const formattedMeta = await formatPracticeMeta(response.data)
440
+ const practiceDuration = formattedMeta.reduce(
441
+ (total, practice) => total + (practice.duration || 0),
442
+ 0
443
+ )
444
+ return {
445
+ data: formattedMeta,
446
+ message: response.message,
447
+ version: response.version,
448
+ practiceDuration,
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Deletes all practice sessions for a specific day.
454
+ *
455
+ * This function retrieves all user practice session IDs for a given day and sends a DELETE request
456
+ * to remove them from the server. It also updates the local context to reflect the deletion.
457
+ *
458
+ * @async
459
+ * @param {string} day - The day (in `YYYY-MM-DD` format) for which practice sessions should be deleted.
460
+ * @returns {Promise&lt;string[]>} - A promise that resolves once the practice session is removed.
461
+ *
462
+ * * @example
463
+ * // Delete practice sessions for April 10, 2025
464
+ * deletePracticeSession("2025-04-10")
465
+ * .then(deletedIds => console.log("Deleted sessions:", response))
466
+ * .catch(error => console.error("Delete failed:", error));
467
+ */
468
+ export async function deletePracticeSession(day) {
469
+ const userPracticesIds = await getUserPracticeIds(day)
470
+ if (!userPracticesIds.length) return []
471
+
472
+ const url = `/api/user/practices/v1/practices${buildQueryString(userPracticesIds)}`
473
+ await userActivityContext.update(
474
+ async function (localContext) {
475
+ if (localContext.data?.[DATA_KEY_PRACTICES]?.[day]) {
476
+ delete localContext.data[DATA_KEY_PRACTICES][day]
477
+ }
478
+ },
479
+ async function () {
480
+ return await fetchHandler(url, 'DELETE', null)
481
+ }
482
+ )
483
+ }
484
+
485
+ /**
486
+ * Restores deleted practice sessions for a specific date.
487
+ *
488
+ * Sends a PUT request to restore any previously deleted practices for a given date.
489
+ * If restored practices are returned, they are added back into the local context.
490
+ *
491
+ * @async
492
+ * @param {string} date - The date (in `YYYY-MM-DD` format) for which deleted practice sessions should be restored.
493
+ * @returns {Promise&lt;Object>} - The response object from the API, containing practices for selected date.
494
+ *
495
+ * @example
496
+ * // Restore practice sessions deleted on April 10, 2025
497
+ * restorePracticeSession("2025-04-10")
498
+ * .then(response => console.log("Practice session restored:", response))
499
+ * .catch(error => console.error("Restore failed:", error));
500
+ */
501
+ export async function restorePracticeSession(date) {
502
+ const url = `/api/user/practices/v1/practices/restore?date=${date}`
503
+ const response = await fetchHandler(url, 'PUT', null)
504
+
505
+ if (response?.data) {
506
+ await userActivityContext.updateLocal(async function (localContext) {
507
+ if (!localContext.data[DATA_KEY_PRACTICES][date]) {
508
+ localContext.data[DATA_KEY_PRACTICES][date] = []
509
+ }
510
+
511
+ response.data.forEach((restoredPractice) => {
512
+ localContext.data[DATA_KEY_PRACTICES][date].push({
513
+ id: restoredPractice.id,
514
+ duration_seconds: restoredPractice.duration_seconds,
515
+ })
516
+ })
517
+ })
518
+ }
519
+
520
+ const formattedMeta = await formatPracticeMeta(response?.data)
521
+ const practiceDuration = formattedMeta.reduce(
522
+ (total, practice) => total + (practice.duration || 0),
523
+ 0
524
+ )
525
+
526
+ return { data: formattedMeta, practiceDuration }
527
+ }
528
+
529
+ /**
530
+ * Retrieves and formats a user's practice sessions for a specific day.
531
+ *
532
+ * @param {Object} params - Parameters for fetching practice sessions.
533
+ * @param {string} params.day - The date for which practice sessions should be retrieved (format: YYYY-MM-DD).
534
+ * @param {number} [params.userId=globalConfig.sessionConfig.userId] - Optional user ID to retrieve sessions for a specific user. Defaults to the logged-in user.
535
+ * @returns {Promise&lt;Object>} - A promise that resolves to an object containing:
536
+ * - `practices`: An array of formatted practice session data.
537
+ * - `practiceDuration`: Total practice duration (in seconds) for the given day.
538
+ *
539
+ * @example
540
+ * // Get practice sessions for the current user on a specific day
541
+ * getPracticeSessions({ day: "2025-03-31" })
542
+ * .then(response => console.log(response))
543
+ * .catch(error => console.error(error));
544
+ *
545
+ * @example
546
+ * // Get practice sessions for another user
547
+ * getPracticeSessions({ day: "2025-03-31", userId: 456 })
548
+ * .then(response => console.log(response))
549
+ * .catch(error => console.error(error));
550
+ */
551
+ export async function getPracticeSessions(params = {}) {
552
+ const { day, userId = globalConfig.sessionConfig.userId } = params
553
+ const userPracticesIds = await getUserPracticeIds(day, userId)
554
+ if (!userPracticesIds.length) return { data: { practices: [], practiceDuration: 0 } }
555
+
556
+ const meta = await fetchUserPracticeMeta(userPracticesIds, userId)
557
+ if (!meta.data.length) return { data: { practices: [], practiceDuration: 0 } }
558
+
559
+ const formattedMeta = await formatPracticeMeta(meta.data)
560
+ const practiceDuration = formattedMeta.reduce(
561
+ (total, practice) => total + (practice.duration || 0),
562
+ 0
563
+ )
564
+
565
+ return { data: { practices: formattedMeta, practiceDuration } }
566
+ }
567
+
568
+ /**
569
+ * Retrieves user practice notes for a specific day.
570
+ *
571
+ * @async
572
+ * @param {string} day - The day (in `YYYY-MM-DD` format) to fetch practice notes for.
573
+ * @returns {Promise&lt;{ data: Object[] }>} - A promise that resolves to an object containing the practice notes.
574
+ *
575
+ * @example
576
+ * // Get notes for April 10, 2025
577
+ * getPracticeNotes("2025-04-10")
578
+ * .then(({ data }) => console.log("Practice notes:", data))
579
+ * .catch(error => console.error("Failed to get notes:", error));
580
+ */
581
+ export async function getPracticeNotes(day) {
582
+ const notes = await fetchUserPracticeNotes(day)
583
+ return { data: notes }
584
+ }
585
+
586
+ /**
587
+ * Retrieves the user's recent activity.
588
+ *
589
+ * Returns an object containing recent practice activity.
590
+ *
591
+ * @async
592
+ * @returns {Promise&lt;{ data: Object[] }>} - A promise that resolves to an object containing recent activity items.
593
+ *
594
+ * @example
595
+ * // Fetch recent practice activity
596
+ * getRecentActivity()
597
+ * .then(({ data }) => console.log("Recent activity:", data))
598
+ * .catch(error => console.error("Failed to get recent activity:", error));
599
+ */
600
+ export async function getRecentActivity({ page = 1, limit = 5, tabName = null } = {}) {
601
+ return await fetchRecentUserActivities({ page, limit, tabName })
602
+ }
603
+
604
+ /**
605
+ * Creates practice notes for a specific date.
606
+ *
607
+ * @param {Object} payload - The data required to create practice notes.
608
+ * @param {string} payload.date - The date for which to create notes (format: YYYY-MM-DD).
609
+ * @param {string} payload.notes - The notes content to be saved.
610
+ * @returns {Promise&lt;Object>} - A promise that resolves to the API response after creating the notes.
611
+ *
612
+ * @example
613
+ * createPracticeNotes({ date: '2025-04-10', notes: 'Worked on scales and arpeggios' })
614
+ * .then(response => console.log(response))
615
+ * .catch(error => console.error(error));
616
+ */
617
+ export async function createPracticeNotes(payload) {
618
+ const url = `/api/user/practices/v1/notes`
619
+ return await fetchHandler(url, 'POST', null, payload)
620
+ }
621
+
622
+ /**
623
+ * Updates existing practice notes for a specific date.
624
+ *
625
+ * @param {Object} payload - The data required to update practice notes.
626
+ * @param {string} payload.date - The date for which to update notes (format: YYYY-MM-DD).
627
+ * @param {string} payload.notes - The updated notes content.
628
+ * @returns {Promise&lt;Object>} - A promise that resolves to the API response after updating the notes.
629
+ *
630
+ * @example
631
+ * updatePracticeNotes({ date: '2025-04-10', notes: 'Updated: Focused on technique and timing' })
632
+ * .then(response => console.log(response))
633
+ * .catch(error => console.error(error));
634
+ */
635
+ export async function updatePracticeNotes(payload) {
636
+ const url = `/api/user/practices/v1/notes`
637
+ return await fetchHandler(url, 'PUT', null, payload)
638
+ }
639
+
640
+ function getStreaksAndMessage(practices) {
641
+ let { currentDailyStreak, currentWeeklyStreak, streakMessage } = calculateStreaks(practices, true)
642
+
643
+ return {
644
+ currentDailyStreak,
645
+ currentWeeklyStreak,
646
+ streakMessage,
647
+ }
648
+ }
649
+
650
+ async function getUserPracticeIds(day = new Date().toISOString().split('T')[0], userId = null) {
651
+ let practices = {}
652
+ if (userId !== globalConfig.sessionConfig.userId) {
653
+ let data = await fetchUserPractices({ userId })
654
+ practices = data?.['data']?.[DATA_KEY_PRACTICES] ?? {}
655
+ } else {
656
+ let data = await userActivityContext.getData()
657
+ practices = data?.[DATA_KEY_PRACTICES] ?? {}
658
+ }
659
+ let userPracticesIds = []
660
+ Object.keys(practices).forEach((date) => {
661
+ if (date === day) {
662
+ practices[date].forEach((practice) => userPracticesIds.push(practice.id))
663
+ }
664
+ })
665
+ return userPracticesIds
666
+ }
667
+
668
+ function buildQueryString(ids, paramName = 'practice_ids') {
669
+ if (!ids.length) return ''
670
+ return '?' + ids.map((id) => `${paramName}[]=${id}`).join('&amp;')
671
+ }
672
+
673
+ // Helper: Calculate streaks
674
+ function calculateStreaks(practices, includeStreakMessage = false) {
675
+ let currentDailyStreak = 0
676
+ let currentWeeklyStreak = 0
677
+ let lastActiveDay = null
678
+ let streakMessage = ''
679
+
680
+ let sortedPracticeDays = Object.keys(practices)
681
+ .map((dateStr) => {
682
+ const [year, month, day] = dateStr.split('-').map(Number)
683
+ const newDate = new Date()
684
+ newDate.setFullYear(year, month - 1, day)
685
+ return newDate
686
+ })
687
+ .sort((a, b) => a - b)
688
+ if (sortedPracticeDays.length === 0) {
689
+ return {
690
+ currentDailyStreak: 0,
691
+ currentWeeklyStreak: 0,
692
+ streakMessage: streakMessages.startStreak,
693
+ }
694
+ }
695
+ lastActiveDay = sortedPracticeDays[sortedPracticeDays.length - 1]
696
+
697
+ let dailyStreak = 0
698
+ let prevDay = null
699
+ sortedPracticeDays.forEach((currentDay) => {
700
+ if (prevDay === null || isNextDay(prevDay, currentDay)) {
701
+ dailyStreak++
702
+ } else {
703
+ dailyStreak = 1
704
+ }
705
+ prevDay = currentDay
706
+ })
707
+ currentDailyStreak = dailyStreak
708
+
709
+ // Weekly streak calculation
710
+ let weekNumbers = new Set(sortedPracticeDays.map((date) => getWeekNumber(date)))
711
+ let weeklyStreak = 0
712
+ let lastWeek = null
713
+ ;[...weekNumbers]
714
+ .sort((a, b) => b - a)
715
+ .forEach((week) => {
716
+ if (lastWeek === null || week === lastWeek - 1) {
717
+ weeklyStreak++
718
+ } else {
719
+ return
720
+ }
721
+ lastWeek = week
722
+ })
723
+ currentWeeklyStreak = weeklyStreak
724
+
725
+ // Calculate streak message only if includeStreakMessage is true
726
+ if (includeStreakMessage) {
727
+ let today = new Date()
728
+ let yesterday = new Date(today)
729
+ yesterday.setDate(today.getDate() - 1)
730
+
731
+ let currentWeekStart = getMonday(today)
732
+ let lastWeekStart = new Date(currentWeekStart)
733
+ lastWeekStart.setDate(currentWeekStart.getDate() - 7)
734
+
735
+ let hasYesterdayPractice = sortedPracticeDays.some((date) => isSameDate(date, yesterday))
736
+ let hasCurrentWeekPractice = sortedPracticeDays.some((date) => date >= currentWeekStart)
737
+ let hasCurrentWeekPreviousPractice = sortedPracticeDays.some(
738
+ (date) => date >= currentWeekStart &amp;&amp; date &lt; today
739
+ )
740
+ let hasLastWeekPractice = sortedPracticeDays.some(
741
+ (date) => date >= lastWeekStart &amp;&amp; date &lt; currentWeekStart
742
+ )
743
+ let hasOlderPractice = sortedPracticeDays.some((date) => date &lt; lastWeekStart)
744
+
745
+ if (isSameDate(lastActiveDay, today)) {
746
+ if (hasYesterdayPractice) {
747
+ streakMessage = streakMessages.dailyStreak(currentDailyStreak)
748
+ } else if (hasCurrentWeekPreviousPractice) {
749
+ streakMessage = streakMessages.weeklyStreak(currentWeeklyStreak)
750
+ } else if (hasLastWeekPractice) {
751
+ streakMessage = streakMessages.greatJobWeeklyStreak(currentWeeklyStreak)
752
+ } else {
753
+ streakMessage = streakMessages.dailyStreakShort(currentDailyStreak)
754
+ }
755
+ } else {
756
+ if (
757
+ (hasYesterdayPractice &amp;&amp; currentDailyStreak >= 2) ||
758
+ (hasYesterdayPractice &amp;&amp; sortedPracticeDays.length == 1) ||
759
+ (hasYesterdayPractice &amp;&amp; !hasLastWeekPractice &amp;&amp; hasOlderPractice)
760
+ ) {
761
+ streakMessage = streakMessages.dailyStreakReminder(currentDailyStreak)
762
+ } else if (hasCurrentWeekPractice) {
763
+ streakMessage = streakMessages.weeklyStreakKeepUp(currentWeeklyStreak)
764
+ } else if (hasLastWeekPractice) {
765
+ streakMessage = streakMessages.weeklyStreakReminder(currentWeeklyStreak)
766
+ } else {
767
+ streakMessage = streakMessages.restartStreak
768
+ }
769
+ }
770
+ }
771
+
772
+ return { currentDailyStreak, currentWeeklyStreak, streakMessage }
773
+ }
774
+
775
+ /**
776
+ * Calculates the longest daily, weekly streaks and totalPracticeSeconds from user practice dates.
777
+ * @returns {{ longestDailyStreak: number, longestWeeklyStreak: number, totalPracticeSeconds:number }}
778
+ */
779
+ export async function calculateLongestStreaks(userId = globalConfig.sessionConfig.userId) {
780
+ let practices = await getUserPractices(userId)
781
+ let totalPracticeSeconds = 0
782
+ // Calculate total practice duration
783
+ for (const date in practices) {
784
+ for (const entry of practices[date]) {
785
+ totalPracticeSeconds += entry.duration_seconds
786
+ }
787
+ }
788
+
789
+ let practiceDates = Object.keys(practices)
790
+ .map((dateStr) => {
791
+ const [y, m, d] = dateStr.split('-').map(Number)
792
+ const newDate = new Date()
793
+ newDate.setFullYear(y, m - 1, d)
794
+ return newDate
795
+ })
796
+ .sort((a, b) => a - b)
797
+
798
+ if (!practiceDates || practiceDates.length === 0) {
799
+ return { longestDailyStreak: 0, longestWeeklyStreak: 0, totalPracticeSeconds: 0 }
800
+ }
801
+
802
+ // Normalize to Date objects
803
+ const normalizedDates = [
804
+ ...new Set(
805
+ practiceDates.map((d) => {
806
+ const date = new Date(d)
807
+ date.setHours(0, 0, 0, 0)
808
+ return date.getTime()
809
+ })
810
+ ),
811
+ ].sort((a, b) => a - b)
812
+
813
+ // ----- Daily Streak -----
814
+ let longestDailyStreak = 1
815
+ let currentDailyStreak = 1
816
+ for (let i = 1; i &lt; normalizedDates.length; i++) {
817
+ const diffInDays = (normalizedDates[i] - normalizedDates[i - 1]) / (1000 * 60 * 60 * 24)
818
+ if (diffInDays === 1) {
819
+ currentDailyStreak++
820
+ longestDailyStreak = Math.max(longestDailyStreak, currentDailyStreak)
821
+ } else {
822
+ currentDailyStreak = 1
823
+ }
824
+ }
825
+
826
+ // ----- Weekly Streak -----
827
+ const weekStartDates = [
828
+ ...new Set(
829
+ normalizedDates.map((ts) => {
830
+ const d = new Date(ts)
831
+ const day = d.getDay()
832
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1) // adjust to Monday
833
+ d.setDate(diff)
834
+ return d.getTime() // timestamp for Monday
835
+ })
836
+ ),
837
+ ].sort((a, b) => a - b)
838
+
839
+ let longestWeeklyStreak = 1
840
+ let currentWeeklyStreak = 1
841
+
842
+ for (let i = 1; i &lt; weekStartDates.length; i++) {
843
+ const diffInWeeks = (weekStartDates[i] - weekStartDates[i - 1]) / (1000 * 60 * 60 * 24 * 7)
844
+ if (diffInWeeks === 1) {
845
+ currentWeeklyStreak++
846
+ longestWeeklyStreak = Math.max(longestWeeklyStreak, currentWeeklyStreak)
847
+ } else {
848
+ currentWeeklyStreak = 1
849
+ }
850
+ }
851
+
852
+ return {
853
+ longestDailyStreak,
854
+ longestWeeklyStreak,
855
+ totalPracticeSeconds,
856
+ }
857
+ }
858
+
859
+ async function formatPracticeMeta(practices) {
860
+ const contentIds = practices.map((p) => p.content_id).filter((id) => id !== null)
861
+ const contents = await fetchByRailContentIds(contentIds)
862
+
863
+ const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
864
+
865
+ return practices.map((practice) => {
866
+ const utcDate = new Date(practice.created_at)
867
+ const content = contents.find((c) => c.id === practice.content_id) || {}
868
+
869
+ return {
870
+ id: practice.id,
871
+ auto: practice.auto,
872
+ thumbnail: practice.content_id ? content.thumbnail : practice.thumbnail_url || '',
873
+ thumbnail_url: practice.content_id ? content.thumbnail : practice.thumbnail_url || '',
874
+ duration: practice.duration_seconds || 0,
875
+ duration_seconds: practice.duration_seconds || 0,
876
+ content_url: content.url || null,
877
+ title: practice.content_id ? content.title : practice.title,
878
+ category_id: practice.category_id,
879
+ instrument_id: practice.instrument_id,
880
+ content_type: getFormattedType(content.type || '', content.brand),
881
+ content_id: practice.content_id || null,
882
+ content_brand: content.brand || null,
883
+ created_at: convertToTimeZone(utcDate, userTimeZone),
884
+ }
885
+ })
886
+ }
887
+
888
+
889
+ /**
890
+ * Records a new user activity in the system.
891
+ *
892
+ * @param {Object} payload - The data representing the user activity.
893
+ * @param {number} payload.user_id - The ID of the user.
894
+ * @param {string} payload.action - The type of action (e.g., 'start', 'complete', 'comment', etc.).
895
+ * @param {string} payload.brand - The brand associated with the activity.
896
+ * @param {string} payload.type - The content type (e.g., 'lesson', 'song', etc.).
897
+ * @param {number} payload.content_id - The ID of the related content.
898
+ * @param {string} payload.date - The date of the activity (ISO format).
899
+ * @returns {Promise&lt;Object>} - A promise that resolves to the API response after recording the activity.
900
+ *
901
+ * @example
902
+ * recordUserActivity({
903
+ * user_id: 123,
904
+ * action: 'start',
905
+ * brand: 'pianote',
906
+ * type: 'lesson',
907
+ * content_id: 4561,
908
+ * date: '2025-05-15'
909
+ * }).then(response => console.log(response))
910
+ * .catch(error => console.error(error));
911
+ */
912
+ export async function recordUserActivity(payload) {
913
+ const url = `/api/user-management-system/v1/activities`
914
+ return await fetchHandler(url, 'POST', null, payload)
915
+ }
916
+
917
+ /**
918
+ * Deletes a specific user activity by its ID.
919
+ *
920
+ * @param {number|string} id - The ID of the user activity to delete.
921
+ * @returns {Promise&lt;Object>} - A promise that resolves to the API response after deletion.
922
+ *
923
+ * @example
924
+ * deleteUserActivity(789)
925
+ * .then(response => console.log('Deleted:', response))
926
+ * .catch(error => console.error(error));
927
+ */
928
+ export async function deleteUserActivity(id) {
929
+ const url = `/api/user-management-system/v1/activities/${id}`
930
+ return await fetchHandler(url, 'DELETE')
931
+ }
932
+ /**
933
+ * Fetches and combines recent user progress rows and playlists, excluding certain types and parents.
934
+ *
935
+ * @param {Object} [options={}] - Options for fetching progress rows.
936
+ * @param {string|null} [options.brand=null] - The brand context for progress data.
937
+ * @param {number} [options.limit=8] - Maximum number of progress rows to return.
938
+ * @returns {Promise&lt;Object>} - A promise that resolves to an object containing progress rows formatted for UI.
939
+ *
940
+ * @example
941
+ * getProgressRows({ brand: 'drumeo', limit: 10 })
942
+ * .then(data => console.log(data))
943
+ * .catch(error => console.error(error));
944
+ */
945
+ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
946
+ const excludedTypes = new Set([
947
+ 'pack-bundle',
948
+ 'learning-path-course',
949
+ 'learning-path-level'
950
+ ]);
951
+
952
+ const recentPlaylists = await fetchUserPlaylists(brand, {
953
+ sort: '-last_progress',
954
+ limit: limit,
955
+ });
956
+ const playlists = recentPlaylists?.data || [];
957
+ const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists);
958
+ const playlistEngagedOnContents = eligiblePlaylistItems.map(item => item.last_engaged_on);
959
+ const playlistsContents = await fetchByRailContentIds(playlistEngagedOnContents, 'progress-tracker');
960
+ const excludedParents = new Set();
961
+ const existingShows = new Set();
962
+ for (const item of playlistsContents) {
963
+ const contentId = item.id ?? item.railcontent_id;
964
+ excludedParents.add(contentId)
965
+ const parentIds = item.parent_content_data || [];
966
+ parentIds.forEach(id => excludedParents.add(id));
967
+ }
968
+
969
+ const progressContents = await getAllStartedOrCompleted({onlyIds: false, brand: brand, excludedIds: Array.from(excludedParents) });
970
+ const contents = await fetchByRailContentIds(Object.keys(progressContents), 'progress-tracker', brand);
971
+ const contentsMap = {};
972
+ contents.forEach(content => {
973
+ contentsMap[content.railcontent_id] = content;
974
+ });
975
+ const childToParentMap = {};
976
+ Object.values(contentsMap).forEach(content => {
977
+ if (Array.isArray(content.parent_content_data) &amp;&amp; content.parent_content_data.length > 0) {
978
+ childToParentMap[content.id] = content.parent_content_data[content.parent_content_data.length - 1];
979
+ }
980
+ });
981
+ const progressMap = new Map();
982
+ for (const [idStr, progress] of Object.entries(progressContents)) {
983
+ const id = parseInt(idStr);
984
+ const content = contentsMap[id];
985
+ if (!content || excludedTypes.has(content.type)) continue;
986
+ const parentId = childToParentMap[id];
987
+ // Handle children with parents
988
+ if (parentId) {
989
+ const parentContent = contentsMap[parentId];
990
+ if (!parentContent || excludedTypes.has(parentContent.type)) continue;
991
+ const existing = progressMap.get(parentId);
992
+ if (existing) {
993
+ // If childIndex isn't already set, set it
994
+ if (existing.childIndex === undefined) {
995
+ existing.childIndex = id;
996
+ }
997
+ } else {
998
+ progressMap.set(parentId, {
999
+ id: parentId,
1000
+ raw: parentContent,
1001
+ state: progress.status,
1002
+ percent: progress.progress,
1003
+ progressTimestamp: progress.last_update * 1000,
1004
+ childIndex: id
1005
+ });
1006
+ }
1007
+ continue;
1008
+ }
1009
+ // Handle standalone parents
1010
+ if (!progressMap.has(id)) {
1011
+ if(!existingShows.has(content.type)){
1012
+ progressMap.set(id, {
1013
+ id,
1014
+ raw: content,
1015
+ state: progress.status,
1016
+ percent: progress.progress,
1017
+ progressTimestamp: progress.last_update * 1000
1018
+ });
1019
+ }
1020
+ if(showsLessonTypes.includes(content.type)) {
1021
+ existingShows.add(content.type)
1022
+ }
1023
+ }
1024
+ }
1025
+ const pinnedItem = await extractPinnedItem({
1026
+ brand,
1027
+ progressMap,
1028
+ playlistItems: eligiblePlaylistItems,
1029
+ })
1030
+ const progressList = Array.from(progressMap.values())
1031
+ if (pinnedItem) {
1032
+ pinnedItem.pinned = true
1033
+ }
1034
+
1035
+ const pinnedId = pinnedItem?.id
1036
+ const filteredProgressList = pinnedId
1037
+ ? progressList.filter(item => item.id !== pinnedId)
1038
+ : progressList;
1039
+ const filteredPlaylists = pinnedId
1040
+ ? eligiblePlaylistItems.filter(item => item.id !== pinnedId)
1041
+ : eligiblePlaylistItems;
1042
+ const combinedBase = [...filteredProgressList, ...filteredPlaylists]
1043
+ const combined = pinnedItem ? [pinnedItem, ...combinedBase] : combinedBase
1044
+
1045
+ const finalCombined = mergeAndSortItems(combined, limit)
1046
+
1047
+ const results = await Promise.all(
1048
+ finalCombined.slice(0, limit).map(item =>
1049
+ item.type === 'playlist'
1050
+ ? processPlaylistItem(item)
1051
+ : processContentItem(item)
1052
+ )
1053
+ );
1054
+
1055
+ return {
1056
+ type: TabResponseType.PROGRESS_ROWS,
1057
+ displayBrowseAll: combined.length > limit,
1058
+ data: results
1059
+ };
1060
+ }
1061
+
1062
+ async function processContentItem(item) {
1063
+ let data = item.raw;
1064
+ const contentType = getFormattedType(data.type, data.brand);
1065
+ const status = item.state;
1066
+
1067
+ let ctaText = 'Continue';
1068
+ if (contentType === 'transcription' || contentType === 'play-along' || contentType === 'jam-track') ctaText = 'Replay Song';
1069
+ if (contentType === 'lesson') ctaText = status === 'completed' ? 'Revisit Lesson' : 'Continue';
1070
+ if ((contentType === 'guided course' || contentType === 'song tutorial' || collectionLessonTypes.includes(contentType)) &amp;&amp; status === 'completed') ctaText = 'Revisit Lessons' ;
1071
+ if (contentType === 'pack' &amp;&amp; status === 'completed') {
1072
+ ctaText = 'View Lessons';
1073
+ }
1074
+
1075
+ if (data.lesson_count > 0) {
1076
+ const lessonIds = extractLessonIds(item);
1077
+ const progressOnItems = await getProgressStateByIds(lessonIds);
1078
+ let completedCount = Object.values(progressOnItems).filter(value => value === 'completed').length;
1079
+ data.completed_children = completedCount;
1080
+
1081
+ if (item.childIndex) {
1082
+ let nextId = item.childIndex
1083
+ const nextByProgress = findIncompleteLesson(progressOnItems, item.childIndex, item.raw.type)
1084
+ nextId = nextByProgress ? nextByProgress : nextId
1085
+
1086
+ const nestedLessons = data.lessons
1087
+ .filter(item => Array.isArray(item.lessons))
1088
+ .flatMap(parent =>
1089
+ parent.lessons.map(lesson => ({
1090
+ ...lesson,
1091
+ parent: {
1092
+ id: parent.id,
1093
+ slug: parent.slug,
1094
+ title: parent.title,
1095
+ type: parent.type
1096
+ }
1097
+ }))
1098
+ );
1099
+
1100
+ const lessons = (nestedLessons.length === 0) ? data.lessons : nestedLessons
1101
+ const nextLesson = lessons.find(lesson => lesson.id === nextId)
1102
+ data.first_incomplete_child = nextLesson?.parent ?? nextLesson
1103
+ data.second_incomplete_child = (nextLesson?.parent) ? nextLesson : null
1104
+ if(data.type === 'challenge' &amp;&amp; nextByProgress !== undefined ){
1105
+ const challenge = await fetchChallengeLessonData(nextByProgress)
1106
+ if(challenge.lesson.is_locked) {
1107
+ const timeRemaining = getTimeRemainingUntilLocal(challenge.lesson.unlock_date, {withTotalSeconds:true})
1108
+ data.is_locked = true
1109
+ data.time_remaining_seconds = timeRemaining.totalSeconds
1110
+ ctaText = 'Next lesson in ' + timeRemaining.formatted
1111
+ }
1112
+ }
1113
+ }
1114
+ }
1115
+
1116
+ if(contentType == 'show'){
1117
+ const shows = await fetchShows(data.brand, data.type)
1118
+ const showIds = shows.map(item => item.id);
1119
+ const progressOnItems = await getProgressStateByIds(showIds);
1120
+ const completedCount = Object.values(progressOnItems).filter(value => value === 'completed').length;
1121
+ if(status == 'completed') {
1122
+ const nextByProgress = findIncompleteLesson(progressOnItems, data.id, data.type);
1123
+ data = shows.find(lesson => lesson.id === nextByProgress);
1124
+ }
1125
+ data.completed_children = completedCount;
1126
+ data.child_count = shows.length;
1127
+ item.percent = Math.round((completedCount / shows.length) * 100);
1128
+ if(completedCount == shows.length) {
1129
+ ctaText = 'Revisit Lessons';
1130
+ }
1131
+ }
1132
+
1133
+ return {
1134
+ id: item.id,
1135
+ progressType: 'content',
1136
+ header: contentType,
1137
+ pinned: item.pinned ?? false,
1138
+ body: {
1139
+ progressPercent: item.percent,
1140
+ thumbnail: data.thumbnail,
1141
+ title: data.title,
1142
+ badge: data.badge ?? null,
1143
+ isLocked: data.is_locked ?? false,
1144
+ subtitle: !data.child_count || data.lesson_count === 1
1145
+ ? (contentType === 'lesson') ? `${item.percent}% Complete`: `${data.difficulty_string} • ${data.artist_name}`
1146
+ : `${data.completed_children} of ${data.lesson_count ?? data.child_count} Lessons Complete`
1147
+ },
1148
+ cta: {
1149
+ text: ctaText,
1150
+ timeRemainingToUnlockSeconds: data.time_remaining_seconds ?? null,
1151
+ action: {
1152
+ type: data.type,
1153
+ brand: data.brand,
1154
+ id: data.id,
1155
+ slug: data.slug,
1156
+ child: data.first_incomplete_child
1157
+ ? {
1158
+ id: data.first_incomplete_child.id,
1159
+ type: data.first_incomplete_child.type,
1160
+ brand: data.first_incomplete_child.brand,
1161
+ slug: data.first_incomplete_child.slug,
1162
+ child: data.second_incomplete_child
1163
+ ? {
1164
+ id: data.second_incomplete_child.id,
1165
+ type: data.second_incomplete_child.type,
1166
+ brand: data.second_incomplete_child.brand,
1167
+ slug: data.second_incomplete_child.slug
1168
+ }
1169
+ : null
1170
+ }
1171
+ : null
1172
+ }
1173
+ },
1174
+ progressTimestamp: item.progressTimestamp
1175
+ };
1176
+ }
1177
+
1178
+ async function processPlaylistItem(item) {
1179
+ const playlist = item.raw;
1180
+ const progressOnItems = await getProgressStateByIds(playlist.items.map(a => a.content_id));
1181
+ const allItemsCompleted = item.raw.items.every(i => {
1182
+ const itemId = i.content_id;
1183
+ const progress = progressOnItems[itemId];
1184
+ return progress &amp;&amp; progress === 'completed';
1185
+ });
1186
+ let nextItem = playlist.items[0] ?? null;
1187
+ if (!allItemsCompleted) {
1188
+ const lastItemProgress = progressOnItems[playlist.last_engaged_on];
1189
+ const index = playlist.items.findIndex(i => i.content_id === playlist.last_engaged_on);
1190
+ if (lastItemProgress === 'completed') {
1191
+ nextItem = playlist.items[index + 1] ?? nextItem;
1192
+ } else {
1193
+ nextItem = playlist.items[index] ?? nextItem;
1194
+ }
1195
+ }
1196
+
1197
+ return {
1198
+ id: playlist.id,
1199
+ progressType: 'playlist',
1200
+ header: 'playlist',
1201
+ pinned: item.pinned ?? false,
1202
+ body: {
1203
+ first_items_thumbnail_url: playlist.first_items_thumbnail_url,
1204
+ title: playlist.name,
1205
+ subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`,
1206
+ total_items: playlist.total_items,
1207
+ },
1208
+ progressTimestamp: item.progressTimestamp,
1209
+ cta: {
1210
+ text: 'Continue',
1211
+ action: {
1212
+ brand: playlist.brand,
1213
+ id: playlist.id,
1214
+ itemId: nextItem.id,
1215
+ type: 'playlists',
1216
+ }
1217
+ }
1218
+ }
1219
+ }
1220
+
1221
+ const getFormattedType = (type, brand) => {
1222
+ for (const [key, values] of Object.entries(progressTypesMapping)) {
1223
+ if (values.includes(type)) {
1224
+ return key === 'songs' ? songs[brand] : key;
1225
+ }
1226
+ }
1227
+
1228
+ return null;
1229
+ };
1230
+
1231
+ function extractLessonIds(data) {
1232
+ const ids = [];
1233
+ function traverse(lessons) {
1234
+ for (const item of lessons) {
1235
+ if (item.lessons) {
1236
+ traverse(item.lessons); // Recursively handle nested lessons
1237
+ }else if (item.id) {
1238
+ ids.push(item.id);
1239
+ }
1240
+ }
1241
+ }
1242
+ if (data.raw &amp;&amp; Array.isArray(data.raw.lessons)) {
1243
+ traverse(data.raw.lessons);
1244
+ }
1245
+
1246
+ return ids;
1247
+ }
1248
+
1249
+
1250
+ async function getEligiblePlaylistItems(playlists) {
1251
+ const eligible = playlists.filter(p => p.last_progress &amp;&amp; p.last_engaged_on);
1252
+ return Promise.all(
1253
+ eligible.map(async p => {
1254
+ const utcDate = new Date(p.last_progress.replace(' ', 'T') + 'Z');
1255
+ const timestamp = utcDate.getTime();
1256
+ return {
1257
+ type: 'playlist',
1258
+ progressTimestamp: timestamp,
1259
+ last_engaged_on: p.last_engaged_on,
1260
+ raw: p
1261
+ };
1262
+ })
1263
+ );
1264
+ }
1265
+
1266
+ function mergeAndSortItems(items, limit) {
1267
+ const seen = new Set();
1268
+ const deduped = [];
1269
+
1270
+ for (const item of items) {
1271
+ const key = `${item.id}-${item.type || item.raw?.type}`;
1272
+ if (!seen.has(key)) {
1273
+ seen.add(key);
1274
+ deduped.push(item);
1275
+ }
1276
+ }
1277
+
1278
+ return deduped
1279
+ .filter(item => typeof item.progressTimestamp === 'number' &amp;&amp; item.progressTimestamp > 0)
1280
+ .sort((a, b) => {
1281
+ if (a.pinned &amp;&amp; !b.pinned) return -1;
1282
+ if (!a.pinned &amp;&amp; b.pinned) return 1;
1283
+ return b.progressTimestamp - a.progressTimestamp;
1284
+ })
1285
+ .slice(0, limit + 5);
1286
+ }
1287
+
1288
+ function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
1289
+ const ids = Object.keys(progressOnItems).map(Number);
1290
+ if (contentType === 'challenge') {
1291
+ // Return first incomplete lesson
1292
+ return ids.find(id => progressOnItems[id] !== 'completed') || ids.at(0);
1293
+ }
1294
+
1295
+ // For other types, find next incomplete after current
1296
+ const currentIndex = ids.indexOf(Number(currentContentId));
1297
+ if (currentIndex === -1) return null;
1298
+
1299
+ for (let i = currentIndex + 1; i &lt; ids.length; i++) {
1300
+ const id = ids[i];
1301
+ if (progressOnItems[id] !== 'completed') {
1302
+ return id;
1303
+ }
1304
+ }
1305
+
1306
+ return ids[0];
1307
+ }
1308
+
1309
+ /**
1310
+ * Pins a specific progress row for a user, scoped by brand.
1311
+ *
1312
+ * @param {string} brand - The brand context for the pin action.
1313
+ * @param {number|string} id - The ID of the progress item to pin.
1314
+ * @param {string} progressType - The type of progress (e.g., 'content', 'playlist').
1315
+ * @returns {Promise&lt;Object>} - A promise resolving to the response from the pin API.
1316
+ *
1317
+ * @example
1318
+ * pinProgressRow('drumeo', 12345, 'content')
1319
+ * .then(response => console.log(response))
1320
+ * .catch(error => console.error(error));
1321
+ */
1322
+ export async function pinProgressRow(brand, id, progressType) {
1323
+ const url = `/api/user-management-system/v1/progress/pin?brand=${brand}&amp;id=${id}&amp;progressType=${progressType}`;
1324
+ const response = await fetchHandler(url, 'PUT', null)
1325
+ if (response &amp;&amp; !response.error) {
1326
+ await updatePinnedProgressRow(brand, {
1327
+ id,
1328
+ progressType,
1329
+ pinnedAt: new Date().toISOString(),
1330
+ });
1331
+ }
1332
+ return response;
1333
+ }
1334
+ /**
1335
+ * Unpins the current pinned progress row for a user, scoped by brand.
1336
+ *
1337
+ * @param {string} brand - The brand context for the unpin action.
1338
+ * @returns {Promise&lt;Object>} - A promise resolving to the response from the unpin API.
1339
+ *
1340
+ * @example
1341
+ * unpinProgressRow('drumeo')
1342
+ * .then(response => console.log(response))
1343
+ * .catch(error => console.error(error));
1344
+ */
1345
+ export async function unpinProgressRow(brand) {
1346
+ const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}`
1347
+ const response = await fetchHandler(url, 'PUT', null)
1348
+ if (response &amp;&amp; !response.error) {
1349
+ await updatePinnedProgressRow(brand, null)
1350
+ }
1351
+ return response
1352
+ }
1353
+
1354
+ async function updatePinnedProgressRow(brand, pinnedData) {
1355
+ const userRaw = await globalConfig.localStorage.getItem('user');
1356
+ const user = userRaw ? JSON.parse(userRaw) : {};
1357
+ user.brand_pinned_progress = user.brand_pinned_progress || {}
1358
+ user.brand_pinned_progress[brand] = pinnedData
1359
+ await globalConfig.localStorage.setItem('user', JSON.stringify(user))
1360
+ }
1361
+
1362
+ async function extractPinnedItem({brand, progressMap, playlistItems}) {
1363
+ const userRaw = await globalConfig.localStorage.getItem('user');
1364
+ const user = userRaw ? JSON.parse(userRaw) : {};
1365
+ user.brand_pinned_progress = user.brand_pinned_progress || {}
1366
+
1367
+ const pinned = user.brand_pinned_progress[brand]
1368
+ if (!pinned) return null
1369
+
1370
+ const {id, progressType, pinnedAt} = pinned
1371
+
1372
+ if (progressType === 'content') {
1373
+ const pinnedId = parseInt(id)
1374
+ if (progressMap.has(pinnedId)) {
1375
+ const item = progressMap.get(pinnedId)
1376
+ progressMap.delete(pinnedId)
1377
+ return item
1378
+ } else {
1379
+ const content = await fetchByRailContentIds([`${pinnedId}`], 'progress-tracker')
1380
+ const firstLessonId = getFirstLeafLessonId(content[0])
1381
+ return {
1382
+ id: pinnedId,
1383
+ state: 'started',
1384
+ percent: 0,
1385
+ raw: content[0],
1386
+ progressTimestamp: new Date(pinnedAt).getTime(),
1387
+ childIndex: firstLessonId
1388
+ }
1389
+ }
1390
+ }
1391
+ if (progressType === 'playlist') {
1392
+ const pinnedPlaylist = playlistItems.find(p => p.raw.id === id)
1393
+ if (pinnedPlaylist) {
1394
+ return pinnedPlaylist
1395
+ }else{
1396
+ const playlist = await fetchPlaylist(id)
1397
+ return {
1398
+ id: id,
1399
+ raw: playlist,
1400
+ progressTimestamp: new Date(pinnedAt).getTime(),
1401
+ type: 'playlist',
1402
+ last_engaged_on: playlist.items[0],
1403
+ }
1404
+ }
1405
+ }
1406
+
1407
+ return null
1408
+ }
1409
+
1410
+ function getFirstLeafLessonId(data) {
1411
+ function findFirstLeaf(lessons) {
1412
+ for (const item of lessons) {
1413
+ if (!item.lessons || item.lessons.length === 0) {
1414
+ return item.id || null
1415
+ }
1416
+ const found = findFirstLeaf(item.lessons)
1417
+ if (found) return found
1418
+ }
1419
+ return null
1420
+ }
1421
+
1422
+ return data.lessons ? findFirstLeaf(data.lessons) : null
1423
+ }
1424
+
1425
+
1426
+
1427
+ </code></pre>
1428
+ </article>
1429
+ </section>
1430
+
1431
+
1432
+
1433
+
1434
+
1435
+
1436
+ </div>
1437
+
1438
+ <br class="clear">
1439
+
1440
+ <footer>
1441
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.3</a> on Mon Jun 16 2025 14:26:22 GMT+0300 (Eastern European Summer Time) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1442
+ </footer>
1443
+
1444
+ <script>prettyPrint();</script>
1445
+ <script src="scripts/polyfill.js"></script>
1446
+ <script src="scripts/linenumber.js"></script>
1447
+
1448
+
1449
+
1450
+ </body>
1451
+ </html>