musora-content-services 2.92.6 → 2.93.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.
@@ -1,61 +1,73 @@
1
- name: Sync V2 Docs to Main
1
+ name: Deploy Docs to GitHub Pages
2
2
  on:
3
3
  push:
4
- branches: [project-v2]
4
+ branches: [main, project-v2]
5
+
6
+ # Required permissions for GitHub Pages deployment
7
+ permissions:
8
+ contents: read
9
+ pages: write
10
+ id-token: write
11
+
12
+ # Allow only one concurrent deployment
13
+ concurrency:
14
+ group: "pages"
15
+ cancel-in-progress: false
5
16
 
6
17
  jobs:
7
- sync-docs:
18
+ deploy-docs:
8
19
  runs-on: ubuntu-latest
20
+ environment:
21
+ name: github-pages
22
+ url: ${{ steps.deployment.outputs.page_url }}
23
+
9
24
  steps:
25
+ - name: Checkout main branch
26
+ uses: actions/checkout@v4
27
+ with:
28
+ ref: main
29
+ path: main-content
30
+
10
31
  - name: Checkout project-v2 branch
11
32
  uses: actions/checkout@v4
12
33
  with:
13
34
  ref: project-v2
14
- path: project-v2-content
35
+ path: v2-content
15
36
 
16
- - name: Checkout main branch
17
- uses: actions/checkout@v4
37
+ - name: Setup Node.js
38
+ uses: actions/setup-node@v4
18
39
  with:
19
- ref: main
20
- path: main-content
21
- token: ${{ secrets.PROJECT_V2_DOCS_TOKEN }} #use separate token to trigger other actions
40
+ node-version: '20'
22
41
 
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
42
+ - name: Install dependencies for v2
43
+ working-directory: v2-content
44
+ run: npm ci
39
45
 
40
- - name: Commit and push changes to main
41
- id: commit
46
+ - name: Generate v2 documentation
47
+ working-directory: v2-content
48
+ run: npm run doc
49
+
50
+ - name: Combine v1 and v2 docs into deployment structure
42
51
  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
