Files
tsv08kulmbach-website/app/components/group-schedule.php
fs 146471bae8 Wettkampfturnen: Karten offen untereinander statt hinter Tabs
Bei zwei Gruppen verstecken Tabs mehr Inhalt als sie ordnen, und die offene
Kartenreihe hält den Bento-Eindruck der Seite (Entscheidung Felix). Das
Kinderturnen bleibt bei Tabs, dort sind es drei textreiche Gruppen — die
Flamingos-Karte allein ist am Handy 2142px hoch.

.group-list--stacked lag vollständig im CSS, wurde aber von KEINEM PHP
ausgegeben: totes CSS aus der Zeit vor den Tabs, das heute morgen sogar noch
meine minmax(0, …)-Korrektur mitbekommen hat, ohne dass es jemand sehen konnte.
Jetzt ist es lebendig. Dritter toter Rest heute: "layout": "blocks" in
data/turnen.json las ebenfalls niemand, ist raus.

Der Karteninhalt steht dafür jetzt einmal statt zweimal. Er wird in zwei
Fassungen gebraucht (Tab-Panel mit role/aria und gestapelt ohne) und ist
deshalb eine Closure geworden — eine zweite Kopie wäre die Stelle, an der beide
in einem halben Jahr auseinanderlaufen.

Die Überschrift ist visually-hidden, nicht entfernt: sonst hätte die Sektion
keinen zugänglichen Namen und die Überschriften-Gliederung eine Lücke. Dasselbe
Muster wie $title_hidden in team-cards. Damit braucht die Seite auch kein
group-tabs.js mehr, das ich zwei Commits vorher erst dazugeschrieben hatte.

Zwei Nachbesserungen, beide gemessen:
- Die versteckte Überschrift hinterließ ein Loch. Zwischen Dashboard und erster
  Karte standen 160px (80 unten von der einen Sektion, 80 oben von der anderen),
  in denen nur nackte Hintergrund-Textur zu sehen war — vorher füllte die
  Überschrift diesen Raum. .group-list-section--tight nimmt das obere Padding
  weg, jetzt 80px.
- Die gestapelten Karten hatten feste 40px Innenabstand; auf --pad-card-lg
  umgestellt, dieselbe Korrektur wie heute morgen beim Tab-Panel. Am Desktop
  unverändert 40px, am Handy 28px.

Nicht behoben, weil kein Fehler: die Hintergrund-Textur wirkt in leeren Flächen
weich. Sie ist 1920x2545 groß und wird bei 1478px Fensterbreite auf Faktor 0,77
VERKLEINERT, ist also nicht unscharf gerechnet. Sichtbar wurde sie durch den
Leerraum plus background-attachment: fixed, das sie beim Scrollen stehen lässt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 22:56:21 +02:00

277 lines
13 KiB
PHP

