Neue Seiten: Mitglied/Partner werden, Sportheimbuchung & Ansprechpartner
Mitglied werden: Hub-Seite mit Online-Anmeldung (membership-form + membership-submit-Action, Honeypot + Time-Trap), Abteilungswahl (department-choice), Beitragstabelle (fee-table) und Turn-Aufnahmeantrag als PDF-Download. Daten in data/mitglied-werden.json. Partner werden & Sportheimbuchung: neue Seiten samt Routing und Navigation. Ansprechpartner: echter Hero (Vereinsgelände), Bereiche aus dem group-Feld der Single Source (Fußball / Turnen) zu zwei Sektionen gebündelt, Disziplin als Karten-Überschrift. Fehlende Turn-Disziplin-Leads (Frisque, Lorz, Kornatz, Trotzke) aus den Turnen-Unterseiten extrahiert und in data/ansprechpartner.json ergänzt (allgemeine Turnabteilungs-Mail als Kontakt). WhatsApp für alle Personen mit Telefonnummer. person-grid: kompaktere Karten, Hairline unter der Bereichsüberschrift, 5 Spalten am Desktop. Startseite auf die Haupt-Ansprechpartner je Bereich begrenzt. Dazu: neue Icons (check-circle, download, file-earmark-text, whatsapp), Anpassungen an Header/Footer/CTA/age-groups sowie tokens/layout/utilities. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RZsKTL5kRSG4KkK8w2LKkE
This commit is contained in:
131
app/actions/membership-submit.php
Normal file
131
app/actions/membership-submit.php
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /mitglied-werden-senden — Mitgliedsantrag Fußball (schlank, ohne Bankdaten).
|
||||||
|
* Versendet eine interne Vereins-Mail (erfolgskritisch) und eine Eingangsbestätigung
|
||||||
|
* an den Antragsteller (optional, Fehler dort wird nicht an den Besucher geleakt).
|
||||||
|
* Antwort: PRG-Redirect zu /mitglied-werden#anmeldung-fussball (?sent=1 | ?error=…)
|
||||||
|
* oder JSON bei fetch (form.js).
|
||||||
|
*/
|
||||||
|
|
||||||
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
|
|
||||||
|
$wantsJson = str_contains($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json');
|
||||||
|
|
||||||
|
$respond = static function (bool $ok, string $error = '') use ($wantsJson): never {
|
||||||
|
if ($wantsJson) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode(['ok' => $ok, 'error' => $error ?: null]);
|
||||||
|
} else {
|
||||||
|
header('Location: ' . url('mitglied-werden') . ($ok ? '?sent=1' : '?error=' . $error) . '#anmeldung-fussball', true, 303);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
};
|
||||||
|
|
||||||
|
$field = static fn (string $key): string => trim((string) ($_POST[$key] ?? ''));
|
||||||
|
|
||||||
|
// --- Spam-Checks: Bots bekommen ein stilles "OK" (kein Feedback-Kanal) ---
|
||||||
|
if ($field('website') !== '' || !form_token_valid($field('ft'))) {
|
||||||
|
$respond(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Eingaben ---
|
||||||
|
$firstName = $field('first_name');
|
||||||
|
$lastName = $field('last_name');
|
||||||
|
$birthDate = $field('birth_date');
|
||||||
|
$phone = $field('phone');
|
||||||
|
$street = $field('street');
|
||||||
|
$zip = $field('zip');
|
||||||
|
$city = $field('city');
|
||||||
|
$email = $field('email');
|
||||||
|
$feeClass = $field('fee_class');
|
||||||
|
|
||||||
|
// Beitragsklassen-Whitelist identisch zur Seite aus data/mitglied-werden.json ableiten.
|
||||||
|
$data = json_load('mitglied-werden');
|
||||||
|
$feeOptions = array_map(
|
||||||
|
static fn (array $r): string => $r['label'] . ' — ' . $r['price'],
|
||||||
|
$data['fussball']['fees']['rows'] ?? []
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Validierung (Whitelist-Prinzip) ---
|
||||||
|
$valid = $firstName !== '' && mb_strlen($firstName) <= 100
|
||||||
|
&& $lastName !== '' && mb_strlen($lastName) <= 100
|
||||||
|
&& preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthDate) === 1
|
||||||
|
&& $phone !== '' && mb_strlen($phone) <= 50
|
||||||
|
&& $street !== '' && mb_strlen($street) <= 200
|
||||||
|
&& $zip !== '' && mb_strlen($zip) <= 10
|
||||||
|
&& $city !== '' && mb_strlen($city) <= 100
|
||||||
|
&& filter_var($email, FILTER_VALIDATE_EMAIL) !== false
|
||||||
|
&& in_array($feeClass, $feeOptions, true)
|
||||||
|
&& ($_POST['statute'] ?? '') === '1'
|
||||||
|
&& ($_POST['privacy'] ?? '') === '1'
|
||||||
|
&& !preg_match('/[\r\n]/', $firstName . $lastName . $city . $street);
|
||||||
|
|
||||||
|
if (!$valid) {
|
||||||
|
$respond(false, 'validation');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Versand ---
|
||||||
|
$smtp = config('smtp');
|
||||||
|
|
||||||
|
$body = "Neuer Mitgliedsantrag (Fußball) über tsv08kulmbach.de\n"
|
||||||
|
. str_repeat('-', 40) . "\n"
|
||||||
|
. "Vorname: {$firstName}\n"
|
||||||
|
. "Nachname: {$lastName}\n"
|
||||||
|
. "Geboren am: {$birthDate}\n"
|
||||||
|
. "Adresse: {$street}, {$zip} {$city}\n"
|
||||||
|
. "Telefon: {$phone}\n"
|
||||||
|
. "E-Mail: {$email}\n"
|
||||||
|
. "Beitragsklasse: {$feeClass}\n"
|
||||||
|
. str_repeat('-', 40) . "\n"
|
||||||
|
. "Satzung anerkannt: ja\n"
|
||||||
|
. "Datenschutz: ja\n"
|
||||||
|
. 'Eingegangen: ' . date('d.m.Y H:i') . "\n\n"
|
||||||
|
. "Hinweis: Bankdaten werden online nicht erhoben – Beitrittserklärung\n"
|
||||||
|
. "per Post zur Unterschrift zusenden.\n";
|
||||||
|
|
||||||
|
$buildMailer = static function () use ($smtp): PHPMailer {
|
||||||
|
$mail = new PHPMailer(true);
|
||||||
|
$mail->isSMTP();
|
||||||
|
$mail->Host = $smtp['host'];
|
||||||
|
$mail->Port = (int) $smtp['port'];
|
||||||
|
$mail->SMTPAuth = true;
|
||||||
|
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
||||||
|
$mail->Username = $smtp['username'];
|
||||||
|
$mail->Password = $smtp['password'];
|
||||||
|
$mail->CharSet = PHPMailer::CHARSET_UTF8;
|
||||||
|
return $mail;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mail 1 (intern) — erfolgskritisch.
|
||||||
|
try {
|
||||||
|
$mail = $buildMailer();
|
||||||
|
$mail->setFrom($smtp['from'], $smtp['from_name']);
|
||||||
|
$mail->addAddress($smtp['to']);
|
||||||
|
$mail->addReplyTo($email, $firstName . ' ' . $lastName);
|
||||||
|
$mail->Subject = 'Mitgliedsantrag Fußball: ' . $firstName . ' ' . $lastName;
|
||||||
|
$mail->Body = $body;
|
||||||
|
$mail->send();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('[' . date('c') . '] Mitgliedsantrag-Versand fehlgeschlagen: ' . $e->getMessage() . "\n", 3, STORAGE_PATH . '/logs/mail.log');
|
||||||
|
$respond(false, 'mail');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mail 2 (Eingangsbestätigung an Antragsteller) — optional, Fehler nicht an Besucher leaken.
|
||||||
|
$welcome = $data['welcome_mail'] ?? null;
|
||||||
|
if (is_array($welcome) && !empty($welcome['subject']) && !empty($welcome['body'])) {
|
||||||
|
try {
|
||||||
|
$confirm = $buildMailer();
|
||||||
|
$confirm->setFrom($smtp['from'], $smtp['from_name']);
|
||||||
|
$confirm->addAddress($email, $firstName . ' ' . $lastName);
|
||||||
|
$confirm->Subject = $welcome['subject'];
|
||||||
|
$confirm->Body = $welcome['body'];
|
||||||
|
$confirm->send();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('[' . date('c') . '] Bestätigungsmail fehlgeschlagen: ' . $e->getMessage() . "\n", 3, STORAGE_PATH . '/logs/mail.log');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$respond(true);
|
||||||
@@ -20,22 +20,25 @@ if ($groups === []) {
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h2><?= e($title) ?></h2>
|
<h2><?= e($title) ?></h2>
|
||||||
<ul class="age-groups__grid">
|
<ul class="age-groups__grid">
|
||||||
<?php foreach ($groups as $g): ?>
|
<?php foreach ($groups as $g): $upcoming = !empty($g['upcoming']); ?>
|
||||||
<li class="age-group">
|
<li class="age-group<?= $upcoming ? ' age-group--upcoming' : '' ?>">
|
||||||
<div class="age-group__media">
|
<div class="age-group__media">
|
||||||
<?php if (!empty($g['photo']['base'])): ?>
|
<?php if (!empty($g['photo']['base'])): ?>
|
||||||
<?php component('img', ['image' => $g['photo'], 'sizes' => '(max-width: 600px) 100vw, 320px', 'class' => 'age-group__img']); ?>
|
<?php component('img', ['image' => $g['photo'], 'sizes' => '(max-width: 600px) 100vw, (max-width: 992px) 50vw, 560px', 'class' => 'age-group__img']); ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<img class="age-group__img" src="<?= e(asset('img/persons/platzhalter.jpg')) ?>" alt="" width="400" height="300" loading="lazy" decoding="async">
|
<img class="age-group__img" src="<?= e(asset('img/persons/platzhalter-spieler.jpg')) ?>" alt="" width="480" height="1047" loading="lazy" decoding="async">
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (!empty($g['code'])): ?>
|
<?php if ($upcoming): ?>
|
||||||
<span class="age-group__badge" aria-hidden="true"><?= e($g['code']) ?></span>
|
<span class="age-group__soon">In Entstehung</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="age-group__body">
|
<div class="age-group__body">
|
||||||
<h3 class="age-group__name"><?= e($g['name']) ?></h3>
|
<h3 class="age-group__name"><?= e($g['name']) ?></h3>
|
||||||
<?php if (!empty($g['training'])): ?>
|
<?php if (!empty($g['training'])): ?>
|
||||||
<p class="age-group__training"><?= icon('clock', 'age-group__icon') ?><span><?= e($g['training']) ?></span></p>
|
<p class="age-group__training">
|
||||||
|
<?= icon('clock', 'age-group__icon') ?>
|
||||||
|
<span><?php foreach ((array) $g['training'] as $i => $t): ?><?= $i ? '<br>' : '' ?><?= e($t) ?><?php endforeach; ?></span>
|
||||||
|
</p>
|
||||||
<?php elseif (!empty($g['note'])): ?>
|
<?php elseif (!empty($g['note'])): ?>
|
||||||
<p class="age-group__training text-muted"><?= e($g['note']) ?></p>
|
<p class="age-group__training text-muted"><?= e($g['note']) ?></p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -3,8 +3,13 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CTA-Button. Props: $cta array{label: string, slug: string, style?: 'primary'|'outline'}
|
* CTA-Button. Props: $cta array{label: string, slug: string,
|
||||||
|
* style?: 'primary'|'outline', size?: 'sm'|'lg'}
|
||||||
*/
|
*/
|
||||||
$style = $cta['style'] ?? 'primary';
|
$style = $cta['style'] ?? 'primary';
|
||||||
|
$size = $cta['size'] ?? null;
|
||||||
|
$class = 'btn'
|
||||||
|
. ($style === 'outline' ? ' btn--outline' : '')
|
||||||
|
. ($size === 'lg' ? ' btn--lg' : ($size === 'sm' ? ' btn--sm' : ''));
|
||||||
?>
|
?>
|
||||||
<a class="btn<?= $style === 'outline' ? ' btn--outline' : '' ?>" href="<?= e(url($cta['slug'])) ?>"><?= e($cta['label']) ?></a>
|
<a class="<?= e($class) ?>" href="<?= e(url($cta['slug'])) ?>"><?= e($cta['label']) ?></a>
|
||||||
|
|||||||
39
app/components/department-choice.php
Normal file
39
app/components/department-choice.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abteilungs-Auswahl: gleichwertige Karten, die auf Anker derselben Seite springen.
|
||||||
|
* Props:
|
||||||
|
* $choice array{title?, intro?, items: [{kicker?, title, text, label, anchor}]}
|
||||||
|
* $anchor string (optional, Sektions-ID)
|
||||||
|
* Bewusst eigenes <a href="#…">-Markup (nicht die cta-Komponente): cta() schickt
|
||||||
|
* jeden Slug durch url() und würde aus "#anker" ein "/#anker" (Startseite) machen.
|
||||||
|
*/
|
||||||
|
$anchor = $anchor ?? null;
|
||||||
|
if (empty($choice['items'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<section class="section department-choice"<?= $anchor ? ' id="' . e($anchor) . '"' : '' ?>>
|
||||||
|
<div class="container">
|
||||||
|
<?php if (!empty($choice['title'])): ?>
|
||||||
|
<h2 class="department-choice__title"><?= e($choice['title']) ?></h2>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($choice['intro'])): ?>
|
||||||
|
<p class="department-choice__intro"><?= e($choice['intro']) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<ul class="department-choice__grid" role="list">
|
||||||
|
<?php foreach ($choice['items'] as $item): ?>
|
||||||
|
<li class="department-choice__card">
|
||||||
|
<h3 class="department-choice__heading"><?= e($item['title']) ?></h3>
|
||||||
|
<?php if (!empty($item['subtitle'])): ?>
|
||||||
|
<p class="department-choice__subtitle"><?= e($item['subtitle']) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<p class="department-choice__text"><?= e($item['text']) ?></p>
|
||||||
|
<a class="btn department-choice__btn" href="#<?= e($item['anchor']) ?>"><?= e($item['label']) ?><?= icon('arrow-right', 'department-choice__arrow') ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
29
app/components/fee-table.php
Normal file
29
app/components/fee-table.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beitrags-/Preistabelle. Props:
|
||||||
|
* $fees array{title?, rows: [{label, price}], note?}
|
||||||
|
* Semantische Definitionsliste (Begriff → Betrag), responsiv ohne <table>-Layout-Zwang.
|
||||||
|
*/
|
||||||
|
if (empty($fees['rows'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="fee-table">
|
||||||
|
<?php if (!empty($fees['title'])): ?>
|
||||||
|
<h3 class="fee-table__title"><?= e($fees['title']) ?></h3>
|
||||||
|
<?php endif; ?>
|
||||||
|
<dl class="fee-table__list">
|
||||||
|
<?php foreach ($fees['rows'] as $row): ?>
|
||||||
|
<div class="fee-table__row">
|
||||||
|
<dt class="fee-table__label"><?= e($row['label']) ?></dt>
|
||||||
|
<dd class="fee-table__price"><?= e($row['price']) ?></dd>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</dl>
|
||||||
|
<?php if (!empty($fees['note'])): ?>
|
||||||
|
<p class="fee-table__note"><?= e($fees['note']) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
@@ -13,20 +13,28 @@ $nav = json_load('navigation');
|
|||||||
$socialLabels = ['instagram' => 'Instagram', 'facebook' => 'Facebook', 'youtube' => 'YouTube'];
|
$socialLabels = ['instagram' => 'Instagram', 'facebook' => 'Facebook', 'youtube' => 'YouTube'];
|
||||||
$socialIcons = ['instagram' => 'instagram', 'facebook' => 'facebook', 'youtube' => 'youtube'];
|
$socialIcons = ['instagram' => 'instagram', 'facebook' => 'facebook', 'youtube' => 'youtube'];
|
||||||
|
|
||||||
// Hauptnav flach ausrollen (Unterpunkte wie Matchcenter einreihen), Anker-Links raus.
|
// Hauptnav in Spalten gruppieren: jeder Hauptpunkt mit Unterpunkten wird eine
|
||||||
$footerNav = [];
|
// Footer-Spalte (Heading verlinkt die Übersicht, darunter die Kinder). Standalone-
|
||||||
|
// Hauptpunkte (ohne Kinder) und CTAs sammeln sich als „Mehr“-Spalte. Anker-Links raus.
|
||||||
|
$sections = [];
|
||||||
|
$extraLinks = [];
|
||||||
foreach ($nav['main'] as $item) {
|
foreach ($nav['main'] as $item) {
|
||||||
if (str_starts_with($item['slug'], '#')) {
|
if (str_starts_with($item['slug'], '#')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$footerNav[] = ['label' => $item['label'], 'slug' => $item['slug']];
|
$children = [];
|
||||||
foreach ($item['children'] ?? [] as $child) {
|
foreach ($item['children'] ?? [] as $child) {
|
||||||
if ($child['slug'] !== $item['slug']) { // „Übersicht“ dupliziert nur den Eltern-Slug
|
if ($child['slug'] !== $item['slug']) { // „Übersicht“ dupliziert nur den Eltern-Slug = Heading
|
||||||
$footerNav[] = $child;
|
$children[] = $child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($children) {
|
||||||
|
$sections[] = ['label' => $item['label'], 'slug' => $item['slug'], 'links' => $children];
|
||||||
|
} else {
|
||||||
|
$extraLinks[] = ['label' => $item['label'], 'slug' => $item['slug']];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$footerNav = array_merge($footerNav, $nav['ctas'] ?? []);
|
$extraLinks = array_merge($extraLinks, $nav['ctas'] ?? []);
|
||||||
?>
|
?>
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<div class="container site-footer__grid">
|
<div class="container site-footer__grid">
|
||||||
@@ -45,14 +53,18 @@ $footerNav = array_merge($footerNav, $nav['ctas'] ?? []);
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<nav aria-label="Footer-Navigation">
|
<?php foreach ($sections as $section): ?>
|
||||||
<h2 class="site-footer__heading">Seiten</h2>
|
<nav aria-label="<?= e($section['label']) ?>">
|
||||||
|
<h2 class="site-footer__heading">
|
||||||
|
<a href="<?= e(url($section['slug'])) ?>"><?= e($section['label']) ?></a>
|
||||||
|
</h2>
|
||||||
<ul class="site-footer__list">
|
<ul class="site-footer__list">
|
||||||
<?php foreach ($footerNav as $item): ?>
|
<?php foreach ($section['links'] as $item): ?>
|
||||||
<li><a href="<?= e(url($item['slug'])) ?>"><?= e($item['label']) ?></a></li>
|
<li><a href="<?= e(url($item['slug'])) ?>"><?= e($item['label']) ?></a></li>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
<?php endforeach; ?>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="site-footer__heading">Kontakt</h2>
|
<h2 class="site-footer__heading">Kontakt</h2>
|
||||||
<address class="site-footer__address">
|
<address class="site-footer__address">
|
||||||
@@ -67,19 +79,23 @@ $footerNav = array_merge($footerNav, $nav['ctas'] ?? []);
|
|||||||
<a href="mailto:<?= e($club['email']) ?>"><?= e($club['email']) ?></a>
|
<a href="mailto:<?= e($club['email']) ?>"><?= e($club['email']) ?></a>
|
||||||
</p>
|
</p>
|
||||||
</address>
|
</address>
|
||||||
</div>
|
<?php if ($extraLinks): ?>
|
||||||
<div>
|
<ul class="site-footer__list site-footer__list--extra">
|
||||||
<h2 class="site-footer__heading">Rechtliches</h2>
|
<?php foreach ($extraLinks as $item): ?>
|
||||||
<ul class="site-footer__list">
|
|
||||||
<?php foreach ($nav['legal'] as $item): ?>
|
|
||||||
<li><a href="<?= e(url($item['slug'])) ?>"><?= e($item['label']) ?></a></li>
|
<li><a href="<?= e(url($item['slug'])) ?>"><?= e($item['label']) ?></a></li>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</ul>
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="site-footer__bottom">
|
<div class="site-footer__bottom">
|
||||||
<div class="container site-footer__bottom-inner">
|
<div class="container site-footer__bottom-inner">
|
||||||
<p>© <?= e(date('Y')) ?> <?= e($club['legal_name']) ?></p>
|
<p>© <?= e(date('Y')) ?> <?= e($club['legal_name']) ?></p>
|
||||||
|
<ul class="site-footer__legal" aria-label="Rechtliches">
|
||||||
|
<?php foreach ($nav['legal'] as $item): ?>
|
||||||
|
<li><a href="<?= e(url($item['slug'])) ?>"><?= e($item['label']) ?></a></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
<p class="site-footer__memberships">Mitglied in: <?= e(implode(' · ', $club['memberships'])) ?></p>
|
<p class="site-footer__memberships">Mitglied in: <?= e(implode(' · ', $club['memberships'])) ?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ $nav = json_load('navigation');
|
|||||||
<div class="container site-header__inner">
|
<div class="container site-header__inner">
|
||||||
<a class="site-header__brand" href="<?= e(url()) ?>" aria-label="<?= e($club['name']) ?> – Startseite">
|
<a class="site-header__brand" href="<?= e(url()) ?>" aria-label="<?= e($club['name']) ?> – Startseite">
|
||||||
<img class="site-header__logo" src="<?= e(asset('img/logo.svg')) ?>" alt="" width="75" height="97">
|
<img class="site-header__logo" src="<?= e(asset('img/logo.svg')) ?>" alt="" width="75" height="97">
|
||||||
|
<span class="site-header__name" aria-hidden="true"><?= e($club['name']) ?></span>
|
||||||
</a>
|
</a>
|
||||||
<?php component('nav', ['current' => $current]); ?>
|
<?php component('nav', ['current' => $current]); ?>
|
||||||
<?php if (!empty($nav['ctas'])): ?>
|
<?php if (!empty($nav['ctas'])): ?>
|
||||||
|
|||||||
113
app/components/membership-form.php
Normal file
113
app/components/membership-form.php
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mitglieds-Antrag Fußball (schlank, ohne Bankdaten — die kommen auf dem Papierantrag).
|
||||||
|
* Props:
|
||||||
|
* $fee_options array Beitragsklassen als Strings (Whitelist – muss zur Action passen)
|
||||||
|
* Felder rendern ausschließlich über component('form-field') — siehe CLAUDE.md.
|
||||||
|
* Liest Flash-Status aus Query-Params (?sent=1 / ?error=…) — PRG-Muster.
|
||||||
|
* Spam-Schutz: Honeypot "website" + signierter Timestamp "ft" (form_token()).
|
||||||
|
*/
|
||||||
|
$club = json_load('club');
|
||||||
|
$fee_options = $fee_options ?? [];
|
||||||
|
|
||||||
|
$sent = isset($_GET['sent']);
|
||||||
|
$error = isset($_GET['error']) ? (string) $_GET['error'] : null;
|
||||||
|
$errorMessages = [
|
||||||
|
'validation' => 'Bitte prüfe deine Eingaben – Pflichtfelder fehlen oder die E-Mail-Adresse ist ungültig.',
|
||||||
|
'mail' => 'Dein Antrag konnte gerade nicht versendet werden. Bitte versuche es später erneut oder schreib uns direkt an ' . $club['email'] . '.',
|
||||||
|
];
|
||||||
|
|
||||||
|
$f = [
|
||||||
|
'first_name' => ['type' => 'text', 'name' => 'first_name', 'id' => 'mm-first-name', 'label' => 'Vorname', 'required' => true, 'autocomplete' => 'given-name', 'maxlength' => 100],
|
||||||
|
'last_name' => ['type' => 'text', 'name' => 'last_name', 'id' => 'mm-last-name', 'label' => 'Nachname', 'required' => true, 'autocomplete' => 'family-name', 'maxlength' => 100],
|
||||||
|
'birth_date' => ['type' => 'date', 'name' => 'birth_date', 'id' => 'mm-birth-date', 'label' => 'Geboren am', 'required' => true, 'autocomplete' => 'bday'],
|
||||||
|
'phone' => ['type' => 'tel', 'name' => 'phone', 'id' => 'mm-phone', 'label' => 'Telefonnummer', 'required' => true, 'autocomplete' => 'tel', 'maxlength' => 50],
|
||||||
|
'street' => ['type' => 'text', 'name' => 'street', 'id' => 'mm-street', 'label' => 'Straße & Hausnummer', 'required' => true, 'autocomplete' => 'street-address', 'maxlength' => 200],
|
||||||
|
'zip' => ['type' => 'text', 'name' => 'zip', 'id' => 'mm-zip', 'label' => 'Postleitzahl', 'required' => true, 'autocomplete' => 'postal-code', 'maxlength' => 10],
|
||||||
|
'city' => ['type' => 'text', 'name' => 'city', 'id' => 'mm-city', 'label' => 'Wohnort', 'required' => true, 'autocomplete' => 'address-level2', 'maxlength' => 100],
|
||||||
|
'email' => ['type' => 'email', 'name' => 'email', 'id' => 'mm-email', 'label' => 'E-Mail', 'required' => true, 'autocomplete' => 'email', 'maxlength' => 200],
|
||||||
|
'fee_class' => ['type' => 'select', 'name' => 'fee_class', 'id' => 'mm-fee-class', 'label' => 'Beitragsklasse', 'required' => true, 'options' => $fee_options, 'placeholder' => 'Bitte wählen'],
|
||||||
|
'statute' => [
|
||||||
|
'type' => 'checkbox', 'name' => 'statute', 'id' => 'mm-statute', 'required' => true,
|
||||||
|
'label' => 'Mit der Beitrittserklärung erkenne ich die Satzung des TSV 1908 Kulmbach e. V. an. Bei Kindern unter 18 Jahren ist die Unterschrift eines Erziehungsberechtigten erforderlich.',
|
||||||
|
'label_html' => 'Mit der Beitrittserklärung erkenne ich die Satzung des TSV 1908 Kulmbach e. V. an. Bei Kindern unter 18 Jahren ist die Unterschrift eines Erziehungsberechtigten erforderlich.',
|
||||||
|
],
|
||||||
|
'privacy' => [
|
||||||
|
'type' => 'checkbox', 'name' => 'privacy', 'id' => 'mm-privacy', 'required' => true,
|
||||||
|
'label' => 'Ich stimme den Datenschutzbestimmungen zu.',
|
||||||
|
'label_html' => 'Ich stimme den <a href="' . e(url('datenschutz')) . '">Datenschutzbestimmungen</a> zu.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
?>
|
||||||
|
<form class="form" method="post" action="<?= e(url('mitglied-werden-senden')) ?>" novalidate>
|
||||||
|
<div class="form__status" role="status" aria-live="polite" tabindex="-1" data-form-status>
|
||||||
|
<?php if ($sent): ?>
|
||||||
|
<p class="form__success">Danke! Dein Mitgliedsantrag ist bei uns eingegangen – wir melden uns und senden dir die Beitrittserklärung per Post zu.</p>
|
||||||
|
<?php elseif ($error !== null): ?>
|
||||||
|
<p class="form__error"><?= e($errorMessages[$error] ?? $errorMessages['mail']) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="form__note">Pflichtfelder sind mit <span class="form__required" aria-hidden="true">*</span><span class="visually-hidden">Stern</span> markiert.</p>
|
||||||
|
|
||||||
|
<fieldset class="form__group">
|
||||||
|
<legend class="form__legend">Zur Person</legend>
|
||||||
|
<div class="form__group-body">
|
||||||
|
<div class="form__row">
|
||||||
|
<?php component('form-field', ['field' => $f['first_name']]); ?>
|
||||||
|
<?php component('form-field', ['field' => $f['last_name']]); ?>
|
||||||
|
</div>
|
||||||
|
<?php component('form-field', ['field' => $f['birth_date']]); ?>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="form__group">
|
||||||
|
<legend class="form__legend">Adresse</legend>
|
||||||
|
<div class="form__group-body">
|
||||||
|
<?php component('form-field', ['field' => $f['street']]); ?>
|
||||||
|
<div class="form__row">
|
||||||
|
<?php component('form-field', ['field' => $f['zip']]); ?>
|
||||||
|
<?php component('form-field', ['field' => $f['city']]); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="form__group">
|
||||||
|
<legend class="form__legend">Kontakt</legend>
|
||||||
|
<div class="form__group-body">
|
||||||
|
<div class="form__row">
|
||||||
|
<?php component('form-field', ['field' => $f['phone']]); ?>
|
||||||
|
<?php component('form-field', ['field' => $f['email']]); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="form__group">
|
||||||
|
<legend class="form__legend">Mitgliedschaft</legend>
|
||||||
|
<div class="form__group-body">
|
||||||
|
<?php component('form-field', ['field' => $f['fee_class']]); ?>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="form__group">
|
||||||
|
<legend class="form__legend">Bestätigung</legend>
|
||||||
|
<div class="form__group-body">
|
||||||
|
<?php component('form-field', ['field' => $f['statute']]); ?>
|
||||||
|
<?php component('form-field', ['field' => $f['privacy']]); ?>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<?php /* Honeypot — für Menschen unsichtbar, Bots füllen es aus */ ?>
|
||||||
|
<div class="visually-hidden" aria-hidden="true">
|
||||||
|
<label for="mm-website">Website (bitte leer lassen)</label>
|
||||||
|
<input type="text" id="mm-website" name="website" tabindex="-1" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="ft" value="<?= e(form_token()) ?>">
|
||||||
|
|
||||||
|
<p class="form__submit">
|
||||||
|
<button class="btn" type="submit">Antrag absenden</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Visitenkarte für eine Person. Props:
|
* Visitenkarte für eine Person. Props:
|
||||||
* $person array{department, name, phone?, email?, image?}
|
* $person array{department, name, phone?, email?, image?, whatsapp?}
|
||||||
* Ohne image greift der Platzhalter-Avatar (img/persons/platzhalter.jpg).
|
* Ohne image greift der Platzhalter-Avatar (img/persons/platzhalter.jpg).
|
||||||
* Kontakt-Link: phone → tel:, sonst email → mailto:.
|
* Kontakt-Link: phone → tel:, sonst email → mailto:.
|
||||||
|
* Optional zusätzlich WhatsApp (wa.me) bei phone + whatsapp=true (opt-in pro Person).
|
||||||
*/
|
*/
|
||||||
$image = $person['image'] ?? null;
|
$image = $person['image'] ?? null;
|
||||||
$src = $image ?: 'img/persons/platzhalter.jpg';
|
$src = $image ?: 'img/persons/platzhalter.jpg';
|
||||||
@@ -22,6 +23,10 @@ if (!empty($person['phone'])) {
|
|||||||
} else {
|
} else {
|
||||||
$contactHref = null;
|
$contactHref = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$waHref = (!empty($person['whatsapp']) && !empty($person['phone']))
|
||||||
|
? 'https://wa.me/' . preg_replace('/\D/', '', $person['phone'])
|
||||||
|
: null;
|
||||||
?>
|
?>
|
||||||
<article class="person-card">
|
<article class="person-card">
|
||||||
<div class="person-card__media">
|
<div class="person-card__media">
|
||||||
@@ -36,6 +41,11 @@ if (!empty($person['phone'])) {
|
|||||||
<a class="person-card__contact" href="<?= e($contactHref) ?>">
|
<a class="person-card__contact" href="<?= e($contactHref) ?>">
|
||||||
<?= icon($contactIcon) ?><span><?= e($contactLabel) ?></span>
|
<?= icon($contactIcon) ?><span><?= e($contactLabel) ?></span>
|
||||||
</a>
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($waHref !== null): ?>
|
||||||
|
<a class="person-card__contact" href="<?= e($waHref) ?>" target="_blank" rel="noopener">
|
||||||
|
<?= icon('whatsapp') ?><span>WhatsApp</span>
|
||||||
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
63
app/pages/ansprechpartner.php
Normal file
63
app/pages/ansprechpartner.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ansprechpartner — Hero (Vereinsgelände) → zwei zusammengefasste person-grid-
|
||||||
|
* Sektionen: Fußball (Herren/Damen/Jugend) und Turnen, aus data/ansprechpartner.json.
|
||||||
|
*/
|
||||||
|
$page = json_load('ansprechpartner');
|
||||||
|
|
||||||
|
// Sektionen aus dem "group"-Feld der Personen ableiten (Single Source): eine
|
||||||
|
// Sektion je Gruppe (Fußball, Turnen), darin die jeweiligen Abteilungen/Disziplinen
|
||||||
|
// als Karten-Überschrift. Reihenfolge = JSON-Reihenfolge.
|
||||||
|
$groups = [];
|
||||||
|
foreach ($page['persons'] ?? [] as $p) {
|
||||||
|
$dept = $p['department'] ?? '';
|
||||||
|
if ($dept === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$label = $p['group'] ?? $dept;
|
||||||
|
if (!in_array($dept, $groups[$label] ?? [], true)) {
|
||||||
|
$groups[$label][] = $dept;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eindeutiger Abschnitts-Anker aus dem Bereichsnamen (kein Slug-Helper im Projekt).
|
||||||
|
$anchor_slug = static function (string $s): string {
|
||||||
|
$s = strtolower(strtr($s, ['ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue', 'ß' => 'ss']));
|
||||||
|
return trim((string) preg_replace('/[^a-z0-9]+/', '-', $s), '-');
|
||||||
|
};
|
||||||
|
|
||||||
|
$meta = [
|
||||||
|
'title' => 'Ansprechpartner',
|
||||||
|
'description' => 'Ansprechpartner und Kontaktwege beim TSV 1908 Kulmbach – Geschäftsstelle, Turnabteilung und Vorstand auf einen Blick.',
|
||||||
|
'schema' => page_schema([
|
||||||
|
['name' => 'Startseite', 'slug' => ''],
|
||||||
|
['name' => 'Der Verein', 'slug' => 'verein'],
|
||||||
|
['name' => 'Ansprechpartner', 'slug' => 'ansprechpartner'],
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
component('hero', ['hero' => [
|
||||||
|
'compact' => true,
|
||||||
|
'kicker' => 'Der Verein',
|
||||||
|
'title' => 'Ansprechpartner',
|
||||||
|
'text' => $page['intro'] ?? '',
|
||||||
|
'image' => [
|
||||||
|
'base' => 'img/pages/vereinsgelaende',
|
||||||
|
'widths' => [1000, 1600],
|
||||||
|
'alt' => '',
|
||||||
|
],
|
||||||
|
]]);
|
||||||
|
|
||||||
|
$i = 0;
|
||||||
|
foreach ($groups as $label => $depts):
|
||||||
|
component('person-grid', [
|
||||||
|
'departments' => $depts,
|
||||||
|
'title' => $label,
|
||||||
|
'intro' => '',
|
||||||
|
'anchor' => 'kontakt-' . $anchor_slug($label),
|
||||||
|
'tinted' => ($i % 2 === 0),
|
||||||
|
]);
|
||||||
|
$i++;
|
||||||
|
endforeach;
|
||||||
@@ -26,5 +26,9 @@ foreach ($home['sections'] as $section) {
|
|||||||
|
|
||||||
component('stats', ['stats' => $home['stats']]);
|
component('stats', ['stats' => $home['stats']]);
|
||||||
component('partner-grid', ['intro' => $home['partners']]);
|
component('partner-grid', ['intro' => $home['partners']]);
|
||||||
component('person-grid');
|
// Auf der Startseite bewusst nur die Haupt-Ansprechpartner je Bereich; die
|
||||||
|
// vollständige Turn-Disziplin-Liste lebt auf /ansprechpartner.
|
||||||
|
component('person-grid', [
|
||||||
|
'departments' => ['Herrenfußball', 'Damenfußball', 'Jugendfußball', 'Erwachsenenturnen'],
|
||||||
|
]);
|
||||||
component('contact-form', ['intro' => $home['contact']]);
|
component('contact-form', ['intro' => $home['contact']]);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
/**
|
/**
|
||||||
* Jugendfußball-Übersichtsseite. Inhalt: data/jugend.json.
|
* Jugendfußball-Übersichtsseite. Inhalt: data/jugend.json.
|
||||||
* Komposition: Hero → Intro-Bento (Bild, Mitmachen, Spielort, Altersklassen) →
|
* Komposition: Hero → Intro-Bento (Bild, Mitmachen, Spielort, Altersklassen) →
|
||||||
* Altersklassen-Karten → Ansprechpartner → CTA → FAQ.
|
* Altersklassen-Karten → FAQ + Ansprechpartner (faq-contact, Slider bei mehreren).
|
||||||
*/
|
*/
|
||||||
$page = json_load('jugend');
|
$page = json_load('jugend');
|
||||||
|
|
||||||
@@ -13,10 +13,17 @@ $club = json_load('club');
|
|||||||
$addr = $club['address'] ?? [];
|
$addr = $club['address'] ?? [];
|
||||||
$mapsQuery = rawurlencode(trim(($addr['venue'] ?? '') . ', ' . ($addr['street'] ?? '') . ', ' . ($addr['zip'] ?? '') . ' ' . ($addr['city'] ?? ''), ', '));
|
$mapsQuery = rawurlencode(trim(($addr['venue'] ?? '') . ', ' . ($addr['street'] ?? '') . ', ' . ($addr['zip'] ?? '') . ' ' . ($addr['city'] ?? ''), ', '));
|
||||||
|
|
||||||
|
// Ansprechpartner Jugendfußball aus der Single-Source (mehrere → Slider in faq-contact).
|
||||||
|
$contacts = array_values(array_filter(
|
||||||
|
json_load('ansprechpartner')['persons'] ?? [],
|
||||||
|
static fn (array $p): bool => ($p['department'] ?? '') === 'Jugendfußball'
|
||||||
|
));
|
||||||
|
|
||||||
$meta = [
|
$meta = [
|
||||||
'title' => 'Jugendfußball in Kulmbach',
|
'title' => 'Jugendfußball in Kulmbach',
|
||||||
'description' => 'Jugendfußball in Kulmbach – entdecke unsere Mannschaften von der G- bis zur C-Jugend, Betreuer und Trainingszeiten. Probiere Fußball in familiärer Atmosphäre aus.',
|
'description' => 'Jugendfußball in Kulmbach – entdecke unsere Mannschaften von der G- bis zur C-Jugend, Betreuer und Trainingszeiten. Probiere Fußball in familiärer Atmosphäre aus.',
|
||||||
'og_image' => 'img/pages/jugendfussball-hero-1440.jpg',
|
'og_image' => 'img/pages/jugendfussball-hero-1440.jpg',
|
||||||
|
'scripts' => count($contacts) > 1 ? ['contact-slider.js'] : [],
|
||||||
'schema' => page_schema(
|
'schema' => page_schema(
|
||||||
[
|
[
|
||||||
['name' => 'Startseite', 'slug' => ''],
|
['name' => 'Startseite', 'slug' => ''],
|
||||||
@@ -61,20 +68,10 @@ component('hero', ['hero' => $page['hero']]);
|
|||||||
<?php
|
<?php
|
||||||
component('age-groups', ['groups' => $page['groups'] ?? []]);
|
component('age-groups', ['groups' => $page['groups'] ?? []]);
|
||||||
|
|
||||||
component('person-grid', [
|
component('faq-contact', [
|
||||||
'departments' => ['Jugendfußball'],
|
'faq' => $page['faq'] ?? [],
|
||||||
'title' => 'Ansprechpartner Jugendfußball',
|
'faqIntro' => 'Ruf uns direkt an oder schreib uns unkompliziert per WhatsApp – wir beantworten alle Fragen rund um Schnuppertraining und die passende Altersgruppe.',
|
||||||
'intro' => 'Fragen zum Schnuppertraining oder zur passenden Altersgruppe? Melde dich direkt.',
|
'persons' => $contacts,
|
||||||
'anchor' => 'kontakt',
|
'contactTitle' => '',
|
||||||
|
'anchor' => 'kontakt',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
component('cta-band', ['band' => [
|
|
||||||
'title' => 'Neugierig geworden?',
|
|
||||||
'text' => 'Komm mit deinem Kind zum kostenlosen Schnuppertraining – ganz unverbindlich.',
|
|
||||||
'ctas' => [
|
|
||||||
['label' => 'Zum Ansprechpartner', 'slug' => 'jugendfussball#kontakt', 'style' => 'primary'],
|
|
||||||
['label' => 'Mitglied werden', 'slug' => 'mitglied-werden', 'style' => 'outline'],
|
|
||||||
],
|
|
||||||
]]);
|
|
||||||
|
|
||||||
component('faq', ['faq' => $page['faq'] ?? []]);
|
|
||||||
|
|||||||
82
app/pages/mitglied-werden.php
Normal file
82
app/pages/mitglied-werden.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mitglied werden (Hub). Inhalt: data/mitglied-werden.json + data/club.json.
|
||||||
|
* Struktur: Hero → Abteilungs-Auswahl → Info-Band → Fußball (Beiträge + Online-Formular)
|
||||||
|
* → Turnen (Hinweise + Beiträge + PDF) → FAQ.
|
||||||
|
*/
|
||||||
|
$page = json_load('mitglied-werden');
|
||||||
|
$club = json_load('club');
|
||||||
|
|
||||||
|
// Beitragsklassen für das Formular aus der Beitragstabelle ableiten (Single Source).
|
||||||
|
// Muss identisch in app/actions/membership-submit.php gebildet werden (Whitelist).
|
||||||
|
$feeOptions = array_map(
|
||||||
|
static fn (array $r): string => $r['label'] . ' — ' . $r['price'],
|
||||||
|
$page['fussball']['fees']['rows'] ?? []
|
||||||
|
);
|
||||||
|
|
||||||
|
$meta = [
|
||||||
|
'title' => 'Mitglied werden',
|
||||||
|
'description' => 'Mitglied werden beim TSV 08 Kulmbach: Fußball bequem online anmelden oder Turnen per Aufnahmeantrag. Alle Beiträge, Aufnahme und Infos für deinen Vereinsbeitritt in Kulmbach.',
|
||||||
|
'og_image' => 'img/hero/hero-1600.jpg',
|
||||||
|
'scripts' => ['form.js'],
|
||||||
|
'schema' => page_schema(
|
||||||
|
[
|
||||||
|
['name' => 'Startseite', 'slug' => ''],
|
||||||
|
['name' => 'Mitglied werden', 'slug' => 'mitglied-werden'],
|
||||||
|
],
|
||||||
|
$page['faq'] ?? []
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
component('hero', ['hero' => $page['hero']]);
|
||||||
|
|
||||||
|
component('department-choice', ['choice' => $page['choice'], 'anchor' => 'abteilung']);
|
||||||
|
?>
|
||||||
|
<section class="section membership-signup" id="anmeldung-fussball">
|
||||||
|
<div class="container membership-signup__inner">
|
||||||
|
<header class="membership-signup__head">
|
||||||
|
<h2><?= e($page['fussball']['title']) ?></h2>
|
||||||
|
<p class="membership-signup__intro"><?= e($page['fussball']['intro']) ?></p>
|
||||||
|
</header>
|
||||||
|
<div class="membership-card">
|
||||||
|
<?php component('fee-table', ['fees' => $page['fussball']['fees']]); ?>
|
||||||
|
<?php component('membership-form', ['fee_options' => $feeOptions]); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section section--tinted membership-signup" id="anmeldung-turnen">
|
||||||
|
<div class="container membership-signup__inner">
|
||||||
|
<header class="membership-signup__head">
|
||||||
|
<h2><?= e($page['turnen']['title']) ?></h2>
|
||||||
|
<p class="membership-signup__intro"><?= e($page['turnen']['intro']) ?></p>
|
||||||
|
</header>
|
||||||
|
<?php if (!empty($page['turnen']['notes'])): ?>
|
||||||
|
<ul class="membership-notes" role="list">
|
||||||
|
<?php foreach ($page['turnen']['notes'] as $note): ?>
|
||||||
|
<li class="membership-notes__item">
|
||||||
|
<?php if (!empty($note['icon'])): ?>
|
||||||
|
<span class="membership-notes__icon"><?= icon($note['icon']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="membership-notes__body">
|
||||||
|
<h3 class="membership-notes__title"><?= e($note['title']) ?></h3>
|
||||||
|
<p><?= e($note['text']) ?></p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="membership-download">
|
||||||
|
<a class="btn membership-download__btn" href="<?= e(asset($page['turnen']['download']['file'])) ?>" download>
|
||||||
|
<?= icon('download', 'membership-download__icon') ?><?= e($page['turnen']['download']['label']) ?>
|
||||||
|
</a>
|
||||||
|
<p class="membership-download__hint"><?= e($page['turnen']['download']['hint']) ?></p>
|
||||||
|
<p class="membership-download__contact">Fragen zur Turnabteilung? Schreib an <a href="mailto:<?= e($club['email_turnen']) ?>"><?= e($club['email_turnen']) ?></a>.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php
|
||||||
|
component('faq', ['faq' => $page['faq'] ?? []]);
|
||||||
46
app/pages/partner-werden.php
Normal file
46
app/pages/partner-werden.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Partner werden — Platzhalter/Stub für Sponsoring/Partnerschaft. Pakete und
|
||||||
|
* Konditionen folgen; Kontaktweg über data/club.json (Single Source of Truth).
|
||||||
|
*/
|
||||||
|
$club = json_load('club');
|
||||||
|
|
||||||
|
$meta = [
|
||||||
|
'title' => 'Partner werden',
|
||||||
|
'description' => 'Werde Partner oder Sponsor des TSV 1908 Kulmbach und unterstütze den Sport im Katzbachtal – so kommst du mit uns ins Gespräch.',
|
||||||
|
'schema' => page_schema([
|
||||||
|
['name' => 'Startseite', 'slug' => ''],
|
||||||
|
['name' => 'Der Verein', 'slug' => 'verein'],
|
||||||
|
['name' => 'Partner werden', 'slug' => 'partner-werden'],
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
?>
|
||||||
|
<section class="section legal">
|
||||||
|
<div class="container">
|
||||||
|
<div class="legal__head">
|
||||||
|
<p class="legal__kicker">Der Verein</p>
|
||||||
|
<h1>Partner werden</h1>
|
||||||
|
<p class="legal__lead">Unterstütze den TSV 08 Kulmbach und werde sichtbar – als Partner mitten im Vereinsleben.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legal__body">
|
||||||
|
<section>
|
||||||
|
<h2>Sponsoring & Partnerschaft</h2>
|
||||||
|
<p>Ob Bandenwerbung, Trikotsponsoring oder Unterstützung einzelner Abteilungen – wir stellen dir gern die passenden Möglichkeiten vor. Eine Übersicht der Partnerpakete und Konditionen ist in Vorbereitung.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Kontakt aufnehmen</h2>
|
||||||
|
<div class="legal__card">
|
||||||
|
<p>
|
||||||
|
<strong><?= e($club['name']) ?></strong><br>
|
||||||
|
E-Mail: <a href="mailto:<?= e($club['email']) ?>"><?= e($club['email']) ?></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
46
app/pages/sportheimbuchung.php
Normal file
46
app/pages/sportheimbuchung.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sportheimbuchung — Platzhalter/Stub. Inhalt (Verfügbarkeiten, Buchungsablauf,
|
||||||
|
* Konditionen) folgt; Kontaktweg über data/club.json (Single Source of Truth).
|
||||||
|
*/
|
||||||
|
$club = json_load('club');
|
||||||
|
|
||||||
|
$meta = [
|
||||||
|
'title' => 'Sportheim mieten & buchen',
|
||||||
|
'description' => 'Das Sportheim des TSV 1908 Kulmbach für Feiern und Veranstaltungen mieten – Ansprechpartner und Buchungsablauf.',
|
||||||
|
'schema' => page_schema([
|
||||||
|
['name' => 'Startseite', 'slug' => ''],
|
||||||
|
['name' => 'Der Verein', 'slug' => 'verein'],
|
||||||
|
['name' => 'Sportheimbuchung', 'slug' => 'sportheimbuchung'],
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
?>
|
||||||
|
<section class="section legal">
|
||||||
|
<div class="container">
|
||||||
|
<div class="legal__head">
|
||||||
|
<p class="legal__kicker">Der Verein</p>
|
||||||
|
<h1>Sportheim mieten & buchen</h1>
|
||||||
|
<p class="legal__lead">Unser Sportheim auf der <?= e($club['address']['venue']) ?> lässt sich für Feiern und Veranstaltungen mieten.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legal__body">
|
||||||
|
<section>
|
||||||
|
<h2>Buchung & Verfügbarkeit</h2>
|
||||||
|
<p>Die Online-Buchung mit Belegungskalender und Konditionen befindet sich gerade in Vorbereitung. Bis dahin nimm bitte direkt Kontakt mit uns auf – wir klären Termin, Ausstattung und Preise unkompliziert mit dir.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Ansprechpartner</h2>
|
||||||
|
<div class="legal__card">
|
||||||
|
<p>
|
||||||
|
<strong><?= e($club['name']) ?></strong><br>
|
||||||
|
E-Mail: <a href="mailto:<?= e($club['email']) ?>"><?= e($club['email']) ?></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -25,6 +25,10 @@ return [
|
|||||||
'turnen/wettkampfturnen' => ['file' => 'turnen-wettkampfturnen.php', 'sitemap' => true],
|
'turnen/wettkampfturnen' => ['file' => 'turnen-wettkampfturnen.php', 'sitemap' => true],
|
||||||
'verein' => ['file' => 'verein.php', 'sitemap' => true],
|
'verein' => ['file' => 'verein.php', 'sitemap' => true],
|
||||||
'historie' => ['file' => 'historie.php', 'sitemap' => true],
|
'historie' => ['file' => 'historie.php', 'sitemap' => true],
|
||||||
|
'sportheimbuchung' => ['file' => 'sportheimbuchung.php', 'sitemap' => true],
|
||||||
|
'ansprechpartner' => ['file' => 'ansprechpartner.php', 'sitemap' => true],
|
||||||
|
'partner-werden' => ['file' => 'partner-werden.php', 'sitemap' => true],
|
||||||
|
'mitglied-werden' => ['file' => 'mitglied-werden.php', 'sitemap' => true],
|
||||||
'impressum' => ['file' => 'impressum.php', 'sitemap' => true],
|
'impressum' => ['file' => 'impressum.php', 'sitemap' => true],
|
||||||
'datenschutz' => ['file' => 'datenschutz.php', 'sitemap' => true],
|
'datenschutz' => ['file' => 'datenschutz.php', 'sitemap' => true],
|
||||||
'404' => ['file' => '404.php', 'sitemap' => false],
|
'404' => ['file' => '404.php', 'sitemap' => false],
|
||||||
|
|||||||
@@ -3,39 +3,79 @@
|
|||||||
"intro": "Du hast Fragen zu einer Abteilung? Unsere Ansprechpartner helfen dir direkt weiter.",
|
"intro": "Du hast Fragen zu einer Abteilung? Unsere Ansprechpartner helfen dir direkt weiter.",
|
||||||
"persons": [
|
"persons": [
|
||||||
{
|
{
|
||||||
|
"group": "Fußball",
|
||||||
"department": "Herrenfußball",
|
"department": "Herrenfußball",
|
||||||
"name": "Benedikt Höfner",
|
"name": "Benedikt Höfner",
|
||||||
"phone": "+49 160 97516404",
|
"phone": "+49 160 97516404",
|
||||||
|
"whatsapp": true,
|
||||||
"image": "img/persons/benedikt-hoefner.jpg"
|
"image": "img/persons/benedikt-hoefner.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"group": "Fußball",
|
||||||
"department": "Herrenfußball",
|
"department": "Herrenfußball",
|
||||||
"name": "Rustem Hajzeraj",
|
"name": "Rustem Hajzeraj",
|
||||||
|
"phone": "+49 171 1886358",
|
||||||
|
"whatsapp": true,
|
||||||
"image": "img/persons/rustem-hajzeraj.jpg"
|
"image": "img/persons/rustem-hajzeraj.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"group": "Fußball",
|
||||||
"department": "Damenfußball",
|
"department": "Damenfußball",
|
||||||
"name": "Sam Kern",
|
"name": "Sam Kern",
|
||||||
"phone": "+49 176 91371064",
|
"phone": "+49 176 91371064",
|
||||||
|
"whatsapp": true,
|
||||||
"image": "img/persons/sam-kern.jpg"
|
"image": "img/persons/sam-kern.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"group": "Fußball",
|
||||||
"department": "Jugendfußball",
|
"department": "Jugendfußball",
|
||||||
"name": "Fabian Früh",
|
"name": "Fabian Früh",
|
||||||
"phone": "+49 160 97516797",
|
"phone": "+49 160 97516797",
|
||||||
|
"whatsapp": true,
|
||||||
"image": "img/persons/fabian-frueh.jpg"
|
"image": "img/persons/fabian-frueh.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"group": "Fußball",
|
||||||
"department": "Jugendfußball",
|
"department": "Jugendfußball",
|
||||||
"name": "Kerstin Tauber",
|
"name": "Kerstin Tauber",
|
||||||
"phone": "+49 176 38957526",
|
"phone": "+49 176 38957526",
|
||||||
|
"whatsapp": true,
|
||||||
"image": null
|
"image": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"department": "Turnen",
|
"group": "Turnen",
|
||||||
|
"department": "Erwachsenenturnen",
|
||||||
"name": "Maxi Roßberg",
|
"name": "Maxi Roßberg",
|
||||||
"email": "turnabteilung.tsv08kulmbach@gmail.com",
|
"email": "turnabteilung.tsv08kulmbach@gmail.com",
|
||||||
"image": null
|
"image": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Turnen",
|
||||||
|
"department": "Eltern-Kind- & Kinderturnen",
|
||||||
|
"name": "Annelie Frisque",
|
||||||
|
"email": "turnabteilung.tsv08kulmbach@gmail.com",
|
||||||
|
"image": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Turnen",
|
||||||
|
"department": "Step Aerobic, Fitness & Pilates",
|
||||||
|
"name": "Karola Lorz",
|
||||||
|
"email": "turnabteilung.tsv08kulmbach@gmail.com",
|
||||||
|
"image": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Turnen",
|
||||||
|
"department": "Trampolinturnen",
|
||||||
|
"name": "Tina Kornatz",
|
||||||
|
"email": "turnabteilung.tsv08kulmbach@gmail.com",
|
||||||
|
"image": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Turnen",
|
||||||
|
"department": "Wettkampfturnen",
|
||||||
|
"name": "Franziska Trotzke",
|
||||||
|
"email": "turnabteilung.tsv08kulmbach@gmail.com",
|
||||||
|
"image": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,14 @@
|
|||||||
{
|
{
|
||||||
"label": "Jugendfußball",
|
"label": "Jugendfußball",
|
||||||
"slug": "jugendfussball",
|
"slug": "jugendfussball",
|
||||||
"style": "primary"
|
"style": "primary",
|
||||||
|
"size": "lg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Mitglied werden",
|
"label": "Mitglied werden",
|
||||||
"slug": "mitglied-werden",
|
"slug": "mitglied-werden",
|
||||||
"style": "outline"
|
"style": "outline",
|
||||||
|
"size": "lg"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"display": true,
|
"display": true,
|
||||||
|
|||||||
@@ -17,11 +17,13 @@
|
|||||||
"text": "Wenn deine Kinder Lust haben, bei uns Fußball auszuprobieren oder mitzuspielen, meldet euch gerne ganz unkompliziert. Wir freuen uns über jedes neue Gesicht und bieten Fußball in familiärer Atmosphäre – vom ersten Ballkontakt bis zum echten Mannschaftsspiel."
|
"text": "Wenn deine Kinder Lust haben, bei uns Fußball auszuprobieren oder mitzuspielen, meldet euch gerne ganz unkompliziert. Wir freuen uns über jedes neue Gesicht und bieten Fußball in familiärer Atmosphäre – vom ersten Ballkontakt bis zum echten Mannschaftsspiel."
|
||||||
},
|
},
|
||||||
"groups": [
|
"groups": [
|
||||||
{ "code": "G", "name": "G-Jugend (Bambini)", "training": "Freitag 16:30 Uhr", "photo": { "base": "img/teams/jugend-g", "widths": [400, 800], "alt": "G-Jugend des TSV 08 Kulmbach" } },
|
{ "code": "G", "name": "G-Jugend (Bambini)", "training": ["Freitag 16:30–17:30 Uhr"], "photo": { "base": "img/teams/jugend-g", "widths": [400, 800], "alt": "G-Jugend des TSV 08 Kulmbach" } },
|
||||||
{ "code": "F", "name": "F-Jugend", "training": "Freitag 16:30 Uhr", "photo": { "base": "img/teams/jugend-f", "widths": [400, 800], "alt": "F-Jugend des TSV 08 Kulmbach" } },
|
{ "code": "F", "name": "F-Jugend", "training": ["Montag 17:00–18:30 Uhr", "Freitag 16:30–17:30 Uhr"], "photo": { "base": "img/teams/jugend-f", "widths": [400, 800], "alt": "F-Jugend des TSV 08 Kulmbach" } },
|
||||||
{ "code": "E", "name": "E-Jugend", "note": "Trainingszeit auf Anfrage", "photo": { "base": "img/teams/jugend-e", "widths": [400, 800], "alt": "E-Jugend des TSV 08 Kulmbach" } },
|
{ "code": "E", "name": "E-Jugend", "training": ["Montag 17:30–18:30 Uhr", "Mittwoch 17:30–19:00 Uhr"], "photo": { "base": "img/teams/jugend-e", "widths": [400, 800], "alt": "E-Jugend des TSV 08 Kulmbach" } },
|
||||||
{ "code": "D", "name": "D-Jugend", "note": "Trainingszeit auf Anfrage", "photo": { "base": "img/teams/jugend-d", "widths": [400, 800], "alt": "D-Jugend des TSV 08 Kulmbach" } },
|
{ "code": "D", "name": "D-Jugend", "training": ["Dienstag 17:30–18:30 Uhr", "Freitag 17:30–18:30 Uhr"], "photo": { "base": "img/teams/jugend-d", "widths": [400, 800], "alt": "D-Jugend des TSV 08 Kulmbach" } },
|
||||||
{ "code": "C", "name": "C-Jugend", "note": "Trainingszeit auf Anfrage", "photo": { "base": "img/teams/jugend-c", "widths": [400, 800], "alt": "C-Jugend des TSV 08 Kulmbach" } }
|
{ "code": "C", "name": "C-Jugend", "training": ["Montag 18:30–19:30 Uhr", "Freitag 17:30–19:00 Uhr"], "photo": { "base": "img/teams/jugend-c", "widths": [400, 800], "alt": "C-Jugend des TSV 08 Kulmbach" } },
|
||||||
|
{ "code": "B", "name": "B-Jugend", "upcoming": true, "note": "Mannschaft im Aufbau – Infos folgen.", "photo": { "base": "img/pages/fussball-hero", "widths": [640, 1000, 1600], "alt": "" } },
|
||||||
|
{ "code": "A", "name": "A-Jugend", "upcoming": true, "note": "Mannschaft im Aufbau – Infos folgen.", "photo": { "base": "img/pages/fussball-hero", "widths": [640, 1000, 1600], "alt": "" } }
|
||||||
],
|
],
|
||||||
"faq": [
|
"faq": [
|
||||||
{ "q": "Ab welchem Alter können Kinder mitspielen?", "a": "Vom Bambini-Alter (G-Jugend) bis zur C-Jugend ist bei uns alles dabei. Am besten kommt ihr einfach zum Schnuppertraining der passenden Altersgruppe vorbei." },
|
{ "q": "Ab welchem Alter können Kinder mitspielen?", "a": "Vom Bambini-Alter (G-Jugend) bis zur C-Jugend ist bei uns alles dabei. Am besten kommt ihr einfach zum Schnuppertraining der passenden Altersgruppe vorbei." },
|
||||||
|
|||||||
84
data/mitglied-werden.json
Normal file
84
data/mitglied-werden.json
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"hero": {
|
||||||
|
"compact": true,
|
||||||
|
"align": "bottom-left",
|
||||||
|
"kicker": "TSV 1908 Kulmbach e. V.",
|
||||||
|
"title": "Mitglied werden",
|
||||||
|
"text": "Werde Teil der über 100-jährigen rot-schwarzen Tradition. Dein Mitgliedsbeitrag hält den Verein lebendig – egal ob du auf dem Platz im Katzbachtal kickst oder in der Turnhalle aktiv wirst. Wähle deine Abteilung und mach den ersten Schritt.",
|
||||||
|
"image": {
|
||||||
|
"base": "img/hero/hero",
|
||||||
|
"widths": [640, 1000, 1600],
|
||||||
|
"alt": "Mitglieder des TSV 08 Kulmbach auf der Hans-Rausch-Sportanlage im Katzbachtal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"choice": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Abteilung Fußball",
|
||||||
|
"subtitle": "Online-Anmeldung",
|
||||||
|
"text": "Schnelle Anmeldung direkt über unser Online-Formular – Herren, Damen und Jugend.",
|
||||||
|
"label": "Zur Online-Anmeldung",
|
||||||
|
"anchor": "anmeldung-fussball"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Abteilung Turnen",
|
||||||
|
"subtitle": "PDF-Antrag beim Übungsleiter",
|
||||||
|
"text": "Aufnahmeantrag herunterladen, ausfüllen und beim Übungsleiter abgeben.",
|
||||||
|
"label": "Infos & Download",
|
||||||
|
"anchor": "anmeldung-turnen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fussball": {
|
||||||
|
"title": "Anmeldung Fußball",
|
||||||
|
"intro": "Fülle das Formular aus – wir senden dir anschließend die Beitrittserklärung per Post zu, die du unterschrieben zurückschickst.",
|
||||||
|
"fees": {
|
||||||
|
"title": "Jahresbeiträge Fußball",
|
||||||
|
"rows": [
|
||||||
|
{ "label": "Kinder & Jugendliche (bis 18 Jahre)", "price": "60 €" },
|
||||||
|
{ "label": "Erwachsene", "price": "70 €" },
|
||||||
|
{ "label": "Senioren (ab 65 Jahre)", "price": "60 €" },
|
||||||
|
{ "label": "Familien (ab 3 Personen)", "price": "136 €" }
|
||||||
|
],
|
||||||
|
"note": "Pro Jahr, inkl. Vereins- und Abteilungsbeitrag, Verbandsabgaben und Sportunfall-Versicherung. Keine Aufnahmegebühr."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"turnen": {
|
||||||
|
"title": "Anmeldung Turnabteilung",
|
||||||
|
"intro": "Die Anmeldung läuft über den Aufnahmeantrag als PDF: herunterladen, ausfüllen und beim Übungsleiter abgeben.",
|
||||||
|
"notes": [
|
||||||
|
{
|
||||||
|
"icon": "check-circle",
|
||||||
|
"title": "Platz vorab prüfen",
|
||||||
|
"text": "Frag vorab beim Übungsleiter, ob in deiner Wunschgruppe ein Platz frei ist – eine Anmeldung garantiert keinen Platz."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "file-earmark-text",
|
||||||
|
"title": "Pro Person ein Antrag",
|
||||||
|
"text": "Für jede Person bitte ein eigenes, vollständig ausgefülltes PDF einreichen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "info-circle",
|
||||||
|
"title": "Auch passiv möglich",
|
||||||
|
"text": "Eine passive Mitgliedschaft („stilles Mitglied“) ist jederzeit möglich – auch ohne Platz in einer aktiven Gruppe."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"download": {
|
||||||
|
"label": "Aufnahmeantrag Turnen (PDF) herunterladen",
|
||||||
|
"file": "downloads/beitrittserklaerung-turnen.pdf",
|
||||||
|
"hint": "Den Antrag bitte ausdrucken und beim Übungsleiter abgeben."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"faq": [
|
||||||
|
{ "q": "Was kostet die Mitgliedschaft beim TSV 08 Kulmbach?", "a": "Der Jahresbeitrag für die Abteilung Fußball beträgt 70 € für Erwachsene, 60 € für Kinder, Jugendliche und Senioren (ab 65) sowie 136 € für Familien ab drei Personen. Den aktuellen Beitrag der Turnabteilung erfährst du beim Übungsleiter bzw. im Aufnahmeantrag." },
|
||||||
|
{ "q": "Gibt es eine Aufnahmegebühr?", "a": "Nein. Eine Aufnahmegebühr erheben wir nicht. Im Jahresbeitrag sind Vereins- und Abteilungsbeitrag, Verbandsabgaben und die Sportunfall-Versicherung bereits enthalten." },
|
||||||
|
{ "q": "Wie melde ich mich für Fußball an?", "a": "Über das Online-Formular auf dieser Seite. Nach Eingang senden wir dir die unterschriftsreife Beitrittserklärung per Post zu, die du unterschrieben an uns zurückschickst." },
|
||||||
|
{ "q": "Wie melde ich mich für Turnen an?", "a": "Für die Turnabteilung lädst du den Aufnahmeantrag als PDF herunter, füllst ihn aus und gibst ihn beim jeweiligen Übungsleiter ab. Bitte prüfe vorab beim Übungsleiter, ob in deiner Wunschgruppe ein Platz frei ist." },
|
||||||
|
{ "q": "Kann ich unverbindlich zum Probetraining kommen?", "a": "Ja. Komm gern zum Probetraining der passenden Mannschaft oder Turngruppe vorbei und sprich einfach den jeweiligen Ansprechpartner an, bevor du Mitglied wirst." },
|
||||||
|
{ "q": "Wie kann ich meine Mitgliedschaft kündigen?", "a": "Der Austritt ist nur zum Ende eines Kalenderjahres möglich und muss bis spätestens 30. September schriftlich erklärt werden. Der Beitrag ist in jedem Fall bis zum Jahresende zu entrichten." }
|
||||||
|
],
|
||||||
|
"welcome_mail": {
|
||||||
|
"subject": "Dein Mitgliedsantrag beim TSV 08 Kulmbach",
|
||||||
|
"body": "Herzlich willkommen in der 08er-Familie!\n\nDein Mitgliedsantrag ist erfolgreich bei uns eingegangen. Wir bearbeiten ihn so schnell wie möglich und senden dir die Beitrittserklärung anschließend per Post zu.\n\nBitte schicke uns die Erklärung dann unterschrieben zurück.\n\nWir freuen uns auf dich!\n\nSportliche Grüße\nTSV 08 Kulmbach"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,10 @@
|
|||||||
"slug": "verein",
|
"slug": "verein",
|
||||||
"children": [
|
"children": [
|
||||||
{ "label": "Übersicht", "slug": "verein" },
|
{ "label": "Übersicht", "slug": "verein" },
|
||||||
{ "label": "Historie", "slug": "historie" }
|
{ "label": "Historie", "slug": "historie" },
|
||||||
|
{ "label": "Ansprechpartner", "slug": "ansprechpartner" },
|
||||||
|
{ "label": "Sportheimbuchung", "slug": "sportheimbuchung" },
|
||||||
|
{ "label": "Partner werden", "slug": "partner-werden" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -607,20 +607,30 @@
|
|||||||
color: var(--clr-text-muted);
|
color: var(--clr-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Max. 3 pro Reihe; unvollständige Reihen (z. B. 3+2) zentrieren sich automatisch */
|
/* Kurze Hairline unter der Bereichs-Überschrift */
|
||||||
|
.persons h2::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2px;
|
||||||
|
margin: 0.6rem auto 0;
|
||||||
|
background: var(--clr-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Max. 5 pro Reihe; unvollständige Reihen zentrieren sich automatisch */
|
||||||
.persons__grid {
|
.persons__grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 1.5rem;
|
gap: 1.25rem;
|
||||||
margin-top: 2.5rem;
|
margin-top: 2.25rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.persons__grid > li {
|
.persons__grid > li {
|
||||||
flex: 0 1 calc((100% - 3rem) / 3);
|
flex: 0 1 calc((100% - 5rem) / 5);
|
||||||
min-width: 220px;
|
min-width: 170px;
|
||||||
max-width: 340px;
|
max-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.person-card {
|
.person-card {
|
||||||
@@ -655,6 +665,15 @@
|
|||||||
background: linear-gradient(180deg, rgb(20 20 20 / 0) 0%, rgb(20 20 20 / 0.35) 100%);
|
background: linear-gradient(180deg, rgb(20 20 20 / 0) 0%, rgb(20 20 20 / 0.35) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Kompaktere Kacheln im Ansprechpartner-Grid (faq-contact bleibt unberührt) */
|
||||||
|
.persons .person-card__media img {
|
||||||
|
aspect-ratio: 4 / 4.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persons .person-card__body {
|
||||||
|
padding: 0.85rem 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.person-card__department {
|
.person-card__department {
|
||||||
font-size: var(--fs-650);
|
font-size: var(--fs-650);
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
@@ -675,6 +694,11 @@
|
|||||||
font-size: var(--fs-500);
|
font-size: var(--fs-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Zweiter Kontakt-Link (z. B. WhatsApp) sitzt enger unter dem ersten */
|
||||||
|
.person-card__contact + .person-card__contact {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.person-card__contact .icon {
|
.person-card__contact .icon {
|
||||||
color: var(--clr-accent);
|
color: var(--clr-accent);
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -867,28 +891,22 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gruppierte Abschnitte (fieldset/legend) — gliedert lange Formulare in ruhige
|
/* Gruppierte Abschnitte (fieldset/legend) — jeder Block ein dezent umrandetes
|
||||||
Blöcke, getrennt durch eine dezente Hairline. Reduziert die optische Last. */
|
Feld mit Titel auf der Rahmenlinie. Gliedert lange Formulare optisch klar. */
|
||||||
.form__group {
|
.form__group {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 1.25rem 1.5rem 1.5rem;
|
||||||
border: 0;
|
border: 1px solid var(--clr-glass-border);
|
||||||
|
border-radius: var(--radius-card);
|
||||||
min-inline-size: 0;
|
min-inline-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form__group + .form__group {
|
|
||||||
padding-top: 1.75rem;
|
|
||||||
border-top: 1px solid var(--clr-glass-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form__legend {
|
.form__legend {
|
||||||
width: 100%;
|
padding: 0 0.5rem;
|
||||||
padding: 0;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: var(--fs-500);
|
font-size: var(--fs-500);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.08em;
|
||||||
color: var(--clr-accent);
|
color: var(--clr-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form__group-body {
|
.form__group-body {
|
||||||
@@ -1908,14 +1926,20 @@
|
|||||||
|
|
||||||
.age-groups__grid {
|
.age-groups__grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 1.5rem;
|
gap: 1.75rem;
|
||||||
margin: 2.5rem 0 0;
|
margin: 2.5rem 0 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.age-groups__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.age-group {
|
.age-group {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
@@ -1940,7 +1964,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
aspect-ratio: 4 / 3;
|
aspect-ratio: 3 / 2;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
transition: transform var(--transition);
|
transition: transform var(--transition);
|
||||||
}
|
}
|
||||||
@@ -1949,50 +1973,58 @@
|
|||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Klassen-Badge (G/F/E/D/C) auf dem Bild. */
|
|
||||||
.age-group__badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.75rem;
|
|
||||||
left: 0.75rem;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
width: 2.6rem;
|
|
||||||
height: 2.6rem;
|
|
||||||
font-family: var(--ff-heading);
|
|
||||||
font-size: var(--fs-650);
|
|
||||||
line-height: 1;
|
|
||||||
color: #fff;
|
|
||||||
background: var(--clr-accent);
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 2px 10px rgb(0 0 0 / 0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
.age-group__body {
|
.age-group__body {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.4rem;
|
gap: 0.6rem;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
padding: 1.1rem 1.3rem 1.3rem;
|
padding: 1.5rem 1.6rem 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.age-group__name {
|
.age-group__name {
|
||||||
font-size: var(--fs-650);
|
font-size: var(--fs-700);
|
||||||
line-height: var(--lh-heading);
|
line-height: var(--lh-heading);
|
||||||
}
|
}
|
||||||
|
|
||||||
.age-group__training {
|
.age-group__training {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: var(--fs-500);
|
font-size: var(--fs-600);
|
||||||
|
line-height: 1.45;
|
||||||
color: var(--clr-text-muted);
|
color: var(--clr-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.age-group__icon {
|
.age-group__icon {
|
||||||
flex: none;
|
flex: none;
|
||||||
|
margin-top: 0.2em;
|
||||||
color: var(--clr-accent);
|
color: var(--clr-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Platzhalter-Karten (B-/A-Jugend „In Entstehung"): Bild gedämpft, Label oben links. */
|
||||||
|
.age-group--upcoming .age-group__img {
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.age-group--upcoming:hover .age-group__img {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.age-group__soon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.9rem;
|
||||||
|
left: 0.9rem;
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
font-family: var(--ff-body);
|
||||||
|
font-size: var(--fs-400);
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--clr-accent);
|
||||||
|
border-radius: var(--radius-btn);
|
||||||
|
}
|
||||||
|
|
||||||
/* === team-cards (Mannschafts-Karten) === */
|
/* === team-cards (Mannschafts-Karten) === */
|
||||||
.team-cards > .container {
|
.team-cards > .container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -2760,6 +2792,14 @@
|
|||||||
color: var(--clr-text-muted);
|
color: var(--clr-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mehrere Beitragstabellen + Austritts-Hinweis gestapelt (Turn-Übersicht). */
|
||||||
|
.fee-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* === membership (Mitglied werden) === */
|
/* === membership (Mitglied werden) === */
|
||||||
.membership-signup__inner {
|
.membership-signup__inner {
|
||||||
max-width: 720px;
|
max-width: 720px;
|
||||||
|
|||||||
@@ -26,19 +26,18 @@
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Desktop: Logo links | Nav exakt zentriert | CTAs rechts */
|
/* Desktop: Logo + Schriftzug links | Nav-Punkte und CTA rechtsbündig zusammen */
|
||||||
.site-header__inner {
|
.site-header__inner {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr auto 1fr;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
min-height: var(--header-h);
|
min-height: var(--header-h);
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-header__brand {
|
.site-header__brand {
|
||||||
justify-self: start;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--clr-text);
|
color: var(--clr-text);
|
||||||
}
|
}
|
||||||
@@ -49,16 +48,25 @@
|
|||||||
filter: drop-shadow(var(--shadow));
|
filter: drop-shadow(var(--shadow));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.site-header__name {
|
||||||
|
font-family: var(--ff-heading);
|
||||||
|
font-size: var(--fs-675);
|
||||||
|
line-height: 0.85;
|
||||||
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
.site-header__actions {
|
.site-header__actions {
|
||||||
justify-self: end;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Navigation --- */
|
/* --- Navigation --- */
|
||||||
|
/* margin-left:auto schiebt Nav + nachfolgende CTAs als Block nach rechts */
|
||||||
.site-nav {
|
.site-nav {
|
||||||
justify-self: center;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-nav__toggle {
|
.site-nav__toggle {
|
||||||
@@ -208,6 +216,7 @@
|
|||||||
|
|
||||||
.site-nav {
|
.site-nav {
|
||||||
order: 3;
|
order: 3;
|
||||||
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-nav__toggle {
|
.site-nav__toggle {
|
||||||
@@ -329,14 +338,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
/* Schmale Screens: CTA(s) kompakter neben dem Burger. */
|
/* Schmale Screens: Schriftzug ausblenden (Logo + aria-label bleiben). */
|
||||||
|
.site-header__name {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CTA(s) kompakter neben dem Burger. */
|
||||||
.site-header__actions a:not(:last-child) {
|
.site-header__actions a:not(:last-child) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-header__actions .btn {
|
.site-header__actions .btn {
|
||||||
font-size: var(--fs-400);
|
font-size: var(--btn-font-sm);
|
||||||
padding: 8px 14px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,21 +407,31 @@
|
|||||||
|
|
||||||
.site-footer__grid {
|
.site-footer__grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.4fr 1fr 1.2fr 1fr;
|
grid-template-columns: 1.5fr repeat(4, minmax(0, 1fr));
|
||||||
gap: 3rem;
|
gap: 2.5rem 3rem;
|
||||||
padding-block: 4.5rem 3.5rem;
|
padding-block: 4.5rem 3.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 992px) {
|
/* Brand-Block bleibt links stehen, Nav-Spalten rücken rechts daneben */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.site-footer__grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
.site-footer__brand {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
.site-footer__grid {
|
.site-footer__grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 500px) {
|
||||||
.site-footer__grid {
|
.site-footer__grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 2.5rem;
|
gap: 2.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,6 +477,16 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.site-footer__heading a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer__heading a:hover {
|
||||||
|
color: var(--clr-accent);
|
||||||
|
}
|
||||||
|
|
||||||
.site-footer__heading::after {
|
.site-footer__heading::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -500,12 +533,20 @@
|
|||||||
|
|
||||||
.site-footer__list a:hover,
|
.site-footer__list a:hover,
|
||||||
.site-footer__address a:hover,
|
.site-footer__address a:hover,
|
||||||
.site-footer__social a:hover {
|
.site-footer__social a:hover,
|
||||||
|
.site-footer__legal a:hover {
|
||||||
color: var(--clr-text);
|
color: var(--clr-text);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-underline-offset: 0.25em;
|
text-underline-offset: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Zusatzlinks (Startseite, Mitglied werden) unter dem Kontakt-Block */
|
||||||
|
.site-footer__list--extra {
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding-top: 1.25rem;
|
||||||
|
border-top: 1px solid var(--clr-glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
/* Bottom-Bar */
|
/* Bottom-Bar */
|
||||||
.site-footer__bottom {
|
.site-footer__bottom {
|
||||||
border-top: 1px solid var(--clr-glass-border);
|
border-top: 1px solid var(--clr-glass-border);
|
||||||
@@ -525,6 +566,21 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.site-footer__legal {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem 1.5rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer__legal a {
|
||||||
|
color: var(--clr-text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
/* Verbände-Zeile bleibt ab Tablet-Breite einzeilig (wrappt als Ganzes in die nächste Reihe) */
|
/* Verbände-Zeile bleibt ab Tablet-Breite einzeilig (wrappt als Ganzes in die nächste Reihe) */
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.site-footer__memberships {
|
.site-footer__memberships {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
--fs-500: 1.125rem; /* 18px Navigation */
|
--fs-500: 1.125rem; /* 18px Navigation */
|
||||||
--fs-600: 1.3125rem; /* 21px Body */
|
--fs-600: 1.3125rem; /* 21px Body */
|
||||||
--fs-650: 1.75rem; /* 28px Zwischengröße (z.B. Footer-Headings) */
|
--fs-650: 1.75rem; /* 28px Zwischengröße (z.B. Footer-Headings) */
|
||||||
|
--fs-675: 2.125rem; /* 34px Header-Wortmarke */
|
||||||
--fs-700: clamp(2rem, 1.2rem + 3.5vw, 3.125rem); /* 50px h3 */
|
--fs-700: clamp(2rem, 1.2rem + 3.5vw, 3.125rem); /* 50px h3 */
|
||||||
--fs-800: clamp(2.25rem, 1.3rem + 4.5vw, 3.75rem); /* 60px h2 */
|
--fs-800: clamp(2.25rem, 1.3rem + 4.5vw, 3.75rem); /* 60px h2 */
|
||||||
--fs-900: clamp(3rem, 1.75rem + 5.5vw, 6rem); /* 96px h1 */
|
--fs-900: clamp(3rem, 1.75rem + 5.5vw, 6rem); /* 96px h1 */
|
||||||
@@ -38,6 +39,14 @@
|
|||||||
--fw-regular: 400;
|
--fw-regular: 400;
|
||||||
--fw-bold: 700;
|
--fw-bold: 700;
|
||||||
|
|
||||||
|
/* --- Buttons --- */
|
||||||
|
--btn-font: 1rem; /* 16px – Default (moderat) */
|
||||||
|
--btn-font-sm: var(--fs-300); /* 13px – dezente Buttons */
|
||||||
|
--btn-font-lg: var(--fs-500); /* 18px – Hero-CTAs */
|
||||||
|
--btn-pad-y: 0.625em; /* skaliert mit font-size */
|
||||||
|
--btn-pad-x: 1.25em;
|
||||||
|
--btn-line: 1.2;
|
||||||
|
|
||||||
/* --- Form & Tiefe --- */
|
/* --- Form & Tiefe --- */
|
||||||
--radius-btn: 8px;
|
--radius-btn: 8px;
|
||||||
--radius-card: 15px;
|
--radius-card: 15px;
|
||||||
@@ -49,7 +58,7 @@
|
|||||||
--container-pad: 1rem;
|
--container-pad: 1rem;
|
||||||
--space-section: 5rem;
|
--space-section: 5rem;
|
||||||
--header-h: 60px;
|
--header-h: 60px;
|
||||||
--logo-h: 97px;
|
--logo-h: 72px;
|
||||||
|
|
||||||
/* --- Bewegung --- */
|
/* --- Bewegung --- */
|
||||||
--transition: 300ms ease;
|
--transition: 300ms ease;
|
||||||
|
|||||||
@@ -3,25 +3,45 @@
|
|||||||
============================================================ */
|
============================================================ */
|
||||||
.btn {
|
.btn {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 10px 18px;
|
padding: var(--btn-pad-y) var(--btn-pad-x);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
background: var(--clr-accent);
|
background: var(--clr-accent);
|
||||||
color: var(--clr-text);
|
color: var(--clr-text);
|
||||||
font-size: var(--fs-500);
|
font-size: var(--btn-font);
|
||||||
|
line-height: var(--btn-line);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px solid var(--clr-accent);
|
border: 1px solid var(--clr-accent);
|
||||||
border-radius: var(--radius-btn);
|
border-radius: var(--radius-btn);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background var(--transition), border-color var(--transition);
|
transition: background var(--transition), border-color var(--transition),
|
||||||
|
color var(--transition), transform var(--transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* primary (Default) */
|
||||||
.btn:hover {
|
.btn:hover {
|
||||||
background: var(--clr-accent-dark);
|
background: var(--clr-accent-dark);
|
||||||
border-color: var(--clr-accent-dark);
|
border-color: var(--clr-accent-dark);
|
||||||
color: var(--clr-text);
|
color: var(--clr-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn:focus-visible {
|
||||||
|
outline: 2px solid var(--clr-text);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn[disabled],
|
||||||
|
.btn[aria-disabled="true"] {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* secondary */
|
||||||
.btn--outline {
|
.btn--outline {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: var(--clr-text);
|
border-color: var(--clr-text);
|
||||||
@@ -30,8 +50,13 @@
|
|||||||
.btn--outline:hover {
|
.btn--outline:hover {
|
||||||
background: var(--clr-glass-bg);
|
background: var(--clr-glass-bg);
|
||||||
border-color: var(--clr-text);
|
border-color: var(--clr-text);
|
||||||
|
color: var(--clr-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Größenmodifier — Padding ist em-basiert, skaliert mit der font-size */
|
||||||
|
.btn--sm { font-size: var(--btn-font-sm); }
|
||||||
|
.btn--lg { font-size: var(--btn-font-lg); }
|
||||||
|
|
||||||
.visually-hidden {
|
.visually-hidden {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
|||||||
BIN
public/assets/downloads/beitrittserklaerung-turnen.pdf
Normal file
BIN
public/assets/downloads/beitrittserklaerung-turnen.pdf
Normal file
Binary file not shown.
2
public/assets/icons/check-circle.svg
Normal file
2
public/assets/icons/check-circle.svg
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
||||||
|
<path d="m10.97 4.97-.02.022-3.473 4.425-2.093-2.094a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05"/></svg>
|
||||||
|
After Width: | Height: | Size: 296 B |
2
public/assets/icons/download.svg
Normal file
2
public/assets/icons/download.svg
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
|
||||||
|
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/></svg>
|
||||||
|
After Width: | Height: | Size: 353 B |
2
public/assets/icons/file-earmark-text.svg
Normal file
2
public/assets/icons/file-earmark-text.svg
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5"/>
|
||||||
|
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/></svg>
|
||||||
|
After Width: | Height: | Size: 390 B |
1
public/assets/icons/whatsapp.svg
Normal file
1
public/assets/icons/whatsapp.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M13.601 2.326A7.85 7.85 0 0 0 7.994 0C3.627 0 .068 3.558.064 7.926c0 1.399.366 2.76 1.057 3.965L0 16l4.204-1.102a7.9 7.9 0 0 0 3.79.965h.004c4.368 0 7.926-3.558 7.93-7.93A7.9 7.9 0 0 0 13.6 2.326zM7.994 14.521a6.6 6.6 0 0 1-3.356-.92l-.24-.144-2.494.654.666-2.433-.156-.251a6.56 6.56 0 0 1-1.007-3.505c0-3.626 2.957-6.584 6.591-6.584a6.56 6.56 0 0 1 4.66 1.931 6.56 6.56 0 0 1 1.928 4.66c-.004 3.639-2.961 6.592-6.592 6.592m3.615-4.934c-.197-.099-1.17-.578-1.353-.646-.182-.065-.315-.099-.445.099-.133.197-.513.646-.627.775-.114.133-.232.148-.43.05-.197-.1-.836-.308-1.592-.985-.59-.525-.985-1.175-1.103-1.372-.114-.198-.011-.304.088-.403.087-.088.197-.232.296-.346.1-.114.133-.198.198-.33.065-.134.034-.248-.015-.347-.05-.099-.445-1.076-.612-1.47-.16-.389-.323-.335-.445-.34-.114-.007-.247-.007-.38-.007a.73.73 0 0 0-.529.247c-.182.198-.691.677-.691 1.654s.71 1.916.81 2.049c.098.133 1.394 2.132 3.383 2.992.47.205.84.326 1.129.418.475.152.904.129 1.246.08.38-.058 1.171-.48 1.338-.943.164-.464.164-.86.114-.943-.049-.084-.182-.133-.38-.232"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -28,6 +28,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $slug === 'kontakt-senden') {
|
|||||||
require APP_PATH . '/actions/contact-submit.php';
|
require APP_PATH . '/actions/contact-submit.php';
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $slug === 'mitglied-werden-senden') {
|
||||||
|
require APP_PATH . '/actions/membership-submit.php';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$routes = require APP_PATH . '/routes.php';
|
$routes = require APP_PATH . '/routes.php';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user