refactor: move frontend JS to external files and enable strict CSP

This commit is contained in:
dev
2026-09-09 10:06:49 +03:00
parent d6e589d2f5
commit 018c65adc5
34 changed files with 2444 additions and 2307 deletions
+157
View File
@@ -0,0 +1,157 @@
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||'')}
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';
}
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('');
const pb = document.getElementById('photosBlock');
if (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.anonymize_names;
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>` : ''}
<div class="desc">${esc(e.description)}</div>
${filesHTML(e.files, password, currentToken)}
</div>
<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();
})();