musora-content-services 2.7.3 → 2.8.1
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 +0 -0
- package/package.json +1 -1
- package/src/contentMetaData.js +2 -2
- package/src/contentTypeConfig.js +10 -5
- package/src/index.d.ts +9 -1
- package/src/index.js +9 -1
- package/src/lib/httpHelper.js +60 -20
- package/src/services/dateUtils.js +29 -0
- package/src/services/user/management.js +83 -10
- package/src/services/userActivity.js +194 -43
- package/test/userActivity.test.js +0 -0
|
@@ -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,24 +1013,27 @@ 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
|
|
1005
|
-
|
|
1006
|
-
const completedCount = Object.values(progressOnItems).filter(value => value === 'completed').length;
|
|
1007
|
-
const nextByProgress = findIncompleteLesson(progressOnItems, item.childIndex, item.raw.type);
|
|
1008
|
-
nextId = nextByProgress ? nextByProgress : nextId;
|
|
1035
|
+
const nextByProgress = findIncompleteLesson(progressOnItems, item.childIndex, item.raw.type)
|
|
1036
|
+
nextId = nextByProgress ? nextByProgress : nextId
|
|
1009
1037
|
|
|
1010
1038
|
const nestedLessons = data.lessons
|
|
1011
1039
|
.filter(item => Array.isArray(item.lessons))
|
|
@@ -1021,36 +1049,57 @@ async function processContentItem(item) {
|
|
|
1021
1049
|
}))
|
|
1022
1050
|
);
|
|
1023
1051
|
|
|
1024
|
-
const lessons = (nestedLessons.length === 0) ? data.lessons : nestedLessons
|
|
1025
|
-
const nextLesson = lessons.find(lesson => lesson.id === nextId)
|
|
1026
|
-
data.first_incomplete_child = nextLesson?.parent ?? nextLesson
|
|
1027
|
-
data.second_incomplete_child = (nextLesson?.parent) ? nextLesson : null
|
|
1028
|
-
data.
|
|
1052
|
+
const lessons = (nestedLessons.length === 0) ? data.lessons : nestedLessons
|
|
1053
|
+
const nextLesson = lessons.find(lesson => lesson.id === nextId)
|
|
1054
|
+
data.first_incomplete_child = nextLesson?.parent ?? nextLesson
|
|
1055
|
+
data.second_incomplete_child = (nextLesson?.parent) ? nextLesson : null
|
|
1056
|
+
if(data.type === 'challenge' && nextByProgress !== undefined ){
|
|
1057
|
+
const challenge = await fetchChallengeLessonData(nextByProgress)
|
|
1058
|
+
if(challenge.lesson.is_locked) {
|
|
1059
|
+
const timeRemaining = getTimeRemainingUntilLocal(challenge.lesson.unlock_date, {withTotalSeconds:true})
|
|
1060
|
+
data.is_locked = true
|
|
1061
|
+
data.time_remaining_seconds = timeRemaining.totalSeconds
|
|
1062
|
+
ctaText = 'Next lesson in ' + timeRemaining.formatted
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1029
1065
|
}
|
|
1030
1066
|
}
|
|
1031
1067
|
|
|
1032
|
-
if(contentType == 'show'
|
|
1068
|
+
if(contentType == 'show'){
|
|
1033
1069
|
const shows = await fetchShows(data.brand, data.type)
|
|
1034
1070
|
const showIds = shows.map(item => item.id);
|
|
1035
1071
|
const progressOnItems = await getProgressStateByIds(showIds);
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1072
|
+
const completedCount = Object.values(progressOnItems).filter(value => value === 'completed').length;
|
|
1073
|
+
if(status == 'completed') {
|
|
1074
|
+
const nextByProgress = findIncompleteLesson(progressOnItems, data.id, data.type);
|
|
1075
|
+
data = shows.find(lesson => lesson.id === nextByProgress);
|
|
1076
|
+
}
|
|
1077
|
+
data.completed_children = completedCount;
|
|
1078
|
+
data.child_count = shows.length;
|
|
1079
|
+
item.percent = Math.round((completedCount / shows.length) * 100);
|
|
1080
|
+
if(completedCount == shows.length) {
|
|
1081
|
+
ctaText = 'Revisit Lessons';
|
|
1082
|
+
}
|
|
1038
1083
|
}
|
|
1039
1084
|
|
|
1040
1085
|
return {
|
|
1041
1086
|
id: item.id,
|
|
1042
1087
|
progressType: 'content',
|
|
1043
1088
|
header: contentType,
|
|
1089
|
+
pinned: item.pinned ?? false,
|
|
1044
1090
|
body: {
|
|
1045
1091
|
progressPercent: item.percent,
|
|
1046
1092
|
thumbnail: data.thumbnail,
|
|
1047
1093
|
title: data.title,
|
|
1094
|
+
badge: data.badge ?? null,
|
|
1095
|
+
isLocked: data.is_locked ?? false,
|
|
1048
1096
|
subtitle: !data.child_count || data.lesson_count === 1
|
|
1049
|
-
? `${data.difficulty_string} • ${data.artist_name}`
|
|
1097
|
+
? (contentType === 'lesson') ? `${item.percent}% Complete`: `${data.difficulty_string} • ${data.artist_name}`
|
|
1050
1098
|
: `${data.completed_children} of ${data.lesson_count ?? data.child_count} Lessons Complete`
|
|
1051
1099
|
},
|
|
1052
1100
|
cta: {
|
|
1053
1101
|
text: ctaText,
|
|
1102
|
+
timeRemainingToUnlockSeconds: data.time_remaining_seconds ?? null,
|
|
1054
1103
|
action: {
|
|
1055
1104
|
type: data.type,
|
|
1056
1105
|
brand: data.brand,
|
|
@@ -1101,10 +1150,12 @@ async function processPlaylistItem(item) {
|
|
|
1101
1150
|
id: playlist.id,
|
|
1102
1151
|
progressType: 'playlist',
|
|
1103
1152
|
header: 'playlist',
|
|
1153
|
+
pinned: item.pinned ?? false,
|
|
1104
1154
|
body: {
|
|
1105
1155
|
first_items_thumbnail_url: playlist.first_items_thumbnail_url,
|
|
1106
1156
|
title: playlist.name,
|
|
1107
|
-
subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}
|
|
1157
|
+
subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`,
|
|
1158
|
+
total_items: playlist.total_items,
|
|
1108
1159
|
},
|
|
1109
1160
|
progressTimestamp: item.progressTimestamp,
|
|
1110
1161
|
cta: {
|
|
@@ -1119,12 +1170,13 @@ async function processPlaylistItem(item) {
|
|
|
1119
1170
|
}
|
|
1120
1171
|
}
|
|
1121
1172
|
|
|
1122
|
-
const getFormattedType = type => {
|
|
1173
|
+
const getFormattedType = (type, brand) => {
|
|
1123
1174
|
for (const [key, values] of Object.entries(progressTypesMapping)) {
|
|
1124
1175
|
if (values.includes(type)) {
|
|
1125
|
-
return key;
|
|
1176
|
+
return key === 'songs' ? songs[brand] : key;
|
|
1126
1177
|
}
|
|
1127
1178
|
}
|
|
1179
|
+
|
|
1128
1180
|
return null;
|
|
1129
1181
|
};
|
|
1130
1182
|
|
|
@@ -1132,11 +1184,10 @@ function extractLessonIds(data) {
|
|
|
1132
1184
|
const ids = [];
|
|
1133
1185
|
function traverse(lessons) {
|
|
1134
1186
|
for (const item of lessons) {
|
|
1135
|
-
if (item.id) {
|
|
1136
|
-
ids.push(item.id);
|
|
1137
|
-
}
|
|
1138
1187
|
if (item.lessons) {
|
|
1139
1188
|
traverse(item.lessons); // Recursively handle nested lessons
|
|
1189
|
+
}else if (item.id) {
|
|
1190
|
+
ids.push(item.id);
|
|
1140
1191
|
}
|
|
1141
1192
|
}
|
|
1142
1193
|
}
|
|
@@ -1165,9 +1216,24 @@ async function getEligiblePlaylistItems(playlists) {
|
|
|
1165
1216
|
}
|
|
1166
1217
|
|
|
1167
1218
|
function mergeAndSortItems(items, limit) {
|
|
1168
|
-
|
|
1219
|
+
const seen = new Set();
|
|
1220
|
+
const deduped = [];
|
|
1221
|
+
|
|
1222
|
+
for (const item of items) {
|
|
1223
|
+
const key = `${item.id}-${item.type || item.raw?.type}`;
|
|
1224
|
+
if (!seen.has(key)) {
|
|
1225
|
+
seen.add(key);
|
|
1226
|
+
deduped.push(item);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
return deduped
|
|
1169
1231
|
.filter(item => typeof item.progressTimestamp === 'number' && item.progressTimestamp > 0)
|
|
1170
|
-
.sort((a, b) =>
|
|
1232
|
+
.sort((a, b) => {
|
|
1233
|
+
if (a.pinned && !b.pinned) return -1;
|
|
1234
|
+
if (!a.pinned && b.pinned) return 1;
|
|
1235
|
+
return b.progressTimestamp - a.progressTimestamp;
|
|
1236
|
+
})
|
|
1171
1237
|
.slice(0, limit + 5);
|
|
1172
1238
|
}
|
|
1173
1239
|
|
|
@@ -1175,7 +1241,7 @@ function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
|
|
|
1175
1241
|
const ids = Object.keys(progressOnItems).map(Number);
|
|
1176
1242
|
if (contentType === 'challenge') {
|
|
1177
1243
|
// Return first incomplete lesson
|
|
1178
|
-
return ids.find(id => progressOnItems[id] !== 'completed') ||
|
|
1244
|
+
return ids.find(id => progressOnItems[id] !== 'completed') || ids.at(0);
|
|
1179
1245
|
}
|
|
1180
1246
|
|
|
1181
1247
|
// For other types, find next incomplete after current
|
|
@@ -1207,7 +1273,15 @@ function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
|
|
|
1207
1273
|
*/
|
|
1208
1274
|
export async function pinProgressRow(brand, id, progressType) {
|
|
1209
1275
|
const url = `/api/user-management-system/v1/progress/pin?brand=${brand}&id=${id}&progressType=${progressType}`;
|
|
1210
|
-
|
|
1276
|
+
const response = await fetchHandler(url, 'PUT', null)
|
|
1277
|
+
if (response && !response.error) {
|
|
1278
|
+
await updatePinnedProgressRow(brand, {
|
|
1279
|
+
id,
|
|
1280
|
+
progressType,
|
|
1281
|
+
pinnedAt: new Date().toISOString(),
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
return response;
|
|
1211
1285
|
}
|
|
1212
1286
|
/**
|
|
1213
1287
|
* Unpins the current pinned progress row for a user, scoped by brand.
|
|
@@ -1221,7 +1295,84 @@ export async function pinProgressRow(brand, id, progressType) {
|
|
|
1221
1295
|
* .catch(error => console.error(error));
|
|
1222
1296
|
*/
|
|
1223
1297
|
export async function unpinProgressRow(brand) {
|
|
1224
|
-
const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}
|
|
1225
|
-
|
|
1298
|
+
const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}`
|
|
1299
|
+
const response = await fetchHandler(url, 'PUT', null)
|
|
1300
|
+
if (response && !response.error) {
|
|
1301
|
+
await updatePinnedProgressRow(brand, null)
|
|
1302
|
+
}
|
|
1303
|
+
return response
|
|
1226
1304
|
}
|
|
1227
1305
|
|
|
1306
|
+
async function updatePinnedProgressRow(brand, pinnedData) {
|
|
1307
|
+
const userRaw = await globalConfig.localStorage.getItem('user');
|
|
1308
|
+
const user = userRaw ? JSON.parse(userRaw) : {};
|
|
1309
|
+
user.brand_pinned_progress = user.brand_pinned_progress || {}
|
|
1310
|
+
user.brand_pinned_progress[brand] = pinnedData
|
|
1311
|
+
await globalConfig.localStorage.setItem('user', JSON.stringify(user))
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
async function extractPinnedItem({brand, progressMap, playlistItems}) {
|
|
1315
|
+
const userRaw = await globalConfig.localStorage.getItem('user');
|
|
1316
|
+
const user = userRaw ? JSON.parse(userRaw) : {};
|
|
1317
|
+
user.brand_pinned_progress = user.brand_pinned_progress || {}
|
|
1318
|
+
|
|
1319
|
+
const pinned = user.brand_pinned_progress[brand]
|
|
1320
|
+
if (!pinned) return null
|
|
1321
|
+
|
|
1322
|
+
const {id, progressType, pinnedAt} = pinned
|
|
1323
|
+
|
|
1324
|
+
if (progressType === 'content') {
|
|
1325
|
+
const pinnedId = parseInt(id)
|
|
1326
|
+
if (progressMap.has(pinnedId)) {
|
|
1327
|
+
const item = progressMap.get(pinnedId)
|
|
1328
|
+
progressMap.delete(pinnedId)
|
|
1329
|
+
return item
|
|
1330
|
+
} else {
|
|
1331
|
+
const content = await fetchByRailContentIds([`${pinnedId}`], 'progress-tracker')
|
|
1332
|
+
const firstLessonId = getFirstLeafLessonId(content[0])
|
|
1333
|
+
return {
|
|
1334
|
+
id: pinnedId,
|
|
1335
|
+
state: 'started',
|
|
1336
|
+
percent: 0,
|
|
1337
|
+
raw: content[0],
|
|
1338
|
+
progressTimestamp: new Date(pinnedAt).getTime(),
|
|
1339
|
+
childIndex: firstLessonId
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (progressType === 'playlist') {
|
|
1344
|
+
const pinnedPlaylist = playlistItems.find(p => p.raw.id === id)
|
|
1345
|
+
if (pinnedPlaylist) {
|
|
1346
|
+
return pinnedPlaylist
|
|
1347
|
+
}else{
|
|
1348
|
+
const playlist = await fetchPlaylist(id)
|
|
1349
|
+
return {
|
|
1350
|
+
id: id,
|
|
1351
|
+
raw: playlist,
|
|
1352
|
+
progressTimestamp: new Date(pinnedAt).getTime(),
|
|
1353
|
+
type: 'playlist',
|
|
1354
|
+
last_engaged_on: playlist.items[0],
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
return null
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function getFirstLeafLessonId(data) {
|
|
1363
|
+
function findFirstLeaf(lessons) {
|
|
1364
|
+
for (const item of lessons) {
|
|
1365
|
+
if (!item.lessons || item.lessons.length === 0) {
|
|
1366
|
+
return item.id || null
|
|
1367
|
+
}
|
|
1368
|
+
const found = findFirstLeaf(item.lessons)
|
|
1369
|
+
if (found) return found
|
|
1370
|
+
}
|
|
1371
|
+
return null
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
return data.lessons ? findFirstLeaf(data.lessons) : null
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
|
|
File without changes
|