From 2c0594811c15290ee4883f26eeaaad6219bce675 Mon Sep 17 00:00:00 2001 From: fs Date: Sat, 20 Jun 2026 20:47:13 +0200 Subject: [PATCH] Mitmachen: Ehrenamtsseite mit Stellen-Filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neue Seite /mitmachen, die offene Ehrenamtsstellen aus data/mitmachen.json als position-card-Komponente listet, mit clientseitigem Bereichsfilter (position-filter.js, progressive enhancement) und JobPosting-Schema (job_posting_schema(), employmentType VOLUNTEER). Route ergänzt. Enthält zudem kleine person-card__contact-Stilanpassungen (gleiche CSS-Sektion). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01HGzc6GhWhmLJt1jC2q1SRZ --- app/components/position-card.php | 64 ++++++ app/helpers.php | 48 +++++ app/pages/mitmachen.php | 91 ++++++++ app/routes.php | 1 + data/mitmachen.json | 129 +++++++++++ public/assets/css/components.css | 264 +++++++++++++++++++++++ public/assets/icons/check2.svg | 1 + public/assets/icons/clipboard-check.svg | 3 + public/assets/icons/clock-history.svg | 3 + public/assets/icons/cup-hot.svg | 2 + public/assets/icons/heart.svg | 1 + public/assets/icons/mortarboard-fill.svg | 2 + public/assets/js/position-filter.js | 44 ++++ 13 files changed, 653 insertions(+) create mode 100644 app/components/position-card.php create mode 100644 app/pages/mitmachen.php create mode 100644 data/mitmachen.json create mode 100644 public/assets/icons/check2.svg create mode 100644 public/assets/icons/clipboard-check.svg create mode 100644 public/assets/icons/clock-history.svg create mode 100644 public/assets/icons/cup-hot.svg create mode 100644 public/assets/icons/heart.svg create mode 100644 public/assets/icons/mortarboard-fill.svg create mode 100644 public/assets/js/position-filter.js diff --git a/app/components/position-card.php b/app/components/position-card.php new file mode 100644 index 0000000..b3c89a3 --- /dev/null +++ b/app/components/position-card.php @@ -0,0 +1,64 @@ +/ → ohne JS bedienbar (FAQ-Mechanik). Eingeklappt zeigt die + * Karte nur Bereich + Titel; Details und Ansprechpartner erscheinen beim Aufklappen. + * Das JobPosting-Schema liefert die Seite über $meta['schema'] (helpers: job_posting_schema). + * Heading-Outline: Titel h3 (in der summary), Ansprechpartner (person-card) h4. + */ +$contact = $contact ?? null; +?> +
+ + + + + + + + +

+
+ +
+
+
+ +

+ + +
    + +
  • + +
