musora-content-services 2.7.2 → 2.8.0
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 +9 -0
- package/docs/ContentOrganization.html +2 -2
- package/docs/Gamification.html +2 -2
- package/docs/UserManagementSystem.html +2 -2
- package/docs/api_types.js.html +2 -2
- package/docs/config.js.html +2 -2
- package/docs/content-org_content-org.js.html +2 -2
- package/docs/content-org_playlists-types.js.html +2 -2
- package/docs/content-org_playlists.js.html +2 -4
- package/docs/content.js.html +2 -2
- package/docs/gamification_awards.js.html +2 -2
- package/docs/gamification_gamification.js.html +2 -2
- package/docs/gamification_types.js.html +2 -2
- package/docs/global.html +2 -2
- package/docs/index.html +2 -2
- package/docs/module-Awards.html +2 -2
- package/docs/module-Config.html +2 -2
- package/docs/module-Content-Services-V2.html +2 -2
- package/docs/module-Interests.html +2 -2
- package/docs/module-Permissions.html +2 -2
- package/docs/module-Playlists.html +13 -13
- package/docs/module-Railcontent-Services.html +2 -2
- package/docs/module-Sanity-Services.html +2 -2
- package/docs/module-Sessions.html +2 -2
- package/docs/module-User-Activity.html +7 -7
- package/docs/module-UserManagement.html +448 -23
- package/docs/module-UserProfile.html +2 -2
- package/docs/railcontent.js.html +2 -2
- package/docs/sanity.js.html +2 -2
- package/docs/userActivity.js.html +2 -12
- package/docs/user_interests.js.html +2 -2
- package/docs/user_management.js.html +85 -12
- package/docs/user_permissions.js.html +2 -2
- package/docs/user_profile.js.html +2 -2
- package/docs/user_sessions.js.html +2 -2
- package/docs/user_types.js.html +2 -2
- package/docs/user_user-management-system.js.html +2 -2
- package/link_mcs.sh +8 -6
- package/package.json +1 -1
- package/src/contentMetaData.js +2 -2
- package/src/contentTypeConfig.js +12 -7
- package/src/index.d.ts +9 -1
- package/src/index.js +9 -1
- package/src/lib/httpHelper.js +60 -20
- package/src/services/content.js +0 -0
- package/src/services/contentProgress.js +0 -0
- package/src/services/dateUtils.js +23 -0
- package/src/services/user/management.js +83 -10
- package/src/services/userActivity.js +183 -37
- package/test/mockData/mockData_progress_content.json +0 -0
- package/test/mockData/mockData_sanity_progress_content.json +0 -0
- package/test/progressRows.test.js +0 -0
- package/test/userActivity.test.js +4 -4
|
@@ -8,14 +8,14 @@ import {
|
|
|
8
8
|
fetchUserPracticeMeta,
|
|
9
9
|
fetchUserPracticeNotes,
|
|
10
10
|
fetchHandler,
|
|
11
|
-
fetchRecentUserActivities,
|
|
11
|
+
fetchRecentUserActivities, fetchChallengeLessonData
|
|
12
12
|
} from './railcontent'
|
|
13
13
|
import { DataContext, UserActivityVersionKey } from './dataContext.js'
|
|
14
14
|
import { fetchByRailContentIds, fetchShows } from './sanity'
|
|
15
|
-
import {fetchUserPlaylists} from "./content-org/playlists";
|
|
16
|
-
import {
|
|
15
|
+
import {fetchPlaylist, fetchUserPlaylists} from "./content-org/playlists";
|
|
16
|
+
import {convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay, getTimeRemainingUntilLocal} from './dateUtils.js'
|
|
17
17
|
import { globalConfig } from './config'
|
|
18
|
-
import {collectionLessonTypes, lessonTypesMapping, progressTypesMapping} from "../contentTypeConfig";
|
|
18
|
+
import {collectionLessonTypes, lessonTypesMapping, progressTypesMapping, showsLessonTypes, songs} from "../contentTypeConfig";
|
|
19
19
|
import {getAllStartedOrCompleted, getProgressStateByIds} from "./contentProgress";
|
|
20
20
|
import {TabResponseType} from "../contentMetaData";
|
|
21
21
|
|
|
@@ -830,7 +830,7 @@ async function formatPracticeMeta(practices) {
|
|
|
830
830
|
title: practice.content_id ? content.title : practice.title,
|
|
831
831
|
category_id: practice.category_id,
|
|
832
832
|
instrument_id: practice.instrument_id,
|
|
833
|
-
content_type: getFormattedType(content.type || ''),
|
|
833
|
+
content_type: getFormattedType(content.type || '', content.brand),
|
|
834
834
|
content_id: practice.content_id || null,
|
|
835
835
|
content_brand: content.brand || null,
|
|
836
836
|
created_at: convertToTimeZone(utcDate, userTimeZone),
|
|
@@ -911,6 +911,7 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
|
|
|
911
911
|
const playlistEngagedOnContents = eligiblePlaylistItems.map(item => item.last_engaged_on);
|
|
912
912
|
const playlistsContents = await fetchByRailContentIds(playlistEngagedOnContents, 'progress-tracker');
|
|
913
913
|
const excludedParents = new Set();
|
|
914
|
+
const existingShows = new Set();
|
|
914
915
|
for (const item of playlistsContents) {
|
|
915
916
|
const contentId = item.id ?? item.railcontent_id;
|
|
916
917
|
excludedParents.add(contentId)
|
|
@@ -960,20 +961,44 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
|
|
|
960
961
|
}
|
|
961
962
|
// Handle standalone parents
|
|
962
963
|
if (!progressMap.has(id)) {
|
|
963
|
-
|
|
964
|
-
id,
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
964
|
+
if(!existingShows.has(content.type)){
|
|
965
|
+
progressMap.set(id, {
|
|
966
|
+
id,
|
|
967
|
+
raw: content,
|
|
968
|
+
state: progress.status,
|
|
969
|
+
percent: progress.progress,
|
|
970
|
+
progressTimestamp: progress.last_update * 1000
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
if(showsLessonTypes.includes(content.type)) {
|
|
974
|
+
existingShows.add(content.type)
|
|
975
|
+
}
|
|
970
976
|
}
|
|
971
977
|
}
|
|
972
|
-
const
|
|
978
|
+
const pinnedItem = await extractPinnedItem({
|
|
979
|
+
brand,
|
|
980
|
+
progressMap,
|
|
981
|
+
playlistItems: eligiblePlaylistItems,
|
|
982
|
+
})
|
|
983
|
+
const progressList = Array.from(progressMap.values())
|
|
984
|
+
if (pinnedItem) {
|
|
985
|
+
pinnedItem.pinned = true
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const pinnedId = pinnedItem?.id
|
|
989
|
+
const filteredProgressList = pinnedId
|
|
990
|
+
? progressList.filter(item => item.id !== pinnedId)
|
|
991
|
+
: progressList;
|
|
992
|
+
const filteredPlaylists = pinnedId
|
|
993
|
+
? eligiblePlaylistItems.filter(item => item.id !== pinnedId)
|
|
994
|
+
: eligiblePlaylistItems;
|
|
995
|
+
const combinedBase = [...filteredProgressList, ...filteredPlaylists]
|
|
996
|
+
const combined = pinnedItem ? [pinnedItem, ...combinedBase] : combinedBase
|
|
997
|
+
|
|
998
|
+
const finalCombined = mergeAndSortItems(combined, limit)
|
|
973
999
|
|
|
974
|
-
const combined = mergeAndSortItems([...progressList, ...eligiblePlaylistItems], limit);
|
|
975
1000
|
const results = await Promise.all(
|
|
976
|
-
|
|
1001
|
+
finalCombined.slice(0, limit).map(item =>
|
|
977
1002
|
item.type === 'playlist'
|
|
978
1003
|
? processPlaylistItem(item)
|
|
979
1004
|
: processContentItem(item)
|
|
@@ -988,22 +1013,25 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
|
|
|
988
1013
|
|
|
989
1014
|
async function processContentItem(item) {
|
|
990
1015
|
let data = item.raw;
|
|
991
|
-
const contentType = getFormattedType(data.type);
|
|
1016
|
+
const contentType = getFormattedType(data.type, data.brand);
|
|
992
1017
|
const status = item.state;
|
|
993
1018
|
|
|
994
1019
|
let ctaText = 'Continue';
|
|
995
1020
|
if (contentType === 'transcription' || contentType === 'play-along' || contentType === 'jam-track') ctaText = 'Replay Song';
|
|
996
1021
|
if (contentType === 'lesson') ctaText = status === 'completed' ? 'Revisit Lesson' : 'Continue';
|
|
997
|
-
if ((contentType === '
|
|
1022
|
+
if ((contentType === 'guided course' || contentType === 'song tutorial' || collectionLessonTypes.includes(contentType)) && status === 'completed') ctaText = 'Revisit Lessons' ;
|
|
1023
|
+
if (contentType === 'pack' && status === 'completed') {
|
|
998
1024
|
ctaText = 'View Lessons';
|
|
999
1025
|
}
|
|
1000
1026
|
|
|
1001
1027
|
if (data.lesson_count > 0) {
|
|
1028
|
+
const lessonIds = extractLessonIds(item);
|
|
1029
|
+
const progressOnItems = await getProgressStateByIds(lessonIds);
|
|
1030
|
+
let completedCount = Object.values(progressOnItems).filter(value => value === 'completed').length;
|
|
1031
|
+
data.completed_children = completedCount;
|
|
1032
|
+
|
|
1002
1033
|
if (item.childIndex) {
|
|
1003
1034
|
let nextId = item.childIndex
|
|
1004
|
-
const lessonIds = extractLessonIds(item);
|
|
1005
|
-
const progressOnItems = await getProgressStateByIds(lessonIds);
|
|
1006
|
-
const completedCount = Object.values(progressOnItems).filter(value => value === 'completed').length;
|
|
1007
1035
|
const nextByProgress = findIncompleteLesson(progressOnItems, item.childIndex, item.raw.type);
|
|
1008
1036
|
nextId = nextByProgress ? nextByProgress : nextId;
|
|
1009
1037
|
|
|
@@ -1025,28 +1053,46 @@ async function processContentItem(item) {
|
|
|
1025
1053
|
const nextLesson = lessons.find(lesson => lesson.id === nextId);
|
|
1026
1054
|
data.first_incomplete_child = nextLesson?.parent ?? nextLesson;
|
|
1027
1055
|
data.second_incomplete_child = (nextLesson?.parent) ? nextLesson : null;
|
|
1028
|
-
data.
|
|
1056
|
+
if(data.type === 'challenge' && nextByProgress !== undefined ){
|
|
1057
|
+
const challenge = await fetchChallengeLessonData(nextByProgress)
|
|
1058
|
+
if(challenge.lesson.is_locked) {
|
|
1059
|
+
data.is_locked = true;
|
|
1060
|
+
ctaText = 'Next lesson in ' + getTimeRemainingUntilLocal(challenge.lesson.unlock_date);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1029
1063
|
}
|
|
1030
1064
|
}
|
|
1031
1065
|
|
|
1032
|
-
if(contentType == 'show'
|
|
1066
|
+
if(contentType == 'show'){
|
|
1033
1067
|
const shows = await fetchShows(data.brand, data.type)
|
|
1034
1068
|
const showIds = shows.map(item => item.id);
|
|
1035
1069
|
const progressOnItems = await getProgressStateByIds(showIds);
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1070
|
+
const completedCount = Object.values(progressOnItems).filter(value => value === 'completed').length;
|
|
1071
|
+
if(status == 'completed') {
|
|
1072
|
+
const nextByProgress = findIncompleteLesson(progressOnItems, data.id, data.type);
|
|
1073
|
+
data = shows.find(lesson => lesson.id === nextByProgress);
|
|
1074
|
+
}
|
|
1075
|
+
data.completed_children = completedCount;
|
|
1076
|
+
data.child_count = shows.length;
|
|
1077
|
+
item.percent = Math.round((completedCount / shows.length) * 100);
|
|
1078
|
+
if(completedCount == shows.length) {
|
|
1079
|
+
ctaText = 'Revisit Lessons';
|
|
1080
|
+
}
|
|
1038
1081
|
}
|
|
1039
1082
|
|
|
1040
1083
|
return {
|
|
1041
1084
|
id: item.id,
|
|
1042
1085
|
progressType: 'content',
|
|
1043
1086
|
header: contentType,
|
|
1087
|
+
pinned: item.pinned ?? false,
|
|
1044
1088
|
body: {
|
|
1045
1089
|
progressPercent: item.percent,
|
|
1046
1090
|
thumbnail: data.thumbnail,
|
|
1047
1091
|
title: data.title,
|
|
1092
|
+
badge: data.badge ?? null,
|
|
1093
|
+
isLocked: data.is_locked ?? false,
|
|
1048
1094
|
subtitle: !data.child_count || data.lesson_count === 1
|
|
1049
|
-
? `${data.difficulty_string} • ${data.artist_name}`
|
|
1095
|
+
? (contentType === 'lesson') ? `${item.percent}% Complete`: `${data.difficulty_string} • ${data.artist_name}`
|
|
1050
1096
|
: `${data.completed_children} of ${data.lesson_count ?? data.child_count} Lessons Complete`
|
|
1051
1097
|
},
|
|
1052
1098
|
cta: {
|
|
@@ -1101,10 +1147,12 @@ async function processPlaylistItem(item) {
|
|
|
1101
1147
|
id: playlist.id,
|
|
1102
1148
|
progressType: 'playlist',
|
|
1103
1149
|
header: 'playlist',
|
|
1150
|
+
pinned: item.pinned ?? false,
|
|
1104
1151
|
body: {
|
|
1105
1152
|
first_items_thumbnail_url: playlist.first_items_thumbnail_url,
|
|
1106
1153
|
title: playlist.name,
|
|
1107
|
-
subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}
|
|
1154
|
+
subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`,
|
|
1155
|
+
total_items: playlist.total_items,
|
|
1108
1156
|
},
|
|
1109
1157
|
progressTimestamp: item.progressTimestamp,
|
|
1110
1158
|
cta: {
|
|
@@ -1119,12 +1167,13 @@ async function processPlaylistItem(item) {
|
|
|
1119
1167
|
}
|
|
1120
1168
|
}
|
|
1121
1169
|
|
|
1122
|
-
const getFormattedType = type => {
|
|
1170
|
+
const getFormattedType = (type, brand) => {
|
|
1123
1171
|
for (const [key, values] of Object.entries(progressTypesMapping)) {
|
|
1124
1172
|
if (values.includes(type)) {
|
|
1125
|
-
return key;
|
|
1173
|
+
return key === 'songs' ? songs[brand] : key;
|
|
1126
1174
|
}
|
|
1127
1175
|
}
|
|
1176
|
+
|
|
1128
1177
|
return null;
|
|
1129
1178
|
};
|
|
1130
1179
|
|
|
@@ -1132,11 +1181,10 @@ function extractLessonIds(data) {
|
|
|
1132
1181
|
const ids = [];
|
|
1133
1182
|
function traverse(lessons) {
|
|
1134
1183
|
for (const item of lessons) {
|
|
1135
|
-
if (item.id) {
|
|
1136
|
-
ids.push(item.id);
|
|
1137
|
-
}
|
|
1138
1184
|
if (item.lessons) {
|
|
1139
1185
|
traverse(item.lessons); // Recursively handle nested lessons
|
|
1186
|
+
}else if (item.id) {
|
|
1187
|
+
ids.push(item.id);
|
|
1140
1188
|
}
|
|
1141
1189
|
}
|
|
1142
1190
|
}
|
|
@@ -1165,9 +1213,24 @@ async function getEligiblePlaylistItems(playlists) {
|
|
|
1165
1213
|
}
|
|
1166
1214
|
|
|
1167
1215
|
function mergeAndSortItems(items, limit) {
|
|
1168
|
-
|
|
1216
|
+
const seen = new Set();
|
|
1217
|
+
const deduped = [];
|
|
1218
|
+
|
|
1219
|
+
for (const item of items) {
|
|
1220
|
+
const key = `${item.id}-${item.type || item.raw?.type}`;
|
|
1221
|
+
if (!seen.has(key)) {
|
|
1222
|
+
seen.add(key);
|
|
1223
|
+
deduped.push(item);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
return deduped
|
|
1169
1228
|
.filter(item => typeof item.progressTimestamp === 'number' && item.progressTimestamp > 0)
|
|
1170
|
-
.sort((a, b) =>
|
|
1229
|
+
.sort((a, b) => {
|
|
1230
|
+
if (a.pinned && !b.pinned) return -1;
|
|
1231
|
+
if (!a.pinned && b.pinned) return 1;
|
|
1232
|
+
return b.progressTimestamp - a.progressTimestamp;
|
|
1233
|
+
})
|
|
1171
1234
|
.slice(0, limit + 5);
|
|
1172
1235
|
}
|
|
1173
1236
|
|
|
@@ -1175,7 +1238,7 @@ function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
|
|
|
1175
1238
|
const ids = Object.keys(progressOnItems).map(Number);
|
|
1176
1239
|
if (contentType === 'challenge') {
|
|
1177
1240
|
// Return first incomplete lesson
|
|
1178
|
-
return ids.find(id => progressOnItems[id] !== 'completed') ||
|
|
1241
|
+
return ids.find(id => progressOnItems[id] !== 'completed') || ids.at(0);
|
|
1179
1242
|
}
|
|
1180
1243
|
|
|
1181
1244
|
// For other types, find next incomplete after current
|
|
@@ -1207,7 +1270,15 @@ function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
|
|
|
1207
1270
|
*/
|
|
1208
1271
|
export async function pinProgressRow(brand, id, progressType) {
|
|
1209
1272
|
const url = `/api/user-management-system/v1/progress/pin?brand=${brand}&id=${id}&progressType=${progressType}`;
|
|
1210
|
-
|
|
1273
|
+
const response = await fetchHandler(url, 'PUT', null)
|
|
1274
|
+
if (response && !response.error) {
|
|
1275
|
+
updatePinnedProgressRow(brand, {
|
|
1276
|
+
id,
|
|
1277
|
+
progressType,
|
|
1278
|
+
pinnedAt: new Date().toISOString(),
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
return response;
|
|
1211
1282
|
}
|
|
1212
1283
|
/**
|
|
1213
1284
|
* Unpins the current pinned progress row for a user, scoped by brand.
|
|
@@ -1221,7 +1292,82 @@ export async function pinProgressRow(brand, id, progressType) {
|
|
|
1221
1292
|
* .catch(error => console.error(error));
|
|
1222
1293
|
*/
|
|
1223
1294
|
export async function unpinProgressRow(brand) {
|
|
1224
|
-
const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}
|
|
1225
|
-
|
|
1295
|
+
const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}`
|
|
1296
|
+
const response = await fetchHandler(url, 'PUT', null)
|
|
1297
|
+
if (response && !response.error) {
|
|
1298
|
+
updatePinnedProgressRow(brand, null)
|
|
1299
|
+
}
|
|
1300
|
+
return response
|
|
1226
1301
|
}
|
|
1227
1302
|
|
|
1303
|
+
function updatePinnedProgressRow(brand, pinnedData) {
|
|
1304
|
+
const user = JSON.parse(globalConfig.localStorage.getItem('user')) || {}
|
|
1305
|
+
user.brand_pinned_progress = user.brand_pinned_progress || {}
|
|
1306
|
+
user.brand_pinned_progress[brand] = pinnedData
|
|
1307
|
+
globalConfig.localStorage.setItem('user', JSON.stringify(user))
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
async function extractPinnedItem({brand, progressMap, playlistItems}) {
|
|
1311
|
+
const user = JSON.parse(globalConfig.localStorage.getItem('user')) || {}
|
|
1312
|
+
user.brand_pinned_progress = user.brand_pinned_progress || {}
|
|
1313
|
+
|
|
1314
|
+
const pinned = user.brand_pinned_progress[brand]
|
|
1315
|
+
if (!pinned) return null
|
|
1316
|
+
|
|
1317
|
+
const {id, progressType, pinnedAt} = pinned
|
|
1318
|
+
|
|
1319
|
+
if (progressType === 'content') {
|
|
1320
|
+
const pinnedId = parseInt(id)
|
|
1321
|
+
if (progressMap.has(pinnedId)) {
|
|
1322
|
+
const item = progressMap.get(pinnedId)
|
|
1323
|
+
progressMap.delete(pinnedId)
|
|
1324
|
+
return item
|
|
1325
|
+
} else {
|
|
1326
|
+
const content = await fetchByRailContentIds([`${pinnedId}`], 'progress-tracker')
|
|
1327
|
+
const firstLessonId = getFirstLeafLessonId(content[0])
|
|
1328
|
+
return {
|
|
1329
|
+
id: pinnedId,
|
|
1330
|
+
state: 'started',
|
|
1331
|
+
percent: 0,
|
|
1332
|
+
raw: content[0],
|
|
1333
|
+
progressTimestamp: new Date(pinnedAt).getTime(),
|
|
1334
|
+
childIndex: firstLessonId
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (progressType === 'playlist') {
|
|
1339
|
+
const pinnedPlaylist = playlistItems.find(p => p.raw.id === id)
|
|
1340
|
+
if (pinnedPlaylist) {
|
|
1341
|
+
return pinnedPlaylist
|
|
1342
|
+
}else{
|
|
1343
|
+
const playlist = await fetchPlaylist(id)
|
|
1344
|
+
return {
|
|
1345
|
+
id: id,
|
|
1346
|
+
raw: playlist,
|
|
1347
|
+
progressTimestamp: new Date(pinnedAt).getTime(),
|
|
1348
|
+
type: 'playlist',
|
|
1349
|
+
last_engaged_on: playlist.items[0],
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
return null
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function getFirstLeafLessonId(data) {
|
|
1358
|
+
function findFirstLeaf(lessons) {
|
|
1359
|
+
for (const item of lessons) {
|
|
1360
|
+
if (!item.lessons || item.lessons.length === 0) {
|
|
1361
|
+
return item.id || null
|
|
1362
|
+
}
|
|
1363
|
+
const found = findFirstLeaf(item.lessons)
|
|
1364
|
+
if (found) return found
|
|
1365
|
+
}
|
|
1366
|
+
return null
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
return data.lessons ? findFirstLeaf(data.lessons) : null
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
|
|
1373
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -69,15 +69,15 @@ describe('User Activity API Tests', function () {
|
|
|
69
69
|
|
|
70
70
|
test('fetches user practices from past', async () => {
|
|
71
71
|
userActivityContext.clearCache()
|
|
72
|
-
const practices = await getUserMonthlyStats( 2025, 1)
|
|
73
|
-
consoleLog(practices)
|
|
72
|
+
const practices = await getUserMonthlyStats( {year:2025, month: 1} )
|
|
73
|
+
consoleLog(practices.data.dailyActiveStats)
|
|
74
74
|
|
|
75
75
|
// Assert that dailyActiveStats contains correct data
|
|
76
76
|
const dailyStats = practices.data.dailyActiveStats
|
|
77
77
|
const feb10 = dailyStats.find(stat => stat.label === '2025-02-10')
|
|
78
78
|
expect(feb10.inStreak).toBe(true)
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
expect(feb10.type).toBe('tracked')
|
|
80
|
+
expect(feb10.isActive).toBe(false)
|
|
81
81
|
})
|
|
82
82
|
|
|
83
83
|
test('fetches user practices for current week', async () => {
|