commit fb46f86ab273a99008c5cddacd3971b64181b892
parent 73de80c74a1ae1e3b5c205a3e1555578a3661c2c
Author: Katja (ctucx) <git@ctu.cx>
Date: Sun, 2 Feb 2025 23:00:48 +0100
parent 73de80c74a1ae1e3b5c205a3e1555578a3661c2c
Author: Katja (ctucx) <git@ctu.cx>
Date: Sun, 2 Feb 2025 23:00:48 +0100
refactor stuff
18 files changed, 636 insertions(+), 604 deletions(-)
M
|
81
++++++++++++++++++++++++++++++++++++++-----------------------------------------
A
|
76
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M
|
450
+++++++++++++++++++++++++++++++------------------------------------------------
M
|
127
++++++++++++++++++++++++++++++++++++++++++-------------------------------------
M
|
83
++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
diff --git a/src/app_functions.js b/src/app_functions.js @@ -24,7 +24,7 @@ const journeySettings = () => { return { export const getFrom = journeys => journeys[0].legs[0].origin; export const getTo = journeys => journeys[0].legs[journeys[0].legs.length-1].destination; -const addJourneys = async data => { +export const addJourneys = async data => { if (!data) return false; const historyEntry = { @@ -73,6 +73,28 @@ export const processLeg = leg => { } }; +export const newJourneys = async (params) => { + const { from, to, ...moreOpts } = params; + let data; + + data = await client.journeys(from, to, { ...journeySettings(), ...moreOpts }); + + for (const journey of data.journeys) { + journey.refreshToken = hafasToTrainsearch(journey.refreshToken); + } + + data.slug = generateSlug(); + data.indexOffset = 0; + data.params = params; + data.settings = journeySettings(); + data.profile = settings.profile; + + await addJourneys(data); + processJourneys(data); + + return data; +}; + export const getJourneys = async slug => { let data = await db.getJourneysOverview(slug); @@ -84,18 +106,6 @@ export const getJourneys = async slug => { }; }; -export const getJourney = async (refreshToken, profile) => { - let journeyObject = await db.getJourney(refreshToken); - - if (!journeyObject || JSON.stringify(journeyObject.settings) != JSON.stringify(journeySettings())) { - journeyObject = await refreshJourney(refreshToken, profile); - } - - processJourney(journeyObject); - - return journeyObject; -}; - export const getMoreJourneys = async (slug, mode) => { const saved = await db.getJourneysOverview(slug); const params = { ...saved.params, ...journeySettings() }; @@ -115,9 +125,7 @@ export const getMoreJourneys = async (slug, mode) => { ...newData, }; - for (const journey of newData.journeys) { - journey.refreshToken = hafasToTrainsearch(journey.refreshToken); - } + for (const journey of newData.journeys) journey.refreshToken = hafasToTrainsearch(journey.refreshToken); if (mode === 'earlier') { res.journeys = newData.journeys.concat(existingJourneys); @@ -138,6 +146,17 @@ export const refreshJourneys = async (slug) => { await Promise.all(saved.journeys.map(x => refreshJourney(x, saved.profile || "db"))); }; +export const getJourney = async (refreshToken, profile) => { + let journeyObject = await db.getJourney(refreshToken); + + if (!journeyObject || JSON.stringify(journeyObject.settings) != JSON.stringify(journeySettings())) + journeyObject = await refreshJourney(refreshToken, profile); + + processJourney(journeyObject); + + return journeyObject; +}; + export const refreshJourney = async (refreshToken, profile) => { const client = await getHafasClient(profile || settings.profile || "db"); const [saved, data] = await Promise.all([ @@ -157,33 +176,11 @@ export const refreshJourney = async (refreshToken, profile) => { return journey; }; -export const newJourneys = async (params) => { - const { from, to, ...moreOpts } = params; - let data; - - data = await client.journeys(from, to, { ...journeySettings(), ...moreOpts }); - - for (const journey of data.journeys) { - journey.refreshToken = hafasToTrainsearch(journey.refreshToken); - } - - data.slug = generateSlug(); - data.indexOffset = 0; - data.params = params; - data.settings = journeySettings(); - data.profile = settings.profile; - - await addJourneys(data); - processJourneys(data); - - return data; -}; - -export const ds100Names = (id) => { - if (!settings.showDS100) return ''; - if (!ds100[Number(id)]) return ''; +export const ds100Name = (id) => { + if (!settings.showDS100) return null; + if (!ds100[Number(id)]) return null; - return '('+ds100[Number(id)]+')'; + return ds100[Number(id)]; }; export const ds100Reverse = (name) => {
diff --git a/src/assets/icons.css b/src/assets/icons.css @@ -0,0 +1,76 @@ +.icon-back { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z" fill="white"/></svg>'); +} + +.icon-reload { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M17.65 6.35A7.96 7.96 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4z" fill="white"/></svg>'); +} + +.icon-close { + content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="30" height="30"><path d="M5.293 5.293a1 1 0 0 1 1.414 0L12 10.586l5.293-5.293a1 1 0 1 1 1.414 1.414L13.414 12l5.293 5.293a1 1 0 0 1-1.414 1.414L12 13.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L10.586 12 5.293 6.707a1 1 0 0 1 0-1.414" fill="white"/></svg>'); +} + +.icon-hint { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M5 3h14a2 2 0 0 1 2 2v14c0 .53-.21 1.04-.59 1.41-.37.38-.88.59-1.41.59H5c-.53 0-1.04-.21-1.41-.59C3.21 20.04 3 19.53 3 19V5c0-1.11.89-2 2-2m6 6h2V7h-2zm3 8v-2h-1v-4h-3v2h1v2h-1v2z"/></svg>'); +} + +.icon-status { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2m8 10V7h-2v6zm0 4v-2h-2v2z"/></svg>'); +} + +.icon-warning { + content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13 13h-2V7h2m-2 8h2v2h-2m4.73-14H8.27L3 8.27v7.46L8.27 21h7.46L21 15.73V8.27z"/></svg>'); +} + +.icon-other { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m1 17h-2v-2h2zm2.07-7.75-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25"/></svg>'); +} + +.icon-arrow1 { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>'); +} + +.icon-arrow2 { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/></svg>'); +} + +.icon-swap { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M16 17.01V10h-2v7.01h-3L15 21l4-3.99zM9 3 5 6.99h3V14h2V6.99h3z"/></svg>'); +} + +.icon-clock { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M12 20a8 8 0 0 0 8-8 8 8 0 0 0-8-8 8 8 0 0 0-8 8 8 8 0 0 0 8 8m0-18a10 10 0 0 1 10 10 10 10 0 0 1-10 10C6.47 22 2 17.5 2 12A10 10 0 0 1 12 2m.5 5v5.25l4.5 2.67-.75 1.23L11 13V7z"/></svg>'); +} + +.icon-settings { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6"/></svg>'); +} + +.icon-walk-fast { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13.49 5.48c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2m-3.6 13.9 1-4.4 2.1 2v6h2v-7.5l-2.1-2 .6-3c1.3 1.5 3.3 2.5 5.5 2.5v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1l-5.2 2.2v4.7h2v-3.4l1.8-.7-1.6 8.1-4.9-1-.4 2z"/></svg>'); +} + +.icon-walk { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13.5 5.5c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2M9.8 8.9 7 23h2.1l1.8-8 2.1 2v6h2v-7.5l-2.1-2 .6-3C14.8 12 16.8 13 19 13v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1L6 8.3V13h2V9.6z"/></svg>'); +} + +.icon-weelchair { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><circle cx="12" cy="4" r="2"/><path d="M19 13v-2c-1.54.02-3.09-.75-4.07-1.83l-1.29-1.43c-.17-.19-.38-.34-.61-.45-.01 0-.01-.01-.02-.01H13c-.35-.2-.75-.3-1.19-.26C10.76 7.11 10 8.04 10 9.09V15c0 1.1.9 2 2 2h5v5h2v-5.5c0-1.1-.9-2-2-2h-3v-3.45c1.29 1.07 3.25 1.94 5 1.95m-6.17 5c-.41 1.16-1.52 2-2.83 2-1.66 0-3-1.34-3-3 0-1.31.84-2.41 2-2.83V12.1a5 5 0 1 0 5.9 5.9z"/></svg>'); +} + +.icon-bike { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M5 20.5A3.5 3.5 0 0 1 1.5 17 3.5 3.5 0 0 1 5 13.5 3.5 3.5 0 0 1 8.5 17 3.5 3.5 0 0 1 5 20.5M5 12a5 5 0 0 0-5 5 5 5 0 0 0 5 5 5 5 0 0 0 5-5 5 5 0 0 0-5-5m9.8-2H19V8.2h-3.2l-1.94-3.27c-.29-.5-.86-.83-1.46-.83-.47 0-.9.19-1.2.5L7.5 8.29C7.19 8.6 7 9 7 9.5c0 .63.33 1.16.85 1.47L11.2 13v5H13v-6.5l-2.25-1.65 2.32-2.35m5.93 13a3.5 3.5 0 0 1-3.5-3.5 3.5 3.5 0 0 1 3.5-3.5 3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m0-8.5a5 5 0 0 0-5 5 5 5 0 0 0 5 5 5 5 0 0 0 5-5 5 5 0 0 0-5-5m-3-7.2c1 0 1.8-.8 1.8-1.8S17 1.2 16 1.2 14.2 2 14.2 3 15 4.8 16 4.8"/></svg>'); +} + +.icon-seat { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M9 19h6v2H9c-2.76 0-5-2.24-5-5V7h2v9c0 1.66 1.34 3 3 3m1.42-13.59c.78-.78.78-2.05 0-2.83s-2.05-.78-2.83 0-.78 2.05 0 2.83c.78.79 2.04.79 2.83 0M11.5 9c0-1.1-.9-2-2-2H9c-1.1 0-2 .9-2 2v6c0 1.66 1.34 3 3 3h5.07l3.5 3.5L20 20.07 14.93 15H11.5z"/></svg>'); +} + +.icon-table { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M3 9h14V7H3zm0 4h14v-2H3zm0 4h14v-2H3zm16 0h2v-2h-2zm0-10v2h2V7zm0 6h2v-2h-2z" fill="white" /></svg>'); +} + +.icon-canvas { + content: url('data:image/svg+xml;utf8,<svg version="1.1" viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m11 5v14h2v-14zm-4-2v14h2v-14zm10 4h-2v14h2z" fill="white"/></svg>'); +} +
diff --git a/src/assets/index.html b/src/assets/index.html @@ -11,36 +11,24 @@ <style> body { background-color: #333; - min-height: 100vh; - overflow-x: hidden; - overflow-y: visible; } #overlay { position: fixed; + display: flex; top: 0; left: 0; height: 100vh; width: 100vw; background-color: rgba(0, 0, 0, .7); - display: flex; - } - - #overlay>* { - margin: auto; - } - - svg { - width: 50vmin; - height: 50vmin; } </style> </head> <body> - <div id="content" class="column"></div> + <div id="content"></div> <div id="overlay"> - <noscript>JavaScript is required to use <%= htmlWebpackPlugin.options.title %></noscript> - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28"><rect rx="4" height="28" width="28" fill="green"/><path d="M14 5.5c-4 0-8 .5-8 4V19c0 1.93 1.57 3.5 3.5 3.5L8 24v.5h2.23l2-2H16l2 2h2V24l-1.5-1.5c1.93 0 3.5-1.57 3.5-3.5V9.5c0-3.5-3.58-4-8-4m-4.5 15c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5m3.5-7H8v-4h5zm2 0v-4h5v4zm3.5 7c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5" fill="white"/></svg> + <noscript style="margin: auto;">JavaScript is required to use <%= htmlWebpackPlugin.options.title %></noscript> + <svg style="margin: auto; width: 50vmin; height: 50vmin;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28"><rect rx="4" height="28" width="28" fill="green"/><path d="M14 5.5c-4 0-8 .5-8 4V19c0 1.93 1.57 3.5 3.5 3.5L8 24v.5h2.23l2-2H16l2 2h2V24l-1.5-1.5c1.93 0 3.5-1.57 3.5-3.5V9.5c0-3.5-3.58-4-8-4m-4.5 15c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5m3.5-7H8v-4h5zm2 0v-4h5v4zm3.5 7c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5" fill="white"/></svg> </div> </body> </html>
diff --git a/src/assets/style.css b/src/assets/style.css @@ -1,34 +1,26 @@ +@import url('./icons.css'); + font-face { font-weight: normal; font-tyle: normal; } +:root { + overscroll-behavior-y: none; +} + * { font-family: sans-serif; box-sizing: border-box; border-collapse: collapse; } -:root { - overscroll-behavior-y: none; -} -html, body { +body { margin: 0; - min-height: 100vh; - overflow-x: hidden; - overflow-y: visible; -} - -#content { - min-height: 100vh; + background-color: #333; } -#overlay { - z-index: 1; - backdrop-filter: blur(10px); -} - a { color: inherit; } @@ -46,26 +38,30 @@ a { transform: rotate(180deg); } -.row { +.flex-row { display: flex; flex-direction: row; } -.column { +.flex-column { display: flex; flex-direction: column; } -.center { +.flex-center { display: flex; justify-content: center; align-items: center; } +.center { + margin: auto; +} + .spinner { margin: calc(50vh - 60px) auto; border: 5px solid rgba(255, 255, 255, .4); - border-top: 5px solid #fff; + border-top: 5px solid white; border-radius: 50%; width: 120px; height: 120px; @@ -81,59 +77,94 @@ a { 100% { transform: rotate(360deg); } } -header { - display: flex; - flex-direction: row; - justify-content: center; - color: white; - background-color: #33691E; - border-bottom: 1px solid rgba(255, 255, 255, .3); +.container { + margin: 0 auto; + max-width: 1000px; +} - h3 { - margin-right: 1.5em; - } +.header-container { + position: sticky; + top: 0; + z-index: 10; - .content { + header { display: flex; flex-direction: row; - flex-wrap: wrap; - flex-grow: 1; - max-width: 1000px; - width: 80vw; - } + justify-content: center; + color: white; + background-color: #33691E; + border-bottom: 1px solid rgba(255, 255, 255, .3); - .icon-back, - .icon-reload { - cursor: pointer; - width: 32px; - height: 32px; - margin: 12px; - user-select: none; - } + .container { + max-width: 1000px; + width: 80vw; + margin: 0; + } - .mode-changers { - display: flex; - margin-left: auto; - - a.active { - border-bottom: 3px solid white; + h3 { + margin-right: 1.5em; } - - a { - border-bottom: 3px solid transparent; - align-items: center; - display: flex; - padding: 0 1em; + + .icon-dots { + float: right; + } + + .icon-back, + .icon-reload, + .icon-share, + .icon-dots { cursor: pointer; - text-decoration: none; - - span { - font-weight: bold; - margin: 1em .4em; + width: 32px; + height: 32px; + margin: 12px; + user-select: none; + } + + .mode-changers { + margin-top: auto; + margin-left: auto; + height: max-content; + + a { + border-bottom: 3px solid transparent; + align-items: center; + display: flex; + padding: 0 1em; + cursor: pointer; + text-decoration: none; + width: max-content; + + span { + font-weight: bold; + margin: 1em .4em; + } + } + + a.active { + border-bottom: 3px solid white; } } } - + +} + +footer { + color: #ddd; + padding: 2em; + width: max-content; + + a { + text-decoration: none; + } + + a:after { + margin: 0 8px; + content: "•"; + } + + :last-child:after { + content: none; + } } .card { @@ -142,7 +173,7 @@ header { table { border-bottom: 1px solid rgba(0, 0, 0, 0.3); width: 100%; - background-color: #fff; + background-color: white; min-width: 390px; max-width: 1000px; } @@ -210,17 +241,17 @@ header { } .settingsView { - .row, - .column { + .flex-row, + .flex-column { padding: 1em; border-bottom: 1px solid rgba(0, 0, 0, .4); } - .row { + .flex-row { align-items: center; } - .row:last-child { + .flex-row:last-child { padding: .5em; border-bottom: unset; justify-content: right; @@ -239,23 +270,15 @@ header { } select, - .row div { + .flex-row div { margin-left: auto; } } -.searchView { - background-color: #222; - flex-grow: 1; - - .title::before { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28"><rect rx="4" height="28" width="28" fill="green"/><path d="M14 5.5c-4 0-8 .5-8 4V19c0 1.93 1.57 3.5 3.5 3.5L8 24v.5h2.23l2-2H16l2 2h2V24l-1.5-1.5c1.93 0 3.5-1.57 3.5-3.5V9.5c0-3.5-3.58-4-8-4m-4.5 15c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5m3.5-7H8v-4h5zm2 0v-4h5v4zm3.5 7c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5" fill="white"/></svg>'); - width: 50px; - height: 50px; - margin: -0.7em .3em -0.5em -0.3em; - } - +.searchView { .title { + padding-top: 3em; + h1 { color: white; font-weight: normal; @@ -269,6 +292,13 @@ header { } } + .title::before { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28"><rect rx="4" height="28" width="28" fill="green"/><path d="M14 5.5c-4 0-8 .5-8 4V19c0 1.93 1.57 3.5 3.5 3.5L8 24v.5h2.23l2-2H16l2 2h2V24l-1.5-1.5c1.93 0 3.5-1.57 3.5-3.5V9.5c0-3.5-3.58-4-8-4m-4.5 15c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5m3.5-7H8v-4h5zm2 0v-4h5v4zm3.5 7c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5" fill="white"/></svg>'); + width: 50px; + height: 50px; + margin: -0.7em .3em -0.5em -0.3em; + } + form { color: white; @@ -282,12 +312,9 @@ header { .button.icon-arrow1, .button.icon-arrow2, - .button.icon-swap { - padding: 0; - } - + .button.icon-swap, .button.icon-clock { - padding: 4px; + padding: .3em .5em; } button[type="submit"], @@ -320,11 +347,12 @@ header { } .suggestions { + position: relative; overflow: visible; - z-index: 999; + z-index: 100; height: 0; margin-left: 4px; - margin-right: 3.55rem; + margin-right: 3.23rem; p { font-size: 1.2em; @@ -338,7 +366,6 @@ header { p:first-child { margin-top: -4px; - border-top: 0px; } p:hover { @@ -357,7 +384,7 @@ header { overflow: hidden; user-select: none; - .row { + .flex-row { justify-content: space-between; cursor: pointer; padding: .3em .6em .3em .3em; @@ -368,11 +395,12 @@ header { border-bottom: 1px solid rgba(0, 0, 0, .2); } - .row:last-child { + .flex-row:last-child { border-bottom: unset; } .via { + font-size: smaller; font-weight: 200; } @@ -394,32 +422,6 @@ header { width: 25px; } } - - footer { - display: flex; - justify-content: center; - color: #ddd; - padding: 5em 0 1em 0; - - a { - text-decoration: none; - } - - a:after { - margin:0 8px; - content: "•"; - } - - :last-child:after { - content: none; - } - } -} - -.journeyView, -.journeysView, -.departuresView { - min-height: 100vh; } .journeyView { @@ -480,14 +482,14 @@ header { } .remarksView { - .row { + .flex-row { align-items: center; flex-wrap: nowrap; padding: .5em; border-bottom: 1px solid rgba(0, 0, 0, .4); } - .row:last-child { + .flex-row:last-child { border-bottom: unset; } @@ -497,59 +499,62 @@ header { } } +#overlay { + z-index: 100; + backdrop-filter: blur(10px); - -.modal { - z-index: 1050; - margin: auto; - background-color: white; - width: max-content; - padding: 15px; - -webkit-overflow-scrolling: touch; -} - -.modal.alert button { - float: right; -} - -.modal.select { - a { - width: 100%; - margin: 5px 0; - text-align: center; + .modal { + z-index: 1050; + margin: auto; + background-color: white; + width: max-content; + padding: 15px; + -webkit-overflow-scrolling: touch; } - a:first-child { - margin-top: unset; + .modal.alert button { + float: right; } - a:last-child { - margin-bottom: unset; + .modal.select { + a { + width: 100%; + margin: 5px 0; + text-align: center; + } + + a:first-child { + margin-top: unset; + } + + a:last-child { + margin-bottom: unset; + } } -} -.modal.dialog { - padding: unset; + .modal.dialog { + padding: unset; - .header { - justify-content: space-between; - background-color: #33691E; - color: white; - padding: 15px; + .header { + justify-content: space-between; + background-color: #33691E; + color: white; + padding: 15px; - h4 { - margin: 0; - } + h4 { + margin: 0; + } - .icon-close { - margin: -15px; - padding: 10px; - border-left: 1px solid rgba(0, 0, 0, .4); - cursor: pointer; - } + .icon-close { + margin: -15px; + padding: 10px; + border-left: 1px solid rgba(0, 0, 0, .4); + cursor: pointer; + } - .icon-close:hover { - background: rgba(0, 0, 0, .4); + .icon-close:hover { + background: rgba(0, 0, 0, .4); + } } } } @@ -577,20 +582,6 @@ input[type="text"] { border-radius: 0; } -input[type="datetime-local"], -input[type="date"], -input[type="time"] { - font-size: 1.6em; -} - -input[type="text"] { - border: 1px solid transparent; -} - -input[type="text"]:focus { - border-bottom: 1px solid rgba(0, 0, 0, .2); -} - input[type="checkbox"] { transform: scale(1.5); } @@ -616,12 +607,12 @@ button:hover, .button:hover { } button.color, .button.color { - background-color: rgba(20, 30, 255, .7); + background-color: #43a047; color: white; } button.color:hover, .button.color:hover { - background-color: rgba(70, 100, 255, .8); + background-color: #388e3c; } .arrowButton { @@ -646,7 +637,7 @@ button.color:hover, .button.color:hover { } input:checked + label { - background: #fff; + background: white } label { @@ -731,10 +722,6 @@ button.color:hover, .button.color:hover { @media (max-width: 799px) { header { - .content { - flex-grow: 1; - } - .icon-back { left: 10px; } @@ -744,11 +731,11 @@ button.color:hover, .button.color:hover { padding: 10px; } - .row { + .flex-row { flex-wrap: wrap; } - .row.nowrap { + .flex-row.nowrap { flex-wrap: unset; } @@ -762,20 +749,10 @@ button.color:hover, .button.color:hover { width: 48px; height: 48px; } - } @media (min-width: 800px) { - #content { - justify-content: center; - } - .searchView { - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - form, .history { width: 80vw; @@ -792,13 +769,6 @@ button.color:hover, .button.color:hover { } } - table { - overflow: hidden; - border: none; - margin: 50px auto; - width: 80vw; - } - .journeysView { .arrowButton.flipped { margin-top: 45px; @@ -809,84 +779,14 @@ button.color:hover, .button.color:hover { } } + table { + overflow: hidden; + border: none; + margin: 50px auto; + width: 80vw; + } .modal.dialog { width: 400px; } } - -.icon-back { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z" fill="white"/></svg>'); -} - -.icon-reload { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M17.65 6.35A7.96 7.96 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4z" fill="white"/></svg>'); -} - -.icon-close { - content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="30" height="30"><path d="M5.293 5.293a1 1 0 0 1 1.414 0L12 10.586l5.293-5.293a1 1 0 1 1 1.414 1.414L13.414 12l5.293 5.293a1 1 0 0 1-1.414 1.414L12 13.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L10.586 12 5.293 6.707a1 1 0 0 1 0-1.414" fill="white"/></svg>'); -} - -.icon-hint { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M5 3h14a2 2 0 0 1 2 2v14c0 .53-.21 1.04-.59 1.41-.37.38-.88.59-1.41.59H5c-.53 0-1.04-.21-1.41-.59C3.21 20.04 3 19.53 3 19V5c0-1.11.89-2 2-2m6 6h2V7h-2zm3 8v-2h-1v-4h-3v2h1v2h-1v2z"/></svg>'); -} - -.icon-status { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2m8 10V7h-2v6zm0 4v-2h-2v2z"/></svg>'); -} - -.icon-warning { - content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13 13h-2V7h2m-2 8h2v2h-2m4.73-14H8.27L3 8.27v7.46L8.27 21h7.46L21 15.73V8.27z"/></svg>'); -} - -.icon-other { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m1 17h-2v-2h2zm2.07-7.75-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25"/></svg>'); -} - -.icon-arrow1 { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>'); -} - -.icon-arrow2 { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/></svg>'); -} - -.icon-swap { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M16 17.01V10h-2v7.01h-3L15 21l4-3.99zM9 3 5 6.99h3V14h2V6.99h3z"/></svg>'); -} - -.icon-clock { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M12 20a8 8 0 0 0 8-8 8 8 0 0 0-8-8 8 8 0 0 0-8 8 8 8 0 0 0 8 8m0-18a10 10 0 0 1 10 10 10 10 0 0 1-10 10C6.47 22 2 17.5 2 12A10 10 0 0 1 12 2m.5 5v5.25l4.5 2.67-.75 1.23L11 13V7z"/></svg>'); -} - -.icon-settings { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6"/></svg>'); -} - -.icon-walk-fast { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13.49 5.48c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2m-3.6 13.9 1-4.4 2.1 2v6h2v-7.5l-2.1-2 .6-3c1.3 1.5 3.3 2.5 5.5 2.5v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1l-5.2 2.2v4.7h2v-3.4l1.8-.7-1.6 8.1-4.9-1-.4 2z"/></svg>'); -} - -.icon-walk { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13.5 5.5c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2M9.8 8.9 7 23h2.1l1.8-8 2.1 2v6h2v-7.5l-2.1-2 .6-3C14.8 12 16.8 13 19 13v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1L6 8.3V13h2V9.6z"/></svg>'); -} - -.icon-weelchair { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><circle cx="12" cy="4" r="2"/><path d="M19 13v-2c-1.54.02-3.09-.75-4.07-1.83l-1.29-1.43c-.17-.19-.38-.34-.61-.45-.01 0-.01-.01-.02-.01H13c-.35-.2-.75-.3-1.19-.26C10.76 7.11 10 8.04 10 9.09V15c0 1.1.9 2 2 2h5v5h2v-5.5c0-1.1-.9-2-2-2h-3v-3.45c1.29 1.07 3.25 1.94 5 1.95m-6.17 5c-.41 1.16-1.52 2-2.83 2-1.66 0-3-1.34-3-3 0-1.31.84-2.41 2-2.83V12.1a5 5 0 1 0 5.9 5.9z"/></svg>'); -} - -.icon-bike { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M5 20.5A3.5 3.5 0 0 1 1.5 17 3.5 3.5 0 0 1 5 13.5 3.5 3.5 0 0 1 8.5 17 3.5 3.5 0 0 1 5 20.5M5 12a5 5 0 0 0-5 5 5 5 0 0 0 5 5 5 5 0 0 0 5-5 5 5 0 0 0-5-5m9.8-2H19V8.2h-3.2l-1.94-3.27c-.29-.5-.86-.83-1.46-.83-.47 0-.9.19-1.2.5L7.5 8.29C7.19 8.6 7 9 7 9.5c0 .63.33 1.16.85 1.47L11.2 13v5H13v-6.5l-2.25-1.65 2.32-2.35m5.93 13a3.5 3.5 0 0 1-3.5-3.5 3.5 3.5 0 0 1 3.5-3.5 3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m0-8.5a5 5 0 0 0-5 5 5 5 0 0 0 5 5 5 5 0 0 0 5-5 5 5 0 0 0-5-5m-3-7.2c1 0 1.8-.8 1.8-1.8S17 1.2 16 1.2 14.2 2 14.2 3 15 4.8 16 4.8"/></svg>'); -} - -.icon-seat { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M9 19h6v2H9c-2.76 0-5-2.24-5-5V7h2v9c0 1.66 1.34 3 3 3m1.42-13.59c.78-.78.78-2.05 0-2.83s-2.05-.78-2.83 0-.78 2.05 0 2.83c.78.79 2.04.79 2.83 0M11.5 9c0-1.1-.9-2-2-2H9c-1.1 0-2 .9-2 2v6c0 1.66 1.34 3 3 3h5.07l3.5 3.5L20 20.07 14.93 15H11.5z"/></svg>'); -} - -.icon-table { - content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M3 9h14V7H3zm0 4h14v-2H3zm0 4h14v-2H3zm16 0h2v-2h-2zm0-10v2h2V7zm0 6h2v-2h-2z" fill="white" /></svg>'); -} - -.icon-canvas { - content: url('data:image/svg+xml;utf8,<svg version="1.1" viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m11 5v14h2v-14zm-4-2v14h2v-14zm10 4h-2v14h2z" fill="white"/></svg>'); -}
diff --git a/src/departuresView.js b/src/departuresView.js @@ -14,15 +14,16 @@ const departuresTemplate = (data, profile) => { let lastArrival; return html` - <div class="departuresView column"> + <div class="header-container"> <header> - <a id="back" class="icon-back hidden" title="${t('back')}" @click=${() => history.back()}></a> - <div class="content"> + <a id="back" class="icon-back invisible" title="${t('back')}" @click=${() => history.back()}></a> + <div class="container"> <h3>Departures from ${data.name}</h3> </div> <a id="reload" class="icon-reload invisible" title="${t("reload")}"></a> </header> - + </div> + <div class="container departuresView"> <div class="card"> <table> <thead> @@ -34,7 +35,7 @@ const departuresTemplate = (data, profile) => { </thead> <tbody> ${(data.departures || []).map(departure => html` - <tr class="departure" @click=${() => go(`/t/${profile}/${departure.tripId}`)}> + <tr @click=${() => go(`/t/${profile}/${departure.tripId}`)}> <td class="${departure.cancelled ? 'cancelled' : nothing}"><span>${timeTemplate(departure)}</span></td> <td class="${departure.cancelled ? 'cancelled' : nothing}"><span>${departure.line.name}${departure.direction ? html` → ${departure.direction}` : nothing}</span></td> ${departure.cancelled ? html` @@ -53,30 +54,35 @@ const departuresTemplate = (data, profile) => { export const departuresView = async (match, isUpdate) => { if (!isUpdate) showLoader(); + let profile, stopId, when, data; + try { profile = match[0]; - stopId = match[1]; + stopId = match[1]; + if (match[2]) when = new Date(parseInt(match[2].substring(1))); + const client = await getHafasClient(profile); const [ {departures}, stopInfo ] = await Promise.all([ client.departures(stopId, { when }), client.stop(stopId), ]); + for (let departure of departures) { processLeg(departure); }; + data = { ...stopInfo, departures }; } catch(e) { showAlertModal(e.toString()); throw e; } + hideOverlay(); render(departuresTemplate(data, profile), ElementById('content')); setThemeColor(queryBackgroundColor('header')); - if (history.length > 0) { - ElementById('back').classList.remove('hidden'); - } + if (history.length > 0) ElementById('back').classList.remove('invisible'); };
diff --git a/src/formatters.js b/src/formatters.js @@ -1,23 +1,25 @@ -import { ds100Names } from './app_functions.js'; +import { ds100Name } from './app_functions.js'; import { padZeros } from './helpers.js'; import { languages } from './languages.js'; export const formatName = (point) => { - let nameHTML = ''; - - if (point.type === 'stop' || point.type === 'station') { - nameHTML += point.name+ds100Names(point.id); - } else if (point.type == 'location') { - if (point.name) { - nameHTML += point.name; - } else if (point.address) { - nameHTML += point.address; - } - } else { - return ''; - } + switch (point.type) { + case 'stop': + case 'station': + let nameHTML = point.name; + + const ds100 = ds100Name(point.id); + if (ds100 !== null) nameHTML += ` (${ds100})`; - return nameHTML; + return nameHTML; + + case 'location': + if (point.address) return point.address; + return point.name; + + default: + return ''; + }; }; export const formatDateTime = (date, format) => { @@ -31,6 +33,10 @@ export const formatDateTime = (date, format) => { return date.getDate() + '.' + (date.getMonth() + 1) + '.' + date.getFullYear(); break; + case 'time': + return `${padZeros(date.getHours())}:${padZeros(date.getMinutes())}`; + break; + default: return false; break; @@ -47,12 +53,10 @@ export const formatDateTime = (date, format) => { export const formatDuration = (duration) => { const mins = duration / 60000; - const h = Math.floor(mins / 60); - const m = mins % 60; + const h = Math.floor(mins / 60); + const m = mins % 60; - if (h > 0) { - return h+'h '+m+'min'; - } + if (h > 0) return h+'h '+m+'min'; return m+'min'; }; @@ -76,28 +80,42 @@ export const formatFromTo = obj => { export const formatPrice = price => { if (!price) return '-'; - const currencies = { USD: '$', EUR: '€', GBP: '£' }; + + const currencies = { + USD: '$', + EUR: '€', + GBP: '£' + }; + let ret = currencies[price.currency] || price.currency; + ret += `${Math.floor(price.amount)}.${padZeros(price.amount * 100 % 100, 2)}`; + return ret; }; export const formatTrainTypes = info => { const counts = {}; + for (let group of info.sequence?.groups) { const name = group.baureihe?.name; + if (!name) continue; + counts[name] = (counts[name] ? counts[name] : 0) + 1; } + return Object.entries(counts).map(([name, count]) => { let text = ""; - if (count > 1) { - text += `${count} x `; - } + + if (count > 1) text += `${count} x `; + text += name; + while (text.length < 12) { text = ' ' + text + ' '; } + return text; }).join(" + "); }; @@ -105,10 +123,12 @@ export const formatTrainTypes = info => { export const formatLineAdditionalName = (line) => { if (!line.name) return null; const splitName = line.name.split(' '); - if (splitName.length === 2 && line.fahrtNr && line.fahrtNr != splitName[1]) + + if (splitName.length === 2 && line.fahrtNr && line.fahrtNr != splitName[1]) { return `${splitName[0]} ${line.fahrtNr}`; - else + } else { return null; + } }; export const formatLineDisplayName = (line) => line?.name || line?.operator?.name || "???";
diff --git a/src/hafasClient.js b/src/hafasClient.js @@ -6,45 +6,54 @@ let createHafasClient; export let client; +export const initHafasClient = async profileName => { + client = await getHafasClient(profileName); +}; + export const getHafasClient = async profileName => { if (!clients[profileName]) { let profile; + switch(profileName) { case "db": clients[profileName] = createVendoClient(dbnavProfile, "trainsearch", {enrichStations: false}); console.info("initialized vendo client"); return clients[profileName]; + case "bvg": const { profile: bvgProfile } = await import('hafas-client/p/bvg/index.js'); profile = bvgProfile; break; + case "nahsh": const { profile: nahshProfile } = await import('hafas-client/p/nahsh/index.js'); profile = nahshProfile; break; + case "rmv": const { profile: rmvProfile } = await import('hafas-client/p/rmv/index.js'); profile = rmvProfile; break; + case "vrn": const { profile: vrnProfile } = await import('hafas-client/p/vrn/index.js'); profile = vrnProfile; break; + case "oebb": const { profile: oebbProfile } = await import('hafas-client/p/oebb/index.js'); profile = oebbProfile; break; + default: throw "Unknown profile name: " + profileName; } + if (!createHafasClient) createHafasClient = (await import("hafas-client")).createClient; + clients[profileName] = createHafasClient(profile, "trainsearch"); } console.info("initialized hafas client with profile " + profileName); return clients[profileName]; } - -export const initHafasClient = async profileName => { - client = await getHafasClient(profileName); -};
diff --git a/src/helpers.js b/src/helpers.js @@ -1,10 +1,10 @@ const loyaltyCards = { - NONE: Symbol('no loyalty card'), - BAHNCARD: Symbol('Bahncard'), - VORTEILSCARD: Symbol('VorteilsCard'), - HALBTAXABO: Symbol('HalbtaxAbo'), - VOORDEELURENABO: Symbol('Voordeelurenabo'), - SHCARD: Symbol('SH-Card'), + NONE: Symbol('no loyalty card'), + BAHNCARD: Symbol('Bahncard'), + VORTEILSCARD: Symbol('VorteilsCard'), + HALBTAXABO: Symbol('HalbtaxAbo'), + VOORDEELURENABO: Symbol('Voordeelurenabo'), + SHCARD: Symbol('SH-Card'), GENERALABONNEMENT: Symbol('General-Abonnement'), };
diff --git a/src/journeyView.js b/src/journeyView.js @@ -1,7 +1,7 @@ import { html, nothing, render } from 'lit-html'; import { cachedCoachSequence } from './coach-sequence/index.js'; import { settings } from './settings.js'; -import { remarksModalTemplate, platformTemplate, stopTemplate, timeTemplate } from './templates.js'; +import { remarksModalTemplate, platformTemplate, stopTemplate, timeTemplate, footerTemplate } from './templates.js'; import { ElementById, setThemeColor, queryBackgroundColor } from './helpers.js'; import { getJourney, refreshJourney } from './app_functions.js'; import { formatName, formatDateTime, formatDuration, formatPrice, formatTrainTypes, formatLineAdditionalName, formatLineDisplayName } from './formatters.js'; @@ -25,61 +25,61 @@ const legTemplate = (leg, profile) => { <p class="change">${t('changeinfo', formatDuration(leg.duration))}</p> ` : html` <div class="card"> - <table> - <thead> - <tr> - <td colspan="4"> - <div class="center"><a href="#/t/${profile}/${leg.tripId}">${formatLineDisplayName(leg.line)}${leg.direction ? html` → ${leg.direction}` : nothing}</a> - ${leg.cancelled ? html`<b class="cancelled-text">${t('cancelled-ride')}</b>` : nothing} - ${!!remarks.length ? html` - <a class="link ${remarksIcon}" @click=${() => showModal(t('remarks'), remarksModalTemplate(remarks))}></a> - ` : nothing}</div> - </td> - </tr> - <tr> - <td colspan="4"> - <div class="train-details center"> - ${formatLineAdditionalName(leg.line) ? html` - <div> - Trip: ${formatLineAdditionalName(leg.line)} - </div> - ` : nothing} - ${leg.line.trainType ? html` - <div> - Train type: ${leg.line.trainType} - </div> - ` : nothing} - ${(leg.arrival && leg.departure) ? html` - <div> - ${t('duration')}: ${formatDuration(leg.arrival - leg.departure)} - </div> - ` : nothing} - ${leg.loadFactor ? html` - <div> - ${t("load-"+leg.loadFactor)} - </div> - ` : nothing} - </div> - </td> - </tr> - <tr> - <th>${t('arrival')}</th> - <th>${t('departure')}</th> - <th class="station">${t('station')}</th> - <th>${t('platform')}</th> - </tr> - </thead> - <tbody> - ${(leg.stopovers || []).map(stop => html` - <tr class="stop ${stop.cancelled ? 'cancelled' : nothing}"> - <td><span>${timeTemplate(stop, 'arrival')}</span></td> - <td><span>${timeTemplate(stop, 'departure')}</span></td> - <td>${stopTemplate(profile, stop.stop)}</td> - <td><span>${platformTemplate(stop)}</span></td> + <table> + <thead> + <tr> + <td colspan="4"> + <div class="center"><a href="#/t/${profile}/${leg.tripId}">${formatLineDisplayName(leg.line)}${leg.direction ? html` → ${leg.direction}` : nothing}</a> + ${leg.cancelled ? html`<b class="cancelled-text">${t('cancelled-ride')}</b>` : nothing} + ${!!remarks.length ? html` + <a class="link ${remarksIcon}" @click=${() => showModal(t('remarks'), remarksModalTemplate(remarks))}></a> + ` : nothing}</div> + </td> </tr> - `)} - </tbody> - </table> + <tr> + <td colspan="4"> + <div class="train-details flex-center"> + ${formatLineAdditionalName(leg.line) ? html` + <div> + Trip: ${formatLineAdditionalName(leg.line)} + </div> + ` : nothing} + ${leg.line.trainType ? html` + <div> + Train type: ${leg.line.trainType} + </div> + ` : nothing} + ${(leg.arrival && leg.departure) ? html` + <div> + ${t('duration')}: ${formatDuration(leg.arrival - leg.departure)} + </div> + ` : nothing} + ${leg.loadFactor ? html` + <div> + ${t("load-"+leg.loadFactor)} + </div> + ` : nothing} + </div> + </td> + </tr> + <tr> + <th>${t('arrival')}</th> + <th>${t('departure')}</th> + <th class="station">${t('station')}</th> + <th>${t('platform')}</th> + </tr> + </thead> + <tbody> + ${(leg.stopovers || []).map(stop => html` + <tr class="stop ${stop.cancelled ? 'cancelled' : nothing}"> + <td><span>${timeTemplate(stop, 'arrival')}</span></td> + <td><span>${timeTemplate(stop, 'departure')}</span></td> + <td>${stopTemplate(profile, stop.stop)}</td> + <td><span>${platformTemplate(stop)}</span></td> + </tr> + `)} + </tbody> + </table> </div> `} `; @@ -87,19 +87,21 @@ const legTemplate = (leg, profile) => { const journeyTemplate = (data, profile) => { let duration = null; - if (data.legs[data.legs.length - 1].arrival && data.legs[0].departure) { + + if (data.legs[data.legs.length - 1].arrival && data.legs[0].departure) duration = data.legs[data.legs.length - 1].arrival - data.legs[0].departure; - } const legs = []; let changes = 0; let lastArrival; + for (const leg of data.legs) { if (!leg.walking && !leg.transfer) { // add change if (lastArrival) { let duration = null; + if (leg.departure && lastArrival) { duration = leg.departure - lastArrival; } @@ -118,22 +120,27 @@ const journeyTemplate = (data, profile) => { // insert a 0 minutes change entry for this lastArrival = leg.arrival; } + legs.push(leg); } return html` - <div class="journeyView column"> + <div class="header-container"> <header> ${data.slug ? html`<a class="icon-back" href="#/${data.slug}/${settings.journeysViewMode}" title="${t('back')}"></a>` : nothing} - <div class="content"> + <div class="container"> <h3>${formatName(data.legs[0].origin)} → ${formatName(data.legs[data.legs.length - 1].destination)}</h3> <p><b>${t('duration')}: ${formatDuration(duration)} | ${t('changes')}: ${changes-1} | ${t('date')}: ${formatDateTime(data.legs[0].plannedDeparture, 'date')}${settings.showPrices && settings.profile === 'db' && data.price ? html` | ${t('price')}: <td><span>${formatPrice(data.price)}</span></td>` : nothing}</b></p> </div> <a class="icon-reload" title="${t("reload")}" @click=${() => refreshJourneyView(data.refreshToken, profile)}></a> </header> + </div> + <div class="container journeyView"> ${legs.map(leg => legTemplate(leg, profile))} </div> + + ${footerTemplate} `; }; @@ -163,7 +170,7 @@ export const journeyView = async (match, isUpdate) => { for (const leg of journeyObject.legs) { if (leg.line && leg.line.name) { - const [category, number] = leg.line.name.split(" "); + const [category, number] = leg.line.name.split(' '); const info = await cachedCoachSequence(category, leg.line.fahrtNr || number, leg.origin.id, leg.plannedDeparture); if (info) leg.line.trainType = formatTrainTypes(info);
diff --git a/src/journeysView.js b/src/journeysView.js @@ -2,7 +2,7 @@ import { html, nothing, render } from 'lit-html'; import { ElementById, setThemeColor, queryBackgroundColor, padZeros } from './helpers.js'; import { getJourneys, getMoreJourneys, refreshJourneys, getFrom, getTo } from './app_functions.js'; import { formatName, formatDuration, formatFromTo, formatPrice } from './formatters.js'; -import { timeTemplate } from './templates.js'; +import { timeTemplate, footerTemplate } from './templates.js'; import { settings, modifySettings } from './settings.js'; import { setupCanvas } from './journeysViewCanvas.js'; import { go } from './router.js'; @@ -10,13 +10,15 @@ import { showAlertModal, showLoader, hideOverlay } from './overlays.js'; import { t } from './languages.js'; const journeysTemplate = (data) => html` - <div class="journeysView column"> + <div class="header-container"> <header id="header"> <a class="icon-back" href="#/" title="${t('back')}"></a> - <div class="content"> - <h3>${t('from')}: ${formatName(getFrom(data.journeys))}</h3> - <h3>${t('to')}: ${formatName(getTo(data.journeys))}</h3> - <div class="mode-changers"> + <div class="container flex-row"> + <div> + <h3>${t('from')}: ${formatName(getFrom(data.journeys))}</h3> + <h3>${t('to')}: ${formatName(getTo(data.journeys))}</h3> + </div> + <div class="mode-changers flex-row"> <a href="#/${data.slug}/table" class="${settings.journeysViewMode === 'table' ? 'active' : ''}"> <div class="icon-table"></div> <span>${t('table-view')}</span> @@ -29,43 +31,51 @@ const journeysTemplate = (data) => html` </div> <a class="icon-reload" title="${t("reload")}" @click=${() => refreshJourneysView(data.slug)}></a> </header> + </div> - ${settings.journeysViewMode === 'canvas' ? html` - <div id="journeysCanvas"> - <canvas id="canvas"></canvas> - </div> - ` : nothing} - - ${settings.journeysViewMode === 'table' ? html` - ${data.earlierRef ? html`<a class="arrowButton flipped icon-arrow2" title="${t('label_earlier')}" @click=${() => moreJourneys(data.slug, 'earlier')}></a>` : nothing} - - <div class="card"> - <table> - <thead> - <tr> - <th>${t('departure')}</th> - <th>${t('arrival')}</th> - <th>${t('duration')}</th> - <th>${t('changes')}</th> - <th>${t('products')}</th> - ${settings.showPrices && settings.profile === 'db' ? html`<th>${t('price')}</th>` : nothing} - <th></th> - </tr> - </thead> - <tbody> - ${Object.entries(data.journeys).map(([key, value]) => journeyOverviewTemplate(data.profile || "db", value, data.slug, key - data.indexOffset))} - </tbody> - </table> - </div> + ${settings.journeysViewMode === 'canvas' ? html` + + <div id="journeysCanvas"> + <canvas id="canvas"></canvas> + </div> - ${data.laterRef ? html`<a class="arrowButton icon-arrow2" title="${t('label_later')}" @click=${() => moreJourneys(data.slug, 'later')}></a>` : nothing} - ` : nothing} + ` : nothing} + + ${settings.journeysViewMode === 'table' ? html` + + <div class="container journeysView"> + ${data.earlierRef ? html`<a class="arrowButton icon-arrow2 flipped flex-center" title="${t('label_earlier')}" @click=${() => moreJourneys(data.slug, 'earlier')}></a>` : nothing} + + <div class="card"> + <table> + <thead> + <tr> + <th>${t('departure')}</th> + <th>${t('arrival')}</th> + <th>${t('duration')}</th> + <th>${t('changes')}</th> + <th>${t('products')}</th> + ${settings.showPrices && settings.profile === 'db' ? html`<th>${t('price')}</th>` : nothing} + <th></th> + </tr> + </thead> + <tbody> + ${Object.entries(data.journeys).map(([key, value]) => journeyOverviewTemplate(data.profile || "db", value, data.slug, key - data.indexOffset))} + </tbody> + </table> + </div> + + ${data.laterRef ? html`<a class="arrowButton icon-arrow2 flex-center" title="${t('label_later')}" @click=${() => moreJourneys(data.slug, 'later')}></a>` : nothing} </div> + + ${footerTemplate} + ` : nothing} `; const journeyOverviewTemplate = (profile, entry, slug, key) => { const firstLeg = entry.legs[0]; const lastLeg = entry.legs[entry.legs.length - 1]; + let changes = 0; const products = {}; let productsString = ''; @@ -124,7 +134,9 @@ export const journeysView = async (match, isUpdate) => { } let data; + if (!isUpdate) showLoader(); + try { data = await getJourneys(slug); } catch(e) { @@ -132,6 +144,7 @@ export const journeysView = async (match, isUpdate) => { go('/'); throw e; } + if (!data) { await showAlertModal(html`journeys overview id invalid. <br />journeys overview links can not be shared across devices in ${APPNAME}.`); go('/');
diff --git a/src/journeysViewCanvas.js b/src/journeysViewCanvas.js @@ -1,17 +1,15 @@ import { moreJourneys } from './journeysView.js'; import { go } from './router.js'; import { padZeros } from './helpers.js'; -import { formatTrainTypes, formatLineDisplayName } from './formatters.js' +import { formatTrainTypes, formatLineDisplayName, formatDateTime } from './formatters.js' import { cachedCoachSequence, coachSequenceCache, coachSequenceCacheKey } from './coach-sequence/index.js'; -const formatTime = (date) => { - return `${padZeros(date.getHours())}:${padZeros(date.getMinutes())}`; -}; const colorFor = (leg, type) => { const product = leg.line?.product || 'walk'; return colors[type][product] || colors[type].default; }; + const loadFactorColors = { 'low-to-medium': [ '#777', '#ccc', '#ccc' ], 'high': [ '#777', '#777', '#ccc' ], @@ -205,7 +203,7 @@ const renderJourneys = () => { ctx.fillStyle = '#aaa'; while (time < lastArrival) { const y = (time - firstDeparture) * scaleFactor + 32; - ctx.fillText(formatTime(time), (window.innerWidth / dpr) > 600 ? 30 : 10, y); + ctx.fillText(formatDateTime(time, 'time'), (window.innerWidth / dpr) > 600 ? 30 : 10, y); ctx.fillRect(0, y, canvas.width / dpr, 1); time = new Date(Number(time) + 3600000);//Math.floor(120/scaleFactor)); } @@ -313,7 +311,7 @@ const renderJourneys = () => { if (journey.legs.indexOf(leg) == journey.legs.length - 1) times.push([leg.departure || leg.plannedDeparture, y - 9.5]); if (journey.legs.indexOf(leg) == 0) times.push([leg.arrival || leg.plannedArrival, y + duration + 7.5]); for (const [time, y] of times) { - preRenderedText = getTextCache(formatTime(time), '#fff', 15); + preRenderedText = getTextCache(formatDateTime(time, 'time'), '#fff', 15); ctx.scale(1 / dpr, 1 / dpr); ctx.drawImage(preRenderedText, Math.ceil(dpr * (x + ((rectWidth - preRenderedText.width/dpr)) / 2)), dpr * (y - 7.5)); ctx.scale(dpr, dpr);
diff --git a/src/languages.js b/src/languages.js @@ -8,8 +8,19 @@ export const getDefaultLanguage = () => { return 'en'; }; +export const getLanguages = () => { + let availableLanguages = {}; + + Object.keys(languages).forEach((element) => { + availableLanguages[element] = languages[settings.language][element]; + }); + + return availableLanguages; +} + export const t = (key, ...params) => { let translation = languages[settings.language][key]; + if (!translation) translation = languages['en'][key] if (!translation) return key; @@ -55,7 +66,7 @@ const languages = { 'optional': '(optional)', 'options': 'Optionen', 'platform': 'Gleis', - 'products': 'Produkte', + 'product': 'Produkt', 'regionaltrain': 'Regionalverkehr', 'remarks': 'Hinweise', 'save': 'Speichern', @@ -82,8 +93,8 @@ const languages = { 'load-very-high': 'Sehr hohe Auslastung', 'load-exceptionally-high': 'Extrem hohe Auslastung', 'table-view': 'Tabelle', - 'experimental-features': 'Experimentelle Funktionen', - 'show-prices': 'Preise anzeigen', + 'show-prices': 'Preise anzeigen', + 'combineDateTime': 'Kombinierte Zeit & Datumseingabe verwenden', 'titleSetDateTimeNow': 'Setze Uhrzeit & Datum auf jetzt', 'titleBikeFriendly': 'Fahrradmitnahme möglich', 'loyaltyCard': 'Ermäßigungskarte', @@ -159,8 +170,7 @@ const languages = { 'load-very-high': 'Zéér druk', 'load-exceptionally-high': 'Extreem druk', 'table-view': 'Tabel', - 'experimental-features': 'Experimentele functies', - 'show-prices': 'Prijzen tonen', + 'show-prices': 'Prijzen tonen', }, 'en': { @@ -206,7 +216,7 @@ const languages = { 'setfromto': 'Set as from/to', 'settings': 'Settings', 'showdebug': 'Write debug messages to log', - 'showds100': 'Show RIL100 (if available)', + 'showds100': 'Show DS100 (if available)', 'station': 'Station', 'suburbantrain': 'Suburban Trains', 'subway': 'Subway', @@ -226,7 +236,6 @@ const languages = { 'load-exceptionally-high': 'Exceptionally high load', 'table-view': 'Table', 'canvas-view': 'Canvas', - 'experimental-features': 'Experimental features', 'show-prices': 'Show prices', 'price': 'Price', 'back': 'Back',
diff --git a/src/overlays.js b/src/overlays.js @@ -1,5 +1,5 @@ -import { ElementById, showElement, hideElement } from './helpers.js'; import { html, render } from 'lit-html'; +import { ElementById, showElement, hideElement } from './helpers.js'; const overlayElement = ElementById('overlay'); @@ -20,7 +20,7 @@ export const showSelectModal = (items) => { showElement(overlayElement); return new Promise(resolve => { render(html` - <div class="modal select column"> + <div class="modal select flex-column"> ${items.map( (item) => html`<a class="button color" @click=${item.action}>${item.label}</a>` )} @@ -35,7 +35,7 @@ export const showModal = (title, content) => { return new Promise(resolve => { render(html` <div class="modal dialog"> - <div class="header row"> + <div class="header flex-row"> <h4>${title}</h4> <div class="icon-close" title="Close" @click=${() => { hideOverlay(); resolve(); }}></div> </div> @@ -47,11 +47,7 @@ export const showModal = (title, content) => { export const showLoader = () => { showElement(overlayElement); - render(html` - <div class="loading"> - <div class="spinner"></div> - </div> - `, overlayElement); + render(html`<div class="spinner"></div>`, overlayElement); }; export const hideOverlay = () => hideElement(overlayElement);
diff --git a/src/router.js b/src/router.js @@ -14,11 +14,16 @@ export const go = (dest) => { export const start = async () => { const dest = window.location.hash.slice(1); + if (currentRoute && currentRoute.unload) currentRoute.unload(); + for (const route of routes) { const match = route.pattern.exec(dest); + if (!match) continue; + currentRoute = await route.handler(match.slice(1)); + return; } };
diff --git a/src/searchView.js b/src/searchView.js @@ -6,6 +6,7 @@ import { formatName, formatFromTo } from './formatters.js'; import { modifySettings, settings } from './settings.js'; import { go } from './router.js'; import { showAlertModal, showSelectModal, showLoader, hideOverlay} from './overlays.js'; +import { footerTemplate } from './templates.js'; import { t } from './languages.js'; import { showSettings } from './settingsView.js'; import { client } from './hafasClient.js'; @@ -70,34 +71,35 @@ const iconFor = id => { }; const searchTemplate = (journeysHistory) => html` - <div class="searchView"> - <div class="title center"> + <div class="container searchView"> + <div class="title flex-center"> <h1>${APPNAME}</h1> </div> - <form class="column" id="form" @submit=${submitForm}> - <div class="row nowrap"> + + <form class="center" id="form" @submit=${submitForm}> + <div class="flex-row nowrap"> <input type="text" name="from" id="from" title="${t('from')}" placeholder="${t('from')}" value="${viewState.fromValue}" autocomplete="off" @focus=${focusHandler} @blur=${blurHandler} @keydown=${keydownHandler} @keyup=${keyupHandler} @input=${loadSuggestions} required> <div class="button icon-arrow2 ${!settings.showVia ? '' : 'flipped'}" id="viaButton" title="${t('via')}" @click=${toggleVia}></div> </div> - <div class="suggestions" id="fromSuggestions" @mouseover=${mouseOverHandler} @mouseout=${mouseOutHandler}></div> + <div class="suggestions" id="fromSuggestions"></div> - <div class="row nowrap ${!settings.showVia ? 'hidden' : ''}" id="viaRow"> + <div class="flex-row nowrap ${!settings.showVia ? 'hidden' : ''}" id="viaRow"> <input type="text" name="via" id="via" title="${t('via')}" placeholder="${t('via')}" value="${viewState.viaValue}" autocomplete="off" @focus=${focusHandler} @blur=${blurHandler} @keydown=${keydownHandler} @keyup=${keyupHandler} @input=${loadSuggestions}> <div class="button icon-arrow2 invisible"></div> </div> - <div class="suggestions" id="viaSuggestions" @mouseover=${mouseOverHandler} @mouseout=${mouseOutHandler}></div> + <div class="suggestions" id="viaSuggestions"></div> - <div class="row nowrap"> + <div class="flex-row nowrap"> <input type="text" name="to" id="to" title="${t('to')}" placeholder="${t('to')}" value="${viewState.toValue}" autocomplete="off" @focus=${focusHandler} @blur=${blurHandler} @keydown=${keydownHandler} @keyup=${keyupHandler} @input=${loadSuggestions} required> <div class="button icon-swap" title="${t('swap')}" @click=${swapFromTo}></div> </div> - <div class="suggestions" id="toSuggestions" @mouseover=${mouseOverHandler} @mouseout=${mouseOutHandler}></div> + <div class="suggestions" id="toSuggestions"></div> - <div class="row"> + <div class="flex-row"> <div class="selector"> <input type="radio" id="departure" name="deparr" ?checked=${!viewState.isArrival}> <label for="departure">${t('departure')}</label> @@ -111,13 +113,14 @@ const searchTemplate = (journeysHistory) => html` <div class="button icon-clock" title="${t('titleSetDateTimeNow')}" @click=${setDateTimeNow}></div> </div> - <div class="row"> + <div class="flex-row"> <div class="selector rectangular"> ${client.profile.products.map(prod => html` <input type="checkbox" id="${prod.id}" name="${prod.id}" checked> <label class="${iconFor(prod.id)}" for="${prod.id}" title="${t('product')}: ${prod.name}"></label> `)} </div> + <div class="selector rectangular ${settings.profile === 'db' ? 'hidden' : ''}"> <input type="radio" id="accessibilityNone" name="accessibility" value="none" ?checked=${settings.accessibility === 'none'}> <label class="icon-walk-fast" for="accessibilityNone" title="${t('accessibility')}: ${t('access_none')}"></label> @@ -128,10 +131,12 @@ const searchTemplate = (journeysHistory) => html` <input type="radio" id="accessibilityComplete" name="accessibility" value="complete" ?checked=${settings.accessibility === 'complete'}> <label class="icon-weelchair" for="accessibilityComplete" title="${t('accessibility')}: ${t('access_full')}"></label> </div> + <div class="selector rectangular"> <input type="checkbox" id="bikeFriendly" name="bikeFriendly" ?checked=${settings.bikeFriendly}> <label class="icon-bike" for="bikeFriendly" title="${t('titleBikeFriendly')}"></label> </div> + <div class="selector rectangular"> <input type="checkbox" id="noTransfers" name="noTransfers" ?checked=${viewState.noTransfers}> <label class="icon-seat" for="noTransfers" title="${t('titleNoTransfers')}"></label> @@ -148,16 +153,14 @@ const searchTemplate = (journeysHistory) => html` ` : nothing} </form> - <div id="history" class="history hidden"> + <div id="history" class="history center hidden"> ${journeysHistory.map(element => html` - <div class="row" @click="${() => {journeysHistoryAction(journeysHistory, element);}}"> + <div class="flex-row" @click="${() => {journeysHistoryAction(journeysHistory, element);}}"> <div class="from"> <small>${t('from')}:</small> ${formatName(element.fromPoint)} ${element.viaPoint ? html` - <div class="via"> - <small>${t('via')} ${formatName(element.viaPoint)}</small> - </div> + <div class="via">${t('via')} ${formatName(element.viaPoint)}</div> ` : nothing} </div> <div class="icon-arrow1"></div> @@ -168,12 +171,9 @@ const searchTemplate = (journeysHistory) => html` </div> `)} </div> - - <footer> - <a href="https://git.ctu.cx/trainsearch" title="commit ${COMMIT} from ${COMMITDATE}">Source-Code (${VERSION})</a> - <a href="https://ctu.cx/imprint.html">Imprint</a> - </footer> </div> + + ${footerTemplate} `; const journeysHistoryAction = (journeysHistory, element) => { @@ -192,7 +192,7 @@ export const searchView = async (clearInputs) => { const journeysHistory = (await db.getHistory(settings.profile)).slice().reverse(); render(searchTemplate(journeysHistory), ElementById('content')); - setThemeColor(queryBackgroundColor('[class="searchView"]')); + setThemeColor(); if (clearInputs === true) { viewState.fromValue = ''; @@ -204,9 +204,9 @@ export const searchView = async (clearInputs) => { ElementById('from').value = ''; ElementById('via').value = ''; ElementById('to').value = ''; - ElementById('fromSuggestions').innerHTML= ''; - ElementById('viaSuggestions').innerHTML = ''; - ElementById('toSuggestions').innerHTML = ''; + ElementById('fromSuggestions').textContent = ''; + ElementById('viaSuggestions').textContent = ''; + ElementById('toSuggestions').textContent = ''; } for (const [product, enabled] of Object.entries(settings.products)) { @@ -370,8 +370,6 @@ const submitForm = async (event) => { hideOverlay(); - console.log(responseData) - go(`/${responseData.slug}/${settings.journeysViewMode}`); }; @@ -464,7 +462,8 @@ const hideSuggestions = (id) => { }; const suggestionsTemplate = (data, inputId) => data.map((element, index) => html` - <p id="${index == 0 ? inputId+'Selected' : ''}" class="suggestion" @click=${(event) => setSuggestion(encodeURI(JSON.stringify(element)), inputId, event.pointerType)}>${formatName(element)}</p> + <p id="${index == 0 ? inputId+'Selected' : ''}" class="suggestion" + @mouseover=${mouseOverHandler} @mouseout=${mouseOutHandler} @click=${(event) => setSuggestion(encodeURI(JSON.stringify(element)), inputId, event.pointerType)}>${formatName(element)}</p> `); const loadSuggestions = async (event) => { @@ -475,18 +474,16 @@ const loadSuggestions = async (event) => { if (elementValue === '') return; - const results = elementValue ? await Promise.all([ - (async () => { - const ds100Result = await ds100Reverse(elementValue); - if (ds100Result !== null) return await client.locations(ds100Result, {'results': 1}); - return []; - }) (), - client.locations(elementValue, {'results': 10}) - ]) : [[], []]; + let results; + const ds100Result = ds100Reverse(elementValue); - const data = results[0].concat(results[1]); + if (ds100Result !== null) { + results = await client.locations(ds100Result, {'results': 1}); + } else { + results = await client.locations(elementValue, {'results': 10}) + } - render(suggestionsTemplate(data, elementId), ElementById(`${elementId}Suggestions`)); + render(suggestionsTemplate(results, elementId), ElementById(`${elementId}Suggestions`)); }; const focusNextElement = (currentElementId) => {
diff --git a/src/settingsView.js b/src/settingsView.js @@ -14,7 +14,7 @@ export const showSettings = async () => { const settingsTemplate = () => html` <div class="settingsView"> - <div class="row"> + <div class="flex-row"> <label for="language">${t('language')}:</label> <select id="language"> <option value="en" ?selected=${settings.language === 'en'}>${t('en')}</option> @@ -23,7 +23,7 @@ const settingsTemplate = () => html` </select> </div> - <div class="row"> + <div class="flex-row"> <label for="profile">${t('datasource')}:</label> <select id="profile" @change="${(event) => {profileChangeHandler(event.target.value)}}"> <option value="db" ?selected=${settings.profile === 'db'}>DB</option> @@ -35,7 +35,7 @@ const settingsTemplate = () => html` </select> </div> - <div class="row" id="walkingSpeedElement"> + <div class="flex-row" id="walkingSpeedElement"> <label for="walkingSpeed">${t('walkingSpeed')}:</label> <select id="walkingSpeed"> <option value="slow" ?selected=${settings.walkingSpeed === 'slow'}>${t('walkingSpeedSlow')}</option> @@ -44,7 +44,7 @@ const settingsTemplate = () => html` </select> </div> - <div class="row" id="loyaltyCardElement"> + <div class="flex-row" id="loyaltyCardElement"> <label for="loyaltyCard">${t('loyaltyCard')}:</label> <select id="loyaltyCard"> <option value="NONE" ?selected=${settings.loyaltyCard === 'NONE'}>${t('loyaltyCardNone')}</option> @@ -57,14 +57,14 @@ const settingsTemplate = () => html` </select> </div> - <div class="column"> + <div class="flex-column"> <span>${t('options')}:</span> <label id="showPricesElement"><input type="checkbox" id="showPrices" ?checked=${settings.showPrices}> ${t('show-prices')} (${t("experimental")})<br></label> <label id="showDS100Element"><input type="checkbox" id="showDS100" ?checked=${settings.showDS100}> ${t('showds100')}<br></label> <label><input type="checkbox" id="combineDateTime" ?checked=${settings.combineDateTime}> ${t('combineDateTime')}</label> </div> - <div class="row"> + <div class="flex-row"> <button class="color" id="clear" @click=${clearStorage}>${t('clearstorage')}</button> <button class="color" id="save" @click=${saveSettings}>${t('save')}</button> </div> @@ -98,23 +98,23 @@ const clearStorage = async () => { }; const saveSettings = async () => { - const oldSettings = { ...settings }; + const profile = ElementById('profile').value; + let clearInputs = false; + + if (profile !== settings.profile) clearInputs = true; await modifySettings(settings => { settings.combineDateTime = ElementById('combineDateTime').checked; settings.showDS100 = ElementById('showDS100').checked; settings.showPrices = ElementById('showPrices').checked; settings.language = ElementById('language').value; - settings.profile = ElementById('profile').value; + settings.profile = profile; settings.loyaltyCard = ElementById('loyaltyCard').value; settings.walkingSpeed = ElementById('walkingSpeed').value; return settings; }); - let clearInputs = false; - if (oldSettings.profile !== settings.profile) clearInputs = true; - await initHafasClient(settings.profile); searchView(clearInputs); hideOverlay();
diff --git a/src/templates.js b/src/templates.js @@ -1,15 +1,15 @@ -import { html } from 'lit-html'; +import { html, nothing } from 'lit-html'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; import { formatDateTime, lineAdditionalName } from './formatters.js'; import { showModal } from './overlays.js'; import { settings } from './settings.js'; -import { ds100Names } from './app_functions.js'; +import { ds100Name } from './app_functions.js'; import { t } from './languages.js'; export const remarksModalTemplate = (remarks) => html` <div class="remarksView"> ${remarks.map(remark => html` - <div class="row"> + <div class="flex-row"> <span class="icon-${remark.type}"></span> <span>${unsafeHTML(remark.text)}</span> </div> @@ -18,7 +18,8 @@ export const remarksModalTemplate = (remarks) => html` `; export const stopTemplate = (profile, stop) => { - return html`<a class="center" href="#/d/${profile}/${stop.id}">${stop.name} ${ds100Names(stop.id)}</a>`; + const ds100 = ds100Name(stop.id); + return html`<a class="flex-center" href="#/d/${profile}/${stop.id}">${stop.name} ${ds100 !== null ? ` (${ds100})` : nothing}</a>`; } export const platformTemplate = (data) => { @@ -75,3 +76,11 @@ export const timeTemplate = (data, mode) => { `} `; }; + + +export const footerTemplate = html` + <footer class="center"> + <a href="https://git.ctu.cx/trainsearch" title="commit ${COMMIT} from ${COMMITDATE}">Source-Code (${VERSION})</a> + <a href="https://ctu.cx/imprint.html">Imprint</a> + </footer> +`;+ \ No newline at end of file
diff --git a/src/tripView.js b/src/tripView.js @@ -27,71 +27,73 @@ const tripTemplate = (data, profile) => { } return html` - <div class="journeyView column"> + <div class="header-container"> <header> <a id="back" class="icon-back hidden" title="${t('back')}" @click=${() => history.back()}>$</a> - <div class="content"> + <div class="container"> <h3>Trip of ${formatLineDisplayName(data.line)} to ${data.direction}</h3> </div> <a class="icon-reload invisible" title="${t('title')}"></a> </header> + </div> + <div class="container journeyView"> <div class="card"> - <table> - <thead> - <tr> - <td colspan="4"> - <div class="center">${bahnExpertUrl ? html` - <a href="${bahnExpertUrl}">${formatLineDisplayName(data.line)}${data.direction ? html` → ${data.direction}` : ''}</a> - ` : html ` - ${formatLineDisplayName(data.line)}${data.direction ? html` → ${data.direction}` : ''} - `} - ${data.cancelled ? html`<b class="cancelled-text">${t('cancelled-ride')}</b>` : ''} - ${!!remarks.length ? html`<a class="link ${remarksIcon}" @click=${() => showModal(t('remarks'), remarksModalTemplate(remarks))}></a>` : nothing}</div> - </td> - </tr> - <tr> - <td colspan="4"> - <div class="train-details center"> - ${formatLineAdditionalName(data.line) ? html` - <div> - Trip: ${formatLineAdditionalName(data.line)} - </div> - ` : nothing} - ${data.line.trainType ? html` - <div> - Train type: ${data.line.trainType} + <table> + <thead> + <tr> + <td colspan="4"> + <div class="center">${bahnExpertUrl ? html` + <a href="${bahnExpertUrl}">${formatLineDisplayName(data.line)}${data.direction ? html` → ${data.direction}` : ''}</a> + ` : html ` + ${formatLineDisplayName(data.line)}${data.direction ? html` → ${data.direction}` : ''} + `} + ${data.cancelled ? html`<b class="cancelled-text">${t('cancelled-ride')}</b>` : ''} + ${!!remarks.length ? html`<a class="link ${remarksIcon}" @click=${() => showModal(t('remarks'), remarksModalTemplate(remarks))}></a>` : nothing}</div> + </td> + </tr> + <tr> + <td colspan="4"> + <div class="train-details center"> + ${formatLineAdditionalName(data.line) ? html` + <div> + Trip: ${formatLineAdditionalName(data.line)} + </div> + ` : nothing} + ${data.line.trainType ? html` + <div> + Train type: ${data.line.trainType} + </div> + ` : nothing} + <div ${data.cancelled ? 'cancelled' : ''}"> + ${t('duration')}: ${formatDuration(data.arrival - (data.departure ? data.departure : data.plannedDeparture))} ${data.departure ? '' : ('(' + t('planned') + ')')} </div> - ` : nothing} - <div ${data.cancelled ? 'cancelled' : ''}"> - ${t('duration')}: ${formatDuration(data.arrival - (data.departure ? data.departure : data.plannedDeparture))} ${data.departure ? '' : ('(' + t('planned') + ')')} + ${data.loadFactor ? html` + <div + ${t("load-"+data.loadFactor)} + </div> + ` : nothing} </div> - ${data.loadFactor ? html` - <div - ${t("load-"+data.loadFactor)} - </div> - ` : nothing} - </div> - </td> - </tr> - <tr> - <th>${t('arrival')}</th> - <th>${t('departure')}</th> - <th class="station">${t('station')}</th> - <th>${t('platform')}</th> - </tr> - </thead> - <tbody> - ${(data.stopovers || []).map(stop => html` - <tr class="${stop.cancelled ? 'cancelled' : ''}"> - <td>${timeTemplate(stop, 'arrival')}</td> - <td>${timeTemplate(stop, 'departure')}</td> - <td>${stopTemplate(profile, stop.stop)}</td> - <td>${platformTemplate(stop)}</td> + </td> </tr> - `)} - </tbody> - </table> + <tr> + <th>${t('arrival')}</th> + <th>${t('departure')}</th> + <th class="station">${t('station')}</th> + <th>${t('platform')}</th> + </tr> + </thead> + <tbody> + ${(data.stopovers || []).map(stop => html` + <tr class="${stop.cancelled ? 'cancelled' : ''}"> + <td>${timeTemplate(stop, 'arrival')}</td> + <td>${timeTemplate(stop, 'departure')}</td> + <td>${stopTemplate(profile, stop.stop)}</td> + <td>${platformTemplate(stop)}</td> + </tr> + `)} + </tbody> + </table> </div> </div> `; @@ -100,6 +102,7 @@ const tripTemplate = (data, profile) => { export const tripView = async (match, isUpdate) => { if (!isUpdate) showLoader(); let profile, refreshToken, data; + try { profile = match[0]; refreshToken = decodeURIComponent(match[1]); @@ -116,7 +119,5 @@ export const tripView = async (match, isUpdate) => { render(tripTemplate(data.trip, profile), ElementById('content')); setThemeColor(queryBackgroundColor('header')); - if (history.length > 0) { - showElement(ElementById('back')); - } + if (history.length > 0) showElement(ElementById('back')); };