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
+110
View File
@@ -0,0 +1,110 @@
function plural(n, one, few, many) {
n = Math.abs(n) % 100; const n1 = n % 10;
if (n > 10 && n < 20) return many;
if (n1 > 1 && n1 < 5) return few;
if (n1 === 1) return one;
return many;
}
function renderStats(s) {
document.getElementById('stats').innerHTML = `
<div class="stat"><div class="num">${s.entries}</div><div class="label">Всего записей</div></div>
<div class="stat"><div class="num">${s.students}</div><div class="label">Учеников</div></div>
<div class="stat"><div class="num">${s.groups}</div><div class="label">Групп</div></div>
<div class="stat"><div class="num">${s.today}</div><div class="label">Сегодня</div></div>
<div class="stat"><div class="num" style="color:${s.trash ? '#ef4444' : 'var(--muted)'}">${s.trash}</div><div class="label"><a href="trash.html">В корзине →</a></div></div>
`;
}
function last14days() {
const out = [];
for (let i = 13; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); out.push(d); }
return out;
}
function renderActivity(activity) {
const map = {};
activity.forEach(a => { map[a.d] = a.n; });
const days = last14days().map(d => {
const iso = d.toISOString().slice(0, 10);
return { iso, label: d.toLocaleDateString('ru', { day: '2-digit', month: '2-digit' }), n: map[iso] || 0 };
});
const max = Math.max(1, ...days.map(x => x.n));
const todayIso = new Date().toISOString().slice(0, 10);
document.getElementById('activityChart').innerHTML = days.map(x => `
<div class="bar-col" title="${x.iso} — ${x.n} ${plural(x.n, 'запись', 'записи', 'записей')}">
<div class="bar ${x.iso === todayIso ? 'today' : ''}" style="height:${Math.round(x.n / max * 100)}%"></div>
<div class="bar-day">${x.label}</div>
</div>`).join('');
}
function renderRecent(rows) {
const el = document.getElementById('recentList');
if (!rows.length) { el.innerHTML = '<div class="dash-empty">Записей пока нет</div>'; return; }
el.innerHTML = rows.map(e => `
<div class="recent-item">
${e.photo_path
? `<img src="${API}${e.photo_path}" data-img="${API}${e.photo_path}" alt="">`
: `<div class="thumb">🎓</div>`}
<div class="r-info">
<div class="r-name">${esc(e.student_name)}</div>
<div class="r-meta">${esc(e.group_name)} · ${new Date(e.created_at).toLocaleString('ru', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}</div>
</div>
<a class="r-link" href="journal.html" title="В журнал">→</a>
</div>`).join('');
}
function renderActive(rows) {
const el = document.getElementById('activeList');
if (!rows.length) { el.innerHTML = '<div class="dash-empty">Сейчас нет идущих групп</div>'; return; }
el.innerHTML = rows.map(g => `
<div class="active-item">
<span class="a-name">${esc(g.name)}</span>
<span class="a-right">
<span class="a-time">${g.day_label} ${g.time_start}–${g.time_end}</span>
<span class="a-now">сейчас</span>
</span>
</div>`).join('');
}
function renderTop(rows) {
const el = document.getElementById('topList');
if (!rows.length) { el.innerHTML = '<div class="dash-empty">Пока нет данных</div>'; return; }
el.innerHTML = rows.map((x, i) => `
<div class="top-item">
<span class="top-rank ${i > 2 ? 'alt' : ''}">${i + 1}</span>
<span class="top-name">${esc(x.student_name)}</span>
<span class="top-count">${x.n}</span>
</div>`).join('');
}
function renderPhotos(rows) {
const el = document.getElementById('dashPhotos');
if (!rows.length) { el.innerHTML = '<div class="dash-empty">Фото групп пока нет</div>'; return; }
el.innerHTML = rows.map(p => `
<div class="dash-photo" title="${esc(p.group_name)}">
<img src="${API}${p.photo_path}" data-img="${API}${p.photo_path}" alt="">
${p.caption ? `<div class="pcap">${esc(p.caption)}</div>` : ''}
</div>`).join('');
}
async function loadDashboard() {
const res = await fetch(`${API}/api/dashboard`, { headers: hdr() });
if (!res.ok) { document.getElementById('stats').innerHTML = '<div class="dash-empty">Не удалось загрузить данные</div>'; return; }
const d = await res.json();
renderStats(d.stats);
renderActivity(d.activity);
renderRecent(d.recent_entries);
renderActive(d.active_groups);
renderTop(d.top_students);
renderPhotos(d.photos);
}
document.getElementById('imgModal').addEventListener('click', e => e.currentTarget.classList.remove('open'));
(async () => {
if (await checkAuth()) {
buildSidebar(document.body.dataset.page);
loadDashboard();
}
})();