musora-content-services 2.5.0 → 2.6.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 (52) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/docs/ContentOrganization.html +2 -2
  3. package/docs/Gamification.html +2 -2
  4. package/docs/UserManagementSystem.html +2 -2
  5. package/docs/api_types.js.html +2 -2
  6. package/docs/config.js.html +2 -2
  7. package/docs/content-org_content-org.js.html +2 -2
  8. package/docs/content-org_playlists-types.js.html +2 -2
  9. package/docs/content-org_playlists.js.html +2 -2
  10. package/docs/content.js.html +2 -4
  11. package/docs/gamification_awards.js.html +2 -2
  12. package/docs/gamification_gamification.js.html +2 -2
  13. package/docs/gamification_types.js.html +2 -2
  14. package/docs/global.html +448 -2
  15. package/docs/index.html +2 -2
  16. package/docs/module-Awards.html +2 -2
  17. package/docs/module-Config.html +2 -2
  18. package/docs/module-Content-Services-V2.html +8 -8
  19. package/docs/module-Interests.html +2 -2
  20. package/docs/module-Permissions.html +2 -2
  21. package/docs/module-Playlists.html +2 -2
  22. package/docs/module-Railcontent-Services.html +754 -44
  23. package/docs/module-Sanity-Services.html +2 -2
  24. package/docs/module-Sessions.html +2 -2
  25. package/docs/module-User-Activity.html +525 -16
  26. package/docs/module-UserManagement.html +2 -2
  27. package/docs/module-UserProfile.html +266 -0
  28. package/docs/railcontent.js.html +41 -2
  29. package/docs/sanity.js.html +2 -2
  30. package/docs/userActivity.js.html +348 -263
  31. package/docs/user_interests.js.html +2 -2
  32. package/docs/user_management.js.html +2 -2
  33. package/docs/user_permissions.js.html +2 -2
  34. package/docs/user_profile.js.html +105 -0
  35. package/docs/user_sessions.js.html +2 -2
  36. package/docs/user_types.js.html +22 -2
  37. package/docs/user_user-management-system.js.html +2 -2
  38. package/package.json +1 -1
  39. package/src/contentMetaData.js +14 -0
  40. package/src/index.d.ts +15 -0
  41. package/src/index.js +15 -0
  42. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
  43. package/src/services/content.js +0 -2
  44. package/src/services/contentProgress.js +9 -0
  45. package/src/services/railcontent.js +39 -0
  46. package/src/services/user/interests.js +0 -0
  47. package/src/services/user/profile.js +33 -0
  48. package/src/services/user/types.js +20 -0
  49. package/src/services/user/user-management-system.js +0 -0
  50. package/src/services/userActivity.js +346 -261
  51. package/test/content.test.js +8 -6
  52. package/test/sanityQueryService.test.js +6 -0
@@ -2,20 +2,13 @@
2
2
  * @module User-Activity
3
3
  */
4
4
 
5
- import {fetchUserPractices, logUserPractice, fetchUserPracticeMeta, fetchUserPracticeNotes, fetchHandler} from './railcontent'
5
+ import {fetchUserPractices, logUserPractice, fetchUserPracticeMeta, fetchUserPracticeNotes, fetchHandler, fetchRecentUserActivities} from './railcontent'
6
6
  import { DataContext, UserActivityVersionKey } from './dataContext.js'
