musora-content-services 2.27.2 → 2.27.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [2.27.4](https://github.com/railroadmedia/musora-content-services/compare/v2.27.3...v2.27.4) (2025-07-23)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **T3PS-306:** add custom method to delete profile picture ([#369](https://github.com/railroadmedia/musora-content-services/issues/369)) ([61634db](https://github.com/railroadmedia/musora-content-services/commit/61634db6923f9ca4fdde9a5c20f18e8b61dd78ec))
11
+
12
+ ### [2.27.3](https://github.com/railroadmedia/musora-content-services/compare/v2.27.0...v2.27.3) (2025-07-23)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **last-engaged:** add playlist last engaged endpoint ([#361](https://github.com/railroadmedia/musora-content-services/issues/361)) ([3396207](https://github.com/railroadmedia/musora-content-services/commit/33962077500e47ff9cba93fb23fd28b63eb4112a))
18
+ * MU2-710 - Update MCS to use new endpoint to get recent activities tabs with data ([5cfcc27](https://github.com/railroadmedia/musora-content-services/commit/5cfcc27745072907e745a1015350249f6dbccb85))
19
+ * Song Tutorials are showing up in Recent Songs as collections ([68d4559](https://github.com/railroadmedia/musora-content-services/commit/68d4559f8ee528441a8a4bff73796fbd48c8321b))
20
+ * T3PS-266 - Practice tracker meta ([fa3c962](https://github.com/railroadmedia/musora-content-services/commit/fa3c962580978ce6aef4fdf5ad8023852bf92a6d))
21
+ * T3PS-415: progress rows for courses/song tutorials/packs ([4ad26cc](https://github.com/railroadmedia/musora-content-services/commit/4ad26ccbfbf2eade11882ae246d102d72093c1c5))
22
+ * Timezone issue - Migrate native Date to Dayjs ([c37def1](https://github.com/railroadmedia/musora-content-services/commit/c37def12b471fe570e15c7be220ab8d52e1dad88))
23
+ * **Userpractice:** add category_id and instrument_id to userPractice ([2c92c6f](https://github.com/railroadmedia/musora-content-services/commit/2c92c6f6adcb7fac657c3b12e1c3f5e927cff214))
24
+
5
25
  ### [2.27.2](https://github.com/railroadmedia/musora-content-services/compare/v2.27.1...v2.27.2) (2025-07-21)
6
26
 
7
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.27.2",
3
+ "version": "2.27.4",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -32,6 +32,7 @@
32
32
  "@babel/preset-env": "^7.25.3",
33
33
  "@sanity/client": "^5.4.2",
34
34
  "babel-jest": "^29.7.0",
35
+ "dayjs": "^1.11.13",
35
36
  "docdash": "^2.0.2",
36
37
  "groqd": "^0.15.12"
37
38
  }
@@ -249,6 +249,7 @@ const commonMetadata = {
249
249
  },
250
250
  'recent-activities': {
251
251
  name: 'Recent Activity',
252
+ onlyAvailableTabs: true,
252
253
  tabs: [
253
254
  Tabs.RecentAll,
254
255
  Tabs.RecentActivityLessons,
@@ -182,15 +182,16 @@ export const lessonTypesMapping = {
182
182
  export const getNextLessonLessonParentTypes = ['course', 'guided-course', 'pack', 'pack-bundle', 'song-tutorial'];
183
183
 
184
184
  export const progressTypesMapping = {
185
- 'lesson': [...singleLessonTypes,...practiceAlongsLessonTypes, ...liveArchivesLessonTypes, ...performancesLessonTypes, ...studentArchivesLessonTypes, ...documentariesLessonTypes, 'live'],
185
+ 'lesson': [...singleLessonTypes,...practiceAlongsLessonTypes, ...liveArchivesLessonTypes, ...performancesLessonTypes, ...studentArchivesLessonTypes, ...documentariesLessonTypes, 'live', 'pack-bundle-lesson', 'course-part'],
186
186
  'course': ['course'],
187
187
  'show': showsLessonTypes,
188
- 'song tutorial': tutorialsLessonTypes,
188
+ 'song tutorial': [...tutorialsLessonTypes, 'song-tutorial-children'],
189
189
  'songs': transcriptionsLessonTypes,
190
190
  'play-along': playAlongLessonTypes,
191
191
  'guided course': ['guided-course'],
192
192
  'pack': ['pack', 'semester-pack'],
193
- 'method': ['learning-path']
193
+ 'method': ['learning-path'],
194
+ 'jam track': ['jam-track'],
194
195
  };
195
196
 
196
197
  export const songs = {
@@ -207,9 +208,9 @@ export const filterTypes = {
207
208
 
208
209
  export const recentTypes = {
209
210
  lessons: [...individualLessonsTypes, 'course-part', 'pack-bundle-lesson', 'challenge-part', 'guided-course-part', 'quick-tips'],
210
- songs: [...tutorialsLessonTypes, ...transcriptionsLessonTypes, ...playAlongLessonTypes],
211
+ songs: [...transcriptionsLessonTypes, ...playAlongLessonTypes, 'song-tutorial-children'],
211
212
  home: [...individualLessonsTypes, ...tutorialsLessonTypes, ...transcriptionsLessonTypes, ...playAlongLessonTypes,
212
- 'guided-course', 'learning-path', 'live']
213
+ 'guided-course', 'learning-path', 'live', 'course', 'pack']
213
214
  }
214
215
 
215
216
  export let contentTypeConfig = {
package/src/index.d.ts CHANGED
@@ -83,7 +83,8 @@ import {
83
83
  getTimeRemainingUntilLocal,
84
84
  getWeekNumber,
85
85
  isNextDay,
86
- isSameDate
86
+ isSameDate,
87
+ toDayjs
87
88
  } from './services/dateUtils.js';
88
89
 
89
90
  import {
@@ -278,6 +279,7 @@ import {
278
279
  } from './services/user/permissions.js';
279
280
 
280
281
  import {
282
+ deleteProfilePicture,
281
283
  otherStats
282
284
  } from './services/user/profile.js';
283
285
 
@@ -291,6 +293,7 @@ import {
291
293
  createPracticeNotes,
292
294
  deletePracticeSession,
293
295
  deleteUserActivity,
296
+ fetchRecentActivitiesActiveTabs,
294
297
  findIncompleteLesson,
295
298
  getPracticeNotes,
296
299
  getPracticeSessions,
@@ -333,6 +336,7 @@ declare module 'musora-content-services' {
333
336
  deletePicture,
334
337
  deletePlaylist,
335
338
  deletePracticeSession,
339
+ deleteProfilePicture,
336
340
  deleteUserActivity,
337
341
  duplicatePlaylist,
338
342
  editComment,
@@ -400,6 +404,7 @@ declare module 'musora-content-services' {
400
404
  fetchPlaylist,
401
405
  fetchPlaylistItems,
402
406
  fetchRecent,
407
+ fetchRecentActivitiesActiveTabs,
403
408
  fetchRecentUserActivities,
404
409
  fetchRelatedLessons,
405
410
  fetchRelatedRecommendedContent,
@@ -529,6 +534,7 @@ declare module 'musora-content-services' {
529
534
  setupAccount,
530
535
  startLiveEventPolling,
531
536
  status,
537
+ toDayjs,
532
538
  togglePlaylistPrivate,
533
539
  unEnrollUserInGuidedCourse,
534
540
  unPinGuidedCourse,
package/src/index.js CHANGED
@@ -83,7 +83,8 @@ import {
83
83
  getTimeRemainingUntilLocal,
84
84
  getWeekNumber,
85
85
  isNextDay,
86
- isSameDate
86
+ isSameDate,
87
+ toDayjs
87
88
  } from './services/dateUtils.js';
88
89
 
89
90
  import {
@@ -278,6 +279,7 @@ import {
278
279
  } from './services/user/permissions.js';
279
280
 
280
281
  import {
282
+ deleteProfilePicture,
281
283
  otherStats
282
284
  } from './services/user/profile.js';
283
285
 
@@ -291,6 +293,7 @@ import {
291
293
  createPracticeNotes,
292
294
  deletePracticeSession,
293
295
  deleteUserActivity,
296
+ fetchRecentActivitiesActiveTabs,
294
297
  findIncompleteLesson,
295
298
  getPracticeNotes,
296
299
  getPracticeSessions,
@@ -332,6 +335,7 @@ export {
332
335
  deletePicture,
333
336
  deletePlaylist,
334
337
  deletePracticeSession,
338
+ deleteProfilePicture,
335
339
  deleteUserActivity,
336
340
  duplicatePlaylist,
337
341
  editComment,
@@ -399,6 +403,7 @@ export {
399
403
  fetchPlaylist,
400
404
  fetchPlaylistItems,
401
405
  fetchRecent,
406
+ fetchRecentActivitiesActiveTabs,
402
407
  fetchRecentUserActivities,
403
408
  fetchRelatedLessons,
404
409
  fetchRelatedRecommendedContent,
@@ -528,6 +533,7 @@ export {
528
533
  setupAccount,
529
534
  startLiveEventPolling,
530
535
  status,
536
+ toDayjs,
531
537
  togglePlaylistPrivate,
532
538
  unEnrollUserInGuidedCourse,
533
539
  unPinGuidedCourse,
@@ -15,6 +15,7 @@ const DATA_KEY_STATUS = 's'
15
15
  const DATA_KEY_PROGRESS = 'p'
16
16
  const DATA_KEY_RESUME_TIME = 't'
17
17
  const DATA_KEY_LAST_UPDATED_TIME = 'u'
18
+ const DATA_KEY_BRAND = 'b'
18
19
 
19
20
  export let dataContext = new DataContext(ContentProgressVersionKey, fetchContentProgress)
20
21
 
@@ -191,7 +192,7 @@ export async function getAllStartedOrCompleted({ limit = null, onlyIds = true, b
191
192
  const isRecent = item[DATA_KEY_LAST_UPDATED_TIME] >= oneMonthAgoInSeconds
192
193
  const isCorrectBrand = !brand || !item.b || item.b === brand
193
194
  const isNotExcluded = !excludedSet.has(id)
194
- return isRelevantStatus && isRecent && isCorrectBrand && isNotExcluded
195
+ return isRelevantStatus && isCorrectBrand && isNotExcluded
195
196
  })
196
197
  .sort(([, a], [, b]) => {
197
198
  const v1 = a[DATA_KEY_LAST_UPDATED_TIME]
@@ -371,7 +372,7 @@ export async function recordWatchSession(
371
372
  //TODO: Good enough for Alpha, Refine in reliability improvements
372
373
  sessionData[sessionId] = sessionData[sessionId] || {}
373
374
  const secondsSinceLastUpdate = Math.ceil(secondsPlayed - (sessionData[sessionId][contentId] ?? 0))
374
- await recordUserPractice({ content_id: contentId, duration_seconds: secondsSinceLastUpdate, category_id: categoryId, instrument_id: instrumentId })
375
+ await recordUserPractice({ content_id: contentId, duration_seconds: secondsSinceLastUpdate, instrument_id: instrumentId })
375
376
  } catch (error) {
376
377
  console.error('Failed to record user practice:', error)
377
378
  }
@@ -438,8 +439,11 @@ function bubbleProgress(hierarchy, contentId, localContext) {
438
439
  return localContext.data[childId]?.[DATA_KEY_PROGRESS] ?? 0
439
440
  })
440
441
  let progress = Math.round(childProgress.reduce((a, b) => a + b, 0) / childProgress.length)
442
+ const brand =localContext.data[contentId]?.[DATA_KEY_BRAND] ?? null
441
443
  data[DATA_KEY_PROGRESS] = progress
442
444
  data[DATA_KEY_STATUS] = progress === 100 ? STATE_COMPLETED : STATE_STARTED
445
+ data[DATA_KEY_LAST_UPDATED_TIME] = Math.round(new Date().getTime() / 1000)
446
+ data[DATA_KEY_BRAND] = brand
443
447
  localContext.data[parentId] = data
444
448
  bubbleProgress(hierarchy, parentId, localContext)
445
449
  }
@@ -1,58 +1,47 @@
1
1
  // dateUtils.js
2
+ import dayjs from "dayjs";
3
+ import utc from "dayjs/plugin/utc";
4
+ import timezone from "dayjs/plugin/timezone";
5
+ import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
6
+ import isoWeek from 'dayjs/plugin/isoWeek';
7
+ import isBetween from 'dayjs/plugin/isBetween'
8
+ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
2
9
 
3
- export function convertToTimeZone(date, timeZone) {
4
- const formatter = new Intl.DateTimeFormat('en-US', {
5
- timeZone,
6
- year: 'numeric',
7
- month: '2-digit',
8
- day: '2-digit',
9
- hour: '2-digit',
10
- minute: '2-digit',
11
- second: '2-digit',
12
- hour12: false,
13
- });
10
+ dayjs.extend(utc);
11
+ dayjs.extend(timezone);
12
+ dayjs.extend(isSameOrAfter)
13
+ dayjs.extend(isoWeek)
14
+ dayjs.extend(isBetween)
15
+ dayjs.extend(isSameOrBefore)
14
16
 
15
- const parts = formatter.formatToParts(date).reduce((acc, part) => {
16
- if (part.type !== 'literal') acc[part.type] = part.value;
17
- return acc;
18
- }, {});
17
+ export function toDayjs(date, timeZone = 'UTC') {
18
+ return dayjs.tz(date, timeZone).startOf('day')
19
+ }
19
20
 
20
- return new Date(`${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}`);
21
+ export function convertToTimeZone(date, timeZone) {
22
+ return dayjs(date).tz(timeZone).format('YYYY-MM-DD');
21
23
  }
22
- // Get start of the week (Monday)
23
- export function getMonday(date) {
24
- const d = new Date(date);
25
- const day = d.getDay();
26
- const diff = d.getDate() - day + (day === 0 ? -6 : 1);
27
- return new Date(d.setDate(diff));
24
+
25
+ export function getMonday(date, timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone) {
26
+ // Use isoWeekday(1) - Monday is 1
27
+ return toDayjs(date, timeZone).isoWeekday(1)
28
28
  }
29
29
 
30
- // Get the week number
31
- export function getWeekNumber(d) {
32
- let newDate = new Date(d.getTime());
33
- newDate.setUTCDate(newDate.getUTCDate() + 4 - (newDate.getUTCDay()||7));
34
- var yearStart = new Date(Date.UTC(newDate.getUTCFullYear(),0,1));
35
- var weekNo = Math.ceil(( ( (newDate - yearStart) / 86400000) + 1)/7);
36
- return weekNo;
30
+ // Get the ISO week number for a dayjs object
31
+ export function getWeekNumber(date) {
32
+ return dayjs(date).isoWeek()
37
33
  }
38
34
  //Check if two dates are the same
39
- export function isSameDate(date1, date2, method = '') {
40
- return date1.toISOString().split('T')[0] === date2.toISOString().split('T')[0];
35
+ export function isSameDate(date1, date2) {
36
+ return dayjs(date1).isSame(dayjs(date2), 'day')
41
37
  }
42
38
 
43
39
  // Check if two dates are consecutive days
44
- export function isNextDay(prev, current) {
45
- const prevDate = new Date(prev.getFullYear(), prev.getMonth(), prev.getDate());
46
- const nextDate = new Date(prevDate);
47
- nextDate.setDate(prevDate.getDate() + 1); // Add 1 day
48
-
49
- return (
50
- nextDate.getFullYear() === current.getFullYear() &&
51
- nextDate.getMonth() === current.getMonth() &&
52
- nextDate.getDate() === current.getDate()
53
- );
40
+ export function isNextDay(date1, date2) {
41
+ const d1 = dayjs(date1).startOf('day')
42
+ const d2 = dayjs(date2).startOf('day')
43
+ return d2.diff(d1, 'day') === 1
54
44
  }
55
-
56
45
  export function getTimeRemainingUntilLocal(targetUtcIsoString, {withTotalSeconds} = {}) {
57
46
  const targetUTC = new Date(targetUtcIsoString);
58
47
  if (isNaN(targetUTC.getTime())) {
File without changes
@@ -791,17 +791,9 @@ export async function fetchUserPractices(currentVersion = 0, { userId } = {}) {
791
791
 
792
792
  const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
793
793
 
794
-
795
794
  const formattedPractices = userPractices.reduce((acc, practice) => {
796
795
  // Convert UTC date to user's local date (still a Date object)
797
- const utcDate = new Date(practice.day);
798
- const localDate = convertToTimeZone(utcDate, userTimeZone);
799
-
800
- const userTimeZoneDay =
801
- localDate.getFullYear() + '-' +
802
- String(localDate.getMonth() + 1).padStart(2, '0') + '-' +
803
- String(localDate.getDate()).padStart(2, '0');
804
-
796
+ const userTimeZoneDay = convertToTimeZone(practice.day, userTimeZone);
805
797
  if (!acc[userTimeZoneDay]) {
806
798
  acc[userTimeZoneDay] = [];
807
799
  }
@@ -31,6 +31,7 @@ import {
31
31
  import { arrayToStringRepresentation, FilterBuilder } from '../filterBuilder.js'
32
32
  import { fetchUserPermissions } from './user/permissions.js'
33
33
  import { getAllCompleted, getAllStarted, getAllStartedOrCompleted } from './contentProgress.js'
34
+ import {fetchRecentActivitiesActiveTabs} from "./userActivity.js";
34
35
 
35
36
  /**
36
37
  * Exported functions that are excluded from index generation.
@@ -2152,7 +2153,12 @@ export async function fetchShowsData(brand) {
2152
2153
  * .catch(error => console.error(error));
2153
2154
  */
2154
2155
  export async function fetchMetadata(brand, type) {
2155
- const processedData = processMetadata(brand, type, true)
2156
+ let processedData = processMetadata(brand, type, true)
2157
+ if(processedData?.onlyAvailableTabs === true) {
2158
+ const activeTabs = await fetchRecentActivitiesActiveTabs()
2159
+ processedData.tabs = activeTabs
2160
+ }
2161
+
2156
2162
  return processedData ? processedData : {}
2157
2163
  }
2158
2164
 
@@ -31,3 +31,16 @@ export async function otherStats(userId = globalConfig.sessionConfig.userId) {
31
31
  total_practice_time: longestStreaks.totalPracticeSeconds,
32
32
  }
33
33
  }
34
+
35
+ export async function deleteProfilePicture() {
36
+ const url = `${baseUrl}/v1/users/profile_picture`
37
+ const response = await fetchHandler(url, 'DELETE')
38
+
39
+ if (!response.ok) {
40
+ const problemDetails = await response.json()
41
+ console.log('Error deleting profile picture:', problemDetails.detail)
42
+ throw new Error(`Delete failed: ${problemDetails.detail}`)
43
+ }
44
+
45
+ return response.json()
46
+ }
@@ -16,7 +16,7 @@ import { DataContext, UserActivityVersionKey } from './dataContext.js'
16
16
  import { fetchByRailContentIds, fetchShows } from './sanity'
17
17
  import {fetchPlaylist, fetchUserPlaylists} from "./content-org/playlists"
18
18
  import {pinnedGuidedCourses} from "./content-org/guided-courses"
19
- import {convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay, getTimeRemainingUntilLocal} from './dateUtils.js'
19
+ import {convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay, getTimeRemainingUntilLocal, toDayjs} from './dateUtils.js'
20
20
  import { globalConfig } from './config'
21
21
  import {collectionLessonTypes, lessonTypesMapping, progressTypesMapping, showsLessonTypes, songs} from "../contentTypeConfig";
22
22
  import {
@@ -27,6 +27,9 @@ import {
27
27
  } from "./contentProgress";
28
28
  import {TabResponseType} from "../contentMetaData";
29
29
  import {isContentLikedByIds} from "./contentLikes.js";
30
+ import dayjs from 'dayjs'
31
+ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
32
+ import weekOfYear from 'dayjs/plugin/weekOfYear'
30
33
 
31
34
  const DATA_KEY_PRACTICES = 'practices'
32
35
  const DATA_KEY_LAST_UPDATED_TIME = 'u'
@@ -86,24 +89,21 @@ export let userActivityContext = new DataContext(UserActivityVersionKey, fetchUs
86
89
  * .catch(error => console.error(error));
87
90
  */
88
91
  export async function getUserWeeklyStats() {
92
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
89
93
  let data = await userActivityContext.getData()
90
94
  let practices = data?.[DATA_KEY_PRACTICES] ?? {}
91
95
  let sortedPracticeDays = Object.keys(practices)
92
- .map((date) => new Date(date))
93
- .sort((a, b) => b - a)
94
-
95
- let today = new Date()
96
- today.setHours(0, 0, 0, 0)
97
- let startOfWeek = getMonday(today) // Get last Monday
96
+ .map((date) => toDayjs(date)) // Convert to dayjs instance
97
+ .sort((a, b) => b.valueOf() - a.valueOf())
98
+ let today = dayjs()
99
+ let startOfWeek = getMonday(today, timeZone) // Get last Monday
98
100
  let dailyStats = []
99
-
100
101
  for (let i = 0; i < 7; i++) {
101
- let day = new Date(startOfWeek)
102
- day.setDate(startOfWeek.getDate() + i)
103
- let hasPractice = sortedPracticeDays.some((practiceDate) => isSameDate(practiceDate, day))
102
+ const day = startOfWeek.add(i, 'day')
103
+ let hasPractice = sortedPracticeDays.some((practiceDate) => isSameDate(practiceDate, day.format('YYYY-MM-DD')))
104
104
  let isActive = isSameDate(today, day)
105
105
  let type = hasPractice ? 'tracked' : isActive ? 'active' : 'none'
106
- dailyStats.push({ key: i, label: DAYS[i], isActive, inStreak: hasPractice, type })
106
+ dailyStats.push({ key: i, label: DAYS[i], isActive, inStreak: hasPractice, type, day: day.format('YYYY-MM-DD') })
107
107
  }
108
108
 
109
109
  let { streakMessage } = getStreaksAndMessage(practices)
@@ -142,83 +142,77 @@ export async function getUserWeeklyStats() {
142
142
  * getUserMonthlyStats({ userId: 123 }).then(console.log);
143
143
  */
144
144
  export async function getUserMonthlyStats(params = {}) {
145
- const now = new Date()
145
+ const now = dayjs()
146
146
  const {
147
- year = now.getFullYear(),
148
- month = now.getMonth(),
149
- day = 1,
147
+ year = now.year(),
148
+ month = now.month(), // 0-indexed
150
149
  userId = globalConfig.sessionConfig.userId,
151
150
  } = params
152
- let practices = await getUserPractices(userId)
151
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
152
+ const practices = await getUserPractices(userId)
153
153
 
154
- // Get the first day of the specified month and the number of days in that month
155
- let firstDayOfMonth = new Date(year, month, 1)
156
- let today = new Date()
157
- today.setHours(0, 0, 0, 0)
154
+ const firstDayOfMonth = dayjs.tz(`${year}-${month + 1}-01`, timeZone).startOf('day')
155
+ const endOfMonth = firstDayOfMonth.endOf('month')
156
+ const today = dayjs().tz(timeZone).startOf('day')
158
157
 
159
- let startOfGrid = getMonday(firstDayOfMonth)
158
+ let startOfGrid = getMonday(firstDayOfMonth, timeZone)
160
159
 
161
- let previousWeekStart = new Date(startOfGrid)
162
- previousWeekStart.setDate(previousWeekStart.getDate() - 7)
163
-
164
- let previousWeekEnd = new Date(startOfGrid)
165
- previousWeekEnd.setDate(previousWeekEnd.getDate() - 1)
160
+ // Previous week range
161
+ const previousWeekStart = startOfGrid.subtract(7, 'day')
162
+ const previousWeekEnd = startOfGrid.subtract(1, 'day')
166
163
 
167
164
  let hadStreakBeforeMonth = false
168
- for (let d = new Date(previousWeekStart); d <= previousWeekEnd; d.setDate(d.getDate() + 1)) {
169
- let dayKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
170
- if (practices[dayKey]) {
165
+ for (let d = previousWeekStart.clone(); d.isSameOrBefore(previousWeekEnd); d = d.add(1, 'day')) {
166
+ const key = d.format('YYYY-MM-DD')
167
+ if (practices[key]) {
171
168
  hadStreakBeforeMonth = true
172
169
  break
173
170
  }
174
171
  }
175
172
 
176
- let endOfMonth = new Date(year, month + 1, 0)
177
- let endOfGrid = new Date(year, month + 1, 0)
178
- while (endOfGrid.getDay() !== 0) {
179
- endOfGrid.setDate(endOfGrid.getDate() + 1)
173
+ // let endOfMonth = new Date(year, month + 1, 0)
174
+ let endOfGrid = endOfMonth.clone()
175
+ while (endOfGrid.day() !== 0) {
176
+ endOfGrid = endOfGrid.add(1, 'day')
180
177
  }
181
- let daysInMonth = Math.ceil((endOfGrid - startOfGrid) / (1000 * 60 * 60 * 24)) + 1
182
-
178
+ const daysInMonth = endOfGrid.diff(startOfGrid, 'day') + 1
183
179
  let dailyStats = []
184
180
  let practiceDuration = 0
185
181
  let daysPracticed = 0
186
182
  let weeklyStats = {}
187
183
 
188
184
  for (let i = 0; i < daysInMonth; i++) {
189
- let day = new Date(startOfGrid)
190
- day.setDate(startOfGrid.getDate() + i)
191
- let dayKey = `${day.getFullYear()}-${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')}`
192
-
193
- // Check if the user has activity for the day
194
- let dayActivity = practices[dayKey] ?? null
185
+ let day = startOfGrid.add(i, 'day')
186
+ let key = day.format('YYYY-MM-DD')
187
+ let activity = practices[key] ?? null
195
188
  let weekKey = getWeekNumber(day)
196
189
 
197
190
  if (!weeklyStats[weekKey]) {
198
191
  weeklyStats[weekKey] = { key: weekKey, inStreak: false }
199
192
  }
200
193
 
201
- if (dayActivity !== null && firstDayOfMonth <= day && day <= endOfMonth) {
202
- practiceDuration += dayActivity.reduce((sum, entry) => sum + entry.duration_seconds, 0)
194
+ if (activity && day.isBetween(firstDayOfMonth, endOfMonth, null, '[]')) {
195
+ practiceDuration += activity.reduce((sum, entry) => sum + entry.duration_seconds, 0)
203
196
  daysPracticed++
204
197
  }
205
198
 
206
- let isActive = isSameDate(today, day)
207
- let type = dayActivity !== null ? 'tracked' : isActive ? 'active' : 'none'
208
- let isInStreak = dayActivity !== null
209
- if (isInStreak) {
199
+ if (activity) {
210
200
  weeklyStats[weekKey].inStreak = true
211
201
  }
212
202
 
203
+ const isActive = day.isSame(today, 'day')
204
+ const type = activity ? 'tracked' : isActive ? 'active' : 'none'
205
+
213
206
  dailyStats.push({
214
207
  key: i,
215
- label: dayKey,
208
+ label: key,
216
209
  isActive,
217
- inStreak: dayActivity !== null,
210
+ inStreak: !!activity,
218
211
  type,
219
212
  })
220
213
  }
221
214
 
215
+ // Continue streak into month
222
216
  if (hadStreakBeforeMonth) {
223
217
  const firstWeekKey = getWeekNumber(startOfGrid)
224
218
  if (weeklyStats[firstWeekKey]) {
@@ -226,14 +220,15 @@ export async function getUserMonthlyStats(params = {}) {
226
220
  }
227
221
  }
228
222
 
229
- let filteredPractices = Object.keys(practices)
230
- .filter((date) => new Date(date) <= endOfMonth)
231
- .reduce((obj, key) => {
232
- obj[key] = practices[key]
233
- return obj
223
+ // Filter past practices only
224
+ let filteredPractices = Object.entries(practices)
225
+ .filter(([date]) => dayjs.tz(date, timeZone).isSameOrBefore(endOfMonth))
226
+ .reduce((acc, [date, val]) => {
227
+ acc[date] = val
228
+ return acc
234
229
  }, {})
235
230
 
236
- let { currentDailyStreak, currentWeeklyStreak } = calculateStreaks(filteredPractices)
231
+ const { currentDailyStreak, currentWeeklyStreak } = calculateStreaks(filteredPractices)
237
232
 
238
233
  return {
239
234
  data: {
@@ -392,12 +387,7 @@ export async function restoreUserPractice(id) {
392
387
  if (restoredPractice) {
393
388
  await userActivityContext.updateLocal(async function (localContext) {
394
389
  const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
395
- const utcDate = new Date(restoredPractice.day)
396
- const localDate = convertToTimeZone(utcDate, userTimeZone)
397
- const date =
398
- localDate.getFullYear() + '-' +
399
- String(localDate.getMonth() + 1).padStart(2, '0') + '-' +
400
- String(localDate.getDate()).padStart(2, '0')
390
+ const date = convertToTimeZone(restoredPractice.day, userTimeZone)
401
391
  if (localContext.data[DATA_KEY_PRACTICES][date]) {
402
392
  localContext.data[DATA_KEY_PRACTICES][date] = []
403
393
  }
@@ -621,7 +611,7 @@ function getStreaksAndMessage(practices) {
621
611
  }
622
612
  }
623
613
 
624
- async function getUserPracticeIds(day = new Date().toISOString().split('T')[0], userId = null) {
614
+ async function getUserPracticeIds(day = dayjs().format('YYYY-MM-DD'), userId = null) {
625
615
  let practices = {}
626
616
  if (userId !== globalConfig.sessionConfig.userId) {
627
617
  let data = await fetchUserPractices(0, { userId: userId })
@@ -651,6 +641,7 @@ function calculateStreaks(practices, includeStreakMessage = false) {
651
641
  let lastActiveDay = null
652
642
  let streakMessage = ''
653
643
 
644
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
654
645
  let sortedPracticeDays = Object.keys(practices)
655
646
  .map((dateStr) => {
656
647
  const [year, month, day] = dateStr.split('-').map(Number)
@@ -702,9 +693,8 @@ function calculateStreaks(practices, includeStreakMessage = false) {
702
693
  let yesterday = new Date(today)
703
694
  yesterday.setDate(today.getDate() - 1)
704
695
 
705
- let currentWeekStart = getMonday(today)
706
- let lastWeekStart = new Date(currentWeekStart)
707
- lastWeekStart.setDate(currentWeekStart.getDate() - 7)
696
+ let currentWeekStart = getMonday(today, timeZone)
697
+ let lastWeekStart = currentWeekStart.subtract(7, 'days')
708
698
 
709
699
  let hasYesterdayPractice = sortedPracticeDays.some((date) => isSameDate(date, yesterday))
710
700
  let hasCurrentWeekPractice = sortedPracticeDays.some((date) => date >= currentWeekStart)
@@ -837,7 +827,6 @@ async function formatPracticeMeta(practices) {
837
827
  const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
838
828
 
839
829
  return practices.map((practice) => {
840
- const utcDate = new Date(practice.created_at)
841
830
  const content = contents.find((c) => c.id === practice.content_id) || {}
842
831
 
843
832
  return {
@@ -854,7 +843,7 @@ async function formatPracticeMeta(practices) {
854
843
  content_type: getFormattedType(content.type || '', content.brand),
855
844
  content_id: practice.content_id || null,
856
845
  content_brand: content.brand || null,
857
- created_at: convertToTimeZone(utcDate, userTimeZone),
846
+ created_at: convertToTimeZone(dayjs(practice.created_at), userTimeZone),
858
847
  sanity_type: content.type || null,
859
848
  content_slug: content.slug || null,
860
849
  }
@@ -973,6 +962,7 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
973
962
  childToParentMap[content.id] = content.parent_content_data[content.parent_content_data.length - 1];
974
963
  }
975
964
  });
965
+
976
966
  const progressMap = new Map();
977
967
  for (const [idStr, progress] of Object.entries(progressContents)) {
978
968
  const id = parseInt(idStr);
@@ -1474,4 +1464,21 @@ function getFirstLeafLessonId(data) {
1474
1464
  return data.lessons ? findFirstLeaf(data.lessons) : null
1475
1465
  }
1476
1466
 
1467
+ export async function fetchRecentActivitiesActiveTabs() {
1468
+ const url = `/api/user-management-system/v1/activities/tabs`
1469
+ try {
1470
+ const tabs = await fetchHandler(url, 'GET');
1471
+ const activitiesTabs = [];
1472
+
1473
+ tabs.forEach(tab => {
1474
+ activitiesTabs.push({ name: tab.label, short_name:tab.label });
1475
+ });
1476
+
1477
+ return activitiesTabs;
1478
+ } catch (error) {
1479
+ console.error('Error fetching activity tabs:', error);
1480
+ return [];
1481
+ }
1482
+ }
1483
+
1477
1484