52
+ mkdir -p _site
53
+ # Copy existing v1 docs from main branch (already committed)
54
+ if [ -d "main-content/docs" ]; then
55
+ cp -r main-content/docs/* _site/
56
+ echo "✅ Copied existing v1 docs from main branch"
61
57
  fi
58
+ # Copy v2 docs to /v2/ subdirectory
59
+ mkdir -p _site/v2
60
+ cp -r v2-content/docs/* _site/v2/
61
+ echo "✅ Combined v1 (root) and v2 (/v2/) documentation"
62
+
63
+ - name: Setup GitHub Pages
64
+ uses: actions/configure-pages@v4
65
+
66
+ - name: Upload combined documentation artifact
67
+ uses: actions/upload-pages-artifact@v3
68
+ with:
69
+ path: './_site'
70
+
71
+ - name: Deploy to GitHub Pages
72
+ id: deployment
73
+ uses: actions/deploy-pages@v4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
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.93.0](https://github.com/railroadmedia/musora-content-services/compare/v2.92.7...v2.93.0) (2025-12-02)
6
+
7
+
8
+ ### Features
9
+
10
+ * **BEH-1192:** revise fetchLearningPathProgressCheckLessons ([#604](https://github.com/railroadmedia/musora-content-services/issues/604)) ([db8cb70](https://github.com/railroadmedia/musora-content-services/commit/db8cb70ed70f4341fdd4651cdd3fea2b8047dc21))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * method progress card subtitle ([#608](https://github.com/railroadmedia/musora-content-services/issues/608)) ([3bbfb56](https://github.com/railroadmedia/musora-content-services/commit/3bbfb56fce08e0753e7708bdf2d1356000b1c398))
16
+ * Pull in permission_v2 ([#602](https://github.com/railroadmedia/musora-content-services/issues/602)) ([289e32d](https://github.com/railroadmedia/musora-content-services/commit/289e32d08bd3a6448191b9f6034801e4cf8d40e5))
17
+
18
+ ### [2.92.7](https://github.com/railroadmedia/musora-content-services/compare/v2.92.6...v2.92.7) (2025-12-02)
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * **agi:** make brand parameter less strict for now ([#603](https://github.com/railroadmedia/musora-content-services/issues/603)) ([b440b92](https://github.com/railroadmedia/musora-content-services/commit/b440b92ae1b4aab80d7e5ac5238c1e386cf09db8))
24
+
5
25
  ### [2.92.6](https://github.com/railroadmedia/musora-content-services/compare/v2.92.5...v2.92.6) (2025-12-02)
6
26
 
7
27
  ### [2.92.5](https://github.com/railroadmedia/musora-content-services/compare/v2.92.3...v2.92.5) (2025-12-02)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.92.6",
3
+ "version": "2.93.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -61,7 +61,7 @@ export const DEFAULT_FIELDS = [
61
61
  `'genre': ${genreField}`,
62
62
  'status',
63
63
  "'slug' : slug.current",
64
- "'permission_id': permission[]->railcontent_id",
64
+ "'permission_id': permission_v2",
65
65
  'child_count',
66
66
  '"parent_id": parent_content_data[0].id',
67
67
  ]
@@ -84,7 +84,7 @@ export const DEFAULT_CHILD_FIELDS = [
84
84
  `'genre': ${genreField}`,
85
85
  'status',
86
86
  "'slug' : slug.current",
87
- "'permission_id': permission[]->railcontent_id",
87
+ "'permission_id': permission_v2",
88
88
  'child_count',
89
89
  '"parent_id": parent_content_data[0].id',
90
90
  ]
package/src/lib/brands.ts CHANGED
@@ -1,8 +1,8 @@
1
- export enum Brand {
2
- MUSORA = 'musora',
3
- DRUMEO = 'drumeo',
4
- PIANOTE = 'pianote',
5
- GUITAREO = 'guitareo',
6
- SINGEO = 'singeo',
7
- PLAYBASS = 'playbass',
1
+ export enum Brands {
2
+ Musora = 'musora',
3
+ Drumeo = 'drumeo',
4
+ Pianote = 'pianote',
5
+ Guitareo = 'guitareo',
6
+ Singeo = 'singeo',
7
+ Playbass = 'playbass',
8
8
  }
@@ -6,7 +6,7 @@ import { FilterBuilder } from '../../filterBuilder.js'
6
6
  import { buildDataAndTotalQuery } from '../../lib/sanity/query'
7
7
  import { fetchSanity, getSortOrder } from '../sanity.js'
8
8
  import { Lesson } from './content'
9
- import { Brand } from '../../lib/brands'
9
+ import { Brands } from '../../lib/brands'
10
10
 
11
11
  export interface Artist {
12
12
  slug: string
@@ -18,7 +18,7 @@ export interface Artist {
18
18
  /**
19
19
  * Fetch all artists with lessons available for a specific brand.
20
20
  *
21
- * @param {Brand} brand - The brand for which to fetch artists.
21
+ * @param {Brands|string} brand - The brand for which to fetch artists.
22
22
  * @returns {Promise<Artist[]|null>} - A promise that resolves to an array of artist objects or null if not found.
23
23
  *
24
24
  * @example
@@ -26,7 +26,7 @@ export interface Artist {
26
26
  * .then(artists => console.log(artists))
27
27
  * .catch(error => console.error(error));
28
28
  */