7
- import {fetchByRailContentIds} from "./sanity";
8
- import {lessonTypesMapping} from "../contentTypeConfig";
9
- import { convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay } from './dateUtils.js';
10
- import {globalConfig} from "./config";
11
-
12
- const recentActivity = [
13
- { id: 5,title: '3 Easy Classical Songs For Beginners', action: 'Comment', thumbnail: 'https://cdn.sanity.io/images/4032r8py/production/8a7fb4d7473306c5fa51ba2e8867e03d44342b18-1920x1080.jpg', 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' },
14
- { id:4, title: 'Piano Man by Billy Joel', action: 'Play', thumbnail:'https://cdn.sanity.io/images/4032r8py/production/107c258114540170399dfd72a50dae51575552f4-1000x1000.jpg', date: '2025-03-25 10:04:48' },
15
- { id:3, title: 'General Piano Discussion', action: 'Post', thumbnail: 'https://cdn.sanity.io/images/4032r8py/production/2331571d237b42dbf72f0cf35fdf163d996c5c5a-1920x1080.jpg', 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' },
16
- { id:2, title: 'Welcome To Guitareo', action: 'Complete', thumbnail: 'https://cdn.sanity.io/images/4032r8py/production/2331571d237b42dbf72f0cf35fdf163d996c5c5a-1920x1080.jpg',date: '2025-03-25 09:34:48' },
17
- { id:1, title: 'Welcome To Guitareo', action: 'Start', thumbnail: 'https://cdn.sanity.io/images/4032r8py/production/2331571d237b42dbf72f0cf35fdf163d996c5c5a-1920x1080.jpg',date: '2025-03-25 09:04:48' },
18
- ]
7
+ import { fetchByRailContentIds } from './sanity'
8
+ import { lessonTypesMapping } from '../contentTypeConfig'
9
+ import { convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay } from './dateUtils.js'
10
+ import { globalConfig } from './config'
11
+
19
12
 
20
13
  const DATA_KEY_PRACTICES = 'practices'
21
14
  const DATA_KEY_LAST_UPDATED_TIME = 'u'
@@ -23,25 +16,43 @@ const DATA_KEY_LAST_UPDATED_TIME = 'u'
23
16
  const DAYS = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
24
17
 
25
18
  const streakMessages = {
26
- startStreak: "Start your streak by taking any lesson!",
27
- restartStreak: "Restart your streak by taking any lesson!",
19
+ startStreak: 'Start your streak by taking any lesson!',
20
+ restartStreak: 'Restart your streak by taking any lesson!',
28
21
 
29
22
  // Messages when last active day is today
30
- dailyStreak: (streak) => `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak! Way to keep it going!`,
31
- dailyStreakShort: (streak) => `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak!`,
32
- weeklyStreak: (streak) => `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep up the momentum!`,
33
- greatJobWeeklyStreak: (streak) => `Great job! You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep it going!`,
23
+ dailyStreak: (streak) =>
24
+ `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak! Way to keep it going!`,
25
+ dailyStreakShort: (streak) =>
26
+ `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak!`,
27
+ weeklyStreak: (streak) =>
28
+ `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep up the momentum!`,
29
+ greatJobWeeklyStreak: (streak) =>
30
+ `Great job! You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep it going!`,
34
31
 
35
32
  // Messages when last active day is NOT today
36
- dailyStreakReminder: (streak) => `You have ${getIndefiniteArticle(streak)} ${streak} day streak! Keep it going with any lesson or song!`,
37
- weeklyStreakKeepUp: (streak) => `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep up the momentum!`,
38
- weeklyStreakReminder: (streak) => `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep it going with any lesson or song!`,
39
- };
33
+ dailyStreakReminder: (streak) =>
34
+ `You have ${getIndefiniteArticle(streak)} ${streak} day streak! Keep it going with any lesson or song!`,
35
+ weeklyStreakKeepUp: (streak) =>
36
+ `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep up the momentum!`,
37
+ weeklyStreakReminder: (streak) =>
38
+ `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep it going with any lesson or song!`,
39
+ }
40
40
 
41
41
  function getIndefiniteArticle(streak) {
42
- return streak === 8 || (streak >= 80 && streak <= 89) || (streak >= 800 && streak <= 899) ? 'an' : 'a'
42
+ return streak === 8 || (streak >= 80 && streak <= 89) || (streak >= 800 && streak <= 899)
43
+ ? 'an'
44
+ : 'a'
43
45
  }
44
46
 
47
+ export async function getUserPractices(userId = globalConfig.sessionConfig.userId) {
48
+ if (userId !== globalConfig.sessionConfig.userId) {
49
+ let data = await fetchUserPractices({ userId })
50
+ return data?.['data']?.[DATA_KEY_PRACTICES] ?? {}
51
+ } else {
52
+ let data = await userActivityContext.getData()
53
+ return data?.[DATA_KEY_PRACTICES] ?? {}
54
+ }
55
+ }
45
56
 
46
57
  export let userActivityContext = new DataContext(UserActivityVersionKey, fetchUserPractices)
47
58
 
@@ -60,24 +71,24 @@ export async function getUserWeeklyStats() {
60
71
  let data = await userActivityContext.getData()
61
72
  let practices = data?.[DATA_KEY_PRACTICES] ?? {}
62
73
  let sortedPracticeDays = Object.keys(practices)
63
- .map(date => new Date(date))
64
- .sort((a, b) => b - a);
74
+ .map((date) => new Date(date))
75
+ .sort((a, b) => b - a)
65
76
 
66
- let today = new Date();
67
- today.setHours(0, 0, 0, 0);
77
+ let today = new Date()
78
+ today.setHours(0, 0, 0, 0)
68
79
  let startOfWeek = getMonday(today) // Get last Monday
69
80
  let dailyStats = []
70
81
 
71
82
  for (let i = 0; i < 7; i++) {
72
83
  let day = new Date(startOfWeek)
73
84
  day.setDate(startOfWeek.getDate() + i)
74
- let hasPractice = sortedPracticeDays.some(practiceDate => isSameDate(practiceDate, day));
85
+ let hasPractice = sortedPracticeDays.some((practiceDate) => isSameDate(practiceDate, day))
75
86
  let isActive = isSameDate(today, day)
76
- let type = (hasPractice ? 'tracked' : (isActive ? 'active' : 'none'))
87
+ let type = hasPractice ? 'tracked' : isActive ? 'active' : 'none'
77
88
  dailyStats.push({ key: i, label: DAYS[i], isActive, inStreak: hasPractice, type })
78
89
  }
79
90
 
80
- let { streakMessage } = getStreaksAndMessage(practices);
91
+ let { streakMessage } = getStreaksAndMessage(practices)
81
92
 
82
93
  return { data: { dailyActiveStats: dailyStats, streakMessage, practices } }
83
94
  }
@@ -112,27 +123,20 @@ export async function getUserWeeklyStats() {
112
123
  * // Get stats for another user
113
124
  * getUserMonthlyStats({ userId: 123 }).then(console.log);
114
125
  */
115
- export async function getUserMonthlyStats( params = {}) {
116
- const now = new Date();
126
+ export async function getUserMonthlyStats(params = {}) {
127
+ const now = new Date()
117
128
  const {
118
129
  year = now.getFullYear(),
119
130
  month = now.getMonth(),
120
131
  day = 1,
121
132
  userId = globalConfig.sessionConfig.userId,
122
- } = params;
123
- let practices = {}
124
- if(userId !== globalConfig.sessionConfig.userId) {
125
- let data = await fetchUserPractices({userId});
126
- practices = data?.["data"]?.[DATA_KEY_PRACTICES]?? {}
127
- }else {
128
- let data = await userActivityContext.getData()
129
- practices = data?.[DATA_KEY_PRACTICES] ?? {}
130
- }
133
+ } = params
134
+ let practices = await getUserPractices(userId)
131
135
 
132
136
  // Get the first day of the specified month and the number of days in that month
133
137
  let firstDayOfMonth = new Date(year, month, 1)
134
138
  let today = new Date()
135
- today.setHours(0, 0, 0, 0);
139
+ today.setHours(0, 0, 0, 0)
136
140
 
137
141
  let startOfGrid = getMonday(firstDayOfMonth)
138
142
 
@@ -156,7 +160,7 @@ export async function getUserMonthlyStats( params = {}) {
156
160
  endOfMonth.setDate(endOfMonth.getDate() + 1)
157
161
  }
158
162
 
159
- let daysInMonth = Math.ceil((endOfMonth - startOfGrid) / (1000 * 60 * 60 * 24)) + 1;
163
+ let daysInMonth = Math.ceil((endOfMonth - startOfGrid) / (1000 * 60 * 60 * 24)) + 1
160
164
 
161
165
  let dailyStats = []
162
166
  let practiceDuration = 0
@@ -166,26 +170,26 @@ export async function getUserMonthlyStats( params = {}) {
166
170
  for (let i = 0; i < daysInMonth; i++) {
167
171
  let day = new Date(startOfGrid)
168
172
  day.setDate(startOfGrid.getDate() + i)
169
- let dayKey = `${day.getFullYear()}-${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')}`;
173
+ let dayKey = `${day.getFullYear()}-${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')}`
170
174
 
171
175
  // Check if the user has activity for the day
172
176
  let dayActivity = practices[dayKey] ?? null
173
177
  let weekKey = getWeekNumber(day)
174
178
 
175
179
  if (!weeklyStats[weekKey]) {
176
- weeklyStats[weekKey] = { key: weekKey, inStreak: false };
180
+ weeklyStats[weekKey] = { key: weekKey, inStreak: false }
177
181
  }
178
182
 
179
183
  if (dayActivity !== null) {
180
184
  practiceDuration += dayActivity.reduce((sum, entry) => sum + entry.duration_seconds, 0)
181
- daysPracticed++;
185
+ daysPracticed++
182
186
  }
183
187
 
184
188
  let isActive = isSameDate(today, day)
185
- let type = ((dayActivity !== null) ? 'tracked' : (isActive ? 'active' : 'none'))
186
- let isInStreak = dayActivity !== null;
189
+ let type = dayActivity !== null ? 'tracked' : isActive ? 'active' : 'none'
190
+ let isInStreak = dayActivity !== null
187
191
  if (isInStreak) {
188
- weeklyStats[weekKey].inStreak = true;
192
+ weeklyStats[weekKey].inStreak = true
189
193
  }
190
194
 
191
195
  dailyStats.push({
@@ -211,16 +215,17 @@ export async function getUserMonthlyStats( params = {}) {
211
215
  return obj
212
216
  }, {})
213
217
 
214
- let { currentDailyStreak, currentWeeklyStreak } = calculateStreaks(filteredPractices);
218
+ let { currentDailyStreak, currentWeeklyStreak } = calculateStreaks(filteredPractices)
215
219
 
216
- return { data: {
217
- dailyActiveStats: dailyStats,
220
+ return {
221
+ data: {
222
+ dailyActiveStats: dailyStats,
218
223
  weeklyActiveStats: Object.values(weeklyStats),
219
224
  practiceDuration,
220
225
  currentDailyStreak,
221
226
  currentWeeklyStreak,
222
227
  daysPracticed,
223
- }
228
+ },
224
229
  }
225
230
  }
226
231
 
@@ -255,37 +260,39 @@ export async function getUserMonthlyStats( params = {}) {
255
260
  * .catch(error => console.error(error));
256
261
  */
257
262
  export async function recordUserPractice(practiceDetails) {
258
- practiceDetails.auto = 0;
263
+ practiceDetails.auto = 0
259
264
  if (practiceDetails.content_id) {
260
- practiceDetails.auto = 1;
265
+ practiceDetails.auto = 1
261
266
  }
262
267
 
263
268
  await userActivityContext.update(
264
269
  async function (localContext) {
265
- let userData = localContext.data ?? { [DATA_KEY_PRACTICES]: {} };
266
- localContext.data = userData;
270
+ let userData = localContext.data ?? { [DATA_KEY_PRACTICES]: {} }
271
+ localContext.data = userData
267
272
  },
268
273
  async function () {
269
- const response = await logUserPractice(practiceDetails);
274
+ const response = await logUserPractice(practiceDetails)
270
275
  if (response) {
271
276
  await userActivityContext.updateLocal(async function (localContext) {
272
277
  const newPractices = response.data ?? []
273
- newPractices.forEach(newPractice => {
274
- const { date } = newPractice;
278
+ newPractices.forEach((newPractice) => {
279
+ const { date } = newPractice
275
280
  if (!localContext.data[DATA_KEY_PRACTICES][date]) {
276
- localContext.data[DATA_KEY_PRACTICES][date] = [];
281
+ localContext.data[DATA_KEY_PRACTICES][date] = []
277
282
  }
278
- localContext.data[DATA_KEY_PRACTICES][date][DATA_KEY_LAST_UPDATED_TIME] = Math.round(new Date().getTime() / 1000)
279
- localContext.data[DATA_KEY_PRACTICES][date].push({
280
- id: newPractice.id,
281
- duration_seconds: newPractice.duration_seconds // Add the new practice for this date
282
- });
283
- });
284
- });
283
+ localContext.data[DATA_KEY_PRACTICES][date][DATA_KEY_LAST_UPDATED_TIME] = Math.round(
284
+ new Date().getTime() / 1000
285
+ )
286
+ localContext.data[DATA_KEY_PRACTICES][date].push({
287
+ id: newPractice.id,
288
+ duration_seconds: newPractice.duration_seconds, // Add the new practice for this date
289
+ })
290
+ })
291
+ })
285
292
  }
286
- return response;
293
+ return response
287
294
  }
288
- );
295
+ )
289
296
  }
290
297
  /**
291
298
  * Updates a user's practice session with new details and syncs the changes remotely.
@@ -328,21 +335,21 @@ export async function updateUserPractice(id, practiceDetails) {
328
335
  * .catch(error => console.error(error));
329
336
  */
330
337
  export async function removeUserPractice(id) {
331
- let url = `/api/user/practices/v1/practices${buildQueryString([id])}`;
338
+ let url = `/api/user/practices/v1/practices${buildQueryString([id])}`
332
339
  await userActivityContext.update(
333
340
  async function (localContext) {
334
341
  if (localContext.data?.[DATA_KEY_PRACTICES]) {
335
- Object.keys(localContext.data[DATA_KEY_PRACTICES]).forEach(date => {
336
- localContext.data[DATA_KEY_PRACTICES][date] = localContext.data[DATA_KEY_PRACTICES][date].filter(
337
- practice => practice.id !== id
338
- );
339
- });
342
+ Object.keys(localContext.data[DATA_KEY_PRACTICES]).forEach((date) => {
343
+ localContext.data[DATA_KEY_PRACTICES][date] = localContext.data[DATA_KEY_PRACTICES][
344
+ date
345
+ ].filter((practice) => practice.id !== id)
346
+ })
340
347
  }
341
348
  },
342
349
  async function () {
343
- return await fetchHandler(url, 'delete');
350
+ return await fetchHandler(url, 'delete')
344
351
  }
345
- );
352
+ )
346
353
  }
347
354
 
348
355
  /**
@@ -358,22 +365,32 @@ export async function removeUserPractice(id) {
358
365
  * .catch(error => console.error(error));
359
366
  */
360
367
  export async function restoreUserPractice(id) {
361
- let url = `/api/user/practices/v1/practices/restore${buildQueryString([id])}`;
362
- const response = await fetchHandler(url, 'put');
368
+ let url = `/api/user/practices/v1/practices/restore${buildQueryString([id])}`
369
+ const response = await fetchHandler(url, 'put')
363
370
  if (response?.data) {
364
371
  await userActivityContext.updateLocal(async function (localContext) {
365
- const restoredPractice = response.data;
366
- const { date } = restoredPractice;
372
+ const restoredPractice = response.data
373
+ const { date } = restoredPractice
367
374
  if (!localContext.data[DATA_KEY_PRACTICES][date]) {
368
- localContext.data[DATA_KEY_PRACTICES][date] = [];
375
+ localContext.data[DATA_KEY_PRACTICES][date] = []
369
376
  }
370
377
  localContext.data[DATA_KEY_PRACTICES][date].push({
371
378
  id: restoredPractice.id,
372
379
  duration_seconds: restoredPractice.duration_seconds,
373
- });
374
- });
380
+ })
381
+ })
382
+ }
383
+ const formattedMeta = await formatPracticeMeta(response.data)
384
+ const practiceDuration = formattedMeta.reduce(
385
+ (total, practice) => total + (practice.duration || 0),
386
+ 0
387
+ )
388
+ return {
389
+ data: formattedMeta,
390
+ message: response.message,
391
+ version: response.version,
392
+ practiceDuration,
375
393
  }
