refactor: move frontend JS to external files and enable strict CSP
This commit is contained in:
+3
-153
@@ -76,167 +76,17 @@ body{font-family:'Segoe UI',system-ui,-apple-system,Roboto,sans-serif;background
|
||||
</div>
|
||||
<div class="grid" id="grid"><div class="loading">Карточки загружаются...</div></div>
|
||||
</main>
|
||||
<div class="lightbox" id="lightbox" onclick="this.classList.remove('open')"><img id="lightboxImg" alt=""></div>
|
||||
<div class="lightbox" id="lightbox"><img id="lightboxImg" alt=""></div>
|
||||
|
||||
<div class="password-modal" id="passwordModal">
|
||||
<div class="password-box">
|
||||
<h3>🔒 Ссылка защищена паролем</h3>
|
||||
<p>Введите пароль для доступа к записям</p>
|
||||
<input type="password" id="passwordInput" placeholder="Пароль" autocomplete="current-password">
|
||||
<button onclick="submitPassword()">Войти</button>
|
||||
<button type="button" id="passwordSubmitBtn">Войти</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
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="#" onclick="showImg('${url}');return false;"`
|
||||
:`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="#" onclick="showImg('${p.photo_path}');return false;">
|
||||
<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}" onclick="showImg('${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('');
|
||||
}
|
||||
|
||||
(async()=>{
|
||||
await loadShare();
|
||||
})();
|
||||
</script>
|
||||
<script src="/js/share.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user