From 278813fba678d0ea9b7b62a7fc116eb391252d51 Mon Sep 17 00:00:00 2001 From: Florian Zumpe Date: Tue, 16 Jun 2026 15:14:46 +0200 Subject: [PATCH] Add all routes as selection --- public/css/app.css | 71 ++++++++++++++++++++++++ public/index.html | 23 +++++++- public/js/app.js | 101 +++++++++++++++++++++++++++++----- public/js/bootstrap-loader.js | 1 + 4 files changed, 180 insertions(+), 16 deletions(-) diff --git a/public/css/app.css b/public/css/app.css index e2bcf41..479866c 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -106,6 +106,70 @@ button, audio { background: var(--notice); } +.route-section + .route-section { + margin-top: 2rem; + padding-top: 1.25rem; + border-top: 1px solid #c8d3ca; +} + +.section-heading-row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(14rem, 22rem); + gap: 1rem; + align-items: end; + margin-bottom: .75rem; +} + +.section-heading-row h2 { + margin: 0; +} + +.route-search-controls label { + display: block; + margin-bottom: .35rem; + color: var(--muted); + font-size: .9rem; + font-weight: 600; +} + +.route-search-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: .35rem; +} + +#route-search { + width: 100%; + min-height: 2.5rem; + box-sizing: border-box; + padding: .5rem .65rem; + border: 1px solid #aebcaf; + border-radius: .3rem; + background: var(--surface); + color: var(--text); +} + +#route-search:focus { + outline: .2rem solid var(--focus); + outline-offset: .1rem; +} + +#clear-route-search.ui-button { + margin: 0; +} + +.route-list-summary { + margin: 0 0 .75rem; + color: var(--muted); + font-size: .9rem; +} + +.ui-autocomplete { + max-height: 16rem; + overflow-y: auto; + overflow-x: hidden; +} + .card-list { display: grid; gap: .75rem; @@ -259,6 +323,13 @@ button, audio { outline-offset: .12rem; } +@media (max-width: 620px) { + .section-heading-row { + grid-template-columns: 1fr; + align-items: stretch; + } +} + @media (max-width: 420px) { .route-facts { grid-template-columns: 1fr; diff --git a/public/index.html b/public/index.html index cc59ffd..0c14e34 100644 --- a/public/index.html +++ b/public/index.html @@ -34,8 +34,27 @@
Standort wird ermittelt.
-

Routen in deiner Nähe

- + +
+

Routen in deiner Nähe

+
    +
    + +
    +
    +

    Alle Routen

    +
    + +
    + + +
    +
    +
    +

    +
      +
      diff --git a/public/js/app.js b/public/js/app.js index 4cd0694..5067d17 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -30,14 +30,30 @@ else if (previous !== pageSelector) history.pushState(historyState, '', pageSelector); } - function renderRoutes() { - const list = $('#route-list').empty(); - if (!state.routes.length) { - list.append('
    • Keine Route in der Nähe gefunden.
    • '); + function normalizeSearch(value) { + return String(value ?? '') + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, '') + .toLocaleLowerCase('de'); + } + + function routeMatchesSearch(route, searchTerm) { + if (!searchTerm) return true; + return normalizeSearch([ + route.name, + route.schoolName, + route.description + ].filter(Boolean).join(' ')).includes(searchTerm); + } + + function renderRouteItems(listSelector, routes, emptyText) { + const list = $(listSelector).empty(); + if (!routes.length) { + list.append(`
    • ${escapeHtml(emptyText)}
    • `); return; } - state.routes.forEach(route => { + routes.forEach(route => { const proximity = route.proximityM == null ? '' : `${escapeHtml(ns.Distance.format(route.proximityM))} entfernt`; @@ -53,6 +69,54 @@ }); } + function renderRoutes() { + const radiusM = ns.Config.routeRadiusKm * 1000; + const nearbyRoutes = state.position + ? state.routes + .filter(route => route.proximityM != null && route.proximityM <= radiusM) + .sort((a, b) => a.proximityM - b.proximityM) + : []; + + const searchTerm = normalizeSearch($('#route-search').val()); + const allRoutes = state.routes + .filter(route => routeMatchesSearch(route, searchTerm)) + .slice() + .sort((a, b) => a.name.localeCompare(b.name, 'de')); + + const nearbyEmptyText = state.position + ? `Keine Route im Umkreis von ${ns.Config.routeRadiusKm} km gefunden. Wähle unten eine beliebige Route aus.` + : 'Für Empfehlungen in der Nähe ist ein Standort erforderlich. Alle Routen bleiben unten auswählbar.'; + + renderRouteItems('#nearby-route-list', nearbyRoutes, nearbyEmptyText); + renderRouteItems('#all-route-list', allRoutes, + searchTerm ? 'Keine Route entspricht dem Suchbegriff.' : 'Es sind noch keine Routen vorhanden.'); + + $('#all-routes-summary').text( + searchTerm + ? `${allRoutes.length} von ${state.routes.length} Routen gefunden.` + : `${state.routes.length} Routen verfügbar.` + ); + } + + function initializeRouteSearch() { + const input = $('#route-search'); + input.autocomplete({ + minLength: 0, + source(request, response) { + const term = normalizeSearch(request.term); + response(state.routes + .filter(route => routeMatchesSearch(route, term)) + .slice(0, 12) + .map(route => ({ label: route.name, value: route.name }))); + }, + select(event, ui) { + input.val(ui.item.value); + renderRoutes(); + return false; + } + }); + } + function renderRoute(route) { state.route = route; state.triggered.clear(); @@ -121,6 +185,12 @@ } function bindEvents() { + $(document).on('input', '#route-search', renderRoutes); + + $(document).on('click', '#clear-route-search', function () { + $('#route-search').val('').trigger('input').trigger('focus'); + }); + $(document).on('click', '[data-route-id]', async function () { const button = $(this); button.prop('disabled', true); @@ -178,19 +248,22 @@ try { const position = await ns.Geo.current(); state.position = position; - $('#location-hint').text(`Standort ermittelt (Genauigkeit ${Math.round(position.accuracy)} m).`); - state.routes = state.routes - .map(route => ({ - ...route, - proximityM: route.start ? ns.Distance.meters(position, route.start) : null - })) - .filter(route => route.proximityM == null || route.proximityM <= ns.Config.routeRadiusKm * 1000) - .sort((a, b) => (a.proximityM ?? Number.MAX_VALUE) - (b.proximityM ?? Number.MAX_VALUE)); + $('#location-hint').text( + `Standort ermittelt (Genauigkeit ${Math.round(position.accuracy)} m). ` + + `Nahe Routen werden bis ${ns.Config.routeRadiusKm} km empfohlen.` + ); + state.routes = state.routes.map(route => ({ + ...route, + proximityM: route.start ? ns.Distance.meters(position, route.start) : null + })); } catch (error) { - $('#location-hint').text(`Standort nicht verfügbar: ${error.message}. Es werden alle Routen angezeigt.`); + $('#location-hint').text( + `Standort nicht verfügbar: ${error.message}. Alle Routen können weiterhin ausgewählt werden.` + ); } context.markStep('location', 'done'); + initializeRouteSearch(); renderRoutes(); updateTrackingButton(); } diff --git a/public/js/bootstrap-loader.js b/public/js/bootstrap-loader.js index 4c4c039..6d13d76 100644 --- a/public/js/bootstrap-loader.js +++ b/public/js/bootstrap-loader.js @@ -45,6 +45,7 @@ $('#bootstrap-retry').button({ icon: 'ui-icon-refresh' }); $('.back-button').button({ icon: 'ui-icon-caret-1-w' }); $('#start-tracking').button({ icon: 'ui-icon-pin-s' }); + $('#clear-route-search').button({ icon: 'ui-icon-close', showLabel: false }); $('#slide-prev').button({ icon: 'ui-icon-caret-1-w', showLabel: false }); $('#slide-next').button({ icon: 'ui-icon-caret-1-e', showLabel: false }); $('#slide-controls').controlgroup();