Add server-side image thumbnails (sharp): /uploads/thumb/<name> route, ?thumb=1 on file endpoints, disk-cached 480px WebP previews; grids now load thumbnails with lazy loading
This commit is contained in:
@@ -4,6 +4,12 @@ let currentUser = null;
|
||||
function hdr() { return { 'X-Auth-Token': token }; }
|
||||
function hdrJson() { return { 'Content-Type': 'application/json', 'X-Auth-Token': token }; }
|
||||
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||
function thumbSrc(p) {
|
||||
if (!p) return p;
|
||||
if (p.startsWith('/uploads/')) return `${API}/uploads/thumb/${p.slice('/uploads/'.length)}`;
|
||||
if (p.includes('/api/files/')) return p + (p.includes('?') ? '&' : '?') + 'thumb=1';
|
||||
return p;
|
||||
}
|
||||
|
||||
// --- Auth check ---
|
||||
async function checkAuth() {
|
||||
|
||||
@@ -56,7 +56,7 @@ function renderRecent(rows) {
|
||||
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="">`
|
||||
? `<img src="${thumbSrc(e.photo_path)}" data-img="${API}${e.photo_path}" loading="lazy" decoding="async" alt="">`
|
||||
: `<div class="thumb"><i data-lucide="graduation-cap"></i></div>`}
|
||||
<div class="r-info">
|
||||
<div class="r-name">${esc(e.student_name)}</div>
|
||||
@@ -95,7 +95,7 @@ function renderPhotos(rows) {
|
||||
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="">
|
||||
<img src="${thumbSrc(p.photo_path)}" data-img="${API}${p.photo_path}" loading="lazy" decoding="async" alt="">
|
||||
${p.caption ? `<div class="pcap">${esc(p.caption)}</div>` : ''}
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ function humanSize(b) {
|
||||
function fileCardHTML(f) {
|
||||
const url = `${API}/api/files/${f.token}`;
|
||||
const thumb = isImageFile(f.name)
|
||||
? `<img src="${url}" data-img="${esc(url)}" alt="">`
|
||||
? `<img src="${thumbSrc(url)}" data-img="${esc(url)}" loading="lazy" decoding="async" alt="">`
|
||||
: `<div class="file-icon">📄</div>`;
|
||||
const target = isImageFile(f.name)
|
||||
? `href="#" data-img="${esc(url)}"`
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ async function loadGroups() {
|
||||
return `
|
||||
<div class="group-card" data-group-id="${g.id}">
|
||||
${g.cover_path
|
||||
? `<img class="group-cover" src="${API}${g.cover_path}" alt="">`
|
||||
? `<img class="group-cover" src="${thumbSrc(g.cover_path)}" loading="lazy" decoding="async" alt="">`
|
||||
: `<div class="group-cover empty-cover">📷</div>`}
|
||||
<div class="group-head">
|
||||
<input class="name-input" id="name-${g.id}" value="${esc(g.name)}">
|
||||
@@ -151,7 +151,7 @@ function photoHTML(p) {
|
||||
return `
|
||||
<div class="photo-item" data-photo-id="${p.id}" data-photo-path="${esc(p.photo_path)}" draggable="true">
|
||||
<div class="drag-hint" title="Перетащите, чтобы изменить порядок">⠿</div>
|
||||
<img src="${API}${p.photo_path}" data-img="${API}${p.photo_path}" alt="">
|
||||
<img src="${thumbSrc(p.photo_path)}" data-img="${API}${p.photo_path}" loading="lazy" decoding="async" alt="">
|
||||
<div class="p-cap">${esc(p.caption || '')}</div>
|
||||
<div class="p-date">${taken ? taken + ' · ' : ''}${uploaded}</div>
|
||||
<div class="p-actions">
|
||||
|
||||
@@ -92,7 +92,7 @@ function gridEntryHTML(e) {
|
||||
const mp = mainPhotoPath(e);
|
||||
return `
|
||||
<div class="card">
|
||||
${mp ? `<img src="${API}${mp}" data-img="${API}${mp}" alt="">` : ''}
|
||||
${mp ? `<img src="${thumbSrc(mp)}" data-img="${API}${mp}" loading="lazy" decoding="async" alt="">` : ''}
|
||||
<div class="info">
|
||||
<div class="name">${esc(e.student_name)}</div>
|
||||
<div class="group">${esc(e.group_name)}</div>
|
||||
@@ -111,7 +111,7 @@ function cardEntryHTML(e) {
|
||||
return `
|
||||
<div class="journal-card">
|
||||
${mp
|
||||
? `<img src="${API}${mp}" data-img="${API}${mp}" alt="">`
|
||||
? `<img src="${thumbSrc(mp)}" data-img="${API}${mp}" 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>`}
|
||||
${e.group_name ? `<span class="group-badge" title="${esc(e.group_name)}">${esc(e.group_name)}</span>` : ''}
|
||||
<div class="body">
|
||||
@@ -252,7 +252,7 @@ function renderEditPhotos(photos, mainPath) {
|
||||
const applyStyle = isMain
|
||||
? 'background:#16a34a;border:none;color:#fff;border-radius:6px;padding:4px 10px;font-size:.75rem;cursor:default;white-space:nowrap'
|
||||
: 'background:var(--card);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:4px 10px;font-size:.75rem;cursor:pointer;white-space:nowrap';
|
||||
div.innerHTML = `<img src="${API}${esc(p.photo_path)}" style="width:48px;height:48px;object-fit:cover;border-radius:4px"> <span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(p.photo_path.split('/').pop())}</span> <button type="button" class="apply-photo-btn" style="${applyStyle}" title="${isMain ? 'Главное фото записи' : 'Сделать главным фото записи'}" ${isMain ? 'disabled' : ''}>${isMain ? 'Применено ✓' : 'Применить'}</button> <button type="button" class="delete-entry-photo" style="background:none;border:none;color:#ef4444;cursor:pointer;font-size:1.1rem;line-height:1" title="Удалить фото">✕</button>`;
|
||||
div.innerHTML = `<img src="${thumbSrc(p.photo_path)}" style="width:48px;height:48px;object-fit:cover;border-radius:4px"> <span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(p.photo_path.split('/').pop())}</span> <button type="button" class="apply-photo-btn" style="${applyStyle}" title="${isMain ? 'Главное фото записи' : 'Сделать главным фото записи'}" ${isMain ? 'disabled' : ''}>${isMain ? 'Применено ✓' : 'Применить'}</button> <button type="button" class="delete-entry-photo" style="background:none;border:none;color:#ef4444;cursor:pointer;font-size:1.1rem;line-height:1" title="Удалить фото">✕</button>`;
|
||||
preview.appendChild(div);
|
||||
const thumb = div.querySelector('img');
|
||||
const showMissing = () => {
|
||||
|
||||
+8
-2
@@ -2,6 +2,12 @@ function esc(s){return String(s==null?'':s).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 thumbSrc(p){
|
||||
if(!p)return p;
|
||||
if(p.startsWith('/uploads/'))return '/uploads/thumb/'+p.slice('/uploads/'.length);
|
||||
if(p.includes('/api/files/'))return p+(p.includes('?')?'&':'?')+'thumb=1';
|
||||
return p;
|
||||
}
|
||||
|
||||
let countdownInterval = null;
|
||||
let countdownTarget = null;
|
||||
@@ -216,7 +222,7 @@ function renderShare(data, password) {
|
||||
if (data.show_group_photos && data.photos && data.photos.length) {
|
||||
document.getElementById('photosGrid').innerHTML = data.photos.map(p => `
|
||||
<a class="gphoto" href="#" data-img="${esc(p.photo_path)}">
|
||||
<img src="${p.photo_path}" alt="${esc(p.caption || '')}" loading="lazy">
|
||||
<img src="${thumbSrc(p.photo_path)}" alt="${esc(p.caption || '')}" loading="lazy" decoding="async">
|
||||
${p.caption ? `<div class="gcap">${esc(p.caption)}</div>` : ''}
|
||||
</a>
|
||||
`).join('');
|
||||
@@ -237,7 +243,7 @@ function renderShare(data, password) {
|
||||
<div class="card">
|
||||
<div class="photo">
|
||||
${e.photo_path
|
||||
? `<img src="${e.photo_path}" data-img="${esc(e.photo_path)}" alt="">`
|
||||
? `<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">
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ function filesHTML(files) {
|
||||
function entryHTML(e) {
|
||||
return `
|
||||
<div class="card">
|
||||
${e.photo_path ? `<img src="${API}${e.photo_path}" data-img="${API}${e.photo_path}" alt="">` : ''}
|
||||
${e.photo_path ? `<img src="${thumbSrc(e.photo_path)}" data-img="${API}${e.photo_path}" loading="lazy" decoding="async" alt="">` : ''}
|
||||
<div class="info">
|
||||
<div class="name">${esc(e.student_name)}</div>
|
||||
<div class="group">${esc(e.group_name)}</div>
|
||||
|
||||
Reference in New Issue
Block a user