265 lines
11 KiB
JavaScript
265 lines
11 KiB
JavaScript
function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
|
function fmt(d){return d?d.split('-').reverse().join('.'):'весь период'}
|
|
function showImg(src){document.getElementById('lightboxImg').src=src;document.getElementById('lightbox').classList.add('open')}
|
|
function isImageFile(name){return /\.(jpe?g|png|gif|webp|bmp|avif|svg|ico)$/i.test(name||'')}
|
|
|
|
let countdownInterval = null;
|
|
let countdownTarget = null;
|
|
let countdownScrollHandler = null;
|
|
|
|
function formatRemaining(ms) {
|
|
if (ms <= 0) return null;
|
|
const totalSec = Math.floor(ms / 1000);
|
|
const days = Math.floor(totalSec / 86400);
|
|
const hours = Math.floor((totalSec % 86400) / 3600);
|
|
const minutes = Math.floor((totalSec % 3600) / 60);
|
|
const seconds = totalSec % 60;
|
|
|
|
const parts = [];
|
|
if (days > 0) parts.push(`${days} ${declension(days, 'день', 'дня', 'дней')}`);
|
|
if (hours > 0) parts.push(`${hours} ${declension(hours, 'час', 'часа', 'часов')}`);
|
|
if (minutes > 0) parts.push(`${minutes} ${declension(minutes, 'минута', 'минуты', 'минут')}`);
|
|
if (days === 0) parts.push(`${seconds} ${declension(seconds, 'секунда', 'секунды', 'секунд')}`);
|
|
return parts.join(' ');
|
|
}
|
|
|
|
function declension(n, one, few, many) {
|
|
const mod10 = n % 10;
|
|
const mod100 = n % 100;
|
|
if (mod100 >= 11 && mod100 <= 19) return many;
|
|
if (mod10 === 1) return one;
|
|
if (mod10 >= 2 && mod10 <= 4) return few;
|
|
return many;
|
|
}
|
|
|
|
function shouldShowCountdown() {
|
|
const hero = document.querySelector('.hero');
|
|
if (!hero) return true;
|
|
return window.scrollY > hero.offsetHeight - 80;
|
|
}
|
|
|
|
function syncCountdownVisibility() {
|
|
const el = document.getElementById('countdown');
|
|
if (!countdownTarget) { el.classList.remove('visible'); return; }
|
|
if (countdownTarget - Date.now() <= 0) { el.classList.remove('visible'); return; }
|
|
if (shouldShowCountdown()) el.classList.add('visible');
|
|
else el.classList.remove('visible');
|
|
}
|
|
|
|
function updateCountdown() {
|
|
const el = document.getElementById('countdown');
|
|
if (!countdownTarget) { el.classList.remove('visible'); return; }
|
|
const now = Date.now();
|
|
const remaining = countdownTarget - now;
|
|
if (remaining <= 0) {
|
|
el.classList.remove('visible');
|
|
if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; }
|
|
return;
|
|
}
|
|
const text = formatRemaining(remaining);
|
|
if (!text) { el.classList.remove('visible'); return; }
|
|
el.classList.remove('warn', 'urgent');
|
|
if (remaining < 3600000) el.classList.add('urgent');
|
|
else if (remaining < 86400000) el.classList.add('warn');
|
|
el.querySelector('.cd-text').innerHTML = `Ссылка доступна ещё <strong>${text}</strong>`;
|
|
syncCountdownVisibility();
|
|
}
|
|
|
|
function startCountdown(expiresAt) {
|
|
if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; }
|
|
if (countdownScrollHandler) { window.removeEventListener('scroll', countdownScrollHandler); countdownScrollHandler = null; }
|
|
const el = document.getElementById('countdown');
|
|
if (!expiresAt) { el.classList.remove('visible'); return; }
|
|
countdownTarget = new Date(expiresAt).getTime();
|
|
countdownScrollHandler = syncCountdownVisibility;
|
|
window.addEventListener('scroll', countdownScrollHandler, { passive: true });
|
|
updateCountdown();
|
|
countdownInterval = setInterval(updateCountdown, 1000);
|
|
}
|
|
|
|
function fileType(name){
|
|
if(/\.(jpe?g|png|gif|webp|bmp|avif|svg|ico)$/i.test(name||'')) return 'image';
|
|
if(/\.(mp3|wav|ogg|flac|m4a|aac|opus)$/i.test(name||'')) return 'music';
|
|
if(/\.(pptx?|odp|key)$/i.test(name||'')) return 'presentation';
|
|
if(/\.(pdf|docx?|xlsx?|txt|rtf|odt|ods|csv|md)$/i.test(name||'')) return 'document';
|
|
return 'other';
|
|
}
|
|
function fileIcon(t){
|
|
const icons = {image:'🖼️',music:'🎵',presentation:'📊',document:'📄',other:'📁'};
|
|
return icons[t]||icons.other;
|
|
}
|
|
function filesHTML(files, password, shareToken){
|
|
if(!files||!files.length)return '';
|
|
const pw = password ? `?password=${encodeURIComponent(password)}` : '';
|
|
return `<div class="files-block">
|
|
<div class="files-label">Работы резидента</div>
|
|
<div class="files">${files.map(f=>{
|
|
const t = fileType(f.name);
|
|
const url = `/api/share/${shareToken}/files/${f.token}${pw}`;
|
|
return `<a class="ftype-${t}" title="${esc(f.name)}" ${isImageFile(f.name)
|
|
?`href="#" data-img="${esc(url)}"`
|
|
:`href="${url}" download`}>
|
|
<span class="ficon" alt="${esc(f.name)}">${fileIcon(t)}</span>
|
|
</a>`;
|
|
}).join('')}</div>
|
|
</div>`;
|
|
}
|
|
|
|
let currentToken = '';
|
|
let currentPassword = null;
|
|
|
|
async function fetchShareData(token, password) {
|
|
const headers = {};
|
|
if (password) headers['x-share-password'] = password;
|
|
const res = await fetch('/api/share/' + token, { headers });
|
|
const data = await res.json().catch(() => ({}));
|
|
return { ok: res.ok, status: res.status, data };
|
|
}
|
|
|
|
async function loadShare() {
|
|
const token = location.pathname.split('/').filter(Boolean).pop() || '';
|
|
currentToken = token;
|
|
currentPassword = null;
|
|
|
|
const tryLoad = async (pwd) => {
|
|
const result = await fetchShareData(token, pwd);
|
|
if (result.ok) {
|
|
renderShare(result.data, pwd);
|
|
return true;
|
|
}
|
|
if (result.status === 401 && result.data.passwordRequired) {
|
|
showPasswordModal();
|
|
return false;
|
|
}
|
|
if (result.status === 410) {
|
|
showError('Срок действия ссылки истёк');
|
|
return true;
|
|
}
|
|
showError('Ссылка не найдена или была удалена');
|
|
return true;
|
|
};
|
|
|
|
await tryLoad(null);
|
|
}
|
|
|
|
function showPasswordModal() {
|
|
document.getElementById('passwordInput').value = '';
|
|
document.getElementById('passwordModal').classList.add('open');
|
|
document.getElementById('passwordInput').focus();
|
|
}
|
|
|
|
function hidePasswordModal() {
|
|
document.getElementById('passwordModal').classList.remove('open');
|
|
}
|
|
|
|
async function submitPassword() {
|
|
const pwd = document.getElementById('passwordInput').value;
|
|
if (!pwd) return;
|
|
const result = await fetchShareData(currentToken, pwd);
|
|
if (result.ok) {
|
|
hidePasswordModal();
|
|
currentPassword = pwd;
|
|
renderShare(result.data, pwd);
|
|
} else {
|
|
alert('Неверный пароль');
|
|
document.getElementById('passwordInput').value = '';
|
|
document.getElementById('passwordInput').focus();
|
|
}
|
|
}
|
|
|
|
function showError(msg) {
|
|
document.getElementById('title').textContent = msg;
|
|
document.getElementById('grid').innerHTML = `<div class="empty"><div class="big">🤔</div>${esc(msg)}</div>`;
|
|
document.getElementById('photosBlock').style.display = 'none';
|
|
document.getElementById('shareMessage').style.display = 'none';
|
|
document.getElementById('shareLinkBlock').style.display = 'none';
|
|
document.getElementById('countdown').classList.remove('visible');
|
|
if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; }
|
|
if (countdownScrollHandler) { window.removeEventListener('scroll', countdownScrollHandler); countdownScrollHandler = null; }
|
|
}
|
|
|
|
function renderShare(data, password) {
|
|
const heading = data.group_name || data.name || 'Записи занятий';
|
|
document.title = heading;
|
|
document.getElementById('title').textContent = heading;
|
|
const chips = [];
|
|
if (data.group_name) chips.push(`Группа: <b>${esc(data.group_name)}</b>`);
|
|
if (data.date_from || data.date_to) chips.push(`Период: <b>${fmt(data.date_from)}</b> — <b>${fmt(data.date_to)}</b>`);
|
|
document.getElementById('chips').innerHTML = chips.map(c => `<span class="chip">${c}</span>`).join('');
|
|
|
|
startCountdown(data.expires_at);
|
|
|
|
const msgEl = document.getElementById('shareMessage');
|
|
if (data.message) {
|
|
msgEl.textContent = data.message;
|
|
msgEl.style.display = 'block';
|
|
} else {
|
|
msgEl.style.display = 'none';
|
|
}
|
|
|
|
const linkBlock = document.getElementById('shareLinkBlock');
|
|
linkBlock.innerHTML = '';
|
|
if (data.link_url && /^https?:\/\//i.test(data.link_url)) {
|
|
const a = document.createElement('a');
|
|
a.className = 'share-link-btn';
|
|
a.href = data.link_url;
|
|
a.target = '_blank';
|
|
a.rel = 'noopener noreferrer';
|
|
a.textContent = '🔗 Перейти по ссылке';
|
|
linkBlock.appendChild(a);
|
|
linkBlock.style.display = 'block';
|
|
} else {
|
|
linkBlock.style.display = 'none';
|
|
}
|
|
|
|
const pb = document.getElementById('photosBlock');
|
|
if (data.show_group_photos && data.photos && data.photos.length) {
|
|
document.getElementById('photosGrid').innerHTML = data.photos.map(p => `
|
|
<a class="gphoto" href="#" data-img="${esc(p.photo_path)}">
|
|
<img src="${p.photo_path}" alt="${esc(p.caption || '')}" loading="lazy">
|
|
${p.caption ? `<div class="gcap">${esc(p.caption)}</div>` : ''}
|
|
</a>
|
|
`).join('');
|
|
pb.style.display = 'block';
|
|
} else {
|
|
pb.style.display = 'none';
|
|
}
|
|
|
|
const grid = document.getElementById('grid');
|
|
if (!data.entries.length) {
|
|
grid.innerHTML = '<div class="empty"><div class="big">🎈</div>Записей пока нет</div>';
|
|
return;
|
|
}
|
|
const isAnonymized = !data.show_student_names;
|
|
const showStudentMessage = !!data.show_student_message;
|
|
const showEntryDate = !!data.show_entry_date;
|
|
grid.innerHTML = data.entries.map(e => `
|
|
<div class="card">
|
|
<div class="photo">
|
|
${e.photo_path
|
|
? `<img src="${e.photo_path}" data-img="${esc(e.photo_path)}" alt="">`
|
|
: `<div class="no-photo"><svg viewBox="0 0 240 160" preserveAspectRatio="xMidYMid meet" aria-hidden="true"><circle cx="105" cy="48" r="15"/><path d="M74 140 C74 106 88 92 105 92 C122 92 136 106 136 140 Z"/><circle cx="145" cy="58" r="18"/><path d="M108 144 C108 104 124 88 145 88 C166 88 182 104 182 144 Z"/></svg></div>`}
|
|
</div>
|
|
<div class="body">
|
|
${!isAnonymized ? `<div class="name">${esc(e.student_name)}</div>` : ''}
|
|
${!isAnonymized && e.group_name ? `<div><span class="group">${esc(e.group_name)}</span></div>` : ''}
|
|
${showStudentMessage && e.description ? `<div class="desc">${esc(e.description)}</div>` : ''}
|
|
${filesHTML(e.files, password, currentToken)}
|
|
</div>
|
|
${showEntryDate ? `<div class="foot">${new Date(e.created_at).toLocaleString('ru',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'})}</div>` : ''}
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
document.getElementById('lightbox').addEventListener('click', () => document.getElementById('lightbox').classList.remove('open'));
|
|
document.getElementById('passwordModal').querySelector('button').addEventListener('click', submitPassword);
|
|
|
|
document.addEventListener('click', e => {
|
|
const imgEl = e.target.closest('[data-img]');
|
|
if (imgEl) { e.preventDefault(); showImg(imgEl.dataset.img); }
|
|
});
|
|
|
|
(async()=>{
|
|
await loadShare();
|
|
})();
|