29
- export async function fetchArtists(brand: Brand): Promise<Artist[] | null> {
29
+ export async function fetchArtists(brand: Brands | string): Promise<Artist[] | null> {
30
30
  const filter = await new FilterBuilder(
31
31
  `_type == "song" && brand == "${brand}" && references(^._id)`,
32
32
  { bypassPermissions: true }
@@ -45,7 +45,7 @@ export async function fetchArtists(brand: Brand): Promise<Artist[] | null> {
45
45
  * Fetch a single artist by their Sanity ID.
46
46
  *
47
47
  * @param {string} slug - The name of the artist to fetch.
48
- * @param {Brand} [brand] - The brand for which to fetch the artist.
48
+ * @param {Brands|string} [brand] - The brand for which to fetch the artist.
49
49
  * @returns {Promise<Artist|null>} - A promise that resolves to an artist objects or null if not found.
50
50
  *
51
51
  * @example
@@ -53,7 +53,10 @@ export async function fetchArtists(brand: Brand): Promise<Artist[] | null> {
53
53
  * .then(artists => console.log(artists))
54
54
  * .catch(error => console.error(error));
55
55
  */
56
- export async function fetchArtistBySlug(slug: string, brand?: Brand): Promise<Artist | null> {
56
+ export async function fetchArtistBySlug(
57
+ slug: string,
58
+ brand?: Brands | string
59
+ ): Promise<Artist | null> {
57
60
  const brandFilter = brand ? `brand == "${brand}" && ` : ''
58
61
  const filter = await new FilterBuilder(`${brandFilter} _type == "song" && references(^._id)`, {
59
62
  bypassPermissions: true,
@@ -84,7 +87,7 @@ export interface LessonsByArtistResponse {
84
87
  /**
85
88
  * Fetch the artist's lessons.
86
89
  * @param {string} slug - The slug of the artist
87
- * @param {Brand} brand - The brand for which to fetch lessons.
90
+ * @param {Brands|string} brand - The brand for which to fetch lessons.
88
91
  * @param {string} contentType - The type of the lessons we need to get from the artist. If not defined, groq will get lessons from all content types
89
92
  * @param {Object} params - Parameters for sorting, searching, pagination and filtering.
90
93
  * @param {string} [params.sort="-published_on"] - The field to sort the lessons by.
@@ -102,7 +105,7 @@ export interface LessonsByArtistResponse {
102
105
  */
103
106
  export async function fetchArtistLessons(
104
107
  slug: string,
105
- brand: Brand,
108
+ brand: Brands | string,
106
109
  contentType: string,
107
110
  {
108
111
  sort = '-published_on',
@@ -6,7 +6,7 @@ import { fetchSanity, getSortOrder } from '../sanity.js'
6
6
  import { FilterBuilder } from '../../filterBuilder.js'
7
7
  import { Lesson } from './content'
8
8
  import { buildDataAndTotalQuery } from '../../lib/sanity/query'
9
- import { Brand } from '../../lib/brands'
9
+ import { Brands } from '../../lib/brands'
10
10
 
11
11
  export interface Genre {
12
12
  name: string
@@ -18,7 +18,7 @@ export interface Genre {
18
18
  /**
19
19
  * Fetch all genres with lessons available for a specific brand.
20
20
  *
21
- * @param {string} [brand] - The brand for which to fetch the genre for. Lesson count will be filtered by this brand if provided.
21
+ * @param {Brands|string} [brand] - The brand for which to fetch the genre for. Lesson count will be filtered by this brand if provided.
22
22
  * @returns {Promise<Genre[]>} - A promise that resolves to an genre object or null if not found.
23
23
  *
24
24
  * @example
@@ -26,7 +26,7 @@ export interface Genre {
26
26
  * .then(genres => console.log(genres))
27
27
  * .catch(error => console.error(error));
28
28
  */
29
- export async function fetchGenres(brand: Brand): Promise<Genre[]> {
29
+ export async function fetchGenres(brand: Brands | string): Promise<Genre[]> {
30
30
  const filter = await new FilterBuilder(`brand == "${brand}" && references(^._id)`, {
31
31
  bypassPermissions: true,
32
32
  }).buildFilter()
@@ -46,7 +46,7 @@ export async function fetchGenres(brand: Brand): Promise<Genre[]> {
46
46
  * Fetch a single genre by their slug and brand
47
47
  *
48
48
  * @param {string} slug - The slug of the genre to fetch.
49
- * @param {Brand} [brand] - The brand for which to fetch the genre. Lesson count will be filtered by this brand if provided.
49
+ * @param {Brands|string} [brand] - The brand for which to fetch the genre. Lesson count will be filtered by this brand if provided.
50
50
  * @returns {Promise<Genre[]|null>} - A promise that resolves to an genre object or null if not found.
51
51
  *
52
52
  * @example
@@ -54,7 +54,10 @@ export async function fetchGenres(brand: Brand): Promise<Genre[]> {
54
54
  * .then(genres => console.log(genres))
55
55
  * .catch(error => console.error(error));
56
56
  */
57
- export async function fetchGenreBySlug(slug: string, brand?: Brand): Promise<Genre | null> {
57
+ export async function fetchGenreBySlug(
58
+ slug: string,
59
+ brand?: Brands | string
60
+ ): Promise<Genre | null> {
58
61
  const brandFilter = brand ? `brand == "${brand}" && ` : ''
59
62
  const filter = await new FilterBuilder(`${brandFilter} references(^._id)`, {
60
63
  bypassPermissions: true,
@@ -88,7 +91,7 @@ export interface LessonsByGenreResponse {
88
91
  /**
89
92
  * Fetch the genre's lessons.
90
93
  * @param {string} slug - The slug of the genre
91
- * @param {Brand} brand - The brand for which to fetch lessons.
94
+ * @param {Brands|string} brand - The brand for which to fetch lessons.
92
95
  * @param {Object} params - Parameters for sorting, searching, pagination and filtering.
93
96
  * @param {string} [params.sort="-published_on"] - The field to sort the lessons by.
94
97
  * @param {string} [params.searchTerm=""] - The search term to filter the lessons.
@@ -105,7 +108,7 @@ export interface LessonsByGenreResponse {
105
108
  */
106
109
  export async function fetchGenreLessons(
107
110
  slug: string,
108
- brand: Brand,
111
+ brand: Brands | string,
109
112
  contentType?: string,
110
113
  {
111
114
  sort = '-published_on',
@@ -6,7 +6,7 @@ import { filtersToGroq, getFieldsForContentType } from '../../contentTypeConfig.
6
6
  import { fetchSanity, getSortOrder } from '../sanity.js'
7
7
  import { Lesson } from './content'
8
8
  import { buildDataAndTotalQuery } from '../../lib/sanity/query'
9
- import { Brand } from '../../lib/brands'
9
+ import { Brands } from '../../lib/brands'
10
10
 
11
11
  export interface Instructor {
12
12
  lessonCount: number
@@ -19,7 +19,7 @@ export interface Instructor {
19
19
  /**
20
20
  * Fetch all instructor with lessons available for a specific brand.
21
21
  *
22
- * @param {Brand} brand - The brand for which to fetch instructors.
22
+ * @param {Brands|string} brand - The brand for which to fetch instructors.
23
23
  * @returns {Promise<Instructor[]>} - A promise that resolves to an array of instructor objects.
24
24
  *
25
25
  * @example
@@ -27,7 +27,7 @@ export interface Instructor {
27
27
  * .then(instructors => console.log(instructors))
28
28
  * .catch(error => console.error(error));
29
29
  */
30
- export async function fetchInstructors(brand: Brand): Promise<Instructor[]> {
30
+ export async function fetchInstructors(brand: Brands | string): Promise<Instructor[]> {
31
31
  const filter = await new FilterBuilder(`brand == "${brand}" && references(^._id)`, {
32
32
  bypassPermissions: true,
33
33
  }).buildFilter()
@@ -46,7 +46,7 @@ export async function fetchInstructors(brand: Brand): Promise<Instructor[]> {
46
46
  * Fetch a single instructor by their name
47
47
  *
48
48
  * @param {string} slug - The slug of the instructor to fetch.
49
- * @param {Brand} [brand] - The brand for which to fetch the instructor. Lesson count will be filtered by this brand if provided.
49
+ * @param {Brands|string} [brand] - The brand for which to fetch the instructor. Lesson count will be filtered by this brand if provided.
50
50
  * @returns {Promise<Instructor[]>} - A promise that resolves to an instructor object or null if not found.
51
51
  *
52
52
  * @example
@@ -56,7 +56,7 @@ export async function fetchInstructors(brand: Brand): Promise<Instructor[]> {
56
56
  */
57
57
  export async function fetchInstructorBySlug(
58
58
  slug: string,
59
- brand?: Brand
59
+ brand?: Brands | string
60
60
  ): Promise<Instructor | null> {
61
61
  const brandFilter = brand ? `brand == "${brand}" && ` : ''
62
62
  const filter = await new FilterBuilder(`${brandFilter} references(^._id)`, {
@@ -89,8 +89,8 @@ export interface InstructorLessonsResponse {
89
89
 
90
90
  /**
91
91
  * Fetch the data needed for the instructor screen.
92
- * @param {string} brand - The brand for which to fetch instructor lessons
93
92
  * @param {string} slug - The slug of the instructor
93
+ * @param {Brands|string} brand - The brand for which to fetch instructor lessons
94
94
  *
95
95
  * @param {FetchInstructorLessonsOptions} options - Parameters for pagination, filtering and sorting.
96
96
  * @param {string} [options.sortOrder="-published_on"] - The field to sort the lessons by.
@@ -107,7 +107,7 @@ export interface InstructorLessonsResponse {
107
107
  */
108
108
  export async function fetchInstructorLessons(
109
109
  slug: string,
110
- brand: Brand,
110
+ brand: Brands | string,
111
111
  {
112
112
  sortOrder = '-published_on',
113
113
  searchTerm = '',
@@ -11,6 +11,7 @@ import {
11
11
  getAllCompletedByIds,
12
12
  getProgressState,
13
13
  } from '../contentProgress.js'
14
+ import { STATE } from '../sync/models/ContentProgress'
14
15
 
15
16
  const BASE_PATH: string = `/api/content-org`
16
17
  const LEARNING_PATHS_PATH = `${BASE_PATH}/v1/user/learning-paths`
@@ -225,29 +226,18 @@ export async function fetchLearningPathLessons(
225
226
  * For an array of contentIds, fetch any content progress with state=completed,
226
227
  * including other learning paths and a la carte progress.
227
228
  *
228
- * @param contentIds The array of content IDs within the learning path
229
- * @param learningPathId The learning path ID
230
- * @returns {Promise<Object>} Response object
231
- * @returns {Array} result.lessons - Array of all learning path lesson contentIds.
232
- * @returns {Array} result.completed_lessons - Array of learning path lesson contentIds that are completed.
233
- * @returns {Array} result.lessons_count - Count of learning path lessons.
234
- * @returns {Array} result.completed_lessons_count - Count of learning path completed lessons.
229
+ * @param {number[]} contentIds The array of content IDs within the learning path
230
+ * @returns {Promise<Object>} Object with content IDs as keys and the progress state as values
235
231
  */
236
- export async function fetchLearningPathProgressCheckLessons(
237
- contentIds: number[],
238
- learningPathId: number
239
- ): Promise<object> {
240
- let query = await getAllCompletedByIds(contentIds, {
241
- id: learningPathId,
242
- type: 'learning-path-v2',
243
- })
244
- let completedContentIds = query.data
245
- return {
246
- lessons: contentIds,
247
- completed_lessons: completedContentIds,
248
- lessons_count: contentIds.length,
249
- completed_lessons_count: completedContentIds.length,
250
- }
232
+ export async function fetchLearningPathProgressCheckLessons(contentIds: number[]): Promise<Object> {
233
+ let query = await getAllCompletedByIds(contentIds)
234
+ let completedLessons = query.data.map(lesson => lesson.content_id)
235
+
236
+ return contentIds.reduce((obj, contentId) => {
237
+ let lessonIsCompleted = completedLessons.includes(contentId)
238
+ obj[contentId] = lessonIsCompleted ? STATE.COMPLETED : ""
239
+ return obj
240
+ }, {})
251
241
  }
252
242
 
253
243
  interface completeMethodIntroVideo {
@@ -162,18 +162,8 @@ export async function getAllCompleted(limit = null) {
162
162
  return db.contentProgress.completedIds(limit).then(r => r.data.map(id => parseInt(id)))
163
163
  }
164
164
 
165
- /**
166
- *
167
- * @param {array} contentIds List of content children within learning path
168
- * @param {object} collection Learning path object
169
- * @returns {Promise<array>} Filtered list of contentIds that are completed
170
- */
171
- export async function getAllCompletedByIds(contentIds, collection) {
172
- // TODO - implement collection filtering
173
- return db.contentProgress.queryAllIds(
174
- Q.whereIn('content_id', contentIds),
175
- Q.where('state', STATE_COMPLETED)
176
- )
165
+ export async function getAllCompletedByIds(contentIds) {
166
+ return db.contentProgress.completedByContentIds(contentIds)
177
167
  }
178
168
 
179
169
  export async function getAllStartedOrCompleted({
@@ -51,9 +51,7 @@ export class PermissionsV2Adapter extends PermissionsAdapter {
51
51
  }
52
52
 
53
53
  // Get content's required permissions
54
- const oldPermissions = content?.permission_id ?? []
55
- const newPermissions = content?.permission_v2 ?? []
56
- const contentPermissions = new Set([...oldPermissions, ...newPermissions])
54
+ const contentPermissions = new Set(content?.permission_id ?? [])
57
55
 
58
56
  // Content with no permissions is accessible to all
59
57
  if (contentPermissions.size === 0) {
@@ -2,16 +2,9 @@
2
2
  * @module ProgressRow
3
3
  */
4
4
 
5
- import {
6
- getDailySession,
7
- getActivePath,
8
- resetAllLearningPaths,
9
- startLearningPath,
10
- fetchLearningPathLessons,
11
- } from '../content-org/learning-paths'
5
+ import { getActivePath, fetchLearningPathLessons } from '../content-org/learning-paths'
12
6
  import { getToday } from '../dateUtils.js'
13
- import { fetchByRailContentId, fetchByRailContentIds, fetchMethodV2IntroVideo } from '../sanity'
14
- import { addContextToContent } from '../contentAggregator.js'
7
+ import { fetchMethodV2IntroVideo } from '../sanity'
15
8
  import { getProgressState } from '../contentProgress'
16
9
 
17
10
  export async function getMethodCard(brand) {
@@ -21,6 +14,10 @@ export async function getMethodCard(brand) {
21
14
  if (introVideoProgressState !== 'completed') {
22
15
  //startLearningPath('drumeo', 422533)
23
16
  const timestamp = Math.floor(Date.now() / 1000)
17
+ const instructorText =
18
+ introVideo.instructor?.length > 1
19
+ ? 'Multiple Instructors'
20
+ : introVideo.instructor?.[0]?.name || ''
24
21
  return {
25
22
  id: 1, // method card has no id
26
23
  type: 'method',
@@ -29,7 +26,7 @@ export async function getMethodCard(brand) {
29
26
  body: {
30
27
  thumbnail: introVideo.thumbnail,
31
28
  title: introVideo.title,
32
- subtitle: `${introVideo.difficulty_string} • ${introVideo.instructor?.[0]?.name}`,
29
+ subtitle: `${introVideo.difficulty_string} • ${instructorText}`,
33
30
  },
34
31
  cta: {
35
32
  text: 'Get Started',
@@ -59,9 +56,7 @@ export async function getMethodCard(brand) {
59
56
  const nextIncompleteLesson = learningPath?.todays_lessons.find(
60
57
  (lesson) => lesson.progressStatus !== 'completed'
61
58
  )
62
- let ctaText,
63
- action,
64
- nextLesson = null
59
+ let ctaText, action
65
60
  if (noneCompleted) {
66
61
  ctaText = 'Start Session'
67
62
  action = getMethodActionCTA(nextIncompleteLesson)
@@ -9,13 +9,8 @@
9
9
 
10
10
  import { HttpClient } from '../../infrastructure/http/HttpClient'
11
11
  import { globalConfig } from '../config.js'
12
- import {
13
- ReportResponse,
14
- ReportableType,
15
- IssueTypeMap,
16
- ReportIssueOption,
17
- } from './types'
18
- import {Brand} from "../../lib/brands";
12
+ import { ReportResponse, ReportableType, IssueTypeMap, ReportIssueOption } from './types'
13
+ import { Brands } from '../../lib/brands'
19
14
 
20
15
  /**
21
16
  * Parameters for submitting a report with type-safe issue values
@@ -30,7 +25,7 @@ export type ReportParams<T extends ReportableType = ReportableType> = {
30
25
  /** Details about the issue - required when issue is 'other', not sent otherwise */
31
26
  details?: string
32
27
  /** Brand context (required: drumeo, pianote, guitareo, singeo, playbass) */
33
- brand: Brand
28
+ brand: Brands | string
34
29
  }
35
30
 
36
31
  /**
@@ -73,7 +68,9 @@ export type ReportParams<T extends ReportableType = ReportableType> = {
73
68
  * brand: 'drumeo'
74
69
  * })
75
70
  */
76
- export async function report<T extends ReportableType>(params: ReportParams<T>): Promise<ReportResponse> {
71
+ export async function report<T extends ReportableType>(
72
+ params: ReportParams<T>
73
+ ): Promise<ReportResponse> {
77
74
  const httpClient = new HttpClient(globalConfig.baseUrl)
78
75
 
79
76
  // Build request body
@@ -125,7 +122,10 @@ export async function report<T extends ReportableType>(params: ReportParams<T>):
125
122
  * // { value: 'other', label: 'Other' }
126
123
  * // ]
127
124
  */
128
- export function getReportIssueOptions(type: ReportableType, isMobileApp: boolean = false): ReportIssueOption[] {
125
+ export function getReportIssueOptions(
126
+ type: ReportableType,
127
+ isMobileApp: boolean = false
128
+ ): ReportIssueOption[] {
129
129
  switch (type) {
130
130
  case 'forum_post':
131
131
  return [
@@ -147,7 +147,10 @@ export function getReportIssueOptions(type: ReportableType, isMobileApp: boolean
147
147
 
148
148
  case 'content':
149
149
  const contentOptions = [
150
- { value: 'incorrect_metadata', label: 'The lesson image, title or description is incorrect' },
150
+ {
151
+ value: 'incorrect_metadata',
152
+ label: 'The lesson image, title or description is incorrect',
153
+ },
151
154
  { value: 'video_issue', label: 'Video issue' },
152
155
  ]
153
156
 
@@ -165,7 +168,10 @@ export function getReportIssueOptions(type: ReportableType, isMobileApp: boolean
165
168
 
166
169
  case 'playlist':
167
170
  const playlistOptions = [
168
- { value: 'incorrect_metadata', label: 'The lesson image, title or description is incorrect' },
171
+ {
172
+ value: 'incorrect_metadata',
173
+ label: 'The lesson image, title or description is incorrect',
174
+ },
169
175
  { value: 'video_issue', label: 'Video issue' },
170
176
  ]
171
177
 
@@ -182,8 +188,6 @@ export function getReportIssueOptions(type: ReportableType, isMobileApp: boolean
182
188
  return playlistOptions
183
189
 
184
190
  default:
185
- return [
186
- { value: 'other', label: 'Other' },
187
- ]
191
+ return [{ value: 'other', label: 'Other' }]
188
192
  }
189
193
  }
@@ -219,7 +219,7 @@ export async function fetchRelatedSongs(brand, songId) {
219
219
  "published_on": published_on,
220
220
  status,
221
221
  "image": thumbnail.asset->url,
222
- "permission_id": permission[]->railcontent_id,
222
+ "permission_id": permission_v2,
223
223
  "fields": [
224
224
  {
225
225
  "key": "title",
@@ -245,7 +245,7 @@ export async function fetchRelatedSongs(brand, songId) {
245
245
  "id": railcontent_id,
246
246
  "url": web_url_path,
247
247
  "published_on": published_on,
248
- "permission_id": permission[]->railcontent_id,
248
+ "permission_id": permission_v2,
249
249
  status,
250
250
  "fields": [
251
251
  {
@@ -306,7 +306,7 @@ export async function fetchNewReleases(
306
306
  published_on,
307
307
  "type": _type,
308
308
  web_url_path,
309
- "permission_id": permission[]->railcontent_id,
309
+ "permission_id": permission_v2,
310
310
  `
311
311
  const query = buildRawQuery(filter, fields, { sortOrder: sortOrder, start, end: end })
312
312
  return fetchSanity(query, true)
@@ -343,7 +343,7 @@ export async function fetchUpcomingEvents(brand, { page = 1, limit = 10 } = {})
343
343
  published_on,
344
344
  "type": _type,
345
345
  web_url_path,
346
- "permission_id": permission[]->railcontent_id,
346
+ "permission_id": permission_v2,
347
347
  live_event_start_time,
348
348
  live_event_end_time,
349
349
  "isLive": live_event_start_time <= '${now}' && live_event_end_time >= '${now}'`
@@ -395,7 +395,7 @@ export async function fetchScheduledReleases(brand, { page = 1, limit = 10 }) {
395
395
  published_on,
396
396
  "type": _type,
397
397
  web_url_path,
398
- "permission_id": permission[]->railcontent_id,
398
+ "permission_id": permission_v2,
399
399
  } | order(published_on asc)[${start}...${end}]`
400
400
  return fetchSanity(query, true)
401
401
  }
@@ -911,7 +911,7 @@ export async function fetchMethod(brand, slug) {
911
911
  "url": *[railcontent_id == ^.id][0].web_url_path
912
912
  } | order(length(url)),
913
913
  "type": _type,
914
- "permission_id": permission[]->railcontent_id,
914
+ "permission_id": permission_v2,
915
915
  "levels": child[${childrenFilter}]->
916
916
  {
917
917
  "id": railcontent_id,
@@ -1128,7 +1128,7 @@ export async function fetchLessonContent(railContentId, { addParent = false } =
1128
1128
  mp3_no_drums_yes_click_url,
1129
1129
  mp3_yes_drums_no_click_url,
1130
1130
  mp3_yes_drums_yes_click_url,
1131
- "permission_id": permission[]->railcontent_id,
1131
+ "permission_id": permission_v2,
1132
1132
  ${parentQuery}
1133
1133
  ...select(
1134
1134
  defined(live_event_start_time) => {
@@ -1266,7 +1266,7 @@ export async function fetchSiblingContent(railContentId, brand = null) {
1266
1266
  }).buildFilter()
1267
1267
 
1268
1268
  const brandString = brand ? ` && brand == "${brand}"` : ''
1269
- const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, artist->, "permission_id": permission[]->railcontent_id, "genre": genre[]->name, "parent_id": parent_content_data[0].id`
1269
+ const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, artist->, "permission_id": permission_v2, "genre": genre[]->name, "parent_id": parent_content_data[0].id`
1270
1270
 
1271
1271
  const query = `*[railcontent_id == ${railContentId}${brandString}]{
1272
1272
  _type, parent_type, 'parent_id': parent_content_data[0].id, railcontent_id,
@@ -1313,7 +1313,7 @@ export async function fetchRelatedLessons(railContentId) {
1313
1313
  { showMembershipRestrictedContent: true }
1314
1314
  ).buildFilter()
1315
1315
 
1316
- const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type, "genre": genre[]->name`
1316
+ const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, railcontent_id, artist->,"permission_id": permission_v2,_type, "genre": genre[]->name`
1317
1317
 
1318
1318
  const query = `*[railcontent_id == ${railContentId} && (!defined(permission) || references(*[_type=='permission']._id))]{
1319
1319
  _type, parent_type, railcontent_id,
@@ -2134,7 +2134,7 @@ export async function fetchScheduledAndNewReleases(
2134
2134
  published_on,
2135
2135
  "type": _type,
2136
2136
  show_in_new_feed,
2137
- "permission_id": permission[]->railcontent_id,
2137
+ "permission_id": permission_v2,
2138
2138
  "isLive": live_event_start_time <= '${now}' && live_event_end_time >= '${now}',
2139
2139
  }`
2140
2140
 
@@ -23,6 +23,13 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
23
23
  )
24
24
  }
25
25
 
26
+ async completedByContentIds(contentIds: number[]) {
27
+ return this.queryAll(
28
+ Q.where('content_id', Q.oneOf(contentIds)),
29
+ Q.where('state', STATE.COMPLETED)
30
+ )
31
+ }
32
+
26
33
  // null collection only
27
34
  async startedOrCompleted(opts: Parameters<typeof this.startedOrCompletedClauses>[0] = {}) {
28
35
  return this.queryAll(...this.startedOrCompletedClauses(opts))
@@ -2,7 +2,7 @@
2
2
  * @module Onboarding
3
3
  */
4
4
  import { HttpClient } from '../../infrastructure/http/HttpClient'
5
- import { Brand } from '../../lib/brands'
5
+ import { Brands } from '../../lib/brands'
6
6
  import { globalConfig } from '../config.js'
7
7
 
8
8
  export interface OnboardingSteps {
@@ -224,13 +224,13 @@ const recommendedContentCache: { [brand: string]: OnboardingRecommendedContent }
224
224
  * Fetches recommended content for onboarding based on the specified brand.
225
225
  *
226
226
  * @param {string} email - The user's email address.
227
- * @param {Brand} brand - The brand identifier.
227
+ * @param {Brands} brand - The brand identifier.
228
228
  * @returns {Promise<OnboardingRecommendedContent>} - A promise that resolves with the recommended content.
229
229
  * @throws {HttpError} - If the HTTP request fails.
230
230
  */
231
231
  export async function getOnboardingRecommendedContent(
232
232
  email: string,
233
- brand: Brand
233
+ brand: Brands
234
234
  ): Promise<OnboardingRecommendedContent> {
235
235
  // TODO: Replace with real API call when available
236
236
  if (recommendedContentCache[brand]) {