musora-content-services 2.2.0 → 2.2.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/.github/workflows/node.js.yml +0 -0
- package/.prettierignore +0 -0
- package/.prettierrc +0 -0
- package/CHANGELOG.md +2 -0
- package/README.md +0 -0
- package/babel.config.cjs +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/scripts/collapse.js +0 -0
- package/docs/scripts/commonNav.js +0 -0
- package/docs/scripts/linenumber.js +0 -0
- package/docs/scripts/nav.js +0 -0
- package/docs/scripts/polyfill.js +0 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
- package/docs/scripts/prettify/lang-css.js +0 -0
- package/docs/scripts/prettify/prettify.js +0 -0
- package/docs/scripts/search.js +0 -0
- package/docs/styles/jsdoc.css +0 -0
- package/docs/styles/prettify.css +0 -0
- package/jest.config.js +0 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +1 -0
- package/src/index.d.ts +35 -2
- package/src/index.js +35 -2
- package/src/services/contentLikes.js +0 -0
- package/src/services/dataContext.js +15 -2
- package/src/services/railcontent.js +60 -0
- package/src/services/sanity.js +10 -7
- package/src/services/userActivity.js +377 -23
- package/test/contentLikes.test.js +2 -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/mockData_fetchByRailContentIds_one_content.json +35 -0
- package/test/userActivity.test.js +118 -0
|
File without changes
|
package/.prettierignore
CHANGED
|
File without changes
|
package/.prettierrc
CHANGED
|
File without changes
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [2.2.1](https://github.com/railroadmedia/musora-content-services/compare/v2.2.0...v2.2.1) (2025-03-28)
|
|
6
|
+
|
|
5
7
|
## [2.2.0](https://github.com/railroadmedia/musora-content-services/compare/v2.1.1...v2.2.0) (2025-03-27)
|
|
6
8
|
|
|
7
9
|
|
package/README.md
CHANGED
|
File without changes
|
package/babel.config.cjs
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/docs/scripts/collapse.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/docs/scripts/nav.js
CHANGED
|
File without changes
|
package/docs/scripts/polyfill.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/docs/scripts/search.js
CHANGED
|
File without changes
|
package/docs/styles/jsdoc.css
CHANGED
|
File without changes
|
package/docs/styles/prettify.css
CHANGED
|
File without changes
|
package/jest.config.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/src/contentTypeConfig.js
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -94,8 +94,11 @@ import {
|
|
|
94
94
|
fetchUserChallengeProgress,
|
|
95
95
|
fetchUserLikes,
|
|
96
96
|
fetchUserPermissionsData,
|
|
97
|
+
fetchUserPracticeMeta,
|
|
98
|
+
fetchUserPractices,
|
|
97
99
|
likeComment,
|
|
98
100
|
likePlaylist,
|
|
101
|
+
logUserPractice,
|
|
99
102
|
openComment,
|
|
100
103
|
pinPlaylist,
|
|
101
104
|
playback,
|
|
@@ -179,6 +182,11 @@ import {
|
|
|
179
182
|
jumpToContinueContent
|
|
180
183
|
} from './services/sanity.js';
|
|
181
184
|
|
|
185
|
+
import {
|
|
186
|
+
blockUser,
|
|
187
|
+
unblockUser
|
|
188
|
+
} from './services/user/management.js';
|
|
189
|
+
|
|
182
190
|
import {
|
|
183
191
|
fetchUserPermissions,
|
|
184
192
|
reset
|
|
@@ -190,7 +198,17 @@ import {
|
|
|
190
198
|
} from './services/user/sessions.js';
|
|
191
199
|
|
|
192
200
|
import {
|
|
193
|
-
|
|
201
|
+
deletePracticeSession,
|
|
202
|
+
getPracticeSessions,
|
|
203
|
+
getRecentActivity,
|
|
204
|
+
getUserMonthlyStats,
|
|
205
|
+
getUserPractices,
|
|
206
|
+
getUserWeeklyStats,
|
|
207
|
+
recordUserPractice,
|
|
208
|
+
removeUserPractice,
|
|
209
|
+
restorePracticeSession,
|
|
210
|
+
restoreUserPractice,
|
|
211
|
+
updateUserPractice
|
|
194
212
|
} from './services/userActivity.js';
|
|
195
213
|
|
|
196
214
|
declare module 'musora-content-services' {
|
|
@@ -199,6 +217,7 @@ declare module 'musora-content-services' {
|
|
|
199
217
|
assignModeratorToComment,
|
|
200
218
|
assignmentStatusCompleted,
|
|
201
219
|
assignmentStatusReset,
|
|
220
|
+
blockUser,
|
|
202
221
|
closeComment,
|
|
203
222
|
contentStatusCompleted,
|
|
204
223
|
contentStatusReset,
|
|
@@ -209,6 +228,7 @@ declare module 'musora-content-services' {
|
|
|
209
228
|
deletePlaylist,
|
|
210
229
|
deletePlaylistItem,
|
|
211
230
|
deletePlaylistLike,
|
|
231
|
+
deletePracticeSession,
|
|
212
232
|
duplicatePlaylist,
|
|
213
233
|
editComment,
|
|
214
234
|
fetchAll,
|
|
@@ -288,6 +308,8 @@ declare module 'musora-content-services' {
|
|
|
288
308
|
fetchUserPermissions,
|
|
289
309
|
fetchUserPermissionsData,
|
|
290
310
|
fetchUserPlaylists,
|
|
311
|
+
fetchUserPracticeMeta,
|
|
312
|
+
fetchUserPractices,
|
|
291
313
|
getActiveDiscussions,
|
|
292
314
|
getAllCompleted,
|
|
293
315
|
getAllStarted,
|
|
@@ -295,17 +317,21 @@ declare module 'musora-content-services' {
|
|
|
295
317
|
getContentRows,
|
|
296
318
|
getLessonContentRows,
|
|
297
319
|
getNewAndUpcoming,
|
|
320
|
+
getPracticeSessions,
|
|
298
321
|
getProgressPercentage,
|
|
299
322
|
getProgressPercentageByIds,
|
|
300
323
|
getProgressState,
|
|
301
324
|
getProgressStateByIds,
|
|
302
325
|
getRecent,
|
|
326
|
+
getRecentActivity,
|
|
303
327
|
getRecommendedForYou,
|
|
304
328
|
getResumeTimeSeconds,
|
|
305
329
|
getScheduleContentRows,
|
|
306
330
|
getSortOrder,
|
|
307
331
|
getTabResults,
|
|
308
|
-
|
|
332
|
+
getUserMonthlyStats,
|
|
333
|
+
getUserPractices,
|
|
334
|
+
getUserWeeklyStats,
|
|
309
335
|
globalConfig,
|
|
310
336
|
initializeService,
|
|
311
337
|
isContentLiked,
|
|
@@ -313,6 +339,7 @@ declare module 'musora-content-services' {
|
|
|
313
339
|
likeComment,
|
|
314
340
|
likeContent,
|
|
315
341
|
likePlaylist,
|
|
342
|
+
logUserPractice,
|
|
316
343
|
login,
|
|
317
344
|
logout,
|
|
318
345
|
openComment,
|
|
@@ -334,17 +361,23 @@ declare module 'musora-content-services' {
|
|
|
334
361
|
rankCategories,
|
|
335
362
|
rankItems,
|
|
336
363
|
recommendations,
|
|
364
|
+
recordUserPractice,
|
|
337
365
|
recordWatchSession,
|
|
366
|
+
removeUserPractice,
|
|
338
367
|
replyToComment,
|
|
339
368
|
reportPlaylist,
|
|
340
369
|
reset,
|
|
370
|
+
restorePracticeSession,
|
|
371
|
+
restoreUserPractice,
|
|
341
372
|
setStudentViewForUser,
|
|
342
373
|
unassignModeratorToComment,
|
|
374
|
+
unblockUser,
|
|
343
375
|
unlikeComment,
|
|
344
376
|
unlikeContent,
|
|
345
377
|
unpinPlaylist,
|
|
346
378
|
updatePlaylist,
|
|
347
379
|
updatePlaylistItem,
|
|
380
|
+
updateUserPractice,
|
|
348
381
|
verifyLocalDataContext,
|
|
349
382
|
}
|
|
350
383
|
}
|
package/src/index.js
CHANGED
|
@@ -94,8 +94,11 @@ import {
|
|
|
94
94
|
fetchUserChallengeProgress,
|
|
95
95
|
fetchUserLikes,
|
|
96
96
|
fetchUserPermissionsData,
|
|
97
|
+
fetchUserPracticeMeta,
|
|
98
|
+
fetchUserPractices,
|
|
97
99
|
likeComment,
|
|
98
100
|
likePlaylist,
|
|
101
|
+
logUserPractice,
|
|
99
102
|
openComment,
|
|
100
103
|
pinPlaylist,
|
|
101
104
|
playback,
|
|
@@ -179,6 +182,11 @@ import {
|
|
|
179
182
|
jumpToContinueContent
|
|
180
183
|
} from './services/sanity.js';
|
|
181
184
|
|
|
185
|
+
import {
|
|
186
|
+
blockUser,
|
|
187
|
+
unblockUser
|
|
188
|
+
} from './services/user/management.js';
|
|
189
|
+
|
|
182
190
|
import {
|
|
183
191
|
fetchUserPermissions,
|
|
184
192
|
reset
|
|
@@ -190,7 +198,17 @@ import {
|
|
|
190
198
|
} from './services/user/sessions.js';
|
|
191
199
|
|
|
192
200
|
import {
|
|
193
|
-
|
|
201
|
+
deletePracticeSession,
|
|
202
|
+
getPracticeSessions,
|
|
203
|
+
getRecentActivity,
|
|
204
|
+
getUserMonthlyStats,
|
|
205
|
+
getUserPractices,
|
|
206
|
+
getUserWeeklyStats,
|
|
207
|
+
recordUserPractice,
|
|
208
|
+
removeUserPractice,
|
|
209
|
+
restorePracticeSession,
|
|
210
|
+
restoreUserPractice,
|
|
211
|
+
updateUserPractice
|
|
194
212
|
} from './services/userActivity.js';
|
|
195
213
|
|
|
196
214
|
export {
|
|
@@ -198,6 +216,7 @@ export {
|
|
|
198
216
|
assignModeratorToComment,
|
|
199
217
|
assignmentStatusCompleted,
|
|
200
218
|
assignmentStatusReset,
|
|
219
|
+
blockUser,
|
|
201
220
|
closeComment,
|
|
202
221
|
contentStatusCompleted,
|
|
203
222
|
contentStatusReset,
|
|
@@ -208,6 +227,7 @@ export {
|
|
|
208
227
|
deletePlaylist,
|
|
209
228
|
deletePlaylistItem,
|
|
210
229
|
deletePlaylistLike,
|
|
230
|
+
deletePracticeSession,
|
|
211
231
|
duplicatePlaylist,
|
|
212
232
|
editComment,
|
|
213
233
|
fetchAll,
|
|
@@ -287,6 +307,8 @@ export {
|
|
|
287
307
|
fetchUserPermissions,
|
|
288
308
|
fetchUserPermissionsData,
|
|
289
309
|
fetchUserPlaylists,
|
|
310
|
+
fetchUserPracticeMeta,
|
|
311
|
+
fetchUserPractices,
|
|
290
312
|
getActiveDiscussions,
|
|
291
313
|
getAllCompleted,
|
|
292
314
|
getAllStarted,
|
|
@@ -294,17 +316,21 @@ export {
|
|
|
294
316
|
getContentRows,
|
|
295
317
|
getLessonContentRows,
|
|
296
318
|
getNewAndUpcoming,
|
|
319
|
+
getPracticeSessions,
|
|
297
320
|
getProgressPercentage,
|
|
298
321
|
getProgressPercentageByIds,
|
|
299
322
|
getProgressState,
|
|
300
323
|
getProgressStateByIds,
|
|
301
324
|
getRecent,
|
|
325
|
+
getRecentActivity,
|
|
302
326
|
getRecommendedForYou,
|
|
303
327
|
getResumeTimeSeconds,
|
|
304
328
|
getScheduleContentRows,
|
|
305
329
|
getSortOrder,
|
|
306
330
|
getTabResults,
|
|
307
|
-
|
|
331
|
+
getUserMonthlyStats,
|
|
332
|
+
getUserPractices,
|
|
333
|
+
getUserWeeklyStats,
|
|
308
334
|
globalConfig,
|
|
309
335
|
initializeService,
|
|
310
336
|
isContentLiked,
|
|
@@ -312,6 +338,7 @@ export {
|
|
|
312
338
|
likeComment,
|
|
313
339
|
likeContent,
|
|
314
340
|
likePlaylist,
|
|
341
|
+
logUserPractice,
|
|
315
342
|
login,
|
|
316
343
|
logout,
|
|
317
344
|
openComment,
|
|
@@ -333,16 +360,22 @@ export {
|
|
|
333
360
|
rankCategories,
|
|
334
361
|
rankItems,
|
|
335
362
|
recommendations,
|
|
363
|
+
recordUserPractice,
|
|
336
364
|
recordWatchSession,
|
|
365
|
+
removeUserPractice,
|
|
337
366
|
replyToComment,
|
|
338
367
|
reportPlaylist,
|
|
339
368
|
reset,
|
|
369
|
+
restorePracticeSession,
|
|
370
|
+
restoreUserPractice,
|
|
340
371
|
setStudentViewForUser,
|
|
341
372
|
unassignModeratorToComment,
|
|
373
|
+
unblockUser,
|
|
342
374
|
unlikeComment,
|
|
343
375
|
unlikeContent,
|
|
344
376
|
unpinPlaylist,
|
|
345
377
|
updatePlaylist,
|
|
346
378
|
updatePlaylistItem,
|
|
379
|
+
updateUserPractice,
|
|
347
380
|
verifyLocalDataContext,
|
|
348
381
|
};
|
|
File without changes
|
|
@@ -10,6 +10,7 @@ const excludeFromGeneratedIndex = []
|
|
|
10
10
|
//These constants need to match MWP UserDataVersionKeyEnum enum
|
|
11
11
|
export const ContentLikesVersionKey = 0
|
|
12
12
|
export const ContentProgressVersionKey = 1
|
|
13
|
+
export const UserActivityVersionKey = 2
|
|
13
14
|
|
|
14
15
|
let cache = null
|
|
15
16
|
|
|
@@ -101,8 +102,10 @@ export class DataContext {
|
|
|
101
102
|
|
|
102
103
|
clearCache() {
|
|
103
104
|
this.clearContext()
|
|
104
|
-
cache
|
|
105
|
-
|
|
105
|
+
if (cache) {
|
|
106
|
+
cache.removeItem(this.localStorageKey)
|
|
107
|
+
cache.removeItem(this.localStorageLastUpdatedKey)
|
|
108
|
+
}
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
clearContext() {
|
|
@@ -134,4 +137,14 @@ export class DataContext {
|
|
|
134
137
|
version() {
|
|
135
138
|
return this.context?.version ?? -1
|
|
136
139
|
}
|
|
140
|
+
async updateLocal(localUpdateFunction) {
|
|
141
|
+
await this.ensureLocalContextLoaded()
|
|
142
|
+
if (this.context) {
|
|
143
|
+
const res = await localUpdateFunction(this.context)
|
|
144
|
+
if (this.context) this.context.version++
|
|
145
|
+
let data = JSON.stringify(this.context)
|
|
146
|
+
cache.setItem(this.localStorageKey, data)
|
|
147
|
+
this.setLastUpdatedTime()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
137
150
|
}
|
|
@@ -1176,6 +1176,66 @@ export async function editComment(commentId, comment) {
|
|
|
1176
1176
|
return await patchDataHandler(url, data)
|
|
1177
1177
|
}
|
|
1178
1178
|
|
|
1179
|
+
export async function fetchUserPractices(currentVersion) {
|
|
1180
|
+
const url = `/api/user/practices/v1/practices`
|
|
1181
|
+
const response = await fetchDataHandler(url, currentVersion)
|
|
1182
|
+
const { data, version } = response;
|
|
1183
|
+
const userPractices = data;
|
|
1184
|
+
|
|
1185
|
+
let formattedPractices = userPractices.reduce((acc, practice) => {
|
|
1186
|
+
// Initialize the array if the day does not exist
|
|
1187
|
+
if (!acc[practice.day]) {
|
|
1188
|
+
acc[practice.day] = [];
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Push the practice entry into the array
|
|
1192
|
+
acc[practice.day].push({ id:practice.id, duration_seconds: practice.duration_seconds });
|
|
1193
|
+
|
|
1194
|
+
return acc;
|
|
1195
|
+
}, {});
|
|
1196
|
+
|
|
1197
|
+
let json = {
|
|
1198
|
+
data: {
|
|
1199
|
+
practices: formattedPractices
|
|
1200
|
+
},
|
|
1201
|
+
version: version
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
return json;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
export async function logUserPractice(practiceDetails) {
|
|
1208
|
+
const url = `/api/user/practices/v1/practices`
|
|
1209
|
+
return await fetchHandler(url, 'POST', null, practiceDetails)
|
|
1210
|
+
}
|
|
1211
|
+
export async function fetchUserPracticeMeta(practiceIds) {
|
|
1212
|
+
if(practiceIds.length == 0)
|
|
1213
|
+
{
|
|
1214
|
+
return [];
|
|
1215
|
+
}
|
|
1216
|
+
let idsString = '';
|
|
1217
|
+
if (practiceIds && practiceIds.length > 0) {
|
|
1218
|
+
idsString = '?';
|
|
1219
|
+
practiceIds.forEach((id, index) => {
|
|
1220
|
+
idsString += `practice_ids[]=${id}${index < practiceIds.length - 1 ? '&' : ''}`;
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
const url = `/api/user/practices/v1/practices${idsString}`
|
|
1224
|
+
return await fetchHandler(url, 'GET', null)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function fetchAbsolute(url, params) {
|
|
1228
|
+
if (globalConfig.railcontentConfig.authToken) {
|
|
1229
|
+
params.headers['Authorization'] = `Bearer ${globalConfig.railcontentConfig.authToken}`
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (globalConfig.railcontentConfig.baseUrl) {
|
|
1233
|
+
if (url.startsWith('/')) {
|
|
1234
|
+
return fetch(globalConfig.railcontentConfig.baseUrl + url, params)
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return fetch(url, params)
|
|
1238
|
+
}
|
|
1179
1239
|
export async function fetchHandler(url, method = 'get', dataVersion = null, body = null) {
|
|
1180
1240
|
return fetchJSONHandler(
|
|
1181
1241
|
url,
|
package/src/services/sanity.js
CHANGED
|
@@ -353,6 +353,7 @@ export async function fetchNewReleases(
|
|
|
353
353
|
"id": railcontent_id,
|
|
354
354
|
title,
|
|
355
355
|
"image": thumbnail.asset->url,
|
|
356
|
+
"thumbnail": thumbnail.asset->url,
|
|
356
357
|
${artistOrInstructorName()},
|
|
357
358
|
"artists": instructor[]->name,
|
|
358
359
|
difficulty,
|
|
@@ -396,6 +397,7 @@ export async function fetchUpcomingEvents(brand, { page = 1, limit = 10 } = {})
|
|
|
396
397
|
"id": railcontent_id,
|
|
397
398
|
title,
|
|
398
399
|
"image": thumbnail.asset->url,
|
|
400
|
+
"thumbnail": thumbnail.asset->url,
|
|
399
401
|
${artistOrInstructorName()},
|
|
400
402
|
"artists": instructor[]->name,
|
|
401
403
|
difficulty,
|
|
@@ -444,6 +446,7 @@ export async function fetchScheduledReleases(brand, { page = 1, limit = 10 }) {
|
|
|
444
446
|
"id": railcontent_id,
|
|
445
447
|
title,
|
|
446
448
|
"image": thumbnail.asset->url,
|
|
449
|
+
"thumbnail": thumbnail.asset->url,
|
|
447
450
|
${artistOrInstructorName()},
|
|
448
451
|
"artists": instructor[]->name,
|
|
449
452
|
difficulty,
|
|
@@ -1071,7 +1074,7 @@ export async function fetchMethod(brand, slug) {
|
|
|
1071
1074
|
child_count,
|
|
1072
1075
|
difficulty,
|
|
1073
1076
|
difficulty_string,
|
|
1074
|
-
"
|
|
1077
|
+
"thumbnail": thumbnail.asset->url,
|
|
1075
1078
|
"instructor": instructor[]->{name},
|
|
1076
1079
|
title,
|
|
1077
1080
|
"type": _type,
|
|
@@ -1097,7 +1100,7 @@ export async function fetchMethodChildren(railcontentId) {
|
|
|
1097
1100
|
"child_count":coalesce(count(child[${childrenFilter}]->), 0),
|
|
1098
1101
|
"id": railcontent_id,
|
|
1099
1102
|
"description": ${descriptionField},
|
|
1100
|
-
"
|
|
1103
|
+
"thumbnail": thumbnail.asset->url,
|
|
1101
1104
|
title,
|
|
1102
1105
|
xp,
|
|
1103
1106
|
total_xp,
|
|
@@ -1261,7 +1264,7 @@ export async function fetchLessonContent(railContentId) {
|
|
|
1261
1264
|
railcontent_id,
|
|
1262
1265
|
"id":railcontent_id,
|
|
1263
1266
|
slug, artist->,
|
|
1264
|
-
"
|
|
1267
|
+
"thumbnail":thumbnail.asset->url,
|
|
1265
1268
|
"url": web_url_path,
|
|
1266
1269
|
soundslice_slug,
|
|
1267
1270
|
"description": description[0].children[0].text,
|
|
@@ -1406,7 +1409,7 @@ export async function fetchRelatedLessons(railContentId, brand) {
|
|
|
1406
1409
|
).buildFilter()
|
|
1407
1410
|
const filterNeighbouringSiblings = await new FilterBuilder(`references(^._id)`).buildFilter()
|
|
1408
1411
|
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
1409
|
-
const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "
|
|
1412
|
+
const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, web_url_path, "type": _type, difficulty, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type, "genre": genre[]->name`
|
|
1410
1413
|
const queryFieldsWithSort = queryFields + ', sort'
|
|
1411
1414
|
const query = `*[railcontent_id == ${railContentId} && brand == "${brand}" && (!defined(permission) || references(*[_type=='permission']._id))]{
|
|
1412
1415
|
_type, parent_type, railcontent_id,
|
|
@@ -1697,11 +1700,11 @@ export async function fetchArtistLessons(
|
|
|
1697
1700
|
const query = `{
|
|
1698
1701
|
"entity":
|
|
1699
1702
|
*[_type == 'artist' && name == '${name}']
|
|
1700
|
-
{'type': _type, name, '
|
|
1703
|
+
{'type': _type, name, 'thumbnail':thumbnail_url.asset->url,
|
|
1701
1704
|
'lessons_count': count(*[${addType} brand == '${brand}' && references(^._id)]),
|
|
1702
1705
|
'lessons': *[${addType} brand == '${brand}' && references(^._id) && (status in ['published'] || (status == 'scheduled' && defined(published_on) && published_on >= '${now}')) ${searchFilter} ${includedFieldsFilter} ${progressFilter}]{${fieldsString}}
|
|
1703
1706
|
[${start}...${end}]}
|
|
1704
|
-
|order(${sortOrder})
|
|
1707
|
+
|order(${sortOrder})f
|
|
1705
1708
|
}`
|
|
1706
1709
|
return fetchSanity(query, true)
|
|
1707
1710
|
}
|
|
@@ -1751,7 +1754,7 @@ export async function fetchGenreLessons(
|
|
|
1751
1754
|
const query = `{
|
|
1752
1755
|
"entity":
|
|
1753
1756
|
*[_type == 'genre' && name == '${name}']
|
|
1754
|
-
{'type': _type, name, '
|
|
1757
|
+
{'type': _type, name, 'thumbnail':thumbnail_url.asset->url,
|
|
1755
1758
|
'lessons_count': count(*[${addType} brand == '${brand}' && references(^._id)]),
|
|
1756
1759
|
'lessons': *[${addType} brand == '${brand}' && references(^._id) && (status in ['published'] || (status == 'scheduled' && defined(published_on) && published_on >= '${now}')) ${searchFilter} ${includedFieldsFilter} ${progressFilter}]{${fieldsString}}
|
|
1757
1760
|
[${start}...${end}]}
|
|
@@ -2,31 +2,385 @@
|
|
|
2
2
|
* @module User-Activity
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {fetchHandler} from
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
import {fetchUserPractices, logUserPractice, fetchUserPracticeMeta, fetchHandler} from './railcontent'
|
|
6
|
+
import { DataContext, UserActivityVersionKey } from './dataContext.js'
|
|
7
|
+
import {fetchByRailContentIds} from "./sanity";
|
|
8
|
+
import {lessonTypesMapping} from "../contentTypeConfig";
|
|
9
|
+
|
|
10
|
+
const recentActivity = {
|
|
11
|
+
data: [
|
|
12
|
+
{ id: 5,title: '3 Easy Classical Songs For Beginners', action: 'Comment', thumbnail: 'https://s3-alpha-sig.figma.com/img/22c3/1eb9/d819a2c6727b78deb2fcf051349a0667?Expires=1743984000&Key-Pair-Id=APKAQ4GOSFWCW27IBOMQ&Signature=RGusttOtnWP80iL68~l4XqFrQNe-kOTnUSLtMXQwGJNLfWNze6~fMZ15LsH4IYpz85mJKgILgCegi1sEPF6loBJpKYF9AH5HC2Zz1fenM1T3V387dgb4FifujKtR-DJuBpknPNFvZ9wC9ebCfoXhc1HLe7BJUDSr8gJssiqsimQPU-9TanAOJAFTaxOfvQ0-WEW1VIdCWLX0OOjn1qs~jZTeOGcxy3b~OD1CxiUmwp5tA3lBgqya18Mf8kmcfHjByNmYysd2FwV5tS19dCnmzbE9hwvLwMOnQ38SYOKhxCLsteDRBIxLNjTGJFDUm4wF~089Kkd1zA8pn8-kVfYtwg__', summary: 'Just completed the advanced groove lesson! I’m finally feeling more confident with my fills. Thanks for the clear explanations and practice tips! ', date: '2025-03-25 10:09:48' },
|
|
13
|
+
{ id:4, title: 'Piano Man by Billy Joel', action: 'Play', thumbnail:'https://s3-alpha-sig.figma.com/img/fab0/fffe/3cb08e0256a02a18ac37b2db1c2b4a1f?Expires=1743984000&Key-Pair-Id=APKAQ4GOSFWCW27IBOMQ&Signature=smjF95zM9b469lFJ8F6mmjpRs4jPlDcBnflDXc9G~Go1ab87fnWpmg-megUoLmSkqu-Rf3s8P5buzqNP-YnqQl413g3grqNURTIwHRaI2HplN1OXL~OBLU9jHjgQxZmMI6VfSLs301W~cU9NHmMLYRr38r9mVQM6ippSMawj7MFSywiPhvHSvAIXt65o6HlNszhq1n5eZmxVdiL7tjifSja~fGVtHDsX0wuD3L-KAN5TIqywAgRzzFFMHw3yYxiOHajbRSi0s0LJNIHRF4iBJFFZWVXY5vdNX5YKmAmblnbfYIK3GrwJiaVEv6rGzOo~nN4Zh-FKJWvjyPd2oBmfbg__', date: '2025-03-25 10:04:48' },
|
|
14
|
+
{ id:3, title: 'General Piano Discussion', action: 'Post', thumbnail: 'https://s3-alpha-sig.figma.com/img/22c3/1eb9/d819a2c6727b78deb2fcf051349a0667?Expires=1743984000&Key-Pair-Id=APKAQ4GOSFWCW27IBOMQ&Signature=RGusttOtnWP80iL68~l4XqFrQNe-kOTnUSLtMXQwGJNLfWNze6~fMZ15LsH4IYpz85mJKgILgCegi1sEPF6loBJpKYF9AH5HC2Zz1fenM1T3V387dgb4FifujKtR-DJuBpknPNFvZ9wC9ebCfoXhc1HLe7BJUDSr8gJssiqsimQPU-9TanAOJAFTaxOfvQ0-WEW1VIdCWLX0OOjn1qs~jZTeOGcxy3b~OD1CxiUmwp5tA3lBgqya18Mf8kmcfHjByNmYysd2FwV5tS19dCnmzbE9hwvLwMOnQ38SYOKhxCLsteDRBIxLNjTGJFDUm4wF~089Kkd1zA8pn8-kVfYtwg__', summary: 'Just completed the advanced groove lesson! I’m finally feeling more confident with my fills. Thanks for the clear explanations and practice tips! ', date: '2025-03-25 09:49:48' },
|
|
15
|
+
{ id:2, title: 'Welcome To Guitareo', action: 'Complete', thumbnail: 'https://s3-alpha-sig.figma.com/img/22c3/1eb9/d819a2c6727b78deb2fcf051349a0667?Expires=1743984000&Key-Pair-Id=APKAQ4GOSFWCW27IBOMQ&Signature=RGusttOtnWP80iL68~l4XqFrQNe-kOTnUSLtMXQwGJNLfWNze6~fMZ15LsH4IYpz85mJKgILgCegi1sEPF6loBJpKYF9AH5HC2Zz1fenM1T3V387dgb4FifujKtR-DJuBpknPNFvZ9wC9ebCfoXhc1HLe7BJUDSr8gJssiqsimQPU-9TanAOJAFTaxOfvQ0-WEW1VIdCWLX0OOjn1qs~jZTeOGcxy3b~OD1CxiUmwp5tA3lBgqya18Mf8kmcfHjByNmYysd2FwV5tS19dCnmzbE9hwvLwMOnQ38SYOKhxCLsteDRBIxLNjTGJFDUm4wF~089Kkd1zA8pn8-kVfYtwg__',date: '2025-03-25 09:34:48' },
|
|
16
|
+
{ id:1, title: 'Welcome To Guitareo', action: 'Start', thumbnail: 'https://s3-alpha-sig.figma.com/img/22c3/1eb9/d819a2c6727b78deb2fcf051349a0667?Expires=1743984000&Key-Pair-Id=APKAQ4GOSFWCW27IBOMQ&Signature=RGusttOtnWP80iL68~l4XqFrQNe-kOTnUSLtMXQwGJNLfWNze6~fMZ15LsH4IYpz85mJKgILgCegi1sEPF6loBJpKYF9AH5HC2Zz1fenM1T3V387dgb4FifujKtR-DJuBpknPNFvZ9wC9ebCfoXhc1HLe7BJUDSr8gJssiqsimQPU-9TanAOJAFTaxOfvQ0-WEW1VIdCWLX0OOjn1qs~jZTeOGcxy3b~OD1CxiUmwp5tA3lBgqya18Mf8kmcfHjByNmYysd2FwV5tS19dCnmzbE9hwvLwMOnQ38SYOKhxCLsteDRBIxLNjTGJFDUm4wF~089Kkd1zA8pn8-kVfYtwg__',date: '2025-03-25 09:04:48' },
|
|
17
|
+
],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DATA_KEY_PRACTICES = 'practices'
|
|
21
|
+
const DATA_KEY_LAST_UPDATED_TIME = 'u'
|
|
22
|
+
|
|
23
|
+
const DAYS = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
|
|
24
|
+
|
|
25
|
+
export let userActivityContext = new DataContext(UserActivityVersionKey, fetchUserPractices)
|
|
26
|
+
|
|
27
|
+
// Get Weekly Stats
|
|
28
|
+
export async function getUserWeeklyStats() {
|
|
29
|
+
let data = await userActivityContext.getData()
|
|
30
|
+
|
|
31
|
+
let practices = data?.[DATA_KEY_PRACTICES] ?? {}
|
|
32
|
+
|
|
33
|
+
let today = new Date()
|
|
34
|
+
let startOfWeek = getMonday(today) // Get last Monday
|
|
35
|
+
let dailyStats = []
|
|
36
|
+
const todayKey = today.toISOString().split('T')[0]
|
|
37
|
+
for (let i = 0; i < 7; i++) {
|
|
38
|
+
let day = new Date(startOfWeek)
|
|
39
|
+
day.setDate(startOfWeek.getDate() + i)
|
|
40
|
+
let dayKey = day.toISOString().split('T')[0]
|
|
41
|
+
|
|
42
|
+
let dayActivity = practices[dayKey] ?? null
|
|
43
|
+
let isActive = dayKey === todayKey
|
|
44
|
+
let type = (dayActivity !== null ? 'tracked' : (isActive ? 'active' : 'none'))
|
|
45
|
+
dailyStats.push({ key: i, label: DAYS[i], isActive, inStreak: dayActivity !== null, type })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let { streakMessage } = getStreaksAndMessage(practices);
|
|
49
|
+
|
|
50
|
+
return { data: { dailyActiveStats: dailyStats, streakMessage } }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function getUserMonthlyStats(year = new Date().getFullYear(), month = new Date().getMonth(), day = 1) {
|
|
54
|
+
let data = await userActivityContext.getData()
|
|
55
|
+
let practices = data?.[DATA_KEY_PRACTICES] ?? {}
|
|
56
|
+
// Get the first day of the specified month and the number of days in that month
|
|
57
|
+
let firstDayOfMonth = new Date(year, month, 1)
|
|
58
|
+
let today = new Date()
|
|
59
|
+
|
|
60
|
+
let startOfMonth = getMonday(firstDayOfMonth)
|
|
61
|
+
let endOfMonth = new Date(year, month + 1, 0)
|
|
62
|
+
while (endOfMonth.getDay() !== 0) {
|
|
63
|
+
endOfMonth.setDate(endOfMonth.getDate() + 1)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let daysInMonth = Math.ceil((endOfMonth - startOfMonth) / (1000 * 60 * 60 * 24)) + 1;
|
|
67
|
+
|
|
68
|
+
let dailyStats = []
|
|
69
|
+
let practiceDuration = 0
|
|
70
|
+
let daysPracticed = 0
|
|
71
|
+
let weeklyStats = {}
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < daysInMonth; i++) {
|
|
74
|
+
let day = new Date(startOfMonth)
|
|
75
|
+
day.setDate(startOfMonth.getDate() + i)
|
|
76
|
+
let dayKey = day.toISOString().split('T')[0]
|
|
77
|
+
|
|
78
|
+
// Check if the user has activity for the day, default to 0 if undefined
|
|
79
|
+
let dayActivity = practices[dayKey] ?? null
|
|
80
|
+
let weekKey = getWeekNumber(day)
|
|
81
|
+
|
|
82
|
+
if (!weeklyStats[weekKey]) {
|
|
83
|
+
weeklyStats[weekKey] = { key: weekKey, inStreak: false };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (dayActivity) {
|
|
87
|
+
practiceDuration += dayActivity.reduce((sum, entry) => sum + entry.duration_seconds, 0)
|
|
88
|
+
daysPracticed++;
|
|
89
|
+
}
|
|
90
|
+
let isActive = dayKey === today.toISOString().split('T')[0]
|
|
91
|
+
let type = (dayActivity !== null ? 'tracked' : (isActive ? 'active' : 'none'))
|
|
92
|
+
|
|
93
|
+
let isInStreak = dayActivity !== null;
|
|
94
|
+
if (isInStreak) {
|
|
95
|
+
weeklyStats[weekKey].inStreak = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
dailyStats.push({
|
|
99
|
+
key: i,
|
|
100
|
+
label: dayKey,
|
|
101
|
+
isActive,
|
|
102
|
+
inStreak: dayActivity !== null,
|
|
103
|
+
type,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let filteredPractices = Object.keys(practices)
|
|
108
|
+
.filter((date) => new Date(date) <= endOfMonth)
|
|
109
|
+
.reduce((obj, key) => {
|
|
110
|
+
obj[key] = practices[key]
|
|
111
|
+
return obj
|
|
112
|
+
}, {})
|
|
113
|
+
|
|
114
|
+
let { currentDailyStreak, currentWeeklyStreak } = calculateStreaks(filteredPractices);
|
|
115
|
+
|
|
116
|
+
return { data: {
|
|
117
|
+
dailyActiveStats: dailyStats,
|
|
118
|
+
weeklyActiveStats: Object.values(weeklyStats),
|
|
119
|
+
practiceDuration,
|
|
120
|
+
currentDailyStreak,
|
|
121
|
+
currentWeeklyStreak,
|
|
122
|
+
daysPracticed,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function getUserPractices() {
|
|
128
|
+
let data = await userActivityContext.getData()
|
|
129
|
+
return data?.[DATA_KEY_PRACTICES] ?? []
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function recordUserPractice(practiceDetails) {
|
|
133
|
+
practiceDetails.auto = 0;
|
|
134
|
+
if (practiceDetails.content_id) {
|
|
135
|
+
practiceDetails.auto = 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await userActivityContext.update(
|
|
139
|
+
async function (localContext) {
|
|
140
|
+
let userData = localContext.data ?? { [DATA_KEY_PRACTICES]: {} };
|
|
141
|
+
localContext.data = userData;
|
|
142
|
+
},
|
|
143
|
+
async function () {
|
|
144
|
+
const response = await logUserPractice(practiceDetails);
|
|
145
|
+
if (response) {
|
|
146
|
+
await userActivityContext.updateLocal(async function (localContext) {
|
|
147
|
+
const newPractices = response.data ?? []
|
|
148
|
+
newPractices.forEach(newPractice => {
|
|
149
|
+
const { date } = newPractice;
|
|
150
|
+
if (!localContext.data[DATA_KEY_PRACTICES][date]) {
|
|
151
|
+
localContext.data[DATA_KEY_PRACTICES][date] = [];
|
|
152
|
+
}
|
|
153
|
+
localContext.data[DATA_KEY_PRACTICES][date][DATA_KEY_LAST_UPDATED_TIME] = Math.round(new Date().getTime() / 1000)
|
|
154
|
+
localContext.data[DATA_KEY_PRACTICES][date].push({
|
|
155
|
+
id: newPractice.id,
|
|
156
|
+
duration_seconds: newPractice.duration_seconds // Add the new practice for this date
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return response;
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function updateUserPractice(id, practiceDetails) {
|
|
167
|
+
const url = `/api/user/practices/v1/practices/${id}`
|
|
168
|
+
return await fetchHandler(url, 'PUT', null, practiceDetails)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function removeUserPractice(id) {
|
|
172
|
+
let url = `/api/user/practices/v1/practices${buildQueryString([id])}`;
|
|
173
|
+
await userActivityContext.update(
|
|
174
|
+
async function (localContext) {
|
|
175
|
+
if (localContext.data?.[DATA_KEY_PRACTICES]) {
|
|
176
|
+
Object.keys(localContext.data[DATA_KEY_PRACTICES]).forEach(date => {
|
|
177
|
+
localContext.data[DATA_KEY_PRACTICES][date] = localContext.data[DATA_KEY_PRACTICES][date].filter(
|
|
178
|
+
practice => practice.id !== id
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
11
182
|
},
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
183
|
+
async function () {
|
|
184
|
+
return await fetchHandler(url, 'delete');
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function restoreUserPractice(id) {
|
|
190
|
+
let url = `/api/user/practices/v1/practices/restore${buildQueryString([id])}`;
|
|
191
|
+
const response = await fetchHandler(url, 'put');
|
|
192
|
+
if (response?.data) {
|
|
193
|
+
await userActivityContext.updateLocal(async function (localContext) {
|
|
194
|
+
const restoredPractice = response.data;
|
|
195
|
+
const { date } = restoredPractice;
|
|
196
|
+
if (!localContext.data[DATA_KEY_PRACTICES][date]) {
|
|
197
|
+
localContext.data[DATA_KEY_PRACTICES][date] = [];
|
|
198
|
+
}
|
|
199
|
+
localContext.data[DATA_KEY_PRACTICES][date].push({
|
|
200
|
+
id: restoredPractice.id,
|
|
201
|
+
duration_seconds: restoredPractice.duration_seconds,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return response;
|
|
29
206
|
}
|
|
30
207
|
|
|
208
|
+
export async function deletePracticeSession(day) {
|
|
209
|
+
const userPracticesIds = await getUserPracticeIds(day);
|
|
210
|
+
if (!userPracticesIds.length) return [];
|
|
211
|
+
|
|
212
|
+
const url = `/api/user/practices/v1/practices${buildQueryString(userPracticesIds)}`;
|
|
213
|
+
return await fetchHandler(url, 'delete', null);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function restorePracticeSession(date) {
|
|
217
|
+
const url = `/api/user/practices/v1/practices/restore?date=${date}`;
|
|
218
|
+
return await fetchHandler(url, 'put', null);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function getPracticeSessions(day) {
|
|
222
|
+
const userPracticesIds = await getUserPracticeIds(day);
|
|
223
|
+
if (!userPracticesIds.length) return { data: { practices: [], practiceDuration: 0 } };
|
|
224
|
+
|
|
225
|
+
const meta = await fetchUserPracticeMeta(userPracticesIds);
|
|
226
|
+
const practiceDuration = meta.data.reduce((total, practice) => total + (practice.duration_seconds || 0), 0);
|
|
227
|
+
const contentIds = meta.data.map(practice => practice.content_id).filter(id => id !== null);
|
|
228
|
+
|
|
229
|
+
const contents = await fetchByRailContentIds(contentIds);
|
|
230
|
+
const getFormattedType = (type) => {
|
|
231
|
+
for (const [key, values] of Object.entries(lessonTypesMapping)) {
|
|
232
|
+
if (values.includes(type)) {
|
|
233
|
+
return key.replace(/\b\w/g, char => char.toUpperCase());
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const formattedMeta = meta.data.map(practice => {
|
|
240
|
+
const content = contents.find(c => c.id === practice.content_id) || {};
|
|
241
|
+
return {
|
|
242
|
+
id: practice.id,
|
|
243
|
+
auto: practice.auto,
|
|
244
|
+
thumbnail: (practice.content_id)? content.thumbnail : '',
|
|
245
|
+
duration: practice.duration_seconds || 0,
|
|
246
|
+
content_url: content.url || null,
|
|
247
|
+
title: (practice.content_id)? content.title : practice.title,
|
|
248
|
+
category_id: practice.category_id || null,
|
|
249
|
+
instrument_id: practice.instrument_id || null,
|
|
250
|
+
content_type: getFormattedType(content.type || ''),
|
|
251
|
+
content_id: practice.content_id || null,
|
|
252
|
+
content_brand: content.brand || null,
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
return { data: { practices: formattedMeta, practiceDuration } };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
export async function getRecentActivity() {
|
|
260
|
+
return { data: recentActivity };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getStreaksAndMessage(practices)
|
|
264
|
+
{
|
|
265
|
+
let { currentDailyStreak, currentWeeklyStreak } = calculateStreaks(practices)
|
|
266
|
+
|
|
267
|
+
let streakMessage = currentWeeklyStreak > 1
|
|
268
|
+
? `That's ${currentWeeklyStreak} weeks in a row! Keep going!`
|
|
269
|
+
: `Nice! You have a ${currentDailyStreak} day streak! Way to keep it going!`
|
|
270
|
+
return {
|
|
271
|
+
currentDailyStreak,
|
|
272
|
+
currentWeeklyStreak,
|
|
273
|
+
streakMessage,
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function getUserPracticeIds(day = new Date().toISOString().split('T')[0]) {
|
|
278
|
+
let data = await userActivityContext.getData();
|
|
279
|
+
let practices = data?.[DATA_KEY_PRACTICES] ?? {};
|
|
280
|
+
let userPracticesIds = [];
|
|
281
|
+
Object.keys(practices).forEach(date => {
|
|
282
|
+
if (date === day) {
|
|
283
|
+
practices[date].forEach(practice => userPracticesIds.push(practice.id));
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return userPracticesIds;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function buildQueryString(ids, paramName = 'practice_ids') {
|
|
291
|
+
if (!ids.length) return '';
|
|
292
|
+
return '?' + ids.map(id => `${paramName}[]=${id}`).join('&');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
// Helper: Get start of the week (Monday)
|
|
297
|
+
function getMonday(d) {
|
|
298
|
+
d = new Date(d)
|
|
299
|
+
var day = d.getDay(),
|
|
300
|
+
diff = d.getDate() - day + (day == 0 ? -6 : 1) // adjust when day is sunday
|
|
301
|
+
return new Date(d.setDate(diff))
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Helper: Get the week number
|
|
305
|
+
function getWeekNumber(date) {
|
|
306
|
+
let startOfYear = new Date(date.getFullYear(), 0, 1)
|
|
307
|
+
let diff = date - startOfYear
|
|
308
|
+
let oneWeekMs = 7 * 24 * 60 * 60 * 1000
|
|
309
|
+
return Math.ceil((diff / oneWeekMs) + startOfYear.getDay() / 7)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Helper: function to check if two dates are consecutive days
|
|
313
|
+
function isNextDay(prevDateStr, currentDateStr) {
|
|
314
|
+
let prevDate = new Date(prevDateStr)
|
|
315
|
+
let currentDate = new Date(currentDateStr)
|
|
316
|
+
let diff = (currentDate - prevDate) / (1000 * 60 * 60 * 24)
|
|
317
|
+
return diff === 1
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Helper: Calculate streaks
|
|
321
|
+
function calculateStreaks(practices) {
|
|
322
|
+
let currentDailyStreak = 0
|
|
323
|
+
let currentWeeklyStreak = 0
|
|
324
|
+
|
|
325
|
+
let sortedPracticeDays = Object.keys(practices).sort() // Ensure dates are sorted in order
|
|
326
|
+
|
|
327
|
+
if (sortedPracticeDays.length === 0) {
|
|
328
|
+
return { currentDailyStreak: 1, currentWeeklyStreak: 1 }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let dailyStreak = 0
|
|
332
|
+
let longestDailyStreak = 0
|
|
333
|
+
let prevDay = null
|
|
334
|
+
|
|
335
|
+
sortedPracticeDays.forEach((dayKey) => {
|
|
336
|
+
if (prevDay === null || isNextDay(prevDay, dayKey)) {
|
|
337
|
+
dailyStreak++
|
|
338
|
+
longestDailyStreak = Math.max(longestDailyStreak, dailyStreak)
|
|
339
|
+
} else {
|
|
340
|
+
dailyStreak = 1 // Reset streak if there's a gap
|
|
341
|
+
}
|
|
342
|
+
prevDay = dayKey
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
currentDailyStreak = dailyStreak
|
|
346
|
+
|
|
347
|
+
// Calculate weekly streaks
|
|
348
|
+
let weeklyStreak = 0
|
|
349
|
+
let prevWeek = null
|
|
350
|
+
let currentWeekActivity = false
|
|
351
|
+
|
|
352
|
+
sortedPracticeDays.forEach((dayKey) => {
|
|
353
|
+
let date = new Date(dayKey)
|
|
354
|
+
let weekNumber = getWeekNumber(date)
|
|
355
|
+
|
|
356
|
+
if (prevWeek === null) {
|
|
357
|
+
prevWeek = weekNumber
|
|
358
|
+
currentWeekActivity = true
|
|
359
|
+
} else if (weekNumber !== prevWeek) {
|
|
360
|
+
// A new week has started
|
|
361
|
+
if (currentWeekActivity) {
|
|
362
|
+
weeklyStreak++
|
|
363
|
+
currentWeekActivity = false
|
|
364
|
+
}
|
|
365
|
+
prevWeek = weekNumber
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (practices[dayKey]) {
|
|
369
|
+
currentWeekActivity = true
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
// If the user has activity in the current week, count it
|
|
374
|
+
if (currentWeekActivity) {
|
|
375
|
+
weeklyStreak++
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
currentWeeklyStreak = weeklyStreak
|
|
379
|
+
|
|
380
|
+
return { currentDailyStreak, currentWeeklyStreak }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
|
|
31
385
|
|
|
32
386
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
unlikeContent,
|
|
6
6
|
} from '../src/services/contentLikes'
|
|
7
7
|
import { initializeTestService } from './initializeTests'
|
|
8
|
+
import { userActivityContext } from '../src/services/userActivity.js'
|
|
8
9
|
|
|
9
10
|
const railContentModule = require('../src/services/railcontent.js')
|
|
10
11
|
|
|
@@ -17,6 +18,7 @@ describe('contentLikesDataContext', function () {
|
|
|
17
18
|
mock = jest.spyOn(dataContext, 'fetchData')
|
|
18
19
|
var json = JSON.parse(`{"version":${testVersion},"data":[308516,308515,308514,308518]}`)
|
|
19
20
|
mock.mockImplementation(() => json)
|
|
21
|
+
dataContext.ensureLocalContextLoaded()
|
|
20
22
|
})
|
|
21
23
|
|
|
22
24
|
test('contentLiked', async () => {
|
|
File without changes
|
|
File without changes
|
package/test/localStorageMock.js
CHANGED
|
File without changes
|
package/test/log.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"permission_id": [
|
|
4
|
+
78,
|
|
5
|
+
89,
|
|
6
|
+
91,
|
|
7
|
+
92,
|
|
8
|
+
88,
|
|
9
|
+
90
|
|
10
|
+
],
|
|
11
|
+
"thumbnail": "https://cdn.sanity.io/images/4032r8py/staging/5f15b20b428c06263fd39599fc310ab00eb05fee-1920x1080.jpg",
|
|
12
|
+
"difficulty_string": "Beginner",
|
|
13
|
+
"url": "/drumeo/quick-tips/how-to-play-drums/415183",
|
|
14
|
+
"lesson_count": null,
|
|
15
|
+
"id": 415183,
|
|
16
|
+
"image": "https://cdn.sanity.io/images/4032r8py/staging/5f15b20b428c06263fd39599fc310ab00eb05fee-1920x1080.jpg",
|
|
17
|
+
"web_url_path": "/drumeo/quick-tips/how-to-play-drums/415183",
|
|
18
|
+
"type": "quick-tips",
|
|
19
|
+
"brand": "drumeo",
|
|
20
|
+
"genre": null,
|
|
21
|
+
"status": "published",
|
|
22
|
+
"xp": 100,
|
|
23
|
+
"railcontent_id": 415183,
|
|
24
|
+
"artist": null,
|
|
25
|
+
"progress_percent": null,
|
|
26
|
+
"child_count": null,
|
|
27
|
+
"sanity_id": "quick-tips_415183",
|
|
28
|
+
"artist_name": "Brandon Toews",
|
|
29
|
+
"title": "How To Play Drums",
|
|
30
|
+
"difficulty": 3,
|
|
31
|
+
"published_on": "2024-12-13T12:00:00.000000Z",
|
|
32
|
+
"length_in_seconds": 576,
|
|
33
|
+
"slug": "how-to-play-drums"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { initializeTestService } from './initializeTests.js'
|
|
2
|
+
import {getUserMonthlyStats, getUserWeeklyStats, userActivityContext, recordUserPractice, getUserPractices} from '../src/services/userActivity.js'
|
|
3
|
+
import { logUserPractice } from '../src/services/railcontent.js'
|
|
4
|
+
import {fetchByRailContentIds} from "../src";
|
|
5
|
+
import mockData_fetchByRailContentIds_one_content from './mockData/mockData_fetchByRailContentIds_one_content.json';
|
|
6
|
+
|
|
7
|
+
global.fetch = jest.fn()
|
|
8
|
+
let mock = null
|
|
9
|
+
const testVersion = 1
|
|
10
|
+
const DEBUG = true
|
|
11
|
+
|
|
12
|
+
jest.mock('../src/services/railcontent', () => ({
|
|
13
|
+
...jest.requireActual('../src/services/railcontent'),
|
|
14
|
+
logUserPractice: jest.fn(() => Promise.resolve()),
|
|
15
|
+
fetchUserPermissionsData: jest.fn(() => ({ permissions: [78, 91, 92], isAdmin: false }))
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
jest.mock('../src/services/sanity', () => ({
|
|
19
|
+
...jest.requireActual('../src/services/sanity'),
|
|
20
|
+
fetchByRailContentIds: jest.fn(() => Promise.resolve(mockData_fetchByRailContentIds_one_content)),
|
|
21
|
+
}))
|
|
22
|
+
describe('User Activity API Tests', function () {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
initializeTestService()
|
|
25
|
+
mock = jest.spyOn(userActivityContext, 'fetchData')
|
|
26
|
+
var json = JSON.parse(
|
|
27
|
+
`{
|
|
28
|
+
"version": ${testVersion},
|
|
29
|
+
"config": { "key": 1, "enabled": 1, "checkInterval": 1, "refreshInterval": 2 },
|
|
30
|
+
"data": {
|
|
31
|
+
"practices": {
|
|
32
|
+
"2025-02-10": [{ "duration_seconds": 190 }],
|
|
33
|
+
"2025-02-11": [{ "duration_seconds": 340 }],
|
|
34
|
+
"2025-02-19": [{ "duration_seconds": 340 }],
|
|
35
|
+
"2025-03-01": [{ "duration_seconds": 360 }],
|
|
36
|
+
"2025-03-03": [{ "duration_seconds": 360 }],
|
|
37
|
+
"2025-03-05": [{ "duration_seconds": 100 }],
|
|
38
|
+
"2025-03-11": [{ "duration_seconds": 190 }],
|
|
39
|
+
"2025-03-14": [{ "duration_seconds": 456 }],
|
|
40
|
+
"2025-03-15": [{ "duration_seconds": 124 }],
|
|
41
|
+
"2025-03-16": [{ "duration_seconds": 452 }, { "duration_seconds": 456 }],
|
|
42
|
+
"2025-03-17": [{ "duration_seconds": 122 }]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}`
|
|
46
|
+
)
|
|
47
|
+
mock.mockImplementation(() => json)
|
|
48
|
+
userActivityContext.ensureLocalContextLoaded()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('fetches user practices successfully', async () => {
|
|
52
|
+
userActivityContext.clearCache()
|
|
53
|
+
const practices = await getUserMonthlyStats()
|
|
54
|
+
consoleLog(practices)
|
|
55
|
+
// Assert that dailyActiveStats contains correct data
|
|
56
|
+
const dailyStats = practices.dailyActiveStats
|
|
57
|
+
const currentDate = new Date()
|
|
58
|
+
const currentDateString = currentDate.toISOString().split('T')[0]
|
|
59
|
+
expect(dailyStats).toHaveLength(42)
|
|
60
|
+
|
|
61
|
+
// Verify current day's stats (e.g., March 17, 2025)
|
|
62
|
+
const current = dailyStats.find(stat => stat.label === currentDateString)
|
|
63
|
+
expect(current).toBeTruthy()
|
|
64
|
+
expect(current.isActive).toBe(true)
|
|
65
|
+
expect(current.type).toBe('active')
|
|
66
|
+
expect(current.inStreak).toBe(false)
|
|
67
|
+
|
|
68
|
+
// Ensure that mock was called as expected
|
|
69
|
+
expect(mock).toHaveBeenCalledTimes(1)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('fetches user practices from past', async () => {
|
|
73
|
+
userActivityContext.clearCache()
|
|
74
|
+
const practices = await getUserMonthlyStats( 2025, 1)
|
|
75
|
+
consoleLog(practices)
|
|
76
|
+
|
|
77
|
+
// Assert that dailyActiveStats contains correct data
|
|
78
|
+
const dailyStats = practices.dailyActiveStats
|
|
79
|
+
const feb10 = dailyStats.find(stat => stat.label === '2025-02-10')
|
|
80
|
+
expect(feb10.inStreak).toBe(true)
|
|
81
|
+
expect(feb10.type).toBe('tracked')
|
|
82
|
+
expect(feb10.isActive).toBe(false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('fetches user practices for current week', async () => {
|
|
86
|
+
userActivityContext.clearCache()
|
|
87
|
+
const practices = await getUserWeeklyStats( )
|
|
88
|
+
consoleLog(practices)
|
|
89
|
+
|
|
90
|
+
const dailyStats = practices.dailyActiveStats
|
|
91
|
+
const monday = dailyStats.find(stat => stat.label === 'M')
|
|
92
|
+
expect(monday).toBeDefined
|
|
93
|
+
const tuesday = dailyStats.find(stat => stat.label === 'T')
|
|
94
|
+
expect(tuesday).toBeDefined
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('should add a new practice entry and call logUserPractice', async () => {
|
|
98
|
+
userActivityContext.clearCache()
|
|
99
|
+
const mockPractice = {
|
|
100
|
+
duration_seconds: 300,
|
|
101
|
+
content_id: 415183
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
jest.spyOn(userActivityContext, 'update').mockImplementation(async (callback) => {
|
|
105
|
+
await callback(userActivityContext)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
await recordUserPractice(mockPractice)
|
|
109
|
+
|
|
110
|
+
expect(userActivityContext.update).toHaveBeenCalledTimes(1)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
function consoleLog(message, object=null, debug=false) {
|
|
114
|
+
if (debug || DEBUG) {
|
|
115
|
+
console.log(message, object);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
})
|