musora-content-services 2.3.25 → 2.4.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.
Files changed (129) hide show
  1. package/.coderabbit.yaml +0 -0
  2. package/.editorconfig +0 -0
  3. package/.github/pull_request_template.md +0 -0
  4. package/.github/workflows/node.js.yml +0 -0
  5. package/.prettierignore +0 -0
  6. package/.prettierrc +0 -0
  7. package/.yarnrc.yml +1 -0
  8. package/CHANGELOG.md +9 -0
  9. package/README.md +0 -0
  10. package/babel.config.cjs +0 -0
  11. package/docs/Content-Organization.html +0 -0
  12. package/docs/ContentOrganization.html +1 -1
  13. package/docs/Gamification.html +1 -1
  14. package/docs/UserManagement.html +0 -0
  15. package/docs/UserManagementSystem.html +1 -1
  16. package/docs/api_types.js.html +1 -1
  17. package/docs/config.js.html +1 -1
  18. package/docs/content-org_content-org.js.html +1 -1
  19. package/docs/content-org_playlists-types.js.html +1 -1
  20. package/docs/content-org_playlists.js.html +1 -1
  21. package/docs/content.js.html +25 -10
  22. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  23. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  24. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  25. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  26. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  27. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  28. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  29. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  30. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  31. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
  32. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  33. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  34. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  35. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  36. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
  37. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  38. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  39. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  40. package/docs/gamification_awards.js.html +1 -1
  41. package/docs/gamification_gamification.js.html +1 -1
  42. package/docs/gamification_types.js.html +1 -1
  43. package/docs/global.html +1 -1
  44. package/docs/global.html#User +0 -0
  45. package/docs/index.html +1 -1
  46. package/docs/module-Awards.html +1 -1
  47. package/docs/module-Config.html +1 -1
  48. package/docs/module-Content-Services-V2.html +9 -8
  49. package/docs/module-Content-Services.html +763 -0
  50. package/docs/module-Permissions.html +1 -1
  51. package/docs/module-Playlists.html +1 -1
  52. package/docs/module-Railcontent-Services.html +1 -1
  53. package/docs/module-Sanity-Services.html +32 -32
  54. package/docs/module-Session-Management.html +0 -0
  55. package/docs/module-Sessions.html +1 -1
  56. package/docs/module-User-Activity.html +7 -7
  57. package/docs/module-User-Management.html +0 -0
  58. package/docs/module-User-Permissions.html +0 -0
  59. package/docs/module-UserManagement.html +1 -1
  60. package/docs/railcontent.js.html +1 -1
  61. package/docs/sanity.js.html +5 -3
  62. package/docs/scripts/collapse.js +0 -0
  63. package/docs/scripts/commonNav.js +0 -0
  64. package/docs/scripts/linenumber.js +0 -0
  65. package/docs/scripts/nav.js +0 -0
  66. package/docs/scripts/polyfill.js +0 -0
  67. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
  68. package/docs/scripts/prettify/lang-css.js +0 -0
  69. package/docs/scripts/prettify/prettify.js +0 -0
  70. package/docs/scripts/search.js +0 -0
  71. package/docs/styles/jsdoc.css +0 -0
  72. package/docs/styles/prettify.css +0 -0
  73. package/docs/types.js.html +0 -0
  74. package/docs/userActivity.js.html +2 -4
  75. package/docs/user_management.js.html +1 -1
  76. package/docs/user_permissions.js.html +1 -1
  77. package/docs/user_sessions.js.html +1 -1
  78. package/docs/user_types.js.html +1 -1
  79. package/docs/user_user-management-system.js.html +1 -1
  80. package/docs/user_user-management.js.html +0 -0
  81. package/jest.config.js +0 -0
  82. package/jsdoc.json +0 -0
  83. package/link_mcs.sh +0 -0
  84. package/package.json +1 -1
  85. package/src/contentMetaData.js +29 -5
  86. package/src/contentTypeConfig.js +3 -2
  87. package/src/index.d.ts +2 -0
  88. package/src/index.js +2 -0
  89. package/src/infrastructure/http/HttpClient.ts +120 -0
  90. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +45 -0
  91. package/src/infrastructure/http/index.ts +13 -0
  92. package/src/infrastructure/http/interfaces/HeaderProvider.ts +3 -0
  93. package/src/infrastructure/http/interfaces/HttpError.ts +7 -0
  94. package/src/infrastructure/http/interfaces/NetworkError.ts +6 -0
  95. package/src/infrastructure/http/interfaces/RequestExecutor.ts +5 -0
  96. package/src/infrastructure/http/interfaces/RequestOptions.ts +5 -0
  97. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +24 -0
  98. package/src/lib/lastUpdated.js +0 -0
  99. package/src/services/config.js +0 -0
  100. package/src/services/content-org/playlists-types.js +0 -0
  101. package/src/services/content-org/playlists.js +1 -1
  102. package/src/services/content.js +24 -9
  103. package/src/services/contentAggregator.js +18 -10
  104. package/src/services/imageSRCBuilder.js +0 -0
  105. package/src/services/railcontent.js +28 -0
  106. package/src/services/recommendations.js +21 -25
  107. package/src/services/user/permissions.js +0 -0
  108. package/src/services/userActivity.js +23 -0
  109. package/test/HttpClient.test.js +257 -0
  110. package/test/content.test.js +0 -0
  111. package/test/contentLikes.test.js +0 -0
  112. package/test/contentProgress.test.js +0 -0
  113. package/test/dataContext.test.js +0 -0
  114. package/test/forum.test.js +0 -0
  115. package/test/imageSRCBuilder.test.js +0 -0
  116. package/test/imageSRCVerify.test.js +0 -0
  117. package/test/initializeTests.js +0 -0
  118. package/test/lib/lastUpdated.test.js +0 -0
  119. package/test/live/contentProgressLive.test.js +0 -0
  120. package/test/live/railcontentLive.test.js +0 -0
  121. package/test/localStorageMock.js +0 -0
  122. package/test/log.js +0 -0
  123. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  124. package/test/mockData/mockData_user_practices.json +0 -0
  125. package/test/sanityQueryService.test.js +0 -0
  126. package/test/streakMessage.test.js +0 -0
  127. package/test/user/permissions.test.js +0 -0
  128. package/test/userActivity.test.js +0 -0
  129. package/tools/generate-index.cjs +0 -0
