musora-content-services 2.11.1 → 2.13.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/.github/workflows/docs.js.yml +61 -0
- package/CHANGELOG.md +21 -0
- package/link_mcs.sh +0 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +4 -4
- package/src/index.d.ts +2 -0
- package/src/index.js +2 -0
- package/src/services/content.js +1 -1
- package/src/services/contentProgress.js +35 -1
- package/src/services/imageSRCVerify.js +0 -0
- package/src/services/sanity.js +43 -13
- package/src/services/user/notifications.js +4 -3
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Sync V2 Docs to Main
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [project-v2]
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
sync-docs:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- name: Checkout project-v2 branch
|
|
11
|
+
uses: actions/checkout@v4
|
|
12
|
+
with:
|
|
13
|
+
ref: project-v2
|
|
14
|
+
path: project-v2-content
|
|
15
|
+
|
|
16
|
+
- name: Checkout main branch
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
ref: main
|
|
20
|
+
path: main-content
|
|
21
|
+
token: ${{ secrets.PROJECT_V2_DOCS_TOKEN }} #use separate token to trigger other actions
|
|
22
|
+
|
|
23
|
+
- name: Copy docs from project-v2 to main
|
|
24
|
+
run: |
|
|
25
|
+
# Create the target directory if it doesn't exist
|
|
26
|
+
mkdir -p main-content/docs/v2
|
|
27
|
+
|
|
28
|
+
# Remove existing v2 docs to ensure clean copy
|
|
29
|
+
rm -rf main-content/docs/v2/*
|
|
30
|
+
|
|
31
|
+
# Copy docs from project-v2 to main/docs/v2
|
|
32
|
+
if [ -d "project-v2-content/docs" ]; then
|
|
33
|
+
cp -r project-v2-content/docs/* main-content/docs/v2/
|
|
34
|
+
echo "✅ Copied docs from project-v2 to main/docs/v2"
|
|
35
|
+
else
|
|
36
|
+
echo "⚠️ No docs folder found in project-v2 branch"
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
- name: Commit and push changes to main
|
|
41
|
+
id: commit
|
|
42
|
+
run: |
|
|
43
|
+
cd main-content
|
|
44
|
+
git config --local user.email "action@github.com"
|
|
45
|
+
git config --local user.name "GitHub Action"
|
|
46
|
+
|
|
47
|
+
# Check if there are any changes
|
|
48
|
+
if [ -n "$(git status --porcelain)" ]; then
|
|
49
|
+
git add docs/v2/
|
|
50
|
+
git commit -m "🔄 Auto-sync: Update v2 docs from project-v2 branch
|
|
51
|
+
|
|
52
|
+
- Synced from project-v2/docs
|
|
53
|
+
- Triggered by commit: ${{ github.sha }}
|
|
54
|
+
- Date: $(date)"
|
|
55
|
+
git push
|
|
56
|
+
echo "✅ Successfully pushed updated docs to main branch"
|
|
57
|
+
echo "changes=true" >> $GITHUB_OUTPUT
|
|
58
|
+
else
|
|
59
|
+
echo "ℹ️ No changes detected in docs"
|
|
60
|
+
echo "changes=false" >> $GITHUB_OUTPUT
|
|
61
|
+
fi
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
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.13.0](https://github.com/railroadmedia/musora-content-services/compare/v2.12.0...v2.13.0) (2025-06-26)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* add logo data to parent in fetchLessonContent ([5c924b6](https://github.com/railroadmedia/musora-content-services/commit/5c924b6ad770d9befc0d5a972d0333840e8c5c83))
|
|
11
|
+
|
|
12
|
+
## [2.12.0](https://github.com/railroadmedia/musora-content-services/compare/v2.11.0...v2.12.0) (2025-06-25)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **MU2-478:** simplified version of getAllStartedOrCompleted method ([b58b97b](https://github.com/railroadmedia/musora-content-services/commit/b58b97bcf95db7ad71019848dbf5e5b508adeeb8))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **MU2-608:** Sort recent lessons by user progress ([e966ffc](https://github.com/railroadmedia/musora-content-services/commit/e966ffcd87847ee6b72d35bed84967ee8fd1db44))
|
|
23
|
+
* **MU2-730:** Pagination on fetchNotifications method ([37e95db](https://github.com/railroadmedia/musora-content-services/commit/37e95dbaacca06a1e408db8a5d6dead3dfb23550))
|
|
24
|
+
* Pagination on fetchNotifications method ([9e613b9](https://github.com/railroadmedia/musora-content-services/commit/9e613b9227bb188814ebc2d8c084c18e115fbcc3))
|
|
25
|
+
|
|
5
26
|
### [2.11.1](https://github.com/railroadmedia/musora-content-services/compare/v2.11.0...v2.11.1) (2025-06-24)
|
|
6
27
|
|
|
7
28
|
## [2.11.0](https://github.com/railroadmedia/musora-content-services/compare/v2.10.0...v2.11.0) (2025-06-24)
|
package/link_mcs.sh
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/src/contentTypeConfig.js
CHANGED
|
@@ -426,8 +426,8 @@ export let contentTypeConfig = {
|
|
|
426
426
|
}`,
|
|
427
427
|
`"resources": ${resourcesField}`,
|
|
428
428
|
'"thumbnail": thumbnail.asset->url',
|
|
429
|
-
'"
|
|
430
|
-
'"
|
|
429
|
+
'"light_mode_logo": light_mode_logo_url.asset->url',
|
|
430
|
+
'"dark_mode_logo": dark_mode_logo_url.asset->url',
|
|
431
431
|
`"description": ${descriptionField}`,
|
|
432
432
|
],
|
|
433
433
|
},
|
|
@@ -449,8 +449,8 @@ export let contentTypeConfig = {
|
|
|
449
449
|
`"resources": ${resourcesField}`,
|
|
450
450
|
'"image": logo_image_url.asset->url',
|
|
451
451
|
'"thumbnail": thumbnail.asset->url',
|
|
452
|
-
'"
|
|
453
|
-
'"
|
|
452
|
+
'"light_mode_logo": light_mode_logo_url.asset->url',
|
|
453
|
+
'"dark_mode_logo": dark_mode_logo_url.asset->url',
|
|
454
454
|
`"description": ${descriptionField}`,
|
|
455
455
|
'total_xp',
|
|
456
456
|
],
|
package/src/index.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
getProgressStateByIds,
|
|
60
60
|
getResumeTimeSeconds,
|
|
61
61
|
getResumeTimeSecondsByIds,
|
|
62
|
+
getStartedOrCompletedProgressOnly,
|
|
62
63
|
recordWatchSession
|
|
63
64
|
} from './services/contentProgress.js';
|
|
64
65
|
|
|
@@ -419,6 +420,7 @@ declare module 'musora-content-services' {
|
|
|
419
420
|
getResumeTimeSecondsByIds,
|
|
420
421
|
getScheduleContentRows,
|
|
421
422
|
getSortOrder,
|
|
423
|
+
getStartedOrCompletedProgressOnly,
|
|
422
424
|
getTabResults,
|
|
423
425
|
getTimeRemainingUntilLocal,
|
|
424
426
|
getUserMonthlyStats,
|
package/src/index.js
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
getProgressStateByIds,
|
|
60
60
|
getResumeTimeSeconds,
|
|
61
61
|
getResumeTimeSecondsByIds,
|
|
62
|
+
getStartedOrCompletedProgressOnly,
|
|
62
63
|
recordWatchSession
|
|
63
64
|
} from './services/contentProgress.js';
|
|
64
65
|
|
|
@@ -418,6 +419,7 @@ export {
|
|
|
418
419
|
getResumeTimeSecondsByIds,
|
|
419
420
|
getScheduleContentRows,
|
|
420
421
|
getSortOrder,
|
|
422
|
+
getStartedOrCompletedProgressOnly,
|
|
421
423
|
getTabResults,
|
|
422
424
|
getTimeRemainingUntilLocal,
|
|
423
425
|
getUserMonthlyStats,
|
package/src/services/content.js
CHANGED
|
@@ -19,7 +19,7 @@ import {recommendations} from "./recommendations";
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
export async function getLessonContentRows (brand='drumeo', pageName = 'lessons') {
|
|
22
|
-
let recentContentIds = await fetchRecent(brand, pageName, { progress: 'recent' });
|
|
22
|
+
let recentContentIds = await fetchRecent(brand, pageName, { progress: 'recent', limit: 10 });
|
|
23
23
|
|
|
24
24
|
let contentRows = await getContentRows(brand, pageName);
|
|
25
25
|
contentRows = Array.isArray(contentRows) ? contentRows : [];
|
|
@@ -118,7 +118,7 @@ export async function getAllStartedOrCompleted({ limit = null, onlyIds = true, b
|
|
|
118
118
|
const isRelevantStatus =
|
|
119
119
|
item[DATA_KEY_STATUS] === STATE_STARTED || item[DATA_KEY_STATUS] === STATE_COMPLETED
|
|
120
120
|
const isRecent = item[DATA_KEY_LAST_UPDATED_TIME] >= oneMonthAgoInSeconds
|
|
121
|
-
const isCorrectBrand = !brand || item.b === brand
|
|
121
|
+
const isCorrectBrand = !brand || !item.b || item.b === brand
|
|
122
122
|
const isNotExcluded = !excludedSet.has(id)
|
|
123
123
|
return isRelevantStatus && isRecent && isCorrectBrand && isNotExcluded
|
|
124
124
|
})
|
|
@@ -151,6 +151,40 @@ export async function getAllStartedOrCompleted({ limit = null, onlyIds = true, b
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Simplified version of `getAllStartedOrCompleted`.
|
|
156
|
+
*
|
|
157
|
+
* Fetches content IDs and progress percentages for items that were
|
|
158
|
+
* started or completed.
|
|
159
|
+
*
|
|
160
|
+
* @param {Object} [options={}] - Optional filtering options.
|
|
161
|
+
* @param {string|null} [options.brand=null] - Brand to filter by (e.g., 'drumeo').
|
|
162
|
+
* @returns {Promise<Object>} - A map of content ID to progress value:
|
|
163
|
+
* {
|
|
164
|
+
* [id]: progressPercentage
|
|
165
|
+
* }
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* const progressMap = await getStartedOrCompletedProgressOnly({ brand: 'drumeo' });
|
|
169
|
+
* console.log(progressMap[123]); // => 52
|
|
170
|
+
*/
|
|
171
|
+
export async function getStartedOrCompletedProgressOnly({ brand = null} = {}) {
|
|
172
|
+
const data = await dataContext.getData()
|
|
173
|
+
const result = {}
|
|
174
|
+
|
|
175
|
+
Object.entries(data).forEach(([key, item]) => {
|
|
176
|
+
const id = parseInt(key)
|
|
177
|
+
const isRelevantStatus = item[DATA_KEY_STATUS] === STATE_STARTED || item[DATA_KEY_STATUS] === STATE_COMPLETED
|
|
178
|
+
const isCorrectBrand = !brand || item.b === brand
|
|
179
|
+
|
|
180
|
+
if (isRelevantStatus && isCorrectBrand) {
|
|
181
|
+
result[id] = item?.[DATA_KEY_PROGRESS] ?? 0
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
return result
|
|
186
|
+
}
|
|
187
|
+
|
|
154
188
|
export async function assignmentStatusCompleted(assignmentId, parentContentId) {
|
|
155
189
|
await dataContext.update(
|
|
156
190
|
async function (localContext) {
|
|
File without changes
|
package/src/services/sanity.js
CHANGED
|
@@ -888,11 +888,11 @@ async function getProgressFilter(progress, progressIds) {
|
|
|
888
888
|
return `&& !(railcontent_id in [${ids.join(',')}])`
|
|
889
889
|
}
|
|
890
890
|
case 'recent': {
|
|
891
|
-
const ids = await getAllStartedOrCompleted()
|
|
891
|
+
const ids = progressIds !== undefined ? progressIds : await getAllStartedOrCompleted()
|
|
892
892
|
return `&& (railcontent_id in [${ids.join(',')}])`
|
|
893
893
|
}
|
|
894
894
|
case 'incomplete': {
|
|
895
|
-
const ids = await getAllStarted()
|
|
895
|
+
const ids = progressIds !== undefined ? progressIds :await getAllStarted()
|
|
896
896
|
return `&& railcontent_id in [${ids.join(',')}]`
|
|
897
897
|
}
|
|
898
898
|
default:
|
|
@@ -1269,7 +1269,6 @@ export async function fetchLessonContent(railContentId) {
|
|
|
1269
1269
|
"id":railcontent_id,
|
|
1270
1270
|
slug, artist->,
|
|
1271
1271
|
"thumbnail":thumbnail.asset->url,
|
|
1272
|
-
"url": web_url_path,
|
|
1273
1272
|
soundslice_slug,
|
|
1274
1273
|
"description": description[0].children[0].text,
|
|
1275
1274
|
"chapters": chapter[]{
|
|
@@ -1299,9 +1298,11 @@ export async function fetchLessonContent(railContentId) {
|
|
|
1299
1298
|
"parent_content_data": parent_content_data[]{
|
|
1300
1299
|
"id": id,
|
|
1301
1300
|
"title": *[railcontent_id == ^.id][0].title,
|
|
1302
|
-
"web_url_path": *[railcontent_id == ^.id][0].web_url_path,
|
|
1303
1301
|
"slug":*[railcontent_id == ^.id][0].slug,
|
|
1304
1302
|
"type": *[railcontent_id == ^.id][0]._type,
|
|
1303
|
+
"logo" : *[railcontent_id == ^.id][0].logo_image_url.asset->url,
|
|
1304
|
+
"dark_mode_logo": *[railcontent_id == ^.id][0].dark_mode_logo_url.asset->url,
|
|
1305
|
+
"light_mode_logo": *[railcontent_id == ^.id][0].light_mode_logo_url.asset->url,
|
|
1305
1306
|
},
|
|
1306
1307
|
sort,
|
|
1307
1308
|
xp,
|
|
@@ -2187,12 +2188,12 @@ async function buildQuery(
|
|
|
2187
2188
|
function buildEntityAndTotalQuery(
|
|
2188
2189
|
filter = '',
|
|
2189
2190
|
fields = '...',
|
|
2190
|
-
{ sortOrder = 'published_on desc', start = 0, end = 10, isSingle = false }
|
|
2191
|
+
{ sortOrder = 'published_on desc', start = 0, end = 10, isSingle = false, withoutPagination = false }
|
|
2191
2192
|
) {
|
|
2192
|
-
const sortString = sortOrder ? `order(${sortOrder})` : ''
|
|
2193
|
-
const countString = isSingle ? '[0...1]' : `[${start}...${end}]`
|
|
2193
|
+
const sortString = sortOrder ? ` | order(${sortOrder})` : ''
|
|
2194
|
+
const countString = isSingle ? '[0...1]' : (withoutPagination ? ``: `[${start}...${end}]`)
|
|
2194
2195
|
const query = `{
|
|
2195
|
-
"entity": *[${filter}]
|
|
2196
|
+
"entity": *[${filter}] ${sortString}${countString}
|
|
2196
2197
|
{
|
|
2197
2198
|
${fields}
|
|
2198
2199
|
},
|
|
@@ -2311,17 +2312,32 @@ export async function fetchTabData(
|
|
|
2311
2312
|
) {
|
|
2312
2313
|
const start = (page - 1) * limit
|
|
2313
2314
|
const end = start + limit
|
|
2314
|
-
|
|
2315
|
+
let withoutPagination = false
|
|
2315
2316
|
// Construct the included fields filter, replacing 'difficulty' with 'difficulty_string'
|
|
2316
2317
|
const includedFieldsFilter =
|
|
2317
2318
|
includedFields.length > 0 ? filtersToGroq(includedFields, [], pageName) : ''
|
|
2318
2319
|
|
|
2320
|
+
let sortOrder = getSortOrder(sort, brand, '')
|
|
2321
|
+
|
|
2322
|
+
switch (progress) {
|
|
2323
|
+
case 'recent':
|
|
2324
|
+
progressIds = await getAllStartedOrCompleted({ brand, onlyIds: true });
|
|
2325
|
+
sortOrder = null;
|
|
2326
|
+
withoutPagination = true;
|
|
2327
|
+
break;
|
|
2328
|
+
case 'incomplete':
|
|
2329
|
+
progressIds = await getAllStarted();
|
|
2330
|
+
sortOrder = null;
|
|
2331
|
+
break;
|
|
2332
|
+
case 'completed':
|
|
2333
|
+
progressIds = await getAllCompleted();
|
|
2334
|
+
sortOrder = null;
|
|
2335
|
+
break;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2319
2338
|
// limits the results to supplied progressIds for started & completed filters
|
|
2320
2339
|
const progressFilter = await getProgressFilter(progress, progressIds)
|
|
2321
2340
|
|
|
2322
|
-
// Determine the sort order
|
|
2323
|
-
const sortOrder = getSortOrder(sort, brand, '')
|
|
2324
|
-
|
|
2325
2341
|
let fields = DEFAULT_FIELDS
|
|
2326
2342
|
let fieldsString = fields.join(',')
|
|
2327
2343
|
|
|
@@ -2348,9 +2364,23 @@ export async function fetchTabData(
|
|
|
2348
2364
|
sortOrder: sortOrder,
|
|
2349
2365
|
start: start,
|
|
2350
2366
|
end: end,
|
|
2367
|
+
withoutPagination: withoutPagination,
|
|
2351
2368
|
})
|
|
2352
2369
|
|
|
2353
|
-
|
|
2370
|
+
let results = await fetchSanity(query, true);
|
|
2371
|
+
|
|
2372
|
+
if (['recent', 'incomplete', 'completed'].includes(progress) && results.entity.length > 1) {
|
|
2373
|
+
const orderMap = new Map(progressIds.map((id, index) => [id, index]))
|
|
2374
|
+
results.entity = results.entity
|
|
2375
|
+
.sort((a, b) => {
|
|
2376
|
+
const aIdx = orderMap.get(a.id) ?? Number.MAX_SAFE_INTEGER;
|
|
2377
|
+
const bIdx = orderMap.get(b.id) ?? Number.MAX_SAFE_INTEGER;
|
|
2378
|
+
return aIdx - bIdx || new Date(b.published_on) - new Date(a.published_on);
|
|
2379
|
+
})
|
|
2380
|
+
.slice(start, end);
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
return results;
|
|
2354
2384
|
}
|
|
2355
2385
|
|
|
2356
2386
|
export async function fetchRecent(
|
|
@@ -12,6 +12,7 @@ const baseUrl = `/api/notifications`
|
|
|
12
12
|
* @param {Object} [options={}] - Options for fetching notifications.
|
|
13
13
|
* @param {string} options.brand - The brand to filter notifications by. (Required)
|
|
14
14
|
* @param {number} [options.limit=10] - The maximum number of notifications to fetch.
|
|
15
|
+
* @param {number} [options.page=1] - The page number for pagination.
|
|
15
16
|
* @param {boolean} [options.onlyUnread=false] - Whether to fetch only unread notifications. If true, adds `unread=1` to the query.
|
|
16
17
|
*
|
|
17
18
|
* @returns {Promise<Array<Object>>} - A promise that resolves to an array of notifications.
|
|
@@ -19,17 +20,17 @@ const baseUrl = `/api/notifications`
|
|
|
19
20
|
* @throws {Error} - Throws an error if the brand is not provided.
|
|
20
21
|
*
|
|
21
22
|
* @example
|
|
22
|
-
* fetchNotifications({ brand: 'drumeo', limit: 5, onlyUnread: true })
|
|
23
|
+
* fetchNotifications({ brand: 'drumeo', limit: 5, onlyUnread: true, page: 2 })
|
|
23
24
|
* .then(notifications => console.log(notifications))
|
|
24
25
|
* .catch(error => console.error(error));
|
|
25
26
|
*/
|
|
26
|
-
export async function fetchNotifications({ brand = null, limit = 10, onlyUnread = false } = {}) {
|
|
27
|
+
export async function fetchNotifications({ brand = null, limit = 10, onlyUnread = false, page = 1 } = {}) {
|
|
27
28
|
if (!brand) {
|
|
28
29
|
throw new Error('brand is required')
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
const unreadParam = onlyUnread ? '&unread=1' : ''
|
|
32
|
-
const url = `${baseUrl}/v1?brand=${brand}${unreadParam}&limit=${limit}`
|
|
33
|
+
const url = `${baseUrl}/v1?brand=${brand}${unreadParam}&limit=${limit}&page=${page}`
|
|
33
34
|
return fetchHandler(url, 'get')
|
|
34
35
|
}
|
|
35
36
|
|