376
- return response;
377
394
  }
378
395
 
379
396
  /**
@@ -393,20 +410,20 @@ export async function restoreUserPractice(id) {
393
410
  * .catch(error => console.error("Delete failed:", error));
394
411
  */
395
412
  export async function deletePracticeSession(day) {
396
- const userPracticesIds = await getUserPracticeIds(day);
397
- if (!userPracticesIds.length) return [];
413
+ const userPracticesIds = await getUserPracticeIds(day)
414
+ if (!userPracticesIds.length) return []
398
415
 
399
- const url = `/api/user/practices/v1/practices${buildQueryString(userPracticesIds)}`;
416
+ const url = `/api/user/practices/v1/practices${buildQueryString(userPracticesIds)}`
400
417
  await userActivityContext.update(
401
418
  async function (localContext) {
402
419
  if (localContext.data?.[DATA_KEY_PRACTICES]?.[day]) {
403
- delete localContext.data[DATA_KEY_PRACTICES][day];
420
+ delete localContext.data[DATA_KEY_PRACTICES][day]
404
421
  }
405
422
  },
406
423
  async function () {
407
- return await fetchHandler(url, 'DELETE', null);
424
+ return await fetchHandler(url, 'DELETE', null)
408
425
  }
409
- );
426
+ )
410
427
  }
