Add cookie notice with settings, jfif preview support, and share page title from link name

This commit is contained in:
dev
2026-09-12 10:05:02 +03:00
parent 192de5e690
commit fd753c1318
11 changed files with 80 additions and 36 deletions
+2
View File
@@ -73,6 +73,8 @@ INSERT INTO settings (key, value) VALUES ('share_show_student_names', 'true')
ON CONFLICT (key) DO NOTHING;
INSERT INTO settings (key, value) VALUES ('share_show_group_photos', 'true')
ON CONFLICT (key) DO NOTHING;
INSERT INTO settings (key, value) VALUES ('cookie_notice_text', 'Этот сайт использует cookie-файлы для корректной работы. Продолжая просмотр, вы соглашаетесь с их использованием.')
ON CONFLICT (key) DO NOTHING;
CREATE TABLE IF NOT EXISTS share_links (
id SERIAL PRIMARY KEY,
+2
View File
@@ -110,6 +110,8 @@ ON CONFLICT (key) DO NOTHING;
DELETE FROM settings WHERE key = 'share_anonymize_names';
INSERT INTO settings (key, value) VALUES ('share_show_group_photos', 'true')
ON CONFLICT (key) DO NOTHING;
INSERT INTO settings (key, value) VALUES ('cookie_notice_text', 'Этот сайт использует cookie-файлы для корректной работы. Продолжая просмотр, вы соглашаетесь с их использованием.')
ON CONFLICT (key) DO NOTHING;
DO $$
BEGIN
+19 -19
View File
@@ -43,25 +43,25 @@ services:
# Публикация через Tailscale (Serve / Funnel) без проброса портов.
# Приложение доступно по https://whatido.<tailnet>.ts.net
tailscale:
image: tailscale/tailscale:latest
hostname: whatido
restart: unless-stopped
network_mode: host
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
SSL_CERT_FILE: /etc/tailscale/app-certs/cert.pem
volumes:
- /var/lib/tailscale:/var/lib/tailscale
- /dev/net/tun:/dev/net/tun
- /lib/modules:/lib/modules:ro
- ./certs:/etc/tailscale/app-certs:ro
- ./start-tailscale.sh:/start-tailscale.sh:ro
command: ["/bin/sh", "/start-tailscale.sh"]
depends_on:
- app
# tailscale:
# image: tailscale/tailscale:latest
# hostname: whatido
# restart: unless-stopped
# network_mode: host
# cap_add:
# - NET_ADMIN
# - SYS_MODULE
# environment:
# SSL_CERT_FILE: /etc/tailscale/app-certs/cert.pem
# volumes:
# - /var/lib/tailscale:/var/lib/tailscale
# - /dev/net/tun:/dev/net/tun
# - /lib/modules:/lib/modules:ro
# - ./certs:/etc/tailscale/app-certs:ro
# - ./start-tailscale.sh:/start-tailscale.sh:ro
# command: ["/bin/sh", "/start-tailscale.sh"]
# depends_on:
# - app
text-corrector:
+1 -1
View File
@@ -21,7 +21,7 @@ function setMode(m) {
}
function isImageFile(name) {
return /\.(jpe?g|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || '');
return /\.(jpe?g|jfif|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || '');
}
function humanSize(b) {
+1 -1
View File
@@ -21,7 +21,7 @@ async function loadLinkSettings() {
}
function isImageFile(name) {
return /\.(jpe?g|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || '');
return /\.(jpe?g|jfif|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || '');
}
function mainPhotoPath(e) {
+3 -1
View File
@@ -1,4 +1,4 @@
const DIRTY_FIELDS = ['spamInterval', 'footerLeft', 'footerRight', 'aiPrompt', 'aiAutoCheck', 'shareShowMessage', 'shareShowDate', 'shareShowNames', 'shareShowPhotos'];
const DIRTY_FIELDS = ['spamInterval', 'footerLeft', 'footerRight', 'aiPrompt', 'aiAutoCheck', 'shareShowMessage', 'shareShowDate', 'shareShowNames', 'shareShowPhotos', 'cookieNoticeText'];
function fieldValue(el) {
return el.type === 'checkbox' ? String(el.checked) : el.value;
@@ -51,6 +51,7 @@ async function loadSettings() {
document.getElementById('shareShowDate').checked = s.share_show_entry_date === 'true';
document.getElementById('shareShowNames').checked = s.share_show_student_names !== 'false';
document.getElementById('shareShowPhotos').checked = s.share_show_group_photos !== 'false';
document.getElementById('cookieNoticeText').value = s.cookie_notice_text || '';
snapshotDirty();
loadAiQueue();
if (stRes.ok) {
@@ -166,6 +167,7 @@ async function saveSettings() {
share_show_entry_date: document.getElementById('shareShowDate').checked ? 'true' : 'false',
share_show_student_names: document.getElementById('shareShowNames').checked ? 'true' : 'false',
share_show_group_photos: document.getElementById('shareShowPhotos').checked ? 'true' : 'false',
cookie_notice_text: document.getElementById('cookieNoticeText').value.trim(),
} })
});
if (res.ok) {
+33 -11
View File
@@ -1,7 +1,7 @@
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 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);
@@ -84,7 +84,7 @@ function startCountdown(expiresAt) {
}
function fileType(name){
if(/\.(jpe?g|png|gif|webp|bmp|avif|svg|ico)$/i.test(name||'')) return 'image';
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';
@@ -185,7 +185,7 @@ function showError(msg) {
}
function renderShare(data, password) {
const heading = data.group_name || data.name || 'Записи занятий';
const heading = (data.name && String(data.name).trim()) || data.group_name || 'Записи занятий';
document.title = heading;
document.getElementById('title').textContent = heading;
const chips = [];
@@ -239,22 +239,25 @@ function renderShare(data, password) {
const isAnonymized = !data.show_student_names;
const showStudentMessage = !!data.show_student_message;
const showEntryDate = !!data.show_entry_date;
grid.innerHTML = data.entries.map(e => `
grid.innerHTML = data.entries.map(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>
<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>` : ''}
${showStudentMessage && e.description ? `<div class="desc">${esc(e.description)}</div>` : ''}
${filesHTML(e.files, password, currentToken)}
</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>
`).join('');
`;
}).join('');
}
document.getElementById('lightbox').addEventListener('click', () => document.getElementById('lightbox').classList.remove('open'));
@@ -268,3 +271,22 @@ document.addEventListener('click', e => {
(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');
});
})();
+1 -1
View File
@@ -4,7 +4,7 @@ let page = 1;
let totalEntries = 0;
function isImageFile(name) {
return /\.(jpe?g|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || '');
return /\.(jpe?g|jfif|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || '');
}
function filesHTML(files) {
+6
View File
@@ -81,6 +81,12 @@
</label>
</div>
</div>
<div class="settings-row">
<div class="settings-field" style="flex:1">
<label for="cookieNoticeText">Уведомление о cookie</label>
<textarea id="cookieNoticeText" class="settings-input" rows="3" style="min-height:80px"></textarea>
</div>
</div>
</div>
<div class="settings-card">
+10
View File
@@ -75,6 +75,12 @@ body{font-family:'Segoe UI',system-ui,-apple-system,Roboto,sans-serif;background
.countdown.warn .cd-text strong{color:#E65100}
.countdown.urgent{background:#FFEBEE}
.countdown.urgent .cd-text strong{color:#C62828}
.cookie-notice{position:fixed;bottom:24px;right:24px;z-index:150;max-width:340px;background:var(--card-bg);border-radius:16px;padding:18px 20px;box-shadow:0 8px 30px rgba(20,30,40,.18);font-size:.9rem;line-height:1.5;color:#3a3f4a}
.cookie-notice.hidden{display:none}
.cookie-notice p{margin-bottom:14px}
.cookie-notice button{width:100%;padding:10px 14px;background:var(--yellow);color:var(--dark);border:none;border-radius:12px;font-size:.9rem;font-weight:700;cursor:pointer}
.cookie-notice button:hover{opacity:.9}
@media(max-width:480px){.cookie-notice{left:24px;right:24px;max-width:none}}
</style>
</head>
<body>
@@ -93,6 +99,10 @@ body{font-family:'Segoe UI',system-ui,-apple-system,Roboto,sans-serif;background
</main>
<div class="lightbox" id="lightbox"><img id="lightboxImg" alt=""></div>
<div id="countdown" class="countdown"><span class="cd-icon">⏳</span><span class="cd-text"></span></div>
<div class="cookie-notice hidden" id="cookieNotice">
<p>Этот сайт использует cookie-файлы для корректной работы. Продолжая просмотр, вы соглашаетесь с их использованием.</p>
<button type="button" id="cookieAcceptBtn">Принять</button>
</div>
<div class="password-modal" id="passwordModal">
<div class="password-box">
+2 -2
View File
@@ -872,7 +872,7 @@ app.get('/api/settings', requireAdmin, async (_, res) => {
app.get('/api/public-settings', apiLimiter, async (_, res) => {
const out = await cacheWrap('public-settings', PUBLIC_TTL_MS, async () => {
const keys = ['footer_left', 'footer_right', 'share_show_student_message', 'share_show_entry_date', 'share_show_student_names', 'share_show_group_photos'];
const keys = ['footer_left', 'footer_right', 'share_show_student_message', 'share_show_entry_date', 'share_show_student_names', 'share_show_group_photos', 'cookie_notice_text'];
const result = {};
for (const k of keys) result[k] = await getSetting(k, '');
return result;
@@ -2308,7 +2308,7 @@ app.post('/api/entries/:id/files', requireAuth, (req, res, next) => {
});
function isImageName(name) {
return /\.(jpe?g|png|gif|webp|bmp|avif|ico)$/i.test(name || '');
return /\.(jpe?g|jfif|png|gif|webp|bmp|avif|ico)$/i.test(name || '');
}
app.get('/api/files', requireAuth, async (req, res) => {