Compare commits

..

32 Commits

Author SHA1 Message Date
Spythere b01d3e894b Merge pull request #63 from Spythere/development
hotfix: statusy dr
2023-11-15 21:07:28 +01:00
Spythere 13dc6a0e32 hotfix: statusy dr 2023-11-15 21:06:16 +01:00
Spythere 96714550d0 Merge pull request #62 - Wersja 1.18.3
Wersja 1.18.3
2023-11-15 02:13:54 +01:00
Spythere 2b6c751f55 hotifx toString 2023-11-13 15:57:01 +01:00
Spythere 08d3a2a03a feature: nawigacja URL w widoku scenerii 2023-11-13 15:32:02 +01:00
Spythere a79ca78781 poprawki animacji statusów danych & tryb offline 2023-11-13 14:59:17 +01:00
Spythere e08333dba1 fix: tłumaczenie statusów dr 2023-11-12 16:40:25 +01:00
Spythere 8705dd1df5 fix filtrowania RJ na posterunkach; favicons index 2023-11-12 15:47:55 +01:00
Spythere 7b4da9d422 poprawki layoutu aplikacji 2023-11-10 16:18:06 +01:00
Spythere e51b2fe2f3 poprawki filtrów poc. 2023-11-10 15:39:49 +01:00
Spythere f8b4ce103f refactor typów danych 2023-11-10 15:04:49 +01:00
Spythere e82b4b8817 bump 1.18.3 2023-11-08 22:04:11 +01:00
Spythere 36e9df82b0 hotfixy 2023-11-08 22:03:45 +01:00
Spythere cbce9af00b nowe pobieranie i przetwarzanie statusów dyżurnych 2023-11-07 20:16:58 +01:00
Spythere 4a4304d65f Merge pull request #61 - Wersja 1.18.2
Wersja 1.18.2
2023-11-04 17:03:51 +01:00
Spythere edad5306f2 bump: 1.18.2 2023-11-04 17:01:01 +01:00
Spythere 5b775dfec9 fix: filtry RJ 2023-11-04 17:00:50 +01:00
Spythere a485652ca5 Merge pull request #60 (hotfix)
hotfix: maksymalny timeout dyżurnych (1.18.1)
2023-11-02 22:44:48 +01:00
Spythere ed308246d7 hotfix: maksymalny timeout dyżurnych 2023-11-02 22:42:28 +01:00
Spythere 621bb1ad55 Merge pull request #59 - Wersja 1.18.1
Wersja 1.18.1
2023-11-02 17:41:59 +01:00
Spythere 154ae2ddac bump 1.18.1 2023-11-02 17:40:55 +01:00
Spythere d9da49a867 rozszerzony wybór regionów przez URL; poprawki headerów 2023-11-02 17:40:31 +01:00
Spythere 826d51f66c Merge pull request #58 - Wersja 1.18
Wersja 1.18
2023-11-02 01:27:29 +01:00
Spythere 1d7fc2955f animacje userów i spawnów scenerii 2023-11-01 17:20:05 +01:00
Spythere c550e7598a bump 1.18.0 2023-10-31 23:03:51 +01:00
Spythere d5168ce59d lock sync 2023-10-31 22:54:53 +01:00
Spythere 380c97655c filtrowanie statusów; poprawki w statystykach 2023-10-31 22:53:18 +01:00
Spythere e4ed24de80 region query 2023-10-31 02:35:41 +01:00
Spythere 8de03b9210 rework reaktywności danych z API i WS 2023-10-30 23:19:17 +01:00
Spythere 12ece46089 migracja assetów 2023-10-04 17:30:30 +02:00
Spythere 085238fada hotfix: zapamiętywanie stanu statystyk 2023-10-04 15:32:36 +02:00
Spythere 45c1d83512 format & lint 2023-10-04 15:01:01 +02:00
197 changed files with 16859 additions and 24434 deletions
+18
View File
@@ -0,0 +1,18 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
rules: {
'vue/multi-word-component-names': 'off'
},
parserOptions: {
ecmaVersion: 'latest'
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}
+25 -11
View File
@@ -1,11 +1,14 @@
<!DOCTYPE html> <!doctype html>
<html lang="pl"> <html lang="pl">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta name="keywords" content="Stacjownik, TD2, Train Driver 2, stacjownik-td2, stacjownik, td2.info.pl" /> <meta
name="keywords"
content="Stacjownik, TD2, Train Driver 2, stacjownik-td2, stacjownik, td2.info.pl"
/>
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" /> <meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
<title>Stacjownik</title> <title>Stacjownik</title>
@@ -18,10 +21,6 @@
<meta name="msapplication-TileColor" content="#da532c" /> <meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#222222" /> <meta name="theme-color" content="#222222" />
<link rel="icon" href="favicon-64.png" sizes="64x64" type="image/png" />
<link rel="icon" href="favicon-62.png" sizes="62x62" type="image/png" />
<link rel="icon" href="favicon-32.png" sizes="32x32" type="image/png" />
<link rel="icon" href="favicon-16.png" sizes="16x16" type="image/png" />
<link rel="icon" href="favicon.ico" /> <link rel="icon" href="favicon.ico" />
<!-- Static OpenGraph meta --> <!-- Static OpenGraph meta -->
@@ -29,18 +28,33 @@
<meta property="og:url" content="https://stacjownik-td2.web.app/" /> <meta property="og:url" content="https://stacjownik-td2.web.app/" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="Stacjownik" /> <meta property="og:title" content="Stacjownik" />
<meta property="og:description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" /> <meta
<meta property="og:image" content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg" /> property="og:description"
content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2"
/>
<meta
property="og:image"
content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg"
/>
<meta property="og:image:width" content="1200" /> <meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" /> <meta property="og:image:height" content="630" />
<meta property="og:site_name" content="Stacjownik" /> <meta property="og:site_name" content="Stacjownik" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Stacjownik" /> <meta name="twitter:title" content="Stacjownik" />
<meta name="twitter:description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" /> <meta
<meta name="twitter:image" content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg" /> name="twitter:description"
content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2"
/>
<meta
name="twitter:image"
content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg"
/>
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=swap" rel="stylesheet" /> <link
href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=swap"
rel="stylesheet"
/>
</head> </head>
<body> <body>
+3027 -7
View File
File diff suppressed because it is too large Load Diff
+13 -3
View File
@@ -1,12 +1,15 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.17.1", "version": "1.18.3",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"deploy": "yarn build && firebase deploy --only hosting", "deploy": "yarn build && firebase deploy --only hosting",
"preview": "yarn build && vite preview" "preview": "yarn build && vite preview",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"core-js": "^3.32.2", "core-js": "^3.32.2",
@@ -25,10 +28,17 @@
"@vite-pwa/assets-generator": "^0.0.10", "@vite-pwa/assets-generator": "^0.0.10",
"@vitejs/plugin-vue": "^4.3.4", "@vitejs/plugin-vue": "^4.3.4",
"axios": "^1.5.0", "axios": "^1.5.0",
"prettier": "^3.0.3",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.4.9", "vite": "^4.4.9",
"vite-plugin-pwa": "^0.16.5", "vite-plugin-pwa": "^0.16.5",
"vue-tsc": "^1.8.11" "vue-tsc": "^1.8.11",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"@rushstack/eslint-patch": "^1.3.3"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",

Before

Width:  |  Height:  |  Size: 363 B

After

Width:  |  Height:  |  Size: 363 B

Before

Width:  |  Height:  |  Size: 537 B

After

Width:  |  Height:  |  Size: 537 B

Before

Width:  |  Height:  |  Size: 482 B

After

Width:  |  Height:  |  Size: 482 B

Before

Width:  |  Height:  |  Size: 271 B

After

Width:  |  Height:  |  Size: 271 B

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 201 B

Before

Width:  |  Height:  |  Size: 212 B

After

Width:  |  Height:  |  Size: 212 B

Before

Width:  |  Height:  |  Size: 170 B

After

Width:  |  Height:  |  Size: 170 B

Before

Width:  |  Height:  |  Size: 350 B

After

Width:  |  Height:  |  Size: 350 B

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 264 B

After

Width:  |  Height:  |  Size: 264 B

Before

Width:  |  Height:  |  Size: 582 B

After

Width:  |  Height:  |  Size: 582 B

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 256 B

After

Width:  |  Height:  |  Size: 256 B

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 201 B

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 632 B

After

Width:  |  Height:  |  Size: 632 B

Before

Width:  |  Height:  |  Size: 398 B

After

Width:  |  Height:  |  Size: 398 B

Before

Width:  |  Height:  |  Size: 938 B

After

Width:  |  Height:  |  Size: 938 B

Before

Width:  |  Height:  |  Size: 356 B

After

Width:  |  Height:  |  Size: 356 B

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 369 B

After

Width:  |  Height:  |  Size: 369 B

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 252 B

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 264 B

After

Width:  |  Height:  |  Size: 264 B

Before

Width:  |  Height:  |  Size: 199 B

After

Width:  |  Height:  |  Size: 199 B

Before

Width:  |  Height:  |  Size: 384 B

After

Width:  |  Height:  |  Size: 384 B

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Before

Width:  |  Height:  |  Size: 270 B

After

Width:  |  Height:  |  Size: 270 B

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 476 B

Before

Width:  |  Height:  |  Size: 863 B

After

Width:  |  Height:  |  Size: 863 B

Before

Width:  |  Height:  |  Size: 304 B

After

Width:  |  Height:  |  Size: 304 B

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 546 B

Before

Width:  |  Height:  |  Size: 230 B

After

Width:  |  Height:  |  Size: 230 B

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before

Width:  |  Height:  |  Size: 199 B

After

Width:  |  Height:  |  Size: 199 B

Before

Width:  |  Height:  |  Size: 522 B

After

Width:  |  Height:  |  Size: 522 B

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 268 B

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before

Width:  |  Height:  |  Size: 478 B

After

Width:  |  Height:  |  Size: 478 B

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 477 B

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 477 B

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 477 B

Before

Width:  |  Height:  |  Size: 457 B

After

Width:  |  Height:  |  Size: 457 B

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 326 B

Before

Width:  |  Height:  |  Size: 285 B

After

