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 +20 -0
- package/package.json +2 -1
- package/src/contentMetaData.js +1 -0
- package/src/contentTypeConfig.js +6 -5
- package/src/index.d.ts +7 -1
- package/src/index.js +7 -1
- package/src/services/contentProgress.js +6 -2
- package/src/services/dateUtils.js +31 -42
- package/src/services/imageSRCVerify.js +0 -0
- package/src/services/railcontent.js +1 -9
- package/src/services/sanity.js +7 -1
- package/src/services/user/profile.js +13 -0
- package/src/services/userActivity.js +75 -68
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.
|
|
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
|
}
|
package/src/contentMetaData.js
CHANGED
package/src/contentTypeConfig.js
CHANGED
|
@@ -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: [...
|
|
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 &&
|
|
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,
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}, {});
|
|
17
|
+
export function toDayjs(date, timeZone = 'UTC') {
|
|
18
|
+
return dayjs.tz(date, timeZone).startOf('day')
|
|
19
|
+
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
export function convertToTimeZone(date, timeZone) {
|
|
22
|
+
return dayjs(date).tz(timeZone).format('YYYY-MM-DD');
|
|
21
23
|
}
|
|
22
|
-
|
|
23
|
-
export function getMonday(date) {
|
|
24
|
-
|
|
25
|
-
|
|
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(
|
|
32
|
-
|
|
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
|
|
40
|
-
return date1
|
|
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(
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
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
|
|
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
|
}
|
package/src/services/sanity.js
CHANGED
|
@@ -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
|
-
|
|
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) =>
|
|
93
|
-
.sort((a, b) => b - a)
|
|
94
|
-
|
|
95
|
-
let
|
|
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
|
-
|
|
102
|
-
|
|
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 =
|
|
145
|
+
const now = dayjs()
|
|
146
146
|
const {
|
|
147
|
-
year = now.
|
|
148
|
-
month = now.
|
|
149
|
-
day = 1,
|
|
147
|
+
year = now.year(),
|
|
148
|
+
month = now.month(), // 0-indexed
|
|
150
149
|
userId = globalConfig.sessionConfig.userId,
|
|
151
150
|
} = params
|
|
152
|
-
|
|
151
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
152
|
+
const practices = await getUserPractices(userId)
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
162
|
-
previousWeekStart.
|
|
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 =
|
|
169
|
-
|
|
170
|
-
if (practices[
|
|
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
|
-
|
|
177
|
-
let endOfGrid =
|
|
178
|
-
while (endOfGrid.
|
|
179
|
-
endOfGrid
|
|
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
|
-
|
|
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 =
|
|
190
|
-
day.
|
|
191
|
-
let
|
|
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 (
|
|
202
|
-
practiceDuration +=
|
|
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
|
-
|
|
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:
|
|
208
|
+
label: key,
|
|
216
209
|
isActive,
|
|
217
|
-
inStreak:
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
.
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|