411
428
 
412
429
  /**
@@ -426,25 +443,31 @@ export async function deletePracticeSession(day) {
426
443
  * .catch(error => console.error("Restore failed:", error));
427
444
  */
428
445
  export async function restorePracticeSession(date) {
429
- const url = `/api/user/practices/v1/practices/restore?date=${date}`;
430
- const response = await fetchHandler(url, 'PUT', null);
446
+ const url = `/api/user/practices/v1/practices/restore?date=${date}`
447
+ const response = await fetchHandler(url, 'PUT', null)
431
448
 
432
449
  if (response?.data) {
433
450
  await userActivityContext.updateLocal(async function (localContext) {
434
451
  if (!localContext.data[DATA_KEY_PRACTICES][date]) {
435
- localContext.data[DATA_KEY_PRACTICES][date] = [];
452
+ localContext.data[DATA_KEY_PRACTICES][date] = []
436
453
  }
437
454
 
438
- response.data.forEach(restoredPractice => {
455
+ response.data.forEach((restoredPractice) => {
439
456
  localContext.data[DATA_KEY_PRACTICES][date].push({
440
457
  id: restoredPractice.id,
441
458
  duration_seconds: restoredPractice.duration_seconds,
442
- });
443
- });
444
- });
459
+ })
460
+ })
461
+ })
445
462
  }
446
463
 
447
- return response;
464
+ const formattedMeta = await formatPracticeMeta(response?.data)
465
+ const practiceDuration = formattedMeta.reduce(
466
+ (total, practice) => total + (practice.duration || 0),
467
+ 0
468
+ )
469
+
470
+ return { data: formattedMeta, practiceDuration }
448
471
  }
449
472
 
450
473
  /**
@@ -469,50 +492,21 @@ export async function restorePracticeSession(date) {
469
492
  * .then(response => console.log(response))
470
493
  * .catch(error => console.error(error));
471
494
  */
