musora-content-services 2.180.0 → 2.182.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 +9 -47
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +26 -0
- package/dist/services/api/types.js +2 -0
- package/jsdoc.json +1 -0
- package/package.json +1 -1
- package/src/index.d.ts +31 -18
- package/src/index.js +31 -18
- package/src/services/config.js +31 -0
- package/src/services/contentProgress.js +1 -1
- package/src/services/my-path/dev/map-types.ts +123 -0
- package/src/services/{content-org → my-path}/learning-paths.ts +22 -8
- package/src/services/my-path/placement-quiz.ts +57 -0
- package/src/services/progress/mutations.ts +1 -1
- package/src/services/progress-row/base.js +26 -10
- package/src/services/progress-row/rows/method-card.js +1 -1
- package/src/services/user/streakCalculator.ts +5 -1
- package/src/services/userActivity.js +1 -1
- package/src/version-info.js +1 -1
|
@@ -1,50 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"Bash(
|
|
5
|
-
"Bash(
|
|
6
|
-
"
|
|
7
|
-
"Bash(counselors ls *)",
|
|
8
|
-
"Bash(counselors groups *)",
|
|
9
|
-
"Bash(counselors run *)",
|
|
4
|
+
"Bash(rg:*)",
|
|
5
|
+
"Bash(npm run lint:*)",
|
|
6
|
+
"Bash(ls:*)",
|
|
10
7
|
"Bash(npm test *)",
|
|
11
|
-
"Bash(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"Read(//tmp/pr-review-v2/**)",
|
|
15
|
-
"Bash(cat /home/alesevero/railenvironment/applications/musora-content-services/AGENTS.md)",
|
|
16
|
-
"Bash(echo \"no AGENTS.md\")",
|
|
17
|
-
"Bash(echo \"exit=$?\")",
|
|
18
|
-
"Bash(git checkout *)",
|
|
19
|
-
"Skill(pr)",
|
|
20
|
-
"Skill(create-decision)",
|
|
21
|
-
"mcp__github__pull_request_read",
|
|
22
|
-
"mcp__github__push_files",
|
|
23
|
-
"Bash(git push *)",
|
|
24
|
-
"mcp__github__create_pull_request",
|
|
25
|
-
"mcp__github__search_pull_requests",
|
|
26
|
-
"Bash(git commit *)",
|
|
27
|
-
"Bash(git add *)",
|
|
28
|
-
"mcp__github__list_pull_requests",
|
|
29
|
-
"mcp__sanity__get_schema",
|
|
30
|
-
"mcp__sanity__query_documents",
|
|
31
|
-
"Bash(git rm *)",
|
|
32
|
-
"Bash(git rebase *)",
|
|
33
|
-
"Bash(git fetch *)",
|
|
34
|
-
"Bash(git --no-pager log --oneline -1)",
|
|
35
|
-
"Bash(git --no-pager log --oneline -5)",
|
|
36
|
-
"Bash(git ls-tree *)",
|
|
37
|
-
"Bash(git --no-pager log --oneline -2)",
|
|
38
|
-
"Bash(git --no-pager diff --stat HEAD~1)",
|
|
39
|
-
"Bash(git check-ignore *)",
|
|
40
|
-
"Bash(python3 -)",
|
|
41
|
-
"Bash(git --no-pager log --oneline -3)",
|
|
42
|
-
"Bash(mkdir *)",
|
|
43
|
-
"Bash(git --no-pager log --oneline origin/main..HEAD)"
|
|
44
|
-
]
|
|
8
|
+
"Bash(npx jest *)"
|
|
9
|
+
],
|
|
10
|
+
"deny": []
|
|
45
11
|
},
|
|
46
|
-
"model": "sonnet",
|
|
47
12
|
"enabledMcpjsonServers": [
|
|
13
|
+
"1password",
|
|
48
14
|
"atlassian",
|
|
49
15
|
"figma",
|
|
50
16
|
"google-workspace",
|
|
@@ -58,14 +24,10 @@
|
|
|
58
24
|
"chrome-devtools",
|
|
59
25
|
"railway",
|
|
60
26
|
"github",
|
|
61
|
-
"asana"
|
|
62
|
-
],
|
|
63
|
-
"disabledMcpjsonServers": [
|
|
27
|
+
"asana",
|
|
64
28
|
"nightwatch",
|
|
65
|
-
"1password",
|
|
66
29
|
"mysql-staging",
|
|
67
30
|
"mysql-local",
|
|
68
31
|
"mobile"
|
|
69
|
-
]
|
|
70
|
-
"effortLevel": "medium"
|
|
32
|
+
]
|
|
71
33
|
}
|
|
Binary file
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
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.182.0](https://github.com/railroadmedia/musora-content-services/compare/v2.181.0...v2.182.0) (2026-09-21)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **BEHLTP-368:** placement quiz endpoints ([#1051](https://github.com/railroadmedia/musora-content-services/issues/1051)) ([248b4c9](https://github.com/railroadmedia/musora-content-services/commit/248b4c9e48b056a651d44c129d5d0cb3ce80777d))
|
|
11
|
+
* **BEHLTP-395:** add resetActiveLearningPath() ([#1055](https://github.com/railroadmedia/musora-content-services/issues/1055)) ([02df2df](https://github.com/railroadmedia/musora-content-services/commit/02df2dffd59844ff0c11a43f357efa6ed346a07f))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* move learning-paths.ts to my-path/ module ([#1054](https://github.com/railroadmedia/musora-content-services/issues/1054)) ([49fb9a9](https://github.com/railroadmedia/musora-content-services/commit/49fb9a983512e1d657285ddd13555d5ae9cb8e7b))
|
|
17
|
+
|
|
18
|
+
## [2.181.0](https://github.com/railroadmedia/musora-content-services/compare/v2.180.0...v2.181.0) (2026-09-17)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* allow merged config ([#1018](https://github.com/railroadmedia/musora-content-services/issues/1018)) ([8d8a9aa](https://github.com/railroadmedia/musora-content-services/commit/8d8a9aaed048e83b7dfeb15a7c12fabd199721f1))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* **BR-717:** expire streak cache at calendar day boundary ([1f4481c](https://github.com/railroadmedia/musora-content-services/commit/1f4481cc5a71afa990c39d5927099b093a475c83))
|
|
29
|
+
* **BR-964:** unpin deleted playlists at fetch time ([#1052](https://github.com/railroadmedia/musora-content-services/issues/1052)) ([0367143](https://github.com/railroadmedia/musora-content-services/commit/036714377469beccbceae93d138a9d2280516395))
|
|
30
|
+
|
|
5
31
|
## [2.180.0](https://github.com/railroadmedia/musora-content-services/compare/v2.179.0...v2.180.0) (2026-09-04)
|
|
6
32
|
|
|
7
33
|
|
package/jsdoc.json
CHANGED
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -20,7 +20,9 @@ import {
|
|
|
20
20
|
getMCSVersion,
|
|
21
21
|
globalConfig,
|
|
22
22
|
initializeEnvVar,
|
|
23
|
-
initializeService
|
|
23
|
+
initializeService,
|
|
24
|
+
setSessionUserData,
|
|
25
|
+
updateSessionConfig
|
|
24
26
|
} from './services/config.js';
|
|
25
27
|
|
|
26
28
|
import {
|
|
@@ -53,23 +55,6 @@ import {
|
|
|
53
55
|
unEnrollUserInGuidedCourse
|
|
54
56
|
} from './services/content-org/guided-courses.ts';
|
|
55
57
|
|
|
56
|
-
import {
|
|
57
|
-
completeLearningPathIntroVideo,
|
|
58
|
-
completeMethodIntroVideo,
|
|
59
|
-
fetchLearningPathLessons,
|
|
60
|
-
fetchLearningPathProgressCheckLessons,
|
|
61
|
-
getActivePath,
|
|
62
|
-
getDailySession,
|
|
63
|
-
getEnrichedLearningPath,
|
|
64
|
-
getEnrichedLearningPaths,
|
|
65
|
-
getLearningPathLessonsByIds,
|
|
66
|
-
isNextLessonLocked,
|
|
67
|
-
mapContentToParent,
|
|
68
|
-
resetAllLearningPaths,
|
|
69
|
-
startLearningPath,
|
|
70
|
-
updateDailySession
|
|
71
|
-
} from './services/content-org/learning-paths.ts';
|
|
72
|
-
|
|
73
58
|
import {
|
|
74
59
|
addItemToPlaylist,
|
|
75
60
|
createPlaylist,
|
|
@@ -243,6 +228,29 @@ import {
|
|
|
243
228
|
updateMultiUserAccount
|
|
244
229
|
} from './services/multi-user-accounts/multi-user-accounts.ts';
|
|
245
230
|
|
|
231
|
+
import {
|
|
232
|
+
completeLearningPathIntroVideo,
|
|
233
|
+
completeMethodIntroVideo,
|
|
234
|
+
fetchLearningPathLessons,
|
|
235
|
+
fetchLearningPathProgressCheckLessons,
|
|
236
|
+
getActivePath,
|
|
237
|
+
getDailySession,
|
|
238
|
+
getEnrichedLearningPath,
|
|
239
|
+
getEnrichedLearningPaths,
|
|
240
|
+
getLearningPathLessonsByIds,
|
|
241
|
+
isNextLessonLocked,
|
|
242
|
+
mapContentToParent,
|
|
243
|
+
resetActiveLearningPath,
|
|
244
|
+
resetAllLearningPaths,
|
|
245
|
+
startLearningPath,
|
|
246
|
+
updateDailySession
|
|
247
|
+
} from './services/my-path/learning-paths.ts';
|
|
248
|
+
|
|
249
|
+
import {
|
|
250
|
+
fetchQuizAnswers,
|
|
251
|
+
storeQuizAnswers
|
|
252
|
+
} from './services/my-path/placement-quiz.ts';
|
|
253
|
+
|
|
246
254
|
import {
|
|
247
255
|
deleteAllNotifications,
|
|
248
256
|
deleteNotification,
|
|
@@ -687,6 +695,7 @@ declare module 'musora-content-services' {
|
|
|
687
695
|
fetchPosts,
|
|
688
696
|
fetchPracticeGoals,
|
|
689
697
|
fetchPublicAnnouncement,
|
|
698
|
+
fetchQuizAnswers,
|
|
690
699
|
fetchRecent,
|
|
691
700
|
fetchRecentActivitiesActiveTabs,
|
|
692
701
|
fetchRecentUserActivities,
|
|
@@ -862,6 +871,7 @@ declare module 'musora-content-services' {
|
|
|
862
871
|
requestEmailChange,
|
|
863
872
|
rescindInvite,
|
|
864
873
|
reset,
|
|
874
|
+
resetActiveLearningPath,
|
|
865
875
|
resetAllAwards,
|
|
866
876
|
resetAllLearningPaths,
|
|
867
877
|
resetPassword,
|
|
@@ -877,6 +887,7 @@ declare module 'musora-content-services' {
|
|
|
877
887
|
sendAccountSetupEmail,
|
|
878
888
|
sendPasswordResetEmail,
|
|
879
889
|
sendRevenueCatPurchaseMetadata,
|
|
890
|
+
setSessionUserData,
|
|
880
891
|
setStudentViewForUser,
|
|
881
892
|
setUserPinnedProgressRow,
|
|
882
893
|
setUserSignature,
|
|
@@ -885,6 +896,7 @@ declare module 'musora-content-services' {
|
|
|
885
896
|
startLiveEventPolling,
|
|
886
897
|
startOnboarding,
|
|
887
898
|
status,
|
|
899
|
+
storeQuizAnswers,
|
|
888
900
|
toDayjs,
|
|
889
901
|
toLocalDay,
|
|
890
902
|
togglePlaylistPrivate,
|
|
@@ -917,6 +929,7 @@ declare module 'musora-content-services' {
|
|
|
917
929
|
updatePracticeGoals,
|
|
918
930
|
updatePracticeNotes,
|
|
919
931
|
updateProfileVisibility,
|
|
932
|
+
updateSessionConfig,
|
|
920
933
|
updateThread,
|
|
921
934
|
updateUserPractice,
|
|
922
935
|
upgradeSubscription,
|
package/src/index.js
CHANGED
|
@@ -24,7 +24,9 @@ import {
|
|
|
24
24
|
getMCSVersion,
|
|
25
25
|
globalConfig,
|
|
26
26
|
initializeEnvVar,
|
|
27
|
-
initializeService
|
|
27
|
+
initializeService,
|
|
28
|
+
setSessionUserData,
|
|
29
|
+
updateSessionConfig
|
|
28
30
|
} from './services/config.js';
|
|
29
31
|
|
|
30
32
|
import {
|
|
@@ -57,23 +59,6 @@ import {
|
|
|
57
59
|
unEnrollUserInGuidedCourse
|
|
58
60
|
} from './services/content-org/guided-courses.ts';
|
|
59
61
|
|
|
60
|
-
import {
|
|
61
|
-
completeLearningPathIntroVideo,
|
|
62
|
-
completeMethodIntroVideo,
|
|
63
|
-
fetchLearningPathLessons,
|
|
64
|
-
fetchLearningPathProgressCheckLessons,
|
|
65
|
-
getActivePath,
|
|
66
|
-
getDailySession,
|
|
67
|
-
getEnrichedLearningPath,
|
|
68
|
-
getEnrichedLearningPaths,
|
|
69
|
-
getLearningPathLessonsByIds,
|
|
70
|
-
isNextLessonLocked,
|
|
71
|
-
mapContentToParent,
|
|
72
|
-
resetAllLearningPaths,
|
|
73
|
-
startLearningPath,
|
|
74
|
-
updateDailySession
|
|
75
|
-
} from './services/content-org/learning-paths.ts';
|
|
76
|
-
|
|
77
62
|
import {
|
|
78
63
|
addItemToPlaylist,
|
|
79
64
|
createPlaylist,
|
|
@@ -247,6 +232,29 @@ import {
|
|
|
247
232
|
updateMultiUserAccount
|
|
248
233
|
} from './services/multi-user-accounts/multi-user-accounts.ts';
|
|
249
234
|
|
|
235
|
+
import {
|
|
236
|
+
completeLearningPathIntroVideo,
|
|
237
|
+
completeMethodIntroVideo,
|
|
238
|
+
fetchLearningPathLessons,
|
|
239
|
+
fetchLearningPathProgressCheckLessons,
|
|
240
|
+
getActivePath,
|
|
241
|
+
getDailySession,
|
|
242
|
+
getEnrichedLearningPath,
|
|
243
|
+
getEnrichedLearningPaths,
|
|
244
|
+
getLearningPathLessonsByIds,
|
|
245
|
+
isNextLessonLocked,
|
|
246
|
+
mapContentToParent,
|
|
247
|
+
resetActiveLearningPath,
|
|
248
|
+
resetAllLearningPaths,
|
|
249
|
+
startLearningPath,
|
|
250
|
+
updateDailySession
|
|
251
|
+
} from './services/my-path/learning-paths.ts';
|
|
252
|
+
|
|
253
|
+
import {
|
|
254
|
+
fetchQuizAnswers,
|
|
255
|
+
storeQuizAnswers
|
|
256
|
+
} from './services/my-path/placement-quiz.ts';
|
|
257
|
+
|
|
250
258
|
import {
|
|
251
259
|
deleteAllNotifications,
|
|
252
260
|
deleteNotification,
|
|
@@ -686,6 +694,7 @@ export {
|
|
|
686
694
|
fetchPosts,
|
|
687
695
|
fetchPracticeGoals,
|
|
688
696
|
fetchPublicAnnouncement,
|
|
697
|
+
fetchQuizAnswers,
|
|
689
698
|
fetchRecent,
|
|
690
699
|
fetchRecentActivitiesActiveTabs,
|
|
691
700
|
fetchRecentUserActivities,
|
|
@@ -861,6 +870,7 @@ export {
|
|
|
861
870
|
requestEmailChange,
|
|
862
871
|
rescindInvite,
|
|
863
872
|
reset,
|
|
873
|
+
resetActiveLearningPath,
|
|
864
874
|
resetAllAwards,
|
|
865
875
|
resetAllLearningPaths,
|
|
866
876
|
resetPassword,
|
|
@@ -876,6 +886,7 @@ export {
|
|
|
876
886
|
sendAccountSetupEmail,
|
|
877
887
|
sendPasswordResetEmail,
|
|
878
888
|
sendRevenueCatPurchaseMetadata,
|
|
889
|
+
setSessionUserData,
|
|
879
890
|
setStudentViewForUser,
|
|
880
891
|
setUserPinnedProgressRow,
|
|
881
892
|
setUserSignature,
|
|
@@ -884,6 +895,7 @@ export {
|
|
|
884
895
|
startLiveEventPolling,
|
|
885
896
|
startOnboarding,
|
|
886
897
|
status,
|
|
898
|
+
storeQuizAnswers,
|
|
887
899
|
toDayjs,
|
|
888
900
|
toLocalDay,
|
|
889
901
|
togglePlaylistPrivate,
|
|
@@ -916,6 +928,7 @@ export {
|
|
|
916
928
|
updatePracticeGoals,
|
|
917
929
|
updatePracticeNotes,
|
|
918
930
|
updateProfileVisibility,
|
|
931
|
+
updateSessionConfig,
|
|
919
932
|
updateThread,
|
|
920
933
|
updateUserPractice,
|
|
921
934
|
upgradeSubscription,
|
package/src/services/config.js
CHANGED
|
@@ -23,9 +23,28 @@ export let globalConfig = {
|
|
|
23
23
|
*/
|
|
24
24
|
const excludeFromGeneratedIndex = []
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @param {*} target
|
|
28
|
+
* @param {*} source
|
|
29
|
+
* @returns {*}
|
|
30
|
+
*/
|
|
31
|
+
function deepMerge(target, source) {
|
|
32
|
+
if (source === undefined) return target
|
|
33
|
+
if (typeof source !== 'object' || source === null || Array.isArray(source)) return source
|
|
34
|
+
|
|
35
|
+
const merged = { ...target }
|
|
36
|
+
for (const key of Object.keys(source)) {
|
|
37
|
+
merged[key] = deepMerge(target?.[key], source[key])
|
|
38
|
+
}
|
|
39
|
+
return merged
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
/**
|
|
27
43
|
* Initializes the service with the given configuration.
|
|
28
44
|
* This function must be called before using any other functions in this library.
|
|
45
|
+
* Each call replaces the prior config wholesale - pass the full config every time.
|
|
46
|
+
* For a partial update to just the session (e.g. userId resolving after boot),
|
|
47
|
+
* use {@link updateSessionConfig} instead.
|
|
29
48
|
* Automatically initializes award definitions with 24-hour cache in the background.
|
|
30
49
|
*
|
|
31
50
|
* @param {Config} config - Configuration object containing API settings.
|
|
@@ -107,6 +126,18 @@ export function initializeService(config) {
|
|
|
107
126
|
}
|
|
108
127
|
}
|
|
109
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Deep-merges partial session data (e.g. userId) into the existing sessionConfig
|
|
131
|
+
* without touching any other part of globalConfig. Use this for reactive updates
|
|
132
|
+
* (e.g. a watcher that re-syncs userId once a user resolves) instead of calling
|
|
133
|
+
* initializeService again, since initializeService replaces sessionConfig wholesale.
|
|
134
|
+
*
|
|
135
|
+
* @param {*} sessionConfig
|
|
136
|
+
*/
|
|
137
|
+
export function updateSessionConfig(sessionConfig) {
|
|
138
|
+
globalConfig.sessionConfig = deepMerge(globalConfig.sessionConfig, sessionConfig)
|
|
139
|
+
}
|
|
140
|
+
|
|
110
141
|
export function initializeEnvVar(config) {
|
|
111
142
|
globalConfig.appEnv = config.appEnv
|
|
112
143
|
}
|
|
@@ -3,7 +3,7 @@ import { db } from './sync'
|
|
|
3
3
|
import { COLLECTION_ID_SELF, COLLECTION_TYPE, STATE } from './sync/models/ContentProgress'
|
|
4
4
|
import { trackUserPractice } from './userActivity'
|
|
5
5
|
import { getNextLessonLessonParentTypes } from '../contentTypeConfig.js'
|
|
6
|
-
import { getDailySession, onLearningPathCompletedActions } from './
|
|
6
|
+
import { getDailySession, onLearningPathCompletedActions } from './my-path/learning-paths.ts'
|
|
7
7
|
import { duplicateProgressToALaCarteOffline } from './offline/progress.ts'
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
type ContentId = number
|
|
4
|
+
type Branch = "essentials" | "classical" | "blues"
|
|
5
|
+
|
|
6
|
+
interface LearningPath {
|
|
7
|
+
id: ContentId
|
|
8
|
+
thumbnail?: string
|
|
9
|
+
title: string
|
|
10
|
+
difficulty?: number
|
|
11
|
+
branch: Branch
|
|
12
|
+
intro_video_id?: number // only if we have intro videos
|
|
13
|
+
published_on?: string
|
|
14
|
+
// ... any other content-related fields we need
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Connection {
|
|
18
|
+
id: string // `${source}:${target}`, stable key
|
|
19
|
+
source: ContentId // prerequisite LP
|
|
20
|
+
target: ContentId // up_next LP
|
|
21
|
+
branch: Branch // which branch this connection belongs to
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface BranchIndex {
|
|
25
|
+
node_ids: ContentId[]
|
|
26
|
+
edge_ids: string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CanonicalMapData {
|
|
30
|
+
nodes: Record<ContentId, LearningPath>
|
|
31
|
+
edges: Connection[]
|
|
32
|
+
branches: Record<Branch, BranchIndex>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// example CanonicalMapData:
|
|
36
|
+
const exampleCurriculumMap: CanonicalMapData = {
|
|
37
|
+
"nodes": {
|
|
38
|
+
"1": { "id": 1, "title": "Essentials 1", "branch": "essentials" },
|
|
39
|
+
"2": { "id": 2, "title": "Essentials 2", "branch": "essentials" },
|
|
40
|
+
"3": { "id": 3, "title": "Essentials 3", "branch": "essentials" },
|
|
41
|
+
"4": { "id": 4, "title": "Classical 1", "branch": "classical" },
|
|
42
|
+
"5": { "id": 5, "title": "Classical 2", "branch": "classical" },
|
|
43
|
+
"6": { "id": 6, "title": "Classical 3", "branch": "classical" },
|
|
44
|
+
"7": { "id": 7, "title": "Blues 1", "branch": "blues" },
|
|
45
|
+
"8": { "id": 8, "title": "Blues 2", "branch": "blues" },
|
|
46
|
+
"9": { "id": 9, "title": "Blues 3", "branch": "blues" },
|
|
47
|
+
},
|
|
48
|
+
"edges": [
|
|
49
|
+
{ "id": "1-2", "source": 1, "target": 2, "branch": "essentials" },
|
|
50
|
+
{ "id": "2-3", "source": 2, "target": 3, "branch": "essentials" },
|
|
51
|
+
{ "id": "3-4", "source": 3, "target": 4, "branch": "classical" },
|
|
52
|
+
{ "id": "4-5", "source": 4, "target": 5, "branch": "classical" },
|
|
53
|
+
{ "id": "5-6", "source": 5, "target": 6, "branch": "classical" },
|
|
54
|
+
{ "id": "3-7", "source": 3, "target": 7, "branch": "blues" },
|
|
55
|
+
{ "id": "7-8", "source": 7, "target": 8, "branch": "blues" },
|
|
56
|
+
{ "id": "8-9", "source": 8, "target": 9, "branch": "blues" },
|
|
57
|
+
],
|
|
58
|
+
"branches": {
|
|
59
|
+
"essentials": {
|
|
60
|
+
"node_ids": [1, 2, 3],
|
|
61
|
+
"edge_ids": ["1-2", "2-3"],
|
|
62
|
+
},
|
|
63
|
+
"classical": {
|
|
64
|
+
"node_ids": [4, 5, 6],
|
|
65
|
+
"edge_ids": ["3-4", "4-5", "5-6"],
|
|
66
|
+
},
|
|
67
|
+
"blues": {
|
|
68
|
+
"node_ids": [7, 8, 9],
|
|
69
|
+
"edge_ids": ["3-7", "7-8", "8-9"],
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
const exampleSongSelectionDTO =
|
|
76
|
+
[
|
|
77
|
+
{
|
|
78
|
+
learning_path_id: 8,
|
|
79
|
+
song_id: 101,
|
|
80
|
+
song_title: "Fur Elise",
|
|
81
|
+
song_artist: "Beethoven",
|
|
82
|
+
song_difficulty: 3,
|
|
83
|
+
song_thumbnail: "https://example.com/fur_elise_thumbnail.jpg",
|
|
84
|
+
song_preview: "https://example.com/fur_elise_preview.mp3"
|
|
85
|
+
},
|
|
86
|
+
// ... more song selections
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
const myPathMap = [
|
|
90
|
+
"path-node-1",
|
|
91
|
+
"path-node-2",
|
|
92
|
+
// ... more path nodes
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
const pathNode = {
|
|
96
|
+
"_id": "path-node-1",
|
|
97
|
+
"name": "Learning Path 1",
|
|
98
|
+
"pathStreamArray": [
|
|
99
|
+
{
|
|
100
|
+
"lpReferences": [
|
|
101
|
+
"LP1" // learning path 1
|
|
102
|
+
],
|
|
103
|
+
"isInABTest": true,
|
|
104
|
+
"specificInstrument": "all"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"lpReferences": [
|
|
108
|
+
"LP1.1", // LP1 split into 2 smaller LPs
|
|
109
|
+
"LP1.2"
|
|
110
|
+
],
|
|
111
|
+
"isInABTest": true,
|
|
112
|
+
"specificInstrument": "all"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"lpReferences": [
|
|
116
|
+
"LP1_pad-only" // a pad-only version of LP1
|
|
117
|
+
],
|
|
118
|
+
"isInABTest": false,
|
|
119
|
+
"specificInstrument": "pad" // matches Placement Quiz instrument options
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
contentStatusReset,
|
|
19
19
|
getAllCompletedByIds,
|
|
20
20
|
getIdsWhereLastAccessedFromMethod,
|
|
21
|
-
getProgressState,
|
|
21
|
+
getProgressState,
|
|
22
|
+
resetStatus,
|
|
22
23
|
} from '../contentProgress.js'
|
|
23
24
|
import { COLLECTION_ID_SELF, COLLECTION_TYPE, CollectionParameter, STATE } from '../sync/models/ContentProgress'
|
|
24
25
|
import { db, SyncWriteDTO } from '../sync'
|
|
@@ -94,7 +95,7 @@ interface CollectionObject {
|
|
|
94
95
|
* @param brand
|
|
95
96
|
* @param userDate - local datetime. must have date and time - format 2025-10-31T13:45:00
|
|
96
97
|
*/
|
|
97
|
-
export async function getDailySession(brand: string, userDate: Date) {
|
|
98
|
+
export async function getDailySession(brand: string, userDate: Date): Promise<DailySessionResponse | '' | null> {
|
|
98
99
|
const dateWithTimezone = formatLocalDateTime(userDate)
|
|
99
100
|
const key = dailySessionKey(brand, dateWithTimezone)
|
|
100
101
|
|
|
@@ -102,13 +103,13 @@ export async function getDailySession(brand: string, userDate: Date) {
|
|
|
102
103
|
return await dataPromiseGET(dailySessionPromises, key, async () => {
|
|
103
104
|
const url = `${LEARNING_PATHS_PATH}/daily-session/get?brand=${brand}&userDate=${encodeURIComponent(dateWithTimezone)}`
|
|
104
105
|
|
|
105
|
-
const response = await GET(url)
|
|
106
|
+
const response = await GET(url)
|
|
106
107
|
|
|
107
108
|
if (!response) {
|
|
108
109
|
return await updateDailySession(brand, userDate, false)
|
|
109
110
|
}
|
|
110
|
-
return response
|
|
111
|
-
})
|
|
111
|
+
return response
|
|
112
|
+
}) as DailySessionResponse | ''
|
|
112
113
|
} catch (error) {
|
|
113
114
|
console.error('Error fetching daily session:', (error as any).message)
|
|
114
115
|
return null
|
|
@@ -125,7 +126,7 @@ export async function updateDailySession(
|
|
|
125
126
|
brand: string,
|
|
126
127
|
userDate: Date,
|
|
127
128
|
keepFirstLearningPath: boolean = false,
|
|
128
|
-
) {
|
|
129
|
+
): Promise<DailySessionResponse | null> {
|
|
129
130
|
const dateWithTimezone = formatLocalDateTime(userDate)
|
|
130
131
|
const key = dailySessionKey(brand, dateWithTimezone)
|
|
131
132
|
const url: string = `${LEARNING_PATHS_PATH}/daily-session/create`
|
|
@@ -155,7 +156,7 @@ function formatLocalDateTime(date: Date): string {
|
|
|
155
156
|
* Gets user's active learning path.
|
|
156
157
|
* @param brand
|
|
157
158
|
*/
|
|
158
|
-
export async function getActivePath(brand: string) {
|
|
159
|
+
export async function getActivePath(brand: string): Promise<ActiveLearningPathResponse | null> {
|
|
159
160
|
const url: string = `${LEARNING_PATHS_PATH}/active-path/get?brand=${brand}`
|
|
160
161
|
|
|
161
162
|
return (await dataPromiseGET(activePathPromises, activePathKey(brand), () =>
|
|
@@ -168,7 +169,7 @@ export async function getActivePath(brand: string) {
|
|
|
168
169
|
* @param brand
|
|
169
170
|
* @param learningPathId
|
|
170
171
|
*/
|
|
171
|
-
export async function startLearningPath(brand: string, learningPathId: number) {
|
|
172
|
+
export async function startLearningPath(brand: string, learningPathId: number): Promise<ActiveLearningPathResponse | null> {
|
|
172
173
|
const url: string = `${LEARNING_PATHS_PATH}/active-path/set`
|
|
173
174
|
const body = { brand: brand, learning_path_id: learningPathId }
|
|
174
175
|
|
|
@@ -185,6 +186,19 @@ export async function startLearningPath(brand: string, learningPathId: number) {
|
|
|
185
186
|
return response
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
/*
|
|
190
|
+
* Resets the user's active learning path and updates the daily session.
|
|
191
|
+
* @param {string} brand
|
|
192
|
+
* @param {number} learningPathId - The ID of the active learning path to reset.
|
|
193
|
+
* @param {Date} userDate - The user's local date.
|
|
194
|
+
* @returns {Promise<DailySessionResponse | '' | null>} - The updated daily session response or null if an error occurs.
|
|
195
|
+
*/
|
|
196
|
+
export async function resetActiveLearningPath(brand: string, learningPathId: number, userDate: Date): Promise<DailySessionResponse | '' | null> {
|
|
197
|
+
const collection: CollectionObject = { id: learningPathId, type: COLLECTION_TYPE.LEARNING_PATH }
|
|
198
|
+
await resetStatus(learningPathId, collection)
|
|
199
|
+
return await updateDailySession(brand, userDate)
|
|
200
|
+
}
|
|
201
|
+
|
|
188
202
|
function dataPromiseGET<T>(
|
|
189
203
|
cache: Map<string, Promise<T>>,
|
|
190
204
|
key: string,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module MyPathPlacementQuiz
|
|
3
|
+
*/
|
|
4
|
+
import { HttpClient } from '../../infrastructure/http/HttpClient'
|
|
5
|
+
import { globalConfig } from '../config.js'
|
|
6
|
+
import { OnboardingRecommendedContent } from '@/services/user/onboarding'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const baseUrl = `/api/my-path/v1/placement-quiz`
|
|
10
|
+
|
|
11
|
+
interface PlacementQuizAnswers {
|
|
12
|
+
skill_level: 'new' | 'beginner' | 'intermediate' | 'advanced' | 'expert'
|
|
13
|
+
genres: string[]
|
|
14
|
+
gear: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface PlacementQuizResponse {
|
|
18
|
+
user_id: string
|
|
19
|
+
brand: string
|
|
20
|
+
answers: PlacementQuizAnswers
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface StorePlacementQuizResponse {
|
|
24
|
+
placement_quiz: PlacementQuizResponse
|
|
25
|
+
recommended_content: OnboardingRecommendedContent
|
|
26
|
+
error?: any
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Fetches the placement quiz answers from the API.
|
|
31
|
+
*
|
|
32
|
+
* @param brand
|
|
33
|
+
* @returns {Promise<PlacementQuizResponse>} - A promise that resolves to the placement quiz answers.
|
|
34
|
+
* @throws {HttpError} - If the HTTP request fails.
|
|
35
|
+
*/
|
|
36
|
+
export async function fetchQuizAnswers(brand: string): Promise<PlacementQuizResponse> {
|
|
37
|
+
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
38
|
+
return httpClient.get<PlacementQuizResponse>(`${baseUrl}?brand=${brand}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Stores the placement quiz answers to the API.
|
|
43
|
+
*
|
|
44
|
+
* @param brand
|
|
45
|
+
* @param answers
|
|
46
|
+
* @returns {Promise<PlacementQuizResponse>} - A promise that resolves to the stored placement quiz answers.
|
|
47
|
+
* @throws {HttpError} - If the HTTP request fails.
|
|
48
|
+
*/
|
|
49
|
+
export async function storeQuizAnswers(brand: string, answers: PlacementQuizAnswers): Promise<StorePlacementQuizResponse> {
|
|
50
|
+
const body = {
|
|
51
|
+
brand,
|
|
52
|
+
answers,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
56
|
+
return httpClient.post<StorePlacementQuizResponse>(baseUrl, body)
|
|
57
|
+
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
CollectionParameter,
|
|
6
6
|
} from '../sync/models/ContentProgress'
|
|
7
7
|
import { getHierarchies, getHierarchy } from '../sanity.js'
|
|
8
|
-
import { onLearningPathCompletedActions } from '../
|
|
8
|
+
import { onLearningPathCompletedActions } from '../my-path/learning-paths'
|
|
9
9
|
import { duplicateProgressToALaCarteOffline } from '../offline/progress'
|
|
10
10
|
import {
|
|
11
11
|
bubbleAndTrickleProgressesSafely,
|
|
@@ -22,7 +22,7 @@ const CACHE_EXPIRY_MS = 5 * 60 * 1000
|
|
|
22
22
|
/**
|
|
23
23
|
* Retrieves user's pinned data by brand, from localStorage or BE call.
|
|
24
24
|
* @param brand
|
|
25
|
-
* @returns {Promise<any|*|{id, type}>}
|
|
25
|
+
* @returns {Promise<any|*|{id, type}>} - only id and type
|
|
26
26
|
*/
|
|
27
27
|
async function getUserPinnedItem(brand) {
|
|
28
28
|
const key = getUserPinProgressKey()
|
|
@@ -170,7 +170,12 @@ export async function getProgressRows({ brand = 'drumeo', limit = 8 } = {}, opti
|
|
|
170
170
|
|
|
171
171
|
const [contentCardMap, playlistCards, methodCard] = await getCards(brand, limit, userPinnedItem)
|
|
172
172
|
|
|
173
|
-
|
|
173
|
+
let pinnedCard = null
|
|
174
|
+
try {
|
|
175
|
+
pinnedCard = await popPinnedItem(userPinnedItem, contentCardMap, playlistCards, methodCard, brand) // todo: catch errors in here
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error('Error popping pinned item:', error)
|
|
178
|
+
}
|
|
174
179
|
|
|
175
180
|
let allResultsLength = playlistCards.length + contentCardMap.size
|
|
176
181
|
if (methodCard) {
|
|
@@ -207,7 +212,7 @@ async function getCards(brand, limit, userPinnedItem) {
|
|
|
207
212
|
* If userPinnedItem is not found, generate the pinned card from scratch.
|
|
208
213
|
*
|
|
209
214
|
**/
|
|
210
|
-
async function popPinnedItem(userPinnedItem, contentCardMap, playlistCards, methodCard) {
|
|
215
|
+
async function popPinnedItem(userPinnedItem, contentCardMap, playlistCards, methodCard, brand) {
|
|
211
216
|
if (!userPinnedItem) return null
|
|
212
217
|
const pinnedId = parseInt(userPinnedItem.id)
|
|
213
218
|
const progressType = userPinnedItem.progressType ?? userPinnedItem.type
|
|
@@ -236,13 +241,24 @@ async function popPinnedItem(userPinnedItem, contentCardMap, playlistCards, meth
|
|
|
236
241
|
if (pinnedPlaylist) {
|
|
237
242
|
item = pinnedPlaylist
|
|
238
243
|
} else {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
let playlist
|
|
245
|
+
try {
|
|
246
|
+
playlist = await fetchPlaylist(pinnedId)
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error?.status !== 404) {
|
|
249
|
+
throw error
|
|
250
|
+
}
|
|
251
|
+
unpinProgressRow(brand)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (playlist) {
|
|
255
|
+
item = processPlaylistItem({
|
|
256
|
+
id: pinnedId,
|
|
257
|
+
playlist: playlist,
|
|
258
|
+
type: 'playlist',
|
|
259
|
+
progressTimestamp: new Date().getTime(),
|
|
260
|
+
})
|
|
261
|
+
}
|
|
246
262
|
}
|
|
247
263
|
} else if (progressType === 'method') {
|
|
248
264
|
// simply get method card and return
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @module ProgressRow
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { getActivePath, fetchLearningPathLessons } from '../../
|
|
5
|
+
import { getActivePath, fetchLearningPathLessons } from '../../my-path/learning-paths.ts'
|
|
6
6
|
import { fetchMethodV2IntroVideo } from '../../sanity'
|
|
7
7
|
import { getProgressState } from '../../contentProgress'
|
|
8
8
|
import { COLLECTION_TYPE, STATE } from '../../sync/models/ContentProgress'
|
|
@@ -22,13 +22,17 @@ export interface PracticeData {
|
|
|
22
22
|
class StreakCalculator {
|
|
23
23
|
private cache: StreakData | null = null
|
|
24
24
|
async getStreakData(): Promise<StreakData> {
|
|
25
|
-
if (this.cache) {
|
|
25
|
+
if (this.cache && this.isCacheFromToday()) {
|
|
26
26
|
return this.cache
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
return await this.recalculate()
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
private isCacheFromToday(): boolean {
|
|
33
|
+
return dayjs(this.cache.calculatedAt).format('YYYY-MM-DD') === dayjs().format('YYYY-MM-DD')
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
async recalculate(): Promise<StreakData> {
|
|
33
37
|
const allPractices = await this.fetchAllPractices()
|
|
34
38
|
|
|
@@ -13,7 +13,7 @@ import dayjs from 'dayjs'
|
|
|
13
13
|
import { addContextToContent } from './contentAggregator.js'
|
|
14
14
|
import { db, Q } from './sync'
|
|
15
15
|
import { streakCalculator } from './user/streakCalculator'
|
|
16
|
-
import { mapContentsThatWereLastProgressedFromMethod } from "./
|
|
16
|
+
import { mapContentsThatWereLastProgressedFromMethod } from "./my-path/learning-paths.ts";
|
|
17
17
|
|
|
18
18
|
const DAYS = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
|
|
19
19
|
|
package/src/version-info.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MCS_VERSION = '2.
|
|
1
|
+
export const MCS_VERSION = '2.182.0'
|