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