musora-content-services 2.94.7 → 2.95.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/.coderabbit.yaml +0 -0
- package/.editorconfig +0 -0
- package/.github/pull_request_template.md +0 -0
- package/.github/workflows/conventional-commits.yaml +0 -0
- package/.github/workflows/docs.js.yml +0 -0
- package/.github/workflows/node.js.yml +0 -0
- package/.prettierignore +0 -0
- package/.prettierrc +0 -0
- package/CHANGELOG.md +23 -0
- package/CLAUDE.md +408 -0
- package/README.md +0 -0
- package/babel.config.cjs +10 -0
- package/jest.config.js +0 -0
- package/jsdoc.json +2 -1
- package/package.json +2 -2
- package/src/constants/award-assets.js +35 -0
- package/src/contentMetaData.js +0 -0
- package/src/filterBuilder.js +7 -2
- package/src/index.d.ts +26 -5
- package/src/index.js +26 -5
- 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/httpHelper.js +0 -0
- package/src/lib/lastUpdated.js +0 -0
- package/src/lib/sanity/query.ts +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 +126 -0
- package/src/services/awards/award-query.js +327 -0
- package/src/services/awards/internal/.indexignore +1 -0
- package/src/services/awards/internal/award-definitions.js +239 -0
- package/src/services/awards/internal/award-events.js +102 -0
- package/src/services/awards/internal/award-manager.js +162 -0
- package/src/services/awards/internal/certificate-builder.js +66 -0
- package/src/services/awards/internal/completion-data-generator.js +84 -0
- package/src/services/awards/internal/content-progress-observer.js +137 -0
- package/src/services/awards/internal/image-utils.js +62 -0
- package/src/services/awards/internal/message-generator.js +17 -0
- package/src/services/awards/internal/types.js +5 -0
- package/src/services/awards/types.d.ts +79 -0
- package/src/services/awards/types.js +101 -0
- package/src/services/config.js +24 -4
- package/src/services/content/artist.ts +0 -0
- package/src/services/content/content.ts +0 -0
- package/src/services/content/genre.ts +0 -0
- package/src/services/content/instructor.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 +19 -15
- package/src/services/content-org/playlists-types.js +0 -0
- package/src/services/content-org/playlists.js +0 -0
- package/src/services/content.js +0 -0
- package/src/services/contentAggregator.js +0 -0
- package/src/services/contentLikes.js +0 -0
- package/src/services/contentProgress.js +2 -1
- 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/forums.ts +0 -0
- package/src/services/forums/posts.ts +0 -0
- package/src/services/forums/threads.ts +0 -0
- package/src/services/forums/types.ts +0 -0
- package/src/services/gamification/awards.ts +114 -83
- 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/PermissionsV2Adapter.ts +0 -0
- package/src/services/permissions/README.md +0 -0
- package/src/services/permissions/index.ts +0 -0
- package/src/services/progress-events.js +58 -0
- package/src/services/progress-row/method-card.js +20 -5
- package/src/services/railcontent.js +0 -0
- 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 +1 -1
- 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/fetch.ts +10 -2
- package/src/services/sync/index.ts +0 -0
- package/src/services/sync/manager.ts +6 -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 +5 -6
- 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 +55 -0
- package/src/services/sync/models/index.ts +1 -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 +47 -25
- package/src/services/sync/repositories/index.ts +1 -0
- package/src/services/sync/repositories/practice-day-notes.ts +0 -0
- package/src/services/sync/repositories/practices.ts +16 -1
- package/src/services/sync/repositories/user-award-progress.ts +133 -0
- package/src/services/sync/repository-proxy.ts +6 -0
- package/src/services/sync/resolver.ts +0 -0
- package/src/services/sync/retry.ts +12 -11
- package/src/services/sync/run-scope.ts +0 -0
- package/src/services/sync/schema/index.ts +18 -3
- 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/index.ts +53 -8
- package/src/services/sync/store/push-coalescer.ts +3 -3
- package/src/services/sync/store-configs.ts +7 -1
- 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/memberships.ts +0 -0
- package/src/services/user/notifications.js +0 -0
- package/src/services/user/onboarding.ts +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/sessions.js +0 -0
- package/src/services/user/types.d.ts +0 -0
- package/src/services/user/types.js +0 -0
- package/src/services/user/user-management-system.js +0 -0
- package/src/services/userActivity.js +0 -1
- package/test/HttpClient.test.js +6 -6
- package/test/awards/award-alacarte-observer.test.js +196 -0
- package/test/awards/award-auto-refresh.test.js +83 -0
- package/test/awards/award-calculations.test.js +33 -0
- package/test/awards/award-certificate-display.test.js +328 -0
- package/test/awards/award-collection-edge-cases.test.js +210 -0
- package/test/awards/award-collection-filtering.test.js +285 -0
- package/test/awards/award-completion-flow.test.js +213 -0
- package/test/awards/award-exclusion-handling.test.js +273 -0
- package/test/awards/award-multi-lesson.test.js +241 -0
- package/test/awards/award-observer-integration.test.js +325 -0
- package/test/awards/award-query-messages.test.js +438 -0
- package/test/awards/award-user-collection.test.js +412 -0
- package/test/awards/duplicate-prevention.test.js +118 -0
- package/test/awards/helpers/completion-mock.js +54 -0
- package/test/awards/helpers/index.js +3 -0
- package/test/awards/helpers/mock-setup.js +69 -0
- package/test/awards/helpers/progress-emitter.js +39 -0
- package/test/awards/message-generator.test.js +162 -0
- package/test/content.test.js +0 -0
- package/test/contentLikes.test.js +0 -0
- package/test/contentProgress.test.js +0 -0
- package/test/dataContext.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/initializeTests.js +6 -0
- package/test/learningPaths.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/localStorageMock.js +0 -0
- package/test/log.js +0 -0
- package/test/mockData/award-definitions.js +171 -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/sanityQueryService.test.js +0 -0
- package/test/streakMessage.test.js +0 -0
- package/test/sync/models/award-database-integration.test.js +519 -0
- package/test/user/permissions.test.js +0 -0
- package/test/userActivity.test.js +0 -0
- package/tools/generate-index.cjs +9 -0
- package/.claude/settings.local.json +0 -14
- package/.yarnrc.yml +0 -1
- package/test/reporting.test.js +0 -132
package/src/index.js
CHANGED
|
@@ -4,6 +4,18 @@ import {
|
|
|
4
4
|
default as EventsAPI
|
|
5
5
|
} from './services/eventsAPI';
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
registerAwardCallback,
|
|
9
|
+
registerProgressCallback
|
|
10
|
+
} from './services/awards/award-callbacks.js';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getAwardStatistics,
|
|
14
|
+
getCompletedAwards,
|
|
15
|
+
getContentAwards,
|
|
16
|
+
getInProgressAwards
|
|
17
|
+
} from './services/awards/award-query.js';
|
|
18
|
+
|
|
7
19
|
import {
|
|
8
20
|
globalConfig,
|
|
9
21
|
initializeService
|
|
@@ -165,9 +177,7 @@ import {
|
|
|
165
177
|
} from './services/forums/threads.ts';
|
|
166
178
|
|
|
167
179
|
import {
|
|
168
|
-
|
|
169
|
-
fetchCertificate,
|
|
170
|
-
getAwardDataForGuidedContent
|
|
180
|
+
fetchCertificate
|
|
171
181
|
} from './services/gamification/awards.ts';
|
|
172
182
|
|
|
173
183
|
import {
|
|
@@ -186,6 +196,11 @@ import {
|
|
|
186
196
|
createTestUser
|
|
187
197
|
} from './services/liveTesting.ts';
|
|
188
198
|
|
|
199
|
+
import {
|
|
200
|
+
emitProgressSaved,
|
|
201
|
+
onProgressSaved
|
|
202
|
+
} from './services/progress-events.js';
|
|
203
|
+
|
|
189
204
|
import {
|
|
190
205
|
getMethodCard
|
|
191
206
|
} from './services/progress-row/method-card.js';
|
|
@@ -452,6 +467,7 @@ export {
|
|
|
452
467
|
deleteUserActivity,
|
|
453
468
|
duplicatePlaylist,
|
|
454
469
|
editComment,
|
|
470
|
+
emitProgressSaved,
|
|
455
471
|
enrollUserInGuidedCourse,
|
|
456
472
|
extractSanityUrl,
|
|
457
473
|
fetchAll,
|
|
@@ -461,7 +477,6 @@ export {
|
|
|
461
477
|
fetchArtistBySlug,
|
|
462
478
|
fetchArtistLessons,
|
|
463
479
|
fetchArtists,
|
|
464
|
-
fetchAwardsForUser,
|
|
465
480
|
fetchByRailContentId,
|
|
466
481
|
fetchByRailContentIds,
|
|
467
482
|
fetchByReference,
|
|
@@ -568,10 +583,13 @@ export {
|
|
|
568
583
|
getAllCompletedByIds,
|
|
569
584
|
getAllStarted,
|
|
570
585
|
getAllStartedOrCompleted,
|
|
571
|
-
|
|
586
|
+
getAwardStatistics,
|
|
587
|
+
getCompletedAwards,
|
|
588
|
+
getContentAwards,
|
|
572
589
|
getContentRows,
|
|
573
590
|
getDailySession,
|
|
574
591
|
getEnrichedLearningPath,
|
|
592
|
+
getInProgressAwards,
|
|
575
593
|
getLastInteractedOf,
|
|
576
594
|
getLearningPathLessonsByIds,
|
|
577
595
|
getLegacyMethods,
|
|
@@ -632,6 +650,7 @@ export {
|
|
|
632
650
|
markNotificationAsUnread,
|
|
633
651
|
markThreadAsRead,
|
|
634
652
|
numberOfActiveUsers,
|
|
653
|
+
onProgressSaved,
|
|
635
654
|
openComment,
|
|
636
655
|
otherStats,
|
|
637
656
|
pauseLiveEventPolling,
|
|
@@ -644,6 +663,8 @@ export {
|
|
|
644
663
|
recordUserActivity,
|
|
645
664
|
recordUserPractice,
|
|
646
665
|
recordWatchSession,
|
|
666
|
+
registerAwardCallback,
|
|
667
|
+
registerProgressCallback,
|
|
647
668
|
removeContentAsInterested,
|
|
648
669
|
removeContentAsNotInterested,
|
|
649
670
|
removeUserPractice,
|
|
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/httpHelper.js
CHANGED
|
File without changes
|
package/src/lib/lastUpdated.js
CHANGED
|
File without changes
|
package/src/lib/sanity/query.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Awards
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import './types.js'
|
|
6
|
+
import { awardEvents } from './internal/award-events'
|
|
7
|
+
|
|
8
|
+
let awardGrantedCallback = null
|
|
9
|
+
let progressUpdateCallback = null
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {AwardCallbackFunction} callback - Function called with award data when an award is earned
|
|
13
|
+
* @returns {UnregisterFunction} Cleanup function to unregister this callback
|
|
14
|
+
*
|
|
15
|
+
* @example // Display award notification
|
|
16
|
+
* const cleanup = registerAwardCallback((award) => {
|
|
17
|
+
* showNotification({
|
|
18
|
+
* title: award.name,
|
|
19
|
+
* message: award.completion_data.message,
|
|
20
|
+
* image: award.badge
|
|
21
|
+
* })
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* // Later, when component unmounts:
|
|
25
|
+
* cleanup()
|
|
26
|
+
*
|
|
27
|
+
* @example // Track award analytics
|
|
28
|
+
* registerAwardCallback((award) => {
|
|
29
|
+
* analytics.track('Award Earned', {
|
|
30
|
+
* awardId: award.awardId,
|
|
31
|
+
* awardName: award.name,
|
|
32
|
+
* practiceMinutes: award.completion_data.practice_minutes,
|
|
33
|
+
* completedAt: award.completed_at
|
|
34
|
+
* })
|
|
35
|
+
* })
|
|
36
|
+
*/
|
|
37
|
+
export function registerAwardCallback(callback) {
|
|
38
|
+
if (typeof callback !== 'function') {
|
|
39
|
+
throw new Error('registerAwardCallback requires a function')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
unregisterAwardCallback()
|
|
43
|
+
|
|
44
|
+
awardGrantedCallback = async (payload) => {
|
|
45
|
+
const { awardId, definition, completionData, popupMessage } = payload
|
|
46
|
+
|
|
47
|
+
const award = {
|
|
48
|
+
awardId: awardId,
|
|
49
|
+
name: definition.name,
|
|
50
|
+
badge: definition.badge,
|
|
51
|
+
completed_at: completionData.completed_at,
|
|
52
|
+
completion_data: {
|
|
53
|
+
completed_at: completionData.completed_at,
|
|
54
|
+
days_user_practiced: completionData.days_user_practiced,
|
|
55
|
+
message: popupMessage,
|
|
56
|
+
practice_minutes: completionData.practice_minutes,
|
|
57
|
+
content_title: completionData.content_title
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
callback(award)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
awardEvents.on('awardGranted', awardGrantedCallback)
|
|
65
|
+
|
|
66
|
+
return unregisterAwardCallback
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function unregisterAwardCallback() {
|
|
70
|
+
if (awardGrantedCallback) {
|
|
71
|
+
awardEvents.off('awardGranted', awardGrantedCallback)
|
|
72
|
+
awardGrantedCallback = null
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {ProgressCallbackFunction} callback - Function called with progress data when award progress changes
|
|
78
|
+
* @returns {UnregisterFunction} Cleanup function to unregister this callback
|
|
79
|
+
*
|
|
80
|
+
* @example // Update progress bar
|
|
81
|
+
* const cleanup = registerProgressCallback(({ awardId, progressPercentage }) => {
|
|
82
|
+
* const progressBar = document.getElementById(`award-${awardId}`)
|
|
83
|
+
* if (progressBar) {
|
|
84
|
+
* progressBar.style.width = `${progressPercentage}%`
|
|
85
|
+
* progressBar.textContent = `${progressPercentage}% Complete`
|
|
86
|
+
* }
|
|
87
|
+
* })
|
|
88
|
+
*
|
|
89
|
+
* // Cleanup on unmount
|
|
90
|
+
* return () => cleanup()
|
|
91
|
+
*
|
|
92
|
+
* @example // React state update
|
|
93
|
+
* useEffect(() => {
|
|
94
|
+
* return registerProgressCallback(({ awardId, progressPercentage }) => {
|
|
95
|
+
* setAwardProgress(prev => ({
|
|
96
|
+
* ...prev,
|
|
97
|
+
* [awardId]: progressPercentage
|
|
98
|
+
* }))
|
|
99
|
+
* })
|
|
100
|
+
* }, [])
|
|
101
|
+
*/
|
|
102
|
+
export function registerProgressCallback(callback) {
|
|
103
|
+
if (typeof callback !== 'function') {
|
|
104
|
+
throw new Error('registerProgressCallback requires a function')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
unregisterProgressCallback()
|
|
108
|
+
|
|
109
|
+
progressUpdateCallback = (payload) => {
|
|
110
|
+
callback({
|
|
111
|
+
awardId: payload.awardId,
|
|
112
|
+
progressPercentage: payload.progressPercentage
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
awardEvents.on('awardProgress', progressUpdateCallback)
|
|
117
|
+
|
|
118
|
+
return unregisterProgressCallback
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function unregisterProgressCallback() {
|
|
122
|
+
if (progressUpdateCallback) {
|
|
123
|
+
awardEvents.off('awardProgress', progressUpdateCallback)
|
|
124
|
+
progressUpdateCallback = null
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Awards
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import './types.js'
|
|
6
|
+
import { awardDefinitions } from './internal/award-definitions'
|
|
7
|
+
import { AwardMessageGenerator } from './internal/message-generator'
|
|
8
|
+
import db from '../sync/repository-proxy'
|
|
9
|
+
import UserAwardProgressRepository from '../sync/repositories/user-award-progress'
|
|
10
|
+
|
|
11
|
+
function enhanceCompletionData(completionData) {
|
|
12
|
+
if (!completionData) return null
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
...completionData,
|
|
16
|
+
message: AwardMessageGenerator.generatePopupMessage(completionData)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {number} contentId - Railcontent ID of the content item
|
|
22
|
+
* @returns {Promise<ContentAwardsResponse>} Status object with award information
|
|
23
|
+
*
|
|
24
|
+
* @example // Check if content has awards
|
|
25
|
+
* const { hasAwards, awards } = await getContentAwards(234567)
|
|
26
|
+
* if (hasAwards) {
|
|
27
|
+
* awards.forEach(award => {
|
|
28
|
+
* console.log(`${award.awardTitle}: ${award.progressPercentage}%`)
|
|
29
|
+
* })
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* @example // Display award progress on course page
|
|
33
|
+
* const { awards } = await getContentAwards(courseId)
|
|
34
|
+
* return (
|
|
35
|
+
* <div>
|
|
36
|
+
* {awards.map(award => (
|
|
37
|
+
* <AwardProgressBar
|
|
38
|
+
* key={award.awardId}
|
|
39
|
+
* title={award.awardTitle}
|
|
40
|
+
* progress={award.progressPercentage}
|
|
41
|
+
* badge={award.isCompleted ? award.badge : null}
|
|
42
|
+
* />
|
|
43
|
+
* ))}
|
|
44
|
+
* </div>
|
|
45
|
+
* )
|
|
46
|
+
*/
|
|
47
|
+
export async function getContentAwards(contentId) {
|
|
48
|
+
try {
|
|
49
|
+
const hasAwards = await awardDefinitions.hasAwards(contentId)
|
|
50
|
+
|
|
51
|
+
if (!hasAwards) {
|
|
52
|
+
return {
|
|
53
|
+
hasAwards: false,
|
|
54
|
+
awards: []
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { definitions, progress } = await db.userAwardProgress.getAwardsForContent(contentId)
|
|
59
|
+
|
|
60
|
+
const awards = definitions.map(def => {
|
|
61
|
+
const userProgress = progress.get(def._id)
|
|
62
|
+
const completionData = enhanceCompletionData(userProgress?.completion_data)
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
awardId: def._id,
|
|
66
|
+
awardTitle: def.name,
|
|
67
|
+
badge: def.badge,
|
|
68
|
+
award: def.award,
|
|
69
|
+
brand: def.brand,
|
|
70
|
+
instructorName: def.instructor_name,
|
|
71
|
+
progressPercentage: userProgress?.progress_percentage ?? 0,
|
|
72
|
+
isCompleted: userProgress ? UserAwardProgressRepository.isCompleted(userProgress) : false,
|
|
73
|
+
completedAt: userProgress?.completed_at
|
|
74
|
+
? new Date(userProgress.completed_at).toISOString()
|
|
75
|
+
: null,
|
|
76
|
+
completionData
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
hasAwards: true,
|
|
82
|
+
awards
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`Failed to get award status for content ${contentId}:`, error)
|
|
86
|
+
return {
|
|
87
|
+
hasAwards: false,
|
|
88
|
+
awards: []
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} [brand=null] - Brand to filter by (drumeo, pianote, guitareo, singeo), or null for all brands
|
|
95
|
+
* @param {AwardPaginationOptions} [options={}] - Optional pagination and filtering
|
|
96
|
+
* @returns {Promise<AwardInfo[]>} Array of completed award objects sorted by completion date
|
|
97
|
+
*
|
|
98
|
+
* @example // Display completed awards gallery
|
|
99
|
+
* const awards = await getCompletedAwards()
|
|
100
|
+
* return (
|
|
101
|
+
* <AwardsGallery>
|
|
102
|
+
* <h2>My Achievements ({awards.length})</h2>
|
|
103
|
+
* {awards.map(award => (
|
|
104
|
+
* <Badge
|
|
105
|
+
* key={award.awardId}
|
|
106
|
+
* image={award.badge}
|
|
107
|
+
* title={award.awardTitle}
|
|
108
|
+
* date={new Date(award.completedAt).toLocaleDateString()}
|
|
109
|
+
* />
|
|
110
|
+
* ))}
|
|
111
|
+
* </AwardsGallery>
|
|
112
|
+
* )
|
|
113
|
+
*
|
|
114
|
+
* @example // Paginated awards list
|
|
115
|
+
* const [page, setPage] = useState(0)
|
|
116
|
+
* const pageSize = 12
|
|
117
|
+
* const awards = await getCompletedAwards('drumeo', {
|
|
118
|
+
* limit: pageSize,
|
|
119
|
+
* offset: page * pageSize
|
|
120
|
+
* })
|
|
121
|
+
*
|
|
122
|
+
* @example // Filter by brand
|
|
123
|
+
* const pianoAwards = await getCompletedAwards('pianote')
|
|
124
|
+
* console.log(`You've earned ${pianoAwards.length} piano awards!`)
|
|
125
|
+
*/
|
|
126
|
+
export async function getCompletedAwards(brand = null, options = {}) {
|
|
127
|
+
try {
|
|
128
|
+
const allProgress = await db.userAwardProgress.getAll()
|
|
129
|
+
|
|
130
|
+
const completed = allProgress.data.filter(p =>
|
|
131
|
+
p.progress_percentage === 100 && p.completed_at !== null
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
let awards = await Promise.all(
|
|
135
|
+
completed.map(async (progress) => {
|
|
136
|
+
const definition = await awardDefinitions.getById(progress.award_id)
|
|
137
|
+
|
|
138
|
+
if (!definition) {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (brand && definition.brand !== brand) {
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const completionData = enhanceCompletionData(progress.completion_data)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
awardId: progress.award_id,
|
|
150
|
+
awardTitle: definition.name,
|
|
151
|
+
badge: definition.badge,
|
|
152
|
+
award: definition.award,
|
|
153
|
+
brand: definition.brand,
|
|
154
|
+
instructorName: definition.instructor_name,
|
|
155
|
+
progressPercentage: progress.progress_percentage,
|
|
156
|
+
isCompleted: true,
|
|
157
|
+
completedAt: new Date(progress.completed_at * 1000).toISOString(),
|
|
158
|
+
completionData
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
awards = awards.filter(award => award !== null)
|
|
164
|
+
|
|
165
|
+
awards.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime())
|
|
166
|
+
|
|
167
|
+
if (options.limit) {
|
|
168
|
+
const offset = options.offset || 0
|
|
169
|
+
awards = awards.slice(offset, offset + options.limit)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return awards
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error('Failed to get completed awards:', error)
|
|
175
|
+
return []
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {string} [brand=null] - Brand to filter by (drumeo, pianote, guitareo, singeo), or null for all brands
|
|
181
|
+
* @param {AwardPaginationOptions} [options={}] - Optional pagination options
|
|
182
|
+
* @returns {Promise<AwardInfo[]>} Array of in-progress award objects sorted by progress
|
|
183
|
+
*
|
|
184
|
+
* @example // Display in-progress awards dashboard
|
|
185
|
+
* const inProgress = await getInProgressAwards()
|
|
186
|
+
* return (
|
|
187
|
+
* <div>
|
|
188
|
+
* <h2>Keep Going! ({inProgress.length})</h2>
|
|
189
|
+
* {inProgress.map(award => (
|
|
190
|
+
* <ProgressCard
|
|
191
|
+
* key={award.awardId}
|
|
192
|
+
* title={award.awardTitle}
|
|
193
|
+
* progress={award.progressPercentage}
|
|
194
|
+
* badge={award.badge}
|
|
195
|
+
* />
|
|
196
|
+
* ))}
|
|
197
|
+
* </div>
|
|
198
|
+
* )
|
|
199
|
+
*
|
|
200
|
+
* @example // Show closest to completion
|
|
201
|
+
* const inProgress = await getInProgressAwards(null, { limit: 3 })
|
|
202
|
+
* console.log('Awards closest to completion:')
|
|
203
|
+
* inProgress.forEach(award => {
|
|
204
|
+
* console.log(`${award.awardTitle}: ${award.progressPercentage}% complete`)
|
|
205
|
+
* })
|
|
206
|
+
*
|
|
207
|
+
* @example // Filter by brand with pagination
|
|
208
|
+
* const guitarAwards = await getInProgressAwards('guitareo', {
|
|
209
|
+
* limit: 10,
|
|
210
|
+
* offset: 0
|
|
211
|
+
* })
|
|
212
|
+
*/
|
|
213
|
+
export async function getInProgressAwards(brand = null, options = {}) {
|
|
214
|
+
try {
|
|
215
|
+
const allProgress = await db.userAwardProgress.getAll()
|
|
216
|
+
const inProgress = allProgress.data.filter(p =>
|
|
217
|
+
p.progress_percentage > 0 && (p.progress_percentage < 100 || p.completed_at === null)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
let awards = await Promise.all(
|
|
221
|
+
inProgress.map(async (progress) => {
|
|
222
|
+
const definition = await awardDefinitions.getById(progress.award_id)
|
|
223
|
+
|
|
224
|
+
if (!definition) {
|
|
225
|
+
return null
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (brand && definition.brand !== brand) {
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const completionData = enhanceCompletionData(progress.completion_data)
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
awardId: progress.award_id,
|
|
236
|
+
awardTitle: definition.name,
|
|
237
|
+
badge: definition.badge,
|
|
238
|
+
award: definition.award,
|
|
239
|
+
brand: definition.brand,
|
|
240
|
+
instructorName: definition.instructor_name,
|
|
241
|
+
progressPercentage: progress.progress_percentage,
|
|
242
|
+
isCompleted: false,
|
|
243
|
+
completedAt: null,
|
|
244
|
+
completionData
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
awards = awards.filter(award => award !== null)
|
|
250
|
+
|
|
251
|
+
awards.sort((a, b) => b.progressPercentage - a.progressPercentage)
|
|
252
|
+
|
|
253
|
+
if (options.limit) {
|
|
254
|
+
const offset = options.offset || 0
|
|
255
|
+
awards = awards.slice(offset, offset + options.limit)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return awards
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error('Failed to get in-progress awards:', error)
|
|
261
|
+
return []
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* @param {string} [brand=null] - Brand to filter by (drumeo, pianote, guitareo, singeo), or null for all brands
|
|
267
|
+
* @returns {Promise<AwardStatistics>} Statistics object with award counts and completion percentage
|
|
268
|
+
*
|
|
269
|
+
* @example // Display stats widget
|
|
270
|
+
* const stats = await getAwardStatistics('drumeo')
|
|
271
|
+
* return (
|
|
272
|
+
* <StatsWidget>
|
|
273
|
+
* <Stat label="Earned" value={stats.completed} />
|
|
274
|
+
* <Stat label="In Progress" value={stats.inProgress} />
|
|
275
|
+
* <Stat label="Completion" value={`${stats.completionPercentage}%`} />
|
|
276
|
+
* </StatsWidget>
|
|
277
|
+
* )
|
|
278
|
+
*
|
|
279
|
+
* @example // Progress bar
|
|
280
|
+
* const stats = await getAwardStatistics()
|
|
281
|
+
* console.log(`${stats.completed}/${stats.totalAvailable} awards earned`)
|
|
282
|
+
* console.log(`${stats.completionPercentage}% complete`)
|
|
283
|
+
*/
|
|
284
|
+
export async function getAwardStatistics(brand = null) {
|
|
285
|
+
try {
|
|
286
|
+
let allDefinitions = await awardDefinitions.getAll()
|
|
287
|
+
|
|
288
|
+
if (brand) {
|
|
289
|
+
allDefinitions = allDefinitions.filter(def => def.brand === brand)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const definitionIds = new Set(allDefinitions.map(def => def._id))
|
|
293
|
+
|
|
294
|
+
const allProgress = await db.userAwardProgress.getAll()
|
|
295
|
+
const filteredProgress = allProgress.data.filter(p => definitionIds.has(p.award_id))
|
|
296
|
+
|
|
297
|
+
const completedCount = filteredProgress.filter(p =>
|
|
298
|
+
p.progress_percentage === 100 && p.completed_at !== null
|
|
299
|
+
).length
|
|
300
|
+
|
|
301
|
+
const inProgressCount = filteredProgress.filter(p =>
|
|
302
|
+
p.progress_percentage > 0 && (p.progress_percentage < 100 || p.completed_at === null)
|
|
303
|
+
).length
|
|
304
|
+
|
|
305
|
+
const totalAvailable = allDefinitions.length
|
|
306
|
+
const notStarted = totalAvailable - completedCount - inProgressCount
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
totalAvailable,
|
|
310
|
+
completed: completedCount,
|
|
311
|
+
inProgress: inProgressCount,
|
|
312
|
+
notStarted: notStarted > 0 ? notStarted : 0,
|
|
313
|
+
completionPercentage: totalAvailable > 0
|
|
314
|
+
? Math.round((completedCount / totalAvailable) * 100 * 10) / 10
|
|
315
|
+
: 0
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.error('Failed to get award statistics:', error)
|
|
319
|
+
return {
|
|
320
|
+
totalAvailable: 0,
|
|
321
|
+
completed: 0,
|
|
322
|
+
inProgress: 0,
|
|
323
|
+
notStarted: 0,
|
|
324
|
+
completionPercentage: 0
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.indexignore
|