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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.11.1",
3
+ "version": "2.13.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -426,8 +426,8 @@ export let contentTypeConfig = {
426
426
  }`,
427
427
  `"resources": ${resourcesField}`,
428
428
  '"thumbnail": thumbnail.asset->url',
429
- '"light_logo": light_mode_logo_url.asset->url',
430
- '"dark_logo": dark_mode_logo_url.asset->url',
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
- '"light_logo": light_mode_logo_url.asset->url',
453
- '"dark_logo": dark_mode_logo_url.asset->url',
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,
@@ -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
@@ -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}] | ${sortString}${countString}
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
- return fetchSanity(query, true)
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