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.
- package/.github/workflows/docs.js.yml +57 -45
- package/CHANGELOG.md +20 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +2 -2
- package/src/lib/brands.ts +7 -7
- package/src/services/content/artist.ts +10 -7
- package/src/services/content/genre.ts +10 -7
- package/src/services/content/instructor.ts +7 -7
- package/src/services/content-org/learning-paths.ts +12 -22
- package/src/services/contentProgress.js +2 -12
- package/src/services/permissions/PermissionsV2Adapter.ts +1 -3
- package/src/services/progress-row/method-card.js +8 -13
- package/src/services/reporting/reporting.ts +19 -15
- package/src/services/sanity.js +10 -10
- package/src/services/sync/repositories/content-progress.ts +7 -0
- package/src/services/user/onboarding.ts +3 -3
|
@@ -1,61 +1,73 @@
|
|
|
1
|
-
name:
|
|
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
|
-
|
|
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:
|
|
35
|
+
path: v2-content
|
|
15
36
|
|
|
16
|
-
- name:
|
|
17
|
-
uses: actions/
|
|
37
|
+
- name: Setup Node.js
|
|
38
|
+
uses: actions/setup-node@v4
|
|
18
39
|
with:
|
|
19
|
-
|
|
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:
|
|
24
|
-
|
|
25
|
-
|
|
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:
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
package/src/contentTypeConfig.js
CHANGED
|
@@ -61,7 +61,7 @@ export const DEFAULT_FIELDS = [
|
|
|
61
61
|
`'genre': ${genreField}`,
|
|
62
62
|
'status',
|
|
63
63
|
"'slug' : slug.current",
|
|
64
|
-
"'permission_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':
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 {
|
|
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 {
|
|
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:
|
|
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 {
|
|
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(
|
|
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 {
|
|
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:
|
|
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 {
|
|
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:
|
|
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 {
|
|
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(
|
|
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 {
|
|
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:
|
|
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 {
|
|
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 {
|
|
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:
|
|
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 {
|
|
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?:
|
|
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:
|
|
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
|
-
* @
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
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 {
|
|
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} • ${
|
|
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
|
-
|
|
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:
|
|
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>(
|
|
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(
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
}
|
package/src/services/sanity.js
CHANGED
|
@@ -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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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 {
|
|
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 {
|
|
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:
|
|
233
|
+
brand: Brands
|
|
234
234
|
): Promise<OnboardingRecommendedContent> {
|
|
235
235
|
// TODO: Replace with real API call when available
|
|
236
236
|
if (recommendedContentCache[brand]) {
|