Add cookie notice with settings, jfif preview support, and share page title from link name
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user