katja's git: oeffisearch

fast and simple tripplanner

commit 73817e3f8294836fed3eeb84c1ff63ad5e36f235
parent 2743c5b459feb0c0acd59d878d003993fb567723
Author: Katja (ctucx) <git@ctu.cx>
Date: Sun, 20 Apr 2025 12:54:17 +0200

improve templates
9 files changed, 413 insertions(+), 321 deletions(-)
M
src/baseView.js
|
58
+++++++++++++++++++++++++---------------------------------
M
src/departuresView.js
|
95
++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
M
src/journeyView.js
|
121
++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
M
src/journeysView.js
|
168
++++++++++++++++++++++++++++++++++++++++++-------------------------------------
M
src/languages.js
|
2
+-
M
src/main.js
|
10
++++------
M
src/searchView.js
|
70
+++++++++++++++++++++++++++++++++++++++-------------------------------
M
src/settingsView.js
|
44
+++++++++++++++++++++++++++-----------------
M
src/tripView.js
|
166
+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
diff --git a/src/baseView.js b/src/baseView.js
@@ -1,4 +1,6 @@
 import { LitElement, html, nothing } from 'lit';
+import { choose } from 'lit/directives/choose.js';
+
 import { t } from './translate.js';
 
 import { baseStyles, flexboxStyles, overlaysStyles, buttonInputStyles, iconStyles } from './styles.js';

@@ -67,40 +69,30 @@ export class BaseView extends LitElement {
 		let overlayContent;
 
 		if (this.overlayState.visible) {
-			switch (this.overlayState.type) {
-				case 'loader':
-					overlayContent = html`<div class="spinner"></div>`;
-					break;
-				case 'dialog':
-					overlayContent = html`
-						<div class="modal dialog">
-							<div class="header flex-row">
-								<h4>${t(this.overlayState.title)}</h4>
-								<div class="icon-close" title="${t('close')}" @click=${this.hideOverlay}></div>
-							</div>
-							<div class="body">${this.overlayState.content}</div>
-						</div>
-					`;
-					break;
-				case 'select':
-					overlayContent = html`
-						<div class="modal select flex-column">
-							${this.overlayState.content.map(item => html`<a class="button color" @click=${item.action}>${t(item.label)}</a>`)}
-							<a class="button color" @click=${this.hideOverlay}>${t('close')}</a>
-						</div>
-					`;
-					break;
-				case 'alert':
-					overlayContent = html`
-						<div class="modal alert" style="overflow:auto">
-							${this.overlayState.content}<br><button class="color" style="float:right" @click=${this.hideOverlay}>OK</button>
+			overlayContent = choose(this.overlayState.type, [
+				[ 'plain',  () => this.overlayState.content ],
+				[ 'loader', () => html`<div class="spinner"></div>` ],
+				[ 'dialog', () => html`
+					<div class="modal dialog">
+						<div class="header flex-row">
+							<h4>${t(this.overlayState.title)}</h4>
+							<div class="icon-close" title="${t('close')}" @click=${this.hideOverlay}></div>
 						</div>
-					`;
-					break;
-				default:
-					overlayContent = this.overlayState.content;
-					break;
-			}
+						<div class="body">${this.overlayState.content}</div>
+					</div>
+				` ],
+				[ 'select', () => html`
+					<div class="modal select flex-column">
+						${this.overlayState.content.map(item => html`<a class="button color" @click=${item.action}>${t(item.label)}</a>`)}
+						<a class="button color" @click=${this.hideOverlay}>${t('close')}</a>
+					</div>
+				` ],
+				[ 'alert', () => html`
+					<div class="modal alert" style="overflow:auto">
+						${this.overlayState.content}<br><button class="color" style="float:right" @click=${this.hideOverlay}>OK</button>
+					</div>
+				` ],
+			]);
 		}
 
 		return [
diff --git a/src/departuresView.js b/src/departuresView.js
@@ -1,4 +1,6 @@
 import { html, nothing } from 'lit';
+import { classMap } from 'lit/directives/class-map.js';
+import { when } from 'lit/directives/when.js';
 import { BaseView } from './baseView.js';
 
 import { sleep, queryBackgroundColor, setThemeColor } from './helpers.js';

@@ -84,48 +86,63 @@ class DeparturesView extends BaseView {
 
 	renderView = () => [
 		html`
-		<div class="header-container">
-			<header>
-				<a class="icon-back ${history.length !== 1 ? '': 'invisible'}" title="${t('back')}" @click=${() => history.back()}></a>
-				<div class="container">
-					${this.viewState !== null ? html`
-					<a class="icon-bahnexpert" href="https://bahn.expert/${encodeURIComponent(this.viewState.name)}"></a>
-					` : nothing}
-					<h3>Departures from ${this.viewState !== null ? this.viewState.name : '...'}</h3>
-				</div>
-				<a class="icon-reload ${!this.isUpdating ? '' : 'spinning'} ${!this.isOffline ? '' : 'invisible'}" title="${t("refresh")}" @click=${this.updateViewState}></a>
-			</header>
-		</div>
+			<div class="header-container">
+				<header>
+					<a class="icon-back ${classMap({ invisible: history.length === 1 })}" title=${t('back')} @click=${() => history.back()}></a>
+					<div class="container">
+						${when(
+							this.viewState,
+							() => html`<a class="icon-bahnexpert" href="https://bahn.expert/${encodeURIComponent(this.viewState.name)}"></a>`
+						)}
+						<h3>Departures from ${when(!this.viewState, () => '...', () => this.viewState.name)}</h3>
+					</div>
+					<a class="icon-reload ${classMap({ spinning: this.isUpdating, invisible: this.isOffline })}" title=${t("refresh")} @click=${this.updateViewState}></a>
+				</header>
+			</div>
 		`,
 
-		this.viewState !== null ? html`
-		<div class="container">
-			<div class="table" style="grid-template-columns: 1fr .5fr 3.5fr 1fr">
-				<div class="row head">
-					<span>Time</span>
-					<span></span>
-					<span></span>
-					<span>${t('platform')}</span>
+		when(!this.viewState,
+			() => when(
+				!this.isOffline,
+				() => html`<div class="spinner"></div>`,
+				() => html`<div class="offline"></div>`
+			),
+			() => html`
+				<div class="container">
+					<div class="table" style="grid-template-columns: 1fr .5fr 3.5fr 1fr">
+						<div class="row head">
+							<span>Time</span>
+							<span></span>
+							<span></span>
+							<span>${t('platform')}</span>
+						</div>
+						${(this.viewState.departures || []).map(departure => html`
+							<a class="row" href="#/t/${this.profile}/${departure.tripId}">
+								<span class=${classMap({ cancelled: departure.cancelled })}>
+									${timeTemplate(departure)}
+								</span>
+								<span class="direction ${classMap({ cancelled: departure.cancelled })}">
+									${departure.line.name}
+								</span>
+								<span class="direction ${classMap({ cancelled: departure.cancelled })}">
+									${when(
+										departure.direction, 
+										() => html` → ${departure.direction}`
+									)}
+								</span>
+								${when(
+									!departure.cancelled,
+									() => html`<span>${platformTemplate(departure)}</span>`,
+									() => html`<span class="cancelled-text">${t('cancelled')}</span>`
+								)}
+							</a>
+						`)}
+						</tbody>
+					</div>
 				</div>
-				${(this.viewState.departures || []).map(departure => html`
-				<a class="row" href="#/t/${this.profile}/${departure.tripId}">
-					<span class="${departure.cancelled ? 'cancelled' : nothing}">${timeTemplate(departure)}</span>
-					<span class="direction ${departure.cancelled ? 'cancelled' : ''}">${departure.line.name}</span>
-					<span class="direction ${departure.cancelled ? 'cancelled' : ''}">${departure.direction ? html` → ${departure.direction}` : nothing}</span>
-					${departure.cancelled ? html`
-					<span class="cancelled-text">${t('cancelled')}</span>
-					` : html`
-					<span>${platformTemplate(departure)}</span>
-					`}
-				</a>
-				`)}
-				</tbody>
-			</div>
-		</div>
-		<footer-component></footer-component>
-		` : !this.isOffline ?
-		html`<div class="spinner"></div>`
-		: html`<div class="offline"></div>`
+				<footer-component></footer-component>
+			`
+		)
 	];
 }
 
diff --git a/src/journeyView.js b/src/journeyView.js
@@ -1,4 +1,6 @@
 import { html, css, nothing } from 'lit';
+import { classMap } from 'lit/directives/class-map.js';
+import { when } from 'lit/directives/when.js';
 import { BaseView } from './baseView.js';
 import { createEvents } from 'ics';
 

@@ -63,45 +65,49 @@ class JourneyView extends BaseView {
 		html`
 			<div class="header-container">
 				<header>
-					<a id="back" class="icon-back ${history.length !== 1 ? '': 'invisible'}" title="${t('back')}" @click=${() => history.back()}></a>
+					<a class="icon-back ${classMap({ invisible: history.length === 1 })}" title=${t('back')} @click=${() => history.back()}></a>
 					<div class="container">
-						<a class="icon-reload ${this.isUpdating ? 'spinning' : ''} ${!this.isOffline ? '' : 'invisible'}" title="${t("refresh")}" @click=${this.refreshJourney}></a>
-						${this.viewState !== null ? html`
-							<h3>
-								<span>${formatPoint(this.viewState.legs[0].origin)}</span>
-								<span>${formatPoint(this.viewState.legs[this.viewState.legs.length - 1].destination)}</span>
-							</h3>
-							<p>
-								<span>${t('duration')}: ${formatDuration(this.viewState.duration)}</span>
-								<span>${t('changes')}: ${this.viewState.changes-1}</span>
-								<span>${t('date')}: ${this.viewState.legs[0].plannedDeparture.formatDate()}</span>
-								${this.settingsState.showPrices && this.viewState.price ? 
-									html`<span> ${t('price')}: ${formatPrice(this.viewState.price)}</span>`
-								: nothing}
-							</p>
-						` : html`
-							<h3>
-								<span>...</span>
-								<span>...</span>
-							</h3>
-							<p>
-								<span>${t('duration')}: ...</span>
-								<span>${t('changes')}: ...</span>
-								<span>${t('date')}: ...</span>
-							</p>
-						`}
+						<a class="icon-reload ${classMap({ spinning: this.isUpdating, invisible: this.isOffline })}" title=${t("refresh")} @click=${this.refreshJourney}></a>
+						${when(!this.viewState,
+							() => html`
+								<h3>
+									<span>...</span>
+									<span>...</span>
+								</h3>
+								<p>
+									<span>${t('duration')}: ...</span>
+									<span>${t('changes')}: ...</span>
+									<span>${t('date')}: ...</span>
+								</p>
+							`,
+							() => html`
+								<h3>
+									<span>${formatPoint(this.viewState.legs[0].origin)}</span>
+									<span>${formatPoint(this.viewState.legs[this.viewState.legs.length - 1].destination)}</span>
+								</h3>
+								<p>
+									<span>${t('duration')}: ${formatDuration(this.viewState.duration)}</span>
+									<span>${t('changes')}: ${this.viewState.changes-1}</span>
+									<span>${t('date')}: ${this.viewState.legs[0].plannedDeparture.formatDate()}</span>
+									${when(
+										this.settingsState.showPrices && this.viewState.price,
+										() => html`<span> ${t('price')}: ${formatPrice(this.viewState.price)}</span>`
+									)}
+								</p>
+							`
+						)}
 					</div>
-					<a class="icon-dots" title="${t("more")}" @click=${this.moreModal}></a>
+					<a class="icon-dots" title=${t("more")} @click=${this.moreModal}></a>
 				</header>
 			</div>
-		`, this.viewState !== null ? html`
-			<div class="container">
-				${this.viewState.legs.map(leg => this.legTemplate(leg))}
-			</div>
-			<footer-component></footer-component>
-		` : html`
-			<div class="spinner"></div>
-		`,
+		`, 
+		when(!this.viewState,
+			() => html`<div class="spinner"></div>`,
+			() => html`
+				<div class="container">${this.viewState.legs.map(leg => this.legTemplate(leg))}</div>
+				<footer-component></footer-component>
+			`
+		)
 	];
 
 	legTemplate = leg => {

@@ -116,16 +122,29 @@ class JourneyView extends BaseView {
 				<div class="card">
 					<div class="train-info flex-center nowrap">
 						<a href="#/t/${this.profile}/${leg.tripId}">${formatLineDisplayName(leg.line)}${!leg.direction ? nothing : html` → ${leg.direction}`}</a>
-						${leg.remarks.length === 0 ? nothing : html`
-							<a class="${leg.remarksIcon}" @click=${() => remarksModal(this, leg.remarks)}></a>
-						`}
+						${when(leg.remarks.length !== 0, () => html`<a class="${leg.remarksIcon}" @click=${() => remarksModal(this, leg.remarks)}></a>`)}
 					</div>
 					<div class="train-info flex-center">
-						${!leg.cancelled                              ? nothing : html`<span class="cancelled-text">${t('cancelled')}</span>`}
-						${formatLineAdditionalName(leg.line) === null ? nothing : html`<span>Trip: ${formatLineAdditionalName(leg.line)}</span>`}
-						${!leg.line.trainType                         ? nothing : html`<span>${t('trainType')}: ${leg.line.trainType}</span>`}
-						${(!leg.arrival && !leg.departure)            ? nothing : html`<span>${t('duration')}: ${formatDuration(leg.arrival - leg.departure)}</span>`}
-						${!leg.loadFactor                             ? nothing : html`<span>${t(`load-${leg.loadFactor}`)}</span>`}
+						${when(
+							leg.cancelled,
+							() => html`<span class="cancelled-text">${t('cancelled')}</span>`
+						)}
+						${when(
+							formatLineAdditionalName(leg.line) !== null,
+							() => html`<span>Trip: ${formatLineAdditionalName(leg.line)}</span>`
+						)}
+						${when(
+							leg.line.trainType,
+							() => html`<span>${t('trainType')}: ${leg.line.trainType}</span>`
+						)}
+						${when(
+							leg.arrival && leg.departure,
+							() => html`<span>${t('duration')}: ${formatDuration(leg.arrival - leg.departure)}</span>`
+						)}
+						${when(
+							leg.loadFactor,
+							() => html`<span>${t(`load-${leg.loadFactor}`)}</span>`
+						)}
 					</div>
 					<div class="table">
 						<div class="row head">

@@ -136,10 +155,18 @@ class JourneyView extends BaseView {
 						</div>
 						${(leg.stopovers || []).map(stop => html`
 						<div class="row">
-							<span class="${!stop.cancelled ? nothing : 'cancelled'}">${timeTemplate(stop, 'arrival')}</span>
-							<span class="${!stop.cancelled ? nothing : 'cancelled'}">${timeTemplate(stop, 'departure')}</span>
-							<span class="station ${!stop.cancelled ? '' : 'cancelled'}">${stopTemplate(this.profile, stop.stop)}</span>
-							<span class="${!stop.cancelled ? nothing : 'cancelled'}">${platformTemplate(stop)}</span>
+							<span class=${classMap({ cancelled: stop.cancelled })}>
+								${timeTemplate(stop, 'arrival')}
+							</span>
+							<span class=${classMap({ cancelled: stop.cancelled })}>
+								${timeTemplate(stop, 'departure')}
+							</span>
+							<span class="station ${classMap({ cancelled: stop.cancelled })}">
+								${stopTemplate(this.profile, stop.stop)}
+							</span>
+							<span class=${classMap({ cancelled: stop.cancelled })}>
+								${platformTemplate(stop)}
+							</span>
 						</div>
 						`)}
 					</div>
diff --git a/src/journeysView.js b/src/journeysView.js
@@ -1,4 +1,8 @@
 import { html, nothing } from 'lit';
+import { classMap } from 'lit/directives/class-map.js';
+import { choose } from 'lit/directives/choose.js';
+import { when } from 'lit/directives/when.js';
+import { join } from 'lit/directives/join.js';
 
 import { sleep, queryBackgroundColor, setThemeColor } from './helpers.js';
 import { getJourneys, getMoreJourneys, refreshJourneys, getFromPoint, getToPoint } from './app_functions.js';

@@ -87,60 +91,66 @@ export class JourneysView extends JourneysCanvas {
 
 	renderView = () => [
 		html`
-		<div class="header-container">
-			<header>
-				<a id="back" class="icon-back" title="${t('back')}" href="#/"></a>
-				<div class="container flex-row">
-					<div>
-						<h3>${t('from')}: ${this.viewState !== null ? formatPoint(getFromPoint(this.viewState.journeys)) : '...'}</h3>
-						<h3>${t('to')}:   ${this.viewState !== null ? formatPoint(getToPoint(this.viewState.journeys))   : '...'}</h3>
+			<div class="header-container">
+				<header>
+					<a class="icon-back" title=${t('back')} href="#/"></a>
+					<div class="container flex-row">
+						<div>
+							<h3>${t('from')}: ${!this.viewState ? '...' : formatPoint(getFromPoint(this.viewState.journeys))}</h3>
+							<h3>${t('to')}:   ${!this.viewState ? '...' : formatPoint(getToPoint(this.viewState.journeys))}</h3>
+						</div>
+						<div class="mode-changers flex-row">
+							<a href="#/${this.slug}/table" class="${this.settingsState.journeysViewMode === 'table' ? 'active' : nothing}">
+								<div class="icon-table"></div>
+								<span>${t('tableView')}</span>
+							</a>
+							<a href="#/${this.slug}/canvas" class="${this.settingsState.journeysViewMode === 'canvas' ? 'active' : nothing}">
+								<div class="icon-canvas"></div>
+								<span>${t('canvasView')}</span>
+							</a>
+						</div>
 					</div>
-					<div class="mode-changers flex-row">
-						<a href="#/${this.slug}/table" class="${this.settingsState.journeysViewMode === 'table' ? 'active' : ''}">
-							<div class="icon-table"></div>
-							<span>${t('tableView')}</span>
-						</a>
-						<a href="#/${this.slug}/canvas" class="${this.settingsState.journeysViewMode === 'canvas' ? 'active' : ''}">
-							<div class="icon-canvas"></div>
-							<span>${t('canvasView')}</span>
-						</a>
-					</div>
-				</div>
-				<a class="icon-reload ${this.isUpdating ? 'spinning' : ''} ${!this.isOffline ? '' : 'invisible'}" title="${t("reload")}" @click=${this.refreshJourneys}></a>
-			</header>
-		</div>
+					<a class="icon-reload ${classMap({ spinning: this.isUpdating, invisible: this.isOffline })}" title=${t("refresh")} @click=${this.refreshJourneys}></a>
+				</header>
+			</div>
 		`,
-
-		this.viewState !== null ? [
-			this.settingsState.journeysViewMode === 'canvas' ? this.getCanvas() : nothing,
-			this.settingsState.journeysViewMode === 'table' ? html`
-			<div class="container">
-				${!this.viewState.earlierRef ? nothing : html`
-				<a class="arrowButton icon-arrow flipped flex-center" title="${t('earlier')}" @click=${() => this.moreJourneys('earlier')}></a>
-				`}
-
-				<div class="table" style="grid-template-columns: repeat(${this.settingsState.showPrices && this.viewState.profile === 'db' ? '6' : '5'}, 1fr) .3fr">
-					<div class="row head">
-						<span>${t('departure')}</span>
-						<span>${t('arrival')}</span>
-						<span>${t('duration')}</span>
-						<span>${t('changes')}</span>
-						<span>${t('products')}</span>
-						${this.settingsState.showPrices && this.viewState.profile === 'db' ? html`
-						<span>${t('price')}</spanv>
-						` : nothing}
-						<span></span>
+		when(
+			!this.viewState,
+			() => html`<div class="spinner"></div>`,
+			() => choose(this.settingsState.journeysViewMode, [
+				[ 'canvas', () => this.getCanvas() ],
+				[ 'table',  () => html`
+					<div class="container">
+						${when(
+							this.viewState.earlierRef,
+							() => html`<a class="arrowButton icon-arrow flipped flex-center" title="${t('earlier')}" @click=${() => this.moreJourneys('earlier')}></a>`
+						)}
+
+						<div class="table" style="grid-template-columns: repeat(${this.settingsState.showPrices && this.viewState.profile === 'db' ? '6' : '5'}, 1fr) .3fr">
+							<div class="row head">
+								<span>${t('departure')}</span>
+								<span>${t('arrival')}</span>
+								<span>${t('duration')}</span>
+								<span>${t('changes')}</span>
+								<span>${t('products')}</span>
+								${when(
+									this.settingsState.showPrices && this.viewState.profile === 'db',
+									() => html`<span>${t('price')}</span>`
+								)}
+								<span></span>
+							</div>
+							${this.viewState.journeys.map(journey => this.journeyTemplate(journey))}
+						</div>
+
+						${when(
+							this.viewState.laterRef,
+							() => html`<a class="arrowButton icon-arrow flex-center" title="${t('later')}" @click=${() => this.moreJourneys('later')}></a>`
+						)}
 					</div>
-					${this.viewState.journeys.map(journey => this.journeyTemplate(journey))}
-				</div>
-
-				${!this.viewState.laterRef ? nothing : html`
-				<a class="arrowButton icon-arrow flex-center" title="${t('later')}" @click=${() => this.moreJourneys('later')}></a>
-				`}
-			</div>
-			<footer-component></footer-component>
-			` : nothing
-		] : html`<div class="spinner"></div>`
+					<footer-component></footer-component>
+				` ],
+			])
+		)
 	];
 
 	journeyTemplate = journey => {

@@ -149,18 +159,22 @@ export class JourneysView extends JourneysCanvas {
 
 		return html`
 			<a class="row" href="#/j/${this.viewState.profile}/${journey.refreshToken}">
-				<span class="${!journey.cancelled ? nothing : 'cancelled'}">${timeTemplate(firstLeg, 'departure')}</span>
-				${journey.cancelled ? html`
-				<span class="cancelled-text">${t('cancelled')}</span>
-				` : html`
-				<span>${timeTemplate(lastLeg, 'arrival')}</span>
-				`}
-				<span class="${!journey.cancelled ? nothing : 'cancelled'}" title="${journey.changesDuration > 0 ? 'including '+formatDuration(journey.changesDuration)+' transfer durations' : ''}">${formatDuration(journey.duration)}</span>
+				<span class=${!journey.cancelled ? nothing : 'cancelled'}>${timeTemplate(firstLeg, 'departure')}</span>
+				${when(
+					!journey.cancelled,
+					() => html`<span>${timeTemplate(lastLeg, 'arrival')}</span>`,
+					() => html`<span class="cancelled-text">${t('cancelled')}</span>`
+				)}
+				<span class=${classMap({ cancelled: journey.cancelled })} title="${when(
+					journey.changesDuration,
+					() => 'including '+formatDuration(journey.changesDuration)+' transfer durations'
+				)}">${formatDuration(journey.duration)}</span>
 				<span>${journey.changes-1}</span>
-				<span>${journey.products}</span>
-				${this.settingsState.showPrices && this.viewState.profile === 'db' ? html`
-				<span>${formatPrice(journey.price)}</span>
-				` : nothing}
+				<span>${join(journey.products, ', ')}</span>
+				${when(
+					this.settingsState.showPrices && this.viewState.profile === 'db',
+					() => html`<span>${formatPrice(journey.price)}</span>`
+				)}
 				<span><i class="icon-arrow" style="transform:rotate(-90deg)"></i></span>
 			</a>
 		`;

@@ -179,34 +193,31 @@ export class JourneysView extends JourneysCanvas {
 			viewState.journeys.forEach((journey, index, journeys) => {
 				const firstLeg = journey.legs[0];
 				const lastLeg  = journey.legs[journey.legs.length - 1];
-				const products = {};
 
-				journeys[index].duration        = Number(lastLeg.arrival || lastLeg.plannedArrival) - Number(firstLeg.departure || firstLeg.plannedDeparture);
 				journeys[index].changesDuration = 0;
+				journeys[index].duration        = Number(lastLeg.arrival || lastLeg.plannedArrival) - Number(firstLeg.departure || firstLeg.plannedDeparture);
 				journeys[index].cancelled       = false;
 				journeys[index].changes         = 0;
+				journeys[index].products        = [];
 
+				let previousLegArrival = null;
 				journey.legs.forEach(leg => {
 					if (leg.cancelled) journeys[index].cancelled = true;
 					if (leg.walking || leg.transfer) return;
 
+					if (previousLegArrival) journeys[index].changesDuration += Number(leg.departure || leg.plannedDeparture) - previousLegArrival;
+
+					previousLegArrival = Number(leg.arrival || leg.plannedArrival)
 					journeys[index].changes++;
 
 					if (leg.line) {
 						const productName = leg.line.name.split(' ')[0];
-						if (!products[productName]) products[productName] = [];
-					}
-					//if (leg.line && leg.line.trainTypeShort) products[leg.line.productName].push(leg.line.trainTypeShort);
-				})
-
-				journeys[index].products = Object.entries(products).map(([prod, types]) => {
-					if (types.length >= 2) {
-						prod += ' (' + types.join(', ') + ')';
-					} else if (types.length) {
-						prod += ' ' + types[0];
+
+						if (!journeys[index].products.includes(productName)) {
+							journeys[index].products.push(productName);
+						}
 					}
-					return prod;
-				}).join(', ');
+				});
 			});
 
 			this.viewState = viewState;

@@ -218,8 +229,7 @@ export class JourneysView extends JourneysCanvas {
 	}
 
 	refreshJourneys = async () => {
-		if (this.isOffline !== false) return;
-		if (this.isUpdating !== false) return false;
+		if (this.isOffline || this.isUpdating) return;
 
 		try {
 			this.isUpdating = true;

@@ -235,7 +245,7 @@ export class JourneysView extends JourneysCanvas {
 	}
 
 	moreJourneys = async mode => {
-		if (this.isOffline !== false) {
+		if (this.isOffline) {
 			this.showAlertOverlay(t('offline'));
 			return;
 		}
diff --git a/src/languages.js b/src/languages.js
@@ -58,7 +58,7 @@ export const languages = {
 		'tableView':               'Table',
 		'canvasView':              'Graphical',
 		'showDS100':               'Show DS100 (if available)',
-		'showPrices ':             'Show prices',
+		'showPrices':              'Show prices',
 		'price':                   'Price',
 		'back':                    'Back',
 		'refresh':                 'Refresh data',
diff --git a/src/main.js b/src/main.js
@@ -69,7 +69,7 @@ class Oeffisearch extends LitElement {
 
 customElements.define('oeffi-search', Oeffisearch);
 
-(async () => {
+window.addEventListener('load', async () => {
 	await initSettingsState();
 	await initDataStorage();
 	await initHafasClient(settingsState.profile);

@@ -80,14 +80,12 @@ customElements.define('oeffi-search', Oeffisearch);
 
 	document.head.appendChild(style);
 	document.body.innerHTML = '<oeffi-search></oeffi-search>';
-})();
 
-if (!isDevServer) {
-	window.addEventListener('load', () => {
+	if (!isDevServer) {
 		navigator.serviceWorker.register('/sw.js').then(registration => {
 			console.log('SW registered: ', registration);
 		}).catch(registrationError => {
 			console.log('SW registration failed: ', registrationError);
 		});
-	});
-}
+	}
+});
diff --git a/src/searchView.js b/src/searchView.js
@@ -1,4 +1,6 @@
 import { LitElement, html, nothing } from 'lit';
+import { classMap } from 'lit/directives/class-map.js';
+import { when } from 'lit/directives/when.js';
 import { BaseView } from './baseView.js';
 
 import { db } from './dataStorage.js';

@@ -121,11 +123,11 @@ class SearchView extends BaseView {
 						autocomplete="off"  ?required=${name !== 'via'}>
 
 						${name !== 'from' ? nothing : html`
-							<div class="button icon-arrow ${this.settingsState.showVia ? 'flipped' : ''}" tabindex="0" title="${t('via')}"
+							<div class="button icon-arrow ${classMap({ flipped: this.settingsState.showVia })}" tabindex="0" title="${t('via')}"
 							@click=${this.settingsState.toggleShowVia}></div>
 						`}
-						${name !== 'via' ? nothing : html`<div class="button invisible"></div>`}
-						${name !== 'to' ? nothing : html`<div class="button icon-swap" tabindex="0" title="${t('swap')}" @click=${this.swapFromTo}></div>`}
+						${name !== 'via' ? nothing : html`<div class="button icon-arrow invisible"></div>`}
+						${name !== 'to' ? nothing : html`<div class="button icon-swap" tabindex="0" title=${t('swap')} @click=${this.swapFromTo}></div>`}
 					</div>
 
 					<div class="suggestions ${this.location[name].suggestionsVisible ? '' : 'hidden'}">

@@ -182,28 +184,31 @@ class SearchView extends BaseView {
 					</div>
 
 					${this.history.length !== 0 ? html`
-					<div id="historyButton" class="arrowButton icon-arrow ${!this.showHistory ? '' : 'flipped'}" title="History" @click=${this.toggleHistory}></div>
+					<div id="historyButton" class="arrowButton icon-arrow ${classMap({ flipped: this.showHistory })}" title=${t('history')} @click=${this.toggleHistory}></div>
 					` : nothing}
 				</form>
 
-				${!this.showHistory ? nothing : html`
-				<div id="history" class="history center">
-					${this.history.map((element, index) => html`
-					<div class="flex-row" @click="${() => this.journeysHistoryAction(index)}">
-						<div class="from">
-							<small>${t('from')}:</small>
-							${formatPoint(element.fromPoint)}
-							${element.viaPoint ? html`<div class="via">${t('via')} ${formatPoint(element.viaPoint)}</div>` : nothing}
-						</div>
-						<div class="icon-arrow1"></div>
-						<div class="to">
-							<small>${t('to')}:</small>
-							${formatPoint(element.toPoint)}
+				${when(
+					this.showHistory,
+					() => html`
+						<div id="history" class="history center">
+						${this.history.map((element, index) => html`
+							<div class="flex-row" @click="${() => this.journeysHistoryAction(index)}">
+								<div class="from">
+									<small>${t('from')}:</small>
+									${formatPoint(element.fromPoint)}
+									${element.viaPoint ? html`<div class="via">${t('via')} ${formatPoint(element.viaPoint)}</div>` : nothing}
+								</div>
+								<div class="icon-arrow1"></div>
+								<div class="to">
+									<small>${t('to')}:</small>
+									${formatPoint(element.toPoint)}
+								</div>
+							</div>
+						`)}
 						</div>
-					</div>
-					`)}
-				</div>
-				`}
+					`
+				)}
 				<footer-component></footer-component>
 			</div>
 	    `;

@@ -345,18 +350,17 @@ class SearchView extends BaseView {
 			from: null,
 			to: null,
 			via: null,
-			bike:         this.settingsState.bikeFriendly,
-			transferTime: this.settingsState.transferTime,
-			results:  6,
+			results: 6,
 			products: {},
+			bike: this.settingsState.bikeFriendly,
+			transferTime: this.settingsState.transferTime,
 		};
 
-		await Promise.all([ 'from', 'via','to' ].map(async mode => {
+		await Promise.all([ 'from', 'via', 'to' ].map(async mode => {
 			if (this.location[mode].value !== '') {
-				if (mode === 'via' && this.settingsState.showVia === false) return false;
-				if (this.location[mode].suggestionSelected !== null) {
-					params[mode] = this.location[mode].suggestionSelected;
-				} else {
+				if (mode === 'via' && !this.settingsState.showVia) return false;
+
+				if (!this.location[mode].suggestionSelected) {
 					if (this.location[mode].suggestions.length !== 0) {
 						params[mode] = this.location[mode].suggestions[0]
 					} else {

@@ -364,11 +368,13 @@ class SearchView extends BaseView {
 						if (!data[0]) return false;
 						params[mode] = data[0];
 					}
+				} else {
+					params[mode] = this.location[mode].suggestionSelected;
 				}
 			}
 		}));
 
-		if (params.from === null || params.to === null) return false;
+		if (!params.from || !params.to) return false;
 
 		if (formatPoint(params.from) === formatPoint(params.to) && params.via === null) {
 			this.showAlertOverlay('From and To are the same place.');

@@ -437,7 +443,7 @@ class SearchView extends BaseView {
 	keydownHandler = event => {
 		const name = event.target.name;
 
-		if (this.location[name].suggestions.length == 0) return true;
+		if (this.location[name].suggestions.length === 0) return true;
 	
 		if (event.key === 'Enter') {
 			event.preventDefault();

@@ -476,6 +482,7 @@ class SearchView extends BaseView {
 				}
 			}
 		}
+
 		this.requestUpdate();
 	}
 

@@ -498,6 +505,7 @@ class SearchView extends BaseView {
 			this.location[name].suggestionSelected = null;
 			this.location[name].suggestion         = 0;
 			this.location[name].suggestions        = suggestions;
+
 			this.requestUpdate();
 		};
 	}
diff --git a/src/settingsView.js b/src/settingsView.js
@@ -1,4 +1,6 @@
 import { html, nothing } from 'lit';
+import { when } from 'lit/directives/when.js';
+import { classMap } from 'lit/directives/class-map.js';
 import { BaseView } from './baseView.js';
 
 import { sleep, queryBackgroundColor, setThemeColor } from './helpers.js';

@@ -43,23 +45,25 @@ class SettingsView extends BaseView {
 	}
 
 
-	render () {
-		return html`
+	render = () => [
+		html`
 			<div class="flex-row">
 				<label for="language">${t('language')}:</label>
 				<select id="language" @change=${this.changeHandler}>
 					${getLanguages().map(lang => html`<option value="${lang}" ?selected=${this.viewState.language === lang}>${t(lang)}</option>`)}
 				</select>
 			</div>
-
+		`,
+		html`
 			<div class="flex-row">
 				<label for="profile">${t('datasource')}:</label>
 				<select id="profile" @change=${this.changeHandler}>
 					${Object.keys(profiles).map(profile => html`<option value="${profile}" ?selected=${this.viewState.profile === profile}>${profiles[profile].name}</option>`)}
 				</select>
 			</div>
-
-			<div class="flex-row ${this.viewState.profile !== 'db' ? '' : 'hidden'}">
+		`,
+		when(this.viewState.profile !== 'db', () => html`
+			<div class="flex-row">
 				<label for="accessibility">${t('accessibility')}:</label>
 				<select id="accessibility" @change=${this.changeHandler}>
 					<option value="none"     ?selected=${this.viewState.accessibility === 'none'}>${t('accessibilityNone')}</option>

@@ -67,8 +71,9 @@ class SettingsView extends BaseView {
 					<option value="complete" ?selected=${this.viewState.accessibility === 'complete'}>${t('accessibilityComplete')}</option>
 				</select>
 			</div>
-
-			<div class="flex-row ${this.viewState.profile !== 'db' ? '' : 'hidden'}">
+		`),
+		when(this.viewState.profile !== 'db', () => html`
+			<div class="flex-row">
 				<label for="walkingSpeed">${t('walkingSpeed')}:</label>
 				<select id="walkingSpeed" @change=${this.changeHandler}>
 					<option value="slow"   ?selected=${this.viewState.walkingSpeed === 'slow'}>${t('walkingSpeedSlow')}</option>

@@ -76,8 +81,9 @@ class SettingsView extends BaseView {
 					<option value="fast"   ?selected=${this.viewState.walkingSpeed === 'fast'}>${t('walkingSpeedFast')}</option>
 				</select>
 			</div>
-
-			<div class="flex-row ${this.viewState.profile !== 'db' ? 'hidden' : ''}">
+		`),
+		when(this.viewState.profile === 'db', () => html`
+			<div class="flex-row">
 				<label for="ageGroup">${t('ageGroup')}:</label>
 				<select id="ageGroup" @change=${this.changeHandler}>
 					<option value="K" ?selected=${this.viewState.ageGroup === 'K'}>${t('ageGroupChild')} (7-14)</option>

@@ -86,8 +92,9 @@ class SettingsView extends BaseView {
 					<option value="S" ?selected=${this.viewState.ageGroup === 'S'}>${t('ageGroupSenior')} (65+)</option>
 				</select>
 			</div>
-
-			<div class="flex-row ${this.viewState.profile !== 'db' ? 'hidden' : ''}">
+		`),
+		when(this.viewState.profile === 'db', () => html`
+			<div class="flex-row">
 				<label for="loyaltyCard">${t('loyaltyCard')}:</label>
 				<select id="loyaltyCard" @change=${this.changeHandler}>
 					<option value="NONE"           ?selected=${this.viewState.loyaltyCard === 'NONE'}>${t('loyaltyCardNone')}</option>

@@ -99,26 +106,29 @@ class SettingsView extends BaseView {
 					<option value="BAHNCARD-100-1" ?selected=${this.viewState.loyaltyCard === 'BAHNCARD-100-1'}>BahnCard 100, 1. ${t("class")}</option>
 				</select>
 			</div>
-
+		`),
+		html`
 			<div class="flex-row">
 				<label for="transferTime">${t('minTransferTime')}:</label>
 				<input type="number" id="transferTime" min="0" max="99" @change=${this.changeHandler} .value="${this.viewState.transferTime}">
 			</div>
-
+		`,
+		html`
 			<div class="flex-column">
 				<span>${t('options')}:</span><br>
 				<label><input type="checkbox" id="showDS100"       @change=${this.changeHandler} ?checked=${this.viewState.showDS100}> ${t('showDS100')}<br></label>
 				<label><input type="checkbox" id="combineDateTime" @change=${this.changeHandler} ?checked=${this.viewState.combineDateTime}> ${t('combineDateTime')}<br></label>
-				<label class="${this.viewState.profile !== 'db' ? 'hidden' : nothing}">
+				<label class="${classMap({ hidden: this.viewState.profile !== 'db' })}">
 					<input type="checkbox" id="showPrices" @change=${this.changeHandler} ?checked=${this.viewState.showPrices}> ${t('showPrices')}
 				</label>
 			</div>
-
+		`,
+		html`
 			<div class="flex-row">
 				<div class="button color icon-trashcan" title="${t('clearstorage')}" @click=${this.clearStorage}></div>
 			</div>
-		`;
-	}
+		`,
+	];
 
 	changeHandler = async event => {
 		const id    = event.target.id;
diff --git a/src/tripView.js b/src/tripView.js
@@ -1,5 +1,7 @@
 import { html, nothing } from 'lit';
 import { ifDefined } from 'lit/directives/if-defined.js';
+import { classMap } from 'lit/directives/class-map.js';
+import { when } from 'lit/directives/when.js';
 import { BaseView } from './baseView.js';
 
 import { sleep, queryBackgroundColor, setThemeColor } from './helpers.js';

@@ -61,9 +63,8 @@ class TripView extends BaseView {
 
 		this.isUpdating = true;
 		try {
-			const client = await getHafasClient(this.profile);
-
-			let viewState = await client.trip(this.refreshToken, {stopovers: true});
+			const client    = await getHafasClient(this.profile);
+			let   viewState = await client.trip(this.refreshToken, {stopovers: true});
 
 			processLeg(viewState.trip);
 

@@ -104,79 +105,108 @@ class TripView extends BaseView {
 		html`
 			<div class="header-container">
 				<header>
-					<a id="back" class="icon-back ${history.length !== 1 ? '': 'invisible'}" title="${t('back')}" @click=${() => history.back()}></a>
+					<a class="icon-back ${classMap({ invisible: history.length === 1 })}" title=${t('back')} @click=${() => history.back()}></a>
 					<div class="container">
-						${!this.viewState ? html`
-							<h3>Trip of ...</h3>
-						` : html`
-							<h3>Trip of ${formatLineDisplayName(this.viewState.trip.line)} to ${this.viewState.trip.direction}</h3>
-						`}
+					${when(
+						!this.viewState,
+						() => html`<h3>Trip of ...</h3>`,
+						() => html`<h3>Trip of ${formatLineDisplayName(this.viewState.trip.line)} to ${this.viewState.trip.direction}</h3>`
+					)}
 					</div>
-					<a class="icon-reload ${!this.isUpdating ? '' : 'spinning'} ${!this.isOffline ? '' : 'invisible'}" title="${t("refresh")}" @click=${this.updateViewState}></a>
+					<a class="icon-reload ${classMap({ spinning: this.isUpdating, invisible: this.isOffline })}" title=${t("refresh")} @click=${this.updateViewState}></a>
 				</header>
 			</div>
 		`,
 
-		this.viewState !== null ? html`
-		<div class="container">
-			<div class="card">
-				<div class="train-info flex-center nowrap">
-					<a href="${ifDefined(this.viewState.trip.bahnExpertUrl)}">
-					${formatLineDisplayName(this.viewState.trip.line)} ${!this.viewState.trip.direction ? '' : html` → ${this.viewState.trip.direction}`}
-					</a>
-					${this.viewState.trip.remarks.length === 0 ? nothing : html`
-					<a class="${this.viewState.trip.remarksIcon}" @click=${() => remarksModal(this, this.viewState.trip.remarks)}></a>
-					`}
-				</div>
-
-				<div class="train-info flex-center">
-					${!this.viewState.trip.cancelled ? nothing : html`
-					<span class="cancelled-text">${t('cancelled')}</span>
-					`}
-
-					${formatLineAdditionalName(this.viewState.trip.line) !== "" ? nothing : html`
-					<span>Trip: ${formatLineAdditionalName(this.viewState.trip.line)}</span>
-					`}
-
-					${!this.viewState.trip.line.trainType ? nothing : html`
-					<span>Train type: ${this.viewState.trip.line.trainType}</span>
-					`}
-
-					${this.viewState.trip.cancelled ? nothing : html`
-					<span>
-						${t('duration')}:
-						${formatDuration(this.viewState.trip.arrival - (this.viewState.trip.departure ? this.viewState.trip.departure : this.viewState.trip.plannedDeparture))}
-						${this.viewState.trip.departure ? '' : ('(' + t('planned') + ')')}
-					</span>
-					`}
-
-					${!this.viewState.trip.loadFactor ? nothing : html`
-					<span>${t("load-"+this.viewState.trip.loadFactor)}</span>
-					`}
-				</div>
-
-				<div class="table">
-					<div class="row head">
-						<span>${t('arrival')}</span>
-						<span>${t('departure')}</span>
-						<span class="station">${t('station')}</span>
-						<span>${t('platform')}</span>
-					</div>
-					${(this.viewState.trip.stopovers || []).map(stop => html`
-					<div class="row">
-						<span class="${!stop.cancelled ? nothing : 'cancelled'}">${timeTemplate(stop, 'arrival')}</span>
-						<span class="${!stop.cancelled ? nothing : 'cancelled'}">${timeTemplate(stop, 'departure')}</span>
-						<span class="station ${!stop.cancelled ? '' : 'cancelled'}">${stopTemplate(this.profile, stop.stop)}</span>
-						<span class="${!stop.cancelled ? nothing : 'cancelled'}">${platformTemplate(stop)}</span>
+		when(
+			!this.viewState,
+			() => when(
+				!this.isOffline,
+				() => html`<div class="spinner"></div>`,
+				() => html`<div class="offline"></div>`
+			),
+			() => html`
+				<div class="container">
+					<div class="card">
+						<div class="train-info flex-center nowrap">
+							<a href=${ifDefined(this.viewState.trip.bahnExpertUrl)}>
+							${formatLineDisplayName(this.viewState.trip.line)}
+							${when(
+								this.viewState.trip.direction,
+								() => html` → ${this.viewState.trip.direction}`
+							)}
+							</a>
+
+							${when(
+								this.viewState.trip.remarks.length !== 0, 
+								() => html`<a class="${this.viewState.trip.remarksIcon}" @click=${() => remarksModal(this, this.viewState.trip.remarks)}></a>`
+							)}
+						</div>
+
+						<div class="train-info flex-center">
+							${when(
+								this.viewState.trip.cancelled,
+								() => html`<span class="cancelled-text">${t('cancelled')}</span>`
+							)}
+
+							${when(
+								formatLineAdditionalName(this.viewState.trip.line),
+								() => html`<span>Trip: ${formatLineAdditionalName(this.viewState.trip.line)}</span>`
+							)}
+
+							${when(
+								this.viewState.trip.line.trainType,
+								() => html`<span>Train type: ${this.viewState.trip.line.trainType}</span>`
+							)}
+
+							${when(
+								!this.viewState.trip.cancelled,
+								() => html`
+									<span>
+										${t('duration')}:
+										${formatDuration(
+											(this.viewState.trip.arrival   ? this.viewState.trip.arrival   : this.viewState.trip.plannedArrival) - 
+											(this.viewState.trip.departure ? this.viewState.trip.departure : this.viewState.trip.plannedDeparture)
+										)}
+									</span>
+								`
+							)}
+
+							${when(
+								this.viewState.trip.loadFactor,
+								() => html`<span>${t("load-"+this.viewState.trip.loadFactor)}</span>`
+							)}
+						</div>
+
+						<div class="table">
+							<div class="row head">
+								<span>${t('arrival')}</span>
+								<span>${t('departure')}</span>
+								<span class="station">${t('station')}</span>
+								<span>${t('platform')}</span>
+							</div>
+							${(this.viewState.trip.stopovers || []).map(stop => html`
+								<div class="row">
+									<span class=${classMap({ cancelled: stop.cancelled })}>
+										${timeTemplate(stop, 'arrival')}
+									</span>
+									<span class=${classMap({ cancelled: stop.cancelled })}>
+										${timeTemplate(stop, 'departure')}
+									</span>
+									<span class="station ${classMap({ cancelled: stop.cancelled })}">
+										${stopTemplate(this.profile, stop.stop)}
+									</span>
+									<span class=${classMap({ cancelled: stop.cancelled })}>
+										${platformTemplate(stop)}
+									</span>
+								</div>
+							`)}
+						</div>
 					</div>
-					`)}
 				</div>
-			</div>
-		</div>
-		<footer-component></footer-component>
-		` : !this.isOffline ?
-		html`<div class="spinner"></div>`
-		: html`<div class="offline"></div>`
+				<footer-component></footer-component>
+			`
+		)
 	];
 }