Add all routes as selection
This commit is contained in:
parent
b6f1a0f825
commit
278813fba6
@ -106,6 +106,70 @@ button, audio {
|
|||||||
background: var(--notice);
|
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 {
|
.card-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: .75rem;
|
gap: .75rem;
|
||||||
@ -259,6 +323,13 @@ button, audio {
|
|||||||
outline-offset: .12rem;
|
outline-offset: .12rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
.section-heading-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 420px) {
|
@media (max-width: 420px) {
|
||||||
.route-facts {
|
.route-facts {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@ -34,8 +34,27 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="app-content">
|
<div class="app-content">
|
||||||
<div id="location-hint" class="notice">Standort wird ermittelt.</div>
|
<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>
|
</div>
|
||||||
<footer class="app-footer">GPS-Lernwege</footer>
|
<footer class="app-footer">GPS-Lernwege</footer>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
101
public/js/app.js
101
public/js/app.js
@ -30,14 +30,30 @@
|
|||||||
else if (previous !== pageSelector) history.pushState(historyState, '', pageSelector);
|
else if (previous !== pageSelector) history.pushState(historyState, '', pageSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRoutes() {
|
function normalizeSearch(value) {
|
||||||
const list = $('#route-list').empty();
|
return String(value ?? '')
|
||||||
if (!state.routes.length) {
|
.normalize('NFKD')
|
||||||
list.append('<li class="empty-list">Keine Route in der Nähe gefunden.</li>');
|
.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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.routes.forEach(route => {
|
routes.forEach(route => {
|
||||||
const proximity = route.proximityM == null
|
const proximity = route.proximityM == null
|
||||||
? ''
|
? ''
|
||||||
: `<span class="route-distance">${escapeHtml(ns.Distance.format(route.proximityM))} entfernt</span>`;
|
: `<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) {
|
function renderRoute(route) {
|
||||||
state.route = route;
|
state.route = route;
|
||||||
state.triggered.clear();
|
state.triggered.clear();
|
||||||
@ -121,6 +185,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindEvents() {
|
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 () {
|
$(document).on('click', '[data-route-id]', async function () {
|
||||||
const button = $(this);
|
const button = $(this);
|
||||||
button.prop('disabled', true);
|
button.prop('disabled', true);
|
||||||
@ -178,19 +248,22 @@
|
|||||||
try {
|
try {
|
||||||
const position = await ns.Geo.current();
|
const position = await ns.Geo.current();
|
||||||
state.position = position;
|
state.position = position;
|
||||||
$('#location-hint').text(`Standort ermittelt (Genauigkeit ${Math.round(position.accuracy)} m).`);
|
$('#location-hint').text(
|
||||||
state.routes = state.routes
|
`Standort ermittelt (Genauigkeit ${Math.round(position.accuracy)} m). ` +
|
||||||
.map(route => ({
|
`Nahe Routen werden bis ${ns.Config.routeRadiusKm} km empfohlen.`
|
||||||
...route,
|
);
|
||||||
proximityM: route.start ? ns.Distance.meters(position, route.start) : null
|
state.routes = state.routes.map(route => ({
|
||||||
}))
|
...route,
|
||||||
.filter(route => route.proximityM == null || route.proximityM <= ns.Config.routeRadiusKm * 1000)
|
proximityM: route.start ? ns.Distance.meters(position, route.start) : null
|
||||||
.sort((a, b) => (a.proximityM ?? Number.MAX_VALUE) - (b.proximityM ?? Number.MAX_VALUE));
|
}));
|
||||||
} catch (error) {
|
} 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');
|
context.markStep('location', 'done');
|
||||||
|
initializeRouteSearch();
|
||||||
renderRoutes();
|
renderRoutes();
|
||||||
updateTrackingButton();
|
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' });
|
$('#bootstrap-retry').button({ icon: 'ui-icon-refresh' });
|
||||||
$('.back-button').button({ icon: 'ui-icon-caret-1-w' });
|
$('.back-button').button({ icon: 'ui-icon-caret-1-w' });
|
||||||
$('#start-tracking').button({ icon: 'ui-icon-pin-s' });
|
$('#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-prev').button({ icon: 'ui-icon-caret-1-w', showLabel: false });
|
||||||
$('#slide-next').button({ icon: 'ui-icon-caret-1-e', showLabel: false });
|
$('#slide-next').button({ icon: 'ui-icon-caret-1-e', showLabel: false });
|
||||||
$('#slide-controls').controlgroup();
|
$('#slide-controls').controlgroup();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user