+ + +
+ +
+
Zeitaufwand
+
+
+ + +
+
Das brauchst du
+
+
+ +
+ +
+ +
+ $contact, 'level' => 'h4']); ?> +
+ +
+
diff --git a/app/helpers.php b/app/helpers.php index b4f03fc..96914bc 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -324,3 +324,51 @@ function sportsevent_nodes(array $upcoming): array } return $nodes; } + +/** + * JobPosting-Knoten für eine Ehrenamtsstelle (Seite /mitmachen). $job: Eintrag aus + * data/mitmachen.json → positions[]; $pageSlug: Seiten-Slug für die Anker-URL. + * employmentType VOLUNTEER; hiringOrganization verweist via #club auf den Org-Knoten; + * jobLocation = Vereinsadresse aus club.json (Single Source). validThrough bewusst + * optional — ein abgelaufenes Datum entfernt die Anzeige aktiv aus den Ergebnissen, + * deshalb nur bei echt befristeten Stellen setzen. Ohne title → leerer Array (Aufrufer filtert). + */ +function job_posting_schema(array $job, string $pageSlug): array +{ + if (empty($job['title'])) { + return []; + } + $club = json_load('club'); + $node = [ + '@type' => 'JobPosting', + 'title' => $job['title'], + 'description' => $job['description'] ?? ($job['summary'] ?? $job['title']), + 'employmentType' => 'VOLUNTEER', + 'hiringOrganization' => ['@id' => abs_url() . '#club'], + 'jobLocation' => [ + '@type' => 'Place', + 'address' => [ + '@type' => 'PostalAddress', + 'streetAddress' => $club['address']['street'], + 'postalCode' => $club['address']['zip'], + 'addressLocality' => $club['address']['city'], + 'addressCountry' => $club['address']['country'], + ], + ], + ]; + if (!empty($job['id'])) { + $node['identifier'] = [ + '@type' => 'PropertyValue', + 'name' => $club['name'], + 'value' => $job['id'], + ]; + $node['url'] = abs_url($pageSlug) . '#' . $job['id']; + } + if (!empty($job['posted'])) { + $node['datePosted'] = $job['posted']; + } + if (!empty($job['valid_through'])) { + $node['validThrough'] = $job['valid_through']; + } + return $node; +} diff --git a/app/pages/mitmachen.php b/app/pages/mitmachen.php new file mode 100644 index 0000000..c00e09a --- /dev/null +++ b/app/pages/mitmachen.php @@ -0,0 +1,91 @@ + 'Mitmachen & Ehrenamt', + 'description' => 'Werde ehrenamtlicher Helfer beim TSV 1908 Kulmbach: Jugendtrainer, Betreuer und Unterstützung bei Veranstaltungen. Offene Aufgaben mit direktem Ansprechpartner – ohne Vorerfahrung.', + 'og_image' => 'img/pages/jugendfussball-hero-1440.jpg', + 'scripts' => ['position-filter.js'], + 'schema' => array_merge( + page_schema( + [ + ['name' => 'Startseite', 'slug' => ''], + ['name' => 'Mitmachen', 'slug' => 'mitmachen'], + ], + $page['faq'] ?? [] + ), + array_values(array_filter(array_map( + static fn (array $job): array => job_posting_schema($job, 'mitmachen'), + $page['positions'] ?? [] + ))) + ), +]; + +component('hero', ['hero' => $page['hero']]); +?> +
+
+

Offene Stellen

