musora-content-services 2.3.6 → 2.3.7
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/workflows/node.js.yml +0 -0
- package/.prettierignore +0 -0
- package/.prettierrc +0 -0
- package/.yarnrc.yml +1 -0
- package/CHANGELOG.md +2 -0
- package/README.md +0 -0
- package/babel.config.cjs +0 -0
- package/docs/Content-Organization.html +0 -0
- package/docs/ContentOrganization.html +0 -0
- package/docs/Gamification.html +0 -0
- package/docs/UserManagement.html +0 -0
- package/docs/UserManagementSystem.html +0 -0
- package/docs/api_types.js.html +0 -0
- package/docs/config.js.html +0 -0
- package/docs/content-org_content-org.js.html +0 -0
- package/docs/content-org_playlists-types.js.html +0 -0
- package/docs/content-org_playlists.js.html +0 -0
- package/docs/content.js.html +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/gamification_awards.js.html +0 -0
- package/docs/gamification_gamification.js.html +0 -0
- package/docs/gamification_types.js.html +0 -0
- package/docs/global.html +0 -0
- package/docs/global.html#User +0 -0
- package/docs/index.html +0 -0
- package/docs/module-Awards.html +0 -0
- package/docs/module-Config.html +0 -0
- package/docs/module-Content-Services-V2.html +0 -0
- package/docs/module-Content-Services.html +763 -0
- package/docs/module-Permissions.html +0 -0
- package/docs/module-Playlists.html +0 -0
- package/docs/module-Railcontent-Services.html +0 -0
- package/docs/module-Sanity-Services.html +0 -0
- package/docs/module-Session-Management.html +0 -0
- package/docs/module-Sessions.html +0 -0
- package/docs/module-User-Management.html +0 -0
- package/docs/module-User-Permissions.html +0 -0
- package/docs/module-UserManagement.html +0 -0
- package/docs/railcontent.js.html +0 -0
- package/docs/sanity.js.html +0 -0
- package/docs/scripts/collapse.js +0 -0
- package/docs/scripts/commonNav.js +0 -0
- package/docs/scripts/linenumber.js +0 -0
- package/docs/scripts/nav.js +0 -0
- package/docs/scripts/polyfill.js +0 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
- package/docs/scripts/prettify/lang-css.js +0 -0
- package/docs/scripts/prettify/prettify.js +0 -0
- package/docs/scripts/search.js +0 -0
- package/docs/styles/jsdoc.css +0 -0
- package/docs/styles/prettify.css +0 -0
- package/docs/types.js.html +0 -0
- package/docs/user_management.js.html +0 -0
- package/docs/user_permissions.js.html +0 -0
- package/docs/user_sessions.js.html +0 -0
- package/docs/user_types.js.html +0 -0
- package/docs/user_user-management-system.js.html +0 -0
- package/docs/user_user-management.js.html +0 -0
- package/jest.config.js +0 -0
- package/jsdoc.json +0 -0
- package/link_mcs.sh +0 -0
- package/package.json +1 -1
- package/src/contentMetaData.js +0 -0
- package/src/filterBuilder.js +0 -0
- package/src/index.d.ts +0 -0
- package/src/index.js +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 -0
- package/src/services/contentLikes.js +0 -0
- package/src/services/contentProgress.js +0 -0
- package/src/services/dataContext.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 +16 -3
- package/src/services/recommendations.js +0 -0
- package/src/services/types.js +0 -0
- package/src/services/user/management.js +0 -0
- package/src/services/user/permissions.js +0 -0
- package/src/services/user/sessions.js +0 -0
- package/src/services/user/types.js +0 -0
- package/src/services/user/user-management-system.js +0 -0
- package/src/services/userActivity.js +301 -90
- package/test/content.test.js +0 -0
- 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 +9 -0
- package/test/sanityQueryService.test.js +0 -0
- package/test/streakMessage.test.js +263 -0
- package/test/user/permissions.test.js +0 -0
- package/test/userActivity.test.js +6 -6
- package/tools/generate-index.cjs +0 -0
|
@@ -20,48 +20,109 @@ const DATA_KEY_LAST_UPDATED_TIME = 'u'
|
|
|
20
20
|
|
|
21
21
|
const DAYS = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
|
|
22
22
|
|
|
23
|
+
const streakMessages = {
|
|
24
|
+
startStreak: "Start your streak by taking any lesson!",
|
|
25
|
+
restartStreak: "Restart your streak by taking any lesson!",
|
|
26
|
+
|
|
27
|
+
// Messages when last active day is today
|
|
28
|
+
dailyStreak: (streak) => `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak! Way to keep it going!`,
|
|
29
|
+
dailyStreakShort: (streak) => `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak!`,
|
|
30
|
+
weeklyStreak: (streak) => `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep up the momentum!`,
|
|
31
|
+
greatJobWeeklyStreak: (streak) => `Great job! You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep it going!`,
|
|
32
|
+
|
|
33
|
+
// Messages when last active day is NOT today
|
|
34
|
+
dailyStreakReminder: (streak) => `You have ${getIndefiniteArticle(streak)} ${streak} day streak! Keep it going with any lesson or song!`,
|
|
35
|
+
weeklyStreakKeepUp: (streak) => `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep up the momentum!`,
|
|
36
|
+
weeklyStreakReminder: (streak) => `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep it going with any lesson or song!`,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function getIndefiniteArticle(streak) {
|
|
40
|
+
return streak === 8 || (streak >= 80 && streak <= 89) || (streak >= 800 && streak <= 899) ? 'an' : 'a'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
23
44
|
export let userActivityContext = new DataContext(UserActivityVersionKey, fetchUserPractices)
|
|
24
45
|
|
|
25
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Retrieves user activity statistics for the current week, including daily activity and streak messages.
|
|
48
|
+
*
|
|
49
|
+
* @returns {Promise<Object>} - A promise that resolves to an object containing weekly user activity statistics.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Retrieve user activity statistics for the current week
|
|
53
|
+
* getUserWeeklyStats()
|
|
54
|
+
* .then(stats => console.log(stats))
|
|
55
|
+
* .catch(error => console.error(error));
|
|
56
|
+
*/
|
|
26
57
|
export async function getUserWeeklyStats() {
|
|
27
58
|
let data = await userActivityContext.getData()
|
|
28
|
-
|
|
29
59
|
let practices = data?.[DATA_KEY_PRACTICES] ?? {}
|
|
60
|
+
let sortedPracticeDays = Object.keys(practices)
|
|
61
|
+
.map(date => new Date(date))
|
|
62
|
+
.sort((a, b) => b - a);
|
|
30
63
|
|
|
31
|
-
let today = new Date()
|
|
64
|
+
let today = new Date();
|
|
65
|
+
today.setHours(0, 0, 0, 0);
|
|
32
66
|
let startOfWeek = getMonday(today) // Get last Monday
|
|
33
67
|
let dailyStats = []
|
|
34
|
-
|
|
68
|
+
|
|
35
69
|
for (let i = 0; i < 7; i++) {
|
|
36
70
|
let day = new Date(startOfWeek)
|
|
37
71
|
day.setDate(startOfWeek.getDate() + i)
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
-
let
|
|
41
|
-
|
|
42
|
-
let type = (dayActivity !== null ? 'tracked' : (isActive ? 'active' : 'none'))
|
|
43
|
-
dailyStats.push({ key: i, label: DAYS[i], isActive, inStreak: dayActivity !== null, type })
|
|
72
|
+
let hasPractice = sortedPracticeDays.some(practiceDate => isSameDate(practiceDate, day));
|
|
73
|
+
let isActive = isSameDate(today, day)
|
|
74
|
+
let type = (hasPractice ? 'tracked' : (isActive ? 'active' : 'none'))
|
|
75
|
+
dailyStats.push({ key: i, label: DAYS[i], isActive, inStreak: hasPractice, type })
|
|
44
76
|
}
|
|
45
77
|
|
|
46
78
|
let { streakMessage } = getStreaksAndMessage(practices);
|
|
47
79
|
|
|
48
|
-
return { data: { dailyActiveStats: dailyStats, streakMessage } }
|
|
80
|
+
return { data: { dailyActiveStats: dailyStats, streakMessage, practices } }
|
|
49
81
|
}
|
|
50
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Retrieves user activity statistics for a specified month, including daily and weekly activity data.
|
|
85
|
+
*
|
|
86
|
+
* @param {number} [year=new Date().getFullYear()] - The year for which to retrieve the statistics.
|
|
87
|
+
* @param {number} [month=new Date().getMonth()] - The month (0-indexed) for which to retrieve the statistics.
|
|
88
|
+
* @returns {Promise<Object>} - A promise that resolves to an object containing user activity statistics.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* // Retrieve user activity statistics for the current month
|
|
92
|
+
* getUserMonthlyStats()
|
|
93
|
+
* .then(stats => console.log(stats))
|
|
94
|
+
* .catch(error => console.error(error));
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* // Retrieve user activity statistics for March 2024
|
|
98
|
+
* getUserMonthlyStats(2024, 2)
|
|
99
|
+
* .then(stats => console.log(stats))
|
|
100
|
+
* .catch(error => console.error(error));
|
|
101
|
+
*/
|
|
51
102
|
export async function getUserMonthlyStats(year = new Date().getFullYear(), month = new Date().getMonth(), day = 1) {
|
|
52
103
|
let data = await userActivityContext.getData()
|
|
53
104
|
let practices = data?.[DATA_KEY_PRACTICES] ?? {}
|
|
105
|
+
let sortedPracticeDays = Object.keys(practices)
|
|
106
|
+
.map(dateStr => {
|
|
107
|
+
const [y, m, d] = dateStr.split('-').map(Number);
|
|
108
|
+
const newDate = new Date();
|
|
109
|
+
newDate.setFullYear(y, m - 1, d);
|
|
110
|
+
return newDate;
|
|
111
|
+
})
|
|
112
|
+
.sort((a, b) => a - b);
|
|
113
|
+
|
|
54
114
|
// Get the first day of the specified month and the number of days in that month
|
|
55
115
|
let firstDayOfMonth = new Date(year, month, 1)
|
|
56
116
|
let today = new Date()
|
|
117
|
+
today.setHours(0, 0, 0, 0);
|
|
57
118
|
|
|
58
|
-
let
|
|
119
|
+
let startOfGrid = getMonday(firstDayOfMonth)
|
|
59
120
|
let endOfMonth = new Date(year, month + 1, 0)
|
|
60
121
|
while (endOfMonth.getDay() !== 0) {
|
|
61
122
|
endOfMonth.setDate(endOfMonth.getDate() + 1)
|
|
62
123
|
}
|
|
63
124
|
|
|
64
|
-
let daysInMonth = Math.ceil((endOfMonth -
|
|
125
|
+
let daysInMonth = Math.ceil((endOfMonth - startOfGrid) / (1000 * 60 * 60 * 24)) + 1;
|
|
65
126
|
|
|
66
127
|
let dailyStats = []
|
|
67
128
|
let practiceDuration = 0
|
|
@@ -69,11 +130,11 @@ export async function getUserMonthlyStats(year = new Date().getFullYear(), month
|
|
|
69
130
|
let weeklyStats = {}
|
|
70
131
|
|
|
71
132
|
for (let i = 0; i < daysInMonth; i++) {
|
|
72
|
-
let day = new Date(
|
|
73
|
-
day.setDate(
|
|
74
|
-
let dayKey = day.
|
|
133
|
+
let day = new Date(startOfGrid)
|
|
134
|
+
day.setDate(startOfGrid.getDate() + i)
|
|
135
|
+
let dayKey = `${day.getFullYear()}-${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')}`;
|
|
75
136
|
|
|
76
|
-
// Check if the user has activity for the day
|
|
137
|
+
// Check if the user has activity for the day
|
|
77
138
|
let dayActivity = practices[dayKey] ?? null
|
|
78
139
|
let weekKey = getWeekNumber(day)
|
|
79
140
|
|
|
@@ -81,13 +142,13 @@ export async function getUserMonthlyStats(year = new Date().getFullYear(), month
|
|
|
81
142
|
weeklyStats[weekKey] = { key: weekKey, inStreak: false };
|
|
82
143
|
}
|
|
83
144
|
|
|
84
|
-
if (dayActivity) {
|
|
145
|
+
if (dayActivity !== null) {
|
|
85
146
|
practiceDuration += dayActivity.reduce((sum, entry) => sum + entry.duration_seconds, 0)
|
|
86
147
|
daysPracticed++;
|
|
87
148
|
}
|
|
88
|
-
let isActive = dayKey === today.toISOString().split('T')[0]
|
|
89
|
-
let type = (dayActivity !== null ? 'tracked' : (isActive ? 'active' : 'none'))
|
|
90
149
|
|
|
150
|
+
let isActive = isSameDate(today, day)
|
|
151
|
+
let type = ((dayActivity !== null) ? 'tracked' : (isActive ? 'active' : 'none'))
|
|
91
152
|
let isInStreak = dayActivity !== null;
|
|
92
153
|
if (isInStreak) {
|
|
93
154
|
weeklyStats[weekKey].inStreak = true;
|
|
@@ -126,7 +187,36 @@ export async function getUserPractices() {
|
|
|
126
187
|
let data = await userActivityContext.getData()
|
|
127
188
|
return data?.[DATA_KEY_PRACTICES] ?? []
|
|
128
189
|
}
|
|
129
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Records user practice data and updates both the remote and local activity context.
|
|
192
|
+
*
|
|
193
|
+
* @param {Object} practiceDetails - The details of the practice session.
|
|
194
|
+
* @param {number} practiceDetails.duration_seconds - The duration of the practice session in seconds.
|
|
195
|
+
* @param {boolean} [practiceDetails.auto=true] - Whether the session was automatically logged.
|
|
196
|
+
* @param {number} [practiceDetails.content_id] - The ID of the practiced content (if available).
|
|
197
|
+
* @param {number} [practiceDetails.category_id] - The ID of the associated category (if available).
|
|
198
|
+
* @param {string} [practiceDetails.title] - The title of the practice session (max 64 characters).
|
|
199
|
+
* @param {string} [practiceDetails.thumbnail_url] - The URL of the session's thumbnail (max 255 characters).
|
|
200
|
+
* @returns {Promise<Object>} - A promise that resolves to the response from logging the user practice.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* // Record an auto practice session with content ID
|
|
204
|
+
* recordUserPractice({ content_id: 123, duration_seconds: 300 })
|
|
205
|
+
* .then(response => console.log(response))
|
|
206
|
+
* .catch(error => console.error(error));
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* // Record a custom practice session with additional details
|
|
210
|
+
* recordUserPractice({
|
|
211
|
+
* duration_seconds: 600,
|
|
212
|
+
* auto: false,
|
|
213
|
+
* category_id: 5,
|
|
214
|
+
* title: "Guitar Warm-up",
|
|
215
|
+
* thumbnail_url: "https://example.com/thumbnail.jpg"
|
|
216
|
+
* })
|
|
217
|
+
* .then(response => console.log(response))
|
|
218
|
+
* .catch(error => console.error(error));
|
|
219
|
+
*/
|
|
130
220
|
export async function recordUserPractice(practiceDetails) {
|
|
131
221
|
practiceDetails.auto = 0;
|
|
132
222
|
if (practiceDetails.content_id) {
|
|
@@ -160,12 +250,46 @@ export async function recordUserPractice(practiceDetails) {
|
|
|
160
250
|
}
|
|
161
251
|
);
|
|
162
252
|
}
|
|
163
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Updates a user's practice session with new details and syncs the changes remotely.
|
|
255
|
+
*
|
|
256
|
+
* @param {number} id - The unique identifier of the practice session to update.
|
|
257
|
+
* @param {Object} practiceDetails - The updated details of the practice session.
|
|
258
|
+
* @param {number} [practiceDetails.duration_seconds] - The duration of the practice session in seconds.
|
|
259
|
+
* @param {number} [practiceDetails.category_id] - The ID of the associated category (if available).
|
|
260
|
+
* @param {string} [practiceDetails.title] - The title of the practice session (max 64 characters).
|
|
261
|
+
* @param {string} [practiceDetails.thumbnail_url] - The URL of the session's thumbnail (max 255 characters).
|
|
262
|
+
* @returns {Promise<Object>} - A promise that resolves to the response from updating the user practice.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* // Update a practice session's duration
|
|
266
|
+
* updateUserPractice(123, { duration_seconds: 600 })
|
|
267
|
+
* .then(response => console.log(response))
|
|
268
|
+
* .catch(error => console.error(error));
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* // Change a practice session to manual and update its category
|
|
272
|
+
* updateUserPractice(456, { auto: false, category_id: 8 })
|
|
273
|
+
* .then(response => console.log(response))
|
|
274
|
+
* .catch(error => console.error(error));
|
|
275
|
+
*/
|
|
164
276
|
export async function updateUserPractice(id, practiceDetails) {
|
|
165
277
|
const url = `/api/user/practices/v1/practices/${id}`
|
|
166
278
|
return await fetchHandler(url, 'PUT', null, practiceDetails)
|
|
167
279
|
}
|
|
168
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Removes a user's practice session by ID, updating both the local and remote activity context.
|
|
283
|
+
*
|
|
284
|
+
* @param {number} id - The unique identifier of the practice session to be removed.
|
|
285
|
+
* @returns {Promise<void>} - A promise that resolves once the practice session is removed.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* // Remove a practice session with ID 123
|
|
289
|
+
* removeUserPractice(123)
|
|
290
|
+
* .then(() => console.log("Practice session removed successfully"))
|
|
291
|
+
* .catch(error => console.error(error));
|
|
292
|
+
*/
|
|
169
293
|
export async function removeUserPractice(id) {
|
|
170
294
|
let url = `/api/user/practices/v1/practices${buildQueryString([id])}`;
|
|
171
295
|
await userActivityContext.update(
|
|
@@ -184,6 +308,18 @@ export async function removeUserPractice(id) {
|
|
|
184
308
|
);
|
|
185
309
|
}
|
|
186
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Restores a previously deleted user's practice session by ID, updating both the local and remote activity context.
|
|
313
|
+
*
|
|
314
|
+
* @param {number} id - The unique identifier of the practice session to be restored.
|
|
315
|
+
* @returns {Promise<Object>} - A promise that resolves to the response containing the restored practice session data.
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* // Restore a deleted practice session with ID 123
|
|
319
|
+
* restoreUserPractice(123)
|
|
320
|
+
* .then(response => console.log("Practice session restored:", response))
|
|
321
|
+
* .catch(error => console.error(error));
|
|
322
|
+
*/
|
|
187
323
|
export async function restoreUserPractice(id) {
|
|
188
324
|
let url = `/api/user/practices/v1/practices/restore${buildQueryString([id])}`;
|
|
189
325
|
const response = await fetchHandler(url, 'put');
|
|
@@ -208,14 +344,51 @@ export async function deletePracticeSession(day) {
|
|
|
208
344
|
if (!userPracticesIds.length) return [];
|
|
209
345
|
|
|
210
346
|
const url = `/api/user/practices/v1/practices${buildQueryString(userPracticesIds)}`;
|
|
211
|
-
|
|
347
|
+
await userActivityContext.update(async function (localContext) {
|
|
348
|
+
if (localContext.data?.[DATA_KEY_PRACTICES]?.[day]) {
|
|
349
|
+
localContext.data[DATA_KEY_PRACTICES][day] = localContext.data[DATA_KEY_PRACTICES][day].filter(
|
|
350
|
+
practice => !userPracticesIds.includes(practice.id)
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
return await fetchHandler(url, 'DELETE', null);
|
|
212
356
|
}
|
|
213
357
|
|
|
214
358
|
export async function restorePracticeSession(date) {
|
|
215
359
|
const url = `/api/user/practices/v1/practices/restore?date=${date}`;
|
|
216
|
-
|
|
360
|
+
const response = await fetchHandler(url, 'PUT', null);
|
|
361
|
+
|
|
362
|
+
if (response?.data) {
|
|
363
|
+
await userActivityContext.updateLocal(async function (localContext) {
|
|
364
|
+
if (!localContext.data[DATA_KEY_PRACTICES][date]) {
|
|
365
|
+
localContext.data[DATA_KEY_PRACTICES][date] = [];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
response.data.forEach(restoredPractice => {
|
|
369
|
+
localContext.data[DATA_KEY_PRACTICES][date].push({
|
|
370
|
+
id: restoredPractice.id,
|
|
371
|
+
duration_seconds: restoredPractice.duration_seconds,
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return response;
|
|
217
378
|
}
|
|
218
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Retrieves and formats a user's practice sessions for a specific day.
|
|
382
|
+
*
|
|
383
|
+
* @param {string} day - The date for which practice sessions should be retrieved (format: YYYY-MM-DD).
|
|
384
|
+
* @returns {Promise<Object>} - A promise that resolves to an object containing the practice sessions and total practice duration.
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* // Get practice sessions for a specific day
|
|
388
|
+
* getPracticeSessions("2025-03-31")
|
|
389
|
+
* .then(response => console.log(response))
|
|
390
|
+
* .catch(error => console.error(error));
|
|
391
|
+
*/
|
|
219
392
|
export async function getPracticeSessions(day) {
|
|
220
393
|
const userPracticesIds = await getUserPracticeIds(day);
|
|
221
394
|
if (!userPracticesIds.length) return { data: { practices: [], practiceDuration: 0 } };
|
|
@@ -259,20 +432,17 @@ export async function getRecentActivity() {
|
|
|
259
432
|
return { data: recentActivity };
|
|
260
433
|
}
|
|
261
434
|
|
|
262
|
-
function getStreaksAndMessage(practices)
|
|
263
|
-
{
|
|
264
|
-
let { currentDailyStreak, currentWeeklyStreak } = calculateStreaks(practices)
|
|
435
|
+
function getStreaksAndMessage(practices) {
|
|
436
|
+
let { currentDailyStreak, currentWeeklyStreak, streakMessage } = calculateStreaks(practices, true);
|
|
265
437
|
|
|
266
|
-
let streakMessage = currentWeeklyStreak > 1
|
|
267
|
-
? `That's ${currentWeeklyStreak} weeks in a row! Keep going!`
|
|
268
|
-
: `Nice! You have a ${currentDailyStreak} day streak! Way to keep it going!`
|
|
269
438
|
return {
|
|
270
439
|
currentDailyStreak,
|
|
271
440
|
currentWeeklyStreak,
|
|
272
441
|
streakMessage,
|
|
273
|
-
}
|
|
442
|
+
};
|
|
274
443
|
}
|
|
275
444
|
|
|
445
|
+
|
|
276
446
|
async function getUserPracticeIds(day = new Date().toISOString().split('T')[0]) {
|
|
277
447
|
let data = await userActivityContext.getData();
|
|
278
448
|
let practices = data?.[DATA_KEY_PRACTICES] ?? {};
|
|
@@ -301,85 +471,126 @@ function getMonday(d) {
|
|
|
301
471
|
}
|
|
302
472
|
|
|
303
473
|
// Helper: Get the week number
|
|
304
|
-
function getWeekNumber(
|
|
305
|
-
let
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
474
|
+
function getWeekNumber(d) {
|
|
475
|
+
let newDate = new Date(d.getTime());
|
|
476
|
+
newDate.setUTCDate(newDate.getUTCDate() + 4 - (newDate.getUTCDay()||7));
|
|
477
|
+
var yearStart = new Date(Date.UTC(newDate.getUTCFullYear(),0,1));
|
|
478
|
+
var weekNo = Math.ceil(( ( (newDate - yearStart) / 86400000) + 1)/7);
|
|
479
|
+
return weekNo;
|
|
309
480
|
}
|
|
310
481
|
|
|
311
482
|
// Helper: function to check if two dates are consecutive days
|
|
312
|
-
function isNextDay(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
483
|
+
function isNextDay(prev, current) {
|
|
484
|
+
const prevDate = new Date(prev.getFullYear(), prev.getMonth(), prev.getDate());
|
|
485
|
+
const nextDate = new Date(prevDate);
|
|
486
|
+
nextDate.setDate(prevDate.getDate() + 1); // Add 1 day
|
|
487
|
+
|
|
488
|
+
return (
|
|
489
|
+
nextDate.getFullYear() === current.getFullYear() &&
|
|
490
|
+
nextDate.getMonth() === current.getMonth() &&
|
|
491
|
+
nextDate.getDate() === current.getDate()
|
|
492
|
+
);
|
|
317
493
|
}
|
|
318
494
|
|
|
319
495
|
// Helper: Calculate streaks
|
|
320
|
-
function calculateStreaks(practices) {
|
|
321
|
-
let currentDailyStreak = 0
|
|
322
|
-
let currentWeeklyStreak = 0
|
|
323
|
-
|
|
324
|
-
let
|
|
325
|
-
|
|
496
|
+
function calculateStreaks(practices, includeStreakMessage = false) {
|
|
497
|
+
let currentDailyStreak = 0;
|
|
498
|
+
let currentWeeklyStreak = 0;
|
|
499
|
+
let lastActiveDay = null;
|
|
500
|
+
let streakMessage = '';
|
|
501
|
+
|
|
502
|
+
let sortedPracticeDays = Object.keys(practices)
|
|
503
|
+
.map(dateStr => {
|
|
504
|
+
const [year, month, day] = dateStr.split('-').map(Number);
|
|
505
|
+
const newDate = new Date();
|
|
506
|
+
newDate.setFullYear(year, month - 1, day);
|
|
507
|
+
return newDate;
|
|
508
|
+
})
|
|
509
|
+
.sort((a, b) => a - b);
|
|
326
510
|
if (sortedPracticeDays.length === 0) {
|
|
327
|
-
return { currentDailyStreak:
|
|
511
|
+
return { currentDailyStreak: 0, currentWeeklyStreak: 0, streakMessage: streakMessages.startStreak };
|
|
328
512
|
}
|
|
513
|
+
lastActiveDay = sortedPracticeDays[sortedPracticeDays.length - 1];
|
|
329
514
|
|
|
330
|
-
let dailyStreak = 0
|
|
331
|
-
let
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (prevDay === null || isNextDay(prevDay, dayKey)) {
|
|
336
|
-
dailyStreak++
|
|
337
|
-
longestDailyStreak = Math.max(longestDailyStreak, dailyStreak)
|
|
515
|
+
let dailyStreak = 0;
|
|
516
|
+
let prevDay = null;
|
|
517
|
+
sortedPracticeDays.forEach((currentDay) => {
|
|
518
|
+
if (prevDay === null || isNextDay(prevDay, currentDay)) {
|
|
519
|
+
dailyStreak++;
|
|
338
520
|
} else {
|
|
339
|
-
dailyStreak = 1
|
|
521
|
+
dailyStreak = 1;
|
|
340
522
|
}
|
|
341
|
-
prevDay =
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
let weeklyStreak = 0
|
|
348
|
-
let
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (prevWeek === null) {
|
|
356
|
-
prevWeek = weekNumber
|
|
357
|
-
currentWeekActivity = true
|
|
358
|
-
} else if (weekNumber !== prevWeek) {
|
|
359
|
-
// A new week has started
|
|
360
|
-
if (currentWeekActivity) {
|
|
361
|
-
weeklyStreak++
|
|
362
|
-
currentWeekActivity = false
|
|
363
|
-
}
|
|
364
|
-
prevWeek = weekNumber
|
|
523
|
+
prevDay = currentDay;
|
|
524
|
+
});
|
|
525
|
+
currentDailyStreak = dailyStreak;
|
|
526
|
+
|
|
527
|
+
// Weekly streak calculation
|
|
528
|
+
let weekNumbers = new Set(sortedPracticeDays.map(date => getWeekNumber(date)));
|
|
529
|
+
let weeklyStreak = 0;
|
|
530
|
+
let lastWeek = null;
|
|
531
|
+
[...weekNumbers].sort((a, b) => b - a).forEach(week => {
|
|
532
|
+
if (lastWeek === null || week === lastWeek - 1) {
|
|
533
|
+
weeklyStreak++;
|
|
534
|
+
} else {
|
|
535
|
+
return;
|
|
365
536
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
537
|
+
lastWeek = week;
|
|
538
|
+
});
|
|
539
|
+
currentWeeklyStreak = weeklyStreak;
|
|
540
|
+
|
|
541
|
+
// Calculate streak message only if includeStreakMessage is true
|
|
542
|
+
if (includeStreakMessage) {
|
|
543
|
+
let today = new Date();
|
|
544
|
+
let yesterday = new Date(today);
|
|
545
|
+
yesterday.setDate(today.getDate() - 1);
|
|
546
|
+
|
|
547
|
+
let currentWeekStart = getMonday(today);
|
|
548
|
+
let lastWeekStart = new Date(currentWeekStart);
|
|
549
|
+
lastWeekStart.setDate(currentWeekStart.getDate() - 7);
|
|
550
|
+
|
|
551
|
+
let hasYesterdayPractice = sortedPracticeDays.some(date =>
|
|
552
|
+
isSameDate(date, yesterday)
|
|
553
|
+
);
|
|
554
|
+
let hasCurrentWeekPractice = sortedPracticeDays.some(date => date >= currentWeekStart);
|
|
555
|
+
let hasCurrentWeekPreviousPractice = sortedPracticeDays.some(date => date >= currentWeekStart && date < today);
|
|
556
|
+
let hasLastWeekPractice = sortedPracticeDays.some(date => date >= lastWeekStart && date < currentWeekStart);
|
|
557
|
+
let hasOlderPractice = sortedPracticeDays.some(date => date < lastWeekStart );
|
|
558
|
+
|
|
559
|
+
if (isSameDate(lastActiveDay, today)) {
|
|
560
|
+
if (hasYesterdayPractice) {
|
|
561
|
+
streakMessage = streakMessages.dailyStreak(currentDailyStreak);
|
|
562
|
+
} else if (hasCurrentWeekPreviousPractice) {
|
|
563
|
+
streakMessage = streakMessages.weeklyStreak(currentWeeklyStreak);
|
|
564
|
+
} else if (hasLastWeekPractice) {
|
|
565
|
+
streakMessage = streakMessages.greatJobWeeklyStreak(currentWeeklyStreak);
|
|
566
|
+
} else {
|
|
567
|
+
streakMessage = streakMessages.dailyStreakShort(currentDailyStreak);
|
|
568
|
+
}
|
|
569
|
+
} else {
|
|
570
|
+
if ((hasYesterdayPractice && currentDailyStreak >= 2) || (hasYesterdayPractice && sortedPracticeDays.length == 1)
|
|
571
|
+
|| (hasYesterdayPractice && !hasLastWeekPractice && hasOlderPractice)){
|
|
572
|
+
streakMessage = streakMessages.dailyStreakReminder(currentDailyStreak);
|
|
573
|
+
} else if (hasCurrentWeekPractice) {
|
|
574
|
+
streakMessage = streakMessages.weeklyStreakKeepUp(currentWeeklyStreak);
|
|
575
|
+
} else if (hasLastWeekPractice) {
|
|
576
|
+
streakMessage = streakMessages.weeklyStreakReminder(currentWeeklyStreak);
|
|
577
|
+
} else {
|
|
578
|
+
streakMessage = streakMessages.restartStreak;
|
|
579
|
+
}
|
|
369
580
|
}
|
|
370
|
-
})
|
|
371
581
|
|
|
372
|
-
// If the user has activity in the current week, count it
|
|
373
|
-
if (currentWeekActivity) {
|
|
374
|
-
weeklyStreak++
|
|
375
582
|
}
|
|
376
583
|
|
|
377
|
-
currentWeeklyStreak
|
|
584
|
+
return { currentDailyStreak, currentWeeklyStreak, streakMessage };
|
|
585
|
+
}
|
|
378
586
|
|
|
379
|
-
|
|
587
|
+
function isSameDate(date1, date2, method = '') {
|
|
588
|
+
return date1.toISOString().split('T')[0] === date2.toISOString().split('T')[0];
|
|
380
589
|
}
|
|
381
590
|
|
|
382
591
|
|
|
383
592
|
|
|
384
593
|
|
|
385
594
|
|
|
595
|
+
|
|
596
|
+
|
package/test/content.test.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/dataContext.test.js
CHANGED
|
File without changes
|
package/test/forum.test.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/initializeTests.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/localStorageMock.js
CHANGED
|
File without changes
|
package/test/log.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|