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.
Files changed (210) hide show
  1. package/.coderabbit.yaml +0 -0
  2. package/.editorconfig +0 -0
  3. package/.github/pull_request_template.md +0 -0
  4. package/.github/workflows/conventional-commits.yaml +0 -0
  5. package/.github/workflows/docs.js.yml +0 -0
  6. package/.github/workflows/node.js.yml +0 -0
  7. package/.prettierignore +0 -0
  8. package/.prettierrc +0 -0
  9. package/CHANGELOG.md +23 -0
  10. package/CLAUDE.md +408 -0
  11. package/README.md +0 -0
  12. package/babel.config.cjs +10 -0
  13. package/jest.config.js +0 -0
  14. package/jsdoc.json +2 -1
  15. package/package.json +2 -2
  16. package/src/constants/award-assets.js +35 -0
  17. package/src/contentMetaData.js +0 -0
  18. package/src/filterBuilder.js +7 -2
  19. package/src/index.d.ts +26 -5
  20. package/src/index.js +26 -5
  21. package/src/infrastructure/http/HttpClient.ts +0 -0
  22. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
  23. package/src/infrastructure/http/index.ts +0 -0
  24. package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
  25. package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
  26. package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
  27. package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
  28. package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
  29. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
  30. package/src/lib/brands.ts +0 -0
  31. package/src/lib/httpHelper.js +0 -0
  32. package/src/lib/lastUpdated.js +0 -0
  33. package/src/lib/sanity/query.ts +0 -0
  34. package/src/services/api/types.js +0 -0
  35. package/src/services/api/types.ts +0 -0
  36. package/src/services/awards/award-callbacks.js +126 -0
  37. package/src/services/awards/award-query.js +327 -0
  38. package/src/services/awards/internal/.indexignore +1 -0
  39. package/src/services/awards/internal/award-definitions.js +239 -0
  40. package/src/services/awards/internal/award-events.js +102 -0
  41. package/src/services/awards/internal/award-manager.js +162 -0
  42. package/src/services/awards/internal/certificate-builder.js +66 -0
  43. package/src/services/awards/internal/completion-data-generator.js +84 -0
  44. package/src/services/awards/internal/content-progress-observer.js +137 -0
  45. package/src/services/awards/internal/image-utils.js +62 -0
  46. package/src/services/awards/internal/message-generator.js +17 -0
  47. package/src/services/awards/internal/types.js +5 -0
  48. package/src/services/awards/types.d.ts +79 -0
  49. package/src/services/awards/types.js +101 -0
  50. package/src/services/config.js +24 -4
  51. package/src/services/content/artist.ts +0 -0
  52. package/src/services/content/content.ts +0 -0
  53. package/src/services/content/genre.ts +0 -0
  54. package/src/services/content/instructor.ts +0 -0
  55. package/src/services/content-org/content-org.js +0 -0
  56. package/src/services/content-org/guided-courses.ts +0 -0
  57. package/src/services/content-org/learning-paths.ts +19 -15
  58. package/src/services/content-org/playlists-types.js +0 -0
  59. package/src/services/content-org/playlists.js +0 -0
  60. package/src/services/content.js +0 -0
  61. package/src/services/contentAggregator.js +0 -0
  62. package/src/services/contentLikes.js +0 -0
  63. package/src/services/contentProgress.js +2 -1
  64. package/src/services/dataContext.js +0 -0
  65. package/src/services/dateUtils.js +0 -0
  66. package/src/services/eventsAPI.js +0 -0
  67. package/src/services/forums/forums.ts +0 -0
  68. package/src/services/forums/posts.ts +0 -0
  69. package/src/services/forums/threads.ts +0 -0
  70. package/src/services/forums/types.ts +0 -0
  71. package/src/services/gamification/awards.ts +114 -83
  72. package/src/services/gamification/gamification.js +0 -0
  73. package/src/services/imageSRCBuilder.js +0 -0
  74. package/src/services/imageSRCVerify.js +0 -0
  75. package/src/services/liveTesting.ts +0 -0
  76. package/src/services/permissions/PermissionsAdapter.ts +0 -0
  77. package/src/services/permissions/PermissionsAdapterFactory.ts +0 -0
  78. package/src/services/permissions/PermissionsV1Adapter.ts +0 -0
  79. package/src/services/permissions/PermissionsV2Adapter.ts +0 -0
  80. package/src/services/permissions/README.md +0 -0
  81. package/src/services/permissions/index.ts +0 -0
  82. package/src/services/progress-events.js +58 -0
  83. package/src/services/progress-row/method-card.js +20 -5
  84. package/src/services/railcontent.js +0 -0
  85. package/src/services/recommendations.js +0 -0
  86. package/src/services/reporting/README.md +0 -0
  87. package/src/services/reporting/reporting.ts +0 -0
  88. package/src/services/reporting/types.ts +0 -0
  89. package/src/services/sanity.js +1 -1
  90. package/src/services/sentry/.indexignore +0 -0
  91. package/src/services/sentry/index.ts +0 -0
  92. package/src/services/sync/.indexignore +0 -0
  93. package/src/services/sync/adapters/factory.ts +0 -0
  94. package/src/services/sync/adapters/lokijs.ts +0 -0
  95. package/src/services/sync/adapters/sqlite.ts +0 -0
  96. package/src/services/sync/concurrency-safety.ts +0 -0
  97. package/src/services/sync/context/index.ts +0 -0
  98. package/src/services/sync/context/providers/base.ts +0 -0
  99. package/src/services/sync/context/providers/connectivity.ts +0 -0
  100. package/src/services/sync/context/providers/durability.ts +0 -0
  101. package/src/services/sync/context/providers/index.ts +0 -0
  102. package/src/services/sync/context/providers/session.ts +0 -0
  103. package/src/services/sync/context/providers/tabs.ts +0 -0
  104. package/src/services/sync/context/providers/visibility.ts +0 -0
  105. package/src/services/sync/database/factory.ts +0 -0
  106. package/src/services/sync/errors/boundary.ts +0 -0
  107. package/src/services/sync/errors/index.ts +0 -0
  108. package/src/services/sync/fetch.ts +10 -2
  109. package/src/services/sync/index.ts +0 -0
  110. package/src/services/sync/manager.ts +6 -0
  111. package/src/services/sync/models/Base.ts +0 -0
  112. package/src/services/sync/models/ContentLike.ts +0 -0
  113. package/src/services/sync/models/ContentProgress.ts +5 -6
  114. package/src/services/sync/models/Practice.ts +0 -0
  115. package/src/services/sync/models/PracticeDayNote.ts +0 -0
  116. package/src/services/sync/models/UserAwardProgress.ts +55 -0
  117. package/src/services/sync/models/index.ts +1 -0
  118. package/src/services/sync/repositories/base.ts +0 -0
  119. package/src/services/sync/repositories/content-likes.ts +0 -0
  120. package/src/services/sync/repositories/content-progress.ts +47 -25
  121. package/src/services/sync/repositories/index.ts +1 -0
  122. package/src/services/sync/repositories/practice-day-notes.ts +0 -0
  123. package/src/services/sync/repositories/practices.ts +16 -1
  124. package/src/services/sync/repositories/user-award-progress.ts +133 -0
  125. package/src/services/sync/repository-proxy.ts +6 -0
  126. package/src/services/sync/resolver.ts +0 -0
  127. package/src/services/sync/retry.ts +12 -11
  128. package/src/services/sync/run-scope.ts +0 -0
  129. package/src/services/sync/schema/index.ts +18 -3
  130. package/src/services/sync/serializers/index.ts +0 -0
  131. package/src/services/sync/serializers/model.ts +0 -0
  132. package/src/services/sync/serializers/raw.ts +0 -0
  133. package/src/services/sync/store/index.ts +53 -8
  134. package/src/services/sync/store/push-coalescer.ts +3 -3
  135. package/src/services/sync/store-configs.ts +7 -1
  136. package/src/services/sync/strategies/base.ts +0 -0
  137. package/src/services/sync/strategies/index.ts +0 -0
  138. package/src/services/sync/strategies/initial.ts +0 -0
  139. package/src/services/sync/strategies/polling.ts +0 -0
  140. package/src/services/sync/telemetry/index.ts +0 -0
  141. package/src/services/sync/telemetry/sampling.ts +0 -0
  142. package/src/services/sync/utils/event-emitter.ts +0 -0
  143. package/src/services/sync/utils/index.ts +0 -0
  144. package/src/services/sync/utils/throttle.ts +0 -0
  145. package/src/services/sync/utils/timers.ts +0 -0
  146. package/src/services/types.js +0 -0
  147. package/src/services/user/account.ts +0 -0
  148. package/src/services/user/chat.js +0 -0
  149. package/src/services/user/interests.js +0 -0
  150. package/src/services/user/management.js +0 -0
  151. package/src/services/user/memberships.ts +0 -0
  152. package/src/services/user/notifications.js +0 -0
  153. package/src/services/user/onboarding.ts +0 -0
  154. package/src/services/user/payments.ts +0 -0
  155. package/src/services/user/permissions.js +0 -0
  156. package/src/services/user/profile.js +0 -0
  157. package/src/services/user/sessions.js +0 -0
  158. package/src/services/user/types.d.ts +0 -0
  159. package/src/services/user/types.js +0 -0
  160. package/src/services/user/user-management-system.js +0 -0
  161. package/src/services/userActivity.js +0 -1
  162. package/test/HttpClient.test.js +6 -6
  163. package/test/awards/award-alacarte-observer.test.js +196 -0
  164. package/test/awards/award-auto-refresh.test.js +83 -0
  165. package/test/awards/award-calculations.test.js +33 -0
  166. package/test/awards/award-certificate-display.test.js +328 -0
  167. package/test/awards/award-collection-edge-cases.test.js +210 -0
  168. package/test/awards/award-collection-filtering.test.js +285 -0
  169. package/test/awards/award-completion-flow.test.js +213 -0
  170. package/test/awards/award-exclusion-handling.test.js +273 -0
  171. package/test/awards/award-multi-lesson.test.js +241 -0
  172. package/test/awards/award-observer-integration.test.js +325 -0
  173. package/test/awards/award-query-messages.test.js +438 -0
  174. package/test/awards/award-user-collection.test.js +412 -0
  175. package/test/awards/duplicate-prevention.test.js +118 -0
  176. package/test/awards/helpers/completion-mock.js +54 -0
  177. package/test/awards/helpers/index.js +3 -0
  178. package/test/awards/helpers/mock-setup.js +69 -0
  179. package/test/awards/helpers/progress-emitter.js +39 -0
  180. package/test/awards/message-generator.test.js +162 -0
  181. package/test/content.test.js +0 -0
  182. package/test/contentLikes.test.js +0 -0
  183. package/test/contentProgress.test.js +0 -0
  184. package/test/dataContext.test.js +0 -0
  185. package/test/forum.test.js +0 -0
  186. package/test/imageSRCBuilder.test.js +0 -0
  187. package/test/imageSRCVerify.test.js +0 -0
  188. package/test/initializeTests.js +6 -0
  189. package/test/learningPaths.test.js +0 -0
  190. package/test/lib/lastUpdated.test.js +0 -0
  191. package/test/live/contentProgressLive.test.js +0 -0
  192. package/test/live/railcontentLive.test.js +0 -0
  193. package/test/localStorageMock.js +0 -0
  194. package/test/log.js +0 -0
  195. package/test/mockData/award-definitions.js +171 -0
  196. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  197. package/test/mockData/mockData_progress_content.json +0 -0
  198. package/test/mockData/mockData_sanity_progress_content.json +0 -0
  199. package/test/mockData/mockData_user_practices.json +0 -0
  200. package/test/notifications.test.js +0 -0
  201. package/test/progressRows.test.js +0 -0
  202. package/test/sanityQueryService.test.js +0 -0
  203. package/test/streakMessage.test.js +0 -0
  204. package/test/sync/models/award-database-integration.test.js +519 -0
  205. package/test/user/permissions.test.js +0 -0
  206. package/test/userActivity.test.js +0 -0
  207. package/tools/generate-index.cjs +9 -0
  208. package/.claude/settings.local.json +0 -14
  209. package/.yarnrc.yml +0 -1
  210. 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
- fetchAwardsForUser,
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
- getAwardDataForGuidedContent,
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
package/src/lib/brands.ts CHANGED
File without changes
File without changes
File without changes
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