Files
tsv08kulmbach-website/app/components/contact-form.php

143 lines
8.5 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
/**
* Kontaktformular (Felder wie auf der alten Seite). Props:
* $intro array{title, text}|null optionaler Intro-Block (Überschrift, Teaser, Direkt-Mail).
* Weglassen nur das Formular (z. B. auf /kontakt, wo der Hero den Kontext liefert).
* $return string optional Slug der absendenden Seite (Default '' = Startseite). Die Action
* leitet nach PRG hierher zurück (whitelist-validiert), damit /kontakt nicht auf die
* Startseite springt.
Wartelisten-Hinweis für Tiger und Hühner, über dem Anfrageformular Die Turnabteilung (Maxi) beantwortet laufend Anfragen nach einem Wartelistenplatz für „Tiger" und „Hühner", den es nicht gibt. Auf der alten Seite stand der Hinweis direkt über dem Anfrageformular, hier fehlte er. Schlimmer: an der Stelle stand das GEGENTEIL. Der bisherige Hinweis lautete "Aktuell sind alle Kinderturngruppen belegt. Bei Interesse setzen wir sie gerne auf die Warteliste." — für alle Kindergruppen, also auch für die beiden, die keine Liste führen. Der Satz hat einen Teil der Anfragen vermutlich erst erzeugt. Er ist nicht gelöscht, sondern auf die übrigen Gruppen eingegrenzt und aufs Du umgestellt (Haus-Stil; vorher stand dort klein "sie"). Der Text steht genau EINMAL, in data/turnen.json unter disciplines.eltern-kind-turnen.notice + notice_title. Zwei Stellen lesen ihn: die rote Kachel auf /turnen/eltern-kind-turnen (gab es schon) und neu ein Block direkt über dem Formular auf /turnen. Ändert die Abteilung die Lage, ist das eine Zeile in einer Datei. Umsetzung: - notice_title ist neu, weil eine rote Fläche ohne Überschrift wie eine Fehlermeldung liest. Abel statt Coolvetica, der Schnitt ist bei ~19px in Versalien unleserlich (CLAUDE.md, Design). - Der Block steht AUSSERHALB des <form> und damit außerhalb der role="status"-Region: ein dauerhafter Hinweis, keine Rückmeldung zum Absenden. In der Live-Region läse ihn ein Screenreader bei jeder Formularmeldung erneut vor. Kein role="alert", er steht schon beim Laden da. - Überschrift als h3 unter der h2 des Intro-Blocks, damit die Folge nicht springt. - .contact__notice übernimmt bewusst die Optik von .turnen-dash__notice, damit beide Stellen als derselbe Hinweis erkennbar sind. Weiß auf --clr-accent erreicht 4,92:1 und trägt damit Fließtext (WCAG 1.4.3). Kommt ein dritter Ort dazu, gehören die gemeinsamen Eigenschaften in eine geteilte .notice-Klasse; für zwei wäre das verfrüht. Gemessen: Hinweisbreite gleich Formularbreite (720px bei 1280, 347px bei 390), kein Überlauf. preflight ohne neue Meldungen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 21:54:10 +02:00
* $notice string optional hervorgehobener Hinweis DIREKT über dem Formular, plus
* $noticeTitle für die Überschrift dazu. Für Dinge, die man wissen muss, BEVOR
* man schreibt (Wartelisten-Status der Turnabteilung). Steht bewusst außerhalb
* des <form> und damit außerhalb der role="status"-Region: das ist ein
* dauerhafter Hinweis, keine Rückmeldung zum Absenden in der Live-Region
* würde ihn ein Screenreader bei jeder Formularmeldung erneut vorlesen.
* Der Text selbst gehört in die data/*.json der aufrufenden Seite, damit er
* nicht zweimal existiert (auf /turnen ist es derselbe Hinweis, den
* group-schedule.php auf der Kinderturn-Unterseite als Kachel zeigt).
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News, Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover, Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das Skript selbst, statt sie von Hand zu prüfen. Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie doppelt gepflegt wird. News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head. Behobene Fehler: - Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen (Ergebnis-Rückblick, Vorschau, Cover). - --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links unter dem Logo. - base_url in der Produktions-Config ohne www, während .htaccess auf www umleitet; preflight prüft das jetzt. - .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen. Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php (Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit Warnung, wenn base_url nicht auf die echte Domain zeigt). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
* $department string optional '' (allgemein) oder 'turnen'. Steuert Auswahlliste,
* Empfänger und Feldregeln über contact_variant() (helpers.php, einzige Quelle).
* Die Turn-Variante geht an die Abteilung statt an info@ und braucht keinen
* Betreff, weil die Disziplin-Auswahl ihn ersetzt. Bewusst dieselbe Komponente
* und dieselbe Action: ein zweites Formular hieße ein zweiter Spam-Schutz.
* 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');
$return = $return ?? '';
$intro = $intro ?? null;
Wartelisten-Hinweis für Tiger und Hühner, über dem Anfrageformular Die Turnabteilung (Maxi) beantwortet laufend Anfragen nach einem Wartelistenplatz für „Tiger" und „Hühner", den es nicht gibt. Auf der alten Seite stand der Hinweis direkt über dem Anfrageformular, hier fehlte er. Schlimmer: an der Stelle stand das GEGENTEIL. Der bisherige Hinweis lautete "Aktuell sind alle Kinderturngruppen belegt. Bei Interesse setzen wir sie gerne auf die Warteliste." — für alle Kindergruppen, also auch für die beiden, die keine Liste führen. Der Satz hat einen Teil der Anfragen vermutlich erst erzeugt. Er ist nicht gelöscht, sondern auf die übrigen Gruppen eingegrenzt und aufs Du umgestellt (Haus-Stil; vorher stand dort klein "sie"). Der Text steht genau EINMAL, in data/turnen.json unter disciplines.eltern-kind-turnen.notice + notice_title. Zwei Stellen lesen ihn: die rote Kachel auf /turnen/eltern-kind-turnen (gab es schon) und neu ein Block direkt über dem Formular auf /turnen. Ändert die Abteilung die Lage, ist das eine Zeile in einer Datei. Umsetzung: - notice_title ist neu, weil eine rote Fläche ohne Überschrift wie eine Fehlermeldung liest. Abel statt Coolvetica, der Schnitt ist bei ~19px in Versalien unleserlich (CLAUDE.md, Design). - Der Block steht AUSSERHALB des <form> und damit außerhalb der role="status"-Region: ein dauerhafter Hinweis, keine Rückmeldung zum Absenden. In der Live-Region läse ihn ein Screenreader bei jeder Formularmeldung erneut vor. Kein role="alert", er steht schon beim Laden da. - Überschrift als h3 unter der h2 des Intro-Blocks, damit die Folge nicht springt. - .contact__notice übernimmt bewusst die Optik von .turnen-dash__notice, damit beide Stellen als derselbe Hinweis erkennbar sind. Weiß auf --clr-accent erreicht 4,92:1 und trägt damit Fließtext (WCAG 1.4.3). Kommt ein dritter Ort dazu, gehören die gemeinsamen Eigenschaften in eine geteilte .notice-Klasse; für zwei wäre das verfrüht. Gemessen: Hinweisbreite gleich Formularbreite (720px bei 1280, 347px bei 390), kein Überlauf. preflight ohne neue Meldungen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 21:54:10 +02:00
$notice = $notice ?? '';
$noticeTitle = $noticeTitle ?? '';
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News, Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover, Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das Skript selbst, statt sie von Hand zu prüfen. Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie doppelt gepflegt wird. News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head. Behobene Fehler: - Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen (Ergebnis-Rückblick, Vorschau, Cover). - --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links unter dem Logo. - base_url in der Produktions-Config ohne www, während .htaccess auf www umleitet; preflight prüft das jetzt. - .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen. Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php (Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit Warnung, wenn base_url nicht auf die echte Domain zeigt). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
$variant = contact_variant((string) ($department ?? ''));
$interests = $variant['interests'];
// Sichtbare Direkt-Adresse: bei einer Abteilungsanfrage die der Abteilung.
$directMail = $variant['to'] !== '' ? $variant['to'] : $club['email'];
// 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 = [
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News, Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover, Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das Skript selbst, statt sie von Hand zu prüfen. Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie doppelt gepflegt wird. News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head. Behobene Fehler: - Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen (Ergebnis-Rückblick, Vorschau, Cover). - --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links unter dem Logo. - base_url in der Produktions-Config ohne www, während .htaccess auf www umleitet; preflight prüft das jetzt. - .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen. Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php (Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit Warnung, wenn base_url nicht auf die echte Domain zeigt). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
'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.',
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News, Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover, Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das Skript selbst, statt sie von Hand zu prüfen. Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie doppelt gepflegt wird. News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head. Behobene Fehler: - Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen (Ergebnis-Rückblick, Vorschau, Cover). - --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links unter dem Logo. - base_url in der Produktions-Config ohne www, während .htaccess auf www umleitet; preflight prüft das jetzt. - .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen. Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php (Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit Warnung, wenn base_url nicht auf die echte Domain zeigt). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
'busy' => 'Bei uns gehen gerade ungewöhnlich viele Anfragen ein. Bitte versuche es später noch einmal oder schreib uns direkt an ' . $directMail . '.',
'mail' => 'Deine Nachricht konnte gerade nicht versendet werden. Bitte versuche es später erneut oder schreib uns direkt an ' . $directMail . '.',
];
$f = [
'name' => ['type' => 'text', 'name' => 'name', 'id' => 'contact-name', 'label' => 'Name', 'required' => true, 'autocomplete' => 'name', 'maxlength' => 200],
'email' => ['type' => 'email', 'name' => 'email', 'id' => 'contact-email', 'label' => 'E-Mail', 'required' => true, 'autocomplete' => 'email', 'maxlength' => 200],
'phone' => ['type' => 'tel', 'name' => 'phone', 'id' => 'contact-phone', 'label' => 'Telefonnummer', 'autocomplete' => 'tel', 'maxlength' => 50],
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News, Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover, Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das Skript selbst, statt sie von Hand zu prüfen. Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie doppelt gepflegt wird. News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head. Behobene Fehler: - Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen (Ergebnis-Rückblick, Vorschau, Cover). - --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links unter dem Logo. - base_url in der Produktions-Config ohne www, während .htaccess auf www umleitet; preflight prüft das jetzt. - .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen. Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php (Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit Warnung, wenn base_url nicht auf die echte Domain zeigt). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
'interest' => ['type' => 'select', 'name' => 'interest', 'id' => 'contact-interest', 'label' => 'Ich interessiere mich für', 'options' => $interests, 'required' => $variant['interest_required']],
Turn-Anfrageformular: Feld „Geburtsdatum des Kindes" wieder da Auf der alten Seite gab es das Feld, beim Neubau ist es untergegangen. Gegen 2024.tsv08kulmbach.de geprüft: dort hieß es input_alter_kind, war type="date", OPTIONAL und trug das Label „Geburtsdatum des Kindes" — genau so wieder eingebaut. Die Abteilung braucht es, weil die Gruppen nach Alter eingeteilt sind. Nur in der Turn-Variante. In der allgemeinen wäre es sinnlos und würde ohne Not personenbezogene Daten Minderjähriger erheben. Dafür drei Stellen: - contact_variant(): Schalter ask_birthdate (true nur für turnen) - contact-form.php: Feld definiert, bedingt gerendert, direkt nach der Gruppenauswahl wie auf der alten Seite - contact-submit.php: Validierung UND Rücksetzen auf leer, wenn die Variante das Feld nicht anbietet — sonst könnte ein manipulierter POST es an der allgemeinen Variante vorbei einschmuggeln Validierung serverseitig, nicht nur per type=date: echtes Datum, nicht in der Zukunft, nicht vor 1900. createFromFormat mit Rückvergleich, weil PHP sonst Scheindaten wie 2019-02-31 stillschweigend auf den 3. März verschiebt. Geprüft mit ausreichend altem Token (die Time-Trap verlangt 3 s, meine ersten Versuche liefen deshalb als Bot durch): 2019-02-31 abgewiesen, 2030-01-01 abgewiesen, gültiges Datum passiert. Ohne JS bleibt der Wert bei HTTP 422 erhalten (value="2019-03-12" im Re-Render nachgewiesen). Kein neues Markup: form-field kann type=date und min/max längst, die Prop für den Hilfetext heißt hint (nicht help, das war mein erster Fehlgriff). Docblock der Komponente um date und min/max ergänzt. Datenschutzerklärung mitgezogen, wie die Projektregel es für neue Formularfelder verlangt — mit Zweckbindung (Zuordnung zur Altersgruppe) und dem ausdrücklichen Hinweis, dass es keine Pflichtangabe ist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 21:10:47 +02:00
// Optional und nur in der Turn-Variante (siehe contact_variant). max/min
// begrenzen die Auswahl im Datepicker schon im Browser; die Action prüft es
// serverseitig noch einmal.
'birthdate' => [
'type' => 'date', 'name' => 'birthdate', 'id' => 'contact-birthdate',
'label' => 'Geburtsdatum des Kindes', 'hint' => 'Optional. Hilft uns, die passende Gruppe zu finden.',
'min' => '1900-01-01', 'max' => date('Y-m-d'),
],
'subject' => ['type' => 'text', 'name' => 'subject', 'id' => 'contact-subject', 'label' => 'Betreff', 'required' => true, 'maxlength' => 200],
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News, Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover, Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das Skript selbst, statt sie von Hand zu prüfen. Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie doppelt gepflegt wird. News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head. Behobene Fehler: - Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen (Ergebnis-Rückblick, Vorschau, Cover). - --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links unter dem Logo. - base_url in der Produktions-Config ohne www, während .htaccess auf www umleitet; preflight prüft das jetzt. - .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen. Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php (Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit Warnung, wenn base_url nicht auf die echte Domain zeigt). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
'message' => ['type' => 'textarea', 'name' => 'message', 'id' => 'contact-message', 'label' => 'Nachricht', 'required' => $variant['message_required'], 'maxlength' => 5000, 'rows' => 6],
'privacy' => [
'type' => 'checkbox', 'name' => 'privacy', 'id' => 'contact-privacy', 'required' => true,
'label' => 'Ich stimme den Datenschutzbestimmungen zu.',
'label_html' => 'Ich stimme den <a href="' . e(url('datenschutz')) . '">Datenschutzbestimmungen</a> zu.',
],
];
?>
<section class="section contact" id="kontakt">
<div class="container contact__inner">
<?php if (!empty($intro)): ?>
<div class="contact__intro">
<h2><?= e($intro['title']) ?></h2>
<p><?= e($intro['text']) ?></p>
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News, Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover, Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das Skript selbst, statt sie von Hand zu prüfen. Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie doppelt gepflegt wird. News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head. Behobene Fehler: - Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen (Ergebnis-Rückblick, Vorschau, Cover). - --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links unter dem Logo. - base_url in der Produktions-Config ohne www, während .htaccess auf www umleitet; preflight prüft das jetzt. - .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen. Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php (Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit Warnung, wenn base_url nicht auf die echte Domain zeigt). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
<p class="text-muted">Oder direkt per Mail an <a href="mailto:<?= e($directMail) ?>"><?= e($directMail) ?></a></p>
</div>
Wartelisten-Hinweis für Tiger und Hühner, über dem Anfrageformular Die Turnabteilung (Maxi) beantwortet laufend Anfragen nach einem Wartelistenplatz für „Tiger" und „Hühner", den es nicht gibt. Auf der alten Seite stand der Hinweis direkt über dem Anfrageformular, hier fehlte er. Schlimmer: an der Stelle stand das GEGENTEIL. Der bisherige Hinweis lautete "Aktuell sind alle Kinderturngruppen belegt. Bei Interesse setzen wir sie gerne auf die Warteliste." — für alle Kindergruppen, also auch für die beiden, die keine Liste führen. Der Satz hat einen Teil der Anfragen vermutlich erst erzeugt. Er ist nicht gelöscht, sondern auf die übrigen Gruppen eingegrenzt und aufs Du umgestellt (Haus-Stil; vorher stand dort klein "sie"). Der Text steht genau EINMAL, in data/turnen.json unter disciplines.eltern-kind-turnen.notice + notice_title. Zwei Stellen lesen ihn: die rote Kachel auf /turnen/eltern-kind-turnen (gab es schon) und neu ein Block direkt über dem Formular auf /turnen. Ändert die Abteilung die Lage, ist das eine Zeile in einer Datei. Umsetzung: - notice_title ist neu, weil eine rote Fläche ohne Überschrift wie eine Fehlermeldung liest. Abel statt Coolvetica, der Schnitt ist bei ~19px in Versalien unleserlich (CLAUDE.md, Design). - Der Block steht AUSSERHALB des <form> und damit außerhalb der role="status"-Region: ein dauerhafter Hinweis, keine Rückmeldung zum Absenden. In der Live-Region läse ihn ein Screenreader bei jeder Formularmeldung erneut vor. Kein role="alert", er steht schon beim Laden da. - Überschrift als h3 unter der h2 des Intro-Blocks, damit die Folge nicht springt. - .contact__notice übernimmt bewusst die Optik von .turnen-dash__notice, damit beide Stellen als derselbe Hinweis erkennbar sind. Weiß auf --clr-accent erreicht 4,92:1 und trägt damit Fließtext (WCAG 1.4.3). Kommt ein dritter Ort dazu, gehören die gemeinsamen Eigenschaften in eine geteilte .notice-Klasse; für zwei wäre das verfrüht. Gemessen: Hinweisbreite gleich Formularbreite (720px bei 1280, 347px bei 390), kein Überlauf. preflight ohne neue Meldungen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 21:54:10 +02:00
<?php endif; ?>
<?php if ($notice !== ''): ?>
<?php /* h3, weil der Intro-Block direkt darüber die h2 der Sektion trägt so bleibt die
Überschriften-Folge ohne Sprung. Kein role="alert": der Hinweis steht schon beim
Laden da und ist keine Reaktion auf eine Eingabe. */ ?>
<aside class="contact__notice">
<?= icon('info-circle', 'contact__notice-icon') ?>
<div>
<?php if ($noticeTitle !== ''): ?>
<h3 class="contact__notice-title"><?= e($noticeTitle) ?></h3>
<?php endif; ?>
<p><?= e($notice) ?></p>
</div>
</aside>
<?php endif; ?>
<form class="form" method="post" action="<?= e(url('kontakt-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 für deine Anfrage! Wir melden uns so schnell wie möglich bei dir.</p>
<?php elseif ($error !== null): ?>
<p class="form__error"><?= e($errorMessages[$error] ?? $errorMessages['mail']) ?></p>
<?php endif; ?>
</div>
<div class="form__row">
<?php component('form-field', ['field' => $f['name']]); ?>
<?php component('form-field', ['field' => $f['email']]); ?>
</div>
<div class="form__row">
<?php component('form-field', ['field' => $f['phone']]); ?>
<?php component('form-field', ['field' => $f['interest']]); ?>
Turn-Anfrageformular: Feld „Geburtsdatum des Kindes" wieder da Auf der alten Seite gab es das Feld, beim Neubau ist es untergegangen. Gegen 2024.tsv08kulmbach.de geprüft: dort hieß es input_alter_kind, war type="date", OPTIONAL und trug das Label „Geburtsdatum des Kindes" — genau so wieder eingebaut. Die Abteilung braucht es, weil die Gruppen nach Alter eingeteilt sind. Nur in der Turn-Variante. In der allgemeinen wäre es sinnlos und würde ohne Not personenbezogene Daten Minderjähriger erheben. Dafür drei Stellen: - contact_variant(): Schalter ask_birthdate (true nur für turnen) - contact-form.php: Feld definiert, bedingt gerendert, direkt nach der Gruppenauswahl wie auf der alten Seite - contact-submit.php: Validierung UND Rücksetzen auf leer, wenn die Variante das Feld nicht anbietet — sonst könnte ein manipulierter POST es an der allgemeinen Variante vorbei einschmuggeln Validierung serverseitig, nicht nur per type=date: echtes Datum, nicht in der Zukunft, nicht vor 1900. createFromFormat mit Rückvergleich, weil PHP sonst Scheindaten wie 2019-02-31 stillschweigend auf den 3. März verschiebt. Geprüft mit ausreichend altem Token (die Time-Trap verlangt 3 s, meine ersten Versuche liefen deshalb als Bot durch): 2019-02-31 abgewiesen, 2030-01-01 abgewiesen, gültiges Datum passiert. Ohne JS bleibt der Wert bei HTTP 422 erhalten (value="2019-03-12" im Re-Render nachgewiesen). Kein neues Markup: form-field kann type=date und min/max längst, die Prop für den Hilfetext heißt hint (nicht help, das war mein erster Fehlgriff). Docblock der Komponente um date und min/max ergänzt. Datenschutzerklärung mitgezogen, wie die Projektregel es für neue Formularfelder verlangt — mit Zweckbindung (Zuordnung zur Altersgruppe) und dem ausdrücklichen Hinweis, dass es keine Pflichtangabe ist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 21:10:47 +02:00
<?php if ($variant['ask_birthdate']): ?>
<?php component('form-field', ['field' => $f['birthdate']]); ?>
<?php endif; ?>
</div>
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News, Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover, Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das Skript selbst, statt sie von Hand zu prüfen. Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie doppelt gepflegt wird. News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head. Behobene Fehler: - Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen (Ergebnis-Rückblick, Vorschau, Cover). - --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links unter dem Logo. - base_url in der Produktions-Config ohne www, während .htaccess auf www umleitet; preflight prüft das jetzt. - .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen. Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php (Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit Warnung, wenn base_url nicht auf die echte Domain zeigt). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
<?php /* Kein Betreff in der Abteilungsvariante: die Disziplin-Auswahl sagt schon,
worum es geht, und die Action baut den Mail-Betreff daraus. */ ?>
<?php if ($variant['ask_subject']): ?>
<?php component('form-field', ['field' => $f['subject']]); ?>
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News, Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover, Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das Skript selbst, statt sie von Hand zu prüfen. Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie doppelt gepflegt wird. News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head. Behobene Fehler: - Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen (Ergebnis-Rückblick, Vorschau, Cover). - --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links unter dem Logo. - base_url in der Produktions-Config ohne www, während .htaccess auf www umleitet; preflight prüft das jetzt. - .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen. Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php (Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit Warnung, wenn base_url nicht auf die echte Domain zeigt). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
<?php endif; ?>
<?php component('form-field', ['field' => $f['message']]); ?>
<?php component('form-field', ['field' => $f['privacy']]); ?>
<?php /* Honeypot — für Menschen unsichtbar, Bots füllen es aus */ ?>
<div class="visually-hidden" aria-hidden="true">
<label for="contact-company-url">Bitte dieses Feld leer lassen</label>
<input type="text" id="contact-company-url" name="company_url" tabindex="-1" autocomplete="off">
</div>
<input type="hidden" name="ft" value="<?= e(form_token()) ?>">
<input type="hidden" name="return" value="<?= e($return) ?>">
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News, Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover, Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das Skript selbst, statt sie von Hand zu prüfen. Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie doppelt gepflegt wird. News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head. Behobene Fehler: - Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen (Ergebnis-Rückblick, Vorschau, Cover). - --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links unter dem Logo. - base_url in der Produktions-Config ohne www, während .htaccess auf www umleitet; preflight prüft das jetzt. - .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen. Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php (Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit Warnung, wenn base_url nicht auf die echte Domain zeigt). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
<input type="hidden" name="department" value="<?= e($variant['department']) ?>">
<p class="form__submit">
<button class="btn" type="submit">Nachricht senden</button>
</p>
</form>
</div>
</section>