@@ -310,7 +310,7 @@ const contentMetadata = {
310
310
  sortBy: 'sort',
311
311
  },
312
312
  'play-along': {
313
- name: 'Play Alongs',
313
+ name: 'Play-Alongs',
314
314
  icon: 'icon-play-alongs',
315
315
  description:
316
316
  'Add your drumming to high-quality drumless play-along tracks - with handy playback tools to help you create the perfect performance.',
@@ -354,7 +354,7 @@ const contentMetadata = {
354
354
  'Blues', 'Christian', 'Classical', 'Country', 'Disco', 'Electronic', 'Folk', 'Funk', 'Hip-Hop/Rap', 'Holiday', 'Jazz', 'Soundtrack',
355
355
  'World', 'Metal', 'Pop', 'R&B/Soul', 'Rock'
356
356
  ],
357
- type: ['Tutorials', 'Transcriptions', 'Play Alongs', 'Jam Tracks'],
357
+ type: ['Tutorials', 'Transcriptions', 'Play-Alongs', 'Jam Tracks'],
358
358
  progress: PROGRESS_NAMES,
359
359
  },
360
360
  sortingOptions: {
@@ -395,7 +395,7 @@ const contentMetadata = {
395
395
  'Blues', 'Christian', 'Classical', 'Country', 'Disco', 'Electronic', 'Folk', 'Funk', 'Hip-Hop/Rap', 'Holiday', 'Jazz', 'Soundtrack',
396
396
  'World', 'Metal', 'Pop', 'R&B/Soul', 'Rock'
397
397
  ],
398
- type: ['Tutorials', 'Sheet Music', 'Play Alongs', 'Jam Tracks'],
398
+ type: ['Tutorials', 'Sheet Music', 'Play-Alongs', 'Jam Tracks'],
399
399
  progress: PROGRESS_NAMES,
400
400
  },
401
401
  sortingOptions: {
@@ -411,6 +411,14 @@ const contentMetadata = {
411
411
  Tabs.ExploreAll
412
412
  ],
413
413
  },
