musora-content-services 2.104.8 → 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.
- package/.claude/settings.local.json +8 -7
- package/.coderabbit.yaml +0 -0
- package/.editorconfig +0 -0
- package/.github/pull_request_template.md +0 -0
- package/.github/workflows/docs.js.yml +0 -0
- package/CHANGELOG.md +15 -0
- package/CLAUDE.md +0 -0
- package/README.md +0 -0
- package/babel.config.cjs +0 -0
- package/jsdoc.json +0 -0
- package/package.json +1 -1
- package/src/constants/award-assets.js +0 -0
- package/src/contentMetaData.js +0 -0
- package/src/contentTypeConfig.js +0 -8
- package/src/infrastructure/http/HttpClient.ts +0 -0
- package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
- package/src/infrastructure/http/index.ts +0 -0
- package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
- package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
- package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
- package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
- package/src/lib/brands.ts +0 -0
- package/src/lib/lastUpdated.js +0 -0
- package/src/services/api/types.js +0 -0
- package/src/services/api/types.ts +0 -0
- package/src/services/awards/award-callbacks.js +2 -0
- package/src/services/awards/award-query.js +8 -14
- package/src/services/awards/internal/.indexignore +0 -0
- package/src/services/awards/internal/award-definitions.js +5 -2
- package/src/services/awards/internal/award-events.js +0 -0
- package/src/services/awards/internal/award-manager.js +1 -1
- package/src/services/awards/internal/certificate-builder.js +1 -4
- package/src/services/awards/internal/completion-data-generator.js +0 -0
- package/src/services/awards/internal/content-progress-observer.js +0 -0
- package/src/services/awards/internal/image-utils.js +0 -0
- package/src/services/awards/internal/message-generator.js +0 -0
- package/src/services/awards/internal/types.js +0 -0
- package/src/services/awards/types.d.ts +1 -0
- package/src/services/awards/types.js +5 -2
- package/src/services/config.js +0 -0
- package/src/services/content/content.ts +0 -0
- package/src/services/content-org/content-org.js +0 -0
- package/src/services/content-org/guided-courses.ts +0 -0
- package/src/services/content-org/learning-paths.ts +2 -0
- package/src/services/content-org/playlists-types.js +0 -0
- package/src/services/content-org/playlists.js +0 -0
- package/src/services/contentLikes.js +0 -0
- package/src/services/contentProgress.js +39 -31
- package/src/services/dataContext.js +0 -0
- package/src/services/dateUtils.js +0 -0
- package/src/services/eventsAPI.js +0 -0
- package/src/services/forums/categories.ts +0 -0
- package/src/services/forums/posts.ts +0 -0
- package/src/services/forums/types.ts +0 -0
- package/src/services/gamification/awards.ts +0 -0
- package/src/services/gamification/gamification.js +0 -0
- package/src/services/imageSRCBuilder.js +0 -0
- package/src/services/imageSRCVerify.js +0 -0
- package/src/services/liveTesting.ts +0 -0
- package/src/services/permissions/PermissionsAdapter.ts +0 -0
- package/src/services/permissions/PermissionsAdapterFactory.ts +0 -0
- package/src/services/permissions/PermissionsV1Adapter.ts +0 -0
- package/src/services/permissions/README.md +0 -0
- package/src/services/permissions/index.ts +0 -0
- package/src/services/progress-row/method-card.js +1 -1
- package/src/services/recommendations.js +0 -0
- package/src/services/reporting/README.md +0 -0
- package/src/services/reporting/reporting.ts +0 -0
- package/src/services/reporting/types.ts +0 -0
- package/src/services/sanity.js +23 -0
- package/src/services/sentry/.indexignore +0 -0
- package/src/services/sentry/index.ts +0 -0
- package/src/services/sync/.indexignore +0 -0
- package/src/services/sync/adapters/factory.ts +0 -0
- package/src/services/sync/adapters/lokijs.ts +0 -0
- package/src/services/sync/adapters/sqlite.ts +0 -0
- package/src/services/sync/concurrency-safety.ts +0 -0
- package/src/services/sync/context/index.ts +0 -0
- package/src/services/sync/context/providers/base.ts +0 -0
- package/src/services/sync/context/providers/connectivity.ts +0 -0
- package/src/services/sync/context/providers/durability.ts +0 -0
- package/src/services/sync/context/providers/index.ts +0 -0
- package/src/services/sync/context/providers/session.ts +0 -0
- package/src/services/sync/context/providers/tabs.ts +0 -0
- package/src/services/sync/context/providers/visibility.ts +0 -0
- package/src/services/sync/database/factory.ts +0 -0
- package/src/services/sync/errors/boundary.ts +0 -0
- package/src/services/sync/errors/index.ts +0 -0
- package/src/services/sync/index.ts +0 -0
- package/src/services/sync/models/Base.ts +0 -0
- package/src/services/sync/models/ContentLike.ts +0 -0
- package/src/services/sync/models/ContentProgress.ts +7 -4
- package/src/services/sync/models/Practice.ts +0 -0
- package/src/services/sync/models/PracticeDayNote.ts +0 -0
- package/src/services/sync/models/UserAwardProgress.ts +3 -3
- package/src/services/sync/models/index.ts +0 -0
- package/src/services/sync/repositories/base.ts +0 -0
- package/src/services/sync/repositories/content-likes.ts +0 -0
- package/src/services/sync/repositories/content-progress.ts +8 -15
- package/src/services/sync/repositories/index.ts +0 -0
- package/src/services/sync/repositories/practice-day-notes.ts +0 -0
- package/src/services/sync/repositories/practices.ts +0 -0
- package/src/services/sync/repositories/user-award-progress.ts +4 -4
- package/src/services/sync/repository-proxy.ts +0 -0
- package/src/services/sync/resolver.ts +0 -0
- package/src/services/sync/run-scope.ts +0 -0
- package/src/services/sync/schema/index.ts +2 -2
- package/src/services/sync/serializers/index.ts +0 -0
- package/src/services/sync/serializers/model.ts +0 -0
- package/src/services/sync/serializers/raw.ts +0 -0
- package/src/services/sync/store/push-coalescer.ts +0 -0
- package/src/services/sync/store-configs.ts +0 -0
- package/src/services/sync/strategies/base.ts +0 -0
- package/src/services/sync/strategies/index.ts +0 -0
- package/src/services/sync/strategies/initial.ts +0 -0
- package/src/services/sync/strategies/polling.ts +0 -0
- package/src/services/sync/telemetry/index.ts +0 -0
- package/src/services/sync/telemetry/sampling.ts +0 -0
- package/src/services/sync/utils/event-emitter.ts +0 -0
- package/src/services/sync/utils/index.ts +0 -0
- package/src/services/sync/utils/throttle.ts +0 -0
- package/src/services/sync/utils/timers.ts +0 -0
- package/src/services/types.js +0 -0
- package/src/services/user/account.ts +0 -0
- package/src/services/user/chat.js +0 -0
- package/src/services/user/interests.js +0 -0
- package/src/services/user/management.js +0 -0
- package/src/services/user/notifications.js +0 -0
- package/src/services/user/payments.ts +0 -0
- package/src/services/user/permissions.js +0 -0
- package/src/services/user/profile.js +0 -0
- package/src/services/user/types.d.ts +0 -2
- package/src/services/user/types.js +0 -2
- package/src/services/user/user-management-system.js +0 -0
- package/src/services/userActivity.js +2 -3
- package/test/HttpClient.test.js +0 -0
- package/test/awards/award-alacarte-observer.test.js +0 -0
- package/test/awards/award-auto-refresh.test.js +0 -0
- package/test/awards/award-calculations.test.js +0 -0
- package/test/awards/award-certificate-display.test.js +0 -0
- package/test/awards/award-collection-edge-cases.test.js +0 -0
- package/test/awards/award-collection-filtering.test.js +0 -0
- package/test/awards/award-completion-flow.test.js +2 -1
- package/test/awards/award-exclusion-handling.test.js +0 -0
- package/test/awards/award-multi-lesson.test.js +0 -0
- package/test/awards/award-observer-integration.test.js +0 -0
- package/test/awards/award-query-messages.test.js +0 -0
- package/test/awards/award-user-collection.test.js +0 -0
- package/test/awards/duplicate-prevention.test.js +0 -0
- package/test/awards/helpers/completion-mock.js +0 -0
- package/test/awards/helpers/index.js +0 -0
- package/test/awards/helpers/mock-setup.js +0 -0
- package/test/awards/helpers/progress-emitter.js +0 -0
- package/test/awards/message-generator.test.js +0 -0
- package/test/content.test.js +0 -0
- package/test/contentLikes.test.js +0 -0
- package/test/contentProgress.test.js +0 -0
- package/test/forum.test.js +0 -0
- package/test/imageSRCBuilder.test.js +0 -0
- package/test/imageSRCVerify.test.js +0 -0
- package/test/lib/lastUpdated.test.js +0 -0
- package/test/live/contentProgressLive.test.js +0 -0
- package/test/live/railcontentLive.test.js +0 -0
- package/test/mockData/award-definitions.js +0 -0
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
- package/test/mockData/mockData_progress_content.json +0 -0
- package/test/mockData/mockData_sanity_progress_content.json +0 -0
- package/test/mockData/mockData_user_practices.json +0 -0
- package/test/notifications.test.js +0 -0
- package/test/progressRows.test.js +0 -0
- package/test/streakMessage.test.js +0 -0
- package/test/sync/models/award-database-integration.test.js +0 -0
- package/test/user/permissions.test.js +0 -0
- package/test/userActivity.test.js +0 -0
- package/tools/generate-index.cjs +0 -0
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"Bash(
|
|
4
|
+
"Bash(find:*)",
|
|
5
|
+
"Bash(docker exec:*)",
|
|
5
6
|
"Bash(npm test:*)",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"Bash(git add:*)",
|
|
10
|
-
"Bash(git commit:*)",
|
|
7
|
+
"WebSearch",
|
|
8
|
+
"WebFetch(domain:watermelondb.dev)",
|
|
9
|
+
"WebFetch(domain:github.com)",
|
|
11
10
|
"Bash(git checkout:*)",
|
|
12
|
-
"Bash(
|
|
11
|
+
"Bash(npm run doc:*)",
|
|
12
|
+
"Bash(cat:*)",
|
|
13
|
+
"Bash(tr:*)"
|
|
13
14
|
],
|
|
14
15
|
"deny": [],
|
|
15
16
|
"ask": []
|
package/.coderabbit.yaml
CHANGED
|
File without changes
|
package/.editorconfig
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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
|
+
|
|
18
|
+
### [2.104.9](https://github.com/railroadmedia/musora-content-services/compare/v2.104.8...v2.104.9) (2025-12-17)
|
|
19
|
+
|
|
5
20
|
### [2.104.8](https://github.com/railroadmedia/musora-content-services/compare/v2.104.7...v2.104.8) (2025-12-16)
|
|
6
21
|
|
|
7
22
|
### [2.104.7](https://github.com/railroadmedia/musora-content-services/compare/v2.104.6...v2.104.7) (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/jsdoc.json
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
File without changes
|
package/src/contentMetaData.js
CHANGED
|
File without changes
|
package/src/contentTypeConfig.js
CHANGED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/lib/brands.ts
CHANGED
|
File without changes
|
package/src/lib/lastUpdated.js
CHANGED
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
*/
|
package/src/services/config.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -124,6 +124,7 @@ export async function getEnrichedLearningPath(learningPathId) {
|
|
|
124
124
|
addProgressStatus: true,
|
|
125
125
|
addProgressPercentage: true,
|
|
126
126
|
addProgressTimestamp: true,
|
|
127
|
+
addResumeTimeSeconds: true,
|
|
127
128
|
addNavigateTo: true,
|
|
128
129
|
}
|
|
129
130
|
)) as any
|
|
@@ -154,6 +155,7 @@ export async function getEnrichedLearningPaths(learningPathIds: number[]) {
|
|
|
154
155
|
addProgressStatus: true,
|
|
155
156
|
addProgressPercentage: true,
|
|
156
157
|
addProgressTimestamp: true,
|
|
158
|
+
addResumeTimeSeconds: true,
|
|
157
159
|
addNavigateTo: true,
|
|
158
160
|
}
|
|
159
161
|
)) as any
|
|
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)
|
|
320
|
+
return db.contentProgress.startedIds(limit)
|
|
320
321
|
}
|
|
321
322
|
|
|
322
323
|
export async function getAllCompleted(limit = null) {
|
|
323
|
-
return db.contentProgress.completedIds(limit)
|
|
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
|
-
|
|
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
|
|
382
|
-
return Object.fromEntries(r.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/services/sanity.js
CHANGED
|
@@ -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
|