musora-content-services 2.157.0 → 2.158.0
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/.claude/settings.local.json +22 -0
- package/.coderabbit.yaml +0 -0
- package/.editorconfig +0 -0
- package/.github/pull_request_template.md +0 -0
- package/.github/workflows/conventional-commits.yaml +0 -0
- package/.github/workflows/docs.js.yml +1 -1
- package/.prettierignore +0 -0
- package/.prettierrc +0 -0
- package/CHANGELOG.md +44 -0
- package/CLAUDE.md +0 -0
- package/README.md +0 -0
- package/babel.config.cjs +0 -0
- package/jest.live.config.js +10 -0
- package/package.json +1 -1
- package/src/constants/award-assets.js +0 -0
- package/src/constants/membership-permissions.ts +0 -0
- package/src/contentMetaData.js +0 -0
- package/src/contentTypeConfig.js +2 -1
- package/src/filterBuilder.js +0 -0
- package/src/index.d.ts +5 -0
- package/src/index.js +5 -0
- package/src/infrastructure/http/HttpClient.ts +0 -0
- package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
- package/src/infrastructure/http/index.ts +0 -0
- package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
- package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
- package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
- package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
- package/src/lib/ads/monoid.ts +0 -0
- package/src/lib/ads/semigroup.ts +0 -0
- package/src/lib/brands.ts +0 -0
- package/src/lib/lastUpdated.js +0 -0
- package/src/lib/sanity/field-access.ts +0 -0
- package/src/lib/sanity/query.ts +0 -0
- package/src/services/api/types.js +0 -0
- package/src/services/api/types.ts +0 -0
- package/src/services/awards/award-callbacks.js +0 -0
- package/src/services/awards/award-query.js +0 -0
- package/src/services/awards/internal/.indexignore +0 -0
- package/src/services/awards/internal/award-definitions.js +0 -0
- package/src/services/awards/internal/award-events.js +0 -0
- package/src/services/awards/internal/award-manager.js +0 -0
- package/src/services/awards/internal/certificate-builder.js +0 -0
- package/src/services/awards/internal/completion-data-generator.js +0 -0
- package/src/services/awards/internal/content-progress-observer.js +0 -0
- package/src/services/awards/internal/image-utils.js +0 -0
- package/src/services/awards/internal/message-generator.js +0 -0
- package/src/services/awards/internal/types.js +0 -0
- package/src/services/awards/types.d.ts +0 -0
- package/src/services/awards/types.js +0 -0
- package/src/services/config.js +0 -0
- package/src/services/content/artist.ts +0 -0
- package/src/services/content/content.ts +0 -0
- package/src/services/content/genre.ts +0 -0
- package/src/services/content/instructor.ts +0 -0
- package/src/services/content-org/content-org.js +0 -0
- package/src/services/content-org/playlists-types.js +0 -0
- package/src/services/content-org/playlists.js +0 -0
- package/src/services/contentAggregator.js +0 -0
- package/src/services/contentLikes.js +0 -0
- package/src/services/dataContext.js +0 -0
- package/src/services/dateUtils.js +0 -0
- package/src/services/endScreen/README.md +62 -0
- package/src/services/endScreen/endScreen.ts +153 -0
- package/src/services/endScreen/types.ts +63 -0
- package/src/services/eventsAPI.js +0 -0
- package/src/services/forums/categories.ts +0 -0
- package/src/services/forums/forums.ts +0 -0
- package/src/services/forums/posts.ts +0 -0
- package/src/services/forums/threads.ts +13 -2
- package/src/services/forums/types.ts +0 -0
- package/src/services/gamification/awards.ts +0 -0
- package/src/services/gamification/gamification.js +0 -0
- package/src/services/imageSRCBuilder.js +0 -0
- package/src/services/imageSRCVerify.js +0 -0
- package/src/services/liveTesting.ts +0 -0
- package/src/services/permissions/PermissionsAdapterFactory.ts +0 -0
- package/src/services/permissions/README.md +0 -0
- package/src/services/progress-events.js +0 -0
- package/src/services/progress-row/base.js +0 -0
- package/src/services/progress-row/rows/.indexignore +0 -0
- package/src/services/progress-row/rows/content-card.js +0 -0
- package/src/services/progress-row/rows/playlist-card.js +0 -0
- package/src/services/railcontent.js +0 -0
- package/src/services/recommendations.js +3 -0
- package/src/services/reporting/README.md +0 -0
- package/src/services/reporting/types.ts +0 -0
- package/src/services/sanity.js +7 -6
- package/src/services/sentry/.indexignore +0 -0
- package/src/services/sentry/index.ts +0 -0
- package/src/services/state.ts +0 -0
- package/src/services/sync/.indexignore +0 -0
- package/src/services/sync/adapters/factory.ts +0 -0
- package/src/services/sync/adapters/lokijs.ts +1 -0
- package/src/services/sync/adapters/sqlite.ts +0 -0
- package/src/services/sync/context/providers/base.ts +0 -0
- package/src/services/sync/context/providers/connectivity.ts +0 -0
- package/src/services/sync/context/providers/durability.ts +0 -0
- package/src/services/sync/context/providers/index.ts +0 -0
- package/src/services/sync/context/providers/session.ts +0 -0
- package/src/services/sync/context/providers/tabs.ts +0 -0
- package/src/services/sync/context/providers/visibility.ts +0 -0
- package/src/services/sync/database/factory.ts +0 -0
- package/src/services/sync/debug.ts +0 -0
- package/src/services/sync/effects/index.ts +0 -0
- package/src/services/sync/effects/logout-warning.ts +0 -0
- package/src/services/sync/errors/boundary.ts +0 -0
- package/src/services/sync/errors/index.ts +0 -0
- package/src/services/sync/errors/validators.ts +0 -0
- package/src/services/sync/fetch.ts +0 -0
- package/src/services/sync/index.ts +0 -0
- package/src/services/sync/manager.ts +0 -0
- package/src/services/sync/models/Base.ts +0 -0
- package/src/services/sync/models/ContentLike.ts +0 -0
- package/src/services/sync/models/ContentProgress.ts +0 -0
- package/src/services/sync/models/Practice.ts +0 -0
- package/src/services/sync/models/PracticeDayNote.ts +0 -0
- package/src/services/sync/models/UserAwardProgress.ts +0 -0
- package/src/services/sync/models/index.ts +0 -0
- package/src/services/sync/repositories/content-likes.ts +0 -0
- package/src/services/sync/repositories/content-progress.ts +0 -0
- package/src/services/sync/repositories/index.ts +0 -0
- package/src/services/sync/repositories/practice-day-notes.ts +0 -0
- package/src/services/sync/repositories/practices.ts +0 -0
- package/src/services/sync/repositories/user-award-progress.ts +0 -0
- package/src/services/sync/repository-proxy.ts +0 -0
- package/src/services/sync/resolver.ts +0 -0
- package/src/services/sync/run-scope.ts +0 -0
- package/src/services/sync/schema/index.ts +0 -0
- package/src/services/sync/serializers/index.ts +0 -0
- package/src/services/sync/serializers/model.ts +0 -0
- package/src/services/sync/serializers/raw.ts +0 -0
- package/src/services/sync/store/index.ts +2 -6
- package/src/services/sync/store-configs.ts +0 -0
- package/src/services/sync/strategies/base.ts +0 -0
- package/src/services/sync/strategies/index.ts +0 -0
- package/src/services/sync/strategies/initial.ts +0 -0
- package/src/services/sync/strategies/polling.ts +0 -0
- package/src/services/sync/telemetry/flood-prevention.ts +0 -0
- package/src/services/sync/telemetry/sampling.ts +0 -0
- package/src/services/sync/utils/event-emitter.ts +0 -0
- package/src/services/sync/utils/index.ts +0 -0
- package/src/services/sync/utils/throttle.ts +0 -0
- package/src/services/sync/utils/timers.ts +0 -0
- package/src/services/types.js +0 -0
- package/src/services/urlBuilder.ts +0 -17
- package/src/services/user/account.ts +0 -0
- package/src/services/user/chat.js +0 -0
- package/src/services/user/interests.js +0 -0
- package/src/services/user/management.js +0 -0
- package/src/services/user/memberships.ts +0 -0
- package/src/services/user/notifications.js +0 -0
- package/src/services/user/payments.ts +0 -0
- package/src/services/user/profile.js +1 -1
- package/src/services/user/sessions.js +0 -0
- package/src/services/user/streakCalculator.ts +0 -0
- package/src/services/user/types.js +0 -0
- package/src/services/user/user-management-system.js +0 -0
- package/test/SKIPPED_TESTS.md +0 -0
- package/test/initializeTests.js +0 -0
- package/test/integration/content.test.js +0 -0
- package/test/integration/contentProgress.test.js +0 -0
- package/test/integration/forum.test.js +0 -0
- package/test/integration/sanityQueryService.test.js +0 -0
- package/test/localStorageMock.js +0 -0
- package/test/log.js +0 -0
- package/test/mockData/award-definitions.js +0 -0
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
- package/test/mockData/mockData_progress_content.json +0 -0
- package/test/mockData/mockData_sanity_progress_content.json +0 -0
- package/test/mockData/mockData_user_practices.json +0 -0
- package/test/setupConsole.js +0 -0
- package/test/setupNetworkGuard.js +0 -0
- package/test/unit/awards/award-callbacks.test.ts +144 -0
- package/test/unit/awards/internal/image-utils.test.ts +86 -0
- package/test/unit/endScreen.test.js +712 -0
- package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +39 -0
- package/test/unit/infrastructure/FetchRequestExecutor.test.ts +88 -0
- package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -0
- package/test/unit/lib/query.test.ts +0 -0
- package/test/unit/progress-row/playlist-card.test.ts +104 -0
- package/test/unit/sentry.test.ts +62 -0
- package/test/unit/sync/context.test.ts +51 -0
- package/test/unit/sync/errors/sync-errors.test.ts +106 -0
- package/test/unit/sync/errors/validators.test.ts +61 -0
- package/test/unit/sync/models/user-award-progress.test.ts +82 -0
- package/test/unit/sync/repositories/user-award-progress.static.test.ts +68 -0
- package/test/unit/sync/run-scope.test.ts +23 -0
- package/test/unit/sync/store-configs.test.ts +37 -0
- package/test/unit/sync/telemetry/sync-telemetry.test.ts +118 -0
- package/test/unit/sync/utils/event-emitter.test.ts +64 -0
- package/test/unit/url-builder.test.ts +72 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(tree:*)",
|
|
5
|
+
"Bash(find:*)",
|
|
6
|
+
"Bash(docker exec:*)",
|
|
7
|
+
"Bash(node:*)",
|
|
8
|
+
"Bash(npm run build-index:*)",
|
|
9
|
+
"Bash(npm run link_mcs:*)",
|
|
10
|
+
"Bash(./link_mcs.sh:*)",
|
|
11
|
+
"Bash(curl:*)",
|
|
12
|
+
"Bash(php artisan:*)",
|
|
13
|
+
"Bash(git checkout:*)",
|
|
14
|
+
"Bash(source .env)",
|
|
15
|
+
"Bash(echo:*)",
|
|
16
|
+
"Bash(grep:*)",
|
|
17
|
+
"Bash(gh api repos/railroadmedia/musora-platform-backend/commits/1f9878a5e681bf1e2233c0d59a618d477b118c49 --jq '.commit.message' 2>/dev/null || echo \"no access\")",
|
|
18
|
+
"WebFetch(domain:github.com)",
|
|
19
|
+
"Read(//app/**)"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
package/.coderabbit.yaml
CHANGED
|
File without changes
|
package/.editorconfig
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/.prettierignore
CHANGED
|
File without changes
|
package/.prettierrc
CHANGED
|
File without changes
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [2.158.0](https://github.com/railroadmedia/musora-content-services/compare/v2.153.0...v2.158.0) (2026-05-05)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **BEHLTP-23:** add multi-user account update endpoint ([#914](https://github.com/railroadmedia/musora-content-services/issues/914)) ([98f6c4b](https://github.com/railroadmedia/musora-content-services/commit/98f6c4bb830c7a8a25ba556ca07fc97023667011))
|
|
11
|
+
* **BEHSTP-167:** offline progress tracking support ([#889](https://github.com/railroadmedia/musora-content-services/issues/889)) ([0528597](https://github.com/railroadmedia/musora-content-services/commit/05285977237ccbfa577a5770a1762c31d855e59e))
|
|
12
|
+
* handle late-start active paths, and set up better resetAllLearningPaths ([#928](https://github.com/railroadmedia/musora-content-services/issues/928)) ([5d80430](https://github.com/railroadmedia/musora-content-services/commit/5d80430fbedfb21668882d402c5f6b07c82cfbf9))
|
|
13
|
+
* **MU2-1384:** enable related lessons for non-members ([#943](https://github.com/railroadmedia/musora-content-services/issues/943)) ([f091bd7](https://github.com/railroadmedia/musora-content-services/commit/f091bd7e7edfcfe8808684d97faff22f62ef6fe0))
|
|
14
|
+
* **MU2-1452:** free tier content sort to front ([#915](https://github.com/railroadmedia/musora-content-services/issues/915)) ([0655e68](https://github.com/railroadmedia/musora-content-services/commit/0655e685f32fe77728d7b8c2505283adbc1a7c97))
|
|
15
|
+
* **MU2-1463:** initialize onboarding flow ([#929](https://github.com/railroadmedia/musora-content-services/issues/929)) ([4daae5f](https://github.com/railroadmedia/musora-content-services/commit/4daae5ff28599b53fa9ee5b9bea8bdb08b706978))
|
|
16
|
+
* muppet - add invite validity/error data ([#923](https://github.com/railroadmedia/musora-content-services/issues/923)) ([3938fa5](https://github.com/railroadmedia/musora-content-services/commit/3938fa56ec9295023b6ba624c5f238d1645a911b))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* add missing vimeo_live_event_id field to downloads field list ([#948](https://github.com/railroadmedia/musora-content-services/issues/948)) ([2b54208](https://github.com/railroadmedia/musora-content-services/commit/2b5420838f200ee061dc7af27029df8a3c38f87c))
|
|
22
|
+
* add typeof before LokiJSAdapter in parameter list ([6a135b5](https://github.com/railroadmedia/musora-content-services/commit/6a135b57c516c277a50d707417544ab2f9eeb194))
|
|
23
|
+
* **BEHSTP-160:** hide future content in related lessons ([#952](https://github.com/railroadmedia/musora-content-services/issues/952)) ([b8d5e06](https://github.com/railroadmedia/musora-content-services/commit/b8d5e0662fae47285978ed1c2d641cc514f25ecd))
|
|
24
|
+
* **BR-460:** report option label updates ([#940](https://github.com/railroadmedia/musora-content-services/issues/940)) ([8ad8f72](https://github.com/railroadmedia/musora-content-services/commit/8ad8f72dd9a1284b4098aa00f08962a53a170275))
|
|
25
|
+
* **BR-627:** add pagination support to fetch followed threads method ([779e308](https://github.com/railroadmedia/musora-content-services/commit/779e30881e76b17e17124852c4d2ee02d62ccf87))
|
|
26
|
+
* **BR-632:** dont trickle progress to children when enrolling in GC ([#916](https://github.com/railroadmedia/musora-content-services/issues/916)) ([67a96d6](https://github.com/railroadmedia/musora-content-services/commit/67a96d69bd8b61f12d0efd45475a7b0b08c58813))
|
|
27
|
+
* **BR-659:** fix resources for null parents ([#951](https://github.com/railroadmedia/musora-content-services/issues/951)) ([b78157b](https://github.com/railroadmedia/musora-content-services/commit/b78157b9dbb9f4d8861bca9546eab79a1c04c01e))
|
|
28
|
+
* export ([#936](https://github.com/railroadmedia/musora-content-services/issues/936)) ([16b78be](https://github.com/railroadmedia/musora-content-services/commit/16b78be3dca3cee8981f1347a37f1b1b00ffe691))
|
|
29
|
+
* remove prevSession args ([#937](https://github.com/railroadmedia/musora-content-services/issues/937)) ([2fe1dc2](https://github.com/railroadmedia/musora-content-services/commit/2fe1dc280a21e060b15567afe60a4cf959cecc90))
|
|
30
|
+
* remove timestamp filtering of practice records in offline mode ([#938](https://github.com/railroadmedia/musora-content-services/issues/938)) ([1a8ac03](https://github.com/railroadmedia/musora-content-services/commit/1a8ac03bfa1b9b3670c45049adb9b833263ef871))
|
|
31
|
+
* revert activity timestamp changes ([#942](https://github.com/railroadmedia/musora-content-services/issues/942)) ([bd9f97d](https://github.com/railroadmedia/musora-content-services/commit/bd9f97d6547409b7f2856bf716c90d6723306c36))
|
|
32
|
+
* **TMA-239:** MCS - Sanity - Add a v=2 query parameter to all sanity requests to force a cache refresh ([#933](https://github.com/railroadmedia/musora-content-services/issues/933)) ([eac5a2c](https://github.com/railroadmedia/musora-content-services/commit/eac5a2c6848aee80c462ba0ccbeba52a92652fa9))
|
|
33
|
+
* **TMA-275:** MCS - Sanity - Add a v2 url segment to to all sanity requests to force a cache refresh ([#946](https://github.com/railroadmedia/musora-content-services/issues/946)) ([b5df1dc](https://github.com/railroadmedia/musora-content-services/commit/b5df1dc943cc9d477dcac5879daaf0725fa60f76))
|
|
34
|
+
|
|
35
|
+
### [2.157.2](https://github.com/railroadmedia/musora-content-services/compare/v2.157.0...v2.157.2) (2026-04-30)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### Bug Fixes
|
|
39
|
+
|
|
40
|
+
* add missing vimeo_live_event_id field to downloads field list ([#948](https://github.com/railroadmedia/musora-content-services/issues/948)) ([2b54208](https://github.com/railroadmedia/musora-content-services/commit/2b5420838f200ee061dc7af27029df8a3c38f87c))
|
|
41
|
+
|
|
42
|
+
### [2.157.1](https://github.com/railroadmedia/musora-content-services/compare/v2.157.0...v2.157.1) (2026-04-30)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
### Bug Fixes
|
|
46
|
+
|
|
47
|
+
* add missing vimeo_live_event_id field to downloads field list ([#948](https://github.com/railroadmedia/musora-content-services/issues/948)) ([2b54208](https://github.com/railroadmedia/musora-content-services/commit/2b5420838f200ee061dc7af27029df8a3c38f87c))
|
|
48
|
+
|
|
5
49
|
## [2.157.0](https://github.com/railroadmedia/musora-content-services/compare/v2.155.11...v2.157.0) (2026-04-29)
|
|
6
50
|
|
|
7
51
|
|
package/CLAUDE.md
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
File without changes
|
package/babel.config.cjs
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
package/src/contentMetaData.js
CHANGED
|
File without changes
|
package/src/contentTypeConfig.js
CHANGED
|
@@ -116,7 +116,7 @@ export const descriptionField = 'description[0].children[0].text'
|
|
|
116
116
|
// this pulls both any defined resources for the document as well as any resources in the parent document
|
|
117
117
|
export const resourcesField = `[
|
|
118
118
|
... resource[]{resource_name, _key, "resource_url": coalesce('${CloudFrontURl}'+string::split(resource_aws.asset->fileURL, '${AWSUrl}')[1], resource_url )},
|
|
119
|
-
... coalesce(parent_content_reference[]->resource[]{resource_name, _key, "resource_url": coalesce('${CloudFrontURl}'+string::split(resource_aws.asset->fileURL, '${AWSUrl}')[1], resource_url )}, []),
|
|
119
|
+
... coalesce(parent_content_reference[count(@->resource) > 0]->resource[]{resource_name, _key, "resource_url": coalesce('${CloudFrontURl}'+string::split(resource_aws.asset->fileURL, '${AWSUrl}')[1], resource_url )}, []),
|
|
120
120
|
]`
|
|
121
121
|
|
|
122
122
|
export const contentAwardField = "*[references(^._id) && _type == 'content-award'][0]"
|
|
@@ -149,6 +149,7 @@ export function getLiveFields(minimum = false) {
|
|
|
149
149
|
"live_event_start_time",
|
|
150
150
|
"live_event_end_time",
|
|
151
151
|
"live_event_stream_id",
|
|
152
|
+
"vimeo_live_event_id",
|
|
152
153
|
"'live_event_is_global': live_global_event == true",
|
|
153
154
|
"'videoId': coalesce(live_event_stream_id, video.external_id)",
|
|
154
155
|
]
|
package/src/filterBuilder.js
CHANGED
|
File without changes
|
package/src/index.d.ts
CHANGED
|
@@ -153,6 +153,10 @@ import {
|
|
|
153
153
|
toLocalDay
|
|
154
154
|
} from './services/dateUtils.js';
|
|
155
155
|
|
|
156
|
+
import {
|
|
157
|
+
getEndScreen
|
|
158
|
+
} from './services/endScreen/endScreen.ts';
|
|
159
|
+
|
|
156
160
|
import {
|
|
157
161
|
createForumCategory,
|
|
158
162
|
deleteForumCategory,
|
|
@@ -667,6 +671,7 @@ declare module 'musora-content-services' {
|
|
|
667
671
|
getContentAwardsByIds,
|
|
668
672
|
getContentRows,
|
|
669
673
|
getDailySession,
|
|
674
|
+
getEndScreen,
|
|
670
675
|
getEnrichedLearningPath,
|
|
671
676
|
getEnrichedLearningPaths,
|
|
672
677
|
getHierarchies,
|
package/src/index.js
CHANGED
|
@@ -157,6 +157,10 @@ import {
|
|
|
157
157
|
toLocalDay
|
|
158
158
|
} from './services/dateUtils.js';
|
|
159
159
|
|
|
160
|
+
import {
|
|
161
|
+
getEndScreen
|
|
162
|
+
} from './services/endScreen/endScreen.ts';
|
|
163
|
+
|
|
160
164
|
import {
|
|
161
165
|
createForumCategory,
|
|
162
166
|
deleteForumCategory,
|
|
@@ -666,6 +670,7 @@ export {
|
|
|
666
670
|
getContentAwardsByIds,
|
|
667
671
|
getContentRows,
|
|
668
672
|
getDailySession,
|
|
673
|
+
getEndScreen,
|
|
669
674
|
getEnrichedLearningPath,
|
|
670
675
|
getEnrichedLearningPaths,
|
|
671
676
|
getHierarchies,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/lib/ads/monoid.ts
CHANGED
|
File without changes
|
package/src/lib/ads/semigroup.ts
CHANGED
|
File without changes
|
package/src/lib/brands.ts
CHANGED
|
File without changes
|
package/src/lib/lastUpdated.js
CHANGED
|
File without changes
|
|
File without changes
|
package/src/lib/sanity/query.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/services/config.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# End Screen Service
|
|
2
|
+
|
|
3
|
+
Determines what content to show after a user finishes a lesson.
|
|
4
|
+
|
|
5
|
+
## Functions
|
|
6
|
+
|
|
7
|
+
| Function | Type | Use case |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `getEndScreen(params)` | async | All content types — lessons, courses, playlists |
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Decision Logic
|
|
14
|
+
|
|
15
|
+
### `getEndScreen`
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
single-song / play-along / jam → countdown-up-next + RecSys recommendation
|
|
19
|
+
playlist (not last item) → countdown-up-next + next item
|
|
20
|
+
playlist (last item) → null
|
|
21
|
+
no course → countdown-up-next + RecSys recommendation
|
|
22
|
+
course (not last lesson) → countdown-up-next + next lesson
|
|
23
|
+
course (last, has next course) → course-complete + first lesson of next course
|
|
24
|
+
course (last, no next course) → course-complete + RecSys recommendation
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
RecSys fallback: if the recommender returns nothing → related lesson (standalone) or related course (course-complete)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## API Reference
|
|
31
|
+
|
|
32
|
+
### `getEndScreen(params): Promise<EndScreenResult>`
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// Parameters
|
|
36
|
+
{
|
|
37
|
+
lesson: { id: number, type?: string } // required
|
|
38
|
+
brand: string // required
|
|
39
|
+
course?: { id: number, children?: ContentItem[] }
|
|
40
|
+
collection?: { id: number, type: string, children?: { id: number, children?: ContentItem[] }[] }
|
|
41
|
+
playlist?: { id: number, items?: ContentItem[] }
|
|
42
|
+
user_playlist_item_index?: number // current item's index in playlist; skips findIndex lookup when provided
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Return value
|
|
46
|
+
{
|
|
47
|
+
variant: 'countdown-up-next' | 'course-complete'
|
|
48
|
+
upNext: object | null // null if RecSys and related content both return nothing
|
|
49
|
+
countdownAutoplay: boolean
|
|
50
|
+
ctaLabels: { primary: string, secondary: string }
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Implementation
|
|
57
|
+
|
|
58
|
+
### When to call
|
|
59
|
+
|
|
60
|
+
Call `getEndScreen` **at page load**, not when the video ends. RecSys calls happen in the background while the user watches — by the time the video ends, the result is ready.
|
|
61
|
+
|
|
62
|
+
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {fetchSimilarItems} from '../recommendations.js'
|
|
2
|
+
import {fetchByRailContentIds, fetchCourseCollectionData, fetchRelatedLessons} from '../sanity.js'
|
|
3
|
+
import { addContextToContent } from '../contentAggregator.js'
|
|
4
|
+
import { playAlongLessonTypes, jamTrackLessonTypes, lessonTypesMapping } from '../../contentTypeConfig.js'
|
|
5
|
+
import {getUserData} from "../user/management.js";
|
|
6
|
+
import type {
|
|
7
|
+
ContentItem,
|
|
8
|
+
Collection,
|
|
9
|
+
Course,
|
|
10
|
+
Playlist,
|
|
11
|
+
CtaLabels,
|
|
12
|
+
EndScreenResult,
|
|
13
|
+
GetEndScreenParams,
|
|
14
|
+
} from './types.ts'
|
|
15
|
+
|
|
16
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const SINGLE_SONG_LESSON_TYPES: string[] = [
|
|
19
|
+
...playAlongLessonTypes,
|
|
20
|
+
...jamTrackLessonTypes,
|
|
21
|
+
...lessonTypesMapping['single lessons'],
|
|
22
|
+
]
|
|
23
|
+
const COURSE_COMPLETE_CTA: CtaLabels = { primary: 'Play Now', secondary: 'Back to Home'}
|
|
24
|
+
const COUNTDOWN_CTA: CtaLabels = { primary: 'Play Now' }
|
|
25
|
+
const COUNTDOWN_CTA_REPLAY: CtaLabels = { primary: 'Play Now', secondary: 'Replay' }
|
|
26
|
+
|
|
27
|
+
export async function getEndScreen({
|
|
28
|
+
lesson,
|
|
29
|
+
course = null,
|
|
30
|
+
collection = null,
|
|
31
|
+
playlist = null,
|
|
32
|
+
user_playlist_item_index = null,
|
|
33
|
+
next_item = null,
|
|
34
|
+
brand
|
|
35
|
+
}: GetEndScreenParams): Promise<EndScreenResult> {
|
|
36
|
+
const userData = await getUserData()
|
|
37
|
+
const isAdmin = userData?.is_admin ?? false
|
|
38
|
+
|
|
39
|
+
if (playlist) {
|
|
40
|
+
const nextItem = next_item ??
|
|
41
|
+
getNextItemInPlaylistOrNull(lesson.id, playlist, user_playlist_item_index, isAdmin)
|
|
42
|
+
|
|
43
|
+
return nextItem ? buildCountdown(nextItem, false) : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (SINGLE_SONG_LESSON_TYPES.includes(lesson.type ?? '')) {
|
|
47
|
+
return buildCountdown(await fetchEndScreenRecommendation(brand, lesson.id, null, isAdmin), true)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!course) {
|
|
51
|
+
return buildCountdown(await fetchEndScreenRecommendation(brand, lesson.id, null, isAdmin), false)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const nextLesson = getNextLessonOrNull(lesson.id, course, isAdmin)
|
|
55
|
+
if (nextLesson) {
|
|
56
|
+
return buildCountdown(nextLesson, false)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// TODO: remove internal fetch if FE provides collection.children directly
|
|
60
|
+
// collection.vue fetches lessonsInCourse (course children) but not the parent collection's courses
|
|
61
|
+
const resolvedCollection: Collection | null =
|
|
62
|
+
collection?.type === 'course' && collection?.parent_id
|
|
63
|
+
? await fetchCourseCollectionData(collection.parent_id)
|
|
64
|
+
: collection
|
|
65
|
+
|
|
66
|
+
const nextCourseFirstLesson = getFirstLessonOfNextCourseOrNull(course.id, resolvedCollection, isAdmin)
|
|
67
|
+
if (nextCourseFirstLesson) {
|
|
68
|
+
return buildCourseComplete(nextCourseFirstLesson)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return buildCourseComplete(await fetchEndScreenRecommendation(brand, lesson.id, course.id, isAdmin))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Builders ─────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function buildCountdown(upNext: any, withReply: boolean): EndScreenResult {
|
|
77
|
+
return { variant: 'countdown-up-next', upNext, countdownAutoplay: true, ctaLabels: withReply ? COUNTDOWN_CTA_REPLAY : COUNTDOWN_CTA }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildCourseComplete(upNext: any): EndScreenResult {
|
|
81
|
+
return { variant: 'course-complete', upNext, countdownAutoplay: false, ctaLabels: COURSE_COMPLETE_CTA }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function getNextItemInPlaylistOrNull(contentId: number, playlist: Playlist, user_playlist_item_index: number|null, isAdmin: boolean): ContentItem | null {
|
|
87
|
+
const items = playlist.items ?? []
|
|
88
|
+
const index = user_playlist_item_index !== null ? user_playlist_item_index : items.findIndex((item) => Number(item.id) === Number(contentId))
|
|
89
|
+
if (index < 0 || index === items.length - 1) return null
|
|
90
|
+
return items.slice(index + 1).find(item => isReleasedContent(item, isAdmin)) ?? null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getNextLessonOrNull(lessonId: number, course: Course, isAdmin: boolean): ContentItem | null {
|
|
94
|
+
const children = course.children ?? []
|
|
95
|
+
const index = children.findIndex((child) => Number(child.id) === Number(lessonId))
|
|
96
|
+
if (index < 0 || index === children.length - 1) return null
|
|
97
|
+
return children.slice(index + 1).find(child => isReleasedContent(child, isAdmin)) ?? null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isReleasedContent(content: ContentItem, isAdmin: boolean): boolean {
|
|
101
|
+
if (isAdmin) return true
|
|
102
|
+
if (!content?.status) return false
|
|
103
|
+
return (content.status === 'published') && !content.need_access
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getFirstLessonOfNextCourseOrNull(
|
|
107
|
+
courseId: number,
|
|
108
|
+
collection: Collection | null,
|
|
109
|
+
isAdmin: boolean
|
|
110
|
+
): ContentItem | null {
|
|
111
|
+
if (!collection) return null
|
|
112
|
+
const courses = collection.children ?? []
|
|
113
|
+
const index = courses.findIndex((course) => Number(course.id) === Number(courseId))
|
|
114
|
+
if (index < 0 || index === courses.length - 1) return null
|
|
115
|
+
const nextCourse = courses.slice(index + 1).find(course => isReleasedContent(course, isAdmin))
|
|
116
|
+
if (!nextCourse?.children) return null
|
|
117
|
+
return nextCourse.children.find(lesson => isReleasedContent(lesson, isAdmin)) ?? null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function fetchEndScreenRecommendation(
|
|
121
|
+
brand: string,
|
|
122
|
+
contentId: number,
|
|
123
|
+
parentId: number | null = null,
|
|
124
|
+
isAdmin: boolean
|
|
125
|
+
): Promise<any | null> {
|
|
126
|
+
try {
|
|
127
|
+
let recData: number[] = await fetchSimilarItems(contentId, brand, 20)
|
|
128
|
+
const contents: ContentItem[] = recData.length > 0 ? await fetchByRailContentIds(recData) : []
|
|
129
|
+
let recommended =
|
|
130
|
+
contents?.find((c) => isReleasedContent(c, isAdmin) && c.id !== parentId && (!c.parent_id || c.parent_id !== parentId)) ??
|
|
131
|
+
contents?.find((c) => isReleasedContent(c, isAdmin) && c.id !== parentId) ??
|
|
132
|
+
null
|
|
133
|
+
|
|
134
|
+
if (!recommended) {
|
|
135
|
+
const relatedLesson = await fetchRelatedLessons(parentId ?? contentId).then((r) =>
|
|
136
|
+
r.related_lessons?.[0] ?? null
|
|
137
|
+
)
|
|
138
|
+
recommended = isReleasedContent(relatedLesson, isAdmin) ? relatedLesson : null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!recommended) {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return await addContextToContent(() => recommended, {
|
|
146
|
+
addProgressPercentage: true,
|
|
147
|
+
addProgressStatus: true,
|
|
148
|
+
addNavigateTo: true,
|
|
149
|
+
})
|
|
150
|
+
} catch {
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module EndScreen Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type EndScreenVariant =
|
|
6
|
+
| 'countdown-up-next'
|
|
7
|
+
| 'course-complete'
|
|
8
|
+
|
|
9
|
+
export interface CtaLabels {
|
|
10
|
+
primary: string
|
|
11
|
+
secondary?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface EndScreenResult {
|
|
15
|
+
variant: EndScreenVariant
|
|
16
|
+
upNext: object | null
|
|
17
|
+
countdownAutoplay: boolean
|
|
18
|
+
ctaLabels: CtaLabels
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ContentItem {
|
|
22
|
+
id: number
|
|
23
|
+
type?: string
|
|
24
|
+
status?: string
|
|
25
|
+
parent_id?: number
|
|
26
|
+
need_access?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Course {
|
|
30
|
+
id: number
|
|
31
|
+
type?: string
|
|
32
|
+
status?: string
|
|
33
|
+
children?: ContentItem[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CourseInCollection {
|
|
37
|
+
id: number
|
|
38
|
+
status?: string
|
|
39
|
+
children?: ContentItem[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Collection {
|
|
43
|
+
id: number
|
|
44
|
+
type: string
|
|
45
|
+
status?: string
|
|
46
|
+
parent_id?: number
|
|
47
|
+
children?: CourseInCollection[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface Playlist {
|
|
51
|
+
id: number
|
|
52
|
+
items?: ContentItem[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface GetEndScreenParams {
|
|
56
|
+
lesson: ContentItem
|
|
57
|
+
course?: Course | null
|
|
58
|
+
collection?: Collection | null
|
|
59
|
+
playlist?: Playlist | null
|
|
60
|
+
user_playlist_item_index?: number | null
|
|
61
|
+
next_item?: any | null
|
|
62
|
+
brand: string
|
|
63
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -184,18 +184,29 @@ export async function unlockThread(threadId: number, brand: string): Promise<voi
|
|
|
184
184
|
return httpClient.delete<void>(`${baseUrl}/v1/threads/${threadId}/lock?brand=${brand}`)
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
export interface FetchFollowedThreadsParams {
|
|
188
|
+
page?: number,
|
|
189
|
+
limit?: number,
|
|
190
|
+
}
|
|
191
|
+
|
|
187
192
|
/**
|
|
188
193
|
* Fetches followed forum Threads for the given brand and current user.
|
|
189
194
|
*
|
|
190
195
|
* @param {string} brand - The brand context (e.g., "drumeo", "singeo").
|
|
196
|
+
* @param {FetchFollowedThreadsParams} params - Optional pagination parameters.
|
|
191
197
|
* @returns {Promise<PaginatedResponse<ForumThread>>} - A promise that resolves to the list of forum threads.
|
|
192
198
|
* @throws {HttpError} - If the request fails.
|
|
193
199
|
*/
|
|
194
200
|
export async function fetchFollowedThreads(
|
|
195
|
-
brand: string
|
|
201
|
+
brand: string,
|
|
202
|
+
params: FetchFollowedThreadsParams = {}
|
|
196
203
|
): Promise<PaginatedResponse<ForumThread>> {
|
|
197
204
|
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
198
|
-
|
|
205
|
+
const queryObj: Record<string, string> = { brand, ...Object.fromEntries(
|
|
206
|
+
Object.entries(params).filter(([_, v]) => v !== undefined && v !== null).map(([k, v]) => [k, String(v)])
|
|
207
|
+
)}
|
|
208
|
+
const query = new URLSearchParams(queryObj).toString()
|
|
209
|
+
return httpClient.get<PaginatedResponse<ForumThread>>(`${baseUrl}/v1/threads?${query}`)
|
|
199
210
|
}
|
|
200
211
|
|
|
201
212
|
export interface FetchLatestThreadParams {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|