From 84e6bfdd82aa89bc25cb1f6175633d9a6cb65596 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 3 May 2026 21:23:41 +0200 Subject: [PATCH] Feature: Wiki Photo-Gallery mit Thumbnails + Lightbox, alle Fotos einer Rasse anklickbar (SW by-v664) --- backend/static/css/components.css | 133 ++++++++++++++++++++++++++++++ backend/static/index.html | 8 +- backend/static/js/app.js | 2 +- backend/static/js/pages/wiki.js | 111 +++++++++++++++++++------ backend/static/sw.js | 2 +- 5 files changed, 223 insertions(+), 33 deletions(-) diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 2ea6a50..c8de6a6 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -5571,6 +5571,139 @@ html.modal-open { border-radius: 0; } +/* ── Wiki Gallery ────────────────────────────────────────── */ +.wiki-gallery-wrap { + position: relative; + margin-bottom: var(--space-3); +} +.wiki-gallery-main { + width: 100%; + height: 240px; + object-fit: cover; + object-position: center top; + border-radius: var(--radius-lg); + display: block; +} +.wiki-gallery-strip { + display: flex; + gap: var(--space-2); + overflow-x: auto; + padding: var(--space-2) 0 0; + scrollbar-width: none; +} +.wiki-gallery-strip::-webkit-scrollbar { display: none; } +.wiki-gallery-thumb { + flex-shrink: 0; + width: 64px; height: 64px; + border-radius: var(--radius-md); + overflow: hidden; + border: 2px solid transparent; + padding: 0; + background: none; + cursor: pointer; + position: relative; + transition: border-color .15s; +} +.wiki-gallery-thumb.active { border-color: var(--c-primary); } +.wiki-gallery-thumb img { + width: 100%; height: 100%; object-fit: cover; +} +.wiki-gallery-thumb-label { + position: absolute; + bottom: 0; left: 0; right: 0; + background: rgba(0,0,0,.55); + color: #fff; + font-size: 8px; + padding: 2px 4px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.wiki-gallery-expand { + position: absolute; + top: var(--space-2); + right: var(--space-2); + width: 34px; height: 34px; + border-radius: 50%; + background: rgba(0,0,0,.45); + border: none; + color: #fff; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(4px); + transition: background .15s; +} +.wiki-gallery-expand:hover { background: rgba(0,0,0,.65); } + +/* ── Wiki Lightbox ───────────────────────────────────────── */ +#wiki-lightbox { + position: fixed; + inset: 0; + z-index: 2000; + display: flex; + align-items: center; + justify-content: center; +} +.wlb-backdrop { + position: absolute; + inset: 0; + background: rgba(0,0,0,.88); + backdrop-filter: blur(6px); +} +.wlb-content { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + max-width: min(92vw, 680px); + width: 100%; + gap: var(--space-2); +} +.wlb-img { + width: 100%; + max-height: 72vh; + object-fit: contain; + border-radius: var(--radius-lg); +} +.wlb-close { + position: absolute; + top: -44px; + right: 0; + background: rgba(255,255,255,.12); + border: none; + color: #fff; + width: 36px; height: 36px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} +.wlb-prev, .wlb-next { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: rgba(255,255,255,.12); + border: none; + color: #fff; + width: 40px; height: 40px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.4rem; + transition: background .15s; +} +.wlb-prev { left: -48px; } +.wlb-next { right: -48px; } +.wlb-prev:hover, .wlb-next:hover { background: rgba(255,255,255,.25); } +.wlb-caption { color: rgba(255,255,255,.75); font-size: var(--text-sm); } +.wlb-counter { color: rgba(255,255,255,.45); font-size: var(--text-xs); } + /* Steckbrief-Grid */ .wiki-steckbrief-grid { display: grid; diff --git a/backend/static/index.html b/backend/static/index.html index bea06b5..824ed47 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -93,9 +93,9 @@ - - - + + + @@ -562,7 +562,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 04d872e..37ede79 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '663'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '664'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.3.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; diff --git a/backend/static/js/pages/wiki.js b/backend/static/js/pages/wiki.js index b2b6390..b0c101d 100644 --- a/backend/static/js/pages/wiki.js +++ b/backend/static/js/pages/wiki.js @@ -730,36 +730,34 @@ window.Page_wiki = (() => { : ''; const _dogSvgLg = _DOG_SILHOUETTE.replace('width="48" height="48"', 'width="56" height="56"'); - const photoHtml = rasse.foto_url - ? `
- ${_esc(rasse.name)} -
- ` + // Alle Fotos: Hauptbild zuerst, dann Community-Fotos + const allFotos = []; + if (rasse.foto_url) allFotos.push({ foto_url: rasse.foto_url, user_name: null }); + (rasse.user_fotos || []).forEach(f => allFotos.push(f)); + + const photoHtml = allFotos.length + ? `` : `
${_dogSvgLg}Kein Foto verfügbar
`; const berichteHtml = _renderBerichteHtml(rasse.berichte || [], slug); - - const userFotosHtml = (rasse.user_fotos || []).length - ? `
-
Community-Fotos
-
- ${rasse.user_fotos.map(f => ` -
- ${_esc(f.user_name)} -
von ${_esc(f.user_name)}
-
- `).join('')} -
-
` - : ''; + const userFotosHtml = ''; const body = ` ${/* 1. Hero */ ''} @@ -851,6 +849,65 @@ window.Page_wiki = (() => { document.getElementById('wiki-zuchter-placeholder')?.remove(); }); + // Gallery-Thumbnails + Lightbox + const mainImg = document.getElementById('wiki-main-photo'); + const strip = document.getElementById('wiki-gallery-strip'); + if (strip && mainImg) { + strip.querySelectorAll('.wiki-gallery-thumb').forEach(btn => { + btn.addEventListener('click', () => { + const idx = parseInt(btn.dataset.idx); + mainImg.src = allFotos[idx].foto_url; + mainImg.style.display = ''; + document.getElementById('wiki-photo-fallback').style.display = 'none'; + strip.querySelectorAll('.wiki-gallery-thumb').forEach(b => b.classList.toggle('active', b === btn)); + }); + }); + } + + document.getElementById('wiki-gallery-expand')?.addEventListener('click', () => { + const src = mainImg?.src || allFotos[0]?.foto_url; + if (!src) return; + let curIdx = allFotos.findIndex(f => f.foto_url && src.endsWith(f.foto_url.split('/').pop())); + if (curIdx < 0) curIdx = 0; + + function _lbOpen(idx) { + const f = allFotos[idx]; + const lb = document.getElementById('wiki-lightbox'); + lb.querySelector('.wlb-img').src = f.foto_url; + lb.querySelector('.wlb-caption').textContent = f.user_name ? `Foto von ${f.user_name}` : rasse.name; + lb.querySelector('.wlb-counter').textContent = `${idx + 1} / ${allFotos.length}`; + lb.querySelector('.wlb-prev').style.display = allFotos.length > 1 ? '' : 'none'; + lb.querySelector('.wlb-next').style.display = allFotos.length > 1 ? '' : 'none'; + curIdx = idx; + } + + const lb = document.createElement('div'); + lb.id = 'wiki-lightbox'; + lb.innerHTML = ` +
+
+ + + + +
+
+
`; + document.body.appendChild(lb); + _lbOpen(curIdx); + + const close = () => lb.remove(); + lb.querySelector('.wlb-close').addEventListener('click', close); + lb.querySelector('.wlb-backdrop').addEventListener('click', close); + lb.querySelector('.wlb-prev').addEventListener('click', () => _lbOpen((curIdx - 1 + allFotos.length) % allFotos.length)); + lb.querySelector('.wlb-next').addEventListener('click', () => _lbOpen((curIdx + 1) % allFotos.length)); + document.addEventListener('keydown', function onKey(e) { + if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); } + if (e.key === 'ArrowLeft') lb.querySelector('.wlb-prev').click(); + if (e.key === 'ArrowRight') lb.querySelector('.wlb-next').click(); + }); + }); + document.getElementById('wiki-bericht-add-btn')?.addEventListener('click', () => { UI.modal.close(); setTimeout(() => _showBerichtForm(slug, rasse.name), 350); diff --git a/backend/static/sw.js b/backend/static/sw.js index 92786e4..6cb3317 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v663'; +const CACHE_VERSION = 'by-v664'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache