Unterseiten: Fußball-Übersicht, Mannschaften, Jugend, Verein, Rechtliches
Neue Seiten + Routen: fussball, fussball/{erste-mannschaft,zweite-mannschaft,
damen}, jugendfussball, verein, impressum, datenschutz. Dazu die Komponenten
team, team-cards/-roster/-photo/-bento, faq/faq-contact, key-facts, age-groups,
cta-band, training-location und der contact-slider.
Daten in teams/fussball/jugend/verein.json; club.json um Vorstand, Bankdaten,
Datenschutz-Kontakt erweitert. person-grid bekommt $names-Filter, partner-grid
einen Intro-Stil. Seiten setzen $meta['schema'] via team_/page_schema.
Styles für alle neuen Komponenten in components.css.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDQZ6FuchNUh2bxSW9PeNv
This commit is contained in:
47
app/components/age-groups.php
Normal file
47
app/components/age-groups.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Jugend-Altersklassen als Karten-Grid. Props:
|
||||
* $groups array [{code, name, photo?{base,widths,alt}, training?, note?}, …]
|
||||
* $title string (optional, Default 'Unsere Jugendmannschaften')
|
||||
* $anchor string (optional, Default 'mannschaften')
|
||||
* Fehlt ein Foto, greift ein Platzhalter; fehlt die Trainingszeit, entfällt die Zeile.
|
||||
*/
|
||||
$title = $title ?? 'Unsere Jugendmannschaften';
|
||||
$anchor = $anchor ?? 'mannschaften';
|
||||
$groups = $groups ?? [];
|
||||
if ($groups === []) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<section class="section age-groups" id="<?= e($anchor) ?>">
|
||||
<div class="container">
|
||||
<h2><?= e($title) ?></h2>
|
||||
<ul class="age-groups__grid">
|
||||
<?php foreach ($groups as $g): ?>
|
||||
<li class="age-group">
|
||||
<div class="age-group__media">
|
||||
<?php if (!empty($g['photo']['base'])): ?>
|
||||
<?php component('img', ['image' => $g['photo'], 'sizes' => '(max-width: 600px) 100vw, 320px', 'class' => 'age-group__img']); ?>
|
||||
<?php else: ?>
|
||||
<img class="age-group__img" src="<?= e(asset('img/persons/platzhalter.jpg')) ?>" alt="" width="400" height="300" loading="lazy" decoding="async">
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($g['code'])): ?>
|
||||
<span class="age-group__badge" aria-hidden="true"><?= e($g['code']) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="age-group__body">
|
||||
<h3 class="age-group__name"><?= e($g['name']) ?></h3>
|
||||
<?php if (!empty($g['training'])): ?>
|
||||
<p class="age-group__training"><?= icon('clock', 'age-group__icon') ?><span><?= e($g['training']) ?></span></p>
|
||||
<?php elseif (!empty($g['note'])): ?>
|
||||
<p class="age-group__training text-muted"><?= e($g['note']) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
30
app/components/cta-band.php
Normal file
30
app/components/cta-band.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Vollbreites Call-to-Action-Band. Props:
|
||||
* $band array{title, text?, ctas: [{label, slug, style?}]}
|
||||
* $anchor string (optional)
|
||||
* Rendert nichts ohne CTAs. Nutzt das bestehende cta-/btn-Muster.
|
||||
*/
|
||||
$anchor = $anchor ?? null;
|
||||
if (empty($band['ctas'])) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<section class="section section--tinted cta-band"<?= $anchor ? ' id="' . e($anchor) . '"' : '' ?>>
|
||||
<div class="container cta-band__inner">
|
||||
<?php if (!empty($band['title'])): ?>
|
||||
<h2 class="cta-band__title"><?= e($band['title']) ?></h2>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($band['text'])): ?>
|
||||
<p class="cta-band__text"><?= e($band['text']) ?></p>
|
||||
<?php endif; ?>
|
||||
<div class="cta-band__actions">
|
||||
<?php foreach ($band['ctas'] as $cta): ?>
|
||||
<?php component('cta', ['cta' => $cta]); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
76
app/components/faq-contact.php
Normal file
76
app/components/faq-contact.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Kombi-Sektion: FAQ (2/3) + einzelner Ansprechpartner (1/3) nebeneinander.
|
||||
* Sinnvoll, wenn es pro Mannschaft nur einen Kontakt gibt. Props:
|
||||
* $faq array [{q, a}]
|
||||
* $persons array — ein oder mehrere Ansprechpartner (person-card). Leer → FAQ volle Breite.
|
||||
* Bei mehreren: Mini-Slider (ohne JS alle Karten gestapelt sichtbar).
|
||||
* $faqTitle string (optional, Default 'Häufige Fragen')
|
||||
* $faqIntro string (optional) — gedämpfte Subline unter der Überschrift
|
||||
* $contactTitle string (optional, Default 'Dein Ansprechpartner')
|
||||
* $anchor string (optional, Default 'kontakt')
|
||||
* Natives <details>/<summary> (ohne JS bedienbar); FAQPage-Schema liefert die Seite.
|
||||
*/
|
||||
$faqTitle = $faqTitle ?? 'Häufige Fragen';
|
||||
$faqIntro = $faqIntro ?? '';
|
||||
$contactTitle = $contactTitle ?? 'Dein Ansprechpartner';
|
||||
$anchor = $anchor ?? 'kontakt';
|
||||
$faq = array_values(array_filter(
|
||||
$faq ?? [],
|
||||
static fn (array $i): bool => !empty($i['q']) && !empty($i['a'])
|
||||
));
|
||||
$persons = array_values(array_filter($persons ?? [], static fn ($p): bool => !empty($p)));
|
||||
$hasPerson = $persons !== [];
|
||||
$isSlider = count($persons) > 1;
|
||||
if ($faq === [] && !$hasPerson) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<section class="section faq-contact<?= $hasPerson ? '' : ' faq-contact--solo' ?>" id="<?= e($anchor) ?>">
|
||||
<div class="container">
|
||||
<h2 class="faq-contact__title"><?= e($faqTitle) ?></h2>
|
||||
<?php if ($faqIntro !== ''): ?>
|
||||
<p class="faq-contact__intro text-muted"><?= e($faqIntro) ?></p>
|
||||
<?php endif; ?>
|
||||
<div class="faq-contact__inner">
|
||||
<div class="faq-contact__main">
|
||||
<div class="faq__list">
|
||||
<?php foreach ($faq as $item): ?>
|
||||
<details class="faq__item">
|
||||
<summary class="faq__q"><span><?= e($item['q']) ?></span><?= icon('chevron-down', 'faq__chevron') ?></summary>
|
||||
<div class="faq__a"><p><?= e($item['a']) ?></p></div>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($hasPerson): ?>
|
||||
<aside class="faq-contact__aside">
|
||||
<?php if ($contactTitle !== ''): ?>
|
||||
<h2><?= e($contactTitle) ?></h2>
|
||||
<?php endif; ?>
|
||||
<?php if ($isSlider): ?>
|
||||
<div class="contact-slider" data-contact-slider>
|
||||
<ul class="contact-slider__track">
|
||||
<?php foreach ($persons as $p): ?>
|
||||
<li class="contact-slider__slide" data-contact-slide>
|
||||
<?php component('person-card', ['person' => $p]); ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<div class="contact-slider__dots">
|
||||
<?php foreach ($persons as $i => $p): ?>
|
||||
<button type="button" class="contact-slider__dot" data-contact-dot<?= $i === 0 ? ' aria-current="true"' : '' ?> aria-label="Ansprechpartner <?= $i + 1 ?>: <?= e($p['name']) ?>"></button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php component('person-card', ['person' => $persons[0]]); ?>
|
||||
<?php endif; ?>
|
||||
</aside>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
35
app/components/faq.php
Normal file
35
app/components/faq.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* FAQ-Accordion. Props:
|
||||
* $faq array [{q, a}, …]
|
||||
* $title string (optional, Default 'Häufige Fragen')
|
||||
* $anchor string (optional, Default 'faq')
|
||||
* Natives <details>/<summary> → ohne JS bedienbar. Das FAQPage-Schema liefert
|
||||
* die Seite über $meta['schema'] (helpers: faq_schema/team_schema), nicht diese Komponente.
|
||||
*/
|
||||
$title = $title ?? 'Häufige Fragen';
|
||||
$anchor = $anchor ?? 'faq';
|
||||
$faq = array_values(array_filter(
|
||||
$faq ?? [],
|
||||
static fn (array $i): bool => !empty($i['q']) && !empty($i['a'])
|
||||
));
|
||||
if ($faq === []) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<section class="section faq" id="<?= e($anchor) ?>">
|
||||
<div class="container faq__inner">
|
||||
<h2><?= e($title) ?></h2>
|
||||
<div class="faq__list">
|
||||
<?php foreach ($faq as $item): ?>
|
||||
<details class="faq__item">
|
||||
<summary class="faq__q"><span><?= e($item['q']) ?></span><?= icon('chevron-down', 'faq__chevron') ?></summary>
|
||||
<div class="faq__a"><p><?= e($item['a']) ?></p></div>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
41
app/components/key-facts.php
Normal file
41
app/components/key-facts.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Kompakte Faktenleiste (Icon + Label + Wert). Props:
|
||||
* $facts array [{icon, label, value}, …] — Einträge mit leerem 'value' werden übersprungen.
|
||||
* $anchor string (optional) — Sektions-ID.
|
||||
* $tinted bool (optional, Default false) — aufgehelltes Band.
|
||||
* GEO/SEO: kompakte, zitierfähige Fakten. Rendert nichts, wenn keine Werte vorliegen.
|
||||
*/
|
||||
$anchor = $anchor ?? null;
|
||||
$tinted = $tinted ?? false;
|
||||
|
||||
$facts = array_values(array_filter(
|
||||
$facts ?? [],
|
||||
static fn (array $f): bool => isset($f['value']) && $f['value'] !== ''
|
||||
));
|
||||
if ($facts === []) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<section class="section key-facts<?= $tinted ? ' section--tinted' : '' ?>"<?= $anchor ? ' id="' . e($anchor) . '"' : '' ?>>
|
||||
<div class="container">
|
||||
<ul class="key-facts__grid">
|
||||
<?php foreach ($facts as $fact): ?>
|
||||
<li class="key-facts__item">
|
||||
<?php if (!empty($fact['icon'])): ?>
|
||||
<span class="key-facts__icon"><?= icon($fact['icon']) ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="key-facts__text">
|
||||
<?php if (!empty($fact['label'])): ?>
|
||||
<span class="key-facts__label"><?= e($fact['label']) ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="key-facts__value"><?= e($fact['value']) ?></span>
|
||||
</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
@@ -12,7 +12,7 @@ $partners = json_load('partners')['partners'] ?? [];
|
||||
<section class="section partners" id="partner">
|
||||
<div class="container">
|
||||
<h2><?= e($intro['title']) ?></h2>
|
||||
<p><?= e($intro['text']) ?></p>
|
||||
<p class="partners__intro text-muted"><?= e($intro['text']) ?></p>
|
||||
<ul class="partners__grid">
|
||||
<?php foreach ($partners as $partner): ?>
|
||||
<li class="partners__item">
|
||||
|
||||
@@ -6,6 +6,7 @@ declare(strict_types=1);
|
||||
* Ansprechpartner-Grid aus data/ansprechpartner.json (Single Source of Truth).
|
||||
* Alle Props optional:
|
||||
* $departments array — nur diese Abteilungen zeigen (z. B. ['Herrenfußball','Damenfußball']).
|
||||
* $names array — zusätzlich auf diese Namen einschränken (z. B. ['Benedikt Höfner']).
|
||||
* $title string — Überschrift überschreiben (Default aus JSON).
|
||||
* $intro string — Intro-Text überschreiben (Default aus JSON; '' = ausblenden).
|
||||
* $anchor string — Sektions-ID (Default 'ansprechpartner').
|
||||
@@ -24,6 +25,12 @@ if (!empty($departments)) {
|
||||
static fn (array $p): bool => in_array($p['department'] ?? '', $departments, true)
|
||||
));
|
||||
}
|
||||
if (!empty($names)) {
|
||||
$persons = array_values(array_filter(
|
||||
$persons,
|
||||
static fn (array $p): bool => in_array($p['name'] ?? '', $names, true)
|
||||
));
|
||||
}
|
||||
|
||||
if ($persons === []) {
|
||||
return;
|
||||
|
||||
82
app/components/team-bento.php
Normal file
82
app/components/team-bento.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Team-Dashboard als Bento-Grid: kombiniert Bild, Einleitung, Trainingszeiten,
|
||||
* Spielklasse und Spielort in einem Kachel-Layout. Props:
|
||||
* $team array — Eintrag aus data/teams.json.
|
||||
* $anchor string (optional, Default 'team-info')
|
||||
* Spielort kommt aus data/club.json (NAP-Single-Source). Deko-Icons sind dekorativ.
|
||||
*/
|
||||
$anchor = $anchor ?? 'team-info';
|
||||
$about = $team['about'] ?? [];
|
||||
$sessions = $team['training']['sessions'] ?? [];
|
||||
$venueOverride = $team['training']['venue'] ?? null; // mannschaftsspezifischer Spielort (z. B. SG-Heimat)
|
||||
$showVenue = !empty($team['training']['venue_from_club']) || !empty($venueOverride);
|
||||
$highlight = !empty($team['league'])
|
||||
? ['label' => 'Spielklasse', 'value' => $team['league']]
|
||||
: ['label' => 'Bereich', 'value' => $team['category'] ?? ''];
|
||||
|
||||
$club = json_load('club');
|
||||
$addr = $club['address'] ?? [];
|
||||
$mapsQuery = !empty($venueOverride['maps_query'])
|
||||
? rawurlencode($venueOverride['maps_query'])
|
||||
: rawurlencode(trim(($addr['venue'] ?? '') . ', ' . ($addr['street'] ?? '') . ', ' . ($addr['zip'] ?? '') . ' ' . ($addr['city'] ?? ''), ', '));
|
||||
?>
|
||||
<section class="section team-bento-section" id="<?= e($anchor) ?>">
|
||||
<div class="container">
|
||||
<div class="team-bento">
|
||||
<article class="bento__tile team-bento__about">
|
||||
<h2><?= e($about['title'] ?? 'Über die Mannschaft') ?></h2>
|
||||
<p><?= e($about['text'] ?? '') ?></p>
|
||||
</article>
|
||||
|
||||
<article class="bento__tile team-bento__training">
|
||||
<h2 class="bento__heading"><?= icon('clock', 'bento__hicon') ?>Trainingszeiten</h2>
|
||||
<?php if ($sessions !== []): ?>
|
||||
<ul class="bento__sessions">
|
||||
<?php foreach ($sessions as $s): ?>
|
||||
<li>
|
||||
<span class="bento__day"><?= e($s['day']) ?></span>
|
||||
<span class="bento__time"><?= e($s['time']) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<p class="text-muted">Die genauen Trainingszeiten erfährst du direkt bei unserem Ansprechpartner.</p>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($team['training']['note'])): ?>
|
||||
<p class="bento__note text-muted"><?= e($team['training']['note']) ?></p>
|
||||
<?php endif; ?>
|
||||
<span class="bento__deco" aria-hidden="true"><?= icon('clock') ?></span>
|
||||
</article>
|
||||
|
||||
<?php if (($highlight['value'] ?? '') !== ''): ?>
|
||||
<article class="bento__tile bento__tile--accent team-bento__klasse">
|
||||
<span class="bento__stat-label"><?= e($highlight['label']) ?></span>
|
||||
<span class="bento__stat-value"><?= e($highlight['value']) ?></span>
|
||||
</article>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($showVenue && ($venueOverride || $addr !== [])): ?>
|
||||
<article class="bento__tile team-bento__venue">
|
||||
<div class="team-bento__venue-main">
|
||||
<h2 class="bento__heading"><?= icon('geo-alt', 'bento__hicon') ?>Spielort</h2>
|
||||
<?php if ($venueOverride): ?>
|
||||
<p class="bento__address">
|
||||
<?php if (!empty($venueOverride['name'])): ?><strong><?= e($venueOverride['name']) ?></strong><?php endif; ?><?php if (!empty($venueOverride['locality'])): ?> · <?= e($venueOverride['locality']) ?><?php endif; ?>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<p class="bento__address">
|
||||
<?php if (!empty($addr['venue'])): ?><strong><?= e($addr['venue']) ?></strong> · <?php endif; ?><?= e($addr['street']) ?>, <?= e($addr['zip']) ?> <?= e($addr['city']) ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<a class="btn btn--outline team-bento__route" href="https://www.google.com/maps/search/?api=1&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>
|
||||
55
app/components/team-cards.php
Normal file
55
app/components/team-cards.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Mannschafts-Karten, die auf die Team-Unterseiten verlinken. Props:
|
||||
* $teams array [{name, slug, category, league?, image?{base,widths,alt}}, …]
|
||||
* $title string (optional, Default 'Unsere Mannschaften')
|
||||
* $title_hidden bool (optional) — H2 für SEO/A11y im DOM, aber visuell versteckt
|
||||
* $intro string (optional)
|
||||
* $intro_below bool (optional) — Intro als „Klärtext" UNTER die Kacheln statt darüber
|
||||
* $anchor string (optional, Default 'mannschaften')
|
||||
* Ganze Karte ist ein Link. Ohne Bild greift ein Platzhalter.
|
||||
*/
|
||||
$title = $title ?? 'Unsere Mannschaften';
|
||||
$title_hidden = $title_hidden ?? false;
|
||||
$anchor = $anchor ?? 'mannschaften';
|
||||
$intro = $intro ?? '';
|
||||
$intro_below = $intro_below ?? false;
|
||||
$teams = $teams ?? [];
|
||||
if ($teams === []) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<section class="section team-cards" id="<?= e($anchor) ?>">
|
||||
<div class="container">
|
||||
<h2<?= $title_hidden ? ' class="visually-hidden"' : '' ?>><?= e($title) ?></h2>
|
||||
<?php if ($intro !== '' && !$intro_below): ?>
|
||||
<p class="team-cards__intro text-muted"><?= e($intro) ?></p>
|
||||
<?php endif; ?>
|
||||
<ul class="team-cards__grid">
|
||||
<?php foreach ($teams as $t): ?>
|
||||
<li>
|
||||
<a class="team-card" href="<?= e(url($t['slug'])) ?>">
|
||||
<div class="team-card__media">
|
||||
<?php if (!empty($t['image']['base'])): ?>
|
||||
<?php component('img', ['image' => $t['image'], 'sizes' => '(max-width: 600px) 100vw, 360px', 'class' => 'team-card__img']); ?>
|
||||
<?php else: ?>
|
||||
<img class="team-card__img" src="<?= e(asset('img/persons/platzhalter.jpg')) ?>" alt="" width="400" height="300" loading="lazy" decoding="async">
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="team-card__body">
|
||||
<h3 class="team-card__name"><?= e($t['name']) ?></h3>
|
||||
<p class="team-card__meta"><?= e(trim($t['category'] . (!empty($t['league']) ? ' · ' . $t['league'] : ''))) ?></p>
|
||||
<span class="team-card__cta">Mehr erfahren <?= icon('arrow-right', 'team-card__arrow') ?></span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php if ($intro !== '' && $intro_below): ?>
|
||||
<p class="team-cards__intro team-cards__intro--below text-muted"><?= e($intro) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
27
app/components/team-photo.php
Normal file
27
app/components/team-photo.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Mannschaftsfoto in voller Breite (Querformat-Gruppenbild). Props:
|
||||
* $photo array{ image: {base,widths,alt} | {src,width,height,alt}, caption? }
|
||||
* $anchor string (optional, Default 'mannschaftsfoto')
|
||||
* Zeigt das Bild im natürlichen Seitenverhältnis, abgerundet; optionale Bildunterschrift.
|
||||
*/
|
||||
$anchor = $anchor ?? 'mannschaftsfoto';
|
||||
$image = $photo['image'] ?? null;
|
||||
if (empty($image)) {
|
||||
return;
|
||||
}
|
||||
$caption = $photo['caption'] ?? '';
|
||||
?>
|
||||
<section class="section team-photo" id="<?= e($anchor) ?>">
|
||||
<div class="container">
|
||||
<figure class="team-photo__figure">
|
||||
<?php component('img', ['image' => $image, 'sizes' => '(max-width: 1100px) 100vw, 1040px', 'class' => 'team-photo__img']); ?>
|
||||
<?php if ($caption !== ''): ?>
|
||||
<figcaption class="team-photo__caption"><?= e($caption) ?></figcaption>
|
||||
<?php endif; ?>
|
||||
</figure>
|
||||
</div>
|
||||
</section>
|
||||
78
app/components/team-roster.php
Normal file
78
app/components/team-roster.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Kader-/Trainerstab-Raster einer Mannschaft. Props:
|
||||
* $roster array{
|
||||
* title?, intro?,
|
||||
* variant? : 'below' (Name unter dem Foto, Default) | 'overlay' (Name im Foto, Coolvetica),
|
||||
* groups? : [{ title?, members:[…] }, …] ODER
|
||||
* members? : [{ name, role?, image? }, …] (eine Gruppe ohne Überschrift)
|
||||
* }
|
||||
* $anchor string (optional, Default 'kader')
|
||||
* Gruppen mit Titel bekommen eine Zwischenüberschrift (h3); leere Gruppen entfallen.
|
||||
* Ohne Foto greift der Platzhalter-Avatar.
|
||||
*/
|
||||
$anchor = $anchor ?? 'kader';
|
||||
|
||||
// Gruppen normalisieren: entweder explizite groups oder flache members als eine Gruppe.
|
||||
$groups = $roster['groups'] ?? null;
|
||||
if ($groups === null) {
|
||||
$flat = $roster['members'] ?? [];
|
||||
$groups = $flat === [] ? [] : [['title' => '', 'members' => $flat]];
|
||||
}
|
||||
// Leere Gruppen rauswerfen.
|
||||
$groups = array_values(array_filter($groups, static fn ($g): bool => !empty($g['members'])));
|
||||
if ($groups === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$variant = ($roster['variant'] ?? 'below') === 'overlay' ? 'overlay' : 'below';
|
||||
$title = $roster['title'] ?? 'Kader';
|
||||
$intro = $roster['intro'] ?? '';
|
||||
$cardMod = $variant === 'overlay' ? ' roster-card--overlay' : '';
|
||||
?>
|
||||
<section class="section team-roster" id="<?= e($anchor) ?>">
|
||||
<div class="container">
|
||||
<h2><?= e($title) ?></h2>
|
||||
<?php if ($intro !== ''): ?>
|
||||
<p class="team-roster__intro text-muted"><?= e($intro) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($groups as $group): ?>
|
||||
<?php if (!empty($group['title'])): ?>
|
||||
<h3 class="team-roster__group"><?= e($group['title']) ?></h3>
|
||||
<?php endif; ?>
|
||||
<ul class="team-roster__grid">
|
||||
<?php foreach ($group['members'] as $m): ?>
|
||||
<?php $src = !empty($m['image']) ? $m['image'] : 'img/persons/platzhalter-spieler.jpg'; ?>
|
||||
<?php $alt = !empty($m['image']) ? e($m['name']) : ''; ?>
|
||||
<?php $staffMod = !empty($m['role']) ? ' roster-card--staff' : ''; ?>
|
||||
<li class="roster-card<?= $cardMod . $staffMod ?>">
|
||||
<?php if ($variant === 'overlay'): ?>
|
||||
<img class="roster-card__img" src="<?= e(asset($src)) ?>" alt="<?= $alt ?>"
|
||||
width="480" height="600" loading="lazy" decoding="async">
|
||||
<div class="roster-card__caption">
|
||||
<?php if (!empty($m['role'])): ?>
|
||||
<span class="roster-card__role"><?= e($m['role']) ?></span>
|
||||
<?php endif; ?>
|
||||
<p class="roster-card__name"><?= e($m['name']) ?></p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="roster-card__media">
|
||||
<img src="<?= e(asset($src)) ?>" alt="<?= $alt ?>"
|
||||
width="480" height="600" loading="lazy" decoding="async">
|
||||
</div>
|
||||
<div class="roster-card__body">
|
||||
<?php if (!empty($m['role'])): ?>
|
||||
<span class="roster-card__role"><?= e($m['role']) ?></span>
|
||||
<?php endif; ?>
|
||||
<p class="roster-card__name"><?= e($m['name']) ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
46
app/components/team.php
Normal file
46
app/components/team.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Mannschafts-Seite (DRY-Template für 1./2. Herren + Damen). Props:
|
||||
* $team array — Eintrag aus data/teams.json.
|
||||
* $slug string — voller Seiten-Slug (für Anker-Links).
|
||||
* Aufbau: Hero → Team-Bento (Bild, Einleitung, Training, Spielort) → FAQ + Ansprechpartner.
|
||||
* Leere Datenblöcke (Training, FAQ, fehlender Kontakt) blenden sich selbst aus.
|
||||
*/
|
||||
$slug = $slug ?? '';
|
||||
|
||||
// Ansprechpartner aus der Single-Source auflösen (ein oder mehrere, Reihenfolge wie angegeben).
|
||||
$allPersons = json_load('ansprechpartner')['persons'] ?? [];
|
||||
$wantDept = $team['contact_department'] ?? '';
|
||||
$wantNames = $team['contact_names'] ?? (isset($team['contact_name']) ? [$team['contact_name']] : []);
|
||||
$contacts = [];
|
||||
foreach ($wantNames as $wn) {
|
||||
foreach ($allPersons as $p) {
|
||||
if (($p['department'] ?? '') === $wantDept && ($p['name'] ?? '') === $wn) {
|
||||
$contacts[] = $p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component('hero', ['hero' => $team['hero']]);
|
||||
|
||||
if (!empty($team['team_photo']['image'])) {
|
||||
component('team-photo', ['photo' => $team['team_photo'], 'anchor' => 'mannschaftsfoto']);
|
||||
}
|
||||
|
||||
component('team-bento', ['team' => $team, 'anchor' => 'team-info']);
|
||||
|
||||
if (!empty($team['roster']['members']) || !empty($team['roster']['groups'])) {
|
||||
component('team-roster', ['roster' => $team['roster'], 'anchor' => 'kader']);
|
||||
}
|
||||
|
||||
component('faq-contact', [
|
||||
'faq' => $team['faq'] ?? [],
|
||||
'faqIntro' => 'Die wichtigsten Fragen rund um die ' . ($team['name'] ?? 'Mannschaft') . ' – kurz beantwortet.',
|
||||
'persons' => $contacts,
|
||||
'contactTitle' => '',
|
||||
'anchor' => 'kontakt',
|
||||
]);
|
||||
73
app/components/training-location.php
Normal file
73
app/components/training-location.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Trainings-/Spielort-„Dashboard" als Bento-Grid. Props:
|
||||
* $training array{sessions: [{day, time}], venue_from_club?: bool, note?: string}
|
||||
* $highlight array{label, value} (optional) — kleiner Akzent-Tile (z. B. Spielklasse).
|
||||
* $anchor string (optional, Default 'training')
|
||||
* Spielort kommt aus data/club.json (NAP-Single-Source). Ohne Daten: nichts.
|
||||
*/
|
||||
$anchor = $anchor ?? 'training';
|
||||
$sessions = $training['sessions'] ?? [];
|
||||
$showVenue = !empty($training['venue_from_club']);
|
||||
$highlight = $highlight ?? null;
|
||||
|
||||
if ($sessions === [] && !$showVenue) {
|
||||
return;
|
||||
}
|
||||
|
||||
$club = json_load('club');
|
||||
$addr = $club['address'] ?? [];
|
||||
$mapsQuery = rawurlencode(trim(($addr['venue'] ?? '') . ', ' . ($addr['street'] ?? '') . ', ' . ($addr['zip'] ?? '') . ' ' . ($addr['city'] ?? ''), ', '));
|
||||
?>
|
||||
<section class="section training" id="<?= e($anchor) ?>">
|
||||
<div class="container">
|
||||
<div class="bento">
|
||||
<article class="bento__tile bento__tile--times">
|
||||
<h2 class="bento__heading"><?= icon('clock', 'bento__hicon') ?>Trainingszeiten</h2>
|
||||
<?php if ($sessions !== []): ?>
|
||||
<ul class="bento__sessions">
|
||||
<?php foreach ($sessions as $s): ?>
|
||||
<li>
|
||||
<span class="bento__day"><?= e($s['day']) ?></span>
|
||||
<span class="bento__time"><?= e($s['time']) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<p class="text-muted">Die genauen Trainingszeiten erfährst du direkt bei unserem Ansprechpartner.</p>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($training['note'])): ?>
|
||||
<p class="bento__note text-muted"><?= e($training['note']) ?></p>
|
||||
<?php endif; ?>
|
||||
<span class="bento__deco" aria-hidden="true"><?= icon('clock') ?></span>
|
||||
</article>
|
||||
|
||||
<?php if ($showVenue && $addr !== []): ?>
|
||||
<article class="bento__tile bento__tile--venue">
|
||||
<h2 class="bento__heading"><?= icon('geo-alt', 'bento__hicon') ?>Spielort</h2>
|
||||
<p class="bento__address">
|
||||
<?php if (!empty($addr['venue'])): ?><strong><?= e($addr['venue']) ?></strong><br><?php endif; ?>
|
||||
<?= e($addr['street']) ?><br>
|
||||
<?= e($addr['zip']) ?> <?= e($addr['city']) ?>
|
||||
</p>
|
||||
<span class="bento__deco" aria-hidden="true"><?= icon('geo-alt') ?></span>
|
||||
</article>
|
||||
|
||||
<article class="bento__tile bento__tile--route">
|
||||
<span class="bento__route-label">Anfahrt</span>
|
||||
<a class="btn btn--outline" href="https://www.google.com/maps/search/?api=1&query=<?= e($mapsQuery) ?>" target="_blank" rel="noopener">Route planen<span class="visually-hidden"> (öffnet Google Maps in neuem Tab)</span></a>
|
||||
</article>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($highlight !== null && ($highlight['value'] ?? '') !== ''): ?>
|
||||
<article class="bento__tile bento__tile--accent">
|
||||
<span class="bento__stat-label"><?= e($highlight['label'] ?? '') ?></span>
|
||||
<span class="bento__stat-value"><?= e($highlight['value']) ?></span>
|
||||
</article>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
Reference in New Issue
Block a user