musora-content-services 2.104.9 → 2.105.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 (184) hide show
  1. package/.claude/settings.local.json +12 -3
  2. package/.coderabbit.yaml +0 -0
  3. package/.editorconfig +0 -0
  4. package/.github/pull_request_template.md +0 -0
  5. package/.github/workflows/conventional-commits.yaml +0 -0
  6. package/.github/workflows/docs.js.yml +0 -0
  7. package/.github/workflows/node.js.yml +0 -0
  8. package/.prettierignore +0 -0
  9. package/.prettierrc +0 -0
  10. package/CHANGELOG.md +13 -0
  11. package/CLAUDE.md +0 -0
  12. package/README.md +0 -0
  13. package/babel.config.cjs +0 -0
  14. package/jest.config.js +0 -0
  15. package/jsdoc.json +0 -0
  16. package/package.json +1 -1
  17. package/src/constants/award-assets.js +0 -0
  18. package/src/contentMetaData.js +0 -0
  19. package/src/contentTypeConfig.js +0 -8
  20. package/src/infrastructure/http/HttpClient.ts +0 -0
  21. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
  22. package/src/infrastructure/http/index.ts +0 -0
  23. package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
  24. package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
  25. package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
  26. package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
  27. package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
  28. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
  29. package/src/lib/brands.ts +0 -0
  30. package/src/lib/lastUpdated.js +0 -0
  31. package/src/services/api/types.js +0 -0
  32. package/src/services/api/types.ts +0 -0
  33. package/src/services/awards/award-callbacks.js +2 -0
  34. package/src/services/awards/award-query.js +8 -14
  35. package/src/services/awards/internal/.indexignore +0 -0
  36. package/src/services/awards/internal/award-definitions.js +5 -2
  37. package/src/services/awards/internal/award-events.js +0 -0
  38. package/src/services/awards/internal/award-manager.js +1 -1
  39. package/src/services/awards/internal/certificate-builder.js +1 -4
  40. package/src/services/awards/internal/completion-data-generator.js +0 -0
  41. package/src/services/awards/internal/content-progress-observer.js +0 -0
  42. package/src/services/awards/internal/image-utils.js +0 -0
  43. package/src/services/awards/internal/message-generator.js +0 -0
  44. package/src/services/awards/internal/types.js +0 -0
  45. package/src/services/awards/types.d.ts +1 -0
  46. package/src/services/awards/types.js +5 -2
  47. package/src/services/config.js +0 -0
  48. package/src/services/content/content.ts +0 -0
  49. package/src/services/content-org/content-org.js +0 -0
  50. package/src/services/content-org/guided-courses.ts +0 -0
  51. package/src/services/content-org/playlists-types.js +0 -0
  52. package/src/services/content-org/playlists.js +0 -0
  53. package/src/services/contentLikes.js +0 -0
  54. package/src/services/contentProgress.js +39 -31
  55. package/src/services/dataContext.js +0 -0
  56. package/src/services/dateUtils.js +0 -0
  57. package/src/services/eventsAPI.js +0 -0
  58. package/src/services/forums/categories.ts +0 -0
  59. package/src/services/forums/posts.ts +0 -0
  60. package/src/services/forums/types.ts +0 -0
  61. package/src/services/gamification/awards.ts +0 -0
  62. package/src/services/gamification/gamification.js +0 -0
  63. package/src/services/imageSRCBuilder.js +0 -0
  64. package/src/services/imageSRCVerify.js +0 -0
  65. package/src/services/liveTesting.ts +0 -0
  66. package/src/services/permissions/PermissionsAdapter.ts +0 -0
  67. package/src/services/permissions/PermissionsAdapterFactory.ts +0 -0
  68. package/src/services/permissions/PermissionsV1Adapter.ts +0 -0
  69. package/src/services/permissions/README.md +0 -0
  70. package/src/services/permissions/index.ts +0 -0
  71. package/src/services/progress-row/method-card.js +1 -1
  72. package/src/services/recommendations.js +0 -0
  73. package/src/services/reporting/README.md +0 -0
  74. package/src/services/reporting/reporting.ts +0 -0
  75. package/src/services/reporting/types.ts +0 -0
  76. package/src/services/sanity.js +23 -0
  77. package/src/services/sentry/.indexignore +0 -0
  78. package/src/services/sentry/index.ts +0 -0
  79. package/src/services/sync/.indexignore +0 -0
  80. package/src/services/sync/adapters/factory.ts +0 -0
  81. package/src/services/sync/adapters/lokijs.ts +0 -0
  82. package/src/services/sync/adapters/sqlite.ts +0 -0
  83. package/src/services/sync/concurrency-safety.ts +0 -0
  84. package/src/services/sync/context/index.ts +0 -0
  85. package/src/services/sync/context/providers/base.ts +0 -0
  86. package/src/services/sync/context/providers/connectivity.ts +0 -0
  87. package/src/services/sync/context/providers/durability.ts +0 -0
  88. package/src/services/sync/context/providers/index.ts +0 -0
  89. package/src/services/sync/context/providers/session.ts +0 -0
  90. package/src/services/sync/context/providers/tabs.ts +0 -0
  91. package/src/services/sync/context/providers/visibility.ts +0 -0
  92. package/src/services/sync/database/factory.ts +0 -0
  93. package/src/services/sync/errors/boundary.ts +0 -0
  94. package/src/services/sync/errors/index.ts +0 -0
  95. package/src/services/sync/index.ts +0 -0
  96. package/src/services/sync/models/Base.ts +0 -0
  97. package/src/services/sync/models/ContentLike.ts +0 -0
  98. package/src/services/sync/models/ContentProgress.ts +7 -4
  99. package/src/services/sync/models/Practice.ts +0 -0
  100. package/src/services/sync/models/PracticeDayNote.ts +0 -0
  101. package/src/services/sync/models/UserAwardProgress.ts +3 -3
  102. package/src/services/sync/models/index.ts +0 -0
  103. package/src/services/sync/repositories/base.ts +0 -0
  104. package/src/services/sync/repositories/content-likes.ts +0 -0
  105. package/src/services/sync/repositories/content-progress.ts +8 -15
  106. package/src/services/sync/repositories/index.ts +0 -0
  107. package/src/services/sync/repositories/practice-day-notes.ts +0 -0
  108. package/src/services/sync/repositories/practices.ts +0 -0
  109. package/src/services/sync/repositories/user-award-progress.ts +4 -4
  110. package/src/services/sync/repository-proxy.ts +0 -0
  111. package/src/services/sync/resolver.ts +0 -0
  112. package/src/services/sync/run-scope.ts +0 -0
  113. package/src/services/sync/schema/index.ts +2 -2
  114. package/src/services/sync/serializers/index.ts +0 -0
  115. package/src/services/sync/serializers/model.ts +0 -0
  116. package/src/services/sync/serializers/raw.ts +0 -0
  117. package/src/services/sync/store/push-coalescer.ts +0 -0
  118. package/src/services/sync/store-configs.ts +0 -0
  119. package/src/services/sync/strategies/base.ts +0 -0
  120. package/src/services/sync/strategies/index.ts +0 -0
  121. package/src/services/sync/strategies/initial.ts +0 -0
  122. package/src/services/sync/strategies/polling.ts +0 -0
  123. package/src/services/sync/telemetry/index.ts +0 -0
  124. package/src/services/sync/telemetry/sampling.ts +0 -0
  125. package/src/services/sync/utils/event-emitter.ts +0 -0
  126. package/src/services/sync/utils/index.ts +0 -0
  127. package/src/services/sync/utils/throttle.ts +0 -0
  128. package/src/services/sync/utils/timers.ts +0 -0
  129. package/src/services/types.js +0 -0
  130. package/src/services/user/account.ts +0 -0
  131. package/src/services/user/chat.js +0 -0
  132. package/src/services/user/interests.js +0 -0
  133. package/src/services/user/management.js +0 -0
  134. package/src/services/user/notifications.js +0 -0
  135. package/src/services/user/payments.ts +0 -0
  136. package/src/services/user/permissions.js +0 -0
  137. package/src/services/user/profile.js +0 -0
  138. package/src/services/user/types.d.ts +0 -2
  139. package/src/services/user/types.js +0 -2
  140. package/src/services/user/user-management-system.js +0 -0
  141. package/src/services/userActivity.js +2 -3
  142. package/test/HttpClient.test.js +0 -0
  143. package/test/awards/award-alacarte-observer.test.js +0 -0
  144. package/test/awards/award-auto-refresh.test.js +0 -0
  145. package/test/awards/award-calculations.test.js +0 -0
  146. package/test/awards/award-certificate-display.test.js +0 -0
  147. package/test/awards/award-collection-edge-cases.test.js +0 -0
  148. package/test/awards/award-collection-filtering.test.js +0 -0
  149. package/test/awards/award-completion-flow.test.js +2 -1
  150. package/test/awards/award-exclusion-handling.test.js +0 -0
  151. package/test/awards/award-multi-lesson.test.js +0 -0
  152. package/test/awards/award-observer-integration.test.js +0 -0
  153. package/test/awards/award-query-messages.test.js +0 -0
  154. package/test/awards/award-user-collection.test.js +0 -0
  155. package/test/awards/duplicate-prevention.test.js +0 -0
  156. package/test/awards/helpers/completion-mock.js +0 -0
  157. package/test/awards/helpers/index.js +0 -0
  158. package/test/awards/helpers/mock-setup.js +0 -0
  159. package/test/awards/helpers/progress-emitter.js +0 -0
  160. package/test/awards/message-generator.test.js +0 -0
  161. package/test/content.test.js +0 -0
  162. package/test/contentLikes.test.js +0 -0
  163. package/test/contentProgress.test.js +0 -0
  164. package/test/dataContext.test.js +0 -0
  165. package/test/forum.test.js +0 -0
  166. package/test/imageSRCBuilder.test.js +0 -0
  167. package/test/imageSRCVerify.test.js +0 -0
  168. package/test/lib/lastUpdated.test.js +0 -0
  169. package/test/live/contentProgressLive.test.js +0 -0
  170. package/test/live/railcontentLive.test.js +0 -0
  171. package/test/localStorageMock.js +0 -0
  172. package/test/log.js +0 -0
  173. package/test/mockData/award-definitions.js +0 -0
  174. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  175. package/test/mockData/mockData_progress_content.json +0 -0
  176. package/test/mockData/mockData_sanity_progress_content.json +0 -0
  177. package/test/mockData/mockData_user_practices.json +0 -0
  178. package/test/notifications.test.js +0 -0
  179. package/test/progressRows.test.js +0 -0
  180. package/test/streakMessage.test.js +0 -0
  181. package/test/sync/models/award-database-integration.test.js +0 -0
  182. package/test/user/permissions.test.js +0 -0
  183. package/test/userActivity.test.js +0 -0
  184. package/tools/generate-index.cjs +0 -0
