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
This commit is contained in:
146
app/components/news-feed.php
Normal file
146
app/components/news-feed.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Zeilen-Liste für die News-Seite: mischt redaktionelle Meldungen, Instagram-
|
||||
* Posts und Ergebnisse. Props:
|
||||
* $items array{date, title, text, image?, href?, link_text?, source}
|
||||
* `link_text` (optional) ist die sichtbare Weiterlesen-Zeile —
|
||||
* gesetzt vom Normalizer, nicht hier abgeleitet: die Komponente
|
||||
* soll die Quellen nicht kennen (gleiche Prop wie in
|
||||
* upcoming_highlights()/event-feature)
|
||||
* $empty_text string (optional)
|
||||
* $featured bool (optional) — neuester Eintrag als großer Aufmacher über
|
||||
* der Liste (gleiche Hierarchie-Idee wie das „Nächste Spiele"-
|
||||
* Band: ein Aufmacher, darunter Zeilen)
|
||||
* $filterable bool (optional) — Filter-Leiste nach Quelle (Alle/Meldungen/
|
||||
* Ergebnisse/Instagram), gleiche Bausteine wie der Stellen-Filter
|
||||
* auf /mitmachen (.filter-bar + .tab, assets/js/news-filter.js).
|
||||
* Ohne JS sind alle Einträge sichtbar — reine Progressive
|
||||
* Enhancement. Die Seite muss news-filter.js laden.
|
||||
* Kennt nichts von den drei Quellstrukturen — bekommt nur die normalisierte
|
||||
* Form (app/pages/news.php baut sie).
|
||||
*
|
||||
* Bewusst eine Zeilen-Liste statt eines Karten-Grids (wie termine-list.php):
|
||||
* die Einträge sind zu unterschiedlich hoch (hohe Bild-Posts neben kurzen
|
||||
* Text-Ergebnissen) — ein Grid reißt dabei Löcher, weil kurze Karten den
|
||||
* Platz neben einer hohen Karte nicht auffüllen können. Eine einspaltige
|
||||
* Liste hat dieses Problem nicht, jede Zeile ist einfach so hoch wie ihr
|
||||
* eigener Inhalt.
|
||||
*/
|
||||
$items = $items ?? [];
|
||||
$emptyText = $empty_text ?? 'Aktuell keine News.';
|
||||
$featured = !empty($featured);
|
||||
$filterable = !empty($filterable);
|
||||
|
||||
$sourceLabels = [
|
||||
'news' => 'Meldung',
|
||||
'instagram' => 'Instagram',
|
||||
'ergebnis' => 'Ergebnis',
|
||||
];
|
||||
$filters = [
|
||||
'all' => 'Alle',
|
||||
'news' => 'Meldungen',
|
||||
'ergebnis' => 'Ergebnisse',
|
||||
'instagram' => 'Instagram',
|
||||
];
|
||||
?>
|
||||
<?php if ($items === []): ?>
|
||||
<p class="text-muted"><?= e($emptyText) ?></p>
|
||||
<?php else: ?>
|
||||
<?php if ($filterable): ?>
|
||||
<div class="filter-bar" role="group" aria-label="News nach Quelle filtern" data-news-filter>
|
||||
<?php $i = 0; foreach ($filters as $value => $label): ?>
|
||||
<button type="button" class="tab filter-bar__tab" data-filter="<?= e($value) ?>" aria-pressed="<?= $i++ === 0 ? 'true' : 'false' ?>"><?= e($label) ?></button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($featured):
|
||||
$feature = array_shift($items);
|
||||
$featureTag = !empty($feature['href']) ? 'a' : 'div';
|
||||
$featureExternal = $feature['source'] === 'instagram';
|
||||
$featureAttrs = !empty($feature['href'])
|
||||
? ' href="' . e($feature['href']) . '"' . ($featureExternal ? ' target="_blank" rel="noopener"' : '')
|
||||
: '';
|
||||
$featureHasMedia = !empty($feature['image']) || !empty($feature['crest']);
|
||||
?>
|
||||
<div class="news-feature" data-source="<?= e($feature['source']) ?>">
|
||||
<<?= $featureTag ?> class="news-feature__link<?= $featureHasMedia ? ' news-feature__link--media' : '' ?>"<?= $featureAttrs ?>>
|
||||
<?php if (!empty($feature['image'])): ?>
|
||||
<div class="news-feature__media">
|
||||
<?php component('img', ['image' => $feature['image'], 'sizes' => '(max-width: 768px) 100vw, 40vw', 'class' => 'news-feature__img', 'eager' => true]); ?>
|
||||
</div>
|
||||
<?php elseif (!empty($feature['crest'])): ?>
|
||||
<div class="news-feature__media news-feature__media--crest">
|
||||
<?php component('crest', ['src' => $feature['crest']['src'], 'name' => $feature['crest']['name']]); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="news-feature__body">
|
||||
<p class="news-row__meta">
|
||||
<span class="news-row__badge news-row__badge--<?= e($feature['source']) ?>"><?= e($sourceLabels[$feature['source']] ?? $feature['source']) ?></span>
|
||||
<?php if ($feature['date'] !== ''): ?>
|
||||
<time datetime="<?= e($feature['date']) ?>"><?= e(german_date($feature['date'])) ?></time>
|
||||
<?php endif; ?>
|
||||
<?php if ($featureExternal): ?>
|
||||
<span class="visually-hidden"> (öffnet in neuem Tab)</span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<p class="news-feature__title"><?= e($feature['title']) ?></p>
|
||||
<?php if (!empty($feature['text']) && $feature['text'] !== $feature['title']): ?>
|
||||
<p class="news-feature__text"><?= e($feature['text']) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($feature['link_text'])): ?>
|
||||
<p class="news-feature__more"><?= e($feature['link_text']) ?> <?= icon('arrow-right') ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</<?= $featureTag ?>>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<ul class="news-list">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<?php
|
||||
$tag = !empty($item['href']) ? 'a' : 'div';
|
||||
$external = $item['source'] === 'instagram';
|
||||
$attrs = '';
|
||||
if (!empty($item['href'])) {
|
||||
$attrs = ' href="' . e($item['href']) . '"' . ($external ? ' target="_blank" rel="noopener"' : '');
|
||||
}
|
||||
?>
|
||||
<li class="news-row" data-source="<?= e($item['source']) ?>">
|
||||
<<?= $tag ?> class="news-row__link"<?= $attrs ?>>
|
||||
<?php if (!empty($item['image'])): ?>
|
||||
<div class="news-row__media">
|
||||
<?php component('img', ['image' => $item['image'], 'sizes' => '96px', 'class' => 'news-row__img']); ?>
|
||||
</div>
|
||||
<?php elseif (!empty($item['crest'])): ?>
|
||||
<div class="news-row__media news-row__media--crest">
|
||||
<?php component('crest', ['src' => $item['crest']['src'], 'name' => $item['crest']['name']]); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="news-row__body">
|
||||
<p class="news-row__meta">
|
||||
<span class="news-row__badge news-row__badge--<?= e($item['source']) ?>"><?= e($sourceLabels[$item['source']] ?? $item['source']) ?></span>
|
||||
<?php if ($item['date'] !== ''): ?>
|
||||
<time datetime="<?= e($item['date']) ?>"><?= e(german_date($item['date'])) ?></time>
|
||||
<?php endif; ?>
|
||||
<?php if ($external): ?>
|
||||
<span class="visually-hidden"> (öffnet in neuem Tab)</span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<p class="news-row__title"><?= e($item['title']) ?></p>
|
||||
<?php if (!empty($item['text']) && $item['text'] !== $item['title']): ?>
|
||||
<p class="news-row__text"><?= e($item['text']) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($item['link_text'])): ?>
|
||||
<p class="news-row__more"><?= e($item['link_text']) ?> <?= icon('arrow-right') ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</<?= $tag ?>>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php if ($filterable): ?>
|
||||
<p class="text-muted" data-news-empty hidden>In dieser Kategorie gibt es gerade nichts Neues.</p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
Reference in New Issue
Block a user