Add all routes as selection

This commit is contained in:
Florian Zumpe 2026-06-16 15:14:46 +02:00
parent b6f1a0f825
commit 278813fba6
4 changed files with 180 additions and 16 deletions

View File

@ -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;

View File

@ -34,8 +34,27 @@
</header>
<div class="app-content">
<div id="location-hint" class="notice">Standort wird ermittelt.</div>
<h2>Routen in deiner Nähe</h2>
<ul id="route-list" class="card-list" aria-live="polite"></ul>
<section class="route-section" aria-labelledby="nearby-routes-heading">
<h2 id="nearby-routes-heading">Routen in deiner Nähe</h2>
<ul id="nearby-route-list" class="card-list" aria-live="polite"></ul>
</section>
<section class="route-section" aria-labelledby="all-routes-heading">
<div class="section-heading-row">
<h2 id="all-routes-heading">Alle Routen</h2>
<div class="route-search-controls">
<label for="route-search">Routen durchsuchen</label>
<div class="route-search-row">
<input id="route-search" type="search" autocomplete="off"
placeholder="Name, Schule oder Beschreibung">
<button id="clear-route-search" type="button">Suche leeren</button>
</div>
</div>
</div>
<p id="all-routes-summary" class="route-list-summary" aria-live="polite"></p>
<ul id="all-route-list" class="card-list" aria-live="polite"></ul>
</section>
</div>
<footer class="app-footer">GPS-Lernwege</footer>
</section>

View File

@ -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('<li class="empty-list">Keine Route in der Nähe gefunden.</li>');
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(`<li class="empty-list">${escapeHtml(emptyText)}</li>`);
return;
}
state.routes.forEach(route => {
routes.forEach(route => {
const proximity = route.proximityM == null
? ''
: `<span class="route-distance">${escapeHtml(ns.Distance.format(route.proximityM))} entfernt</span>`;
@ -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 => ({
$('#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
}))
.filter(route => route.proximityM == null || route.proximityM <= ns.Config.routeRadiusKm * 1000)
.sort((a, b) => (a.proximityM ?? Number.MAX_VALUE) - (b.proximityM ?? Number.MAX_VALUE));
}));
} 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();
}

View File

@ -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();