feat(report): student report page with works/gallery/files sections

This commit is contained in:
dev
2026-09-18 17:50:50 +03:00
parent 12c612eccd
commit d434732f41
9 changed files with 1187 additions and 109 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -49,6 +49,7 @@ async function loadLinks() {
</div>
<div style="display:flex;gap:8px;flex-shrink:0;align-items:center">
<a class="btn-link" style="text-decoration:none" href="/s/${l.token}" target="_blank" title="Открыть">Открыть</a>
<a class="btn-link" style="text-decoration:none" href="/r/${l.token}" target="_blank" title="Отчёт ученика">Отчёт</a>
<button class="btn-link" data-copy="${esc(linkUrl(l.token))}" title="Копировать ссылку">Копировать</button>
<button class="btn-link" data-edit-link="${l.id}" title="Редактировать">✎</button>
<button class="btn-link" style="color:var(--muted)" data-del-link="${l.id}" title="Удалить">&times;</button>
+546
View File
@@ -0,0 +1,546 @@
const state={token:'',password:null,showNames:true,cdTarget:0,cdTimer:null,cdBound:false};
function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#039;')}
function pad(n){return String(n).padStart(2,'0')}
function toDate(v){if(!v)return null;const d=new Date(v);return isNaN(d.getTime())?null:d}
function fmtDate(v){const d=toDate(v);return d?pad(d.getDate())+'.'+pad(d.getMonth()+1)+'.'+d.getFullYear():''}
function fmtShort(v){const d=toDate(v);return d?pad(d.getDate())+'.'+pad(d.getMonth()+1)+'.'+String(d.getFullYear()).slice(2):''}
function fmtLong(v){const d=toDate(v);return d?d.toLocaleDateString('ru-RU',{day:'numeric',month:'long',year:'numeric'}):''}
function plural(n,one,few,many){const m10=n%10,m100=n%100;if(m100>=11&&m100<=14)return many;if(m10===1)return one;if(m10>=2&&m10<=4)return few;return many}
function words(n,one,few,many){return n+' '+plural(n,one,few,many)}
function isImageFile(name){return /\.(jpe?g|jfif|png|gif|webp|bmp|avif|svg|ico)$/i.test(name||'')}
function extLabel(name){const m=String(name||'').match(/\.([a-z0-9]{1,5})$/i);return m?m[1].toUpperCase():'FILE'}
function baseName(name){return String(name||'файл').replace(/^.*[\\/]/,'').slice(0,80)}
function thumbSrc(p){if(!p)return '';if(p.indexOf('/uploads/')===0)return '/uploads/thumb/'+p.slice(9);if(p.indexOf('/api/files/')>=0)return p+(p.indexOf('?')>=0?'&':'?')+'thumb=1';return p}
function fileUrl(token){return '/api/share/'+encodeURIComponent(state.token)+'/files/'+encodeURIComponent(token)+(state.password?'?password='+encodeURIComponent(state.password):'')}
function initials(name){const parts=String(name||'').trim().split(/\s+/).filter(Boolean);if(!parts.length)return 'WI';const a=parts[0].charAt(0);const b=parts.length>1?parts[1].charAt(0):(parts[0].length>1?parts[0].charAt(1):'');return (a+b).toUpperCase()}
function setText(id,text){const el=document.getElementById(id);if(el)el.textContent=text==null?'':String(text)}
function setHTML(id,html){const el=document.getElementById(id);if(el)el.innerHTML=html}
function hideEl(el,on){if(el)el.classList.toggle('rep-hidden',!!on)}
function node(html){const t=document.createElement('template');t.innerHTML=html.trim();return t.content.firstElementChild}
function periodLabel(from,to){
if(!from&&!to)return '';
const a=toDate(from),b=toDate(to);
if(a&&b)return fmtDate(a)+' — '+fmtDate(b);
return fmtDate(a)||fmtDate(b);
}
function yearLabel(from,to){
const a=toDate(from),b=toDate(to);
if(!a&&!b)return '';
const ay=a?a.getFullYear():null,by=b?b.getFullYear():null;
if(ay&&by)return ay===by?String(ay):ay+' — '+String(by).slice(2);
return String(ay||by);
}
function countsLabel(photos,files){
const out=[];
if(photos)out.push(words(photos,'фото','фото','фото'));
if(files)out.push(words(files,'файл','файла','файлов'));
return out.join(' · ');
}
async function fetchShare(token,password){
const headers={};
if(password)headers['x-share-password']=password;
const res=await fetch('/api/share/'+encodeURIComponent(token),{headers});
const data=await res.json().catch(function(){return {}});
return {ok:res.ok,status:res.status,data:data};
}
function showError(msg){
stopCountdown();
setText('errorText',msg);
const box=document.getElementById('errorBox');
if(box)box.style.display='flex';
const content=document.getElementById('content');
if(content)content.style.display='none';
}
function stopCountdown(){
if(state.cdTimer){clearInterval(state.cdTimer);state.cdTimer=null}
state.cdTarget=0;
const el=document.getElementById('countdown');
if(el)el.style.display='none';
}
function formatRemaining(ms){
const total=Math.floor(ms/1000);
const days=Math.floor(total/86400);
const hours=Math.floor((total%86400)/3600);
const minutes=Math.floor((total%3600)/60);
const seconds=total%60;
const parts=[];
if(days>0)parts.push(words(days,'день','дня','дней'));
if(hours>0)parts.push(words(hours,'час','часа','часов'));
if(minutes>0)parts.push(words(minutes,'минуту','минуты','минут'));
if(days===0)parts.push(words(seconds,'секунду','секунды','секунд'));
return parts.slice(0,2).join(' ');
}
function syncCountdown(){
const el=document.getElementById('countdown');
if(!el||!state.cdTarget)return;
if(state.cdTarget-Date.now()<=0){el.style.display='none';return}
const hero=document.getElementById('hero');
const passed=hero?window.scrollY>hero.offsetHeight*0.5:true;
el.style.display=passed?'inline-flex':'none';
}
function tickCountdown(){
const el=document.getElementById('countdown');
if(!el||!state.cdTarget)return;
const left=state.cdTarget-Date.now();
if(left<=0){stopCountdown();return}
el.classList.toggle('rep-urgent',left<3600000);
el.classList.toggle('rep-warn',left>=3600000&&left<86400000);
const txt=el.querySelector('.rep-cd-text');
if(txt)txt.innerHTML='Отчёт доступен ещё <strong>'+esc(formatRemaining(left))+'</strong>';
syncCountdown();
}
function startCountdown(expiresAt){
stopCountdown();
const t=toDate(expiresAt);
if(!t)return;
state.cdTarget=t.getTime();
if(state.cdTarget-Date.now()<=0){state.cdTarget=0;return}
if(!state.cdBound){state.cdBound=true;window.addEventListener('scroll',syncCountdown,{passive:true})}
tickCountdown();
state.cdTimer=setInterval(tickCountdown,1000);
}
function initSlider(slider,update){
if(!slider)return;
const track=slider.querySelector('.blocklist__list');
if(!track)return;
const prev=slider.querySelector('[data-slider-prev]');
const next=slider.querySelector('[data-slider-next]');
const step=function(){
const first=track.firstElementChild;
const w=first?first.getBoundingClientRect().width:track.clientWidth;
const visible=Math.max(1,Math.round(track.clientWidth/Math.max(1,w)));
return {width:Math.max(1,w),visible:visible};
};
const sync=function(){
const s=step();
const max=track.scrollWidth-track.clientWidth;
const one=track.scrollWidth<=track.clientWidth+4;
hideEl(prev,one||track.scrollLeft<=4);
hideEl(next,one||track.scrollLeft>=max-4);
};
const move=function(dir){
const s=step();
track.scrollBy({left:dir*s.width*s.visible,behavior:'smooth'});
};
if(prev)prev.addEventListener('click',function(){move(-1)});
if(next)next.addEventListener('click',function(){move(1)});
track.addEventListener('scroll',sync,{passive:true});
window.addEventListener('resize',sync);
sync();
if(update)update(sync);
}
function stepSlider(slider,dir){
const track=slider.querySelector('.blocklist__list');
if(!track)return;
const first=track.firstElementChild;
const w=first?first.getBoundingClientRect().width:track.clientWidth;
const visible=Math.max(1,Math.round(track.clientWidth/Math.max(1,w)));
track.scrollBy({left:dir*w*visible,behavior:'smooth'});
}
function scrollSliderTo(slider,index){
const track=slider.querySelector('.blocklist__list');
if(!track)return;
const child=track.children[index];
if(child)track.scrollTo({left:child.offsetLeft-track.offsetLeft,behavior:'smooth'});
}
function renderSlider(slider,items){
const track=slider.querySelector('.blocklist__list');
if(!track)return;
track.innerHTML=items.join('');
window.requestAnimationFrame(function(){initSlider(slider)});
}
function setNavVisible(name,on){
document.querySelectorAll('[data-nav="'+name+'"]').forEach(function(el){hideEl(el,!on)});
}
function makeNavLinks(){
document.querySelectorAll('.rep-nav .menu__link-lvl1').forEach(function(a){
a.addEventListener('click',function(){closeMenu()});
});
}
function openMenu(){
const panel=document.getElementById('navPanel');
const overlay=document.getElementById('navOverlay');
const close=document.getElementById('navClose');
if(panel)panel.classList.add('is-open');
if(overlay)overlay.classList.add('is-open');
if(close)close.classList.add('is-open');
document.documentElement.style.overflow='hidden';
}
function closeMenu(){
const panel=document.getElementById('navPanel');
const overlay=document.getElementById('navOverlay');
const close=document.getElementById('navClose');
if(panel)panel.classList.remove('is-open');
if(overlay)overlay.classList.remove('is-open');
if(close)close.classList.remove('is-open');
document.documentElement.style.overflow='';
}
function openModal(){const m=document.getElementById('passwordModal');if(m)m.classList.add('is-open');modalError('');const i=document.getElementById('passwordInput');if(i){i.value='';i.focus()}}
function closeModal(){const m=document.getElementById('passwordModal');if(m)m.classList.remove('is-open')}
function modalError(msg){const e=document.getElementById('passwordError');if(!e)return;e.textContent=msg||'';e.style.display=msg?'block':'none'}
function openLightbox(src){const lb=document.getElementById('lightbox');const img=document.getElementById('lightboxImg');if(!lb||!img)return;img.src=src;lb.classList.add('is-open')}
function photoPool(data){
const pool=[];
(data.entries||[]).forEach(function(e){if(e.photo_path)pool.push(e.photo_path)});
(data.photos||[]).forEach(function(p){if(p.photo_path)pool.push(p.photo_path)});
return pool;
}
function fileChips(files){
if(!files||!files.length)return '';
const items=files.map(function(f){
const url=fileUrl(f.token);
const label=baseName(f.name);
if(isImageFile(f.name))return '<a class="rep-chip" href="#" data-img="'+esc(url)+'" title="'+esc(label)+'"><span>'+esc(label)+'</span></a>';
return '<a class="rep-chip" href="'+esc(url)+'" download title="'+esc(label)+'"><span>'+esc(label)+'</span></a>';
}).join('');
return '<div class="rep-files">'+items+'</div>';
}
function hideSection(id){
const el=document.getElementById(id);
if(el)el.style.display='none';
}
function renderHero(data,heading,pool){
const entries=data.entries||[];
setText('heroName',heading);
setText('heroRole',data.group_name?'Ученик группы «'+data.group_name+'»':'Отчёт о проделанной работе');
setText('heroPeriod',yearLabel(data.date_from,data.date_to)||yearLabel(data.created_at));
const lead=(data.message&&String(data.message).trim())||('Здесь собраны все занятия за выбранный период: '+words(entries.length,'запись','записи','записей')+'.');
setText('heroLead',lead);
const box=document.getElementById('heroPhotoBox');
const img=document.getElementById('heroPhoto');
if(box&&img&&pool.length){
img.src=thumbSrc(pool[0]);
img.alt=heading;
box.setAttribute('data-img',pool[0]);
}else if(box){
if(img)img.remove();
const mono=document.createElement('span');
mono.className='rep-monogram';
mono.textContent=initials(heading);
box.appendChild(mono);
box.classList.add('rep-hero-photo');
}
}
function statItem(num,cap,variant){
const sets=[
{item:'list__item list__item--u-ix00fc47p',num:'text text--u-iif5jnrfz text-number-1-script',cap:'text t2 text--u-ih2l1ywk8'},
{item:'list__item list__item--u-ic8gt20s1',num:'text text--u-ipk87vesx text-number-1-script',cap:'text t2 text--u-i53xasxdu'},
{item:'list__item list__item--u-ici0pcuqz',num:'text text--u-ir07nprxn text-number-1-script',cap:'text t2 text--u-ise2s2948'},
{item:'list__item list__item--u-ici0pcuqz list__item--u-irn5njhv8',num:'text text--u-ir07nprxn text-number-1-script',cap:'text t2 text--u-ise2s2948'}
];
const s=sets[variant]||sets[0];
return '<div class="'+s.item+'">'+
'<div class="'+s.num+'"><span class="text-block-wrap-div">'+esc(num)+'</span></div>'+
'<div class="'+s.cap+'"><span class="text-block-wrap-div">'+esc(cap)+'</span></div>'+
'</div>';
}
function statSep(variant){
const extra=variant===1?' list__item--u-ip5wdmggw':(variant===2?' list__item--u-ixebr6v8q':'');
return '<div class="list__item list__item--u-isd3lg3iz'+extra+'"></div>';
}
function renderAbout(data,pool){
const entries=data.entries||[];
const photos=(data.photos||[]).length;
const files=entries.reduce(function(n,e){return n+((e.files||[]).length)},0);
const months={};
entries.forEach(function(e){const d=toDate(e.created_at);if(d)months[d.getFullYear()+'-'+d.getMonth()]=1});
const photosTotal=entries.filter(function(e){return e.photo_path}).length+photos;
setText('aboutTitle',(data.name&&String(data.name).trim())||'Отчёт');
setText('aboutLead',(data.message&&String(data.message).trim())||('Мы собрали '+words(entries.length,'запись','записи','записей')+' о работе за этот период — с фотографиями, файлами и описанием каждого занятия.'));
const parts=[];
if(periodLabel(data.date_from,data.date_to))parts.push('Период: '+periodLabel(data.date_from,data.date_to));
if(data.group_name)parts.push('Группа «'+data.group_name+'»');
setText('aboutNote',parts.join(' · '));
setHTML('statsList',
statItem(entries.length,'записей о проделанной работе',0)+statSep(0)+
statItem(photosTotal,'фото с занятий',1)+statSep(1)+
statItem(files,'проектных файлов',2)+statSep(2)+
statItem(Object.keys(months).length,'месяцев активных занятий',3)
);
const box=document.getElementById('aboutPhotoBox');
const img=document.getElementById('aboutPhoto');
const photo=pool[1]||pool[0];
if(box&&img&&photo){img.src=thumbSrc(photo);img.alt='';box.setAttribute('data-img',photo);}
else if(box){
if(img)img.remove();
const mono=document.createElement('span');
mono.className='rep-about-monogram';
mono.textContent=initials((data.name&&String(data.name).trim())||data.student_name||'WhatIDo');
box.appendChild(mono);
}
}
function renderTimeline(data){
const entries=data.entries||[];
const list=document.getElementById('timelineList');
if(!list)return;
if(!entries.length){hideSection('timeline');setNavVisible('timeline',false);return}
const showDate=data.show_entry_date!==false;
const showDesc=!!data.show_student_message;
list.innerHTML=entries.map(function(e,index){
const files=e.files||[];
const stamp=showDate?fmtShort(e.created_at):pad(index+1);
const aside=[];
if(data.group_name)aside.push(data.group_name);
const counts=countsLabel(e.photo_path?1:0,files.length);
if(counts)aside.push(counts);
return '<div class="list__item list__item--u-ipltrh05x">'+
'<div class="text text--u-ilncqhajz text-number-2"><span class="text-block-wrap-div">'+esc(stamp)+'</span></div>'+
'<div class="div div--u-i26e5rzsk"><div class="div div--u-it0l5f3ro">'+
'<div class="div div--u-izku4a3eb"></div><div class="div div--u-il2x5kjti"></div>'+
(e.photo_path?'<div class="image image--u-i2etuklnt" data-img="'+esc(e.photo_path)+'"><img class="image__img image__img--s2-iyzrya226" src="'+esc(thumbSrc(e.photo_path))+'" alt="" loading="lazy"></div>':'')+
'<div class="div div--u-iaj0y0mvl"></div><div class="div div--u-iu4k5l3n1"></div>'+
'</div></div>'+
'<div class="text text--u-ie3ure5d7 h3 rep-stack">'+
'<span class="text-block-wrap-div">'+esc(e.student_name||'Занятие')+'</span>'+
(showDesc&&e.description?'<div class="text t2 rep-tl-desc"><span class="text-block-wrap-div">'+esc(e.description)+'</span></div>':'')+
fileChips(files)+
'</div>'+
'<div class="div div--u-ib8lera1q rep-tl-aside">'+
aside.map(function(a){return '<div class="text text--u-i8vddz288 t2"><span class="text-block-wrap-div">'+esc(a)+'</span></div>'}).join('')+
'</div>'+
'</div>';
}).join('');
}
function workTitle(e){
const d=String(e.description||'').replace(/\s+/g,' ').trim();
if(d)return d.length>88?d.slice(0,88).trim()+'…':d;
return e.student_name||'Работа';
}
function renderWorks(data){
const slider=document.querySelector('.rep-slider[data-slider="works"]');
if(!slider)return;
const entries=(data.entries||[]).filter(function(e){return e.photo_path});
if(!entries.length){hideSection('works');setNavVisible('works',false);return}
const items=entries.slice(0,12).map(function(e,index){
return '<div class="blocklist__item-wrapper blocklist__item-wrapper--s2-in9zna1lp">'+
'<div class="blocklist__item blocklist__item--u-iqn443glf blocklist__item--s2-iqn443glf">'+
'<div class="div div--u-ifxd1g9rk">'+
'<div class="div div--u-ibren39lw"></div>'+
'<div class="div div--u-ixtm7p8ov"></div>'+
'<div class="text h3 text--u-ib8dkw227"><span class="text-block-wrap-div">'+esc(pad(index+1))+'</span></div>'+
'<div class="image image--u-iijk97g72 rep-work-photo" data-img="'+esc(e.photo_path)+'"><img class="image__img" src="'+esc(thumbSrc(e.photo_path))+'" alt="" loading="lazy"></div>'+
'<div class="div div--u-ilvcq6gdk">'+
'<div class="text text--u-i8ydj7eyx h3-script"><span class="text-block-wrap-div">'+esc(workTitle(e))+'</span></div>'+
'<div class="div div--u-ipp2467j8"><div class="text t2 text--u-iz3df5dls"><span class="text-block-wrap-div">'+esc(data.show_entry_date!==false?fmtDate(e.created_at):(e.student_name||''))+'</span></div></div>'+
'</div>'+
(data.show_student_message&&e.description&&workTitle(e)!==String(e.description).trim()?'<div class="text text--u-iz3df5dls l1 rep-work-desc"><span class="text-block-wrap-div">'+esc(e.description)+'</span></div>':'')+
fileChips(e.files)+
'<div class="div div--u-iidtqgnjo"></div>'+
'<div class="div div--u-iohjzr62g"></div>'+
'</div>'+
'</div>'+
'</div>';
});
renderSlider(slider,items);
}
function renderFiles(data){
const list=document.getElementById('filesList');
if(!list)return;
const rows=[];
(data.entries||[]).forEach(function(e){
(e.files||[]).forEach(function(f){rows.push({file:f,entry:e})});
});
if(!rows.length){hideSection('files');setNavVisible('files',false);return}
list.innerHTML=rows.map(function(row){
const f=row.file;
const url=fileUrl(f.token);
const label=baseName(f.name);
const isImg=isImageFile(f.name);
const meta=[];
if(data.show_entry_date!==false)meta.push(fmtDate(row.entry.created_at));
if(row.entry.student_name)meta.push(row.entry.student_name);
if(data.group_name)meta.push(data.group_name);
const icon=isImg
?'<div class="image image--u-i1bx3pcch"><img class="image__img" src="'+esc(thumbSrc(url))+'" alt="" loading="lazy"></div>'
:'<div class="image image--u-i1bx3pcch rep-file-icon">'+esc(extLabel(f.name))+'</div>';
const head=isImg
?'<a class="list__item list__item--u-iusd5amsk rep-file-item" href="#" data-img="'+esc(url)+'">'
:'<a class="list__item list__item--u-iusd5amsk rep-file-item" href="'+esc(url)+'" download>';
return head+
'<div class="div div--u-i65kawao9"><div class="text t2 text--u-inm34sjkb"><span class="text-block-wrap-div">'+esc(fmtDate(row.entry.created_at))+'</span></div></div>'+
'<div class="div div--u-iqdk5s4vv">'+
'<div class="div div--u-iq1qpzuod"></div>'+
'<div class="div div--u-itqr1x7ye"></div>'+
icon+
'<div class="div div--u-iauar4gpi"></div>'+
'<div class="div div--u-i78hvbbl0"></div>'+
'</div>'+
'<div class="text h3 text--u-ih08rddti rep-file-title"><span class="text-block-wrap-div">'+esc(label)+'</span></div>'+
'<div class="text l1 text--u-i0erozcli rep-stack rep-file-meta">'+
'<span class="text-block-wrap-div">'+esc(meta.join(' · '))+'</span>'+
'<span class="rep-file-cta">'+(isImg?'Посмотреть':'Скачать')+'</span>'+
'</div>'+
'</a>';
}).join('');
}
function renderGallery(data){
const slider=document.querySelector('.rep-slider[data-slider="gallery"]');
if(!slider)return;
const photos=data.photos||[];
if(!photos.length){hideSection('gallery');setNavVisible('gallery',false);return}
const items=photos.slice(0,12).map(function(p){
const caption=String(p.caption||'').trim();
return '<div class="blocklist__item-wrapper blocklist__item-wrapper--s2-iuntcnkpn">'+
'<div class="blocklist__item blocklist__item--u-iefuhgv1p blocklist__item--s2-iefuhgv1p">'+
'<div class="div div--u-iova3e6no">'+
'<div class="div div--u-iy3zasv7f"></div>'+
'<div class="div div--u-ish1rimi9"></div>'+
'<div class="image image--u-ipuomcrxx rep-gallery-photo" data-img="'+esc(p.photo_path)+'"><img class="image__img" src="'+esc(thumbSrc(p.photo_path))+'" alt="" loading="lazy"></div>'+
'<div class="div div--u-ij727gv6j"></div>'+
'<div class="div div--u-iymundgii"></div>'+
'</div>'+
'<div class="div div--u-iq00jif9z">'+
'<div class="text t2 text--u-i6vup9a8s rep-gallery-caption"><span class="text-block-wrap-div">'+esc(caption||(data.group_name?'Группа «'+data.group_name+'»':'Фото занятия'))+'</span></div>'+
'<div class="text t2 text--u-iwrgrsqzk rep-gallery-date"><span class="text-block-wrap-div">'+esc(fmtDate(p.taken_at||p.created_at))+'</span></div>'+
'<div class="text text--u-ig38djg6x l1 rep-gallery-text"><span class="text-block-wrap-div">'+esc(caption?'Фотография с занятий группы':'Фотография с занятий')+'</span></div>'+
'<div class="div div--u-i8lri9goh"></div>'+
'<div class="div div--u-i4kmjxmbi"></div>'+
'</div>'+
'</div>'+
'</div>';
});
renderSlider(slider,items);
}
function renderFooter(data,heading,pool){
setText('brandMark',initials(heading));
setText('brandMarkFooter',initials(heading));
setText('footGroup',data.group_name?'Группа «'+data.group_name+'»':'Группа не указана');
setText('footPeriod',periodLabel(data.date_from,data.date_to)?'Период: '+periodLabel(data.date_from,data.date_to):'Период: весь отчёт');
setText('footCreated','Обновлён: '+fmtDate(data.created_at));
const expires=toDate(data.expires_at);
setText('footExpires',expires?'Доступно до '+fmtDate(data.expires_at):'Срок не ограничен');
const link=document.getElementById('footLink');
if(link&&data.link_url&&/^https?:\/\//i.test(data.link_url)){link.href=data.link_url;link.style.display='inline-flex'}
else if(link){link.style.display='none'}
const note=document.getElementById('contactNote');
if(note)note.innerHTML='<span class="text-block-wrap-div">Если появятся вопросы по отчёту — напишите преподавателю<span style="color:rgba(68,68,68,1)"> — мы ответим в течение 24 часов.</span></span>';
setText('contactLead',(data.name&&String(data.name).trim())||'Отчёт о работе');
const sub=[];
if(periodLabel(data.date_from,data.date_to))sub.push('Период: '+periodLabel(data.date_from,data.date_to));
sub.push('Обновлён: '+fmtDate(data.created_at));
if(expires)sub.push('Доступен до: '+fmtDate(data.expires_at));
setText('contactSub',sub.join('\n'));
const btn=document.getElementById('contactBtn');
if(btn&&data.link_url&&/^https?:\/\//i.test(data.link_url)){
btn.href=data.link_url;
btn.target='_blank';
btn.rel='noopener noreferrer';
const label=btn.querySelector('.text-block-wrap-div');
if(label)label.textContent='Перейти по ссылке';
}
setText('footCopy','WhatIDo © '+new Date().getFullYear());
setText('footBrand',heading);
const fbox=document.getElementById('footerPhotoBox');
const fimg=document.getElementById('footerPhoto');
const photo=pool[2]||pool[0];
if(fbox&&fimg&&photo){fimg.src=thumbSrc(photo);fimg.alt='';fbox.setAttribute('data-img',photo)}
else if(fbox){hideEl(fbox,true)}
}
function renderReport(data){
const heading=(data.name&&String(data.name).trim())||data.student_name||data.group_name||'Отчёт';
document.title=heading;
const pool=photoPool(data);
renderHero(data,heading,pool);
renderAbout(data,pool);
renderTimeline(data);
renderWorks(data);
renderFiles(data);
renderGallery(data);
renderFooter(data,heading,pool);
startCountdown(data.expires_at);
const content=document.getElementById('content');
if(content)content.style.display='block';
}
async function loadReport(){
const token=location.pathname.split('/').filter(Boolean).pop()||'';
state.token=token;
state.password=null;
const result=await fetchShare(token,null);
if(result.ok){
state.showNames=result.data.show_student_names!==false;
renderReport(result.data);
return;
}
if(result.status===401&&result.data.passwordRequired){
openModal();
return;
}
if(result.status===410)showError('Срок действия ссылки истёк.');
else showError('Ссылка не найдена или была удалена.');
setText('brandMark',initials('WhatIDo'));
setText('brandMarkFooter',initials('WhatIDo'));
}
function bindEvents(){
const burger=document.getElementById('navBurger');
const close=document.getElementById('navClose');
const overlay=document.getElementById('navOverlay');
if(burger)burger.addEventListener('click',openMenu);
if(close)close.addEventListener('click',closeMenu);
if(overlay)overlay.addEventListener('click',closeMenu);
document.querySelectorAll('.rep-nav .menu__link-lvl1, .rep-nav .link--u-i2ifdcrc4').forEach(function(a){
a.addEventListener('click',function(){closeMenu()});
});
document.addEventListener('click',function(ev){
const prev=ev.target.closest('[data-slider-prev]');
const next=ev.target.closest('[data-slider-next]');
if(prev||next){
const slider=(prev||next).closest('.rep-slider');
if(slider){ev.preventDefault();stepSlider(slider,prev?-1:1)}
return;
}
const imgEl=ev.target.closest('[data-img]');
if(imgEl){ev.preventDefault();openLightbox(imgEl.getAttribute('data-img'));return}
const lb=document.getElementById('lightbox');
if(lb&&lb.classList.contains('is-open'))closeLightbox();
});
const submit=document.getElementById('passwordSubmitBtn');
const input=document.getElementById('passwordInput');
if(submit)submit.addEventListener('click',async function(){
const value=input?input.value.trim():'';
if(!value)return;
submit.disabled=true;
const result=await fetchShare(state.token,value);
submit.disabled=false;
if(result.ok){
state.password=value;
closeModal();
state.showNames=result.data.show_student_names!==false;
renderReport(result.data);
}else{
modalError(result.status===410?'Срок действия ссылки истёк':'Неверный пароль');
if(input){input.value='';input.focus()}
}
});
if(input)input.addEventListener('keydown',function(ev){
if(ev.key==='Enter'&&submit)submit.click();
});
const modal=document.getElementById('passwordModal');
if(modal)modal.addEventListener('click',function(ev){if(ev.target===modal)closeModal()});
document.addEventListener('keydown',function(ev){
if(ev.key!=='Escape')return;
closeLightbox();
closeMenu();
});
window.addEventListener('resize',function(){
if(window.innerWidth>991)closeMenu();
});
}
bindEvents();
makeNavLinks();
loadReport();
function closeLightbox(){const lb=document.getElementById('lightbox');if(lb)lb.classList.remove('is-open')}
File diff suppressed because one or more lines are too long
+185
View File
@@ -0,0 +1,185 @@
/* WhatIDo - report page layer on top of report.css (design system from the reference layout). */
@font-face{
font-family:"Inter";
font-style:normal;
font-weight:400;
font-display:swap;
src:url("/fonts/inter-400.woff2") format("woff2");
}
@font-face{
font-family:"Inter";
font-style:normal;
font-weight:500;
font-display:swap;
src:url("/fonts/inter-500-cyr.woff2") format("woff2");
unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
@font-face{
font-family:"Inter";
font-style:normal;
font-weight:500;
font-display:swap;
src:url("/fonts/inter-500-lat.woff2") format("woff2");
unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
:root{
--rep-accent:rgb(118,132,255);
--rep-accent-hover:rgb(89,99,185);
--rep-bg:rgb(21,21,21);
--rep-line:rgba(255,255,255,.1);
--rep-line-soft:rgba(255,255,255,.16);
--rep-muted:rgba(174,176,176,1);
--sf-img-0:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMjUnIGhlaWdodD0nMjUnIHZpZXdCb3g9JzAgMCAyNSAyNScgZmlsbD0nbm9uZScgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48cGF0aCBkPSdNMTUuNSA2LjVMOS41IDEyLjVMMTUuNSAxOC41JyBzdHJva2U9JyM5QzlDOUMnIHN0cm9rZS1saW5lY2FwPSdyb3VuZCcgc3Ryb2tlLWxpbmVqb2luPSdyb3VuZCcvPjwvc3ZnPgo=");
--sf-img-1:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMjUnIGhlaWdodD0nMjUnIHZpZXdCb3g9JzAgMCAyNSAyNScgZmlsbD0nbm9uZScgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48cGF0aCBkPSdNOS41IDE4LjVMMTUuNSAxMi41TDkuNSA2LjUnIHN0cm9rZT0nIzlDOUM5Qycgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJy8+PC9zdmc+Cg==");
--sf-img-7:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNS45OTg0IDUuMzkzNDFMMjUuMTkwNiAxNC41ODU2TDI1LjE5MDYgMTQuNTg1NkwyNi42MDQ4IDE1Ljk5OThMMjYuNjA0OCAxNS45OTk4TDI2LjYwNSAxNkwyNS4xOTA3IDE3LjQxNDJMMjUuMTkwNiAxNy40MTRMMTUuOTk4MiAyNi42MDY0TDE0LjU4NCAyNS4xOTIyTDIyLjc4NSAxNi45OTEyTDYuNDk1MjMgMTYuOTkxMkw2LjQ5NTIzIDE1LjAwODRMMjIuNzg0OSAxNS4wMDg0TDE0LjU4NDEgNi44MDc2MkwxNS45OTg0IDUuMzkzNDFaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K");
--sf-img-8:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNi4wMDE2IDI2LjYwNjZMNi44MDk0NCAxNy40MTQ0TDYuODA5NDQgMTcuNDE0NEw1LjM5NTIyIDE2LjAwMDJMNS4zOTUyMyAxNi4wMDAyTDUuMzk1MDQgMTZMNi44MDkyNiAxNC41ODU4TDYuODA5NDQgMTQuNTg2TDE2LjAwMTggNS4zOTM1OUwxNy40MTYgNi44MDc4TDkuMjE0OTkgMTUuMDA4OUwyNS41MDQ4IDE1LjAwODhMMjUuNTA0OCAxNi45OTE2TDkuMjE1MDggMTYuOTkxNkwxNy40MTU5IDI1LjE5MjRMMTYuMDAxNiAyNi42MDY2WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
--sf-img-9:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTEiIHZpZXdCb3g9IjAgMCAxMiAxMSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEuMTk5MjIgNC44NTYzNkw0Ljc5OTIyIDkuNzMyNTVMMTAuNzk5MiAxLjE5OTIyIiBzdHJva2U9IndoaXRlIi8+Cjwvc3ZnPgo=");
}
html{scroll-behavior:smooth}
html,body{background-color:var(--rep-bg)}
body{overflow-x:hidden}
::selection{background-color:var(--rep-accent);color:#fff}
.container--u-i80idakgi{opacity:1!important;transform:none!important}
.rep-hidden{display:none!important}
.rep-static{cursor:default;color:var(--rep-muted)}
[data-img]{cursor:zoom-in}
button,a{-webkit-tap-highlight-color:transparent}
.rep-logo-box{width:auto;min-width:44px;height:40px;align-items:center;justify-content:center;overflow:visible}
.rep-logo{font-size:20px;font-weight:500;letter-spacing:-.05em;line-height:1;color:rgb(21,21,21);white-space:nowrap}
.rep-logo-box--footer{height:20px;min-width:0}
.rep-logo--white{font-size:18px;color:#fff}
.rep-overlay{display:none}
.rep-overlay.is-open{display:block}
@media (min-width:992px){
.rep-burger,.rep-menu-close{display:none!important}
}
@media (max-width:991px){
.rep-menu-panel.is-open{display:flex!important;position:fixed;top:0;right:0;bottom:0;left:0;z-index:85;flex-direction:column;align-items:flex-start;background-color:rgb(255,255,255);padding:20px;overflow-y:auto}
.rep-menu-panel.is-open .menu__scroll-wrapper{display:block!important;width:100%;min-width:0;height:auto}
.rep-menu-panel.is-open .menu__wrapper-lvl1{display:block!important;position:static;background-image:none;background-color:transparent;padding:0}
.rep-menu-panel.is-open .menu__list-lvl1{display:flex!important;flex-direction:column;align-items:flex-start;row-gap:16px;padding:50px 10px 0}
.rep-menu-panel.is-open .menu__link-lvl1{font-size:26px;line-height:110%;padding:0}
.rep-menu-close.is-open{display:block!important;position:absolute;top:22px;right:22px}
.rep-burger{display:block}
}
.rep-hero-photo{position:relative;display:flex;align-items:center;justify-content:center}
.rep-monogram{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:clamp(56px,9vw,140px);font-weight:600;letter-spacing:-.06em;color:rgba(255,255,255,.06)}
.rep-hero-tag{color:rgba(255,255,255,.35)}
.rep-hero-lead{white-space:pre-line;word-break:break-word}
.rep-slider{width:100%;max-width:100%;min-width:0}
.rep-slider .blocklist__list{display:flex;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;scroll-snap-type:x mandatory;scrollbar-width:none;-ms-overflow-style:none}
.rep-slider .blocklist__list::-webkit-scrollbar{display:none;width:0;height:0}
.rep-slider .blocklist__item-wrapper{flex:0 0 100%;max-width:100%;scroll-snap-align:start}
@media (min-width:992px){
.rep-slider .blocklist__item-wrapper{flex:0 0 50%}
}
.rep-slider [data-slider-prev],.rep-slider [data-slider-next]{cursor:pointer}
.rep-slider .rep-arrow-off{opacity:.06!important;pointer-events:none}
.rep-work-photo{width:100%;max-width:280px;height:auto;aspect-ratio:1/1;border-radius:55px}
@media (max-width:991px){
.rep-work-photo{max-width:200px}
}
.rep-work-photo .image__img{width:100%;height:100%;object-fit:cover}
.rep-gallery-photo{width:180px;height:180px;max-width:180px;border-radius:50px}
@media (max-width:991px){
.rep-gallery-photo{width:140px;height:140px;max-width:140px;border-radius:40px}
}
.rep-gallery-photo .image__img{width:100%;height:100%;object-fit:cover}
.rep-gallery-caption{color:#fff}
.rep-gallery-date{color:rgba(56,56,56,1);margin-bottom:auto}
.rep-gallery-text{color:#fff}
.rep-stack{flex-direction:column;align-items:flex-start;row-gap:12px}
.rep-tl-desc{color:rgba(255,255,255,.7);max-width:350px;line-height:130%}
.rep-tl-aside{row-gap:12px}
.rep-tl-count{color:rgba(56,56,56,1)}
.rep-files{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px;max-width:350px}
.rep-chip{display:inline-flex;align-items:center;gap:8px;max-width:280px;padding:9px 18px;border:1px solid var(--rep-line-soft);border-radius:100px;font-size:13px;font-weight:500;letter-spacing:-.05em;line-height:100%;color:rgba(255,255,255,.7);text-decoration:none;transition:border-color .1s linear,color .1s linear}
.rep-chip:hover{border-color:rgba(255,255,255,.65);color:#fff}
.rep-chip span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rep-chip--static{cursor:default}
.rep-chip--static:hover{border-color:var(--rep-line-soft);color:rgba(255,255,255,.7)}
.rep-empty{color:rgba(174,176,176,1);font-size:15px;font-weight:500;letter-spacing:-.05em;padding:20px 0}
.rep-file-item{text-decoration:none;color:inherit;cursor:pointer;transition:background-color .15s linear}
.rep-file-item:hover{background-color:rgb(255,255,255)}
.rep-file-icon{display:flex;align-items:center;justify-content:center;width:180px;height:180px;max-width:180px;border-radius:50px;background-color:rgb(255,255,255);border:1px solid rgba(0,0,0,.06);font-size:20px;font-weight:500;letter-spacing:-.05em;color:rgba(21,21,21,.5);text-transform:uppercase}
@media (max-width:991px){
.rep-file-icon{width:140px;height:140px;max-width:140px;border-radius:40px;font-size:16px}
}
.rep-file-title{color:rgb(21,21,21);overflow-wrap:anywhere}
.rep-file-meta{color:var(--rep-muted)}
.rep-file-cta{display:inline-flex;margin-top:12px;padding:9px 18px;border:1px solid rgba(187,189,190,1);border-radius:100px;font-size:13px;font-weight:500;letter-spacing:-.05em;color:rgba(21,21,21,.75);text-decoration:none}
.rep-file-item:hover .rep-file-cta{border-color:rgb(21,21,21);color:rgb(21,21,21)}
.rep-contact{width:auto;max-width:420px;align-items:flex-start;row-gap:16px}
.rep-contact-title{color:#fff;font-size:24px;font-weight:500;letter-spacing:-.05em;line-height:115%}
.rep-contact-note{color:var(--rep-muted);font-size:15px;font-weight:500;letter-spacing:-.05em;line-height:140%}
.rep-contact-btn{width:100%;height:60px;background-color:var(--rep-accent);border-radius:10px;color:#fff}
.rep-contact-btn:hover{background-color:var(--rep-accent-hover)}
.rep-footer-photo{width:180px;height:180px;max-width:180px;border-radius:55px}
@media (max-width:991px){
.rep-footer-photo{width:140px;height:140px;max-width:140px}
}
.rep-footer-photo .image__img{width:100%;height:100%;object-fit:cover}
.rep-countdown{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);z-index:60;display:inline-flex;align-items:center;gap:8px;max-width:calc(100% - 32px);padding:12px 22px;border:1px solid var(--rep-line-soft);border-radius:100px;background-color:rgba(21,21,21,.94);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);color:rgba(255,255,255,.8);font-size:14px;font-weight:500;letter-spacing:-.05em;line-height:120%;white-space:nowrap;overflow:hidden}
.rep-countdown strong{color:#fff;font-weight:500}
.rep-countdown.rep-warn{border-color:rgba(250,192,0,.45)}
.rep-countdown.rep-urgent{border-color:rgba(255,79,82,.55)}
.rep-error{display:flex;align-items:center;justify-content:center;min-height:70vh;padding:80px 30px}
.rep-error-inner{display:flex;flex-direction:column;align-items:center;row-gap:20px;max-width:540px;text-align:center}
.rep-error-title{color:#fff;font-size:clamp(32px,4vw,50px);font-weight:500;letter-spacing:-.05em;line-height:100%}
.rep-error-text{color:var(--rep-muted);font-size:15px;font-weight:500;letter-spacing:-.05em;line-height:140%}
.rep-error-btn{width:auto;height:60px;padding:0 30px;background-color:var(--rep-accent);border-radius:10px;color:#fff}
.rep-error-btn:hover{background-color:var(--rep-accent-hover)}
.rep-lightbox{position:fixed;top:0;right:0;bottom:0;left:0;z-index:300;display:none;align-items:center;justify-content:center;padding:30px;background-color:rgba(0,0,0,.9);cursor:zoom-out}
.rep-lightbox.is-open{display:flex}
.rep-lightbox img{max-width:100%;max-height:100%;object-fit:contain;border-radius:10px}
.rep-modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:400;display:none;align-items:center;justify-content:center;padding:20px;background-color:rgba(0,0,0,.72);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}
.rep-modal.is-open{display:flex}
.rep-modal-box{display:flex;flex-direction:column;row-gap:16px;width:100%;max-width:420px;padding:35px;border:1px solid rgba(44,44,44,1);border-radius:20px;background-color:var(--rep-bg)}
.rep-modal-title{color:#fff;font-size:24px;font-weight:500;letter-spacing:-.05em;line-height:115%}
.rep-modal-text{color:var(--rep-muted);font-size:15px;font-weight:500;letter-spacing:-.05em;line-height:140%}
.rep-modal-input{width:100%;height:55px;padding:0 18px;border:1px solid var(--rep-line-soft);border-radius:10px;background-color:transparent;color:#fff;font-size:15px;font-weight:500;letter-spacing:-.05em;outline:none}
.rep-modal-input::placeholder{color:rgba(255,255,255,.35)}
.rep-modal-input:focus{border-color:rgba(255,255,255,.5)}
.rep-modal-btn{width:100%;height:55px;background-color:var(--rep-accent);border-radius:10px;color:#fff}
.rep-modal-btn:hover{background-color:var(--rep-accent-hover)}
.rep-modal-error{color:rgb(255,79,82);font-size:14px;font-weight:500;letter-spacing:-.05em}
@media (max-width:640px){
.rep-countdown{bottom:14px;padding:10px 16px;font-size:13px}
.rep-modal-box{padding:25px}
}
.rep-work-desc{color:rgba(255,255,255,.7);max-width:350px;line-height:130%}
.rep-stack>.text-block-wrap-div{flex-grow:0}
.rep-file-meta .text-block-wrap-div{flex-grow:0}
.rep-about-photo{display:block;width:100%;height:100%}
.rep-about-photo .image__img{width:100%;height:100%;object-fit:cover}
.rep-about-monogram{display:flex;align-items:center;justify-content:center;width:100%;height:100%;font-size:64px;font-weight:600;letter-spacing:-.06em;color:rgba(21,21,21,.12)}
#works.section--u-ibq7nxfzo{background-color:rgb(255,255,255)}
#works .text--u-ihb2xbsz6{color:rgba(21,21,21,1)}
#works .blocklist__item--u-iqn443glf{background-color:rgb(255,255,255)}
#works .div--u-ifxd1g9rk{border-top-color:rgba(228,231,232,1);border-right-color:rgba(228,231,232,1);border-bottom-color:rgba(228,231,232,1);border-left-color:rgba(228,231,232,1)}
#works .text--u-ib8dkw227{color:rgba(220,220,220,1)}
#works .text--u-i8ydj7eyx{color:rgba(21,21,21,1)}
#works .div--u-ipp2467j8{border-top-color:rgba(187,189,190,1);border-right-color:rgba(187,189,190,1);border-bottom-color:rgba(187,189,190,1);border-left-color:rgba(187,189,190,1)}
#works .text--u-iz3df5dls{color:rgba(21,21,21,1)}
#works .div--u-ibren39lw,#works .div--u-ixtm7p8ov,#works .div--u-iidtqgnjo,#works .div--u-iohjzr62g{background-color:rgb(21,21,21)}
#works .rep-work-desc{color:rgba(21,21,21,.7)}
#works .rep-chip{border-color:rgb(187,189,190);color:rgba(21,21,21,.75)}
#works .rep-chip:hover{border-color:rgb(21,21,21);color:rgb(21,21,21)}
+281
View File
@@ -0,0 +1,281 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#151515">
<meta name="robots" content="noindex,nofollow">
<title>Отчёт</title>
<link rel="stylesheet" href="/report.css">
<link rel="stylesheet" href="/report.custom.css">
</head>
<body>
<div class="mosaic-wrap">
<div class="root root--u-i8z5xo3zc" id="top">
<section class="section paddings--left-right section--u-i7l3q2nvu rep-nav">
<div class="container container--u-i80idakgi">
<a href="#top" class="link-block link-block--u-ij7yf5mm2">
<div class="image image--u-irhpgss26 rep-logo-box"><span class="rep-logo" id="brandMark">—</span></div>
</a>
<div class="menu menu--u-i9yrdei10 menu--s2-i9yrdei10">
<div class="menu__fixed-wrapper menu__fixed-wrapper--s2-iek5sxvy1 rep-menu-panel" id="navPanel">
<div class="menu__scroll-wrapper menu__scroll-wrapper--u-ihg46fvlv menu__scroll-wrapper--s2-ihg46fvlv">
<div class="menu__wrapper-lvl1 menu__wrapper-lvl1--u-ic1ljomjg menu__wrapper-lvl1--s2-ic1ljomjg">
<div class="menu__list-lvl1 menu__list-lvl1--u-ioyaxt4m1 menu__list-lvl1--s2-ioyaxt4m1">
<div class="menu__item-lvl1 menu__item-lvl1--u-imeieea6o" data-nav="about">
<a href="#about" class="menu__link-lvl1 menu__link-lvl1--u-i6278jyvl">
<div class="menu__link-text menu__link-text--u-i2qgb1ij1"><span class="text-block-wrap-div">Обо мне</span></div>
</a>
</div>
<div class="menu__item-lvl1 menu__item-lvl1--u-imeieea6o" data-nav="timeline">
<a href="#timeline" class="menu__link-lvl1 menu__link-lvl1--u-i6278jyvl">
<div class="menu__link-text menu__link-text--u-i2qgb1ij1"><span class="text-block-wrap-div">Хроника</span></div>
</a>
</div>
<div class="menu__item-lvl1 menu__item-lvl1--u-imeieea6o" data-nav="works">
<a href="#works" class="menu__link-lvl1 menu__link-lvl1--u-i6278jyvl">
<div class="menu__link-text menu__link-text--u-i2qgb1ij1"><span class="text-block-wrap-div">Работы</span></div>
</a>
</div>
<div class="menu__item-lvl1 menu__item-lvl1--u-imeieea6o" data-nav="gallery">
<a href="#gallery" class="menu__link-lvl1 menu__link-lvl1--u-i6278jyvl">
<div class="menu__link-text menu__link-text--u-i2qgb1ij1"><span class="text-block-wrap-div">Фото</span></div>
</a>
</div>
<div class="menu__item-lvl1 menu__item-lvl1--u-imeieea6o" data-nav="files">
<a href="#files" class="menu__link-lvl1 menu__link-lvl1--u-i6278jyvl">
<div class="menu__link-text menu__link-text--u-i2qgb1ij1"><span class="text-block-wrap-div">Файлы</span></div>
</a>
</div>
</div>
</div>
</div>
<div role="button" class="menu__close-button menu__close-button--u-ib386a3r1 menu__close-button--s2-ib386a3r1 rep-menu-close" id="navClose"></div>
</div>
<div role="button" class="menu__burger-button menu__burger-button--u-ibf8kzqjr menu__burger-button--s2-ibf8kzqjr rep-burger" id="navBurger"></div>
<div class="menu__overlay menu__overlay--u-io05iogd9 rep-overlay" id="navOverlay"></div>
</div>
<a href="#contact" class="link text--link-1 link--u-i2ifdcrc4"><span class="text-block-wrap-div">Контакты</span></a>
</div>
</section>
<div id="errorBox" class="rep-error" style="display:none">
<div class="rep-error-inner">
<div class="rep-error-title">Отчёт недоступен</div>
<div class="rep-error-text" id="errorText"></div>
<a href="/" class="button button--u-i4a1jl1f8 text--button rep-error-btn">
<span class="text-button text-button--u-i4t8wl9m2"><span class="text-block-wrap-div">На главную</span></span>
</a>
</div>
</div>
<main id="content" style="display:none">
<section class="section section--u-imrochvid" id="hero">
<div class="text text--u-ihro87s5g h1" id="heroName"><span class="text-block-wrap-div">—</span></div>
<div class="div div--u-iypw3r3hy"></div>
<div class="div div--u-iac2fpjoy paddings--left-right">
<div class="text text--u-if3tqh9j6 t2" id="heroRole"><span class="text-block-wrap-div">—</span></div>
<div class="text text--u-il0q6mzel t2" id="heroPeriod"><span class="text-block-wrap-div">—</span></div>
</div>
<div class="div div--u-ii07pa6dy">
<div class="image image--u-ivhonc1br">
<img class="image__img" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDExIiBoZWlnaHQ9IjQxMiIgdmlld0JveD0iMCAwIDQxMSA0MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxjaXJjbGUgY3g9IjIwNSIgY3k9IjIwNiIgcj0iMjA0LjUiIHN0cm9rZT0iIzIxMjEyMSIvPgo8cmVjdCB3aWR0aD0iNTgxIiBoZWlnaHQ9IjEiIHRyYW5zZm9ybT0icm90YXRlKDQ1KSIgZmlsbD0iIzIxMjEyMSIvPgo8L3N2Zz4KCg==" alt="">
</div>
<div class="div div--u-ivyxhpbts">
<div class="div div--u-inb3arto0">
<div class="div div--u-iloks34xh">
<div class="image image--u-id09xtq63">
<img class="image__img image__img--s2-ipfbguwr1" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCA0MCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAgMEwyMCAyMEgwVjBaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjAgMjBMNDAgLTguNzQyMjhlLTA3TDQwIDIwTDIwIDIwWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cgo=" alt="">
</div>
<div class="text text--u-is1fxv2no l1 rep-hero-lead" id="heroLead"><span class="text-block-wrap-div"></span></div>
<div class="text text--u-ig2k8b0st l1" id="heroTag"><span class="text-block-wrap-div">[Отчёт]</span></div>
<div class="image image--u-i5hwy71ca">
<img class="image__img image__img--s2-icfu2drw3" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCA0MCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQwIDIwTDIwIC0xLjc0ODQ2ZS0wNkw0MCAwTDQwIDIwWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTIwIDBMLTguNzQyMjhlLTA3IDIwTDAgLTguNzQyMjhlLTA3TDIwIDBaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4KCg==" alt="">
</div>
<div class="div div--u-iasmwwdzw"></div>
<div class="div div--u-i9jct2f9m"></div>
<div class="div div--u-is7e9z5nq"></div>
<div class="div div--u-i0k4yxpc8"></div>
</div>
</div>
<div class="image image--u-is0urbrfg rep-hero-photo" id="heroPhotoBox">
<img class="image__img" id="heroPhoto" alt="">
</div>
</div>
<div class="image image--u-i8jhhfeik">
<img class="image__img" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDEwIiBoZWlnaHQ9IjQxMiIgdmlld0JveD0iMCAwIDQxMCA0MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxjaXJjbGUgY3g9IjIwNSIgY3k9IjIwNSIgcj0iMjA0LjUiIHN0cm9rZT0iIzIxMjEyMSIvPgo8cmVjdCB4PSI0MTEiIHk9IjEiIHdpZHRoPSI1ODAiIGhlaWdodD0iMSIgdHJhbnNmb3JtPSJyb3RhdGUoMTM1IDQxMSAxKSIgZmlsbD0iIzIxMjEyMSIvPgo8L3N2Zz4KCg==" alt="">
</div>
</div>
<div class="div div--u-ivqnx6ncb"></div>
<div class="div div--u-ivysvnhdd"></div>
</section>
<section class="section section--u-ije008ww5 paddings--left-right" id="about">
<div class="div div--u-ikil9thol"></div>
<div class="text text--u-ivqsnwblc h2" id="aboutTitle"><span class="text-block-wrap-div">Обо мне</span></div>
<div class="div div--u-isbf1g6de"></div>
<div class="div div--u-iayi3al5a">
<div class="div div--u-i7arpju57"></div>
<div class="div div--u-i6wqm8o03">
<div class="text text--u-iaopidyhq h3" id="aboutLead"><span class="text-block-wrap-div"></span></div>
</div>
<div class="div div--u-isrs3fsi8">
<div class="div div--u-ih80fbo71"></div>
<div class="div div--u-inkx4saax">
<div class="image image--u-iidjge3zd rep-about-photo" id="aboutPhotoBox">
<img class="image__img" id="aboutPhoto" alt="">
</div>
<div class="div div--u-irqublwm9"></div>
<div class="div div--u-ikxc1qjen"></div>
<div class="div div--u-i88rv34ah"></div>
<div class="div div--u-i8l8bxdx6"></div>
</div>
<div class="div div--u-iryjyivpv">
<div class="text text--u-igq5dogo6 t2" id="aboutNote"><span class="text-block-wrap-div"></span></div>
</div>
</div>
<div class="list list--u-i59hjh2ej" id="statsList"></div>
</div>
<a href="#contact" class="button button--u-i4a1jl1f8 text--button">
<span class="text-button text-button--u-i4t8wl9m2"><span class="text-block-wrap-div">Связаться</span></span>
</a>
</section>
<section class="section paddings--left-right section--u-iw83bdcw9" id="timeline">
<div class="text text--u-i6dz2a56i h2"><span class="text-block-wrap-div">Хроника занятий</span></div>
<div class="list list--u-iaxl58kyi" id="timelineList"></div>
</section>
<section class="section section--u-ibq7nxfzo paddings--left-right" id="works">
<div class="text h2 text--u-ihb2xbsz6"><span class="text-block-wrap-div">Работы</span></div>
<div class="div rep-slider" data-slider="works">
<div class="blocklist blocklist--u-ivlioq6ab blocklist--s2-ivlioq6ab">
<div class="blocklist__list-wrapper blocklist__list-wrapper--s2-i4t2ufz23">
<div class="blocklist__list blocklist__list--s2-iou286xss" id="worksTrack"></div>
</div>
<div class="blocklist__controls-wrapper blocklist__controls-wrapper--s2-igcvqnkng">
<div class="blocklist__controls blocklist__controls--u-igz87d7pf">
<div class="blocklist__control-arrows blocklist__control-arrows--u-ihwh9bgqr">
<div role="button" class="blocklist__arrow-prev2 blocklist__arrow-prev2--u-i72uloioe" data-slider-prev></div>
<div role="button" class="blocklist__arrow-next2 blocklist__arrow-next2--u-ivlw85q4f" data-slider-next></div>
</div>
<div class="blocklist__controls-pagination-wrapper"></div>
</div>
</div>
</div>
</div>
</section>
<section class="section paddings--left-right section--u-ip0mjje82" id="files">
<div class="text text--u-i24jt3mcb h2"><span class="text-block-wrap-div">Проектные файлы</span></div>
<div class="list list--u-iix2uh3ie" id="filesList"></div>
</section>
<section class="section section--u-i0y44hvpw paddings--left-right" id="gallery">
<div class="text h2 text--u-ikcqharhc"><span class="text-block-wrap-div">Фото группы</span></div>
<div class="div div--u-ig1bsrybs"></div>
<div class="div div--u-i0kioh3ih"></div>
<div class="div rep-slider" data-slider="gallery">
<div class="blocklist blocklist--u-iohyxn81v blocklist--s2-iohyxn81v">
<div class="blocklist__list-wrapper blocklist__list-wrapper--s2-ijirftpw5">
<div class="blocklist__list blocklist__list--s2-iak70wit9" id="galleryTrack"></div>
</div>
<div class="blocklist__controls-wrapper blocklist__controls-wrapper--s2-ig787iibk">
<div class="blocklist__controls blocklist__controls--u-iau8c9wem">
<div class="blocklist__control-arrows blocklist__control-arrows--u-ihw55qli4">
<div role="button" class="blocklist__arrow-prev2 blocklist__arrow-prev2--u-i8t1smxl7" data-slider-prev></div>
<div role="button" class="blocklist__arrow-next2 blocklist__arrow-next2--u-ixzxe7qta" data-slider-next></div>
</div>
<div class="blocklist__controls-pagination-wrapper"></div>
</div>
</div>
</div>
</div>
</section>
<section class="section section--u-ihbjvn71x" id="contact">
<div class="text text--u-ifdrgto5v h2"><span class="text-block-wrap-div">Связь с нами</span></div>
<div class="div div--u-ivyhwi7sr"></div>
<div class="div div--u-iq5a3c8eo">
<div class="image image--u-ipv2zlpi2">
<img class="image__img" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDExIiBoZWlnaHQ9IjQxMiIgdmlld0JveD0iMCAwIDQxMSA0MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxjaXJjbGUgY3g9IjIwNSIgY3k9IjIwNiIgcj0iMjA0LjUiIHN0cm9rZT0iIzIxMjEyMSIvPgo8cmVjdCB3aWR0aD0iNTgxIiBoZWlnaHQ9IjEiIHRyYW5zZm9ybT0icm90YXRlKDQ1KSIgZmlsbD0iIzIxMjEyMSIvPgo8L3N2Zz4KCg==" alt="">
</div>
<div class="text text--u-ieyliov62 h3" id="contactNote"><span class="text-block-wrap-div"></span></div>
<div class="image image--u-im8xge6of">
<img class="image__img" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDEwIiBoZWlnaHQ9IjQxMiIgdmlld0JveD0iMCAwIDQxMCA0MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxjaXJjbGUgY3g9IjIwNSIgY3k9IjIwNSIgcj0iMjA0LjUiIHN0cm9rZT0iIzIxMjEyMSIvPgo8cmVjdCB4PSI0MTEiIHk9IjEiIHdpZHRoPSI1ODAiIGhlaWdodD0iMSIgdHJhbnNmb3JtPSJyb3RhdGUoMTM1IDQxMSAxKSIgZmlsbD0iIzIxMjEyMSIvPgo8L3N2Zz4KCg==" alt="">
</div>
</div>
<div class="div div--u-ifj6asx15">
<div class="div div--u-i95yztn00"></div>
<div class="div div--u-iqswnal3i"></div>
<div class="image image--u-i9ff2hgvg rep-footer-photo" id="footerPhotoBox">
<img class="image__img" id="footerPhoto" alt="">
</div>
<div class="image image--u-ishqnsp3y">
<img class="image__img" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCA0MCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAgMEwyMCAyMEgwVjBaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjAgMjBMNDAgLTguNzQyMjhlLTA3TDQwIDIwTDIwIDIwWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cgo=" alt="">
</div>
<div class="div div--u-iosm3syjx"></div>
<div class="div div--u-imsajm4rq"></div>
</div>
<div class="div div--u-i0phithov">
<div class="div div--u-i82ybieml">
<a href="#top" class="link-block link-block--u-ic5ntfotz">
<div class="image image--u-iwz9z9ztv rep-logo-box rep-logo-box--footer"><span class="rep-logo rep-logo--white" id="brandMarkFooter">—</span></div>
</a>
<div class="div div--u-iqpqkl8er">
<div class="div div--u-i230b7fin">
<div class="text t2 text--u-iukrw4zbo"><span class="text-block-wrap-div">Меню</span></div>
<a href="#about" class="link text--link-1 link--u-ihn1tumex"><span class="text-block-wrap-div">Обо мне</span></a>
<a href="#timeline" class="link text--link-1 link--u-iw5fg3iez"><span class="text-block-wrap-div">Хроника занятий</span></a>
<a href="#works" class="link text--link-1 link--u-i6sruiw9p" data-nav="works"><span class="text-block-wrap-div">Работы</span></a>
<a href="#files" class="link text--link-1 link--u-ivy8mkxoi" data-nav="files"><span class="text-block-wrap-div">Файлы</span></a>
<a href="#gallery" class="link text--link-1 link--u-i17jh0lsx" data-nav="gallery"><span class="text-block-wrap-div">Фото группы</span></a>
</div>
<div class="div div--u-i3z8k7bsc">
<div class="text t2 text--u-isp07chjo"><span class="text-block-wrap-div">Отчёт</span></div>
<div class="link text--link-1 rep-static"><span class="text-block-wrap-div" id="footGroup"></span></div>
<div class="link text--link-1 rep-static"><span class="text-block-wrap-div" id="footPeriod"></span></div>
<div class="link text--link-1 rep-static"><span class="text-block-wrap-div" id="footCreated"></span></div>
</div>
<div class="div div--u-i94yzqcsn">
<div class="text t2 text--u-ibz851gxs"><span class="text-block-wrap-div">Ссылка</span></div>
<a href="#" class="link text--link-1 link--u-ioxiekbkh" id="footLink" target="_blank" rel="noopener noreferrer" style="display:none"><span class="text-block-wrap-div">Перейти по ссылке</span></a>
<div class="link text--link-1 rep-static"><span class="text-block-wrap-div" id="footExpires"></span></div>
</div>
</div>
</div>
<div class="form form--u-i3ge2e3aw rep-contact">
<div class="rep-contact-title" id="contactLead"></div>
<div class="rep-contact-note" id="contactSub"></div>
<a href="#top" class="button button--u-i4a1jl1f8 text--button rep-contact-btn" id="contactBtn">
<span class="text-button text-button--u-i4t8wl9m2"><span class="text-block-wrap-div">В начало отчёта</span></span>
</a>
</div>
</div>
<div class="div div--u-inush9rz9">
<div class="text t2 text--u-irub9snx4"><span class="text-block-wrap-div" id="footCopy"></span></div>
<div class="link text--link-1 rep-static"><span class="text-block-wrap-div" id="footBrand">WhatIDo</span></div>
</div>
</section>
</main>
</div>
</div>
<div class="rep-countdown" id="countdown" style="display:none"><span class="rep-cd-text"></span></div>
<div class="rep-lightbox" id="lightbox"><img id="lightboxImg" alt=""></div>
<div class="rep-modal" id="passwordModal">
<div class="rep-modal-box">
<div class="rep-modal-title">Ссылка защищена паролем</div>
<div class="rep-modal-text">Введите пароль, чтобы открыть отчёт</div>
<input type="password" id="passwordInput" class="rep-modal-input" placeholder="Пароль" autocomplete="current-password">
<button type="button" id="passwordSubmitBtn" class="button button--u-i4a1jl1f8 text--button rep-modal-btn">
<span class="text-button text-button--u-i4t8wl9m2"><span class="text-block-wrap-div">Войти</span></span>
</button>
<div class="rep-modal-error" id="passwordError" style="display:none">Неверный пароль</div>
</div>
</div>
<script src="/js/report.js"></script>
</body>
</html>
+165 -109
View File
@@ -1911,6 +1911,10 @@ app.get('/s/:token', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'share.html'));
});
app.get('/r/:token', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'report.html'));
});
// --- Groups CRUD ---
app.get('/api/groups', apiLimiter, optionalAuth, async (req, res) => {
const rows = await cacheWrap('groups:list:' + scopeKey(req.user), PUBLIC_TTL_MS, async () => {
@@ -2522,10 +2526,17 @@ function renderStudentReport(data, opts) {
else if (IMG_EXT.has(ext)) { gallery.push({ type: 'image', src: 'files/' + f.saved }); galIdx.set('files/' + f.saved, gallery.length - 1); }
}
const avatar = showPhotos && photos.length ? photos[0].stored : null;
const statsChips = [];
if (showEntries) statsChips.push(`<div class="chip"><b>${entries.length}</b><span>занятий</span></div>`);
if (showPhotos) statsChips.push(`<div class="chip"><b>${photos.length}</b><span>фотографий</span></div>`);
if (showFiles) statsChips.push(`<div class="chip"><b>${files.length}</b><span>файлов</span></div>`);
const plural = (n, one, few, many) => {
const m10 = n % 10, m100 = n % 100;
if (m10 === 1 && m100 !== 11) return one;
if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return few;
return many;
};
const counters = [];
if (showEntries) counters.push(`<div class="counter c1"><b>${entries.length}</b><span>${plural(entries.length, 'занятие', 'занятия', 'занятий')}</span></div>`);
if (showPhotos) counters.push(`<div class="counter c2"><b>${photos.length}</b><span>${plural(photos.length, 'фотография', 'фотографии', 'фотографий')}</span></div>`);
if (showFiles) counters.push(`<div class="counter c3"><b>${files.length}</b><span>${plural(files.length, 'работа', 'работы', 'работ')}</span></div>`);
counters.push(`<div class="counter c4"><b>${groups.length}</b><span>${plural(groups.length, 'группа', 'группы', 'групп')}</span></div>`);
const photoCards = showPhotos ? photos.map(p => {
let caption = '';
if (showCaptions && p.caption) caption = truncate(p.caption, 120);
@@ -2537,19 +2548,22 @@ function renderStudentReport(data, opts) {
${caption ? `<figcaption>${escapeHtml(caption)}</figcaption>` : ''}
</figure>`;
}).join('') : '';
const fileRows = showFiles ? files.map(f => {
const projectCards = showFiles ? files.map((f, i) => {
const ext = f.original.indexOf('.') >= 0 ? f.original.split('.').pop().toUpperCase().slice(0, 8) : 'FILE';
const meta = showDates ? `${fmtLongDate(f.createdAt)} · ${fmtBytes(f.size)}` : fmtBytes(f.size);
const fg = galIdx.get('files/' + f.saved);
const gattr = fg !== undefined ? ` class="gfile" data-g="${fg}"` : ' target="_blank" rel="noopener"';
const gattr = fg !== undefined ? ` data-g="${fg}"` : ' target="_blank" rel="noopener"';
return `
<li>
<a href="files/${escapeHtml(f.saved)}"${gattr}>
<span class="badge">${escapeHtml(ext)}</span>
<span class="fname">${escapeHtml(f.original)}</span>
<span class="fmeta">${escapeHtml(meta)}</span>
<article class="proj">
<a class="proj-link${fg !== undefined ? ' gfile' : ''}" href="files/${escapeHtml(f.saved)}"${gattr}>
<span class="pnum">${String(i + 1).padStart(2, '0')}</span>
<span class="pbody">
<span class="pname">${escapeHtml(f.original)}</span>
<span class="pmeta"><span class="badge">${escapeHtml(ext)}</span><span class="pdate">${escapeHtml(meta)}</span></span>
</span>
<span class="parrow">&#8599;</span>
</a>
</li>`;
</article>`;
}).join('') : '';
const photosByEntry = new Map();
for (const p of photos) {
@@ -2564,13 +2578,15 @@ function renderStudentReport(data, opts) {
const desc = e.description ? `<p class="ldesc">${escapeHtml(e.description)}</p>` : '';
const gr = e.group_name ? `<span class="lgroup">${escapeHtml(e.group_name)}</span>` : '';
const dt = showDates && e.created_at ? `<span class="ldate">${escapeHtml(fmtLongDate(e.created_at))}</span>` : '';
const head = dt + gr;
return `
<article class="lesson">
${head ? `<div class="lhead">${head}</div>` : ''}
${desc}
${thumbs ? `<div class="lthumbs">${thumbs}</div>` : ''}
${fls}
${dt ? `<div class="ldate">${escapeHtml(fmtLongDate(e.created_at))}</div>` : ''}
<div class="lmain">
${gr ? `<span class="lgroup">${escapeHtml(e.group_name)}</span>` : ''}
${desc}
${thumbs ? `<div class="lthumbs">${thumbs}</div>` : ''}
${fls}
</div>
</article>`;
}).join('') : '';
const groupLine = groups.length ? escapeHtml(groups.join(' · ')) : '';
@@ -2581,10 +2597,11 @@ function renderStudentReport(data, opts) {
metaBits.push(`Сформировано ${genLabel}`);
const heroAvatar = avatar ? `<div class="avatar"><img src="photos/${escapeHtml(avatar)}" alt="${escapeHtml(name)}"></div>` : '';
const navItems = [];
navItems.push('<a class="navlink" href="#overview">Обзор</a>');
if (showPhotos) navItems.push('<a class="navlink" href="#photos">Фотографии</a>');
if (showFiles) navItems.push('<a class="navlink" href="#files">Работы</a>');
if (showFiles) navItems.push('<a class="navlink" href="#works">Работы</a>');
if (showEntries) navItems.push('<a class="navlink" href="#lessons">Занятия</a>');
const nav = navItems.length >= 2 ? '<nav class="nav" id="topNav">' + navItems.join('') + '</nav>' : '';
const nav = navItems.join('');
return `<!DOCTYPE html>
<html lang="ru">
<head>
@@ -2592,129 +2609,160 @@ function renderStudentReport(data, opts) {
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${escapeHtml(name)} — портфолио</title>
<style>
:root{--cream:#FFF7EC;--ink:#2B2B33;--muted:#98919B;--yellow:#F7C90E;--teal:#14B8A6;--violet:#8B5CF6;--coral:#FF6B6B;--sky:#38BDF8;--line:#FFEAD2}
:root{--bg:#0c0c0c;--bg2:#141414;--ink:#ffffff;--muted:#8a8a8a;--line:#232323;--accent:#3290ff}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{font-family:"Segoe UI",system-ui,-apple-system,Roboto,"Helvetica Neue",Arial,sans-serif;background:var(--cream);color:var(--ink);line-height:1.55}
body::before{content:"";position:fixed;inset:0;z-index:-1;background:radial-gradient(520px 400px at 8% -2%,rgba(247,201,14,.18),transparent 62%),radial-gradient(620px 430px at 100% 3%,rgba(20,184,166,.15),transparent 58%)}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 58px}
body{font-family:Inter,"Inter Tight","Segoe UI",system-ui,-apple-system,Roboto,"Helvetica Neue",Arial,sans-serif;background:var(--bg);color:var(--ink);line-height:1.45;-webkit-font-smoothing:antialiased}
.wrap{max-width:1080px;margin:0 auto;padding:0 32px}
.nav{position:sticky;top:12px;z-index:40;display:flex;justify-content:center;gap:8px;flex-wrap:wrap;background:rgba(255,255,255,.92);border:1px solid #F1E2C6;border-radius:999px;padding:8px 14px;box-shadow:0 12px 26px -16px rgba(20,30,50,.35);margin-top:26px}
.nav .navlink{text-decoration:none;color:var(--ink);background:#fff;border:1px solid #F1E2C6;border-radius:999px;padding:8px 18px;font-size:.9rem;font-weight:600;transition:all .15s}
.nav .navlink:hover{transform:translateY(-1px)}
.nav .navlink.on{background:var(--yellow);color:#231f12;border-color:transparent;box-shadow:0 4px 12px -4px rgba(247,201,14,.5)}
.topnav{position:sticky;top:0;z-index:40;display:flex;justify-content:space-between;align-items:center;gap:24px;background:rgba(12,12,12,.85);backdrop-filter:blur(10px);border-bottom:1px solid var(--line);padding:18px 32px}
.topnav .brand{font-size:.8rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;text-decoration:none;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.topnav .brand::before{content:"";display:inline-block;width:10px;height:10px;border-radius:50%;background:var(--accent);margin-right:10px}
.topnav .links{display:flex;gap:26px;flex-wrap:wrap;justify-content:flex-end}
.topnav .navlink{text-decoration:none;color:var(--muted);font-size:.78rem;font-weight:500;letter-spacing:.1em;text-transform:uppercase;position:relative;padding-bottom:4px;transition:color .15s}
.topnav .navlink:hover{color:var(--ink)}
.topnav .navlink.on{color:var(--ink)}
.topnav .navlink.on::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;background:var(--accent)}
.hero{overflow:hidden;border-radius:34px;padding:36px 34px;background:linear-gradient(135deg,#FFF6DB,#FFEDBF 52%,#FFE19A);box-shadow:0 24px 50px -24px rgba(247,201,14,.5);position:relative}
.hero .deco{position:absolute;border-radius:50%;background:rgba(255,255,255,.55);pointer-events:none}
.hero .deco.d1{width:150px;height:150px;right:-40px;top:-46px}
.hero .deco.d2{width:80px;height:80px;left:40%;bottom:-30px;background:rgba(20,184,166,.14)}
.hero .deco.d3{width:44px;height:44px;left:-14px;top:30%}
.avatar-col{position:relative;display:flex;gap:24px;align-items:center}
.avatar{flex:0 0 auto;width:104px;height:104px;border-radius:50%;overflow:hidden;border:5px solid #fff;background:#fff;box-shadow:0 10px 24px -6px rgba(0,0,0,.26);transform:rotate(-4deg)}
.avatar img{width:100%;height:100%;object-fit:cover;display:block;border-radius:50%}
.avatar.aemp{display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#F7C90E,#FFB34A);font-size:2.4rem;transform:rotate(-4deg)}
.kicker{font-size:.82rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#B07A00;margin-bottom:6px}
h1{font-size:clamp(1.65rem,4.4vw,2.35rem);font-weight:800;letter-spacing:-.02em;line-height:1.12;margin-bottom:6px}
.meta{font-size:.95rem;color:#7A6A33;display:flex;flex-wrap:wrap;gap:6px;margin-bottom:18px}
.stats{display:flex;gap:12px;flex-wrap:wrap}
.chip{background:#fff;border:1px solid #F3DFC0;border-radius:18px;padding:10px 16px;display:flex;flex-direction:column;min-width:92px;box-shadow:0 8px 18px -12px rgba(20,30,50,.35)}
.chip b{font-size:1.5rem;line-height:1.05}
.chip span{font-size:.78rem;color:var(--muted)}
.stats .chip:nth-child(1) b{color:var(--coral)}
.stats .chip:nth-child(2) b{color:var(--teal)}
.stats .chip:nth-child(3) b{color:var(--violet)}
section{background:#fff;border:1px solid var(--line);border-radius:30px;padding:26px 26px 28px;margin-top:26px;box-shadow:0 16px 34px -22px rgba(20,30,50,.4)}
.p-sec{border-top:6px solid var(--teal)}
.f-sec{border-top:6px solid var(--violet)}
.l-sec{border-top:6px solid var(--coral)}
h2{font-size:1.25rem;font-weight:800;display:flex;align-items:center;gap:10px;margin-bottom:18px}
h2 .sico{width:38px;height:38px;border-radius:12px;display:inline-flex;align-items:center;justify-content:center;font-size:1.25rem;flex:0 0 auto}
.p-sec h2 .sico{background:#E3FAF7}
.f-sec h2 .sico{background:#EFE9FF}
.l-sec h2 .sico{background:#FFE9EE}
.empty{color:var(--muted);border:2px dashed #F0DDBE;border-radius:20px;padding:24px 18px;text-align:center;font-size:.95rem}
.hero{padding:88px 0 72px;border-bottom:1px solid var(--line)}
.hero-head{display:flex;align-items:center;gap:28px}
.avatar{flex:0 0 auto;width:88px;height:88px;border-radius:50%;overflow:hidden;border:1px solid var(--line);background:var(--bg2)}
.avatar img{width:100%;height:100%;object-fit:cover;display:block}
.avatar.aemp{display:flex;align-items:center;justify-content:center;background:var(--accent);color:#fff;font-size:2rem}
.kicker{font-size:.78rem;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);display:flex;align-items:center;gap:12px;margin-bottom:14px}
.kicker::before{content:"[";color:var(--accent);font-weight:700}
.kicker::after{content:"]";color:var(--accent);font-weight:700}
h1{font-size:clamp(2.6rem,6.5vw,4.4rem);font-weight:800;letter-spacing:-.04em;line-height:1.02}
.meta{font-size:.92rem;color:var(--muted);margin-top:12px}
.cta-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:36px}
.btn{display:inline-block;border-radius:999px;padding:13px 28px;font-size:.92rem;font-weight:600;letter-spacing:-.01em;text-decoration:none;transition:opacity .15s,background .15s,color .15s;border:1px solid transparent}
.btn-solid{background:var(--accent);color:#fff}
.btn-solid:hover{opacity:.85}
.btn-line{border-color:var(--line);color:var(--ink)}
.btn-line:hover{border-color:#3a3a3a;background:var(--bg2)}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}
.ph{background:#fff;border-radius:20px;overflow:hidden;cursor:zoom-in;border:3px solid #fff;box-shadow:0 10px 22px -14px rgba(20,30,50,.4);transition:transform .16s ease,box-shadow .16s ease}
.ph:hover{transform:translateY(-4px) rotate(-1deg);box-shadow:0 18px 30px -16px rgba(20,30,50,.45)}
.ph img{width:100%;aspect-ratio:4/3;object-fit:cover;display:block}
.ph figcaption{background:rgba(255,247,236,.92);padding:9px 12px;font-size:.82rem;color:#6A6155;line-height:1.35}
.sec{padding:72px 0;border-bottom:1px solid var(--line)}
.sec-head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:36px}
.sec-head h2{font-size:clamp(1.6rem,3.2vw,2.2rem);font-weight:700;letter-spacing:-.03em;line-height:1.05}
.sec-head .count{color:var(--muted);font-size:.9rem;font-variant-numeric:tabular-nums;white-space:nowrap}
.sec-head .count::before{content:"[";color:var(--accent)}
.sec-head .count::after{content:"]";color:var(--accent)}
.note{color:var(--muted);font-size:.95rem}
.filelist{list-style:none;display:flex;flex-direction:column;gap:9px}
.filelist li a{display:flex;align-items:center;gap:14px;background:#FDFAF4;border:1px solid #F1E2C6;border-radius:18px;padding:12px 16px;text-decoration:none;color:var(--ink);transition:transform .15s,box-shadow .15s}
.filelist li a:hover{transform:translateX(4px);box-shadow:0 10px 20px -12px rgba(20,30,50,.35)}
.badge{flex:0 0 auto;min-width:52px;text-align:center;font-size:.68rem;font-weight:800;letter-spacing:.04em;padding:6px 9px;border-radius:10px;background:linear-gradient(135deg,#F7C90E,#FFB34A);color:#231f12}
.fname{flex:1;min-width:0;font-weight:700;word-break:break-word}
.fmeta{flex:0 0 auto;color:var(--muted);font-size:.8rem;white-space:nowrap}
.counters{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:24px}
.counter{padding:10px 26px;border-left:1px solid var(--line)}
.counter:first-child{border-left:none;padding-left:0}
.counter b{display:block;font-size:clamp(2.8rem,5.5vw,4.2rem);font-weight:800;letter-spacing:-.04em;line-height:1;font-variant-numeric:tabular-nums;color:var(--accent)}
.counter span{display:block;margin-top:12px;font-size:.85rem;color:var(--muted)}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:16px}
.ph{cursor:zoom-in}
.ph img{width:100%;aspect-ratio:4/3;object-fit:cover;display:block;border-radius:16px;border:1px solid var(--line);transition:transform .3s}
.ph:hover img{transform:scale(1.02)}
.ph figcaption{padding:10px 2px 0;font-size:.85rem;color:var(--muted)}
.projects{display:flex;flex-direction:column}
.proj{border-top:1px solid var(--line)}
.proj:last-child{border-bottom:1px solid var(--line)}
.proj-link{display:grid;grid-template-columns:80px 1fr auto auto;gap:22px;align-items:center;padding:26px 2px;text-decoration:none;color:var(--ink)}
.pnum{font-size:2rem;font-weight:800;letter-spacing:-.04em;line-height:1;color:#2b2b2b;font-variant-numeric:tabular-nums;transition:color .15s}
.pbody{min-width:0;display:flex;flex-direction:column;gap:6px}
.pname{font-size:1.2rem;font-weight:600;letter-spacing:-.02em;word-break:break-word;transition:color .15s}
.pmeta{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.badge{flex:0 0 auto;font-size:.66rem;font-weight:600;letter-spacing:.08em;border:1px solid var(--line);border-radius:999px;padding:4px 12px;color:var(--muted)}
.pdate{color:var(--muted);font-size:.85rem;white-space:nowrap}
.parrow{color:var(--muted);font-size:1.05rem;transition:transform .15s,color .15s}
.proj-link:hover .pnum{color:var(--accent)}
.proj-link:hover .pname{color:var(--accent)}
.proj-link:hover .parrow{transform:translate(2px,-2px);color:var(--accent)}
.gfile{cursor:pointer}
.lessons{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px;align-items:stretch}
.lesson{background:#FFFCF6;border:1px solid #F3E4CA;border-left:6px solid var(--coral);border-radius:20px;padding:16px 20px}
.lhead{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:8px}
.ldate{background:var(--coral);color:#fff;border-radius:999px;padding:3px 12px;font-size:.74rem;font-weight:700}
.lgroup{background:rgba(20,184,166,.14);color:#0C9488;font-weight:700;border-radius:999px;padding:3px 12px;font-size:.74rem}
.ldesc{white-space:pre-wrap;color:#3A3733;font-size:.95rem}
.lthumbs{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
.tph{width:80px;height:80px;object-fit:cover;border-radius:14px;cursor:zoom-in;border:2px solid #fff;box-shadow:0 6px 12px -8px rgba(20,30,50,.35)}
.lfiles{margin-top:10px;display:flex;flex-wrap:wrap;gap:8px}
.lfiles a{font-size:.8rem;color:#0C9488;background:rgba(20,184,166,.1);border-radius:999px;padding:5px 12px;text-decoration:none;word-break:break-word}
.lfiles a:hover{background:rgba(20,184,166,.18)}
.lessons{display:flex;flex-direction:column}
.lesson{display:grid;grid-template-columns:150px 1fr;gap:26px;padding:28px 2px;border-top:1px solid var(--line)}
.lesson:last-child{border-bottom:1px solid var(--line)}
.ldate{color:var(--accent);font-size:.85rem;font-weight:600;font-variant-numeric:tabular-nums;padding-top:4px}
.lmain{min-width:0}
.lgroup{display:inline-block;font-size:.7rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--accent);margin-bottom:10px}
.ldesc{white-space:pre-wrap;font-size:1rem;letter-spacing:-.01em;max-width:660px;color:#b5b5b5}
.lthumbs{display:flex;gap:10px;margin-top:16px;flex-wrap:wrap}
.tph{width:92px;height:92px;object-fit:cover;border-radius:14px;border:1px solid var(--line);cursor:zoom-in;transition:transform .2s}
.tph:hover{transform:scale(1.04)}
.lfiles{margin-top:14px;display:flex;flex-wrap:wrap;gap:10px}
.lfiles a{font-size:.88rem;color:#cfcfcf;text-decoration:none;border:1px solid var(--line);border-radius:12px;padding:8px 14px;word-break:break-word;transition:border-color .15s}
.lfiles a:hover{border-color:var(--accent);color:#fff}
footer{margin-top:44px;color:#B9B4BE;font-size:.82rem;text-align:center}
.lightbox{position:fixed;inset:0;background:rgba(22,20,40,.9);display:none;flex-direction:column;z-index:60;padding:16px 92px 48px;cursor:zoom-out;overflow:hidden}
.totop{position:fixed;right:22px;bottom:22px;z-index:45;width:46px;height:46px;border-radius:50%;border:1px solid var(--line);background:var(--bg2);color:var(--ink);font-size:1.1rem;cursor:pointer;opacity:0;pointer-events:none;transition:opacity .2s,background .15s}
.totop.show{opacity:1;pointer-events:auto}
.totop:hover{background:var(--accent);border-color:var(--accent);color:#fff}
footer{padding:36px 0 30px;display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;color:var(--muted);font-size:.85rem}
.lightbox{position:fixed;inset:0;background:rgba(0,0,0,.94);display:none;flex-direction:column;z-index:60;padding:16px 92px 48px;cursor:zoom-out;overflow:hidden}
.lightbox.open{display:flex}
.lightbox .lb-media{display:flex;align-items:center;justify-content:center;flex:1;min-height:0}
.lightbox .lb-media img,.lightbox .lb-media video{display:block;max-width:100%;max-height:100%;object-fit:contain;border-radius:14px}
.lightbox .lb-media video{background:#000;max-height:calc(100% - 16px)}
.lightbox .lb-btn{position:absolute;top:50%;transform:translateY(-50%);z-index:5;width:52px;height:52px;border-radius:50%;border:none;background:rgba(247,201,14,.94);color:#231f12;font-size:1.7rem;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 10px 22px -8px rgba(0,0,0,.5)}
.lightbox .lb-btn:hover{background:#F7C90E}
.lightbox .lb-btn{position:absolute;top:50%;transform:translateY(-50%);z-index:5;width:52px;height:52px;border-radius:50%;border:1px solid var(--line);background:var(--bg2);color:#fff;font-size:1.7rem;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s}
.lightbox .lb-btn:hover{background:var(--accent);border-color:var(--accent)}
.lightbox .lb-prev{left:20px}
.lightbox .lb-next{right:20px}
.lightbox .lb-count{position:absolute;bottom:16px;left:50%;transform:translateX(-50%);background:rgba(22,20,40,.72);color:#fff;border-radius:999px;padding:6px 16px;font-size:.85rem;letter-spacing:.03em}
.lightbox .lb-close{position:absolute;top:16px;right:20px;z-index:6;width:44px;height:44px;border-radius:50%;border:none;background:rgba(22,20,40,.6);color:#fff;font-size:1.35rem;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 8px 18px -8px rgba(0,0,0,.5)}
.lightbox .lb-close:hover{background:rgba(247,201,14,.92);color:#231f12}
.lightbox .lb-count{position:absolute;bottom:16px;left:50%;transform:translateX(-50%);background:rgba(20,20,20,.85);color:#fff;border:1px solid var(--line);border-radius:999px;padding:6px 16px;font-size:.85rem;letter-spacing:.03em}
.lightbox .lb-close{position:absolute;top:16px;right:20px;z-index:6;width:44px;height:44px;border-radius:50%;border:1px solid var(--line);background:rgba(20,20,20,.8);color:#fff;font-size:1.35rem;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s}
.lightbox .lb-close:hover{background:var(--accent);border-color:var(--accent)}
@media print{.topnav{display:none}.cta-row{display:none}.totop{display:none}.wrap{max-width:none;padding:0}body{background:#fff;color:#111}.sec,.proj,.lesson{break-inside:avoid}}
@media (max-width:900px){.lesson{grid-template-columns:1fr;gap:8px;padding:24px 0}.proj-link{grid-template-columns:56px 1fr auto auto;gap:14px}.pdate{display:none}.hero-head{flex-direction:column;align-items:flex-start;gap:18px}.counters{grid-template-columns:repeat(2,1fr)}.counter{padding:14px 0 4px 16px}.counter:nth-child(odd){border-left:none;padding-left:0}.counter b{font-size:2.4rem}}
@media (max-width:560px){.topnav{padding:14px 18px}.topnav .links{gap:14px}.wrap{padding:0 18px}.hero{padding:44px 0 40px}.proj-link{grid-template-columns:44px 1fr auto;padding:18px 0}.pnum{font-size:1.2rem}.pname{font-size:1rem}.grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr))}.lightbox{padding:8px 6px 44px}.lb-prev{left:6px}.lb-next{right:6px}.lb-btn{width:44px;height:44px;font-size:1.4rem}.lb-close{width:38px;height:38px;top:10px;right:10px;font-size:1.15rem}}
@media print{body::before{display:none}.nav{display:none}.wrap{max-width:none;padding:0;-webkit-print-color-adjust:exact}body{background:#fff}.hero,section{box-shadow:none}section,.lesson,.ph{break-inside:avoid}}
@media (max-width:560px){.avatar-col{flex-direction:column;align-items:flex-start;gap:16px}.hero{padding:26px 22px}.grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr))}.stats{gap:8px}.fmeta{display:none}.chip{min-width:74px;padding:8px 12px}.lessons{grid-template-columns:1fr}.lightbox{padding:8px 6px 44px}.lb-prev{left:6px}.lb-next{right:6px}.lb-btn{width:44px;height:44px;font-size:1.4rem}.lb-close{width:38px;height:38px;top:10px;right:10px;font-size:1.15rem}}
</style>
</head>
<body>
<div class="wrap">
<header class="hero">
<span class="deco d1"></span>
<span class="deco d2"></span>
<span class="deco d3"></span>
<div class="avatar-col">
<nav class="topnav" id="topNav">
<a class="brand" href="#top">✦ ${escapeHtml(name)}</a>
<div class="links">${nav}</div>
</nav>
<div class="wrap" id="top">
<header class="hero" id="overview">
<div class="hero-head">
${heroAvatar || '<div class="avatar aemp"><span>🎨</span></div>'}
<div>
<p class="kicker">✦ Портфолио ученика</p>
<p class="kicker">Портфолио ученика</p>
<h1>${escapeHtml(name)}</h1>
<p class="meta">${metaBits.join(' · ')}</p>
<div class="stats">
${statsChips.join('')}
</div>
</div>
</div>
<div class="cta-row">
${showFiles ? '<a class="btn btn-solid" href="#works">Смотреть работы</a>' : ''}
${showEntries ? '<a class="btn btn-line" href="#lessons">Хронология занятий</a>' : ''}
</div>
</header>
${nav}
<section id="overview-stats" class="sec">
<div class="sec-head"><h2>Обзор</h2></div>
<div class="counters">
${counters.join('')}
</div>
</section>
${showPhotos ? `<section id="photos" class="p-sec">
<h2><span class="sico">🏞️</span>Фотографии</h2>
${photoCards ? `<div class="grid">${photoCards}</div>` : '<p class="empty">🎈 Фотографий пока нет</p>'}
${showPhotos ? `<section id="photos" class="sec">
<div class="sec-head"><h2>Фотографии</h2><span class="count">[${photos.length}]</span></div>
${photoCards ? `<div class="grid">${photoCards}</div>` : '<p class="note">Фотографий пока нет.</p>'}
</section>` : ''}
${showFiles ? `<section id="files" class="f-sec">
<h2><span class="sico">📁</span>Работы и файлы</h2>
${fileRows ? `<ul class="filelist">${fileRows}</ul>` : '<p class="empty">🎈 Файлов пока нет</p>'}
${showFiles ? `<section id="works" class="sec">
<div class="sec-head"><h2>Проекты и работы</h2><span class="count">[${files.length}]</span></div>
${projectCards ? `<div class="projects">${projectCards}</div>` : '<p class="note">Работ пока нет.</p>'}
</section>` : ''}
${showEntries ? `<section id="lessons" class="l-sec">
<h2><span class="sico">🎒</span>Журнал занятий</h2>
${lessonRows ? `<div class="lessons">${lessonRows}</div>` : '<p class="empty">🎈 Записей о занятиях пока нет</p>'}
${showEntries ? `<section id="lessons" class="sec">
<div class="sec-head"><h2>Журнал занятий</h2><span class="count">[${entries.length}]</span></div>
${lessonRows ? `<div class="lessons">${lessonRows}</div>` : '<p class="note">Записей о занятиях пока нет.</p>'}
</section>` : ''}
<footer>Сформировано ${genLabel} · WhatIDo</footer>
<footer>
<span>Сформировано ${genLabel}</span>
<span>WhatIDo · Портфолио</span>
</footer>
</div>
<div class="lightbox" id="lightbox">
<button type="button" class="lb-btn lb-prev" id="lbPrev" aria-label="Назад">&#10094;</button>
@@ -2782,6 +2830,14 @@ footer{margin-top:44px;color:#B9B4BE;font-size:.82rem;text-align:center}
}
if (links.length) window.addEventListener('scroll', spy, { passive: true });
spy();
var toTop = document.getElementById('toTop');
function toTopSpy() {
if (window.scrollY > 420) toTop.classList.add('show');
else toTop.classList.remove('show');
}
toTop.addEventListener('click', function () { window.scrollTo({ top: 0, behavior: 'smooth' }); });
window.addEventListener('scroll', toTopSpy, { passive: true });
toTopSpy();
})();
</script>
</body>
@@ -4561,7 +4617,7 @@ app.delete('/api/trash', requireAuth, requireAdmin, async (req, res) => {
const ERROR_HTML = fs.readFileSync(path.join(__dirname, 'public', 'error.html'), 'utf8');
function isApiRoute(req) {
return req.path.startsWith('/api/') || req.path.startsWith('/s/');
return req.path.startsWith('/api/') || req.path.startsWith('/s/') || req.path.startsWith('/r/');
}
function escapeHtml(str) {