Files
tsv08kulmbach-website/app/pages/mitmachen.php
fs eacf9449e0 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

92 lines
4.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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',
'description' => 'Ehrenamt beim TSV 08 Kulmbach: offene Aufgaben als Jugendtrainer, Betreuer oder Helfer bei Veranstaltungen mit direktem Ansprechpartner, ohne Vorerfahrung.',
'og_image' => 'img/pages/jugendfussball-hero-1440.jpg',
'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): ?>
<button type="button" class="tab filter-bar__tab" data-filter="<?= e($f['value']) ?>" aria-pressed="<?= $i === 0 ? 'true' : 'false' ?>"><?= e($f['label']) ?></button>
<?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>
<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>
</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'] ?? []]);