From f5c01f97c9ef4c6e021ba37960e7ad9a2fb91cb0 Mon Sep 17 00:00:00 2001 From: Dan Cojocaru Date: Wed, 13 Jul 2022 00:33:59 +0300 Subject: [PATCH] Initial commit --- back.js | 8 ++ base.css | 199 +++++++++++++++++++++++++++++++++++++ index.html | 28 ++++++ items.js | 48 +++++++++ manifest.webapp | 16 +++ train.html | 30 ++++++ train.js | 109 ++++++++++++++++++++ view-train.css | 75 ++++++++++++++ view-train.html | 42 ++++++++ view-train.js | 258 ++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 813 insertions(+) create mode 100644 back.js create mode 100644 base.css create mode 100644 index.html create mode 100644 items.js create mode 100644 manifest.webapp create mode 100644 train.html create mode 100644 train.js create mode 100644 view-train.css create mode 100644 view-train.html create mode 100644 view-train.js diff --git a/back.js b/back.js new file mode 100644 index 0000000..0c8361b --- /dev/null +++ b/back.js @@ -0,0 +1,8 @@ +window.addEventListener('load', function (e) { + this.document.body.addEventListener('keydown', function (e) { + if (e.key == 'Backspace') { + e.preventDefault() + window.history.back() + } + }) +}) diff --git a/base.css b/base.css new file mode 100644 index 0000000..7308c21 --- /dev/null +++ b/base.css @@ -0,0 +1,199 @@ +html { + height: 100vh; + padding: 0; + margin: 0; +} + +body { + height: 100vh; + padding: 0; + margin: 0; + + font-size: 17px; + font-weight: 400; + + display: flex; + flex-direction: column; + align-items: stretch; +} + +.content { + height: 100%; + + overflow-y: scroll; + + -ms-overflow-style: none; + scrollbar-width: none; +} + +.content::-webkit-scrollbar { + display: none; +} + +footer { + margin-top: auto; + display: flex; + + background-color: #e0e0e0; +} + +footer * { + text-transform: capitalize; +} + +footer .lsk { + text-align: start; +} + +footer .csk { + flex-grow: 1; + text-align: center; + text-transform: uppercase; +} + +footer .rsk { + text-align: end; +} + +h1 { + font-size: 17px; + font-weight: 400; + text-align: center; + + margin: 8px; +} + +h2 { + font-size: 17px; + font-weight: 600; + + margin: 1px 0; + padding-left: 8px; + padding-right: 8px; +} + +h3 { + font-size: 14px; + font-weight: 400; + + margin: 1px 0; + padding-left: 8px; + padding-right: 8px; +} + +h4 { + font-size: 14px; + font-weight: 400; + + margin: 1px 0; + padding-left: 8px; + padding-right: 8px; + + color: #606060; + background-color: #f0f0f0; +} + +h5 { + font-size: 14px; + font-weight: 600; +} + +p.pri { + font-size: 17px; + font-weight: 400; + + margin: 0 8px; +} + +p.sec { + font-size: 14px; + font-weight: 400; + + margin: 0 8px; + + color: gray; +} + +p.thi { + font-size: 12px; + font-weight: 400; + + margin: 0 8px; + + color: gray; +} + +p, ul { + font-size: 17px; + font-weight: 400; +} + +p.link, a { + font-size: 17px; + font-weight: 700; +} + +p.btn, button { + font-size: 17px; + font-weight: 400; + + margin: 8px; + padding: 8px; + border: 1px solid grey; + border-radius: 10px; +} + +ul { + padding: 0; + margin: 0; +} + +li { + list-style: none; + margin: 0; + + display: block; +} + +li:focus { + color: white; + background-color: blue; +} + +a { + display: block; + + padding: 8px; + + color: black; + text-decoration: none; +} + +a.disabled { + color: grey; +} + +a:not(.disabled):hover:not(:focus) { + color: black; + background-color: lightskyblue; +} + +a:focus { + color: white; + background-color: blue; +} + +input { + display: block; + + box-sizing: border-box; + width: calc(100% - 16px); + + margin: 8px; + padding: 2px; + border: 2px solid grey; +} + +.hidden { + display: none; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..ca003cf --- /dev/null +++ b/index.html @@ -0,0 +1,28 @@ + + + + + + + InfoTren + + + + + + +

InfoTren

+ +
+ +
+ + + + \ No newline at end of file diff --git a/items.js b/items.js new file mode 100644 index 0000000..3125c78 --- /dev/null +++ b/items.js @@ -0,0 +1,48 @@ +var currentIndex = 0 + +function nav(offset) { + var items = document.querySelectorAll('.items:not(.disabled)') + if (offset === -1) { + if (currentIndex <= 0) { + return + } + } + else if (offset === 1) { + if (currentIndex >= items.length - 1) { + return + } + } + else { + console.error(`nav called with unknown offset: ${offset}`) + } + + currentIndex += offset + items[currentIndex].focus() + items[currentIndex].addEventListener('keydown', handleKeyDown) +} + +function handleKeyDown(e) { + switch (e.key) { + case 'ArrowUp': + e.preventDefault() + e.stopPropagation() + nav(-1) + break + case 'ArrowDown': + e.preventDefault() + e.stopPropagation() + nav(1) + break + } +} + +window.addEventListener('load', function (e) { + // Select first item + var items = document.querySelectorAll('.items:not(.disabled)') + if (items.length > 0) { + items[0].focus() + items[0].addEventListener('keydown', handleKeyDown) + } + + document.body.addEventListener('keydown', handleKeyDown) +}) diff --git a/manifest.webapp b/manifest.webapp new file mode 100644 index 0000000..a954a30 --- /dev/null +++ b/manifest.webapp @@ -0,0 +1,16 @@ +{ + "version": "1", + "name": "InfoTren", + "launch_path": "/index.html", + "description": "Frontend for InfoFer scraper", + "developer": { + "name": "Dan Cojocaru", + "url": "https://dcdev.ro" + }, + "installs_allowed_from": [ + "*" + ], + "default_locale": "en", + "permissions": {}, + "cursor": false +} \ No newline at end of file diff --git a/train.html b/train.html new file mode 100644 index 0000000..55630d3 --- /dev/null +++ b/train.html @@ -0,0 +1,30 @@ + + + + + + + Train - InfoTren + + + + + + + + +

Train Information

+ +

Train Number

+ + +

Suggestions

+
+ +
+ + + + \ No newline at end of file diff --git a/train.js b/train.js new file mode 100644 index 0000000..79b0cde --- /dev/null +++ b/train.js @@ -0,0 +1,109 @@ +var knownTrains = [] + +function goToTrain(number) { + var url = new URL(window.location.href) + url.pathname = 'view-train.html' + url.searchParams.set('train', number) + url.searchParams.set('date', new Date().toISOString()) + window.location.href = url.toString() +} + +var _rebuildDebounce = null +var _rebuildRequested = false +function rebuildSuggestions() { + if (_rebuildDebounce !== null) { + _rebuildRequested = true + return + } + + _rebuildRequested = false + _rebuildDebounce = 123 + + var suggestionsArea = document.getElementById('suggestionsArea') + while (suggestionsArea.childNodes.length > 0) { + suggestionsArea.childNodes[0].remove() + } + + var trainNumberInput = document.getElementById('trainNumber') + var trainNumber = trainNumberInput.value.trim() + + var suggestions = [] + for (var i = 0; i < knownTrains.length; i++) { + if (trainNumber) { + if (!knownTrains[i].number.includes(trainNumber)) { + continue + } + } + suggestions.push(knownTrains[i]) + } + + suggestions.forEach(function (suggestion, index) { + var suggestionLi = document.createElement('li') + suggestionsArea.appendChild(suggestionLi) + + setTimeout(function () { + suggestionLi.classList.add('items') + suggestionLi.tabIndex = index + 1 + suggestionLi.style.padding = '2px 0' + + function onAction(e) { + goToTrain(suggestion.number) + } + suggestionLi.addEventListener('click', onAction) + suggestionLi.addEventListener('keypress', function (e) { + if (e.key == 'Enter') { + onAction(e) + } + }) + + var trainNameP = document.createElement('p') + suggestionLi.appendChild(trainNameP) + + trainNameP.textContent = `${suggestion.rank} ${suggestion.number}` + trainNameP.classList.add('pri', 'trainName') + + var trainCompanyP = document.createElement('p') + suggestionLi.appendChild(trainCompanyP) + + trainCompanyP.textContent = suggestion.company + trainCompanyP.classList.add('thi') + }, 0) + }) + + setTimeout(function () { + _rebuildDebounce = null + if (_rebuildRequested) { + rebuildSuggestions() + } + }, 500) +} + +window.addEventListener('load', function (e) { + var trainNumber = document.getElementById('trainNumber') + trainNumber.addEventListener('input', function (e) { + rebuildSuggestions() + }) + trainNumber.addEventListener('focus', function (e) { + document.getElementsByClassName('csk')[0].textContent = 'Search' + }) + trainNumber.addEventListener('blur', function (e) { + document.getElementsByClassName('csk')[0].textContent = 'Select' + }) + trainNumber.addEventListener('keypress', function (e) { + if (e.key == 'Enter') { + goToTrain(trainNumber.value.trim()) + } + }) + + fetch('https://scraper.infotren.dcdev.ro/v2/trains') + .then(function (response) { + return response.json() + }) + .then(function (response) { + knownTrains = response + knownTrains.sort(function(a, b) { return a.number - b.number }) + }) + .then(function () { + rebuildSuggestions() + }) +}) diff --git a/view-train.css b/view-train.css new file mode 100644 index 0000000..7d603f5 --- /dev/null +++ b/view-train.css @@ -0,0 +1,75 @@ +.IR, .IRN { + color: red; +} + +.early { + color: green; +} + +.late { + color: red; +} + +.station { + color: black; +} + +#company { + text-align: center; +} + +.stationItem { + display: grid; + grid-template-columns: 50px auto 50px; + grid-template-rows: auto; + grid-template-areas: + "arr name dep" + "arr km dep" + "arr platform dep"; + padding: 4px 0; +} + +.stationItem .name { + text-align: center; + grid-area: name; +} + +.stationItem .arrival { + text-align: start; + grid-area: arr; + + align-items: flex-start; +} + +.stationItem .departure { + text-align: end; + grid-area: dep; + + align-items: flex-end; +} + +.stationItem .arrival, .station .departure { + align-self: center; + + display: flex; + flex-direction: column; +} + +.stationItem .arrival .original, .stationItem .departure .original { + color: #a0a0a0; + text-decoration: line-through; +} + +.stationItem .arrival .not-real, .stationItem .departure .not-real { + font-style: italic; +} + +.stationItem .km { + text-align: center; + grid-area: km; +} + +.stationItem .platform { + text-align: center; + grid-area: platform; +} diff --git a/view-train.html b/view-train.html new file mode 100644 index 0000000..d910528 --- /dev/null +++ b/view-train.html @@ -0,0 +1,42 @@ + + + + + + + View Train - InfoTren + + + + + + + + +

Train Info

+

+ +
+
+

Route

+

From

+

+

To

+

+
+ +
+

Stations

+
+
+ + + + \ No newline at end of file diff --git a/view-train.js b/view-train.js new file mode 100644 index 0000000..551275e --- /dev/null +++ b/view-train.js @@ -0,0 +1,258 @@ +var trainNumber +var date + +var showKm = false + +var trainData = null + +function onTrainData(data) { + var title = document.getElementsByTagName('h1')[0] + title.textContent = '' + title.appendChild(document.createTextNode('Train ')) + var rankSpan = document.createElement('span') + rankSpan.textContent = data.rank + rankSpan.classList.add(data.rank) + title.appendChild(rankSpan) + title.appendChild(document.createTextNode(` ${data.number}`)) + + document.getElementById('company').textContent = data.operator + + document.getElementById('route-from').textContent = data.route.from + document.getElementById('route-to').textContent = data.route.to + + if (data.status) { + document.getElementById('status').classList.remove('hidden') + + var statusDelay = document.getElementById('status-delay') + while (statusDelay.childNodes.length > 0) { + statusDelay.childNodes[0].remove() + } + var delayString = '' + var delayMinutes = data.status.delay + if (delayMinutes === 0) { + delayString = 'No delay' + statusDelay.appendChild(document.createTextNode(delayString)) + } + else { + var early = false + if (delayMinutes < 0) { + early = true + delayMinutes = -delayMinutes + } + + if (delayMinutes >= 60) { + var hours = Math.floor(delayMinutes / 60) + delayMinutes = delayMinutes % 60 + delayString += hours.toString() + delayString += ' hour' + if (hours > 1) { + delayString += 's' + } + } + if (delayMinutes > 0) { + if (delayString.length > 0) { + delayString += ' and ' + } + delayString += delayMinutes.toString() + delayString += ' minute' + if (delayMinutes > 1) { + delayString += 's' + } + } + delayString += ' ' + statusDelay.appendChild(document.createTextNode(delayString)) + + var kindSpan = document.createElement('span') + statusDelay.appendChild(kindSpan) + if (early) { + kindSpan.textContent = 'early' + kindSpan.classList.add('early') + } + else { + kindSpan.textContent = 'late' + kindSpan.classList.add('late') + } + } + + var statusLocation = document.getElementById('status-location') + while (statusLocation.childNodes.length > 0) { + statusLocation.childNodes[0].remove() + } + var stateString = '' + if (data.status.state === 'arrival') { + stateString += 'when arriving at ' + } + else if (data.status.state === 'departure') { + stateString += 'when departing from ' + } + else if (data.status.state === 'passing') { + stateString += 'while passing through ' + } + statusLocation.appendChild(document.createTextNode(stateString)) + var stationSpan = document.createElement('span') + statusLocation.appendChild(stationSpan) + stationSpan.textContent = data.status.station + stationSpan.classList.add('station') + } + else { + document.getElementById('status').classList.add('hidden') + } + + var stationsDiv = document.getElementById('stations') + while (stationsDiv.childNodes.length > 0) { + stationsDiv.childNodes[0].remove() + } + + var separator = document.createElement('h4') + stationsDiv.appendChild(separator) + separator.textContent = 'Stations' + + var stationsList = document.createElement('ul') + stationsDiv.appendChild(stationsList) + + data.stations.forEach(function (station) { + var stationItem = document.createElement('li') + stationsList.appendChild(stationItem) + stationItem.classList.add('stationItem') + + var stationName = document.createElement('p') + stationItem.appendChild(stationName) + stationName.textContent = station.name + stationName.classList.add('pri', 'name') + + if (station.arrival) { + var stationArrival = document.createElement('div') + stationItem.appendChild(stationArrival) + stationArrival.classList.add('arrival') + + var originalArr = document.createElement('p') + stationArrival.appendChild(originalArr) + var arrDate = new Date(station.arrival.scheduleTime) + originalArr.textContent = `${arrDate.getHours().toString().padStart(2, "0")}:${arrDate.getMinutes().toString().padStart(2, "0")}` + originalArr.classList.add('pri') + if (station.arrival.status && station.arrival.status.delay != 0) { + originalArr.classList.remove('pri') + originalArr.classList.add('thi', 'original') + + var actualArr = document.createElement('p') + stationArrival.appendChild(actualArr) + arrDate.setMinutes(arrDate.getMinutes() + station.arrival.status.delay) + actualArr.textContent = `${arrDate.getHours().toString().padStart(2, "0")}:${arrDate.getMinutes().toString().padStart(2, "0")}` + actualArr.classList.add('pri', station.arrival.status.delay > 0 ? 'late' : 'early') + if (!station.arrival.status.real) { + actualArr.classList.add('not-real') + } + } + } + + if (station.departure) { + var stationDeparture = document.createElement('div') + stationItem.appendChild(stationDeparture) + stationDeparture.classList.add('departure') + + var originalDep = document.createElement('p') + stationDeparture.appendChild(originalDep) + var depDate = new Date(station.departure.scheduleTime) + originalDep.textContent = `${depDate.getHours().toString().padStart(2, "0")}:${depDate.getMinutes().toString().padStart(2, "0")}` + originalDep.classList.add('pri') + if (station.departure.status && station.departure.status.delay != 0) { + originalDep.classList.remove('pri') + originalDep.classList.add('thi', 'original') + + var actualDep = document.createElement('p') + stationDeparture.appendChild(actualDep) + depDate.setMinutes(depDate.getMinutes() + station.departure.status.delay) + actualDep.textContent = `${depDate.getHours().toString().padStart(2, "0")}:${depDate.getMinutes().toString().padStart(2, "0")}` + actualDep.classList.add('pri', station.departure.status.delay > 0 ? 'late' : 'early') + if (!station.departure.status.real) { + actualDep.classList.add('not-real') + } + } + } + + var stationKm = document.createElement('p') + stationItem.appendChild(stationKm) + stationKm.textContent = `${station.km} km` + stationKm.classList.add('thi', 'km') + if (!showKm) { + stationKm.classList.add('hidden') + } + + if (station.platform) { + var stationPlatform = document.createElement('p') + stationItem.appendChild(stationPlatform) + stationPlatform.textContent = `platform ${station.platform}` + stationPlatform.classList.add('thi', 'platform') + } + }) +} + +var refreshStopToken = null +function refresh() { + fetch(`https://scraper.infotren.dcdev.ro/v2/train/${trainNumber}?date=${date.toISOString()}`) + .then(function (response) { + return response.json() + }) + .then(function (response) { + trainData = response + onTrainData(response) + }) + .then(function () { + refreshStopToken = setTimeout(function () { + refresh() + }, 60000) + }) +} + +window.addEventListener('unload', function (e) { + if (refreshStopToken != null) { + clearTimeout(refreshStopToken) + } +}) + +window.addEventListener('load', function (e) { + if (!new URL(window.location.href).searchParams.has('train')) { + window.history.back() + this.setTimeout(function () { + var url = new URL(window.location.href) + url.pathname = 'train.html' + window.location.href = url.toString() + }, 100) + } + + var sp = new URL(window.location.href).searchParams + + trainNumber = sp.get('train') + date = sp.has('date') ? new Date(sp.get('date')) : new Date() + + var content = document.getElementsByClassName('content')[0] + content.focus() + content.addEventListener('keydown', function (e) { + switch (e.key) { + case 'ArrowUp': + content.scrollBy(0, -50) + break + case 'ArrowDown': + content.scrollBy(0, 50) + break + case 'SoftRight': + refresh() + break + case '*': + showKm = !showKm + document.querySelectorAll('.km').forEach(function (kmItem) { + if (showKm) { + kmItem.classList.remove('hidden') + } + else { + kmItem.classList.add('hidden') + } + }) + break + default: + console.log(e.key) + } + }) + + refresh() +})