musora-content-services 2.94.8 → 2.95.1
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 +18 -0
- package/CHANGELOG.md +18 -0
- package/CLAUDE.md +408 -0
- package/babel.config.cjs +10 -0
- package/jsdoc.json +2 -1
- package/package.json +2 -2
- package/src/constants/award-assets.js +35 -0
- package/src/filterBuilder.js +7 -2
- package/src/index.d.ts +26 -5
- package/src/index.js +26 -5
- package/src/services/awards/award-callbacks.js +165 -0
- package/src/services/awards/award-query.js +495 -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-org/learning-paths.ts +19 -15
- package/src/services/gamification/awards.ts +114 -83
- package/src/services/progress-events.js +58 -0
- package/src/services/progress-row/method-card.js +20 -5
- package/src/services/sanity.js +1 -1
- package/src/services/sync/fetch.ts +10 -2
- package/src/services/sync/manager.ts +6 -0
- package/src/services/sync/models/ContentProgress.ts +5 -6
- package/src/services/sync/models/UserAwardProgress.ts +55 -0
- package/src/services/sync/models/index.ts +1 -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/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/retry.ts +12 -11
- package/src/services/sync/schema/index.ts +18 -3
- 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/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/initializeTests.js +6 -0
- package/test/mockData/award-definitions.js +171 -0
- package/test/sync/models/award-database-integration.test.js +519 -0
- package/tools/generate-index.cjs +9 -0
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,
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
* @description
|
|
16
|
+
* Registers a callback to be notified when the user earns a new award. Only one
|
|
17
|
+
* callback can be registered at a time - registering a new one replaces the previous.
|
|
18
|
+
* Always call the returned cleanup function when your component unmounts.
|
|
19
|
+
*
|
|
20
|
+
* The callback receives an award object with:
|
|
21
|
+
* - `awardId` - Unique Sanity award ID
|
|
22
|
+
* - `name` - Display name of the award
|
|
23
|
+
* - `badge` - URL to badge image
|
|
24
|
+
* - `completed_at` - ISO timestamp
|
|
25
|
+
* - `completion_data.message` - Pre-generated congratulations message
|
|
26
|
+
* - `completion_data.practice_minutes` - Total practice time
|
|
27
|
+
* - `completion_data.days_user_practiced` - Days spent practicing
|
|
28
|
+
* - `completion_data.content_title` - Title of completed content
|
|
29
|
+
*
|
|
30
|
+
* @example // React Native - Show award celebration modal
|
|
31
|
+
* function useAwardNotification() {
|
|
32
|
+
* const [award, setAward] = useState(null)
|
|
33
|
+
*
|
|
34
|
+
* useEffect(() => {
|
|
35
|
+
* return registerAwardCallback((awardData) => {
|
|
36
|
+
* setAward({
|
|
37
|
+
* title: awardData.name,
|
|
38
|
+
* badge: awardData.badge,
|
|
39
|
+
* message: awardData.completion_data.message,
|
|
40
|
+
* practiceMinutes: awardData.completion_data.practice_minutes
|
|
41
|
+
* })
|
|
42
|
+
* })
|
|
43
|
+
* }, [])
|
|
44
|
+
*
|
|
45
|
+
* return { award, dismissAward: () => setAward(null) }
|
|
46
|
+
* }
|
|
47
|
+
*
|
|
48
|
+
* @example // Track award in analytics
|
|
49
|
+
* useEffect(() => {
|
|
50
|
+
* return registerAwardCallback((award) => {
|
|
51
|
+
* analytics.track('Award Earned', {
|
|
52
|
+
* awardId: award.awardId,
|
|
53
|
+
* awardName: award.name,
|
|
54
|
+
* practiceMinutes: award.completion_data.practice_minutes,
|
|
55
|
+
* contentTitle: award.completion_data.content_title
|
|
56
|
+
* })
|
|
57
|
+
* })
|
|
58
|
+
* }, [])
|
|
59
|
+
*/
|
|
60
|
+
export function registerAwardCallback(callback) {
|
|
61
|
+
if (typeof callback !== 'function') {
|
|
62
|
+
throw new Error('registerAwardCallback requires a function')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
unregisterAwardCallback()
|
|
66
|
+
|
|
67
|
+
awardGrantedCallback = async (payload) => {
|
|
68
|
+
const { awardId, definition, completionData, popupMessage } = payload
|
|
69
|
+
|
|
70
|
+
const award = {
|
|
71
|
+
awardId: awardId,
|
|
72
|
+
name: definition.name,
|
|
73
|
+
badge: definition.badge,
|
|
74
|
+
completed_at: completionData.completed_at,
|
|
75
|
+
completion_data: {
|
|
76
|
+
completed_at: completionData.completed_at,
|
|
77
|
+
days_user_practiced: completionData.days_user_practiced,
|
|
78
|
+
message: popupMessage,
|
|
79
|
+
practice_minutes: completionData.practice_minutes,
|
|
80
|
+
content_title: completionData.content_title
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
callback(award)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
awardEvents.on('awardGranted', awardGrantedCallback)
|
|
88
|
+
|
|
89
|
+
return unregisterAwardCallback
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function unregisterAwardCallback() {
|
|
93
|
+
if (awardGrantedCallback) {
|
|
94
|
+
awardEvents.off('awardGranted', awardGrantedCallback)
|
|
95
|
+
awardGrantedCallback = null
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {ProgressCallbackFunction} callback - Function called with progress data when award progress changes
|
|
101
|
+
* @returns {UnregisterFunction} Cleanup function to unregister this callback
|
|
102
|
+
*
|
|
103
|
+
* @description
|
|
104
|
+
* Registers a callback to be notified when award progress changes (but award is not
|
|
105
|
+
* yet complete). Only one callback can be registered at a time. Use this to update
|
|
106
|
+
* progress bars or show "almost there" encouragement.
|
|
107
|
+
*
|
|
108
|
+
* The callback receives:
|
|
109
|
+
* - `awardId` - Unique Sanity award ID
|
|
110
|
+
* - `progressPercentage` - Current completion percentage (0-99)
|
|
111
|
+
*
|
|
112
|
+
* Note: When an award reaches 100%, `registerAwardCallback` fires instead.
|
|
113
|
+
*
|
|
114
|
+
* @example // React Native - Update progress in learning path screen
|
|
115
|
+
* function LearningPathScreen({ learningPathId }) {
|
|
116
|
+
* const [awardProgress, setAwardProgress] = useState({})
|
|
117
|
+
*
|
|
118
|
+
* useEffect(() => {
|
|
119
|
+
* return registerProgressCallback(({ awardId, progressPercentage }) => {
|
|
120
|
+
* setAwardProgress(prev => ({
|
|
121
|
+
* ...prev,
|
|
122
|
+
* [awardId]: progressPercentage
|
|
123
|
+
* }))
|
|
124
|
+
* })
|
|
125
|
+
* }, [])
|
|
126
|
+
*
|
|
127
|
+
* // Use awardProgress to update UI
|
|
128
|
+
* }
|
|
129
|
+
*
|
|
130
|
+
* @example // Show encouragement toast at milestones
|
|
131
|
+
* useEffect(() => {
|
|
132
|
+
* return registerProgressCallback(({ awardId, progressPercentage }) => {
|
|
133
|
+
* if (progressPercentage === 50) {
|
|
134
|
+
* showToast('Halfway to your award!')
|
|
135
|
+
* } else if (progressPercentage >= 90) {
|
|
136
|
+
* showToast('Almost there! Just a few more lessons.')
|
|
137
|
+
* }
|
|
138
|
+
* })
|
|
139
|
+
* }, [])
|
|
140
|
+
*/
|
|
141
|
+
export function registerProgressCallback(callback) {
|
|
142
|
+
if (typeof callback !== 'function') {
|
|
143
|
+
throw new Error('registerProgressCallback requires a function')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
unregisterProgressCallback()
|
|
147
|
+
|
|
148
|
+
progressUpdateCallback = (payload) => {
|
|
149
|
+
callback({
|
|
150
|
+
awardId: payload.awardId,
|
|
151
|
+
progressPercentage: payload.progressPercentage
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
awardEvents.on('awardProgress', progressUpdateCallback)
|
|
156
|
+
|
|
157
|
+
return unregisterProgressCallback
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function unregisterProgressCallback() {
|
|
161
|
+
if (progressUpdateCallback) {
|
|
162
|
+
awardEvents.off('awardProgress', progressUpdateCallback)
|
|
163
|
+
progressUpdateCallback = null
|
|
164
|
+
}
|
|
165
|
+
}
|