musora-content-services 2.3.6 → 2.3.8
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 +9 -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 +13 -0
- package/src/index.js +13 -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/dateUtils.js +55 -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 +30 -18
- package/src/services/recommendations.js +10 -9
- 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 +289 -106
- 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
|
@@ -6,6 +6,7 @@ import {fetchUserPractices, logUserPractice, fetchUserPracticeMeta, fetchHandler
|
|
|
6
6
|
import { DataContext, UserActivityVersionKey } from './dataContext.js'
|
|
7
7
|
import {fetchByRailContentIds} from "./sanity";
|
|
8
8
|
import {lessonTypesMapping} from "../contentTypeConfig";
|
|
9
|
+
import { convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay } from './dateUtils.js';
|
|
9
10
|
|
|
10
11
|
const recentActivity = [
|
|
11
12
|
{ 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' },
|
|
@@ -20,48 +21,109 @@ const DATA_KEY_LAST_UPDATED_TIME = 'u'
|
|
|
20
21
|
|
|
21
22
|
const DAYS = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
|
|
22
23
|
|
|
24
|
+
const streakMessages = {
|
|
25
|
+
startStreak: "Start your streak by taking any lesson!",
|
|
26
|
+
restartStreak: "Restart your streak by taking any lesson!",
|
|
27
|
+
|
|
28
|
+
// Messages when last active day is today
|
|
29
|
+
dailyStreak: (streak) => `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak! Way to keep it going!`,
|
|
30
|
+
dailyStreakShort: (streak) => `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak!`,
|
|
31
|
+
weeklyStreak: (streak) => `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep up the momentum!`,
|
|
32
|
+
greatJobWeeklyStreak: (streak) => `Great job! You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep it going!`,
|
|
33
|
+
|
|
34
|
+
// Messages when last active day is NOT today
|
|
35
|
+
dailyStreakReminder: (streak) => `You have ${getIndefiniteArticle(streak)} ${streak} day streak! Keep it going with any lesson or song!`,
|
|
36
|
+
weeklyStreakKeepUp: (streak) => `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep up the momentum!`,
|
|
37
|
+
weeklyStreakReminder: (streak) => `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep it going with any lesson or song!`,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function getIndefiniteArticle(streak) {
|
|
41
|
+
return streak === 8 || (streak >= 80 && streak <= 89) || (streak >= 800 && streak <= 899) ? 'an' : 'a'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
23
45
|
export let userActivityContext = new DataContext(UserActivityVersionKey, fetchUserPractices)
|
|
24
46
|
|
|
25
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Retrieves user activity statistics for the current week, including daily activity and streak messages.
|
|
49
|
+
*
|
|
50
|
+
* @returns {Promise<Object>} - A promise that resolves to an object containing weekly user activity statistics.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // Retrieve user activity statistics for the current week
|
|
54
|
+
* getUserWeeklyStats()
|
|
55
|
+
* .then(stats => console.log(stats))
|
|
56
|
+
* .catch(error => console.error(error));
|
|
57
|
+
*/
|
|
26
58
|
export async function getUserWeeklyStats() {
|
|
27
59
|
let data = await userActivityContext.getData()
|
|
28
|
-
|
|
29
60
|
let practices = data?.[DATA_KEY_PRACTICES] ?? {}
|
|
61
|
+
let sortedPracticeDays = Object.keys(practices)
|
|
62
|
+
.map(date => new Date(date))
|
|
63
|
+
.sort((a, b) => b - a);
|
|
30
64
|
|
|
31
|
-
let today = new Date()
|
|
65
|
+
let today = new Date();
|
|
66
|
+
today.setHours(0, 0, 0, 0);
|
|
32
67
|
let startOfWeek = getMonday(today) // Get last Monday
|
|
33
68
|
let dailyStats = []
|
|
34
|
-
|
|
69
|
+
|
|
35
70
|
for (let i = 0; i < 7; i++) {
|
|
36
71
|
let day = new Date(startOfWeek)
|
|
37
72
|
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 })
|
|
73
|
+
let hasPractice = sortedPracticeDays.some(practiceDate => isSameDate(practiceDate, day));
|
|
74
|
+
let isActive = isSameDate(today, day)
|
|
75
|
+
let type = (hasPractice ? 'tracked' : (isActive ? 'active' : 'none'))
|
|
76
|
+
dailyStats.push({ key: i, label: DAYS[i], isActive, inStreak: hasPractice, type })
|
|
44
77
|
}
|
|
45
78
|
|
|
46
79
|
let { streakMessage } = getStreaksAndMessage(practices);
|
|
47
80
|
|
|
48
|
-
return { data: { dailyActiveStats: dailyStats, streakMessage } }
|
|
81
|
+
return { data: { dailyActiveStats: dailyStats, streakMessage, practices } }
|
|
49
82
|
}
|
|
50
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Retrieves user activity statistics for a specified month, including daily and weekly activity data.
|
|
86
|
+
*
|
|
87
|
+
* @param {number} [year=new Date().getFullYear()] - The year for which to retrieve the statistics.
|
|
88
|
+
* @param {number} [month=new Date().getMonth()] - The month (0-indexed) for which to retrieve the statistics.
|
|
89
|
+
* @returns {Promise<Object>} - A promise that resolves to an object containing user activity statistics.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* // Retrieve user activity statistics for the current month
|
|
93
|
+
* getUserMonthlyStats()
|
|
94
|
+
* .then(stats => console.log(stats))
|
|
95
|
+
* .catch(error => console.error(error));
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // Retrieve user activity statistics for March 2024
|
|
99
|
+
* getUserMonthlyStats(2024, 2)
|
|
100
|
+
* .then(stats => console.log(stats))
|
|
101
|
+
* .catch(error => console.error(error));
|
|
102
|
+
*/
|
|
51
103
|
export async function getUserMonthlyStats(year = new Date().getFullYear(), month = new Date().getMonth(), day = 1) {
|
|
52
104
|
let data = await userActivityContext.getData()
|
|
53
105
|
let practices = data?.[DATA_KEY_PRACTICES] ?? {}
|
|
106
|
+
let sortedPracticeDays = Object.keys(practices)
|
|
107
|
+
.map(dateStr => {
|
|
108
|
+
const [y, m, d] = dateStr.split('-').map(Number);
|
|
109
|
+
const newDate = new Date();
|
|
110
|
+
newDate.setFullYear(y, m - 1, d);
|
|
111
|
+
return newDate;
|
|
112
|
+
})
|
|
113
|
+
.sort((a, b) => a - b);
|
|
114
|
+
|
|
54
115
|
// Get the first day of the specified month and the number of days in that month
|
|
55
116
|
let firstDayOfMonth = new Date(year, month, 1)
|
|
56
117
|
let today = new Date()
|
|
118
|
+
today.setHours(0, 0, 0, 0);
|
|
57
119
|
|
|
58
|
-
let
|
|
120
|
+
let startOfGrid = getMonday(firstDayOfMonth)
|
|
59
121
|
let endOfMonth = new Date(year, month + 1, 0)
|
|
60
122
|
while (endOfMonth.getDay() !== 0) {
|
|
61
123
|
endOfMonth.setDate(endOfMonth.getDate() + 1)
|
|
62
124
|
}
|
|
63
125
|
|
|
64
|
-
let daysInMonth = Math.ceil((endOfMonth -
|
|
126
|
+
let daysInMonth = Math.ceil((endOfMonth - startOfGrid) / (1000 * 60 * 60 * 24)) + 1;
|
|
65
127
|
|
|
66
128
|
let dailyStats = []
|
|
67
129
|
let practiceDuration = 0
|
|
@@ -69,11 +131,11 @@ export async function getUserMonthlyStats(year = new Date().getFullYear(), month
|
|
|
69
131
|
let weeklyStats = {}
|
|
70
132
|
|
|
71
133
|
for (let i = 0; i < daysInMonth; i++) {
|
|
72
|
-
let day = new Date(
|
|
73
|
-
day.setDate(
|
|
74
|
-
let dayKey = day.
|
|
134
|
+
let day = new Date(startOfGrid)
|
|
135
|
+
day.setDate(startOfGrid.getDate() + i)
|
|
136
|
+
let dayKey = `${day.getFullYear()}-${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')}`;
|
|
75
137
|
|
|
76
|
-
// Check if the user has activity for the day
|
|
138
|
+
// Check if the user has activity for the day
|
|
77
139
|
let dayActivity = practices[dayKey] ?? null
|
|
78
140
|
let weekKey = getWeekNumber(day)
|
|
79
141
|
|
|
@@ -81,13 +143,13 @@ export async function getUserMonthlyStats(year = new Date().getFullYear(), month
|
|
|
81
143
|
weeklyStats[weekKey] = { key: weekKey, inStreak: false };
|
|
82
144
|
}
|
|
83
145
|
|
|
84
|
-
if (dayActivity) {
|
|
146
|
+
if (dayActivity !== null) {
|
|
85
147
|
practiceDuration += dayActivity.reduce((sum, entry) => sum + entry.duration_seconds, 0)
|
|
86
148
|
daysPracticed++;
|
|
87
149
|
}
|
|
88
|
-
let isActive = dayKey === today.toISOString().split('T')[0]
|
|
89
|
-
let type = (dayActivity !== null ? 'tracked' : (isActive ? 'active' : 'none'))
|
|
90
150
|
|
|
151
|
+
let isActive = isSameDate(today, day)
|
|
152
|
+
let type = ((dayActivity !== null) ? 'tracked' : (isActive ? 'active' : 'none'))
|
|
91
153
|
let isInStreak = dayActivity !== null;
|
|
92
154
|
if (isInStreak) {
|
|
93
155
|
weeklyStats[weekKey].inStreak = true;
|
|
@@ -126,7 +188,36 @@ export async function getUserPractices() {
|
|
|
126
188
|
let data = await userActivityContext.getData()
|
|
127
189
|
return data?.[DATA_KEY_PRACTICES] ?? []
|
|
128
190
|
}
|
|
129
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Records user practice data and updates both the remote and local activity context.
|
|
193
|
+
*
|
|
194
|
+
* @param {Object} practiceDetails - The details of the practice session.
|
|
195
|
+
* @param {number} practiceDetails.duration_seconds - The duration of the practice session in seconds.
|
|
196
|
+
* @param {boolean} [practiceDetails.auto=true] - Whether the session was automatically logged.
|
|
197
|
+
* @param {number} [practiceDetails.content_id] - The ID of the practiced content (if available).
|
|
198
|
+
* @param {number} [practiceDetails.category_id] - The ID of the associated category (if available).
|
|
199
|
+
* @param {string} [practiceDetails.title] - The title of the practice session (max 64 characters).
|
|
200
|
+
* @param {string} [practiceDetails.thumbnail_url] - The URL of the session's thumbnail (max 255 characters).
|
|
201
|
+
* @returns {Promise<Object>} - A promise that resolves to the response from logging the user practice.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* // Record an auto practice session with content ID
|
|
205
|
+
* recordUserPractice({ content_id: 123, duration_seconds: 300 })
|
|
206
|
+
* .then(response => console.log(response))
|
|
207
|
+
* .catch(error => console.error(error));
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* // Record a custom practice session with additional details
|
|
211
|
+
* recordUserPractice({
|
|
212
|
+
* duration_seconds: 600,
|
|
213
|
+
* auto: false,
|
|
214
|
+
* category_id: 5,
|
|
215
|
+
* title: "Guitar Warm-up",
|
|
216
|
+
* thumbnail_url: "https://example.com/thumbnail.jpg"
|
|
217
|
+
* })
|
|
218
|
+
* .then(response => console.log(response))
|
|
219
|
+
* .catch(error => console.error(error));
|
|
220
|
+
*/
|
|
130
221
|
export async function recordUserPractice(practiceDetails) {
|
|
131
222
|
practiceDetails.auto = 0;
|
|
132
223
|
if (practiceDetails.content_id) {
|
|
@@ -160,12 +251,46 @@ export async function recordUserPractice(practiceDetails) {
|
|
|
160
251
|
}
|
|
161
252
|
);
|
|
162
253
|
}
|
|
163
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Updates a user's practice session with new details and syncs the changes remotely.
|
|
256
|
+
*
|
|
257
|
+
* @param {number} id - The unique identifier of the practice session to update.
|
|
258
|
+
* @param {Object} practiceDetails - The updated details of the practice session.
|
|
259
|
+
* @param {number} [practiceDetails.duration_seconds] - The duration of the practice session in seconds.
|
|
260
|
+
* @param {number} [practiceDetails.category_id] - The ID of the associated category (if available).
|
|
261
|
+
* @param {string} [practiceDetails.title] - The title of the practice session (max 64 characters).
|
|
262
|
+
* @param {string} [practiceDetails.thumbnail_url] - The URL of the session's thumbnail (max 255 characters).
|
|
263
|
+
* @returns {Promise<Object>} - A promise that resolves to the response from updating the user practice.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* // Update a practice session's duration
|
|
267
|
+
* updateUserPractice(123, { duration_seconds: 600 })
|
|
268
|
+
* .then(response => console.log(response))
|
|
269
|
+
* .catch(error => console.error(error));
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* // Change a practice session to manual and update its category
|
|
273
|
+
* updateUserPractice(456, { auto: false, category_id: 8 })
|
|
274
|
+
* .then(response => console.log(response))
|
|
275
|
+
* .catch(error => console.error(error));
|
|
276
|
+
*/
|
|
164
277
|
export async function updateUserPractice(id, practiceDetails) {
|
|
165
278
|
const url = `/api/user/practices/v1/practices/${id}`
|
|
166
279
|
return await fetchHandler(url, 'PUT', null, practiceDetails)
|
|
167
280
|
}
|
|
168
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Removes a user's practice session by ID, updating both the local and remote activity context.
|
|
284
|
+
*
|
|
285
|
+
* @param {number} id - The unique identifier of the practice session to be removed.
|
|
286
|
+
* @returns {Promise<void>} - A promise that resolves once the practice session is removed.
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* // Remove a practice session with ID 123
|
|
290
|
+
* removeUserPractice(123)
|
|
291
|
+
* .then(() => console.log("Practice session removed successfully"))
|
|
292
|
+
* .catch(error => console.error(error));
|
|
293
|
+
*/
|
|
169
294
|
export async function removeUserPractice(id) {
|
|
170
295
|
let url = `/api/user/practices/v1/practices${buildQueryString([id])}`;
|
|
171
296
|
await userActivityContext.update(
|
|
@@ -184,6 +309,18 @@ export async function removeUserPractice(id) {
|
|
|
184
309
|
);
|
|
185
310
|
}
|
|
186
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Restores a previously deleted user's practice session by ID, updating both the local and remote activity context.
|
|
314
|
+
*
|
|
315
|
+
* @param {number} id - The unique identifier of the practice session to be restored.
|
|
316
|
+
* @returns {Promise<Object>} - A promise that resolves to the response containing the restored practice session data.
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* // Restore a deleted practice session with ID 123
|
|
320
|
+
* restoreUserPractice(123)
|
|
321
|
+
* .then(response => console.log("Practice session restored:", response))
|
|
322
|
+
* .catch(error => console.error(error));
|
|
323
|
+
*/
|
|
187
324
|
export async function restoreUserPractice(id) {
|
|
188
325
|
let url = `/api/user/practices/v1/practices/restore${buildQueryString([id])}`;
|
|
189
326
|
const response = await fetchHandler(url, 'put');
|
|
@@ -208,14 +345,51 @@ export async function deletePracticeSession(day) {
|
|
|
208
345
|
if (!userPracticesIds.length) return [];
|
|
209
346
|
|
|
210
347
|
const url = `/api/user/practices/v1/practices${buildQueryString(userPracticesIds)}`;
|
|
211
|
-
|
|
348
|
+
await userActivityContext.update(async function (localContext) {
|
|
349
|
+
if (localContext.data?.[DATA_KEY_PRACTICES]?.[day]) {
|
|
350
|
+
localContext.data[DATA_KEY_PRACTICES][day] = localContext.data[DATA_KEY_PRACTICES][day].filter(
|
|
351
|
+
practice => !userPracticesIds.includes(practice.id)
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return await fetchHandler(url, 'DELETE', null);
|
|
212
357
|
}
|
|
213
358
|
|
|
214
359
|
export async function restorePracticeSession(date) {
|
|
215
360
|
const url = `/api/user/practices/v1/practices/restore?date=${date}`;
|
|
216
|
-
|
|
361
|
+
const response = await fetchHandler(url, 'PUT', null);
|
|
362
|
+
|
|
363
|
+
if (response?.data) {
|
|
364
|
+
await userActivityContext.updateLocal(async function (localContext) {
|
|
365
|
+
if (!localContext.data[DATA_KEY_PRACTICES][date]) {
|
|
366
|
+
localContext.data[DATA_KEY_PRACTICES][date] = [];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
response.data.forEach(restoredPractice => {
|
|
370
|
+
localContext.data[DATA_KEY_PRACTICES][date].push({
|
|
371
|
+
id: restoredPractice.id,
|
|
372
|
+
duration_seconds: restoredPractice.duration_seconds,
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return response;
|
|
217
379
|
}
|
|
218
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Retrieves and formats a user's practice sessions for a specific day.
|
|
383
|
+
*
|
|
384
|
+
* @param {string} day - The date for which practice sessions should be retrieved (format: YYYY-MM-DD).
|
|
385
|
+
* @returns {Promise<Object>} - A promise that resolves to an object containing the practice sessions and total practice duration.
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* // Get practice sessions for a specific day
|
|
389
|
+
* getPracticeSessions("2025-03-31")
|
|
390
|
+
* .then(response => console.log(response))
|
|
391
|
+
* .catch(error => console.error(error));
|
|
392
|
+
*/
|
|
219
393
|
export async function getPracticeSessions(day) {
|
|
220
394
|
const userPracticesIds = await getUserPracticeIds(day);
|
|
221
395
|
if (!userPracticesIds.length) return { data: { practices: [], practiceDuration: 0 } };
|
|
@@ -235,7 +409,10 @@ export async function getPracticeSessions(day) {
|
|
|
235
409
|
return null;
|
|
236
410
|
};
|
|
237
411
|
|
|
412
|
+
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
413
|
+
|
|
238
414
|
const formattedMeta = meta.data.map(practice => {
|
|
415
|
+
const utcDate = new Date(practice.created_at);
|
|
239
416
|
const content = contents.find(c => c.id === practice.content_id) || {};
|
|
240
417
|
return {
|
|
241
418
|
id: practice.id,
|
|
@@ -249,6 +426,7 @@ export async function getPracticeSessions(day) {
|
|
|
249
426
|
content_type: getFormattedType(content.type || ''),
|
|
250
427
|
content_id: practice.content_id || null,
|
|
251
428
|
content_brand: content.brand || null,
|
|
429
|
+
created_at: convertToTimeZone(utcDate, userTimeZone)
|
|
252
430
|
};
|
|
253
431
|
});
|
|
254
432
|
return { data: { practices: formattedMeta, practiceDuration } };
|
|
@@ -259,20 +437,17 @@ export async function getRecentActivity() {
|
|
|
259
437
|
return { data: recentActivity };
|
|
260
438
|
}
|
|
261
439
|
|
|
262
|
-
function getStreaksAndMessage(practices)
|
|
263
|
-
{
|
|
264
|
-
let { currentDailyStreak, currentWeeklyStreak } = calculateStreaks(practices)
|
|
440
|
+
function getStreaksAndMessage(practices) {
|
|
441
|
+
let { currentDailyStreak, currentWeeklyStreak, streakMessage } = calculateStreaks(practices, true);
|
|
265
442
|
|
|
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
443
|
return {
|
|
270
444
|
currentDailyStreak,
|
|
271
445
|
currentWeeklyStreak,
|
|
272
446
|
streakMessage,
|
|
273
|
-
}
|
|
447
|
+
};
|
|
274
448
|
}
|
|
275
449
|
|
|
450
|
+
|
|
276
451
|
async function getUserPracticeIds(day = new Date().toISOString().split('T')[0]) {
|
|
277
452
|
let data = await userActivityContext.getData();
|
|
278
453
|
let practices = data?.[DATA_KEY_PRACTICES] ?? {};
|
|
@@ -291,95 +466,103 @@ function buildQueryString(ids, paramName = 'practice_ids') {
|
|
|
291
466
|
return '?' + ids.map(id => `${paramName}[]=${id}`).join('&');
|
|
292
467
|
}
|
|
293
468
|
|
|
294
|
-
|
|
295
|
-
// Helper: Get start of the week (Monday)
|
|
296
|
-
function getMonday(d) {
|
|
297
|
-
d = new Date(d)
|
|
298
|
-
var day = d.getDay(),
|
|
299
|
-
diff = d.getDate() - day + (day == 0 ? -6 : 1) // adjust when day is sunday
|
|
300
|
-
return new Date(d.setDate(diff))
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Helper: Get the week number
|
|
304
|
-
function getWeekNumber(date) {
|
|
305
|
-
let startOfYear = new Date(date.getFullYear(), 0, 1)
|
|
306
|
-
let diff = date - startOfYear
|
|
307
|
-
let oneWeekMs = 7 * 24 * 60 * 60 * 1000
|
|
308
|
-
return Math.ceil((diff / oneWeekMs) + startOfYear.getDay() / 7)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Helper: function to check if two dates are consecutive days
|
|
312
|
-
function isNextDay(prevDateStr, currentDateStr) {
|
|
313
|
-
let prevDate = new Date(prevDateStr)
|
|
314
|
-
let currentDate = new Date(currentDateStr)
|
|
315
|
-
let diff = (currentDate - prevDate) / (1000 * 60 * 60 * 24)
|
|
316
|
-
return diff === 1
|
|
317
|
-
}
|
|
318
|
-
|
|
319
469
|
// Helper: Calculate streaks
|
|
320
|
-
function calculateStreaks(practices) {
|
|
321
|
-
let currentDailyStreak = 0
|
|
322
|
-
let currentWeeklyStreak = 0
|
|
323
|
-
|
|
324
|
-
let
|
|
325
|
-
|
|
470
|
+
function calculateStreaks(practices, includeStreakMessage = false) {
|
|
471
|
+
let currentDailyStreak = 0;
|
|
472
|
+
let currentWeeklyStreak = 0;
|
|
473
|
+
let lastActiveDay = null;
|
|
474
|
+
let streakMessage = '';
|
|
475
|
+
|
|
476
|
+
let sortedPracticeDays = Object.keys(practices)
|
|
477
|
+
.map(dateStr => {
|
|
478
|
+
const [year, month, day] = dateStr.split('-').map(Number);
|
|
479
|
+
const newDate = new Date();
|
|
480
|
+
newDate.setFullYear(year, month - 1, day);
|
|
481
|
+
return newDate;
|
|
482
|
+
})
|
|
483
|
+
.sort((a, b) => a - b);
|
|
326
484
|
if (sortedPracticeDays.length === 0) {
|
|
327
|
-
return { currentDailyStreak:
|
|
485
|
+
return { currentDailyStreak: 0, currentWeeklyStreak: 0, streakMessage: streakMessages.startStreak };
|
|
328
486
|
}
|
|
487
|
+
lastActiveDay = sortedPracticeDays[sortedPracticeDays.length - 1];
|
|
329
488
|
|
|
330
|
-
let dailyStreak = 0
|
|
331
|
-
let
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (prevDay === null || isNextDay(prevDay, dayKey)) {
|
|
336
|
-
dailyStreak++
|
|
337
|
-
longestDailyStreak = Math.max(longestDailyStreak, dailyStreak)
|
|
489
|
+
let dailyStreak = 0;
|
|
490
|
+
let prevDay = null;
|
|
491
|
+
sortedPracticeDays.forEach((currentDay) => {
|
|
492
|
+
if (prevDay === null || isNextDay(prevDay, currentDay)) {
|
|
493
|
+
dailyStreak++;
|
|
338
494
|
} else {
|
|
339
|
-
dailyStreak = 1
|
|
495
|
+
dailyStreak = 1;
|
|
340
496
|
}
|
|
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
|
|
497
|
+
prevDay = currentDay;
|
|
498
|
+
});
|
|
499
|
+
currentDailyStreak = dailyStreak;
|
|
500
|
+
|
|
501
|
+
// Weekly streak calculation
|
|
502
|
+
let weekNumbers = new Set(sortedPracticeDays.map(date => getWeekNumber(date)));
|
|
503
|
+
let weeklyStreak = 0;
|
|
504
|
+
let lastWeek = null;
|
|
505
|
+
[...weekNumbers].sort((a, b) => b - a).forEach(week => {
|
|
506
|
+
if (lastWeek === null || week === lastWeek - 1) {
|
|
507
|
+
weeklyStreak++;
|
|
508
|
+
} else {
|
|
509
|
+
return;
|
|
365
510
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
511
|
+
lastWeek = week;
|
|
512
|
+
});
|
|
513
|
+
currentWeeklyStreak = weeklyStreak;
|
|
514
|
+
|
|
515
|
+
// Calculate streak message only if includeStreakMessage is true
|
|
516
|
+
if (includeStreakMessage) {
|
|
517
|
+
let today = new Date();
|
|
518
|
+
let yesterday = new Date(today);
|
|
519
|
+
yesterday.setDate(today.getDate() - 1);
|
|
520
|
+
|
|
521
|
+
let currentWeekStart = getMonday(today);
|
|
522
|
+
let lastWeekStart = new Date(currentWeekStart);
|
|
523
|
+
lastWeekStart.setDate(currentWeekStart.getDate() - 7);
|
|
524
|
+
|
|
525
|
+
let hasYesterdayPractice = sortedPracticeDays.some(date =>
|
|
526
|
+
isSameDate(date, yesterday)
|
|
527
|
+
);
|
|
528
|
+
let hasCurrentWeekPractice = sortedPracticeDays.some(date => date >= currentWeekStart);
|
|
529
|
+
let hasCurrentWeekPreviousPractice = sortedPracticeDays.some(date => date >= currentWeekStart && date < today);
|
|
530
|
+
let hasLastWeekPractice = sortedPracticeDays.some(date => date >= lastWeekStart && date < currentWeekStart);
|
|
531
|
+
let hasOlderPractice = sortedPracticeDays.some(date => date < lastWeekStart );
|
|
532
|
+
|
|
533
|
+
if (isSameDate(lastActiveDay, today)) {
|
|
534
|
+
if (hasYesterdayPractice) {
|
|
535
|
+
streakMessage = streakMessages.dailyStreak(currentDailyStreak);
|
|
536
|
+
} else if (hasCurrentWeekPreviousPractice) {
|
|
537
|
+
streakMessage = streakMessages.weeklyStreak(currentWeeklyStreak);
|
|
538
|
+
} else if (hasLastWeekPractice) {
|
|
539
|
+
streakMessage = streakMessages.greatJobWeeklyStreak(currentWeeklyStreak);
|
|
540
|
+
} else {
|
|
541
|
+
streakMessage = streakMessages.dailyStreakShort(currentDailyStreak);
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
if ((hasYesterdayPractice && currentDailyStreak >= 2) || (hasYesterdayPractice && sortedPracticeDays.length == 1)
|
|
545
|
+
|| (hasYesterdayPractice && !hasLastWeekPractice && hasOlderPractice)){
|
|
546
|
+
streakMessage = streakMessages.dailyStreakReminder(currentDailyStreak);
|
|
547
|
+
} else if (hasCurrentWeekPractice) {
|
|
548
|
+
streakMessage = streakMessages.weeklyStreakKeepUp(currentWeeklyStreak);
|
|
549
|
+
} else if (hasLastWeekPractice) {
|
|
550
|
+
streakMessage = streakMessages.weeklyStreakReminder(currentWeeklyStreak);
|
|
551
|
+
} else {
|
|
552
|
+
streakMessage = streakMessages.restartStreak;
|
|
553
|
+
}
|
|
369
554
|
}
|
|
370
|
-
})
|
|
371
555
|
|
|
372
|
-
// If the user has activity in the current week, count it
|
|
373
|
-
if (currentWeekActivity) {
|
|
374
|
-
weeklyStreak++
|
|
375
556
|
}
|
|
376
557
|
|
|
377
|
-
currentWeeklyStreak
|
|
378
|
-
|
|
379
|
-
return { currentDailyStreak, currentWeeklyStreak }
|
|
558
|
+
return { currentDailyStreak, currentWeeklyStreak, streakMessage };
|
|
380
559
|
}
|
|
381
560
|
|
|
382
561
|
|
|
383
562
|
|
|
384
563
|
|
|
385
564
|
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
|
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
|