472
- export async function getPracticeSessions(params ={}) {
473
- const {
474
- day,
475
- userId = globalConfig.sessionConfig.userId,
476
- } = params;
477
- const userPracticesIds = await getUserPracticeIds(day, userId);
478
- if (!userPracticesIds.length) return { data: { practices: [], practiceDuration: 0} };
479
-
480
- const meta = await fetchUserPracticeMeta(userPracticesIds, userId);
481
- if (!meta.data.length) return { data: { practices: [], practiceDuration: 0 } };
482
- const practiceDuration = meta.data.reduce((total, practice) => total + (practice.duration_seconds || 0), 0);
483
- const contentIds = meta.data.map(practice => practice.content_id).filter(id => id !== null);
484
-
485
- const contents = await fetchByRailContentIds(contentIds);
486
- const getFormattedType = (type) => {
487
- for (const [key, values] of Object.entries(lessonTypesMapping)) {
488
- if (values.includes(type)) {
489
- return key.replace(/\b\w/g, char => char.toUpperCase());
490
- }
491
- }
492
- return null;
493
- };
495
+ export async function getPracticeSessions(params = {}) {
496
+ const { day, userId = globalConfig.sessionConfig.userId } = params
497
+ const userPracticesIds = await getUserPracticeIds(day, userId)
498
+ if (!userPracticesIds.length) return { data: { practices: [], practiceDuration: 0 } }
494
499
 
495
- const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
500
+ const meta = await fetchUserPracticeMeta(userPracticesIds, userId)
501
+ if (!meta.data.length) return { data: { practices: [], practiceDuration: 0 } }
496
502
 
497
- const formattedMeta = meta.data.map(practice => {
498
- const utcDate = new Date(practice.created_at);
499
- const content = contents.find(c => c.id === practice.content_id) || {};
500
- return {
501
- id: practice.id,
502
- auto: practice.auto,
503
- thumbnail: (practice.content_id)? content.thumbnail : '',
504
- duration: practice.duration_seconds || 0,
505
- content_url: content.url || null,
506
- title: (practice.content_id)? content.title : practice.title,
507
- category_id: practice.category_id,
508
- instrument_id: practice.instrument_id ,
509
- content_type: getFormattedType(content.type || ''),
510
- content_id: practice.content_id || null,
511
- content_brand: content.brand || null,
512
- created_at: convertToTimeZone(utcDate, userTimeZone)
513
- };
514
- });
515
- return { data: { practices: formattedMeta, practiceDuration} };
503
+ const formattedMeta = await formatPracticeMeta(meta.data)
504
+ const practiceDuration = formattedMeta.reduce(
505
+ (total, practice) => total + (practice.duration || 0),
506
+ 0
507
+ )
508
+
509
+ return { data: { practices: formattedMeta, practiceDuration } }
516
510
  }
517
511
 
518
512
  /**
@@ -529,8 +523,8 @@ export async function getPracticeSessions(params ={}) {
529
523
  * .catch(error => console.error("Failed to get notes:", error));
530
524
  */
531
525
  export async function getPracticeNotes(day) {
532
- const notes = await fetchUserPracticeNotes(day);
533
- return { data: notes };
526
+ const notes = await fetchUserPracticeNotes(day)
527
+ return { data: notes }
534
528
  }
535
529
 
536
530
  /**
@@ -547,8 +541,12 @@ export async function getPracticeNotes(day) {
547
541
  * .then(({ data }) => console.log("Recent activity:", data))
548
542
  * .catch(error => console.error("Failed to get recent activity:", error));
549
543
  */
550
- export async function getRecentActivity() {
551
- return { data: recentActivity };
544
+ export async function getRecentActivity({
545
+ page = 1,
546
+ limit = 5,
547
+ tabName = null
548
+ } = {}) {
549
+ return await fetchRecentUserActivities({ page, limit, tabName });
552
550
  }
553
551
 
554
552
  /**
@@ -588,215 +586,302 @@ export async function updatePracticeNotes(payload) {
588
586
  }
589
587
 
590
588
  function getStreaksAndMessage(practices) {
591
- let { currentDailyStreak, currentWeeklyStreak, streakMessage } = calculateStreaks(practices, true);
589
+ let { currentDailyStreak, currentWeeklyStreak, streakMessage } = calculateStreaks(practices, true)
592
590
 
593
591
  return {
594
592
  currentDailyStreak,
595
593
  currentWeeklyStreak,
596
594
  streakMessage,
597
- };
595
+ }
598
596
  }
599
597
 
600
598
  async function getUserPracticeIds(day = new Date().toISOString().split('T')[0], userId = null) {
601
- let practices = {};
602
- if(userId !== globalConfig.sessionConfig.userId) {
603
- let data = await fetchUserPractices({userId});
604
- practices = data?.["data"]?.[DATA_KEY_PRACTICES]?? {}
605
- }else {
599
+ let practices = {}
600
+ if (userId !== globalConfig.sessionConfig.userId) {
601
+ let data = await fetchUserPractices({ userId })
602
+ practices = data?.['data']?.[DATA_KEY_PRACTICES] ?? {}
603
+ } else {
606
604
  let data = await userActivityContext.getData()
607
605
  practices = data?.[DATA_KEY_PRACTICES] ?? {}
608
606
  }
609
- let userPracticesIds = [];
610
- Object.keys(practices).forEach(date => {
607
+ let userPracticesIds = []
608
+ Object.keys(practices).forEach((date) => {
611
609
  if (date === day) {
612
- practices[date].forEach(practice => userPracticesIds.push(practice.id));
610
+ practices[date].forEach((practice) => userPracticesIds.push(practice.id))
613
611
  }
614
- });
615
- return userPracticesIds;
612
+ })
613
+ return userPracticesIds
616
614
  }
617
615
 
618
616
  function buildQueryString(ids, paramName = 'practice_ids') {
619
- if (!ids.length) return '';
620
- return '?' + ids.map(id => `${paramName}[]=${id}`).join('&');
617
+ if (!ids.length) return ''
618
+ return '?' + ids.map((id) => `${paramName}[]=${id}`).join('&')
621
619
  }
622
620
 
623
621
  // Helper: Calculate streaks
624
622
  function calculateStreaks(practices, includeStreakMessage = false) {
625
- let currentDailyStreak = 0;
626
- let currentWeeklyStreak = 0;
627
- let lastActiveDay = null;
628
- let streakMessage = '';
623
+ let currentDailyStreak = 0
624
+ let currentWeeklyStreak = 0
625
+ let lastActiveDay = null
626
+ let streakMessage = ''
629
627
 
630
628
  let sortedPracticeDays = Object.keys(practices)
631
- .map(dateStr => {
632
- const [year, month, day] = dateStr.split('-').map(Number);
633
- const newDate = new Date();
634
- newDate.setFullYear(year, month - 1, day);
635
- return newDate;
629
+ .map((dateStr) => {
630
+ const [year, month, day] = dateStr.split('-').map(Number)
631
+ const newDate = new Date()
632
+ newDate.setFullYear(year, month - 1, day)
633
+ return newDate
636
634
  })
637
- .sort((a, b) => a - b);
635
+ .sort((a, b) => a - b)
638
636
  if (sortedPracticeDays.length === 0) {
639
- return { currentDailyStreak: 0, currentWeeklyStreak: 0, streakMessage: streakMessages.startStreak };
637
+ return {
638
+ currentDailyStreak: 0,
639
+ currentWeeklyStreak: 0,
640
+ streakMessage: streakMessages.startStreak,
641
+ }
640
642
  }
641
- lastActiveDay = sortedPracticeDays[sortedPracticeDays.length - 1];
643
+ lastActiveDay = sortedPracticeDays[sortedPracticeDays.length - 1]
642
644
 
643
- let dailyStreak = 0;
644
- let prevDay = null;
645
+ let dailyStreak = 0
646
+ let prevDay = null
645
647
  sortedPracticeDays.forEach((currentDay) => {
646
648
  if (prevDay === null || isNextDay(prevDay, currentDay)) {
647
- dailyStreak++;
649
+ dailyStreak++
648
650
  } else {
649
- dailyStreak = 1;
651
+ dailyStreak = 1
650
652
  }
651
- prevDay = currentDay;
652
- });
653
- currentDailyStreak = dailyStreak;
653
+ prevDay = currentDay
654
+ })
655
+ currentDailyStreak = dailyStreak
654
656
 
655
657
  // Weekly streak calculation
656
- let weekNumbers = new Set(sortedPracticeDays.map(date => getWeekNumber(date)));
657
- let weeklyStreak = 0;
658
- let lastWeek = null;
659
- [...weekNumbers].sort((a, b) => b - a).forEach(week => {
660
- if (lastWeek === null || week === lastWeek - 1) {
661
- weeklyStreak++;
662
- } else {
663
- return;
664
- }
665
- lastWeek = week;
666
- });
667
- currentWeeklyStreak = weeklyStreak;
658
+ let weekNumbers = new Set(sortedPracticeDays.map((date) => getWeekNumber(date)))
659
+ let weeklyStreak = 0
660
+ let lastWeek = null
661
+ ;[...weekNumbers]
662
+ .sort((a, b) => b - a)
663
+ .forEach((week) => {
664
+ if (lastWeek === null || week === lastWeek - 1) {
665
+ weeklyStreak++
666
+ } else {
667
+ return
668
+ }
669
+ lastWeek = week
670
+ })
671
+ currentWeeklyStreak = weeklyStreak
668
672
 
669
673
  // Calculate streak message only if includeStreakMessage is true
670
674
  if (includeStreakMessage) {
671
- let today = new Date();
672
- let yesterday = new Date(today);
673
- yesterday.setDate(today.getDate() - 1);
674
-
675
- let currentWeekStart = getMonday(today);
676
- let lastWeekStart = new Date(currentWeekStart);
677
- lastWeekStart.setDate(currentWeekStart.getDate() - 7);
678
-
679
- let hasYesterdayPractice = sortedPracticeDays.some(date =>
680
- isSameDate(date, yesterday)
681
- );
682
- let hasCurrentWeekPractice = sortedPracticeDays.some(date => date >= currentWeekStart);
683
- let hasCurrentWeekPreviousPractice = sortedPracticeDays.some(date => date >= currentWeekStart && date < today);
684
- let hasLastWeekPractice = sortedPracticeDays.some(date => date >= lastWeekStart && date < currentWeekStart);
685
- let hasOlderPractice = sortedPracticeDays.some(date => date < lastWeekStart );
675
+ let today = new Date()
676
+ let yesterday = new Date(today)
677
+ yesterday.setDate(today.getDate() - 1)
678
+
679
+ let currentWeekStart = getMonday(today)
680
+ let lastWeekStart = new Date(currentWeekStart)
681
+ lastWeekStart.setDate(currentWeekStart.getDate() - 7)
682
+
683
+ let hasYesterdayPractice = sortedPracticeDays.some((date) => isSameDate(date, yesterday))
684
+ let hasCurrentWeekPractice = sortedPracticeDays.some((date) => date >= currentWeekStart)
685
+ let hasCurrentWeekPreviousPractice = sortedPracticeDays.some(
686
+ (date) => date >= currentWeekStart && date < today
687
+ )
688
+ let hasLastWeekPractice = sortedPracticeDays.some(
689
+ (date) => date >= lastWeekStart && date < currentWeekStart
690
+ )
691
+ let hasOlderPractice = sortedPracticeDays.some((date) => date < lastWeekStart)
686
692
 
687
693
  if (isSameDate(lastActiveDay, today)) {
688
694
  if (hasYesterdayPractice) {
689
- streakMessage = streakMessages.dailyStreak(currentDailyStreak);
695
+ streakMessage = streakMessages.dailyStreak(currentDailyStreak)
690
696
  } else if (hasCurrentWeekPreviousPractice) {
691
- streakMessage = streakMessages.weeklyStreak(currentWeeklyStreak);
697
+ streakMessage = streakMessages.weeklyStreak(currentWeeklyStreak)
692
698
  } else if (hasLastWeekPractice) {
693
- streakMessage = streakMessages.greatJobWeeklyStreak(currentWeeklyStreak);
699
+ streakMessage = streakMessages.greatJobWeeklyStreak(currentWeeklyStreak)
694
700
  } else {
695
- streakMessage = streakMessages.dailyStreakShort(currentDailyStreak);
701
+ streakMessage = streakMessages.dailyStreakShort(currentDailyStreak)
696
702
  }
697
703
  } else {
698
- if ((hasYesterdayPractice && currentDailyStreak >= 2) || (hasYesterdayPractice && sortedPracticeDays.length == 1)
699
- || (hasYesterdayPractice && !hasLastWeekPractice && hasOlderPractice)){
700
- streakMessage = streakMessages.dailyStreakReminder(currentDailyStreak);
704
+ if (
705
+ (hasYesterdayPractice && currentDailyStreak >= 2) ||
706
+ (hasYesterdayPractice && sortedPracticeDays.length == 1) ||
707
+ (hasYesterdayPractice && !hasLastWeekPractice && hasOlderPractice)
708
+ ) {
709
+ streakMessage = streakMessages.dailyStreakReminder(currentDailyStreak)
701
710
  } else if (hasCurrentWeekPractice) {
702
- streakMessage = streakMessages.weeklyStreakKeepUp(currentWeeklyStreak);
711
+ streakMessage = streakMessages.weeklyStreakKeepUp(currentWeeklyStreak)
703
712
  } else if (hasLastWeekPractice) {
704
- streakMessage = streakMessages.weeklyStreakReminder(currentWeeklyStreak);
713
+ streakMessage = streakMessages.weeklyStreakReminder(currentWeeklyStreak)
705
714
  } else {
706
- streakMessage = streakMessages.restartStreak;
715
+ streakMessage = streakMessages.restartStreak
707
716
  }
708
717
  }
709
718
  }
710
719
 
711
- return { currentDailyStreak, currentWeeklyStreak, streakMessage };
720
+ return { currentDailyStreak, currentWeeklyStreak, streakMessage }
712
721
  }
713
722
 
714
723
  /**
715
724
  * Calculates the longest daily, weekly streaks and totalPracticeSeconds from user practice dates.
716
725
  * @returns {{ longestDailyStreak: number, longestWeeklyStreak: number, totalPracticeSeconds:number }}
717
726
  */
718
- export async function calculateLongestStreaks() {
719
- let data = await userActivityContext.getData()
720
- let practices = data?.[DATA_KEY_PRACTICES] ?? {}
721
- let totalPracticeSeconds = 0;
727
+ export async function calculateLongestStreaks(userId = globalConfig.sessionConfig.userId) {
728
+ let practices = await getUserPractices(userId)
729
+ let totalPracticeSeconds = 0
722
730
  // Calculate total practice duration
723
731
  for (const date in practices) {
724
732
  for (const entry of practices[date]) {
725
- totalPracticeSeconds += entry.duration_seconds;
733
+ totalPracticeSeconds += entry.duration_seconds
726
734
  }
727
735
  }
728
736
 
729
737
  let practiceDates = Object.keys(practices)
730
- .map(dateStr => {
731
- const [y, m, d] = dateStr.split('-').map(Number);
732
- const newDate = new Date();
733
- newDate.setFullYear(y, m - 1, d);
734
- return newDate;
738
+ .map((dateStr) => {
739
+ const [y, m, d] = dateStr.split('-').map(Number)
740
+ const newDate = new Date()
741
+ newDate.setFullYear(y, m - 1, d)
742
+ return newDate
735
743
  })
736
- .sort((a, b) => a - b);
744
+ .sort((a, b) => a - b)
737
745
 
738
746
  if (!practiceDates || practiceDates.length === 0) {
739
- return {longestDailyStreak: 0, longestWeeklyStreak: 0, totalPracticeSeconds: 0};
747
+ return { longestDailyStreak: 0, longestWeeklyStreak: 0, totalPracticeSeconds: 0 }
740
748
  }
741
749
 
742
750
  // Normalize to Date objects
743
751
  const normalizedDates = [
744
- ...new Set(practiceDates.map(d => {
745
- const date = new Date(d);
746
- date.setHours(0, 0, 0, 0);
747
- return date.getTime();
748
- }))
749
- ].sort((a, b) => a - b);
752
+ ...new Set(
753
+ practiceDates.map((d) => {
754
+ const date = new Date(d)
755
+ date.setHours(0, 0, 0, 0)
756
+ return date.getTime()
757
+ })
758
+ ),
759
+ ].sort((a, b) => a - b)
750
760
 
751
761
  // ----- Daily Streak -----
752
- let longestDailyStreak = 1;
753
- let currentDailyStreak = 1;
762
+ let longestDailyStreak = 1
763
+ let currentDailyStreak = 1
754
764
  for (let i = 1; i < normalizedDates.length; i++) {
755
- const diffInDays = (normalizedDates[i] - normalizedDates[i - 1]) / (1000 * 60 * 60 * 24);
765
+ const diffInDays = (normalizedDates[i] - normalizedDates[i - 1]) / (1000 * 60 * 60 * 24)
756
766
  if (diffInDays === 1) {
757
- currentDailyStreak++;
758
- longestDailyStreak = Math.max(longestDailyStreak, currentDailyStreak);
767
+ currentDailyStreak++
768
+ longestDailyStreak = Math.max(longestDailyStreak, currentDailyStreak)
759
769
  } else {
760
- currentDailyStreak = 1;
770
+ currentDailyStreak = 1
761
771
  }
762
772
  }
763
773
 
764
774
  // ----- Weekly Streak -----
765
775
  const weekStartDates = [
766
- ...new Set(normalizedDates.map(ts => {
767
- const d = new Date(ts);
768
- const day = d.getDay();
769
- const diff = d.getDate() - day + (day === 0 ? -6 : 1); // adjust to Monday
770
- d.setDate(diff);
771
- return d.getTime(); // timestamp for Monday
772
- }))
773
- ].sort((a, b) => a - b);
774
-
775
- let longestWeeklyStreak = 1;
776
- let currentWeeklyStreak = 1;
776
+ ...new Set(
777
+ normalizedDates.map((ts) => {
778
+ const d = new Date(ts)
779
+ const day = d.getDay()
780
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1) // adjust to Monday
781
+ d.setDate(diff)
782
+ return d.getTime() // timestamp for Monday
783
+ })
784
+ ),
785
+ ].sort((a, b) => a - b)
786
+
787
+ let longestWeeklyStreak = 1
788
+ let currentWeeklyStreak = 1
777
789
 
778
790
  for (let i = 1; i < weekStartDates.length; i++) {
779
- const diffInWeeks = (weekStartDates[i] - weekStartDates[i - 1]) / (1000 * 60 * 60 * 24 * 7);
791
+ const diffInWeeks = (weekStartDates[i] - weekStartDates[i - 1]) / (1000 * 60 * 60 * 24 * 7)
780
792
  if (diffInWeeks === 1) {
781
- currentWeeklyStreak++;
782
- longestWeeklyStreak = Math.max(longestWeeklyStreak, currentWeeklyStreak);
793
+ currentWeeklyStreak++
794
+ longestWeeklyStreak = Math.max(longestWeeklyStreak, currentWeeklyStreak)
783
795
  } else {
784
- currentWeeklyStreak = 1;
796
+ currentWeeklyStreak = 1
785
797
  }
786
798
  }
787
799
 
788
800
  return {
789
801
  longestDailyStreak,
790
802
  longestWeeklyStreak,
791
- totalPracticeSeconds
792
- };
803
+ totalPracticeSeconds,
804
+ }
793
805
  }
794
806
 
807
+ async function formatPracticeMeta(practices) {
808
+ const contentIds = practices.map((p) => p.content_id).filter((id) => id !== null)
809
+ const contents = await fetchByRailContentIds(contentIds)
795
810
 
811
+ const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
796
812
 
813
+ return practices.map((practice) => {
814
+ const utcDate = new Date(practice.created_at)
815
+ const content = contents.find((c) => c.id === practice.content_id) || {}
797
816
 
817
+ return {
818
+ id: practice.id,
819
+ auto: practice.auto,
820
+ thumbnail: practice.content_id ? content.thumbnail : practice.thumbnail_url || '',
821
+ thumbnail_url: practice.content_id ? content.thumbnail : practice.thumbnail_url || '',
822
+ duration: practice.duration_seconds || 0,
823
+ duration_seconds: practice.duration_seconds || 0,
824
+ content_url: content.url || null,
825
+ title: practice.content_id ? content.title : practice.title,
826
+ category_id: practice.category_id,
827
+ instrument_id: practice.instrument_id,
828
+ content_type: getFormattedType(content.type || ''),
829
+ content_id: practice.content_id || null,
830
+ content_brand: content.brand || null,
831
+ created_at: convertToTimeZone(utcDate, userTimeZone),
832
+ }
833
+ })
834
+ }
798
835
 
836
+ export function getFormattedType(type) {
837
+ for (const [key, values] of Object.entries(lessonTypesMapping)) {
838
+ if (values.includes(type)) {
839
+ return key.replace(/\b\w/g, (char) => char.toUpperCase())
840
+ }
841
+ }
842
+ return null
843
+ }
799
844
 
845
+ /**
846
+ * Records a new user activity in the system.
847
+ *
848
+ * @param {Object} payload - The data representing the user activity.
849
+ * @param {number} payload.user_id - The ID of the user.
850
+ * @param {string} payload.action - The type of action (e.g., 'start', 'complete', 'comment', etc.).
851
+ * @param {string} payload.brand - The brand associated with the activity.
852
+ * @param {string} payload.type - The content type (e.g., 'lesson', 'song', etc.).
853
+ * @param {number} payload.content_id - The ID of the related content.
854
+ * @param {string} payload.date - The date of the activity (ISO format).
855
+ * @returns {Promise<Object>} - A promise that resolves to the API response after recording the activity.
856
+ *
857
+ * @example
858
+ * recordUserActivity({
859
+ * user_id: 123,
860
+ * action: 'start',
861
+ * brand: 'pianote',
862
+ * type: 'lesson',
863
+ * content_id: 4561,
864
+ * date: '2025-05-15'
865
+ * }).then(response => console.log(response))
866
+ * .catch(error => console.error(error));
867
+ */
868
+ export async function recordUserActivity(payload) {
869
+ const url = `/api/user-management-system/v1/activities`
870
+ return await fetchHandler(url, 'POST', null, payload)
871
+ }
800
872
 
801
-
802
-
873
+ /**
874
+ * Deletes a specific user activity by its ID.
875
+ *
876
+ * @param {number|string} id - The ID of the user activity to delete.
877
+ * @returns {Promise<Object>} - A promise that resolves to the API response after deletion.
878
+ *
879
+ * @example
880
+ * deleteUserActivity(789)
881
+ * .then(response => console.log('Deleted:', response))
882
+ * .catch(error => console.error(error));
883
+ */
884
+ export async function deleteUserActivity(id) {
885
+ const url = `/api/user-management-system/v1/activities/${id}`
886
+ return await fetchHandler(url, 'DELETE')
887
+ }