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