Files
tsv08kulmbach-website/app/components/membership-form.php
fs 87055bd14b Design-System: Spacing auf Tokens migrieren
Alle padding/margin/gap-Deklarationen in components.css, layout.css und
base.css nutzen jetzt die Spacing-Skala — 414 Token-Verwendungen, kein roher
rem-Wert mehr in einer Abstands-Deklaration.

Die Substitution lief mechanisch (nächste Rasterstufe, Gleichstand aufgerundet),
damit sie deterministisch und nachvollziehbar ist statt 450-mal von Hand
geraten. Rund 325 Werte lagen bereits exakt auf einer Stufe und sind
verlustfrei übernommen. 125 Werte verschieben sich:

  ±0,4–2,0px   106 Werte  (0.1/0.15/0.3/0.35/0.4/0.6/0.85/0.9/1.1/1.6rem …)
  ±4px          12 Werte  (2.25 -> 2.5, 2.75 -> 3rem)
  ±8px           8 Werte  (3.5 -> 4, 4.5 -> 5, 7.5 -> 7rem)

Die 8px-Sprünge treffen ausschließlich Sektionsabstände (padding-block großer
Bänder, margin-top von Blöcken), wo sie optisch nicht ins Gewicht fallen. Die
±4px-Gruppe ist 2.25rem, das exakt zwischen zwei Stufen lag.

em-Werte bleiben bewusst roh: sie skalieren mit der font-size (Button-Padding,
Badge-Innenabstände) und gehören damit nicht auf ein festes px-Raster. Nullen,
auto und Prozentwerte sind unangetastet, calc() wurde korrekt mitgezogen
(calc(var(--header-h) + var(--space-5xl))).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSsVdooFLgPA8gPFp7HwZU
2026-07-27 10:09:33 +02:00

117 lines
7.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/**
* 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 "company_url" + signierter Timestamp "ft" (form_token()).
*/
$club = json_load('club');
$fee_options = $fee_options ?? [];
// Fehlerquelle: PRG-Query (?error=…) oder — beim 422-Re-Render ohne JS — die
// serverseitigen Feldfehler aus der Action.
$sent = isset($_GET['sent']);
$error = isset($_GET['error']) ? (string) $_GET['error'] : (form_has_errors() ? 'validation' : null);
$errorMessages = [
'validation' => 'Bitte prüfe deine Eingaben die markierten Felder brauchen noch etwas Aufmerksamkeit.',
'ratelimit' => 'Zu viele Anfragen in kurzer Zeit. Bitte versuche es in ein paar Minuten erneut.',
'busy' => 'Bei uns gehen gerade ungewöhnlich viele Anfragen ein. Bitte versuche es später noch einmal oder schreib uns direkt an ' . $club['email'] . '.',
'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.&nbsp;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>
<?php /* autofocus nur beim 422-Re-Render: bringt den Fokus auch ohne JS in die Fehlermeldung */ ?>
<div class="form__status" role="status" aria-live="polite" tabindex="-1" data-form-status<?= form_has_errors() ? ' autofocus' : '' ?>>
<?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>
<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-company-url">Bitte dieses Feld leer lassen</label>
<input type="text" id="mm-company-url" name="company_url" 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>