Add all routes as selection
This commit is contained in:
parent
b6f1a0f825
commit
278813fba6
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
1
public/js/bootstrap-loader.js
vendored
1
public/js/bootstrap-loader.js
vendored
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user