<?php
declare(strict_types=1);
/**
* Turn-Disziplin im einheitlichen Bento-Look (wie die Mannschaftsseiten). Props:
* $location array{venue, street?, zip?, city?} (optional) — Trainingsort + „Route planen"
* $lead string (optional) — verantwortliche Übungsleitung (Akzent-Kachel)
* $notice string (optional) — hervorgehobener Hinweis (z. B. Wartelisten-Status)
* $noticeTitle string (optional) — Überschrift dazu
* $noticeTone 'info' (Default) | 'stop' — Tonlage, siehe components/notice.php
* $noticeIcon string (optional) — Icon-Name, siehe components/notice.php
* Gerendert wird über component('notice') — dieselbe Komponente, die
* contact-form.php über dem Formular benutzt, damit derselbe Hinweis an
* beiden Orten gleich aussieht und nur EINMAL in data/turnen.json steht.
* $groups array von Gruppen:
* { name, level?, contact?, intro?, details?:[{heading,text}], recommendation?,
* note?, trainers?:[…], sessions:[{ label?, day, time, note?, trainers?:[…] }] }
* contact = Ansprechperson NUR dieser Gruppe. Braucht es, sobald eine Disziplin
* mehrere Gruppen mit verschiedenen Ansprechpersonen hat (Wettkampfturnen:
* Flamingos → Theresa Diener, Leistungsturnen → Franziska Trotzke) — die
* Kontakt-Kachel oben kann nur EINE nennen und wäre für die andere falsch.
* Beschriftet als „Kontakt" wie die Kachel, nicht als „Ansprechpartner":
* das Label kennt das Geschlecht der Person nicht (Haus-Stil Paarformen).
* $footnote string (optional) — Erläuterung (z. B. zur Sternchen-Kennzeichnung)
* $groupsTitle string (optional, Default 'Unsere Gruppen')
* $groupsTitleHidden bool (optional) — H2 bleibt im DOM (Dokumentgliederung für
* Screenreader, Sektion behält ihren Namen), ist aber visuell versteckt.
* Dasselbe Muster wie $title_hidden in team-cards. NICHT die Überschrift
* weglassen: dann hätte die Sektion keinen zugänglichen Namen mehr.
* $layout 'tabs' (Default) | 'stacked' — greift nur bei beschreibenden Gruppen.
* 'stacked' zeigt alle Karten offen untereinander (wenige Gruppen),
* 'tabs' ordnet viele textreiche Gruppen (Kinderturnen).
* $anchor string (optional, Default 'training')
*
* Eine Gruppe → Trainingszeiten landen als Bento-Kachel im Dashboard.
* Mehrere → Dashboard zeigt nur Ort/Verantwortlich, darunter eine Gruppen-Sektion.
* Gruppen mit Beschreibung werden gestapelt, schlichte Gruppen als Raster.
*/
$anchor = $anchor ?? 'training';
$groupsTitle = $groupsTitle ?? 'Unsere Gruppen';
$groupsTitleHidden = $groupsTitleHidden ?? false;
$layout = ($layout ?? 'tabs') === 'stacked' ? 'stacked' : 'tabs';
$lead = $lead ?? '';
$notice = $notice ?? '';
$noticeTitle = $noticeTitle ?? '';
$noticeTone = $noticeTone ?? 'info';
$noticeIcon = $noticeIcon ?? 'info-circle';
$footnote = $footnote ?? '';
$groups = $groups ?? [];
$location = $location ?? [];
if ($groups === []) {
return;
}
$club = json_load('club');
$email = $club['email_turnen'] ?? ($club['email'] ?? '');
$single = count($groups) === 1;
$rich = false;
foreach ($groups as $g) {
if (!empty($g['intro']) || !empty($g['details'])) {
$rich = true;
break;
}
}
$hasVenue = !empty($location['venue']);
$mapsQuery = $hasVenue ? rawurlencode(trim(
($location['venue'] ?? '') . ', '
. ($location['street'] ?? '') . ', '
. ($location['zip'] ?? '') . ' ' . ($location['city'] ?? ''),
', '
)) : '';
/** Eine Trainingszeit (Tag/Zeit + optional Label, Notiz, Übungsleitung) ausgeben. */
$renderSession = static function (array $s): void {
?>
<li class="schedule-row">
<?php if (!empty($s['label'])): ?>
<span class="schedule-row__label"><?= e($s['label']) ?></span>
<?php endif; ?>
<span class="schedule-row__when">
<span class="schedule-row__day"><?= e($s['day']) ?></span>
<span class="schedule-row__time"><?= e($s['time']) ?></span>
</span>
<?php if (!empty($s['note'])): ?>
<span class="schedule-row__note text-muted"><?= e($s['note']) ?></span>
<?php endif; ?>
<?php if (!empty($s['trainers'])): ?>
<span class="schedule-row__trainers"><span class="schedule-row__trainers-label">Übungsleitung:</span> <?= e(implode(', ', $s['trainers'])) ?></span>
<?php endif; ?>
</li>
<?php
};
/**
* Inhalt einer beschreibenden Gruppen-Karte (ohne das <article> drumherum). Als Closure,
* weil es diese Karte in zwei Fassungen gibt: als Tab-Panel (mit role/aria) und gestapelt
* (ohne). Vorher stand das Markup nur in der Tab-Fassung; eine zweite Kopie daneben wäre
* die Stelle, an der beide auseinanderlaufen.
*/
$renderCardBody = static function (array $g) use ($renderSession): void {
?>
<div class="group-card__body">
<div class="group-card__header">
<h3 class="group-card__name"><?= e($g['name']) ?></h3>
<?php if (!empty($g['level'])): ?>
<p class="group-card__level"><?= e($g['level']) ?></p>
<?php endif; ?>
</div>
<?php if (!empty($g['intro'])): ?>
<p class="group-card__intro"><?= e($g['intro']) ?></p>
<?php endif; ?>
<?php foreach ($g['details'] ?? [] as $det): ?>
<?php if (!empty($det['heading'])): ?>
<h4 class="group-card__detail-heading"><?= e($det['heading']) ?></h4>
<?php endif; ?>
<?php if (!empty($det['text'])): ?>
<p class="group-card__detail-text"><?= e($det['text']) ?></p>
<?php endif; ?>
<?php endforeach; ?>
<?php if (!empty($g['recommendation'])): ?>
<p class="group-card__recommendation text-muted"><?= e($g['recommendation']) ?></p>
<?php endif; ?>
</div>
<?php if (!empty($g['sessions'])): ?>
<div class="group-card__schedule">
<p class="group-card__schedule-title"><?= icon('clock', 'group-card__schedule-icon') ?>Trainingszeiten</p>
<ul class="schedule-list">
<?php foreach ($g['sessions'] as $s): $renderSession($s); endforeach; ?>
</ul>
<?php if (!empty($g['contact'])): ?>
<p class="group-card__trainers"><span class="schedule-row__trainers-label">Kontakt:</span> <?= e($g['contact']) ?></p>
<?php endif; ?>
<?php if (!empty($g['trainers'])): ?>
<p class="group-card__trainers"><span class="schedule-row__trainers-label">Übungsleitung:</span> <?= e(implode(', ', $g['trainers'])) ?></p>
<?php endif; ?>
<?php if (!empty($g['note'])): ?>
<p class="group-card__note text-muted"><?= e($g['note']) ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
<?php
};
$soleSessions = $single ? ($groups[0]['sessions'] ?? []) : [];
$soleNote = $single ? ($groups[0]['note'] ?? '') : '';
?>
<section class="section turnen-dash-section" id="<?= e($anchor) ?>">
<div class="container">
<div class="turnen-dash<?= $single ? ' turnen-dash--single' : '' ?><?= $notice !== '' ? ' turnen-dash--notice' : '' ?>">
<?php if ($notice !== ''): ?>
<?php component('notice', [
'title' => $noticeTitle,
'text' => $notice,
'tone' => $noticeTone,
'icon' => $noticeIcon,
'class' => 'turnen-dash__notice',
]); ?>
<?php endif; ?>
<?php if ($single): ?>
<article class="bento__tile turnen-dash__training">
<h2 class="bento__heading"><?= icon('clock', 'bento__hicon') ?>Trainingszeiten</h2>
<ul class="bento__sessions">
<?php foreach ($soleSessions as $s): ?>
<li>
<span class="bento__day"><?= e($s['day']) ?></span>
<span class="bento__time"><?= e($s['time']) ?></span>
</li>
<?php endforeach; ?>
</ul>
<?php if ($soleNote !== ''): ?>
<p class="bento__note text-muted"><?= e($soleNote) ?></p>
<?php endif; ?>
<span class="bento__deco" aria-hidden="true"><?= icon('clock') ?></span>
</article>
<?php endif; ?>
<?php if ($lead !== '' || $email !== ''): ?>
<article class="bento__tile bento__tile--accent turnen-dash__contact">
<span class="bento__stat-label">Kontakt</span>
<?php if ($lead !== ''): ?>
<span class="bento__stat-value"><?= e($lead) ?></span>
<?php endif; ?>
<?php if ($email !== ''): ?>
<a class="turnen-dash__mail" href="mailto:<?= e($email) ?>"><?= icon('envelope', 'turnen-dash__mail-icon') ?>E-Mail schreiben</a>
<?php endif; ?>
</article>
<?php endif; ?>
<?php if ($hasVenue): ?>
<article class="bento__tile turnen-dash__venue">
<div class="turnen-dash__venue-main">
<h2 class="bento__heading"><?= icon('geo-alt', 'bento__hicon') ?>Trainingsort</h2>
<p class="bento__address">
<strong><?= e($location['venue']) ?></strong>
<?php if (!empty($location['street'])): ?> · <?= e($location['street']) ?><?php endif; ?>
<?php if (!empty($location['zip']) || !empty($location['city'])): ?>, <?= e(trim(($location['zip'] ?? '') . ' ' . ($location['city'] ?? ''))) ?><?php endif; ?>
</p>
</div>
<a class="btn btn--outline turnen-dash__route" href="https://www.google.com/maps/search/?api=1&amp;query=<?= e($mapsQuery) ?>" target="_blank" rel="noopener">Route planen<span class="visually-hidden"> (öffnet Google Maps in neuem Tab)</span></a>
<span class="bento__deco" aria-hidden="true"><?= icon('geo-alt') ?></span>
</article>
<?php endif; ?>
</div>
</div>
</section>
<?php if (!$single): ?>
<section class="section group-list-section<?= $groupsTitleHidden ? ' group-list-section--tight' : '' ?>" id="gruppen">
<div class="container">
<h2 class="group-list__title<?= $groupsTitleHidden ? ' visually-hidden' : '' ?>"><?= e($groupsTitle) ?></h2>
<?php if ($rich && $layout === 'stacked'): ?>
<?php /* Karten direkt untereinander, alle sichtbar. Für Disziplinen mit wenigen Gruppen:
Tabs verstecken dort mehr Inhalt als sie ordnen, und die offene Kartenreihe hält
den Bento-Eindruck der Seite. Bei vielen textreichen Gruppen (Kinderturnen) sind
Tabs weiter richtig, sonst wird die Seite endlos. */ ?>
<div class="group-list group-list--stacked">
<?php foreach ($groups as $g): ?>
<article class="group-card">
<?php $renderCardBody($g); ?>
</article>
<?php endforeach; ?>
</div>
<?php elseif ($rich): ?>
<div class="group-tabs" data-group-tabs>
<div class="group-tabs__bar" role="tablist" aria-label="<?= e($groupsTitle) ?>">
<?php foreach ($groups as $i => $g): ?>
<button class="tab group-tabs__tab" role="tab" type="button"
id="gtab-<?= $i ?>"
aria-controls="gpanel-<?= $i ?>"
aria-selected="<?= $i === 0 ? 'true' : 'false' ?>"
><?= e($g['name']) ?></button>
<?php endforeach; ?>
</div>
<?php foreach ($groups as $i => $g): ?>
<article class="group-card group-tabs__panel" role="tabpanel"
id="gpanel-<?= $i ?>"
aria-labelledby="gtab-<?= $i ?>"
tabindex="0"
>
<?php $renderCardBody($g); ?>
</article>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="group-list group-list--grid">
<?php foreach ($groups as $g): ?>
<article class="group-card">
<div class="group-card__body">
<div class="group-card__header">
<h3 class="group-card__name"><?= e($g['name']) ?></h3>
<?php if (!empty($g['level'])): ?>
<p class="group-card__level"><?= e($g['level']) ?></p>
<?php endif; ?>
</div>
</div>
<?php if (!empty($g['sessions'])): ?>
<div class="group-card__schedule">
<ul class="schedule-list">
<?php foreach ($g['sessions'] as $s): $renderSession($s); endforeach; ?>
</ul>
</div>
<?php endif; ?>
</article>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($footnote !== ''): ?>
<p class="group-list__footnote text-muted"><?= e($footnote) ?></p>
<?php endif; ?>
</div>
</section>
<?php endif; ?>