414
+ 'recent': {
415
+ name: 'Recent Lessons',
416
+ tabs: [
417
+ Tabs.RecentAll,
418
+ Tabs.RecentIncomplete,
419
+ Tabs.RecentCompleted
420
+ ],
421
+ },
414
422
  },
415
423
  guitareo: {
416
424
  instructor: {
@@ -438,7 +446,7 @@ const contentMetadata = {
438
446
  'Blues', 'Christian', 'Classical', 'Country', 'Disco', 'Electronic', 'Folk', 'Funk', 'Hip-Hop/Rap', 'Holiday', 'Jazz', 'Soundtrack',
439
447
  'World', 'Metal', 'Pop', 'R&B/Soul', 'Rock'
440
448
  ],
441
- type: ['Tutorials', 'Tabs', 'Play Alongs', 'Jam Tracks'],
449
+ type: ['Tutorials', 'Tabs', 'Play-Alongs', 'Jam Tracks'],
442
450
  progress: PROGRESS_NAMES,
443
451
  },
444
452
  sortingOptions: {
@@ -454,6 +462,14 @@ const contentMetadata = {
454
462
  Tabs.ExploreAll
455
463
  ],
456
464
  },
465
+ 'recent': {
466
+ name: 'Recent Lessons',
467
+ tabs: [
468
+ Tabs.RecentAll,
469
+ Tabs.RecentIncomplete,
470
+ Tabs.RecentCompleted
471
+ ],
472
+ },
457
473
  },
458
474
  singeo: {
459
475
  'student-review': {
@@ -470,7 +486,7 @@ const contentMetadata = {
470
486
  'Blues', 'Christian', 'Classical', 'Country', 'Disco', 'Electronic', 'Folk', 'Funk', 'Hip-Hop/Rap', 'Holiday', 'Jazz', 'Soundtrack',
471
487
  'World', 'Metal', 'Pop', 'R&B/Soul', 'Rock'
472
488
  ],
473
- type: ['Tutorials', 'Sheet Music', 'Play Alongs', 'Jam Tracks'],
489
+ type: ['Tutorials', 'Sheet Music', 'Play-Alongs', 'Jam Tracks'],
474
490
  progress: PROGRESS_NAMES,
475
491
  },
476
492
  sortingOptions: {
@@ -486,6 +502,14 @@ const contentMetadata = {
486
502
  Tabs.ExploreAll
487
503
  ],
488
504
  },
505
+ 'recent': {
506
+ name: 'Recent Lessons',
507
+ tabs: [
508
+ Tabs.RecentAll,
509
+ Tabs.RecentIncomplete,
510
+ Tabs.RecentCompleted
511
+ ],
512
+ },
489
513
  }
490
514
  }
491
515
 
@@ -176,13 +176,14 @@ export const lessonTypesMapping = {
176
176
  'transcriptions': transcriptionsLessonTypes,
177
177
  'tabs': transcriptionsLessonTypes,
178
178
  'sheet music': transcriptionsLessonTypes,
179
- 'play alongs': playAlongLessonTypes,
179
+ 'play-alongs': playAlongLessonTypes,
180
+ 'jam tracks': ['jam-track'],
180
181
  };
181
182
 
182
183
 
183
184
  export const filterTypes = {
184
185
  lessons: [...individualLessonsTypes, ...collectionLessonTypes],
185
- songs: [...tutorialsLessonTypes, ...transcriptionsLessonTypes, ...playAlongLessonTypes]
186
+ songs: [...tutorialsLessonTypes, ...transcriptionsLessonTypes, ...playAlongLessonTypes, 'jam-track'],
186
187
  }
187
188
 