+ +
+ $f): ?> + + +
+ +
+ + $pos, 'contact' => $contact_for($pos)]); ?> + +
+ +
+
+ + + + $page['cta'], 'anchor' => 'kontakt-mitmachen']); + +component('faq', ['faq' => $page['faq'] ?? []]); diff --git a/app/routes.php b/app/routes.php index e1cc943..c826fa4 100644 --- a/app/routes.php +++ b/app/routes.php @@ -23,6 +23,7 @@ return [ 'turnen/step-aerobic-fitness-pilates' => ['file' => 'turnen-step-aerobic.php', 'sitemap' => true], 'turnen/trampolin' => ['file' => 'turnen-trampolin.php', 'sitemap' => true], 'turnen/wettkampfturnen' => ['file' => 'turnen-wettkampfturnen.php', 'sitemap' => true], + 'mitmachen' => ['file' => 'mitmachen.php', 'sitemap' => true], 'verein' => ['file' => 'verein.php', 'sitemap' => true], 'historie' => ['file' => 'historie.php', 'sitemap' => true], 'sportheimbuchung' => ['file' => 'sportheimbuchung.php', 'sitemap' => true], diff --git a/data/mitmachen.json b/data/mitmachen.json new file mode 100644 index 0000000..071081a --- /dev/null +++ b/data/mitmachen.json @@ -0,0 +1,129 @@ +{ + "hero": { + "compact": true, + "align": "bottom-left", + "title": "Werde Teil des Teams", + "text": "Ein Verein lebt von Menschen, die anpacken – auf dem Platz, in der Halle, am Kiosk und hinter den Kulissen. Egal ob ein paar Stunden im Jahr oder regelmäßig: Hier findest du, wo wir dich brauchen. Keine Vorerfahrung nötig, wir lernen dich ein.", + "image": { + "base": "img/pages/jugendfussball-hero", + "widths": [640, 1000, 1440], + "alt": "Jugendmannschaft des TSV 08 Kulmbach beim Training auf dem Sportplatz" + } + }, + "filters": [ + { "value": "all", "label": "Alle" }, + { "value": "fussball", "label": "Fußball" }, + { "value": "turnen", "label": "Turnen" } + ], + "positions": [ + { + "id": "jugendtrainer", + "group": "fussball", + "area": "Jugendfußball", + "icon": "people-fill", + "title": "Jugendtrainer:in & Co-Trainer:in", + "summary": "Begleite eine unserer Jugendmannschaften beim Training und an Spieltagen. Du gibst Kindern und Jugendlichen Freude an der Bewegung, am Teamgeist und am Fußball – fachliche Unterstützung und Trainerlizenzen ermöglichen wir dir über den Verein.", + "description": "Als Jugendtrainer:in oder Co-Trainer:in betreust du eine feste Altersgruppe beim TSV 08 Kulmbach. Du planst und leitest die Trainingseinheiten, begleitest dein Team zu den Punktspielen und bist Ansprechpartner für Kinder und Eltern. Erfahrung im Fußball ist hilfreich, aber kein Muss – wichtiger ist, dass du gerne mit jungen Menschen arbeitest. Trainerlizenzen und Schulungen über den Bayerischen Fußball-Verband übernimmt der Verein.", + "tasks": [ + "Trainingseinheiten für deine Altersgruppe planen und durchführen", + "Dein Team an Spieltagen begleiten", + "Ansprechpartner für Kinder und Eltern sein" + ], + "commitment": "1–2 Trainings pro Woche + Spiele am Wochenende", + "requirements": ["Freude an der Arbeit mit Kindern", "kein Trainerschein nötig – wir bilden dich aus"], + "posted": "2026-06-01", + "contact_name": "Fabian Früh" + }, + { + "id": "jugend-betreuer", + "group": "fussball", + "area": "Jugendfußball", + "icon": "clipboard-check", + "title": "Betreuer:in für Jugendteams", + "summary": "Du musst nicht selbst trainieren, um zu helfen: Als Betreuer:in kümmerst du dich um die Organisation rund ums Team – Fahrgemeinschaften, Trikots, Termine und ein offenes Ohr für die Kinder.", + "description": "Betreuerinnen und Betreuer halten einer Jugendmannschaft den Rücken frei. Du organisierst Fahrten zu Auswärtsspielen, kümmerst dich um Trikots und Ausrüstung, koordinierst Termine mit den Eltern und bist an Spieltagen vor Ort. Eine fußballerische Ausbildung brauchst du dafür nicht – nur Zuverlässigkeit und Spaß am Organisieren.", + "tasks": [ + "Fahrgemeinschaften und Termine koordinieren", + "Trikots und Ausrüstung im Blick behalten", + "An Spieltagen das Team unterstützen" + ], + "commitment": "flexibel, v. a. an Spieltagen", + "requirements": ["Zuverlässigkeit", "Organisationstalent"], + "posted": "2026-06-01", + "contact_name": "Kerstin Tauber" + }, + { + "id": "event-verkauf", + "group": "verein", + "area": "Veranstaltungen & Feste", + "icon": "cup-hot", + "title": "Unterstützung beim Verkauf bei Events", + "summary": "Sportfest, Heimspiele, Feiern: Bei unseren Veranstaltungen brauchen wir Hände hinter Theke und Grill – Getränke und Speisen verkaufen, kassieren, mit anpacken. Schon ein einzelner Einsatz hilft, die Schichten teilen wir fair auf.", + "description": "Rund um Sportfest, Heimspiele und Vereinsfeste sorgt unser Verkaufsteam für das leibliche Wohl: Getränke ausschenken, Bratwürste grillen, Kaffee und Kuchen verkaufen, kassieren und beim Auf- und Abbau helfen. Die Einsätze laufen in Schichten, die wir gemeinsam einteilen – du kannst dich so oft oder so selten eintragen, wie es dir passt. Einarbeitung und ein eingespieltes Team gibt es dazu.", + "tasks": [ + "Getränke und Speisen verkaufen und kassieren", + "Grill und Theke mitbetreuen", + "Beim Auf- und Abbau anpacken" + ], + "commitment": "einzelne Schichten bei Veranstaltungen", + "requirements": ["Volljährigkeit für den Ausschank", "Zuverlässigkeit"], + "posted": "2026-06-01", + "contact": { + "department": "Veranstaltungen & Feste", + "name": "Vorstandschaft TSV 08", + "email": "info@tsv08kulmbach.de" + } + } + ], + "arguments": { + "title": "Warum mithelfen?", + "intro": "Ehrenamt beim TSV 08 ist mehr als ein Gefallen – es bringt auch dir etwas.", + "items": [ + { + "icon": "people-fill", + "title": "Echte Gemeinschaft", + "text": "Du wirst Teil eines eingespielten Teams, lernst Leute aus Kulmbach kennen und erlebst Vereinsleben von innen." + }, + { + "icon": "mortarboard-fill", + "title": "Qualifikation inklusive", + "text": "Trainer- und Übungsleiter-Lizenzen sowie Schulungen über die Verbände übernimmt der Verein – du sammelst nebenbei echte Qualifikationen." + }, + { + "icon": "clock-history", + "title": "Du bestimmst den Umfang", + "text": "Ob einzelne Schicht beim Sportfest oder feste Aufgabe im Training: Du hilfst genau so viel, wie es in dein Leben passt." + }, + { + "icon": "heart", + "title": "Wirkung vor Ort", + "text": "Dein Einsatz hält Kinder- und Jugendsport, Turnen und das Vereinsleben in Kulmbach lebendig – Generation für Generation." + } + ] + }, + "cta": { + "title": "Du weißt noch nicht, wo du am besten passt?", + "text": "Kein Problem. Schreib uns kurz, was dich interessiert und wie viel Zeit du hast – wir finden gemeinsam die passende Aufgabe für dich.", + "ctas": [ + { "label": "Jetzt Kontakt aufnehmen", "slug": "", "hash": "kontakt", "style": "primary" } + ] + }, + "faq": [ + { + "q": "Muss ich Vereinsmitglied sein, um zu helfen?", + "a": "Nein. Viele Aufgaben – etwa die Unterstützung beim Verkauf bei Veranstaltungen – kannst du auch ohne Mitgliedschaft übernehmen. Für regelmäßige Tätigkeiten als Trainer:in oder Betreuer:in ist eine Mitgliedschaft sinnvoll, aber keine Voraussetzung für ein erstes Gespräch." + }, + { + "q": "Brauche ich Vorerfahrung oder eine Lizenz?", + "a": "Für die meisten Aufgaben nicht. Wo eine Lizenz nötig ist – etwa als Jugendtrainer:in – ermöglicht und bezahlt der Verein die entsprechenden Schulungen über den Bayerischen Fußball-Verband. Du wirst nie ins kalte Wasser geworfen." + }, + { + "q": "Wie viel Zeit muss ich investieren?", + "a": "Das hängt von der Aufgabe ab und steht bei jeder Stelle dabei. Manche Einsätze sind einzelne Schichten oder Tage im Jahr, andere eine feste Stunde pro Woche. Wir richten uns nach dem, was bei dir möglich ist." + }, + { + "q": "Wie melde ich mich?", + "a": "Bei jeder Stelle findest du einen direkten Ansprechpartner mit Telefonnummer oder E-Mail – melde dich einfach dort. Wenn du noch unsicher bist, schreib uns über das Kontaktformular, dann finden wir gemeinsam die passende Aufgabe." + } + ] +} diff --git a/public/assets/css/components.css b/public/assets/css/components.css index bf49752..714a2ca 100644 --- a/public/assets/css/components.css +++ b/public/assets/css/components.css @@ -713,6 +713,270 @@ text-underline-offset: 0.25em; } +/* === mitmachen / positions (Ehrenamt-Stellen) === */ +/* Filter-Stripe (Alle/Fußball/Turnen) — Unterstrich-Tabbar wie group-tabs */ +.filter-bar { + display: flex; + flex-wrap: wrap; + margin-bottom: 2rem; +} + +.filter-bar__tab { + padding: 0.65rem 1.2rem; + background: none; + border: none; + box-shadow: inset 0 -2px 0 var(--clr-glass-border); + font-family: var(--ff-body); + font-size: var(--fs-500); + color: var(--clr-text-muted); + cursor: pointer; + white-space: nowrap; + transition: color var(--transition), box-shadow var(--transition); +} + +.filter-bar__tab:first-child { + padding-inline-start: 0; +} + +.filter-bar__tab:hover { + color: var(--clr-text); +} + +.filter-bar__tab[aria-pressed="true"] { + color: var(--clr-text); + box-shadow: inset 0 -2px 0 var(--clr-accent); +} + +.filter-bar__tab:focus-visible { + outline: 2px solid var(--clr-accent); + outline-offset: -2px; +} + +.positions__list { + display: grid; + gap: 1.5rem; +} + +.positions__empty { + margin: 0; + padding: 2.5rem 0; + max-width: 60ch; + color: var(--clr-text-muted); + font-size: var(--fs-500); + line-height: var(--lh-body); +} + +.positions__empty[hidden] { + display: none; +} + +.positions__empty a { + color: var(--clr-text); + text-decoration: underline; + text-underline-offset: 0.2em; +} + +/* "Warum mithelfen?": geteiltes sponsor-arguments-Raster, hier mit Überschrift + (nur auf dieser Seite — Partner-/Sportheim-Variante bleibt ohne Heading unberührt) */ +#warum-mithelfen > .container { + text-align: center; +} + +#warum-mithelfen .sponsor-arguments__intro { + max-width: 60ch; + margin: 1rem auto 0; + color: var(--clr-text-muted); +} + +#warum-mithelfen .sponsor-arguments__grid { + margin-top: 2.5rem; + text-align: left; +} + +/* Aufklappbare Stelle (details/summary, FAQ-Mechanik): eingeklappt nur Bereich + Titel */ +.position-card { + background: var(--clr-glass-bg); + border: 1px solid var(--clr-glass-border); + border-radius: var(--radius-card); + overflow: hidden; + transition: border-color var(--transition); +} + +.position-card[open] { + border-color: var(--clr-glass-border-strong, var(--clr-glass-border)); +} + +/* display:grid o. Ä. würde sonst [hidden]{display:none} überschreiben — explizit halten */ +.position-card[hidden] { + display: none; +} + +/* Klickzeile: Icon + Bereich/Titel + Chevron */ +.position-card__head { + display: flex; + align-items: center; + gap: 1rem; + padding: 1.25rem 1.5rem; + cursor: pointer; + list-style: none; + transition: background var(--transition); +} + +.position-card__head::-webkit-details-marker { + display: none; +} + +.position-card__head:hover { + background: rgb(255 255 255 / 0.04); +} + +.position-card__head:focus-visible { + outline: 2px solid var(--clr-accent); + outline-offset: -2px; +} + +.position-card__icon { + flex-shrink: 0; + width: 2.5rem; + height: 2.5rem; + background: var(--clr-accent); + border-radius: 8px; + display: grid; + place-items: center; + font-size: 1.15rem; + color: #fff; +} + +.position-card__heading { + flex: 1; + min-width: 0; +} + +.position-card__area { + display: block; + font-family: var(--ff-body); + font-size: var(--fs-300); + font-weight: var(--fw-bold); + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--clr-text-muted); + margin: 0 0 0.2rem; +} + +.position-card__title { + font-family: var(--ff-heading); + font-size: var(--fs-675); + text-transform: uppercase; + line-height: var(--lh-heading); + margin: 0; +} + +.position-card__chevron { + flex: none; + font-size: 0.9rem; + color: var(--clr-text-muted); + transition: transform var(--transition), color var(--transition); +} + +.position-card[open] .position-card__chevron { + transform: rotate(180deg); + color: var(--clr-text); +} + +/* Aufgeklappter Inhalt: Detailspalte + Ansprechpartner */ +.position-card__body { + display: grid; + grid-template-columns: 1fr minmax(0, 16rem); + gap: 2.5rem; + align-items: start; + padding: 0 1.5rem 1.5rem 5rem; + animation: faq-reveal 260ms ease; +} + +.position-card__main > :first-child { + margin-top: 0; +} + +.position-card__summary { + color: var(--clr-text-muted); + font-size: var(--fs-500); + line-height: var(--lh-body); + margin: 0; +} + +.position-card__tasks { + list-style: none; + padding: 0; + margin: 1.25rem 0 0; + display: flex; + flex-direction: column; + gap: 0.625rem; +} + +.position-card__tasks li { + display: flex; + align-items: flex-start; + gap: 0.625rem; + font-size: var(--fs-500); + line-height: var(--lh-body); +} + +.position-card__check { + flex-shrink: 0; + margin-top: 0.2em; + font-size: 1rem; + color: var(--clr-accent); +} + +.position-card__meta { + margin: 1.25rem 0 0; + display: grid; + gap: 0.75rem; +} + +.position-card__meta-row { + display: grid; + gap: 0.2rem; +} + +.position-card__meta dt { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-size: var(--fs-300); + font-weight: var(--fw-bold); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--clr-text-muted); +} + +.position-card__meta-icon { + color: var(--clr-accent); +} + +.position-card__meta dd { + margin: 0; + font-size: var(--fs-500); + line-height: var(--lh-body); +} + +/* Kompakte Kontakt-Karte: flacheres Foto als im Ansprechpartner-Grid */ +.position-card__contact .person-card__media img { + aspect-ratio: 4 / 3.4; +} + +@media (max-width: 768px) { + .position-card__body { + grid-template-columns: 1fr; + gap: 1.5rem; + padding: 0 1.5rem 1.5rem; + } + + .position-card__contact { + max-width: 18rem; + } +} + /* === contact form === */ .contact__inner { display: grid; diff --git a/public/assets/icons/check2.svg b/public/assets/icons/check2.svg new file mode 100644 index 0000000..79cf6e8 --- /dev/null +++ b/public/assets/icons/check2.svg @@ -0,0 +1 @@ + diff --git a/public/assets/icons/clipboard-check.svg b/public/assets/icons/clipboard-check.svg new file mode 100644 index 0000000..4b107ca --- /dev/null +++ b/public/assets/icons/clipboard-check.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/clock-history.svg b/public/assets/icons/clock-history.svg new file mode 100644 index 0000000..ad24532 --- /dev/null +++ b/public/assets/icons/clock-history.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/cup-hot.svg b/public/assets/icons/cup-hot.svg new file mode 100644 index 0000000..86e6a25 --- /dev/null +++ b/public/assets/icons/cup-hot.svg @@ -0,0 +1,2 @@ + + diff --git a/public/assets/icons/heart.svg b/public/assets/icons/heart.svg new file mode 100644 index 0000000..0a488d8 --- /dev/null +++ b/public/assets/icons/heart.svg @@ -0,0 +1 @@ + diff --git a/public/assets/icons/mortarboard-fill.svg b/public/assets/icons/mortarboard-fill.svg new file mode 100644 index 0000000..d63fcbb --- /dev/null +++ b/public/assets/icons/mortarboard-fill.svg @@ -0,0 +1,2 @@ + + diff --git a/public/assets/js/position-filter.js b/public/assets/js/position-filter.js new file mode 100644 index 0000000..253e517 --- /dev/null +++ b/public/assets/js/position-filter.js @@ -0,0 +1,44 @@ +(function () { + 'use strict'; + + // Filtert die Stellen-Karten (/mitmachen) nach Bereich. Ohne JS sind alle Karten + // sichtbar (Default-Filter "all"), der Empty-State bleibt verborgen — die Tabs + // sind reine Progressive Enhancement. + function init(bar) { + var section = bar.closest('section'); + if (!section) { return; } + + var buttons = [].slice.call(bar.querySelectorAll('[data-filter]')); + var cards = [].slice.call(section.querySelectorAll('.position-card')); + var empty = section.querySelector('[data-position-empty]'); + if (!buttons.length || !cards.length) { return; } + + function apply(filter) { + var visible = 0; + cards.forEach(function (card) { + var match = filter === 'all' || card.getAttribute('data-group') === filter; + card.hidden = !match; + if (match) { visible++; } + }); + if (empty) { empty.hidden = visible !== 0; } + } + + buttons.forEach(function (btn, i) { + btn.addEventListener('click', function () { + buttons.forEach(function (b) { b.setAttribute('aria-pressed', 'false'); }); + btn.setAttribute('aria-pressed', 'true'); + apply(btn.getAttribute('data-filter')); + }); + btn.addEventListener('keydown', function (e) { + var next = null; + if (e.key === 'ArrowRight') { next = (i + 1) % buttons.length; } + if (e.key === 'ArrowLeft') { next = (i - 1 + buttons.length) % buttons.length; } + if (e.key === 'Home') { next = 0; } + if (e.key === 'End') { next = buttons.length - 1; } + if (next !== null) { e.preventDefault(); buttons[next].focus(); } + }); + }); + } + + document.querySelectorAll('[data-position-filter]').forEach(init); +}());