feat(students): multi-photo gallery, group photos in export, KIBERone rebrand
This commit is contained in:
+89
-45
@@ -248,7 +248,7 @@ function buildProfileView(data, opts) {
|
||||
const dbModules = arr(data.modules);
|
||||
const photos = arr(data.photos);
|
||||
const files = arr(data.files);
|
||||
const name = str(data.name, 150) || str(student.name, 150) || 'Ученик';
|
||||
const name = str(data.name, 150) || str(student.name, 150) || 'Резидент';
|
||||
const profContacts = arr(profile.contacts);
|
||||
const profAchievements = arr(profile.achievements);
|
||||
|
||||
@@ -281,6 +281,19 @@ function buildProfileView(data, opts) {
|
||||
}
|
||||
}
|
||||
|
||||
const groupPhotoCards = arr(data.groupPhotos).map(gp => {
|
||||
const src = 'photos/' + gp.stored;
|
||||
const g = galleryAdd('image', src, gp.caption || '');
|
||||
const when = gp.takenAt || gp.createdAt;
|
||||
return {
|
||||
src,
|
||||
g,
|
||||
caption: showCaptions ? (str(gp.caption, 160) || '') : '',
|
||||
date: showDates && when ? longDate(when) : '',
|
||||
group: str(gp.groupName, 80) || '',
|
||||
};
|
||||
});
|
||||
|
||||
const photoByEntry = new Map();
|
||||
for (const p of photos) {
|
||||
if (!photoByEntry.has(p.entryId)) photoByEntry.set(p.entryId, []);
|
||||
@@ -375,12 +388,12 @@ function buildProfileView(data, opts) {
|
||||
});
|
||||
|
||||
const stats = [];
|
||||
if (showEntries) stats.push({ icon: 'clipboard-list', value: entries.length, suffix: plural(entries.length, 'запись', 'записи', 'записей'), label: 'Записей в журнале', hint: 'что делал ученик на занятиях' });
|
||||
if (showEntries) stats.push({ icon: 'clipboard-list', value: entries.length, suffix: plural(entries.length, 'запись', 'записи', 'записей'), label: 'Записей в журнале', hint: 'что делал резидент на занятиях' });
|
||||
if (showPhotos) stats.push({ icon: 'image', value: photos.length, suffix: plural(photos.length, 'фото', 'фото', 'фото'), label: 'Фотографий', hint: 'снимки с занятий' });
|
||||
if (showFiles) stats.push({ icon: 'folder-git-2', value: files.length, suffix: plural(files.length, 'работа', 'работы', 'работ'), label: 'Работ в портфолио', hint: 'файлы, прикреплённые к занятиям' });
|
||||
stats.push({ icon: 'users-round', value: groups.length, suffix: plural(groups.length, 'группа', 'группы', 'групп'), label: 'Групп', hint: groups.join(' · ') || '—' });
|
||||
stats.push({ icon: 'book-open-check', value: education.length, suffix: plural(education.length, 'тема', 'темы', 'тем'), label: 'Тем программы', hint: 'модули, которые проходил ученик' });
|
||||
if (days && showEntries) stats.push({ icon: 'calendar-clock', value: days, suffix: plural(days, 'день', 'дня', 'дней'), label: 'В академии', hint: 'от первой записи в журнале' });
|
||||
stats.push({ icon: 'book-open-check', value: education.length, suffix: plural(education.length, 'тема', 'темы', 'тем'), label: 'Тем программы', hint: 'модули, которые проходил резидент' });
|
||||
if (days && showEntries) stats.push({ icon: 'calendar-clock', value: days, suffix: plural(days, 'день', 'дня', 'дней'), label: 'В KIBERone', hint: 'от первой записи в журнале' });
|
||||
for (const s of arr(profile.stats)) {
|
||||
const label = str(s && s.label, 80);
|
||||
const value = (s && s.value !== undefined && s.value !== null) ? String(s.value).trim().slice(0, 20) : '';
|
||||
@@ -439,29 +452,31 @@ function buildProfileView(data, opts) {
|
||||
const city = str(profile.city, 120) || str(student.branch_name, 120);
|
||||
if (city) facts.push({ icon: 'map-pin', text: city });
|
||||
if (groups.length) facts.push({ icon: 'users-round', text: groups.join(' · ') });
|
||||
const joined = str(profile.joined, 120) || (student.created_at ? 'В академии с ' + monthYear(student.created_at) : null);
|
||||
const joined = str(profile.joined, 120) || (student.created_at ? 'В KIBERone с ' + monthYear(student.created_at) : null);
|
||||
if (joined) facts.push({ icon: 'calendar-check', text: joined });
|
||||
const mentor = str(profile.mentor, 150);
|
||||
if (mentor) facts.push({ icon: 'user-check', text: 'Наставник: ' + mentor });
|
||||
if (education.length) facts.push({ icon: 'book-open', text: education.length + ' ' + plural(education.length, 'тема', 'темы', 'тем') + ' программы' });
|
||||
|
||||
const autoBio = [];
|
||||
if (groups.length) autoBio.push('Ученик группы ' + groups.join(', ') + '.');
|
||||
if (groups.length) autoBio.push('Резидент группы ' + groups.join(', ') + '.');
|
||||
if (entries.length) autoBio.push('В журнале ' + entries.length + ' ' + plural(entries.length, 'запись', 'записи', 'записей') + ' о занятиях.');
|
||||
if (photos.length) autoBio.push('Фотографий: ' + photos.length + '.');
|
||||
if (files.length) autoBio.push('Работ в портфолио: ' + files.length + '.');
|
||||
const tagList = arr(profile.tags).map(t => str(t, 40)).filter(Boolean);
|
||||
const heroTags = tagList.length ? tagList : education.slice(0, 6).map(m => m.title);
|
||||
const avatar = showPhotos && photoCards.length ? photoCards[0] : null;
|
||||
const cover = showPhotos && photoCards.length > 1 ? photoCards[1] : null;
|
||||
const avatar = showPhotos && photoCards.length ? (photoCards[1] || photoCards[0]) : null;
|
||||
const cover = showPhotos && photoCards.length ? photoCards[0] : null;
|
||||
const mainPhotoCard = showPhotos && groupPhotoCards.length ? groupPhotoCards[0] : null;
|
||||
const profilePhotoCard = showPhotos && photoCards.length ? photoCards[0] : null;
|
||||
const eduYears = education.map(m => m.period).filter(Boolean);
|
||||
const idLabel = student.id ? 'ID S-' + String(student.id).padStart(4, '0') : null;
|
||||
|
||||
return {
|
||||
name, student, profile, entries, education, projects, photoCards, tasks, gallery, groups, stats,
|
||||
name, student, profile, entries, education, projects, photoCards, groupPhotoCards, tasks, gallery, groups, stats,
|
||||
skillGroups, experience, contacts, achievements, facts, heroTags,
|
||||
avatar, cover, idLabel,
|
||||
role: str(profile.role, 200) || (education.length ? 'Ученик программы: ' + education[0].title : null),
|
||||
avatar, cover, idLabel, mainPhotoCard, profilePhotoCard,
|
||||
role: str(profile.role, 200) || (education.length ? 'Резидент программы: ' + education[0].title : null),
|
||||
bio: str(profile.bio, 2000) || (autoBio.join(' ') || null),
|
||||
quote: str(profile.quote, 300),
|
||||
status: str(profile.status, 60) || 'Активно учится',
|
||||
@@ -563,10 +578,11 @@ svg{display:block;flex:none}
|
||||
.media--wide{aspect-ratio:21/9}
|
||||
.media[data-g]{cursor:zoom-in}
|
||||
.hero{padding:clamp(20px,2.6vw,34px);overflow:hidden;position:relative}
|
||||
.hero__cover{border-radius:var(--radius-sm);margin-bottom:clamp(-72px,-5vw,-48px)}
|
||||
.hero__grid{display:grid;grid-template-columns:minmax(240px,380px) minmax(0,1fr);gap:clamp(18px,2.6vw,34px);align-items:end}
|
||||
.hero__cover{border-radius:var(--radius-sm);margin-bottom:clamp(24px,3vw,40px);position:relative;z-index:0}
|
||||
.hero__cover .media{border-radius:var(--radius-sm)}
|
||||
.hero__grid{display:grid;grid-template-columns:minmax(240px,380px) minmax(0,1fr);gap:clamp(18px,2.6vw,34px);align-items:end;position:relative;z-index:1}
|
||||
@media (max-width:900px){.hero__grid{grid-template-columns:1fr;align-items:start}.hero__cover{display:none}}
|
||||
.hero__media{position:relative;padding-top:clamp(10px,2vw,26px)}
|
||||
.hero__media{position:relative;margin-top:clamp(-72px,-5vw,-48px);padding-top:clamp(10px,2vw,26px)}
|
||||
.hero__avatar{border-radius:26px;overflow:hidden;box-shadow:var(--shadow)}
|
||||
.hero__status{position:absolute;left:14px;bottom:14px;padding:7px 12px;border-radius:999px;background:rgba(6,10,24,.6);backdrop-filter:blur(8px);border:1px solid var(--stroke);color:#eaf0ff;font-size:.78rem;font-weight:800;display:inline-flex;align-items:center;gap:7px}
|
||||
.hero__pulse{width:8px;height:8px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 0 rgba(52,211,153,.7);animation:pulse 2s infinite}
|
||||
@@ -667,11 +683,11 @@ svg{display:block;flex:none}
|
||||
.task__check svg{width:20px;height:20px;stroke-width:2.4}
|
||||
.task__check--progress{background:rgba(251,191,36,.14);border-color:rgba(251,191,36,.32);color:var(--warn)}
|
||||
.task__check--error{background:rgba(251,113,133,.14);border-color:rgba(251,113,133,.32);color:var(--danger)}
|
||||
.task__main{min-width:0;display:flex;flex-direction:column;gap:6px}
|
||||
.task__main{min-width:0;width:100%;display:flex;flex-direction:column;gap:6px}
|
||||
.task__title{font-size:.95rem;font-weight:800;overflow-wrap:anywhere}
|
||||
.task__sub{font-size:.79rem;color:var(--muted);font-weight:600;overflow-wrap:anywhere}
|
||||
.task__right{display:flex;flex-direction:column;align-items:flex-end;gap:6px}
|
||||
.task__extra{display:flex;flex-wrap:wrap;gap:6px}
|
||||
.task__extra{grid-column:1 / -1;display:flex;flex-wrap:wrap;gap:6px}
|
||||
.task__extra a{display:inline-flex;align-items:center;gap:5px;font-size:.75rem;font-weight:700;color:var(--muted);border:1px solid var(--stroke);border-radius:999px;padding:3px 9px;background:var(--surface-2)}
|
||||
.task__extra a:hover{color:var(--text);border-color:var(--stroke-strong)}
|
||||
.task__extra a svg{width:12px;height:12px}
|
||||
@@ -790,12 +806,16 @@ function emptyBlock(icon, title, text) {
|
||||
}
|
||||
|
||||
function heroHtml(v) {
|
||||
const cover = v.cover
|
||||
? mediaBlock(v.cover.src, 'wide', 'Обложка профиля', v.cover.g)
|
||||
: phBlock('Обложка профиля', 1440, 480, 'wallpaper', 'border-radius:var(--radius-sm)');
|
||||
const avatar = v.avatar
|
||||
? mediaBlock(v.avatar.src, 'square', v.name, v.avatar.g)
|
||||
: phBlock('Фото ученика', 480, 480, 'user-round');
|
||||
const cover = v.mainPhotoCard
|
||||
? mediaBlock(v.mainPhotoCard.src, 'wide', 'Обложка профиля', v.mainPhotoCard.g)
|
||||
: (v.cover
|
||||
? mediaBlock(v.cover.src, 'wide', 'Обложка профиля', v.cover.g)
|
||||
: phBlock('Обложка профиля', 1440, 480, 'wallpaper', 'border-radius:var(--radius-sm)'));
|
||||
const avatar = v.profilePhotoCard
|
||||
? mediaBlock(v.profilePhotoCard.src, 'square', v.name, v.profilePhotoCard.g)
|
||||
: (v.avatar
|
||||
? mediaBlock(v.avatar.src, 'square', v.name, v.avatar.g)
|
||||
: phBlock('Фото резидента', 480, 480, 'user-round'));
|
||||
const eyebrow = [v.idLabel, str(v.periodLabel, 60)].filter(Boolean).join(' · ');
|
||||
const actions = [];
|
||||
actions.push('<button class="btn btn--primary" type="button" data-action="print">' + iconTag('printer') + 'Печать / PDF</button>');
|
||||
@@ -842,7 +862,7 @@ function statsHtml(v) {
|
||||
function skillsHtml(v) {
|
||||
if (!v.skillGroups.length) {
|
||||
return emptyBlock('sparkles', 'Навыки пока не заполнены',
|
||||
'Список навыков и технологий можно добавить в данных профиля ученика.');
|
||||
'Список навыков и технологий можно добавить в данных профиля резидента.');
|
||||
}
|
||||
return v.skillGroups.map(g => '<article class="card skill-group reveal">' +
|
||||
'<div class="skill-group__head">' + iconTag(g.icon) + '<span>' + escapeHtml(g.title) + '</span>' +
|
||||
@@ -865,7 +885,7 @@ function projectMedia(p) {
|
||||
function projectsHtml(v) {
|
||||
if (!v.projects.length) {
|
||||
return emptyBlock('folder-git-2', 'Работ в портфолио пока нет',
|
||||
'Файлы появятся здесь, как только ученик прикрепит их к записи занятия.');
|
||||
'Файлы появятся здесь, как только резидент прикрепит их к записи занятия.');
|
||||
}
|
||||
return v.projects.map(p => '<article class="card prj-card">' +
|
||||
projectMedia(p) +
|
||||
@@ -885,7 +905,7 @@ function projectsHtml(v) {
|
||||
function photosHtml(v) {
|
||||
if (!v.photoCards.length) {
|
||||
return emptyBlock('image', 'Фотографий пока нет',
|
||||
'Фотографии занятий появятся здесь, как только ученик отправит их вместе с записью.');
|
||||
'Фотографии занятий появятся здесь, как только резидент отправит их вместе с записью.');
|
||||
}
|
||||
return v.photoCards.map(p => '<figure class="card photo-card reveal">' +
|
||||
mediaBlock(p.src, '4x3', v.name + ' — фото', p.g) +
|
||||
@@ -896,22 +916,37 @@ function photosHtml(v) {
|
||||
'</figure>').join('');
|
||||
}
|
||||
|
||||
function groupPhotosHtml(v) {
|
||||
if (!v.groupPhotoCards.length) {
|
||||
return emptyBlock('images', 'Фото группы пока нет',
|
||||
'Фотографии группы появятся здесь, как только их добавят в галерее группы.');
|
||||
}
|
||||
return v.groupPhotoCards.map(p => '<figure class="card photo-card reveal">' +
|
||||
mediaBlock(p.src, '4x3', (p.group || 'Группа') + ' — фото', p.g) +
|
||||
(p.caption || p.date || p.group
|
||||
? '<figcaption>' + (p.caption ? '<b>' + escapeHtml(p.caption) + '</b>' : '') +
|
||||
(p.date || p.group ? '<span>' + escapeHtml([p.date, p.group].filter(Boolean).join(' · ')) + '</span>' : '') + '</figcaption>'
|
||||
: '') +
|
||||
'</figure>').join('');
|
||||
}
|
||||
|
||||
function tasksHtml(v) {
|
||||
if (!v.tasks.length) {
|
||||
return emptyBlock('list-checks', 'Записей о занятиях пока нет',
|
||||
'Как только ученик отправит запись через форму, она появится в этом разделе.');
|
||||
'Как только резидент отправит запись через форму, она появится в этом разделе.');
|
||||
}
|
||||
return v.tasks.map(t => {
|
||||
const checkCls = t.done ? '' : (t.icon === 'triangle-alert' ? ' task__check--error' : ' task__check--progress');
|
||||
const right = ['<span class="badge ' + (t.done ? 'badge--ok' : (t.icon === 'triangle-alert' ? 'badge--muted' : 'badge--progress')) + '">' + escapeHtml(t.statusLabel) + '</span>'];
|
||||
if (t.thumbs.length) right.push('<div class="task__thumbs">' + t.thumbs.map(x => '<img src="' + escapeHtml(x.src) + '" alt="фото" data-g="' + x.g + '" loading="lazy">').join('') + '</div>');
|
||||
if (t.files.length) right.push('<div class="task__extra">' + t.files.map(f => '<a href="' + escapeHtml(f.href) + '"' +
|
||||
(f.g >= 0 ? ' data-g="' + f.g + '"' : ' target="_blank" rel="noopener"') + '>' + iconTag(f.icon) + escapeHtml(truncate(f.title, 28)) + '</a>').join('') + '</div>');
|
||||
if (t.files.length) var extra = '<div class="task__extra">' + t.files.map(f => '<a href="' + escapeHtml(f.href) + '"' +
|
||||
(f.g >= 0 ? ' data-g="' + f.g + '"' : ' target="_blank" rel="noopener"') + '>' + iconTag(f.icon) + escapeHtml(truncate(f.title, 28)) + '</a>').join('') + '</div>';
|
||||
return '<article class="card task reveal">' +
|
||||
'<span class="task__check' + checkCls + '">' + iconTag(t.icon) + '</span>' +
|
||||
'<div class="task__main"><p class="task__title">' + escapeHtml(t.title) + '</p>' +
|
||||
(t.sub ? '<p class="task__sub">' + escapeHtml(t.sub) + '</p>' : '') + '</div>' +
|
||||
'<div class="task__right">' + right.join('') + '</div>' +
|
||||
(extra || '') +
|
||||
'</article>';
|
||||
}).join('');
|
||||
}
|
||||
@@ -920,16 +955,20 @@ function aboutHtml(v) {
|
||||
const bio = '<article class="card about__bio reveal">' +
|
||||
'<h3 class="section__title" style="font-size:1.15rem">' + iconTag('user-round') + 'Коротко о себе</h3>' +
|
||||
(v.bio ? '<p class="about__text">' + escapeHtml(v.bio) + '</p>'
|
||||
: '<p class="about__text">Здесь появится рассказ ученика о себе. Добавьте текст в данных профиля.</p>') +
|
||||
: '<p class="about__text">Здесь появится рассказ резидента о себе. Добавьте текст в данных профиля.</p>') +
|
||||
(v.quote ? '<p class="about__quote">«' + escapeHtml(v.quote) + '»</p>' : '') +
|
||||
(v.facts.length ? '<div class="hero__facts">' + v.facts.map(f => '<span class="fact">' + iconTag(f.icon) + '<span>' + escapeHtml(f.text) + '</span></span>').join('') + '</div>' : '') +
|
||||
'</article>';
|
||||
|
||||
const photoCard = '<article class="card about__card reveal"><h3>' + iconTag('image') + 'Фото автора</h3>' +
|
||||
(v.avatar
|
||||
? '<div class="about__photo"><img src="' + escapeHtml(v.avatar.src) + '" alt="' + escapeHtml(v.name) + '" data-g="' + v.avatar.g + '" loading="lazy"></div>'
|
||||
: phBlock('Фото автора', 800, 600, 'image')) +
|
||||
'<p class="meta">' + iconTag('info') + (v.avatar ? 'Фотография из архива занятий' : 'Место под реальное фото автора профиля') + '</p></article>';
|
||||
(v.profilePhotoCard
|
||||
? '<div class="about__photo"><img src="' + escapeHtml(v.profilePhotoCard.src) + '" alt="' + escapeHtml(v.name) + '" data-g="' + v.profilePhotoCard.g + '" loading="lazy"></div>'
|
||||
: (v.mainPhotoCard
|
||||
? '<div class="about__photo"><img src="' + escapeHtml(v.mainPhotoCard.src) + '" alt="' + escapeHtml(v.name) + '" data-g="' + v.mainPhotoCard.g + '" loading="lazy"></div>'
|
||||
: (v.avatar
|
||||
? '<div class="about__photo"><img src="' + escapeHtml(v.avatar.src) + '" alt="' + escapeHtml(v.name) + '" data-g="' + v.avatar.g + '" loading="lazy"></div>'
|
||||
: phBlock('Фото автора', 800, 600, 'image')))) +
|
||||
'<p class="meta">' + iconTag('info') + ((v.profilePhotoCard || v.mainPhotoCard || v.avatar) ? 'Фотография из архива занятий' : 'Место под реальное фото автора профиля') + '</p></article>';
|
||||
|
||||
const contactsCard = '<article class="card about__card reveal"><h3>' + iconTag('at-sign') + 'Контакты</h3>' +
|
||||
(v.contacts.length
|
||||
@@ -950,9 +989,10 @@ function footerHtml(v, generatedAt) {
|
||||
const links = [];
|
||||
if (v.entries.length) links.push('<a href="#lessons">Занятия</a>');
|
||||
if (v.photoCards.length) links.push('<a href="#photos">Фотографии</a>');
|
||||
if (v.groupPhotoCards.length) links.push('<a href="#group-photos">Фото группы</a>');
|
||||
if (v.projects.length) links.push('<a href="#projects">Работы</a>');
|
||||
links.push('<a href="#top">Наверх</a>');
|
||||
return '<footer class="footer"><p>© <span id="year"></span> Академия · профиль ученика ' + escapeHtml(v.name) +
|
||||
return '<footer class="footer"><p>© <span id="year"></span> KIBERone · профиль резидента ' + escapeHtml(v.name) +
|
||||
' · сформировано ' + escapeHtml(longDate(generatedAt)) + '</p>' +
|
||||
'<nav class="footer__links" aria-label="Футер">' + links.join('') + '</nav></footer>';
|
||||
}
|
||||
@@ -960,6 +1000,7 @@ function footerHtml(v, generatedAt) {
|
||||
function navItems(v) {
|
||||
const items = ['<a class="nav-link" href="#profile" data-spy="profile">' + iconTag('user-round') + '<span>Профиль</span></a>'];
|
||||
if (v.photoCards.length) items.push('<a class="nav-link" href="#photos" data-spy="photos">' + iconTag('images') + '<span>Фотографии</span></a>');
|
||||
if (v.groupPhotoCards.length) items.push('<a class="nav-link" href="#group-photos" data-spy="group-photos">' + iconTag('users-round') + '<span>Фото группы</span></a>');
|
||||
items.push('<a class="nav-link" href="#education" data-spy="education">' + iconTag('book-open') + '<span>Образование</span></a>');
|
||||
if (v.experience.length) items.push('<a class="nav-link" href="#experience" data-spy="experience">' + iconTag('briefcase') + '<span>Опыт</span></a>');
|
||||
if (v.skillGroups.length) items.push('<a class="nav-link" href="#skills" data-spy="skills">' + iconTag('sparkles') + '<span>Навыки</span></a>');
|
||||
@@ -972,7 +1013,7 @@ function navItems(v) {
|
||||
function educationHtml(v) {
|
||||
if (!v.education.length) {
|
||||
return emptyBlock('book-open', 'Темы программы ещё не заполнены',
|
||||
'Добавьте темы модулей в админке (раздел «Темы модулей») или отметьте пройденные модули в данных профиля ученика.');
|
||||
'Добавьте темы модулей в админке (раздел «Темы модулей») или отметьте пройденные модули в данных профиля резидента.');
|
||||
}
|
||||
return v.education.map(m => {
|
||||
const p = m.progress;
|
||||
@@ -1003,7 +1044,7 @@ function educationHtml(v) {
|
||||
function experienceHtml(v) {
|
||||
if (!v.experience.length) {
|
||||
return emptyBlock('briefcase', 'Опыт работы пока не заполнен',
|
||||
'Кейсы, стажировки и командные проекты можно добавить в данных профиля ученика — они появятся в этом архиве.');
|
||||
'Кейсы, стажировки и командные проекты можно добавить в данных профиля резидента — они появятся в этом архиве.');
|
||||
}
|
||||
return v.experience.map(e => {
|
||||
const media = e.media ? mediaBlock(e.media, '16x9', e.title, -1) : phBlock('Фото кейса', 1280, 720, 'image');
|
||||
@@ -1102,41 +1143,44 @@ function renderStudentReport(data, opts) {
|
||||
const sections = [];
|
||||
sections.push('<section class="section" id="profile">' + heroHtml(v) + '</section>');
|
||||
sections.push('<section class="section" id="stats">' +
|
||||
sectionHead('activity', 'Информация об авторе', 'Показатели ученика по данным журнала занятий и архива работ.') +
|
||||
sectionHead('activity', 'Информация об авторе', 'Показатели резидента по данным журнала занятий и архива работ.') +
|
||||
(periodChip ? '<div class="hero__tags">' + periodChip + '</div>' : '') +
|
||||
'<div class="stats">' + statsHtml(v) + '</div></section>');
|
||||
sections.push('<section class="section" id="photos">' +
|
||||
sectionHead('images', 'Фотографии', 'Снимки из записей занятий. Нажмите на фотографию, чтобы открыть её крупнее.') +
|
||||
'<div class="photos">' + photosHtml(v) + '</div></section>');
|
||||
sections.push('<section class="section" id="group-photos">' +
|
||||
sectionHead('users-round', 'Фотографии группы', 'Снимки из хроники группы резидента. Нажмите на фотографию, чтобы открыть её крупнее.') +
|
||||
'<div class="photos">' + groupPhotosHtml(v) + '</div></section>');
|
||||
sections.push('<section class="section" id="education">' +
|
||||
sectionHead('book-open', 'Образование', 'Темы программы, которые проходил ученик, и прогресс по ним.') +
|
||||
sectionHead('book-open', 'Образование', 'Темы программы, которые проходил резидент, и прогресс по ним.') +
|
||||
'<div class="modules">' + educationHtml(v) + '</div></section>');
|
||||
sections.push('<section class="section" id="experience">' +
|
||||
railHead('briefcase', 'Опыт работы', 'Стажировки, фриланс и командные проекты — ленту можно прокручивать вбок.', 'experienceRail') +
|
||||
'<div class="rail-wrap"><div class="rail rail--half" id="experienceRail" data-rail="experienceRail" tabindex="0" aria-label="Лента опыта">' +
|
||||
experienceHtml(v) + '</div></div></section>');
|
||||
sections.push('<section class="section" id="skills">' +
|
||||
sectionHead('sparkles', 'Навыки', 'Темы и технологии, которые ученик уже проходил.') +
|
||||
sectionHead('sparkles', 'Навыки', 'Темы и технологии, которые резидент уже проходил.') +
|
||||
'<div class="skills">' + skillsHtml(v) + '</div></section>');
|
||||
sections.push('<section class="section" id="projects">' +
|
||||
railHead('folder-git-2', 'Работы', 'Файлы, прикреплённые учеником к записям занятий.', 'projectsRail') +
|
||||
railHead('folder-git-2', 'Работы', 'Файлы, прикреплённые резидентом к записям занятий.', 'projectsRail') +
|
||||
'<div class="rail-wrap"><div class="rail rail--third" id="projectsRail" data-rail="projectsRail" tabindex="0" aria-label="Лента работ">' +
|
||||
projectsHtml(v) + '</div></div></section>');
|
||||
sections.push('<section class="section" id="lessons">' +
|
||||
sectionHead('list-checks', 'Занятия', 'Записи журнала: что ученик делал, фотографии и файлы занятий.') +
|
||||
sectionHead('list-checks', 'Занятия', 'Записи журнала: что резидент делал, фотографии и файлы занятий.') +
|
||||
'<div class="tasks">' + tasksHtml(v) + '</div></section>');
|
||||
sections.push('<section class="section" id="about">' +
|
||||
sectionHead('info', 'Немного о себе', 'Рассказ ученика, контакты и достижения.') +
|
||||
sectionHead('info', 'Немного о себе', 'Рассказ резидента, контакты и достижения.') +
|
||||
'<div class="about">' + aboutHtml(v) + '</div></section>');
|
||||
|
||||
const topbar = '<header class="topbar" id="top"><div class="topbar__inner">' +
|
||||
'<a class="brand" href="#top"><span class="brand__logo">' + iconTag('graduation-cap') + '</span>' +
|
||||
'<span class="brand__text">Академия<small>Профиль ученика</small></span></a>' +
|
||||
'<span class="brand__text">KIBERone<small>Профиль резидента</small></span></a>' +
|
||||
'<nav class="topbar__nav" id="mainNav" aria-label="Меню профиля">' + navItems(v) + '</nav>' +
|
||||
'<div class="topbar__actions">' +
|
||||
'<button class="icon-btn" type="button" data-action="print" title="Печать / сохранить в PDF">' + iconTag('printer') + '</button>' +
|
||||
'<button class="icon-btn" id="themeToggle" type="button" title="Светлая / тёмная тема">' + iconTag('moon') + '</button>' +
|
||||
'<div class="user-chip" title="Ученик"><span class="user-chip__ph">' + iconTag('user-round') + '</span>' +
|
||||
'<div class="user-chip" title="Резидент"><span class="user-chip__ph">' + iconTag('user-round') + '</span>' +
|
||||
'<span class="user-chip__name">' + escapeHtml(chipInit(v.name)) + '</span></div>' +
|
||||
'<button class="icon-btn topbar__burger" id="burger" type="button" aria-expanded="false" aria-controls="mainNav" aria-label="Открыть меню">' +
|
||||
iconTag('menu') + '</button></div></div></header>';
|
||||
@@ -1152,7 +1196,7 @@ function renderStudentReport(data, opts) {
|
||||
|
||||
return '<!DOCTYPE html>\n<html lang="ru" data-theme="dark">\n<head>\n' +
|
||||
'<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n' +
|
||||
'<title>' + escapeHtml(v.name) + ' — профиль ученика</title>\n' +
|
||||
'<title>' + escapeHtml(v.name) + ' — профиль резидента</title>\n' +
|
||||
'<link rel="preconnect" href="https://fonts.googleapis.com">\n' +
|
||||
'<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>\n' +
|
||||
'<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">\n' +
|
||||
|
||||
Reference in New Issue
Block a user