Sportfest-Ausgabe der Stadionzeitung zum Heimspiel gegen ATS Wartenfels

Neue Live-Seiten, geschaltet vom chat-gepflegten Feld `veranstaltung` (Slug aus
data/veranstaltungen.json) — Programm und Turnierpläne kommen damit aus derselben
Quelle wie die Website, nicht als Kopie ins Heft:

- Programm-Seite (programm[], rendert zugleich auf /veranstaltungen/sportfest-2026)
- vier Turnierplan-Seiten (turniere[]) mit leerer Ergebnisspalte und leerer
  Abschlusstabelle zum Eintragen am Platz
- Gegnerseite je Mannschaft mit Heimspiel am Fest statt nur der tragenden:
  stadionzeitung_gegner_fixtures() ist die einzige Stelle, die das entscheidet,
  Seitenplanung und Snapshot lesen beide dort. `gegner` ist deshalb eine Map je
  Team-Key; die Vorschau überspringt jedes Spiel mit eigener Gegnerseite
- Danke-Seite mit Partner-Logo-Wand aus data/partners.json und QR auf
  /partner-werden, dazu das Anzeigen-Feld `platzierung` für Motive mit festem
  Platz auf einer Live-Seite (Emons über der Logo-Wand). Vorher klebte der Dank
  als leere Fläche unten an einem Anzeigen-Bild
- Cover einer Sonderausgabe: das Fest als Titelgeschichte statt einer der drei
  Begegnungen, dazu das Hauptplakat schräg gelegt an Stelle des Spielerfotos

Ausgabe 2026-09-04-ats-wartenfels als ENTWURF angelegt, 48 Seiten (12 Bögen),
Tabellen auf gerader Seite, längster Anzeigenblock zwei. Veröffentlicht wird auf
dem Server, dort liegt der frische Matchcenter-Stand.

Nebenbei behoben: bin/anzeige-add.php räumte beim Anlegen alle Motive weg, deren
Slug mit demselben Text beginnt (`emons` löschte `emons-partnerdank` und
`emons-stuebinger`). Muster jetzt auf die Breiten-Endung eingeschränkt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KubsajzuV6N6QsSd3xKNjC
This commit is contained in:
2026-09-03 10:57:52 +02:00
parent b4d2cde149
commit f2f193b223
40 changed files with 2273 additions and 77 deletions

View File

@@ -36,11 +36,12 @@ $issues = $issues ?? [];
<time datetime="<?= e($issue['date']) ?>"><?= e(german_date($issue['date'])) ?></time>
· <?= (int) $issue['pages'] ?> Seiten
</p>
<?php /* Coverzeile des Interviews als Teaser — dieselbe Quelle, die auch auf dem
Cover unter „Im Heft" steht. Sie füllt die breite Zeile mit echtem Inhalt
statt Leerfläche und sagt, warum man die Ausgabe öffnen sollte. Trägt eine
Ausgabe kein Interview, entfällt die Zeile (wie bei .veranstaltung-card__text). */
$kartenTeaser = trim((string) ($issue['interview']['teaser'] ?? '')); ?>
<?php /* Coverzeile als Teaser — dieselbe Quelle (stadionzeitung_teaser()), die auch
auf dem Cover unter „Im Heft" steht: cover.teaser einer Sonderausgabe, sonst
die Coverzeile des Interviews. Sie füllt die breite Zeile mit echtem Inhalt
statt Leerfläche und sagt, warum man die Ausgabe öffnen sollte. Gibt es
beides nicht, entfällt die Zeile (wie bei .veranstaltung-card__text). */
$kartenTeaser = stadionzeitung_teaser($issue); ?>
<?php if ($kartenTeaser !== ''): ?>
<p class="stadionzeitung-card__text"><?= e($kartenTeaser) ?></p>
<?php endif; ?>

View File

