Files
WhatIDo/public/js/share.js
T

336 lines
13 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|jfif|png|gif|webp|bmp|avif|svg|ico)$/i.test(name||'')}
function thumbSrc(p){
if(!p)return p;
if(p.startsWith('/uploads/'))return '/uploads/thumb/'+p.slice('/uploads/'.length);
if(p.includes('/api/files/'))return p+(p.includes('?')?'&':'?')+'thumb=1';
return p;
}
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|jfif|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.name && String(data.name).trim()) || data.group_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="${thumbSrc(p.photo_path)}" alt="${esc(p.caption || '')}" loading="lazy" decoding="async">
${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;
const dayKey = e => {
const d = new Date(e.created_at);
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
};
const dayLabel = k => {
const [y,m,d] = k.split('-').map(Number);
return new Date(y, m-1, d).toLocaleDateString('ru-RU', { day:'numeric', month:'long', year:'numeric' });
};
const cardHTML = e => {
const 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)
].join('');
return `
<div class="card">
<div class="photo">
${e.photo_path
? `<img src="${thumbSrc(e.photo_path)}" data-img="${esc(e.photo_path)}" loading="lazy" decoding="async" 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>
${body ? `<div class="body">${body}</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>
`;
};
const groups = [];
let lastKey = null;
data.entries.forEach(e => {
const k = dayKey(e);
if (k !== lastKey) { groups.push({ key: k, entries: [] }); lastKey = k; }
groups[groups.length-1].entries.push(e);
});
grid.innerHTML = groups.map(g =>
`<div class="date-sep"><span class="date-chip">${esc(dayLabel(g.key))}</span><span class="date-line"></span></div>` +
g.entries.map(cardHTML).join('')
).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();
})();
(async function(){
const notice = document.getElementById('cookieNotice');
const acceptBtn = document.getElementById('cookieAcceptBtn');
const COOKIE_KEY = 'whatido_cookie_consent';
if (!notice || !acceptBtn || localStorage.getItem(COOKIE_KEY)) return;
try {
const res = await fetch('/api/public-settings');
const s = await res.json();
const text = (s.cookie_notice_text || '').trim();
if (!text) return;
notice.querySelector('p').textContent = text;
notice.classList.remove('hidden');
} catch (e) { return; }
acceptBtn.addEventListener('click', () => {
localStorage.setItem(COOKIE_KEY, '1');
notice.classList.add('hidden');
});
})();
(async function(){
try {
const res = await fetch('/api/public-settings');
if (!res.ok) return;
const s = await res.json();
const head = document.getElementById('brandHead');
if (!head) return;
const logo = document.getElementById('brandLogo');
const name = document.getElementById('brandName');
let any = false;
if (name && (s.system_name || '').trim()) {
name.textContent = s.system_name;
name.hidden = false;
any = true;
}
if (logo && s.system_logo) {
logo.src = s.system_logo;
logo.hidden = false;
any = true;
}
if (any) head.hidden = false;
} catch (e) {}
})();