musora-content-services 2.75.0 → 2.77.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 +11 -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 +0 -0
- package/.github/workflows/node.js.yml +0 -0
- package/.prettierignore +0 -0
- package/.prettierrc +0 -0
- package/.yarnrc.yml +1 -0
- package/CHANGELOG.md +51 -0
- package/README.md +0 -0
- package/babel.config.cjs +0 -0
- package/docs/ContentOrganization.html +2 -2
- package/docs/Forums.html +2 -2
- package/docs/Gamification.html +2 -2
- package/docs/TestUser.html +2 -2
- package/docs/UserManagementSystem.html +2 -2
- package/docs/api_types.js.html +2 -2
- package/docs/config.js.html +2 -2
- package/docs/content-org_content-org.js.html +2 -2
- package/docs/content-org_guided-courses.ts.html +2 -2
- package/docs/content-org_learning-paths.ts.html +55 -35
- package/docs/content-org_playlists-types.js.html +2 -2
- package/docs/content-org_playlists.js.html +2 -2
- package/docs/content.js.html +2 -2
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/forums_categories.ts.html +2 -2
- package/docs/forums_discussions.js.html +0 -0
- package/docs/forums_forum.js.html +0 -0
- package/docs/forums_forums.ts.html +2 -2
- package/docs/forums_posts.ts.html +2 -2
- package/docs/forums_threads.ts.html +2 -2
- package/docs/gamification_awards.js.html +0 -0
- package/docs/gamification_awards.ts.html +2 -2
- package/docs/gamification_gamification.js.html +2 -2
- package/docs/gamification_types.js.html +0 -0
- package/docs/global.html +56 -8
- package/docs/index.html +2 -2
- package/docs/liveTesting.ts.html +2 -2
- package/docs/module-Accounts.html +191 -2
- package/docs/module-Awards.html +2 -2
- package/docs/module-Categories.html +0 -0
- package/docs/module-Config.html +2 -2
- package/docs/module-Content-Services-V2.html +2 -2
- package/docs/module-ForumCategories.html +0 -0
- package/docs/module-ForumDiscussions.html +0 -0
- package/docs/module-Forums.html +2 -2
- package/docs/module-GuidedCourses.html +2 -2
- package/docs/module-Interests.html +2 -2
- package/docs/module-LearningPaths.html +343 -3
- package/docs/module-Onboarding.html +2 -2
- package/docs/module-Payments.html +2 -2
- package/docs/module-Permissions.html +2 -2
- package/docs/module-Playlists.html +2 -2
- package/docs/module-ProgressRow.html +2 -2
- package/docs/module-Railcontent-Services.html +2 -2
- package/docs/module-Sanity-Services.html +2 -2
- package/docs/module-Sessions.html +2 -2
- package/docs/module-Threads.html +0 -0
- package/docs/module-UserActivity.html +4 -4
- package/docs/module-UserChat.html +2 -2
- package/docs/module-UserManagement.html +2 -2
- package/docs/module-UserMemberships.html +2 -2
- package/docs/module-UserNotifications.html +2 -2
- package/docs/module-UserProfile.html +2 -2
- package/docs/progress-row_method-card.js.html +40 -43
- package/docs/railcontent.js.html +2 -2
- package/docs/sanity.js.html +3 -3
- package/docs/scripts/collapse.js +0 -0
- package/docs/scripts/commonNav.js +0 -0
- package/docs/scripts/linenumber.js +0 -0
- package/docs/scripts/nav.js +0 -0
- package/docs/scripts/polyfill.js +0 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
- package/docs/scripts/prettify/lang-css.js +0 -0
- package/docs/scripts/prettify/prettify.js +0 -0
- package/docs/scripts/search.js +0 -0
- package/docs/styles/jsdoc.css +0 -0
- package/docs/styles/prettify.css +0 -0
- package/docs/userActivity.js.html +5 -6
- package/docs/user_account.ts.html +29 -2
- package/docs/user_chat.js.html +2 -2
- package/docs/user_interests.js.html +2 -2
- package/docs/user_management.js.html +2 -2
- package/docs/user_memberships.js.html +0 -0
- package/docs/user_memberships.ts.html +2 -2
- package/docs/user_notifications.js.html +2 -2
- package/docs/user_onboarding.ts.html +2 -2
- package/docs/user_payments.ts.html +2 -2
- package/docs/user_permissions.js.html +2 -2
- package/docs/user_profile.js.html +2 -2
- package/docs/user_sessions.js.html +2 -2
- package/docs/user_types.js.html +4 -2
- package/docs/user_user-management-system.js.html +2 -2
- package/jest.config.js +0 -0
- package/jsdoc.json +0 -0
- package/package.json +1 -1
- package/src/contentMetaData.js +0 -0
- package/src/contentTypeConfig.js +18 -10
- package/src/filterBuilder.js +0 -0
- package/src/index.d.ts +9 -1
- package/src/index.js +9 -1
- package/src/infrastructure/http/HttpClient.ts +5 -5
- 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/httpHelper.js +0 -0
- package/src/lib/lastUpdated.js +0 -0
- package/src/services/api/types.js +0 -0
- package/src/services/api/types.ts +0 -0
- package/src/services/config.js +0 -0
- package/src/services/content-org/content-org.js +0 -0
- package/src/services/content-org/guided-courses.ts +0 -0
- package/src/services/content-org/learning-paths.ts +53 -33
- package/src/services/content-org/playlists-types.js +0 -0
- package/src/services/content-org/playlists.js +0 -0
- package/src/services/content.js +0 -0
- package/src/services/contentAggregator.js +54 -45
- package/src/services/contentLikes.js +0 -0
- package/src/services/contentProgress.js +9 -8
- package/src/services/dataContext.js +0 -0
- package/src/services/dateUtils.js +0 -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 +20 -1
- package/src/services/forums/threads.ts +14 -1
- package/src/services/forums/types.ts +1 -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/progress-row/method-card.js +38 -41
- package/src/services/railcontent.js +0 -0
- package/src/services/recommendations.js +0 -0
- package/src/services/sanity.js +1 -1
- package/src/services/types.js +0 -0
- package/src/services/user/account.ts +27 -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/onboarding.ts +0 -0
- package/src/services/user/payments.ts +0 -0
- package/src/services/user/permissions.js +0 -0
- package/src/services/user/profile.js +0 -0
- package/src/services/user/sessions.js +0 -0
- package/src/services/user/types.js +2 -0
- package/src/services/user/user-management-system.js +0 -0
- package/src/services/userActivity.js +3 -4
- package/test/HttpClient.test.js +0 -0
- package/test/content.test.js +0 -0
- package/test/contentLikes.test.js +0 -0
- package/test/contentProgress.test.js +0 -0
- package/test/dataContext.test.js +0 -0
- package/test/forum.test.js +0 -0
- package/test/imageSRCBuilder.test.js +0 -0
- package/test/imageSRCVerify.test.js +0 -0
- package/test/initializeTests.js +0 -0
- package/test/learningPaths.test.js +5 -3
- package/test/lib/lastUpdated.test.js +0 -0
- package/test/live/contentProgressLive.test.js +0 -0
- package/test/live/railcontentLive.test.js +0 -0
- package/test/localStorageMock.js +0 -0
- package/test/log.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/notifications.test.js +0 -0
- package/test/progressRows.test.js +0 -0
- package/test/sanityQueryService.test.js +0 -0
- package/test/streakMessage.test.js +0 -0
- package/test/user/permissions.test.js +0 -0
- package/test/userActivity.test.js +0 -0
- package/tools/generate-index.cjs +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
getLastInteractedOf,
|
|
3
|
-
|
|
2
|
+
getLastInteractedOf,
|
|
3
|
+
getNavigateTo,
|
|
4
|
+
getNextLesson,
|
|
5
|
+
getProgressDateByIds,
|
|
4
6
|
getProgressPercentageByIds,
|
|
5
7
|
getProgressStateByIds,
|
|
6
|
-
getResumeTimeSecondsByIds
|
|
7
|
-
} from
|
|
8
|
-
import {isContentLikedByIds} from
|
|
9
|
-
import {fetchLastInteractedChild, fetchLikeCount} from
|
|
8
|
+
getResumeTimeSecondsByIds,
|
|
9
|
+
} from './contentProgress'
|
|
10
|
+
import { isContentLikedByIds } from './contentLikes'
|
|
11
|
+
import { fetchLastInteractedChild, fetchLikeCount } from './railcontent'
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Combine sanity data with BE contextual data.
|
|
@@ -57,8 +59,7 @@ import {fetchLastInteractedChild, fetchLikeCount} from "./railcontent"
|
|
|
57
59
|
|
|
58
60
|
// need to add method support.
|
|
59
61
|
// this means returning collection_type and collection_id
|
|
60
|
-
export async function addContextToContent(dataPromise, ...dataArgs)
|
|
61
|
-
{
|
|
62
|
+
export async function addContextToContent(dataPromise, ...dataArgs) {
|
|
62
63
|
const lastArg = dataArgs[dataArgs.length - 1]
|
|
63
64
|
const options = typeof lastArg === 'object' && !Array.isArray(lastArg) ? lastArg : {}
|
|
64
65
|
|
|
@@ -80,18 +81,27 @@ export async function addContextToContent(dataPromise, ...dataArgs)
|
|
|
80
81
|
|
|
81
82
|
let data = await dataPromise(...dataParam)
|
|
82
83
|
const isDataAnArray = Array.isArray(data)
|
|
83
|
-
if(isDataAnArray && data.length === 0) return data
|
|
84
|
-
if(!data) return false
|
|
84
|
+
if (isDataAnArray && data.length === 0) return data
|
|
85
|
+
if (!data) return false
|
|
85
86
|
|
|
86
87
|
const items = extractItemsFromData(data, dataField, isDataAnArray, dataField_includeParent) ?? []
|
|
87
|
-
const ids = items.map(item => item?.id).filter(Boolean)
|
|
88
|
-
if(ids.length === 0) return data
|
|
88
|
+
const ids = items.map((item) => item?.id).filter(Boolean)
|
|
89
|
+
if (ids.length === 0) return data
|
|
89
90
|
|
|
90
|
-
const [
|
|
91
|
-
|
|
91
|
+
const [
|
|
92
|
+
progressData,
|
|
93
|
+
isLikedData,
|
|
94
|
+
resumeTimeData,
|
|
95
|
+
lastInteractedChildData,
|
|
96
|
+
nextLessonData,
|
|
97
|
+
navigateToData,
|
|
98
|
+
] = await Promise.all([
|
|
99
|
+
addProgressPercentage || addProgressStatus || addProgressTimestamp
|
|
100
|
+
? getProgressDateByIds(ids)
|
|
101
|
+
: Promise.resolve(null),
|
|
92
102
|
addIsLiked ? isContentLikedByIds(ids) : Promise.resolve(null),
|
|
93
103
|
addResumeTimeSeconds ? getResumeTimeSecondsByIds(ids) : Promise.resolve(null),
|
|
94
|
-
addLastInteractedChild ? fetchLastInteractedChild(ids)
|
|
104
|
+
addLastInteractedChild ? fetchLastInteractedChild(ids) : Promise.resolve(null),
|
|
95
105
|
addNextLesson ? getNextLesson(items) : Promise.resolve(null),
|
|
96
106
|
addNavigateTo ? getNavigateTo(items) : Promise.resolve(null),
|
|
97
107
|
])
|
|
@@ -105,33 +115,40 @@ export async function addContextToContent(dataPromise, ...dataArgs)
|
|
|
105
115
|
...(addLikeCount && ids.length === 1 ? { likeCount: await fetchLikeCount(item.id) } : {}),
|
|
106
116
|
...(addResumeTimeSeconds ? { resumeTime: resumeTimeData?.[item.id] } : {}),
|
|
107
117
|
...(addLastInteractedChild ? { lastInteractedChild: lastInteractedChildData?.[item.id] } : {}),
|
|
108
|
-
...(addNextLesson
|
|
118
|
+
...(addNextLesson
|
|
119
|
+
? {
|
|
120
|
+
nextLesson: nextLessonData?.[item.id], //deprecated
|
|
121
|
+
next_lesson_id: nextLessonData?.[item.id],
|
|
122
|
+
next_lesson: item?.children?.find((child) => child.id === nextLessonData?.[item.id]),
|
|
123
|
+
}
|
|
124
|
+
: {}),
|
|
109
125
|
...(addNavigateTo ? { navigateTo: navigateToData?.[item.id] } : {}),
|
|
110
126
|
})
|
|
111
127
|
|
|
112
128
|
return await processItems(data, addContext, dataField, isDataAnArray, dataField_includeParent)
|
|
113
129
|
}
|
|
114
130
|
|
|
115
|
-
export async function getNavigateToForPlaylists(data, {dataField = null} = {}
|
|
116
|
-
{
|
|
131
|
+
export async function getNavigateToForPlaylists(data, { dataField = null } = {}) {
|
|
117
132
|
let playlists = extractItemsFromData(data, dataField, false, false)
|
|
118
133
|
let allIds = []
|
|
119
|
-
playlists.forEach(
|
|
120
|
-
|
|
134
|
+
playlists.forEach(
|
|
135
|
+
(playlist) => (allIds = [...allIds, ...playlist.items.map((a) => a.content_id)])
|
|
136
|
+
)
|
|
137
|
+
const progressOnItems = await getProgressStateByIds(allIds)
|
|
121
138
|
const addContext = async (playlist) => {
|
|
122
|
-
const allItemsCompleted = playlist.items.every(i => {
|
|
123
|
-
const itemId = i.content_id
|
|
124
|
-
const progress = progressOnItems[itemId]
|
|
125
|
-
return progress && progress === 'completed'
|
|
126
|
-
})
|
|
127
|
-
let nextItem = playlist.items[0] ?? null
|
|
139
|
+
const allItemsCompleted = playlist.items.every((i) => {
|
|
140
|
+
const itemId = i.content_id
|
|
141
|
+
const progress = progressOnItems[itemId]
|
|
142
|
+
return progress && progress === 'completed'
|
|
143
|
+
})
|
|
144
|
+
let nextItem = playlist.items[0] ?? null
|
|
128
145
|
if (!allItemsCompleted) {
|
|
129
|
-
const lastItemProgress = progressOnItems[playlist.last_engaged_on]
|
|
130
|
-
const index = playlist.items.findIndex(i => i.content_id === playlist.last_engaged_on)
|
|
146
|
+
const lastItemProgress = progressOnItems[playlist.last_engaged_on]
|
|
147
|
+
const index = playlist.items.findIndex((i) => i.content_id === playlist.last_engaged_on)
|
|
131
148
|
if (lastItemProgress === 'completed') {
|
|
132
|
-
nextItem = playlist.items[index + 1] ?? nextItem
|
|
149
|
+
nextItem = playlist.items[index + 1] ?? nextItem
|
|
133
150
|
} else {
|
|
134
|
-
nextItem = playlist.items[index] ?? nextItem
|
|
151
|
+
nextItem = playlist.items[index] ?? nextItem
|
|
135
152
|
}
|
|
136
153
|
}
|
|
137
154
|
playlist.navigateTo = {
|
|
@@ -140,11 +157,10 @@ export async function getNavigateToForPlaylists(data, {dataField = null} = {} )
|
|
|
140
157
|
}
|
|
141
158
|
return playlist
|
|
142
159
|
}
|
|
143
|
-
return await processItems(data, addContext, dataField, false, false
|
|
160
|
+
return await processItems(data, addContext, dataField, false, false)
|
|
144
161
|
}
|
|
145
162
|
|
|
146
|
-
function extractItemsFromData(data, dataField, isParentArray, includeParent)
|
|
147
|
-
{
|
|
163
|
+
function extractItemsFromData(data, dataField, isParentArray, includeParent) {
|
|
148
164
|
let items = []
|
|
149
165
|
if (dataField) {
|
|
150
166
|
if (isParentArray) {
|
|
@@ -164,18 +180,17 @@ function extractItemsFromData(data, dataField, isParentArray, includeParent)
|
|
|
164
180
|
}
|
|
165
181
|
}
|
|
166
182
|
} else if (Array.isArray(data)) {
|
|
167
|
-
items = data
|
|
183
|
+
items = data
|
|
168
184
|
} else if (data?.id) {
|
|
169
185
|
items = [data]
|
|
170
186
|
}
|
|
171
187
|
return items
|
|
172
188
|
}
|
|
173
189
|
|
|
174
|
-
async function processItems(data, addContext, dataField, isParentArray, includeParent)
|
|
175
|
-
{
|
|
190
|
+
async function processItems(data, addContext, dataField, isParentArray, includeParent) {
|
|
176
191
|
if (dataField) {
|
|
177
192
|
if (isParentArray) {
|
|
178
|
-
for(let parent of data) {
|
|
193
|
+
for (let parent of data) {
|
|
179
194
|
parent[dataField] = Array.isArray(parent[dataField])
|
|
180
195
|
? await Promise.all(parent[dataField].map(addContext))
|
|
181
196
|
: await addContext(parent[dataField])
|
|
@@ -186,16 +201,10 @@ async function processItems(data, addContext, dataField, isParentArray, includeP
|
|
|
186
201
|
: await addContext(data[dataField])
|
|
187
202
|
}
|
|
188
203
|
if (includeParent) {
|
|
189
|
-
data = isParentArray
|
|
190
|
-
? await Promise.all(data.map(addContext))
|
|
191
|
-
: await addContext(data)
|
|
204
|
+
data = isParentArray ? await Promise.all(data.map(addContext)) : await addContext(data)
|
|
192
205
|
}
|
|
193
206
|
return data
|
|
194
207
|
} else {
|
|
195
|
-
return Array.isArray(data)
|
|
196
|
-
? await Promise.all(data.map(addContext))
|
|
197
|
-
: await addContext(data)
|
|
208
|
+
return Array.isArray(data) ? await Promise.all(data.map(addContext)) : await addContext(data)
|
|
198
209
|
}
|
|
199
210
|
}
|
|
200
|
-
|
|
201
|
-
|
|
File without changes
|
|
@@ -67,7 +67,7 @@ export async function getNextLesson(data) {
|
|
|
67
67
|
const lastInteractedStatus = childrenStates[lastInteracted]
|
|
68
68
|
|
|
69
69
|
//different nextLesson behaviour for different content types
|
|
70
|
-
if (content.type === 'course' || content.type === 'pack-bundle') {
|
|
70
|
+
if (content.type === 'course' || content.type === 'pack-bundle' || content.type === 'skill-pack') {
|
|
71
71
|
if (lastInteractedStatus === STATE_STARTED) {
|
|
72
72
|
nextLessonData[content.id] = lastInteracted
|
|
73
73
|
} else {
|
|
@@ -117,16 +117,13 @@ export async function getNavigateTo(data) {
|
|
|
117
117
|
const firstChild = content.children[0]
|
|
118
118
|
let lastInteractedChildNavToData = await getNavigateTo([firstChild])
|
|
119
119
|
lastInteractedChildNavToData = lastInteractedChildNavToData[firstChild.id] ?? null
|
|
120
|
-
navigateToData[content.id] = buildNavigateTo(
|
|
121
|
-
firstChild,
|
|
122
|
-
lastInteractedChildNavToData
|
|
123
|
-
)
|
|
120
|
+
navigateToData[content.id] = buildNavigateTo(firstChild, lastInteractedChildNavToData)
|
|
124
121
|
} else {
|
|
125
122
|
const childrenStates = await getProgressStateByIds(childrenIds)
|
|
126
123
|
const lastInteracted = await getLastInteractedOf(childrenIds)
|
|
127
124
|
const lastInteractedStatus = childrenStates[lastInteracted]
|
|
128
125
|
|
|
129
|
-
if (content.type === 'course' || content.type === 'pack-bundle') {
|
|
126
|
+
if (content.type === 'course' || content.type === 'pack-bundle' || content.type === 'skill-pack') {
|
|
130
127
|
if (lastInteractedStatus === STATE_STARTED) {
|
|
131
128
|
navigateToData[content.id] = buildNavigateTo(children.get(lastInteracted))
|
|
132
129
|
} else {
|
|
@@ -265,7 +262,6 @@ export async function getAllCompleted(limit = null) {
|
|
|
265
262
|
return ids
|
|
266
263
|
}
|
|
267
264
|
|
|
268
|
-
// todo: either refactor to use watermelon, or add method functionality to dataContext (more work overall)
|
|
269
265
|
export async function getAllStartedOrCompleted({
|
|
270
266
|
limit = null,
|
|
271
267
|
onlyIds = true,
|
|
@@ -401,7 +397,12 @@ function startStatusInLocalContext(localContext, contentId, hierarchy) {
|
|
|
401
397
|
setStartedOrCompletedStatusInLocalContext(localContext, contentId, false, hierarchy)
|
|
402
398
|
}
|
|
403
399
|
|
|
404
|
-
function setStartedOrCompletedStatusInLocalContext(
|
|
400
|
+
function setStartedOrCompletedStatusInLocalContext(
|
|
401
|
+
localContext,
|
|
402
|
+
contentId,
|
|
403
|
+
isCompleted,
|
|
404
|
+
hierarchy
|
|
405
|
+
) {
|
|
405
406
|
let data = localContext.data[contentId] ?? {}
|
|
406
407
|
data[DATA_KEY_PROGRESS] = isCompleted ? 100 : 0
|
|
407
408
|
data[DATA_KEY_STATUS] = isCompleted ? STATE_COMPLETED : STATE_STARTED
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -5,6 +5,7 @@ import { HttpClient } from '../../infrastructure/http/HttpClient'
|
|
|
5
5
|
import { globalConfig } from '../config.js'
|
|
6
6
|
import { ForumPost } from './types'
|
|
7
7
|
import { PaginatedResponse } from '../api/types'
|
|
8
|
+
import { markThreadAsRead } from './threads'
|
|
8
9
|
|
|
9
10
|
const baseUrl = `/api/forums`
|
|
10
11
|
|
|
@@ -47,6 +48,7 @@ export interface FetchPostParams {
|
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
49
50
|
* Fetches posts for the given thread.
|
|
51
|
+
* Automatically marks the thread as read when posts are fetched.
|
|
50
52
|
*
|
|
51
53
|
* @param {number} threadId - The ID of the forum thread.
|
|
52
54
|
* @param {string} brand - The brand context (e.g., "drumeo", "singeo").
|
|
@@ -71,6 +73,12 @@ export async function fetchPosts(
|
|
|
71
73
|
const query = new URLSearchParams(queryObj).toString()
|
|
72
74
|
|
|
73
75
|
const url = `${baseUrl}/v1/threads/${threadId}/posts?${query}`
|
|
76
|
+
|
|
77
|
+
// Mark thread as read in background (non-blocking)
|
|
78
|
+
markThreadAsRead(threadId, brand).catch(error => {
|
|
79
|
+
console.error('Failed to mark thread as read:', error)
|
|
80
|
+
})
|
|
81
|
+
|
|
74
82
|
return httpClient.get<PaginatedResponse<ForumPost>>(url)
|
|
75
83
|
}
|
|
76
84
|
|
|
@@ -165,6 +173,7 @@ export async function search(
|
|
|
165
173
|
|
|
166
174
|
/**
|
|
167
175
|
* Fetches posts for the given post, jumping to the post's location in the thread.
|
|
176
|
+
* Automatically marks the thread as read when posts are fetched.
|
|
168
177
|
*
|
|
169
178
|
* @param {number} postId - The ID of the forum post.
|
|
170
179
|
* @param {string} brand - The brand context (e.g., "drumeo", "singeo").
|
|
@@ -189,5 +198,15 @@ export async function jumpToPost(
|
|
|
189
198
|
const query = new URLSearchParams(queryObj).toString()
|
|
190
199
|
|
|
191
200
|
const url = `${baseUrl}/v1/posts/${postId}/jump?${query}`
|
|
192
|
-
|
|
201
|
+
const response = await httpClient.get<PaginatedResponse<ForumPost>>(url)
|
|
202
|
+
|
|
203
|
+
// Mark thread as read in background (non-blocking)
|
|
204
|
+
// Extract thread from first post if available
|
|
205
|
+
if (response.data.length > 0 && response.data[0].thread?.id) {
|
|
206
|
+
markThreadAsRead(response.data[0].thread.id, brand).catch(error => {
|
|
207
|
+
console.error('Failed to mark thread as read:', error)
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return response
|
|
193
212
|
}
|
|
@@ -59,7 +59,7 @@ export async function updateThread(
|
|
|
59
59
|
*/
|
|
60
60
|
export async function followThread(threadId: number, brand: string): Promise<void> {
|
|
61
61
|
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
62
|
-
return httpClient.
|
|
62
|
+
return httpClient.put<void>(`${baseUrl}/v1/threads/${threadId}/follow`, { brand })
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
@@ -75,6 +75,19 @@ export async function unfollowThread(threadId: number, brand: string): Promise<v
|
|
|
75
75
|
return httpClient.delete<void>(`${baseUrl}/v1/threads/${threadId}/follow?brand=${brand}`)
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Marks a thread as read for the authenticated user.
|
|
80
|
+
*
|
|
81
|
+
* @param {number} threadId - The ID of the thread to mark as read.
|
|
82
|
+
* @param {string} brand - The brand context (e.g., "drumeo", "singeo").
|
|
83
|
+
* @return {Promise<void>} - A promise that resolves when the thread is marked as read.
|
|
84
|
+
* @throws {HttpError} - If the request fails.
|
|
85
|
+
*/
|
|
86
|
+
export async function markThreadAsRead(threadId: number, brand: string): Promise<void> {
|
|
87
|
+
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
88
|
+
return httpClient.put<void>(`${baseUrl}/v1/threads/${threadId}/read?brand=${brand}`, {})
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
export interface FetchThreadParams {
|
|
79
92
|
is_followed?: boolean,
|
|
80
93
|
page?: number,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -4,21 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
getDailySession,
|
|
7
|
+
getActivePath,
|
|
7
8
|
resetAllLearningPaths,
|
|
8
9
|
startLearningPath,
|
|
9
|
-
|
|
10
|
+
fetchLearningPathLessons,
|
|
11
|
+
} from '../content-org/learning-paths'
|
|
10
12
|
import { getToday } from '../dateUtils.js'
|
|
11
13
|
import { fetchByRailContentId, fetchByRailContentIds, fetchMethodV2IntroVideo } from '../sanity'
|
|
12
14
|
import { addContextToContent } from '../contentAggregator.js'
|
|
15
|
+
import { getProgressState } from '../contentProgress'
|
|
13
16
|
|
|
14
17
|
export async function getMethodCard(brand) {
|
|
15
|
-
const
|
|
16
|
-
const
|
|
18
|
+
const introVideo = await fetchMethodV2IntroVideo(brand)
|
|
19
|
+
const introVideoProgressState = await getProgressState(introVideo.id)
|
|
17
20
|
//resetAllLearningPaths()
|
|
18
|
-
if (
|
|
21
|
+
if (introVideoProgressState != 'completed') {
|
|
19
22
|
//startLearningPath('drumeo', 422533)
|
|
20
|
-
|
|
21
|
-
const introVideo = await fetchMethodV2IntroVideo(brand)
|
|
22
23
|
const timestamp = Math.floor(Date.now() / 1000)
|
|
23
24
|
return {
|
|
24
25
|
id: 0, // method card has no id
|
|
@@ -36,36 +37,30 @@ export async function getMethodCard(brand) {
|
|
|
36
37
|
progressTimestamp: timestamp,
|
|
37
38
|
}
|
|
38
39
|
} else {
|
|
39
|
-
|
|
40
|
-
const
|
|
40
|
+
//TODO: Optimize loading of dailySessions/Path, should not need multiple requests
|
|
41
|
+
const activeLearningPath = await getActivePath(brand)
|
|
42
|
+
const learningPath = await fetchLearningPathLessons(
|
|
43
|
+
activeLearningPath.active_learning_path_id,
|
|
44
|
+
brand,
|
|
45
|
+
getToday()
|
|
46
|
+
)
|
|
41
47
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const todaysLessons = await addContextToContent(
|
|
48
|
-
fetchByRailContentIds,
|
|
49
|
-
todayContentIds,
|
|
50
|
-
addContextParameters
|
|
48
|
+
const allCompleted = learningPath?.todays_lessons.every(
|
|
49
|
+
(lesson) => lesson.progressStatus === 'completed'
|
|
50
|
+
)
|
|
51
|
+
const anyCompleted = learningPath?.todays_lessons.some(
|
|
52
|
+
(lesson) => lesson.progressStatus === 'completed'
|
|
51
53
|
)
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
nextContentIds,
|
|
55
|
-
addContextParameters
|
|
54
|
+
const noneCompleted = learningPath?.todays_lessons.every(
|
|
55
|
+
(lesson) => lesson.progressStatus !== 'completed'
|
|
56
56
|
)
|
|
57
|
-
const allCompleted = todaysLessons.every((lesson) => lesson.progressStatus === 'completed')
|
|
58
|
-
const anyCompleted = todaysLessons.some((lesson) => lesson.progressStatus === 'completed')
|
|
59
|
-
const noneCompleted = todaysLessons.every((lesson) => lesson.progressStatus !== 'completed')
|
|
60
57
|
|
|
61
|
-
const nextIncompleteLesson =
|
|
58
|
+
const nextIncompleteLesson = learningPath?.todays_lessons.find(
|
|
62
59
|
(lesson) => lesson.progressStatus !== 'completed'
|
|
63
60
|
)
|
|
64
|
-
let
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
let ctaText, action
|
|
61
|
+
let ctaText,
|
|
62
|
+
action,
|
|
63
|
+
nextLesson = null
|
|
69
64
|
if (noneCompleted) {
|
|
70
65
|
ctaText = 'Start Session'
|
|
71
66
|
action = getMethodActionCTA(nextIncompleteLesson)
|
|
@@ -73,26 +68,28 @@ export async function getMethodCard(brand) {
|
|
|
73
68
|
ctaText = 'Continue Session'
|
|
74
69
|
action = getMethodActionCTA(nextIncompleteLesson)
|
|
75
70
|
} else if (allCompleted) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
ctaText = nextAvailableLesson ? 'Start Next Lesson' : 'Browse Lessons'
|
|
80
|
-
action = nextAvailableLesson
|
|
81
|
-
? getMethodActionCTA(nextAvailableLesson)
|
|
71
|
+
ctaText = learningPath.next_lesson ? 'Start Next Lesson' : 'Browse Lessons'
|
|
72
|
+
action = learningPath.next_lesson
|
|
73
|
+
? getMethodActionCTA(learningPath.next_lesson)
|
|
82
74
|
: {
|
|
83
75
|
type: 'lessons',
|
|
84
76
|
brand: brand,
|
|
85
77
|
}
|
|
86
78
|
}
|
|
79
|
+
|
|
80
|
+
let maxProgressTimestamp = Math.max(
|
|
81
|
+
...learningPath?.children.map((lesson) => lesson.progressTimestamp)
|
|
82
|
+
)
|
|
83
|
+
if (!maxProgressTimestamp) {
|
|
84
|
+
maxProgressTimestamp = learningPath.active_learning_path_created_at
|
|
85
|
+
}
|
|
86
|
+
|
|
87
87
|
return {
|
|
88
88
|
id: 0,
|
|
89
|
-
type: '
|
|
89
|
+
type: 'learning-path-v2',
|
|
90
90
|
progressType: 'content',
|
|
91
91
|
header: 'Method',
|
|
92
|
-
body:
|
|
93
|
-
todays_lessons: todaysLessons,
|
|
94
|
-
next_learning_path_lessons: nextLPLessons,
|
|
95
|
-
},
|
|
92
|
+
body: learningPath,
|
|
96
93
|
cta: {
|
|
97
94
|
text: ctaText,
|
|
98
95
|
action: action,
|
|
File without changes
|
|
File without changes
|
package/src/services/sanity.js
CHANGED
|
@@ -2299,7 +2299,7 @@ export async function fetchShows(brand, type, sort = 'sort') {
|
|
|
2299
2299
|
export async function fetchMethodV2IntroVideo(brand) {
|
|
2300
2300
|
const type = "method-intro";
|
|
2301
2301
|
const filter = `_type == '${type}' && brand == '${brand}'`;
|
|
2302
|
-
const fields = getIntroVideoFields();
|
|
2302
|
+
const fields = getIntroVideoFields('method');
|
|
2303
2303
|
|
|
2304
2304
|
const query = `*[${filter}] { ${fields.join(", ")} }`;
|
|
2305
2305
|
return fetchSanity(query, false);
|
package/src/services/types.js
CHANGED
|
File without changes
|
|
@@ -163,3 +163,30 @@ export async function numberOfActiveUsers(): Promise<number> {
|
|
|
163
163
|
const response = await httpClient.get<{ active_users: number }>(apiUrl)
|
|
164
164
|
return response.active_users
|
|
165
165
|
}
|
|
166
|
+
|
|
167
|
+
export interface UserResource {
|
|
168
|
+
id: number
|
|
169
|
+
email: string
|
|
170
|
+
display_name: string
|
|
171
|
+
first_name: string
|
|
172
|
+
last_name: string
|
|
173
|
+
permission_level: string
|
|
174
|
+
use_student_view: boolean
|
|
175
|
+
is_admin: boolean
|
|
176
|
+
show_admin_toggle: boolean
|
|
177
|
+
[key: string]: any // Allow additional properties from the API
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Toggles the student view mode for admin users.
|
|
182
|
+
* When enabled, admins see the platform as a regular student would.
|
|
183
|
+
*
|
|
184
|
+
* @param {boolean} useStudentView - Whether to enable student view mode (true) or admin view mode (false).
|
|
185
|
+
* @returns {Promise<UserResource>} - A promise that resolves to the updated user resource.
|
|
186
|
+
* @throws {HttpError} - Throws HttpError if the request fails or user is not an admin.
|
|
187
|
+
*/
|
|
188
|
+
export async function toggleStudentView(useStudentView: boolean): Promise<UserResource> {
|
|
189
|
+
const apiUrl = `/api/user-management-system/v1/user/student-view`
|
|
190
|
+
const httpClient = new HttpClient(globalConfig.baseUrl, globalConfig.sessionConfig.token)
|
|
191
|
+
return httpClient.patch<UserResource>(apiUrl, { use_student_view: useStudentView })
|
|
192
|
+
}
|
|
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
|
|
@@ -73,6 +73,8 @@
|
|
|
73
73
|
* @property {number} send_mobile_app_push_notifications
|
|
74
74
|
* @property {number} send_email_notifications
|
|
75
75
|
* @property {number} use_legacy_video_player
|
|
76
|
+
* @property {boolean} use_student_view
|
|
77
|
+
* @property {boolean} show_admin_toggle
|
|
76
78
|
* @property {number} drumeo_ship_magazine
|
|
77
79
|
* @property {string|null} magazine_shipping_address_id
|
|
78
80
|
* @property {string|null} ios_latest_review_display_date
|
|
File without changes
|
|
@@ -1055,12 +1055,11 @@ function generateContentsMap(contents, playlistsContents, methodProgressContents
|
|
|
1055
1055
|
*/
|
|
1056
1056
|
export async function getProgressRows({ brand = 'drumeo', limit = 8 } = {}) {
|
|
1057
1057
|
// TODO slice progress to a reasonable number, say 100
|
|
1058
|
-
|
|
1059
|
-
const [recentPlaylists, progressContents, userPinnedItem
|
|
1058
|
+
const methodCardPromise = getMethodCard(brand)
|
|
1059
|
+
const [recentPlaylists, progressContents, userPinnedItem] = await Promise.all([
|
|
1060
1060
|
fetchUserPlaylists(brand, { sort: '-last_progress', limit: limit }),
|
|
1061
1061
|
getAllStartedOrCompleted({ onlyIds: false, brand: brand }),
|
|
1062
1062
|
getUserPinnedItem(brand),
|
|
1063
|
-
getMethodCard(brand),
|
|
1064
1063
|
])
|
|
1065
1064
|
|
|
1066
1065
|
const playlists = recentPlaylists?.data || []
|
|
@@ -1103,7 +1102,7 @@ export async function getProgressRows({ brand = 'drumeo', limit = 8 } = {}) {
|
|
|
1103
1102
|
])
|
|
1104
1103
|
|
|
1105
1104
|
const contentsMap = generateContentsMap(contents, playlistsContents)
|
|
1106
|
-
|
|
1105
|
+
const methodCard = await methodCardPromise
|
|
1107
1106
|
let combined = await extractPinnedItemsAndSortAllItems(
|
|
1108
1107
|
userPinnedItem,
|
|
1109
1108
|
contentsMap,
|
package/test/HttpClient.test.js
CHANGED
|
File without changes
|
package/test/content.test.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/dataContext.test.js
CHANGED
|
File without changes
|
package/test/forum.test.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/initializeTests.js
CHANGED
|
File without changes
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { initializeTestService } from './initializeTests.js'
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
fetchLearningPathLessons,
|
|
4
|
+
getLearningPath,
|
|
5
|
+
} from '../src/services/content-org/learning-paths.ts'
|
|
4
6
|
import { contentStatusCompleted } from '../src/services/contentProgress.js'
|
|
5
7
|
describe('learning-paths', function () {
|
|
6
8
|
beforeEach(async () => {
|
|
@@ -8,7 +10,7 @@ describe('learning-paths', function () {
|
|
|
8
10
|
})
|
|
9
11
|
|
|
10
12
|
test('getLearningPathsV2Test', async () => {
|
|
11
|
-
const results = await
|
|
13
|
+
const results = await getLearningPath(417140)
|
|
12
14
|
})
|
|
13
15
|
test('getlearningPathLessonsTestNew', async () => {
|
|
14
16
|
await contentStatusCompleted(417105)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/localStorageMock.js
CHANGED
|
File without changes
|
package/test/log.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|