188
189
  export const recentTypes = {
package/src/index.d.ts CHANGED
@@ -111,6 +111,7 @@ import {
111
111
  fetchContentPageUserData,
112
112
  fetchContentProgress,
113
113
  fetchHandler,
114
+ fetchLastInteractedChild,
114
115
  fetchLikeCount,
115
116
  fetchNextContentDataForParent,
116
117
  fetchOwnedChallenges,
@@ -292,6 +293,7 @@ declare module 'musora-content-services' {
292
293
  fetchGenreLessons,
293
294
  fetchHandler,
294
295
  fetchHierarchy,
296
+ fetchLastInteractedChild,
295
297
  fetchLeaving,
296
298
  fetchLessonContent,
297
299
  fetchLessonsFeaturingThisContent,
package/src/index.js CHANGED
@@ -111,6 +111,7 @@ import {
111
111
  fetchContentPageUserData,
112
112
  fetchContentProgress,
113
113
  fetchHandler,
114
+ fetchLastInteractedChild,
114
115
  fetchLikeCount,
115
116
  fetchNextContentDataForParent,
116
117
  fetchOwnedChallenges,
@@ -291,6 +292,7 @@ export {
291
292
  fetchGenreLessons,
292
293
  fetchHandler,
293
294
  fetchHierarchy,
295
+ fetchLastInteractedChild,
294
296
  fetchLeaving,
295
297
  fetchLessonContent,
296
298
  fetchLessonsFeaturingThisContent,
@@ -0,0 +1,120 @@
1
+ import { HeaderProvider } from './interfaces/HeaderProvider'
2
+ import { RequestExecutor } from './interfaces/RequestExecutor'
3
+ import { RequestOptions } from './interfaces/RequestOptions'
4
+ import { HttpError } from './interfaces/HttpError'
5
+ import { NetworkError } from './interfaces/NetworkError'
6
+ import { DefaultHeaderProvider } from './providers/DefaultHeaderProvider'
7
+ import { FetchRequestExecutor } from './executors/FetchRequestExecutor'
8
+
9
+ export class HttpClient {
10
+ private baseUrl: string
11
+ private token: string | null
12
+ private headerProvider: HeaderProvider
13
+ private requestExecutor: RequestExecutor
14
+
15
+ constructor(
16
+ baseUrl: string,
17
+ token: string | null = null,
18
+ headerProvider: HeaderProvider = new DefaultHeaderProvider(),
19
+ requestExecutor: RequestExecutor = new FetchRequestExecutor()
20
+ ) {
21
+ this.baseUrl = baseUrl
22
+ this.token = token
23
+ this.headerProvider = headerProvider
24
+ this.requestExecutor = requestExecutor
25
+ }
26
+
27
+ public setToken(token: string): void {
28
+ this.token = token
29
+ }
30
+
31
+ public async get<T>(url: string, dataVersion: string | null = null): Promise<T> {
32
+ return this.request<T>(url, 'get', dataVersion)
33
+ }
34
+
35
+ public async post<T>(url: string, data: any, dataVersion: string | null = null): Promise<T> {
36
+ return this.request<T>(url, 'post', dataVersion, data)
37
+ }
38
+
39
+ public async put<T>(url: string, data: any, dataVersion: string | null = null): Promise<T> {
40
+ return this.request<T>(url, 'put', dataVersion, data)
41
+ }
42
+
43
+ public async patch<T>(url: string, data: any, dataVersion: string | null = null): Promise<T> {
44
+ return this.request<T>(url, 'patch', dataVersion, data)
45
+ }
46
+
47
+ public async delete<T>(url: string, dataVersion: string | null = null): Promise<T> {
48
+ return this.request<T>(url, 'delete', dataVersion)
49
+ }
50
+
51
+ private async request<T>(
52
+ url: string,
53
+ method: string,
54
+ dataVersion: string | null = null,
55
+ body: any = null
56
+ ): Promise<T> {
57
+ try {
58
+ const headers = this.buildHeaders(dataVersion)
59
+ const options = this.buildRequestOptions(method, headers, body)
60
+ const fullUrl = this.resolveUrl(url)
61
+
62
+ return await this.requestExecutor.execute<T>(fullUrl, options)
63
+ } catch (error: any) {
64
+ return this.handleError(error, url, method)
65
+ }
66
+ }
67
+
68
+ private buildHeaders(dataVersion: string | null): Record<string, string> {
69
+ const headers = this.headerProvider.getHeaders()
70
+
71
+ // Add data version if provided
72
+ if (dataVersion) {
73
+ headers['Data-Version'] = dataVersion
74
+ }
75
+
76
+ // Add auth token if available
77
+ if (this.token) {
78
+ headers['Authorization'] = `Bearer ${this.token}`
79
+ }
80
+
81
+ return headers
82
+ }
83
+
84
+ private buildRequestOptions(
85
+ method: string,
86
+ headers: Record<string, string>,
87
+ body: any
88
+ ): RequestOptions {
89
+ const options: RequestOptions = {
90
+ method,
91
+ headers,
92
+ }
93
+
94
+ // Add body for non-GET requests
95
+ if (body) {
96
+ options.body = JSON.stringify(body)
97
+ }
98
+
99
+ return options
100
+ }
101
+
102
+ private resolveUrl(url: string): string {
103
+ return url.startsWith('/') ? this.baseUrl + url : url
104
+ }
105
+
106
+ private handleError(error: any, url: string, method: string): never {
107
+ if ('status' in error) {
108
+ // This is our formatted HTTP error from above
109
+ throw error as HttpError
110
+ }
111
+
112
+ // Network or other errors
113
+ throw {
114
+ message: error.message || 'Network error',
115
+ url,
116
+ method,
117
+ originalError: error,
118
+ } as NetworkError
119
+ }
120
+ }
@@ -0,0 +1,45 @@
1
+ import { RequestExecutor } from '../interfaces/RequestExecutor'
2
+ import { RequestOptions } from '../interfaces/RequestOptions'
3
+ import { HttpError } from '../interfaces/HttpError'
4
+
5
+ export class FetchRequestExecutor implements RequestExecutor {
6
+ async execute<T>(url: string, options: RequestOptions): Promise<T> {
7
+ const response = await fetch(url, options)
8
+
9
+ if (!response.ok) {
10
+ throw await this.createHttpError(response, url, options.method)
11
+ }
12
+
13
+ return this.parseResponse<T>(response)
14
+ }
15
+
16
+ private async createHttpError(
17
+ response: Response,
18
+ url: string,
19
+ method: string
20
+ ): Promise<HttpError> {
21
+ const error: HttpError = {
22
+ status: response.status,
23
+ statusText: response.statusText,
24
+ url,
25
+ method,
26
+ }
27
+
28
+ try {
29
+ error.body = await response.json()
30
+ } catch (e) {
31
+ error.body = await response.text()
32
+ }
33
+
34
+ return error
35
+ }
36
+
37
+ private async parseResponse<T>(response: Response): Promise<T> {
38
+ const contentType = response.headers.get('content-type')
39
+ if (contentType && contentType.indexOf('application/json') !== -1) {
40
+ return (await response.json()) as T
41
+ } else {
42
+ return (await response.text()) as unknown as T
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,13 @@
1
+ // Interfaces
2
+ export { RequestOptions } from './interfaces/RequestOptions'
3
+ export { HttpError } from './interfaces/HttpError'
4
+ export { NetworkError } from './interfaces/NetworkError'
5
+ export { HeaderProvider } from './interfaces/HeaderProvider'
6
+ export { RequestExecutor } from './interfaces/RequestExecutor'
7
+
8
+ // Implementations
9
+ export { DefaultHeaderProvider } from './providers/DefaultHeaderProvider'
10
+ export { FetchRequestExecutor } from './executors/FetchRequestExecutor'
11
+
12
+ // Main client
13
+ export { HttpClient } from './HttpClient'
@@ -0,0 +1,3 @@
1
+ export interface HeaderProvider {
2
+ getHeaders(): Record<string, string>
3
+ }
@@ -0,0 +1,7 @@
1
+ export interface HttpError {
2
+ status: number
3
+ statusText: string
4
+ url: string
5
+ method: string
6
+ body?: any
7
+ }
@@ -0,0 +1,6 @@
1
+ export interface NetworkError {
2
+ message: string
3
+ url: string
4
+ method: string
5
+ originalError: Error
6
+ }
@@ -0,0 +1,5 @@
1
+ import { RequestOptions } from './RequestOptions'
2
+
3
+ export interface RequestExecutor {
4
+ execute<T>(url: string, options: RequestOptions): Promise<T>
5
+ }
@@ -0,0 +1,5 @@
1
+ export interface RequestOptions {
2
+ method: string
3
+ headers: Record<string, string>
4
+ body?: string
5
+ }
@@ -0,0 +1,24 @@
1
+ import { HeaderProvider } from '../interfaces/HeaderProvider'
2
+ import { globalConfig } from '../../../services/config.js'
3
+
4
+ export class DefaultHeaderProvider implements HeaderProvider {
5
+ getHeaders(): Record<string, string> {
6
+ const headers: Record<string, string> = {
7
+ 'Content-Type': 'application/json',
8
+ Accept: 'application/json',
9
+ }
10
+
11
+ // Handle timezone
12
+ if (globalConfig.localTimezoneString) {
13
+ headers['M-Client-Timezone'] = globalConfig.localTimezoneString
14
+ }
15
+
16
+ // Add CSRF token if present in document
17
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
18
+ if (csrfToken) {
19
+ headers['X-CSRF-TOKEN'] = csrfToken
20
+ }
21
+
22
+ return headers
23
+ }
24
+ }
File without changes
File without changes
File without changes
@@ -232,7 +232,7 @@ export async function updatePlaylist(playlistId, {
232
232
  ...item_order && { item_order },
233
233
  }
234
234
  const url = `${BASE_PATH}/v1/user/playlists/${playlistId}`
235
- return await fetchHandler(url, 'POST', null, data);
235
+ return await fetchHandler(url, 'PUT', null, data);
236
236
  }
237
237
 
238
238
  /**
@@ -21,23 +21,18 @@ import {recommendations} from "./recommendations";
21
21
 
22
22
  export async function getLessonContentRows (brand='drumeo', pageName = 'lessons') {
23
23
  let recentContentIds = await fetchRecent(brand, pageName, { progress: 'recent' });
24
- recentContentIds = recentContentIds.map(item => item.id);
25
24
 
26
25
  let contentRows = await getContentRows(brand, pageName);
27
26
  contentRows = Array.isArray(contentRows) ? contentRows : [];
28
27
  contentRows.unshift({
29
28
  id: 'recent',
30
29
  title: 'Recent ' + capitalizeFirstLetter(pageName),
31
- content: recentContentIds || []
30
+ items: recentContentIds || []
32
31
  });
33
32
 
34
33
  const results = await Promise.all(
35
34
  contentRows.map(async (row) => {
36
- if (row.content.length == 0){
37
- return { id: row.id, title: row.title, items: [] }
38
- }
39
- const data = await fetchByRailContentIds(row.content)
40
- return { id: row.id, title: row.title, items: data }
35
+ return { id: row.id, title: row.title, items: row.items }
41
36
  })
42
37
  )
43
38
  return results
@@ -163,7 +158,8 @@ export async function getRecent(brand, pageName, tabName = 'all', {
163
158
  * @param {Object} params - Parameters for pagination.
164
159
  * @param {number} [params.page=1] - The page number for pagination.
165
160
  * @param {number} [params.limit=10] - The maximum number of content items per row.
166
- * @returns {Promise<Object>} - The fetched content rows.
161
+ * @returns {Promise<Object>} - The fetched content rows with complete Sanity data instead of just content IDs.
162
+ * When contentRowId is provided, returns an object with type, data, and meta properties.
167
163
  *
168
164
  * @example
169
165
  * getContentRows('drumeo', 'lessons', 'Your-Daily-Warmup', {
@@ -179,7 +175,26 @@ export async function getContentRows(brand, pageName, contentRowId , {
179
175
  } = {}) {
180
176
  const contentRow = contentRowId ? `&content_row_id=${contentRowId}` : ''
181
177
  const url = `/api/content/v1/rows?brand=${brand}&page_name=${pageName}${contentRow}&page=${page}&limit=${limit}`;
182
- return await fetchHandler(url, 'get', null);
178
+ const contentRows = await fetchHandler(url, 'get', null) || [];
179
+ const results = await Promise.all(
180
+ contentRows.map(async (row) => {
181
+ if (row.content.length === 0){
182
+ return { id: row.id, title: row.title, items: [] }
183
+ }
184
+ const data = await fetchByRailContentIds(row.content)
185
+ return { id: row.id, title: row.title, items: data }
186
+ })
187
+ )
188
+
189
+ if (contentRowId) {
190
+ return {
191
+ type: TabResponseType.CATALOG,
192
+ data: results[0].items,
193
+ meta: {}
194
+ };
195
+ }
196
+
197
+ return results
183
198
  }
184
199
 
185
200
  /**
@@ -1,6 +1,6 @@
1
1
  import { getProgressStateByIds, getProgressPercentageByIds, getResumeTimeSecondsByIds } from "./contentProgress"
2
2
  import { isContentLikedByIds } from "./contentLikes"
3
- import { fetchLikeCount } from "./railcontent"
3
+ import { fetchLikeCount, fetchLastInteractedChild } from "./railcontent"
4
4
 
5
5
 
6
6
 
@@ -15,7 +15,8 @@ export async function addContextToContent(dataPromise, ...dataArgs)
15
15
  addIsLiked = false,
16
16
  addLikeCount = false,
17
17
  addProgressStatus = false,
18
- addResumeTimeSeconds = false
18
+ addResumeTimeSeconds = false,
19
+ addLastInteractedChild = false,
19
20
  } = options
20
21
 
21
22
  const dataParam = lastArg === options ? dataArgs.slice(0, -1) : dataArgs
@@ -35,13 +36,15 @@ export async function addContextToContent(dataPromise, ...dataArgs)
35
36
 
36
37
  if(ids.length === 0) return false
37
38
 
38
- const [progressPercentageData, progressStatusData, isLikedData, resumeTimeData] = await Promise.all([
39
+ const [progressPercentageData, progressStatusData, isLikedData, resumeTimeData, lastInteractedChildData] = await Promise.all([
39
40
  addProgressPercentage ? getProgressPercentageByIds(ids) : Promise.resolve(null),
40
41
  addProgressStatus ? getProgressStateByIds(ids) : Promise.resolve(null),
41
42
  addIsLiked ? isContentLikedByIds(ids) : Promise.resolve(null),
42
43
  addResumeTimeSeconds ? getResumeTimeSecondsByIds(ids) : Promise.resolve(null),
44
+ addLastInteractedChild ? fetchLastInteractedChild(ids) : Promise.resolve(null),
43
45
  ])
44
-
46
+ console.log('ids', ids)
47
+ console.log('lastInteractedChildData', lastInteractedChildData)
45
48
  const addContext = async (item) => ({
46
49
  ...item,
47
50
  ...(addProgressPercentage ? { progressPercentage: progressPercentageData?.[item.id] } : {}),
@@ -49,12 +52,17 @@ export async function addContextToContent(dataPromise, ...dataArgs)
49
52
  ...(addIsLiked ? { isLiked: isLikedData?.[item.id] } : {}),
50
53
  ...(addLikeCount && ids.length === 1 ? { likeCount: await fetchLikeCount(item.id) } : {}),
51
54
  ...(addResumeTimeSeconds ? { resumeTime: resumeTimeData?.[item.id] } : {}),
55
+ ...(addLastInteractedChild ? { lastInteractedChild: lastInteractedChildData?.[item.id] } : {}),
52
56
  })
53
-
54
- const newData = Array.isArray(data)
55
- ? await Promise.all(data.map(addContext))
56
- : await addContext(data)
57
-
58
- return newData
57
+ if (dataField) {
58
+ data[dataField] = Array.isArray(data[dataField])
59
+ ? await Promise.all(data[dataField].map(addContext))
60
+ : await addContext(data[dataField])
61
+ return data
62
+ } else {
63
+ return Array.isArray(data)
64
+ ? await Promise.all(data.map(addContext))
65
+ : await addContext(data)
66
+ }
59
67
  }
60
68
 
File without changes
@@ -821,6 +821,34 @@ export async function fetchUserPracticeNotes(date) {
821
821
  return await fetchHandler(url, 'GET', null)
822
822
  }
823
823
 
824
+
825
+ /**
826
+ * Get the id and slug of last interacted child. Only valid for certain content types
827
+ *
828
+ * @async
829
+ * @function fetchLastInteractedChild
830
+ * @param {array} content_ids - Content ids of to get the last interacted child of
831
+ *
832
+ *
833
+ * @returns {Promise<Object>} - keyed object per valid content ids with the child
834
+ *
835
+ * @example
836
+ * try {
837
+ * const response = await fetchLastInteractedChild([191369, 410427]);
838
+ * console.log('child id', response[191369].content_id)
839
+ * console.log('child slug', response[191369].slug)
840
+ * } catch (error) {
841
+ * console.error('Failed to get children', error);
842
+ * }
843
+ */
844
+ export async function fetchLastInteractedChild(content_ids) {
845
+ const params = new URLSearchParams();
846
+ content_ids.forEach(id => params.append('content_ids[]', id));
847
+ const url = `/api/content/v1/user/last_interacted_child?${params.toString()}`
848
+ return await fetchHandler(url, 'GET', null)
849
+ }
850
+
851
+
824
852
  function fetchAbsolute(url, params) {
825
853
  if (globalConfig.sessionConfig.authToken) {
826
854
  params.headers['Authorization'] = `Bearer ${globalConfig.sessionConfig.authToken}`
@@ -2,8 +2,8 @@
2
2
  * @module Railcontent-Services
3
3
  */
4
4
 
5
- import {globalConfig} from './config.js'
6
- import {fetchJSONHandler} from '../lib/httpHelper.js'
5
+ import { globalConfig } from './config.js'
6
+ import { HttpClient } from '../infrastructure/http/HttpClient'
7
7
 
8
8
  /**
9
9
  * Exported functions that are excluded from index generation.
@@ -36,7 +36,11 @@ export async function fetchSimilarItems(content_id, brand, count = 10) {
36
36
  }
37
37
  const url = `/similar_items/`
38
38
  try {
39
- const response = await fetchHandler(url, 'POST', data)
39
+ const httpClient = new HttpClient(
40
+ globalConfig.recommendationsConfig.baseUrl,
41
+ globalConfig.recommendationsConfig.token
42
+ )
43
+ const response = await httpClient.post(url, data)
40
44
  // we requested count + 1 then filtered out the extra potential value, so we need slice to the correct size if necessary
41
45
  return response['similar_items'].filter((item) => item !== content_id).slice(0, count)
42
46
  } catch (error) {
@@ -71,7 +75,11 @@ export async function rankCategories(brand, categories) {
71
75
  }
72
76
  const url = `/rank_each_list/`
73
77
  try {
74
- const response = await fetchHandler(url, 'POST', data)
78
+ const httpClient = new HttpClient(
79
+ globalConfig.recommendationsConfig.baseUrl,
80
+ globalConfig.recommendationsConfig.token
81
+ )
82
+ const response = await httpClient.post(url, data)
75
83
  let rankedCategories = {}
76
84
  response['ranked_playlists'].forEach(
77
85
  (category) =>
@@ -106,7 +114,11 @@ export async function rankItems(brand, content_ids) {
106
114
  }
107
115
  const url = `/rank_items/`
108
116
  try {
109
- const response = await fetchHandler(url, 'POST', data)
117
+ const httpClient = new HttpClient(
118
+ globalConfig.recommendationsConfig.baseUrl,
119
+ globalConfig.recommendationsConfig.token
120
+ )
121
+ const response = await httpClient.post(url, data)
110
122
  return response['ranked_content_ids']
111
123
  } catch (error) {
112
124
  console.error('Fetch error:', error)
@@ -114,31 +126,15 @@ export async function rankItems(brand, content_ids) {
114
126
  }
115
127
  }
116
128
 
117
- export async function recommendations(brand, {section = ''} = {}) {
129
+ export async function recommendations(brand, { section = '' } = {}) {
118
130
  section = section.toUpperCase().replace('-', '_')
119
- const sectionString = section ? `&section=${section}` : '';
131
+ const sectionString = section ? `&section=${section}` : ''
120
132
  const url = `/api/content/v1/recommendations?brand=${brand}${sectionString}`
121
133
  try {
122
- // This goes through the MPB, not the recommendations api, so we use fetchJSONHandler instead of the local handler
123
- return fetchJSONHandler(
124
- url,
125
- globalConfig.sessionConfig.token,
126
- globalConfig.baseUrl,
127
- 'get'
128
- )
134
+ const httpClient = new HttpClient(globalConfig.baseUrl, globalConfig.sessionConfig.token)
135
+ return httpClient.get(url)
129
136
  } catch (error) {
130
137
  console.error('Fetch error:', error)
131
138
  return null
132
139
  }
133
140
  }
134
-
135
- async function fetchHandler(url, method = 'get', body = null) {
136
- return fetchJSONHandler(
137
- url,
138
- globalConfig.recommendationsConfig.token,
139
- globalConfig.recommendationsConfig.baseUrl,
140
- method,
141
- null,
142
- body
143
- )
144
- }
File without changes