Compare commits
12 Commits
fef80bf669
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b59a478c2 | |||
| fd305673aa | |||
| bd8f1dd129 | |||
| 33b73087cf | |||
| 5a107a955a | |||
| 5be1a25c07 | |||
| 2ae5ddb1ac | |||
| 70831b9bea | |||
| fbb23a8732 | |||
| 7aae3d5ca9 | |||
| 0ee75aa93b | |||
| 8f864368f0 |
88
CLAUDE.md
@@ -54,7 +54,7 @@ selbst gehosteter Stack ohne Abhängigkeiten von Drittanbietern.
|
|||||||
| Partner/Sponsoren | `data/partners.json` | — |
|
| Partner/Sponsoren | `data/partners.json` | — |
|
||||||
| Instagram-Beiträge | `data/instagram.json` + `public/assets/img/instagram/` | Aufgenommen ausschließlich mit **`php bin/instagram-add.php <post-url>`** (holt Bild, Text und Datum) — Einträge nie von Hand tippen, Bilder nie von Hand skalieren. **Kein Cron**, chat-gepflegt wie `events.json`. Eintrag und Bild müssen immer zusammen existieren, `bin/preflight.php` prüft beide Richtungen |
|
| Instagram-Beiträge | `data/instagram.json` + `public/assets/img/instagram/` | Aufgenommen ausschließlich mit **`php bin/instagram-add.php <post-url>`** (holt Bild, Text und Datum) — Einträge nie von Hand tippen, Bilder nie von Hand skalieren. **Kein Cron**, chat-gepflegt wie `events.json`. Eintrag und Bild müssen immer zusammen existieren, `bin/preflight.php` prüft beide Richtungen |
|
||||||
| Matchcenter (Spiele/Ergebnisse/Tabellen) | `data/matchcenter.json` + `public/assets/img/crests/` | **maschinenverwaltet von `bin/matchcenter-sync.php` — nie von Hand editieren**. Team-/Wettbewerbs-IDs in `config.php` (`matchcenter.teams`) |
|
| Matchcenter (Spiele/Ergebnisse/Tabellen) | `data/matchcenter.json` + `public/assets/img/crests/` | **maschinenverwaltet von `bin/matchcenter-sync.php` — nie von Hand editieren**. Team-/Wettbewerbs-IDs in `config.php` (`matchcenter.teams`) |
|
||||||
| Stadionzeitung (Ausgaben-Archiv) | `data/stadionzeitung.json` + `public/assets/img/stadionzeitung/` | **maschinenverwaltet von `bin/stadionzeitung-add.php` + `bin/stadionzeitung-publish.php` (Status/Snapshot) — nie von Hand editieren; einzige Chat-Pflege-Ausnahmen sind `vorwort`, `interview`, `aufruf`, `veranstaltung` (Slug-Verweis auf `veranstaltungen.json`) und `cover` (`teaser` einer Sonderausgabe).** Der Ausgaben-Ordner wird bei jedem Lauf komplett geleert und neu gefüllt |
|
| Stadionzeitung (Ausgaben-Archiv) | `data/stadionzeitung.json` + `public/assets/img/stadionzeitung/` | **maschinenverwaltet von `bin/stadionzeitung-add.php` + `bin/stadionzeitung-publish.php` (Status/Snapshot) — nie von Hand editieren; einzige Chat-Pflege-Ausnahmen sind `vorwort`, `interview`, `aufruf`, `beitrag` (Text/Quelle/Bildnachweis; `image` bleibt script-verwaltet), `rueckblick` (Text/Bildnachweis; `image` bleibt script-verwaltet), `veranstaltung` (Slug-Verweis auf `veranstaltungen.json`), `cover` (`teaser` einer Sonderausgabe), `opponent_short` (gedruckter Anzeigename des Gegners) und `gegner_teams` (weitere Mannschaften mit Gegnerseite).** Der Ausgaben-Ordner wird bei jedem Lauf komplett geleert und neu gefüllt |
|
||||||
| Sponsoren-Anzeigen (Stadionzeitung) | `data/anzeigen.json` + `public/assets/img/anzeigen/` | Dauerhafter Bestand, **nicht** pro Ausgabe: Motive ändern sich selten, das Heft erscheint oft. `sponsor`, `alt` (optional), `aktiv`, `bis` (optional, Enddatum gegen das Ausgabedatum), `platzierung` (optional, fester Platz auf einer Live-Seite statt im Streu-Pool) und die **Reihenfolge** (= Reihenfolge im Heft) im Chat gepflegt; `base`/`widths` schreibt ausschließlich `bin/anzeige-add.php`. Anzeigen nie in den Ausgaben-Ordner legen |
|
| Sponsoren-Anzeigen (Stadionzeitung) | `data/anzeigen.json` + `public/assets/img/anzeigen/` | Dauerhafter Bestand, **nicht** pro Ausgabe: Motive ändern sich selten, das Heft erscheint oft. `sponsor`, `alt` (optional), `aktiv`, `bis` (optional, Enddatum gegen das Ausgabedatum), `platzierung` (optional, fester Platz auf einer Live-Seite statt im Streu-Pool) und die **Reihenfolge** (= Reihenfolge im Heft) im Chat gepflegt; `base`/`widths` schreibt ausschließlich `bin/anzeige-add.php`. Anzeigen nie in den Ausgaben-Ordner legen |
|
||||||
| Termine/Events (`/termine`) | `data/events.json` (Vereins-Termine **ohne** eigene Detailseite) — Spieltermine kommen weiterhin nur aus `data/matchcenter.json`, Feste mit eigener Seite nur aus `data/veranstaltungen.json`; die Termine-Seite mergt alle drei | Von Claude im Chat gepflegt wie `teams.json`/`partners.json` — **keine** Automatisierung, kein Bin-Skript. Spieltermine nie hier duplizieren. Ein Fest, das eine Detailseite hat, steht **nur** in `veranstaltungen.json` (`bin/preflight.php` prüft das) |
|
| Termine/Events (`/termine`) | `data/events.json` (Vereins-Termine **ohne** eigene Detailseite) — Spieltermine kommen weiterhin nur aus `data/matchcenter.json`, Feste mit eigener Seite nur aus `data/veranstaltungen.json`; die Termine-Seite mergt alle drei | Von Claude im Chat gepflegt wie `teams.json`/`partners.json` — **keine** Automatisierung, kein Bin-Skript. Spieltermine nie hier duplizieren. Ein Fest, das eine Detailseite hat, steht **nur** in `veranstaltungen.json` (`bin/preflight.php` prüft das) |
|
||||||
| Veranstaltungen (`/veranstaltungen` + Detailseiten) | `data/veranstaltungen.json` + `public/assets/img/veranstaltungen/` | Termin, Texte, Programm, Turnierpläne (`turniere[]`, nur fürs Heft), Abschnitte von Claude im Chat gepflegt — **außer `plakate` und `galerie`: die schreibt ausschließlich `bin/veranstaltung-bilder.php`**. Adresse nie in die Texte schreiben (steht in `club.json`) |
|
| Veranstaltungen (`/veranstaltungen` + Detailseiten) | `data/veranstaltungen.json` + `public/assets/img/veranstaltungen/` | Termin, Texte, Programm, Turnierpläne (`turniere[]`, nur fürs Heft), Abschnitte von Claude im Chat gepflegt — **außer `plakate` und `galerie`: die schreibt ausschließlich `bin/veranstaltung-bilder.php`**. Adresse nie in die Texte schreiben (steht in `club.json`) |
|
||||||
@@ -521,6 +521,19 @@ Besucher laden alle Matchcenter-Inhalte ausschließlich von unserer Domain.
|
|||||||
|
|
||||||
## Stadionzeitung (Ausgaben-Archiv)
|
## Stadionzeitung (Ausgaben-Archiv)
|
||||||
|
|
||||||
|
**CLI-Abrufe bei lokalem DNS-Ausfall:** `TSV_HTTP_DOH_URL=https://1.1.1.1/dns-query`
|
||||||
|
kann für `bin/matchcenter-sync.php` und `bin/instagram-add.php` gesetzt werden.
|
||||||
|
Optional, keine Änderung am System-DNS; Cron/Web ignoriert den Override.
|
||||||
|
|
||||||
|
**Redaktioneller Lesetipp (24.10.2026):** optionales chat-gepflegtes `beitrag`-Feld
|
||||||
|
(`kicker`, `heading`, `paragraphs[]`, `source`, `source_url` als HTTPS-Adresse,
|
||||||
|
`cta_label`, `image_credit`, `image.alt`). `bin/stadionzeitung-add.php` übernimmt
|
||||||
|
das Feld über erneute Läufe, schreibt `image` aus `<ordner>/beitrag.*` und erzeugt
|
||||||
|
`qr-beitrag.png` direkt zur externen Quelle. Die Seite steht im anzeigenfreien
|
||||||
|
Aufschlag hinter Vorwort/Interview; `cover.teaser` kündigt sie unabhängig von
|
||||||
|
einem Interview an. Rendering gemeinsam für Viewer/Druck über
|
||||||
|
`stadionzeitung-beitrag.php`; Preflight prüft Quelle, Foto und QR.
|
||||||
|
|
||||||
Digitale Fassung der Stadionzeitung zu jedem Heimspiel — Archiv-Übersicht (`/stadionzeitung`) als
|
Digitale Fassung der Stadionzeitung zu jedem Heimspiel — Archiv-Übersicht (`/stadionzeitung`) als
|
||||||
**Liste über die volle Breite, Cover links und die Angaben rechts daneben** (dasselbe Muster wie
|
**Liste über die volle Breite, Cover links und die Angaben rechts daneben** (dasselbe Muster wie
|
||||||
`.veranstaltung-card`, und aus demselben Grund: der Bestand ist klein). Vorher war das ein
|
`.veranstaltung-card`, und aus demselben Grund: der Bestand ist klein). Vorher war das ein
|
||||||
@@ -548,6 +561,8 @@ Inhalt:
|
|||||||
Rand: Freistellungs-Exporte lassen oft viel leeren, transparenten
|
Rand: Freistellungs-Exporte lassen oft viel leeren, transparenten
|
||||||
Rand stehen, sonst wirkt der Spieler auf dem Cover kleiner als nötig)
|
Rand stehen, sonst wirkt der Spieler auf dem Cover kleiner als nötig)
|
||||||
<ordner>/verfasser.* optional, Porträtfoto für die Signatur im automatischen Vorwort
|
<ordner>/verfasser.* optional, Porträtfoto für die Signatur im automatischen Vorwort
|
||||||
|
<ordner>/galerie/* optional, Fotos vom letzten Spiel (natsort, das erste ist der
|
||||||
|
Aufmacher, höchstens sieben) für die Bento-Galerie-Seite
|
||||||
```
|
```
|
||||||
**Keine Anzeigen im Ablieferordner** (seit 31.07.2026): die kommen aus dem dauerhaften Bestand
|
**Keine Anzeigen im Ablieferordner** (seit 31.07.2026): die kommen aus dem dauerhaften Bestand
|
||||||
`data/anzeigen.json`, gepflegt mit `php bin/anzeige-add.php <bild-oder-ordner>` (Dateiname = Slug,
|
`data/anzeigen.json`, gepflegt mit `php bin/anzeige-add.php <bild-oder-ordner>` (Dateiname = Slug,
|
||||||
@@ -571,6 +586,7 @@ Inhalt ist eine Liste von **Blöcken**, die Anzeigen füllen die Lücken dazwisc
|
|||||||
```
|
```
|
||||||
Cover → [Vorwort → Programm* → Interview → Aufruf (jedes nur wenn die Ausgabe es trägt)] ⌷
|
Cover → [Vorwort → Programm* → Interview → Aufruf (jedes nur wenn die Ausgabe es trägt)] ⌷
|
||||||
[Turnierpläne*: je Turnier eine Seite, EIN Block] ⌷ Ergebnis-Rückblick ⌷
|
[Turnierpläne*: je Turnier eine Seite, EIN Block] ⌷ Ergebnis-Rückblick ⌷
|
||||||
|
[Galerie, nur wenn galerie/ geliefert] ⌷
|
||||||
[Gegnerstatistik: normal nur die tragende Mannschaft, in der Sportfest-Ausgabe je
|
[Gegnerstatistik: normal nur die tragende Mannschaft, in der Sportfest-Ausgabe je
|
||||||
Mannschaft mit Heimspiel am Fest eine — EIN Block, automatisch aus deren Tabelle] ⌷
|
Mannschaft mit Heimspiel am Fest eine — EIN Block, automatisch aus deren Tabelle] ⌷
|
||||||
[je Mannschaft eine Tabelle, Reihenfolge = matchcenter.json — EIN Block, dazwischen nie eine
|
[je Mannschaft eine Tabelle, Reihenfolge = matchcenter.json — EIN Block, dazwischen nie eine
|
||||||
@@ -606,7 +622,7 @@ Lücken bekommen 7 Lücken zwei. Zwei hintereinander ist das Optimum, nicht eins
|
|||||||
Zweier-Paar auf einer Doppelseite landen kann, schlägt man das Heft gelegentlich auf zwei Anzeigen auf.
|
Zweier-Paar auf einer Doppelseite landen kann, schlägt man das Heft gelegentlich auf zwei Anzeigen auf.
|
||||||
Ein `images[]`-Eintrag hat
|
Ein `images[]`-Eintrag hat
|
||||||
`"type": "image"` (wie gehabt, `base`/`widths`/`alt`) oder `"type": "live"` (`page`: `cover` |
|
`"type": "image"` (wie gehabt, `base`/`widths`/`alt`) oder `"type": "live"` (`page`: `cover` |
|
||||||
`vorwort` | `interview` | `programm` | `aufruf` | `turnier` | `tabelle` | `termine` | `news` | `ergebnisse` | `gegner` | `vorschau` | `kontakt` | `mitglied` | `historie` | `sportheim` | `partner` | `danke` | `momente` | `impressum`, kein Bild;
|
`vorwort` | `interview` | `programm` | `aufruf` | `turnier` | `tabelle` | `termine` | `news` | `ergebnisse` | `gegner` | `vorschau` | `kontakt` | `mitglied` | `historie` | `sportheim` | `partner` | `danke` | `momente` | `galerie` | `impressum`, kein Bild;
|
||||||
`tabelle` und `gegner` zusätzlich mit `team`: Team-Key aus `matchcenter.json`, ohne `team` greift der
|
`tabelle` und `gegner` zusätzlich mit `team`: Team-Key aus `matchcenter.json`, ohne `team` greift der
|
||||||
Team-Key der Ausgabe; `turnier` mit `turnier`: Key aus `turniere[]` der Ausgaben-Veranstaltung;
|
Team-Key der Ausgabe; `turnier` mit `turnier`: Key aus `turniere[]` der Ausgaben-Veranstaltung;
|
||||||
`danke` mit `anzeige`: das platzierte Anzeigenmotiv, siehe unten). Fehlt `type` (alte Phase-1-Ausgaben) → Fallback `image`, bricht nicht. **Gerendert
|
`danke` mit `anzeige`: das platzierte Anzeigenmotiv, siehe unten). Fehlt `type` (alte Phase-1-Ausgaben) → Fallback `image`, bricht nicht. **Gerendert
|
||||||
@@ -795,6 +811,74 @@ keinen farbigen Rahmen über das Spielerfoto** — der Kicker bleibt „Saison
|
|||||||
die Ausgabe nicht, in die die Felder gehören. `bin/preflight.php` prüft, dass der Slug und jeder
|
die Ausgabe nicht, in die die Felder gehören. `bin/preflight.php` prüft, dass der Slug und jeder
|
||||||
Turnier-Key wirklich existieren — ein Tippfehler ergäbe sonst eine **leere** A5-Seite ohne Fehler.
|
Turnier-Key wirklich existieren — ein Tippfehler ergäbe sonst eine **leere** A5-Seite ohne Fehler.
|
||||||
|
|
||||||
|
**Gegnerseiten ohne Fest, und der Anzeigename des Gegners (17.09.2026).** Zwei Felder, beide
|
||||||
|
chat-gepflegt und von `bin/stadionzeitung-add.php` über erneute Läufe durchgetragen wie `vorwort`:
|
||||||
|
|
||||||
|
- **`gegner_teams`** (Liste von Team-Keys) gibt weiteren Mannschaften eine Gegnerseite, ohne dass es
|
||||||
|
eine Veranstaltung braucht. Der Sportfest-Weg blieb unverändert daneben stehen (Heimspiele im
|
||||||
|
Festzeitraum); dieser hier nimmt das nächste Spiel im Fenster **zwei Tage vor bis drei Tage nach**
|
||||||
|
dem Ausgabedatum, **auch auswärts** — am normalen Spielwochenende fährt eine der drei Mannschaften
|
||||||
|
fast immer weg, und ihr Gegner ist trotzdem eine Seite wert. `stadionzeitung_gegner_fixtures()`
|
||||||
|
bleibt die eine entscheidende Stelle. Findet eine benannte Mannschaft kein Spiel im Fenster,
|
||||||
|
entsteht keine Seite und das Skript **warnt** — ein Tippfehler im Team-Key sähe sonst aus wie ein
|
||||||
|
spielfreies Wochenende. Auf der Gegnerseite steht seitdem **immer unsere Mannschaft links**
|
||||||
|
(Klasse `--own`), bei einem Auswärtsspiel läuft die Zeile „… bei …" statt „… gegen …"; vorher war
|
||||||
|
die linke Spalte fest die Heimmannschaft und die Hervorhebung lag beim Gegner.
|
||||||
|
- **`opponent_short`** ist der gedruckte Anzeigename des Gegners, gelesen über
|
||||||
|
**`stadionzeitung_opponent_display()`** (Cover, Archivzeile, Ausgaben-Kopf, Titel, Meta, Alt-Text
|
||||||
|
der Rückseite). **`opponent` selbst muss die BFV-Schreibweise behalten** — die Gegnerseite sucht
|
||||||
|
damit ihre Tabellenzeile und ihre Begegnung in `matchcenter.json` über einen exakten
|
||||||
|
Namensvergleich. Bei Spielgemeinschaften ist das eine Zeile wie „(SG1) TSV Wirsberg I/ FC
|
||||||
|
Kupferberg I", die auf einem Cover nichts zu suchen hat; `opponent_short` trägt dann
|
||||||
|
„Wirsberg/Kupferberg". Fehlt das Feld, bleibt es beim vollen Namen.
|
||||||
|
|
||||||
|
Nebenbei löst das oft die Seitenzahl: drei Gegnerseiten statt einer sind **zwei Seiten mehr**, und
|
||||||
|
ein Heft, das bei 42 stand, steht damit auf 44.
|
||||||
|
|
||||||
|
**Galerie-Seite (02.10.2026): Fotos vom letzten Spiel als Bento-Raster.** Optional, hinter dem
|
||||||
|
Ergebnis-Rückblick (`stadionzeitung-galerie.php`). Das Feld `galerie` mischt zwei Pfleger, wie bei
|
||||||
|
den Veranstaltungsbildern: `kicker`, `heading`, `text` und die **Alt-Texte** sind Chat-Pflege,
|
||||||
|
`bilder[]` schreibt `bin/stadionzeitung-add.php` aus `<ordner>/galerie/` (Breiten 640/1200).
|
||||||
|
Alt-Texte hängen an `source` (Originaldateiname) und folgen ihrem Bild; neue Fotos bekommen die
|
||||||
|
Überschrift als Platzhalter, das Skript nennt sie per WARN. **Der Ordner ist der komplette
|
||||||
|
Bestand:** ein Lauf ohne `galerie/` lässt die Seite entfallen, weil der Ausgaben-Ordner geleert
|
||||||
|
wurde. Kein Snapshot nötig, die Bilder liegen im Ausgaben-Ordner und kein Sync fasst sie an.
|
||||||
|
Raster: 6×4-Gitter, das erste Foto 4×2, die Anordnung je Anzahl (1 bis 7) als Klasse
|
||||||
|
`__bento--nN` mit `grid-template-areas`. Entstanden als Füllseite, die eine 43-seitige Ausgabe
|
||||||
|
auf 44 brachte.
|
||||||
|
|
||||||
|
**Redaktioneller Spielrückblick (16.10.2026).** Optionales Feld `rueckblick` einer Ausgabe:
|
||||||
|
`kicker`, `heading`, `meta`, `result`, `paragraphs[]`, `credit`, `source_url`
|
||||||
|
sind Chat-Pflege; `image` (inklusive chat-gepflegtem Alt-Text) verwaltet das Add-Skript aus
|
||||||
|
`<ordner>/rueckblick.*`. Die Live-Seite steht nach den Ergebnissen, vor der Galerie. Das Foto
|
||||||
|
liegt dauerhaft im Ausgaben-Ordner. Bei erneutem Erzeugen muss die Quelle wieder mitgeliefert
|
||||||
|
werden; fehlt sie, bricht das Skript vor dem Leeren des Zielordners ab. Preflight prüft Foto
|
||||||
|
und Bildnachweis. Der gemeinsame Bild-/Textnachweis steht einmal direkt unter dem Foto
|
||||||
|
(im Web zur Quelle verlinkt, auch im Druck lesbar). Kein Bild hochskalieren oder fremd einbinden.
|
||||||
|
Für die Untersteinach-Ausgabe: öffentliches Derby-Vorschaubild
|
||||||
|
`https://www.anpfiff.info/upload/images/Teaser8/843447.jpg` (560 × 270), Bildquelle anpfiff.info.
|
||||||
|
Felix hat die Nutzungsgenehmigung im Chat am 16.10.2026 bestätigt. Ein individueller Fotograf
|
||||||
|
ist am frei sichtbaren Bild nicht ausgewiesen; deshalb keinen Namen vermuten.
|
||||||
|
|
||||||
|
**Gegner- und Ergebnisseite neu gestaltet (19.09.2026).** Beide Seiten waren inhaltlich richtig und
|
||||||
|
gestalterisch leer, der Umbau lief über mehrere verworfene Entwürfe. Was davon bleibt:
|
||||||
|
- **Ergebnisse als Anzeigetafel** (Vorbild kicker/Sofascore): Wappen und Name je Seite, das Ergebnis
|
||||||
|
groß in der Mitte. Vorher waren es Listenzeilen, und eine Zeile stellt Heim und Gast untereinander
|
||||||
|
statt gegenüber. Verworfen und nicht wieder einzubauen: Tore am rechten Blattrand (halbe
|
||||||
|
Seitenbreite Leere zwischen Name und Ziffer), eine getönte Karte je Spiel („altbacken"), ein roter
|
||||||
|
Strich unter der Mannschaftszeile (lauter als die Ergebnisse) und graue Verlierer-Ziffern (auf
|
||||||
|
Papier schlechter lesbar). Vereinsnamen laufen **einzeilig** und werden sonst ausgepunktet.
|
||||||
|
- **Gegnerseite**: Duell symmetrisch und leise (beide Wappen gleich groß, der Name des Gegners steht
|
||||||
|
nur in der Überschrift laut da), darunter eine abgeleitete Aufmacher-Zeile („Sieben Siege aus acht
|
||||||
|
Spielen", aus den Tabellenwerten gebaut), der Vergleich als **Balkenpaar aus der Mitte** und zum
|
||||||
|
Schluss ein **Tabellenausschnitt** über die geteilte `league-table`-Komponente. Der Tabellenplatz
|
||||||
|
steht bewusst dort und nicht als Balken (klein ist besser, der Balken läge falsch herum) und auch
|
||||||
|
nicht als lose Ziffer im Kopf (war gebaut, wirkte unruhig).
|
||||||
|
- **Balkenlängen kommen aus Klassen in 5-Prozent-Stufen**, nie aus einem `style`-Attribut — die CSP
|
||||||
|
verwirft inline styles stillschweigend, die Balken wären erst nach dem Deploy unsichtbar.
|
||||||
|
- Die Gegnerseite druckt **`opponent_display`** (Kurzname der Ausgabe, wenn die Seite den Gegner der
|
||||||
|
Ausgabe zeigt); `opponent` bleibt die BFV-Schreibweise für den Namensvergleich.
|
||||||
|
|
||||||
**Cover (automatisch generiert, Phase 3):** einzige fotografische Stadionzeitungs-Seite —
|
**Cover (automatisch generiert, Phase 3):** einzige fotografische Stadionzeitungs-Seite —
|
||||||
`app/components/stadionzeitung-cover.php`. Echtes Magazin-Cover statt Website-Baustein: **kein**
|
`app/components/stadionzeitung-cover.php`. Echtes Magazin-Cover statt Website-Baustein: **kein**
|
||||||
Stadionfoto mehr als Hintergrund, stattdessen ein ruhiger Verlauf in Vereinsfarben, auf dem das
|
Stadionfoto mehr als Hintergrund, stattdessen ein ruhiger Verlauf in Vereinsfarben, auf dem das
|
||||||
|
|||||||
44
app/components/stadionzeitung-beitrag.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/** Redaktioneller Lesetipp. Text/Quelle: Chat-Pflege; Foto/QR: add-Skript. */
|
||||||
|
$beitrag = $issue['beitrag'] ?? null;
|
||||||
|
if (!is_array($beitrag)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$sourceUrl = (string) ($beitrag['source_url'] ?? '');
|
||||||
|
$validUrl = filter_var($sourceUrl, FILTER_VALIDATE_URL)
|
||||||
|
&& parse_url($sourceUrl, PHP_URL_SCHEME) === 'https';
|
||||||
|
$qrPath = 'img/stadionzeitung/' . (string) ($issue['slug'] ?? '') . '/qr-beitrag.png';
|
||||||
|
?>
|
||||||
|
<div class="stadionzeitung-beitrag">
|
||||||
|
<header class="stadionzeitung-page__head">
|
||||||
|
<p class="stadionzeitung-page__kicker"><?= e((string) ($beitrag['kicker'] ?? 'Lesetipp')) ?></p>
|
||||||
|
<h2 class="stadionzeitung-page__heading"><?= e((string) ($beitrag['heading'] ?? '')) ?></h2>
|
||||||
|
</header>
|
||||||
|
<span class="stadionzeitung-page__rule" aria-hidden="true"></span>
|
||||||
|
<figure class="stadionzeitung-beitrag__figure">
|
||||||
|
<?php component('img', ['image' => $beitrag['image'] ?? [], 'sizes' => '(min-width: 520px) 430px, 90vw', 'class' => 'stadionzeitung-beitrag__foto', 'eager' => $eager ?? false]); ?>
|
||||||
|
<figcaption class="stadionzeitung-rueckblick__credit"><?= e((string) ($beitrag['image_credit'] ?? '')) ?></figcaption>
|
||||||
|
</figure>
|
||||||
|
<?php foreach ($beitrag['paragraphs'] ?? [] as $paragraph): ?>
|
||||||
|
<p class="stadionzeitung-vorwort__text"><?= e((string) $paragraph) ?></p>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<p class="stadionzeitung-beitrag__source"><?= e((string) ($beitrag['source'] ?? '')) ?></p>
|
||||||
|
<?php if ($validUrl): ?>
|
||||||
|
<a class="stadionzeitung-page__cta" href="<?= e($sourceUrl) ?>" target="_blank" rel="noopener noreferrer">
|
||||||
|
<div class="stadionzeitung-page__cta-text">
|
||||||
|
<p class="stadionzeitung-page__cta-label"><?= e((string) ($beitrag['cta_label'] ?? 'Lesetipp')) ?></p>
|
||||||
|
<p class="stadionzeitung-page__cta-title">Den vollständigen Beitrag lesen</p>
|
||||||
|
<p class="stadionzeitung-page__cta-link"><?= e((string) parse_url($sourceUrl, PHP_URL_HOST)) ?></p>
|
||||||
|
</div>
|
||||||
|
<?php if (is_file(PUBLIC_PATH . '/assets/' . $qrPath)): ?>
|
||||||
|
<div class="stadionzeitung-page__cta-abriss">
|
||||||
|
<img class="stadionzeitung-page__cta-qr" src="<?= e(asset($qrPath)) ?>" alt="QR-Code zum Originalartikel" width="100" height="100" loading="lazy" decoding="async">
|
||||||
|
<span class="stadionzeitung-page__cta-scan">Zum Artikel</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
@@ -31,7 +31,7 @@ $issues = $issues ?? [];
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="stadionzeitung-card__body">
|
<div class="stadionzeitung-card__body">
|
||||||
<h3 class="stadionzeitung-card__name">Stadionzeitung zum Heimspiel gegen <?= e($issue['opponent']) ?></h3>
|
<h3 class="stadionzeitung-card__name">Stadionzeitung zum Heimspiel gegen <?= e(stadionzeitung_opponent_display($issue)) ?></h3>
|
||||||
<p class="stadionzeitung-card__meta">
|
<p class="stadionzeitung-card__meta">
|
||||||
<time datetime="<?= e($issue['date']) ?>"><?= e(german_date($issue['date'])) ?></time>
|
<time datetime="<?= e($issue['date']) ?>"><?= e(german_date($issue['date'])) ?></time>
|
||||||
· <?= (int) $issue['pages'] ?> Seiten
|
· <?= (int) $issue['pages'] ?> Seiten
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ declare(strict_types=1);
|
|||||||
* der Gegner läuft über den Initialen-Fallback der crest-Komponente.
|
* der Gegner läuft über den Initialen-Fallback der crest-Komponente.
|
||||||
*/
|
*/
|
||||||
$issue = $issue ?? [];
|
$issue = $issue ?? [];
|
||||||
$opponent = (string) ($issue['opponent'] ?? '');
|
$opponent = stadionzeitung_opponent_display($issue);
|
||||||
$date = (string) ($issue['date'] ?? '');
|
$date = (string) ($issue['date'] ?? '');
|
||||||
$teamKey = (string) ($issue['team_key'] ?? '');
|
$teamKey = (string) ($issue['team_key'] ?? '');
|
||||||
$playerImage = $issue['player_image'] ?? null;
|
$playerImage = $issue['player_image'] ?? null;
|
||||||
|
|||||||
@@ -40,19 +40,28 @@ $matchcenterLink = club_domain('matchcenter');
|
|||||||
<section class="stadionzeitung-ergebnisse__team">
|
<section class="stadionzeitung-ergebnisse__team">
|
||||||
<h3 class="stadionzeitung-ergebnisse__team-name"><?= e(trim(implode(' · ', array_filter([(string) ($group['team_name'] ?? ''), (string) ($group['league'] ?? '')])))) ?></h3>
|
<h3 class="stadionzeitung-ergebnisse__team-name"><?= e(trim(implode(' · ', array_filter([(string) ($group['team_name'] ?? ''), (string) ($group['league'] ?? '')])))) ?></h3>
|
||||||
<?php foreach ($group['matches'] ?? [] as $match): ?>
|
<?php foreach ($group['matches'] ?? [] as $match): ?>
|
||||||
<div class="stadionzeitung-ergebnisse__match">
|
<?php
|
||||||
<p class="stadionzeitung-ergebnisse__meta">
|
$sides = [
|
||||||
<?= e(trim((string) ($match['date_display'] ?? ''))) ?><?php if (($match['competition'] ?? 'Liga') !== 'Liga'): ?> · <?= e((string) $match['competition']) ?><?php endif; ?>
|
['club' => $match['home'] ?? '', 'crest' => $match['home_crest'] ?? null, 'goals' => $match['goals_home'] ?? null],
|
||||||
</p>
|
['club' => $match['away'] ?? '', 'crest' => $match['away_crest'] ?? null, 'goals' => $match['goals_away'] ?? null],
|
||||||
<?php foreach ([['club' => $match['home'] ?? '', 'crest' => $match['home_crest'] ?? null, 'goals' => $match['goals_home'] ?? null],
|
];
|
||||||
['club' => $match['away'] ?? '', 'crest' => $match['away_crest'] ?? null, 'goals' => $match['goals_away'] ?? null]] as $side): ?>
|
?>
|
||||||
<div class="stadionzeitung-ergebnisse__row<?= str_contains((string) $side['club'], 'TSV 08 Kulmbach') ? ' stadionzeitung-ergebnisse__row--own' : '' ?>">
|
<article class="stadionzeitung-ergebnisse__match">
|
||||||
|
<?php foreach ($sides as $i => $side): ?>
|
||||||
|
<?php if ($i === 1): ?>
|
||||||
|
<div class="stadionzeitung-ergebnisse__score">
|
||||||
|
<p class="stadionzeitung-ergebnisse__result">
|
||||||
|
<span class="stadionzeitung-ergebnisse__goals"><?= e((string) $sides[0]['goals']) ?></span><span class="stadionzeitung-ergebnisse__colon">:</span><span class="stadionzeitung-ergebnisse__goals"><?= e((string) $sides[1]['goals']) ?></span>
|
||||||
|
</p>
|
||||||
|
<p class="stadionzeitung-ergebnisse__meta"><?= e(trim((string) ($match['date_display'] ?? ''))) ?><?php if (($match['competition'] ?? 'Liga') !== 'Liga'): ?> · <?= e((string) $match['competition']) ?><?php endif; ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="stadionzeitung-ergebnisse__side<?= str_contains((string) $side['club'], 'TSV 08 Kulmbach') ? ' stadionzeitung-ergebnisse__side--own' : '' ?>">
|
||||||
<?php component('crest', ['src' => $side['crest'], 'name' => (string) $side['club']]); ?>
|
<?php component('crest', ['src' => $side['crest'], 'name' => (string) $side['club']]); ?>
|
||||||
<span class="stadionzeitung-ergebnisse__club"><?= e((string) $side['club']) ?></span>
|
<span class="stadionzeitung-ergebnisse__club"><?= e((string) $side['club']) ?></span>
|
||||||
<span class="stadionzeitung-ergebnisse__goals"><?= e((string) $side['goals']) ?></span>
|
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</article>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</section>
|
</section>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|||||||
50
app/components/stadionzeitung-galerie.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Galerie-Seite der Stadionzeitung (Live-Seite, optional) — Fotos vom letzten
|
||||||
|
* Spiel als Bento-Raster: ein großes Aufmacherfoto, die übrigen in kleineren
|
||||||
|
* Kacheln darum. Das Raster füllt die ganze Blattfläche unter dem Kopf, die
|
||||||
|
* Fotos werden per object-fit auf ihre Kachel beschnitten.
|
||||||
|
*
|
||||||
|
* Props: $issue Eintrag aus data/stadionzeitung.json. Gelesen wird das Feld
|
||||||
|
* `galerie`: `kicker`, `heading`, `text?` sind Chat-Pflege, `bilder[]`
|
||||||
|
* (base/widths/alt/source) schreibt bin/stadionzeitung-add.php aus
|
||||||
|
* <ordner>/galerie/. Alt-Texte kommen aus dem Chat und hängen an `source`.
|
||||||
|
*
|
||||||
|
* Die Bilder liegen im Ausgaben-Ordner und werden von keinem Sync angefasst,
|
||||||
|
* deshalb braucht die Seite keinen Snapshot.
|
||||||
|
*/
|
||||||
|
$galerie = is_array($issue['galerie'] ?? null) ? $issue['galerie'] : [];
|
||||||
|
$bilder = array_slice(array_values(array_filter(
|
||||||
|
$galerie['bilder'] ?? [],
|
||||||
|
static fn ($b): bool => is_array($b) && !empty($b['base']) && !empty($b['widths'])
|
||||||
|
)), 0, 7);
|
||||||
|
$kicker = trim((string) ($galerie['kicker'] ?? 'Rückblick'));
|
||||||
|
$heading = trim((string) ($galerie['heading'] ?? 'Fotos vom letzten Spiel'));
|
||||||
|
$text = trim((string) ($galerie['text'] ?? ''));
|
||||||
|
?>
|
||||||
|
<div class="stadionzeitung-galerie">
|
||||||
|
<header class="stadionzeitung-page__head">
|
||||||
|
<?php if ($kicker !== ''): ?>
|
||||||
|
<p class="stadionzeitung-page__kicker"><?= e($kicker) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<h2 class="stadionzeitung-page__heading"><?= e($heading) ?></h2>
|
||||||
|
<?php if ($text !== ''): ?>
|
||||||
|
<p class="stadionzeitung-galerie__text"><?= e($text) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</header>
|
||||||
|
<?php if ($bilder !== []): ?>
|
||||||
|
<div class="stadionzeitung-galerie__bento stadionzeitung-galerie__bento--n<?= count($bilder) ?>">
|
||||||
|
<?php foreach ($bilder as $i => $bild): ?>
|
||||||
|
<?php component('img', [
|
||||||
|
'image' => $bild,
|
||||||
|
// Aufmacher gut zwei Drittel der Seitenbreite, die Kacheln ein Drittel.
|
||||||
|
'sizes' => $i === 0 ? '(min-width: 520px) 340px, 70vw' : '(min-width: 520px) 240px, 50vw',
|
||||||
|
'class' => 'stadionzeitung-galerie__foto',
|
||||||
|
]); ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
@@ -18,15 +18,29 @@ $teamName = trim((string) ($info['team_name'] ?? ''));
|
|||||||
$fixture = is_array($info['fixture'] ?? null) ? $info['fixture'] : [];
|
$fixture = is_array($info['fixture'] ?? null) ? $info['fixture'] : [];
|
||||||
$own = is_array($info['own'] ?? null) ? $info['own'] : null;
|
$own = is_array($info['own'] ?? null) ? $info['own'] : null;
|
||||||
$opponent = is_array($info['opponent_stats'] ?? null) ? $info['opponent_stats'] : null;
|
$opponent = is_array($info['opponent_stats'] ?? null) ? $info['opponent_stats'] : null;
|
||||||
$opponentName = (string) ($info['opponent'] ?? 'Unser Gegner');
|
// Gedruckt wird der Anzeigename (Kurzname der Ausgabe, wenn gepflegt) — die
|
||||||
|
// BFV-Schreibweise „(SG1) TSV Wirsberg I/ FC Kupferberg I" ist ein Suchschlüssel,
|
||||||
|
// keine Überschrift.
|
||||||
|
$ausschnitt = is_array($info['table_excerpt'] ?? null) ? $info['table_excerpt'] : [];
|
||||||
|
$opponentName = (string) ($info['opponent_display'] ?? $info['opponent'] ?? 'Unser Gegner');
|
||||||
|
|
||||||
|
// Links steht IMMER unsere Mannschaft (Klasse --own hebt sie hervor), rechts der
|
||||||
|
// Gegner. Bei einem Heimspiel ist das ohnehin die Reihenfolge Heim/Gast; fährt die
|
||||||
|
// Mannschaft aber auswärts, trüge die Hervorhebung sonst der gegnerische Verein.
|
||||||
|
$istHeim = !empty($fixture['is_home']);
|
||||||
|
$ownName = (string) (($istHeim ? ($fixture['home'] ?? null) : ($fixture['away'] ?? null)) ?? 'TSV 08 Kulmbach');
|
||||||
|
$ownCrest = ($istHeim ? ($fixture['home_crest'] ?? null) : ($fixture['away_crest'] ?? null)) ?? $own['crest'] ?? null;
|
||||||
|
$gegnerLabel = $opponentName;
|
||||||
|
$gegnerCrest = ($istHeim ? ($fixture['away_crest'] ?? null) : ($fixture['home_crest'] ?? null)) ?? $opponent['crest'] ?? null;
|
||||||
|
|
||||||
$kickoff = (string) ($fixture['kickoff'] ?? '');
|
$kickoff = (string) ($fixture['kickoff'] ?? '');
|
||||||
$kickoffDate = $kickoff !== '' ? substr($kickoff, 0, 10) : '';
|
$kickoffDate = $kickoff !== '' ? substr($kickoff, 0, 10) : '';
|
||||||
$kickoffTime = $kickoff !== '' ? substr($kickoff, 11, 5) : '';
|
$kickoffTime = $kickoff !== '' ? substr($kickoff, 11, 5) : '';
|
||||||
|
|
||||||
|
// Der Tabellenplatz steht NICHT in der Balkenliste: klein ist dort besser,
|
||||||
|
// ein Balken läge genau falsch herum. Er gehört nach oben neben die Wappen.
|
||||||
|
// „Spiele“ fällt ebenfalls raus (für beide gleich, steht in der Kopfzeile).
|
||||||
$rows = [
|
$rows = [
|
||||||
['label' => 'Tabellenplatz', 'key' => 'rank'],
|
|
||||||
['label' => 'Spiele', 'key' => 'played'],
|
|
||||||
['label' => 'Siege', 'key' => 'won'],
|
['label' => 'Siege', 'key' => 'won'],
|
||||||
['label' => 'Unentschieden', 'key' => 'drawn'],
|
['label' => 'Unentschieden', 'key' => 'drawn'],
|
||||||
['label' => 'Niederlagen', 'key' => 'lost'],
|
['label' => 'Niederlagen', 'key' => 'lost'],
|
||||||
@@ -41,6 +55,57 @@ $format = static function (?array $row, string $key, bool $signed = false): stri
|
|||||||
$value = (int) $row[$key];
|
$value = (int) $row[$key];
|
||||||
return $signed && $value > 0 ? '+' . $value : (string) $value;
|
return $signed && $value > 0 ? '+' . $value : (string) $value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Balkenlänge als KLASSE, nie als style-Attribut: die CSP verwirft inline
|
||||||
|
// styles stillschweigend (siehe CLAUDE.md), der Balken wäre in Produktion
|
||||||
|
// unsichtbar. Deshalb 5-Prozent-Stufen, die components.css ausdefiniert.
|
||||||
|
$barStep = static function (?array $row, ?array $other, string $key): int {
|
||||||
|
if ($row === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$value = abs((int) ($row[$key] ?? 0));
|
||||||
|
$scale = max($value, $other === null ? 0 : abs((int) ($other[$key] ?? 0)));
|
||||||
|
if ($scale <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (int) round($value / $scale * 20);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ein Satz statt einer Zahlenkolonne: derselbe Bestand, nur in Sprache. Die
|
||||||
|
// erste zutreffende Regel gewinnt, damit immer genau eine Aussage dasteht.
|
||||||
|
$zahlwort = static function (int $n): string {
|
||||||
|
$woerter = [0 => 'Kein', 1 => 'Ein', 2 => 'Zwei', 3 => 'Drei', 4 => 'Vier', 5 => 'Fünf',
|
||||||
|
6 => 'Sechs', 7 => 'Sieben', 8 => 'Acht', 9 => 'Neun', 10 => 'Zehn', 11 => 'Elf',
|
||||||
|
12 => 'Zwölf', 13 => 'Dreizehn', 14 => 'Vierzehn', 15 => 'Fünfzehn'];
|
||||||
|
return $woerter[$n] ?? (string) $n;
|
||||||
|
};
|
||||||
|
$aufmacher = '';
|
||||||
|
if ($opponent !== null) {
|
||||||
|
$played = (int) ($opponent['played'] ?? 0);
|
||||||
|
$won = (int) ($opponent['won'] ?? 0);
|
||||||
|
$lost = (int) ($opponent['lost'] ?? 0);
|
||||||
|
$rank = (int) ($opponent['rank'] ?? 0);
|
||||||
|
$points = (int) ($opponent['points'] ?? 0);
|
||||||
|
$diff = (int) ($opponent['goal_diff'] ?? 0);
|
||||||
|
if ($played > 0 && $lost === 0) {
|
||||||
|
$aufmacher = 'Noch ohne Niederlage';
|
||||||
|
} elseif ($played > 0 && $won === 0) {
|
||||||
|
$aufmacher = 'Noch ohne Sieg';
|
||||||
|
} elseif ($played > 0 && $won / $played >= 0.7) {
|
||||||
|
$aufmacher = $zahlwort($won) . ' Siege aus ' . mb_strtolower($zahlwort($played)) . ' Spielen';
|
||||||
|
} elseif ($rank === 1) {
|
||||||
|
$aufmacher = 'Tabellenführer mit ' . $points . ' Punkten';
|
||||||
|
} elseif ($played > 0 && $diff <= -8) {
|
||||||
|
$aufmacher = $zahlwort(abs($diff)) . ' Tore mehr kassiert als geschossen';
|
||||||
|
} elseif ($played > 0 && $diff >= 8) {
|
||||||
|
$aufmacher = $zahlwort($diff) . ' Tore mehr geschossen als kassiert';
|
||||||
|
} elseif ($played > 0) {
|
||||||
|
$aufmacher = $points . ' Punkte aus ' . $played . ' Spielen';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$spieltagInfo = $opponent !== null && (int) ($opponent['played'] ?? 0) > 0
|
||||||
|
? 'Stand nach ' . (int) $opponent['played'] . ' Spieltagen'
|
||||||
|
: '';
|
||||||
?>
|
?>
|
||||||
<div class="stadionzeitung-gegner">
|
<div class="stadionzeitung-gegner">
|
||||||
<header class="stadionzeitung-page__head">
|
<header class="stadionzeitung-page__head">
|
||||||
@@ -53,25 +118,26 @@ $format = static function (?array $row, string $key, bool $signed = false): stri
|
|||||||
<span class="stadionzeitung-page__rule" aria-hidden="true"></span>
|
<span class="stadionzeitung-page__rule" aria-hidden="true"></span>
|
||||||
|
|
||||||
<div class="stadionzeitung-gegner__duell">
|
<div class="stadionzeitung-gegner__duell">
|
||||||
<div class="stadionzeitung-gegner__club stadionzeitung-gegner__club--own">
|
<div class="stadionzeitung-gegner__club">
|
||||||
<?php component('crest', ['src' => $fixture['home_crest'] ?? $own['crest'] ?? null, 'name' => $fixture['home'] ?? 'TSV 08 Kulmbach']); ?>
|
<?php component('crest', ['src' => $ownCrest, 'name' => $ownName]); ?>
|
||||||
<strong><?= e((string) ($fixture['home'] ?? 'TSV 08 Kulmbach')) ?></strong>
|
<span class="stadionzeitung-gegner__club-name"><?= e($ownName) ?></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stadionzeitung-gegner__match">
|
<div class="stadionzeitung-gegner__vs">
|
||||||
<span>gegen</span>
|
<p class="stadionzeitung-gegner__vs-wort"><?= $istHeim ? 'gegen' : 'bei' ?></p>
|
||||||
<?php if ($kickoffDate !== ''): ?>
|
<?php if ($kickoffDate !== ''): ?>
|
||||||
<strong><?= e(german_date($kickoffDate)) ?></strong>
|
<p class="stadionzeitung-gegner__anstoss"><?= e(german_date($kickoffDate)) ?><?php if ($kickoffTime !== ''): ?><br><?= e($kickoffTime) ?> Uhr<?php endif; ?></p>
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($kickoffTime !== ''): ?>
|
|
||||||
<span><?= e($kickoffTime) ?> Uhr</span>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="stadionzeitung-gegner__club">
|
<div class="stadionzeitung-gegner__club">
|
||||||
<?php component('crest', ['src' => $fixture['away_crest'] ?? $opponent['crest'] ?? null, 'name' => $fixture['away'] ?? $opponentName]); ?>
|
<?php component('crest', ['src' => $gegnerCrest, 'name' => $gegnerLabel]); ?>
|
||||||
<strong><?= e((string) ($fixture['away'] ?? $opponentName)) ?></strong>
|
<span class="stadionzeitung-gegner__club-name"><?= e($gegnerLabel) ?></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if ($aufmacher !== ''): ?>
|
||||||
|
<p class="stadionzeitung-gegner__aufmacher"><?= e($aufmacher) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($own !== null && $opponent !== null): ?>
|
<?php if ($own !== null && $opponent !== null): ?>
|
||||||
<div class="stadionzeitung-gegner__vergleich" role="table" aria-label="Tabellenvergleich">
|
<div class="stadionzeitung-gegner__vergleich" role="table" aria-label="Tabellenvergleich">
|
||||||
<div class="stadionzeitung-gegner__vergleich-head" role="row">
|
<div class="stadionzeitung-gegner__vergleich-head" role="row">
|
||||||
@@ -79,18 +145,33 @@ $format = static function (?array $row, string $key, bool $signed = false): stri
|
|||||||
Markup — das Kürzel des Marktschorgast-Gegners wäre in jeder Ausgabe
|
Markup — das Kürzel des Marktschorgast-Gegners wäre in jeder Ausgabe
|
||||||
mitgedruckt worden. Die Überschrift nennt den Gegner ohnehin. */ ?>
|
mitgedruckt worden. Die Überschrift nennt den Gegner ohnehin. */ ?>
|
||||||
<span role="columnheader">Wir</span>
|
<span role="columnheader">Wir</span>
|
||||||
<span role="columnheader">Vergleich</span>
|
<span role="columnheader"><?= e($spieltagInfo !== '' ? $spieltagInfo : 'Vergleich') ?></span>
|
||||||
<span role="columnheader">Gegner</span>
|
<span role="columnheader">Gegner</span>
|
||||||
</div>
|
</div>
|
||||||
<?php foreach ($rows as $row): ?>
|
<?php foreach ($rows as $row): ?>
|
||||||
<div class="stadionzeitung-gegner__vergleich-row" role="row">
|
<div class="stadionzeitung-gegner__vergleich-row" role="row">
|
||||||
<strong role="cell"><?= e($format($own, (string) $row['key'], (bool) ($row['signed'] ?? false))) ?></strong>
|
<strong class="stadionzeitung-gegner__wert" role="cell"><?= e($format($own, (string) $row['key'], (bool) ($row['signed'] ?? false))) ?></strong>
|
||||||
<span role="rowheader"><?= e((string) $row['label']) ?></span>
|
<span class="stadionzeitung-gegner__balken stadionzeitung-gegner__balken--own" aria-hidden="true"><span class="stadionzeitung-gegner__balken-fuellung stadionzeitung-gegner__balken-fuellung--s<?= $barStep($own, $opponent, (string) $row['key']) ?>"></span></span>
|
||||||
<strong role="cell"><?= e($format($opponent, (string) $row['key'], (bool) ($row['signed'] ?? false))) ?></strong>
|
<span class="stadionzeitung-gegner__label" role="rowheader"><?= e((string) $row['label']) ?></span>
|
||||||
|
<span class="stadionzeitung-gegner__balken" aria-hidden="true"><span class="stadionzeitung-gegner__balken-fuellung stadionzeitung-gegner__balken-fuellung--s<?= $barStep($opponent, $own, (string) $row['key']) ?>"></span></span>
|
||||||
|
<strong class="stadionzeitung-gegner__wert" role="cell"><?= e($format($opponent, (string) $row['key'], (bool) ($row['signed'] ?? false))) ?></strong>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<p class="stadionzeitung-gegner__empty">Die aktuellen Vergleichsdaten waren beim letzten Abruf nicht verfügbar.</p>
|
<p class="stadionzeitung-gegner__empty">Die aktuellen Vergleichsdaten waren beim letzten Abruf nicht verfügbar.</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php /* Der Tabellenplatz stand kurz als zwei lose Ziffern oben im Duell und
|
||||||
|
wirkte dort unruhig (Felix, 19.09.2026). Hier zeigt ihn der Ausschnitt
|
||||||
|
im Zusammenhang: wer steht zwischen uns und ihnen. Gerendert mit der
|
||||||
|
geteilten league-table-Komponente, nie mit einer zweiten Tabelle. */ ?>
|
||||||
|
<?php if ($ausschnitt !== []): ?>
|
||||||
|
<div class="stadionzeitung-gegner__ausschnitt">
|
||||||
|
<p class="stadionzeitung-gegner__ausschnitt-titel">Die Tabelle dazwischen</p>
|
||||||
|
<?php foreach ($ausschnitt as $block): ?>
|
||||||
|
<?php component('league-table', ['table' => $block, 'caption' => 'Tabellenausschnitt ' . (string) ($info['league'] ?? '')]); ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Mitglied-werden-Seite der Stadionzeitung (Live-Seite) — Poster statt Liste:
|
* Mitglied-werden-Seite der Stadionzeitung (Live-Seite) — Poster statt Liste:
|
||||||
* riesige zentrierte Coolvetica-Headline („Werde Teil des Nullachters", das
|
* riesige zentrierte Coolvetica-Headline („Werde Teil unseres Vereins", das
|
||||||
* letzte Wort in Vereinsrot), darunter Claim-Satz und Abteilungszeile,
|
* letzte Wort in Vereinsrot), darunter Claim-Satz und Abteilungszeile,
|
||||||
* unten der geteilte CTA-Fuß mit QR auf /mitglied-werden. Fester Baustein
|
* unten der geteilte CTA-Fuß mit QR auf /mitglied-werden. Fester Baustein
|
||||||
* jeder Ausgabe (bin/stadionzeitung-add.php plant sie nach den
|
* jeder Ausgabe (bin/stadionzeitung-add.php plant sie nach den
|
||||||
@@ -25,8 +25,8 @@ $mitgliedLink = club_domain('mitglied-werden');
|
|||||||
<img class="stadionzeitung-mitglied__logo" src="<?= e(asset('img/logo.svg')) ?>" alt="" width="75" height="97">
|
<img class="stadionzeitung-mitglied__logo" src="<?= e(asset('img/logo.svg')) ?>" alt="" width="75" height="97">
|
||||||
<h2 class="stadionzeitung-mitglied__headline">
|
<h2 class="stadionzeitung-mitglied__headline">
|
||||||
<span class="stadionzeitung-mitglied__line">Werde Teil</span>
|
<span class="stadionzeitung-mitglied__line">Werde Teil</span>
|
||||||
<span class="stadionzeitung-mitglied__line">des</span>
|
<span class="stadionzeitung-mitglied__line">unseres</span>
|
||||||
<span class="stadionzeitung-mitglied__line stadionzeitung-mitglied__line--accent">Nullachters</span>
|
<span class="stadionzeitung-mitglied__line stadionzeitung-mitglied__line--accent">Vereins</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p class="stadionzeitung-mitglied__claim">Seit 1908 in Kulmbach zuhause. Egal ob auf dem Platz, in der Halle oder am Spielfeldrand: bei uns ist Platz für dich.</p>
|
<p class="stadionzeitung-mitglied__claim">Seit 1908 in Kulmbach zuhause. Egal ob auf dem Platz, in der Halle oder am Spielfeldrand: bei uns ist Platz für dich.</p>
|
||||||
<p class="stadionzeitung-mitglied__departments">Fußball · Jugend · Turnen</p>
|
<p class="stadionzeitung-mitglied__departments">Fußball · Jugend · Turnen</p>
|
||||||
|
|||||||
41
app/components/stadionzeitung-rueckblick.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/** Redaktioneller Spielrückblick. Text/Quelle: Chat-Pflege; image: add-Skript. */
|
||||||
|
$rueckblick = $issue['rueckblick'] ?? [];
|
||||||
|
$sourceUrl = (string) ($rueckblick['source_url'] ?? '');
|
||||||
|
$sourceLink = filter_var($sourceUrl, FILTER_VALIDATE_URL)
|
||||||
|
&& in_array(parse_url($sourceUrl, PHP_URL_SCHEME), ['https', 'http'], true);
|
||||||
|
?>
|
||||||
|
<div class="stadionzeitung-rueckblick">
|
||||||
|
<header class="stadionzeitung-page__head">
|
||||||
|
<p class="stadionzeitung-page__kicker"><?= e((string) ($rueckblick['kicker'] ?? 'Rückblick')) ?></p>
|
||||||
|
<h2 class="stadionzeitung-page__heading"><?= e((string) ($rueckblick['heading'] ?? '')) ?></h2>
|
||||||
|
</header>
|
||||||
|
<span class="stadionzeitung-page__rule" aria-hidden="true"></span>
|
||||||
|
<div class="stadionzeitung-rueckblick__scoreline">
|
||||||
|
<p class="stadionzeitung-ergebnisse__result"><?= e((string) ($rueckblick['result'] ?? '')) ?></p>
|
||||||
|
<p class="stadionzeitung-page__sub"><?= e((string) ($rueckblick['meta'] ?? '')) ?></p>
|
||||||
|
</div>
|
||||||
|
<figure class="stadionzeitung-rueckblick__figure">
|
||||||
|
<?php component('img', [
|
||||||
|
'image' => $rueckblick['image'],
|
||||||
|
'sizes' => '(min-width: 520px) 430px, 90vw',
|
||||||
|
'class' => 'stadionzeitung-rueckblick__foto',
|
||||||
|
'eager' => $eager ?? false,
|
||||||
|
]); ?>
|
||||||
|
<figcaption class="stadionzeitung-rueckblick__credit">
|
||||||
|
<?php if ($sourceLink): ?>
|
||||||
|
<a href="<?= e($sourceUrl) ?>" target="_blank" rel="noopener noreferrer"><?= e((string) ($rueckblick['credit'] ?? '')) ?></a>
|
||||||
|
<?php else: ?>
|
||||||
|
<?= e((string) ($rueckblick['credit'] ?? '')) ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
<div class="stadionzeitung-rueckblick__text">
|
||||||
|
<?php foreach ($rueckblick['paragraphs'] ?? [] as $paragraph): ?>
|
||||||
|
<p class="stadionzeitung-vorwort__text"><?= e((string) $paragraph) ?></p>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -11,7 +11,8 @@ declare(strict_types=1);
|
|||||||
* type 'image' (base/widths/alt),
|
* type 'image' (base/widths/alt),
|
||||||
* type 'live' (page: cover|vorwort|interview|programm|aufruf|turnier|
|
* type 'live' (page: cover|vorwort|interview|programm|aufruf|turnier|
|
||||||
* tabelle|termine|news|ergebnisse|gegner|vorschau|kontakt|
|
* tabelle|termine|news|ergebnisse|gegner|vorschau|kontakt|
|
||||||
* mitglied|historie|sportheim|partner|danke|momente|impressum;
|
* mitglied|historie|sportheim|partner|danke|momente|galerie|
|
||||||
|
* impressum;
|
||||||
* tabelle und gegner optional mit team: Team-Key aus
|
* tabelle und gegner optional mit team: Team-Key aus
|
||||||
* matchcenter.json; turnier mit turnier: Key aus
|
* matchcenter.json; turnier mit turnier: Key aus
|
||||||
* veranstaltungen.json → turniere[] der Ausgaben-Veranstaltung),
|
* veranstaltungen.json → turniere[] der Ausgaben-Veranstaltung),
|
||||||
@@ -49,6 +50,9 @@ $type = $entry['type'] ?? 'image';
|
|||||||
<?php case 'interview': ?>
|
<?php case 'interview': ?>
|
||||||
<?php component('stadionzeitung-interview', ['issue' => $issue]); ?>
|
<?php component('stadionzeitung-interview', ['issue' => $issue]); ?>
|
||||||
<?php break; ?>
|
<?php break; ?>
|
||||||
|
<?php case 'beitrag': ?>
|
||||||
|
<?php component('stadionzeitung-beitrag', ['issue' => $issue, 'eager' => $eager]); ?>
|
||||||
|
<?php break; ?>
|
||||||
<?php case 'aufruf': ?>
|
<?php case 'aufruf': ?>
|
||||||
<?php component('stadionzeitung-aufruf', ['issue' => $issue]); ?>
|
<?php component('stadionzeitung-aufruf', ['issue' => $issue]); ?>
|
||||||
<?php break; ?>
|
<?php break; ?>
|
||||||
@@ -96,6 +100,12 @@ $type = $entry['type'] ?? 'image';
|
|||||||
wie jede andere Anzeige in images[] eingefroren ist. */ ?>
|
wie jede andere Anzeige in images[] eingefroren ist. */ ?>
|
||||||
<?php component('stadionzeitung-danke', ['anzeige' => $entry['anzeige'] ?? null]); ?>
|
<?php component('stadionzeitung-danke', ['anzeige' => $entry['anzeige'] ?? null]); ?>
|
||||||
<?php break; ?>
|
<?php break; ?>
|
||||||
|
<?php case 'galerie': ?>
|
||||||
|
<?php component('stadionzeitung-galerie', ['issue' => $issue]); ?>
|
||||||
|
<?php break; ?>
|
||||||
|
<?php case 'rueckblick': ?>
|
||||||
|
<?php component('stadionzeitung-rueckblick', ['issue' => $issue, 'eager' => $eager]); ?>
|
||||||
|
<?php break; ?>
|
||||||
<?php case 'momente': ?>
|
<?php case 'momente': ?>
|
||||||
<?php component('stadionzeitung-momente', ['items' => $data['momente'] ?? []]); ?>
|
<?php component('stadionzeitung-momente', ['items' => $data['momente'] ?? []]); ?>
|
||||||
<?php break; ?>
|
<?php break; ?>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ $total = count($images);
|
|||||||
if ($images === []) {
|
if ($images === []) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$opponent = (string) ($issue['opponent'] ?? '');
|
$opponent = stadionzeitung_opponent_display($issue);
|
||||||
|
|
||||||
// Live-Daten nur laden, wenn die Ausgabe tatsächlich eine Live-Seite enthält.
|
// Live-Daten nur laden, wenn die Ausgabe tatsächlich eine Live-Seite enthält.
|
||||||
$hasLive = false;
|
$hasLive = false;
|
||||||
|
|||||||
103
app/helpers.php
@@ -992,6 +992,22 @@ function stadionzeitung_teaser(array $issue): string
|
|||||||
return trim((string) ($issue['interview']['teaser'] ?? ''));
|
return trim((string) ($issue['interview']['teaser'] ?? ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Name des Gegners, wie er GEDRUCKT wird — Cover, Archivzeile, Kopf, Meta.
|
||||||
|
*
|
||||||
|
* `opponent` muss die BFV-Schreibweise tragen, weil die Gegnerseite damit ihre
|
||||||
|
* Tabellenzeile und ihre Begegnung in matchcenter.json findet (exakter
|
||||||
|
* Namensvergleich). Bei Spielgemeinschaften ist das eine Zeile wie
|
||||||
|
* „(SG1) TSV Wirsberg I/ FC Kupferberg I", die auf einem Cover nichts zu
|
||||||
|
* suchen hat. `opponent_short` ist deshalb der im Chat gepflegte Anzeigename
|
||||||
|
* („Wirsberg/Kupferberg"); fehlt er, bleibt es beim vollen Namen.
|
||||||
|
*/
|
||||||
|
function stadionzeitung_opponent_display(array $issue): string
|
||||||
|
{
|
||||||
|
$short = trim((string) ($issue['opponent_short'] ?? ''));
|
||||||
|
return $short !== '' ? $short : trim((string) ($issue['opponent'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Welche Mannschaften bekommen in einer Ausgabe eine Gegnerseite — die EINE
|
* Welche Mannschaften bekommen in einer Ausgabe eine Gegnerseite — die EINE
|
||||||
* Logik dafür, benutzt von bin/stadionzeitung-add.php (Seitenplanung) UND
|
* Logik dafür, benutzt von bin/stadionzeitung-add.php (Seitenplanung) UND
|
||||||
@@ -999,10 +1015,19 @@ function stadionzeitung_teaser(array $issue): string
|
|||||||
* oder Daten ohne Seite entstehen.
|
* oder Daten ohne Seite entstehen.
|
||||||
*
|
*
|
||||||
* Immer: die tragende Mannschaft (Gegner = `opponent`, Tag = `date`).
|
* Immer: die tragende Mannschaft (Gegner = `opponent`, Tag = `date`).
|
||||||
* Zusätzlich, wenn die Ausgabe eine `veranstaltung` (Slug) mit Zeitraum trägt
|
* Zusätzlich auf zwei Wegen, die sich nicht in die Quere kommen:
|
||||||
* (Sportfest-Ausgabe): jede weitere Mannschaft aus matchcenter.json mit einem
|
*
|
||||||
* HEIMspiel in diesem Zeitraum — drei Heimspiele am Fest, drei Gegnerseiten.
|
* 1. `veranstaltung` (Slug) mit Zeitraum — die Sportfest-Ausgabe: jede weitere
|
||||||
* Der Gegner ist dann der andere Verein der Begegnung. Reihenfolge wie in
|
* Mannschaft aus matchcenter.json mit einem HEIMspiel in diesem Zeitraum.
|
||||||
|
* Am Fest spielen alle zuhause, das ist der Aufhänger.
|
||||||
|
* 2. `gegner_teams` (Liste von Team-Keys, im Chat gepflegt) — für ein normales
|
||||||
|
* Spielwochenende, an dem auch die anderen Mannschaften antreten. Genommen
|
||||||
|
* wird ihr nächstes Spiel im Fenster von zwei Tagen vor bis drei Tage nach
|
||||||
|
* dem Ausgabedatum, AUCH auswärts: der Gegner der Frauen ist auch dann eine
|
||||||
|
* Seite wert, wenn wir zu ihm fahren. Ohne Spiel in diesem Fenster entsteht
|
||||||
|
* keine Seite; bin/stadionzeitung-add.php meldet das.
|
||||||
|
*
|
||||||
|
* Der Gegner ist jeweils der andere Verein der Begegnung. Reihenfolge wie in
|
||||||
* matchcenter.json (= config 'matchcenter.teams').
|
* matchcenter.json (= config 'matchcenter.teams').
|
||||||
*
|
*
|
||||||
* Rückgabe: [team_key => ['opponent' => string, 'date' => 'YYYY-MM-DD']]
|
* Rückgabe: [team_key => ['opponent' => string, 'date' => 'YYYY-MM-DD']]
|
||||||
@@ -1013,14 +1038,19 @@ function stadionzeitung_gegner_fixtures(array $issue): array
|
|||||||
$issueTeam = (string) ($issue['team_key'] ?? '');
|
$issueTeam = (string) ($issue['team_key'] ?? '');
|
||||||
$issueOpponent = trim((string) ($issue['opponent'] ?? ''));
|
$issueOpponent = trim((string) ($issue['opponent'] ?? ''));
|
||||||
|
|
||||||
$von = '';
|
$eventVon = '';
|
||||||
$bis = '';
|
$eventBis = '';
|
||||||
$veranstaltung = veranstaltung_find((string) ($issue['veranstaltung'] ?? ''));
|
$veranstaltung = veranstaltung_find((string) ($issue['veranstaltung'] ?? ''));
|
||||||
if ($veranstaltung !== null) {
|
if ($veranstaltung !== null) {
|
||||||
$von = (string) ($veranstaltung['date_start'] ?? '');
|
$eventVon = (string) ($veranstaltung['date_start'] ?? '');
|
||||||
$bis = (string) ($veranstaltung['date_end'] ?? $von);
|
$eventBis = (string) ($veranstaltung['date_end'] ?? $eventVon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$extraTeams = array_values(array_filter(array_map(
|
||||||
|
static fn ($k): string => trim((string) $k),
|
||||||
|
is_array($issue['gegner_teams'] ?? null) ? $issue['gegner_teams'] : []
|
||||||
|
)));
|
||||||
|
|
||||||
$fixtures = [];
|
$fixtures = [];
|
||||||
foreach (json_load('matchcenter')['teams'] ?? [] as $team) {
|
foreach (json_load('matchcenter')['teams'] ?? [] as $team) {
|
||||||
$key = (string) ($team['key'] ?? '');
|
$key = (string) ($team['key'] ?? '');
|
||||||
@@ -1033,18 +1063,32 @@ function stadionzeitung_gegner_fixtures(array $issue): array
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Weg 2: ausdrücklich benannte Mannschaft, Fenster ums Ausgabedatum,
|
||||||
|
// Heim wie auswärts. Steht sie in `gegner_teams`, gilt dieses Fenster
|
||||||
|
// und nicht der Veranstaltungszeitraum.
|
||||||
|
if (in_array($key, $extraTeams, true) && $date !== '') {
|
||||||
|
$von = date('Y-m-d', strtotime($date . ' -2 days'));
|
||||||
|
$bis = date('Y-m-d', strtotime($date . ' +3 days'));
|
||||||
|
$nurHeim = false;
|
||||||
|
} else {
|
||||||
|
$von = $eventVon;
|
||||||
|
$bis = $eventBis;
|
||||||
|
$nurHeim = true;
|
||||||
|
}
|
||||||
if ($von === '') {
|
if ($von === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
foreach ($team['next_matches'] ?? [] as $match) {
|
foreach ($team['next_matches'] ?? [] as $match) {
|
||||||
if (empty($match['is_home'])) {
|
$istHeim = !empty($match['is_home']);
|
||||||
|
if ($nurHeim && !$istHeim) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$matchDate = substr((string) ($match['kickoff'] ?? ''), 0, 10);
|
$matchDate = substr((string) ($match['kickoff'] ?? ''), 0, 10);
|
||||||
if ($matchDate < $von || $matchDate > $bis) {
|
if ($matchDate < $von || $matchDate > $bis) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$opponent = trim((string) ($match['away'] ?? ''));
|
$opponent = trim((string) ($istHeim ? ($match['away'] ?? '') : ($match['home'] ?? '')));
|
||||||
if ($opponent === '' || $opponent === 'SPIELFREI') {
|
if ($opponent === '' || $opponent === 'SPIELFREI') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1140,14 +1184,53 @@ function stadionzeitung_build_snapshot(array $issue): array
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `opponent` behält die BFV-Schreibweise (der Namensvergleich oben
|
||||||
|
// hängt daran), gedruckt wird `opponent_display`: bei der tragenden
|
||||||
|
// Mannschaft der Kurzname der Ausgabe, sonst der volle Name.
|
||||||
|
$gegnerDisplay = strcasecmp($gegnerName, trim((string) ($issue['opponent'] ?? ''))) === 0
|
||||||
|
? stadionzeitung_opponent_display($issue)
|
||||||
|
: $gegnerName;
|
||||||
|
|
||||||
|
// Tabellenausschnitt statt zweier loser Platz-Ziffern im Kopf: ein
|
||||||
|
// Fenster um beide Mannschaften herum (mit einer Zeile Luft davor
|
||||||
|
// und dahinter). Liegen sie weit auseinander, werden es zwei
|
||||||
|
// Blöcke — gerendert wird beides mit der geteilten
|
||||||
|
// league-table-Komponente, nie mit einer zweiten Tabelle.
|
||||||
|
$ausschnitt = [];
|
||||||
|
if ($ownRow !== null && $opponentRow !== null && $table !== []) {
|
||||||
|
$ranks = [(int) ($ownRow['rank'] ?? 0), (int) ($opponentRow['rank'] ?? 0)];
|
||||||
|
sort($ranks);
|
||||||
|
$fenster = static function (int $von, int $bis) use ($table): array {
|
||||||
|
return array_values(array_filter($table, static function (array $row) use ($von, $bis): bool {
|
||||||
|
$rank = (int) ($row['rank'] ?? 0);
|
||||||
|
return $rank >= $von && $rank <= $bis;
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
if ($ranks[1] - $ranks[0] <= 5) {
|
||||||
|
$block = $fenster($ranks[0], $ranks[1]);
|
||||||
|
if ($block !== []) {
|
||||||
|
$ausschnitt[] = $block;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foreach ($ranks as $rank) {
|
||||||
|
$block = $fenster($rank, $rank + 1);
|
||||||
|
if ($block !== []) {
|
||||||
|
$ausschnitt[] = $block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$gegner[$key] = [
|
$gegner[$key] = [
|
||||||
'team_name' => (string) ($team['name'] ?? ''),
|
'team_name' => (string) ($team['name'] ?? ''),
|
||||||
'opponent' => $gegnerName,
|
'opponent' => $gegnerName,
|
||||||
|
'opponent_display' => $gegnerDisplay,
|
||||||
'league' => (string) ($team['league'] ?? ''),
|
'league' => (string) ($team['league'] ?? ''),
|
||||||
'season' => (string) ($team['season'] ?? ''),
|
'season' => (string) ($team['season'] ?? ''),
|
||||||
'fixture' => $fixture,
|
'fixture' => $fixture,
|
||||||
'own' => $ownRow,
|
'own' => $ownRow,
|
||||||
'opponent_stats' => $opponentRow,
|
'opponent_stats' => $opponentRow,
|
||||||
|
'table_excerpt' => $ausschnitt,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,14 +78,14 @@ $firstImage = $issue['images'][0] ?? null;
|
|||||||
$cover = ($firstImage !== null && ($firstImage['type'] ?? 'image') === 'image') ? $firstImage : null;
|
$cover = ($firstImage !== null && ($firstImage['type'] ?? 'image') === 'image') ? $firstImage : null;
|
||||||
|
|
||||||
$meta = [
|
$meta = [
|
||||||
'title' => 'Stadionzeitung gegen ' . $issue['opponent'] . ($isDruck ? ' (Druckfassung)' : ''),
|
'title' => 'Stadionzeitung gegen ' . stadionzeitung_opponent_display($issue) . ($isDruck ? ' (Druckfassung)' : ''),
|
||||||
'description' => 'Stadionzeitung zum Heimspiel gegen ' . $issue['opponent'] . ' am ' . german_date($issue['date']) . ' – digital zum Durchblättern.',
|
'description' => 'Stadionzeitung zum Heimspiel gegen ' . stadionzeitung_opponent_display($issue) . ' am ' . german_date($issue['date']) . ' – digital zum Durchblättern.',
|
||||||
'scripts' => [$isDruck ? 'stadionzeitung-druck.js' : 'stadionzeitung-viewer.js'],
|
'scripts' => [$isDruck ? 'stadionzeitung-druck.js' : 'stadionzeitung-viewer.js'],
|
||||||
'schema' => page_schema([
|
'schema' => page_schema([
|
||||||
['name' => 'Startseite', 'slug' => ''],
|
['name' => 'Startseite', 'slug' => ''],
|
||||||
['name' => 'Fußball', 'slug' => 'fussball'],
|
['name' => 'Fußball', 'slug' => 'fussball'],
|
||||||
['name' => 'Stadionzeitung', 'slug' => 'stadionzeitung'],
|
['name' => 'Stadionzeitung', 'slug' => 'stadionzeitung'],
|
||||||
['name' => 'Gegen ' . $issue['opponent'], 'slug' => 'stadionzeitung/' . $issue['slug']],
|
['name' => 'Gegen ' . stadionzeitung_opponent_display($issue), 'slug' => 'stadionzeitung/' . $issue['slug']],
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
if ($cover !== null) {
|
if ($cover !== null) {
|
||||||
@@ -113,7 +113,7 @@ $druckBlaetter = (int) ceil($druckSeiten / 4);
|
|||||||
<a class="stadionzeitung-issue-head__back" href="<?= e($isDruck ? url('stadionzeitung/' . $issue['slug']) : url('stadionzeitung')) ?>">
|
<a class="stadionzeitung-issue-head__back" href="<?= e($isDruck ? url('stadionzeitung/' . $issue['slug']) : url('stadionzeitung')) ?>">
|
||||||
<?= icon('chevron-left') ?> <?= $isDruck ? 'Ausgabe' : 'Archiv' ?>
|
<?= icon('chevron-left') ?> <?= $isDruck ? 'Ausgabe' : 'Archiv' ?>
|
||||||
</a>
|
</a>
|
||||||
<p class="stadionzeitung-issue-head__title">Gegen <?= e($issue['opponent']) ?></p>
|
<p class="stadionzeitung-issue-head__title">Gegen <?= e(stadionzeitung_opponent_display($issue)) ?></p>
|
||||||
<?php if ($isDruck): ?>
|
<?php if ($isDruck): ?>
|
||||||
<p class="stadionzeitung-issue-head__actions">
|
<p class="stadionzeitung-issue-head__actions">
|
||||||
<button class="btn btn--sm stadionzeitung-issue-head__print" type="button" data-druck-print hidden>
|
<button class="btn btn--sm stadionzeitung-issue-head__print" type="button" data-druck-print hidden>
|
||||||
|
|||||||
@@ -149,6 +149,11 @@ if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) {
|
|||||||
// --- HTTP-Helfer, unverändert aus bin/instagram-sync.php übernommen ---
|
// --- HTTP-Helfer, unverändert aus bin/instagram-sync.php übernommen ---
|
||||||
$fetch = static function (string $url, array $headers = []): string|false {
|
$fetch = static function (string $url, array $headers = []): string|false {
|
||||||
$ch = curl_init($url);
|
$ch = curl_init($url);
|
||||||
|
// Optional wie im Matchcenter-Sync, wenn der lokale DNS-Dienst ausfällt.
|
||||||
|
$dohUrl = getenv('TSV_HTTP_DOH_URL') ?: '';
|
||||||
|
if ($dohUrl !== '') {
|
||||||
|
curl_setopt($ch, CURLOPT_DOH_URL, $dohUrl);
|
||||||
|
}
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_FOLLOWLOCATION => true,
|
CURLOPT_FOLLOWLOCATION => true,
|
||||||
|
|||||||
@@ -94,6 +94,11 @@ if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) {
|
|||||||
// --- HTTP-Helfer mit Browser-typischen Headern (gegen HTTP 418) ---
|
// --- HTTP-Helfer mit Browser-typischen Headern (gegen HTTP 418) ---
|
||||||
$fetch = static function (string $url, bool $json = true) use ($referer): string|false {
|
$fetch = static function (string $url, bool $json = true) use ($referer): string|false {
|
||||||
$ch = curl_init($url);
|
$ch = curl_init($url);
|
||||||
|
// Optionaler CLI-Ausweg bei defektem lokalem DNS; keine Systemänderung.
|
||||||
|
$dohUrl = PHP_SAPI === 'cli' ? (getenv('TSV_HTTP_DOH_URL') ?: '') : '';
|
||||||
|
if ($dohUrl !== '') {
|
||||||
|
curl_setopt($ch, CURLOPT_DOH_URL, $dohUrl);
|
||||||
|
}
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_FOLLOWLOCATION => true,
|
CURLOPT_FOLLOWLOCATION => true,
|
||||||
|
|||||||
@@ -386,6 +386,63 @@ if (is_file($szFile)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Stadionzeitung: Galerie-Fotos vorhanden? ---
|
||||||
|
// Die Galerie-Bilder liegen im Ausgaben-Ordner, den bin/stadionzeitung-add.php bei
|
||||||
|
// jedem Lauf leert und neu füllt. Fehlt eine Datei (Lauf ohne galerie/ im Ordner,
|
||||||
|
// Bild nicht mitcommittet), zeigt die Seite ein kaputtes Bild.
|
||||||
|
if (is_file($szFile)) {
|
||||||
|
$szData = $szData ?? (json_decode((string) file_get_contents($szFile), true) ?: []);
|
||||||
|
foreach ($szData['issues'] ?? [] as $ausgabe) {
|
||||||
|
if (is_array($ausgabe['beitrag'] ?? null)) {
|
||||||
|
$beitrag = $ausgabe['beitrag'];
|
||||||
|
foreach (['heading', 'source', 'source_url', 'image_credit'] as $feld) {
|
||||||
|
if (trim((string) ($beitrag[$feld] ?? '')) === '') {
|
||||||
|
$errors[] = "Stadionzeitung '{$ausgabe['slug']}': Beitrag ohne {$feld}.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$quelle = (string) ($beitrag['source_url'] ?? '');
|
||||||
|
if (!filter_var($quelle, FILTER_VALIDATE_URL) || parse_url($quelle, PHP_URL_SCHEME) !== 'https') {
|
||||||
|
$errors[] = "Stadionzeitung '{$ausgabe['slug']}': Beitrag mit ungültiger Quellen-URL.";
|
||||||
|
}
|
||||||
|
$foto = $beitrag['image'] ?? [];
|
||||||
|
if (empty($foto['base']) || empty($foto['widths']) || empty($foto['alt'])) {
|
||||||
|
$errors[] = "Stadionzeitung '{$ausgabe['slug']}': Beitragsfoto oder Alt-Text fehlt.";
|
||||||
|
}
|
||||||
|
foreach ($foto['widths'] ?? [] as $w) {
|
||||||
|
if (!is_file(PUBLIC_PATH . '/assets/' . ($foto['base'] ?? '') . "-{$w}.jpg")) {
|
||||||
|
$errors[] = "Stadionzeitung '{$ausgabe['slug']}': Beitragsfoto fehlt ({$w}px).";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/' . $ausgabe['slug'] . '/qr-beitrag.png')) {
|
||||||
|
$errors[] = "Stadionzeitung '{$ausgabe['slug']}': Beitrags-QR fehlt.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (is_array($ausgabe['rueckblick'] ?? null)) {
|
||||||
|
$rueckblick = $ausgabe['rueckblick'];
|
||||||
|
$foto = $rueckblick['image'] ?? [];
|
||||||
|
if (empty($foto['base']) || empty($foto['widths'])) {
|
||||||
|
$errors[] = "Stadionzeitung '{$ausgabe['slug']}': Foto zum Spielrückblick fehlt.";
|
||||||
|
}
|
||||||
|
foreach ($foto['widths'] ?? [] as $w) {
|
||||||
|
if (!is_file(PUBLIC_PATH . '/assets/' . ($foto['base'] ?? '') . "-{$w}.jpg")) {
|
||||||
|
$errors[] = "Stadionzeitung '{$ausgabe['slug']}': Rückblick-Foto fehlt ({$w}px).";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (trim((string) ($rueckblick['credit'] ?? '')) === '') {
|
||||||
|
$errors[] = "Stadionzeitung '{$ausgabe['slug']}': Bildnachweis zum Spielrückblick fehlt.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($ausgabe['galerie']['bilder'] ?? [] as $bild) {
|
||||||
|
foreach ($bild['widths'] ?? [] as $w) {
|
||||||
|
$datei = PUBLIC_PATH . '/assets/' . ($bild['base'] ?? '') . "-{$w}.jpg";
|
||||||
|
if (!is_file($datei)) {
|
||||||
|
$errors[] = "Stadionzeitung '{$ausgabe['slug']}': Galerie-Foto fehlt (" . ($bild['base'] ?? '?') . "-{$w}.jpg).";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Inline-Styles (die CSP verwirft sie stillschweigend) ---
|
// --- Inline-Styles (die CSP verwirft sie stillschweigend) ---
|
||||||
// Diese Prüfung existiert wegen eines echten Ausfalls am 31.07.2026: der
|
// Diese Prüfung existiert wegen eines echten Ausfalls am 31.07.2026: der
|
||||||
// Banner-Slider setzte sein Hintergrundbild per style="--banner-bg: url(…)". Auf
|
// Banner-Slider setzte sein Hintergrundbild per style="--banner-bg: url(…)". Auf
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ declare(strict_types=1);
|
|||||||
* <ordner>/spieler.* optional, freigestelltes Spielerfoto (PNG mit Transparenz)
|
* <ordner>/spieler.* optional, freigestelltes Spielerfoto (PNG mit Transparenz)
|
||||||
* fürs Cover — wird unverändert kopiert, nie über GD neu kodiert
|
* fürs Cover — wird unverändert kopiert, nie über GD neu kodiert
|
||||||
* <ordner>/verfasser.* optional, Porträtfoto für die Vorwort-Signatur
|
* <ordner>/verfasser.* optional, Porträtfoto für die Vorwort-Signatur
|
||||||
|
* <ordner>/galerie/* optional, Fotos vom letzten Spiel (natsort = Reihenfolge,
|
||||||
|
* das erste ist der Aufmacher) für die Bento-Galerie-
|
||||||
|
* Seite hinter dem Ergebnis-Rückblick, höchstens sieben
|
||||||
*
|
*
|
||||||
* KEINE Anzeigen im Ordner (seit 31.07.2026): die kommen aus dem dauerhaften
|
* KEINE Anzeigen im Ordner (seit 31.07.2026): die kommen aus dem dauerhaften
|
||||||
* Bestand data/anzeigen.json, gepflegt mit bin/anzeige-add.php. Sponsorenmotive
|
* Bestand data/anzeigen.json, gepflegt mit bin/anzeige-add.php. Sponsorenmotive
|
||||||
@@ -157,6 +160,9 @@ $vorwort = $findOne('vorwort');
|
|||||||
$rueckseite = $findOne('rueckseite');
|
$rueckseite = $findOne('rueckseite');
|
||||||
$spieler = $findOne('spieler');
|
$spieler = $findOne('spieler');
|
||||||
$verfasser = $findOne('verfasser');
|
$verfasser = $findOne('verfasser');
|
||||||
|
$rueckblickFoto = $findOne('rueckblick');
|
||||||
|
$beitragFoto = $findOne('beitrag');
|
||||||
|
$galerieQuellen = $findAll('galerie');
|
||||||
// Anzeigen kommen NICHT mehr aus dem Ablieferordner, sondern aus dem dauerhaften
|
// Anzeigen kommen NICHT mehr aus dem Ablieferordner, sondern aus dem dauerhaften
|
||||||
// Bestand (data/anzeigen.json, gepflegt über bin/anzeige-add.php). Ein noch
|
// Bestand (data/anzeigen.json, gepflegt über bin/anzeige-add.php). Ein noch
|
||||||
// vorhandener anzeigen/-Unterordner wäre also wirkungslos — dann besser sagen,
|
// vorhandener anzeigen/-Unterordner wäre also wirkungslos — dann besser sagen,
|
||||||
@@ -196,12 +202,34 @@ foreach ($existing['issues'] ?? [] as $entry) {
|
|||||||
$oldVorwort = $oldIssue['vorwort'] ?? null;
|
$oldVorwort = $oldIssue['vorwort'] ?? null;
|
||||||
// Interview wie das Vorwort chat-gepflegt — überlebt erneute Läufe ebenso.
|
// Interview wie das Vorwort chat-gepflegt — überlebt erneute Läufe ebenso.
|
||||||
$oldInterview = $oldIssue['interview'] ?? null;
|
$oldInterview = $oldIssue['interview'] ?? null;
|
||||||
|
$beitrag = $oldIssue['beitrag'] ?? null;
|
||||||
|
if (is_array($beitrag) && $beitragFoto === null) {
|
||||||
|
$fail('Redaktioneller Beitrag vorhanden, aber beitrag.* fehlt im Ablieferordner.');
|
||||||
|
}
|
||||||
|
if (is_array($beitrag)) {
|
||||||
|
$beitragLink = trim((string) ($beitrag['source_url'] ?? ''));
|
||||||
|
if (!filter_var($beitragLink, FILTER_VALIDATE_URL) || parse_url($beitragLink, PHP_URL_SCHEME) !== 'https') {
|
||||||
|
$fail('Beitrag: source_url muss eine gültige HTTPS-Adresse sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
// Aufruf-Seite (z. B. Sportfest-Helferaufruf) ebenso chat-gepflegt.
|
// Aufruf-Seite (z. B. Sportfest-Helferaufruf) ebenso chat-gepflegt.
|
||||||
$oldAufruf = $oldIssue['aufruf'] ?? null;
|
$oldAufruf = $oldIssue['aufruf'] ?? null;
|
||||||
// Sportfest-Ausgabe: Slug-Verweis auf data/veranstaltungen.json (Programm,
|
// Sportfest-Ausgabe: Slug-Verweis auf data/veranstaltungen.json (Programm,
|
||||||
// Turnierpläne, Gegnerseiten je Heimspiel) und Cover-Kennung — chat-gepflegt.
|
// Turnierpläne, Gegnerseiten je Heimspiel) und Cover-Kennung — chat-gepflegt.
|
||||||
$oldVeranstaltung = $oldIssue['veranstaltung'] ?? null;
|
$oldVeranstaltung = $oldIssue['veranstaltung'] ?? null;
|
||||||
$oldCover = $oldIssue['cover'] ?? null;
|
$oldCover = $oldIssue['cover'] ?? null;
|
||||||
|
// Anzeigename des Gegners fürs Cover und die Liste ("Wirsberg/Kupferberg" statt
|
||||||
|
// der BFV-Zeile) und die Liste der Mannschaften, die zusätzlich eine
|
||||||
|
// Gegnerseite bekommen — beides Chat-Pflege, überlebt erneute Läufe wie vorwort.
|
||||||
|
$oldOpponentShort = $oldIssue['opponent_short'] ?? null;
|
||||||
|
// Galerie-Seite: Kopf (kicker/heading/text) und Alt-Texte sind Chat-Pflege,
|
||||||
|
// die Bilder schreibt dieser Lauf aus <ordner>/galerie/ (siehe unten).
|
||||||
|
$oldGalerie = is_array($oldIssue['galerie'] ?? null) ? $oldIssue['galerie'] : null;
|
||||||
|
$rueckblick = is_array($oldIssue['rueckblick'] ?? null) ? $oldIssue['rueckblick'] : null;
|
||||||
|
if ($rueckblick !== null && $rueckblickFoto === null) {
|
||||||
|
$fail('Spielrückblick vorhanden, aber rueckblick.* fehlt im Ablieferordner.');
|
||||||
|
}
|
||||||
|
$oldGegnerTeams = is_array($oldIssue['gegner_teams'] ?? null) ? array_values($oldIssue['gegner_teams']) : null;
|
||||||
$veranstaltung = null;
|
$veranstaltung = null;
|
||||||
if (is_string($oldVeranstaltung) && $oldVeranstaltung !== '') {
|
if (is_string($oldVeranstaltung) && $oldVeranstaltung !== '') {
|
||||||
$veranstaltung = veranstaltung_find($oldVeranstaltung);
|
$veranstaltung = veranstaltung_find($oldVeranstaltung);
|
||||||
@@ -312,6 +340,61 @@ $bild = static function (string $src, string $name, string $alt) use ($destDir,
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Galerie (optional): Fotos vom letzten Spiel als Bento-Seite. Wie bei den
|
||||||
|
// Veranstaltungsbildern ist der Ordner immer der KOMPLETTE Bestand (der
|
||||||
|
// Ausgaben-Ordner wurde oben geleert), Alt-Texte hängen an `source`, dem
|
||||||
|
// Originaldateinamen, und folgen ihrem Bild auch bei neuer Reihenfolge. Zwei
|
||||||
|
// Breiten reichen: der Aufmacher steht mit gut zwei Dritteln der Seitenbreite. ---
|
||||||
|
$galerieMax = 7;
|
||||||
|
$galerie = $oldGalerie;
|
||||||
|
if ($galerieQuellen !== []) {
|
||||||
|
$altBySource = [];
|
||||||
|
foreach ($oldGalerie['bilder'] ?? [] as $b) {
|
||||||
|
if (is_array($b) && !empty($b['source'])) {
|
||||||
|
$altBySource[(string) $b['source']] = (string) ($b['alt'] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$platzhalter = trim((string) ($oldGalerie['heading'] ?? '')) ?: 'Fotos vom letzten Spiel';
|
||||||
|
if (count($galerieQuellen) > $galerieMax) {
|
||||||
|
$log(sprintf('galerie/ enthält %d Fotos, das Raster fasst %d — die übrigen bleiben weg.', count($galerieQuellen), $galerieMax), 'WARN');
|
||||||
|
}
|
||||||
|
$galerieBilder = [];
|
||||||
|
$ohneAlt = [];
|
||||||
|
foreach (array_slice($galerieQuellen, 0, $galerieMax) as $n => $src) {
|
||||||
|
$source = basename($src);
|
||||||
|
$name = sprintf('galerie-%02d', $n + 1);
|
||||||
|
try {
|
||||||
|
$variants = resize_image_variants($src, "{$destDir}/{$name}", [640, 1200], 80);
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
$fail("Galerie-Foto {$source} fehlgeschlagen: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
$generated = array_values(array_unique(array_column($variants, 'width')));
|
||||||
|
sort($generated);
|
||||||
|
$alt = trim($altBySource[$source] ?? '');
|
||||||
|
if ($alt === '' || $alt === $platzhalter) {
|
||||||
|
$ohneAlt[] = $source;
|
||||||
|
$alt = $platzhalter;
|
||||||
|
}
|
||||||
|
$galerieBilder[] = [
|
||||||
|
'source' => $source,
|
||||||
|
'base' => "img/stadionzeitung/{$slug}/{$name}",
|
||||||
|
'widths' => $generated,
|
||||||
|
'alt' => $alt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$galerie = ($oldGalerie ?? []) + ['kicker' => 'Rückblick', 'heading' => $platzhalter];
|
||||||
|
$galerie['bilder'] = $galerieBilder;
|
||||||
|
$log(count($galerieBilder) . ' Galerie-Fotos übernommen.');
|
||||||
|
if ($ohneAlt !== []) {
|
||||||
|
$log('Galerie: noch ohne eigenen Alt-Text (im Chat ergänzen): ' . implode(', ', $ohneAlt), 'WARN');
|
||||||
|
}
|
||||||
|
} elseif (!empty($oldGalerie['bilder'])) {
|
||||||
|
// Die Dateien sind mit dem Ausgaben-Ordner weg; ohne galerie/ im Ordner
|
||||||
|
// lieferte die Seite sonst kaputte Bilder.
|
||||||
|
$log('Die Ausgabe hatte eine Galerie, der Ordner liefert aber kein galerie/ mehr — die Galerie-Seite entfällt.', 'WARN');
|
||||||
|
$galerie['bilder'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Inhalt als Liste von BLÖCKEN, Anzeigen kommen anschließend in die Lücken
|
* Inhalt als Liste von BLÖCKEN, Anzeigen kommen anschließend in die Lücken
|
||||||
* dazwischen. Ein Block ist eine Folge von Seiten, die zusammenbleiben MUSS;
|
* dazwischen. Ein Block ist eine Folge von Seiten, die zusammenbleiben MUSS;
|
||||||
@@ -345,6 +428,10 @@ try {
|
|||||||
if ($oldInterview !== null) {
|
if ($oldInterview !== null) {
|
||||||
$aufschlag[] = $live('interview');
|
$aufschlag[] = $live('interview');
|
||||||
}
|
}
|
||||||
|
if (is_array($beitrag)) {
|
||||||
|
$beitrag['image'] = $bild($beitragFoto, 'beitrag', (string) ($beitrag['image']['alt'] ?? $beitrag['heading'] ?? 'Vereinsbeitrag'));
|
||||||
|
$aufschlag[] = $live('beitrag');
|
||||||
|
}
|
||||||
// Aufruf-Seite (Helferaufruf o. Ä.) reiht sich in denselben anzeigenfreien
|
// Aufruf-Seite (Helferaufruf o. Ä.) reiht sich in denselben anzeigenfreien
|
||||||
// Aufschlag ein, direkt hinter Vorwort/Interview — ein redaktioneller
|
// Aufschlag ein, direkt hinter Vorwort/Interview — ein redaktioneller
|
||||||
// Einstieg ohne Werbung dazwischen (chat-gepflegtes `aufruf`-Feld).
|
// Einstieg ohne Werbung dazwischen (chat-gepflegtes `aufruf`-Feld).
|
||||||
@@ -370,19 +457,41 @@ try {
|
|||||||
|
|
||||||
$bloecke[] = ['name' => 'ergebnisse', 'seiten' => [$live('ergebnisse')], 'anzeigen_danach' => true];
|
$bloecke[] = ['name' => 'ergebnisse', 'seiten' => [$live('ergebnisse')], 'anzeigen_danach' => true];
|
||||||
|
|
||||||
|
// Redaktioneller Spielrückblick: Text und Bildnachweis sind Chat-Pflege,
|
||||||
|
// das Foto kommt aus dem Ablieferordner und bleibt in dieser Ausgabe.
|
||||||
|
if ($rueckblick !== null) {
|
||||||
|
$rueckblick['image'] = $bild($rueckblickFoto, 'rueckblick', (string) ($rueckblick['image']['alt'] ?? $rueckblick['heading'] ?? 'Spielrückblick'));
|
||||||
|
$bloecke[] = ['name' => 'rueckblick', 'seiten' => [$live('rueckblick')], 'anzeigen_danach' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Galerie direkt hinter dem Ergebnis-Rückblick: erst das Ergebnis, dann die
|
||||||
|
// Bilder dazu. Eigener Block, eine Anzeige dazwischen ist unbedenklich.
|
||||||
|
if (!empty($galerie['bilder'])) {
|
||||||
|
$bloecke[] = ['name' => 'galerie', 'seiten' => [$live('galerie')], 'anzeigen_danach' => true];
|
||||||
|
}
|
||||||
|
|
||||||
// Gegnerseite(n): normal nur die tragende Mannschaft; in der Sportfest-Ausgabe
|
// Gegnerseite(n): normal nur die tragende Mannschaft; in der Sportfest-Ausgabe
|
||||||
// je Mannschaft mit Heimspiel am Fest eine (drei Heimspiele, drei Seiten), als
|
// je Mannschaft mit Heimspiel am Fest eine (drei Heimspiele, drei Seiten), als
|
||||||
// EIN Block. Welche das sind, entscheidet stadionzeitung_gegner_fixtures() —
|
// EIN Block. Welche das sind, entscheidet stadionzeitung_gegner_fixtures() —
|
||||||
// dieselbe Funktion, aus der stadionzeitung_build_snapshot() die Daten baut,
|
// dieselbe Funktion, aus der stadionzeitung_build_snapshot() die Daten baut,
|
||||||
// damit nie eine Seite ohne Daten oder Daten ohne Seite entstehen.
|
// damit nie eine Seite ohne Daten oder Daten ohne Seite entstehen.
|
||||||
$gegnerSeiten = [];
|
$gegnerSeiten = [];
|
||||||
foreach (stadionzeitung_gegner_fixtures([
|
$gegnerFixtures = stadionzeitung_gegner_fixtures([
|
||||||
'date' => $date, 'team_key' => $teamKey, 'opponent' => $opponent,
|
'date' => $date, 'team_key' => $teamKey, 'opponent' => $opponent,
|
||||||
'veranstaltung' => $oldVeranstaltung,
|
'veranstaltung' => $oldVeranstaltung, 'gegner_teams' => $oldGegnerTeams ?? [],
|
||||||
]) as $gegnerTeam => $fixture) {
|
]);
|
||||||
|
foreach ($gegnerFixtures as $gegnerTeam => $fixture) {
|
||||||
$gegnerSeiten[] = $live('gegner', ['team' => (string) $gegnerTeam]);
|
$gegnerSeiten[] = $live('gegner', ['team' => (string) $gegnerTeam]);
|
||||||
$log("Gegnerseite {$gegnerTeam}: {$fixture['opponent']} ({$fixture['date']}).");
|
$log("Gegnerseite {$gegnerTeam}: {$fixture['opponent']} ({$fixture['date']}).");
|
||||||
}
|
}
|
||||||
|
// Eine benannte Mannschaft ohne Spiel im Fenster bekommt keine Seite. Das ist
|
||||||
|
// richtig so, darf aber nicht stillschweigend passieren: ein Tippfehler im
|
||||||
|
// Team-Key sähe genauso aus wie ein spielfreies Wochenende.
|
||||||
|
foreach ($oldGegnerTeams ?? [] as $gewuenscht) {
|
||||||
|
if (!isset($gegnerFixtures[(string) $gewuenscht])) {
|
||||||
|
$log("gegner_teams: '{$gewuenscht}' hat kein Spiel im Fenster um {$date} (oder der Team-Key ist falsch) — keine Gegnerseite.", 'WARN');
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($gegnerSeiten === []) {
|
if ($gegnerSeiten === []) {
|
||||||
// Ohne Treffer (Gegner leer) bleibt die Seite der tragenden Mannschaft —
|
// Ohne Treffer (Gegner leer) bleibt die Seite der tragenden Mannschaft —
|
||||||
// sie meldet dann selbst, dass keine Vergleichsdaten vorliegen.
|
// sie meldet dann selbst, dass keine Vergleichsdaten vorliegen.
|
||||||
@@ -425,7 +534,7 @@ try {
|
|||||||
if ($rueckseite !== null) {
|
if ($rueckseite !== null) {
|
||||||
$bloecke[] = [
|
$bloecke[] = [
|
||||||
'name' => 'rueckseite',
|
'name' => 'rueckseite',
|
||||||
'seiten' => [$bild($rueckseite, 'rueckseite', "Rückseite der Stadionzeitung gegen {$opponent}")],
|
'seiten' => [$bild($rueckseite, 'rueckseite', "Rückseite der Stadionzeitung gegen " . stadionzeitung_opponent_display(['opponent' => $opponent, 'opponent_short' => $oldOpponentShort]))],
|
||||||
'anzeigen_danach' => false,
|
'anzeigen_danach' => false,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -667,6 +776,12 @@ foreach (['mitmachen', 'news', 'termine', 'matchcenter', 'mitglied-werden', 'his
|
|||||||
// (qr-aufruf.png) — der wird bei jedem Lauf ohnehin neu befüllt. club_url()
|
// (qr-aufruf.png) — der wird bei jedem Lauf ohnehin neu befüllt. club_url()
|
||||||
// wie überall, damit der gedruckte Code auf die echte Domain zeigt.
|
// wie überall, damit der gedruckte Code auf die echte Domain zeigt.
|
||||||
$aufrufLink = trim((string) ($oldAufruf['link'] ?? ''));
|
$aufrufLink = trim((string) ($oldAufruf['link'] ?? ''));
|
||||||
|
$beitragLink = trim((string) ($beitrag['source_url'] ?? ''));
|
||||||
|
if ($beitragLink !== '') {
|
||||||
|
if (!generate_qr_png($beitragLink, "{$destDir}/qr-beitrag.png")) {
|
||||||
|
$fail('QR-Code für den redaktionellen Beitrag konnte nicht erzeugt werden.');
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($aufrufLink !== '') {
|
if ($aufrufLink !== '') {
|
||||||
if (generate_qr_png(club_url($aufrufLink), "{$destDir}/qr-aufruf.png")) {
|
if (generate_qr_png(club_url($aufrufLink), "{$destDir}/qr-aufruf.png")) {
|
||||||
$log("QR-Code für die Aufruf-Seite (/{$aufrufLink}) erzeugt.");
|
$log("QR-Code für die Aufruf-Seite (/{$aufrufLink}) erzeugt.");
|
||||||
@@ -734,6 +849,8 @@ if ($oldIssue !== null && ($oldIssue['status'] ?? '') === 'veroeffentlicht') {
|
|||||||
$issues[] = [
|
$issues[] = [
|
||||||
'slug' => $slug,
|
'slug' => $slug,
|
||||||
'opponent' => $opponent,
|
'opponent' => $opponent,
|
||||||
|
'opponent_short' => $oldOpponentShort,
|
||||||
|
'gegner_teams' => $oldGegnerTeams,
|
||||||
'date' => $date,
|
'date' => $date,
|
||||||
'team_key' => $teamKey,
|
'team_key' => $teamKey,
|
||||||
'status' => 'entwurf',
|
'status' => 'entwurf',
|
||||||
@@ -742,9 +859,12 @@ $issues[] = [
|
|||||||
'qr_image' => $qrImage,
|
'qr_image' => $qrImage,
|
||||||
'vorwort' => $oldVorwort,
|
'vorwort' => $oldVorwort,
|
||||||
'interview' => $oldInterview,
|
'interview' => $oldInterview,
|
||||||
|
'beitrag' => $beitrag,
|
||||||
'aufruf' => $oldAufruf,
|
'aufruf' => $oldAufruf,
|
||||||
'veranstaltung' => $oldVeranstaltung,
|
'veranstaltung' => $oldVeranstaltung,
|
||||||
'cover' => $oldCover,
|
'cover' => $oldCover,
|
||||||
|
'galerie' => $galerie,
|
||||||
|
'rueckblick' => $rueckblick,
|
||||||
'verfasser_photo' => $verfasserPhoto,
|
'verfasser_photo' => $verfasserPhoto,
|
||||||
'termine_snapshot' => $termineSnapshot,
|
'termine_snapshot' => $termineSnapshot,
|
||||||
'images' => $images,
|
'images' => $images,
|
||||||
|
|||||||
@@ -1,7 +1,106 @@
|
|||||||
{
|
{
|
||||||
"fetched_at": 1789636733,
|
"fetched_at": 1792834512,
|
||||||
"username": "tsv08kulmbach",
|
"username": "tsv08kulmbach",
|
||||||
"posts": [
|
"posts": [
|
||||||
|
{
|
||||||
|
"shortcode": "De0vwrajLXG",
|
||||||
|
"caption": "🔴⚫️ Vorschau",
|
||||||
|
"alt": "🔴⚫️ Vorschau",
|
||||||
|
"taken_at": 1792731262,
|
||||||
|
"is_video": false,
|
||||||
|
"image": "img/instagram/De0vwrajLXG.jpg",
|
||||||
|
"url": "https://www.instagram.com/p/De0vwrajLXG/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shortcode": "Derh4uhDLDh",
|
||||||
|
"caption": "🔴⚫️ Ergebnisse \nKann sich sehen lassen 🙌",
|
||||||
|
"alt": "🔴⚫️ Ergebnisse",
|
||||||
|
"taken_at": 1792421998,
|
||||||
|
"is_video": false,
|
||||||
|
"image": "img/instagram/Derh4uhDLDh.jpg",
|
||||||
|
"url": "https://www.instagram.com/p/Derh4uhDLDh/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shortcode": "DehQgDrDB0Z",
|
||||||
|
"caption": "🔴⚫️ Vorschau",
|
||||||
|
"alt": "🔴⚫️ Vorschau",
|
||||||
|
"taken_at": 1792077339,
|
||||||
|
"is_video": false,
|
||||||
|
"image": "img/instagram/DehQgDrDB0Z.jpg",
|
||||||
|
"url": "https://www.instagram.com/p/DehQgDrDB0Z/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shortcode": "DeeldfjMZEm",
|
||||||
|
"caption": "Neue Trikots für unsere E-Jugend! ⚽️😍\n„Euer Ort, euer Verein, euer Trikot“ – unter diesem Motto sorgt die bundesweite TUI-Aktion auch beim TSV 08 Kulmbach für strahlende Gesichter! 👕😊\nDas ReiseCenter Schaffranek sicherte sich unter mehr als 500 Bewerbern einen von 200 Trikotsätzen. Wer sich darüber freuen durfte, entschieden anschließend die Verantwortlichen im ReiseCenter per Los: Sie befüllten die Lostrommel mit den Namen aller Vereine im Kulmbacher Stadtgebiet, die Jugendarbeit bis zur E-Jugend anbieten, und führten die Ziehung durch. Das gezogene Los: TSV 08 Kulmbach! 🍀🙌\nMelanie Schaffranek überreichte die neuen Trikots bei unserem E-Jugendfestival und konnte unsere Nachwuchskicker direkt darin in Aktion erleben. ⚽\nEin herzliches Dankeschön an TUI sowie an Melanie Schaffranek und das gesamte Team des ReiseCenter Schaffranek für diese tolle Unterstützung unserer Jugendarbeit! ❤️🖤\nUnsere E-Jugend trägt das TUI-Lächeln ab sofort auf dem Trikot – und die Freude darüber im Gesicht! 😃\n\n#tsv08kulmbach #jugendfussball #reisetui \nDanke @reisebuero_schaffranek",
|
||||||
|
"alt": "Neue Trikots für unsere E-Jugend! ⚽️😍",
|
||||||
|
"taken_at": 1791987664,
|
||||||
|
"is_video": false,
|
||||||
|
"image": "img/instagram/DeeldfjMZEm.jpg",
|
||||||
|
"url": "https://www.instagram.com/p/DeeldfjMZEm/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shortcode": "DePJUbkjKPc",
|
||||||
|
"caption": "🔴⚫️ Vorschau",
|
||||||
|
"alt": "🔴⚫️ Vorschau",
|
||||||
|
"taken_at": 1791469594,
|
||||||
|
"is_video": false,
|
||||||
|
"image": "img/instagram/DePJUbkjKPc.jpg",
|
||||||
|
"url": "https://www.instagram.com/p/DePJUbkjKPc/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shortcode": "DeG8wTzjAVR",
|
||||||
|
"caption": "Ergebnisse vom Wochenende 🔴⚫️\n\nDerbysieg für unsere Erste 🤹🏻♂️ und unsere Frauen bleiben weiterhin ungeschlagen! 💪🏻",
|
||||||
|
"alt": "Ergebnisse vom Wochenende 🔴⚫️",
|
||||||
|
"taken_at": 1791194571,
|
||||||
|
"is_video": false,
|
||||||
|
"image": "img/instagram/DeG8wTzjAVR.jpg",
|
||||||
|
"url": "https://www.instagram.com/p/DeG8wTzjAVR/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shortcode": "Dd9tmXplrZx",
|
||||||
|
"caption": "🔴⚫️ Vorschau inkl. Stadtderby 🔥",
|
||||||
|
"alt": "🔴⚫️ Vorschau inkl. Stadtderby 🔥",
|
||||||
|
"taken_at": 1790884635,
|
||||||
|
"is_video": false,
|
||||||
|
"image": "img/instagram/Dd9tmXplrZx.jpg",
|
||||||
|
"url": "https://www.instagram.com/p/Dd9tmXplrZx/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shortcode": "Dd1S1UkDOfO",
|
||||||
|
"caption": "🔴⚫️ Ergebnisse",
|
||||||
|
"alt": "🔴⚫️ Ergebnisse",
|
||||||
|
"taken_at": 1790602166,
|
||||||
|
"is_video": false,
|
||||||
|
"image": "img/instagram/Dd1S1UkDOfO.jpg",
|
||||||
|
"url": "https://www.instagram.com/p/Dd1S1UkDOfO/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shortcode": "DdtJFZajIds",
|
||||||
|
"caption": "🔴⚫️-Vorschau",
|
||||||
|
"alt": "🔴⚫️-Vorschau",
|
||||||
|
"taken_at": 1790328620,
|
||||||
|
"is_video": false,
|
||||||
|
"image": "img/instagram/DdtJFZajIds.jpg",
|
||||||
|
"url": "https://www.instagram.com/p/DdtJFZajIds/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shortcode": "Ddjq90MjLvH",
|
||||||
|
"caption": "Maximale Punkteausbeute für uns am Wochenende!\n❤️🖤",
|
||||||
|
"alt": "Maximale Punkteausbeute für uns am Wochenende!",
|
||||||
|
"taken_at": 1790010839,
|
||||||
|
"is_video": false,
|
||||||
|
"image": "img/instagram/Ddjq90MjLvH.jpg",
|
||||||
|
"url": "https://www.instagram.com/p/Ddjq90MjLvH/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shortcode": "DdZ3vzhDAZS",
|
||||||
|
"caption": "🔴⚫️ Vorschau",
|
||||||
|
"alt": "🔴⚫️ Vorschau",
|
||||||
|
"taken_at": 1789681996,
|
||||||
|
"is_video": false,
|
||||||
|
"image": "img/instagram/DdZ3vzhDAZS.jpg",
|
||||||
|
"url": "https://www.instagram.com/p/DdZ3vzhDAZS/"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"shortcode": "DdROjQqjI1_",
|
"shortcode": "DdROjQqjI1_",
|
||||||
"caption": "Ergebnisse 🔴⚫️",
|
"caption": "Ergebnisse 🔴⚫️",
|
||||||
|
|||||||
@@ -132,8 +132,9 @@
|
|||||||
],
|
],
|
||||||
"recommendation": "Bei besonderem Potenzial für Wettkampf- oder Leistungsgruppen sprechen unsere Trainer eine Empfehlung aus.",
|
"recommendation": "Bei besonderem Potenzial für Wettkampf- oder Leistungsgruppen sprechen unsere Trainer eine Empfehlung aus.",
|
||||||
"sessions": [
|
"sessions": [
|
||||||
{ "label": "Mäuse I", "day": "Dienstag", "time": "17:30–18:30 Uhr", "trainers": ["Maxi Roßberg*", "Katrin und Marcus Braese"] },
|
{ "label": "Mäuse I", "day": "Dienstag", "time": "17:30–18:30 Uhr", "trainers": ["Maxi Roßberg*", "Katrin und Markus Braese", "Kristina Dorsch", "Katja Knappich"] },
|
||||||
{ "label": "Mäuse II", "day": "Donnerstag", "time": "16:30–17:30 Uhr", "trainers": ["Franziska Trotzke"] }
|
{ "label": "Mäuse II", "day": "Donnerstag", "time": "16:30–17:30 Uhr", "trainers": ["Franziska Trotzke", "Lisa Benker"] },
|
||||||
|
{ "label": "Mäuse III", "day": "Freitag", "time": "17:00–18:00 Uhr", "trainers": ["Annelie Frisque*", "Eva Wünsch*"] }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -211,9 +212,9 @@
|
|||||||
"contact_email": "tina.kornatz@gmx.de",
|
"contact_email": "tina.kornatz@gmx.de",
|
||||||
"contact_phone": "0163 6032410",
|
"contact_phone": "0163 6032410",
|
||||||
"footnote": "In den Schulferien findet kein Training statt.",
|
"footnote": "In den Schulferien findet kein Training statt.",
|
||||||
"notice_icon": "calendar-event",
|
"notice_tone": "stop",
|
||||||
"notice_title": "Schnuppern nach Absprache",
|
"notice_title": "Hinweis zur Warteliste",
|
||||||
"notice": "Schnuppertraining ist bei uns jederzeit nach vorheriger Absprache möglich. Melde dich davor einfach kurz bei Tina, dann finden wir einen passenden Termin.",
|
"notice": "Aktuell sind alle Plätze beim Trampolinturnen vergeben. Wir nehmen dich oder dein Kind aber gerne auf die Warteliste. Melde dich dafür einfach kurz bei Tina.",
|
||||||
"groups": [
|
"groups": [
|
||||||
{
|
{
|
||||||
"name": "Kinder und Erwachsene",
|
"name": "Kinder und Erwachsene",
|
||||||
|
|||||||
@@ -7261,74 +7261,136 @@ a.news-feature__link:hover .news-feature__more {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* === stadionzeitung-ergebnisse (Live-Seite) === */
|
/* === stadionzeitung-ergebnisse (Live-Seite) === */
|
||||||
/* Ergebnis-Rückblick: zwei Zeilen pro Spiel (Heim über Gast, wie das
|
/* Ergebnis-Rückblick als Anzeigetafel: Wappen und Vereinsname je Seite, das
|
||||||
„Nächste Spiele"-Band) — lange Vereinsnamen passen auf der schmalen
|
Ergebnis groß in der Mitte (Vorbild kicker/Sofascore). Vierter Entwurf,
|
||||||
A5-Seite nie in eine Zeile. Eigene Mannschaft über Gewicht der Body-Schrift
|
19.09.2026 — die drei Vorgänger scheiterten alle an derselben Ursache:
|
||||||
markiert (Abel hat echte Schnitte — anders als Coolvetica erlaubt, siehe
|
sie waren Listenzeilen, und eine Zeile stellt Heim und Gast untereinander
|
||||||
CLAUDE.md „nie über font-weight" bei Coolvetica). */
|
statt gegenüber, obwohl eine Begegnung etwas Symmetrisches ist. Mit den
|
||||||
|
Toren rechts am Blattrand lag halbe Seitenbreite Leere zwischen Name und
|
||||||
|
Ziffer, eine getönte Karte dagegen sah „altbacken" aus, und mit den Toren
|
||||||
|
links fehlte der Seite die Hierarchie.
|
||||||
|
|
||||||
|
Die Mittelachse löst zusätzlich das Namensproblem: lange Namen wie
|
||||||
|
„(SG1) TSV Melkendorf III/ TSV 08 Kulmbach II" umbrechen in ihrer Spalte
|
||||||
|
über mehrere Zeilen, ohne dass irgendetwas verrutscht — deshalb steht der
|
||||||
|
Name UNTER dem Wappen und mittig, nicht daneben.
|
||||||
|
|
||||||
|
Flächen gibt es keine, nur Linien: Haarlinie zwischen den Begegnungen,
|
||||||
|
roter Strich unter der Mannschaftszeile, sonst weißes Blatt. */
|
||||||
|
/* Trennung der Mannschaften: Haarlinie plus reichlich Abstand auf beiden
|
||||||
|
Seiten. Die Linie liegt am Abschnitt, nicht an der letzten Begegnung darin —
|
||||||
|
sonst wanderte sie bei mehreren Spielen pro Mannschaft mitten in den Block. */
|
||||||
.stadionzeitung-ergebnisse__team {
|
.stadionzeitung-ergebnisse__team {
|
||||||
margin-bottom: 4.2cqi;
|
padding-bottom: 4.6cqi;
|
||||||
}
|
margin-bottom: 4.6cqi;
|
||||||
|
|
||||||
.stadionzeitung-ergebnisse__team-name {
|
|
||||||
margin-bottom: 1.7cqi;
|
|
||||||
font-family: var(--ff-body);
|
|
||||||
font-weight: var(--fw-bold);
|
|
||||||
font-size: 2.7cqi;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: var(--ls-caps);
|
|
||||||
color: var(--clr-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stadionzeitung-ergebnisse__match {
|
|
||||||
padding-bottom: 1.7cqi;
|
|
||||||
margin-bottom: 1.7cqi;
|
|
||||||
border-bottom: 1px solid var(--clr-glass-border);
|
border-bottom: 1px solid var(--clr-glass-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stadionzeitung-ergebnisse__match:last-child {
|
.stadionzeitung-ergebnisse__team:last-of-type {
|
||||||
|
padding-bottom: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stadionzeitung-ergebnisse__meta {
|
/* Mannschaftszeile bewusst leise: die Gliederung macht der Abstand zwischen
|
||||||
font-size: 2.5cqi;
|
den Mannschaften, keine Linie (ein roter Strich darunter war gebaut und am
|
||||||
|
19.09.2026 wieder entfernt — er zog mehr Aufmerksamkeit als die Ergebnisse
|
||||||
|
darunter). */
|
||||||
|
.stadionzeitung-ergebnisse__team-name {
|
||||||
|
margin-bottom: 1.6cqi;
|
||||||
|
font-family: var(--ff-body);
|
||||||
|
font-size: 2.4cqi;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: var(--ls-caps);
|
||||||
color: var(--clr-text-faint);
|
color: var(--clr-text-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Das Zeilen-Layout gehört auf die ZEILE, nicht auf das Wappen darin (stand
|
.stadionzeitung-ergebnisse__match {
|
||||||
hier mal als `.stadionzeitung-ergebnisse__row .crest` — dann wurde das Wappen
|
|
||||||
selbst zum Grid, lag als Block-Element auf eigener Zeile, und Vereinsname und
|
|
||||||
Tore rutschten als Inline-Spans darunter zusammen). `--crest-size` bleibt
|
|
||||||
trotzdem hier: Custom Properties erben, das Wappen bleibt eine dumme
|
|
||||||
Komponente und der Kontext gibt ihm seine Größe — so machen es alle anderen
|
|
||||||
Wappen-Kontexte im Projekt auch. */
|
|
||||||
.stadionzeitung-ergebnisse__row {
|
|
||||||
--crest-size: 5.8cqi;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.7cqi;
|
gap: 2.1cqi;
|
||||||
padding-block: 0.6cqi;
|
padding-block: 1.4cqi;
|
||||||
|
border-bottom: 1px solid var(--clr-glass-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-ergebnisse__match:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Beide Seiten sind gleich gebaut und gleich breit (1fr), damit die Mittelachse
|
||||||
|
wirklich in der Mitte liegt — auch wenn eine Spielgemeinschaft drei Zeilen
|
||||||
|
Namen mitbringt und der Gegner eine. `--crest-size` setzt der Kontext, die
|
||||||
|
crest-Komponente bleibt dumm (gleiches Muster wie in allen anderen
|
||||||
|
Wappen-Kontexten). */
|
||||||
|
.stadionzeitung-ergebnisse__side {
|
||||||
|
--crest-size: 8.5cqi;
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 1.2cqi;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Einzeilig, notfalls ausgepunktet: ein umbrechender Name schiebt die
|
||||||
|
Anzeigetafel auseinander und die Zeilenpaare stehen dann ungleich hoch
|
||||||
|
nebeneinander. Die volle Schreibweise steht ohnehin im Matchcenter. */
|
||||||
.stadionzeitung-ergebnisse__club {
|
.stadionzeitung-ergebnisse__club {
|
||||||
min-width: 0;
|
max-width: 100%;
|
||||||
font-size: 3.75cqi;
|
overflow: hidden;
|
||||||
line-height: 1.2;
|
font-size: 3.2cqi;
|
||||||
|
line-height: 1.15;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stadionzeitung-ergebnisse__row--own .stadionzeitung-ergebnisse__club {
|
.stadionzeitung-ergebnisse__side--own .stadionzeitung-ergebnisse__club {
|
||||||
font-weight: var(--fw-bold);
|
font-weight: var(--fw-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stadionzeitung-ergebnisse__goals {
|
.stadionzeitung-ergebnisse__score {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 0.8cqi;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-ergebnisse__result {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.8cqi;
|
||||||
font-family: var(--ff-heading);
|
font-family: var(--ff-heading);
|
||||||
font-size: 5.8cqi;
|
font-size: 7.4cqi;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Das Ergebnis läuft durchgehend in voller Textfarbe: eine zurückgenommene
|
||||||
|
Verlierer-Ziffer war gebaut und am 19.09.2026 verworfen (Felix) — auf dem
|
||||||
|
gedruckten Blatt ist eine graue Ziffer schlechter lesbar, und wer gewonnen
|
||||||
|
hat, sagt die Zahl selbst. */
|
||||||
|
.stadionzeitung-ergebnisse__meta {
|
||||||
|
font-size: 2.5cqi;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: var(--ls-caps);
|
||||||
|
color: var(--clr-text-faint);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/* === stadionzeitung-gegner (Live-Seite) === */
|
/* === stadionzeitung-gegner (Live-Seite) === */
|
||||||
|
/* Gegnerseite als Duell-Aufmacher plus Balkenvergleich (19.09.2026). Vorher
|
||||||
|
war das eine rosa getönte Karte mit der Begegnung und darunter eine
|
||||||
|
Zahlenkolonne aus sieben Zeilen: inhaltlich richtig, gestalterisch ohne
|
||||||
|
Blickfang, und die untere Blatthälfte blieb leer.
|
||||||
|
|
||||||
|
Drei Entscheidungen tragen den Aufbau:
|
||||||
|
- Das GEGNERWAPPEN ist der Bildanker (deutlich größer als unseres). Die
|
||||||
|
Seite handelt von ihm, nicht von der Begegnung.
|
||||||
|
- Der Tabellenplatz steht oben bei den Wappen, NICHT als Balken: dort ist
|
||||||
|
klein besser, ein Balken läge genau falsch herum.
|
||||||
|
- Der Vergleich läuft als Balkenpaar aus der Mitte (Vorbild Sofascore).
|
||||||
|
Die Länge kommt aus einer KLASSE in 5-Prozent-Stufen, nie aus einem
|
||||||
|
style-Attribut: die CSP verwirft inline styles stillschweigend, der
|
||||||
|
Balken wäre in Produktion unsichtbar (siehe CLAUDE.md). */
|
||||||
.stadionzeitung-gegner {
|
.stadionzeitung-gegner {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
@@ -7336,90 +7398,204 @@ a.news-feature__link:hover .news-feature__more {
|
|||||||
.stadionzeitung-gegner__duell {
|
.stadionzeitung-gegner__duell {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||||
align-items: center;
|
align-items: start;
|
||||||
gap: 2.5cqi;
|
gap: 2.5cqi;
|
||||||
margin-bottom: 5cqi;
|
margin-top: 2.5cqi;
|
||||||
padding: 3.3cqi;
|
|
||||||
border: 1px solid var(--clr-glass-border);
|
|
||||||
border-radius: var(--radius-btn);
|
|
||||||
background: color-mix(in srgb, var(--clr-accent) 5%, #fff);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Beide Seiten sind gleich gebaut und gleich groß. Der Gegner war hier kurz
|
||||||
|
mit doppelt so großem Wappen und seinem Namen in Coolvetica gesetzt — damit
|
||||||
|
stand er zweimal laut auf derselben Seite (Überschrift und Duell) und das
|
||||||
|
Blatt wirkte unruhig (Felix, 19.09.2026). Die Überschrift nennt ihn, das
|
||||||
|
Duell zeigt nur noch, wer gegen wen und auf welchem Platz. */
|
||||||
.stadionzeitung-gegner__club {
|
.stadionzeitung-gegner__club {
|
||||||
--crest-size: 12cqi;
|
--crest-size: 11cqi;
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-rows: auto minmax(7.4cqi, auto);
|
||||||
align-items: center;
|
justify-items: center;
|
||||||
gap: 1.7cqi;
|
gap: 1.2cqi;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stadionzeitung-gegner__club strong {
|
.stadionzeitung-gegner__club-name {
|
||||||
font-size: 3.2cqi;
|
font-size: 3.2cqi;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stadionzeitung-gegner__club--own strong {
|
/* Mittelspalte: „gegen" und der Anstoß stehen zusammen, damit im Duell nur
|
||||||
color: var(--clr-accent);
|
noch drei Dinge auf einer Höhe liegen (wir, wann, sie). Zwei lose
|
||||||
}
|
Platz-Ziffern und eine freistehende Datumszeile standen hier kurz und
|
||||||
|
machten den Kopf unruhig (Felix, 19.09.2026) — der Tabellenplatz steht
|
||||||
.stadionzeitung-gegner__match {
|
jetzt im Ausschnitt unter den Balken. */
|
||||||
display: flex;
|
.stadionzeitung-gegner__vs {
|
||||||
flex-direction: column;
|
display: grid;
|
||||||
align-items: center;
|
justify-items: center;
|
||||||
gap: 0.5cqi;
|
gap: 1.2cqi;
|
||||||
font-size: 2.5cqi;
|
padding-top: 3.3cqi;
|
||||||
color: var(--clr-text-muted);
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stadionzeitung-gegner__match strong {
|
.stadionzeitung-gegner__vs-wort {
|
||||||
font-size: 2.9cqi;
|
font-size: 2.7cqi;
|
||||||
color: var(--clr-text);
|
text-transform: uppercase;
|
||||||
|
letter-spacing: var(--ls-caps);
|
||||||
|
color: var(--clr-text-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-gegner__anstoss {
|
||||||
|
font-size: 2.7cqi;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--clr-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Im Ausschnitt entfällt die Zonen-Legende: sie erklärt Auf- und Abstieg,
|
||||||
|
und das ist die Aufgabe der vollständigen Tabellenseite weiter hinten im
|
||||||
|
Heft. Hier geht es nur um die Nachbarschaft der beiden Mannschaften. */
|
||||||
|
.stadionzeitung-gegner__ausschnitt {
|
||||||
|
margin-top: 3.3cqi;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-gegner__ausschnitt .league-table__legend {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Der Ausschnitt läuft eine Spur enger als die vollständige Tabellenseite:
|
||||||
|
er teilt sich das Blatt mit Duell, Aufmacher und Balken. Gemessen: mit den
|
||||||
|
Maßen der Tabellenseite stand die Seite 16px über der A5-Kante. */
|
||||||
|
.stadionzeitung-gegner__ausschnitt .league-table th,
|
||||||
|
.stadionzeitung-gegner__ausschnitt .league-table td {
|
||||||
|
padding: 0.7cqi 0.9cqi;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-gegner__ausschnitt .league-table tbody .crest {
|
||||||
|
--crest-size: 4cqi;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-gegner__ausschnitt-titel {
|
||||||
|
margin-bottom: 1.4cqi;
|
||||||
|
font-size: 2.4cqi;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: var(--ls-caps);
|
||||||
|
color: var(--clr-text-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zwei Blöcke (wenn die Mannschaften weit auseinanderliegen) bekommen Luft
|
||||||
|
dazwischen, sonst sähe es aus wie eine durchgehende Tabelle mit Sprung. */
|
||||||
|
.stadionzeitung-gegner__ausschnitt .table-scroll-frame + .table-scroll-frame {
|
||||||
|
margin-top: 2.1cqi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Die Tabelle in einem Satz: derselbe Bestand, nur in Sprache. Coolvetica
|
||||||
|
trägt hier, weil die Zeile kurz ist und nie umbricht. */
|
||||||
|
.stadionzeitung-gegner__aufmacher {
|
||||||
|
margin-top: 3.3cqi;
|
||||||
|
padding-top: 2.5cqi;
|
||||||
|
border-top: 0.5cqi solid var(--clr-accent);
|
||||||
|
font-family: var(--ff-heading);
|
||||||
|
font-size: 5.8cqi;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: var(--ls-heading);
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stadionzeitung-gegner__vergleich {
|
.stadionzeitung-gegner__vergleich {
|
||||||
border-block: 1px solid var(--clr-glass-border);
|
margin-top: 2.5cqi;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stadionzeitung-gegner__vergleich-head,
|
.stadionzeitung-gegner__vergleich-head,
|
||||||
.stadionzeitung-gegner__vergleich-row {
|
.stadionzeitung-gegner__vergleich-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 2fr 1fr;
|
grid-template-columns: 6cqi minmax(0, 1fr) 24cqi minmax(0, 1fr) 6cqi;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 1.7cqi;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stadionzeitung-gegner__vergleich-head {
|
.stadionzeitung-gegner__vergleich-head {
|
||||||
padding-block: 1.7cqi;
|
padding-bottom: 1.4cqi;
|
||||||
font-size: 2.5cqi;
|
margin-bottom: 1.4cqi;
|
||||||
font-weight: var(--fw-bold);
|
border-bottom: 1px solid var(--clr-glass-border);
|
||||||
|
font-size: 2.4cqi;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: var(--ls-caps);
|
letter-spacing: var(--ls-caps);
|
||||||
color: var(--clr-text-muted);
|
color: var(--clr-text-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Die beiden äußeren Spaltenköpfe sitzen über den Zahlen, der mittlere über
|
||||||
|
dem Label — deshalb spannt er über die drei inneren Spalten. */
|
||||||
|
.stadionzeitung-gegner__vergleich-head span:nth-child(2) {
|
||||||
|
grid-column: 2 / 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stadionzeitung-gegner__vergleich-row {
|
.stadionzeitung-gegner__vergleich-row {
|
||||||
min-height: 6.5cqi;
|
min-height: 5.8cqi;
|
||||||
border-top: 1px solid var(--clr-glass-border);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stadionzeitung-gegner__vergleich-row span {
|
.stadionzeitung-gegner__wert {
|
||||||
|
font-family: var(--ff-heading);
|
||||||
|
font-size: 4.4cqi;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-gegner__label {
|
||||||
font-size: 2.8cqi;
|
font-size: 2.8cqi;
|
||||||
color: var(--clr-text-muted);
|
color: var(--clr-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stadionzeitung-gegner__vergleich-row strong {
|
/* Balkenpaar: der linke wächst aus der Mitte nach links, der rechte nach
|
||||||
font-family: var(--ff-heading);
|
rechts. Die Spur bleibt sichtbar (helle Fläche), damit auch ein Wert von
|
||||||
font-size: 4.8cqi;
|
null als Zeile lesbar ist. */
|
||||||
line-height: 1;
|
.stadionzeitung-gegner__balken {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
block-size: 2.2cqi;
|
||||||
|
border-radius: 1.1cqi;
|
||||||
|
background: var(--clr-glass-border);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stadionzeitung-gegner__vergleich-row strong:first-child {
|
.stadionzeitung-gegner__balken--own {
|
||||||
color: var(--clr-accent);
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-gegner__balken-fuellung {
|
||||||
|
display: block;
|
||||||
|
block-size: 100%;
|
||||||
|
border-radius: 1.1cqi;
|
||||||
|
background: var(--clr-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Unsere Seite rot, die des Gegners schwarz: die einzige Farbe der Seite
|
||||||
|
steht damit bei uns, ohne dass Rot Fließtext tragen müsste. */
|
||||||
|
.stadionzeitung-gegner__balken--own .stadionzeitung-gegner__balken-fuellung {
|
||||||
|
background: var(--clr-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5-Prozent-Stufen, siehe Kommentar oben (kein style-Attribut). */
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s0 { inline-size: 0%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s1 { inline-size: 5%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s2 { inline-size: 10%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s3 { inline-size: 15%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s4 { inline-size: 20%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s5 { inline-size: 25%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s6 { inline-size: 30%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s7 { inline-size: 35%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s8 { inline-size: 40%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s9 { inline-size: 45%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s10 { inline-size: 50%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s11 { inline-size: 55%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s12 { inline-size: 60%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s13 { inline-size: 65%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s14 { inline-size: 70%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s15 { inline-size: 75%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s16 { inline-size: 80%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s17 { inline-size: 85%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s18 { inline-size: 90%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s19 { inline-size: 95%; }
|
||||||
|
.stadionzeitung-gegner__balken-fuellung--s20 { inline-size: 100%; }
|
||||||
|
|
||||||
.stadionzeitung-gegner__empty {
|
.stadionzeitung-gegner__empty {
|
||||||
padding: 4.2cqi;
|
padding: 4.2cqi;
|
||||||
border: 1px solid var(--clr-glass-border);
|
border: 1px solid var(--clr-glass-border);
|
||||||
@@ -8452,6 +8628,162 @@ p.stadionzeitung-turnier__label:first-of-type {
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === stadionzeitung-beitrag (redaktioneller Lesetipp) === */
|
||||||
|
.stadionzeitung-beitrag {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
.stadionzeitung-beitrag__figure {
|
||||||
|
margin: 0 0 3cqi;
|
||||||
|
}
|
||||||
|
.stadionzeitung-beitrag .stadionzeitung-page__heading {
|
||||||
|
font-size: 7cqi;
|
||||||
|
}
|
||||||
|
.stadionzeitung-beitrag .stadionzeitung-vorwort__text {
|
||||||
|
font-size: 3.2cqi;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 2.4cqi;
|
||||||
|
}
|
||||||
|
.stadionzeitung-beitrag__foto {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 48cqi;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.stadionzeitung-beitrag__source {
|
||||||
|
font-size: 2.5cqi;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--clr-text-muted);
|
||||||
|
margin: 0 0 4cqi;
|
||||||
|
}
|
||||||
|
.stadionzeitung-beitrag .stadionzeitung-page__cta {
|
||||||
|
margin-top: auto;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.stadionzeitung-beitrag .stadionzeitung-page__cta-qr {
|
||||||
|
width: 20cqi;
|
||||||
|
height: 20cqi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === stadionzeitung-rueckblick (redaktionelle Seite mit Bildnachweis) === */
|
||||||
|
.stadionzeitung-rueckblick {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kopf/Trennlinie, Ergebnis und Fließtext verwenden die vorhandenen
|
||||||
|
Heft-Klassen. Hier nur die Anordnung von Bild und Ergebnis definieren. */
|
||||||
|
.stadionzeitung-rueckblick__scoreline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4cqi;
|
||||||
|
margin-bottom: 4cqi;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-rueckblick__scoreline .stadionzeitung-ergebnisse__result {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-rueckblick__figure {
|
||||||
|
margin: 0 0 3cqi;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-rueckblick__foto {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 65cqi;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-rueckblick__credit {
|
||||||
|
font-size: 2.5cqi;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--clr-text-muted);
|
||||||
|
margin-top: 1cqi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === stadionzeitung-galerie (Live-Seite, optional: Fotos vom letzten Spiel) === */
|
||||||
|
/* Bento-Raster auf einem 6×4-Gitter: das erste Foto ist der Aufmacher (4×2),
|
||||||
|
die übrigen legen sich je nach Anzahl darum. Die Fläche unter dem Kopf gehört
|
||||||
|
komplett den Fotos, beschnitten wird per object-fit — wie bei den Momenten
|
||||||
|
ohne Bildunterschriften, der Alt-Text trägt die Beschreibung.
|
||||||
|
Das Raster wächst per flex in die feste A5-Höhe; min-height ist der Halt für
|
||||||
|
den gestapelten Viewer ohne JS, wo die Seite keine feste Höhe hat (dann
|
||||||
|
kollabierten die 1fr-Zeilen auf null). 95cqi passt unter jeden Kopf mit
|
||||||
|
Textzeile (gemessen gegen 141,9cqi Blatthöhe). */
|
||||||
|
.stadionzeitung-galerie {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-galerie .stadionzeitung-page__head {
|
||||||
|
margin-bottom: 2.5cqi;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-galerie__text {
|
||||||
|
font-size: 3.4cqi;
|
||||||
|
color: var(--clr-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-galerie__bento {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 95cqi;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
grid-template-rows: repeat(4, 1fr);
|
||||||
|
gap: 1.5cqi;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-galerie__foto {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-galerie__foto:nth-child(1) { grid-area: a; }
|
||||||
|
.stadionzeitung-galerie__foto:nth-child(2) { grid-area: b; }
|
||||||
|
.stadionzeitung-galerie__foto:nth-child(3) { grid-area: c; }
|
||||||
|
.stadionzeitung-galerie__foto:nth-child(4) { grid-area: d; }
|
||||||
|
.stadionzeitung-galerie__foto:nth-child(5) { grid-area: e; }
|
||||||
|
.stadionzeitung-galerie__foto:nth-child(6) { grid-area: f; }
|
||||||
|
.stadionzeitung-galerie__foto:nth-child(7) { grid-area: g; }
|
||||||
|
|
||||||
|
.stadionzeitung-galerie__bento--n1 {
|
||||||
|
grid-template-areas: "a a a a a a" "a a a a a a" "a a a a a a" "a a a a a a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-galerie__bento--n2 {
|
||||||
|
grid-template-areas: "a a a a a a" "a a a a a a" "b b b b b b" "b b b b b b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-galerie__bento--n3 {
|
||||||
|
grid-template-areas: "a a a a a a" "a a a a a a" "b b b c c c" "b b b c c c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-galerie__bento--n4 {
|
||||||
|
grid-template-areas: "a a a a b b" "a a a a b b" "c c c d d d" "c c c d d d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-galerie__bento--n5 {
|
||||||
|
grid-template-areas: "a a a a b b" "a a a a c c" "d d d e e e" "d d d e e e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-galerie__bento--n6 {
|
||||||
|
grid-template-areas: "a a a a b b" "a a a a c c" "d d e e f f" "d d e e f f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadionzeitung-galerie__bento--n7 {
|
||||||
|
grid-template-areas: "a a a a b b" "a a a a c c" "d d e e f f" "d d e e g g";
|
||||||
|
}
|
||||||
|
|
||||||
/* === stadionzeitung-druck (Druckfassung, Broschüren-Montage) === */
|
/* === stadionzeitung-druck (Druckfassung, Broschüren-Montage) === */
|
||||||
/* Bildschirm-Ansicht der Route ?druck=1: jedes Blatt als A4-quer-Vorschau
|
/* Bildschirm-Ansicht der Route ?druck=1: jedes Blatt als A4-quer-Vorschau
|
||||||
(297:210) mit zwei A5-Seiten, in Broschüren-Reihenfolge (die Montage
|
(297:210) mit zwei A5-Seiten, in Broschüren-Reihenfolge (die Montage
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 16 KiB |
BIN
public/assets/img/crests/01S7GFJ77S000000VS548984VTJ68QLL.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/assets/img/instagram/Dd1S1UkDOfO.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
public/assets/img/instagram/Dd9tmXplrZx.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
public/assets/img/instagram/DdZ3vzhDAZS.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
public/assets/img/instagram/Ddjq90MjLvH.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
public/assets/img/instagram/DdtJFZajIds.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
public/assets/img/instagram/De0vwrajLXG.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
public/assets/img/instagram/DeG8wTzjAVR.jpg
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
public/assets/img/instagram/DePJUbkjKPc.jpg
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
public/assets/img/instagram/DeeldfjMZEm.jpg
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
public/assets/img/instagram/DehQgDrDB0Z.jpg
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
public/assets/img/instagram/Derh4uhDLDh.jpg
Normal file
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 589 B |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 227 KiB |
|
After Width: | Height: | Size: 840 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 6.5 KiB |