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.
- package/CHANGELOG.md +9 -0
- package/docs/ContentOrganization.html +2 -2
- package/docs/Gamification.html +2 -2
- package/docs/UserManagementSystem.html +2 -2
- package/docs/api_types.js.html +2 -2
- package/docs/config.js.html +2 -2
- package/docs/content-org_content-org.js.html +2 -2
- package/docs/content-org_playlists-types.js.html +2 -2
- package/docs/content-org_playlists.js.html +2 -2
- package/docs/content.js.html +2 -4
- package/docs/gamification_awards.js.html +2 -2
- package/docs/gamification_gamification.js.html +2 -2
- package/docs/gamification_types.js.html +2 -2
- package/docs/global.html +448 -2
- package/docs/index.html +2 -2
- package/docs/module-Awards.html +2 -2
- package/docs/module-Config.html +2 -2
- package/docs/module-Content-Services-V2.html +8 -8
- package/docs/module-Interests.html +2 -2
- package/docs/module-Permissions.html +2 -2
- package/docs/module-Playlists.html +2 -2
- package/docs/module-Railcontent-Services.html +754 -44
- package/docs/module-Sanity-Services.html +2 -2
- package/docs/module-Sessions.html +2 -2
- package/docs/module-User-Activity.html +525 -16
- package/docs/module-UserManagement.html +2 -2
- package/docs/module-UserProfile.html +266 -0
- package/docs/railcontent.js.html +41 -2
- package/docs/sanity.js.html +2 -2
- package/docs/userActivity.js.html +348 -263
- package/docs/user_interests.js.html +2 -2
- package/docs/user_management.js.html +2 -2
- package/docs/user_permissions.js.html +2 -2
- package/docs/user_profile.js.html +105 -0
- package/docs/user_sessions.js.html +2 -2
- package/docs/user_types.js.html +22 -2
- package/docs/user_user-management-system.js.html +2 -2
- package/package.json +1 -1
- package/src/contentMetaData.js +14 -0
- package/src/index.d.ts +15 -0
- package/src/index.js +15 -0
- package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
- package/src/services/content.js +0 -2
- package/src/services/contentProgress.js +9 -0
- package/src/services/railcontent.js +39 -0
- package/src/services/user/interests.js +0 -0
- package/src/services/user/profile.js +33 -0
- package/src/services/user/types.js +20 -0
- package/src/services/user/user-management-system.js +0 -0
- package/src/services/userActivity.js +346 -261
- package/test/content.test.js +8 -6
- 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
|
|
8
|
-
import {lessonTypesMapping} from
|
|
9
|
-
import { convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay } from './dateUtils.js'
|
|
10
|
-
import {globalConfig} from
|
|
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:
|
|
27
|
-
restartStreak:
|
|
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) =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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) =>
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 {
|
|
217
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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][
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
|
500
|
+
const meta = await fetchUserPracticeMeta(userPracticesIds, userId)
|
|
501
|
+
if (!meta.data.length) return { data: { practices: [], practiceDuration: 0 } }
|
|
496
502
|
|
|
497
|
-
const formattedMeta = meta.data
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
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?.[
|
|
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 {
|
|
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]
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
let hasLastWeekPractice = sortedPracticeDays.some(
|
|
685
|
-
|
|
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 (
|
|
699
|
-
|
|
700
|
-
|
|
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
|
|
720
|
-
let
|
|
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(
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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(
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
+
}
|