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.
Files changed (132) hide show
  1. package/.coderabbit.yaml +0 -0
  2. package/.editorconfig +0 -0
  3. package/.github/workflows/node.js.yml +0 -0
  4. package/.prettierignore +0 -0
  5. package/.prettierrc +0 -0
  6. package/.yarnrc.yml +1 -0
  7. package/CHANGELOG.md +9 -0
  8. package/README.md +0 -0
  9. package/babel.config.cjs +0 -0
  10. package/docs/Content-Organization.html +0 -0
  11. package/docs/ContentOrganization.html +0 -0
  12. package/docs/Gamification.html +0 -0
  13. package/docs/UserManagement.html +0 -0
  14. package/docs/UserManagementSystem.html +0 -0
  15. package/docs/api_types.js.html +0 -0
  16. package/docs/config.js.html +0 -0
  17. package/docs/content-org_content-org.js.html +0 -0
  18. package/docs/content-org_playlists-types.js.html +0 -0
  19. package/docs/content-org_playlists.js.html +0 -0
  20. package/docs/content.js.html +0 -0
  21. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  22. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  23. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  24. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  25. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  26. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  27. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  28. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  29. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  30. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
  31. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  32. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  33. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  34. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  35. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
  36. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  37. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  38. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  39. package/docs/gamification_awards.js.html +0 -0
  40. package/docs/gamification_gamification.js.html +0 -0
  41. package/docs/gamification_types.js.html +0 -0
  42. package/docs/global.html +0 -0
  43. package/docs/global.html#User +0 -0
  44. package/docs/index.html +0 -0
  45. package/docs/module-Awards.html +0 -0
  46. package/docs/module-Config.html +0 -0
  47. package/docs/module-Content-Services-V2.html +0 -0
  48. package/docs/module-Content-Services.html +763 -0
  49. package/docs/module-Permissions.html +0 -0
  50. package/docs/module-Playlists.html +0 -0
  51. package/docs/module-Railcontent-Services.html +0 -0
  52. package/docs/module-Sanity-Services.html +0 -0
  53. package/docs/module-Session-Management.html +0 -0
  54. package/docs/module-Sessions.html +0 -0
  55. package/docs/module-User-Management.html +0 -0
  56. package/docs/module-User-Permissions.html +0 -0
  57. package/docs/module-UserManagement.html +0 -0
  58. package/docs/railcontent.js.html +0 -0
  59. package/docs/sanity.js.html +0 -0
  60. package/docs/scripts/collapse.js +0 -0
  61. package/docs/scripts/commonNav.js +0 -0
  62. package/docs/scripts/linenumber.js +0 -0
  63. package/docs/scripts/nav.js +0 -0
  64. package/docs/scripts/polyfill.js +0 -0
  65. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
  66. package/docs/scripts/prettify/lang-css.js +0 -0
  67. package/docs/scripts/prettify/prettify.js +0 -0
  68. package/docs/scripts/search.js +0 -0
  69. package/docs/styles/jsdoc.css +0 -0
  70. package/docs/styles/prettify.css +0 -0
  71. package/docs/types.js.html +0 -0
  72. package/docs/user_management.js.html +0 -0
  73. package/docs/user_permissions.js.html +0 -0
  74. package/docs/user_sessions.js.html +0 -0
  75. package/docs/user_types.js.html +0 -0
  76. package/docs/user_user-management-system.js.html +0 -0
  77. package/docs/user_user-management.js.html +0 -0
  78. package/jest.config.js +0 -0
  79. package/jsdoc.json +0 -0
  80. package/link_mcs.sh +0 -0
  81. package/package.json +1 -1
  82. package/src/contentMetaData.js +0 -0
  83. package/src/filterBuilder.js +0 -0
  84. package/src/index.d.ts +13 -0
  85. package/src/index.js +13 -0
  86. package/src/lib/httpHelper.js +0 -0
  87. package/src/lib/lastUpdated.js +0 -0
  88. package/src/services/api/types.js +0 -0
  89. package/src/services/config.js +0 -0
  90. package/src/services/content-org/content-org.js +0 -0
  91. package/src/services/content-org/playlists-types.js +0 -0
  92. package/src/services/content-org/playlists.js +0 -0
  93. package/src/services/content.js +0 -0
  94. package/src/services/contentLikes.js +0 -0
  95. package/src/services/contentProgress.js +0 -0
  96. package/src/services/dataContext.js +0 -0
  97. package/src/services/dateUtils.js +55 -0
  98. package/src/services/forum.js +0 -0
  99. package/src/services/gamification/awards.js +0 -0
  100. package/src/services/gamification/gamification.js +0 -0
  101. package/src/services/gamification/types.js +0 -0
  102. package/src/services/imageSRCBuilder.js +0 -0
  103. package/src/services/imageSRCVerify.js +0 -0
  104. package/src/services/railcontent.js +30 -18
  105. package/src/services/recommendations.js +10 -9
  106. package/src/services/types.js +0 -0
  107. package/src/services/user/management.js +0 -0
  108. package/src/services/user/permissions.js +0 -0
  109. package/src/services/user/sessions.js +0 -0
  110. package/src/services/user/types.js +0 -0
  111. package/src/services/user/user-management-system.js +0 -0
  112. package/src/services/userActivity.js +289 -106
  113. package/test/content.test.js +0 -0
  114. package/test/contentLikes.test.js +0 -0
  115. package/test/contentProgress.test.js +0 -0
  116. package/test/dataContext.test.js +0 -0
  117. package/test/forum.test.js +0 -0
  118. package/test/imageSRCBuilder.test.js +0 -0
  119. package/test/imageSRCVerify.test.js +0 -0
  120. package/test/initializeTests.js +0 -0
  121. package/test/lib/lastUpdated.test.js +0 -0
  122. package/test/live/contentProgressLive.test.js +0 -0
  123. package/test/live/railcontentLive.test.js +0 -0
  124. package/test/localStorageMock.js +0 -0
  125. package/test/log.js +0 -0
  126. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  127. package/test/mockData/mockData_user_practices.json +9 -0
  128. package/test/sanityQueryService.test.js +0 -0
  129. package/test/streakMessage.test.js +263 -0
  130. package/test/user/permissions.test.js +0 -0
  131. package/test/userActivity.test.js +6 -6
  132. 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
- // Get Weekly Stats
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
- const todayKey = today.toISOString().split('T')[0]
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 dayKey = day.toISOString().split('T')[0]
39
-
40
- let dayActivity = practices[dayKey] ?? null
41
- let isActive = dayKey === todayKey
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 startOfMonth = getMonday(firstDayOfMonth)
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 - startOfMonth) / (1000 * 60 * 60 * 24)) + 1;
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(startOfMonth)
73
- day.setDate(startOfMonth.getDate() + i)
74
- let dayKey = day.toISOString().split('T')[0]
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, default to 0 if undefined
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
- return await fetchHandler(url, 'delete', null);
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
- return await fetchHandler(url, 'put', null);
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 sortedPracticeDays = Object.keys(practices).sort() // Ensure dates are sorted in order
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: 1, currentWeeklyStreak: 1 }
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 longestDailyStreak = 0
332
- let prevDay = null
333
-
334
- sortedPracticeDays.forEach((dayKey) => {
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 // Reset streak if there's a gap
495
+ dailyStreak = 1;
340
496
  }
341
- prevDay = dayKey
342
- })
343
-
344
- currentDailyStreak = dailyStreak
345
-
346
- // Calculate weekly streaks
347
- let weeklyStreak = 0
348
- let prevWeek = null
349
- let currentWeekActivity = false
350
-
351
- sortedPracticeDays.forEach((dayKey) => {
352
- let date = new Date(dayKey)
353
- let weekNumber = getWeekNumber(date)
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
- if (practices[dayKey]) {
368
- currentWeekActivity = true
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 = weeklyStreak
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
+
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/test/log.js CHANGED
File without changes
@@ -0,0 +1,9 @@
1
+ {
2
+ "version": 1,
3
+ "config": { "key": 1, "enabled": 1, "checkInterval": 1, "refreshInterval": 2 },
4
+ "data": {
5
+ "practices": {
6
+ "DATE_PLACEHOLDER": [{ "duration_seconds": "DURATION_PLACEHOLDER" }]
7
+ }
8
+ }
9
+ }
File without changes