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.
- package/.coderabbit.yaml +0 -0
- package/.editorconfig +0 -0
- package/.github/pull_request_template.md +0 -0
- package/.github/workflows/node.js.yml +0 -0
- package/.prettierignore +0 -0
- package/.prettierrc +0 -0
- package/CHANGELOG.md +14 -0
- package/README.md +0 -0
- package/babel.config.cjs +0 -0
- package/docs/Content-Organization.html +0 -0
- package/docs/ContentOrganization.html +2 -2
- package/docs/Gamification.html +2 -2
- package/docs/UserManagement.html +0 -0
- 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/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/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/global.html#User +0 -0
- 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-Session-Management.html +0 -0
- package/docs/module-Sessions.html +2 -2
- package/docs/module-User-Activity.html +521 -12
- package/docs/module-User-Management.html +0 -0
- package/docs/module-User-Permissions.html +0 -0
- 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/scripts/collapse.js +0 -0
- package/docs/scripts/commonNav.js +0 -0
- package/docs/scripts/linenumber.js +0 -0
- package/docs/scripts/nav.js +0 -0
- package/docs/scripts/polyfill.js +0 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
- package/docs/scripts/prettify/lang-css.js +0 -0
- package/docs/scripts/prettify/prettify.js +0 -0
- package/docs/scripts/search.js +0 -0
- package/docs/styles/jsdoc.css +0 -0
- package/docs/styles/prettify.css +0 -0
- package/docs/types.js.html +0 -0
- package/docs/userActivity.js.html +350 -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/docs/user_user-management.js.html +0 -0
- package/jest.config.js +0 -0
- package/jsdoc.json +0 -0
- package/package.json +1 -1
- package/src/contentMetaData.js +14 -0
- package/src/filterBuilder.js +0 -0
- package/src/index.d.ts +15 -0
- package/src/index.js +15 -0
- package/src/infrastructure/http/HttpClient.ts +0 -0
- package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
- package/src/infrastructure/http/index.ts +0 -0
- package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
- package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
- package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
- package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
- package/src/lib/httpHelper.js +0 -0
- package/src/lib/lastUpdated.js +0 -0
- package/src/services/api/types.js +0 -0
- package/src/services/config.js +0 -0
- package/src/services/content-org/content-org.js +0 -0
- package/src/services/content-org/playlists-types.js +0 -0
- package/src/services/content-org/playlists.js +0 -0
- package/src/services/content.js +0 -2
- package/src/services/contentAggregator.js +0 -0
- package/src/services/contentLikes.js +0 -0
- package/src/services/dataContext.js +0 -0
- package/src/services/dateUtils.js +0 -0
- package/src/services/forum.js +0 -0
- package/src/services/gamification/awards.js +0 -0
- package/src/services/gamification/gamification.js +0 -0
- package/src/services/gamification/types.js +0 -0
- package/src/services/imageSRCBuilder.js +0 -0
- package/src/services/imageSRCVerify.js +0 -0
- package/src/services/railcontent.js +39 -0
- package/src/services/recommendations.js +0 -0
- package/src/services/types.js +0 -0
- package/src/services/user/interests.js +0 -0
- package/src/services/user/management.js +0 -0
- package/src/services/user/permissions.js +0 -0
- package/src/services/user/profile.js +33 -0
- package/src/services/user/sessions.js +0 -0
- package/src/services/user/types.js +20 -0
- package/src/services/user/user-management-system.js +0 -0
- package/src/services/userActivity.js +348 -261
- package/test/HttpClient.test.js +0 -0
- package/test/content.test.js +8 -6
- package/test/contentLikes.test.js +0 -0
- package/test/contentProgress.test.js +0 -0
- package/test/dataContext.test.js +0 -0
- package/test/forum.test.js +0 -0
- package/test/imageSRCBuilder.test.js +0 -0
- package/test/imageSRCVerify.test.js +0 -0
- package/test/initializeTests.js +0 -0
- package/test/lib/lastUpdated.test.js +0 -0
- package/test/live/contentProgressLive.test.js +0 -0
- package/test/live/railcontentLive.test.js +0 -0
- package/test/localStorageMock.js +0 -0
- package/test/log.js +0 -0
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
- package/test/mockData/mockData_user_practices.json +0 -0
- package/test/sanityQueryService.test.js +6 -0
- package/test/streakMessage.test.js +0 -0
- package/test/user/permissions.test.js +0 -0
- package/test/userActivity.test.js +0 -0
- package/tools/generate-index.cjs +0 -0
- package/.yarnrc.yml +0 -1
- package/docs/module-Content-Services.html +0 -763
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
* @module User-Activity
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
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
|
|
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
|
-
]
|
|
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:
|
|
27
|
-
restartStreak:
|
|
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) =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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) =>
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 {
|
|
217
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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][
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
};
|
|
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
|
|
506
|
+
const meta = await fetchUserPracticeMeta(userPracticesIds, userId)
|
|
507
|
+
if (!meta.data.length) return { data: { practices: [], practiceDuration: 0 } }
|
|
496
508
|
|
|
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} };
|
|
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 {
|
|
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?.[
|
|
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 {
|
|
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]
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
let hasLastWeekPractice = sortedPracticeDays.some(
|
|
685
|
-
|
|
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 (
|
|
699
|
-
|
|
700
|
-
|
|
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
|
|
720
|
-
let
|
|
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(
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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(
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
+
}
|