@@ -69,18 +69,64 @@ if ($match !== null) {
// Gedruckte Zeile neben dem QR-Code — immer die echte Domain (club_domain()).
$linkText = club_domain();
$kicker = trim($season . ($date !== '' ? ' · ' . german_date($date) : ''));
// Aufmacher unten links: normal die Begegnung (Wappen + „Heimspiel gegen …").
// Zeigt die Ausgabe auf eine Veranstaltung (Sportfest-Ausgabe), tritt an deren
// Stelle das FEST als Titelgeschichte — denn ein Fest bringt mehrere Heimspiele
// mit (Erste, Zweite, Frauen), und ein einzelnes davon aufs Cover zu heben wäre
// falsch (Felix, 03.09.2026). Titel und Zeitraum kommen aus
// data/veranstaltungen.json, nie neu getippt.
$feature = null;
$featureV = veranstaltung_find((string) ($issue['veranstaltung'] ?? ''));
if ($featureV !== null) {
$feature = [
'title' => trim((string) ($featureV['title'] ?? '')),
'range' => german_date_range((string) ($featureV['date_start'] ?? ''), $featureV['date_end'] ?? null),
];
}
// Bildmotiv rechts: normal das freigestellte Spielerfoto. Eine Sonderausgabe mit
// Veranstaltung zeigt stattdessen deren HAUPTPLAKAT (erstes Plakat, dieselbe
// Quelle wie /veranstaltungen/<slug>, nie eine Kopie) — leicht schräg gelegt mit
// zwei abgerundeten Ecken, wie ein aufs Cover gelegter Aushang (Felix,
// 03.09.2026). Ohne Plakat bleibt es beim Spielerfoto, ohne beides trägt der
// Hintergrund die Seite allein.
$poster = null;
if ($featureV !== null) {
$ersteseite = $featureV['plakate'][0] ?? null;
if (is_array($ersteseite) && !empty($ersteseite['base'])) {
$poster = $ersteseite;
}
}
// Kicker: Saison · Datum, auf jedem Cover gleich. Eine Sonderausgabe bekommt
// bewusst KEINE zusätzliche Kennung im Kopf: ein roter Kasten „Sportfest-Ausgabe"
// war gebaut und am 03.09.2026 wieder entfernt (Felix) — er stach aus dem
// ruhigen Kopf heraus, und die Ausgabe kennzeichnet sich unten links ohnehin
// selbst durch den großen Fest-Titel.
$kicker = implode(' · ', array_filter([
$season,
$date !== '' ? german_date($date) : '',
], static fn (string $s): bool => $s !== ''));
// Zweite Coverzeile (Magazin-Praxis: Teaser auf die Geschichte im Heft) —
// erscheint automatisch, sobald die Ausgabe ein Interview trägt
// (interview-Feld, chat-gepflegt wie das Vorwort).
$teaser = trim((string) ($issue['interview']['teaser'] ?? ''));
// aus stadionzeitung_teaser(): `cover.teaser` einer Sonderausgabe, sonst die
// Coverzeile des Interviews. Dieselbe Funktion speist die Archivkarte.
$teaser = stadionzeitung_teaser($issue);
?>
<div class="stadionzeitung-cover">
<div class="stadionzeitung-cover<?= $poster !== null ? ' stadionzeitung-cover--poster' : '' ?>">
<?php if ($heroImage): ?>
<?php component('img', ['image' => $heroImage, 'sizes' => '480px', 'class' => 'stadionzeitung-cover__bg']); ?>
<?php endif; ?>
<?php if (!empty($playerImage)): ?>
<?php if ($poster !== null): ?>
<?php /* Alt-Text bewusst leer: der Plakat-Alt-Text aus veranstaltungen.json
beschreibt die Termine, die drei Zeilen weiter unten als Text stehen —
auf dem Cover wäre er eine Dopplung fürs Screenreader-Ohr. */ ?>
<?php component('img', [
'image' => ['base' => $poster['base'], 'widths' => $poster['widths'] ?? [], 'alt' => ''],
'class' => 'stadionzeitung-cover__poster',
'sizes' => '(max-width: 600px) 55vw, 260px',
]); ?>
<?php elseif (!empty($playerImage)): ?>
<?php
/* Varianten-Set (base/widths/ext=png) statt der Originaldatei. Das Original ist
ein Freisteller mit 2584x5093 Pixeln und 12 MB: iOS Safari verwirft Bilder,
@@ -112,6 +158,17 @@ $teaser = trim((string) ($issue['interview']['teaser'] ?? ''));
<p class="stadionzeitung-cover__tagline">Rund ums Heimspiel im Katzbachtal</p>
</header>
<div class="stadionzeitung-cover__corner">
<?php if ($feature !== null): ?>
<?php /* Sonderausgabe: das Fest ist die Titelgeschichte, nicht eine der
Begegnungen. Titel in der Display-Schrift (Versalien wie der Masthead),
Zeitraum darunter als gesperrte Zeile. */ ?>
<div class="stadionzeitung-cover__feature">
<p class="stadionzeitung-cover__feature-title"><?= e($feature['title']) ?></p>
<?php if ($feature['range'] !== ''): ?>
<p class="stadionzeitung-cover__meta"><?= e($feature['range']) ?></p>
<?php endif; ?>
</div>
<?php else: ?>
<div class="stadionzeitung-cover__headline">
<?php /* Bewusst ohne Trennzeichen zwischen den Wappen (Felix, 29.07.2026:
ruhiger) — die beiden Logos sprechen für sich. */ ?>
@@ -124,6 +181,7 @@ $teaser = trim((string) ($issue['interview']['teaser'] ?? ''));
<p class="stadionzeitung-cover__meta"><?= e(trim($teamName . ($league !== '' ? ' · ' . $league : ''))) ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($teaser !== ''): ?>
<p class="stadionzeitung-cover__teaser">
<span class="stadionzeitung-cover__teaser-label">Im Heft</span>

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/**
* Danke-Seite der Stadionzeitung (Live-Seite): der Dank an unsere Partner mit
* der Logo-Wand aus data/partners.json (Single Source, dieselbe Quelle wie die
* Startseite) und dem QR auf /partner-werden. Gegenstück zur Akquise-Seite
* stadionzeitung-partner.php: hier stehen die, die schon dabei sind.
*
* Vorgeschichte: der Dank war Teil eines Canva-Bilds, das unten an die
* Emons-Anzeige geklebt war — eine leere Fläche mit Wappen und zwei Zeilen
* (Felix, 03.09.2026: „sieht schrecklich aus"). Seitdem eine echte Seite, die
* die Partner auch zeigt statt nur zu erwähnen.
*
* Die Anzeige oben ist KEIN Bestandteil der Komponente, sondern kommt als Prop:
* bin/stadionzeitung-add.php nimmt Anzeigen mit `platzierung: danke` aus dem
* Streu-Pool und schreibt das Motiv in den Seiteneintrag. Dadurch ist es in
* images[] eingefroren wie jede andere Anzeige — eine veröffentlichte Ausgabe
* ändert sich nicht mehr, wenn der Sponsor später ein neues Motiv liefert.
*
* Props: $anzeige (Varianten-Set {base,widths,alt} oder null)
*/
$anzeige = (is_array($anzeige ?? null) && !empty($anzeige['base'])) ? $anzeige : null;
$partners = json_load('partners')['partners'] ?? [];
$qr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-partner-werden.png')
? 'img/stadionzeitung/qr-partner-werden.png'
: null;
?>
<div class="stadionzeitung-danke">
<?php if ($anzeige !== null): ?>
<?php component('img', ['image' => $anzeige, 'sizes' => '420px', 'class' => 'stadionzeitung-danke__anzeige']); ?>
<?php endif; ?>
<header class="stadionzeitung-page__head stadionzeitung-danke__head">
<p class="stadionzeitung-page__kicker">Unsere Partner</p>
<?php /* Überschrift bewusst OHNE Anrede: Partner sind Unternehmen, die wir mit
„Sie" ansprechen (Sprachregel, CLAUDE.md), ein „danke, dass ihr dabei
seid" würde sie duzen. „Danke für die Unterstützung" trägt den Dank
ohne diesen Konflikt, der CTA darunter bleibt in der Sie-Form. */ ?>
<h2 class="stadionzeitung-page__heading">Danke für die Unterstützung</h2>
</header>
<?php if ($partners !== []): ?>
<ul class="stadionzeitung-danke__logos">
<?php foreach ($partners as $partner): ?>
<?php if (empty($partner['logo'])) { continue; } ?>
<li class="stadionzeitung-danke__logo">
<img src="<?= e(asset((string) $partner['logo'])) ?>" alt="<?= e((string) ($partner['name'] ?? '')) ?>"<?= img_intrinsic_attrs((string) $partner['logo']) ?> loading="lazy" decoding="async">
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<div class="stadionzeitung-page__cta">
<div class="stadionzeitung-page__cta-text">
<p class="stadionzeitung-page__cta-label">Mitmachen</p>
<p class="stadionzeitung-page__cta-title">Werden Sie unser Partner</p>
<p class="stadionzeitung-page__cta-link"><?= e(club_domain('partner-werden')) ?></p>
</div>
<?php if ($qr !== null): ?>
<div class="stadionzeitung-page__cta-abriss">
<img class="stadionzeitung-page__cta-qr" src="<?= e(asset($qr)) ?>" alt="" width="60" height="60" loading="lazy" decoding="async">
<span class="stadionzeitung-page__cta-scan">Scan mich</span>
</div>
<?php endif; ?>
</div>
</div>

View File

@@ -7,9 +7,14 @@ declare(strict_types=1);
* aufgebauten Stadionzeitungs-Snapshot; die Komponente rendert nur. Dadurch
* sind Entwurf und veröffentlichte Druckausgabe konsistent und archivfest.
*
* Props: $info stadionzeitung_live_data($issue)['gegner']
* Props: $info stadionzeitung_live_data($issue)['gegner'][team] — ein Eintrag der
* Map je Team-Key (team_name/opponent/league/season/fixture/own/
* opponent_stats). Normal nur die tragende Mannschaft; in der
* Sportfest-Ausgabe eine Seite je Heimspiel am Fest, deshalb
* nennt der Kicker die Mannschaft.
*/
$info = is_array($info ?? null) ? $info : [];
$teamName = trim((string) ($info['team_name'] ?? ''));
$fixture = is_array($info['fixture'] ?? null) ? $info['fixture'] : [];
$own = is_array($info['own'] ?? null) ? $info['own'] : null;
$opponent = is_array($info['opponent_stats'] ?? null) ? $info['opponent_stats'] : null;
@@ -39,7 +44,7 @@ $format = static function (?array $row, string $key, bool $signed = false): stri
?>
<div class="stadionzeitung-gegner">
<header class="stadionzeitung-page__head">
<p class="stadionzeitung-page__kicker">Unser Gegner</p>
<p class="stadionzeitung-page__kicker"><?= e($teamName !== '' ? 'Unser Gegner · ' . $teamName : 'Unser Gegner') ?></p>
<h2 class="stadionzeitung-page__heading"><?= e($opponentName) ?></h2>
<?php if (!empty($info['league']) || !empty($info['season'])): ?>
<p class="stadionzeitung-page__sub"><?= e(trim(implode(' · ', array_filter([(string) ($info['league'] ?? ''), (string) ($info['season'] ?? '')])))) ?></p>
@@ -70,9 +75,12 @@ $format = static function (?array $row, string $key, bool $signed = false): stri
<?php if ($own !== null && $opponent !== null): ?>
<div class="stadionzeitung-gegner__vergleich" role="table" aria-label="Tabellenvergleich">
<div class="stadionzeitung-gegner__vergleich-head" role="row">
<span role="columnheader">TSV 08</span>
<?php /* Spaltenköpfe generisch: vorher standen hier „TSV 08" und „ASV" fest im
Markup — das Kürzel des Marktschorgast-Gegners wäre in jeder Ausgabe
mitgedruckt worden. Die Überschrift nennt den Gegner ohnehin. */ ?>
<span role="columnheader">Wir</span>
<span role="columnheader">Vergleich</span>
<span role="columnheader">ASV</span>
<span role="columnheader">Gegner</span>
</div>
<?php foreach ($rows as $row): ?>
<div class="stadionzeitung-gegner__vergleich-row" role="row">

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/**
* Programm-Seite der Stadionzeitung (Live-Seite, Sportfest-Ausgabe): das
* Tagesprogramm einer Veranstaltung, je Tag eine Gruppe (Wochentag + Datum),
* darunter Zeit | Titel (+ Zusatz). Kopf im Muster der Geschwister-Seiten.
*
* Quelle ist AUSSCHLIESSLICH data/veranstaltungen.json → programm[] der
* Veranstaltung, auf die die Ausgabe zeigt (`veranstaltung`-Feld, Slug). Dieselbe
* Liste rendert die Website-Detailseite über veranstaltung-programm.php — eine
* Quelle für Heft und Website, hier nur eine zweite Darstellung fürs A5-Blatt
* (cqi-Maße statt Website-Tokens). Bewusst live, kein Snapshot: chat-gepflegte
* Daten wie bei kontakt/aufruf, kein Sync räumt hier etwas weg.
*
* Props: $issue (Eintrag aus data/stadionzeitung.json)
*/
$issue = $issue ?? [];
$v = veranstaltung_find((string) ($issue['veranstaltung'] ?? ''));
$programm = is_array($v['programm'] ?? null) ? $v['programm'] : [];
if ($v === null || $programm === []) {
return;
}
$zeitraum = german_date_range((string) ($v['date_start'] ?? ''), $v['date_end'] ?? null);
$sub = trim(implode(' · ', array_filter([$zeitraum, (string) ($v['location'] ?? '')])));
?>
<div class="stadionzeitung-programm">
<header class="stadionzeitung-page__head">
<p class="stadionzeitung-page__kicker"><?= e((string) ($v['title'] ?? 'Veranstaltung')) ?></p>
<h2 class="stadionzeitung-page__heading">Programm</h2>
<?php if ($sub !== ''): ?>
<p class="stadionzeitung-page__sub"><?= e($sub) ?></p>
<?php endif; ?>
</header>
<span class="stadionzeitung-page__rule" aria-hidden="true"></span>
<?php foreach ($programm as $tag): ?>
<?php
$tagIso = (string) ($tag['tag'] ?? '');
$weekday = $tagIso !== '' ? german_weekday($tagIso) : '';
$tagText = trim(($weekday !== '' ? $weekday . ', ' : '') . ($tagIso !== '' ? german_date($tagIso) : ''));
$punkte = is_array($tag['punkte'] ?? null) ? $tag['punkte'] : [];
if ($punkte === []) {
continue;
}
?>
<section class="stadionzeitung-programm__tag">
<?php if ($tagText !== ''): ?>
<h3 class="stadionzeitung-programm__tag-titel"><time datetime="<?= e($tagIso) ?>"><?= e($tagText) ?></time></h3>
<?php endif; ?>
<ul class="stadionzeitung-programm__liste">
<?php foreach ($punkte as $punkt): ?>
<?php $zeit = trim((string) ($punkt['zeit'] ?? '')); ?>
<li class="stadionzeitung-programm__punkt">
<?php if ($zeit !== ''): ?>
<span class="stadionzeitung-programm__zeit"><time datetime="<?= e($tagIso !== '' ? $tagIso . 'T' . $zeit : $zeit) ?>"><?= e($zeit) ?></time></span>
<?php else: ?>
<span aria-hidden="true"></span>
<?php endif; ?>
<span class="stadionzeitung-programm__body">
<span class="stadionzeitung-programm__titel"><?= e((string) ($punkt['titel'] ?? '')) ?></span>
<?php if (!empty($punkt['text'])): ?>
<span class="stadionzeitung-programm__text"><?= e((string) $punkt['text']) ?></span>
<?php endif; ?>
</span>
</li>
<?php endforeach; ?>
</ul>
</section>
<?php endforeach; ?>
</div>

View File

@@ -9,9 +9,12 @@ declare(strict_types=1);
* anderer Anordnung (Lesereihenfolge vs. Broschüren-Reihenfolge). Props:
* $entry Eintrag aus issue.images[]:
* type 'image' (base/widths/alt),
* type 'live' (page: cover|vorwort|interview|aufruf|tabelle|termine|
* news|ergebnisse|gegner|vorschau|kontakt; tabelle optional
* mit team: Team-Key aus matchcenter.json),
* type 'live' (page: cover|vorwort|interview|programm|aufruf|turnier|
* tabelle|termine|news|ergebnisse|gegner|vorschau|kontakt|
* mitglied|historie|sportheim|partner|danke|momente|impressum;
* tabelle und gegner optional mit team: Team-Key aus
* matchcenter.json; turnier mit turnier: Key aus
* veranstaltungen.json → turniere[] der Ausgaben-Veranstaltung),
* type 'leer' (weiße Füllseite, nur Druckfassung — hält die
* Seitenzahl auf einem Vielfachen von 4)
* $issue Eintrag aus data/stadionzeitung.json
@@ -56,7 +59,19 @@ $type = $entry['type'] ?? 'image';
<?php component('stadionzeitung-ergebnisse', ['groups' => $data['ergebnisse'] ?? []]); ?>
<?php break; ?>
<?php case 'gegner': ?>
<?php component('stadionzeitung-gegner', ['info' => $data['gegner'] ?? null]); ?>
<?php
// `gegner` ist eine Map je Team-Key (wie `tabellen`), seit die Sportfest-
// Ausgabe je Heimspiel eine Gegnerseite hat. Ohne team-Feld (Ausgaben vor
// dieser Änderung) greift die tragende Mannschaft.
$gegnerTeam = (string) ($entry['team'] ?? $issue['team_key'] ?? '');
component('stadionzeitung-gegner', ['info' => $data['gegner'][$gegnerTeam] ?? null]);
?>
<?php break; ?>
<?php case 'programm': ?>
<?php component('stadionzeitung-programm', ['issue' => $issue]); ?>
<?php break; ?>
<?php case 'turnier': ?>
<?php component('stadionzeitung-turnier', ['issue' => $issue, 'turnier' => (string) ($entry['turnier'] ?? '')]); ?>
<?php break; ?>
<?php case 'vorschau': ?>
<?php component('stadionzeitung-vorschau', ['items' => $data['vorschau'] ?? []]); ?>
@@ -76,6 +91,11 @@ $type = $entry['type'] ?? 'image';
<?php case 'partner': ?>
<?php component('stadionzeitung-partner', []); ?>
<?php break; ?>
<?php case 'danke': ?>
<?php /* Das Anzeigenmotiv steht im Seiteneintrag (platzierung: danke), damit es
wie jede andere Anzeige in images[] eingefroren ist. */ ?>
<?php component('stadionzeitung-danke', ['anzeige' => $entry['anzeige'] ?? null]); ?>
<?php break; ?>
<?php case 'momente': ?>
<?php component('stadionzeitung-momente', ['items' => $data['momente'] ?? []]); ?>
<?php break; ?>

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
/**
* Turnierplan-Seite der Stadionzeitung (Live-Seite, Sportfest-Ausgabe): EIN
* Jugendturnier pro Seite — Kopf (Veranstaltung · Tag / „Turnier F1-Jugend" /
* Beginn · Spieldauer · Felder · Modus), Teilnehmer, Spielplan mit LEERER
* Ergebnisspalte und eine leere Abschlusstabelle. Das Blatt ist zum
* Beschreiben gedacht: am Platz trägt man Ergebnisse und Plätze von Hand ein.
*
* Quelle ist AUSSCHLIESSLICH data/veranstaltungen.json → turniere[] der
* Veranstaltung, auf die die Ausgabe zeigt (`veranstaltung`-Feld); welches
* Turnier, sagt der `turnier`-Key des Seiteneintrags. Bewusst live, kein
* Snapshot (chat-gepflegte Daten, wie kontakt/aufruf/programm).
*
* Eintrag: key, title, date, start, spieldauer (Minuten), felder? (Zahl),
* modus? (Freitext), teams[], spiele[]{nr, zeit, feld?, heim, gast}
*
* Eigenes Team wie league-table tr.is-own hervorgehoben (Tönung + Abel-Fett);
* erkannt über den Vereinsnamen aus data/club.json als Präfix, damit auch
* „TSV 08 Kulmbach 2" getroffen wird.
*
* Props: $issue (Eintrag aus data/stadionzeitung.json), $turnier (Key)
*/
$issue = $issue ?? [];
$turnierKey = trim((string) ($turnier ?? ''));
$v = veranstaltung_find((string) ($issue['veranstaltung'] ?? ''));
$t = null;
foreach ($v['turniere'] ?? [] as $eintrag) {
if (is_array($eintrag) && (string) ($eintrag['key'] ?? '') === $turnierKey) {
$t = $eintrag;
break;
}
}
if ($v === null || $t === null) {
return;
}
$clubName = trim((string) (json_load('club')['name'] ?? 'TSV 08 Kulmbach'));
$isOwn = static fn (string $team): bool => $clubName !== ''
&& str_starts_with(mb_strtolower(trim($team)), mb_strtolower($clubName));
$date = (string) ($t['date'] ?? '');
$weekday = $date !== '' ? german_weekday($date) : '';
$kicker = trim(implode(' · ', array_filter([
(string) ($v['title'] ?? ''),
trim(($weekday !== '' ? $weekday . ', ' : '') . ($date !== '' ? german_date($date) : '')),
])));
$teams = array_values(array_filter(
array_map(static fn ($x): string => trim((string) $x), $t['teams'] ?? []),
static fn (string $x): bool => $x !== ''
));
$spiele = array_values(array_filter($t['spiele'] ?? [], 'is_array'));
$felder = (int) ($t['felder'] ?? 0);
$hatFeld = $felder > 1;
$meta = [];
if (!empty($t['start'])) {
$meta[] = 'Beginn ' . (string) $t['start'] . ' Uhr';
}
if (!empty($t['spieldauer'])) {
$meta[] = 'Spieldauer ' . (int) $t['spieldauer'] . ' Min.';
}
if ($hatFeld) {
$meta[] = $felder . ' Felder';
}
if (!empty($t['modus'])) {
$meta[] = (string) $t['modus'];
}
$sub = trim(implode(' · ', $meta));
$title = trim((string) ($t['title'] ?? $turnierKey));
// Ab neun Spielen (E-Jugend: zehn) rücken die Zeilen enger, sonst läuft der
// Spielplan mit der Abschlusstabelle über die Blattkante.
$dicht = count($spiele) > 8;
?>
<div class="stadionzeitung-turnier<?= $dicht ? ' stadionzeitung-turnier--dicht' : '' ?>">
<header class="stadionzeitung-page__head">
<?php if ($kicker !== ''): ?>
<p class="stadionzeitung-page__kicker"><?= e($kicker) ?></p>
<?php endif; ?>
<h2 class="stadionzeitung-page__heading"><?= e('Turnier ' . $title) ?></h2>
<?php if ($sub !== ''): ?>
<p class="stadionzeitung-page__sub"><?= e($sub) ?></p>
<?php endif; ?>
</header>
<span class="stadionzeitung-page__rule" aria-hidden="true"></span>
<?php if ($teams !== []): ?>
<p class="stadionzeitung-turnier__label">Teilnehmer</p>
<p class="stadionzeitung-turnier__teams">
<?php foreach ($teams as $i => $team): ?>
<?= $i > 0 ? ' · ' : '' ?><span class="stadionzeitung-turnier__team<?= $isOwn($team) ? ' is-own' : '' ?>"><?= e($team) ?></span>
<?php endforeach; ?>
</p>
<?php endif; ?>
<?php if ($spiele !== []): ?>
<table class="stadionzeitung-turnier__spiele">
<?php /* Sichtbares Label IST die Tabellenbeschriftung (caption), nicht zusätzlich
eine versteckte: sonst liest ein Screenreader beides und damit doppelt. */ ?>
<caption class="stadionzeitung-turnier__label">Spielplan</caption>
<thead>
<tr>
<th scope="col">Nr.</th>
<th scope="col">Zeit</th>
<?php if ($hatFeld): ?>
<th scope="col">Feld</th>
<?php endif; ?>
<th scope="col">Begegnung</th>
<th scope="col" class="stadionzeitung-turnier__ergebnis">Ergebnis</th>
</tr>
</thead>
<tbody>
<?php foreach ($spiele as $i => $spiel): ?>
<?php
$heim = trim((string) ($spiel['heim'] ?? ''));
$gast = trim((string) ($spiel['gast'] ?? ''));
$zeit = trim((string) ($spiel['zeit'] ?? ''));
?>
<tr>
<td class="stadionzeitung-turnier__nr"><?= e((string) ($spiel['nr'] ?? ($i + 1))) ?></td>
<td class="stadionzeitung-turnier__zeit"><?php if ($zeit !== ''): ?><time datetime="<?= e($date !== '' ? $date . 'T' . $zeit : $zeit) ?>"><?= e($zeit) ?></time><?php endif; ?></td>
<?php if ($hatFeld): ?>
<td class="stadionzeitung-turnier__nr"><?= e((string) ($spiel['feld'] ?? '')) ?></td>
<?php endif; ?>
<td class="stadionzeitung-turnier__paarung"><span class="stadionzeitung-turnier__team<?= $isOwn($heim) ? ' is-own' : '' ?>"><?= e($heim) ?></span> <span class="stadionzeitung-turnier__vs">gegen</span> <span class="stadionzeitung-turnier__team<?= $isOwn($gast) ? ' is-own' : '' ?>"><?= e($gast) ?></span></td>
<td class="stadionzeitung-turnier__ergebnis"><span class="stadionzeitung-turnier__blank" aria-hidden="true"></span><span class="stadionzeitung-turnier__colon">:</span><span class="stadionzeitung-turnier__blank" aria-hidden="true"></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php if ($teams !== []): ?>
<table class="stadionzeitung-turnier__tabelle">
<caption class="stadionzeitung-turnier__label">Platzierung zum Eintragen</caption>
<thead>
<tr>
<th scope="col">Pl.</th>
<th scope="col">Teilnehmer</th>
<th scope="col"><abbr title="Tore">T</abbr></th>
<th scope="col"><abbr title="Tordifferenz">TD</abbr></th>
<th scope="col"><abbr title="Punkte">Pkt</abbr></th>
</tr>
</thead>
<tbody>
<?php for ($i = 1; $i <= count($teams); $i++): ?>
<tr>
<td class="stadionzeitung-turnier__nr"><?= $i ?>.</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<?php endfor; ?>
</tbody>
</table>
<?php endif; ?>
</div>

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
/**
* Swipe-Viewer für eine Stadionzeitung-Ausgabe. Props:
* $issue Eintrag aus data/stadionzeitung.json (opponent, team_key, status,
* images[]{type: 'image'{base,widths,alt} | 'live'{page[,team]}})
* images[]{type: 'image'{base,widths,alt} | 'live'{page[,team][,turnier]}})
*
* Ohne JS: alle Seiten normal gestapelt (stadionzeitung-seite-Komponente) —
* voll lesbar, sequentiell auch per Screenreader. Mit JS