Files
fs a101b0c3ec Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.

Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.

News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.

Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
  Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
  (Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
  Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
  unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
  umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.

Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00

152 lines
5.5 KiB
JavaScript

/* Lightbox: rüstet eine Bildergalerie ([data-lightbox-gallery]) von normalen
Bildlinks auf eine Großansicht im Overlay um. Ohne JS bleibt jedes
Vorschaubild ein Link auf die größte Variante der Datei — der Klick führt
dann einfach zum Bild, nichts ist kaputt oder tot.
Bewusst generisch gehalten (keine „veranstaltung"-Bezüge, Hooks nur über
data-lightbox-*), damit eine zweite Galerie auf der Seite dieselbe Datei
nutzen kann — dasselbe Prinzip wie beim generischen carousel.js.
Das Overlay-Markup liefert PHP und ist `hidden`, bis JS es übernimmt (wie die
Steuerung des Stadionzeitungs-Viewers). Die Großansicht erbt ihr srcset vom
angeklickten Vorschaubild: so lädt ein Handy die kleine Variante und es gibt
nur EINE Bildliste im Markup. */
(function () {
'use strict';
function initGallery(root) {
var links = Array.prototype.slice.call(root.querySelectorAll('[data-lightbox]'));
var dialog = root.querySelector('[data-lightbox-dialog]');
if (!dialog || links.length === 0) {
return;
}
var image = dialog.querySelector('[data-lightbox-img]');
var counter = dialog.querySelector('[data-lightbox-current]');
var closeBtn = dialog.querySelector('[data-lightbox-close]');
var prevBtn = dialog.querySelector('[data-lightbox-prev]');
var nextBtn = dialog.querySelector('[data-lightbox-next]');
if (!image || !closeBtn) {
return;
}
var index = 0;
var isOpen = false;
function show(i) {
index = Math.max(0, Math.min(links.length - 1, i));
var thumb = links[index].querySelector('img');
image.src = links[index].getAttribute('href');
image.srcset = thumb ? thumb.getAttribute('srcset') || '' : '';
image.alt = thumb ? thumb.getAttribute('alt') || '' : '';
if (counter) {
counter.textContent = String(index + 1);
}
if (prevBtn) {
prevBtn.disabled = index === 0;
}
if (nextBtn) {
nextBtn.disabled = index === links.length - 1;
}
// Am ersten/letzten Bild wird der gerade fokussierte Knopf disabled
// und der Fokus fiele auf <body> — dann griffe die Fokusfalle unten
// nicht mehr und Tab liefe hinter das Overlay.
if (document.activeElement && document.activeElement.disabled) {
closeBtn.focus();
}
}
function onKeydown(event) {
if (event.key === 'Escape') {
event.preventDefault();
hide();
return;
}
if (event.key === 'ArrowLeft') {
event.preventDefault();
show(index - 1);
return;
}
if (event.key === 'ArrowRight') {
event.preventDefault();
show(index + 1);
return;
}
if (event.key !== 'Tab') {
return;
}
// Fokusfalle: im Overlay sind nur diese Knöpfe erreichbar.
var focusable = [prevBtn, nextBtn, closeBtn].filter(function (btn) {
return btn && !btn.disabled;
});
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
function reveal(i) {
show(i);
dialog.hidden = false;
// Hintergrund festhalten, sonst scrollt die Seite unter dem Overlay
// weg (gleiches Muster wie body.nav-open beim Burger-Menü).
document.body.classList.add('lightbox-open');
document.addEventListener('keydown', onKeydown);
isOpen = true;
closeBtn.focus();
}
function hide() {
if (!isOpen) {
return;
}
dialog.hidden = true;
document.body.classList.remove('lightbox-open');
document.removeEventListener('keydown', onKeydown);
isOpen = false;
// Fokus zurück auf das ZULETZT gezeigte Vorschaubild, nicht auf das
// zuerst angeklickte: nach mehrmals Weiter ist das die Stelle, an
// der man im Raster gerade steht.
links[index].focus();
}
links.forEach(function (link, i) {
link.addEventListener('click', function (event) {
event.preventDefault();
reveal(i);
});
});
if (prevBtn) {
prevBtn.addEventListener('click', function () { show(index - 1); });
}
if (nextBtn) {
nextBtn.addEventListener('click', function () { show(index + 1); });
}
closeBtn.addEventListener('click', hide);
dialog.addEventListener('click', function (event) {
// Klick auf die Fläche neben dem Bild schließt; Klicks auf Bild und
// Knöpfe nicht.
if (event.target === dialog) {
hide();
}
});
root.classList.add('is-enhanced');
}
Array.prototype.slice.call(document.querySelectorAll('[data-lightbox-gallery]')).forEach(initGallery);
})();