musora-content-services 2.92.3 → 2.92.7

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 (121) hide show
  1. package/.github/workflows/docs.js.yml +64 -47
  2. package/CHANGELOG.md +23 -0
  3. package/package.json +1 -1
  4. package/src/index.d.ts +2 -0
  5. package/src/index.js +2 -0
  6. package/src/lib/brands.ts +7 -7
  7. package/src/services/content/artist.ts +10 -7
  8. package/src/services/content/genre.ts +10 -7
  9. package/src/services/content/instructor.ts +7 -7
  10. package/src/services/content-org/learning-paths.ts +3 -3
  11. package/src/services/progress-row/method-card.js +1 -1
  12. package/src/services/reporting/reporting.ts +19 -15
  13. package/src/services/sync/fetch.ts +3 -0
  14. package/src/services/user/onboarding.ts +3 -3
  15. package/docs/Content.html +0 -269
  16. package/docs/ContentOrganization.html +0 -245
  17. package/docs/Forums.html +0 -269
  18. package/docs/Gamification.html +0 -245
  19. package/docs/TestUser.html +0 -260
  20. package/docs/UserManagementSystem.html +0 -317
  21. package/docs/api_types.js.html +0 -97
  22. package/docs/config.js.html +0 -140
  23. package/docs/content-org_content-org.js.html +0 -76
  24. package/docs/content-org_guided-courses.ts.html +0 -110
  25. package/docs/content-org_learning-paths.ts.html +0 -391
  26. package/docs/content-org_playlists-types.js.html +0 -128
  27. package/docs/content-org_playlists.js.html +0 -440
  28. package/docs/content.js.html +0 -603
  29. package/docs/content_artist.ts.html +0 -206
  30. package/docs/content_content.ts.html +0 -77
  31. package/docs/content_genre.ts.html +0 -209
  32. package/docs/content_instructor.ts.html +0 -206
  33. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  34. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  35. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  36. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  37. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  38. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  39. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  40. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  41. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  42. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -978
  43. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  44. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  45. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  46. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  47. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -1049
  48. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  49. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  50. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  51. package/docs/forums_categories.ts.html +0 -156
  52. package/docs/forums_discussions.js.html +0 -95
  53. package/docs/forums_forum.js.html +0 -95
  54. package/docs/forums_forums.ts.html +0 -160
  55. package/docs/forums_posts.ts.html +0 -284
  56. package/docs/forums_threads.ts.html +0 -284
  57. package/docs/gamification_awards.js.html +0 -165
  58. package/docs/gamification_awards.ts.html +0 -195
  59. package/docs/gamification_gamification.js.html +0 -76
  60. package/docs/gamification_types.js.html +0 -80
  61. package/docs/global.html +0 -6019
  62. package/docs/index.html +0 -167
  63. package/docs/liveTesting.ts.html +0 -103
  64. package/docs/module-Accounts.html +0 -2283
  65. package/docs/module-Artist.html +0 -993
  66. package/docs/module-Awards.html +0 -836
  67. package/docs/module-Categories.html +0 -711
  68. package/docs/module-Config.html +0 -431
  69. package/docs/module-Content-Services-V2.html +0 -2998
  70. package/docs/module-ForumCategories.html +0 -687
  71. package/docs/module-ForumDiscussions.html +0 -370
  72. package/docs/module-Forums.html +0 -16599
  73. package/docs/module-Genre.html +0 -981
  74. package/docs/module-GuidedCourses.html +0 -108
  75. package/docs/module-Instructor.html +0 -929
  76. package/docs/module-Interests.html +0 -1066
  77. package/docs/module-LearningPaths.html +0 -2424
  78. package/docs/module-Onboarding.html +0 -882
  79. package/docs/module-Payments.html +0 -392
  80. package/docs/module-Permissions.html +0 -406
  81. package/docs/module-Playlists.html +0 -3030
  82. package/docs/module-ProgressRow.html +0 -108
  83. package/docs/module-Railcontent-Services.html +0 -5876
  84. package/docs/module-Sanity-Services.html +0 -8244
  85. package/docs/module-Sessions.html +0 -575
  86. package/docs/module-Threads.html +0 -1119
  87. package/docs/module-UserActivity.html +0 -4534
  88. package/docs/module-UserChat.html +0 -410
  89. package/docs/module-UserManagement.html +0 -1932
  90. package/docs/module-UserMemberships.html +0 -829
  91. package/docs/module-UserNotifications.html +0 -2595
  92. package/docs/module-UserProfile.html +0 -370
  93. package/docs/progress-row_method-card.js.html +0 -184
  94. package/docs/railcontent.js.html +0 -724
  95. package/docs/sanity.js.html +0 -2322
  96. package/docs/scripts/collapse.js +0 -39
  97. package/docs/scripts/commonNav.js +0 -28
  98. package/docs/scripts/linenumber.js +0 -25
  99. package/docs/scripts/nav.js +0 -12
  100. package/docs/scripts/polyfill.js +0 -4
  101. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
  102. package/docs/scripts/prettify/lang-css.js +0 -2
  103. package/docs/scripts/prettify/prettify.js +0 -28
  104. package/docs/scripts/search.js +0 -99
  105. package/docs/styles/jsdoc.css +0 -776
  106. package/docs/styles/prettify.css +0 -80
  107. package/docs/userActivity.js.html +0 -1512
  108. package/docs/user_account.ts.html +0 -265
  109. package/docs/user_chat.js.html +0 -98
  110. package/docs/user_interests.js.html +0 -150
  111. package/docs/user_management.js.html +0 -258
  112. package/docs/user_memberships.js.html +0 -144
  113. package/docs/user_memberships.ts.html +0 -292
  114. package/docs/user_notifications.js.html +0 -374
  115. package/docs/user_onboarding.ts.html +0 -329
  116. package/docs/user_payments.ts.html +0 -146
  117. package/docs/user_permissions.js.html +0 -110
  118. package/docs/user_profile.js.html +0 -115
  119. package/docs/user_sessions.js.html +0 -170
  120. package/docs/user_types.js.html +0 -224
  121. package/docs/user_user-management-system.js.html +0 -79
@@ -1,1512 +0,0 @@
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-Accounts.html">Accounts</a><ul class='methods'><li data-type='method'><a href="module-Accounts.html#~confirmEmailChange">confirmEmailChange</a></li><li data-type='method'><a href="module-Accounts.html#~deleteAccount">deleteAccount</a></li><li data-type='method'><a href="module-Accounts.html#~numberOfActiveUsers">numberOfActiveUsers</a></li><li data-type='method'><a href="module-Accounts.html#~requestEmailChange">requestEmailChange</a></li><li data-type='method'><a href="module-Accounts.html#~resetPassword">resetPassword</a></li><li data-type='method'><a href="module-Accounts.html#~sendAccountSetupEmail">sendAccountSetupEmail</a></li><li data-type='method'><a href="module-Accounts.html#~sendPasswordResetEmail">sendPasswordResetEmail</a></li><li data-type='method'><a href="module-Accounts.html#~setupAccount">setupAccount</a></li><li data-type='method'><a href="module-Accounts.html#~status">status</a></li><li data-type='method'><a href="module-Accounts.html#~toggleStudentView">toggleStudentView</a></li></ul></li><li><a href="module-Artist.html">Artist</a><ul class='methods'><li data-type='method'><a href="module-Artist.html#~fetchArtistBySlug">fetchArtistBySlug</a></li><li data-type='method'><a href="module-Artist.html#~fetchArtistLessons">fetchArtistLessons</a></li><li data-type='method'><a href="module-Artist.html#~fetchArtists">fetchArtists</a></li></ul></li><li><a href="module-Awards.html">Awards</a><ul class='methods'><li data-type='method'><a href="module-Awards.html#~fetchAwardsForUser">fetchAwardsForUser</a></li><li data-type='method'><a href="module-Awards.html#~fetchCertificate">fetchCertificate</a></li><li data-type='method'><a href="module-Awards.html#~getAwardDataForGuidedContent">getAwardDataForGuidedContent</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#.getLegacyMethods">getLegacyMethods</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#.getOwnedContent">getOwnedContent</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-Forums.html">Forums</a><ul class='methods'><li data-type='method'><a href="module-Forums.html#~createForumCategory">createForumCategory</a></li><li data-type='method'><a href="module-Forums.html#~createPost">createPost</a></li><li data-type='method'><a href="module-Forums.html#~createThread">createThread</a></li><li data-type='method'><a href="module-Forums.html#~deleteForumCategory">deleteForumCategory</a></li><li data-type='method'><a href="module-Forums.html#~deletePost">deletePost</a></li><li data-type='method'><a href="module-Forums.html#~deleteThread">deleteThread</a></li><li data-type='method'><a href="module-Forums.html#~fetchCommunityGuidelines">fetchCommunityGuidelines</a></li><li data-type='method'><a href="module-Forums.html#~fetchFollowedThreads">fetchFollowedThreads</a></li><li data-type='method'><a href="module-Forums.html#~fetchForumCategories">fetchForumCategories</a></li><li data-type='method'><a href="module-Forums.html#~fetchLatestThreads">fetchLatestThreads</a></li><li data-type='method'><a href="module-Forums.html#~fetchPosts">fetchPosts</a></li><li data-type='method'><a href="module-Forums.html#~fetchThreads">fetchThreads</a></li><li data-type='method'><a href="module-Forums.html#~followThread">followThread</a></li><li data-type='method'><a href="module-Forums.html#~jumpToPost">jumpToPost</a></li><li data-type='method'><a href="module-Forums.html#~likePost">likePost</a></li><li data-type='method'><a href="module-Forums.html#~lockThread">lockThread</a></li><li data-type='method'><a href="module-Forums.html#~markThreadAsRead">markThreadAsRead</a></li><li data-type='method'><a href="module-Forums.html#~pinThread">pinThread</a></li><li data-type='method'><a href="module-Forums.html#~search">search</a></li><li data-type='method'><a href="module-Forums.html#~unfollowThread">unfollowThread</a></li><li data-type='method'><a href="module-Forums.html#~unlikePost">unlikePost</a></li><li data-type='method'><a href="module-Forums.html#~unlockThread">unlockThread</a></li><li data-type='method'><a href="module-Forums.html#~unpinThread">unpinThread</a></li><li data-type='method'><a href="module-Forums.html#~updateForumCategory">updateForumCategory</a></li><li data-type='method'><a href="module-Forums.html#~updatePost">updatePost</a></li><li data-type='method'><a href="module-Forums.html#~updateThread">updateThread</a></li></ul></li><li></li><li></li><li><a href="module-Genre.html">Genre</a><ul class='methods'><li data-type='method'><a href="module-Genre.html#~fetchGenreBySlug">fetchGenreBySlug</a></li><li data-type='method'><a href="module-Genre.html#~fetchGenreLessons">fetchGenreLessons</a></li><li data-type='method'><a href="module-Genre.html#~fetchGenres">fetchGenres</a></li></ul></li><li><a href="module-GuidedCourses.html">GuidedCourses</a></li><li><a href="module-Instructor.html">Instructor</a><ul class='methods'><li data-type='method'><a href="module-Instructor.html#~fetchInstructorBySlug">fetchInstructorBySlug</a></li><li data-type='method'><a href="module-Instructor.html#~fetchInstructorLessons">fetchInstructorLessons</a></li><li data-type='method'><a href="module-Instructor.html#~fetchInstructors">fetchInstructors</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-LearningPaths.html">LearningPaths</a><ul class='methods'><li data-type='method'><a href="module-LearningPaths.html#~completeLearningPathIntroVideo">completeLearningPathIntroVideo</a></li><li data-type='method'><a href="module-LearningPaths.html#~completeMethodIntroVideo">completeMethodIntroVideo</a></li><li data-type='method'><a href="module-LearningPaths.html#~fetchLearningPathLessons">fetchLearningPathLessons</a></li><li data-type='method'><a href="module-LearningPaths.html#~fetchLearningPathProgressCheckLessons">fetchLearningPathProgressCheckLessons</a></li><li data-type='method'><a href="module-LearningPaths.html#~getActivePath">getActivePath</a></li><li data-type='method'><a href="module-LearningPaths.html#~getDailySession">getDailySession</a></li><li data-type='method'><a href="module-LearningPaths.html#~getEnrichedLearningPath">getEnrichedLearningPath</a></li><li data-type='method'><a href="module-LearningPaths.html#~getLearningPathLessonsByIds">getLearningPathLessonsByIds</a></li><li data-type='method'><a href="module-LearningPaths.html#~mapContentToParent">mapContentToParent</a></li><li data-type='method'><a href="module-LearningPaths.html#~resetAllLearningPaths">resetAllLearningPaths</a></li><li data-type='method'><a href="module-LearningPaths.html#~startLearningPath">startLearningPath</a></li><li data-type='method'><a href="module-LearningPaths.html#~updateDailySession">updateDailySession</a></li></ul></li><li><a href="module-Onboarding.html">Onboarding</a><ul class='methods'><li data-type='method'><a href="module-Onboarding.html#~getOnboardingRecommendedContent">getOnboardingRecommendedContent</a></li><li data-type='method'><a href="module-Onboarding.html#~startOnboarding">startOnboarding</a></li><li data-type='method'><a href="module-Onboarding.html#~updateOnboarding">updateOnboarding</a></li><li data-type='method'><a href="module-Onboarding.html#~userOnboardingForBrand">userOnboardingForBrand</a></li></ul></li><li><a href="module-Payments.html">Payments</a><ul class='methods'><li data-type='method'><a href="module-Payments.html#~fetchCustomerPayments">fetchCustomerPayments</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#~deleteItemsFromPlaylist">deleteItemsFromPlaylist</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#~restoreItemFromPlaylist">restoreItemFromPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~unlikePlaylist">unlikePlaylist</a></li></ul></li><li><a href="module-ProgressRow.html">ProgressRow</a></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#.fetchComment">fetchComment</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#.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#.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#.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#.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#.restoreComment">restoreComment</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#.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#.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#.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#.fetchMethodV2IntroVideo">fetchMethodV2IntroVideo</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodV2Structure">fetchMethodV2Structure</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodV2StructureFromId">fetchMethodV2StructureFromId</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#.fetchOwnedContent">fetchOwnedContent</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#.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#.fetchSiblingContent">fetchSiblingContent</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#.fetchTopLevelParentId">fetchTopLevelParentId</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#~getQueryFromPage">getQueryFromPage</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#.restoreUserActivity">restoreUserActivity</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#.blockedUsers">blockedUsers</a></li><li data-type='method'><a href="module-UserManagement.html#.deletePicture">deletePicture</a></li><li data-type='method'><a href="module-UserManagement.html#.getUserData">getUserData</a></li><li data-type='method'><a href="module-UserManagement.html#.getUserSignature">getUserSignature</a></li><li data-type='method'><a href="module-UserManagement.html#.isUsernameAvailable">isUsernameAvailable</a></li><li data-type='method'><a href="module-UserManagement.html#.setUserSignature">setUserSignature</a></li><li data-type='method'><a href="module-UserManagement.html#.toggleSignaturePrivate">toggleSignaturePrivate</a></li><li data-type='method'><a href="module-UserManagement.html#.unblockUser">unblockUser</a></li><li data-type='method'><a href="module-UserManagement.html#.updateDisplayName">updateDisplayName</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-UserMemberships.html">UserMemberships</a><ul class='methods'><li data-type='method'><a href="module-UserMemberships.html#~fetchMemberships">fetchMemberships</a></li><li data-type='method'><a href="module-UserMemberships.html#~fetchRechargeTokens">fetchRechargeTokens</a></li><li data-type='method'><a href="module-UserMemberships.html#~restorePurchases">restorePurchases</a></li><li data-type='method'><a href="module-UserMemberships.html#~upgradeSubscription">upgradeSubscription</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#.fetchLiveEventPollingState">fetchLiveEventPollingState</a></li><li data-type='method'><a href="module-UserNotifications.html#.fetchNotificationSettings">fetchNotificationSettings</a></li><li data-type='method'><a href="module-UserNotifications.html#.fetchNotifications">fetchNotifications</a></li><li data-type='method'><a href="module-UserNotifications.html#.fetchUnreadCount">fetchUnreadCount</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><li data-type='method'><a href="module-UserNotifications.html#.pauseLiveEventPolling">pauseLiveEventPolling</a></li><li data-type='method'><a href="module-UserNotifications.html#.restoreNotification">restoreNotification</a></li><li data-type='method'><a href="module-UserNotifications.html#.startLiveEventPolling">startLiveEventPolling</a></li><li data-type='method'><a href="module-UserNotifications.html#.updateNotificationSetting">updateNotificationSetting</a></li></ul></li><li><a href="module-UserProfile.html">UserProfile</a><ul class='methods'><li data-type='method'><a href="module-UserProfile.html#.deleteProfilePicture">deleteProfilePicture</a></li><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="Forums.html">Forums</a></li><li><a href="Gamification.html">Gamification</a></li><li><a href="UserManagementSystem.html">UserManagementSystem</a></li></ul><h3>Interfaces</h3><ul><li><a href="TestUser.html">TestUser</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createTestUser">createTestUser</a></li></ul>
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
- fetchUserPracticeMeta,
55
- fetchUserPracticeNotes,
56
- fetchHandler,
57
- fetchRecentUserActivities,
58
- } from './railcontent'
59
- import { DataContext, UserActivityVersionKey } from './dataContext.js'
60
- import {
61
- fetchByRailContentId,
62
- fetchByRailContentIds,
63
- fetchMethodV2IntroVideo,
64
- fetchShows,
65
- } from './sanity'
66
- import { fetchPlaylist, fetchUserPlaylists } from './content-org/playlists'
67
- import { guidedCourses } from './content-org/guided-courses'
68
- import {
69
- getMonday,
70
- getWeekNumber,
71
- isSameDate,
72
- isNextDay,
73
- getTimeRemainingUntilLocal,
74
- toDayjs,
75
- } from './dateUtils.js'
76
- import { globalConfig } from './config'
77
- import {
78
- collectionLessonTypes,
79
- progressTypesMapping,
80
- recentTypes,
81
- showsLessonTypes,
82
- songs,
83
- } from '../contentTypeConfig'
84
- import { getAllStartedOrCompleted, getProgressStateByIds } from './contentProgress'
85
- import { TabResponseType } from '../contentMetaData'
86
- import dayjs from 'dayjs'
87
- import { addContextToContent } from './contentAggregator.js'
88
- import { getMethodCard } from './progress-row/method-card.js'
89
- import { db, Q } from './sync'
90
-
91
- const DATA_KEY_PRACTICES = 'practices'
92
-
93
- const DAYS = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
94
-
95
- const streakMessages = {
96
- startStreak: 'Start your streak by taking any lesson!',
97
- restartStreak: 'Restart your streak by taking any lesson!',
98
-
99
- // Messages when last active day is today
100
- dailyStreak: (streak) =>
101
- `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak! Way to keep it going!`,
102
- dailyStreakShort: (streak) =>
103
- `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak!`,
104
- weeklyStreak: (streak) =>
105
- `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep up the momentum!`,
106
- greatJobWeeklyStreak: (streak) =>
107
- `Great job! You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep it going!`,
108
-
109
- // Messages when last active day is NOT today
110
- dailyStreakReminder: (streak) =>
111
- `You have ${getIndefiniteArticle(streak)} ${streak} day streak! Keep it going with any lesson or song!`,
112
- weeklyStreakKeepUp: (streak) =>
113
- `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep up the momentum!`,
114
- weeklyStreakReminder: (streak) =>
115
- `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep it going with any lesson or song!`,
116
- }
117
-
118
- function getIndefiniteArticle(streak) {
119
- return streak === 8 || (streak >= 80 &amp;&amp; streak &lt;= 89) || (streak >= 800 &amp;&amp; streak &lt;= 899)
120
- ? 'an'
121
- : 'a'
122
- }
123
-
124
- async function getUserPractices(userId) {
125
- if (userId === globalConfig.sessionConfig.userId) {
126
- return getOwnPractices()
127
- } else {
128
- return await fetchUserPractices(userId)
129
- }
130
- }
131
-
132
- async function getOwnPractices(...clauses) {
133
- const query = await db.practices.queryAll(...clauses)
134
- const data = query.data.reduce((acc, practice) => {
135
- acc[practice.date] = acc[practice.date] || []
136
- acc[practice.date].push({
137
- id: practice.id,
138
- duration_seconds: practice.duration_seconds,
139
- })
140
- return acc
141
- }, {})
142
-
143
- return data
144
- }
145
-
146
- export let userActivityContext = new DataContext(UserActivityVersionKey, function() {})
147
-
148
- /**
149
- * Retrieves user activity statistics for the current week, including daily activity and streak messages.
150
- *
151
- * @returns {Promise&lt;Object>} - A promise that resolves to an object containing weekly user activity statistics.
152
- *
153
- * @example
154
- * // Retrieve user activity statistics for the current week
155
- * getUserWeeklyStats()
156
- * .then(stats => console.log(stats))
157
- * .catch(error => console.error(error));
158
- */
159
- export async function getUserWeeklyStats() {
160
- const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
161
- const today = dayjs()
162
- const startOfWeek = getMonday(today, timeZone)
163
- const weekDays = Array.from({ length: 7 }, (_, i) => startOfWeek.add(i, 'day').format('YYYY-MM-DD'))
164
-
165
- const practices = await getOwnPractices(
166
- Q.where('date', Q.oneOf(weekDays)),
167
- Q.sortBy('date', 'desc')
168
- )
169
- const practiceDaysSet = new Set(Object.keys(practices))
170
- let dailyStats = []
171
- for (let i = 0; i &lt; 7; i++) {
172
- const day = startOfWeek.add(i, 'day')
173
- const dayStr = day.format('YYYY-MM-DD')
174
- let hasPractice = practiceDaysSet.has(dayStr)
175
- let isActive = isSameDate(today.format(), dayStr)
176
- let type = hasPractice ? 'tracked' : isActive ? 'active' : 'none'
177
- dailyStats.push({
178
- key: i,
179
- label: DAYS[i],
180
- isActive,
181
- inStreak: hasPractice,
182
- type,
183
- day: dayStr,
184
- })
185
- }
186
-
187
- let { streakMessage } = getStreaksAndMessage(practices)
188
-
189
- return { data: { dailyActiveStats: dailyStats, streakMessage, practices } }
190
- }
191
-
192
- /**
193
- * Retrieves user activity statistics for a specified month and user, including daily and weekly activity data.
194
- * If no parameters are provided, defaults to the current year, current month, and the logged-in user.
195
- *
196
- * @param {Object} [params={}] - Parameters for fetching user statistics.
197
- * @param {number} [params.year=new Date().getFullYear()] - The year for which to retrieve the statistics.
198
- * @param {number} [params.month=new Date().getMonth()] - The 0-based month index (0 = January).
199
- * @param {number} [params.day=1] - The starting day (not used for grid calc, but kept for flexibility).
200
- * @param {number} [params.userId=globalConfig.sessionConfig.userId] - The user ID for whom to retrieve stats.
201
- *
202
- * @returns {Promise&lt;Object>} A promise that resolves to an object containing:
203
- * - `dailyActiveStats`: Array of daily activity data for the calendar grid.
204
- * - `weeklyActiveStats`: Array of weekly streak summaries.
205
- * - `practiceDuration`: Total number of seconds practiced in the month.
206
- * - `currentDailyStreak`: Count of consecutive active days.
207
- * - `currentWeeklyStreak`: Count of consecutive active weeks.
208
- * - `daysPracticed`: Total number of active days in the month.
209
- *
210
- * @example
211
- * // Get stats for current user and month
212
- * getUserMonthlyStats().then(console.log);
213
- *
214
- * @example
215
- * // Get stats for March 2024
216
- * getUserMonthlyStats({ year: 2024, month: 2 }).then(console.log);
217
- *
218
- * @example
219
- * // Get stats for another user
220
- * getUserMonthlyStats({ userId: 123 }).then(console.log);
221
- */
222
- export async function getUserMonthlyStats(params = {}) {
223
- const now = dayjs()
224
- const {
225
- year = now.year(),
226
- month = now.month(), // 0-indexed
227
- userId = globalConfig.sessionConfig.userId,
228
- } = params
229
- const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
230
- const practices = await getUserPractices(userId)
231
-
232
- const firstDayOfMonth = dayjs.tz(`${year}-${month + 1}-01`, timeZone).startOf('day')
233
- const endOfMonth = firstDayOfMonth.endOf('month')
234
- const today = dayjs().tz(timeZone).startOf('day')
235
-
236
- let startOfGrid = getMonday(firstDayOfMonth, timeZone)
237
-
238
- // Previous week range
239
- const previousWeekStart = startOfGrid.subtract(7, 'day')
240
- const previousWeekEnd = startOfGrid.subtract(1, 'day')
241
-
242
- let hadStreakBeforeMonth = false
243
- for (let d = previousWeekStart.clone(); d.isSameOrBefore(previousWeekEnd); d = d.add(1, 'day')) {
244
- const key = d.format('YYYY-MM-DD')
245
- if (practices[key]) {
246
- hadStreakBeforeMonth = true
247
- break
248
- }
249
- }
250
-
251
- // let endOfMonth = new Date(year, month + 1, 0)
252
- let endOfGrid = endOfMonth.clone()
253
- while (endOfGrid.day() !== 0) {
254
- endOfGrid = endOfGrid.add(1, 'day')
255
- }
256
- const daysInMonth = endOfGrid.diff(startOfGrid, 'day') + 1
257
- let dailyStats = []
258
- let practiceDuration = 0
259
- let daysPracticed = 0
260
- let weeklyStats = {}
261
-
262
- for (let i = 0; i &lt; daysInMonth; i++) {
263
- let day = startOfGrid.add(i, 'day')
264
- let key = day.format('YYYY-MM-DD')
265
- let activity = practices[key] ?? null
266
- let weekKey = getWeekNumber(day)
267
-
268
- if (!weeklyStats[weekKey]) {
269
- weeklyStats[weekKey] = { key: weekKey, inStreak: false }
270
- }
271
-
272
- if (activity &amp;&amp; day.isBetween(firstDayOfMonth, endOfMonth, null, '[]')) {
273
- practiceDuration += activity.reduce((sum, entry) => sum + entry.duration_seconds, 0)
274
- daysPracticed++
275
- }
276
-
277
- if (activity) {
278
- weeklyStats[weekKey].inStreak = true
279
- }
280
-
281
- const isActive = day.isSame(today, 'day')
282
- const type = activity ? 'tracked' : isActive ? 'active' : 'none'
283
-
284
- dailyStats.push({
285
- key: i,
286
- label: key,
287
- isActive,
288
- inStreak: !!activity,
289
- type,
290
- })
291
- }
292
-
293
- // Continue streak into month
294
- if (hadStreakBeforeMonth) {
295
- const firstWeekKey = getWeekNumber(startOfGrid)
296
- if (weeklyStats[firstWeekKey]) {
297
- weeklyStats[firstWeekKey].continueStreak = true
298
- }
299
- }
300
-
301
- // Filter past practices only
302
- let filteredPractices = Object.entries(practices)
303
- .filter(([date]) => dayjs(date).isSameOrBefore(endOfMonth))
304
- .reduce((acc, [date, val]) => {
305
- acc[date] = val
306
- return acc
307
- }, {})
308
-
309
- const { currentDailyStreak, currentWeeklyStreak } = calculateStreaks(filteredPractices)
310
-
311
- return {
312
- data: {
313
- dailyActiveStats: dailyStats,
314
- weeklyActiveStats: Object.values(weeklyStats),
315
- practiceDuration,
316
- currentDailyStreak,
317
- currentWeeklyStreak,
318
- daysPracticed,
319
- },
320
- }
321
- }
322
-
323
- /**
324
- * Records a manual user practice data, updating the local database and syncing with remote.
325
- *
326
- * @param {Object} practiceDetails - The details of the practice session.
327
- * @param {number} practiceDetails.duration_seconds - The duration of the practice session in seconds.
328
- * @param {string} [practiceDetails.title] - The title of the practice session (max 64 characters).
329
- * @param {number} [practiceDetails.category_id] - The ID of the associated category (if available).
330
- * @param {string} [practiceDetails.thumbnail_url] - The URL of the session's thumbnail (max 255 characters).
331
- * @param {number} [practiceDetails.instrument_id] - The ID of the associated instrument (if available).
332
- * @returns {Promise&lt;Object>} - A promise that resolves to the response from logging the user practice.
333
- *
334
- * @example
335
- * // Record a manual practice session with a title
336
- * recordUserPractice({ title: "Some title", duration_seconds: 300 })
337
- * .then(response => console.log(response))
338
- * .catch(error => console.error(error));
339
- *
340
- */
341
- export async function recordUserPractice(practiceDetails) {
342
- const day = new Date().toLocaleDateString('sv-SE'); // YYYY-MM-DD wall clock date in user's timezone
343
- const durationSeconds = practiceDetails.duration_seconds
344
-
345
- return await db.practices.recordManualPractice(day, durationSeconds, {
346
- title: practiceDetails.title ?? null,
347
- category_id: practiceDetails.category_id ?? null,
348
- thumbnail_url: practiceDetails.thumbnail_url ?? null,
349
- instrument_id: practiceDetails.instrument_id ?? null,
350
- })
351
- }
352
-
353
- export async function trackUserPractice(contentId, incSeconds) {
354
- const day = new Date().toLocaleDateString('sv-SE'); // YYYY-MM-DD wall clock date in user's timezone
355
- return await db.practices.trackAutoPractice(contentId, day, incSeconds);
356
- }
357
-
358
- /**
359
- * Updates a user's practice session with new details and syncs the changes remotely.
360
- *
361
- * @param {string} id - The unique identifier of the practice session to update.
362
- * @param {Object} practiceDetails - The updated details of the practice session.
363
- * @param {number} [practiceDetails.duration_seconds] - The duration of the practice session in seconds.
364
- * @param {number} [practiceDetails.category_id] - The ID of the associated category (if available).
365
- * @param {string} [practiceDetails.title] - The title of the practice session (max 64 characters).
366
- * @param {string} [practiceDetails.thumbnail_url] - The URL of the session's thumbnail (max 255 characters).
367
- * @param {number} [practiceDetails.instrument_id] - The ID of the associated instrument (if available).
368
- * @returns {Promise&lt;Object>} - A promise that resolves to the response from updating the user practice.
369
- *
370
- * @example
371
- * // Update a practice session's duration
372
- * updateUserPractice(123, { duration_seconds: 600 })
373
- * .then(response => console.log(response))
374
- * .catch(error => console.error(error));
375
- *
376
- */
377
- export async function updateUserPractice(id, practiceDetails) {
378
- return await db.practices.updateDetails(id, 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
- return await db.practices.deleteOne(id)
395
- }
396
-
397
- /**
398
- * Restores a previously deleted user's practice session by ID, updating both the local and remote activity context.
399
- *
400
- * @param {number} id - The unique identifier of the practice session to be restored.
401
- * @returns {Promise&lt;Object>} - A promise that resolves to the response containing the restored practice session data.
402
- *
403
- * @example
404
- * // Restore a deleted practice session with ID 123
405
- * restoreUserPractice(123)
406
- * .then(response => console.log("Practice session restored:", response))
407
- * .catch(error => console.error(error));
408
- */
409
- export async function restoreUserPractice(id) {
410
- let url = `/api/user/practices/v1/practices/restore${buildQueryString([id])}`
411
- const response = await fetchHandler(url, 'put')
412
- if (response?.data?.length) {
413
- const restoredPractice = response.data.find((p) => p.id === id)
414
- if (restoredPractice) {
415
- await userActivityContext.updateLocal(async function (localContext) {
416
- if (!localContext.data[DATA_KEY_PRACTICES][restoredPractice.day]) {
417
- localContext.data[DATA_KEY_PRACTICES][restoredPractice.day] = []
418
- }
419
- response.data.forEach((restoredPractice) => {
420
- localContext.data[DATA_KEY_PRACTICES][restoredPractice.day].push({
421
- id: restoredPractice.id,
422
- duration_seconds: restoredPractice.duration_seconds,
423
- })
424
- })
425
- })
426
- }
427
- }
428
- const formattedMeta = await formatPracticeMeta(response.data || [])
429
- const practiceDuration = formattedMeta.reduce(
430
- (total, practice) => total + (practice.duration || 0),
431
- 0
432
- )
433
- return {
434
- data: formattedMeta,
435
- message: response.message,
436
- version: response.version,
437
- practiceDuration,
438
- }
439
- }
440
-
441
- /**
442
- * Deletes all practice sessions for a specific day.
443
- *
444
- * This function retrieves all user practice session IDs for a given day and sends a DELETE request
445
- * to remove them from the server. It also updates the local context to reflect the deletion.
446
- *
447
- * @async
448
- * @param {string} day - The day (in `YYYY-MM-DD` format) for which practice sessions should be deleted.
449
- * @returns {Promise&lt;string[]>} - A promise that resolves once the practice session is removed.
450
- *
451
- * * @example
452
- * // Delete practice sessions for April 10, 2025
453
- * deletePracticeSession("2025-04-10")
454
- * .then(deletedIds => console.log("Deleted sessions:", response))
455
- * .catch(error => console.error("Delete failed:", error));
456
- */
457
- export async function deletePracticeSession(day) {
458
- const ids = await db.practices.queryAllIds(Q.where('date', day))
459
- return await db.practices.deleteSome(ids.data)
460
- }
461
-
462
- /**
463
- * Restores deleted practice sessions for a specific date.
464
- *
465
- * Sends a PUT request to restore any previously deleted practices for a given date.
466
- * If restored practices are returned, they are added back into the local context.
467
- *
468
- * @async
469
- * @param {string} date - The date (in `YYYY-MM-DD` format) for which deleted practice sessions should be restored.
470
- * @returns {Promise&lt;Object>} - The response object from the API, containing practices for selected date.
471
- *
472
- * @example
473
- * // Restore practice sessions deleted on April 10, 2025
474
- * restorePracticeSession("2025-04-10")
475
- * .then(response => console.log("Practice session restored:", response))
476
- * .catch(error => console.error("Restore failed:", error));
477
- */
478
- export async function restorePracticeSession(date) {
479
- const url = `/api/user/practices/v1/practices/restore?date=${date}`
480
- const response = await fetchHandler(url, 'PUT', null)
481
-
482
- if (response?.data) {
483
- await userActivityContext.updateLocal(async function (localContext) {
484
- if (!localContext.data[DATA_KEY_PRACTICES][date]) {
485
- localContext.data[DATA_KEY_PRACTICES][date] = []
486
- }
487
-
488
- response.data.forEach((restoredPractice) => {
489
- localContext.data[DATA_KEY_PRACTICES][date].push({
490
- id: restoredPractice.id,
491
- duration_seconds: restoredPractice.duration_seconds,
492
- })
493
- })
494
- })
495
- }
496
-
497
- const formattedMeta = await formatPracticeMeta(response?.data)
498
- const practiceDuration = formattedMeta.reduce(
499
- (total, practice) => total + (practice.duration || 0),
500
- 0
501
- )
502
-
503
- return { data: formattedMeta, practiceDuration }
504
- }
505
-
506
- /**
507
- * Retrieves and formats a user's practice sessions for a specific day.
508
- *
509
- * @param {Object} params - Parameters for fetching practice sessions.
510
- * @param {string} params.day - The date for which practice sessions should be retrieved (format: YYYY-MM-DD).
511
- * @param {number} [params.userId=globalConfig.sessionConfig.userId] - Optional user ID to retrieve sessions for a specific user. Defaults to the logged-in user.
512
- * @returns {Promise&lt;Object>} - A promise that resolves to an object containing:
513
- * - `practices`: An array of formatted practice session data.
514
- * - `practiceDuration`: Total practice duration (in seconds) for the given day.
515
- *
516
- * @example
517
- * // Get practice sessions for the current user on a specific day
518
- * getPracticeSessions({ day: "2025-03-31" })
519
- * .then(response => console.log(response))
520
- * .catch(error => console.error(error));
521
- *
522
- * @example
523
- * // Get practice sessions for another user
524
- * getPracticeSessions({ day: "2025-03-31", userId: 456 })
525
- * .then(response => console.log(response))
526
- * .catch(error => console.error(error));
527
- */
528
- export async function getPracticeSessions(params = {}) {
529
- const { day, userId = globalConfig.sessionConfig.userId } = params
530
-
531
- let data
532
-
533
- if (userId === globalConfig.sessionConfig.userId) {
534
- const query = await db.practices.queryAll(
535
- Q.where('date', day),
536
- Q.sortBy('created_at', 'asc')
537
- )
538
- data = query.data
539
- } else {
540
- const query = await fetchUserPracticeMeta(day, userId)
541
- data = query.data
542
- }
543
-
544
- if (!data.length) return { data: { practices: [], practiceDuration: 0 } }
545
-
546
- const formattedMeta = await formatPracticeMeta(data)
547
- const practiceDuration = formattedMeta.reduce(
548
- (total, practice) => total + (practice.duration || 0),
549
- 0
550
- )
551
-
552
- return { data: { practices: formattedMeta, practiceDuration } }
553
- }
554
-
555
- /**
556
- * Retrieves user practice notes for a specific day.
557
- *
558
- * @async
559
- * @param {string} day - The day (in `YYYY-MM-DD` format) to fetch practice notes for.
560
- * @returns {Promise&lt;{ data: Object[] }>} - A promise that resolves to an object containing the practice notes.
561
- *
562
- * @example
563
- * // Get notes for April 10, 2025
564
- * getPracticeNotes("2025-04-10")
565
- * .then(({ data }) => console.log("Practice notes:", data))
566
- * .catch(error => console.error("Failed to get notes:", error));
567
- */
568
- export async function getPracticeNotes(date) {
569
- return await db.practiceDayNotes.queryOne(Q.where('date', date))
570
- }
571
-
572
- /**
573
- * Retrieves the user's recent activity.
574
- *
575
- * Returns an object containing recent practice activity.
576
- *
577
- * @async
578
- * @returns {Promise&lt;{ data: Object[] }>} - A promise that resolves to an object containing recent activity items.
579
- *
580
- * @example
581
- * // Fetch recent practice activity
582
- * getRecentActivity()
583
- * .then(({ data }) => console.log("Recent activity:", data))
584
- * .catch(error => console.error("Failed to get recent activity:", error));
585
- */
586
- export async function getRecentActivity({ page = 1, limit = 5, tabName = null } = {}) {
587
- const recentActivityData = await fetchRecentUserActivities({ page, limit, tabName })
588
- const contentIds = recentActivityData.data.map((p) => p.contentId).filter((id) => id !== null)
589
- const contents = await addContextToContent(fetchByRailContentIds, contentIds, {
590
- addNavigateTo: true,
591
- addNextLesson: true,
592
- })
593
- recentActivityData.data = recentActivityData.data.map((practice) => {
594
- const content = contents?.find((c) => c.id === practice.contentId) || {}
595
- return {
596
- ...practice,
597
- parent_id: content.parent_id || null,
598
- navigateTo: content.navigateTo,
599
- }
600
- })
601
- return recentActivityData
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
- return await db.practiceDayNotes.upsertOne(payload.date, r => {
619
- r.date = payload.date
620
- r.notes = payload.notes
621
- })
622
- }
623
-
624
- /**
625
- * Updates existing practice notes for a specific date.
626
- *
627
- * @param {Object} payload - The data required to update practice notes.
628
- * @param {string} payload.date - The date for which to update notes (format: YYYY-MM-DD).
629
- * @param {string} payload.notes - The updated notes content.
630
- * @returns {Promise&lt;Object>} - A promise that resolves to the API response after updating the notes.
631
- *
632
- * @example
633
- * updatePracticeNotes({ date: '2025-04-10', notes: 'Updated: Focused on technique and timing' })
634
- * .then(response => console.log(response))
635
- * .catch(error => console.error(error));
636
- */
637
- export async function updatePracticeNotes(payload) {
638
- return await db.practiceDayNotes.updateOneId(payload.date, r => {
639
- r.notes = payload.notes
640
- })
641
- }
642
-
643
- function getStreaksAndMessage(practices) {
644
- let { currentDailyStreak, currentWeeklyStreak, streakMessage } = calculateStreaks(practices, true)
645
-
646
- return {
647
- currentDailyStreak,
648
- currentWeeklyStreak,
649
- streakMessage,
650
- }
651
- }
652
-
653
- function buildQueryString(ids, paramName = 'practice_ids') {
654
- if (!ids.length) return ''
655
- return '?' + ids.map((id) => `${paramName}[]=${id}`).join('&amp;')
656
- }
657
-
658
- // Helper: Calculate streaks
659
- function calculateStreaks(practices, includeStreakMessage = false) {
660
- let currentDailyStreak = 0
661
- let currentWeeklyStreak = 0
662
- let lastActiveDay = null
663
- let streakMessage = ''
664
-
665
- const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
666
- let sortedPracticeDays = Object.keys(practices)
667
- .map((dateStr) => {
668
- const [year, month, day] = dateStr.split('-').map(Number)
669
- const newDate = new Date()
670
- newDate.setFullYear(year, month - 1, day)
671
- return newDate
672
- })
673
- .sort((a, b) => a - b)
674
- if (sortedPracticeDays.length === 0) {
675
- return {
676
- currentDailyStreak: 0,
677
- currentWeeklyStreak: 0,
678
- streakMessage: streakMessages.startStreak,
679
- }
680
- }
681
- lastActiveDay = sortedPracticeDays[sortedPracticeDays.length - 1]
682
-
683
- let dailyStreak = 0
684
- let prevDay = null
685
- sortedPracticeDays.forEach((currentDay) => {
686
- if (prevDay === null || isNextDay(prevDay, currentDay)) {
687
- dailyStreak++
688
- } else {
689
- dailyStreak = 1
690
- }
691
- prevDay = currentDay
692
- })
693
- currentDailyStreak = dailyStreak
694
-
695
- // Weekly streak calculation
696
- let weekNumbers = new Set(sortedPracticeDays.map((date) => getWeekNumber(date)))
697
- let weeklyStreak = 0
698
- let lastWeek = null
699
- ;[...weekNumbers]
700
- .sort((a, b) => b - a)
701
- .forEach((week) => {
702
- if (lastWeek === null || week === lastWeek - 1) {
703
- weeklyStreak++
704
- } else {
705
- return
706
- }
707
- lastWeek = week
708
- })
709
- currentWeeklyStreak = weeklyStreak
710
-
711
- // Calculate streak message only if includeStreakMessage is true
712
- if (includeStreakMessage) {
713
- let today = new Date()
714
- let yesterday = new Date(today)
715
- yesterday.setDate(today.getDate() - 1)
716
-
717
- let currentWeekStart = getMonday(today, timeZone)
718
- let lastWeekStart = currentWeekStart.subtract(7, 'days')
719
-
720
- let hasYesterdayPractice = sortedPracticeDays.some((date) => isSameDate(date, yesterday))
721
- let hasCurrentWeekPractice = sortedPracticeDays.some((date) => date >= currentWeekStart)
722
- let hasCurrentWeekPreviousPractice = sortedPracticeDays.some(
723
- (date) => date >= currentWeekStart &amp;&amp; date &lt; today
724
- )
725
- let hasLastWeekPractice = sortedPracticeDays.some(
726
- (date) => date >= lastWeekStart &amp;&amp; date &lt; currentWeekStart
727
- )
728
- let hasOlderPractice = sortedPracticeDays.some((date) => date &lt; lastWeekStart)
729
-
730
- if (isSameDate(lastActiveDay, today)) {
731
- if (hasYesterdayPractice) {
732
- streakMessage = streakMessages.dailyStreak(currentDailyStreak)
733
- } else if (hasCurrentWeekPreviousPractice) {
734
- streakMessage = streakMessages.weeklyStreak(currentWeeklyStreak)
735
- } else if (hasLastWeekPractice) {
736
- streakMessage = streakMessages.greatJobWeeklyStreak(currentWeeklyStreak)
737
- } else {
738
- streakMessage = streakMessages.dailyStreakShort(currentDailyStreak)
739
- }
740
- } else {
741
- if (
742
- (hasYesterdayPractice &amp;&amp; currentDailyStreak >= 2) ||
743
- (hasYesterdayPractice &amp;&amp; sortedPracticeDays.length == 1) ||
744
- (hasYesterdayPractice &amp;&amp; !hasLastWeekPractice &amp;&amp; hasOlderPractice)
745
- ) {
746
- streakMessage = streakMessages.dailyStreakReminder(currentDailyStreak)
747
- } else if (hasCurrentWeekPractice) {
748
- streakMessage = streakMessages.weeklyStreakKeepUp(currentWeeklyStreak)
749
- } else if (hasLastWeekPractice) {
750
- streakMessage = streakMessages.weeklyStreakReminder(currentWeeklyStreak)
751
- } else {
752
- streakMessage = streakMessages.restartStreak
753
- }
754
- }
755
- }
756
-
757
- return { currentDailyStreak, currentWeeklyStreak, streakMessage }
758
- }
759
-
760
- /**
761
- * Calculates the longest daily, weekly streaks and totalPracticeSeconds from user practice dates.
762
- * @returns {{ longestDailyStreak: number, longestWeeklyStreak: number, totalPracticeSeconds:number }}
763
- */
764
- export async function calculateLongestStreaks(userId = globalConfig.sessionConfig.userId) {
765
- let practices = await getUserPractices(userId)
766
- let totalPracticeSeconds = 0
767
- // Calculate total practice duration
768
- for (const date in practices) {
769
- for (const entry of practices[date]) {
770
- totalPracticeSeconds += entry.duration_seconds
771
- }
772
- }
773
-
774
- let practiceDates = Object.keys(practices)
775
- .map((dateStr) => {
776
- const [y, m, d] = dateStr.split('-').map(Number)
777
- const newDate = new Date()
778
- newDate.setFullYear(y, m - 1, d)
779
- return newDate
780
- })
781
- .sort((a, b) => a - b)
782
-
783
- if (!practiceDates || practiceDates.length === 0) {
784
- return { longestDailyStreak: 0, longestWeeklyStreak: 0, totalPracticeSeconds: 0 }
785
- }
786
-
787
- // Normalize to Date objects
788
- const normalizedDates = [
789
- ...new Set(
790
- practiceDates.map((d) => {
791
- const date = new Date(d)
792
- date.setHours(0, 0, 0, 0)
793
- return date.getTime()
794
- })
795
- ),
796
- ].sort((a, b) => a - b)
797
-
798
- // ----- Daily Streak -----
799
- let longestDailyStreak = 1
800
- let currentDailyStreak = 1
801
- for (let i = 1; i &lt; normalizedDates.length; i++) {
802
- const diffInDays = (normalizedDates[i] - normalizedDates[i - 1]) / (1000 * 60 * 60 * 24)
803
- if (diffInDays === 1) {
804
- currentDailyStreak++
805
- longestDailyStreak = Math.max(longestDailyStreak, currentDailyStreak)
806
- } else {
807
- currentDailyStreak = 1
808
- }
809
- }
810
-
811
- // ----- Weekly Streak -----
812
- const weekStartDates = [
813
- ...new Set(
814
- normalizedDates.map((ts) => {
815
- const d = new Date(ts)
816
- const day = d.getDay()
817
- const diff = d.getDate() - day + (day === 0 ? -6 : 1) // adjust to Monday
818
- d.setDate(diff)
819
- return d.getTime() // timestamp for Monday
820
- })
821
- ),
822
- ].sort((a, b) => a - b)
823
-
824
- let longestWeeklyStreak = 1
825
- let currentWeeklyStreak = 1
826
-
827
- for (let i = 1; i &lt; weekStartDates.length; i++) {
828
- const diffInWeeks = (weekStartDates[i] - weekStartDates[i - 1]) / (1000 * 60 * 60 * 24 * 7)
829
- if (diffInWeeks === 1) {
830
- currentWeeklyStreak++
831
- longestWeeklyStreak = Math.max(longestWeeklyStreak, currentWeeklyStreak)
832
- } else {
833
- currentWeeklyStreak = 1
834
- }
835
- }
836
-
837
- return {
838
- longestDailyStreak,
839
- longestWeeklyStreak,
840
- totalPracticeSeconds,
841
- }
842
- }
843
-
844
- async function formatPracticeMeta(practices = []) {
845
- const contentIds = practices.map((p) => p.content_id).filter((id) => id !== null)
846
- const contents = await addContextToContent(fetchByRailContentIds, contentIds, {
847
- addNavigateTo: true,
848
- addNextLesson: true,
849
- })
850
-
851
- return practices.map((practice) => {
852
- const content =
853
- contents &amp;&amp; contents.length > 0 ? contents.find((c) => c.id === practice.content_id) : {}
854
-
855
- return {
856
- id: practice.id,
857
- auto: practice.auto,
858
- thumbnail: practice.content_id ? content.thumbnail : practice.thumbnail_url || '',
859
- thumbnail_url: practice.content_id ? content.thumbnail : practice.thumbnail_url || '',
860
- duration: practice.duration_seconds || 0,
861
- duration_seconds: practice.duration_seconds || 0,
862
- content_url: content?.url || null,
863
- title: practice.content_id ? content.title : practice.title,
864
- category_id: practice.category_id,
865
- instrument_id: practice.instrument_id,
866
- content_type: getFormattedType(content?.type || '', content?.brand || null),
867
- content_id: practice.content_id || null,
868
- content_brand: content?.brand || null,
869
- created_at: dayjs(practice.created_at),
870
- sanity_type: content?.type || null,
871
- content_slug: content?.slug || null,
872
- parent_id: content?.parent_id || null,
873
- navigateTo: content?.navigateTo || null,
874
- }
875
- })
876
- }
877
-
878
- /**
879
- * Records a new user activity in the system.
880
- *
881
- * @param {Object} payload - The data representing the user activity.
882
- * @param {number} payload.user_id - The ID of the user.
883
- * @param {string} payload.action - The type of action (e.g., 'start', 'complete', 'comment', etc.).
884
- * @param {string} payload.brand - The brand associated with the activity.
885
- * @param {string} payload.type - The content type (e.g., 'lesson', 'song', etc.).
886
- * @param {number} payload.content_id - The ID of the related content.
887
- * @param {string} payload.date - The date of the activity (ISO format).
888
- * @returns {Promise&lt;Object>} - A promise that resolves to the API response after recording the activity.
889
- *
890
- * @example
891
- * recordUserActivity({
892
- * user_id: 123,
893
- * action: 'start',
894
- * brand: 'pianote',
895
- * type: 'lesson',
896
- * content_id: 4561,
897
- * date: '2025-05-15'
898
- * }).then(response => console.log(response))
899
- * .catch(error => console.error(error));
900
- */
901
- export async function recordUserActivity(payload) {
902
- const url = `/api/user-management-system/v1/activities`
903
- return await fetchHandler(url, 'POST', null, payload)
904
- }
905
-
906
- /**
907
- * Deletes a specific user activity by its ID.
908
- *
909
- * @param {number|string} id - The ID of the user activity to delete.
910
- * @returns {Promise&lt;Object>} - A promise that resolves to the API response after deletion.
911
- *
912
- * @example
913
- * deleteUserActivity(789)
914
- * .then(response => console.log('Deleted:', response))
915
- * .catch(error => console.error(error));
916
- */
917
- export async function deleteUserActivity(id) {
918
- const url = `/api/user-management-system/v1/activities/${id}`
919
- return await fetchHandler(url, 'DELETE')
920
- }
921
-
922
- /**
923
- * Restores a specific user activity by its ID.
924
- *
925
- * @param {number|string} id - The ID of the user activity to restore.
926
- * @returns {Promise&lt;Object>} - A promise that resolves to the API response after restoration.
927
- *
928
- * @example
929
- * restoreUserActivity(789)
930
- * .then(response => console.log('Restored:', response))
931
- * .catch(error => console.error(error));
932
- */
933
- export async function restoreUserActivity(id) {
934
- const url = `/api/user-management-system/v1/activities/${id}`
935
- return await fetchHandler(url, 'POST')
936
- }
937
-
938
- async function extractPinnedItemsAndSortAllItems(
939
- userPinnedItem,
940
- contentsMap,
941
- eligiblePlaylistItems,
942
- methodCard,
943
- //method contents
944
- limit
945
- ) {
946
- let pinnedItem = await popPinnedItemFromContentsOrPlaylistMap(
947
- userPinnedItem,
948
- contentsMap,
949
- eligiblePlaylistItems,
950
- methodCard
951
- )
952
-
953
- let combined = []
954
-
955
- if (pinnedItem) {
956
- pinnedItem.pinned = true
957
- combined.push(pinnedItem)
958
- }
959
-
960
- if (!(pinnedItem &amp;&amp; pinnedItem.progressType === 'method')) {
961
- combined.push(methodCard)
962
- }
963
-
964
- const progressList = Array.from(contentsMap.values())
965
- //need another for methodContents?
966
-
967
- combined = [...combined, ...progressList, ...eligiblePlaylistItems]
968
- return mergeAndSortItems(combined, limit)
969
- }
970
-
971
- function generateContentsMap(contents, playlistsContents) {
972
- const excludedTypes = new Set(['pack-bundle', 'guided-course-part'])
973
- const existingShows = new Set()
974
- const contentsMap = new Map()
975
- const childToParentMap = {}
976
- if (!contents) return contentsMap
977
- contents.forEach((content) => {
978
- if (Array.isArray(content.parent_content_data) &amp;&amp; content.parent_content_data.length > 0) {
979
- childToParentMap[content.id] =
980
- content.parent_content_data[content.parent_content_data.length - 1]
981
- }
982
- })
983
-
984
- const allRecentTypeSet = new Set(Object.values(recentTypes).flat())
985
- contents.forEach((content) => {
986
- const id = content.id
987
- const type = content.type
988
- if (
989
- excludedTypes.has(type) ||
990
- (!allRecentTypeSet.has(type) &amp;&amp; !showsLessonTypes.includes(type))
991
- )
992
- return
993
- if (!childToParentMap[id]) {
994
- // Shows don't have a parent to link them, but need to be handled as if they're a set of children
995
- if (!existingShows.has(type)) {
996
- contentsMap.set(id, content)
997
- }
998
- if (showsLessonTypes.includes(type)) {
999
- existingShows.add(type)
1000
- }
1001
- }
1002
- })
1003
-
1004
- if (playlistsContents) {
1005
- for (const item of playlistsContents) {
1006
- const contentId = item.id
1007
- contentsMap.delete(contentId)
1008
- const parentIds = item.parent_content_data || []
1009
- parentIds.forEach((id) => contentsMap.delete(id))
1010
- }
1011
- }
1012
-
1013
- return contentsMap
1014
- }
1015
-
1016
- /**
1017
- * Fetches and combines recent user progress rows and playlists, excluding certain types and parents.
1018
- *
1019
- * @param {Object} [options={}] - Options for fetching progress rows.
1020
- * @param {string|null} [options.brand=null] - The brand context for progress data.
1021
- * @param {number} [options.limit=8] - Maximum number of progress rows to return.
1022
- * @returns {Promise&lt;Object>} - A promise that resolves to an object containing progress rows formatted for UI.
1023
- *
1024
- * @example
1025
- * getProgressRows({ brand: 'drumeo', limit: 10 })
1026
- * .then(data => console.log(data))
1027
- * .catch(error => console.error(error));
1028
- */
1029
- export async function getProgressRows({ brand = 'drumeo', limit = 8 } = {}) {
1030
- // TODO slice progress to a reasonable number, say 100
1031
- const methodCardPromise = getMethodCard(brand)
1032
- const [recentPlaylists, progressContents, userPinnedItem] = await Promise.all([
1033
- fetchUserPlaylists(brand, { sort: '-last_progress', limit: limit }),
1034
- getAllStartedOrCompleted({ onlyIds: false, brand: brand, limit }),
1035
- getUserPinnedItem(brand),
1036
- ])
1037
-
1038
- const playlists = recentPlaylists?.data || []
1039
- const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists)
1040
- const playlistEngagedOnContents = eligiblePlaylistItems.map(
1041
- (item) => item.playlist.last_engaged_on
1042
- )
1043
-
1044
- // todo post v2: refactor this once we migrate playlist progress tracking to new system
1045
- const nonPlaylistContentIds = Object.keys(progressContents)
1046
- if (userPinnedItem?.progressType === 'content') {
1047
- nonPlaylistContentIds.push(userPinnedItem.id)
1048
- }
1049
- //need to update addContextToContent to accept collection info
1050
- const [playlistsContents, contents] = await Promise.all([
1051
- (playlistEngagedOnContents.length > 0)
1052
- ? addContextToContent(fetchByRailContentIds, playlistEngagedOnContents, 'progress-tracker', {
1053
- addNavigateTo: true,
1054
- addProgressStatus: true,
1055
- addProgressPercentage: true,
1056
- addProgressTimestamp: true,
1057
- })
1058
- : Promise.resolve([]),
1059
- (nonPlaylistContentIds.length > 0)
1060
- ? addContextToContent(
1061
- fetchByRailContentIds,
1062
- nonPlaylistContentIds,
1063
- 'progress-tracker',
1064
- brand,
1065
- {
1066
- addNavigateTo: true,
1067
- addProgressStatus: true,
1068
- addProgressPercentage: true,
1069
- addProgressTimestamp: true,
1070
- }
1071
- )
1072
- : Promise.resolve([]),
1073
- ])
1074
-
1075
- const contentsMap = generateContentsMap(contents, playlistsContents)
1076
- const methodCard = await methodCardPromise
1077
- let combined = await extractPinnedItemsAndSortAllItems(
1078
- userPinnedItem,
1079
- contentsMap,
1080
- eligiblePlaylistItems,
1081
- methodCard,
1082
- limit
1083
- )
1084
- const results = await Promise.all(
1085
- combined.slice(0, limit).map((item) => {
1086
- switch (item.type) {
1087
- case 'playlist':
1088
- return processPlaylistItem(item)
1089
- case 'learning-path-v2':
1090
- case 'method':
1091
- return item
1092
- default:
1093
- return processContentItem(item)
1094
- }
1095
- })
1096
- )
1097
- return {
1098
- type: TabResponseType.PROGRESS_ROWS,
1099
- displayBrowseAll: combined.length > limit,
1100
- data: results,
1101
- }
1102
- }
1103
-
1104
- async function getUserPinnedItem(brand) {
1105
- const userRaw = await globalConfig.localStorage.getItem('user')
1106
- const user = userRaw ? JSON.parse(userRaw) : {}
1107
- user.brand_pinned_progress = user.brand_pinned_progress || {}
1108
- return user.brand_pinned_progress[brand] ?? null
1109
- }
1110
-
1111
- async function processContentItem(content) {
1112
- const contentType = getFormattedType(content.type, content.brand)
1113
- const isLive = content.isLive ?? false
1114
- let ctaText = getDefaultCTATextForContent(content, contentType)
1115
-
1116
- content.completed_children = await getCompletedChildren(content, contentType)
1117
-
1118
- if (content.type === 'guided-course') {
1119
- const nextLessonPublishedOn = content.children.find(
1120
- (child) => child.id === content.navigateTo.id
1121
- )?.published_on
1122
- let isLocked = new Date(nextLessonPublishedOn) > new Date()
1123
- if (isLocked) {
1124
- content.is_locked = true
1125
- const timeRemaining = getTimeRemainingUntilLocal(nextLessonPublishedOn, {
1126
- withTotalSeconds: true,
1127
- })
1128
- content.time_remaining_seconds = timeRemaining.totalSeconds
1129
- ctaText = 'Next lesson in ' + timeRemaining.formatted
1130
- } else if (
1131
- !content.progressStatus ||
1132
- content.progressStatus === 'not-started' ||
1133
- content.progressPercentage === 0
1134
- ) {
1135
- ctaText = 'Start Course'
1136
- }
1137
- }
1138
-
1139
- if (contentType === 'show') {
1140
- const shows = await fetchShows(content.brand, content.type)
1141
- const showIds = shows.map((item) => item.id)
1142
- const progressOnItems = await getProgressStateByIds(showIds)
1143
- const completedShows = content.completed_children
1144
- const progressTimestamp = content.progressTimestamp
1145
- const wasPinned = content.pinned ?? false
1146
- if (content.progressStatus === 'completed') {
1147
- // this could be handled more gracefully if their was a parent content type for shows
1148
- const nextByProgress = findIncompleteLesson(progressOnItems, content.id, content.type)
1149
- content = shows.find((lesson) => lesson.id === nextByProgress)
1150
- content.completed_children = completedShows
1151
- content.progressTimestamp = progressTimestamp
1152
- content.progressTimestamp = progressTimestamp
1153
- content.pinned = wasPinned
1154
- }
1155
- content.child_count = shows.length
1156
- content.progressPercentage = Math.round((completedShows / shows.length) * 100)
1157
- if (completedShows === shows.length) {
1158
- ctaText = 'Revisit Show'
1159
- }
1160
- }
1161
- console.log('Progress Timestamp', content.progressTimestamp)
1162
- return {
1163
- id: content.id,
1164
- progressType: 'content',
1165
- header: contentType,
1166
- pinned: content.pinned ?? false,
1167
- content: content,
1168
- body: {
1169
- progressPercent: isLive ? undefined : content.progressPercentage,
1170
- thumbnail: content.thumbnail,
1171
- title: content.title,
1172
- isLive: isLive,
1173
- badge: content.badge ?? null,
1174
- isLocked: content.is_locked ?? false,
1175
- subtitle:
1176
- collectionLessonTypes.includes(content.type) || content.lesson_count > 1
1177
- ? `${content.completed_children} of ${content.lesson_count ?? content.child_count} Lessons Complete`
1178
- : contentType === 'lesson' &amp;&amp; isLive === false
1179
- ? `${content.progressPercentage}% Complete`
1180
- : `${content.difficulty_string} • ${content.artist_name}`,
1181
- },
1182
- cta: {
1183
- text: ctaText,
1184
- timeRemainingToUnlockSeconds: content.time_remaining_seconds ?? null,
1185
- action: {
1186
- type: content.type,
1187
- brand: content.brand,
1188
- id: content.id,
1189
- slug: content.slug,
1190
- child: content.navigateTo,
1191
- },
1192
- },
1193
- // *1000 is to match playlists which are saved in millisecond accuracy
1194
- progressTimestamp: content.progressTimestamp * 1000,
1195
- }
1196
- }
1197
-
1198
- function getDefaultCTATextForContent(content, contentType) {
1199
- let ctaText = 'Continue'
1200
- if (content.progressStatus === 'completed') {
1201
- if (
1202
- contentType === songs[content.brand] ||
1203
- contentType === 'play along' ||
1204
- contentType === 'jam track'
1205
- )
1206
- ctaText = 'Replay Song'
1207
- if (contentType === 'lesson') ctaText = 'Revisit Lesson'
1208
- if (contentType === 'song tutorial' || collectionLessonTypes.includes(content.type))
1209
- ctaText = 'Revisit Lessons'
1210
- if (contentType === 'pack') ctaText = 'View Lessons'
1211
- }
1212
- return ctaText
1213
- }
1214
-
1215
- async function getCompletedChildren(content, contentType) {
1216
- let completedChildren = null
1217
- if (contentType === 'show') {
1218
- const shows = await addContextToContent(fetchShows, content.brand, content.type, {
1219
- addProgressStatus: true,
1220
- })
1221
- completedChildren = Object.values(shows).filter(
1222
- (show) => show.progressStatus === 'completed'
1223
- ).length
1224
- } else if (content.lesson_count > 0) {
1225
- const lessonIds = getLeafNodes(content)
1226
- const progressOnItems = await getProgressStateByIds(lessonIds)
1227
- completedChildren = Object.values(progressOnItems).filter(
1228
- (value) => value === 'completed'
1229
- ).length
1230
- }
1231
- return completedChildren
1232
- }
1233
-
1234
- async function processPlaylistItem(item) {
1235
- const playlist = item.playlist
1236
-
1237
- return {
1238
- id: playlist.id,
1239
- progressType: 'playlist',
1240
- header: 'playlist',
1241
- pinned: item.pinned ?? false,
1242
- playlist: playlist,
1243
- body: {
1244
- first_items_thumbnail_url: playlist.first_items_thumbnail_url,
1245
- title: playlist.name,
1246
- subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`,
1247
- total_items: playlist.total_items,
1248
- },
1249
- progressTimestamp: item.progressTimestamp,
1250
- cta: {
1251
- text: 'Continue',
1252
- action: {
1253
- brand: playlist.brand,
1254
- item_id: playlist.navigateTo.id ?? null,
1255
- content_id: playlist.navigateTo.content_id ?? null,
1256
- type: 'playlists',
1257
- // TODO depreciated, maintained to avoid breaking changes
1258
- id: playlist.id,
1259
- },
1260
- },
1261
- }
1262
- }
1263
-
1264
- const getFormattedType = (type, brand) => {
1265
- for (const [key, values] of Object.entries(progressTypesMapping)) {
1266
- if (values.includes(type)) {
1267
- return key === 'songs' ? songs[brand] : key
1268
- }
1269
- }
1270
-
1271
- return null
1272
- }
1273
-
1274
- function getLeafNodes(content) {
1275
- const ids = []
1276
- function traverse(children) {
1277
- for (const item of children) {
1278
- if (item.children) {
1279
- traverse(item.children) // Recursively handle nested lessons
1280
- } else if (item.id) {
1281
- ids.push(item.id)
1282
- }
1283
- }
1284
- }
1285
- if (content &amp;&amp; Array.isArray(content.children)) {
1286
- traverse(content.children)
1287
- }
1288
- return ids
1289
- }
1290
-
1291
- async function getEligiblePlaylistItems(playlists) {
1292
- const eligible = playlists.filter((p) => p.last_progress &amp;&amp; p.last_engaged_on)
1293
- return Promise.all(
1294
- eligible.map(async (p) => {
1295
- const utcDate = new Date(p.last_progress.replace(' ', 'T') + 'Z')
1296
- const timestamp = utcDate.getTime()
1297
- return {
1298
- type: 'playlist',
1299
- // Content timestamps are millisecond accurate so for comparison we bring this to the same resolution
1300
- progressTimestamp: timestamp / 1000,
1301
- playlist: p,
1302
- id: p.id,
1303
- }
1304
- })
1305
- )
1306
- }
1307
-
1308
- function mergeAndSortItems(items, limit) {
1309
- const seen = new Set()
1310
- const deduped = []
1311
-
1312
- for (const item of items) {
1313
- const key = `${item.id}-${item.type}`
1314
- if (!seen.has(key)) {
1315
- seen.add(key)
1316
- deduped.push(item)
1317
- }
1318
- }
1319
-
1320
- return deduped
1321
- .filter((item) => typeof item.progressTimestamp === 'number' &amp;&amp; item.progressTimestamp >= 0)
1322
- .sort((a, b) => {
1323
- if (a.pinned &amp;&amp; !b.pinned) return -1
1324
- if (!a.pinned &amp;&amp; b.pinned) return 1
1325
- return b.progressTimestamp - a.progressTimestamp
1326
- })
1327
- .slice(0, limit + 5)
1328
- }
1329
-
1330
- export function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
1331
- const ids = Object.keys(progressOnItems).map(Number)
1332
- if (contentType === 'guided-course' || contentType === 'learning-path-v2') {
1333
- // Return first incomplete lesson
1334
- return ids.find((id) => progressOnItems[id] !== 'completed') || ids.at(0)
1335
- }
1336
-
1337
- // For other types, find next incomplete after current
1338
- const currentIndex = ids.indexOf(Number(currentContentId))
1339
- if (currentIndex === -1) return null
1340
-
1341
- for (let i = currentIndex + 1; i &lt; ids.length; i++) {
1342
- const id = ids[i]
1343
- if (progressOnItems[id] !== 'completed') {
1344
- return id
1345
- }
1346
- }
1347
-
1348
- return ids[0]
1349
- }
1350
-
1351
- async function popPinnedItemFromContentsOrPlaylistMap(
1352
- pinned,
1353
- contentsMap,
1354
- playlistItems,
1355
- methodCard
1356
- ) {
1357
- if (!pinned) return null
1358
- const { id, pinnedAt } = pinned
1359
- let item = null
1360
- const progressType = pinned.progressType ?? pinned.type
1361
-
1362
- if (progressType === 'content') {
1363
- const pinnedId = parseInt(id)
1364
- if (contentsMap.has(pinnedId)) {
1365
- item = contentsMap.get(pinnedId)
1366
- contentsMap.delete(pinnedId)
1367
- } else {
1368
- // we use fetchByRailContentIds so that we don't have the _type restriction in the query
1369
- let data = await fetchByRailContentIds([id], 'progress-tracker')
1370
- item = await addContextToContent(() => data[0] ?? null, {
1371
- addNextLesson: true,
1372
- addNavigateTo: true,
1373
- addProgressStatus: true,
1374
- addProgressPercentage: true,
1375
- addProgressTimestamp: true,
1376
- })
1377
- }
1378
- }
1379
- if (progressType === 'playlist') {
1380
- const pinnedPlaylist = playlistItems.find((p) => p.playlist.id === id)
1381
- if (pinnedPlaylist) {
1382
- playlistItems = playlistItems.filter((p) => p.playlist.id !== id)
1383
- item = pinnedPlaylist
1384
- } else {
1385
- const playlist = await fetchPlaylist(id)
1386
- item = {
1387
- id: id,
1388
- playlist: playlist,
1389
- type: 'playlist',
1390
- progressTimestamp: new Date(pinnedAt).getTime(),
1391
- }
1392
- }
1393
- }
1394
- if (progressType === 'method') {
1395
- // simply get method card and return
1396
- item = methodCard
1397
- //todo remove method card
1398
- }
1399
- return item
1400
- }
1401
-
1402
- function popContentAndRemoveChildrenFromContentsMap(content, contentsMap) {
1403
- if (!content.children || content.children.length === 0) {
1404
- console.warn(`content ${content.id} has no children`, content)
1405
- } else {
1406
- const children = content.children.map((child) => child.id)
1407
- if (contentsMap.has(content.id)) {
1408
- contentsMap.delete(content.id)
1409
- }
1410
- children.forEach((child) => {
1411
- if (contentsMap.has(child)) {
1412
- contentsMap.delete(child)
1413
- }
1414
- })
1415
- }
1416
- return contentsMap
1417
- }
1418
-
1419
- /**
1420
- * Pins a specific progress row for a user, scoped by brand.
1421
- *
1422
- * @param {string} brand - The brand context for the pin action.
1423
- * @param {number|string} id - The ID of the progress item to pin.
1424
- * @param {string} progressType - The type of progress (e.g., 'content', 'playlist').
1425
- * @returns {Promise&lt;Object>} - A promise resolving to the response from the pin API.
1426
- *
1427
- * @example
1428
- * pinProgressRow('drumeo', 12345, 'content')
1429
- * .then(response => console.log(response))
1430
- * .catch(error => console.error(error));
1431
- */
1432
- export async function pinProgressRow(brand, id, progressType) {
1433
- const url = `/api/user-management-system/v1/progress/pin?brand=${brand}&amp;id=${id}&amp;progressType=${progressType}`
1434
- const response = await fetchHandler(url, 'PUT', null)
1435
- if (response &amp;&amp; !response.error) {
1436
- await updateUserPinnedProgressRow(brand, {
1437
- id,
1438
- progressType,
1439
- pinnedAt: new Date().toISOString(),
1440
- })
1441
- }
1442
- return response
1443
- }
1444
- /**
1445
- * Unpins the current pinned progress row for a user, scoped by brand.
1446
- *
1447
- * @param {string} brand - The brand context for the unpin action.
1448
- * @returns {Promise&lt;Object>} - A promise resolving to the response from the unpin API.
1449
- *
1450
- * @example
1451
- * unpinProgressRow('drumeo', 123456)
1452
- * .then(response => console.log(response))
1453
- * .catch(error => console.error(error));
1454
- */
1455
- export async function unpinProgressRow(brand) {
1456
- const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}`
1457
- const response = await fetchHandler(url, 'PUT', null)
1458
- if (response &amp;&amp; !response.error) {
1459
- await updateUserPinnedProgressRow(brand, null)
1460
- }
1461
- return response
1462
- }
1463
-
1464
- async function updateUserPinnedProgressRow(brand, pinnedData) {
1465
- const userRaw = await globalConfig.localStorage.getItem('user')
1466
- const user = userRaw ? JSON.parse(userRaw) : {}
1467
- user.brand_pinned_progress = user.brand_pinned_progress || {}
1468
- user.brand_pinned_progress[brand] = pinnedData
1469
- await globalConfig.localStorage.setItem('user', JSON.stringify(user))
1470
- }
1471
-
1472
- export async function fetchRecentActivitiesActiveTabs() {
1473
- const url = `/api/user-management-system/v1/activities/tabs`
1474
- try {
1475
- const tabs = await fetchHandler(url, 'GET')
1476
- const activitiesTabs = []
1477
-
1478
- tabs.forEach((tab) => {
1479
- activitiesTabs.push({ name: tab.label, short_name: tab.label })
1480
- })
1481
-
1482
- return activitiesTabs
1483
- } catch (error) {
1484
- console.error('Error fetching activity tabs:', error)
1485
- return []
1486
- }
1487
- }
1488
- </code></pre>
1489
- </article>
1490
- </section>
1491
-
1492
-
1493
-
1494
-
1495
-
1496
-
1497
- </div>
1498
-
1499
- <br class="clear">
1500
-
1501
- <footer>
1502
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.3</a> on Fri Nov 28 2025 11:22:56 GMT-0800 (Pacific Standard Time) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1503
- </footer>
1504
-
1505
- <script>prettyPrint();</script>
1506
- <script src="scripts/polyfill.js"></script>
1507
- <script src="scripts/linenumber.js"></script>
1508
-
1509
-
1510
-
1511
- </body>
1512
- </html>