Width:  |  Height:  |  Size: 285 B

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+99 -105
View File
@@ -1,105 +1,99 @@
@import './styles/responsive.scss'; @import './styles/responsive.scss';
@import './styles/variables.scss'; @import './styles/variables.scss';
@import './styles/global.scss'; @import './styles/global.scss';
// VUE ROUTE CHANGE ANIMATION // VUE ROUTE CHANGE ANIMATION
.view-anim { .view-anim {
&-enter-from, &-enter-from,
&-leave-to { &-leave-to {
opacity: 0.02; opacity: 0.02;
} }
&-enter-active, &-enter-active,
&-leave-active { &-leave-active {
transition: all $animDuration $animType; transition: all $animDuration $animType;
min-height: 100%; min-height: 100%;
} }
} }
.modal-anim { .modal-anim {
&-enter-active, &-enter-active,
&-leave-active { &-leave-active {
transition: all $animDuration $animType; transition: all $animDuration $animType;
} }
&-enter-from, &-enter-from,
&-leave-to { &-leave-to {
transform: translateY(-25%); transform: translateY(-25%);
opacity: 0; opacity: 0;
} }
} }
.route { .route {
margin: 0 0.2em; margin: 0 0.2em;
&-active, &-active,
&[data-active='true'] { &[data-active='true'] {
color: $accentCol; color: $accentCol;
font-weight: bold; font-weight: bold;
} }
} }
// APP // APP
#app { #app {
color: white; color: white;
font-size: 1rem; font-size: 1rem;
overflow-x: hidden;
@include smallScreen() {
font-size: calc(0.55rem + 1.1vw); @include smallScreen() {
} font-size: calc(0.65rem + 0.8vw);
}
@include screenLandscape() {
font-size: calc(0.45rem + 0.8vw); @include screenLandscape() {
} font-size: calc(0.45rem + 0.8vw);
} }
}
// CONTAINER
.app_container { // CONTAINER
display: flex; .app_container {
flex-flow: column; // display: flex;
// flex-flow: column;
min-height: 100vh; display: grid;
grid-template-rows: auto 1fr auto;
header { grid-template-columns: 100%;
flex: 0 0 auto;
} min-height: 100vh;
}
main {
flex: 1 1 auto; .app_main {
padding: 0 0.5em;
padding: 0 0.5em; }
}
.warning {
footer { background-color: firebrick;
flex: 0 1 0.2em; text-align: center;
} padding: 0.5em 0.4em;
} max-width: 1100px;
margin: 0 auto;
.warning {
background-color: firebrick; border-radius: 0 0 1em 1em;
text-align: center; }
padding: 0.5em 0.4em;
max-width: 1100px; // FOOTER
margin: 0 auto; footer.app_footer {
max-width: 100%;
border-radius: 0 0 1em 1em; padding: 0.5em;
}
img {
// FOOTER width: 1.1em;
footer.app_footer { vertical-align: text-bottom;
max-width: 100%; }
padding: 0.5em;
z-index: 10;
img {
width: 1.1em; background: #111;
vertical-align: text-bottom; color: white;
}
text-align: center;
z-index: 10; vertical-align: middle;
}
background: #111;
color: white;
text-align: center;
vertical-align: middle;
}
+159 -178
View File
@@ -1,178 +1,159 @@
<template> <template>
<div class="app_container"> <div class="app_container">
<transition name="modal-anim"> <transition name="modal-anim">
<keep-alive> <keep-alive>
<TrainModal v-if="store.chosenModalTrainId" /> <TrainModal v-if="store.chosenModalTrainId" />
</keep-alive> </keep-alive>
</transition> </transition>
<UpdatePrompt /> <AppHeader :current-lang="currentLang" @change-lang="changeLang" />
<AppHeader :current-lang="currentLang" @change-lang="changeLang" /> <main class="app_main">
<router-view v-slot="{ Component }">
<main class="app_main"> <keep-alive exclude="JournalView,SceneryView">
<router-view v-slot="{ Component }"> <component :is="Component" :key="$route.name" />
<keep-alive exclude="JournalView"> </keep-alive>
<component :is="Component" :key="$route.name" /> </router-view>
</keep-alive> </main>
</router-view>
</main> <footer class="app_footer">
&copy;
<footer class="app_footer"> <a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
&copy; {{ new Date().getUTCFullYear() }} |
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a> <a :href="releaseURL" target="_blank">v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}</a>
{{ new Date().getUTCFullYear() }} | <br />
<a :href="releaseURL" target="_blank">v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}</a> <a href="https://discord.gg/x2mpNN3svk">
<br /> <img src="/images/icon-discord.png" alt="" />&nbsp;<b>{{ $t('footer.discord') }}</b>
<a href="https://discord.gg/x2mpNN3svk"><img :src="getIcon('discord', 'png')" alt="">&nbsp;<b>{{ $t('footer.discord') }}</b></a> </a>
<div style="display: none">&int; ukryta taktyczna całka do programowania w HTMLu</div> <div style="display: none">&int; ukryta taktyczna całka do programowania w HTMLu</div>
</footer> </footer>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, KeepAlive, provide, ref, watch } from 'vue'; import { defineComponent, watch } from 'vue';
import Clock from './components/App/Clock.vue'; import Clock from './components/App/Clock.vue';
import packageInfo from '.././package.json'; import packageInfo from '.././package.json';
import { regions } from './data/options.json';
import StatusIndicator from './components/App/StatusIndicator.vue';
import SelectBox from './components/Global/SelectBox.vue'; import { useStore } from './store/mainStore';
import { useStore } from './store/store'; import StatusIndicator from './components/App/StatusIndicator.vue';
import TrainModal from './components/Global/TrainModal.vue'; import TrainModal from './components/Global/TrainModal.vue';
import StorageManager from './scripts/managers/storageManager'; import AppHeader from './components/App/AppHeader.vue';
import imageMixin from './mixins/imageMixin'; import axios from 'axios';
import AppHeader from './components/App/AppHeader.vue'; import StorageManager from './managers/storageManager';
import axios from 'axios';
import UpdatePrompt from './components/App/UpdatePrompt.vue'; export default defineComponent({
import { VERSION } from 'vue-i18n'; components: {
import { RouterView } from 'vue-router'; Clock,
import useCustomSW from './mixins/useCustomSW'; StatusIndicator,
TrainModal,
export default defineComponent({ AppHeader
components: { },
Clock,
StatusIndicator, data: () => ({
SelectBox, VERSION: packageInfo.version,
TrainModal, store: useStore(),
AppHeader,
UpdatePrompt, currentLang: 'pl',
}, releaseURL: '',
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
mixins: [imageMixin], }),
setup() { created() {
const store = useStore(); this.loadLang();
store.connectToAPI(); this.store.connectToAPI();
const { offlineReady } = useCustomSW(); this.store.isOffline = !window.navigator.onLine;
const isFilterCardVisible = ref(false); window.addEventListener('offline', () => {
this.store.isOffline = true;
provide('isFilterCardVisible', isFilterCardVisible);
this.store.activeData.activeSceneries = [];
return { this.store.activeData.trains = [];
store, this.store.activeData.connectedSocketCount = 0;
isFilterCardVisible,
onlineDispatchers: computed(() => this.store.setStatuses();
store.stationList.filter((station) => station.onlineInfo && station.onlineInfo.region == store.region.id) });
),
window.addEventListener('online', () => {
dispatcherDataStatus: store.dataStatuses.dispatchers, this.store.isOffline = false;
}; });
}, },
data: () => ({ async mounted() {
VERSION: packageInfo.version, this.setReleaseURL();
currentLang: 'pl', watch(
releaseURL: '', () => this.store.blockScroll,
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app', (value) => {
}), if (value) document.body.classList.add('no-scroll');
else document.body.classList.remove('no-scroll');
created() { }
this.loadLang(); );
},
this.store.isOffline = !window.navigator.onLine;
watch: {
window.addEventListener('offline', () => { '$route.query.region': {
this.store.isOffline = true; immediate: true,
handler(regionQuery: string) {
this.store.apiData = { if (regionQuery) {
stations: [], this.store.region.id =
dispatchers: [], regions.find(
trains: [], (reg) =>
connectedSocketCount: 0, reg.id == regionQuery.toLocaleLowerCase() ||
}; reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase()
)?.id || 'eu';
this.store.setOnlineData(); }
}); }
}
window.addEventListener('online', () => { },
this.store.isOffline = false;
}); methods: {
}, changeLang(lang: string) {
this.$i18n.locale = lang;
async mounted() { this.currentLang = lang;
this.setReleaseURL();
StorageManager.setStringValue('lang', lang);
watch( },
() => this.store.blockScroll,
(value) => { async setReleaseURL() {
if (value) { try {
document.body.classList.add('no-scroll'); const releaseData = await (
return; await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
} ).data;
document.body.classList.remove('no-scroll'); if (!releaseData) return;
}
); this.releaseURL = releaseData.html_url;
}, } catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
methods: { return;
changeLang(lang: string) { }
this.$i18n.locale = lang; },
this.currentLang = lang;
loadLang() {
StorageManager.setStringValue('lang', lang); const storageLang = StorageManager.getStringValue('lang');
},
if (storageLang) {
async setReleaseURL() { this.changeLang(storageLang);
try { return;
const releaseData = await ( }
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
).data; if (!window.navigator.language) return;
if (!releaseData) return; const naviLanguage = window.navigator.language.toString();
this.releaseURL = releaseData.html_url; if (naviLanguage.includes('en')) {
} catch (error) { this.changeLang('en');
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`); return;
return; }
} }
}, }
});
loadLang() { </script>
const storageLang = StorageManager.getStringValue('lang');
<style lang="scss" src="./App.scss"></style>
if (storageLang) {
this.changeLang(storageLang);
return;
}
if (!window.navigator.language) return;
const naviLanguage = window.navigator.language.toString();
if (naviLanguage.includes('en')) {
this.changeLang('en');
return;
}
},
},
});
</script>
<style lang="scss" src="./App.scss"></style>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

+214 -237
View File
@@ -1,237 +1,214 @@
<template> <template>
<header class="app_header"> <header class="app_header">
<div class="header_container"> <div class="header_container">
<div class="header_icons"> <div class="header_icons">
<span class="icons-top"> <span class="icons-top">
<img :src="getIcon('pl')" alt="icon-pl" @click="changeLang('en')" v-if="currentLang == 'pl'" /> <img
<img :src="getIcon('en', 'jpg')" alt="icon-en" @click="changeLang('pl')" v-else /> src="/images/icon-pl.svg"
</span> alt="icon-pl"
</div> @click="changeLang('en')"
v-if="currentLang == 'pl'"
<div class="header_body"> />
<StatusIndicator /> <img src="/images/icon-en.jpg" alt="icon-en" @click="changeLang('pl')" v-else />
</span>
<span class="header_brand"> </div>
<router-link to="/">
<img :src="getImage('stacjownik-header-logo.svg')" alt="Stacjownik" /> <div class="header_body">
</router-link> <StatusIndicator />
</span>
<span class="header_brand">
<span class="header_info"> <router-link to="/">
<Clock /> <img src="/images/stacjownik-header-logo.svg" alt="Stacjownik" />
</router-link>
<div class="info_counter"> </span>
<img :src="getIcon('dispatcher')" alt="icon dispatcher" />
<span class="text--primary">{{ onlineDispatchersCount }}</span> <span class="header_info">
<Clock />
<!-- <span class="g-tooltip">
<b class="text--primary">{{ factorU }}U</b> <div class="info_counter">
<div class="content">Test</div> <img src="/images/icon-dispatcher.svg" alt="icon dispatcher" />
</span> --> <span class="text--primary">{{ onlineDispatchersCount }}</span>
<span class="text--grayed"> / </span> <!-- <span class="g-tooltip">
<span class="text--primary">{{ onlineTrainsCount }}</span> <b class="text--primary">{{ factorU }}U</b>
<img :src="getIcon('train')" alt="icon train" /> <div class="content">Test</div>
</div> </span> -->
<span class="info_region"> <span class="text--grayed"> / </span>
<SelectBox :itemList="computedRegions" :defaultItemIndex="0" @selected="changeRegion" /> <span class="text--primary">{{ onlineTrainsCount }}</span>
</span> <img src="/images/icon-train.svg" alt="icon train" />
</span> </div>
<span class="header_links"> <div class="info_region">
<router-link class="route" active-class="route-active" to="/" exact> <RegionDropdown />
{{ $t('app.sceneries') }} </div>
</router-link> </span>
/
<router-link class="route" active-class="route-active" to="/trains">{{ $t('app.trains') }}</router-link> <span class="header_links">
/ <router-link class="route" active-class="route-active" to="/" exact>
<router-link {{ $t('app.sceneries') }}
class="route" </router-link>
active-class="route-active" /
:data-active="$route.path.startsWith('/journal')" <router-link class="route" active-class="route-active" to="/trains">{{
to="/journal" $t('app.trains')
> }}</router-link>
{{ $t('app.journal') }} /
</router-link> <router-link
</span> class="route"
</div> active-class="route-active"
</div> :data-active="$route.path.startsWith('/journal')"
</header> to="/journal"
</template> >
<script lang="ts"> {{ $t('app.journal') }}
import { defineComponent } from 'vue'; </router-link>
import { useStore } from '../../store/store'; </span>
import options from '../../data/options.json'; </div>
import imageMixin from '../../mixins/imageMixin'; </div>
import SelectBox from '../Global/SelectBox.vue'; </header>
import StatusIndicator from './StatusIndicator.vue'; </template>
import Clock from './Clock.vue'; <script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({ import { useStore } from '../../store/mainStore';
emits: ['changeLang'], import StatusIndicator from './StatusIndicator.vue';
mixins: [imageMixin], import Clock from './Clock.vue';
props: { import RegionDropdown from '../Global/RegionDropdown.vue';
currentLang: {
type: String, export default defineComponent({
required: true, emits: ['changeLang'],
}, props: {
}, currentLang: {
setup() { type: String,
return { required: true
store: useStore(), }
}; },
},
methods: { setup() {
changeRegion(region: { id: string; value: string }) { return {
this.store.changeRegion(region); store: useStore()
}, };
changeLang(lang: string) { },
this.$emit('changeLang', lang);
}, methods: {
}, changeLang(lang: string) {
computed: { this.$emit('changeLang', lang);
onlineTrainsCount() { }
return this.store.trainList.filter((train) => train.online).length; },
},
computed: {
onlineDispatchersCount() { onlineTrainsCount() {
return this.store.stationList.filter( return this.store.trainList.filter((train) => train.region == this.store.region.id).length;
(station) => station.onlineInfo && station.onlineInfo.region == this.store.region.id },
).length;
}, onlineDispatchersCount() {
return this.store.onlineSceneryList.filter(
factorU() { (scenery) => scenery.region == this.store.region.id
return this.onlineDispatchersCount == 0 ? '-' : (this.onlineTrainsCount / this.onlineDispatchersCount).toFixed(2); ).length;
}, },
computedRegions() { factorU() {
return options.regions.map((region) => { return this.onlineDispatchersCount == 0
const regionStationCount = ? '-'
this.store.apiData.stations?.filter((station) => station.region == region.id && station.isOnline).length || 0; : (this.onlineTrainsCount / this.onlineDispatchersCount).toFixed(2);
const regionTrainCount = }
this.store.apiData.trains?.filter((train) => train.region == region.id && train.online).length || 0; },
return { components: { StatusIndicator, Clock, RegionDropdown }
id: region.id, });
value: `${region.value} <div class='text--grayed'>${regionStationCount} / ${regionTrainCount}</div>`, </script>
selectedValue: region.value, <style lang="scss" scoped>
}; @import '../../styles/variables.scss';
}); @import '../../styles/responsive.scss';
},
}, // HEADER
components: { SelectBox, StatusIndicator, Clock }, .app_header {
}); display: flex;
</script> justify-content: center;
<style lang="scss" scoped>
@import '../../styles/variables.scss'; position: relative;
@import '../../styles/responsive.scss'; background-color: $primaryCol;
}
// HEADER
.app_header { .header {
display: flex; &_body {
justify-content: center; position: relative;
max-width: 20em;
position: relative; }
background-color: $primaryCol;
} &_container {
display: flex;
.header { justify-content: center;
&_body {
position: relative; border-radius: 0 0 1em 1em;
max-width: 20em;
} @include smallScreen {
position: relative;
&_container { margin-top: 0.5em;
display: flex; }
justify-content: center; }
border-radius: 0 0 1em 1em; &_brand {
display: flex;
@include smallScreen {
position: relative; img {
margin-top: 0.5em; width: 100%;
}
} margin: 0 auto;
}
&_brand { }
display: flex;
&_info {
img { display: grid;
width: 100%; grid-template-columns: 1fr 1fr 1fr;
font-size: 1.15em;
margin: 0 auto; }
}
} &_links {
display: flex;
&_info { justify-content: center;
display: grid;
grid-template-columns: 1fr 1fr 1fr; border-radius: 0.7em;
font-size: 1.15em;
} font-size: 1.25em;
padding: 0.5em;
&_links { }
display: flex;
justify-content: center; &_icons {
position: absolute;
border-radius: 0.7em; right: 0;
top: 0;
font-size: 1.25em;
padding: 0.5em; padding: 0.5em;
}
@include smallScreen {
&_icons { transform: translateX(85%);
position: absolute; }
right: 0; }
top: 0; }
padding: 0.5em; // ICONS
.icons-top {
@include smallScreen { img {
transform: translateX(85%); width: 2.5em;
} cursor: pointer;
} }
} }
// ICONS // COUNTER
.icons-top { .info_counter {
img { display: flex;
width: 2.5em; justify-content: center;
cursor: pointer; align-items: center;
}
} span {
margin: 0 0.15em;
// COUNTER }
.info_counter {
display: flex; img {
justify-content: center; width: 1.35em;
align-items: center; }
}
span {
margin: 0 0.15em; .info_region {
} display: flex;
justify-content: flex-end;
img { }
width: 1.35em; </style>
}
}
// REGION SELECTION
.info_region {
color: white;
font-weight: bold;
display: flex;
justify-content: flex-end;
.select-box_content button {
background-color: transparent;
font-weight: bold;
padding: 0.1em 0.5em;
color: paleturquoise;
}
.options {
font-size: 0.9em;
}
}
</style>
+37 -36
View File
@@ -1,36 +1,37 @@
<template> <template>
<div class="clock">{{ computedDate }}</div> <div class="clock">{{ computedDate }}</div>
</template> </template>
<script lang="ts">
<script lang="ts"> import { computed, defineComponent, ref } from 'vue';
import { computed, defineComponent, ref } from "vue"; export default defineComponent({
export default defineComponent({ name: 'VueClock',
name: "clock", data: () => ({
data: () => ({ timestamp: Date.now()
timestamp: Date.now(), }),
}), setup() {
setup() { let timestamp = ref(Date.now());
let timestamp = ref(Date.now());
const computedDate = computed(() =>
const computedDate = computed(() => new Date(timestamp.value).toLocaleString("pl-PL", { new Date(timestamp.value).toLocaleString('pl-PL', {
hour: "2-digit", hour: '2-digit',
minute: "2-digit", minute: '2-digit',
second: "2-digit", second: '2-digit'
})); })
);
setInterval(() => (timestamp.value = Date.now()), 1000);
setInterval(() => (timestamp.value = Date.now()), 1000);
return { computedDate }
} return { computedDate };
}); }
</script> });
</script>
<style lang="scss" scoped>
@import "../../styles/responsive.scss"; <style lang="scss" scoped>
@import '../../styles/responsive.scss';
.clock {
display: flex; .clock {
align-items: center; display: flex;
} align-items: center;
</style> }
</style>
+65 -34
View File
@@ -43,7 +43,13 @@
<g v-if="greenBlinkLight" filter="url(#filter0_d_843_28)"> <g v-if="greenBlinkLight" filter="url(#filter0_d_843_28)">
<circle cx="15" cy="17" r="7" fill="#00FF0A" /> <circle cx="15" cy="17" r="7" fill="#00FF0A" />
<animate attributeType="XML" attributeName="opacity" values="1;0;1" dur="1s" repeatCount="indefinite" /> <animate
attributeType="XML"
attributeName="opacity"
values="1;0;1"
dur="1s"
repeatCount="indefinite"
/>
</g> </g>
<g v-if="redTopLight" filter="url(#filter1_d_843_28)"> <g v-if="redTopLight" filter="url(#filter1_d_843_28)">
@@ -56,7 +62,13 @@
<g v-if="redBottomLight" filter="url(#filter3_d_843_28)"> <g v-if="redBottomLight" filter="url(#filter3_d_843_28)">
<circle cx="15" cy="74" r="7" fill="#F40000" /> <circle cx="15" cy="74" r="7" fill="#F40000" />
<animate attributeType="XML" attributeName="opacity" values="1;0;1" dur="1s" repeatCount="indefinite" /> <animate
attributeType="XML"
attributeName="opacity"
values="1;0;1"
dur="1s"
repeatCount="indefinite"
/>
</g> </g>
</g> </g>
@@ -82,7 +94,12 @@
<feComposite in2="hardAlpha" operator="out" /> <feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.04 0 0 0 1 0" /> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.04 0 0 0 1 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_843_28" /> <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_843_28" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_843_28" result="shape" /> <feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_843_28"
result="shape"
/>
</filter> </filter>
<filter <filter
id="filter1_d_843_28" id="filter1_d_843_28"
@@ -104,7 +121,12 @@
<feGaussianBlur stdDeviation="2.5" /> <feGaussianBlur stdDeviation="2.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0.770833 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" /> <feColorMatrix type="matrix" values="0 0 0 0 0.770833 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_843_28" /> <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_843_28" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_843_28" result="shape" /> <feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_843_28"
result="shape"
/>
</filter> </filter>
<filter <filter
id="filter2_d_843_28" id="filter2_d_843_28"
@@ -126,7 +148,12 @@
<feGaussianBlur stdDeviation="2.5" /> <feGaussianBlur stdDeviation="2.5" />
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.72 0 0 0 0 0 0 0 0 1 0" /> <feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.72 0 0 0 0 0 0 0 0 1 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_843_28" /> <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_843_28" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_843_28" result="shape" /> <feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_843_28"
result="shape"
/>
</filter> </filter>
<filter <filter
id="filter3_d_843_28" id="filter3_d_843_28"
@@ -148,7 +175,12 @@
<feGaussianBlur stdDeviation="2.5" /> <feGaussianBlur stdDeviation="2.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0.770833 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" /> <feColorMatrix type="matrix" values="0 0 0 0 0.770833 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_843_28" /> <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_843_28" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_843_28" result="shape" /> <feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_843_28"
result="shape"
/>
</filter> </filter>
</defs> </defs>
</svg> </svg>
@@ -162,9 +194,9 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { StoreState } from '../../store/typings';
import { useStore } from '../../store/store'; import { useStore } from '../../store/mainStore';
import { StoreState } from '../../scripts/interfaces/store/storeTypes'; import { Status } from '../../typings/common';
export default defineComponent({ export default defineComponent({
data() { data() {
@@ -172,20 +204,20 @@ export default defineComponent({
tooltipActive: false, tooltipActive: false,
indicator: { indicator: {
offline: false, offline: false,
status: DataStatus.Loading, status: Status.Data.Loading,
message: 'data-status.S3', message: 'data-status.S3'
}, },
greenLight: false, greenLight: false,
greenBlinkLight: false, greenBlinkLight: false,
redTopLight: false, redTopLight: false,
orangeLight: false, orangeLight: false,
redBottomLight: false, redBottomLight: false
}; };
}, },
mounted() { mounted() {
this.setSignalStatus(DataStatus.Loading); this.setSignalStatus(Status.Data.Loading);
}, },
setup() { setup() {
@@ -193,7 +225,7 @@ export default defineComponent({
return { return {
dataStatus: store.dataStatuses, dataStatus: store.dataStatuses,
store, store
}; };
}, },
@@ -208,80 +240,80 @@ export default defineComponent({
const dispatcherDataStatus = statuses.dispatchers; const dispatcherDataStatus = statuses.dispatchers;
if (this.store.isOffline) { if (this.store.isOffline) {
this.setSignalStatus(DataStatus.Initialized); this.setSignalStatus(Status.Data.Initialized);
this.indicator.status = DataStatus.Initialized; this.indicator.status = Status.Data.Initialized;
this.indicator.message = 'data-status.S1-offline'; this.indicator.message = 'data-status.S1-offline';
return; return;
} }
if (connectionStatus == DataStatus.Error) { if (connectionStatus == Status.Data.Error) {
this.setSignalStatus(connectionStatus); this.setSignalStatus(connectionStatus);
this.indicator.status = connectionStatus; this.indicator.status = connectionStatus;
this.indicator.message = 'data-status.S1a-connection'; this.indicator.message = 'data-status.S1a-connection';
return; return;
} }
if (sceneryDataStatus == DataStatus.Error) { if (sceneryDataStatus == Status.Data.Error) {
this.setSignalStatus(sceneryDataStatus); this.setSignalStatus(sceneryDataStatus);
this.indicator.status = sceneryDataStatus; this.indicator.status = sceneryDataStatus;
this.indicator.message = 'data-status.S1a-sceneries'; this.indicator.message = 'data-status.S1a-sceneries';
return; return;
} }
if (trainsDataStatus == DataStatus.Warning) { if (trainsDataStatus == Status.Data.Warning) {
this.setSignalStatus(trainsDataStatus); this.setSignalStatus(trainsDataStatus);
this.indicator.status = trainsDataStatus; this.indicator.status = trainsDataStatus;
this.indicator.message = 'data-status.S5-trains'; this.indicator.message = 'data-status.S5-trains';
return; return;
} }
if (dispatcherDataStatus == DataStatus.Warning) { if (dispatcherDataStatus == Status.Data.Warning) {
this.setSignalStatus(dispatcherDataStatus); this.setSignalStatus(dispatcherDataStatus);
this.indicator.status = dispatcherDataStatus; this.indicator.status = dispatcherDataStatus;
this.indicator.message = 'data-status.S5-dispatchers'; this.indicator.message = 'data-status.S5-dispatchers';
return; return;
} }
if (sceneryDataStatus == DataStatus.Loaded) { if (sceneryDataStatus == Status.Data.Loaded) {
this.setSignalStatus(DataStatus.Loaded); this.setSignalStatus(Status.Data.Loaded);
this.indicator.status = DataStatus.Loaded; this.indicator.status = Status.Data.Loaded;
this.indicator.message = 'data-status.S2'; this.indicator.message = 'data-status.S2';
} }
}, }
}, }
}, },
methods: { methods: {
setSignalStatus(status: DataStatus) { setSignalStatus(status: Status.Data) {
this.greenLight = false; this.greenLight = false;
this.greenBlinkLight = false; this.greenBlinkLight = false;
this.redTopLight = false; this.redTopLight = false;
this.orangeLight = false; this.orangeLight = false;
this.redBottomLight = false; this.redBottomLight = false;
if (status == DataStatus.Initialized) { if (status == Status.Data.Initialized) {
this.redTopLight = true; this.redTopLight = true;
} }
if (status == DataStatus.Loaded) { if (status == Status.Data.Loaded) {
this.greenLight = true; this.greenLight = true;
} }
if (status == DataStatus.Warning) { if (status == Status.Data.Warning) {
this.orangeLight = true; this.orangeLight = true;
} }
if (status == DataStatus.Error) { if (status == Status.Data.Error) {
this.redTopLight = true; this.redTopLight = true;
this.redBottomLight = true; this.redBottomLight = true;
} }
if (status == DataStatus.Loading) { if (status == Status.Data.Loading) {
this.greenBlinkLight = true; this.greenBlinkLight = true;
} }
}, }
}, }
}); });
</script> </script>
@@ -375,4 +407,3 @@ export default defineComponent({
} }
} }
</style> </style>
-168
View File
@@ -1,168 +0,0 @@
<template>
<transition name="modal-anim">
<section class="update-modal card" v-if="releaseData && modalOpen">
<h2 class="modal_header text--primary">
<img :src="getImage('stacjownik-header-logo.svg')" alt="stacjownik logo" />
{{ releaseData.tag_name }}
</h2>
<div class="horizontal"></div>
<div class="modal_content">
<h3>{{ $t('update.title') }}</h3>
<a :href="releaseData.html_url" target="_blank">{{ $t('update.release-link') }}</a>
<br />
<br />
<p>{{ $t('update.paragraph1') }}</p>
<!-- <div class="modal_changelog" v-html="markdownReleaseBody"></div> -->
</div>
<div class="modal_actions">
<button class="btn btn--option" @click="modalOpen = false">{{ $t('update.confirm-button') }}</button>
</div>
</section>
</transition>
</template>
<script lang="ts">
import axios from 'axios';
import { defineComponent } from 'vue';
import packageInfo from '../../../package.json';
import imageMixin from '../../mixins/imageMixin';
import { ReleaseAPIData } from '../../scripts/interfaces/github_api/ReleaseAPIData';
import StorageManager from '../../scripts/managers/storageManager';
import { useStore } from '../../store/store';
const GH_LASTEST_RELEASE_URL = 'https://api.github.com/repos/Spythere/stacjownik/releases/latest';
export default defineComponent({
mixins: [imageMixin],
mounted() {
this.fetchReleases();
},
data() {
return {
modalOpen: false,
releaseData: null as ReleaseAPIData | null,
};
},
setup() {
return {
store: useStore()
}
},
methods: {
async fetchReleases() {
const storedVersion = StorageManager.getStringValue('appVersion');
const appVersion = packageInfo.version;
// Zmiana
if (appVersion != storedVersion) {
StorageManager.setStringValue('appVersion', appVersion);
// Znajdź changelog na GitHubie, jeśli jest pokaż modal
try {
const releaseData: ReleaseAPIData = await (await axios.get(GH_LASTEST_RELEASE_URL)).data;
if (!releaseData) return;
const lastReleaseVersion = releaseData.tag_name.slice(1);
if (lastReleaseVersion == appVersion) {
this.releaseData = releaseData;
this.modalOpen = true;
StorageManager.setStringValue('releaseURL', releaseData.html_url);
}
} catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
}
}
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/card.scss';
@import '../../styles/responsive.scss';
.modal-anim {
&-enter-active,
&-leave-active {
transition: all $animDuration $animType;
}
&-enter-from,
&-leave-to {
opacity: 0;
transform: translate(-50%, -50%) scale(0.45);
}
}
.update-modal {
text-align: center;
background-color: var(--clr-secondary);
padding: 1em;
}
.horizontal {
margin: 1em 0;
height: 2px;
width: 100%;
background-color: white;
}
.modal_header {
font-size: 1.6em;
img {
width: 50%;
vertical-align: text-top;
}
}
.modal_content {
font-size: 1.1em;
a {
text-decoration: underline;
}
}
.modal_actions {
margin-top: 2em;
button {
color: white;
padding: 0.5em;
font-size: 1.2em;
background-color: black;
}
}
.modal_changelog {
font-size: 0.8em;
margin-top: 2em;
}
@include smallScreen {
.update-modal {
height: auto;
max-width: 95%;
}
}
</style>
-69
View File
@@ -1,69 +0,0 @@
<template>
<div class="update-prompt">
<transition name="prompt-anim">
<div class="prompt_content" v-if="!hidePrompt && needRefresh">
<div>{{ $t('update.title') }}</div>
<div class="prompt_actions">
<button class="btn btn--filled" @click="updateServiceWorker(true)">{{ $t('update.confirm-button') }}</button>
<button class="btn btn--filled" @click="hidePrompt = true">{{ $t('update.later-button') }}</button>
</div>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import useCustomSW from '../../mixins/useCustomSW';
const hidePrompt = ref(false);
const { needRefresh, updateServiceWorker } = useCustomSW();
</script>
<style lang="scss" scoped>
@import '../../styles/variables.scss';
.update-prompt {
position: fixed;
bottom: 0;
right: 0;
z-index: 200;
}
.prompt_content {
margin: 1em;
padding: 1em;
font-weight: bold;
background-color: black;
box-shadow: 0 0 10px 1px $accentCol;
border-radius: 1em;
}
.prompt_actions {
display: flex;
margin-top: 1em;
gap: 0.5em;
button {
width: 100%;
}
}
// Animation
.prompt-anim {
&-enter-active,
&-leave-active {
transition: all 120ms ease-in;
transform: translateY(0);
}
&-enter-from,
&-leave-to {
transform: translateY(100%);
}
}
</style>
+24 -24
View File
@@ -1,24 +1,24 @@
<template> <template>
<button class="action-btn btn--filled"> <button class="action-btn btn--filled">
<div class="button_content"> <div class="button_content">
<slot></slot> <slot></slot>
</div> </div>
</button> </button>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
export default defineComponent({}); export default defineComponent({});
</script> </script>
<style lang="scss"> <style lang="scss">
@import "../../styles/variables"; @import '../../styles/variables';
@import "../../styles/responsive"; @import '../../styles/responsive';
.button_content { .button_content {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
</style> </style>
+6 -6
View File
@@ -15,18 +15,18 @@ export default defineComponent({
props: { props: {
scrollNoMoreData: { scrollNoMoreData: {
type: Boolean, type: Boolean,
required: true, required: true
}, },
scrollDataLoaded: { scrollDataLoaded: {
type: Boolean, type: Boolean,
required: true, required: true
}, },
list: { list: {
type: Array as PropType<any[]>, type: Array as PropType<any[]>,
required: true, required: true
}, }
}, },
emits: ['addHistoryData'], emits: ['addHistoryData'],
@@ -34,8 +34,8 @@ export default defineComponent({
methods: { methods: {
addHistoryData() { addHistoryData() {
this.$emit('addHistoryData'); this.$emit('addHistoryData');
}, }
}, }
}); });
</script> </script>
+1 -1
View File
@@ -12,7 +12,7 @@ import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
setup() { setup() {
return {}; return {};
}, }
}); });
</script> </script>
+65 -62
View File
@@ -1,62 +1,65 @@
<template> <template>
<div class="progress-bar"> <div class="progress-bar">
<span class="bar-bg"></span> <span class="bar-bg"></span>
<span class="bar-fg" :style="{ width: `${~~progressPercent}%`, backgroundColor: bgColor }"></span> <span
</div> class="bar-fg"
</template> :style="{ width: `${~~progressPercent}%`, backgroundColor: bgColor }"
></span>
<script lang="ts"> </div>
import { defineComponent } from 'vue'; </template>
export default defineComponent({ <script lang="ts">
props: { import { defineComponent } from 'vue';
progressPercent: {
type: Number, export default defineComponent({
required: true, props: {
}, progressPercent: {
progressType: { type: Number,
type: String, required: true
required: false, },
}, progressType: {
}, type: String,
required: false
computed: { }
bgColor() { },
switch (this.progressType) {
case 'abandoned': computed: {
return 'salmon'; bgColor() {
switch (this.progressType) {
default: case 'abandoned':
return 'springgreen'; return 'salmon';
}
}, default:
}, return 'springgreen';
}); }
</script> }
}
<style lang="scss" scoped> });
.progress-bar { </script>
position: relative;
<style lang="scss" scoped>
width: 6em; .progress-bar {
height: 1em; position: relative;
margin: 0.5em 0;
width: 6em;
.bar-fg, height: 1em;
.bar-bg { margin: 0.5em 0;
position: absolute;
height: 1em; .bar-fg,
width: 100%; .bar-bg {
position: absolute;
left: 0; height: 1em;
} width: 100%;
.bar-fg { left: 0;
background-color: springgreen; }
}
.bar-fg {
.bar-bg { background-color: springgreen;
background-color: #5b5b5b; }
}
} .bar-bg {
</style> background-color: #5b5b5b;
}
}
</style>
@@ -1,217 +1,225 @@
<template> <template>
<div class="select-box"> <div class="region-dropdown" v-click-outside="clickedOutside">
<div class="select-box_content"> <div class="content">
<button class="selected" @click="toggleBox"> <button class="selected-region" @click="toggleBox">
<span>{{ computedSelectedItem.selectedValue || computedSelectedItem.value }}</span> <span>{{ selectedItem.name }}</span>
<div class="arrow"> <img :src="`/images/icon-arrow-${listOpen ? 'asc' : 'desc'}.svg`" alt="Arrow icon" />
<img :src="listOpen ? getIcon('arrow-asc') : getIcon('arrow-desc')" alt="arrow-icon" /> </button>
</div>
</button> <ul class="options">
<li class="option" v-for="(item, i) in regionList" :key="item.id">
<ul class="options" :ref="(el) => (listRef = el as Element)"> <transition
<li class="option" v-for="(item, i) in itemList" :key="item.id"> name="unfold"
<transition :style="`
name="unfold" --delay-in: ${i * 55}ms;
:style="` --delay-out: ${(regionList.length - 1 - i) * 55}ms`"
--delay-in: ${i * 55}ms; >
--delay-out: ${(itemList.length - 1 - i) * 55}ms`" <label :for="item.id" v-if="listOpen">
> <input type="button" :id="item.id" name="select-box" @click="selectOption(item)" />
<label :for="item.id" v-if="listOpen"> <span :style="selectedItem.id == item.id ? 'color: gold;' : ''" v-html="item.value">
<input type="button" :id="item.id" name="select-box" @click="selectOption(item)" /> </span>
<span :style="computedSelectedItem.id == item.id ? 'color: gold;' : ''" v-html="item.value"> </span> </label>
</label> </transition>
</transition> </li>
</li> </ul>
</ul> </div>
</div> </div>
</div> </template>
</template>
<script lang="ts">
<script lang="ts"> import { defineComponent, Ref, ref } from 'vue';
import { defineComponent, Ref, ref, computed } from 'vue'; import { regions as regionsJSON } from '../../data/options.json';
import imageMixin from '../../mixins/imageMixin'; import { useStore } from '../../store/mainStore';
interface Item { interface Item {
id: string; id: string;
value: string; value: string;
selectedValue?: string; name: string;
} }
export default defineComponent({ export default defineComponent({
emits: ['selected'], data() {
mixins: [imageMixin], return {
store: useStore(),
props: { selectedItemIndex: 0,
itemList: { listOpen: false
type: Array as () => Item[], };
required: true, },
},
setup() {
defaultItemIndex: { let buttonRef: Ref<HTMLButtonElement | null> = ref(null);
type: Number,
default: 0, return {
}, buttonRef
};
prefix: { },
type: String,
default: '', watch: {
}, 'store.region.id': {
}, handler(regionId) {
this.selectedItemIndex = this.regionList.findIndex((reg) => reg.id == regionId);
setup(props) { }
let listRef: Ref<Element | null> = ref(null); }
let buttonRef: Ref<HTMLButtonElement | null> = ref(null); },
let activeEl: Ref<Element | null> = ref(document.activeElement); computed: {
selectedItem() {
let listOpen = ref(false); return this.regionList[this.selectedItemIndex] || null;
let selectedItem: Ref<Item> = ref(props.itemList[props.defaultItemIndex]); },
const computedSelectedItem = computed(() => { regionList() {
return props.itemList.find((item) => item.id === selectedItem.value.id) || props.itemList[props.defaultItemIndex]; return regionsJSON.map((region) => {
}); const regionStationCount = this.store.onlineSceneryList.filter(
(scenery) => scenery.region == region.id
return { ).length;
computedSelectedItem,
listOpen, const regionTrainCount =
selectedItem, this.store.trainList.filter((train) => train.region == region.id).length || 0;
listRef,
buttonRef, return {
activeEl, id: region.id,
}; value: `${region.value} <div class='text--grayed'>${regionStationCount} / ${regionTrainCount}</div>`,
}, name: region.name
};
methods: { });
selectOption(item: Item) { }
this.selectedItem = item; },
this.listOpen = false;
methods: {
this.$emit('selected', item); selectOption(selectedRegion: Item) {
}, this.store.region = selectedRegion;
this.listOpen = false;
toggleBox(e: Event) { },
this.listOpen = !this.listOpen;
toggleBox(e: Event) {
if (!this.listOpen) (e.target as HTMLButtonElement).blur(); this.listOpen = !this.listOpen;
},
if (!this.listOpen) (e.target as HTMLButtonElement).blur();
clickedOutside() { },
this.listOpen = false;
this.buttonRef?.blur(); clickedOutside() {
}, this.listOpen = false;
}, this.buttonRef?.blur();
}); }
</script> }
});
<style lang="scss" scoped> </script>
@import '../../styles/variables.scss';
<style lang="scss" scoped>
.unfold { @import '../../styles/variables.scss';
&-enter-from,
&-leave-to { .region-dropdown {
opacity: 0; display: flex;
transform: translateY(-10px) scale(0.85); align-items: center;
} justify-content: space-between;
}
&-enter-active,
&-leave-active { button img {
transition: all 110ms ease-out; vertical-align: middle;
} width: 1.35em;
}
&-enter-active {
transition-delay: var(--delay-in); button.selected-region {
} display: flex;
justify-content: space-between;
&-leave-active { color: paleturquoise;
transition-delay: var(--delay-out);
} font-weight: bold;
} padding: 0.1em 0.5em;
.select-box { &:focus {
display: flex; background-color: #262626;
align-items: center; }
}
span {
.arrow { margin-right: 10px;
img { }
vertical-align: middle; }
width: 1.35em;
} .content {
} position: relative;
margin: 0 auto;
button.selected { font-weight: bold;
color: paleturquoise;
height: 100%;
font-weight: bold;
padding: 0.1em 0.5em; text-align: center;
}
&:focus {
background-color: #262626; ul.options {
} position: absolute;
} top: 100%;
left: 0;
.select-box_content {
position: relative; height: auto;
margin: 0 auto;
z-index: 100;
height: 100%; width: 100%;
text-align: center; font-size: 0.9em;
} }
ul.options { li.option {
position: absolute; input {
top: 100%; position: absolute;
left: 0; top: 0;
left: 0;
height: auto;
-webkit-appearance: none;
z-index: 100; -moz-appearance: none;
width: 100%; appearance: none;
border: none;
font-size: 0.9em; outline: none;
} background: none;
li.option { &:focus + span {
input { color: $accentCol;
position: absolute; font-weight: 800;
top: 0; }
left: 0; }
-webkit-appearance: none; &:last-child label {
-moz-appearance: none; border-radius: 0 0 1em 1em;
appearance: none; }
border: none;
outline: none; label {
background: none; position: relative;
&:focus + span { display: inline-block;
color: $accentCol; background-color: #262626f2;
font-weight: 800;
} &:hover,
} &:focus {
background-color: #333333f2;
&:last-child label { }
border-radius: 0 0 1em 1em;
} padding: 0.5em 0;
label { width: 100%;
position: relative;
cursor: pointer;
display: inline-block; }
background-color: #262626f2; }
&:hover, .unfold {
&:focus { &-enter-from,
background-color: #333333f2; &-leave-to {
} opacity: 0;
transform: translateY(-10px) scale(0.85);
padding: 0.5em 0; }
width: 100%; &-enter-active,
&-leave-active {
cursor: pointer; transition: all 110ms ease-out;
} }
}
</style> &-enter-active {
transition-delay: var(--delay-in);
}
&-leave-active {
transition-delay: var(--delay-out);
}
}
</style>
+17 -25
View File
@@ -7,39 +7,31 @@
@keypress="updateValue" @keypress="updateValue"
/> />
<img <img class="search-exit" src="/images/icon-exit.svg" alt="exit-icon" @click="clearSearchValue" />
class="search-exit"
:src="getIcon('exit')"
alt="exit-icon"
@click="clearValue"
/>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, watch } from "vue"; import { defineComponent, ref, watch } from 'vue';
import imageMixin from "../../mixins/imageMixin";
export default defineComponent({ export default defineComponent({
mixins: [imageMixin], emits: ['update:searchedValue', 'clearValue'],
emits: ["update:searchedValue", "clearValue"],
props: { props: {
searchedValue: { searchedValue: {
type: String, type: String,
required: true, required: true
}, },
updateOnInput: { updateOnInput: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
titleToTranslate: { titleToTranslate: {
type: String, type: String,
required: true, required: true
}, },
clearValue: { clearValue: {
type: Function, type: Function
}, }
}, },
setup(props, { emit }) { setup(props, { emit }) {
@@ -49,32 +41,32 @@ export default defineComponent({
watch( watch(
() => compSearchedValue.value, () => compSearchedValue.value,
(value) => { (value) => {
emit("update:searchedValue", value); emit('update:searchedValue', value);
} }
); );
} }
const clearValue = () => { const clearSearchValue = () => {
compSearchedValue.value = ""; compSearchedValue.value = '';
emit("clearValue"); emit('clearValue');
}; };
const updateValue = (e: any) => { const updateValue = (e: any) => {
if (!props.updateOnInput && e.keyCode == 13) if (!props.updateOnInput && e.keyCode == 13)
emit("update:searchedValue", compSearchedValue.value); emit('update:searchedValue', compSearchedValue.value);
}; };
return { return {
compSearchedValue, compSearchedValue,
updateValue, updateValue,
clearValue, clearSearchValue
}; };
}, }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../styles/responsive"; @import '../../styles/responsive';
.search { .search {
&-box { &-box {
@@ -109,4 +101,4 @@ export default defineComponent({
width: 1em; width: 1em;
} }
} }
</style> </style>
+129 -90
View File
@@ -1,90 +1,129 @@
<template> <template>
<span class="status-badge" :class="statusID" v-if="isOnline"> <span class="status-badge" :class="statusName" v-if="isOnline">
{{ $t(`status.${statusID}`) }} {{ $t(`status.${statusName}`) }}
{{ statusID == 'online' ? timestampToString(statusTimestamp!) : '' }} {{
</span> statusName == 'online' && dispatcherStatus && dispatcherStatus > 5
? timestampToString(dispatcherStatus)
<span class="status-badge free" v-else> : ''
{{ $t('status.free') }} }}
</span> </span>
</template>
<span class="status-badge free" v-else>
<script lang="ts"> {{ $t('status.free') }}
import { defineComponent } from 'vue'; </span>
import dateMixin from '../../mixins/dateMixin'; </template>
export default defineComponent({ <script lang="ts">
props: { import { PropType, defineComponent } from 'vue';
statusID: { import dateMixin from '../../mixins/dateMixin';
type: String, import { Status } from '../../typings/common';
},
statusTimestamp: { export default defineComponent({
type: Number, props: {
}, dispatcherStatus: {
isOnline: { type: Number as PropType<Status.ActiveDispatcher | number>
type: Boolean, },
}, isOnline: {
}, type: Boolean
mixins: [dateMixin], }
}); },
</script> mixins: [dateMixin],
<style lang="scss" scoped> computed: {
$free: #8a8a8a; statusName() {
$ending: #e6c300; if (this.dispatcherStatus === undefined) return 'free';
$no-limit: #117fc9;
$unav: #ff3d5d; switch (this.dispatcherStatus) {
$brb: #e6a100; case Status.ActiveDispatcher.AFK:
$no-space: #222; return 'afk';
$online: #09a116;
$unknown: rgb(185, 60, 60); case Status.ActiveDispatcher.NO_LIMIT:
return 'no-limit';
.status-badge {
border-radius: 1rem; case Status.ActiveDispatcher.ENDING:
font-weight: 500; return 'ending';
padding: 0.2em 0.55em; case Status.ActiveDispatcher.INVALID:
return 'invalid';
background-color: $online;
case Status.ActiveDispatcher.NOT_LOGGED_IN:
&.free { return 'not-signed';
background-color: $free;
font-size: 0.95em; case Status.ActiveDispatcher.NO_SPACE:
} return 'no-space';
&.ending { case Status.ActiveDispatcher.UNAVAILABLE:
background-color: $ending; return 'unavailable';
color: black;
font-size: 0.9em; case Status.ActiveDispatcher.UNKNOWN:
} return 'unknown';
&.no-limit { default:
background-color: $no-limit; if (this.dispatcherStatus >= Date.now() + 25500000) return 'no-limit';
font-size: 0.85em; return 'online';
} }
}
&.not-signed, }
&.unavailable { });
background-color: $unav; </script>
font-size: 0.85em;
} <style lang="scss" scoped>
$free: #8a8a8a;
&.brb { $ending: #e6c300;
background-color: $brb; $no-limit: #117fc9;
color: black; $unav: #ff3d5d;
font-size: 0.95em; $afk: #e6a100;
} $no-space: #222;
$online: #09a116;
&.no-space { $unknown: #b93c3c;
background-color: $no-space;
border: 1px solid white; .status-badge {
color: white; border-radius: 1rem;
font-size: 0.85em; font-weight: 500;
}
padding: 0.2em 0.55em;
&.unknown {
background-color: $unknown; background-color: $online;
font-size: 0.95em;
} &.free {
} background-color: $free;
</style> font-size: 0.95em;
}
&.ending {
background-color: $ending;
color: black;
font-size: 0.9em;
}
&.no-limit {
background-color: $no-limit;
font-size: 0.85em;
}
&.not-signed,
&.unavailable {
background-color: $unav;
font-size: 0.85em;
}
&.afk {
background-color: $afk;
color: black;
font-size: 0.95em;
}
&.no-space {
background-color: $no-space;
border: 1px solid white;
color: white;
font-size: 0.85em;
}
&.unknown,
&.invalid {
background-color: $unknown;
font-size: 0.95em;
}
}
</style>
+27 -17
View File
@@ -1,12 +1,17 @@
<template> <template>
<div class="stock-list"> <div class="stock-list">
<ul> <ul>
<li v-for="(stockName, i) in trainStockList"> <li v-for="(stockName, i) in trainStockList" :key="i">
<p>{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }} {{ stockName.split(':')[1] }}</p> <p>
{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }}
{{ stockName.split(':')[1] }}
</p>
<span> <span>
<img <img
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${/^EN/.test(stockName) ? 'rb' : ''}.png`" :src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${
/^EN/.test(stockName) ? 'rb' : ''
}.png`"
@error="onImageError($event, stockName)" @error="onImageError($event, stockName)"
width="400" width="400"
height="60" height="60"
@@ -15,21 +20,27 @@
<img <img
v-if="/^(EN|2EN)/.test(stockName)" v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`" :src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@error="(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')" @error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
"
/> />
<img <img
class="train-thumbnail" class="train-thumbnail"
v-if="/^EN71/.test(stockName)" v-if="/^EN71/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`" :src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@error="(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')" @error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
"
/> />
<img <img
class="train-thumbnail" class="train-thumbnail"
v-if="/^(EN|2EN)/.test(stockName)" v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}ra.png`" :src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}ra.png`"
@error="(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-ra.png')" @error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-ra.png')
"
/> />
</span> </span>
</li> </li>
@@ -39,23 +50,20 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import { useStore } from '../../store/store'; import { useStore } from '../../store/mainStore';
import { RollingStockInfo } from '../../scripts/interfaces/github_api/StockInfoGithubData'; import { API } from '../../typings/api';
import imageMixin from '../../mixins/imageMixin';
export default defineComponent({ export default defineComponent({
mixins: [imageMixin],
props: { props: {
trainStockList: { trainStockList: {
type: Array as PropType<string[]>, type: Array as PropType<string[]>,
required: true, required: true
}, }
}, },
data() { data() {
return { return {
store: useStore(), store: useStore()
}; };
}, },
@@ -63,12 +71,14 @@ export default defineComponent({
onImageError(event: Event, stockName: string) { onImageError(event: Event, stockName: string) {
const fallbackName = const fallbackName =
Object.keys(this.store.rollingStockData!.info).find((type) => { Object.keys(this.store.rollingStockData!.info).find((type) => {
return this.store.rollingStockData!.info[type as keyof RollingStockInfo].find((v) => v[0] === stockName.split(':')[0]); return this.store.rollingStockData!.info[type as keyof API.RollingStock.Info].find(
(v) => v[0] === stockName.split(':')[0]
);
}) || 'vehicle-unknown'; }) || 'vehicle-unknown';
(event.target as HTMLImageElement).src = `/images/icon-${fallbackName}.png`; (event.target as HTMLImageElement).src = `/images/icon-${fallbackName}.png`;
}, }
}, }
}); });
</script> </script>
+123 -119
View File
@@ -1,119 +1,123 @@
<template> <template>
<span class="stop-date"> <span class="stop-date">
<span <span
class="date arrival" class="date arrival"
v-if="!stop.beginsHere" v-if="!stop.beginsHere"
:class="{ :class="{
delayed: stop.arrivalDelay > 0 && (stop.confirmed || stop.stopped), delayed: stop.arrivalDelay > 0 && (stop.confirmed || stop.stopped),
preponed: stop.arrivalDelay < 0 && (stop.confirmed || stop.stopped), preponed: stop.arrivalDelay < 0 && (stop.confirmed || stop.stopped),
'on-time': stop.arrivalDelay == 0 && stop.confirmed, 'on-time': stop.arrivalDelay == 0 && stop.confirmed
}" }"
> >
<span v-if="stop.arrivalDelay != 0 && (stop.confirmed || stop.stopped)"> <span v-if="stop.arrivalDelay != 0 && (stop.confirmed || stop.stopped)">
<s>{{ timestampToString(stop.arrivalTimestamp) }}</s> <s>{{ timestampToString(stop.arrivalTimestamp) }}</s>
{{ timestampToString(stop.arrivalRealTimestamp) }} {{ timestampToString(stop.arrivalRealTimestamp) }}
({{ stop.arrivalDelay > 0 ? '+' : '' }}{{ stop.arrivalDelay }}) ({{ stop.arrivalDelay > 0 ? '+' : '' }}{{ stop.arrivalDelay }})
</span> </span>
<span v-else> <span v-else>
{{ timestampToString(stop.arrivalTimestamp) }} {{ timestampToString(stop.arrivalTimestamp) }}
</span> </span>
</span> </span>
<span class="date stop" v-if="stop.stopTime || stop.stopped" :class="stop.stopType.replace(', ', '-')"> <span
{{ stop.stopTime }} {{ stop.stopType == '' ? 'pt' : stop.stopType }} class="date stop"
</span> v-if="stop.stopTime || stop.stopped"
:class="stop.stopType.replace(', ', '-')"
<span >
class="date departure" {{ stop.stopTime }} {{ stop.stopType == '' ? 'pt' : stop.stopType }}
v-if="!stop.terminatesHere && (stop.stopTime != 0 || stop.stopped)" </span>
:class="{
delayed: stop.departureDelay > 0 && stop.confirmed, <span
preponed: stop.departureDelay < 0 && stop.confirmed, class="date departure"
}" v-if="!stop.terminatesHere && (stop.stopTime != 0 || stop.stopped)"
> :class="{
<span v-if="stop.departureDelay != 0 && stop.confirmed"> delayed: stop.departureDelay > 0 && stop.confirmed,
<s>{{ timestampToString(stop.departureTimestamp) }}</s> preponed: stop.departureDelay < 0 && stop.confirmed
{{ timestampToString(stop.departureRealTimestamp) }} }"
>
({{ stop.departureDelay > 0 ? '+' : '' }}{{ stop.departureDelay }}) <span v-if="stop.departureDelay != 0 && stop.confirmed">
</span> <s>{{ timestampToString(stop.departureTimestamp) }}</s>
{{ timestampToString(stop.departureRealTimestamp) }}
<span v-else>
{{ timestampToString(stop.departureTimestamp) }} ({{ stop.departureDelay > 0 ? '+' : '' }}{{ stop.departureDelay }})
</span> </span>
</span>
</span> <span v-else>
</template> {{ timestampToString(stop.departureTimestamp) }}
</span>
<script lang="ts"> </span>
import { defineComponent } from 'vue'; </span>
import dateMixin from '../../mixins/dateMixin'; </template>
import TrainStop from '../../scripts/interfaces/TrainStop';
<script lang="ts">
export default defineComponent({ import { PropType, defineComponent } from 'vue';
mixins: [dateMixin], import dateMixin from '../../mixins/dateMixin';
import { TrainStop } from '../../store/typings';
props: {
stop: { export default defineComponent({
type: Object as () => TrainStop, mixins: [dateMixin],
required: true,
}, props: {
}, stop: {
type: Object as PropType<TrainStop>,
setup() { required: true
return {}; }
}, },
});
</script> setup() {
return {};
<style lang="scss" scoped> }
$preponedClr: lime; });
$delayedClr: salmon; </script>
$dateClr: #525151;
$stopExchangeClr: #db8e29; <style lang="scss" scoped>
$stopDefaultClr: #252525; $preponedClr: lime;
$delayedClr: salmon;
.stop-date { $dateClr: #525151;
display: flex; $stopExchangeClr: #db8e29;
align-items: center; $stopDefaultClr: #252525;
.date { .stop-date {
background: $dateClr; display: flex;
padding: 0.3em 0.5em; align-items: center;
}
.date {
.stop { background: $dateClr;
&.ph, padding: 0.3em 0.5em;
&.ph-pm, }
&.pm {
background: $stopExchangeClr; .stop {
} &.ph,
&.ph-pm,
background: $stopDefaultClr; &.pm {
} background: $stopExchangeClr;
}
.arrival,
.departure { background: $stopDefaultClr;
&.delayed { }
s {
color: #999; .arrival,
} .departure {
&.delayed {
span { s {
color: $delayedClr; color: #999;
} }
}
span {
&.preponed { color: $delayedClr;
s { }
color: #999; }
}
&.preponed {
span { s {
color: $preponedClr; color: #999;
} }
}
} span {
} color: $preponedClr;
</style> }
}
}
}
</style>
+8 -17
View File
@@ -3,7 +3,7 @@
<div class="modal_background" @click="closeModal"></div> <div class="modal_background" @click="closeModal"></div>
<div class="modal_content" ref="content" tabindex="0"> <div class="modal_content" ref="content" tabindex="0">
<button class="btn exit" @click="closeModal"> <button class="btn exit" @click="closeModal">
<img :src="getIcon('exit')" alt="close card" /> <img src="/images/icon-exit.svg" alt="close card" />
</button> </button>
<TrainInfo :train="chosenTrain" :extended="false" ref="trainInfo" /> <TrainInfo :train="chosenTrain" :extended="false" ref="trainInfo" />
@@ -14,28 +14,18 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import trainInfoMixin from '../../mixins/trainInfoMixin'; import trainInfoMixin from '../../mixins/trainInfoMixin';
import { useStore } from '../../store/store';
import TrainInfo from '../TrainsView/TrainInfo.vue'; import TrainInfo from '../TrainsView/TrainInfo.vue';
import TrainSchedule from '../TrainsView/TrainSchedule.vue'; import TrainSchedule from '../TrainsView/TrainSchedule.vue';
export default defineComponent({ export default defineComponent({
components: { TrainInfo, TrainSchedule }, components: { TrainInfo, TrainSchedule },
mixins: [trainInfoMixin, modalTrainMixin, imageMixin], mixins: [trainInfoMixin, modalTrainMixin],
data() { data() {
return { return {
isTopBarVisible: false, isTopBarVisible: false
};
},
setup() {
const store = useStore();
return {
store,
}; };
}, },
@@ -49,12 +39,14 @@ export default defineComponent({
methods: { methods: {
handleContentScroll(e: Event) { handleContentScroll(e: Event) {
const trainInfoCompHeight: number = (this.$refs['trainInfo'] as any).$el.getBoundingClientRect().height; const trainInfoCompHeight: number = (
this.$refs['trainInfo'] as any
).$el.getBoundingClientRect().height;
const posTop = (e.target as HTMLElement).scrollTop; const posTop = (e.target as HTMLElement).scrollTop;
this.isTopBarVisible = posTop > trainInfoCompHeight; this.isTopBarVisible = posTop > trainInfoCompHeight;
}, }
}, }
}); });
</script> </script>
@@ -144,7 +136,6 @@ export default defineComponent({
} }
@include smallScreen { @include smallScreen {
.modal_content { .modal_content {
max-height: 85vh; max-height: 85vh;
} }
+15 -12
View File
@@ -4,7 +4,9 @@
<img <img
class="train-thumbnail" class="train-thumbnail"
v-else v-else
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${name.split(':')[0]}${stockType == 'loco-ezt' ? 'rb' : ''}.png`" :src="`https://rj.td2.info.pl/dist/img/thumbnails/${name.split(':')[0]}${
stockType == 'loco-ezt' ? 'rb' : ''
}.png`"
@error="onImageError" @error="onImageError"
@load="onImageLoad" @load="onImageLoad"
width="220" width="220"
@@ -14,28 +16,27 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import imageMixin from '../../mixins/imageMixin'; import { useStore } from '../../store/mainStore';
import { useStore } from '../../store/store'; import { API } from '../../typings/api';
import { RollingStockInfo } from '../../scripts/interfaces/github_api/StockInfoGithubData';
export default defineComponent({ export default defineComponent({
props: { props: {
name: { name: {
type: String, type: String,
required: true, required: true
}, },
onlyFirstSegment: { onlyFirstSegment: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}, },
data() { data() {
return { return {
store: useStore(), store: useStore(),
isNotFound: false, isNotFound: false,
isLoaded: false, isLoaded: false
}; };
}, },
@@ -53,10 +54,12 @@ export default defineComponent({
return ( return (
Object.keys(this.store.rollingStockData.info).find((type) => { Object.keys(this.store.rollingStockData.info).find((type) => {
return this.store.rollingStockData?.info[type as keyof RollingStockInfo].find((v) => v[0] === this.name.split(':')[0]); return this.store.rollingStockData?.info[type as keyof API.RollingStock.Info].find(
(v) => v[0] === this.name.split(':')[0]
);
}) || 'vehicle-unknown' }) || 'vehicle-unknown'
); );
}, }
}, },
methods: { methods: {
@@ -68,8 +71,8 @@ export default defineComponent({
onImageLoad() { onImageLoad() {
this.isNotFound = false; this.isNotFound = false;
this.isLoaded = true; this.isLoaded = true;
}, }
}, }
}); });
</script> </script>
+51 -60
View File
@@ -1,7 +1,7 @@
<template> <template>
<section class="daily-stats"> <section class="daily-stats">
<span :data-active="statsStatus"> <span :data-active="statsStatus">
<b v-if="statsStatus == DataStatus.Loading"> <b v-if="statsStatus == Status.Data.Loading">
{{ $t('app.loading') }} {{ $t('app.loading') }}
</b> </b>
@@ -32,64 +32,66 @@
</i18n-t> </i18n-t>
</div> </div>
<div v-if="stats.timetableId"> <div v-if="stats.maxTimetable">
&bull; &bull;
<i18n-t keypath="journal.timetable-stats-longest"> <i18n-t keypath="journal.timetable-stats-longest">
<template #id> <template #id>
<router-link :to="`/journal/timetables?timetableId=${stats.timetableId}`"> <router-link :to="`/journal/timetables?timetableId=${stats.maxTimetable.id}`">
<b>{{ stats.timetableId }}</b> <b>{{ stats.maxTimetable.id }}</b>
</router-link> </router-link>
</template> </template>
<template #author> <template #author>
<router-link :to="`/journal/dispatchers?dispatcherName=${stats.timetableAuthor}`"> <router-link
<b>{{ stats.timetableAuthor }}</b> :to="`/journal/dispatchers?dispatcherName=${stats.maxTimetable.authorName}`"
>
<b>{{ stats.maxTimetable.authorName }}</b>
</router-link> </router-link>
</template> </template>
<template #driver> <template #driver>
<b class="text--primary">{{ stats.timetableDriver }}</b> <b class="text--primary">{{ stats.maxTimetable.driverName }}</b>
</template> </template>
<template #distance> <template #distance>
<b class="text--primary">{{ stats.timetableRouteDistance }} km</b> <b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
</template> </template>
</i18n-t> </i18n-t>
</div> </div>
<div v-if="firstPlaceDispatchers.length == 1"> <div v-if="topDispatchers.length == 1">
&bull; &bull;
<i18n-t keypath="journal.timetable-stats-most-active-dr"> <i18n-t keypath="journal.timetable-stats-most-active-dr">
<template #dispatcher> <template #dispatcher>
<router-link :to="`/journal/dispatchers?dispatcherName=${firstPlaceDispatchers[0].name}`"> <router-link :to="`/journal/dispatchers?dispatcherName=${topDispatchers[0].name}`">
<b>{{ firstPlaceDispatchers[0].name }}</b> <b>{{ topDispatchers[0].name }}</b>
</router-link> </router-link>
</template> </template>
<template #count> <template #count>
<b class="text--primary"> <b class="text--primary">
{{ firstPlaceDispatchers[0].count }} {{ topDispatchers[0].count }}
{{ $t('journal.timetable-count', firstPlaceDispatchers[0].count) }} {{ $t('journal.timetable-count', topDispatchers[0].count) }}
</b> </b>
</template> </template>
</i18n-t> </i18n-t>
</div> </div>
<div v-if="firstPlaceDispatchers.length > 1"> <div v-if="topDispatchers.length > 1">
&bull; &bull;
<i18n-t keypath="journal.timetable-stats-most-active-dr-many"> <i18n-t keypath="journal.timetable-stats-most-active-dr-many">
<template #dispatchers> <template #dispatchers>
<span v-for="(disp, i) in firstPlaceDispatchers"> <span v-for="(disp, i) in topDispatchers" :key="i">
<span v-if="i == firstPlaceDispatchers.length - 1"> {{ $t('general.and') }} </span> <span v-if="i == topDispatchers.length - 1"> {{ $t('general.and') }} </span>
<router-link :to="`/journal/dispatchers?dispatcherName=${disp.name}`"> <router-link :to="`/journal/dispatchers?dispatcherName=${disp.name}`">
<b>{{ disp.name }}</b> <b>{{ disp.name }}</b>
</router-link> </router-link>
<span v-if="i < firstPlaceDispatchers.length - 2">, </span> <span v-if="i < topDispatchers.length - 2">, </span>
</span> </span>
</template> </template>
<template #count> <template #count>
<b class="text--primary"> <b class="text--primary">
{{ firstPlaceDispatchers[0].count }} {{ topDispatchers[0].count }}
{{ $t('journal.timetable-count', firstPlaceDispatchers[0].count) }} {{ $t('journal.timetable-count', topDispatchers[0].count) }}
</b> </b>
</template> </template>
</i18n-t> </i18n-t>
@@ -99,7 +101,9 @@
&bull; &bull;
<i18n-t keypath="journal.timetable-stats-longest-duties"> <i18n-t keypath="journal.timetable-stats-longest-duties">
<template #dispatcher> <template #dispatcher>
<router-link :to="`/journal/dispatchers?dispatcherName=${stats.longestDuties[0].name}`"> <router-link
:to="`/journal/dispatchers?dispatcherName=${stats.longestDuties[0].name}`"
>
<b>{{ stats.longestDuties[0].name }}</b> <b>{{ stats.longestDuties[0].name }}</b>
</router-link> </router-link>
</template> </template>
@@ -132,9 +136,10 @@
import axios from 'axios'; import axios from 'axios';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { ITimetablesDailyStats, ITimetablesDailyStatsResponse } from '../../scripts/interfaces/api/StatsAPIData';
import { URLs } from '../../scripts/utils/apiURLs'; import { URLs } from '../../scripts/utils/apiURLs';
import { API } from '../../typings/api';
import { Status } from '../../typings/common';
export default defineComponent({ export default defineComponent({
mixins: [dateMixin], mixins: [dateMixin],
@@ -142,22 +147,11 @@ export default defineComponent({
data() { data() {
return { return {
DataStatus, Status,
statsStatus: DataStatus.Loading, statsStatus: Status.Data.Loading,
intervalId: -1, intervalId: -1,
stats: { stats: {} as API.DailyStats.Response
totalTimetables: 0,
distanceSum: 0,
distanceAvg: 0,
timetableAuthor: '',
timetableDriver: '',
timetableId: 0,
timetableRouteDistance: 0,
longestDuties: [],
mostActiveDrivers: [],
mostActiveDispatchers: [],
} as ITimetablesDailyStats,
}; };
}, },
@@ -171,39 +165,41 @@ export default defineComponent({
}, },
computed: { computed: {
firstPlaceDispatchers() { topDispatchers() {
if (this.stats.mostActiveDispatchers.length == 0) return []; if (this.stats.mostActiveDispatchers.length == 0) return [];
const maxCount = this.stats.mostActiveDispatchers[0].count; const maxCount = this.stats.mostActiveDispatchers[0].count;
return this.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount); return this.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
}, }
}, },
methods: { methods: {
async fetchDailyTimetableStats() { async fetchDailyTimetableStats() {
try { try {
const res: ITimetablesDailyStatsResponse = await ( const res: API.DailyStats.Response = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDailyTimetableStats`) await axios.get(`${URLs.stacjownikAPI}/api/getDailyTimetableStats`)
).data; ).data;
this.stats = { // this.stats = {
totalTimetables: res.totalTimetables, // totalTimetables: res.totalTimetables,
distanceSum: res.distanceSum, // distanceSum: res.distanceSum,
distanceAvg: res.distanceAvg, // distanceAvg: res.distanceAvg,
timetableAuthor: res.maxTimetable?.authorName || '', // // timetableAuthor: res.maxTimetable?.authorName || '',
timetableDriver: res.maxTimetable?.driverName || '', // // timetableDriver: res.maxTimetable?.driverName || '',
timetableId: res.maxTimetable?.id || 0, // // timetableId: res.maxTimetable?.id || 0,
timetableRouteDistance: res.maxTimetable?.routeDistance || 0, // // timetableRouteDistance: res.maxTimetable?.routeDistance || 0,
mostActiveDispatchers: res.mostActiveDispatchers, // mostActiveDispatchers: res.mostActiveDispatchers,
mostActiveDrivers: res.mostActiveDrivers, // mostActiveDrivers: res.mostActiveDrivers,
longestDuties: res.longestDuties, // longestDuties: res.longestDuties
}; // };
this.statsStatus = DataStatus.Loaded; this.stats = res;
this.statsStatus = Status.Data.Loaded;
} catch (error) { } catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...'); console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
this.statsStatus = DataStatus.Error; this.statsStatus = Status.Data.Error;
} }
}, },
@@ -218,8 +214,8 @@ export default defineComponent({
stopFetchingDailyStats() { stopFetchingDailyStats() {
clearInterval(this.intervalId); clearInterval(this.intervalId);
this.intervalId = -1; this.intervalId = -1;
}, }
}, }
}); });
</script> </script>
@@ -238,13 +234,8 @@ export default defineComponent({
} }
@include smallScreen { @include smallScreen {
.daily-stats {
text-align: justify;
}
h3 { h3 {
text-align: center; text-align: center;
} }
} }
</style> </style>
+19 -27
View File
@@ -14,7 +14,7 @@
<div v-else> <div v-else>
<h3>STATYSTYKI WYSTAWIONYCH ROZKŁADÓW</h3> <h3>STATYSTYKI WYSTAWIONYCH ROZKŁADÓW</h3>
<div class="info-stats" v-if="store.dispatcherStatsData._count._all"> <div class="info-stats" v-if="store.dispatcherStatsData._count._all">
<span class="stat-badge"> <span class="stat-badge">
<span>LICZBA</span> <span>LICZBA</span>
@@ -36,8 +36,9 @@
<h3>OSTATNIE WYSTAWIONE ROZKŁADY</h3> <h3>OSTATNIE WYSTAWIONE ROZKŁADY</h3>
<div class="last-timetables"> <div class="last-timetables">
<div class="timetable-row" v-for="timetable in timetables"> <div class="timetable-row" v-for="timetable in timetables" :key="timetable.id">
#{{ timetable.timetableId }} | <b>{{ timetable.trainCategoryCode }} {{ timetable.trainNo }}</b> | #{{ timetable.timetableId }} |
<b>{{ timetable.trainCategoryCode }} {{ timetable.trainNo }}</b> |
{{ timetable.driverName }} ({{ timetable.routeDistance }}km) {{ timetable.driverName }} ({{ timetable.routeDistance }}km)
<div>{{ timetable.route.replace('|', ' > ') }}</div> <div>{{ timetable.route.replace('|', ' > ') }}</div>
</div> </div>
@@ -49,14 +50,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import axios from 'axios'; import axios from 'axios';
import { computed, defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { DispatcherStatsAPIData } from '../../scripts/interfaces/api/DispatcherStatsAPIData';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '../../scripts/utils/apiURLs'; import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store'; import { useStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import { API } from '../../typings/api';
export default defineComponent({ export default defineComponent({
components: { Loading }, components: { Loading },
@@ -64,15 +63,8 @@ export default defineComponent({
setup() { setup() {
const store = useStore(); const store = useStore();
const statsData2 = computed(async () => {
return await (
await axios.get(`${URLs.stacjownikAPI}/api/getDispatcherInfo?name=${store.dispatcherStatsName}`)
).data;
});
return { return {
store, store
statsData2,
}; };
}, },
@@ -80,7 +72,7 @@ export default defineComponent({
return { return {
cardVisible: false, cardVisible: false,
lastDispatcherName: '', lastDispatcherName: '',
timetables: [] as TimetableHistory[], timetables: [] as API.TimetableHistory.Response
}; };
}, },
@@ -97,19 +89,23 @@ export default defineComponent({
this.store.dispatcherStatsData = undefined; this.store.dispatcherStatsData = undefined;
} }
const statsData: DispatcherStatsAPIData = await ( const statsData: API.DispatcherStats.Response = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDispatcherInfo?name=${this.store.dispatcherStatsName}`) await axios.get(
`${URLs.stacjownikAPI}/api/getDispatcherInfo?name=${this.store.dispatcherStatsName}`
)
).data; ).data;
const timetables: TimetableHistory[] = await ( const timetables: API.TimetableHistory.Response = await (
await axios.get(`${URLs.stacjownikAPI}/api/getTimetables?authorName=${this.store.dispatcherStatsName}`) await axios.get(
`${URLs.stacjownikAPI}/api/getTimetables?authorName=${this.store.dispatcherStatsName}`
)
).data; ).data;
this.timetables = timetables; this.timetables = timetables;
this.store.dispatcherStatsData = statsData; this.store.dispatcherStatsData = statsData;
this.lastDispatcherName = this.store.dispatcherStatsName; this.lastDispatcherName = this.store.dispatcherStatsName;
}, }
}, }
}); });
</script> </script>
@@ -163,11 +159,7 @@ h3 {
text-align: center; text-align: center;
} }
.last-timetables { .last-timetables {
overflow-y: auto; overflow-y: auto;
} }
</style> </style>
@@ -1,241 +1,253 @@
<template> <template>
<div> <div>
<transition name="status-anim" mode="out-in"> <transition name="status-anim" mode="out-in">
<div :key="dataStatus"> <div :key="dataStatus">
<div class="journal_warning" v-if="store.isOffline"> <div class="journal_warning" v-if="store.isOffline">
{{ $t('app.offline') }} {{ $t('app.offline') }}
</div> </div>
<Loading v-else-if="dataStatus == DataStatus.Loading" /> <Loading v-else-if="dataStatus == Status.Data.Loading" />
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error"> <div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
{{ $t('app.error') }} {{ $t('app.error') }}
</div> </div>
<div class="journal_warning" v-else-if="dispatcherHistory.length == 0"> <div class="journal_warning" v-else-if="dispatcherHistory.length == 0">
{{ $t('app.no-result') }} {{ $t('app.no-result') }}
</div> </div>
<div v-else> <div v-else>
<table class="scenery-history-table"> <table class="scenery-history-table">
<thead> <thead>
<th>{{ $t('journal.history-name') }}</th> <th>{{ $t('journal.history-name') }}</th>
<th>{{ $t('journal.history-hash') }}</th> <th>{{ $t('journal.history-hash') }}</th>
<th>{{ $t('journal.history-dispatcher') }}</th> <th>{{ $t('journal.history-dispatcher') }}</th>
<th>{{ $t('journal.history-level') }}</th> <th>{{ $t('journal.history-level') }}</th>
<th>{{ $t('journal.history-rate') }}</th> <th>{{ $t('journal.history-rate') }}</th>
<th>{{ $t('journal.history-region') }}</th> <th>{{ $t('journal.history-region') }}</th>
<th>{{ $t('journal.history-date') }}</th> <th>{{ $t('journal.history-date') }}</th>
</thead> </thead>
<tbody> <tbody>
<transition-group name="list-anim"> <transition-group name="list-anim">
<tr v-for="historyItem in dispatcherHistory" :key="historyItem.id"> <tr v-for="historyItem in dispatcherHistory" :key="historyItem.id">
<td> <td>
<router-link :to="`/journal/dispatchers?sceneryName=${historyItem.stationName}`"> <router-link
<b>{{ historyItem.stationName }}</b> :to="`/journal/dispatchers?sceneryName=${historyItem.stationName}`"
</router-link> >
</td> <b>{{ historyItem.stationName }}</b>
<td>#{{ historyItem.stationHash }}</td> </router-link>
<td> </td>
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`"> <td>#{{ historyItem.stationHash }}</td>
<b>{{ historyItem.dispatcherName }}</b> <td>
</router-link> <router-link
</td> :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`"
<td> >
<b <b>{{ historyItem.dispatcherName }}</b>
v-if="historyItem.dispatcherLevel !== null" </router-link>
class="level-badge dispatcher" </td>
:style="calculateExpStyle(historyItem.dispatcherLevel, historyItem.dispatcherIsSupporter)" <td>
> <b
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }} v-if="historyItem.dispatcherLevel !== null"
</b> class="level-badge dispatcher"
</td> :style="
<td class="text--primary"> calculateExpStyle(
<b>{{ historyItem.dispatcherRate }}</b> historyItem.dispatcherLevel,
</td> historyItem.dispatcherIsSupporter
<td> )
<b class="region-badge" :aria-describedby="historyItem.region">{{ "
regions.find((r) => r.id == historyItem.region)?.value || '???' >
}}</b> {{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</td> </b>
<td style="min-width: 200px" class="time"> </td>
<span v-if="historyItem.timestampTo" class="text--offline"> <td class="text--primary">
<b>{{ $d(historyItem.timestampFrom) }}</b> <b>{{ historyItem.dispatcherRate }}</b>
{{ timestampToString(historyItem.timestampFrom) }} </td>
- {{ timestampToString(historyItem.timestampTo) }} ({{ <td>
calculateDuration(historyItem.currentDuration) <b class="region-badge" :aria-describedby="historyItem.region">{{
}}) regions.find((r) => r.id == historyItem.region)?.value || '???'
</span> }}</b>
<span class="dispatcher-online" v-else> </td>
<b class="text--online"> <td style="min-width: 200px" class="time">
<router-link :to="`/scenery?station=${historyItem.stationName}`">{{ <span v-if="historyItem.timestampTo" class="text--offline">
$t('journal.online-since') <b>{{ $d(historyItem.timestampFrom) }}</b>
}}</router-link> {{ timestampToString(historyItem.timestampFrom) }}
{{ timestampToString(historyItem.timestampFrom) }} - {{ timestampToString(historyItem.timestampTo) }} ({{
</b> calculateDuration(historyItem.currentDuration)
({{ calculateDuration(historyItem.currentDuration) }}) }})
</span> </span>
</td> <span class="dispatcher-online" v-else>
</tr> <b class="text--online">
</transition-group> <router-link :to="`/scenery?station=${historyItem.stationName}`">{{
</tbody> $t('journal.online-since')
</table> }}</router-link>
{{ timestampToString(historyItem.timestampFrom) }}
<AddDataButton </b>
:list="dispatcherHistory" ({{ calculateDuration(historyItem.currentDuration) }})
:scrollDataLoaded="scrollDataLoaded" </span>
:scrollNoMoreData="scrollNoMoreData" </td>
@addHistoryData="addHistoryData" </tr>
/> </transition-group>
</div> </tbody>
</div> </table>
</transition>
<AddDataButton
<div class="journal_warning" v-if="scrollNoMoreData"> :list="dispatcherHistory"
{{ $t('journal.no-further-data') }} :scrollDataLoaded="scrollDataLoaded"
</div> :scrollNoMoreData="scrollNoMoreData"
@addHistoryData="addHistoryData"
<div class="journal_warning" v-else-if="!scrollDataLoaded"> />
{{ $t('journal.loading-further-data') }} </div>
</div> </div>
</div> </transition>
</template>
<div class="journal_warning" v-if="scrollNoMoreData">
<script lang="ts"> {{ $t('journal.no-further-data') }}
import { defineComponent, PropType } from 'vue'; </div>
import dateMixin from '../../mixins/dateMixin';
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData'; <div class="journal_warning" v-else-if="!scrollDataLoaded">
import styleMixin from '../../mixins/styleMixin'; {{ $t('journal.loading-further-data') }}
import imageMixin from '../../mixins/imageMixin'; </div>
import { DataStatus } from '../../scripts/enums/DataStatus'; </div>
import { useStore } from '../../store/store'; </template>
import Loading from '../Global/Loading.vue';
import { regions } from '../../data/options.json'; <script lang="ts">
import AddDataButton from '../Global/AddDataButton.vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
export default defineComponent({ import styleMixin from '../../mixins/styleMixin';
components: { Loading, AddDataButton }, import { useStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue';
mixins: [dateMixin, styleMixin, imageMixin], import { regions } from '../../data/options.json';
import AddDataButton from '../Global/AddDataButton.vue';
props: { import { API } from '../../typings/api';
dispatcherHistory: { import { Status } from '../../typings/common';
type: Array as PropType<DispatcherHistory[]>,
required: true, export default defineComponent({
}, components: { Loading, AddDataButton },
scrollNoMoreData: {
type: Boolean, mixins: [dateMixin, styleMixin],
},
scrollDataLoaded: { props: {
type: Boolean, dispatcherHistory: {
}, type: Array as PropType<API.DispatcherHistory.Response>,
addHistoryData: { required: true
type: Function as PropType<() => void>, },
}, scrollNoMoreData: {
dataStatus: { type: Boolean
type: Number as PropType<DataStatus>, },
}, scrollDataLoaded: {
}, type: Boolean
},
data() { addHistoryData: {
return { type: Function as PropType<() => void>
DataStatus, },
store: useStore(), dataStatus: {
regions, type: Number as PropType<Status.Data>
}; }
}, },
computed: { data() {
computedDispatcherHistory() { return {
console.log(this.dispatcherHistory.length); Status,
store: useStore(),
return this.dispatcherHistory.reduce((acc, historyItem, i) => { regions
if (this.isAnotherDay(i - 1, i)) acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL')); };
acc.push(historyItem); },
return acc; computed: {
}, [] as (DispatcherHistory | string)[]); computedDispatcherHistory() {
}, console.log(this.dispatcherHistory.length);
},
return this.dispatcherHistory.reduce(
methods: { (acc, historyItem, i) => {
navigateToScenery(name: string, isOnline: boolean) { if (this.isAnotherDay(i - 1, i))
if (!isOnline) return; acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
acc.push(historyItem);
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
}, return acc;
},
isAnotherDay(prevIndex: number, currIndex: number) { [] as (API.DispatcherHistory.Data | string)[]
if (currIndex == 0) return true; );
}
return ( },
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate() methods: {
); navigateToScenery(name: string, isOnline: boolean) {
}, if (!isOnline) return;
},
}); this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
</script> },
<style lang="scss" scoped> isAnotherDay(prevIndex: number, currIndex: number) {
@import '../../styles/animations.scss'; if (currIndex == 0) return true;
@import '../../styles/responsive.scss';
@import '../../styles/badge.scss'; return (
@import '../../styles/variables.scss'; new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
@import '../../styles/JournalSection.scss'; new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
);
table.scenery-history-table { }
--_bg-table: #111; }
--_bg-head: #101010; });
--_bg-row: #2f2f2f; </script>
width: 100%; <style lang="scss" scoped>
border-collapse: collapse; @import '../../styles/animations.scss';
position: relative; @import '../../styles/responsive.scss';
text-align: center; @import '../../styles/badge.scss';
@import '../../styles/variables.scss';
margin-bottom: 1em; @import '../../styles/JournalSection.scss';
thead { table.scenery-history-table {
position: sticky; --_bg-table: #111;
top: 0; --_bg-head: #101010;
background-color: var(--_bg-head); --_bg-row: #2f2f2f;
}
width: 100%;
th { border-collapse: collapse;
padding: 0.5em; position: relative;
} text-align: center;
tr { margin-bottom: 1em;
background-color: var(--_bg-row);
border-bottom: 2px solid black; thead {
position: sticky;
&:last-child { top: 0;
border: none; background-color: var(--_bg-head);
} }
}
th {
td { padding: 0.5em;
padding: 0.75em; }
.level-badge { tr {
margin: 0 auto; background-color: var(--_bg-row);
} border-bottom: 2px solid black;
}
&:last-child {
@media screen and (max-width: 550px) { border: none;
font-size: 0.9em; }
} }
}
td {
.text { padding: 0.75em;
&--online {
color: springgreen; .level-badge {
} margin: 0 auto;
}
&--offline { }
color: #ddd;
} @media screen and (max-width: 550px) {
} font-size: 0.9em;
</style> }
}
.text {
&--online {
color: springgreen;
}
&--offline {
color: #ddd;
}
}
</style>
@@ -2,13 +2,17 @@
<div class="journal-stats"> <div class="journal-stats">
<span v-if="store.driverStatsData"> <span v-if="store.driverStatsData">
<h3> <h3>
{{ $t('journal.stats-title') }} <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span> {{ $t('journal.stats-title') }}
<span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
</h3> </h3>
<div class="info-stats"> <div class="info-stats">
<span class="stat-badge"> <span class="stat-badge">
<span>{{ $t('journal.stats-timetables') }}</span> <span>{{ $t('journal.stats-timetables') }}</span>
<span>{{ store.driverStatsData._count.fulfilled }} / {{ store.driverStatsData._count._all }}</span> <span
>{{ store.driverStatsData._count.fulfilled }} /
{{ store.driverStatsData._count._all }}</span
>
</span> </span>
<span class="stat-badge"> <span class="stat-badge">
@@ -39,8 +43,10 @@
</div> </div>
</span> </span>
<b v-else-if="store.driverStatsStatus == DataStatus.Loading">{{ $t('journal.stats-loading') }}</b> <b v-else-if="store.driverStatsStatus == Status.Data.Loading">{{
<b v-else-if="store.driverStatsStatus == DataStatus.Error"> $t('journal.stats-loading')
}}</b>
<b v-else-if="store.driverStatsStatus == Status.Data.Error">
{{ $t('journal.stats-error ') }} {{ $t('journal.stats-error ') }}
</b> </b>
<b v-else>{{ $t('journal.driver-stats-info') }}</b> <b v-else>{{ $t('journal.driver-stats-info') }}</b>
@@ -49,16 +55,16 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { useStore } from '../../store/mainStore';
import { useStore } from '../../store/store'; import { Status } from '../../typings/common';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
store: useStore(), store: useStore(),
DataStatus, Status: Status
}; };
}, }
}); });
</script> </script>
+304 -300
View File
@@ -1,300 +1,304 @@
<template> <template>
<div class="filters-options" @keydown.esc="showOptions = false"> <div class="filters-options" @keydown.esc="showOptions = false">
<div class="bg" v-if="showOptions" @click="showOptions = false"></div> <div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<div class="actions-bar"> <div class="actions-bar">
<button class="filter-button btn--filled btn--image" @click="showOptions = !showOptions" ref="button"> <button
<img :src="getIcon('filter2')" alt="Open filters" /> class="filter-button btn--filled btn--image"
{{ $t('options.filters') }} [F] @click="showOptions = !showOptions"
<span class="active-indicator" v-if="currentOptionsActive"></span> ref="button"
</button> >
<img src="/images/icon-filter2.svg" alt="Open filters" />
<button class="filter-button btn--filled btn--image" @click="refreshData"> {{ $t('options.filters') }} [F]
<img :src="getIcon('refresh')" alt="Refresh data" /> <span class="active-indicator" v-if="currentOptionsActive"></span>
{{ $t('general.refresh') }} </button>
</button>
</div> <button class="filter-button btn--filled btn--image" @click="refreshData">
<img src="/images/icon-refresh.svg" alt="Refresh data" />
<datalist id="search-driver"> {{ $t('general.refresh') }}
<option v-for="sugg in driverSuggestions" :value="sugg"></option> </button>
</datalist> </div>
<datalist id="search-dispatcher"> <datalist id="search-driver">
<option v-for="sugg in dispatcherSuggestions" :value="sugg"></option> <option v-for="(sugg, i) in driverSuggestions" :key="i" :value="sugg"></option>
</datalist> </datalist>
<transition name="options-anim"> <datalist id="search-dispatcher">
<div class="options_wrapper" v-if="showOptions"> <option v-for="(sugg, i) in dispatcherSuggestions" :key="i" :value="sugg"></option>
<div class="options_content"> </datalist>
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
<div class="search_content"> <transition name="options-anim">
<div class="search" v-for="(_, propName) in searchersValues" :key="propName"> <div class="options_wrapper" v-if="showOptions">
<label v-if="propName == 'search-date'" for="date">{{ $t(`options.search-${optionsType}-date`) }}</label> <div class="options_content">
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
<div class="search-box"> <div class="search_content">
<input <div class="search" v-for="(_, propName) in searchersValues" :key="propName">
class="search-input" <label v-if="propName == 'search-date'" for="date">{{
v-model="searchersValues[propName]" $t(`options.search-${optionsType}-date`)
@keydown.enter="onSearchConfirm" }}</label>
@focus="preventKeyDown = true"
@blur="preventKeyDown = false" <div class="search-box">
:placeholder="$t(`options.${propName}`)" <input
:type="propName == 'search-date' ? 'date' : 'text'" class="search-input"
:min="propName == 'search-date' ? '2022-02-01' : undefined" v-model="searchersValues[propName]"
:list="propName.toString()" @keydown.enter="onSearchConfirm"
/> @focus="preventKeyDown = true"
@blur="preventKeyDown = false"
<button class="search-exit" v-if="propName != 'search-date'"> :placeholder="$t(`options.${propName}`)"
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" /> :type="propName == 'search-date' ? 'date' : 'text'"
</button> :min="propName == 'search-date' ? '2022-02-01' : undefined"
</div> :list="propName.toString()"
</div> />
</div>
<button class="search-exit" v-if="propName != 'search-date'">
<h1 class="option-title">{{ $t('options.sort-title') }}</h1> <img
<div class="options_sorters"> src="/images/icon-exit.svg"
<div v-for="opt in translatedSorterOptions"> alt="exit-icon"
<button @click="onInputClear(propName)"
class="sort-option btn--option" />
:data-selected="opt.id == sorterActive.id" </button>
@click="onSorterChange(opt)" </div>
> </div>
{{ opt.value.toUpperCase() }} </div>
</button>
</div> <h1 class="option-title">{{ $t('options.sort-title') }}</h1>
</div> <div class="options_sorters">
<div v-for="opt in translatedSorterOptions" :key="opt.id">
<h1 class="option-title" v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1> <button
class="sort-option btn--option"
<div class="options_filter-sections" v-if="filters.length != 0 && filterList"> :data-selected="opt.id == sorterActive.id"
<section class="filter-section" v-for="section in JournalFilterSection"> @click="onSorterChange(opt)"
<p>{{ $t(`options.filter-section-${section}`) }}</p> >
{{ opt.value.toUpperCase() }}
<div class="options_filters"> </button>
<button </div>
v-for="filter in filterList.filter((f) => f.filterSection == section)" </div>
class="filter-option btn--option"
:class="{ checked: filter.isActive }" <h1 class="option-title" v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1>
:id="filter.id"
@click="onFilterChange(filter)" <div class="options_filter-sections" v-if="filters.length != 0 && filterList">
> <section class="filter-section" v-for="section in JournalFilterSection" :key="section">
{{ $t(`options.filter-${filter.id}`) }} <p>{{ $t(`options.filter-section-${section}`) }}</p>
</button>
</div> <div class="options_filters">
</section> <button
</div> v-for="filter in filterList.filter((f) => f.filterSection == section)"
:key="filter.id"
<div class="options_actions"> class="filter-option btn--option"
<button class="btn--action" @click="onResetButtonClick"> :class="{ checked: filter.isActive }"
{{ $t('options.reset-button') }} :id="filter.id"
</button> @click="onFilterChange(filter)"
<button class="btn--action" @click="onSearchButtonConfirm"> >
{{ $t('options.search-button') }} {{ $t(`options.filter-${filter.id}`) }}
</button> </button>
</div> </div>
</div> </section>
</div> </div>
</transition>
</div> <div class="options_actions">
</template> <button class="btn--action" @click="onResetButtonClick">
{{ $t('options.reset-button') }}
<script lang="ts"> </button>
import axios from 'axios'; <button class="btn--action" @click="onSearchButtonConfirm">
import { defineComponent, inject, PropType } from 'vue'; {{ $t('options.search-button') }}
import imageMixin from '../../mixins/imageMixin'; </button>
import keyMixin from '../../mixins/keyMixin'; </div>
import { DataStatus } from '../../scripts/enums/DataStatus'; </div>
import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData'; </div>
import { URLs } from '../../scripts/utils/apiURLs'; </transition>
import { useStore } from '../../store/store'; </div>
import ActionButton from '../Global/ActionButton.vue'; </template>
import SelectBox from '../Global/SelectBox.vue';
import { JournalFilterSection } from '../../scripts/enums/JournalFilterType'; <script lang="ts">
import { JournalFilter } from '../../scripts/types/JournalTimetablesTypes'; import axios from 'axios';
import { defineComponent, inject, PropType } from 'vue';
export default defineComponent({ import keyMixin from '../../mixins/keyMixin';
components: { SelectBox, ActionButton }, import { URLs } from '../../scripts/utils/apiURLs';
emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'], import { useStore } from '../../store/mainStore';
mixins: [imageMixin, keyMixin], import { Journal } from './typings';
import { API } from '../../typings/api';
props: { import { Status } from '../../typings/common';
sorterOptionIds: {
type: Array as PropType<Array<string>>, export default defineComponent({
required: true, emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'],
}, mixins: [keyMixin],
filters: { props: {
type: Array as PropType<JournalFilter[]>, sorterOptionIds: {
default: [], type: Array as PropType<Array<string>>,
}, required: true
},
dataStatus: {
type: Number as PropType<DataStatus>, filters: {
default: DataStatus.Initialized, type: Array as PropType<Journal.TimetableFilter[]>,
}, default: () => []
},
currentOptionsActive: {
type: Boolean, dataStatus: {
default: false, type: Number as PropType<Status.Data>,
}, default: -1
},
optionsType: {
type: String, currentOptionsActive: {
required: true, type: Boolean,
}, default: false
}, },
data() { optionsType: {
return { type: String,
showOptions: false, required: true
JournalFilterSection, }
},
driverSuggestions: [] as string[],
dispatcherSuggestions: [] as string[], data() {
return {
searchTimeout: 0, showOptions: false,
store: useStore(),
driverSuggestions: [] as string[],
DataStatus, dispatcherSuggestions: [] as string[],
};
}, searchTimeout: 0,
store: useStore(),
setup() {
return { JournalFilterSection: Journal.FilterSection
searchersValues: inject('searchersValues') as { [key: string]: string }, };
sorterActive: inject('sorterActive') as { id: string | number; dir: number }, },
// journalFilterActive: inject('journalFilterActive') as JournalFilter,
filterList: inject('filterList') as JournalFilter[] | undefined, setup() {
}; return {
}, searchersValues: inject('searchersValues') as { [key: string]: string },
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
computed: { filterList: inject('filterList') as Journal.TimetableFilter[] | undefined
driverStatsName() { };
return this.store.driverStatsName; },
},
computed: {
translatedSorterOptions() { translatedSorterOptions() {
return this.$props.sorterOptionIds.map((id) => ({ return this.$props.sorterOptionIds.map((id) => ({
id, id,
value: this.$t(`options.sort-${id}`), value: this.$t(`options.sort-${id}`)
})); }));
}, }
}, },
watch: { watch: {
async driverStatsName(value: string) { async 'store.driverStatsName'() {
await this.fetchDriverStats(); await this.fetchDriverStats();
// if (value) this.store.currentStatsTab = 'driver'; // if (value) this.store.currentStatsTab = 'driver';
}, },
async 'searchersValues.search-driver'(value: string | undefined) { async 'searchersValues.search-driver'(value: string | undefined) {
clearTimeout(this.searchTimeout); clearTimeout(this.searchTimeout);
if (!value || value == '') return; if (!value || value == '') return;
if (value.length < 3) return; if (value.length < 3) return;
this.startSearchTimeout('driver', value); this.startSearchTimeout('driver', value);
}, },
async 'searchersValues.search-dispatcher'(value: string | undefined) { async 'searchersValues.search-dispatcher'(value: string | undefined) {
if (!value || value == '') return; if (!value || value == '') return;
if (value.length < 3) return; if (value.length < 3) return;
this.startSearchTimeout('dispatcher', value); this.startSearchTimeout('dispatcher', value);
}, }
}, },
methods: { methods: {
async fetchDriverStats() { async fetchDriverStats() {
this.store.driverStatsData = undefined; this.store.driverStatsData = undefined;
if (!this.store.driverStatsName) { if (!this.store.driverStatsName) {
this.store.driverStatsStatus = DataStatus.Initialized; this.store.driverStatsStatus = Status.Data.Initialized;
return; return;
} }
try { try {
this.store.driverStatsStatus = DataStatus.Loading; this.store.driverStatsStatus = Status.Data.Loading;
const statsData: DriverStatsAPIData = await ( const statsData: API.DriverStats.Response = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`) await axios.get(
).data; `${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`
)
this.store.driverStatsData = statsData; ).data;
this.store.driverStatsStatus = DataStatus.Loaded;
} catch (error) { this.store.driverStatsData = statsData;
this.store.driverStatsStatus = DataStatus.Error; this.store.driverStatsStatus = Status.Data.Loaded;
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/'); } catch (error) {
} this.store.driverStatsStatus = Status.Data.Error;
}, console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/');
}
refreshData() { },
this.$emit('onRefreshData');
}, refreshData() {
this.$emit('onRefreshData');
startSearchTimeout(type: 'driver' | 'dispatcher', value: string) { },
if (this[`${type}Suggestions`].includes(value)) return;
startSearchTimeout(type: 'driver' | 'dispatcher', value: string) {
window.clearTimeout(this.searchTimeout); if (this[`${type}Suggestions`].includes(value)) return;
this.searchTimeout = setTimeout(async () => { window.clearTimeout(this.searchTimeout);
try {
const suggestions: string[] = await ( this.searchTimeout = setTimeout(async () => {
await axios.get(`${URLs.stacjownikAPI}/api/get${type}Suggestions?name=${value}`) try {
).data; const suggestions: string[] = await (
await axios.get(`${URLs.stacjownikAPI}/api/get${type}Suggestions?name=${value}`)
this[`${type}Suggestions`] = suggestions; ).data;
} catch (error) {
this[`${type}Suggestions`] = []; this[`${type}Suggestions`] = suggestions;
} } catch (error) {
}, 450); this[`${type}Suggestions`] = [];
}, }
}, 450);
// Override keyMixin function },
onKeyDownFunction() {
this.showOptions = !this.showOptions; // Override keyMixin function
onKeyDownFunction() {
this.$nextTick(() => { this.showOptions = !this.showOptions;
if (this.showOptions) (this.$refs['button'] as HTMLButtonElement)?.focus();
}); this.$nextTick(() => {
}, if (this.showOptions) (this.$refs['button'] as HTMLButtonElement)?.focus();
});
onSorterChange(item: { id: string | number; value: string }) { },
this.sorterActive.id = item.id;
this.sorterActive.dir = -1; onSorterChange(item: { id: string | number; value: string }) {
this.$emit('onSearchConfirm'); this.sorterActive.id = item.id;
}, this.sorterActive.dir = -1;
this.$emit('onSearchConfirm');
onFilterChange(filter: JournalFilter) { },
// this.journalFilterActive = filter;
this.filterList?.filter((f) => f.filterSection === filter.filterSection).forEach((f) => (f.isActive = false)); onFilterChange(filter: Journal.TimetableFilter) {
filter.isActive = true; // this.journalFilterActive = filter;
this.filterList
this.$emit('onSearchConfirm'); ?.filter((f) => f.filterSection === filter.filterSection)
}, .forEach((f) => (f.isActive = false));
filter.isActive = true;
onInputClear(id: any) {
this.searchersValues[id] = ''; this.$emit('onSearchConfirm');
this.$emit('onSearchConfirm'); },
},
onInputClear(id: any) {
onSearchConfirm() { this.searchersValues[id] = '';
this.$emit('onSearchConfirm'); this.$emit('onSearchConfirm');
}, },
onSearchButtonConfirm() { onSearchConfirm() {
this.showOptions = false; this.$emit('onSearchConfirm');
this.$emit('onSearchConfirm'); },
},
onSearchButtonConfirm() {
onResetButtonClick() { this.showOptions = false;
this.$emit('onOptionsReset'); this.$emit('onSearchConfirm');
}, },
},
}); onResetButtonClick() {
</script> this.$emit('onOptionsReset');
}
<style lang="scss" scoped> }
@import '../../styles/filters_options.scss'; });
</style> </script>
<style lang="scss" scoped>
@import '../../styles/filters_options.scss';
</style>
+14 -10
View File
@@ -3,6 +3,7 @@
<div class="tabs"> <div class="tabs">
<button <button
v-for="tab in data.tabs" v-for="tab in data.tabs"
:key="tab.name"
class="btn--filled" class="btn--filled"
:data-selected="tab.name == store.currentStatsTab && areStatsOpen" :data-selected="tab.name == store.currentStatsTab && areStatsOpen"
:data-inactive="tab.inactive" :data-inactive="tab.inactive"
@@ -16,7 +17,10 @@
<div class="stats-tab" v-show="areStatsOpen"> <div class="stats-tab" v-show="areStatsOpen">
<keep-alive> <keep-alive>
<JournalDailyStats v-if="store.currentStatsTab == 'daily'" @toggleStatsOpen="toggleStatsOpen" /> <JournalDailyStats
v-if="store.currentStatsTab == 'daily'"
@toggleStatsOpen="toggleStatsOpen"
/>
<JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" /> <JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" />
</keep-alive> </keep-alive>
</div> </div>
@@ -24,11 +28,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, KeepAlive, onMounted, reactive, Ref, ref, watch } from 'vue'; import { computed, onMounted, reactive, Ref, ref, watch } from 'vue';
import { useStore } from '../../store/store'; import { useStore } from '../../store/mainStore';
import JournalDailyStats from './DailyStats.vue'; import JournalDailyStats from './DailyStats.vue';
import JournalDriverStats from './JournalDriverStats.vue'; import JournalDriverStats from './JournalDriverStats.vue';
import StorageManager from '../../scripts/managers/storageManager'; import StorageManager from '../../managers/storageManager';
// Types // Types
type TStatTab = 'daily' | 'driver'; type TStatTab = 'daily' | 'driver';
@@ -44,19 +48,20 @@ let data = reactive({
tabs: [ tabs: [
{ {
name: 'daily', name: 'daily',
titlePath: 'journal.daily-stats-title', titlePath: 'journal.daily-stats-title'
}, },
{ {
name: 'driver', name: 'driver',
titlePath: 'journal.driver-stats-title', titlePath: 'journal.driver-stats-title'
// inactive: true, // inactive: true,
}, }
] as { name: TStatTab; titlePath: string; inactive?: boolean }[], ] as { name: TStatTab; titlePath: string; inactive?: boolean }[]
}); });
// Methods // Methods
function onTabButtonClick(tab: TStatTab) { function onTabButtonClick(tab: TStatTab) {
if (lastClickedTab.value == tab || !areStatsOpen.value) areStatsOpen.value = !areStatsOpen.value; if (lastClickedTab.value == tab || !lastClickedTab.value || !areStatsOpen.value)
areStatsOpen.value = !areStatsOpen.value;
if (tab == 'daily') { if (tab == 'daily') {
StorageManager.setBooleanValue('dailyStatsOpen', areStatsOpen.value); StorageManager.setBooleanValue('dailyStatsOpen', areStatsOpen.value);
@@ -115,4 +120,3 @@ onMounted(() => {
} }
} }
</style> </style>
@@ -1,82 +1,83 @@
<template> <template>
<div> <div>
<transition name="status-anim" mode="out-in"> <transition name="status-anim" mode="out-in">
<div :key="dataStatus"> <div :key="dataStatus">
<div class="journal_warning" v-if="store.isOffline"> <div class="journal_warning" v-if="store.isOffline">
{{ $t('app.offline') }} {{ $t('app.offline') }}
</div> </div>
<Loading v-else-if="dataStatus == DataStatus.Loading" /> <Loading v-else-if="dataStatus == Status.Data.Loading" />
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error"> <div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
{{ $t('app.error') }} {{ $t('app.error') }}
</div> </div>
<div v-else-if="timetableHistory.length == 0" class="journal_warning"> <div v-else-if="timetableHistory.length == 0" class="journal_warning">
{{ $t('app.no-result') }} {{ $t('app.no-result') }}
</div> </div>
<div v-else> <div v-else>
<TimetableHistoryList :timetableHistory="timetableHistory" /> <TimetableHistoryList :timetableHistory="timetableHistory" />
<AddDataButton <AddDataButton
:list="timetableHistory" :list="timetableHistory"
:scrollDataLoaded="scrollDataLoaded" :scrollDataLoaded="scrollDataLoaded"
:scrollNoMoreData="scrollNoMoreData" :scrollNoMoreData="scrollNoMoreData"
@addHistoryData="addHistoryData" @addHistoryData="addHistoryData"
/> />
</div> </div>
</div> </div>
</transition> </transition>
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div> <div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div> <div class="journal_warning" v-else-if="!scrollDataLoaded">
</div> {{ $t('journal.loading-further-data') }}
</template> </div>
</div>
<script lang="ts"> </template>
import { defineComponent, PropType } from 'vue';
import { DataStatus } from '../../../scripts/enums/DataStatus'; <script lang="ts">
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData'; import { defineComponent, PropType } from 'vue';
import { useStore } from '../../../store/store';
import Loading from '../../Global/Loading.vue';
import Loading from '../../Global/Loading.vue'; import AddDataButton from '../../Global/AddDataButton.vue';
import ProgressBar from '../../Global/ProgressBar.vue'; import TimetableHistoryList from './TimetableHistoryList.vue';
import AddDataButton from '../../Global/AddDataButton.vue'; import { useStore } from '../../../store/mainStore';
import TimetableHistoryList from './TimetableHistoryList.vue'; import { Status } from '../../../typings/common';
import { API } from '../../../typings/api';
export default defineComponent({
components: { ProgressBar, Loading, AddDataButton, TimetableHistoryList }, export default defineComponent({
components: { Loading, AddDataButton, TimetableHistoryList },
props: {
timetableHistory: { props: {
type: Array as PropType<TimetableHistory[]>, timetableHistory: {
required: true, type: Array as PropType<API.TimetableHistory.Response>,
}, required: true
scrollNoMoreData: { },
type: Boolean, scrollNoMoreData: {
}, type: Boolean
scrollDataLoaded: { },
type: Boolean, scrollDataLoaded: {
}, type: Boolean
addHistoryData: { },
type: Function as PropType<() => void>, addHistoryData: {
}, type: Function as PropType<() => void>
dataStatus: { },
type: Number as PropType<DataStatus>, dataStatus: {
}, type: Number as PropType<Status.Data>
}, }
},
data() {
return { data() {
DataStatus, return {
store: useStore(), Status,
}; store: useStore()
}, };
}); }
</script> });
</script>
<style lang="scss" scoped>
@import '../../../styles/JournalSection.scss'; <style lang="scss" scoped>
@import '../../../styles/animations.scss'; @import '../../../styles/JournalSection.scss';
</style> @import '../../../styles/animations.scss';
</style>
@@ -18,7 +18,11 @@
<span class="badge"> <span class="badge">
<span>{{ $t('journal.stock-length') }}</span> <span>{{ $t('journal.stock-length') }}</span>
<span> <span>
{{ currentHistoryIndex == 0 ? timetable.stockLength : stockHistory[currentHistoryIndex].stockLength || timetable.stockLength }}m {{
currentHistoryIndex == 0
? timetable.stockLength
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength
}}m
</span> </span>
</span> </span>
@@ -26,7 +30,11 @@
<span>{{ $t('journal.stock-mass') }}</span> <span>{{ $t('journal.stock-mass') }}</span>
<span> <span>
{{ {{
Math.floor((currentHistoryIndex == 0 ? timetable.stockMass! : stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000) Math.floor(
(currentHistoryIndex == 0
? timetable.stockMass!
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
)
}}t }}t
</span> </span>
</span> </span>
@@ -34,13 +42,26 @@
<!-- Historia zmian w składzie --> <!-- Historia zmian w składzie -->
<div class="stock-history" v-if="stockHistory.length > 1"> <div class="stock-history" v-if="stockHistory.length > 1">
<button class="btn--action" v-for="(sh, i) in stockHistory" :data-checked="i == currentHistoryIndex" @click.stop="currentHistoryIndex = i"> <button
v-for="(sh, i) in stockHistory"
:key="i"
class="btn--action"
:data-checked="i == currentHistoryIndex"
@click.stop="currentHistoryIndex = i"
>
{{ sh.updatedAt }} {{ sh.updatedAt }}
</button> </button>
</div> </div>
<!-- <StockList :trainStockList="currentHistoryIndex == 0 ? timetable.stockString : stockHistory[currentHistoryIndex].stockString).split(';')" /> --> <!-- <StockList :trainStockList="currentHistoryIndex == 0 ? timetable.stockString : stockHistory[currentHistoryIndex].stockString).split(';')" /> -->
<StockList :trainStockList="(currentHistoryIndex == 0 ? timetable.stockString : stockHistory[currentHistoryIndex].stockString).split(';') " /> <StockList
:trainStockList="
(currentHistoryIndex == 0
? timetable.stockString
: stockHistory[currentHistoryIndex].stockString
).split(';')
"
/>
<!-- <ul class="stock-list"> <!-- <ul class="stock-list">
<li <li
@@ -56,26 +77,24 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
import imageMixin from '../../../mixins/imageMixin';
import TrainThumbnail from '../../Global/TrainThumbnail.vue';
import StockList from '../../Global/StockList.vue'; import StockList from '../../Global/StockList.vue';
import { API } from '../../../typings/api';
export default defineComponent({ export default defineComponent({
mixins: [imageMixin], components: { StockList },
props: { props: {
showExtraInfo: { showExtraInfo: {
type: Boolean, type: Boolean,
required: true, required: true
}, },
timetable: { timetable: {
type: Object as PropType<TimetableHistory>, type: Object as PropType<API.TimetableHistory.Data>,
required: true, required: true
}, }
}, },
data() { data() {
return { return {
currentHistoryIndex: 0, currentHistoryIndex: 0
}; };
}, },
computed: { computed: {
@@ -88,22 +107,21 @@ export default defineComponent({
return { return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, { updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit'
}), }),
stockString: historyData[1], stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined, stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined, stockLength: Number(historyData[3]) || undefined
}; };
}); });
}, }
}, },
methods: { methods: {
onImageError(e: Event) { onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement; const imageEl = e.target as HTMLImageElement;
imageEl.src = this.getImage('unknown.png'); imageEl.src = '/images/icon-unknown.png';
}, }
}, }
components: { TrainThumbnail, StockList },
}); });
</script> </script>
@@ -45,7 +45,7 @@
:class="{ :class="{
fulfilled: timetable.fulfilled, fulfilled: timetable.fulfilled,
terminated: timetable.terminated && !timetable.fulfilled, terminated: timetable.terminated && !timetable.fulfilled,
active: !timetable.terminated, active: !timetable.terminated
}" }"
> >
{{ {{
@@ -62,29 +62,29 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
import dateMixin from '../../../mixins/dateMixin'; import dateMixin from '../../../mixins/dateMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin'; import modalTrainMixin from '../../../mixins/modalTrainMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import { API } from '../../../typings/api';
export default defineComponent({ export default defineComponent({
mixins: [dateMixin, modalTrainMixin, styleMixin], mixins: [dateMixin, modalTrainMixin, styleMixin],
props: { props: {
timetable: { timetable: {
type: Object as PropType<TimetableHistory>, type: Object as PropType<API.TimetableHistory.Data>,
required: true, required: true
}, }
}, },
methods: { methods: {
showTimetable(timetable: TimetableHistory, target: EventTarget | null) { showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) {
if (timetable?.terminated) return; if (timetable?.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target); this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target);
}, }
}, }
}); });
</script> </script>
@@ -23,7 +23,10 @@
<button class="btn--option btn--show"> <button class="btn--option btn--show">
{{ $t('journal.stock-info') }} {{ $t('journal.stock-info') }}
<img :src="getIcon(`arrow-${showExtraInfo.value ? 'asc' : 'desc'}`)" alt="Arrow" /> <img
:src="`/images/icon-arrow-${showExtraInfo.value ? 'asc' : 'desc'}.svg`"
alt="Arrow icon"
/>
</button> </button>
<!-- Extra --> <!-- Extra -->
<TimetableExtra :timetable="timetable" :showExtraInfo="showExtraInfo.value" /> <TimetableExtra :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
@@ -35,32 +38,30 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent, ref } from 'vue'; import { PropType, defineComponent, ref } from 'vue';
import imageMixin from '../../../mixins/imageMixin';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
import TimetableGeneral from './TimetableGeneral.vue'; import TimetableGeneral from './TimetableGeneral.vue';
import TimetableStops from './TimetableStops.vue'; import TimetableStops from './TimetableStops.vue';
import TimetableStatus from './TimetableStatus.vue'; import TimetableStatus from './TimetableStatus.vue';
import TimetableExtra from './TimetableExtra.vue'; import TimetableExtra from './TimetableExtra.vue';
import { API } from '../../../typings/api';
export default defineComponent({ export default defineComponent({
mixins: [imageMixin],
props: { props: {
timetableHistory: { timetableHistory: {
type: Array as PropType<TimetableHistory[]>, type: Array as PropType<API.TimetableHistory.Response>,
required: true, required: true
}, }
}, },
computed: { computed: {
computedTimetableHistory() { computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({ return this.timetableHistory.map((timetable) => ({
timetable, timetable,
showExtraInfo: ref(false), showExtraInfo: ref(false)
})); }));
}, }
}, },
methods: {}, methods: {},
components: { TimetableGeneral, TimetableStops, TimetableStatus, TimetableExtra }, components: { TimetableGeneral, TimetableStops, TimetableStatus, TimetableExtra }
}); });
</script> </script>
@@ -6,13 +6,19 @@
/> />
<span> <span>
<span :style="{ color: timetable.fulfilled ? 'lightgreen' : timetable.terminated ? 'salmon' : '' }"> <span
:style="{
color: timetable.fulfilled ? 'lightgreen' : timetable.terminated ? 'salmon' : ''
}"
>
{{ timetable.currentDistance + ' km' }} {{ timetable.currentDistance + ' km' }}
</span> </span>
<span> / </span> <span> / </span>
<span class="text--primary">{{ timetable.routeDistance }} km</span> <span class="text--primary">{{ timetable.routeDistance }} km</span>
| |
<span class="text--grayed">{{ timetable.confirmedStopsCount }}/{{ timetable.allStopsCount }}</span> <span class="text--grayed"
>{{ timetable.confirmedStopsCount }}/{{ timetable.allStopsCount }}</span
>
</span> </span>
<span class="text--grayed" v-if="timetable.currentSceneryName"> <span class="text--grayed" v-if="timetable.currentSceneryName">
@@ -38,17 +44,17 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
import ProgressBar from '../../Global/ProgressBar.vue'; import ProgressBar from '../../Global/ProgressBar.vue';
import { API } from '../../../typings/api';
export default defineComponent({ export default defineComponent({
components: { ProgressBar }, components: { ProgressBar },
props: { props: {
timetable: { timetable: {
type: Object as PropType<TimetableHistory>, type: Object as PropType<API.TimetableHistory.Data>,
required: true, required: true
}, }
}, }
}); });
</script> </script>
@@ -24,8 +24,7 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import dateMixin from '../../../mixins/dateMixin'; import dateMixin from '../../../mixins/dateMixin';
import { API } from '../../../typings/api';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
export default defineComponent({ export default defineComponent({
mixins: [dateMixin], mixins: [dateMixin],
@@ -37,9 +36,9 @@ export default defineComponent({
}, },
timetable: { timetable: {
type: Object as PropType<TimetableHistory>, type: Object as PropType<API.TimetableHistory.Data>,
required: true, required: true
}, }
}, },
computed: { computed: {
@@ -65,12 +64,18 @@ export default defineComponent({
if (i == 0) return { stopName, html: beginDateHTML, confirmed }; if (i == 0) return { stopName, html: beginDateHTML, confirmed };
if (i == stopNames.length - 1) return { stopName, html: endDateHTML, confirmed }; if (i == stopNames.length - 1) return { stopName, html: endDateHTML, confirmed };
const departureDateScheduled = this.stringToDate(timetable.checkpointDeparturesScheduled?.at(i)); const departureDateScheduled = this.stringToDate(
timetable.checkpointDeparturesScheduled?.at(i)
);
const departureDateReal = this.stringToDate(timetable.checkpointDepartures?.at(i)); const departureDateReal = this.stringToDate(timetable.checkpointDepartures?.at(i));
const arrivalDateScheduled = this.stringToDate(timetable.checkpointArrivalsScheduled?.at(i)); const arrivalDateScheduled = this.stringToDate(
timetable.checkpointArrivalsScheduled?.at(i)
);
const arrivalDateReal = this.stringToDate(timetable.checkpointArrivals?.at(i)); const arrivalDateReal = this.stringToDate(timetable.checkpointArrivals?.at(i));
const arrivalHTML = const arrivalHTML =
(arrivalDateReal && arrivalDateScheduled && arrivalDateReal?.getTime() != arrivalDateScheduled?.getTime() (arrivalDateReal &&
arrivalDateScheduled &&
arrivalDateReal?.getTime() != arrivalDateScheduled?.getTime()
? `<s class="text--grayed">${this.parseDateToTimeString(arrivalDateScheduled)}</s> ` ? `<s class="text--grayed">${this.parseDateToTimeString(arrivalDateScheduled)}</s> `
: '') + this.parseDateToTimeString(arrivalDateReal || arrivalDateScheduled); : '') + this.parseDateToTimeString(arrivalDateReal || arrivalDateScheduled);
const departureHTML = const departureHTML =
@@ -83,8 +88,8 @@ export default defineComponent({
if (html) html = ` (${html})`; if (html) html = ` (${html})`;
return { stopName, html, confirmed }; return { stopName, html, confirmed };
}); });
}, }
}, }
}); });
</script> </script>
+49
View File
@@ -0,0 +1,49 @@
export namespace Journal {
export type DispatcherSearcher = {
[key in 'search-dispatcher' | 'search-station' | 'search-date']: string;
};
export interface DispatcherSorter {
id: 'timestampFrom' | 'duration';
dir: -1 | 1;
}
export type TimetableSearchKey =
| 'search-driver'
| 'search-train'
| 'search-date'
| 'search-dispatcher'
| 'search-issuedFrom';
export type TimetableSearchType = {
[key in TimetableSearchKey]: string;
};
export const enum TimetableFilterId {
ACTIVE = 'active',
FULFILLED = 'fulfilled',
ABANDONED = 'abandoned',
ALL = 'all',
TWR = 'twr',
SKR = 'skr',
TWR_SKR = 'twr-skr'
}
export enum FilterSection {
TIMETABLE_STATUS = 'timetable-status',
TWRSKR = 'twrskr'
}
export interface TimetableFilter {
id: TimetableFilterId;
filterSection: string;
isActive: boolean;
}
export type TimetableSorterKey = 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
export interface TimetableSorter {
id: TimetableSorterKey;
dir: 'asc' | 'desc';
}
}
@@ -1,143 +1,161 @@
<template> <template>
<section class="scenery-table-section"> <section class="scenery-table-section">
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" /> <Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
<div class="no-history" v-else-if="historyList.length == 0">{{ $t('scenery.history-list-empty') }}</div>
<div class="no-history" v-else-if="historyList.length == 0">
<table class="scenery-history-table" v-else="historyList.length"> {{ $t('scenery.history-list-empty') }}
<thead> </div>
<th>{{ $t('scenery.dispatchers-history-hash') }}</th>
<th>{{ $t('scenery.dispatchers-history-dispatcher') }}</th> <table class="scenery-history-table" v-else>
<th>{{ $t('scenery.dispatchers-history-level') }}</th> <thead>
<th>{{ $t('scenery.dispatchers-history-rate') }}</th> <th>{{ $t('scenery.dispatchers-history-hash') }}</th>
<th>{{ $t('scenery.dispatchers-history-date') }}</th> <th>{{ $t('scenery.dispatchers-history-dispatcher') }}</th>
</thead> <th>{{ $t('scenery.dispatchers-history-level') }}</th>
<th>{{ $t('scenery.dispatchers-history-rate') }}</th>
<tbody> <th>{{ $t('scenery.dispatchers-history-date') }}</th>
<tr v-for="historyItem in historyList"> </thead>
<td>#{{ historyItem.stationHash }}</td>
<td> <tbody>
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`"> <tr v-for="historyItem in historyList" :key="historyItem.id">
<b>{{ historyItem.dispatcherName }}</b> <td>#{{ historyItem.stationHash }}</td>
</router-link> <td>
</td> <router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
<td> <b>{{ historyItem.dispatcherName }}</b>
<b </router-link>
v-if="historyItem.dispatcherLevel !== null" </td>
class="level-badge dispatcher" <td>
:style="calculateExpStyle(historyItem.dispatcherLevel, historyItem.dispatcherIsSupporter)" <b
> v-if="historyItem.dispatcherLevel !== null"
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }} class="level-badge dispatcher"
</b> :style="
</td> calculateExpStyle(historyItem.dispatcherLevel, historyItem.dispatcherIsSupporter)
<td class="text--primary"> "
<b>{{ historyItem.dispatcherRate }}</b> >
</td> {{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
<td style="min-width: 300px"> </b>
<div v-if="historyItem.timestampTo"> </td>
<b>{{ $d(historyItem.timestampFrom) }}</b> <td class="text--primary">
<b>{{ historyItem.dispatcherRate }}</b>
{{ timestampToString(historyItem.timestampFrom) }} </td>
- {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }}) <td style="min-width: 300px">
</div> <div v-if="historyItem.timestampTo">
<b>{{ $d(historyItem.timestampFrom) }}</b>
<div class="dispatcher-online" v-else>
{{ $t('journal.online-since') }} {{ timestampToString(historyItem.timestampFrom) }}
<b>{{ timestampToString(historyItem.timestampFrom) }}</b> - {{ timestampToString(historyItem.timestampTo) }} ({{
({{ calculateDuration(historyItem.currentDuration) }}) calculateDuration(historyItem.currentDuration)
</div> }})
</td> </div>
</tr>
</tbody> <div class="dispatcher-online" v-else>
</table> {{ $t('journal.online-since') }}
</section> <b>{{ timestampToString(historyItem.timestampFrom) }}</b>
({{ calculateDuration(historyItem.currentDuration) }})
<div class="bottom-info"> </div>
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory"> </td>
{{ $t('scenery.bottom-info') }} </tr>
</button> </tbody>
</div> </table>
</template> </section>
<script lang="ts"> <div class="bottom-info">
import axios from 'axios'; <button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
import { defineComponent, PropType } from 'vue'; {{ $t('scenery.bottom-info') }}
import dateMixin from '../../mixins/dateMixin'; </button>
import { DataStatus } from '../../scripts/enums/DataStatus'; </div>
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData'; </template>
import Station from '../../scripts/interfaces/Station';
import { URLs } from '../../scripts/utils/apiURLs'; <script lang="ts">
import Loading from '../Global/Loading.vue'; import axios from 'axios';
import styleMixin from '../../mixins/styleMixin'; import { defineComponent, PropType } from 'vue';
import listObserverMixin from '../../mixins/listObserverMixin'; import dateMixin from '../../mixins/dateMixin';
import Station from '../../scripts/interfaces/Station';
export default defineComponent({ import { URLs } from '../../scripts/utils/apiURLs';
name: 'SceneryDispatchersHistory', import Loading from '../Global/Loading.vue';
mixins: [dateMixin, styleMixin, listObserverMixin], import styleMixin from '../../mixins/styleMixin';
props: { import listObserverMixin from '../../mixins/listObserverMixin';
station: { import { OnlineScenery } from '../../store/typings';
type: Object as PropType<Station>, import { API } from '../../typings/api';
required: true, import { Status } from '../../typings/common';
},
}, export default defineComponent({
name: 'SceneryDispatchersHistory',
data() { mixins: [dateMixin, styleMixin, listObserverMixin],
return { components: { Loading },
historyList: [] as DispatcherHistory[], props: {
dataStatus: DataStatus.Loading, station: {
DataStatus, type: Object as PropType<Station>,
}; required: true
}, },
onlineScenery: {
async activated() { type: Object as PropType<OnlineScenery>,
// if (this.historyList.length == 0) { required: false
const fetchedHistory = await this.fetchAPIData(); }
if (fetchedHistory) this.historyList = fetchedHistory; },
// }
}, data() {
return {
methods: { historyList: [] as API.DispatcherHistory.Response,
async fetchAPIData(countFrom = 0, countLimit = 30): Promise<DispatcherHistory[] | null> { dataStatus: Status.Data.Loading,
try { DataStatus: Status.Data
this.dataStatus = DataStatus.Loading; };
},
const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
const historyAPIData: DispatcherHistory[] = await (await axios.get(requestString)).data; async activated() {
// if (this.historyList.length == 0) {
this.dataStatus = DataStatus.Loaded; const fetchedHistory = await this.fetchAPIData();
return historyAPIData; if (fetchedHistory) this.historyList = fetchedHistory;
} catch (error) { // }
this.dataStatus = DataStatus.Error; },
console.error(error);
return null; methods: {
} async fetchAPIData(
}, countFrom = 0,
navigateToHistory() { countLimit = 30
this.$router.push(`/journal/dispatchers?sceneryName=${this.station.name}`); ): Promise<API.DispatcherHistory.Response | null> {
}, try {
}, this.dataStatus = Status.Data.Loading;
components: { Loading },
}); const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
</script> const historyAPIData: API.DispatcherHistory.Response = await (
await axios.get(requestString)
<style lang="scss" scoped> ).data;
@import '../../styles/responsive.scss';
@import '../../styles/sceneryViewTables.scss'; this.dataStatus = Status.Data.Loaded;
return historyAPIData;
.level-badge { } catch (error) {
margin: 0 auto; this.dataStatus = Status.Data.Error;
} console.error(error);
return null;
.dispatcher-online { }
color: springgreen; },
} navigateToHistory() {
this.$router.push(`/journal/dispatchers?sceneryName=${this.station.name}`);
@include smallScreen { }
.history-list { }
font-size: 1.1em; });
} </script>
.list-item {
align-items: center; <style lang="scss" scoped>
flex-direction: column; @import '../../styles/responsive.scss';
} @import '../../styles/sceneryViewTables.scss';
}
</style> .level-badge {
margin: 0 auto;
}
.dispatcher-online {
color: springgreen;
}
@include smallScreen {
.history-list {
font-size: 1.1em;
}
.list-item {
align-items: center;
flex-direction: column;
}
}
</style>
../../store/storeTypes
+12 -6
View File
@@ -8,21 +8,27 @@
{{ $t('scenery.abbrev') }} <b>{{ station.generalInfo?.abbr }}</b> {{ $t('scenery.abbrev') }} <b>{{ station.generalInfo?.abbr }}</b>
</div> </div>
<div class="scenery-hash" v-if="station.onlineInfo?.hash">#{{ station.onlineInfo.hash }}</div> <div class="scenery-hash" v-if="onlineScenery?.hash">#{{ onlineScenery.hash }}</div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
import { OnlineScenery } from '../../store/typings';
export default defineComponent({ export default defineComponent({
props: { props: {
station: { station: {
type: Object as () => Station, type: Object as PropType<Station>,
default: {}, required: true
}, },
},
onlineScenery: {
type: Object as PropType<OnlineScenery>,
required: false
}
}
}); });
</script> </script>
@@ -52,4 +58,4 @@ export default defineComponent({
font-size: 1.2em; font-size: 1.2em;
} }
</style> </style>
../../store/storeTypes
+162 -139
View File
@@ -1,139 +1,162 @@
<template> <template>
<div class="scenery-info"> <div class="scenery-info">
<section v-if="!timetableOnly"> <section>
<div class="scenery-info-general" v-if="station.generalInfo"> <div class="scenery-info-general" v-if="station.generalInfo">
<SceneryInfoIcons :station="station" /> <SceneryInfoIcons :station="station" />
<div class="scenery-general-list"> <div class="scenery-general-list">
<span> <span>
<b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }} <b>{{ $t('availability.title') }}:</b>
{{ $t(`availability.${station.generalInfo.availability}`) }}
<span v-if="station.generalInfo.reqLevel > -1">
- {{ $t('scenery.req-level', { lvl: station.generalInfo.reqLevel }, station.generalInfo.reqLevel) }} <span v-if="station.generalInfo.reqLevel > -1">
</span> -
</span> {{
$t(
<span> 'scenery.req-level',
&bull; <b>{{ $t('controls.title') }}:</b> {{ $t(`controls.${station.generalInfo.controlType}`) }} { lvl: station.generalInfo.reqLevel },
</span> station.generalInfo.reqLevel
)
<span> }}
&bull; <b>{{ $t('signals.title') }}:</b> {{ $t(`signals.${station.generalInfo.signalType}`) }} </span>
</span> </span>
<span v-if="station.generalInfo.lines"> <span>
&bull; <b>{{ $t('scenery.lines-title') }}:</b> {{ station.generalInfo.lines }} &bull; <b>{{ $t('controls.title') }}:</b>
</span> {{ $t(`controls.${station.generalInfo.controlType}`) }}
<span v-if="station.generalInfo.project"> </span>
&bull; <b>{{ $t('scenery.project-title') }}: </b>
<a style="color: salmon; text-decoration: underline; font-weight: bold" :href="station.generalInfo.projectUrl" target="_blank"> <span>
{{ station.generalInfo.project }} &bull; <b>{{ $t('signals.title') }}:</b>
</a> {{ $t(`signals.${station.generalInfo.signalType}`) }}
</span> </span>
</div>
<span v-if="station.generalInfo.lines">
<SceneryInfoRoutes :station="station" /> &bull; <b>{{ $t('scenery.lines-title') }}:</b> {{ station.generalInfo.lines }}
</span>
<div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0"> <span v-if="station.generalInfo.project">
<b> {{ $t('scenery.authors-title', { authors: station.generalInfo.authors.length }, station.generalInfo.authors.length) }}: </b> &bull; <b>{{ $t('scenery.project-title') }}: </b>
{{ station.generalInfo.authors.join(', ') }} <a
</div> style="color: salmon; text-decoration: underline; font-weight: bold"
</div> :href="station.generalInfo.projectUrl"
target="_blank"
<div style="margin: 2em 0; height: 2px; background-color: white"></div> >
{{ station.generalInfo.project }}
<!-- info dispatcher --> </a>
<SceneryInfoDispatcher :station="station" :onlineFrom="onlineFrom" /> </span>
</div>
<div class="info-lists">
<!-- user list --> <SceneryInfoRoutes :station="station" />
<SceneryInfoUserList :station="station" />
<div
<!-- spawn list --> class="scenery-authors"
<SceneryInfoSpawnList :station="station" /> v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0"
</div> >
</section> <b>
</div> {{
</template> $t(
'scenery.authors-title',
<script lang="ts"> { authors: station.generalInfo.authors.length },
import { defineComponent } from '@vue/runtime-core'; station.generalInfo.authors.length
)
import SceneryInfoDispatcher from './SceneryInfo/SceneryInfoDispatcher.vue'; }}:
import SceneryInfoIcons from './SceneryInfo/SceneryInfoIcons.vue'; </b>
import SceneryInfoStats from './SceneryInfo/SceneryInfoStats.vue'; {{ station.generalInfo.authors.join(', ') }}
import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue'; </div>
import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue'; </div>
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
import Station from '../../scripts/interfaces/Station'; <div style="margin: 2em 0; height: 2px; background-color: white"></div>
export default defineComponent({ <!-- info dispatcher -->
components: { <SceneryInfoDispatcher :onlineScenery="onlineScenery" />
SceneryInfoDispatcher,
SceneryInfoIcons, <div class="info-lists">
SceneryInfoStats, <!-- user list -->
SceneryInfoUserList, <SceneryInfoUserList :onlineScenery="onlineScenery" />
SceneryInfoSpawnList,
SceneryInfoRoutes, <!-- spawn list -->
}, <SceneryInfoSpawnList :onlineScenery="onlineScenery" />
props: { </div>
station: { </section>
type: Object as () => Station, </div>
default: {}, </template>
},
<script lang="ts">
timetableOnly: Boolean, import { PropType, defineComponent } from 'vue';
},
import SceneryInfoDispatcher from './SceneryInfo/SceneryInfoDispatcher.vue';
data: () => ({ import SceneryInfoIcons from './SceneryInfo/SceneryInfoIcons.vue';
onlineFrom: -1, import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue';
}), import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
}); import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
</script> import Station from '../../scripts/interfaces/Station';
import { OnlineScenery } from '../../store/typings';
<style lang="scss">
@import '../../styles/responsive.scss'; export default defineComponent({
@import '../../styles/badge.scss'; components: {
SceneryInfoDispatcher,
h3.section-header { SceneryInfoIcons,
margin: 0.5em 0; SceneryInfoUserList,
padding: 0.3em; SceneryInfoSpawnList,
SceneryInfoRoutes
display: flex; },
justify-content: center; props: {
align-items: center; station: {
type: Object as PropType<Station>,
font-size: 1.2em; required: true
},
img {
width: 1.1em; onlineScenery: {
margin-left: 0.5em; type: Object as PropType<OnlineScenery>,
} required: false
} }
}
.info-lists { });
display: flex; </script>
flex-wrap: wrap;
justify-content: space-around; <style lang="scss">
@import '../../styles/responsive.scss';
margin-top: 1em; @import '../../styles/badge.scss';
}
h3.section-header {
.scenery-info-general { margin: 0.5em 0;
margin-top: 1em; padding: 0.3em;
}
display: flex;
.scenery-general-list { justify-content: center;
display: flex; align-items: center;
justify-content: center;
flex-wrap: wrap; font-size: 1.2em;
span { img {
margin: 0 0.15em; width: 1.1em;
} margin-left: 0.5em;
} }
}
.scenery-topic a {
font-weight: bold; .info-lists {
} display: flex;
</style> flex-wrap: wrap;
justify-content: space-around;
margin-top: 1em;
}
.scenery-info-general {
margin-top: 1em;
}
.scenery-general-list {
display: flex;
justify-content: center;
flex-wrap: wrap;
span {
margin: 0 0.15em;
}
}
.scenery-topic a {
font-weight: bold;
}
</style>
@@ -1,56 +1,50 @@
<template> <template>
<section class="info-dispatcher"> <section class="info-dispatcher">
<div class="dispatcher" v-if="station.onlineInfo"> <div class="dispatcher" v-if="onlineScenery">
<span <span
class="dispatcher_level" class="dispatcher_level"
:style="calculateExpStyle(station.onlineInfo.dispatcherExp, station.onlineInfo.dispatcherIsSupporter)" :style="calculateExpStyle(onlineScenery.dispatcherExp, onlineScenery.dispatcherIsSupporter)"
> >
{{ station.onlineInfo.dispatcherExp > 1 ? station.onlineInfo.dispatcherExp : 'L' }} {{ onlineScenery.dispatcherExp > 1 ? onlineScenery.dispatcherExp : 'L' }}
</span> </span>
<router-link <router-link
class="dispatcher_name" class="dispatcher_name"
:to="`/journal/dispatchers?dispatcherName=${station.onlineInfo.dispatcherName}`" :to="`/journal/dispatchers?dispatcherName=${onlineScenery.dispatcherName}`"
> >
{{ station.onlineInfo.dispatcherName }} {{ onlineScenery.dispatcherName }}
</router-link> </router-link>
<span class="dispatcher_likes text--primary"> <span class="dispatcher_likes text--primary">
<img :src="getIcon('like')" alt="icon-like" /> <img src="/images/icon-like.svg" alt="Likes count icon" />
<span>{{ station.onlineInfo?.dispatcherRate || '0' }}</span> <span>{{ onlineScenery?.dispatcherRate || '0' }}</span>
</span> </span>
</div> </div>
<StationStatusBadge <StationStatusBadge
:statusID="station.onlineInfo?.statusID" :isOnline="onlineScenery ? true : false"
:isOnline="station.onlineInfo ? true : false" :dispatcherStatus="onlineScenery?.dispatcherStatus"
:statusTimestamp="station.onlineInfo?.statusTimestamp"
/> />
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import dateMixin from '../../../mixins/dateMixin'; import dateMixin from '../../../mixins/dateMixin';
import imageMixin from '../../../mixins/imageMixin';
import routerMixin from '../../../mixins/routerMixin'; import routerMixin from '../../../mixins/routerMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import Station from '../../../scripts/interfaces/Station';
import StationStatusBadge from '../../Global/StationStatusBadge.vue'; import StationStatusBadge from '../../Global/StationStatusBadge.vue';
import { OnlineScenery } from '../../../store/typings';
export default defineComponent({ export default defineComponent({
mixins: [styleMixin, dateMixin, routerMixin, imageMixin], mixins: [styleMixin, dateMixin, routerMixin],
props: { props: {
station: { onlineScenery: {
type: Object as () => Station, type: Object as PropType<OnlineScenery>,
default: {}, required: false
}, }
onlineFrom: { },
type: Number, components: { StationStatusBadge }
default: -1,
},
},
components: { StationStatusBadge }
}); });
</script> </script>
@@ -98,4 +92,3 @@ export default defineComponent({
} }
} }
</style> </style>
@@ -20,7 +20,7 @@
<img <img
v-if="station.generalInfo?.SUP" v-if="station.generalInfo?.SUP"
class="icon-info" class="icon-info"
:src="getIcon('SUP')" src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)" alt="SUP (RASP-UZK)"
:title="$t('desc.SUP')" :title="$t('desc.SUP')"
/> />
@@ -28,7 +28,7 @@
<img <img
v-if="station.generalInfo?.signalType" v-if="station.generalInfo?.signalType"
class="icon-info" class="icon-info"
:src="getIcon(station.generalInfo.signalType)" :src="`/images/icon-${station.generalInfo.signalType}.svg`"
:alt="station.generalInfo.signalType" :alt="station.generalInfo.signalType"
:title="$t('desc.signals-type') + $t(`signals.${station.generalInfo.signalType}`)" :title="$t('desc.signals-type') + $t(`signals.${station.generalInfo.signalType}`)"
/> />
@@ -36,7 +36,7 @@
<img <img
v-if="station.generalInfo?.availability == 'nonPublic'" v-if="station.generalInfo?.availability == 'nonPublic'"
class="icon-info" class="icon-info"
:src="getIcon('lock')" src="/images/icon-lock.svg"
alt="Non-public scenery" alt="Non-public scenery"
:title="$t('desc.non-public')" :title="$t('desc.non-public')"
/> />
@@ -44,7 +44,7 @@
<img <img
v-if="station.generalInfo?.availability == 'unavailable'" v-if="station.generalInfo?.availability == 'unavailable'"
class="icon-info" class="icon-info"
:src="getIcon('unavailable')" src="/images/icon-unavailable.svg"
alt="Unavailable scenery" alt="Unavailable scenery"
:title="$t('desc.unavailable')" :title="$t('desc.unavailable')"
/> />
@@ -52,7 +52,7 @@
<img <img
v-if="station.generalInfo?.availability == 'abandoned'" v-if="station.generalInfo?.availability == 'abandoned'"
class="icon-info" class="icon-info"
:src="getIcon('abandoned')" src="/images/icon-abandoned.svg"
alt="Abandoned scenery" alt="Abandoned scenery"
:title="$t('desc.abandoned')" :title="$t('desc.abandoned')"
/> />
@@ -60,7 +60,7 @@
<img <img
v-if="station.generalInfo?.lines" v-if="station.generalInfo?.lines"
class="icon-info" class="icon-info"
:src="getIcon('real')" src="/images/icon-real.svg"
alt="real scenery" alt="real scenery"
:title="`${$t('desc.real')} ${station.generalInfo.lines}`" :title="`${$t('desc.real')} ${station.generalInfo.lines}`"
/> />
@@ -68,7 +68,7 @@
<img <img
v-if="!station.generalInfo" v-if="!station.generalInfo"
class="icon-info" class="icon-info"
:src="getIcon('unknown')" src="/images/icon-unknown.svg"
alt="icon-unknown" alt="icon-unknown"
:title="$t('desc.unknown')" :title="$t('desc.unknown')"
/> />
@@ -76,20 +76,19 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import imageMixin from '../../../mixins/imageMixin';
import stationInfoMixin from '../../../mixins/stationInfoMixin'; import stationInfoMixin from '../../../mixins/stationInfoMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import Station from '../../../scripts/interfaces/Station'; import Station from '../../../scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
mixins: [stationInfoMixin, styleMixin, imageMixin], mixins: [stationInfoMixin, styleMixin],
props: { props: {
station: { station: {
type: Object as () => Station, type: Object as PropType<Station>,
default: {}, required: true
}, }
}, }
}); });
</script> </script>
@@ -118,4 +117,3 @@ export default defineComponent({
} }
} }
</style> </style>
@@ -1,129 +1,142 @@
<template> <template>
<section class="info-routes" v-if="station.generalInfo"> <section class="info-routes" v-if="station.generalInfo">
<div class="routes one-way" v-if="station.generalInfo.routes.oneWay.length > 0"> <div class="routes one-way" v-if="station.generalInfo.routes.oneWay.length > 0">
<b>{{ $t('scenery.one-way-routes') }}</b> <b>{{ $t('scenery.one-way-routes') }}</b>
<ul class="routes-list"> <ul class="routes-list">
<li v-for="route in station.generalInfo.routes.oneWay" @click="setActiveShowLength(route.name)"> <li
<span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"> {{ route.name }}</span> v-for="route in station.generalInfo.routes.oneWay"
<span v-if="route.speed" class="speed"> :key="route.name"
{{ activeShowLength.includes(route.name) ? route.length + 'm' : route.speed }} @click="setActiveShowLength(route.name)"
</span> >
<span v-if="route.SBL" class="sbl">SBL</span> <span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }">
</li> {{ route.name }}</span
</ul> >
</div> <span v-if="route.speed" class="speed">
{{ activeShowLength.includes(route.name) ? route.length + 'm' : route.speed }}
<div class="routes two-way" v-if="station.generalInfo.routes.twoWay.length > 0"> </span>
<b>{{ $t('scenery.two-way-routes') }}</b> <span v-if="route.SBL" class="sbl">SBL</span>
</li>
<ul class="routes-list"> </ul>
<li v-for="(route, i) in station.generalInfo.routes.twoWay" @click="setActiveShowLength(route.name)"> </div>
<span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }">{{ route.name }}</span>
<span v-if="route.speed" class="speed"> <div class="routes two-way" v-if="station.generalInfo.routes.twoWay.length > 0">
{{ activeShowLength.includes(route.name) ? route.length + 'm' : route.speed }} <b>{{ $t('scenery.two-way-routes') }}</b>
</span>
<span v-if="route.SBL" class="sbl">SBL</span> <ul class="routes-list">
</li> <li
</ul> v-for="route in station.generalInfo.routes.twoWay"
</div> :key="route.name"
</section> @click="setActiveShowLength(route.name)"
</template> >
<span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }">{{
<script lang="ts"> route.name
import { defineComponent } from 'vue'; }}</span>
import Station from '../../../scripts/interfaces/Station'; <span v-if="route.speed" class="speed">
{{ activeShowLength.includes(route.name) ? route.length + 'm' : route.speed }}
export default defineComponent({ </span>
props: { <span v-if="route.SBL" class="sbl">SBL</span>
station: { </li>
type: Object as () => Station, </ul>
default: {}, </div>
}, </section>
}, </template>
methods: { <script lang="ts">
setActiveShowLength(name: string) { import { PropType, defineComponent } from 'vue';
if (this.activeShowLength.includes(name)) this.activeShowLength.splice(this.activeShowLength.indexOf(name), 1); import Station from '../../../scripts/interfaces/Station';
else this.activeShowLength.push(name);
}, export default defineComponent({
}, props: {
station: {
data() { type: Object as PropType<Station>,
return { required: true
activeShowLength: [] as string[], }
}; },
},
}); methods: {
</script> setActiveShowLength(name: string) {
if (this.activeShowLength.includes(name))
<style lang="scss" scoped> this.activeShowLength.splice(this.activeShowLength.indexOf(name), 1);
.info-routes { else this.activeShowLength.push(name);
display: flex; }
justify-content: center; },
flex-wrap: wrap;
data() {
margin: 1em 0; return {
} activeShowLength: [] as string[]
};
.routes { }
display: flex; });
justify-content: center; </script>
align-items: center;
flex-wrap: wrap; <style lang="scss" scoped>
.info-routes {
padding: 0.25em; display: flex;
} justify-content: center;
flex-wrap: wrap;
ul.routes-list {
margin: 0.45em 0.25em; margin: 1em 0;
display: flex; }
justify-content: center;
flex-wrap: wrap; .routes {
display: flex;
li { justify-content: center;
margin: 0.5em 0.25em; align-items: center;
cursor: pointer; flex-wrap: wrap;
user-select: none; padding: 0.25em;
-moz-user-select: none; }
-webkit-user-select: none;
ul.routes-list {
span { margin: 0.45em 0.25em;
padding: 0.2em 0.25em; display: flex;
background-color: #007599; justify-content: center;
font-weight: bold; flex-wrap: wrap;
&.no-catenary { li {
background-color: #686868; margin: 0.5em 0.25em;
} cursor: pointer;
&.internal { user-select: none;
text-decoration: underline; -moz-user-select: none;
} -webkit-user-select: none;
&.speed { span {
background-color: #404040; padding: 0.2em 0.25em;
color: #cfcfcf; background-color: #007599;
} font-weight: bold;
&.sbl { &.no-catenary {
color: var(--clr-primary); background-color: #686868;
background-color: #404040; }
}
&.internal {
&:last-child { text-decoration: underline;
border-radius: 0 0.5em 0.5em 0; }
}
&.speed {
&:first-child { background-color: #404040;
border-radius: 0.5em 0 0 0.5em; color: #cfcfcf;
} }
&:only-child { &.sbl {
border-radius: 0.5em; color: var(--clr-primary);
} background-color: #404040;
} }
}
} &:last-child {
</style> border-radius: 0 0.5em 0.5em 0;
}
&:first-child {
border-radius: 0.5em 0 0 0.5em;
}
&:only-child {
border-radius: 0.5em;
}
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More