From 48466999c0eaa89a6553d25238d90ef1800ab217 Mon Sep 17 00:00:00 2001
From: Serverlele30 <46974281+Serverlele30@users.noreply.github.com>
Date: Fri, 10 Jul 2026 12:16:47 +0200
Subject: [PATCH] Add files via upload
---
CHANGELOG.md | 15 ++++++
index.html | 41 ++++++++++++++---
js/api.js | 2 +-
js/app.js | 103 +++++++++++++++++++++++++++++++++++++++++-
js/extras.js | 20 ++++++--
js/transitous.js | 56 ++++++++++++++++++++++-
package.json | 2 +-
service-worker.js | 2 +-
styles.css | 48 ++++++++++++++++++++
tests/adapter.test.js | 30 ++++++++++++
tests/smoke.test.js | 6 +++
11 files changed, 307 insertions(+), 18 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7efd6a..568180e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,21 @@ Alle wichtigen Änderungen an diesem Projekt werden in dieser Datei dokumentiert
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/),
und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
+## [37.5.0] - 2026-07-08
+
+### Klickbare Kacheln + die restlichen Feature-Ideen
+
+#### Added - Routenplaner
+- **Via-Station**: Optionales Feld "Über" mit Autocomplete - Verbindungen laufen dann über diese Zwischenstation (API akzeptiert nur Stop-IDs, der Adapter löst Namen automatisch auf).
+- **Barrierefrei-Modus** (♿): nutzt das Rollstuhl-Profil für Fußwege und Umstiege.
+- **Fahrrad-Mitnahme** (🚲): nur Verbindungen, in denen alle Fahrten Radmitnahme erlauben.
+- **Ticketpreise (experimentell)**: withFares ist aktiviert; liefert der Feed Preisdaten, erscheint ein grünes Preis-Badge an der Verbindung. Defensiv gebaut - ohne Daten erscheint schlicht nichts. (Die MOTIS-Spec markiert das Feature selbst als experimentell.)
+- **Losgeh-Erinnerung** (🔔): Button an jeder Verbindung, Notification 10 min vor Abfahrt. Ehrliche Einschränkung: funktioniert nur, solange die App/der Tab offen ist - echte Push-Benachrichtigungen bräuchten einen Server.
+
+#### Changed
+- **Alle vier Home-Kacheln sind jetzt klickbar** und öffnen ihre View (vorher nur die Entwickler-Kachel); einheitlich über data-view gelöst, der Spezial-Handler der Dev-Kachel ist raus.
+- **API-Budget-Anzeige im Entwickler-Tab**: zeigt live "X/90 Requests frei (letzte 60s)".
+
## [37.4.0] - 2026-07-08
### Favoriten-Routen, Standort auf der Karte, smarterer Geocode
diff --git a/index.html b/index.html
index 301ad17..495e6bc 100644
--- a/index.html
+++ b/index.html
@@ -111,28 +111,31 @@
-
+
-
+
+
-
+
+
-
+
+
+
+
+
+ ×
+
+
+
+
+
+
+
+
+ ♿ Barrierefrei
+ 🚲 Fahrrad dabei
+
+
+
@@ -349,6 +375,7 @@
Version:
Release:
+
API-Budget:
–
Lizenz:
MIT
Status:
✅ Stabil
diff --git a/js/api.js b/js/api.js
index d9587c0..4d4740a 100644
--- a/js/api.js
+++ b/js/api.js
@@ -7,7 +7,7 @@
// ZENTRALE VERSIONSNUMMER - einzige Stelle, die bei Releases
// angepasst wird. Alle Anzeigen (Startseite, Entwickler-Tab)
// lesen von hier; package.json + SW-Cache-Name manuell mitziehen.
- const APP_VERSION = '37.4.0';
+ const APP_VERSION = '37.5.0';
const APP_RELEASE_DATE = '07.07.2026';
// Seit v35: Transitous (api.transitous.org) ist die einzige
diff --git a/js/app.js b/js/app.js
index 0171dd0..2bef232 100644
--- a/js/app.js
+++ b/js/app.js
@@ -1097,6 +1097,8 @@
// Journey Planner State
let journeyFromStation = null;
let journeyToStation = null;
+ let journeyViaStation = null; // optionale Zwischenstation
+ const journeyOptions = { wheelchair: false, bike: false };
// Journey Autocomplete
let journeyFromTimeout = null;
@@ -1174,6 +1176,38 @@
checkJourneySearchReady();
});
+ // Via-Station (optional - beeinflusst den Such-Button nicht)
+ const journeyVia = document.getElementById('journeyVia');
+ const suggestionsVia = document.getElementById('suggestionsVia');
+ const clearViaBtn = document.getElementById('clearViaBtn');
+ if (journeyVia && suggestionsVia) {
+ setupJourneyAutocomplete(journeyVia, suggestionsVia, (id, name, lat, lon) => {
+ journeyViaStation = { id, name, lat, lon };
+ });
+ journeyVia.addEventListener('input', () => {
+ // Freitext ohne Auswahl gilt nicht als Via
+ if (journeyVia.value === '') journeyViaStation = null;
+ });
+ if (clearViaBtn) {
+ clearViaBtn.addEventListener('click', () => {
+ journeyVia.value = '';
+ journeyViaStation = null;
+ suggestionsVia.classList.remove('active');
+ });
+ }
+ }
+
+ // Options-Chips (Barrierefrei / Fahrrad) togglen
+ document.addEventListener('click', (e) => {
+ const chip = e.target.closest('.journey-option-chip');
+ if (!chip) return;
+ const opt = chip.dataset.option;
+ journeyOptions[opt] = !journeyOptions[opt];
+ chip.classList.toggle('active', journeyOptions[opt]);
+ chip.setAttribute('aria-pressed', String(journeyOptions[opt]));
+ if (navigator.vibrate) navigator.vibrate(5);
+ });
+
// Clear buttons
clearFromBtn.addEventListener('click', () => {
journeyFrom.value = '';
@@ -1279,6 +1313,56 @@
// Cursor der letzten Suche (für "Früher"/"Später"-Blättern)
let journeyPageCursors = { prev: null, next: null };
+ // Losgeh-Erinnerung: Notification 10 min vor Abfahrt.
+ // EHRLICHE EINSCHRÄNKUNG: funktioniert nur, solange die App/der Tab
+ // offen ist (echte Push-Benachrichtigungen bräuchten einen Server).
+ const REMINDER_LEAD_MIN = 10;
+
+ async function scheduleReminder(btn) {
+ const depTime = new Date(btn.dataset.dep);
+ const fireAt = depTime.getTime() - REMINDER_LEAD_MIN * 60000;
+ const delay = fireAt - Date.now();
+
+ if (delay <= 0) {
+ btn.textContent = '⏰ zu knapp';
+ setTimeout(() => { btn.textContent = '🔔'; }, 2500);
+ return;
+ }
+
+ if (!('Notification' in window)) {
+ btn.textContent = '🚫';
+ btn.title = 'Benachrichtigungen werden von diesem Browser nicht unterstützt';
+ return;
+ }
+
+ let permission = Notification.permission;
+ if (permission === 'default') {
+ permission = await Notification.requestPermission();
+ }
+ if (permission !== 'granted') {
+ btn.textContent = '🚫';
+ btn.title = 'Benachrichtigungen sind blockiert (Browser-Einstellungen)';
+ return;
+ }
+
+ const label = btn.dataset.label;
+ setTimeout(() => {
+ try {
+ new Notification('🚇 Zeit loszugehen!', {
+ body: `${label} – Abfahrt in ${REMINDER_LEAD_MIN} Minuten`,
+ tag: 'vbb-reminder-' + btn.dataset.dep
+ });
+ if (navigator.vibrate) navigator.vibrate([100, 50, 100]);
+ } catch (e) { /* Tab evtl. eingefroren */ }
+ }, delay);
+
+ const fireTime = new Date(fireAt).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
+ btn.textContent = `🔔 ${fireTime} ✓`;
+ btn.classList.add('active');
+ btn.title = `Erinnerung um ${fireTime} gesetzt (solange die App offen bleibt)`;
+ if (navigator.vibrate) navigator.vibrate(10);
+ }
+
async function runJourneySearch(pageCursor = null) {
journeyContainer.innerHTML = '