/* GETCOURSE — USER COURSES / v0.3.7 */
(function () {
'use strict';
const COURSE_PATH_PATTERN =
/^\/teach\/control(?:\/stream(?:\/index)?)?\/?$/;
const IS_ROUTE =
COURSE_PATH_PATTERN.test(
location.pathname
) ||
(
window.controllerId ===
'control/stream' &&
window.actionId ===
'index'
);
const STORAGE_KEY =
'psych-user-courses-state-v1';
const MAX_TRAININGS_PER_ROOT =
120;
const REQUEST_TIMEOUT =
12000;
const fetchCache =
new Map();
function isUser() {
return (
window.userInfo?.isAdmin === false
);
}
function isTargetPage() {
return (
IS_ROUTE &&
isUser() &&
document.querySelector(
'.main-page-block > .container'
)
);
}
if (
IS_ROUTE &&
isUser()
) {
document.documentElement
.classList.add(
'psych-user-courses-loading'
);
}
function cleanText(value) {
return String(
value || ''
)
.replace(
/\s+/g,
' '
)
.trim();
}
function getTrainingTypeFromDocument(
doc
) {
const marker =
doc.querySelector(
'.psych-training-type[data-training-type]'
);
return cleanText(
marker?.getAttribute(
'data-training-type'
)
).toLowerCase();
}
function isFreeWebinarsType(
value
) {
return (
String(value || '')
.toLowerCase() ===
'free-webinars'
);
}
function absoluteUrl(href) {
try {
return new URL(
href,
location.origin
).href;
} catch (error) {
return href || '#';
}
}
function readState() {
try {
const raw =
window.localStorage
.getItem(
STORAGE_KEY
);
if (!raw) {
return {
lastCourseId: null,
visited: {}
};
}
const state =
JSON.parse(raw);
return {
lastCourseId:
state.lastCourseId ||
null,
visited:
state.visited &&
typeof state.visited ===
'object'
? state.visited
: {}
};
} catch (error) {
return {
lastCourseId: null,
visited: {}
};
}
}
function writeState(state) {
try {
window.localStorage.setItem(
STORAGE_KEY,
JSON.stringify(state)
);
} catch (error) {
/* localStorage may be unavailable */
}
}
function markCourseOpened(course) {
if (
!course ||
!course.id
) {
return;
}
const state =
readState();
state.lastCourseId =
course.id;
state.visited[
course.id
] = Date.now();
writeState(state);
}
function getCourses() {
return Array.from(
document.querySelectorAll(
'.xdget-trainingList ' +
'.stream-table ' +
'.training-row'
)
)
.map(row => {
const link =
row.querySelector(
'td > a[href]'
);
const title =
row.querySelector(
'.stream-title'
);
if (
!link ||
!title
) {
return null;
}
const metaNode =
link.querySelector(
'div'
);
const href =
link.getAttribute(
'href'
) || '#';
const id =
row.getAttribute(
'data-training-id'
) ||
href ||
cleanText(
title.textContent
);
return {
id:
String(id),
href,
absoluteHref:
absoluteUrl(href),
title:
cleanText(
title.textContent
),
meta:
metaNode
? cleanText(
metaNode.textContent
)
: ''
};
})
.filter(Boolean);
}
function iconBook() {
return `
<svg
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<path
d="M5.5 4.75A2.75 2.75 0 0 1 8.25 2h10.25v16.25H8.25A2.75 2.75 0 0 0 5.5 21V4.75Z"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.5 18.25A2.75 2.75 0 0 1 8.25 15.5H18.5"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
/>
</svg>
`;
}
function iconVideo() {
return `
<svg
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<rect
x="3"
y="5"
width="18"
height="14"
rx="3"
stroke="currentColor"
stroke-width="1.7"
/>
<path
d="M10 9.25 15 12l-5 2.75v-5.5Z"
fill="currentColor"
/>
</svg>
`;
}
function iconForward() {
return `
<svg
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<path
d="M5 12h14M14 7l5 5-5 5"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
`;
}
function iconClock() {
return `
<svg
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<circle
cx="12"
cy="12"
r="8"
stroke="currentColor"
stroke-width="1.7"
/>
<path
d="M12 7.5V12l3 2"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
`;
}
function createRequestLimiter(
maxParallel
) {
let active =
0;
const queue =
[];
function runNext() {
if (
active >= maxParallel ||
!queue.length
) {
return;
}
const item =
queue.shift();
active += 1;
Promise.resolve()
.then(
item.task
)
.then(
item.resolve,
item.reject
)
.finally(
function () {
active -= 1;
runNext();
}
);
runNext();
}
return function limit(task) {
return new Promise(
function (
resolve,
reject
) {
queue.push({
task,
resolve,
reject
});
runNext();
}
);
};
}
const limitRequest =
createRequestLimiter(
4
);
async function fetchDocument(
url
) {
const absolute =
absoluteUrl(url);
if (
fetchCache.has(
absolute
)
) {
return fetchCache.get(
absolute
);
}
const promise =
limitRequest(
async function () {
const controller =
new AbortController();
const timeoutId =
window.setTimeout(
function () {
controller.abort();
},
REQUEST_TIMEOUT
);
try {
const response =
await fetch(
absolute,
{
credentials:
'same-origin',
signal:
controller.signal,
headers: {
'X-Requested-With':
'XMLHttpRequest'
}
}
);
if (
!response.ok
) {
throw new Error(
'HTTP ' +
response.status
);
}
const html =
await response.text();
return new DOMParser()
.parseFromString(
html,
'text/html'
);
} finally {
window.clearTimeout(
timeoutId
);
}
}
);
fetchCache.set(
absolute,
promise
);
try {
return await promise;
} catch (error) {
fetchCache.delete(
absolute
);
throw error;
}
}
function getTrainingIdFromDocument(
doc,
fallback
) {
const marker =
doc.querySelector(
'span[id^="training-id-"]'
);
if (marker) {
const match =
marker.id.match(
/^training-id-(.+)$/
);
if (
match &&
match[1]
) {
return String(
match[1]
);
}
}
return String(
fallback || ''
);
}
function getTrainingTitleFromDocument(
doc,
fallback
) {
const heading =
doc.querySelector(
'.page-header h1'
);
return (
cleanText(
heading?.textContent
) ||
fallback ||
'Тренинг'
);
}
function extractLessons(
doc,
training
) {
return Array.from(
doc.querySelectorAll(
'.xdget-lessonList ' +
'.lesson-list ' +
'> li[data-lesson-id]'
)
).map(
item => {
const classes =
item.classList;
const realLink =
item.querySelector(
'a.item-a[href]'
);
const titleNode =
item.querySelector(
'.link.title'
);
const fallbackHref =
titleNode
?.getAttribute(
'href'
) ||
'';
const status =
classes.contains(
'user-state-accomplished'
)
? 'accomplished'
: classes.contains(
'user-state-answered'
)
? 'answered'
: classes.contains(
'user-state-not_reached'
)
? 'not_reached'
: classes.contains(
'user-state-need_accomplish'
)
? 'need_accomplish'
: realLink
? 'available'
: 'unknown';
return {
id:
String(
item.getAttribute(
'data-lesson-id'
) || ''
),
title:
cleanText(
titleNode
? Array.from(
titleNode.childNodes
)
.filter(
node =>
!(
node.nodeType === 1 &&
node.classList?.contains(
'lesson-list__video-watched'
)
)
)
.map(
node =>
node.textContent || ''
)
.join(' ')
: ''
) ||
'Урок',
status,
available:
Boolean(
realLink
) &&
status !==
'not_reached',
href:
realLink
?.getAttribute(
'href'
) ||
fallbackHref ||
'',
navigableHref:
realLink
?.getAttribute(
'href'
) ||
'',
trainingId:
training.id,
trainingTitle:
training.title,
trainingHref:
training.href
};
}
);
}
function extractChildren(
doc
) {
return Array.from(
doc.querySelectorAll(
'.xdget-root ' +
'.xdget-trainingList ' +
'.stream-table ' +
'.training-row'
)
)
.map(
row => {
const link =
row.querySelector(
'td > a[href]'
);
const title =
row.querySelector(
'.stream-title'
);
if (
!link ||
!title
) {
return null;
}
const href =
link.getAttribute(
'href'
);
if (!href) {
return null;
}
return {
id:
String(
row.getAttribute(
'data-training-id'
) ||
href
),
href,
absoluteHref:
absoluteUrl(
href
),
title:
cleanText(
title.textContent
) ||
'Подтренинг'
};
}
)
.filter(Boolean);
}
async function loadTrainingTree(
descriptor,
context
) {
const urlKey =
descriptor.absoluteHref ||
absoluteUrl(
descriptor.href
);
if (
context.visited.has(
urlKey
)
) {
return null;
}
if (
context.visited.size >=
MAX_TRAININGS_PER_ROOT
) {
context.partial =
true;
return null;
}
context.visited.add(
urlKey
);
let doc;
try {
doc =
await fetchDocument(
descriptor.href
);
} catch (error) {
context.partial =
true;
return {
id:
descriptor.id,
title:
descriptor.title,
href:
descriptor.href,
lessons: [],
children: [],
error: true
};
}
const training = {
id:
getTrainingIdFromDocument(
doc,
descriptor.id
),
title:
getTrainingTitleFromDocument(
doc,
descriptor.title
),
href:
descriptor.href,
type:
getTrainingTypeFromDocument(
doc
)
};
const lessons =
extractLessons(
doc,
training
);
const childDescriptors =
extractChildren(
doc
);
const children =
(
await Promise.all(
childDescriptors.map(
child =>
loadTrainingTree(
child,
context
)
)
)
).filter(Boolean);
return {
...training,
lessons,
children,
error: false
};
}
function flattenLessons(
node,
output
) {
if (!node) {
return output;
}
node.lessons.forEach(
lesson =>
output.push(
lesson
)
);
node.children.forEach(
child =>
flattenLessons(
child,
output
)
);
return output;
}
function summarizeCourse(
course,
tree,
context,
wasOpened
) {
const lessons =
flattenLessons(
tree,
[]
);
const total =
lessons.length;
const accomplished =
lessons.filter(
lesson =>
lesson.status ===
'accomplished'
).length;
const answered =
lessons.filter(
lesson =>
lesson.status ===
'answered'
).length;
const unavailable =
lessons.filter(
lesson =>
lesson.status ===
'not_reached'
).length;
const nextLesson =
lessons.find(
lesson =>
lesson.available &&
lesson.status !==
'accomplished' &&
lesson.status !==
'answered' &&
lesson.navigableHref
) ||
null;
const progress =
total > 0
? Math.round(
(
accomplished /
total
) * 100
)
: 0;
const complete =
total > 0 &&
accomplished ===
total;
const hasActivity =
accomplished > 0 ||
answered > 0 ||
wasOpened;
let status =
'not-started';
let statusText =
'Не начато';
let statusClass =
'';
if (
tree?.error &&
total === 0
) {
status =
'error';
statusText =
'Нет данных';
statusClass =
'is-error';
} else if (
total === 0
) {
status =
'empty';
statusText =
'Без уроков';
statusClass =
'is-empty';
} else if (
complete
) {
status =
'complete';
statusText =
'Завершено';
statusClass =
'is-complete';
} else if (
answered > 0 &&
!nextLesson
) {
status =
'review';
statusText =
'На проверке';
statusClass =
'is-review';
} else if (
hasActivity
) {
status =
'progress';
statusText =
'В процессе';
statusClass =
'is-progress';
} else if (
!nextLesson &&
unavailable > 0
) {
status =
'waiting';
statusText =
'Ожидает открытия';
statusClass =
'is-waiting';
}
return {
course,
tree,
type:
tree?.type || '',
isFreeWebinars:
isFreeWebinarsType(
tree?.type
),
lessons,
total,
accomplished,
answered,
unavailable,
nextLesson,
progress,
complete,
hasActivity,
status,
statusText,
statusClass,
partial:
Boolean(
context.partial
)
};
}
async function loadCourseAnalysis(
course,
state
) {
const context = {
visited:
new Set(),
partial:
false
};
const tree =
await loadTrainingTree(
course,
context
);
return summarizeCourse(
course,
tree,
context,
Boolean(
state.visited[
course.id
]
)
);
}
function getLoadingAnalysis(
course,
state
) {
return {
course,
tree: null,
type: '',
isFreeWebinars: false,
lessons: [],
total: null,
accomplished: null,
answered: 0,
unavailable: 0,
nextLesson: null,
progress: 0,
complete: false,
hasActivity:
Boolean(
state.visited[
course.id
]
),
status: 'loading',
statusText:
'Загрузка',
statusClass:
'is-loading',
partial: false
};
}
function getFooterText(
analysis
) {
if (
analysis.isFreeWebinars
) {
return 'Смотреть записи';
}
switch (
analysis.status
) {
case 'complete':
return 'Повторить материалы';
case 'progress':
case 'review':
case 'waiting':
return 'Продолжить';
case 'loading':
return 'Открыть курс';
default:
return 'Начать обучение';
}
}
function getCourseDestination(
analysis
) {
if (
analysis.nextLesson
?.navigableHref
) {
return analysis
.nextLesson
.navigableHref;
}
return analysis
.course
.href;
}
function buildCard(
analysis
) {
const course =
analysis.course;
const card =
document.createElement(
'a'
);
card.className =
'psych-user-course-card';
if (
analysis.isFreeWebinars
) {
card.classList.add(
'is-free-webinars'
);
}
if (
analysis.complete &&
!analysis.isFreeWebinars
) {
card.classList.add(
'is-complete'
);
}
/*
* Карточка тренинга всегда ведёт в корень тренинга.
* Переход к следующему/последнему уроку остаётся только
* в отдельном блоке «Продолжить обучение».
*/
card.href =
course.href;
const progressText =
analysis.total === null
? 'Загружаем данные…'
: analysis.isFreeWebinars
? (
analysis.total === 1
? '1 запись вебинара'
: `${analysis.total} записей вебинаров`
)
: (
course.meta ||
'Учебная программа'
);
const nextHtml =
!analysis.isFreeWebinars &&
analysis.nextLesson
? `
<div
class="psych-user-course-card__next"
>
<span
class="psych-user-course-card__next-label"
>
Далее
</span>
<span
class="psych-user-course-card__next-title"
>
<span
class="psych-user-course-card__next-training"
></span>
<span
class="psych-user-course-card__next-lesson"
></span>
</span>
</div>
`
: '';
const noticeHtml =
!analysis.isFreeWebinars &&
analysis.answered > 0
? `
<div
class="psych-user-course-card__notice"
>
${iconClock()}
<span></span>
</div>
`
: '';
const progressHtml =
!analysis.isFreeWebinars &&
analysis.total !== null &&
analysis.total > 0
? `
<div
class="psych-user-course-card__progress"
>
<div
class="psych-user-course-card__progress-head"
>
<span>
${analysis.accomplished} из ${analysis.total} уроков
</span>
</div>
<div
class="psych-user-course-card__progress-track"
aria-hidden="true"
>
<span
class="psych-user-course-card__progress-fill"
style="width:${analysis.progress}%"
></span>
</div>
</div>
`
: '';
card.innerHTML = `
<div
class="psych-user-course-card__top"
>
<span
class="psych-user-course-card__icon"
>
${analysis.isFreeWebinars
? iconVideo()
: iconBook()}
</span>
<span
class="psych-user-course-card__status ${analysis.isFreeWebinars ? 'is-library' : analysis.statusClass}"
>
${analysis.isFreeWebinars
? 'Видеотека'
: analysis.statusText}
</span>
</div>
<div
class="psych-user-course-card__body"
>
<h3
class="psych-user-course-card__title"
></h3>
<p
class="psych-user-course-card__meta"
></p>
${progressHtml}
${noticeHtml}
${nextHtml}
</div>
<div
class="psych-user-course-card__footer"
>
<span>
${getFooterText(
analysis
)}
</span>
${iconForward()}
</div>
`;
card.querySelector(
'.psych-user-course-card__title'
).textContent =
course.title;
card.querySelector(
'.psych-user-course-card__meta'
).textContent =
progressText;
const notice =
card.querySelector(
'.psych-user-course-card__notice span'
);
if (notice) {
notice.textContent =
analysis.answered === 1
? '1 задание ожидает проверки'
: (
analysis.answered +
' заданий ожидают проверки'
);
}
if (
analysis.nextLesson &&
!analysis.isFreeWebinars
) {
const trainingNode =
card.querySelector(
'.psych-user-course-card__next-training'
);
const lessonNode =
card.querySelector(
'.psych-user-course-card__next-lesson'
);
if (
trainingNode &&
lessonNode
) {
const showTraining =
analysis.nextLesson
.trainingTitle &&
analysis.nextLesson
.trainingTitle !==
course.title;
trainingNode.textContent =
showTraining
? (
analysis.nextLesson
.trainingTitle +
' · '
)
: '';
lessonNode.textContent =
analysis.nextLesson
.title;
}
}
card.addEventListener(
'click',
function () {
markCourseOpened(
course
);
}
);
return card;
}
function pickFocusAnalysis(
analyses,
state
) {
const useful =
analyses.filter(
analysis =>
!analysis.isFreeWebinars &&
analysis.status !==
'complete' &&
analysis.status !==
'empty' &&
analysis.status !==
'error'
);
if (!useful.length) {
return null;
}
if (
state.lastCourseId
) {
const last =
useful.find(
analysis =>
String(
analysis
.course
.id
) ===
String(
state.lastCourseId
)
);
if (last) {
return last;
}
}
return (
useful.find(
analysis =>
analysis.status ===
'progress'
) ||
useful.find(
analysis =>
analysis.status ===
'review'
) ||
useful.find(
analysis =>
analysis.status ===
'waiting'
) ||
useful[0]
);
}
function buildContinueBlock(
analysis
) {
if (!analysis) {
return null;
}
const course =
analysis.course;
const wrapper =
document.createElement(
'div'
);
wrapper.className =
'psych-user-courses-continue';
const card =
document.createElement(
'a'
);
card.className =
'psych-user-courses-continue__card';
card.href =
getCourseDestination(
analysis
);
let label =
'Продолжить обучение';
let action =
'Продолжить';
let meta =
course.meta ||
'Учебная программа';
if (
analysis.nextLesson
) {
const prefix =
analysis.nextLesson
.trainingTitle !==
course.title
? (
analysis.nextLesson
.trainingTitle +
' · '
)
: '';
meta =
'Далее: ' +
prefix +
analysis.nextLesson
.title;
} else if (
analysis.status ===
'review'
) {
label =
'Задание на проверке';
action =
'Открыть курс';
meta =
analysis.answered === 1
? '1 задание ожидает проверки'
: (
analysis.answered +
' заданий ожидают проверки'
);
} else if (
analysis.status ===
'waiting'
) {
label =
'Обучение продолжается';
action =
'Открыть курс';
meta =
'Следующие уроки пока недоступны';
} else if (
analysis.status ===
'not-started'
) {
label =
'Начать обучение';
action =
'Начать';
meta =
analysis.nextLesson
? (
'Первый урок: ' +
analysis.nextLesson
.title
)
: meta;
}
const progressHtml =
analysis.total > 0
? `
<span
class="psych-user-courses-continue__progress"
>
<span
class="psych-user-courses-continue__progress-track"
aria-hidden="true"
>
<span
class="psych-user-courses-continue__progress-fill"
style="width:${analysis.progress}%"
></span>
</span>
<span
class="psych-user-courses-continue__progress-text"
>
${analysis.accomplished} из ${analysis.total} уроков
</span>
</span>
`
: '';
card.innerHTML = `
<span
class="psych-user-courses-continue__icon"
>
${iconBook()}
</span>
<span
class="psych-user-courses-continue__content"
>
<span
class="psych-user-courses-continue__label"
></span>
<span
class="psych-user-courses-continue__title"
></span>
<span
class="psych-user-courses-continue__meta"
></span>
</span>
<span
class="psych-user-courses-continue__action"
>
${action}
${iconForward()}
</span>
${progressHtml}
`;
card.querySelector(
'.psych-user-courses-continue__label'
).textContent =
label;
card.querySelector(
'.psych-user-courses-continue__title'
).textContent =
course.title;
card.querySelector(
'.psych-user-courses-continue__meta'
).textContent =
meta;
card.addEventListener(
'click',
function () {
markCourseOpened(
course
);
}
);
wrapper.appendChild(
card
);
return wrapper;
}
function buildDashboard(
analyses,
options
) {
const state =
readState();
const root =
document.createElement(
'section'
);
root.className =
'psych-user-courses-dashboard';
root.innerHTML = `
<div
class="psych-user-courses-hero"
>
<div
class="psych-user-courses-hero__content"
>
<h1
class="psych-user-courses-hero__title"
>
Моё обучение
</h1>
<p
class="psych-user-courses-hero__subtitle"
>
Ваши курсы, события и чаты
</p>
</div>
</div>
`;
if (
options?.loading
) {
const loadingNote =
document.createElement(
'div'
);
loadingNote.className =
'psych-user-courses-loading-note';
loadingNote.textContent =
'Загружаем актуальный прогресс по курсам и подтренингам…';
root.appendChild(
loadingNote
);
} else {
const focus =
pickFocusAnalysis(
analyses,
state
);
const continueBlock =
buildContinueBlock(
focus
);
if (continueBlock) {
root.appendChild(
continueBlock
);
}
}
const section =
document.createElement(
'div'
);
section.innerHTML = `
<div
class="psych-user-courses-section"
>
<h2
class="psych-user-courses-section__title"
>
Мои программы
</h2>
<span
class="psych-user-courses-section__count"
>
${analyses.length}
</span>
</div>
<div
class="psych-user-courses-grid"
></div>
`;
root.appendChild(
section
);
const grid =
root.querySelector(
'.psych-user-courses-grid'
);
if (
!analyses.length
) {
const empty =
document.createElement(
'div'
);
empty.className =
'psych-user-courses-empty';
empty.innerHTML = `
<div
class="psych-user-courses-empty__icon"
aria-hidden="true"
>
${iconBook()}
</div>
<h3
class="psych-user-courses-empty__title"
>
Пока нет доступных программ
</h3>
<p
class="psych-user-courses-empty__text"
>
Когда вам откроют доступ к обучению, программы появятся здесь.
</p>
`;
grid.replaceWith(
empty
);
return root;
}
analyses.forEach(
analysis => {
grid.appendChild(
buildCard(
analysis
)
);
}
);
return root;
}
function replaceDashboard(
nextDashboard
) {
const current =
document.querySelector(
'.psych-user-courses-dashboard'
);
if (current) {
current.replaceWith(
nextDashboard
);
}
}
async function hydrateProgress(
courses
) {
const state =
readState();
const analyses =
await Promise.all(
courses.map(
course =>
loadCourseAnalysis(
course,
state
)
)
);
replaceDashboard(
buildDashboard(
analyses,
{
loading: false
}
)
);
}
function releasePreload(
showDashboard
) {
document.documentElement
.classList.remove(
'psych-user-courses-loading'
);
if (showDashboard) {
document.documentElement
.classList.add(
'psych-user-courses-ready'
);
document.body
.classList.add(
'psych-user-courses-ready'
);
} else {
document.documentElement
.classList.remove(
'psych-user-courses-ready'
);
document.body
.classList.remove(
'psych-user-courses-ready'
);
}
}
let initStarted =
false;
function init() {
if (
initStarted ||
!IS_ROUTE ||
!isUser()
) {
return;
}
if (
!isTargetPage()
) {
return;
}
const trainingList =
document.querySelector(
'.xdget-trainingList'
);
const container =
document.querySelector(
'.main-page-block > .container'
);
if (
!container
) {
return;
}
initStarted =
true;
const courses =
getCourses();

/*
 * GetCourse при отсутствии доступных тренингов вообще не
 * выводит .xdget-trainingList. В этом случае всё равно
 * показываем кастомную оболочку и empty-state.
 */
if (
!trainingList ||
!courses.length
) {
container.appendChild(
buildDashboard(
[],
{
loading: false
}
)
);
releasePreload(
true
);
return;
}

const state =
readState();
const loadingAnalyses =
courses.map(
course =>
getLoadingAnalysis(
course,
state
)
);
const dashboard =
buildDashboard(
loadingAnalyses,
{
loading: true
}
);
container.appendChild(
dashboard
);
/*
 * Не показываем промежуточное состояние.
 * Страница остаётся скрытой до завершения расчёта прогресса,
 * после чего появляется уже готовый интерфейс одним кадром.
 */
hydrateProgress(
courses
).then(
function () {
releasePreload(
true
);
}
).catch(
function (error) {
console.warn(
'[USER COURSES] Progress loading failed:',
error
);
/*
 * Если получение прогресса не удалось, всё равно освобождаем
 * страницу, чтобы пользователь не остался с пустым экраном.
 */
releasePreload(
true
);
}
);
}
function scheduleInit() {
init();
window.setTimeout(
init,
250
);
window.setTimeout(
init,
700
);
window.setTimeout(
init,
1400
);
}
if (
document.readyState ===
'loading'
) {
document.addEventListener(
'DOMContentLoaded',
scheduleInit,
{
once: true
}
);
} else {
scheduleInit();
}
if (
IS_ROUTE &&
isUser()
) {
window.setTimeout(
function () {
if (
!document.querySelector(
'.psych-user-courses-dashboard'
)
) {
releasePreload(
false
);
}
},
2200
);
}
})();