2026-06-20 20:47:13 +02:00
< ? php
declare ( strict_types = 1 );
/**
* Mitmachen & Ehrenamt — „Stellenbörse " für ehrenamtliche Helfer.
* Struktur : Hero → Stellen ( position - card je Stelle ) → CTA - Band → FAQ .
* Inhalte : data / mitmachen . json . Ansprechpartner : data / ansprechpartner . json ( Single Source ) .
* Je Stelle ein JobPosting - Knoten ( helpers : job_posting_schema ) im $meta [ 'schema' ]
* → GEO / semantisches SEO , verknüpft via #club mit dem Organization-Knoten.
*/
$page = json_load ( 'mitmachen' );
// Ansprechpartner nach Namen indexieren (Single Source: ansprechpartner.json).
$directory = [];
foreach ( json_load ( 'ansprechpartner' )[ 'persons' ] ? ? [] as $p ) {
$directory [ $p [ 'name' ]] = $p ;
}
// Pro Stelle: inline contact bevorzugt, sonst Lookup über contact_name.
$contact_for = static function ( array $pos ) use ( $directory ) : ? array {
if ( ! empty ( $pos [ 'contact' ]) && is_array ( $pos [ 'contact' ])) {
return $pos [ 'contact' ];
}
return $directory [ $pos [ 'contact_name' ] ? ? '' ] ? ? null ;
};
$meta = [
'title' => 'Mitmachen & Ehrenamt' ,
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
'description' => 'Ehrenamt beim TSV 08 Kulmbach: offene Aufgaben als Jugendtrainer, Betreuer oder Helfer bei Veranstaltungen. Direkte Ansprechpartner, keine Vorerfahrung nötig!' ,
'og_image' => 'img/pages/mitmachen-hero-1600.jpg' ,
2026-06-20 20:47:13 +02:00
'scripts' => [ 'position-filter.js' ],
'schema' => array_merge (
page_schema (
[
[ 'name' => 'Startseite' , 'slug' => '' ],
[ 'name' => 'Mitmachen' , 'slug' => 'mitmachen' ],
],
$page [ 'faq' ] ? ? []
),
array_values ( array_filter ( array_map (
static fn ( array $job ) : array => job_posting_schema ( $job , 'mitmachen' ),
$page [ 'positions' ] ? ? []
)))
),
];
component ( 'hero' , [ 'hero' => $page [ 'hero' ]]);
?>
< section class = " section positions " id = " stellen " aria - labelledby = " positions-heading " >
< div class = " container " >
< h2 id = " positions-heading " class = " visually-hidden " > Offene Stellen </ h2 >
< ? php if ( ! empty ( $page [ 'filters' ])) : ?>
< div class = " filter-bar " role = " group " aria - label = " Stellen nach Bereich filtern " data - position - filter >
< ? php foreach ( $page [ 'filters' ] as $i => $f ) : ?>
Design-System: Button-Varianten konsolidieren, 44px-Trefferflächen
Dritter Teil. .btn hatte nur primary + outline + sm/lg, daneben lebten mehrere
Controls als Eigenbau — teils zeichengleich dupliziert.
Tabs: .group-tabs__tab und .filter-bar__tab waren bis auf flex-shrink und 4px
Innenabstand identisch. Beide laufen jetzt über eine gemeinsame .tab-Basis, die
aria-pressed (Filter) und aria-selected (Tabs) gleich behandelt; von den
BEM-Klassen bleibt nur das jeweilige Delta. Innenabstand vereinheitlicht auf
--space-m, die Gruppen-Tabs verlieren dadurch 4px pro Seite.
Icon-Buttons: .ad-banner__nav-btn und .ad-banner__toggle waren vollständige
Duplikate voneinander. Sie tragen jetzt .btn .btn--icon und behalten nur die
Glas-Optik als Delta. Der Selektor schreibt .btn mit — utilities.css lädt nach
components.css, bei gleicher Spezifität gewänne sonst die rote .btn-Fläche.
Trefferflächen auf 44px (WCAG 2.5.5): .btn bekommt min-height und wird
inline-flex, damit Icon und Label ohne Zusatz-Markup zentriert liegen.
Bezeichnend auch hier — .department-choice__btn hatte inline-flex, align-items
und exakt denselben gap bereits selbst deklariert und gibt sie jetzt ab.
Controls, die optisch klein bleiben sollen, strecken die Fläche stattdessen
unsichtbar per Pseudo-Element (.tap-target, automatisch auch für .btn--sm) —
gewertet wird die Trefferfläche, nicht die gezeichnete.
Der Banner-Indikator brauchte einen anderen Weg: er trug overflow: hidden, um
den Fortschrittsbalken zu beschneiden, was jede Erweiterung wieder abgeschnitten
hätte. Der sichtbare 6px-Balken liegt jetzt im ::before, der Fortschritt ist
selbst rund — overflow entfällt, der Button ist 48x44px.
Haupt-Nav-Links, Dropdown-Trigger und Footer-Social-Links bekommen min-height
statt einer Pseudo-Erweiterung: sie stehen in Reihen, wo überlappende
Trefferflächen den Nachbarn verdecken würden. In der 60px hohen Kopfleiste ist
die Höhe ohnehin nicht sichtbar.
Bewusst unverändert: die Formular-Checkbox bleibt bei 24px (WCAG 2.5.8).
<input> ist ein Replaced Element, dort greift ::after nicht zuverlässig — und
das zugehörige <label> ist klickbar und liefert die große Fläche.
.btn--ghost ist angelegt, hat aber aktuell keinen Abnehmer: die vermuteten
nackten Links sind bei näherem Hinsehen Bildkarten (.instagram__link),
Wrapper (.split__cta) oder eigenständige Karten-Affordanzen (.team-card__cta),
keine tertiären Buttons.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSsVdooFLgPA8gPFp7HwZU
2026-07-27 10:28:02 +02:00
< button type = " button " class = " tab filter-bar__tab " data - filter = " <?= e( $f['value'] ) ?> " aria - pressed = " <?= $i === 0 ? 'true' : 'false' ?> " >< ? = e ( $f [ 'label' ]) ?> </button>
2026-06-20 20:47:13 +02:00
< ? php endforeach ; ?>
</ div >
< ? php endif ; ?>
< div class = " positions__list " data - position - list >
< ? php foreach ( $page [ 'positions' ] as $pos ) : ?>
< ? php component ( 'position-card' , [ 'position' => $pos , 'contact' => $contact_for ( $pos )]); ?>
< ? php endforeach ; ?>
</ div >
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
< p class = " positions__empty " data - position - empty hidden > In diesem Bereich sind aktuell keine Stellen ausgeschrieben . Schau bald wieder vorbei oder sprich uns einfach < a href = " <?= e(url('') . '#kontakt') ?> " > direkt an </ a > , wir finden gemeinsam eine passende Aufgabe .</ p >
2026-06-20 20:47:13 +02:00
</ div >
</ section >
< ? php if ( ! empty ( $page [ 'arguments' ][ 'items' ])) : ?>
< section class = " section sponsor-arguments " id = " warum-mithelfen " aria - labelledby = " warum-heading " >
< div class = " container " >
< h2 id = " warum-heading " >< ? = e ( $page [ 'arguments' ][ 'title' ]) ?> </h2>
< ? php if ( ! empty ( $page [ 'arguments' ][ 'intro' ])) : ?>
< p class = " sponsor-arguments__intro " >< ? = e ( $page [ 'arguments' ][ 'intro' ]) ?> </p>
< ? php endif ; ?>
< ul class = " sponsor-arguments__grid " role = " list " >
< ? php foreach ( $page [ 'arguments' ][ 'items' ] as $arg ) : ?>
< li class = " sponsor-arguments__item " >
< div class = " sponsor-arguments__icon " aria - hidden = " true " >< ? = icon ( $arg [ 'icon' ]) ?> </div>
< div class = " sponsor-arguments__body " >
< h3 class = " sponsor-arguments__title " >< ? = e ( $arg [ 'title' ]) ?> </h3>
< p class = " sponsor-arguments__text " >< ? = e ( $arg [ 'text' ]) ?> </p>
</ div >
</ li >
< ? php endforeach ; ?>
</ ul >
</ div >
</ section >
< ? php endif ; ?>
< ? php
component ( 'cta-band' , [ 'band' => $page [ 'cta' ], 'anchor' => 'kontakt-mitmachen' ]);
component ( 'faq' , [ 'faq' => $page [ 'faq' ] ? ? []]);