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.
- package/CHANGELOG.md +40 -0
- package/package.json +11 -3
- package/src/index.d.ts +9 -31
- package/src/index.js +12 -34
- package/src/services/content-org/learning-paths.ts +33 -3
- package/src/services/contentAggregator.js +2 -2
- package/src/services/contentLikes.js +6 -39
- package/src/services/contentProgress.js +181 -479
- package/src/services/dataContext.js +0 -2
- package/src/services/progress-row/method-card.js +2 -1
- package/src/services/railcontent.js +12 -135
- package/src/services/sentry/.indexignore +0 -0
- package/src/services/sentry/index.ts +23 -0
- package/src/services/sync/.indexignore +0 -0
- package/src/services/sync/adapters/factory.ts +26 -0
- package/src/services/sync/adapters/lokijs.ts +1 -0
- package/src/services/sync/adapters/sqlite.ts +1 -0
- package/src/services/sync/concurrency-safety.ts +4 -0
- package/src/services/sync/context/index.ts +43 -0
- package/src/services/sync/context/providers/base.ts +4 -0
- package/src/services/sync/context/providers/connectivity.ts +14 -0
- package/src/services/sync/context/providers/durability.ts +5 -0
- package/src/services/sync/context/providers/index.ts +5 -0
- package/src/services/sync/context/providers/session.ts +8 -0
- package/src/services/sync/context/providers/tabs.ts +18 -0
- package/src/services/sync/context/providers/visibility.ts +14 -0
- package/src/services/sync/database/factory.ts +10 -0
- package/src/services/sync/errors/boundary.ts +45 -0
- package/src/services/sync/errors/index.ts +49 -0
- package/src/services/sync/fetch.ts +313 -0
- package/src/services/sync/index.ts +80 -0
- package/src/services/sync/manager.ts +139 -0
- package/src/services/sync/models/Base.ts +47 -0
- package/src/services/sync/models/ContentLike.ts +16 -0
- package/src/services/sync/models/ContentProgress.ts +69 -0
- package/src/services/sync/models/Practice.ts +72 -0
- package/src/services/sync/models/PracticeDayNote.ts +23 -0
- package/src/services/sync/models/index.ts +4 -0
- package/src/services/sync/repositories/base.ts +247 -0
- package/src/services/sync/repositories/content-likes.ts +26 -0
- package/src/services/sync/repositories/content-progress.ts +160 -0
- package/src/services/sync/repositories/index.ts +4 -0
- package/src/services/sync/repositories/practice-day-notes.ts +4 -0
- package/src/services/sync/repositories/practices.ts +52 -0
- package/src/services/sync/repository-proxy.ts +48 -0
- package/src/services/sync/resolver.ts +84 -0
- package/src/services/sync/retry.ts +88 -0
- package/src/services/sync/run-scope.ts +30 -0
- package/src/services/sync/schema/index.ts +66 -0
- package/src/services/sync/serializers/index.ts +2 -0
- package/src/services/sync/serializers/model.ts +32 -0
- package/src/services/sync/serializers/raw.ts +21 -0
- package/src/services/sync/store/index.ts +779 -0
- package/src/services/sync/store/push-coalescer.ts +57 -0
- package/src/services/sync/store-configs.ts +41 -0
- package/src/services/sync/strategies/base.ts +21 -0
- package/src/services/sync/strategies/index.ts +12 -0
- package/src/services/sync/strategies/initial.ts +11 -0
- package/src/services/sync/strategies/polling.ts +54 -0
- package/src/services/sync/telemetry/index.ts +140 -0
- package/src/services/sync/telemetry/sampling.ts +91 -0
- package/src/services/sync/utils/event-emitter.ts +24 -0
- package/src/services/sync/utils/index.ts +1 -0
- package/src/services/sync/utils/throttle.ts +93 -0
- package/src/services/sync/utils/timers.ts +9 -0
- package/src/services/userActivity.js +83 -148
- package/test/contentProgress.test.js +6 -39
- package/test/live/contentProgressLive.test.js +2 -31
- package/tools/generate-index.cjs +10 -4
- package/babel.config.cjs +0 -3
- package/docs/Content.html +0 -269
- package/docs/ContentOrganization.html +0 -245
- package/docs/Forums.html +0 -269
- package/docs/Gamification.html +0 -245
- package/docs/TestUser.html +0 -260
- package/docs/UserManagementSystem.html +0 -317
- package/docs/api_types.js.html +0 -97
- package/docs/config.js.html +0 -140
- package/docs/content-org_content-org.js.html +0 -76
- package/docs/content-org_guided-courses.ts.html +0 -110
- package/docs/content-org_learning-paths.ts.html +0 -379
- package/docs/content-org_playlists-types.js.html +0 -128
- package/docs/content-org_playlists.js.html +0 -440
- package/docs/content.js.html +0 -603
- package/docs/content_artist.ts.html +0 -206
- package/docs/content_content.ts.html +0 -77
- package/docs/content_genre.ts.html +0 -209
- package/docs/content_instructor.ts.html +0 -206
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -978
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -1049
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/forums_categories.ts.html +0 -156
- package/docs/forums_discussions.js.html +0 -95
- package/docs/forums_forum.js.html +0 -95
- package/docs/forums_forums.ts.html +0 -160
- package/docs/forums_posts.ts.html +0 -284
- package/docs/forums_threads.ts.html +0 -284
- package/docs/gamification_awards.js.html +0 -165
- package/docs/gamification_awards.ts.html +0 -195
- package/docs/gamification_gamification.js.html +0 -76
- package/docs/gamification_types.js.html +0 -80
- package/docs/global.html +0 -6019
- package/docs/index.html +0 -167
- package/docs/liveTesting.ts.html +0 -103
- package/docs/module-Accounts.html +0 -2283
- package/docs/module-Artist.html +0 -993
- package/docs/module-Awards.html +0 -836
- package/docs/module-Categories.html +0 -711
- package/docs/module-Config.html +0 -431
- package/docs/module-Content-Services-V2.html +0 -2998
- package/docs/module-ForumCategories.html +0 -687
- package/docs/module-ForumDiscussions.html +0 -370
- package/docs/module-Forums.html +0 -16599
- package/docs/module-Genre.html +0 -981
- package/docs/module-GuidedCourses.html +0 -108
- package/docs/module-Instructor.html +0 -929
- package/docs/module-Interests.html +0 -1066
- package/docs/module-LearningPaths.html +0 -2298
- package/docs/module-Onboarding.html +0 -882
- package/docs/module-Payments.html +0 -392
- package/docs/module-Permissions.html +0 -406
- package/docs/module-Playlists.html +0 -3030
- package/docs/module-ProgressRow.html +0 -108
- package/docs/module-Railcontent-Services.html +0 -6735
- package/docs/module-Sanity-Services.html +0 -8244
- package/docs/module-Sessions.html +0 -575
- package/docs/module-Threads.html +0 -1119
- package/docs/module-UserActivity.html +0 -4580
- package/docs/module-UserChat.html +0 -410
- package/docs/module-UserManagement.html +0 -1932
- package/docs/module-UserMemberships.html +0 -829
- package/docs/module-UserNotifications.html +0 -2595
- package/docs/module-UserProfile.html +0 -370
- package/docs/progress-row_method-card.js.html +0 -183
- package/docs/railcontent.js.html +0 -847
- package/docs/sanity.js.html +0 -2322
- package/docs/scripts/collapse.js +0 -39
- package/docs/scripts/commonNav.js +0 -28
- package/docs/scripts/linenumber.js +0 -25
- package/docs/scripts/nav.js +0 -12
- package/docs/scripts/polyfill.js +0 -4
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
- package/docs/scripts/prettify/lang-css.js +0 -2
- package/docs/scripts/prettify/prettify.js +0 -28
- package/docs/scripts/search.js +0 -99
- package/docs/styles/jsdoc.css +0 -776
- package/docs/styles/prettify.css +0 -80
- package/docs/userActivity.js.html +0 -1577
- package/docs/user_account.ts.html +0 -265
- package/docs/user_chat.js.html +0 -98
- package/docs/user_interests.js.html +0 -150
- package/docs/user_management.js.html +0 -258
- package/docs/user_memberships.js.html +0 -144
- package/docs/user_memberships.ts.html +0 -292
- package/docs/user_notifications.js.html +0 -374
- package/docs/user_onboarding.ts.html +0 -325
- package/docs/user_payments.ts.html +0 -146
- package/docs/user_permissions.js.html +0 -110
- package/docs/user_profile.js.html +0 -115
- package/docs/user_sessions.js.html +0 -170
- package/docs/user_types.js.html +0 -224
- 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 && streak <= 89) || (streak >= 800 && streak <= 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<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 < 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<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 < 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 && 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<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<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<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<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<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<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<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<{ 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<{ 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<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<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('&')
|
|
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 && date < today
|
|
789
|
-
)
|
|
790
|
-
let hasLastWeekPractice = sortedPracticeDays.some(
|
|
791
|
-
(date) => date >= lastWeekStart && date < currentWeekStart
|
|
792
|
-
)
|
|
793
|
-
let hasOlderPractice = sortedPracticeDays.some((date) => date < 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 && currentDailyStreak >= 2) ||
|
|
808
|
-
(hasYesterdayPractice && sortedPracticeDays.length == 1) ||
|
|
809
|
-
(hasYesterdayPractice && !hasLastWeekPractice && 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 < 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 < 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 && 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<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<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<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 && 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) && 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) && !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<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' && 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 && 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 && 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' && item.progressTimestamp >= 0)
|
|
1387
|
-
.sort((a, b) => {
|
|
1388
|
-
if (a.pinned && !b.pinned) return -1
|
|
1389
|
-
if (!a.pinned && 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 < 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<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}&id=${id}&progressType=${progressType}`
|
|
1499
|
-
const response = await fetchHandler(url, 'PUT', null)
|
|
1500
|
-
if (response && !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<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 && !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>
|