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
package/docs/sanity.js.html
DELETED
|
@@ -1,2322 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
|
|
5
|
-
<meta charset="utf-8">
|
|
6
|
-
<title>sanity.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">sanity.js</h1>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<section>
|
|
47
|
-
<article>
|
|
48
|
-
<pre class="prettyprint source linenums"><code>/**
|
|
49
|
-
* @module Sanity-Services
|
|
50
|
-
*/
|
|
51
|
-
import {
|
|
52
|
-
artistOrInstructorName,
|
|
53
|
-
instructorField,
|
|
54
|
-
chapterField,
|
|
55
|
-
assignmentsField,
|
|
56
|
-
descriptionField,
|
|
57
|
-
resourcesField,
|
|
58
|
-
contentTypeConfig,
|
|
59
|
-
getIntroVideoFields,
|
|
60
|
-
DEFAULT_FIELDS,
|
|
61
|
-
getFieldsForContentType,
|
|
62
|
-
filtersToGroq,
|
|
63
|
-
getUpcomingEventsTypes,
|
|
64
|
-
showsTypes,
|
|
65
|
-
getNewReleasesTypes,
|
|
66
|
-
coachLessonsTypes,
|
|
67
|
-
getFieldsForContentTypeWithFilteredChildren,
|
|
68
|
-
getChildFieldsForContentType,
|
|
69
|
-
SONG_TYPES,
|
|
70
|
-
SONG_TYPES_WITH_CHILDREN,
|
|
71
|
-
} from '../contentTypeConfig.js'
|
|
72
|
-
import { fetchSimilarItems, recommendations } from './recommendations.js'
|
|
73
|
-
import { processMetadata, typeWithSortOrder } from '../contentMetaData.js'
|
|
74
|
-
|
|
75
|
-
import { globalConfig } from './config.js'
|
|
76
|
-
|
|
77
|
-
import { fetchNextContentDataForParent, fetchHandler } from './railcontent.js'
|
|
78
|
-
import { arrayToStringRepresentation, FilterBuilder } from '../filterBuilder.js'
|
|
79
|
-
import { getPermissionsAdapter } from './permissions/index.ts'
|
|
80
|
-
import { getAllCompleted, getAllStarted, getAllStartedOrCompleted } from './contentProgress.js'
|
|
81
|
-
import { fetchRecentActivitiesActiveTabs } from './userActivity.js'
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Exported functions that are excluded from index generation.
|
|
85
|
-
*
|
|
86
|
-
* @type {string[]}
|
|
87
|
-
*/
|
|
88
|
-
const excludeFromGeneratedIndex = ['fetchRelatedByLicense']
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Fetch a song by its document ID from Sanity.
|
|
92
|
-
*
|
|
93
|
-
* @param {string} documentId - The ID of the document to fetch.
|
|
94
|
-
* @returns {Promise<Object|null>} - A promise that resolves to an object containing the song data or null if not found.
|
|
95
|
-
*
|
|
96
|
-
* @example
|
|
97
|
-
* fetchSongById('abc123')
|
|
98
|
-
* .then(song => console.log(song))
|
|
99
|
-
* .catch(error => console.error(error));
|
|
100
|
-
*/
|
|
101
|
-
export async function fetchSongById(documentId) {
|
|
102
|
-
const fields = getFieldsForContentType('song')
|
|
103
|
-
const filterParams = {}
|
|
104
|
-
const query = await buildQuery(
|
|
105
|
-
`_type == "song" && railcontent_id == ${documentId}`,
|
|
106
|
-
filterParams,
|
|
107
|
-
fields,
|
|
108
|
-
{
|
|
109
|
-
isSingle: true,
|
|
110
|
-
}
|
|
111
|
-
)
|
|
112
|
-
return fetchSanity(query, false)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* fetches from Sanity all content marked for removal next quarter
|
|
117
|
-
*
|
|
118
|
-
* @string brand
|
|
119
|
-
* @number pageNumber
|
|
120
|
-
* @number contentPerPage
|
|
121
|
-
* @returns {Promise<Object|null>}
|
|
122
|
-
*/
|
|
123
|
-
export async function fetchLeaving(brand, { pageNumber = 1, contentPerPage = 20 } = {}) {
|
|
124
|
-
const today = new Date()
|
|
125
|
-
const isoDateOnly = getDateOnly(today)
|
|
126
|
-
const filterString = `brand == '${brand}' && quarter_removed > '${isoDateOnly}'`
|
|
127
|
-
const startEndOrder = getQueryFromPage(pageNumber, contentPerPage)
|
|
128
|
-
const sortOrder = {
|
|
129
|
-
sortOrder: 'quarter_removed asc, published_on desc, id desc',
|
|
130
|
-
start: startEndOrder['start'],
|
|
131
|
-
end: startEndOrder['end'],
|
|
132
|
-
}
|
|
133
|
-
const query = await buildQuery(
|
|
134
|
-
filterString,
|
|
135
|
-
{ pullFutureContent: false, availableContentStatuses: ['published'] },
|
|
136
|
-
getFieldsForContentType('leaving'),
|
|
137
|
-
sortOrder
|
|
138
|
-
)
|
|
139
|
-
return fetchSanity(query, true)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* fetches from Sanity all content marked for return next quarter
|
|
144
|
-
*
|
|
145
|
-
* @string brand
|
|
146
|
-
* @number pageNumber
|
|
147
|
-
* @number contentPerPage
|
|
148
|
-
* @returns {Promise<Object|null>}
|
|
149
|
-
*/
|
|
150
|
-
export async function fetchReturning(brand, { pageNumber = 1, contentPerPage = 20 } = {}) {
|
|
151
|
-
const today = new Date()
|
|
152
|
-
const isoDateOnly = getDateOnly(today)
|
|
153
|
-
const filterString = `brand == '${brand}' && quarter_published >= '${isoDateOnly}'`
|
|
154
|
-
const startEndOrder = getQueryFromPage(pageNumber, contentPerPage)
|
|
155
|
-
const sortOrder = {
|
|
156
|
-
sortOrder: 'quarter_published asc, published_on desc, id desc',
|
|
157
|
-
start: startEndOrder['start'],
|
|
158
|
-
end: startEndOrder['end'],
|
|
159
|
-
}
|
|
160
|
-
const query = await buildQuery(
|
|
161
|
-
filterString,
|
|
162
|
-
{ pullFutureContent: true, availableContentStatuses: ['draft'] },
|
|
163
|
-
getFieldsForContentType('returning'),
|
|
164
|
-
sortOrder
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
return fetchSanity(query, true)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* fetches from Sanity all songs coming soon (new) next quarter
|
|
172
|
-
*
|
|
173
|
-
* @string brand
|
|
174
|
-
* @number pageNumber
|
|
175
|
-
* @number contentPerPage
|
|
176
|
-
* @returns {Promise<Object|null>}
|
|
177
|
-
*/
|
|
178
|
-
export async function fetchComingSoon(brand, { pageNumber = 1, contentPerPage = 20 } = {}) {
|
|
179
|
-
const filterString = `brand == '${brand}' && _type == 'song'`
|
|
180
|
-
const startEndOrder = getQueryFromPage(pageNumber, contentPerPage)
|
|
181
|
-
const sortOrder = {
|
|
182
|
-
sortOrder: 'published_on desc, id desc',
|
|
183
|
-
start: startEndOrder['start'],
|
|
184
|
-
end: startEndOrder['end'],
|
|
185
|
-
}
|
|
186
|
-
const query = await buildQuery(
|
|
187
|
-
filterString,
|
|
188
|
-
{ getFutureContentOnly: true },
|
|
189
|
-
getFieldsForContentType(),
|
|
190
|
-
sortOrder
|
|
191
|
-
)
|
|
192
|
-
return fetchSanity(query, true)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
*
|
|
197
|
-
* @number page
|
|
198
|
-
* @returns {number[]}
|
|
199
|
-
*/
|
|
200
|
-
function getQueryFromPage(pageNumber, contentPerPage) {
|
|
201
|
-
const start = contentPerPage * (pageNumber - 1)
|
|
202
|
-
const end = contentPerPage * pageNumber
|
|
203
|
-
let result = []
|
|
204
|
-
result['start'] = start
|
|
205
|
-
result['end'] = end
|
|
206
|
-
return result
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Fetch current number of artists for songs within a brand.
|
|
211
|
-
* @param {string} brand - The current brand.
|
|
212
|
-
* @returns {Promise<int|null>} - The fetched count of artists.
|
|
213
|
-
*/
|
|
214
|
-
export async function fetchSongArtistCount(brand) {
|
|
215
|
-
const filter = await new FilterBuilder(
|
|
216
|
-
`_type == "song" && brand == "${brand}" && references(^._id)`,
|
|
217
|
-
{ bypassPermissions: true }
|
|
218
|
-
).buildFilter()
|
|
219
|
-
const query = `
|
|
220
|
-
count(*[_type == "artist"]{
|
|
221
|
-
name,
|
|
222
|
-
"lessonsCount": count(*[${filter}])
|
|
223
|
-
}[lessonsCount > 0])`
|
|
224
|
-
return fetchSanity(query, true, { processNeedAccess: false })
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export async function fetchPlayAlongsCount(
|
|
228
|
-
brand,
|
|
229
|
-
{ searchTerm, includedFields, progressIds, progress }
|
|
230
|
-
) {
|
|
231
|
-
const searchFilter = searchTerm
|
|
232
|
-
? `&& (artist->name match "${searchTerm}*" || instructor[]->name match "${searchTerm}*" || title match "${searchTerm}*" || name match "${searchTerm}*")`
|
|
233
|
-
: ''
|
|
234
|
-
|
|
235
|
-
// Construct the included fields filter, replacing 'difficulty' with 'difficulty_string'
|
|
236
|
-
const includedFieldsFilter = includedFields.length > 0 ? filtersToGroq(includedFields) : ''
|
|
237
|
-
|
|
238
|
-
// limits the results to supplied progressIds for started & completed filters
|
|
239
|
-
const progressFilter = await getProgressFilter(progress, progressIds)
|
|
240
|
-
const query = `count(*[brand == '${brand}' && _type == "play-along" ${searchFilter} ${includedFieldsFilter} ${progressFilter} ]) `
|
|
241
|
-
return fetchSanity(query, true, { processNeedAccess: false })
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Fetch related songs for a specific brand and song ID.
|
|
246
|
-
*
|
|
247
|
-
* @param {string} brand - The brand for which to fetch related songs.
|
|
248
|
-
* @param {string} songId - The ID of the song to find related songs for.
|
|
249
|
-
* @returns {Promise<Object|null>} - A promise that resolves to an array of related song objects or null if not found.
|
|
250
|
-
*
|
|
251
|
-
* @example
|
|
252
|
-
* fetchRelatedSongs('drumeo', '12345')
|
|
253
|
-
* .then(relatedSongs => console.log(relatedSongs))
|
|
254
|
-
* .catch(error => console.error(error));
|
|
255
|
-
*/
|
|
256
|
-
export async function fetchRelatedSongs(brand, songId) {
|
|
257
|
-
const now = getSanityDate(new Date())
|
|
258
|
-
const query = `
|
|
259
|
-
*[_type == "song" && railcontent_id == ${songId}]{
|
|
260
|
-
"entity": array::unique([
|
|
261
|
-
...(*[_type == "song" && brand == "${brand}" && railcontent_id != ${songId} && references(^.artist->_id)
|
|
262
|
-
&& (status in ['published'] || (status == 'scheduled' && defined(published_on) && published_on >= '${now}'))]{
|
|
263
|
-
"type": _type,
|
|
264
|
-
"id": railcontent_id,
|
|
265
|
-
"url": web_url_path,
|
|
266
|
-
"published_on": published_on,
|
|
267
|
-
status,
|
|
268
|
-
"image": thumbnail.asset->url,
|
|
269
|
-
"permission_id": permission[]->railcontent_id,
|
|
270
|
-
"fields": [
|
|
271
|
-
{
|
|
272
|
-
"key": "title",
|
|
273
|
-
"value": title
|
|
274
|
-
},
|
|
275
|
-
{
|
|
276
|
-
"key": "artist",
|
|
277
|
-
"value": artist->name
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
"key": "difficulty",
|
|
281
|
-
"value": difficulty
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
"key": "length_in_seconds",
|
|
285
|
-
"value": soundslice[0].soundslice_length_in_second
|
|
286
|
-
}
|
|
287
|
-
],
|
|
288
|
-
}[0...10]),
|
|
289
|
-
...(*[_type == "song" && brand == "${brand}" && railcontent_id != ${songId} && references(^.genre[]->_id)
|
|
290
|
-
&& (status in ['published'] || (status == 'scheduled' && defined(published_on) && published_on >= '${now}'))]{
|
|
291
|
-
"type": _type,
|
|
292
|
-
"id": railcontent_id,
|
|
293
|
-
"url": web_url_path,
|
|
294
|
-
"published_on": published_on,
|
|
295
|
-
"permission_id": permission[]->railcontent_id,
|
|
296
|
-
status,
|
|
297
|
-
"fields": [
|
|
298
|
-
{
|
|
299
|
-
"key": "title",
|
|
300
|
-
"value": title
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
"key": "artist",
|
|
304
|
-
"value": artist->name
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
"key": "difficulty",
|
|
308
|
-
"value": difficulty
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
"key": "length_in_seconds",
|
|
312
|
-
"value": soundslice[0].soundslice_length_in_second
|
|
313
|
-
}
|
|
314
|
-
],
|
|
315
|
-
"data": [{
|
|
316
|
-
"key": "thumbnail_url",
|
|
317
|
-
"value": thumbnail.asset->url
|
|
318
|
-
}]
|
|
319
|
-
}[0...10])
|
|
320
|
-
])[0...10]
|
|
321
|
-
}`
|
|
322
|
-
|
|
323
|
-
// Fetch the related songs data
|
|
324
|
-
return fetchSanity(query, false)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Fetch the latest new releases for a specific brand.
|
|
329
|
-
* @param {string} brand - The brand for which to fetch new releases.
|
|
330
|
-
* @returns {Promise<Object|null>} - The fetched new releases data or null if not found.
|
|
331
|
-
*/
|
|
332
|
-
export async function fetchNewReleases(
|
|
333
|
-
brand,
|
|
334
|
-
{ page = 1, limit = 20, sort = '-published_on' } = {}
|
|
335
|
-
) {
|
|
336
|
-
const newTypes = getNewReleasesTypes(brand)
|
|
337
|
-
const typesString = arrayToStringRepresentation(newTypes)
|
|
338
|
-
const start = (page - 1) * limit
|
|
339
|
-
const end = start + limit
|
|
340
|
-
const sortOrder = getSortOrder(sort, brand)
|
|
341
|
-
const now = getDateOnly()
|
|
342
|
-
const filter = `_type in ${typesString} && brand == '${brand}' && (status == 'published' && show_in_new_feed == true && published_on <= '${now}')`
|
|
343
|
-
const fields = `
|
|
344
|
-
"id": railcontent_id,
|
|
345
|
-
title,
|
|
346
|
-
"image": thumbnail.asset->url,
|
|
347
|
-
"thumbnail": thumbnail.asset->url,
|
|
348
|
-
${artistOrInstructorName()},
|
|
349
|
-
"artists": instructor[]->name,
|
|
350
|
-
difficulty,
|
|
351
|
-
difficulty_string,
|
|
352
|
-
length_in_seconds,
|
|
353
|
-
published_on,
|
|
354
|
-
"type": _type,
|
|
355
|
-
web_url_path,
|
|
356
|
-
"permission_id": permission[]->railcontent_id,
|
|
357
|
-
`
|
|
358
|
-
const query = buildRawQuery(filter, fields, { sortOrder: sortOrder, start, end: end })
|
|
359
|
-
return fetchSanity(query, true)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Fetch upcoming events for a specific brand.
|
|
364
|
-
*
|
|
365
|
-
* @param {string} brand - The brand for which to fetch upcoming events.
|
|
366
|
-
* @returns {Promise<Object|null>} - A promise that resolves to an array of upcoming event objects or null if not found.
|
|
367
|
-
*
|
|
368
|
-
* @example
|
|
369
|
-
* fetchUpcomingEvents('drumeo', {
|
|
370
|
-
* page: 2,
|
|
371
|
-
* limit: 20,
|
|
372
|
-
* })
|
|
373
|
-
* .then(events => console.log(events))
|
|
374
|
-
* .catch(error => console.error(error));
|
|
375
|
-
*/
|
|
376
|
-
export async function fetchUpcomingEvents(brand, { page = 1, limit = 10 } = {}) {
|
|
377
|
-
const now = getSanityDate(new Date())
|
|
378
|
-
const start = (page - 1) * limit
|
|
379
|
-
const end = start + limit
|
|
380
|
-
const fields = `
|
|
381
|
-
"id": railcontent_id,
|
|
382
|
-
title,
|
|
383
|
-
"image": thumbnail.asset->url,
|
|
384
|
-
"thumbnail": thumbnail.asset->url,
|
|
385
|
-
${artistOrInstructorName()},
|
|
386
|
-
"artists": instructor[]->name,
|
|
387
|
-
difficulty,
|
|
388
|
-
difficulty_string,
|
|
389
|
-
length_in_seconds,
|
|
390
|
-
published_on,
|
|
391
|
-
"type": _type,
|
|
392
|
-
web_url_path,
|
|
393
|
-
"permission_id": permission[]->railcontent_id,
|
|
394
|
-
live_event_start_time,
|
|
395
|
-
live_event_end_time,
|
|
396
|
-
"isLive": live_event_start_time <= '${now}' && live_event_end_time >= '${now}'`
|
|
397
|
-
const query = buildRawQuery(
|
|
398
|
-
`defined(live_event_start_time) && live_event_start_time >= '${now}' && (!defined(live_event_end_time) || live_event_end_time >= '${now}' ) && brand == '${brand}' && status == 'scheduled'`,
|
|
399
|
-
fields,
|
|
400
|
-
{
|
|
401
|
-
sortOrder: 'published_on asc',
|
|
402
|
-
start: start,
|
|
403
|
-
end: end,
|
|
404
|
-
}
|
|
405
|
-
)
|
|
406
|
-
return fetchSanity(query, true)
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Fetch scheduled releases for a specific brand.
|
|
411
|
-
*
|
|
412
|
-
* @param {string} brand - The brand for which to fetch scheduled releasess.
|
|
413
|
-
* @returns {Promise<Object|null>} - A promise that resolves to an array of scheduled release objects or null if not found.
|
|
414
|
-
*
|
|
415
|
-
* @example
|
|
416
|
-
* fetchScheduledReleases('drumeo', {
|
|
417
|
-
* page: 2,
|
|
418
|
-
* limit: 20,
|
|
419
|
-
* })
|
|
420
|
-
* .then(content => console.log(content))
|
|
421
|
-
* .catch(error => console.error(error));
|
|
422
|
-
*/
|
|
423
|
-
export async function fetchScheduledReleases(brand, { page = 1, limit = 10 }) {
|
|
424
|
-
const upcomingTypes = getUpcomingEventsTypes(brand)
|
|
425
|
-
const newTypes = getNewReleasesTypes(brand)
|
|
426
|
-
|
|
427
|
-
const scheduledTypes = merge(upcomingTypes, newTypes)
|
|
428
|
-
const typesString = arrayJoinWithQuotes(scheduledTypes)
|
|
429
|
-
const now = getSanityDate(new Date())
|
|
430
|
-
const start = (page - 1) * limit
|
|
431
|
-
const end = start + limit
|
|
432
|
-
const query = `*[_type in [${typesString}] && brand == '${brand}' && status in ['published','scheduled'] && (!defined(live_event_end_time) || live_event_end_time < '${now}' ) && published_on > '${now}']{
|
|
433
|
-
"id": railcontent_id,
|
|
434
|
-
title,
|
|
435
|
-
"image": thumbnail.asset->url,
|
|
436
|
-
"thumbnail": thumbnail.asset->url,
|
|
437
|
-
${artistOrInstructorName()},
|
|
438
|
-
"artists": instructor[]->name,
|
|
439
|
-
difficulty,
|
|
440
|
-
difficulty_string,
|
|
441
|
-
length_in_seconds,
|
|
442
|
-
published_on,
|
|
443
|
-
"type": _type,
|
|
444
|
-
web_url_path,
|
|
445
|
-
"permission_id": permission[]->railcontent_id,
|
|
446
|
-
} | order(published_on asc)[${start}...${end}]`
|
|
447
|
-
return fetchSanity(query, true)
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Fetch content by a specific Railcontent ID.
|
|
452
|
-
*
|
|
453
|
-
* @param {string} id - The Railcontent ID of the content to fetch.
|
|
454
|
-
* @param {string} contentType - The document type of content to fetch
|
|
455
|
-
* @returns {Promise<Object|null>} - A promise that resolves to the content object or null if not found.
|
|
456
|
-
*
|
|
457
|
-
* @example
|
|
458
|
-
* fetchByRailContentId('abc123')
|
|
459
|
-
* .then(content => console.log(content))
|
|
460
|
-
* .catch(error => console.error(error));
|
|
461
|
-
*/
|
|
462
|
-
export async function fetchByRailContentId(id, contentType) {
|
|
463
|
-
const fields = await getFieldsForContentTypeWithFilteredChildren(contentType, true)
|
|
464
|
-
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
465
|
-
const entityFieldsString = ` ${fields}
|
|
466
|
-
'child_count': coalesce(count(child[${childrenFilter}]->), 0) ,
|
|
467
|
-
'length_in_seconds': coalesce(
|
|
468
|
-
math::sum(
|
|
469
|
-
select(
|
|
470
|
-
child[${childrenFilter}]->length_in_seconds
|
|
471
|
-
)
|
|
472
|
-
),
|
|
473
|
-
length_in_seconds
|
|
474
|
-
),`
|
|
475
|
-
|
|
476
|
-
const query = buildRawQuery(
|
|
477
|
-
`railcontent_id == ${id} && _type == '${contentType}'`,
|
|
478
|
-
entityFieldsString,
|
|
479
|
-
{
|
|
480
|
-
isSingle: true,
|
|
481
|
-
}
|
|
482
|
-
)
|
|
483
|
-
|
|
484
|
-
return fetchSanity(query, false)
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Fetch content by an array of Railcontent IDs.
|
|
489
|
-
*
|
|
490
|
-
* @param {Array<string|number>} ids - The array of Railcontent IDs of the content to fetch.
|
|
491
|
-
* @param {string} [contentType] - The content type the IDs to add needed fields to the response.
|
|
492
|
-
* @returns {Promise<Array<Object>|null>} - A promise that resolves to an array of content objects or null if not found.
|
|
493
|
-
*
|
|
494
|
-
* @example
|
|
495
|
-
* fetchByRailContentIds(['abc123', 'def456', 'ghi789'])
|
|
496
|
-
* .then(contents => console.log(contents))
|
|
497
|
-
* .catch(error => console.error(error));
|
|
498
|
-
*/
|
|
499
|
-
export async function fetchByRailContentIds(
|
|
500
|
-
ids,
|
|
501
|
-
contentType = undefined,
|
|
502
|
-
brand = undefined,
|
|
503
|
-
includePermissionsAndStatusFilter = false
|
|
504
|
-
) {
|
|
505
|
-
if (!ids?.length) {
|
|
506
|
-
return []
|
|
507
|
-
}
|
|
508
|
-
ids = [...new Set(ids.filter((item) => item !== null && item !== undefined))]
|
|
509
|
-
const idsString = ids.join(',')
|
|
510
|
-
const brandFilter = brand ? ` && brand == "${brand}"` : ''
|
|
511
|
-
const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`, {
|
|
512
|
-
pullFutureContent: true,
|
|
513
|
-
}).buildFilter()
|
|
514
|
-
const fields = await getFieldsForContentTypeWithFilteredChildren(contentType, true)
|
|
515
|
-
const baseFilter = `railcontent_id in [${idsString}]${brandFilter}`
|
|
516
|
-
const finalFilter = includePermissionsAndStatusFilter
|
|
517
|
-
? await new FilterBuilder(baseFilter).buildFilter()
|
|
518
|
-
: baseFilter
|
|
519
|
-
const query = `*[
|
|
520
|
-
${finalFilter}
|
|
521
|
-
]{
|
|
522
|
-
${fields}
|
|
523
|
-
'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
|
|
524
|
-
live_event_start_time,
|
|
525
|
-
live_event_end_time,
|
|
526
|
-
}`
|
|
527
|
-
|
|
528
|
-
const customPostProcess = (results) => {
|
|
529
|
-
const now = getSanityDate(new Date(), false)
|
|
530
|
-
const liveProcess = (result) => {
|
|
531
|
-
if (result.live_event_start_time && result.live_event_end_time) {
|
|
532
|
-
result.isLive = result.live_event_start_time <= now && result.live_event_end_time >= now
|
|
533
|
-
} else {
|
|
534
|
-
result.isLive = false
|
|
535
|
-
}
|
|
536
|
-
return result
|
|
537
|
-
}
|
|
538
|
-
return results.map(liveProcess)
|
|
539
|
-
}
|
|
540
|
-
const results = await fetchSanity(query, true, {
|
|
541
|
-
customPostProcess: customPostProcess,
|
|
542
|
-
processNeedAccess: true,
|
|
543
|
-
})
|
|
544
|
-
|
|
545
|
-
const sortFuction = function compare(a, b) {
|
|
546
|
-
const indexA = ids.indexOf(a['id'])
|
|
547
|
-
const indexB = ids.indexOf(b['id'])
|
|
548
|
-
if (indexA === indexB) return 0
|
|
549
|
-
if (indexA > indexB) return 1
|
|
550
|
-
return -1
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Sort results to match the order of the input IDs
|
|
554
|
-
const sortedResults = results?.sort(sortFuction) ?? null
|
|
555
|
-
|
|
556
|
-
return sortedResults
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
export async function fetchContentRows(brand, pageName, contentRowSlug) {
|
|
560
|
-
if (pageName === 'lessons') pageName = 'lesson'
|
|
561
|
-
if (pageName === 'songs') pageName = 'song'
|
|
562
|
-
const rowString = contentRowSlug ? ` && slug.current == "${contentRowSlug.toLowerCase()}"` : ''
|
|
563
|
-
const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`, {
|
|
564
|
-
pullFutureContent: true,
|
|
565
|
-
showMembershipRestrictedContent: true,
|
|
566
|
-
}).buildFilter()
|
|
567
|
-
const childFilter = await new FilterBuilder('', {
|
|
568
|
-
isChildrenFilter: true,
|
|
569
|
-
showMembershipRestrictedContent: true,
|
|
570
|
-
}).buildFilter()
|
|
571
|
-
const query = `*[_type == 'recommended-content-row' && brand == '${brand}' && type == '${pageName}'${rowString}]{
|
|
572
|
-
brand,
|
|
573
|
-
name,
|
|
574
|
-
'slug': slug.current,
|
|
575
|
-
'content': content[${childFilter}]->{
|
|
576
|
-
'children': child[${childFilter}]->{ 'id': railcontent_id,
|
|
577
|
-
'type': _type, brand, 'thumbnail': thumbnail.asset->url,
|
|
578
|
-
'children': child[${childFilter}]->{'id': railcontent_id}, },
|
|
579
|
-
${getFieldsForContentType('tab-data')}
|
|
580
|
-
'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
|
|
581
|
-
},
|
|
582
|
-
}`
|
|
583
|
-
return fetchSanity(query, true, { processNeedAccess: true })
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Fetch all content for a specific brand and type with pagination, search, and grouping options.
|
|
588
|
-
* @param {string} brand - The brand for which to fetch content.
|
|
589
|
-
* @param {string} type - The content type to fetch (e.g., 'song', 'artist').
|
|
590
|
-
* @param {Object} params - Parameters for pagination, filtering, sorting, and grouping.
|
|
591
|
-
* @param {number} [params.page=1] - The page number for pagination.
|
|
592
|
-
* @param {number} [params.limit=10] - The number of items per page.
|
|
593
|
-
* @param {string} [params.searchTerm=""] - The search term to filter content by title or artist.
|
|
594
|
-
* @param {string} [params.sort="-published_on"] - The field to sort the content by.
|
|
595
|
-
* @param {Array<string>} [params.includedFields=[]] - The fields to include in the query.
|
|
596
|
-
* @param {string} [params.groupBy=""] - The field to group the results by (e.g., 'artist', 'genre').
|
|
597
|
-
* @param {Array<string>} [params.progressIds=undefined] - An array of railcontent IDs to filter the results by. Used for filtering by progress.
|
|
598
|
-
* @param {boolean} [params.useDefaultFields=true] - use the default sanity fields for content Type
|
|
599
|
-
* @param {Array<string>} [params.customFields=[]] - An array of sanity fields to include in the request
|
|
600
|
-
* @param {string} [params.progress="all"] - An string representing which progress filter to use ("all", "in progress", "complete", "not started").
|
|
601
|
-
* @returns {Promise<Object|null>} - The fetched content data or null if not found.
|
|
602
|
-
*
|
|
603
|
-
* @example
|
|
604
|
-
* fetchAll('drumeo', 'song', {
|
|
605
|
-
* page: 2,
|
|
606
|
-
* limit: 20,
|
|
607
|
-
* searchTerm: 'jazz',
|
|
608
|
-
* sort: '-popularity',
|
|
609
|
-
* includedFields: ['difficulty,Intermediate'],
|
|
610
|
-
* groupBy: 'artist',
|
|
611
|
-
* progressIds: [123, 321],
|
|
612
|
-
* useDefaultFields: false,
|
|
613
|
-
* customFields: ['is_house_coach', 'slug.current', "'instructors': instructor[]->name"],
|
|
614
|
-
* })
|
|
615
|
-
* .then(content => console.log(content))
|
|
616
|
-
* .catch(error => console.error(error));
|
|
617
|
-
*/
|
|
618
|
-
export async function fetchAll(
|
|
619
|
-
brand,
|
|
620
|
-
type,
|
|
621
|
-
{
|
|
622
|
-
page = 1,
|
|
623
|
-
limit = 10,
|
|
624
|
-
searchTerm = '',
|
|
625
|
-
sort = '-published_on',
|
|
626
|
-
includedFields = [],
|
|
627
|
-
groupBy = '',
|
|
628
|
-
progressIds = undefined,
|
|
629
|
-
useDefaultFields = true,
|
|
630
|
-
customFields = [],
|
|
631
|
-
progress = 'all',
|
|
632
|
-
} = {}
|
|
633
|
-
) {
|
|
634
|
-
let config = contentTypeConfig[type] ?? {}
|
|
635
|
-
let additionalFields = config?.fields ?? []
|
|
636
|
-
let isGroupByOneToOne = (groupBy ? config?.relationships?.[groupBy]?.isOneToOne : false) ?? false
|
|
637
|
-
let webUrlPathType = config?.slug ?? type
|
|
638
|
-
const start = (page - 1) * limit
|
|
639
|
-
const end = start + limit
|
|
640
|
-
let bypassStatusAndPublishedValidation =
|
|
641
|
-
type == 'instructor' || groupBy == 'artist' || groupBy == 'genre' || groupBy == 'instructor'
|
|
642
|
-
let bypassPermissions = bypassStatusAndPublishedValidation
|
|
643
|
-
// Construct the type filter
|
|
644
|
-
let typeFilter
|
|
645
|
-
|
|
646
|
-
if (type === 'archives') {
|
|
647
|
-
typeFilter = `&& status == "archived"`
|
|
648
|
-
bypassStatusAndPublishedValidation = true
|
|
649
|
-
} else if (type === 'lessons' || type === 'songs') {
|
|
650
|
-
typeFilter = ``
|
|
651
|
-
} else if (type === 'pack') {
|
|
652
|
-
typeFilter = `&& (_type == 'pack' || _type == 'semester-pack')`
|
|
653
|
-
} else {
|
|
654
|
-
typeFilter = type ? `&& _type == '${type}'` : ''
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Construct the search filter
|
|
658
|
-
const searchFilter = searchTerm
|
|
659
|
-
? groupBy !== ''
|
|
660
|
-
? `&& (^.name match "${searchTerm}*" || title match "${searchTerm}*")`
|
|
661
|
-
: `&& (artist->name match "${searchTerm}*" || instructor[]->name match "${searchTerm}*" || title match "${searchTerm}*" || name match "${searchTerm}*")`
|
|
662
|
-
: ''
|
|
663
|
-
|
|
664
|
-
// Construct the included fields filter, replacing 'difficulty' with 'difficulty_string'
|
|
665
|
-
const includedFieldsFilter = includedFields.length > 0 ? filtersToGroq(includedFields) : ''
|
|
666
|
-
|
|
667
|
-
// limits the results to supplied progressIds for started & completed filters
|
|
668
|
-
const progressFilter = await getProgressFilter(progress, progressIds)
|
|
669
|
-
|
|
670
|
-
// Determine the sort order
|
|
671
|
-
const sortOrder = getSortOrder(sort, brand, groupBy)
|
|
672
|
-
|
|
673
|
-
let fields = useDefaultFields
|
|
674
|
-
? customFields.concat(DEFAULT_FIELDS, additionalFields)
|
|
675
|
-
: customFields
|
|
676
|
-
let fieldsString = fields.join(',')
|
|
677
|
-
|
|
678
|
-
let customFilter = ''
|
|
679
|
-
if (type == 'instructor') {
|
|
680
|
-
customFilter = '&& coach_card_image != null'
|
|
681
|
-
}
|
|
682
|
-
// Determine the group by clause
|
|
683
|
-
let query = ''
|
|
684
|
-
let entityFieldsString = ''
|
|
685
|
-
let filter = ''
|
|
686
|
-
if (groupBy !== '' && isGroupByOneToOne) {
|
|
687
|
-
const webUrlPath = 'artists'
|
|
688
|
-
const lessonsFilter = `_type == '${type}' && brand == '${brand}' && ^._id == ${groupBy}._ref ${searchFilter} ${includedFieldsFilter} ${progressFilter} ${customFilter}`
|
|
689
|
-
const lessonsFilterWithRestrictions = await new FilterBuilder(lessonsFilter).buildFilter()
|
|
690
|
-
entityFieldsString = `
|
|
691
|
-
'id': railcontent_id,
|
|
692
|
-
'type': _type,
|
|
693
|
-
name,
|
|
694
|
-
'head_shot_picture_url': thumbnail_url.asset->url,
|
|
695
|
-
'web_url_path': '/${brand}/${webUrlPath}/'+name+'?included_fieds[]=type,${type}',
|
|
696
|
-
'all_lessons_count': count(*[${lessonsFilterWithRestrictions}]._id),
|
|
697
|
-
'children': *[${lessonsFilterWithRestrictions}]{
|
|
698
|
-
${fieldsString},
|
|
699
|
-
${groupBy}
|
|
700
|
-
}[0...20]
|
|
701
|
-
`
|
|
702
|
-
filter = `_type == '${groupBy}' && count(*[${lessonsFilterWithRestrictions}]._id) > 0`
|
|
703
|
-
} else if (groupBy !== '') {
|
|
704
|
-
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
705
|
-
|
|
706
|
-
const webUrlPath = groupBy == 'genre' ? '/genres' : ''
|
|
707
|
-
const lessonsFilter = `brand == '${brand}' && ^._id in ${groupBy}[]._ref ${typeFilter} ${searchFilter} ${includedFieldsFilter} ${progressFilter} ${customFilter}`
|
|
708
|
-
const lessonsFilterWithRestrictions = await new FilterBuilder(lessonsFilter).buildFilter()
|
|
709
|
-
|
|
710
|
-
entityFieldsString = `
|
|
711
|
-
'id': railcontent_id,
|
|
712
|
-
'type': _type,
|
|
713
|
-
name,
|
|
714
|
-
'head_shot_picture_url': thumbnail_url.asset->url,
|
|
715
|
-
'web_url_path': select(defined(web_url_path)=> web_url_path +'?included_fieds[]=type,${type}',!defined(web_url_path)=> '/${brand}${webUrlPath}/'+name+'/${webUrlPathType}'),
|
|
716
|
-
'all_lessons_count': count(*[${lessonsFilterWithRestrictions}]._id),
|
|
717
|
-
'children': *[${lessonsFilterWithRestrictions}]{
|
|
718
|
-
${fieldsString},
|
|
719
|
-
'lesson_count': coalesce(count(child[${childrenFilter}]->), 0) ,
|
|
720
|
-
${groupBy}
|
|
721
|
-
}[0...20]`
|
|
722
|
-
filter = `_type == '${groupBy}' && count(*[${lessonsFilterWithRestrictions}]._id) > 0`
|
|
723
|
-
} else {
|
|
724
|
-
filter = `brand == "${brand}" ${typeFilter} ${searchFilter} ${includedFieldsFilter} ${progressFilter} ${customFilter}`
|
|
725
|
-
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
726
|
-
entityFieldsString = ` ${fieldsString},
|
|
727
|
-
'lesson_count': coalesce(count(child[${childrenFilter}]->), 0) ,
|
|
728
|
-
'length_in_seconds': coalesce(
|
|
729
|
-
math::sum(
|
|
730
|
-
select(
|
|
731
|
-
child[${childrenFilter}]->length_in_seconds
|
|
732
|
-
)
|
|
733
|
-
),
|
|
734
|
-
length_in_seconds
|
|
735
|
-
),`
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
const filterWithRestrictions = await new FilterBuilder(filter, {
|
|
739
|
-
bypassStatuses: bypassStatusAndPublishedValidation,
|
|
740
|
-
bypassPermissions: bypassPermissions,
|
|
741
|
-
bypassPublishedDateRestriction: bypassStatusAndPublishedValidation,
|
|
742
|
-
}).buildFilter()
|
|
743
|
-
query = buildEntityAndTotalQuery(filterWithRestrictions, entityFieldsString, {
|
|
744
|
-
sortOrder: sortOrder,
|
|
745
|
-
start: start,
|
|
746
|
-
end: end,
|
|
747
|
-
})
|
|
748
|
-
|
|
749
|
-
return fetchSanity(query, true)
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
async function getProgressFilter(progress, progressIds) {
|
|
753
|
-
switch (progress) {
|
|
754
|
-
case 'all':
|
|
755
|
-
return progressIds !== undefined ? `&& railcontent_id in [${progressIds.join(',')}]` : ''
|
|
756
|
-
case 'in progress': {
|
|
757
|
-
const ids = await getAllStarted()
|
|
758
|
-
return `&& railcontent_id in [${ids.join(',')}]`
|
|
759
|
-
}
|
|
760
|
-
case 'completed': {
|
|
761
|
-
const ids = await getAllCompleted()
|
|
762
|
-
return `&& railcontent_id in [${ids.join(',')}]`
|
|
763
|
-
}
|
|
764
|
-
case 'not started': {
|
|
765
|
-
const ids = await getAllStartedOrCompleted()
|
|
766
|
-
return `&& !(railcontent_id in [${ids.join(',')}])`
|
|
767
|
-
}
|
|
768
|
-
case 'recent': {
|
|
769
|
-
const ids = progressIds !== undefined ? progressIds : await getAllStartedOrCompleted()
|
|
770
|
-
return `&& (railcontent_id in [${ids.join(',')}])`
|
|
771
|
-
}
|
|
772
|
-
case 'incomplete': {
|
|
773
|
-
const ids = progressIds !== undefined ? progressIds : await getAllStarted()
|
|
774
|
-
return `&& railcontent_id in [${ids.join(',')}]`
|
|
775
|
-
}
|
|
776
|
-
default:
|
|
777
|
-
throw new Error(`'${progress}' progress option not implemented`)
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
export function getSortOrder(sort = '-published_on', brand, groupBy) {
|
|
782
|
-
const sanitizedSort = sort?.trim() || '-published_on'
|
|
783
|
-
let isDesc = sanitizedSort.startsWith('-')
|
|
784
|
-
const sortField = isDesc ? sanitizedSort.substring(1) : sanitizedSort
|
|
785
|
-
|
|
786
|
-
let sortOrder = ''
|
|
787
|
-
|
|
788
|
-
switch (sortField) {
|
|
789
|
-
case 'slug':
|
|
790
|
-
sortOrder = groupBy ? 'name' : '!defined(title), lower(title)'
|
|
791
|
-
break
|
|
792
|
-
|
|
793
|
-
case 'popularity':
|
|
794
|
-
if (groupBy == 'artist' || groupBy == 'genre') {
|
|
795
|
-
sortOrder = isDesc ? `coalesce(popularity.${brand}, -1)` : 'popularity'
|
|
796
|
-
} else {
|
|
797
|
-
sortOrder = isDesc ? 'coalesce(popularity, -1)' : 'popularity'
|
|
798
|
-
}
|
|
799
|
-
break
|
|
800
|
-
|
|
801
|
-
case 'recommended':
|
|
802
|
-
sortOrder = 'published_on'
|
|
803
|
-
isDesc = true
|
|
804
|
-
break
|
|
805
|
-
|
|
806
|
-
default:
|
|
807
|
-
sortOrder = sortField
|
|
808
|
-
break
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
sortOrder += isDesc ? ' desc' : ' asc'
|
|
812
|
-
return sortOrder
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
/**
|
|
816
|
-
* Fetches all available filter options based on brand, filters, and various optional criteria.
|
|
817
|
-
*
|
|
818
|
-
* This function constructs a query to retrieve the total number of results and filter options such as difficulty, instrument type, and genre.
|
|
819
|
-
* The filter options are dynamically generated based on the provided filters, style, artist, and content type.
|
|
820
|
-
* If a coachId is provided, the content type must be 'coach-lessons'.
|
|
821
|
-
*
|
|
822
|
-
* @param {string} brand - Brand to filter.
|
|
823
|
-
* @param {string[]} filters - Key-value pairs to filter the query.
|
|
824
|
-
* @param {string} [style] - Optional style/genre filter.
|
|
825
|
-
* @param {string} [artist] - Optional artist name filter.
|
|
826
|
-
* @param {string} contentType - Content type (e.g., 'song', 'lesson').
|
|
827
|
-
* @param {string} [term] - Optional search term for title, album, artist, or genre.
|
|
828
|
-
* @param {Array<string>} [progressIds] - Optional array of progress IDs to filter by.
|
|
829
|
-
* @param {string} [coachId] - Optional coach ID (only valid if contentType is 'coach-lessons').
|
|
830
|
-
* @param {boolean} [includeTabs=false] - Whether to include tabs in the returned metadata.
|
|
831
|
-
* @returns {Promise<Object>} - The filter options and metadata.
|
|
832
|
-
* @throws {Error} If coachId is provided but contentType isn't 'coach-lessons'.
|
|
833
|
-
*
|
|
834
|
-
* @example
|
|
835
|
-
* // Fetch filter options for 'song' content type:
|
|
836
|
-
* fetchAllFilterOptions('myBrand', [], 'Rock', 'John Doe', 'song', 'Love')
|
|
837
|
-
* .then(options => console.log(options))
|
|
838
|
-
* .catch(error => console.error(error));
|
|
839
|
-
*
|
|
840
|
-
* @example
|
|
841
|
-
* // Fetch filter options for a coach's lessons with coachId:
|
|
842
|
-
* fetchAllFilterOptions('myBrand', [], 'Rock', 'John Doe', 'coach-lessons', 'Love', undefined, '123')
|
|
843
|
-
* .then(options => console.log(options))
|
|
844
|
-
* .catch(error => console.error(error));
|
|
845
|
-
*/
|
|
846
|
-
export async function fetchAllFilterOptions(
|
|
847
|
-
brand,
|
|
848
|
-
filters = [],
|
|
849
|
-
style,
|
|
850
|
-
artist,
|
|
851
|
-
contentType,
|
|
852
|
-
term,
|
|
853
|
-
progressIds,
|
|
854
|
-
coachId,
|
|
855
|
-
includeTabs = false
|
|
856
|
-
) {
|
|
857
|
-
if (contentType == 'lessons' || contentType == 'songs') {
|
|
858
|
-
const metaData = processMetadata(brand, contentType, true)
|
|
859
|
-
return {
|
|
860
|
-
meta: metaData,
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
if (coachId && contentType !== 'coach-lessons') {
|
|
865
|
-
throw new Error(
|
|
866
|
-
`Invalid contentType: '${contentType}' for coachId. It must be 'coach-lessons'.`
|
|
867
|
-
)
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
const includedFieldsFilter = filters?.length ? filtersToGroq(filters) : undefined
|
|
871
|
-
const progressFilter = progressIds ? `&& railcontent_id in [${progressIds.join(',')}]` : ''
|
|
872
|
-
const adapter = getPermissionsAdapter()
|
|
873
|
-
const userPermissionsData = await adapter.fetchUserPermissions()
|
|
874
|
-
const isAdmin = adapter.isAdmin(userPermissionsData)
|
|
875
|
-
|
|
876
|
-
const constructCommonFilter = (excludeFilter) => {
|
|
877
|
-
const filterWithoutOption = excludeFilter
|
|
878
|
-
? filtersToGroq(filters, excludeFilter)
|
|
879
|
-
: includedFieldsFilter
|
|
880
|
-
const statusFilter = ' && status == "published"'
|
|
881
|
-
const includeStatusFilter = !isAdmin && !['instructor', 'artist', 'genre'].includes(contentType)
|
|
882
|
-
|
|
883
|
-
return coachId
|
|
884
|
-
? `brand == '${brand}' && status == "published" && references(*[_type=='instructor' && railcontent_id == ${coachId}]._id) ${filterWithoutOption || ''} ${term ? ` && (title match "${term}" || album match "${term}" || artist->name match "${term}" || genre[]->name match "${term}")` : ''}`
|
|
885
|
-
: `_type == '${contentType}' && brand == "${brand}"${includeStatusFilter ? statusFilter : ''}${style && excludeFilter !== 'style' ? ` && '${style}' in genre[]->name` : ''}${artist && excludeFilter !== 'artist' ? ` && artist->name == '${artist}'` : ''} ${progressFilter} ${filterWithoutOption || ''} ${term ? ` && (title match "${term}" || album match "${term}" || artist->name match "${term}" || genre[]->name match "${term}")` : ''}`
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
const metaData = processMetadata(brand, contentType, true)
|
|
889
|
-
const allowableFilters = metaData?.allowableFilters || []
|
|
890
|
-
const tabs = metaData?.tabs || []
|
|
891
|
-
const catalogName = metaData?.shortname || metaData?.name
|
|
892
|
-
|
|
893
|
-
const dynamicFilterOptions = allowableFilters
|
|
894
|
-
.map((filter) => getFilterOptions(filter, constructCommonFilter(filter), contentType, brand))
|
|
895
|
-
.join(' ')
|
|
896
|
-
|
|
897
|
-
const query = `
|
|
898
|
-
{
|
|
899
|
-
"meta": {
|
|
900
|
-
"totalResults": count(*[${constructCommonFilter()}
|
|
901
|
-
${term ? ` && (title match "${term}" || album match "${term}" || artist->name match "${term}" || genre[]->name match "${term}")` : ''}]),
|
|
902
|
-
"filterOptions": {
|
|
903
|
-
${dynamicFilterOptions}
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
}`
|
|
907
|
-
|
|
908
|
-
const results = await fetchSanity(query, true, { processNeedAccess: false })
|
|
909
|
-
|
|
910
|
-
return includeTabs ? { ...results, tabs, catalogName } : results
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
//Daniel Nov 14 2025 note - keeping this for when we migrate foundations to packs, so we know what fields to use.
|
|
914
|
-
/**
|
|
915
|
-
* Fetch the Foundations 2019.
|
|
916
|
-
* @param {string} slug - The slug of the method.
|
|
917
|
-
* @returns {Promise<Object|null>} - The fetched foundation data or null if not found.
|
|
918
|
-
*/
|
|
919
|
-
export async function fetchFoundation(slug) {
|
|
920
|
-
const filterParams = {}
|
|
921
|
-
const query = await buildQuery(
|
|
922
|
-
`_type == 'foundation' && slug.current == "${slug}"`,
|
|
923
|
-
filterParams,
|
|
924
|
-
getFieldsForContentType('foundation'),
|
|
925
|
-
{
|
|
926
|
-
sortOrder: 'published_on asc',
|
|
927
|
-
isSingle: true,
|
|
928
|
-
}
|
|
929
|
-
)
|
|
930
|
-
return fetchSanity(query, false)
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Fetch the Method (learning-paths) for a specific brand.
|
|
935
|
-
* @param {string} brand - The brand for which to fetch methods.
|
|
936
|
-
* @param {string} slug - The slug of the method.
|
|
937
|
-
* @returns {Promise<Object|null>} - The fetched methods data or null if not found.
|
|
938
|
-
*/
|
|
939
|
-
//todo BEH-1446 depreciated. remove all old method functions
|
|
940
|
-
export async function fetchMethod(brand, slug) {
|
|
941
|
-
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
942
|
-
|
|
943
|
-
const query = `*[_type == 'learning-path' && brand == "${brand}" && slug.current == "${slug}"] {
|
|
944
|
-
"description": ${descriptionField},
|
|
945
|
-
"instructors":instructor[]->name,
|
|
946
|
-
published_on,
|
|
947
|
-
"id": railcontent_id,
|
|
948
|
-
railcontent_id,
|
|
949
|
-
"slug": slug.current,
|
|
950
|
-
status,
|
|
951
|
-
title,
|
|
952
|
-
video,
|
|
953
|
-
length_in_seconds,
|
|
954
|
-
parent_content_data,
|
|
955
|
-
"breadcrumbs_data": parent_content_data[] {
|
|
956
|
-
"id": id,
|
|
957
|
-
"title": *[railcontent_id == ^.id][0].title,
|
|
958
|
-
"url": *[railcontent_id == ^.id][0].web_url_path
|
|
959
|
-
} | order(length(url)),
|
|
960
|
-
"type": _type,
|
|
961
|
-
"permission_id": permission[]->railcontent_id,
|
|
962
|
-
"levels": child[${childrenFilter}]->
|
|
963
|
-
{
|
|
964
|
-
"id": railcontent_id,
|
|
965
|
-
published_on,
|
|
966
|
-
child_count,
|
|
967
|
-
difficulty,
|
|
968
|
-
difficulty_string,
|
|
969
|
-
"thumbnail": thumbnail.asset->url,
|
|
970
|
-
"instructor": instructor[]->{name},
|
|
971
|
-
title,
|
|
972
|
-
"type": _type,
|
|
973
|
-
"description": ${descriptionField},
|
|
974
|
-
"url": web_url_path,
|
|
975
|
-
web_url_path,
|
|
976
|
-
xp,
|
|
977
|
-
total_xp
|
|
978
|
-
}
|
|
979
|
-
} | order(published_on asc)`
|
|
980
|
-
return fetchSanity(query, false)
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
/**
|
|
984
|
-
* Fetch the child courses for a specific method by Railcontent ID.
|
|
985
|
-
* @param {string} railcontentId - The Railcontent ID of the current lesson.
|
|
986
|
-
* @returns {Promise<Object|null>} - The fetched next lesson data or null if not found.
|
|
987
|
-
*/
|
|
988
|
-
export async function fetchMethodChildren(railcontentId) {
|
|
989
|
-
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
990
|
-
|
|
991
|
-
const query = `*[railcontent_id == ${railcontentId}]{
|
|
992
|
-
"child_count":coalesce(count(child[${childrenFilter}]->), 0),
|
|
993
|
-
"id": railcontent_id,
|
|
994
|
-
"description": ${descriptionField},
|
|
995
|
-
"thumbnail": thumbnail.asset->url,
|
|
996
|
-
title,
|
|
997
|
-
xp,
|
|
998
|
-
total_xp,
|
|
999
|
-
parent_content_data,
|
|
1000
|
-
"resources": ${resourcesField},
|
|
1001
|
-
"breadcrumbs_data": parent_content_data[] {
|
|
1002
|
-
"id": id,
|
|
1003
|
-
"title": *[railcontent_id == ^.id][0].title,
|
|
1004
|
-
"url": *[railcontent_id == ^.id][0].web_url_path
|
|
1005
|
-
} | order(length(url)),
|
|
1006
|
-
'children': child[(${childrenFilter})]->{
|
|
1007
|
-
${getFieldsForContentType('method')}
|
|
1008
|
-
},
|
|
1009
|
-
}[0..1]`
|
|
1010
|
-
return fetchSanity(query, true)
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
/**
|
|
1014
|
-
* Fetch the next lesson for a specific method by Railcontent ID.
|
|
1015
|
-
* @param {string} railcontentId - The Railcontent ID of the current lesson.
|
|
1016
|
-
* @param {string} methodId - The RailcontentID of the method
|
|
1017
|
-
* @returns {Promise<Object|null>} - object with `nextLesson` and `previousLesson` attributes
|
|
1018
|
-
* @example
|
|
1019
|
-
* fetchMethodPreviousNextLesson(241284, 241247)
|
|
1020
|
-
* .then(data => { console.log('nextLesson', data.nextLesson); console.log('prevlesson', data.prevLesson);})
|
|
1021
|
-
* .catch(error => console.error(error));
|
|
1022
|
-
*/
|
|
1023
|
-
export async function fetchMethodPreviousNextLesson(railcontentId, methodId) {
|
|
1024
|
-
const sortedChildren = await fetchMethodChildrenIds(methodId)
|
|
1025
|
-
const index = sortedChildren.indexOf(Number(railcontentId))
|
|
1026
|
-
let nextId = sortedChildren[index + 1]
|
|
1027
|
-
let previousId = sortedChildren[index - 1]
|
|
1028
|
-
let ids = []
|
|
1029
|
-
if (nextId) ids.push(nextId)
|
|
1030
|
-
if (previousId) ids.push(previousId)
|
|
1031
|
-
let nextPrev = await fetchByRailContentIds(ids)
|
|
1032
|
-
const nextLesson = nextPrev.find((elem) => {
|
|
1033
|
-
return elem['id'] === nextId
|
|
1034
|
-
})
|
|
1035
|
-
const prevLesson = nextPrev.find((elem) => {
|
|
1036
|
-
return elem['id'] === previousId
|
|
1037
|
-
})
|
|
1038
|
-
return { nextLesson, prevLesson }
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
/**
|
|
1042
|
-
* Fetch all children of a specific method by Railcontent ID.
|
|
1043
|
-
* @param {string} railcontentId - The Railcontent ID of the method.
|
|
1044
|
-
* @returns {Promise<Array<Object>|null>} - The fetched children data or null if not found.
|
|
1045
|
-
*/
|
|
1046
|
-
export async function fetchMethodChildrenIds(railcontentId) {
|
|
1047
|
-
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
1048
|
-
|
|
1049
|
-
const query = `*[ railcontent_id == ${railcontentId}]{
|
|
1050
|
-
'children': child[${childrenFilter}]-> {
|
|
1051
|
-
'id': railcontent_id,
|
|
1052
|
-
'type' : _type,
|
|
1053
|
-
'children': child[${childrenFilter}]-> {
|
|
1054
|
-
'id': railcontent_id,
|
|
1055
|
-
'type' : _type,
|
|
1056
|
-
'children': child[${childrenFilter}]-> {
|
|
1057
|
-
'id': railcontent_id,
|
|
1058
|
-
'type' : _type,
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
}`
|
|
1063
|
-
let allChildren = await fetchSanity(query, false)
|
|
1064
|
-
return getChildrenToDepth(allChildren, 4)
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
function getChildrenToDepth(parent, depth = 1) {
|
|
1068
|
-
let allChildrenIds = []
|
|
1069
|
-
if (parent && parent['children'] && depth > 0) {
|
|
1070
|
-
parent['children'].forEach((child) => {
|
|
1071
|
-
if (!child['children']) {
|
|
1072
|
-
allChildrenIds.push(child['id'])
|
|
1073
|
-
}
|
|
1074
|
-
allChildrenIds = allChildrenIds.concat(getChildrenToDepth(child, depth - 1))
|
|
1075
|
-
})
|
|
1076
|
-
}
|
|
1077
|
-
return allChildrenIds
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
/**
|
|
1081
|
-
* Fetch the next and previous lessons for a specific lesson by Railcontent ID.
|
|
1082
|
-
* @param {string} railcontentId - The Railcontent ID of the current lesson.
|
|
1083
|
-
* @returns {Promise<Object|null>} - The fetched next and previous lesson data or null if found.
|
|
1084
|
-
*/
|
|
1085
|
-
export async function fetchNextPreviousLesson(railcontentId) {
|
|
1086
|
-
const document = await fetchLessonContent(railcontentId)
|
|
1087
|
-
if (document.parent_content_data && document.parent_content_data.length > 0) {
|
|
1088
|
-
const lastElement = document.parent_content_data[document.parent_content_data.length - 1]
|
|
1089
|
-
const results = await fetchMethodPreviousNextLesson(railcontentId, lastElement.id)
|
|
1090
|
-
return results
|
|
1091
|
-
}
|
|
1092
|
-
const processedData = processMetadata(document.brand, document.type, true)
|
|
1093
|
-
let sortBy = processedData?.sortBy ?? 'published_on'
|
|
1094
|
-
const isDesc = sortBy.startsWith('-')
|
|
1095
|
-
sortBy = isDesc ? sortBy.substring(1) : sortBy
|
|
1096
|
-
let sortValue = document[sortBy]
|
|
1097
|
-
if (sortValue == null) {
|
|
1098
|
-
sortBy = 'railcontent_id'
|
|
1099
|
-
sortValue = document['railcontent_id']
|
|
1100
|
-
}
|
|
1101
|
-
const isNumeric = !isNaN(sortValue)
|
|
1102
|
-
let prevComparison = isNumeric ? `${sortBy} <= ${sortValue}` : `${sortBy} <= "${sortValue}"`
|
|
1103
|
-
let nextComparison = isNumeric ? `${sortBy} >= ${sortValue}` : `${sortBy} >= "${sortValue}"`
|
|
1104
|
-
const fields = getFieldsForContentType(document.type)
|
|
1105
|
-
const query = `{
|
|
1106
|
-
"prevLesson": *[brand == "${document.brand}" && status == "${document.status}" && _type == "${document.type}" && ${prevComparison} && railcontent_id != ${railcontentId}] | order(${sortBy} desc){${fields}}[0...1][0],
|
|
1107
|
-
"nextLesson": *[brand == "${document.brand}" && status == "${document.status}" && _type == "${document.type}" && ${nextComparison} && railcontent_id != ${railcontentId}] | order(${sortBy} asc){${fields}}[0...1][0]
|
|
1108
|
-
}`
|
|
1109
|
-
|
|
1110
|
-
return await fetchSanity(query, true)
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
/**
|
|
1114
|
-
* Fetch the next piece of content under a parent by Railcontent ID
|
|
1115
|
-
* @param {int} railcontentId - The Railcontent ID of the parent content
|
|
1116
|
-
* @returns {Promise<{next: (Object|null)}|null>} - object with 'next' attribute
|
|
1117
|
-
* @example
|
|
1118
|
-
* jumpToContinueContent(296693)
|
|
1119
|
-
* then.(data => { console.log('next', data.next);})
|
|
1120
|
-
* .catch(error => console.error(error));
|
|
1121
|
-
*/
|
|
1122
|
-
export async function jumpToContinueContent(railcontentId) {
|
|
1123
|
-
const nextContent = await fetchNextContentDataForParent(railcontentId)
|
|
1124
|
-
if (!nextContent || !nextContent.id) {
|
|
1125
|
-
return null
|
|
1126
|
-
}
|
|
1127
|
-
let next = await fetchByRailContentId(nextContent.id, nextContent.type)
|
|
1128
|
-
return { next }
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
/**
|
|
1132
|
-
* Fetch the page data for a specific lesson by Railcontent ID.
|
|
1133
|
-
* @param {string} railContentId - The Railcontent ID of the current lesson.
|
|
1134
|
-
* @parent {boolean} addParent - Whether to include parent content data in the response.
|
|
1135
|
-
* @returns {Promise<Object|null>} - The fetched page data or null if found.
|
|
1136
|
-
*
|
|
1137
|
-
* @example
|
|
1138
|
-
* fetchLessonContent('lesson123')
|
|
1139
|
-
* .then(data => console.log(data))
|
|
1140
|
-
* .catch(error => console.error(error));
|
|
1141
|
-
*/
|
|
1142
|
-
export async function fetchLessonContent(railContentId, { addParent = false } = {}) {
|
|
1143
|
-
const filterParams = {
|
|
1144
|
-
isSingle: true,
|
|
1145
|
-
pullFutureContent: true,
|
|
1146
|
-
showMembershipRestrictedContent: true,
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
const parentQuery = addParent
|
|
1150
|
-
? `"parent_content_data": *[railcontent_id in [...(^.parent_content_data[].id)]]{
|
|
1151
|
-
"id": railcontent_id,
|
|
1152
|
-
title,
|
|
1153
|
-
slug,
|
|
1154
|
-
"type": _type,
|
|
1155
|
-
"logo" : logo_image_url.asset->url,
|
|
1156
|
-
"dark_mode_logo": dark_mode_logo_url.asset->url,
|
|
1157
|
-
"light_mode_logo": light_mode_logo_url.asset->url,
|
|
1158
|
-
"badge": *[references(^._id) && _type == 'content-award'][0].badge.asset->url,
|
|
1159
|
-
},`
|
|
1160
|
-
: ''
|
|
1161
|
-
|
|
1162
|
-
const fields = `${getFieldsForContentType()}
|
|
1163
|
-
"resources": ${resourcesField},
|
|
1164
|
-
soundslice,
|
|
1165
|
-
instrumentless,
|
|
1166
|
-
soundslice_slug,
|
|
1167
|
-
"description": ${descriptionField},
|
|
1168
|
-
"chapters": ${chapterField},
|
|
1169
|
-
"instructors":instructor[]->name,
|
|
1170
|
-
"instructor": ${instructorField},
|
|
1171
|
-
${assignmentsField}
|
|
1172
|
-
video,
|
|
1173
|
-
length_in_seconds,
|
|
1174
|
-
mp3_no_drums_no_click_url,
|
|
1175
|
-
mp3_no_drums_yes_click_url,
|
|
1176
|
-
mp3_yes_drums_no_click_url,
|
|
1177
|
-
mp3_yes_drums_yes_click_url,
|
|
1178
|
-
"permission_id": permission[]->railcontent_id,
|
|
1179
|
-
${parentQuery}
|
|
1180
|
-
...select(
|
|
1181
|
-
defined(live_event_start_time) => {
|
|
1182
|
-
"live_event_start_time": live_event_start_time,
|
|
1183
|
-
"live_event_end_time": live_event_end_time,
|
|
1184
|
-
"live_event_stream_id": live_event_stream_id,
|
|
1185
|
-
"videoId": coalesce(live_event_stream_id, video.external_id),
|
|
1186
|
-
"live_event_is_global": live_global_event == true
|
|
1187
|
-
}
|
|
1188
|
-
)
|
|
1189
|
-
`
|
|
1190
|
-
|
|
1191
|
-
const query = await buildQuery(`railcontent_id == ${railContentId}`, filterParams, fields, {
|
|
1192
|
-
isSingle: true,
|
|
1193
|
-
})
|
|
1194
|
-
const chapterProcess = (result) => {
|
|
1195
|
-
const now = getSanityDate(new Date(), false)
|
|
1196
|
-
if (result.live_event_start_time && result.live_event_end_time) {
|
|
1197
|
-
result.isLive = result.live_event_start_time <= now && result.live_event_end_time >= now
|
|
1198
|
-
}
|
|
1199
|
-
const chapters = result.chapters ?? []
|
|
1200
|
-
if (chapters.length === 0) return result
|
|
1201
|
-
result.chapters = chapters.map((chapter, index) => ({
|
|
1202
|
-
...chapter,
|
|
1203
|
-
chapter_thumbnail_url: `https://musora-web-platform.s3.amazonaws.com/chapters/${result.brand}/Chapter${index + 1}.jpg`,
|
|
1204
|
-
}))
|
|
1205
|
-
return result
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
return fetchSanity(query, false, { customPostProcess: chapterProcess, processNeedAccess: true })
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
/**
|
|
1212
|
-
* Returns a list of recommended content based on the provided railContentId.
|
|
1213
|
-
* If no recommendations found in recsys, falls back to fetching related lessons.
|
|
1214
|
-
*
|
|
1215
|
-
* @param railContentId
|
|
1216
|
-
* @param brand
|
|
1217
|
-
* @param count
|
|
1218
|
-
* @returns {Promise<Array<Object>>}
|
|
1219
|
-
*/
|
|
1220
|
-
export async function fetchRelatedRecommendedContent(railContentId, brand, count = 10) {
|
|
1221
|
-
const recommendedItems = await fetchSimilarItems(railContentId, brand, count)
|
|
1222
|
-
if (recommendedItems && recommendedItems.length > 0) {
|
|
1223
|
-
return fetchByRailContentIds(recommendedItems, 'tab-data', brand, true)
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
return await fetchRelatedLessons(railContentId, brand).then((result) =>
|
|
1227
|
-
result.related_lessons?.splice(0, count)
|
|
1228
|
-
)
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
/**
|
|
1232
|
-
* Get song type (transcriptions, jam packs, play alongs, tutorial children) content documents that share content information with the provided railcontent document.
|
|
1233
|
-
* These are linked through content that shares a license with the provided railcontent document
|
|
1234
|
-
*
|
|
1235
|
-
* @param railcontentId
|
|
1236
|
-
* @param brand
|
|
1237
|
-
* @param count
|
|
1238
|
-
* @returns {Promise<Array<Object>>}
|
|
1239
|
-
*/
|
|
1240
|
-
export async function fetchOtherSongVersions(railcontentId, brand, count = 3) {
|
|
1241
|
-
return fetchRelatedByLicense(railcontentId, brand, true, count)
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
/**
|
|
1245
|
-
* Get non-song content documents that share content information with the provided railcontent document.
|
|
1246
|
-
* These are linked through content that shares a license with the provided railcontent document
|
|
1247
|
-
*
|
|
1248
|
-
* @param {integer} railcontentId
|
|
1249
|
-
* @param {string} brand
|
|
1250
|
-
* @param {integer:3} count
|
|
1251
|
-
* @returns {Promise<Array<Object>>}
|
|
1252
|
-
*/
|
|
1253
|
-
export async function fetchLessonsFeaturingThisContent(railcontentId, brand, count = 3) {
|
|
1254
|
-
return fetchRelatedByLicense(railcontentId, brand, false, count)
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
/**
|
|
1258
|
-
* Get content documents that share license information with the provided railcontent id
|
|
1259
|
-
*
|
|
1260
|
-
* @param {integer} railcontentId
|
|
1261
|
-
* @param {string} brand
|
|
1262
|
-
* @param {boolean} onlyUseSongTypes - if true, only return the song type documents. If false, return everything except those
|
|
1263
|
-
* @param {integer:3} count
|
|
1264
|
-
* @returns {Promise<Array<Object>>}
|
|
1265
|
-
*/
|
|
1266
|
-
async function fetchRelatedByLicense(railcontentId, brand, onlyUseSongTypes, count) {
|
|
1267
|
-
const typeCheck = `@->_type in [${arrayJoinWithQuotes(SONG_TYPES)}]`
|
|
1268
|
-
let typeCheckString = `@->brand == '${brand}' && `
|
|
1269
|
-
typeCheckString += onlyUseSongTypes ? `${typeCheck}` : `!(${typeCheck})`
|
|
1270
|
-
const contentFromLicenseFilter = `_type == 'license' && references(^._id)].content[${typeCheckString} && @->railcontent_id != ${railcontentId}`
|
|
1271
|
-
let filterSongTypesWithSameLicense = await new FilterBuilder(contentFromLicenseFilter, {
|
|
1272
|
-
isChildrenFilter: true,
|
|
1273
|
-
}).buildFilter()
|
|
1274
|
-
let queryFields = getFieldsForContentType()
|
|
1275
|
-
const baseParentQuery = `railcontent_id == ${railcontentId}`
|
|
1276
|
-
let parentQuery = await new FilterBuilder(baseParentQuery).buildFilter()
|
|
1277
|
-
|
|
1278
|
-
// queryFields = 'railcontent_id, title'
|
|
1279
|
-
// parentQuery = baseParentQuery
|
|
1280
|
-
// filterSongTypesWithSameLicense = contentFromLicenseFilter
|
|
1281
|
-
const query = `*[${parentQuery}]{
|
|
1282
|
-
_type, railcontent_id,
|
|
1283
|
-
"related_by_license" :
|
|
1284
|
-
*[${filterSongTypesWithSameLicense}]->{${queryFields}}|order(published_on desc, title asc)[0...${count}],
|
|
1285
|
-
}[0...1]`
|
|
1286
|
-
const results = await fetchSanity(query, false)
|
|
1287
|
-
return results ? (results['related_by_license'] ?? []) : []
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
/**
|
|
1291
|
-
* Fetch sibling lessons to a specific lesson
|
|
1292
|
-
* @param {string} railContentId - The RailContent ID of the current lesson.
|
|
1293
|
-
* @param {string} brand - The current brand.
|
|
1294
|
-
* @returns {Promise<Array<Object>|null>} - The fetched related lessons data or null if not found.
|
|
1295
|
-
*/
|
|
1296
|
-
export async function fetchSiblingContent(railContentId, brand = null) {
|
|
1297
|
-
const filterGetParent = await new FilterBuilder(`references(^._id) && _type == ^.parent_type`, {
|
|
1298
|
-
pullFutureContent: true,
|
|
1299
|
-
showMembershipRestrictedContent: true, // Show parent even without permissions
|
|
1300
|
-
}).buildFilter()
|
|
1301
|
-
const filterForParentList = await new FilterBuilder(
|
|
1302
|
-
`references(^._id) && _type == ^.parent_type`,
|
|
1303
|
-
{
|
|
1304
|
-
pullFutureContent: true,
|
|
1305
|
-
isParentFilter: true,
|
|
1306
|
-
showMembershipRestrictedContent: true, // Show parent even without permissions
|
|
1307
|
-
}
|
|
1308
|
-
).buildFilter()
|
|
1309
|
-
|
|
1310
|
-
const childrenFilter = await new FilterBuilder(``, {
|
|
1311
|
-
isChildrenFilter: true,
|
|
1312
|
-
showMembershipRestrictedContent: true, // Show all lessons in sidebar, need_access applied on individual page
|
|
1313
|
-
}).buildFilter()
|
|
1314
|
-
|
|
1315
|
-
const brandString = brand ? ` && brand == "${brand}"` : ''
|
|
1316
|
-
const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, artist->, "permission_id": permission[]->railcontent_id, "genre": genre[]->name, "parent_id": parent_content_data[0].id`
|
|
1317
|
-
|
|
1318
|
-
const query = `*[railcontent_id == ${railContentId}${brandString}]{
|
|
1319
|
-
_type, parent_type, 'parent_id': parent_content_data[0].id, railcontent_id,
|
|
1320
|
-
'for-calculations': *[${filterGetParent}][0]{
|
|
1321
|
-
'siblings-list': child[]->railcontent_id,
|
|
1322
|
-
'parents-list': *[${filterForParentList}][0].child[]->railcontent_id
|
|
1323
|
-
},
|
|
1324
|
-
"related_lessons" : *[${filterGetParent}][0].child[${childrenFilter}]->{${queryFields}}
|
|
1325
|
-
}`
|
|
1326
|
-
|
|
1327
|
-
let result = await fetchSanity(query, false, { processNeedAccess: true })
|
|
1328
|
-
|
|
1329
|
-
//there's no way in sanity to retrieve the index of an array, so we must calculate after fetch
|
|
1330
|
-
if (result['for-calculations'] && result['for-calculations']['parents-list']) {
|
|
1331
|
-
const calc = result['for-calculations']
|
|
1332
|
-
const parentCount = calc['parents-list'].length
|
|
1333
|
-
const currentParentIndex = calc['parents-list'].indexOf(result['parent_id']) + 1
|
|
1334
|
-
const siblingCount = calc['siblings-list'].length
|
|
1335
|
-
const currentSiblingIndex = calc['siblings-list'].indexOf(result['railcontent_id']) + 1
|
|
1336
|
-
|
|
1337
|
-
delete result['for-calculations']
|
|
1338
|
-
result = { ...result, parentCount, currentParentIndex, siblingCount, currentSiblingIndex }
|
|
1339
|
-
return result
|
|
1340
|
-
} else {
|
|
1341
|
-
delete result['for-calculations']
|
|
1342
|
-
return result
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
/**
|
|
1347
|
-
* Fetch lessons related to a specific lesson by RailContent ID and type.
|
|
1348
|
-
* @param {string} railContentId - The RailContent ID of the current lesson.
|
|
1349
|
-
* @returns {Promise<Array<Object>|null>} - The fetched related lessons data or null if not found.
|
|
1350
|
-
*/
|
|
1351
|
-
export async function fetchRelatedLessons(railContentId) {
|
|
1352
|
-
const defaultFilterFields = `_type==^._type && brand == ^.brand && railcontent_id != ${railContentId}`
|
|
1353
|
-
|
|
1354
|
-
const filterSameArtist = await new FilterBuilder(
|
|
1355
|
-
`${defaultFilterFields} && references(^.artist->_id)`,
|
|
1356
|
-
{ showMembershipRestrictedContent: true }
|
|
1357
|
-
).buildFilter()
|
|
1358
|
-
const filterSameGenre = await new FilterBuilder(
|
|
1359
|
-
`${defaultFilterFields} && references(^.genre[]->_id)`,
|
|
1360
|
-
{ showMembershipRestrictedContent: true }
|
|
1361
|
-
).buildFilter()
|
|
1362
|
-
|
|
1363
|
-
const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type, "genre": genre[]->name`
|
|
1364
|
-
|
|
1365
|
-
const query = `*[railcontent_id == ${railContentId} && (!defined(permission) || references(*[_type=='permission']._id))]{
|
|
1366
|
-
_type, parent_type, railcontent_id,
|
|
1367
|
-
"related_lessons" : array::unique([
|
|
1368
|
-
...(*[${filterSameArtist}]{${queryFields}}|order(published_on desc, title asc)[0...10]),
|
|
1369
|
-
...(*[${filterSameGenre}]{${queryFields}}|order(published_on desc, title asc)[0...10]),
|
|
1370
|
-
])[0...10]}`
|
|
1371
|
-
|
|
1372
|
-
return await fetchSanity(query, false, { processNeedAccess: true })
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
/**
|
|
1376
|
-
* Fetch all packs.
|
|
1377
|
-
* @param {string} brand - The brand for which to fetch packs.
|
|
1378
|
-
* @param {string} [searchTerm=""] - The search term to filter packs.
|
|
1379
|
-
* @param {string} [sort="-published_on"] - The field to sort the packs by.
|
|
1380
|
-
* @param {number} [params.page=1] - The page number for pagination.
|
|
1381
|
-
* @param {number} [params.limit=10] - The number of items per page.
|
|
1382
|
-
* @returns {Promise<Array<Object>|null>} - The fetched pack content data or null if not found.
|
|
1383
|
-
*/
|
|
1384
|
-
export async function fetchAllPacks(
|
|
1385
|
-
brand,
|
|
1386
|
-
sort = '-published_on',
|
|
1387
|
-
searchTerm = '',
|
|
1388
|
-
page = 1,
|
|
1389
|
-
limit = 10
|
|
1390
|
-
) {
|
|
1391
|
-
const sortOrder = getSortOrder(sort, brand)
|
|
1392
|
-
const filter = `(_type == 'pack' || _type == 'semester-pack') && brand == '${brand}' && title match "${searchTerm}*"`
|
|
1393
|
-
const filterParams = {}
|
|
1394
|
-
const start = (page - 1) * limit
|
|
1395
|
-
const end = start + limit
|
|
1396
|
-
|
|
1397
|
-
const query = await buildQuery(
|
|
1398
|
-
filter,
|
|
1399
|
-
filterParams,
|
|
1400
|
-
await getFieldsForContentTypeWithFilteredChildren('pack'),
|
|
1401
|
-
{
|
|
1402
|
-
sortOrder: sortOrder,
|
|
1403
|
-
start,
|
|
1404
|
-
end,
|
|
1405
|
-
}
|
|
1406
|
-
)
|
|
1407
|
-
return fetchSanity(query, true)
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
/**
|
|
1411
|
-
* Fetch all content for a specific pack by Railcontent ID.
|
|
1412
|
-
* @param {string} railcontentId - The Railcontent ID of the pack.
|
|
1413
|
-
* @returns {Promise<Array<Object>|null>} - The fetched pack content data or null if not found.
|
|
1414
|
-
*/
|
|
1415
|
-
export async function fetchPackAll(railcontentId, type = 'pack') {
|
|
1416
|
-
return fetchByRailContentId(railcontentId, type)
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
export async function fetchLiveEvent(brand, forcedContentId = null) {
|
|
1420
|
-
const LIVE_EXTRA_MINUTES = 30
|
|
1421
|
-
//calendarIDs taken from addevent.php
|
|
1422
|
-
// TODO import instructor calendars to Sanity
|
|
1423
|
-
let defaultCalendarID = ''
|
|
1424
|
-
switch (brand) {
|
|
1425
|
-
case 'drumeo':
|
|
1426
|
-
defaultCalendarID = 'GP142387'
|
|
1427
|
-
break
|
|
1428
|
-
case 'pianote':
|
|
1429
|
-
defaultCalendarID = 'be142408'
|
|
1430
|
-
break
|
|
1431
|
-
case 'guitareo':
|
|
1432
|
-
defaultCalendarID = 'IJ142407'
|
|
1433
|
-
break
|
|
1434
|
-
case 'singeo':
|
|
1435
|
-
defaultCalendarID = 'bk354284'
|
|
1436
|
-
break
|
|
1437
|
-
default:
|
|
1438
|
-
break
|
|
1439
|
-
}
|
|
1440
|
-
let startDateTemp = new Date()
|
|
1441
|
-
let endDateTemp = new Date()
|
|
1442
|
-
|
|
1443
|
-
startDateTemp = new Date(
|
|
1444
|
-
startDateTemp.setMinutes(startDateTemp.getMinutes() + LIVE_EXTRA_MINUTES)
|
|
1445
|
-
)
|
|
1446
|
-
endDateTemp = new Date(endDateTemp.setMinutes(endDateTemp.getMinutes() - LIVE_EXTRA_MINUTES))
|
|
1447
|
-
|
|
1448
|
-
// See LiveStreamEventService.getCurrentOrNextLiveEvent for some nice complicated logic which I don't think is actually importart
|
|
1449
|
-
// this has some +- on times
|
|
1450
|
-
// But this query just finds the first scheduled event (sorted by start_time) that ends after now()
|
|
1451
|
-
const query =
|
|
1452
|
-
forcedContentId !== null
|
|
1453
|
-
? `*[railcontent_id == ${forcedContentId} ]{
|
|
1454
|
-
'slug': slug.current,
|
|
1455
|
-
'id': railcontent_id,
|
|
1456
|
-
live_event_start_time,
|
|
1457
|
-
live_event_end_time,
|
|
1458
|
-
live_event_stream_id,
|
|
1459
|
-
railcontent_id,
|
|
1460
|
-
published_on,
|
|
1461
|
-
'event_coach_url' : instructor[0]->web_url_path,
|
|
1462
|
-
'event_coach_calendar_id': coalesce(calendar_id, '${defaultCalendarID}'),
|
|
1463
|
-
title,
|
|
1464
|
-
"thumbnail": thumbnail.asset->url,
|
|
1465
|
-
${artistOrInstructorName()},
|
|
1466
|
-
difficulty_string,
|
|
1467
|
-
"instructors": ${instructorField},
|
|
1468
|
-
'videoId': coalesce(live_event_stream_id, video.external_id),
|
|
1469
|
-
} | order(live_event_start_time)[0...1]`
|
|
1470
|
-
: `*[status == 'scheduled' && brand == '${brand}' && defined(live_event_start_time) && live_event_start_time <= '${getSanityDate(startDateTemp, false)}' && live_event_end_time >= '${getSanityDate(endDateTemp, false)}']{
|
|
1471
|
-
'slug': slug.current,
|
|
1472
|
-
'id': railcontent_id,
|
|
1473
|
-
live_event_start_time,
|
|
1474
|
-
live_event_end_time,
|
|
1475
|
-
live_event_stream_id,
|
|
1476
|
-
railcontent_id,
|
|
1477
|
-
published_on,
|
|
1478
|
-
'event_coach_url' : instructor[0]->web_url_path,
|
|
1479
|
-
'event_coach_calendar_id': coalesce(calendar_id, '${defaultCalendarID}'),
|
|
1480
|
-
title,
|
|
1481
|
-
"thumbnail": thumbnail.asset->url,
|
|
1482
|
-
${artistOrInstructorName()},
|
|
1483
|
-
difficulty_string,
|
|
1484
|
-
"instructors": instructor[]->{
|
|
1485
|
-
name,
|
|
1486
|
-
web_url_path,
|
|
1487
|
-
},
|
|
1488
|
-
'videoId': coalesce(live_event_stream_id, video.external_id),
|
|
1489
|
-
} | order(live_event_start_time)[0...1]`
|
|
1490
|
-
|
|
1491
|
-
return await fetchSanity(query, false, { processNeedAccess: false })
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
/**
|
|
1495
|
-
* Fetch the data needed for the Pack Overview screen.
|
|
1496
|
-
* @param {number} id - The Railcontent ID of the pack
|
|
1497
|
-
* @returns {Promise<Object|null>} - The pack information and lessons or null if not found.
|
|
1498
|
-
*
|
|
1499
|
-
* @example
|
|
1500
|
-
* fetchPackData(404048)
|
|
1501
|
-
* .then(pack => console.log(pack))
|
|
1502
|
-
* .catch(error => console.error(error));
|
|
1503
|
-
*/
|
|
1504
|
-
export async function fetchPackData(id) {
|
|
1505
|
-
const builder = await new FilterBuilder(`railcontent_id == ${id}`).buildFilter()
|
|
1506
|
-
const query = `*[${builder}]{
|
|
1507
|
-
${await getFieldsForContentTypeWithFilteredChildren('pack')}
|
|
1508
|
-
} [0...1]`
|
|
1509
|
-
return fetchSanity(query, false)
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
/**
|
|
1513
|
-
* Fetch the data needed for the coach screen.
|
|
1514
|
-
* @param {string} id - The Railcontent ID of the coach
|
|
1515
|
-
*
|
|
1516
|
-
* @returns {Promise<Object|null>} - The lessons for the instructor or null if not found.
|
|
1517
|
-
*
|
|
1518
|
-
* @example
|
|
1519
|
-
* fetchCoachLessons('coach123')
|
|
1520
|
-
* .then(lessons => console.log(lessons))
|
|
1521
|
-
* .catch(error => console.error(error));
|
|
1522
|
-
*/
|
|
1523
|
-
export async function fetchByReference(
|
|
1524
|
-
brand,
|
|
1525
|
-
{ sortOrder = '-published_on', searchTerm = '', page = 1, limit = 20, includedFields = [] } = {}
|
|
1526
|
-
) {
|
|
1527
|
-
const fieldsString = getFieldsForContentType()
|
|
1528
|
-
const start = (page - 1) * limit
|
|
1529
|
-
const end = start + limit
|
|
1530
|
-
const searchFilter = searchTerm ? `&& title match "${searchTerm}*"` : ''
|
|
1531
|
-
const includedFieldsFilter = includedFields.length > 0 ? includedFields.join(' && ') : ''
|
|
1532
|
-
|
|
1533
|
-
const filter = `brand == '${brand}' ${searchFilter} && references(*[${includedFieldsFilter}]._id)`
|
|
1534
|
-
const filterWithRestrictions = await new FilterBuilder(filter).buildFilter()
|
|
1535
|
-
const query = buildEntityAndTotalQuery(filterWithRestrictions, fieldsString, {
|
|
1536
|
-
sortOrder: getSortOrder(sortOrder, brand),
|
|
1537
|
-
start: start,
|
|
1538
|
-
end: end,
|
|
1539
|
-
})
|
|
1540
|
-
return fetchSanity(query, true)
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
/**
|
|
1544
|
-
*
|
|
1545
|
-
* Return the top level parent content railcontent_id.
|
|
1546
|
-
* Ignores learning-path-v2 parents.
|
|
1547
|
-
* ex: if railcontentId is of type 'skill-pack-lesson', return the corresponding 'skill-pack' railcontent_id
|
|
1548
|
-
*
|
|
1549
|
-
* @param {int} railcontentId
|
|
1550
|
-
* @returns {Promise<int|null>}
|
|
1551
|
-
*/
|
|
1552
|
-
export async function fetchTopLevelParentId(railcontentId) {
|
|
1553
|
-
const parentFilter = "railcontent_id in [...(^.parent_content_data[].id)]"
|
|
1554
|
-
const statusFilter = "&& status in ['scheduled', 'published', 'archived', 'unlisted']"
|
|
1555
|
-
|
|
1556
|
-
const query = `*[railcontent_id == ${railcontentId}]{
|
|
1557
|
-
railcontent_id,
|
|
1558
|
-
'parents': *[${parentFilter} ${statusFilter}]{
|
|
1559
|
-
railcontent_id
|
|
1560
|
-
}
|
|
1561
|
-
}`
|
|
1562
|
-
let response = await fetchSanity(query, false, { processNeedAccess: false })
|
|
1563
|
-
if (!response) return null
|
|
1564
|
-
let parents = response['parents']
|
|
1565
|
-
let parentsLength = parents ? response['parents'].length : 0
|
|
1566
|
-
if (parentsLength > 0) {
|
|
1567
|
-
// return the last parent
|
|
1568
|
-
return parents[parentsLength - 1]['railcontent_id']
|
|
1569
|
-
}
|
|
1570
|
-
return response['railcontent_id']
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
export async function fetchHierarchy(railcontentId) {
|
|
1574
|
-
let topLevelId = await fetchTopLevelParentId(railcontentId)
|
|
1575
|
-
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
1576
|
-
const query = `*[railcontent_id == ${topLevelId}]{
|
|
1577
|
-
railcontent_id,
|
|
1578
|
-
'assignments': assignment[]{railcontent_id},
|
|
1579
|
-
'children': child[${childrenFilter}]->{
|
|
1580
|
-
railcontent_id,
|
|
1581
|
-
'assignments': assignment[]{railcontent_id},
|
|
1582
|
-
'children': child[${childrenFilter}]->{
|
|
1583
|
-
railcontent_id,
|
|
1584
|
-
'assignments': assignment[]{railcontent_id},
|
|
1585
|
-
'children': child[${childrenFilter}]->{
|
|
1586
|
-
railcontent_id,
|
|
1587
|
-
'assignments': assignment[]{railcontent_id},
|
|
1588
|
-
'children': child[${childrenFilter}]->{
|
|
1589
|
-
railcontent_id,
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
},
|
|
1594
|
-
}`
|
|
1595
|
-
let response = await fetchSanity(query, false, { processNeedAccess: false })
|
|
1596
|
-
if (!response) return null
|
|
1597
|
-
let data = {
|
|
1598
|
-
topLevelId: topLevelId,
|
|
1599
|
-
parents: {},
|
|
1600
|
-
children: {},
|
|
1601
|
-
}
|
|
1602
|
-
populateHierarchyLookups(response, data, null)
|
|
1603
|
-
return data
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
function populateHierarchyLookups(currentLevel, data, parentId) {
|
|
1607
|
-
let contentId = currentLevel['railcontent_id']
|
|
1608
|
-
let children = currentLevel['children']
|
|
1609
|
-
|
|
1610
|
-
data.parents[contentId] = parentId
|
|
1611
|
-
if (children) {
|
|
1612
|
-
data.children[contentId] = children.map((child) => child['railcontent_id'])
|
|
1613
|
-
for (let i = 0; i < children.length; i++) {
|
|
1614
|
-
populateHierarchyLookups(children[i], data, contentId)
|
|
1615
|
-
}
|
|
1616
|
-
} else {
|
|
1617
|
-
data.children[contentId] = []
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
let assignments = currentLevel['assignments']
|
|
1621
|
-
if (assignments) {
|
|
1622
|
-
let assignmentIds = assignments.map((assignment) => assignment['railcontent_id'])
|
|
1623
|
-
data.children[contentId] = (data.children[contentId] ?? []).concat(assignmentIds)
|
|
1624
|
-
assignmentIds.forEach((assignmentId) => {
|
|
1625
|
-
data.parents[assignmentId] = contentId
|
|
1626
|
-
})
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
/**
|
|
1631
|
-
* Fetch data for comment mod page
|
|
1632
|
-
*
|
|
1633
|
-
* @param {array} ids - List of ids get data for
|
|
1634
|
-
* @returns {Promise<Object|null>} - A promise that resolves to an object containing the data
|
|
1635
|
-
*/
|
|
1636
|
-
export async function fetchCommentModContentData(ids) {
|
|
1637
|
-
const idsString = ids.join(',')
|
|
1638
|
-
const fields = `"id": railcontent_id, "type": _type, title, "url": web_url_path, "parent": *[^._id in child[]._ref]{"id": railcontent_id, title}`
|
|
1639
|
-
const query = await buildQuery(
|
|
1640
|
-
`railcontent_id in [${idsString}]`,
|
|
1641
|
-
{ bypassPermissions: true },
|
|
1642
|
-
fields,
|
|
1643
|
-
{ end: 50 }
|
|
1644
|
-
)
|
|
1645
|
-
let data = await fetchSanity(query, true)
|
|
1646
|
-
let mapped = {}
|
|
1647
|
-
data.forEach(function (content) {
|
|
1648
|
-
mapped[content.id] = {
|
|
1649
|
-
id: content.id,
|
|
1650
|
-
type: content.type,
|
|
1651
|
-
title: content.title,
|
|
1652
|
-
url: content.url,
|
|
1653
|
-
parentTitle: content.parent[0]?.title ?? null,
|
|
1654
|
-
}
|
|
1655
|
-
})
|
|
1656
|
-
return mapped
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
/**
|
|
1660
|
-
*
|
|
1661
|
-
* @param {string} query - The GROQ query to execute against the Sanity API.
|
|
1662
|
-
* @param {boolean} isList - Whether to return an array or a single result.
|
|
1663
|
-
* @param {Object} options - Additional options for fetching data.
|
|
1664
|
-
* @param {Function} [options.customPostProcess=null] - custom post process callback
|
|
1665
|
-
* @param {boolean} [options.processNeedAccess=true] - execute the needs_access callback
|
|
1666
|
-
* @param {boolean} [options.processPageType=true] - execute the page_type callback
|
|
1667
|
-
* @returns {Promise<*|null>} - A promise that resolves to the fetched data or null if an error occurs or no results are found.
|
|
1668
|
-
*
|
|
1669
|
-
* @example
|
|
1670
|
-
* const query = `*[_type == "song"]{title, artist->name}`;
|
|
1671
|
-
* fetchSanity(query, true)
|
|
1672
|
-
* .then(data => console.log(data))
|
|
1673
|
-
* .catch(error => console.error(error));
|
|
1674
|
-
*/
|
|
1675
|
-
|
|
1676
|
-
export async function fetchSanity(
|
|
1677
|
-
query,
|
|
1678
|
-
isList,
|
|
1679
|
-
{ customPostProcess = null, processNeedAccess = true, processPageType = true } = {}
|
|
1680
|
-
) {
|
|
1681
|
-
// Check the config object before proceeding
|
|
1682
|
-
if (!checkSanityConfig(globalConfig)) {
|
|
1683
|
-
return null
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
const perspective = globalConfig.sanityConfig.perspective ?? 'published'
|
|
1687
|
-
const api = globalConfig.sanityConfig.useCachedAPI ? 'apicdn' : 'api'
|
|
1688
|
-
const url = `https://${globalConfig.sanityConfig.projectId}.${api}.sanity.io/v${globalConfig.sanityConfig.version}/data/query/${globalConfig.sanityConfig.dataset}?perspective=${perspective}`
|
|
1689
|
-
const headers = {
|
|
1690
|
-
'Content-Type': 'application/json',
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
try {
|
|
1694
|
-
const method = 'post'
|
|
1695
|
-
const options = {
|
|
1696
|
-
method,
|
|
1697
|
-
headers,
|
|
1698
|
-
body: JSON.stringify({ query: query }),
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
const adapter = getPermissionsAdapter()
|
|
1702
|
-
let promisesResult = await Promise.all([
|
|
1703
|
-
fetch(url, options),
|
|
1704
|
-
processNeedAccess ? adapter.fetchUserPermissions() : null,
|
|
1705
|
-
])
|
|
1706
|
-
const response = promisesResult[0]
|
|
1707
|
-
const userPermissions = promisesResult[1]
|
|
1708
|
-
|
|
1709
|
-
if (!response.ok) {
|
|
1710
|
-
throw new Error(`Sanity API error: ${response.status} - ${response.statusText}`)
|
|
1711
|
-
}
|
|
1712
|
-
const result = await response.json()
|
|
1713
|
-
if (result.result) {
|
|
1714
|
-
let results = isList ? result.result : result.result[0]
|
|
1715
|
-
if (!results) {
|
|
1716
|
-
throw new Error('No results found')
|
|
1717
|
-
}
|
|
1718
|
-
results = processNeedAccess ? await needsAccessDecorator(results, userPermissions) : results
|
|
1719
|
-
results = processPageType ? pageTypeDecorator(results) : results
|
|
1720
|
-
return customPostProcess ? customPostProcess(results) : results
|
|
1721
|
-
} else {
|
|
1722
|
-
throw new Error('No results found')
|
|
1723
|
-
}
|
|
1724
|
-
} catch (error) {
|
|
1725
|
-
console.error('fetchSanity: Fetch error:', { error, query })
|
|
1726
|
-
return null
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
function contentResultsDecorator(results, fieldName, callback) {
|
|
1731
|
-
if (Array.isArray(results)) {
|
|
1732
|
-
results.forEach((result) => {
|
|
1733
|
-
// Check if this is a content row structure
|
|
1734
|
-
if (result.content && Array.isArray(result.content)) {
|
|
1735
|
-
// Content rows structure: array of rows, each with a content array
|
|
1736
|
-
result.content.forEach((contentItem) => {
|
|
1737
|
-
if (contentItem) {
|
|
1738
|
-
contentItem[fieldName] = callback(contentItem)
|
|
1739
|
-
}
|
|
1740
|
-
})
|
|
1741
|
-
} else {
|
|
1742
|
-
result[fieldName] = callback(result)
|
|
1743
|
-
}
|
|
1744
|
-
})
|
|
1745
|
-
} else if (results.entity && Array.isArray(results.entity)) {
|
|
1746
|
-
// Group By
|
|
1747
|
-
results.entity.forEach((result) => {
|
|
1748
|
-
if (result.lessons) {
|
|
1749
|
-
result.lessons.forEach((lesson) => {
|
|
1750
|
-
lesson[fieldName] = callback(lesson) // Updated to check lesson access
|
|
1751
|
-
})
|
|
1752
|
-
} else {
|
|
1753
|
-
result[fieldName] = callback(result)
|
|
1754
|
-
}
|
|
1755
|
-
})
|
|
1756
|
-
} else if (results.related_lessons && Array.isArray(results.related_lessons)) {
|
|
1757
|
-
results.related_lessons.forEach((result) => {
|
|
1758
|
-
result[fieldName] = callback(result)
|
|
1759
|
-
})
|
|
1760
|
-
} else {
|
|
1761
|
-
results[fieldName] = callback(results)
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
return results
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
function pageTypeDecorator(results) {
|
|
1768
|
-
return contentResultsDecorator(results, 'page_type', function (content) {
|
|
1769
|
-
return SONG_TYPES_WITH_CHILDREN.includes(content['type']) ? 'song' : 'lesson'
|
|
1770
|
-
})
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
function needsAccessDecorator(results, userPermissions) {
|
|
1774
|
-
if (globalConfig.sanityConfig.useDummyRailContentMethods) return results
|
|
1775
|
-
const adapter = getPermissionsAdapter()
|
|
1776
|
-
return contentResultsDecorator(results, 'need_access', function (content) {
|
|
1777
|
-
return adapter.doesUserNeedAccess(content, userPermissions)
|
|
1778
|
-
})
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
function doesUserNeedAccessToContent(result, userPermissions) {
|
|
1782
|
-
// Legacy function - now delegates to adapter
|
|
1783
|
-
// Kept for backwards compatibility if used elsewhere
|
|
1784
|
-
const adapter = getPermissionsAdapter()
|
|
1785
|
-
return adapter.doesUserNeedAccess(result, userPermissions)
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
/**
|
|
1789
|
-
* Fetch shows data for a brand.
|
|
1790
|
-
*
|
|
1791
|
-
* @param brand - The brand for which to fetch shows.
|
|
1792
|
-
* @returns {Promise<{name, description, type: *, thumbnailUrl}>}
|
|
1793
|
-
*
|
|
1794
|
-
* @example
|
|
1795
|
-
*
|
|
1796
|
-
* fetchShowsData('drumeo')
|
|
1797
|
-
* .then(data => console.log(data))
|
|
1798
|
-
* .catch(error => console.error(error));
|
|
1799
|
-
*/
|
|
1800
|
-
export async function fetchShowsData(brand) {
|
|
1801
|
-
let shows = showsTypes[brand] ?? []
|
|
1802
|
-
const showsInfo = []
|
|
1803
|
-
|
|
1804
|
-
shows.forEach((type) => {
|
|
1805
|
-
const processedData = processMetadata(brand, type)
|
|
1806
|
-
if (processedData) showsInfo.push(processedData)
|
|
1807
|
-
})
|
|
1808
|
-
|
|
1809
|
-
return showsInfo
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
/**
|
|
1813
|
-
* Fetch metadata from the contentMetaData.js based on brand and type.
|
|
1814
|
-
* For v2 you need to provide page type('lessons' or 'songs') in type parameter
|
|
1815
|
-
*
|
|
1816
|
-
* @param {string} brand - The brand for which to fetch metadata.
|
|
1817
|
-
* @param {string} type - The type for which to fetch metadata.
|
|
1818
|
-
* @returns {Promise<{name, description, type: *, thumbnailUrl}>}
|
|
1819
|
-
*
|
|
1820
|
-
* @example
|
|
1821
|
-
*
|
|
1822
|
-
* fetchMetadata('drumeo','song')
|
|
1823
|
-
* .then(data => console.log(data))
|
|
1824
|
-
* .catch(error => console.error(error));
|
|
1825
|
-
*/
|
|
1826
|
-
export async function fetchMetadata(brand, type) {
|
|
1827
|
-
let processedData = processMetadata(brand, type, true)
|
|
1828
|
-
if (processedData?.onlyAvailableTabs === true) {
|
|
1829
|
-
const activeTabs = await fetchRecentActivitiesActiveTabs()
|
|
1830
|
-
processedData.tabs = activeTabs
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
return processedData ? processedData : {}
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
export async function fetchChatAndLiveEnvent(brand, forcedId = null) {
|
|
1837
|
-
const liveEvent =
|
|
1838
|
-
forcedId !== null ? await fetchByRailContentIds([forcedId]) : [await fetchLiveEvent(brand)]
|
|
1839
|
-
if (liveEvent.length === 0 || (liveEvent.length === 1 && liveEvent[0] === undefined)) {
|
|
1840
|
-
return null
|
|
1841
|
-
}
|
|
1842
|
-
let url = `/content/live-chat?brand=${brand}`
|
|
1843
|
-
const chatData = await fetchHandler(url)
|
|
1844
|
-
const mergedData = { ...chatData, ...liveEvent[0] }
|
|
1845
|
-
return mergedData
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
//Helper Functions
|
|
1849
|
-
function arrayJoinWithQuotes(array, delimiter = ',') {
|
|
1850
|
-
const wrapped = array.map((value) => `'${value}'`)
|
|
1851
|
-
return wrapped.join(delimiter)
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
export function getSanityDate(date, roundToHourForCaching = true) {
|
|
1855
|
-
if (roundToHourForCaching) {
|
|
1856
|
-
// We need to set the published on filter date to be a round time so that it doesn't bypass the query cache
|
|
1857
|
-
// with every request by changing the filter date every second. I've set it to one minute past the current hour
|
|
1858
|
-
// because publishing usually publishes content on the hour exactly which means it should still skip the cache
|
|
1859
|
-
// when the new content is available.
|
|
1860
|
-
// Round to the start of the current hour
|
|
1861
|
-
const roundedDate = new Date(
|
|
1862
|
-
date.getFullYear(),
|
|
1863
|
-
date.getMonth(),
|
|
1864
|
-
date.getDate(),
|
|
1865
|
-
date.getHours()
|
|
1866
|
-
)
|
|
1867
|
-
|
|
1868
|
-
return roundedDate.toISOString()
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
return date.toISOString()
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
function getDateOnly(date = new Date()) {
|
|
1875
|
-
return date.toISOString().split('T')[0]
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
const merge = (a, b, predicate = (a, b) => a === b) => {
|
|
1879
|
-
const c = [...a] // copy to avoid side effects
|
|
1880
|
-
// add all items from B to copy C if they're not already present
|
|
1881
|
-
b.forEach((bItem) => (c.some((cItem) => predicate(bItem, cItem)) ? null : c.push(bItem)))
|
|
1882
|
-
return c
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
function checkSanityConfig(config) {
|
|
1886
|
-
if (!config.sanityConfig.token) {
|
|
1887
|
-
console.warn('fetchSanity: The "token" property is missing in the config object.')
|
|
1888
|
-
return false
|
|
1889
|
-
}
|
|
1890
|
-
if (!config.sanityConfig.projectId) {
|
|
1891
|
-
console.warn('fetchSanity: The "projectId" property is missing in the config object.')
|
|
1892
|
-
return false
|
|
1893
|
-
}
|
|
1894
|
-
if (!config.sanityConfig.dataset) {
|
|
1895
|
-
console.warn('fetchSanity: The "dataset" property is missing in the config object.')
|
|
1896
|
-
return false
|
|
1897
|
-
}
|
|
1898
|
-
if (!config.sanityConfig.version) {
|
|
1899
|
-
console.warn('fetchSanity: The "version" property is missing in the config object.')
|
|
1900
|
-
return false
|
|
1901
|
-
}
|
|
1902
|
-
return true
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
function buildRawQuery(
|
|
1906
|
-
filter = '',
|
|
1907
|
-
fields = '...',
|
|
1908
|
-
{ sortOrder = 'published_on desc', start = 0, end = 10, isSingle = false }
|
|
1909
|
-
) {
|
|
1910
|
-
const sortString = sortOrder ? `order(${sortOrder})` : ''
|
|
1911
|
-
const countString = isSingle ? '[0...1]' : `[${start}...${end}]`
|
|
1912
|
-
const query = `*[${filter}]{
|
|
1913
|
-
${fields}
|
|
1914
|
-
} | ${sortString}${countString}`
|
|
1915
|
-
return query
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
async function buildQuery(
|
|
1919
|
-
baseFilter = '',
|
|
1920
|
-
filterParams = { pullFutureContent: false },
|
|
1921
|
-
fields = '...',
|
|
1922
|
-
{ sortOrder = 'published_on desc', start = 0, end = 10, isSingle = false }
|
|
1923
|
-
) {
|
|
1924
|
-
const filter = await new FilterBuilder(baseFilter, filterParams).buildFilter()
|
|
1925
|
-
return buildRawQuery(filter, fields, { sortOrder, start, end, isSingle })
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
export function buildEntityAndTotalQuery(
|
|
1929
|
-
filter = '',
|
|
1930
|
-
fields = '...',
|
|
1931
|
-
{
|
|
1932
|
-
sortOrder = 'published_on desc',
|
|
1933
|
-
start = 0,
|
|
1934
|
-
end = 10,
|
|
1935
|
-
isSingle = false,
|
|
1936
|
-
withoutPagination = false,
|
|
1937
|
-
}
|
|
1938
|
-
) {
|
|
1939
|
-
const sortString = sortOrder ? ` | order(${sortOrder})` : ''
|
|
1940
|
-
const countString = isSingle ? '[0...1]' : withoutPagination ? `` : `[${start}...${end}]`
|
|
1941
|
-
const query = `{
|
|
1942
|
-
"entity": *[${filter}] ${sortString}${countString}
|
|
1943
|
-
{
|
|
1944
|
-
${fields}
|
|
1945
|
-
},
|
|
1946
|
-
"total": 0
|
|
1947
|
-
}`
|
|
1948
|
-
return query
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
function getFilterOptions(option, commonFilter, contentType, brand) {
|
|
1952
|
-
let filterGroq = ''
|
|
1953
|
-
const types = Array.from(new Set([...coachLessonsTypes, ...showsTypes[brand]]))
|
|
1954
|
-
|
|
1955
|
-
switch (option) {
|
|
1956
|
-
case 'difficulty':
|
|
1957
|
-
filterGroq = `
|
|
1958
|
-
"difficulty": [
|
|
1959
|
-
{"type": "All", "count": count(*[${commonFilter} && difficulty_string == "All"])},
|
|
1960
|
-
{"type": "Introductory", "count": count(*[${commonFilter} && (difficulty_string == "Novice" || difficulty_string == "Introductory")])},
|
|
1961
|
-
{"type": "Beginner", "count": count(*[${commonFilter} && difficulty_string == "Beginner"])},
|
|
1962
|
-
{"type": "Intermediate", "count": count(*[${commonFilter} && difficulty_string == "Intermediate" ])},
|
|
1963
|
-
{"type": "Advanced", "count": count(*[${commonFilter} && difficulty_string == "Advanced" ])},
|
|
1964
|
-
{"type": "Expert", "count": count(*[${commonFilter} && difficulty_string == "Expert" ])}
|
|
1965
|
-
][count > 0],`
|
|
1966
|
-
break
|
|
1967
|
-
case 'type':
|
|
1968
|
-
const typesString = types
|
|
1969
|
-
.map((t) => {
|
|
1970
|
-
return `{"type": "${t}"}`
|
|
1971
|
-
})
|
|
1972
|
-
.join(', ')
|
|
1973
|
-
filterGroq = `"type": [${typesString}]{type, 'count': count(*[_type == ^.type && ${commonFilter}])}[count > 0],`
|
|
1974
|
-
break
|
|
1975
|
-
case 'genre':
|
|
1976
|
-
case 'essential':
|
|
1977
|
-
case 'focus':
|
|
1978
|
-
case 'theory':
|
|
1979
|
-
case 'topic':
|
|
1980
|
-
case 'lifestyle':
|
|
1981
|
-
case 'creativity':
|
|
1982
|
-
filterGroq = `
|
|
1983
|
-
"${option}": *[_type == '${option}' ${contentType ? ` && '${contentType}' in filter_types` : ''} ] {
|
|
1984
|
-
"type": name,
|
|
1985
|
-
"count": count(*[${commonFilter} && references(^._id)])
|
|
1986
|
-
}[count > 0],`
|
|
1987
|
-
break
|
|
1988
|
-
case 'instrumentless':
|
|
1989
|
-
filterGroq = `
|
|
1990
|
-
"${option}": [
|
|
1991
|
-
{"type": "Full Song Only", "count": count(*[${commonFilter} && instrumentless == false ])},
|
|
1992
|
-
{"type": "Instrument Removed", "count": count(*[${commonFilter} && instrumentless == true ])}
|
|
1993
|
-
][count > 0],`
|
|
1994
|
-
break
|
|
1995
|
-
case 'gear':
|
|
1996
|
-
filterGroq = `
|
|
1997
|
-
"${option}": [
|
|
1998
|
-
{"type": "Practice Pad", "count": count(*[${commonFilter} && gear match 'Practice Pad' ])},
|
|
1999
|
-
{"type": "Drum-Set", "count": count(*[${commonFilter} && gear match 'Drum-Set'])}
|
|
2000
|
-
][count > 0],`
|
|
2001
|
-
break
|
|
2002
|
-
case 'bpm':
|
|
2003
|
-
filterGroq = `
|
|
2004
|
-
"${option}": [
|
|
2005
|
-
{"type": "50-90", "count": count(*[${commonFilter} && bpm > 50 && bpm < 91])},
|
|
2006
|
-
{"type": "91-120", "count": count(*[${commonFilter} && bpm > 90 && bpm < 121])},
|
|
2007
|
-
{"type": "121-150", "count": count(*[${commonFilter} && bpm > 120 && bpm < 151])},
|
|
2008
|
-
{"type": "151-180", "count": count(*[${commonFilter} && bpm > 150 && bpm < 181])},
|
|
2009
|
-
{"type": "180+", "count": count(*[${commonFilter} && bpm > 180])},
|
|
2010
|
-
][count > 0],`
|
|
2011
|
-
break
|
|
2012
|
-
default:
|
|
2013
|
-
filterGroq = ''
|
|
2014
|
-
break
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
return filterGroq
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
|
-
function cleanUpGroq(query) {
|
|
2021
|
-
// Split the query into clauses based on the logical operators
|
|
2022
|
-
const clauses = query.split(/(\s*&&|\s*\|\|)/).map((clause) => clause.trim())
|
|
2023
|
-
|
|
2024
|
-
// Filter out empty clauses
|
|
2025
|
-
const filteredClauses = clauses.filter((clause) => clause.length > 0)
|
|
2026
|
-
|
|
2027
|
-
// Check if there are valid conditions in the clauses
|
|
2028
|
-
const hasConditions = filteredClauses.some((clause) => !clause.match(/^\s*&&\s*|\s*\|\|\s*$/))
|
|
2029
|
-
|
|
2030
|
-
if (!hasConditions) {
|
|
2031
|
-
// If no valid conditions, return an empty string or the original query
|
|
2032
|
-
return ''
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
|
-
// Remove occurrences of '&& ()'
|
|
2036
|
-
const cleanedQuery = filteredClauses
|
|
2037
|
-
.join(' ')
|
|
2038
|
-
.replace(/&&\s*\(\)/g, '')
|
|
2039
|
-
.replace(/(\s*&&|\s*\|\|)(?=\s*[\s()]*$|(?=\s*&&|\s*\|\|))/g, '')
|
|
2040
|
-
.trim()
|
|
2041
|
-
|
|
2042
|
-
return cleanedQuery
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
// V2 methods
|
|
2046
|
-
|
|
2047
|
-
export async function fetchTabData(
|
|
2048
|
-
brand,
|
|
2049
|
-
pageName,
|
|
2050
|
-
{
|
|
2051
|
-
page = 1,
|
|
2052
|
-
limit = 10,
|
|
2053
|
-
sort = '-published_on',
|
|
2054
|
-
includedFields = [],
|
|
2055
|
-
progressIds = undefined,
|
|
2056
|
-
progress = 'all',
|
|
2057
|
-
showMembershipRestrictedContent = false,
|
|
2058
|
-
} = {}
|
|
2059
|
-
) {
|
|
2060
|
-
const start = (page - 1) * limit
|
|
2061
|
-
const end = start + limit
|
|
2062
|
-
// Construct the included fields filter, replacing 'difficulty' with 'difficulty_string'
|
|
2063
|
-
const includedFieldsFilter =
|
|
2064
|
-
includedFields.length > 0 ? filtersToGroq(includedFields, [], pageName) : ''
|
|
2065
|
-
|
|
2066
|
-
let sortOrder = getSortOrder(sort, brand, '')
|
|
2067
|
-
|
|
2068
|
-
switch (progress) {
|
|
2069
|
-
case 'recent':
|
|
2070
|
-
progressIds = await getAllStartedOrCompleted({ brand, onlyIds: true })
|
|
2071
|
-
sortOrder = null
|
|
2072
|
-
break
|
|
2073
|
-
case 'incomplete':
|
|
2074
|
-
progressIds = await getAllStarted()
|
|
2075
|
-
sortOrder = null
|
|
2076
|
-
break
|
|
2077
|
-
case 'completed':
|
|
2078
|
-
progressIds = await getAllCompleted()
|
|
2079
|
-
sortOrder = null
|
|
2080
|
-
break
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
// limits the results to supplied progressIds for started & completed filters
|
|
2084
|
-
const progressFilter = await getProgressFilter(progress, progressIds)
|
|
2085
|
-
const fieldsString = getFieldsForContentType('tab-data')
|
|
2086
|
-
const now = getSanityDate(new Date())
|
|
2087
|
-
|
|
2088
|
-
// Determine the group by clause
|
|
2089
|
-
let query = ''
|
|
2090
|
-
let entityFieldsString = ''
|
|
2091
|
-
let filter = ''
|
|
2092
|
-
|
|
2093
|
-
filter = `brand == "${brand}" && (defined(railcontent_id)) ${includedFieldsFilter} ${progressFilter}`
|
|
2094
|
-
const childrenFilter = await new FilterBuilder(``, {
|
|
2095
|
-
isChildrenFilter: true,
|
|
2096
|
-
showMembershipRestrictedContent: true,
|
|
2097
|
-
}).buildFilter()
|
|
2098
|
-
const childrenFields = await getChildFieldsForContentType('tab-data')
|
|
2099
|
-
const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`).buildFilter()
|
|
2100
|
-
entityFieldsString = ` ${fieldsString}
|
|
2101
|
-
'children': child[${childrenFilter}]->{ ${childrenFields} 'children': child[${childrenFilter}]->{ ${childrenFields} }, },
|
|
2102
|
-
'isLive': live_event_start_time <= "${now}" && live_event_end_time >= "${now}",
|
|
2103
|
-
'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
|
|
2104
|
-
'length_in_seconds': coalesce(
|
|
2105
|
-
math::sum(
|
|
2106
|
-
select(
|
|
2107
|
-
child[${childrenFilter}]->length_in_seconds
|
|
2108
|
-
)
|
|
2109
|
-
),
|
|
2110
|
-
length_in_seconds
|
|
2111
|
-
),`
|
|
2112
|
-
const filterWithRestrictions = await new FilterBuilder(filter, {
|
|
2113
|
-
showMembershipRestrictedContent: true,
|
|
2114
|
-
}).buildFilter()
|
|
2115
|
-
query = buildEntityAndTotalQuery(filterWithRestrictions, entityFieldsString, {
|
|
2116
|
-
sortOrder: sortOrder,
|
|
2117
|
-
start: start,
|
|
2118
|
-
end: end,
|
|
2119
|
-
})
|
|
2120
|
-
|
|
2121
|
-
let results = await fetchSanity(query, true, { processNeedAccess: true })
|
|
2122
|
-
|
|
2123
|
-
if (['recent', 'incomplete', 'completed'].includes(progress) && results.entity.length > 1) {
|
|
2124
|
-
const orderMap = new Map(progressIds.map((id, index) => [id, index]))
|
|
2125
|
-
results.entity = results.entity
|
|
2126
|
-
.sort((a, b) => {
|
|
2127
|
-
const aIdx = orderMap.get(a.id) ?? Number.MAX_SAFE_INTEGER
|
|
2128
|
-
const bIdx = orderMap.get(b.id) ?? Number.MAX_SAFE_INTEGER
|
|
2129
|
-
return aIdx - bIdx || new Date(b.published_on) - new Date(a.published_on)
|
|
2130
|
-
})
|
|
2131
|
-
.slice(start, end)
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
return results
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
export async function fetchRecent(
|
|
2138
|
-
brand,
|
|
2139
|
-
pageName,
|
|
2140
|
-
{ page = 1, limit = 10, sort = '-published_on', includedFields = [], progress = 'recent' } = {}
|
|
2141
|
-
) {
|
|
2142
|
-
const mergedIncludedFields = [...includedFields, `tab,all`]
|
|
2143
|
-
const results = await fetchTabData(brand, pageName, {
|
|
2144
|
-
page,
|
|
2145
|
-
limit,
|
|
2146
|
-
sort,
|
|
2147
|
-
includedFields: mergedIncludedFields,
|
|
2148
|
-
progress: progress.toLowerCase(),
|
|
2149
|
-
})
|
|
2150
|
-
return results.entity
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
export async function fetchScheduledAndNewReleases(
|
|
2154
|
-
brand,
|
|
2155
|
-
{ page = 1, limit = 20, sort = '-published_on' } = {}
|
|
2156
|
-
) {
|
|
2157
|
-
const upcomingTypes = getUpcomingEventsTypes(brand)
|
|
2158
|
-
const newTypes = getNewReleasesTypes(brand)
|
|
2159
|
-
|
|
2160
|
-
const scheduledTypes = merge(upcomingTypes, newTypes)
|
|
2161
|
-
const typesString = arrayJoinWithQuotes(scheduledTypes)
|
|
2162
|
-
const now = getSanityDate(new Date())
|
|
2163
|
-
|
|
2164
|
-
const start = (page - 1) * limit
|
|
2165
|
-
const end = start + limit
|
|
2166
|
-
const sortOrder = getSortOrder(sort, brand)
|
|
2167
|
-
|
|
2168
|
-
const query = `
|
|
2169
|
-
*[_type in [${typesString}] && brand == '${brand}' && ((status in ['published','scheduled'] )||(show_in_new_feed == true)) ]
|
|
2170
|
-
[${start}...${end}]
|
|
2171
|
-
| order(published_on asc) {
|
|
2172
|
-
"id": railcontent_id,
|
|
2173
|
-
title,
|
|
2174
|
-
"image": thumbnail.asset->url,
|
|
2175
|
-
"thumbnail": thumbnail.asset->url,
|
|
2176
|
-
${artistOrInstructorName()},
|
|
2177
|
-
"artists": instructor[]->name,
|
|
2178
|
-
difficulty,
|
|
2179
|
-
difficulty_string,
|
|
2180
|
-
length_in_seconds,
|
|
2181
|
-
published_on,
|
|
2182
|
-
"type": _type,
|
|
2183
|
-
show_in_new_feed,
|
|
2184
|
-
"permission_id": permission[]->railcontent_id,
|
|
2185
|
-
"isLive": live_event_start_time <= '${now}' && live_event_end_time >= '${now}',
|
|
2186
|
-
}`
|
|
2187
|
-
|
|
2188
|
-
return fetchSanity(query, true)
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
export async function fetchShows(brand, type, sort = 'sort') {
|
|
2192
|
-
const sortOrder = getSortOrder(sort, brand)
|
|
2193
|
-
const filter = `_type == '${type}' && brand == '${brand}'`
|
|
2194
|
-
const filterParams = {}
|
|
2195
|
-
|
|
2196
|
-
const query = await buildQuery(filter, filterParams, getFieldsForContentType(type), {
|
|
2197
|
-
sortOrder: sortOrder,
|
|
2198
|
-
end: 100, // Adrian: added for homepage progress rows, this should be handled gracefully
|
|
2199
|
-
})
|
|
2200
|
-
return fetchSanity(query, true)
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
/**
|
|
2204
|
-
* Fetch the method intro video for a given brand.
|
|
2205
|
-
* @param brand
|
|
2206
|
-
* @returns {Promise<*|null>}
|
|
2207
|
-
*/
|
|
2208
|
-
export async function fetchMethodV2IntroVideo(brand) {
|
|
2209
|
-
const type = 'method-intro'
|
|
2210
|
-
const filter = `_type == '${type}' && brand == '${brand}'`
|
|
2211
|
-
const fields = getIntroVideoFields('method-v2')
|
|
2212
|
-
|
|
2213
|
-
const query = `*[${filter}] { ${fields.join(', ')} }`
|
|
2214
|
-
return fetchSanity(query, false)
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
/**
|
|
2218
|
-
* Fetch the structure (just ids) of the Method for a given brand.
|
|
2219
|
-
* @param brand
|
|
2220
|
-
* @returns {Promise<*|null>}
|
|
2221
|
-
*/
|
|
2222
|
-
export async function fetchMethodV2Structure(brand) {
|
|
2223
|
-
const _type = 'method-v2'
|
|
2224
|
-
const query = `*[_type == '${_type}' && brand == '${brand}'][0...1]{
|
|
2225
|
-
'sanity_id': _id,
|
|
2226
|
-
'learning_paths': child[]->{
|
|
2227
|
-
'id': railcontent_id,
|
|
2228
|
-
'children': child[]->railcontent_id
|
|
2229
|
-
}
|
|
2230
|
-
}`
|
|
2231
|
-
return await fetchSanity(query, false)
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
/**
|
|
2235
|
-
* Fetch the structure (just ids) of the Method of a given learning path or learning path lesson.
|
|
2236
|
-
* @param contentId
|
|
2237
|
-
* @returns {Promise<*|null>}
|
|
2238
|
-
*/
|
|
2239
|
-
export async function fetchMethodV2StructureFromId(contentId) {
|
|
2240
|
-
const _type = "method-v2";
|
|
2241
|
-
const query = `*[_type == '${_type}' && brand == *[railcontent_id == ${contentId}][0].brand][0...1]{
|
|
2242
|
-
'sanity_id': _id,
|
|
2243
|
-
'learning_paths': child[]->{
|
|
2244
|
-
'id': railcontent_id,
|
|
2245
|
-
'children': child[]->railcontent_id
|
|
2246
|
-
}
|
|
2247
|
-
}`
|
|
2248
|
-
return await fetchSanity(query, false);
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
/**
|
|
2252
|
-
* Fetch content owned by the user (excluding membership content).
|
|
2253
|
-
* Shows only content accessible through purchases/entitlements, not through membership.
|
|
2254
|
-
*
|
|
2255
|
-
* @param {string} brand - The brand to filter content by
|
|
2256
|
-
* @param {Object} options - Fetch options
|
|
2257
|
-
* @param {Array<string>} options.type - Content type(s) to filter (optional array, default: [])
|
|
2258
|
-
* @param {number} options.page - Page number (default: 1)
|
|
2259
|
-
* @param {number} options.limit - Items per page (default: 10)
|
|
2260
|
-
* @param {string} options.sort - Sort field and direction (default: '-published_on')
|
|
2261
|
-
* @returns {Promise<Object>} Object with 'entity' (content array) and 'total' (count)
|
|
2262
|
-
*/
|
|
2263
|
-
export async function fetchOwnedContent(
|
|
2264
|
-
brand,
|
|
2265
|
-
{ type = [], page = 1, limit = 10, sort = '-published_on' } = {}
|
|
2266
|
-
) {
|
|
2267
|
-
const start = (page - 1) * limit
|
|
2268
|
-
const end = start + limit
|
|
2269
|
-
|
|
2270
|
-
// Determine the sort order
|
|
2271
|
-
const sortOrder = getSortOrder(sort, brand)
|
|
2272
|
-
|
|
2273
|
-
// Build the type filter
|
|
2274
|
-
let typeFilter = ''
|
|
2275
|
-
if (type.length > 0) {
|
|
2276
|
-
const typesString = type.map((t) => `'${t}'`).join(', ')
|
|
2277
|
-
typeFilter = `&& _type in [${typesString}]`
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
// Build the base filter
|
|
2281
|
-
const filter = `brand == "${brand}" ${typeFilter}`
|
|
2282
|
-
|
|
2283
|
-
// Apply owned content filter
|
|
2284
|
-
const filterWithRestrictions = await new FilterBuilder(filter, {
|
|
2285
|
-
showOnlyOwnedContent: true, // Key parameter: exclude membership content
|
|
2286
|
-
}).buildFilter()
|
|
2287
|
-
|
|
2288
|
-
const fieldsString = DEFAULT_FIELDS.join(',')
|
|
2289
|
-
|
|
2290
|
-
const query = buildEntityAndTotalQuery(filterWithRestrictions, fieldsString, {
|
|
2291
|
-
sortOrder: sortOrder,
|
|
2292
|
-
start: start,
|
|
2293
|
-
end: end,
|
|
2294
|
-
})
|
|
2295
|
-
|
|
2296
|
-
return fetchSanity(query, true)
|
|
2297
|
-
}
|
|
2298
|
-
</code></pre>
|
|
2299
|
-
</article>
|
|
2300
|
-
</section>
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
</div>
|
|
2308
|
-
|
|
2309
|
-
<br class="clear">
|
|
2310
|
-
|
|
2311
|
-
<footer>
|
|
2312
|
-
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.
|
|
2313
|
-
</footer>
|
|
2314
|
-
|
|
2315
|
-
<script>prettyPrint();</script>
|
|
2316
|
-
<script src="scripts/polyfill.js"></script>
|
|
2317
|
-
<script src="scripts/linenumber.js"></script>
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
</body>
|
|
2322
|
-
</html>
|