const state={token:'',password:null,showNames:true,cdTarget:0,cdTimer:null,cdBound:false,systemName:'WhatIDo'};
function loadBrand(){
return fetch('/api/public-settings').then(function(res){
return res.ok?res.json():null;
}).then(function(s){
if(!s)return;
if((s.system_name||'').trim())state.systemName=s.system_name;
var logo=document.getElementById('brandLogo');
if(logo&&s.system_logo){
logo.src=s.system_logo;
logo.hidden=false;
var mark=document.getElementById('brandMark');
if(mark)mark.style.display='none';
}
var name=document.getElementById('brandName');
if(name&&(s.system_name||'').trim()){
name.textContent=s.system_name;
name.hidden=false;
}
}).catch(function(){});
}
function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''')}
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 dtIsDateOnly(v){return typeof v==='string'&&/^\d{4}-\d{2}-\d{2}/.test(v)}
function fmtDate(v){return dtIsDateOnly(v)?fmtDateOnlyIso(v):(v?fmtDateFull(v):'')}
function fmtShort(v){return dtIsDateOnly(v)?fmtShortIso(v):(v?fmtDateShort(v):'')}
function fmtLong(v){return dtIsDateOnly(v)?fmtDateOnlyLongIso(v):(v?fmtDateLong(v):'')}
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 '';
if(from&&to)return fmtDate(from)+' — '+fmtDate(to);
return fmtDate(from)||fmtDate(to);
}
function dtIsoYear(v){
if(!v)return null;
if(dtIsDateOnly(v))return String(v).slice(0,4);
const d=toDate(v);
if(!d)return null;
try{return new Intl.DateTimeFormat('en-CA',{timeZone:DateTime.tz,year:'numeric'}).format(d).slice(0,4)}catch{return String(d.getFullYear())}
}
function yearLabel(from,to){
if(!from&&!to)return '';
const ay=dtIsoYear(from),by=dtIsoYear(to);
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='Отчёт доступен ещё '+esc(formatRemaining(left))+'';
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 ''+esc(label)+'';
return ''+esc(label)+'';
}).join('');
return '
'+items+'
';
}
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 ''+
'
'+esc(num)+'
'+
'
'+esc(cap)+'
'+
'
';
}
function statSep(variant){
const extra=variant===1?' list__item--u-ip5wdmggw':(variant===2?' list__item--u-ixebr6v8q':'');
return '';
}
function dtYearMonth(d){
try{return new Intl.DateTimeFormat('en-CA',{timeZone:DateTime.tz,year:'numeric',month:'2-digit'}).format(d).slice(0,7)}catch{return d.getFullYear()+'-'+d.getMonth()}
}
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[dtYearMonth(d)]=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 ''+
'
'+esc(stamp)+'
'+
'
'+
'
'+
(e.photo_path?'
':'')+
'
'+
'
'+
'
'+
'
'+esc(e.student_name||'Занятие')+''+
(showDesc&&e.description?'
'+esc(e.description)+'
':'')+
fileChips(files)+
'
'+
'
'+
aside.map(function(a){return '
'+esc(a)+'
'}).join('')+
'
'+
'
';
}).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 ''+
'
'+
'
'+
'
'+
'
'+
'
'+esc(pad(index+1))+'
'+
'
'+
'
'+
'
'+esc(workTitle(e))+'
'+
'
'+esc(data.show_entry_date!==false?fmtDate(e.created_at):(e.student_name||''))+'
'+
'
'+
(data.show_student_message&&e.description&&workTitle(e)!==String(e.description).trim()?'
'+esc(e.description)+'
':'')+
fileChips(e.files)+
'
'+
'
'+
'
'+
'
'+
'
';
});
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
?''
:''+esc(extLabel(f.name))+'
';
const head=isImg
?''
:'';
return head+
''+esc(fmtDate(row.entry.created_at))+'
'+
''+
'
'+
'
'+
icon+
'
'+
'
'+
'
'+
''+esc(label)+'
'+
''+
''+esc(meta.join(' · '))+''+
''+(isImg?'Посмотреть':'Скачать')+''+
'
'+
'';
}).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 ''+
'
'+
'
'+
'
'+
'
'+esc(caption||(data.group_name?'Группа «'+data.group_name+'»':'Фото занятия'))+'
'+
'
'+esc(fmtDate(p.taken_at||p.created_at))+'
'+
'
'+esc(caption?'Фотография с занятий группы':'Фотография с занятий')+'
'+
'
'+
'
'+
'
'+
'
'+
'
';
});
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='Если появятся вопросы по отчёту — напишите преподавателю — мы ответим в течение 24 часов.';
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',state.systemName+' © '+nowYear());
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();
initDateTime().then(function(){return loadBrand()}).then(function(){loadReport()});
function closeLightbox(){const lb=document.getElementById('lightbox');if(lb)lb.classList.remove('is-open')}