feat(branding): system name/logo, horizontal logo on all pages

This commit is contained in:
dev
2026-09-23 15:19:23 +03:00
parent e435b4ad43
commit f2d465e0c4
11 changed files with 275 additions and 18 deletions
+5 -1
View File
@@ -13,4 +13,8 @@ if (details) {
}
})();
document.querySelector('.error-actions button').addEventListener('click', () => history.back());
document.querySelector('.error-actions button').addEventListener('click', () => history.back());
fetch('/api/public-settings').then(r => r.ok ? r.json() : null).then(s => {
if (s && (s.system_name || '').trim()) document.title = `Ошибка — ${s.system_name}`;
}).catch(() => {});
+11
View File
@@ -202,6 +202,17 @@ async function resolveModuleId(name) {
try {
const fRes = await fetch(`${API}/api/public-settings`);
const f = await fRes.json();
const title = document.getElementById('title');
if (title && (f.system_name || '').trim()) {
title.textContent = f.system_name;
document.title = f.system_name;
}
const brandLogo = document.getElementById('brandLogo');
if (brandLogo && f.system_logo) {
brandLogo.src = f.system_logo;
brandLogo.hidden = false;
brandLogo.alt = f.system_name || '';
}
if (f.footer_left) document.getElementById('footerLeft').textContent = f.footer_left;
if (f.footer_right) document.getElementById('footerRight').textContent = f.footer_right;
const c = parseInt(f.spam_interval_min, 10);
+20 -1
View File
@@ -28,4 +28,23 @@ async function doLogin() {
document.getElementById('pwdInput').addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
document.getElementById('userInput').addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
document.querySelector('.login button').addEventListener('click', doLogin);
document.querySelector('.login button').addEventListener('click', doLogin);
(async () => {
try {
const res = await fetch(`${API}/api/public-settings`);
if (!res.ok) return;
const s = await res.json();
const title = document.getElementById('loginTitle');
if (title && (s.system_name || '').trim()) title.textContent = s.system_name;
const logo = document.getElementById('loginLogo');
if (logo && s.system_logo) {
const img = document.createElement('img');
img.src = s.system_logo;
img.alt = '';
logo.appendChild(img);
logo.style.display = 'flex';
}
document.title = title ? title.textContent + ' — Вход' : document.title;
} catch { /* ignore */ }
})();
+11 -3
View File
@@ -1,4 +1,12 @@
const state={token:'',password:null,showNames:true,cdTarget:0,cdTimer:null,cdBound:false};
const state={token:'',password:null,showNames:true,cdTarget:0,cdTimer:null,cdBound:false,systemName:'WhatIDo'};
function loadBrand(){
return fetch('/api/public-settings').then(function(res){
return res.ok?res.json():null;
}).then(function(s){
if(s&&(s.system_name||'').trim())state.systemName=s.system_name;
}).catch(function(){});
}
function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#039;')}
function pad(n){return String(n).padStart(2,'0')}
@@ -440,7 +448,7 @@ function renderFooter(data,heading,pool){
const label=btn.querySelector('.text-block-wrap-div');
if(label)label.textContent='Перейти по ссылке';
}
setText('footCopy','WhatIDo © '+new Date().getFullYear());
setText('footCopy',state.systemName+' © '+new Date().getFullYear());
setText('footBrand',heading);
const fbox=document.getElementById('footerPhotoBox');
const fimg=document.getElementById('footerPhoto');
@@ -542,5 +550,5 @@ function bindEvents(){
bindEvents();
makeNavLinks();
loadReport();
loadBrand().then(function(){loadReport()});
function closeLightbox(){const lb=document.getElementById('lightbox');if(lb)lb.classList.remove('is-open')}
+83 -2
View File
@@ -1,5 +1,5 @@
const DIRTY_FIELDS = ['spamInterval', 'footerLeft', 'footerRight', 'aiPrompt', 'aiAutoCheck', 'shareShowMessage', 'shareShowDate', 'shareShowNames', 'shareShowPhotos', 'cookieNoticeText', 'photoResolution', 'photoQuality', 'photoEngine'];
const SECTION_IDS = ['sec-system', 'sec-antispam', 'sec-share', 'sec-form', 'sec-ai', 'sec-backup', 'sec-bans'];
const DIRTY_FIELDS = ['systemName', 'spamInterval', 'footerLeft', 'footerRight', 'aiPrompt', 'aiAutoCheck', 'shareShowMessage', 'shareShowDate', 'shareShowNames', 'shareShowPhotos', 'cookieNoticeText', 'photoResolution', 'photoQuality', 'photoEngine'];
const SECTION_IDS = ['sec-system', 'sec-brand', 'sec-antispam', 'sec-share', 'sec-ai', 'sec-backup', 'sec-form', 'sec-bans'];
const SPAM_MAX = 10080;
const BAN_REASONS = {
'honeypot': 'Антиспам-поле',
@@ -8,6 +8,21 @@ const BAN_REASONS = {
'manual': 'Вручную',
};
let currentLogo = '';
let logoOriginal = '';
function renderLogoPreview() {
const img = document.getElementById('logoPreviewImg');
const fb = document.getElementById('logoPreviewFallback');
const rm = document.getElementById('logoRemoveBtn');
if (!img || !fb || !rm) return;
const hasLogo = !!currentLogo;
img.style.display = hasLogo ? 'block' : 'none';
fb.style.display = hasLogo ? 'none' : '';
if (hasLogo) img.src = currentLogo;
rm.hidden = !hasLogo;
}
function fieldValue(el) {
return el.type === 'checkbox' ? String(el.checked) : el.value;
}
@@ -101,6 +116,8 @@ function revertChanges() {
if (el.type === 'checkbox') el.checked = original === 'true';
else el.value = original;
});
currentLogo = logoOriginal;
renderLogoPreview();
clearFieldError('spamInterval');
updateSpamUi();
updateDirty();
@@ -143,6 +160,10 @@ async function loadSettings() {
]);
if (!sRes.ok) { showError('Не удалось загрузить настройки'); return; }
const s = await sRes.json();
document.getElementById('systemName').value = s.system_name !== undefined ? s.system_name : 'WhatIDo';
currentLogo = s.system_logo || '';
logoOriginal = currentLogo;
renderLogoPreview();
if (s.spam_interval_min !== undefined) document.getElementById('spamInterval').value = s.spam_interval_min;
document.getElementById('footerLeft').value = s.footer_left || '';
document.getElementById('footerRight').value = s.footer_right || '';
@@ -487,6 +508,7 @@ async function saveSettings() {
method: 'PUT',
headers: hdrJson(),
body: JSON.stringify({ settings: {
system_name: document.getElementById('systemName').value.trim(),
spam_interval_min: String(Math.round(Number(v))),
footer_left: document.getElementById('footerLeft').value.trim(),
footer_right: document.getElementById('footerRight').value.trim(),
@@ -504,6 +526,7 @@ async function saveSettings() {
});
if (res.ok) {
showToast('Настройки сохранены');
logoOriginal = currentLogo;
snapshotDirty();
updateSpamUi();
} else {
@@ -637,6 +660,64 @@ if (statsRefreshBtn) statsRefreshBtn.addEventListener('click', async () => {
statsRefreshBtn.classList.remove('spinning');
});
function logoStatusText(html) {
const st = document.getElementById('logoUploadStatus');
if (st) st.innerHTML = html;
}
document.getElementById('logoUploadBtn').addEventListener('click', () => document.getElementById('logoInput').click());
document.getElementById('logoInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const btn = document.getElementById('logoUploadBtn');
if (file.size > 1024 * 1024) {
e.target.value = '';
showError('Логотип не должен превышать 1 МБ');
return;
}
btn.disabled = true;
logoStatusText('<span class="spinner dark"></span> Загрузка...');
try {
const fd = new FormData();
fd.append('logo', file);
const res = await fetch(`${API}/api/settings/logo`, { method: 'POST', headers: hdr(), body: fd });
const d = await res.json().catch(() => ({}));
if (res.ok) {
currentLogo = d.system_logo || '';
renderLogoPreview();
logoStatusText('');
showToast('Логотип обновлён');
} else {
logoStatusText(`<span class="fail">${esc(d.error || 'Ошибка загрузки')}</span>`);
}
} catch {
logoStatusText('<span class="fail">Ошибка сети</span>');
} finally {
btn.disabled = false;
e.target.value = '';
}
});
document.getElementById('logoRemoveBtn').addEventListener('click', async () => {
if (!confirm('Удалить логотип? Название системы сохранится.')) return;
logoStatusText('<span class="spinner dark"></span> Удаление...');
try {
const res = await fetch(`${API}/api/settings/logo`, { method: 'DELETE', headers: hdr() });
if (res.ok) {
currentLogo = '';
logoOriginal = '';
renderLogoPreview();
logoStatusText('');
showToast('Логотип удалён');
} else {
logoStatusText('<span class="fail">Ошибка удаления</span>');
}
} catch {
logoStatusText('<span class="fail">Ошибка сети</span>');
}
});
document.addEventListener('keydown', e => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
e.preventDefault();