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