@@ -1,9 +1,18 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(rg:*)",
5
- "Bash(npm run lint:*)"
4
+ "Bash(find:*)",
5
+ "Bash(docker exec:*)",
6
+ "Bash(npm test:*)",
7
+ "WebSearch",
8
+ "WebFetch(domain:watermelondb.dev)",
9
+ "WebFetch(domain:github.com)",
10
+ "Bash(git checkout:*)",
11
+ "Bash(npm run doc:*)",
12
+ "Bash(cat:*)",
13
+ "Bash(tr:*)"
6
14
  ],
7
- "deny": []
15
+ "deny": [],
16
+ "ask": []
8
17
  }
9
18
  }
package/.coderabbit.yaml CHANGED
File without changes
package/.editorconfig CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
package/.prettierignore CHANGED
File without changes
package/.prettierrc CHANGED
File without changes
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
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.105.0](https://github.com/railroadmedia/musora-content-services/compare/v2.104.9...v2.105.0) (2025-12-17)
6
+
7
+
8
+ ### Features
9
+
10
+ * Remove xp and total_xp ([#656](https://github.com/railroadmedia/musora-content-services/issues/656)) ([e6238a6](https://github.com/railroadmedia/musora-content-services/commit/e6238a605e095dc81e152c25f0abf4885b2bb091))
11
+ * update type for method progress card ([#662](https://github.com/railroadmedia/musora-content-services/issues/662)) ([ad2d6df](https://github.com/railroadmedia/musora-content-services/commit/ad2d6df35b321afd7df1a302ee6bfbcbce1a31b7))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * Minor progress fixes ([#654](https://github.com/railroadmedia/musora-content-services/issues/654)) ([202cceb](https://github.com/railroadmedia/musora-content-services/commit/202ccebc85d119ce9a63007e600a8fd92c3f94d7))
17
+
5
18
  ### [2.104.9](https://github.com/railroadmedia/musora-content-services/compare/v2.104.8...v2.104.9) (2025-12-17)
6
19
 
7
20
  ### [2.104.8](https://github.com/railroadmedia/musora-content-services/compare/v2.104.7...v2.104.8) (2025-12-16)
package/CLAUDE.md CHANGED
File without changes
package/README.md CHANGED
File without changes
package/babel.config.cjs CHANGED
File without changes
package/jest.config.js CHANGED
File without changes
package/jsdoc.json CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.104.9",
3
+ "version": "2.105.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
File without changes
File without changes
@@ -425,8 +425,6 @@ export let contentTypeConfig = {
425
425
  '"instructors": instructor[]->name',
426
426
  `"description": ${descriptionField}`,
427
427
  `"resource": ${resourcesField}`,
428
- 'xp',
429
- 'total_xp',
430
428
  `"lessons": child[]->{
431
429
  "id": railcontent_id,
432
430
  title,
@@ -515,11 +513,9 @@ export let contentTypeConfig = {
515
513
  pack: {
516
514
  fields: [
517
515
  '"lesson_count": coalesce(count(child[]->.child[]->), 0)',
518
- 'xp',
519
516
  `"description": ${descriptionField}`,
520
517
  '"instructors": instructor[]->{ "id": railcontent_id, name, "thumbnail_url": thumbnail_url.asset->url }',
521
518
  '"logo_image_url": logo_image_url.asset->url',
522
- 'total_xp',
523
519
  `"resources": ${resourcesField}`,
524
520
  '"thumbnail": thumbnail.asset->url',
525
521
  '"light_mode_logo": light_mode_logo_url.asset->url',
@@ -552,7 +548,6 @@ export let contentTypeConfig = {
552
548
  '"light_mode_logo": light_mode_logo_url.asset->url',
553
549
  '"dark_mode_logo": dark_mode_logo_url.asset->url',
554
550
  `"description": ${descriptionField}`,
555
- 'total_xp',
556
551
  ],
557
552
  childFields: [`"description": ${descriptionField}`],
558
553
  },
@@ -574,7 +569,6 @@ export let contentTypeConfig = {
574
569
  title,
575
570
  "type": _type,
576
571
  "description": ${descriptionField},
577
- xp,
578
572
  web_url_path,
579
573
  "url": web_url_path,
580
574
  }`,
@@ -586,8 +580,6 @@ export let contentTypeConfig = {
586
580
  '"instructors": instructor[]->name',
587
581
  `"description": ${descriptionField}`,
588
582
  `"resource": ${resourcesField}`,
589
- 'xp',
590
- 'total_xp',
591
583
  `"lessons": child[]->{
592
584
  "id": railcontent_id,
593
585
  title,
File without changes
File without changes
File without changes
package/src/lib/brands.ts CHANGED
File without changes
File without changes
File without changes
File without changes
@@ -21,6 +21,7 @@ let progressUpdateCallback = null
21
21
  * - `awardId` - Unique Sanity award ID
22
22
  * - `name` - Display name of the award
23
23
  * - `badge` - URL to badge image
24
+ * - `contentType` - Content type ('guided-course' or 'learning-path-v2')
24
25
  * - `completed_at` - ISO timestamp
25
26
  * - `isCompleted` - Boolean indicating the award is completed (always true for granted awards)
26
27
  * - `completion_data.message` - Pre-generated congratulations message
@@ -72,6 +73,7 @@ export function registerAwardCallback(callback) {
72
73
  awardId: awardId,
73
74
  name: definition.name,
74
75
  badge: definition.badge,
76
+ contentType: definition.content_type,
75
77
  completed_at: completionData.completed_at,
76
78
  isCompleted: true,
77
79
  completionData: {
@@ -136,9 +136,7 @@ export async function getContentAwards(contentId) {
136
136
  instructorName: def.instructor_name,
137
137
  progressPercentage: userProgress?.progress_percentage ?? 0,
138
138
  isCompleted: userProgress ? UserAwardProgressRepository.isCompleted(userProgress) : false,
139
- completedAt: userProgress?.completed_at
140
- ? new Date(userProgress.completed_at).toISOString()
141
- : null,
139
+ completedAt: userProgress?.completed_at,
142
140
  completionData
143
141
  }
144
142
  })
@@ -168,8 +166,7 @@ export async function getContentAwards(contentId) {
168
166
  * - Badge and award images for display
169
167
  * - Completion date for "Earned on X" display
170
168
  * - `completionData.message` - Pre-generated congratulations text
171
- * - `completionData.practice_minutes` - Total practice time for this award
172
- * - `completionData.days_user_practiced` - Days spent earning this award
169
+ * - `completionData.XXX` - other fields are award type dependant
173
170
  *
174
171
  * Returns empty array `[]` on error (never throws).
175
172
  *
@@ -226,16 +223,14 @@ export async function getContentAwards(contentId) {
226
223
  */
227
224
  export async function getCompletedAwards(brand = null, options = {}) {
228
225
  try {
229
- const allProgress = await db.userAwardProgress.getAll()
226
+ const allProgress = await db.userAwardProgress.getCompleted()
230
227
 
231
228
  const completed = allProgress.data.filter(p =>
232
229
  p.progress_percentage === 100 && p.completed_at !== null
233
230
  )
234
-
235
231
  let awards = await Promise.all(
236
232
  completed.map(async (progress) => {
237
233
  const definition = await awardDefinitions.getById(progress.award_id)
238
-
239
234
  if (!definition) {
240
235
  return null
241
236
  }
@@ -243,24 +238,24 @@ export async function getCompletedAwards(brand = null, options = {}) {
243
238
  if (brand && definition.brand !== brand) {
244
239
  return null
245
240
  }
246
-
247
- const completionData = enhanceCompletionData(progress.completion_data)
248
-
241
+ const completionData = definition.type === awardDefinitions.CONTENT_AWARD ? enhanceCompletionData(progress.completion_data) : progress.completion_data;
242
+ const hasCertificate = definition.type === awardDefinitions.CONTENT_AWARD
249
243
  return {
250
244
  awardId: progress.award_id,
251
245
  awardTitle: definition.name,
246
+ awardType: definition.type,
252
247
  badge: definition.badge,
253
248
  award: definition.award,
254
249
  brand: definition.brand,
250
+ hasCertificate: hasCertificate,
255
251
  instructorName: definition.instructor_name,
256
252
  progressPercentage: progress.progress_percentage,
257
253
  isCompleted: true,
258
- completedAt: new Date(progress.completed_at * 1000).toISOString(),
254
+ completedAt: progress.completed_at,
259
255
  completionData
260
256
  }
261
257
  })
262
258
  )
263
-
264
259
  awards = awards.filter(award => award !== null)
265
260
 
266
261
  awards.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime())
@@ -269,7 +264,6 @@ export async function getCompletedAwards(brand = null, options = {}) {
269
264
  const offset = options.offset || 0
270
265
  awards = awards.slice(offset, offset + options.limit)
271
266
  }
272
-
273
267
  return awards
274
268
  } catch (error) {
275
269
  console.error('Failed to get completed awards:', error)
File without changes
@@ -9,6 +9,9 @@
9
9
  const STORAGE_KEY = 'musora_award_definitions_last_fetch'
10
10
 
11
11
  class AwardDefinitionsService {
12
+
13
+ CONTENT_AWARD = 'content-award'
14
+ EXP_AWARD = 'exp-award'
12
15
  constructor() {
13
16
  /** @type {AwardDefinitionsMap} */
14
17
  this.definitions = new Map()
@@ -90,7 +93,7 @@ class AwardDefinitionsService {
90
93
  bypassPermissions: true,
91
94
  }).buildFilter()
92
95
 
93
- const query = `*[_type == 'content-award'] {
96
+ const query = `*[_type in ['content-award', 'exp-award']] {
94
97
  _id,
95
98
  is_active,
96
99
  name,
@@ -107,7 +110,7 @@ class AwardDefinitionsService {
107
110
  'child_ids': content->child[${childFilter}]->railcontent_id,
108
111
  }`
109
112
 
110
- const awards = await fetchSanity(query, true, { processNeedAccess: false })
113
+ const awards = await fetchSanity(query, true, { processNeedAccess: false, processPageType: false })
111
114
 
112
115
  this.definitions.clear()
113
116
  this.contentIndex.clear()
File without changes
@@ -99,7 +99,7 @@ export class AwardManager {
99
99
  const popupMessage = AwardMessageGenerator.generatePopupMessage(completionData)
100
100
 
101
101
  await db.userAwardProgress.recordAwardProgress(award._id, 100, {
102
- completedAt: Date.now(),
102
+ completedAt: new Date().toISOString(),
103
103
  completionData,
104
104
  progressData,
105
105
  immediate: true
@@ -39,10 +39,7 @@ export async function buildCertificateData(awardId) {
39
39
  return {
40
40
  userId: globalConfig.sessionConfig.userId,
41
41
  userName: userData?.display_name || userData?.name || 'User',
42
- completedAt: userProgress.data.completed_at
43
- ? new Date(userProgress.data.completed_at * 1000).toISOString()
44
- : new Date().toISOString(),
45
-
42
+ completedAt: userProgress.data?.completed_at ?? new Date().toISOString(),
46
43
  awardId: awardDef._id,
47
44
  awardType: awardDef.type || 'content-award',
48
45
  awardTitle: awardDef.name,
File without changes
File without changes
File without changes
@@ -65,6 +65,7 @@ export interface AwardCallbackPayload {
65
65
  awardId: string
66
66
  name: string
67
67
  badge: string
68
+ contentType: string
68
69
  completed_at: string
69
70
  isCompleted: boolean
70
71
  completion_data: AwardCompletionData
@@ -37,10 +37,12 @@
37
37
  * @typedef {Object} AwardInfo
38
38
  * @property {string} awardId - Unique Sanity award ID
39
39
  * @property {string} awardTitle - Display name of the award
40
+ * @property {string} awardType - Type of the award
41
+ * @property {boolean} hasCertificate - flag to indicate if the award includes a downloadable certificate
40
42
  * @property {string} badge - URL to badge image
41
43
  * @property {string} award - URL to award image
42
- * @property {string} brand - Brand (drumeo, pianote, guitareo, singeo)
43
- * @property {string} instructorName - Name of the instructor
44
+ * @property {string} brand - Brand (drumeo, pianote, guitareo, singeo, playbass)
45
+ * @property {string|null} instructorName - Name of the instructor
44
46
  * @property {number} progressPercentage - Completion percentage (0-100). Progress is tracked per collection context for learning paths.
45
47
  * @property {boolean} isCompleted - Whether the award is fully completed
46
48
  * @property {string|null} completedAt - ISO timestamp of completion, or null if not completed
@@ -73,6 +75,7 @@
73
75
  * @property {string} awardId - Unique Sanity award ID
74
76
  * @property {string} name - Display name of the award
75
77
  * @property {string} badge - URL to badge image
78
+ * @property {string} contentType - Content type ('guided-course' or 'learning-path-v2')
76
79
  * @property {string} completed_at - ISO timestamp of completion
77
80
  * @property {AwardCompletionData} completion_data - Practice statistics
78
81
  */
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -6,6 +6,7 @@ import { getNextLessonLessonParentTypes } from '../contentTypeConfig.js'
6
6
  import { emitContentCompleted } from './progress-events'
7
7
  import {getDailySession} from "./content-org/learning-paths.ts";
8
8
  import {getToday} from "./dateUtils.js";
9
+ import { fetchBrandsByContentIds } from './sanity.js'
9
10
 
10
11
  const STATE_STARTED = STATE.STARTED
11
12
  const STATE_COMPLETED = STATE.COMPLETED
@@ -316,48 +317,25 @@ async function getByIdsAndCollections(tuples, dataKey, defaultValue) {
316
317
  }
317
318
 
318
319
  export async function getAllStarted(limit = null) {
319
- return db.contentProgress.startedIds(limit).then((r) => r.data.map((id) => parseInt(id)))
320
+ return db.contentProgress.startedIds(limit)
320
321
  }
321
322
 
322
323
  export async function getAllCompleted(limit = null) {
323
- return db.contentProgress.completedIds(limit).then((r) => r.data.map((id) => parseInt(id)))
324
+ return db.contentProgress.completedIds(limit)
324
325
  }
325
326
 
326
327
  export async function getAllCompletedByIds(contentIds) {
327
328
  return db.contentProgress.completedByContentIds(normalizeContentIds(contentIds))
328
329
  }
329
330
 
331
+ /**
332
+ * Fetches content **IDs** for items that were started or completed.
333
+ */
330
334
  export async function getAllStartedOrCompleted({
331
- onlyIds = true,
332
335
  brand = null,
333
336
  limit = null,
334
337
  } = {}) {
335
- const agoInSeconds = Math.floor(Date.now() / 1000) - 60 * 24 * 60 * 60 // 60 days in seconds
336
- const filters = {
337
- brand: brand ?? undefined,
338
- updatedAfter: agoInSeconds,
339
- limit: limit ?? undefined,
340
- }
341
-
342
- if (onlyIds) {
343
- return db.contentProgress
344
- .startedOrCompletedIds(filters)
345
- .then((r) => r.data.map((id) => parseInt(id)))
346
- } else {
347
- return db.contentProgress.startedOrCompleted(filters).then((r) => {
348
- return Object.fromEntries(
349
- r.data.map((p) => [
350
- p.content_id,
351
- {
352
- last_update: p.updated_at,
353
- progress: p.progress_percent,
354
- status: p.state,
355
- brand: p.content_brand,
356
- },
357
- ])
358
- )
359
- })
360
- }
338
+ return await _getAllStartedOrCompleted({ brand, limit }).then(recs => recs.map(rec => rec.content_id))
361
339
  }
362
340
 
363
341
  /**
@@ -378,11 +356,41 @@ export async function getAllStartedOrCompleted({
378
356
  * console.log(progressMap[123]); // => 52
379
357
  */
380
358
  export async function getStartedOrCompletedProgressOnly({ brand = undefined } = {}) {
381
- return db.contentProgress.startedOrCompleted({ brand: brand }).then((r) => {
382
- return Object.fromEntries(r.data.map((p) => [p.content_id, p.progress_percent]))
359
+ return _getAllStartedOrCompleted({ brand }).then((r) => {
360
+ return Object.fromEntries(r.map((p) => [p.content_id, p.progress_percent]))
383
361
  })
384
362
  }
385
363
 
364
+ async function _getAllStartedOrCompleted({
365
+ brand = null,
366
+ limit = null,
367
+ } = {}) {
368
+ const agoInSeconds = Math.floor(Date.now() / 1000) - 60 * 24 * 60 * 60 // 60 days in seconds
369
+ const baseFilters = {
370
+ updatedAfter: agoInSeconds,
371
+ }
372
+
373
+ if (!brand) {
374
+ return await db.contentProgress.startedOrCompleted({ ...baseFilters, limit }).then(r => r.data)
375
+ }
376
+
377
+ // content_brand can be null (i.e., when progress records created locally)
378
+ // TODO: eventually put content metadata into watermelon so we can
379
+ // always have brand info in progress records and avoid all this
380
+
381
+ // for now though, null-ish brands shouldn't be too numerous, so safe to have undefined limit
382
+ const [strictRecs, looseRecs] = await Promise.all([
383
+ db.contentProgress.startedOrCompleted({ ...baseFilters, brand, limit }),
384
+ db.contentProgress.startedOrCompleted({ ...baseFilters, brand: null, limit: undefined })
385
+ ]);
386
+
387
+ const map = await fetchBrandsByContentIds(looseRecs.data.map(r => r.content_id));
388
+ const filteredLooseRecs = looseRecs.data.filter(r => map[r.content_id] === brand).map(r => ({ ...r, content_brand: brand }));
389
+
390
+ const records = [...strictRecs.data, ...filteredLooseRecs].sort((a, b) => b.updated_at - a.updated_at).slice(0, limit || undefined);
391
+ return records;
392
+ }
393
+
386
394
  /**
387
395
  * Record watch session
388
396
  * @return {string} sessionId - provide in future calls to update progress
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -98,7 +98,7 @@ export async function getMethodCard(brand) {
98
98
  action = nextLesson
99
99
  ? getMethodActionCTA(nextLesson)
100
100
  : {
101
- type: 'method',
101
+ type: 'method-complete',
102
102
  brand,
103
103
  }
104
104
  }
File without changes
File without changes
File without changes
File without changes
@@ -2083,3 +2083,26 @@ export async function fetchOwnedContent(
2083
2083
 
2084
2084
  return fetchSanity(query, true)
2085
2085
  }
2086
+
2087
+ /**
2088
+ * Fetch brands for given content IDs.
2089
+ *
2090
+ * @param {Array<number>} contentIds - Array of railcontent IDs
2091
+ * @returns {Promise<Object>} - A promise that resolves to an object mapping content IDs to brands
2092
+ */
2093
+ export async function fetchBrandsByContentIds(contentIds) {
2094
+ if (!contentIds || contentIds.length === 0) {
2095
+ return {}
2096
+ }
2097
+ const idsString = contentIds.join(',')
2098
+ const query = `*[railcontent_id in [${idsString}]]{
2099
+ railcontent_id,
2100
+ brand
2101
+ }`
2102
+ const results = await fetchSanity(query, true)
2103
+ const brandMap = {}
2104
+ results.forEach((item) => {
2105
+ brandMap[item.railcontent_id] = item.brand
2106
+ })
2107
+ return brandMap
2108
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes