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.
Files changed (194) hide show
  1. package/.claude/settings.local.json +22 -0
  2. package/.coderabbit.yaml +0 -0
  3. package/.editorconfig +0 -0
  4. package/.github/pull_request_template.md +0 -0
  5. package/.github/workflows/conventional-commits.yaml +0 -0
  6. package/.github/workflows/docs.js.yml +1 -1
  7. package/.prettierignore +0 -0
  8. package/.prettierrc +0 -0
  9. package/CHANGELOG.md +44 -0
  10. package/CLAUDE.md +0 -0
  11. package/README.md +0 -0
  12. package/babel.config.cjs +0 -0
  13. package/jest.live.config.js +10 -0
  14. package/package.json +1 -1
  15. package/src/constants/award-assets.js +0 -0
  16. package/src/constants/membership-permissions.ts +0 -0
  17. package/src/contentMetaData.js +0 -0
  18. package/src/contentTypeConfig.js +2 -1
  19. package/src/filterBuilder.js +0 -0
  20. package/src/index.d.ts +5 -0
  21. package/src/index.js +5 -0
  22. package/src/infrastructure/http/HttpClient.ts +0 -0
  23. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
  24. package/src/infrastructure/http/index.ts +0 -0
  25. package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
  26. package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
  27. package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
  28. package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
  29. package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
  30. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
  31. package/src/lib/ads/monoid.ts +0 -0
  32. package/src/lib/ads/semigroup.ts +0 -0
  33. package/src/lib/brands.ts +0 -0
  34. package/src/lib/lastUpdated.js +0 -0
  35. package/src/lib/sanity/field-access.ts +0 -0
  36. package/src/lib/sanity/query.ts +0 -0
  37. package/src/services/api/types.js +0 -0
  38. package/src/services/api/types.ts +0 -0
  39. package/src/services/awards/award-callbacks.js +0 -0
  40. package/src/services/awards/award-query.js +0 -0
  41. package/src/services/awards/internal/.indexignore +0 -0
  42. package/src/services/awards/internal/award-definitions.js +0 -0
  43. package/src/services/awards/internal/award-events.js +0 -0
  44. package/src/services/awards/internal/award-manager.js +0 -0
  45. package/src/services/awards/internal/certificate-builder.js +0 -0
  46. package/src/services/awards/internal/completion-data-generator.js +0 -0
  47. package/src/services/awards/internal/content-progress-observer.js +0 -0
  48. package/src/services/awards/internal/image-utils.js +0 -0
  49. package/src/services/awards/internal/message-generator.js +0 -0
  50. package/src/services/awards/internal/types.js +0 -0
  51. package/src/services/awards/types.d.ts +0 -0
  52. package/src/services/awards/types.js +0 -0
  53. package/src/services/config.js +0 -0
  54. package/src/services/content/artist.ts +0 -0
  55. package/src/services/content/content.ts +0 -0
  56. package/src/services/content/genre.ts +0 -0
  57. package/src/services/content/instructor.ts +0 -0
  58. package/src/services/content-org/content-org.js +0 -0
  59. package/src/services/content-org/playlists-types.js +0 -0
  60. package/src/services/content-org/playlists.js +0 -0
  61. package/src/services/contentAggregator.js +0 -0
  62. package/src/services/contentLikes.js +0 -0
  63. package/src/services/dataContext.js +0 -0
  64. package/src/services/dateUtils.js +0 -0
  65. package/src/services/endScreen/README.md +62 -0
  66. package/src/services/endScreen/endScreen.ts +153 -0
  67. package/src/services/endScreen/types.ts +63 -0
  68. package/src/services/eventsAPI.js +0 -0
  69. package/src/services/forums/categories.ts +0 -0
  70. package/src/services/forums/forums.ts +0 -0
  71. package/src/services/forums/posts.ts +0 -0
  72. package/src/services/forums/threads.ts +13 -2
  73. package/src/services/forums/types.ts +0 -0
  74. package/src/services/gamification/awards.ts +0 -0
  75. package/src/services/gamification/gamification.js +0 -0
  76. package/src/services/imageSRCBuilder.js +0 -0
  77. package/src/services/imageSRCVerify.js +0 -0
  78. package/src/services/liveTesting.ts +0 -0
  79. package/src/services/permissions/PermissionsAdapterFactory.ts +0 -0
  80. package/src/services/permissions/README.md +0 -0
  81. package/src/services/progress-events.js +0 -0
  82. package/src/services/progress-row/base.js +0 -0
  83. package/src/services/progress-row/rows/.indexignore +0 -0
  84. package/src/services/progress-row/rows/content-card.js +0 -0
  85. package/src/services/progress-row/rows/playlist-card.js +0 -0
  86. package/src/services/railcontent.js +0 -0
  87. package/src/services/recommendations.js +3 -0
  88. package/src/services/reporting/README.md +0 -0
  89. package/src/services/reporting/types.ts +0 -0
  90. package/src/services/sanity.js +7 -6
  91. package/src/services/sentry/.indexignore +0 -0
  92. package/src/services/sentry/index.ts +0 -0
  93. package/src/services/state.ts +0 -0
  94. package/src/services/sync/.indexignore +0 -0
  95. package/src/services/sync/adapters/factory.ts +0 -0
  96. package/src/services/sync/adapters/lokijs.ts +1 -0
  97. package/src/services/sync/adapters/sqlite.ts +0 -0
  98. package/src/services/sync/context/providers/base.ts +0 -0
  99. package/src/services/sync/context/providers/connectivity.ts +0 -0
  100. package/src/services/sync/context/providers/durability.ts +0 -0
  101. package/src/services/sync/context/providers/index.ts +0 -0
  102. package/src/services/sync/context/providers/session.ts +0 -0
  103. package/src/services/sync/context/providers/tabs.ts +0 -0
  104. package/src/services/sync/context/providers/visibility.ts +0 -0
  105. package/src/services/sync/database/factory.ts +0 -0
  106. package/src/services/sync/debug.ts +0 -0
  107. package/src/services/sync/effects/index.ts +0 -0
  108. package/src/services/sync/effects/logout-warning.ts +0 -0
  109. package/src/services/sync/errors/boundary.ts +0 -0
  110. package/src/services/sync/errors/index.ts +0 -0
  111. package/src/services/sync/errors/validators.ts +0 -0
  112. package/src/services/sync/fetch.ts +0 -0
  113. package/src/services/sync/index.ts +0 -0
  114. package/src/services/sync/manager.ts +0 -0
  115. package/src/services/sync/models/Base.ts +0 -0
  116. package/src/services/sync/models/ContentLike.ts +0 -0
  117. package/src/services/sync/models/ContentProgress.ts +0 -0
  118. package/src/services/sync/models/Practice.ts +0 -0
  119. package/src/services/sync/models/PracticeDayNote.ts +0 -0
  120. package/src/services/sync/models/UserAwardProgress.ts +0 -0
  121. package/src/services/sync/models/index.ts +0 -0
  122. package/src/services/sync/repositories/content-likes.ts +0 -0
  123. package/src/services/sync/repositories/content-progress.ts +0 -0
  124. package/src/services/sync/repositories/index.ts +0 -0
  125. package/src/services/sync/repositories/practice-day-notes.ts +0 -0
  126. package/src/services/sync/repositories/practices.ts +0 -0
  127. package/src/services/sync/repositories/user-award-progress.ts +0 -0
  128. package/src/services/sync/repository-proxy.ts +0 -0
  129. package/src/services/sync/resolver.ts +0 -0
  130. package/src/services/sync/run-scope.ts +0 -0
  131. package/src/services/sync/schema/index.ts +0 -0
  132. package/src/services/sync/serializers/index.ts +0 -0
  133. package/src/services/sync/serializers/model.ts +0 -0
  134. package/src/services/sync/serializers/raw.ts +0 -0
  135. package/src/services/sync/store/index.ts +2 -6
  136. package/src/services/sync/store-configs.ts +0 -0
  137. package/src/services/sync/strategies/base.ts +0 -0
  138. package/src/services/sync/strategies/index.ts +0 -0
  139. package/src/services/sync/strategies/initial.ts +0 -0
  140. package/src/services/sync/strategies/polling.ts +0 -0
  141. package/src/services/sync/telemetry/flood-prevention.ts +0 -0
  142. package/src/services/sync/telemetry/sampling.ts +0 -0
  143. package/src/services/sync/utils/event-emitter.ts +0 -0
  144. package/src/services/sync/utils/index.ts +0 -0
  145. package/src/services/sync/utils/throttle.ts +0 -0
  146. package/src/services/sync/utils/timers.ts +0 -0
  147. package/src/services/types.js +0 -0
  148. package/src/services/urlBuilder.ts +0 -17
  149. package/src/services/user/account.ts +0 -0
  150. package/src/services/user/chat.js +0 -0
  151. package/src/services/user/interests.js +0 -0
  152. package/src/services/user/management.js +0 -0
  153. package/src/services/user/memberships.ts +0 -0
  154. package/src/services/user/notifications.js +0 -0
  155. package/src/services/user/payments.ts +0 -0
  156. package/src/services/user/profile.js +1 -1
  157. package/src/services/user/sessions.js +0 -0
  158. package/src/services/user/streakCalculator.ts +0 -0
  159. package/src/services/user/types.js +0 -0
  160. package/src/services/user/user-management-system.js +0 -0
  161. package/test/SKIPPED_TESTS.md +0 -0
  162. package/test/initializeTests.js +0 -0
  163. package/test/integration/content.test.js +0 -0
  164. package/test/integration/contentProgress.test.js +0 -0
  165. package/test/integration/forum.test.js +0 -0
  166. package/test/integration/sanityQueryService.test.js +0 -0
  167. package/test/localStorageMock.js +0 -0
  168. package/test/log.js +0 -0
  169. package/test/mockData/award-definitions.js +0 -0
  170. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  171. package/test/mockData/mockData_progress_content.json +0 -0
  172. package/test/mockData/mockData_sanity_progress_content.json +0 -0
  173. package/test/mockData/mockData_user_practices.json +0 -0
  174. package/test/setupConsole.js +0 -0
  175. package/test/setupNetworkGuard.js +0 -0
  176. package/test/unit/awards/award-callbacks.test.ts +144 -0
  177. package/test/unit/awards/internal/image-utils.test.ts +86 -0
  178. package/test/unit/endScreen.test.js +712 -0
  179. package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +39 -0
  180. package/test/unit/infrastructure/FetchRequestExecutor.test.ts +88 -0
  181. package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -0
  182. package/test/unit/lib/query.test.ts +0 -0
  183. package/test/unit/progress-row/playlist-card.test.ts +104 -0
  184. package/test/unit/sentry.test.ts +62 -0
  185. package/test/unit/sync/context.test.ts +51 -0
  186. package/test/unit/sync/errors/sync-errors.test.ts +106 -0
  187. package/test/unit/sync/errors/validators.test.ts +61 -0
  188. package/test/unit/sync/models/user-award-progress.test.ts +82 -0
  189. package/test/unit/sync/repositories/user-award-progress.static.test.ts +68 -0
  190. package/test/unit/sync/run-scope.test.ts +23 -0
  191. package/test/unit/sync/store-configs.test.ts +37 -0
  192. package/test/unit/sync/telemetry/sync-telemetry.test.ts +118 -0
  193. package/test/unit/sync/utils/event-emitter.test.ts +64 -0
  194. 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
@@ -31,7 +31,7 @@ jobs:
31
31
  - name: Setup Node.js
32
32
  uses: actions/setup-node@v4
33
33
  with:
34
- node-version: '20'
34
+ node-version: '24'
35
35
 
36
36
  - name: Install dependencies for v2
37
37
  working-directory: main-content
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
@@ -0,0 +1,10 @@
1
+ /** @type {import('jest').Config} */
2
+ import baseConfig from './jest.config.js'
3
+
4
+ export default {
5
+ ...baseConfig,
6
+ modulePathIgnorePatterns: [],
7
+ setupFilesAfterEnv: ['dotenv/config'],
8
+ testTimeout: 1000000,
9
+ collectCoverage: false,
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.157.0",
3
+ "version": "2.158.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
File without changes
File without changes
File without changes
@@ -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
  ]
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
package/src/lib/brands.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
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
- return httpClient.get<PaginatedResponse<ForumThread>>(`${baseUrl}/v1/threads?brand=${brand}`)
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