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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { awardManager } from './award-manager'
|
|
2
|
+
import { awardDefinitions } from './award-definitions'
|
|
3
|
+
import { onProgressSaved } from '../../progress-events'
|
|
4
|
+
import { COLLECTION_TYPE } from '../../sync/models/ContentProgress'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ContentProgressObserver {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.subscription = null
|
|
10
|
+
this.progressEventUnsubscribe = null
|
|
11
|
+
this.isObserving = false
|
|
12
|
+
this.processingContentIds = new Set()
|
|
13
|
+
this.debounceTimers = new Map()
|
|
14
|
+
this.debounceMs = 50
|
|
15
|
+
this.allChildIds = new Set()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @returns {Promise<() => void>} */
|
|
19
|
+
async start(database) {
|
|
20
|
+
if (this.isObserving) {
|
|
21
|
+
return () => {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await awardDefinitions.refresh()
|
|
25
|
+
const allAwards = await awardDefinitions.getAll()
|
|
26
|
+
|
|
27
|
+
this.allChildIds.clear()
|
|
28
|
+
for (const award of allAwards) {
|
|
29
|
+
if (award.content_id) {
|
|
30
|
+
this.allChildIds.add(award.content_id)
|
|
31
|
+
}
|
|
32
|
+
if (award.child_ids) {
|
|
33
|
+
award.child_ids.forEach(childId => this.allChildIds.add(childId))
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (this.allChildIds.size === 0) {
|
|
38
|
+
return () => {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.progressEventUnsubscribe = onProgressSaved((event) => {
|
|
42
|
+
if (this.allChildIds.has(event.contentId)) {
|
|
43
|
+
this.handleProgressChange({
|
|
44
|
+
content_id: event.contentId,
|
|
45
|
+
state: event.progressStatus,
|
|
46
|
+
progress_percent: event.progressPercent,
|
|
47
|
+
collection_type: event.collectionType,
|
|
48
|
+
collection_id: event.collectionId
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
this.isObserving = true
|
|
54
|
+
|
|
55
|
+
return () => this.stop()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @returns {Promise<void>} */
|
|
59
|
+
async handleProgressChange(progressRecord) {
|
|
60
|
+
try {
|
|
61
|
+
const childContentId = progressRecord.content_id
|
|
62
|
+
const collectionType = progressRecord.collection_type
|
|
63
|
+
const collectionId = progressRecord.collection_id
|
|
64
|
+
|
|
65
|
+
const allAwards = await awardDefinitions.getAll()
|
|
66
|
+
let parentAwards = []
|
|
67
|
+
|
|
68
|
+
const isLearningPathContext = collectionType === COLLECTION_TYPE.LEARNING_PATH && collectionId
|
|
69
|
+
if (isLearningPathContext) {
|
|
70
|
+
parentAwards = allAwards.filter(award => {
|
|
71
|
+
return award.child_ids?.includes(childContentId) &&
|
|
72
|
+
award.content_type === collectionType &&
|
|
73
|
+
award.content_id === collectionId
|
|
74
|
+
})
|
|
75
|
+
} else {
|
|
76
|
+
parentAwards = allAwards.filter(award => award.child_ids?.includes(childContentId))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const award of parentAwards) {
|
|
80
|
+
if (award.content_id) {
|
|
81
|
+
this.debounceAwardCheck(award.content_id)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('[ContentProgressObserver] Error handling progress change:', error)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
debounceAwardCheck(contentId) {
|
|
90
|
+
if (this.debounceTimers.has(contentId)) {
|
|
91
|
+
clearTimeout(this.debounceTimers.get(contentId))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const timerId = setTimeout(async () => {
|
|
95
|
+
this.debounceTimers.delete(contentId)
|
|
96
|
+
await this.checkAward(contentId)
|
|
97
|
+
}, this.debounceMs)
|
|
98
|
+
|
|
99
|
+
this.debounceTimers.set(contentId, timerId)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @returns {Promise<void>} */
|
|
103
|
+
async checkAward(contentId) {
|
|
104
|
+
if (this.processingContentIds.has(contentId)) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.processingContentIds.add(contentId)
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await awardManager.onContentCompleted(contentId)
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(`[ContentProgressObserver] Error checking award for content ${contentId}:`, error)
|
|
114
|
+
} finally {
|
|
115
|
+
this.processingContentIds.delete(contentId)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
stop() {
|
|
120
|
+
if (this.progressEventUnsubscribe) {
|
|
121
|
+
this.progressEventUnsubscribe()
|
|
122
|
+
this.progressEventUnsubscribe = null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const timerId of this.debounceTimers.values()) {
|
|
126
|
+
clearTimeout(timerId)
|
|
127
|
+
}
|
|
128
|
+
this.debounceTimers.clear()
|
|
129
|
+
this.processingContentIds.clear()
|
|
130
|
+
this.allChildIds.clear()
|
|
131
|
+
|
|
132
|
+
this.isObserving = false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
export const contentProgressObserver = new ContentProgressObserver()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Awards
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} url
|
|
8
|
+
* @returns {Promise<string>}
|
|
9
|
+
*/
|
|
10
|
+
export async function urlToBase64(url) {
|
|
11
|
+
try {
|
|
12
|
+
if (!url) {
|
|
13
|
+
return ''
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const response = await fetch(url)
|
|
17
|
+
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
console.warn(`Failed to fetch image: ${url}`)
|
|
20
|
+
return ''
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const blob = await response.blob()
|
|
24
|
+
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const reader = new FileReader()
|
|
27
|
+
|
|
28
|
+
reader.onloadend = () => {
|
|
29
|
+
const base64String = reader.result
|
|
30
|
+
const base64Data = base64String.split(',')[1]
|
|
31
|
+
resolve(base64Data || '')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
reader.onerror = () => {
|
|
35
|
+
console.error(`Failed to convert image to base64: ${url}`)
|
|
36
|
+
reject(new Error('Failed to convert image to base64'))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
reader.readAsDataURL(blob)
|
|
40
|
+
})
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`Error converting URL to base64: ${url}`, error)
|
|
43
|
+
return ''
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {Object<string, string>} urlMap
|
|
49
|
+
* @returns {Promise<Object<string, string>>}
|
|
50
|
+
*/
|
|
51
|
+
export async function urlMapToBase64(urlMap) {
|
|
52
|
+
const entries = Object.entries(urlMap)
|
|
53
|
+
|
|
54
|
+
const results = await Promise.all(
|
|
55
|
+
entries.map(async ([key, url]) => {
|
|
56
|
+
const base64 = await urlToBase64(url)
|
|
57
|
+
return [key, base64]
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return Object.fromEntries(results)
|
|
62
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class AwardMessageGenerator {
|
|
2
|
+
/** @returns {string} */
|
|
3
|
+
static generatePopupMessage(completionData) {
|
|
4
|
+
const { days_user_practiced, practice_minutes, content_title } = completionData
|
|
5
|
+
const daysLabel = days_user_practiced === 1 ? 'day' : 'days'
|
|
6
|
+
|
|
7
|
+
return `You received this award for completing ${content_title}! You practiced a total of ${practice_minutes} minutes over the past ${days_user_practiced} ${daysLabel}.`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** @returns {string} */
|
|
11
|
+
static generateCertificateMessage(completionData, awardCustomText) {
|
|
12
|
+
const { content_title, practice_minutes } = completionData
|
|
13
|
+
const customText = awardCustomText ? ` ${awardCustomText}` : ''
|
|
14
|
+
|
|
15
|
+
return `You practiced for a total of ${practice_minutes} minutes during ${content_title}, earning your official certificate of completion.${customText} Well Done!`
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export interface AwardDefinition {
|
|
2
|
+
_id: string
|
|
3
|
+
is_active: boolean
|
|
4
|
+
name: string
|
|
5
|
+
logo: string | null
|
|
6
|
+
badge: string
|
|
7
|
+
award: string
|
|
8
|
+
content_id: number
|
|
9
|
+
content_type: string
|
|
10
|
+
type: string
|
|
11
|
+
brand: string
|
|
12
|
+
content_title: string
|
|
13
|
+
award_custom_text: string | null
|
|
14
|
+
instructor_name: string | null
|
|
15
|
+
child_ids: number[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CompletionData {
|
|
19
|
+
content_title: string
|
|
20
|
+
completed_at: string
|
|
21
|
+
days_user_practiced: number
|
|
22
|
+
practice_minutes: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AwardCompletionData {
|
|
26
|
+
completed_at: string
|
|
27
|
+
days_user_practiced: number
|
|
28
|
+
practice_minutes: number
|
|
29
|
+
content_title: string
|
|
30
|
+
message: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AwardInfo {
|
|
34
|
+
awardId: string
|
|
35
|
+
awardTitle: string
|
|
36
|
+
badge: string
|
|
37
|
+
award: string
|
|
38
|
+
brand: string
|
|
39
|
+
instructorName: string
|
|
40
|
+
progressPercentage: number
|
|
41
|
+
isCompleted: boolean
|
|
42
|
+
completedAt: string | null
|
|
43
|
+
completionData: AwardCompletionData | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ContentAwardsResponse {
|
|
47
|
+
hasAwards: boolean
|
|
48
|
+
awards: AwardInfo[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface AwardStatistics {
|
|
52
|
+
totalAvailable: number
|
|
53
|
+
completed: number
|
|
54
|
+
inProgress: number
|
|
55
|
+
notStarted: number
|
|
56
|
+
completionPercentage: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface AwardPaginationOptions {
|
|
60
|
+
limit?: number
|
|
61
|
+
offset?: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface AwardCallbackPayload {
|
|
65
|
+
awardId: string
|
|
66
|
+
name: string
|
|
67
|
+
badge: string
|
|
68
|
+
completed_at: string
|
|
69
|
+
completion_data: AwardCompletionData
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ProgressCallbackPayload {
|
|
73
|
+
awardId: string
|
|
74
|
+
progressPercentage: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type AwardCallbackFunction = (award: AwardCallbackPayload) => void
|
|
78
|
+
export type ProgressCallbackFunction = (progress: ProgressCallbackPayload) => void
|
|
79
|
+
export type UnregisterFunction = () => void
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} AwardDefinition
|
|
3
|
+
* @property {string} _id - Unique Sanity award ID
|
|
4
|
+
* @property {boolean} is_active - Whether the award is active
|
|
5
|
+
* @property {string} name - Display name of the award
|
|
6
|
+
* @property {string|null} logo - URL to logo image
|
|
7
|
+
* @property {string} badge - URL to badge image
|
|
8
|
+
* @property {string} award - URL to award image
|
|
9
|
+
* @property {number} content_id - Railcontent ID of the parent content
|
|
10
|
+
* @property {string} content_type - Type of content (guided-course, learning-path-v2, etc.)
|
|
11
|
+
* @property {string} type - Sanity document type
|
|
12
|
+
* @property {string} brand - Brand (drumeo, pianote, guitareo, singeo)
|
|
13
|
+
* @property {string} content_title - Title of the associated content
|
|
14
|
+
* @property {string|null} award_custom_text - Custom text for the award
|
|
15
|
+
* @property {string|null} instructor_name - Name of the instructor
|
|
16
|
+
* @property {number[]} child_ids - Array of child content IDs required for completion
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} CompletionData
|
|
21
|
+
* @property {string} content_title - Title of the completed content
|
|
22
|
+
* @property {string} completed_at - ISO timestamp of completion
|
|
23
|
+
* @property {number} days_user_practiced - Number of days the user practiced
|
|
24
|
+
* @property {number} practice_minutes - Total practice time in minutes
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} AwardCompletionData
|
|
29
|
+
* @property {string} completed_at - ISO timestamp of completion
|
|
30
|
+
* @property {number} days_user_practiced - Number of days the user practiced
|
|
31
|
+
* @property {number} practice_minutes - Total practice time in minutes
|
|
32
|
+
* @property {string} content_title - Title of the completed content
|
|
33
|
+
* @property {string} message - Congratulations message for display
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} AwardInfo
|
|
38
|
+
* @property {string} awardId - Unique Sanity award ID
|
|
39
|
+
* @property {string} awardTitle - Display name of the award
|
|
40
|
+
* @property {string} badge - URL to badge image
|
|
41
|
+
* @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 {number} progressPercentage - Completion percentage (0-100)
|
|
45
|
+
* @property {boolean} isCompleted - Whether the award is fully completed
|
|
46
|
+
* @property {string|null} completedAt - ISO timestamp of completion, or null if not completed
|
|
47
|
+
* @property {AwardCompletionData|null} completionData - Practice statistics, or null if not started
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {Object} ContentAwardsResponse
|
|
52
|
+
* @property {boolean} hasAwards - Whether the content has any associated awards
|
|
53
|
+
* @property {AwardInfo[]} awards - Array of award objects with progress information
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} AwardStatistics
|
|
58
|
+
* @property {number} totalAvailable - Total number of awards available
|
|
59
|
+
* @property {number} completed - Number of completed awards
|
|
60
|
+
* @property {number} inProgress - Number of awards in progress
|
|
61
|
+
* @property {number} notStarted - Number of awards not yet started
|
|
62
|
+
* @property {number} completionPercentage - Overall completion percentage (0-100)
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @typedef {Object} AwardPaginationOptions
|
|
67
|
+
* @property {number} [limit] - Maximum number of results to return
|
|
68
|
+
* @property {number} [offset] - Number of results to skip for pagination
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {Object} AwardCallbackPayload
|
|
73
|
+
* @property {string} awardId - Unique Sanity award ID
|
|
74
|
+
* @property {string} name - Display name of the award
|
|
75
|
+
* @property {string} badge - URL to badge image
|
|
76
|
+
* @property {string} completed_at - ISO timestamp of completion
|
|
77
|
+
* @property {AwardCompletionData} completion_data - Practice statistics
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @typedef {Object} ProgressCallbackPayload
|
|
82
|
+
* @property {string} awardId - Unique Sanity award ID
|
|
83
|
+
* @property {number} progressPercentage - Completion percentage (0-100)
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @callback AwardCallbackFunction
|
|
88
|
+
* @param {AwardCallbackPayload} award - Award data when an award is earned
|
|
89
|
+
* @returns {void}
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @callback ProgressCallbackFunction
|
|
94
|
+
* @param {ProgressCallbackPayload} progress - Progress data when award progress changes
|
|
95
|
+
* @returns {void}
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @callback UnregisterFunction
|
|
100
|
+
* @returns {void}
|
|
101
|
+
*/
|
package/src/services/config.js
CHANGED
|
@@ -25,11 +25,11 @@ const excludeFromGeneratedIndex = []
|
|
|
25
25
|
/**
|
|
26
26
|
* Initializes the service with the given configuration.
|
|
27
27
|
* This function must be called before using any other functions in this library.
|
|
28
|
+
* Automatically initializes award definitions with 24-hour cache in the background.
|
|
28
29
|
*
|
|
29
30
|
* @param {Config} config - Configuration object containing API settings.
|
|
30
31
|
*
|
|
31
|
-
* @example
|
|
32
|
-
* // Initialize the service in your app.js
|
|
32
|
+
* @example Web Application
|
|
33
33
|
* initializeService({
|
|
34
34
|
* sanityConfig: {
|
|
35
35
|
* token: 'your-sanity-api-token',
|
|
@@ -43,18 +43,30 @@ const excludeFromGeneratedIndex = []
|
|
|
43
43
|
* token: 'your-user-api-token',
|
|
44
44
|
* userId: 'current-user-id',
|
|
45
45
|
* baseUrl: 'https://web-staging-one.musora.com',
|
|
46
|
-
* authToken 'your-auth-token',
|
|
46
|
+
* authToken: 'your-auth-token',
|
|
47
47
|
* },
|
|
48
48
|
* sessionConfig: {
|
|
49
49
|
* token: 'your-user-api-token',
|
|
50
50
|
* userId: 'current-user-id',
|
|
51
|
-
* authToken 'your-auth-token',
|
|
51
|
+
* authToken: 'your-auth-token',
|
|
52
52
|
* },
|
|
53
53
|
* baseUrl: 'https://web-staging-one.musora.com',
|
|
54
54
|
* localStorage: localStorage,
|
|
55
55
|
* isMA: false,
|
|
56
56
|
* permissionsVersion: 'v1', // Optional: 'v1' (default) or 'v2'
|
|
57
57
|
* });
|
|
58
|
+
*
|
|
59
|
+
* @example React Native Application
|
|
60
|
+
* import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
61
|
+
*
|
|
62
|
+
* initializeService({
|
|
63
|
+
* sanityConfig: { ... },
|
|
64
|
+
* railcontentConfig: { ... },
|
|
65
|
+
* sessionConfig: { ... },
|
|
66
|
+
* baseUrl: 'https://web-staging-one.musora.com',
|
|
67
|
+
* localStorage: AsyncStorage,
|
|
68
|
+
* isMA: true,
|
|
69
|
+
* });
|
|
58
70
|
*/
|
|
59
71
|
export function initializeService(config) {
|
|
60
72
|
globalConfig.sanityConfig = config.sanityConfig
|
|
@@ -65,4 +77,12 @@ export function initializeService(config) {
|
|
|
65
77
|
globalConfig.isMA = config.isMA || false
|
|
66
78
|
globalConfig.localTimezoneString = config.localTimezoneString || null
|
|
67
79
|
globalConfig.permissionsVersion = config.permissionsVersion || 'v2'
|
|
80
|
+
|
|
81
|
+
if (config.localStorage) {
|
|
82
|
+
import('./awards/internal/award-definitions')
|
|
83
|
+
.then(({ awardDefinitions }) => awardDefinitions.initialize())
|
|
84
|
+
.catch(error => {
|
|
85
|
+
console.error('Failed to initialize award definitions:', error)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
68
88
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -173,7 +173,7 @@ export async function fetchLearningPathLessons(
|
|
|
173
173
|
userDate: Date
|
|
174
174
|
) {
|
|
175
175
|
const learningPath = await getEnrichedLearningPath(learningPathId)
|
|
176
|
-
let dailySession = await getDailySession(brand, userDate)
|
|
176
|
+
let dailySession = await getDailySession(brand, userDate) // what if the call just fails, and a DS does exist?
|
|
177
177
|
if (!dailySession) {
|
|
178
178
|
dailySession = await updateDailySession(brand, userDate, false)
|
|
179
179
|
}
|
|
@@ -185,19 +185,22 @@ export async function fetchLearningPathLessons(
|
|
|
185
185
|
is_active_learning_path: isActiveLearningPath,
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
|
+
// this assumes that the first entry is active_path, based on user flows
|
|
188
189
|
const todayContentIds = dailySession.daily_session[0]?.content_ids || []
|
|
189
|
-
const
|
|
190
|
+
const todayLearningPathId = dailySession.daily_session[0]?.learning_path_id
|
|
191
|
+
|
|
190
192
|
const nextContentIds = dailySession.daily_session[1]?.content_ids || []
|
|
191
193
|
const nextLearningPathId = dailySession.daily_session[1]?.learning_path_id
|
|
194
|
+
|
|
192
195
|
const completedLessons = []
|
|
193
|
-
let
|
|
194
|
-
let
|
|
196
|
+
let thisLPDailies = []
|
|
197
|
+
let nextLPDailies = []
|
|
195
198
|
let previousLearningPathTodays = []
|
|
196
199
|
const upcomingLessons = []
|
|
197
200
|
|
|
198
201
|
learningPath.children.forEach((lesson: any) => {
|
|
199
202
|
if (todayContentIds.includes(lesson.id)) {
|
|
200
|
-
|
|
203
|
+
thisLPDailies.push(lesson)
|
|
201
204
|
} else if (lesson.progressStatus === 'completed') {
|
|
202
205
|
completedLessons.push(lesson)
|
|
203
206
|
} else {
|
|
@@ -205,22 +208,23 @@ export async function fetchLearningPathLessons(
|
|
|
205
208
|
}
|
|
206
209
|
})
|
|
207
210
|
|
|
208
|
-
if (
|
|
211
|
+
if (thisLPDailies.length == 0) {
|
|
209
212
|
// Daily sessions first lessons are not part of the active learning path, but next lessons are
|
|
210
213
|
// load todays lessons from previous learning path
|
|
211
214
|
previousLearningPathTodays = await getLearningPathLessonsByIds(
|
|
212
215
|
todayContentIds,
|
|
213
|
-
|
|
216
|
+
todayLearningPathId
|
|
214
217
|
)
|
|
215
|
-
} else if (
|
|
216
|
-
nextContentIds.length > 0
|
|
217
|
-
todaysLessons.length < 3 &&
|
|
218
|
-
upcomingLessons.length === 0
|
|
218
|
+
} else if ( // show next LP dailies if they exist
|
|
219
|
+
nextContentIds.length > 0
|
|
219
220
|
) {
|
|
220
221
|
// Daily sessions first lessons are the active learning path and the next lessons are not
|
|
221
222
|
// load next lessons from next learning path
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
const lessons = await getLearningPathLessonsByIds(nextContentIds, nextLearningPathId)
|
|
224
|
+
nextLPDailies = lessons.map(lesson => ({
|
|
225
|
+
...lesson,
|
|
226
|
+
in_next_learning_path: STATE.COMPLETED === learningPath.progressStatus
|
|
227
|
+
}))
|
|
224
228
|
}
|
|
225
229
|
|
|
226
230
|
|
|
@@ -231,8 +235,8 @@ export async function fetchLearningPathLessons(
|
|
|
231
235
|
active_learning_path_id: dailySession?.active_learning_path_id,
|
|
232
236
|
active_learning_path_created_at: dailySession?.active_learning_path_created_at,
|
|
233
237
|
upcoming_lessons: upcomingLessons,
|
|
234
|
-
todays_lessons:
|
|
235
|
-
next_learning_path_lessons:
|
|
238
|
+
todays_lessons: thisLPDailies,
|
|
239
|
+
next_learning_path_lessons: nextLPDailies,
|
|
236
240
|
next_learning_path_id: nextLearningPathId,
|
|
237
241
|
completed_lessons: completedLessons,
|
|
238
242
|
previous_learning_path_todays: previousLearningPathTodays,
|
|
File without changes
|
|
File without changes
|
package/src/services/content.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -432,9 +432,10 @@ function normalizeContentIds(contentIds) {
|
|
|
432
432
|
function normalizeCollection(collection) {
|
|
433
433
|
if (!collection) return null
|
|
434
434
|
|
|
435
|
-
if (COLLECTION_TYPE.
|
|
435
|
+
if (!Object.values(COLLECTION_TYPE).includes(collection.type)) {
|
|
436
436
|
throw new Error(`Invalid collection type: ${collection.type}`)
|
|
437
437
|
}
|
|
438
|
+
|
|
438
439
|
if (typeof collection.id === 'string' && isNaN(+collection.id)) {
|
|
439
440
|
throw new Error(`Invalid collection id: ${collection.id}`)
|
|
440
441
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|