Compare commits

...

113 Commits

Author SHA1 Message Date
Spythere fcb8357489 Dodano animację aktywnych RJ; poprawki 2022-10-11 22:16:46 +02:00
Spythere ceffd8e675 Bump wersji 2022-10-11 18:36:49 +02:00
Spythere 5aa53521f7 Ulepszono informacje o szlakach i statusach rozkładów scenerii 2022-10-11 18:33:53 +02:00
Spythere d8b559694b Upgrade paczek 2022-10-10 15:57:04 +02:00
Spythere c82ac04a91 Poprawka tłumaczenia 2022-10-08 20:42:06 +02:00
Spythere 284bdcbf2a Aktualizacja vue-tsc 2022-10-08 20:41:54 +02:00
Spythere 7f4df98349 Bump wersji 2022-10-02 00:56:27 +02:00
Spythere aecbcf62df Aktualizacja odnośnika do changelogu 2022-10-02 00:54:05 +02:00
Spythere 2a817365a6 Tłumaczenie statystyk maszynistów 2022-10-02 00:40:10 +02:00
Spythere ecf3a00cab Statystyki maszynistów 2022-10-01 15:55:10 +02:00
Spythere beb2f3c0d4 Tłumaczenie 2022-10-01 13:16:40 +02:00
Spythere a65b09981b Poprawki responsywności 2022-09-30 14:56:49 +02:00
Spythere 4ec544e8a9 Dodano informację o timeoucie SWDRa 2022-09-30 00:00:36 +02:00
Spythere 7e108c5183 Bump wersji 2022-09-29 19:41:55 +02:00
Spythere 72361b157e Tłumaczenie PL 2022-09-29 19:41:26 +02:00
Spythere 1cc4d76e4d Poprawki filtrów 2022-09-29 19:40:15 +02:00
Spythere 846d4d0547 Filtry scenerii 2022-09-29 19:27:54 +02:00
Spythere 751cadd218 Poprawki stylistyczne 2022-09-28 16:36:26 +02:00
Spythere 3b44adff44 Poprawki responsywności 2022-09-27 19:36:34 +02:00
Spythere 29a02dd98f Poprawki responsywności; dodano wyszukiwanie scenerii 2022-09-27 18:58:46 +02:00
Spythere c5e68c4d03 Bump wersji 2022-09-27 14:52:47 +02:00
Spythere 95f7c2a4d9 Poprawki 2022-09-27 14:52:24 +02:00
Spythere 84412822ff Zmiana hostingu API 2022-09-26 00:31:55 +02:00
Spythere 42bb056e66 Poprawki dostępności searchboxów 2022-09-25 23:30:37 +02:00
Spythere 053e9d2b6a Update package-lock 2022-09-25 19:44:56 +02:00
Spythere c729d75541 Poprawki dostępności (c.d.) 2022-09-23 23:01:09 +02:00
Spythere a9b72d0b7a Poprawki dostępności 2022-09-23 22:58:23 +02:00
Spythere 95a027f284 Filtrowanie po nicku autora RJ w dzienniku 2022-09-23 22:39:38 +02:00
Spythere dbba83b28b Dodano id pociągu jako parametr 2022-09-22 19:09:28 +02:00
Spythere 65abe550f5 Poprawki list dzienników 2022-09-22 17:16:10 +02:00
Spythere 531108c25a Wygląd filtrów 2022-09-22 15:08:22 +02:00
Spythere bcf750d451 Wywoływanie filtrów za pomocą klawisza F 2022-09-22 14:57:03 +02:00
Spythere 0a8bfe4c52 Poprawki; usunięto github workflows 2022-09-22 14:15:53 +02:00
Spythere 0f19bc767a Poprawki wyglądu; cleanup kodu 2022-09-22 13:59:19 +02:00
Spythere 8eb0266874 Merge branch 'development' 2022-09-15 12:38:57 +02:00
Spythere ae5b5ff965 Responsywność i ułożenie opcji filtrów 2022-09-15 12:38:36 +02:00
Spythere 3a0c4bc151 Aktualizacja 1.10.4
Aktualizacja Stacjownika do wersji 1.10.4
2022-09-11 14:06:59 +02:00
Spythere 4f5fcb3189 Bump wersji 2022-09-11 13:59:08 +02:00
Spythere 3a2978bbe3 Usprawniono działanie listy dziennika dyżurnych 2022-09-11 02:00:58 +02:00
Spythere a81cc4559b Poprawki w filtrach i ustawieniach dzienników 2022-09-10 22:49:56 +02:00
Spythere 065143c359 JournalTimetables: dodano resetowanie filtrów 2022-09-10 18:22:00 +02:00
Spythere 1661881127 Poprawki w stylach 2022-09-10 18:12:07 +02:00
Spythere 93aa889414 Cleanup kodu 2022-09-10 17:57:43 +02:00
Spythere 2a131ab1fb Poprawiono tłumaczenie 2022-09-10 15:14:36 +02:00
Spythere 387f42985a Poprawiono filtrowanie datą 2022-09-10 15:10:39 +02:00
Spythere 6c83ce90bf Dodano filtrowanie po dacie w opcjach 2022-09-09 00:23:18 +02:00
Spythere 3d519e874f Opcje filtrów: tłumaczenia 2022-09-08 23:24:58 +02:00
Spythere 99cdb3442a Opcje filtrów: animacja i poprawki 2022-09-08 23:15:54 +02:00
Spythere a6c0fe86c8 Poprawki filtrów 2022-09-08 12:47:30 +02:00
Spythere 828421efe0 Filtry aktywnych pociągów 2022-09-08 12:21:27 +02:00
Spythere 21bacb1c95 Filtry dzienników; poprawki stylistyczne 2022-09-07 20:37:58 +02:00
Spythere 0d9a3f4b4f Rozszerzone opcje filtrów dzienników 2022-09-06 12:44:18 +02:00
Spythere 76b8534d63 Poprawki responsywności selectboxów 2022-09-06 00:26:49 +02:00
Spythere 0821fd708e Stylistyka informacji o składzie 2022-09-05 23:44:36 +02:00
Spythere b0a9939446 Cleanup kodu; poprawki funkcjonalności 2022-09-05 23:32:27 +02:00
Spythere 2a64b8f10d Dodatkowe informacje i poprawki wyglądu dziennika RJ 2022-09-04 17:12:44 +02:00
Spythere dc1c457ea4 Fix: wykrywanie scrolla dzienników 2022-09-04 16:46:44 +02:00
Spythere 1f95bc5230 Tłumaczenie i poprawki do wersji 1.10.3 2022-09-04 01:27:12 +02:00
Spythere 5a06920e5b Dodano tłumaczenie; poprawki 2022-09-04 01:25:27 +02:00
Spythere ee0d9e7ed4 Wersja 1.10.3
Wersja 1.10.3
2022-09-04 01:14:24 +02:00
Spythere 30ad3ad4f2 Bump wersji 2022-09-04 01:12:04 +02:00
Spythere c2bd5a8a1b Poprawiono mobilny scroll bar 2022-09-04 01:10:56 +02:00
Spythere 7101d0972d Przywrócono ikonę pociągu mobilnego widoku aktywnych RJ 2022-09-04 01:06:30 +02:00
Spythere 82bbfcdf70 Dokończenie widoku dziennika RJ 2022-09-04 01:04:04 +02:00
Spythere b90ac6c09e Zmiany w wyglądzie i funkcjonalnościach dziennika RJ 2022-09-03 00:11:42 +02:00
Spythere 76d0ff88f1 Zmiany w designie dziennika rozkładów jazdy 2022-09-01 01:56:16 +02:00
Spythere 951afcedeb Bump wersji 2022-08-29 19:12:56 +02:00
Spythere 96de3f0dcc Scroll lock przy otwartym modalu 2022-08-29 19:12:19 +02:00
Spythere 03950eef66 Bump wersji 2022-08-27 20:19:03 +02:00
Spythere 6dd8cb2dad Cleanup c.d. 2022-08-27 14:05:35 +02:00
Spythere aae51d4139 Hotfix 2022-08-27 14:04:02 +02:00
Spythere 9994a541b1 Cleanup 2022-08-27 14:02:42 +02:00
Spythere bc3a603ba2 Poprawiono sortowanie stacji 2022-08-27 13:44:04 +02:00
Spythere 7857377cab Merge branch 'development' 2022-08-09 00:01:40 +02:00
Spythere 0034f43be4 Fix: zła ikonka przy nieznanej scenerii 2022-08-08 23:59:55 +02:00
Spythere c09fc81886 Aktualizacja 1.10.0
Aktualizacja aplikacji do wersji 1.10.0
2022-07-24 00:00:18 +02:00
Spythere 30f72d518d Bump wersji 1.10.0 (prod) 2022-07-23 23:46:17 +02:00
Spythere 9b86e07152 Poprawki responsywności 2022-07-23 23:45:53 +02:00
Spythere 4e0fb5dc01 Fix reaktywności SRJP 2022-07-19 23:32:16 +02:00
Spythere a392991030 PWA: wyłączono funkcję 2022-07-19 22:56:27 +02:00
Spythere ff7ca27fe6 Dodano informację o offline 2022-07-19 14:45:46 +02:00
Spythere 94cd7aaa60 Bump wersji (dev) 2022-07-16 23:21:31 +02:00
Spythere 843289d8d7 Poprawki URL 2022-07-16 17:21:08 +02:00
Spythere 66cae68e19 Update package lock 2022-07-16 17:12:32 +02:00
Spythere b38e50396a package lock, gitignore 2022-07-16 17:10:25 +02:00
Spythere 7888e59117 Poprawki po migracji 2022-07-16 17:07:57 +02:00
Spythere 46e700583d Migracja na Vite 2022-07-16 16:12:31 +02:00
Spythere fc56c38c45 Poprawki stylistyczne modalu aktualizacji; link do najnowszego wydania w stopce 2022-07-16 12:41:24 +02:00
Spythere 9594e2c21a Bump wersji 2022-07-16 00:56:41 +02:00
Spythere a8bab5283b Modal aktualizacji aplikacji 2022-07-16 00:56:25 +02:00
Spythere 1cc799706c Globalny TrainModal; animacja przejścia 2022-07-16 00:27:37 +02:00
Spythere 5ee8f72652 Poprawka semantyki wersji 2022-07-15 15:36:06 +02:00
Spythere 942f883b91 Dodano modal aktualizacji 2022-07-15 15:32:12 +02:00
Spythere 54b47d44e5 PWA: odświeżanie przy wykryciu aktualizacji 2022-07-14 21:25:17 +02:00
Spythere f9aaf21f7a Test PWA 2022-07-14 20:52:53 +02:00
Spythere d79705ca5c Test cachingu scenerii 2022-07-14 20:44:43 +02:00
Spythere 55c64d5f0a Caching service workera 2022-07-14 17:50:17 +02:00
Spythere 4ca1c7bb9c Dodano wsparcie PWA 2022-07-14 14:57:44 +02:00
Spythere abc8fda98e Poprawka kolorów 2022-07-14 14:13:55 +02:00
Spythere aaec23d210 Poprawiono wygląd modalu RJ 2022-07-13 16:34:38 +02:00
Spythere 0af7b68138 Poprawki w widoku RJ 2022-07-12 18:45:18 +02:00
Spythere ae24eaf8e4 Poprawka w info o pozycji pociągu 2022-07-12 13:37:18 +02:00
Spythere f73a07daee Poprawiono wygląd posterunków SBL w widoku RJ 2022-07-12 13:25:16 +02:00
Spythere 89f5bf2e95 Dodano górny pasek z informacjami dla widoku RJ 2022-07-12 13:15:49 +02:00
Spythere 8137c1ff95 Widok pociągów: nowe odznaki statusów 2022-07-12 12:06:27 +02:00
Spythere 4b0d9b887e Historia dr scenerii: dodano odnośniki do dziennika 2022-07-12 11:20:19 +02:00
Spythere 506064cf9a Sceneria: przeniesiono link do wątku forum 2022-07-12 11:14:56 +02:00
Spythere 825e25434a Poprawki tłumaczenia 2022-07-12 11:02:49 +02:00
Spythere 32c601d50a Historia RJ scenerii: dodano odnośniki do dziennika 2022-07-12 10:59:29 +02:00
Spythere b88a96237e Dziennik: dodano filtrowanie po ID rozkładu 2022-07-12 10:54:48 +02:00
Spythere 6c724440d7 Bump wersji: 1.9.9 -> 1.9.10 2022-07-11 18:11:26 +02:00
Spythere 71016e63bb Bump wersji: 1.9.9 -> 1.9.9.1 2022-07-11 18:06:07 +02:00
Spythere fb85352ce3 Modal widoku pociągu 2022-07-11 18:04:07 +02:00
119 changed files with 7179 additions and 25421 deletions
@@ -1,20 +0,0 @@
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on merge
'on':
push:
branches:
- master
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci && npm run build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
channelId: live
projectId: stacjownik-td2
@@ -1,14 +0,0 @@
name: Deploy to Firebase Hosting on PR
'on': pull_request
jobs:
build_and_preview:
if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci && npm run build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
projectId: stacjownik-td2
+1
View File
@@ -1,6 +1,7 @@
.DS_Store .DS_Store
node_modules node_modules
/dist /dist
/dev-dist
# local env files # local env files
.env.local .env.local
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
+48 -54
View File
@@ -1,54 +1,48 @@
<!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" /> <meta name="keywords" content="Stacjownik, TD2, Train Driver 2, stacjownik-td2" />
<meta name="description" content="Automatycznie odświeżana strona wyświetlająca stacje w Train Driver 2!" /> <meta name="description" content="Automatycznie odświeżana strona wyświetlająca stacje w Train Driver 2!" />
<title>Stacjownik</title> <title>Stacjownik</title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" /> <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" /> <meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
<link rel="icon" href="favicon-64.png" sizes="64x64" type="image/png" /> <link rel="icon" href="favicon-64.png" sizes="64x64" type="image/png" />
<link rel="icon" href="favicon-32.png" sizes="32x32" type="image/png" /> <link rel="icon" href="favicon-32.png" sizes="32x32" type="image/png" />
<link rel="icon" href="favicon-62.png" sizes="62x62" type="image/png" /> <link rel="icon" href="favicon-62.png" sizes="62x62" type="image/png" />
<link rel="icon" href="favicon-16.png" sizes="16x16" 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" />
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;700&display=swap" rel="stylesheet" />
<script src="https://www.gstatic.com/firebasejs/8.1.1/firebase-app.js"></script> <script src="https://www.gstatic.com/firebasejs/8.1.1/firebase-app.js"></script>
<script> <script>
const firebaseConfig = { const firebaseConfig = {
apiKey: 'AIzaSyBI36X2-p7vU1flxoJdCEc0noByyTe1mpw', apiKey: 'AIzaSyBI36X2-p7vU1flxoJdCEc0noByyTe1mpw',
authDomain: 'stacjownik-td2.firebaseapp.com', authDomain: 'stacjownik-td2.firebaseapp.com',
databaseURL: 'https://stacjownik-td2.firebaseio.com', databaseURL: 'https://stacjownik-td2.firebaseio.com',
projectId: 'stacjownik-td2', projectId: 'stacjownik-td2',
storageBucket: 'stacjownik-td2.appspot.com', storageBucket: 'stacjownik-td2.appspot.com',
}; };
firebase.initializeApp(firebaseConfig); firebase.initializeApp(firebaseConfig);
</script> </script>
</head> </head>
<body> <body>
<noscript> <div id="app"></div>
<strong <script type="module" src="/src/main.ts"></script>
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please </body>
enable it to continue.</strong </html>
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
-20365
View File
File diff suppressed because it is too large Load Diff
+15 -20
View File
@@ -1,37 +1,32 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.9.9", "version": "1.10.8",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite",
"build": "vue-cli-service build", "build": "vue-tsc --noEmit && vite build",
"deploy-prod": "npm run build && firebase deploy --only hosting:prod", "deploy": "yarn build && firebase deploy --only hosting",
"deploy-dev": "npm run build && firebase deploy --only hosting:dev" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"core-js": "^3.12.1", "core-js": "^3.12.1",
"dotenv": "^8.6.0", "dotenv": "^16.0.3",
"firebase": "^9.8.1", "firebase": "^9.8.1",
"howler": "^2.2.1", "howler": "^2.2.1",
"pinia": "^2.0.14", "pinia": "^2.0.14",
"sass": "^1.53.0",
"socket.io-client": "^4.4.1", "socket.io-client": "^4.4.1",
"vue": "^3.2.34", "vue": "^3.2.37",
"vue-i18n": "^9.1.6", "vue-i18n": "^9.1.6",
"vue-router": "^4.0.0-0", "vue-router": "^4.0.0-0"
"vuex": "^4.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^17.0.35", "@types/node": "^18.8.3",
"@vue/cli-plugin-babel": "^5.0.4", "@vitejs/plugin-vue": "^3.0.0",
"@vue/cli-plugin-router": "^5.0.4", "axios": "^1.1.2",
"@vue/cli-plugin-typescript": "^5.0.4", "typescript": "^4.6.4",
"@vue/cli-plugin-vuex": "^5.0.4", "vite": "^3.0.0",
"@vue/cli-service": "^5.0.4", "vue-tsc": "^1.0.3"
"@vue/compiler-sfc": "^3.1.0",
"axios": "^0.21.1",
"sass": "^1.32.13",
"sass-loader": "^8.0.2",
"typescript": "^4.7.3"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 215 B

+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow:
+6 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "", "name": "Stacjownik TD2",
"short_name": "", "short_name": "Stacjownik",
"icons": [ "icons": [
{ {
"src": "/android-chrome-192x192.png", "src": "/android-chrome-192x192.png",
@@ -13,7 +13,8 @@
"type": "image/png" "type": "image/png"
} }
], ],
"theme_color": "#ffffff", "theme_color": "#ffc014",
"background_color": "#ffffff", "background_color": "#4d4d4d",
"display": "standalone" "display": "standalone",
"start_url": "."
} }
+17 -161
View File
@@ -17,6 +17,19 @@
} }
} }
.modal-anim {
&-enter-active,
&-leave-active {
transition: all $animDuration $animType;
}
&-enter-from,
&-leave-to {
transform: translateY(-25%);
opacity: 0;
}
}
.route { .route {
margin: 0 0.2em; margin: 0 0.2em;
@@ -27,12 +40,12 @@
} }
// APP // APP
.app { #app {
color: white; color: white;
font-size: 1rem; font-size: 1rem;
@include smallScreen() { @include smallScreen() {
font-size: calc(0.4rem + 1.4vw); font-size: calc(0.55rem + 1vw);
} }
} }
@@ -40,8 +53,8 @@
.app_container { .app_container {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
height: 100vh;
min-height: 800px; min-height: 100vh;
header { header {
flex: 0 0 auto; flex: 0 0 auto;
@@ -68,163 +81,6 @@
border-radius: 0 0 1em 1em; border-radius: 0 0 1em 1em;
} }
// Error icon
.wip-alert {
padding: 0 0.5em;
text-align: center;
}
.icon-error {
width: 13em;
margin: 0.5em 0;
}
// HEADER
.app_header {
display: flex;
justify-content: center;
position: relative;
background-color: $primaryCol;
}
.header {
&_body {
max-width: 21em;
}
&_container {
display: flex;
justify-content: center;
position: relative;
width: 1350px;
padding: 0.5em 0.3em 0 0.3em;
border-radius: 0 0 1em 1em;
}
&_brand {
img {
width: 100%;
}
}
&_info {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
max-width: 100%;
font-size: 1.2em;
}
&_links {
display: flex;
justify-content: center;
border-radius: 0.7em;
font-size: 1.25em;
padding: 0.5em;
}
&_icons {
position: absolute;
right: 0;
top: 0;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
padding: 0.5em 0.5em;
@include smallScreen() {
right: auto;
left: 0.75em;
padding: 0;
align-items: center;
}
}
}
// ICONS
.icons {
position: relative;
&-top {
img {
width: 2.5em;
cursor: pointer;
}
margin-bottom: 0.5em;
}
&-bottom {
display: flex;
a {
margin-left: 0.6em;
user-select: none;
}
img {
width: 1.9em;
}
@include smallScreen() {
flex-direction: column;
a {
margin: 0.25em 0;
}
}
}
}
// COUNTER
.info_counter {
display: flex;
justify-content: center;
align-items: center;
span {
margin: 0 0.15em;
}
img {
width: 1.35em;
}
}
// 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;
}
.arrow {
padding: 0;
}
}
// FOOTER // FOOTER
footer.app_footer { footer.app_footer {
max-width: 100%; max-width: 100%;
+57 -138
View File
@@ -1,106 +1,58 @@
<template> <template>
<div class="app"> <div class="app_container">
<div class="app_container"> <transition name="modal-anim">
<!-- <div class="wip-alert"> <keep-alive>
<img class="icon-error" :src="iconError" alt="error" /> <TrainModal v-if="store.chosenModalTrainId" />
<h2>Stacjownik tymczasowo nieaktywny!</h2> </keep-alive>
<p>Absolutny zakaz wjazdu!</p> </transition>
</div> -->
<header class="app_header">
<div class="header_container">
<div class="header_icons">
<span class="icons-top">
<img :src="icons.pl" alt="icon-pl" @click="changeLang('en')" v-if="currentLang == 'pl'" />
<img :src="icons.en" alt="icon-en" @click="changeLang('pl')" v-else />
</span>
<span class="icons-bottom">
<a href="https://www.paypal.com/paypalme/spythere" target="_blank">
<img :src="icons.dollar" alt="icon paypal" />
</a>
<a href="https://discord.gg/x2mpNN3svk" target="_blank"> <AppHeader :current-lang="currentLang" @change-lang="changeLang" />
<img :src="icons.discord" alt="icon discord" />
</a>
</span>
</div>
<div class="header_body"> <main class="app_main">
<status-indicator /> <router-view v-slot="{ Component }">
<span class="header_brand"> <keep-alive>
<img :src="brand_logo" alt="Stacjownik" /> <component :is="Component" :key="$route.name" />
</span> </keep-alive>
</router-view>
</main>
<span class="header_info"> <footer class="app_footer">
<Clock /> &copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} | <a :href="releaseURL" target="_blank">v{{ VERSION }}</a>
<div class="info_counter"> <div style="display: none">&int; ukryta taktyczna całka do programowania w HTMLu</div>
<img src="@/assets/icon-dispatcher.svg" alt="icon dispatcher" /> </footer>
<span class="text--primary">{{ onlineDispatchers.length }}</span>
<span class="text--grayed"> / </span>
<span class="text--primary">{{ trainList.length }}</span>
<img src="@/assets/icon-train.svg" alt="icon train" />
</div>
<span class="info_region">
<SelectBox :itemList="computedRegions" :defaultItemIndex="0" @selected="changeRegion" />
</span>
</span>
<span class="header_links">
<router-link class="route" active-class="route-active" to="/" exact>
{{ $t('app.sceneries') }}
</router-link>
/
<router-link class="route" active-class="route-active" to="/trains">{{ $t('app.trains') }}</router-link>
/
<router-link class="route" active-class="route-active" to="/journal">
{{ $t('app.journal') }}
</router-link>
</span>
</div>
</div>
</header>
<main class="app_main">
<router-view v-slot="{ Component }">
<!-- <transition name="view-anim" mode="out-in"> -->
<keep-alive>
<component :is="Component" :key="$route.path" />
</keep-alive>
</router-view>
</main>
<footer class="app_footer">
&copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} | v{{ VERSION }}
<div style="display: none">&int; ukryta taktyczna całka do programowania w HTMLu</div>
</footer>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, provide, ref } from 'vue'; import { computed, defineComponent, provide, ref, watch } from 'vue';
import Clock from '@/components/App/Clock.vue'; import Clock from './components/App/Clock.vue';
import StorageManager from '@/scripts/managers/storageManager';
import packageInfo from '.././package.json'; import packageInfo from '.././package.json';
import options from '@/data/options.json';
import StatusIndicator from '@/components/App/StatusIndicator.vue'; import StatusIndicator from './components/App/StatusIndicator.vue';
import SelectBox from '@/components/Global/SelectBox.vue'; import SelectBox from './components/Global/SelectBox.vue';
import { useStore } from './store/store'; import { useStore } from './store/store';
import TrainModal from './components/Global/TrainModal.vue';
import StorageManager from './scripts/managers/storageManager';
import imageMixin from './mixins/imageMixin';
import AppHeader from './components/App/AppHeader.vue';
import axios from 'axios';
export default defineComponent({ export default defineComponent({
components: { components: {
Clock, Clock,
StatusIndicator, StatusIndicator,
SelectBox, SelectBox,
TrainModal,
AppHeader,
}, },
mixins: [imageMixin],
setup() { setup() {
const store = useStore(); const store = useStore();
store.connectToAPI(); store.connectToAPI();
@@ -120,45 +72,11 @@ export default defineComponent({
}; };
}, },
computed: {
trainList() {
return this.store.trainList.filter((train) => train.online);
},
computedRegions() {
return this.options.regions.map((region) => {
const regionStationCount =
this.store.apiData.stations?.filter((station) => station.region == region.id && station.isOnline).length || 0;
const regionTrainCount = this.store.apiData.trains?.filter((train) => train.region == region.id && train.online).length || 0;
return {
id: region.id,
value: `${region.value} <div class='text--grayed'>${regionStationCount} / ${regionTrainCount}</div>`,
selectedValue: region.value,
};
});
},
},
data: () => ({ data: () => ({
VERSION: packageInfo.version, VERSION: packageInfo.version,
updateModalVisible: false,
hasReleaseNotes: false,
options,
currentLang: 'pl', currentLang: 'pl',
releaseURL: '',
brand_logo: require('@/assets/stacjownik-header-logo.svg'),
icons: {
en: require('@/assets/icon-en.jpg'),
pl: require('@/assets/icon-pl.svg'),
error: require('@/assets/icon-error.svg'),
dollar: require('@/assets/icon-dollar.svg'),
dispatcher: require('@/assets/icon-dispatcher.svg'),
train: require('@/assets/icon-train.svg'),
discord: require('@/assets/icon-discord.png'),
},
}), }),
created() { created() {
@@ -166,27 +84,22 @@ export default defineComponent({
}, },
async mounted() { async mounted() {
if (StorageManager.getStringValue('version') != this.VERSION) { this.setReleaseURL();
StorageManager.setStringValue('version', this.VERSION);
if (this.hasReleaseNotes) StorageManager.setBooleanValue('version_notes_read', false); watch(
} () => this.store.blockScroll,
(value) => {
if (value) {
document.body.classList.add('no-scroll');
return;
}
this.updateModalVisible = this.hasReleaseNotes && !StorageManager.getBooleanValue('version_notes_read'); document.body.classList.remove('no-scroll');
}
this.updateToNewestVersion(); );
}, },
methods: { methods: {
toggleUpdateModal() {
this.updateModalVisible = !this.updateModalVisible;
StorageManager.setBooleanValue('version_notes_read', true);
},
changeRegion(region: { id: string; value: string }) {
this.store.changeRegion(region);
},
changeLang(lang: string) { changeLang(lang: string) {
this.$i18n.locale = lang; this.$i18n.locale = lang;
this.currentLang = lang; this.currentLang = lang;
@@ -194,12 +107,18 @@ export default defineComponent({
StorageManager.setStringValue('lang', lang); StorageManager.setStringValue('lang', lang);
}, },
updateToNewestVersion() { async setReleaseURL() {
if (!StorageManager.isRegistered('unavailable-status')) { try {
StorageManager.setBooleanValue('unavailable-status', true); const releaseData = await (
StorageManager.setBooleanValue('ending-status', true); await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
StorageManager.setBooleanValue('no-space-status', true); ).data;
StorageManager.setBooleanValue('afk-status', true);
if (!releaseData) return;
this.releaseURL = releaseData.html_url;
} catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
return;
} }
}, },
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" ?><svg enable-background="new 0 0 32 32" id="Glyph" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M27.414,24.586l-5.077-5.077C23.386,17.928,24,16.035,24,14c0-5.514-4.486-10-10-10S4,8.486,4,14 s4.486,10,10,10c2.035,0,3.928-0.614,5.509-1.663l5.077,5.077c0.78,0.781,2.048,0.781,2.828,0 C28.195,26.633,28.195,25.367,27.414,24.586z M7,14c0-3.86,3.14-7,7-7s7,3.14,7,7s-3.14,7-7,7S7,17.86,7,14z" id="XMLID_223_" fill="white" /></svg>

After

Width:  |  Height:  |  Size: 546 B

+266
View File
@@ -0,0 +1,266 @@
<template>
<header class="app_header">
<div class="header_container">
<div class="header_icons">
<span class="icons-top">
<img :src="getIcon('pl')" alt="icon-pl" @click="changeLang('en')" v-if="currentLang == 'pl'" />
<img :src="getIcon('en', 'jpg')" alt="icon-en" @click="changeLang('pl')" v-else />
</span>
<span class="icons-bottom">
<a href="https://www.paypal.com/paypalme/spythere" target="_blank">
<img :src="getIcon('dollar')" alt="icon paypal" />
</a>
<a href="https://discord.gg/x2mpNN3svk" target="_blank">
<img :src="getIcon('discord', 'png')" alt="icon discord" />
</a>
</span>
</div>
<div class="header_body">
<StatusIndicator />
<span class="header_brand">
<img :src="getImage('stacjownik-header-logo.svg')" alt="Stacjownik" />
</span>
<span class="header_info">
<Clock />
<div class="info_counter">
<img :src="getIcon('dispatcher')" alt="icon dispatcher" />
<span class="text--primary">{{ onlineDispatchersCount }}</span>
<span class="text--grayed"> / </span>
<span class="text--primary">{{ onlineTrainsCount }}</span>
<img :src="getIcon('train')" alt="icon train" />
</div>
<span class="info_region">
<SelectBox :itemList="computedRegions" :defaultItemIndex="0" @selected="changeRegion" />
</span>
</span>
<span class="header_links">
<router-link class="route" active-class="route-active" to="/" exact>
{{ $t('app.sceneries') }}
</router-link>
/
<router-link class="route" active-class="route-active" to="/trains">{{ $t('app.trains') }}</router-link>
/
<router-link class="route" active-class="route-active" to="/journal/timetables">
{{ $t('app.journal') }}
</router-link>
</span>
</div>
</div>
</header>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../../store/store';
import options from '../../data/options.json';
import imageMixin from '../../mixins/imageMixin';
import SelectBox from '../Global/SelectBox.vue';
import StatusIndicator from './StatusIndicator.vue';
import Clock from './Clock.vue';
export default defineComponent({
emits: ["changeLang"],
mixins: [imageMixin],
props: {
currentLang: {
type: String,
required: true,
},
},
setup() {
return {
store: useStore(),
};
},
methods: {
changeRegion(region: {
id: string;
value: string;
}) {
this.store.changeRegion(region);
},
changeLang(lang: string) {
this.$emit("changeLang", lang);
},
},
computed: {
onlineTrainsCount() {
return this.store.trainList.filter((train) => train.online).length;
},
onlineDispatchersCount() {
return this.store.stationList.filter((station) => station.onlineInfo && station.onlineInfo.region == this.store.region.id).length;
},
computedRegions() {
return options.regions.map((region) => {
const regionStationCount = this.store.apiData.stations?.filter((station) => station.region == region.id && station.isOnline).length || 0;
const regionTrainCount = this.store.apiData.trains?.filter((train) => train.region == region.id && train.online).length || 0;
return {
id: region.id,
value: `${region.value} <div class='text--grayed'>${regionStationCount} / ${regionTrainCount}</div>`,
selectedValue: region.value,
};
});
},
},
components: { SelectBox, StatusIndicator, Clock }
});
</script>
<style lang="scss" scoped>
@import '../../styles/variables.scss';
@import '../../styles/responsive.scss';
// HEADER
.app_header {
display: flex;
justify-content: center;
position: relative;
background-color: $primaryCol;
}
.header {
&_body {
max-width: 21em;
@include smallScreen {
max-width: 18em;
}
}
&_container {
display: flex;
justify-content: center;
position: relative;
width: 1350px;
padding: 0.5em 0.3em 0 0.3em;
border-radius: 0 0 1em 1em;
}
&_brand {
display: flex;
img {
width: 100%;
margin: 0 auto;
}
}
&_info {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
max-width: 100%;
font-size: 1.2em;
}
&_links {
display: flex;
justify-content: center;
border-radius: 0.7em;
font-size: 1.25em;
padding: 0.5em;
}
&_icons {
position: absolute;
right: 0;
top: 0;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
padding: 0.5em 0.5em;
@include smallScreen() {
right: auto;
left: 0.75em;
padding: 0;
align-items: center;
}
}
}
// ICONS
.icons {
position: relative;
&-top {
img {
width: 2.5em;
cursor: pointer;
}
margin-bottom: 0.5em;
}
&-bottom {
display: flex;
a {
margin-left: 0.6em;
user-select: none;
}
img {
width: 1.9em;
}
@include smallScreen() {
flex-direction: column;
a {
margin: 0.25em 0;
}
}
}
}
// COUNTER
.info_counter {
display: flex;
justify-content: center;
align-items: center;
span {
margin: 0 0.15em;
}
img {
width: 1.35em;
}
}
// 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>
+1 -1
View File
@@ -3,7 +3,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "@vue/runtime-core"; import { defineComponent } from "vue";
export default defineComponent({ export default defineComponent({
props: ["message"], props: ["message"],
+5 -8
View File
@@ -161,18 +161,15 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { DataStatus } from '@/scripts/enums/DataStatus';
import { StoreData } from '@/scripts/interfaces/StoreData'; import { defineComponent } from 'vue';
import { useStore } from '@/store/store'; import { DataStatus } from '../../scripts/enums/DataStatus';
import { StoreState } from '@/store/storeTypes'; import { useStore } from '../../store/store';
import { computed, defineComponent, watch } from 'vue'; import { StoreState } from '../../store/storeTypes';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
icons: {
statusIndicator: require('@/assets/signal-status-indicator.svg'),
},
tooltipActive: false, tooltipActive: false,
indicator: { indicator: {
status: DataStatus.Loading, status: DataStatus.Loading,
+168
View File
@@ -0,0 +1,168 @@
<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>
-47
View File
@@ -1,47 +0,0 @@
<template>
<section
class="updates card"
v-if="cardOpen"
>
<h2>Ostatnie aktualizacje w Stacjowniku</h2>
<p>Tutaj będą pojawiać się informacje o kolejnych nowościach na stronie :)</p>
<ul>
<li
v-for="(update, i) in updates"
:key="i"
>
<div>{{update.date}}</div>
<div>
<span
v-for="(line, l) in content"
:key="l"
>{{line}}</span>
</div>
</li>
</ul>
</section>
</template>
<script>
import { defineComponent } from "@vue/runtime-core";
export default defineComponent({
data() {
return {
updates: {
date: "08/08/20",
content: [
"Lekko odświeżono wygląd strony, dodano nowy widok z pociągami online",
"Dodano animacje zmieniania widoków (zakładek)",
"Dodano przycisk zamykający kartę z filtrami",
],
},
};
},
});
</script>
<style lang="scss" scoped>
</style>
+1 -42
View File
@@ -1,5 +1,5 @@
<template> <template>
<button class="action-btn"> <button class="action-btn btn--filled">
<div class="button_content"> <div class="button_content">
<slot></slot> <slot></slot>
</div> </div>
@@ -16,47 +16,6 @@ export default defineComponent({});
@import "../../styles/variables"; @import "../../styles/variables";
@import "../../styles/responsive"; @import "../../styles/responsive";
.action-btn {
background: #333;
border: none;
color: #bdbdbd;
font-size: 1em;
font-weight: 500;
padding: 0.35em 0.65em;
cursor: pointer;
transition: all 0.3s;
&.outlined {
border: 1px solid white;
}
img {
width: 1.25em;
vertical-align: middle;
margin-right: 0.35em;
}
p {
font-size: 1em;
overflow: hidden;
}
&.open {
color: $accentCol;
border: none;
}
&:hover,
&:focus {
color: $accentCol;
background: #5c5c5c;
}
}
.button_content { .button_content {
display: flex; display: flex;
justify-content: center; justify-content: center;
+5 -5
View File
@@ -9,7 +9,7 @@
<img <img
class="search-exit" class="search-exit"
:src="exitIcon" :src="getIcon('exit')"
alt="exit-icon" alt="exit-icon"
@click="clearValue" @click="clearValue"
/> />
@@ -18,11 +18,11 @@
<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({
data: () => ({ mixins: [imageMixin],
exitIcon: require("@/assets/icon-exit.svg"),
}),
emits: ["update:searchedValue", "clearValue"], emits: ["update:searchedValue", "clearValue"],
props: { props: {
searchedValue: { searchedValue: {
@@ -59,7 +59,7 @@ export default defineComponent({
emit("clearValue"); emit("clearValue");
}; };
const updateValue = (e) => { 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);
}; };
+21 -18
View File
@@ -1,8 +1,7 @@
<template> <template>
<div class="select-box" > <div class="select-box">
<div class="select-box_content"> <div class="select-box_content">
<button class="selected" @click="toggleBox"> <button class="selected" @click="toggleBox">
<span class="text--primary">{{ prefix }}</span>
<span>{{ computedSelectedItem.selectedValue || computedSelectedItem.value }}</span> <span>{{ computedSelectedItem.selectedValue || computedSelectedItem.value }}</span>
</button> </button>
@@ -24,13 +23,14 @@
</div> </div>
<div class="arrow"> <div class="arrow">
<img :src="listOpen ? ascIcon : descIcon" alt="arrow-icon" /> <img :src="listOpen ? getIcon('arrow-asc') : getIcon('arrow-desc')" alt="arrow-icon" />
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, Ref, ref } from '@vue/runtime-core'; import { defineComponent, Ref, ref, computed } from 'vue';
import imageMixin from '../../mixins/imageMixin';
interface Item { interface Item {
id: string; id: string;
@@ -40,6 +40,7 @@ interface Item {
export default defineComponent({ export default defineComponent({
emits: ['selected'], emits: ['selected'],
mixins: [imageMixin],
props: { props: {
itemList: { itemList: {
@@ -58,11 +59,6 @@ export default defineComponent({
}, },
}, },
data: () => ({
ascIcon: require('@/assets/icon-arrow-asc.svg'),
descIcon: require('@/assets/icon-arrow-desc.svg'),
}),
setup(props) { setup(props) {
let listRef: Ref<Element | null> = ref(null); let listRef: Ref<Element | null> = ref(null);
let buttonRef: Ref<HTMLButtonElement | null> = ref(null); let buttonRef: Ref<HTMLButtonElement | null> = ref(null);
@@ -134,13 +130,14 @@ export default defineComponent({
.select-box { .select-box {
position: relative; position: relative;
width: auto;
} }
.arrow { .arrow {
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 0; right: 0;
padding: 0.5em; padding: 0;
img { img {
vertical-align: middle; vertical-align: middle;
@@ -153,13 +150,17 @@ export default defineComponent({
} }
button.selected { button.selected {
background: #333; background-color: transparent;
color: white; color: paleturquoise;
font-size: 1em; font-size: 1em;
font-weight: bold;
padding: 0.1em 0.5em;
margin-right: 2em;
display: flex;
padding: 0.35em 0.5em;
margin-right: 1.4em;
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
@@ -170,7 +171,7 @@ button.selected {
text-align: left; text-align: left;
&:focus { &:focus {
background: #555; background-color: #262626;
} }
} }
@@ -191,8 +192,9 @@ ul.options {
height: auto; height: auto;
z-index: 100; z-index: 100;
width: 100%; width: 100%;
font-size: 0.9em;
} }
li.option { li.option {
@@ -206,6 +208,7 @@ li.option {
appearance: none; appearance: none;
border: none; border: none;
outline: none; outline: none;
background: none;
&:focus + span { &:focus + span {
color: $accentCol; color: $accentCol;
@@ -221,11 +224,11 @@ li.option {
position: relative; position: relative;
display: inline-block; display: inline-block;
background-color: hsla(0, 0%, 15%, 0.95); background-color: #262626f2;
&:hover, &:hover,
&:focus { &:focus {
background-color: hsla(0, 0%, 20%, 0.95); background-color: #333333f2;
} }
padding: 0.5em 0; padding: 0.5em 0;
+2 -2
View File
@@ -47,9 +47,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import dateMixin from '@/mixins/dateMixin';
import TrainStop from '@/scripts/interfaces/TrainStop';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import TrainStop from '../../scripts/interfaces/TrainStop';
export default defineComponent({ export default defineComponent({
mixins: [dateMixin], mixins: [dateMixin],
+152
View File
@@ -0,0 +1,152 @@
<template>
<div class="train-modal" v-if="chosenTrain" @keydown.esc="closeModal">
<div class="modal_background" @click="closeModal"></div>
<div class="modal_content" ref="content" tabindex="0">
<button class="btn exit" @click="closeModal">
<img :src="getIcon('exit')" alt="close card" />
</button>
<TrainInfo :train="chosenTrain" :extended="false" ref="trainInfo" />
<TrainSchedule :train="chosenTrain" tabindex="0" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import trainInfoMixin from '../../mixins/trainInfoMixin';
import { useStore } from '../../store/store';
import TrainInfo from '../TrainsView/TrainInfo.vue';
import TrainSchedule from '../TrainsView/TrainSchedule.vue';
export default defineComponent({
components: { TrainInfo, TrainSchedule },
mixins: [trainInfoMixin, modalTrainMixin, imageMixin],
data() {
return {
isTopBarVisible: false,
};
},
setup() {
const store = useStore();
return {
store,
};
},
activated() {
const contentEl = this.$refs['content'] as HTMLElement;
this.$nextTick(() => {
contentEl.focus();
});
},
methods: {
handleContentScroll(e: Event) {
const trainInfoCompHeight: number = (this.$refs['trainInfo'] as any).$el.getBoundingClientRect().height;
const posTop = (e.target as HTMLElement).scrollTop;
this.isTopBarVisible = posTop > trainInfoCompHeight;
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/card.scss';
.top-info-bar-anim {
&-enter-active,
&-leave-active {
transition: all 150ms ease-in-out;
}
&-enter-from,
&-leave-to {
transform: translate(-50%, -50%) scale(0.8);
opacity: 0;
}
}
.exit {
position: absolute;
top: 0;
right: 0;
margin: 0.5em 1em;
padding: 0.25em;
z-index: 201;
img {
width: 1.5rem;
vertical-align: middle;
}
}
.train-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
color: white;
z-index: 200;
display: flex;
justify-content: center;
text-align: left;
}
.modal_background {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.55);
}
.modal_content {
position: relative;
overflow-y: scroll;
margin-top: 1em;
width: 95vw;
max-height: 96vh;
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e;
}
@include midScreen {
.exit {
margin: 0.5em;
img {
width: 1.75rem;
}
}
}
@include smallScreen {
.modal_content {
max-height: 85vh;
}
}
</style>
+7 -36
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="stats_container" v-click-outside="() => (cardVisible = false)"> <div class="stats_container" v-click-outside="() => (cardVisible = false)">
<button class="stats_button btn btn--option" @click="toggleCard"> <button class="stats_button" @click="toggleCard">
Statystyki dyżurnego {{ store.dispatcherStatsName }} Statystyki dyżurnego {{ store.dispatcherStatsName }}
</button> </button>
@@ -14,6 +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>
@@ -48,12 +49,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { DispatcherStatsAPIData } from '@/scripts/interfaces/api/DispatcherStatsAPIData';
import { TimetableHistory } from '@/scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '@/scripts/utils/apiURLs';
import { useStore } from '@/store/store';
import axios from 'axios'; import axios from 'axios';
import { computed, defineComponent } from 'vue'; import { computed, defineComponent } from 'vue';
import { DispatcherStatsAPIData } from '../../scripts/interfaces/api/DispatcherStatsAPIData';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
export default defineComponent({ export default defineComponent({
@@ -161,42 +163,11 @@ h3 {
text-align: center; text-align: center;
} }
.info-stats {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin-top: 1em;
}
.last-timetables { .last-timetables {
overflow-y: auto; overflow-y: auto;
} }
.stat-badge {
margin-right: 0.5em;
padding-bottom: 1em;
span {
padding: 0.25em 0.3em;
}
span:first-child {
background-color: #4d4d4d;
}
span:last-child {
background-color: $accentCol;
color: black;
font-weight: bold;
}
}
@include smallScreen() {
.stats_card {
text-align: center;
left: 50%;
transform: translateX(-50%);
border-radius: 0 0 1em 1em;
}
}
</style> </style>
+94 -140
View File
@@ -1,140 +1,94 @@
<template> <template>
<div class="card-dimmer" @click="closeCard"></div> <div class="journal-stats" v-if="store.driverStatsData?._sum.routeDistance != null">
<h1>
<div class="stats-card card"> {{ $t('journal.stats-title') }} <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
<div> </h1>
<h2 class="card-title">
STATYSTYKI MASZYNISTY <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span> <div class="info-stats">
</h2> <span class="stat-badge">
<span>{{ $t('journal.stats-timetables') }}</span>
<div class="loading" v-if="!store.driverStatsData">Ładowanie...</div> <span>{{ store.driverStatsData._count.fulfilled }} / {{ store.driverStatsData._count._all }}</span>
</span>
<div v-else>
<div class="info-stats" v-if="store.driverStatsData._sum.routeDistance != null"> <span class="stat-badge">
<span class="stat-badge"> <span>{{ $t('journal.stats-longest-timetable') }}</span>
<span>PRZEBYTO</span> <span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
<span>{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km</span> </span>
</span>
<span class="stat-badge"> <span class="stat-badge">
<span>PORZUCONO</span> <span>{{ $t('journal.stats-avg-timetable') }}</span>
<span> <span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
{{ (store.driverStatsData._sum.routeDistance - store.driverStatsData._sum.currentDistance).toFixed(2) }}km </span>
</span>
</span> <span class="stat-badge">
<span>{{ $t('journal.stats-distance') }}</span>
<span class="stat-badge"> <span>
<span>WYPEŁNIONO</span> {{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
<span>{{ store.driverStatsData._count.fulfilled }} RJ</span> {{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
</span> </span>
</span>
<span class="stat-badge">
<span>PORZUCONO</span> <span class="stat-badge">
<span>{{ store.driverStatsData._count._all - store.driverStatsData._count.fulfilled }} RJ</span> <span>{{ $t('journal.stats-stations') }}</span>
</span> <span>
{{ store.driverStatsData._sum.confirmedStopsCount }} /
<span class="stat-badge"> {{ store.driverStatsData._sum.allStopsCount }}
<span>ZATWIERDZONO</span> </span>
<span>{{ store.driverStatsData._sum.confirmedStopsCount }} stacji</span> </span>
</span> </div>
</div>
<span class="stat-badge"> </template>
<span>PORZUCONO</span>
<span> <script lang="ts">
{{ store.driverStatsData._sum.allStopsCount - store.driverStatsData._sum.confirmedStopsCount }} import axios from 'axios';
stacji import { computed, defineComponent, ref } from 'vue';
</span> import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
</span> import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
</div> import { URLs } from '../../scripts/utils/apiURLs';
</div> import { useStore } from '../../store/store';
</div>
</div> export default defineComponent({
</template> emits: ['closeCard'],
<script lang="ts"> setup() {
import { DriverStatsAPIData } from '@/scripts/interfaces/api/DriverStatsAPIData'; const store = useStore();
import { TimetableHistory } from '@/scripts/interfaces/api/TimetablesAPIData'; return {
import { URLs } from '@/scripts/utils/apiURLs'; store,
import { useStore } from '@/store/store'; driverStatsName: computed(() => store.driverStatsName),
import axios from 'axios'; };
import { defineComponent } from 'vue'; },
export default defineComponent({ data() {
emits: ['closeCard'], return {
test: Math.random(),
setup() { lastDispatcherName: '',
const store = useStore();
return { lastTimetables: [] as TimetableHistory[],
store, };
}; },
},
watch: {
data() { driverStatsName(value: string) {
return { this.fetchDispatcherStats();
test: Math.random(), },
lastDispatcherName: '', },
lastTimetables: [] as TimetableHistory[], methods: {
}; async fetchDispatcherStats() {
}, this.store.driverStatsData = undefined;
activated() { if (!this.store.driverStatsName) return;
this.fetchDispatcherStats();
}, const statsData: DriverStatsAPIData = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`)
methods: { ).data;
async fetchDispatcherStats() {
this.store.driverStatsData = undefined; this.store.driverStatsData = statsData;
},
const statsData: DriverStatsAPIData = await ( },
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`) });
).data; </script>
const recentTimetablesData: TimetableHistory[] = await ( <style lang="scss" scoped>
await axios.get(`${URLs.stacjownikAPI}/api/getTimetables?driverName=${this.store.driverStatsName}`) @import '../../styles/JournalStats.scss';
).data; </style>
this.store.driverStatsData = statsData;
this.lastTimetables = recentTimetablesData || [];
},
closeCard() {
this.$emit('closeCard');
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
.timetable-row {
display: grid;
grid-template-columns: 4fr 1fr 1fr 2fr 2fr;
gap: 0.2em;
margin: 0.5em 0;
text-align: center;
span {
min-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
background-color: #4d4d4d;
padding: 0.5em 0.2em;
}
@include smallScreen() {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
span {
padding: 0.2em 0.3em;
}
grid-template-columns: 1fr;
background-color: #4d4d4d;
}
}
</style>
+264 -425
View File
@@ -1,425 +1,264 @@
<template> <template>
<section class="journal-timetables"> <section class="journal-timetables">
<div class="journal-wrapper"> <div class="journal_wrapper">
<div class="journal_top-bar"> <JournalOptions
<JournalOptions @on-search-confirm="searchHistory"
@on-filter-change="search" @on-options-reset="resetOptions"
@on-input-change="search" :sorter-option-ids="['timestampFrom', 'duration']"
@on-sorter-change="search" :data-status="dataStatus"
:sorter-option-ids="['timestampFrom', 'duration']" />
/>
<div class="list_wrapper" @scroll="handleScroll">
<!-- <DispatcherStats /> --> <!-- <transition name="warning" mode="out-in"> -->
</div> <!-- <div :key="dataStatus"> -->
<Loading v-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
<div class="journal-list">
<div class="list-wrapper" ref="scrollElement"> <div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
<transition name="warning" mode="out-in"> {{ $t('app.error') }}
<div :key="historyDataStatus.status"> </div>
<Loading v-if="isDataLoading || isDataInit" />
<div class="journal_warning" v-else-if="historyList.length == 0">
<div v-else-if="isDataError" class="journal_warning error"> {{ $t('app.no-result') }}
{{ $t('app.error') }} </div>
</div>
<div v-else>
<div class="journal_warning" v-else-if="historyList.length == 0"> <JournalDispatchersList :dispatcherHistory="computedHistoryList" />
{{ $t('app.no-result') }}
</div> <button
class="btn btn--option btn--load-data"
<ul v-else> v-if="!scrollNoMoreData && scrollDataLoaded && computedHistoryList.length > 15"
<transition-group name="journal-list-anim"> @click="addHistoryData"
<li v-for="(doc, i) in computedHistoryList" :key="doc.id"> >
<div class="journal_day" v-if="isAnotherDay(i - 1, i)"> {{ $t('journal.load-data') }}
<span>{{ new Date(doc.timestampFrom).toLocaleDateString('pl-PL') }}</span> </button>
</div> </div>
<!-- </div>
<div </transition> -->
class="journal_item"
:class="{ online: doc.isOnline }" <div class="journal_warning" v-if="scrollNoMoreData">
@click="navigateToScenery(doc.stationName, doc.isOnline)" {{ $t('journal.no-further-data') }}
@keydown.enter="navigateToScenery(doc.stationName, doc.isOnline)" </div>
tabindex="0"
> <div class="journal_warning" v-else-if="!scrollDataLoaded">
<span> {{ $t('journal.loading-further-data') }}
<b class="text--primary">{{ doc.dispatcherName }}</b> &bull; <b>{{ doc.stationName }}</b> </div>
<span class="text--grayed">&nbsp;#{{ doc.stationHash }}&nbsp;</span> </div>
<span class="region-badge" :class="doc.region">PL1</span> </div>
</span> </section>
<span> </template>
<span :data-status="doc.isOnline">
{{ doc.isOnline ? $t('journal.online-since') : 'OFFLINE' }}&nbsp; <script lang="ts">
</span> import { defineComponent, provide, reactive, Ref, ref } from 'vue';
<span> import axios from 'axios';
{{ new Date(doc.timestampFrom).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
</span> import ActionButton from '../../components/Global/ActionButton.vue';
import JournalOptions from '../../components/JournalView/JournalOptions.vue';
<span v-if="doc.currentDuration && doc.isOnline"> import DispatcherStats from '../../components/JournalView/DispatcherStats.vue';
({{ calculateDuration(doc.currentDuration) }}) import SearchBox from '../Global/SearchBox.vue';
</span>
import Loading from '../Global/Loading.vue';
<span v-if="doc.timestampTo"> import { URLs } from '../../scripts/utils/apiURLs';
&gt; import { DataStatus } from '../../scripts/enums/DataStatus';
{{ new Date(doc.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }} import { useStore } from '../../store/store';
({{ $t('journal.duty-lasted') }} {{ calculateDuration(doc.currentDuration!) }}) import JournalDispatchersList from './JournalDispatchersList.vue';
</span> import { JournalDispatcherSearcher, JournalDispatcherSorter } from '../../types/Journal/JournalDispatcherTypes';
</span> import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
</div> import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
</li>
</transition-group> const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
</ul>
</div> export default defineComponent({
</transition> components: { SearchBox, ActionButton, JournalOptions, DispatcherStats, Loading, JournalDispatchersList },
</div> name: 'JournalDispatchers',
</div>
props: {
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div> sceneryName: {
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div> type: String,
</div> required: false,
</section> },
</template>
dispatcherName: {
<script lang="ts"> type: String,
import { computed, defineComponent, JournalFilter, JournalSearcher, provide, reactive, Ref, ref, watch } from 'vue'; required: false,
import axios from 'axios'; },
},
import SearchBox from '@/components/Global/SearchBox.vue';
import dateMixin from '@/mixins/dateMixin'; data: () => ({
import { DataStatus } from '@/scripts/enums/DataStatus'; currentQuery: '',
scrollDataLoaded: true,
import ActionButton from '@/components/Global/ActionButton.vue'; scrollNoMoreData: false,
import JournalOptions from '@/components/JournalView/JournalOptions.vue';
import DispatcherStats from '@/components/JournalView/DispatcherStats.vue'; showReturnButton: false,
statsCardOpen: false,
import { URLs } from '@/scripts/utils/apiURLs';
import { useStore } from '@/store/store'; dataStatus: DataStatus.Initialized,
import { DispatcherStatsAPIData } from '@/scripts/interfaces/api/DispatcherStatsAPIData'; DataStatus,
import Loading from '../Global/Loading.vue';
import { useRoute, useRouter } from 'vue-router'; historyList: [] as DispatcherHistory[],
}),
const PROD_MODE = process.env.VUE_APP_JORUNAL_DISPATCHERS_DEV != '1' || process.env.NODE_ENV === 'production';
setup() {
const DISPATCHERS_API_URL = (PROD_MODE ? `${URLs.stacjownikAPI}/api` : 'http://localhost:3001/api') + '/getDispatchers'; const sorterActive: JournalDispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
const journalFilterActive = ref({});
interface DispatcherHistoryItem {
id: string; const searchersValues = reactive({
'search-dispatcher': '',
stationName: string; 'search-station': '',
stationHash: string; 'search-date': '',
region: string; } as JournalDispatcherSearcher);
dispatcherName: string; const countFromIndex = ref(0);
dispatcherId: number; const countLimit = 15;
timestampFrom: number; provide('sorterActive', sorterActive);
timestampTo?: number; provide('journalFilterActive', journalFilterActive);
currentDuration?: number; provide('searchersValues', searchersValues);
lastOnlineTimestamp: number; const scrollElement: Ref<HTMLElement | null> = ref(null);
isOnline: boolean; return {
} store: useStore(),
export default defineComponent({ sorterActive,
components: { SearchBox, ActionButton, JournalOptions, DispatcherStats, Loading }, searchersValues,
mixins: [dateMixin],
name: 'JournalDispatchers', countFromIndex,
countLimit,
props: {
sceneryName: { scrollElement,
type: String, maxCount: ref(15),
required: false, };
}, },
dispatcherName: { computed: {
type: String, computedHistoryList() {
required: false, return this.historyList.filter(
}, (doc) => doc.isOnline || (doc.currentDuration && doc.currentDuration > 10 * 60000)
}, );
},
data: () => ({ },
icons: {
arrow: require('@/assets/icon-arrow-asc.svg'), activated() {
}, if (this.sceneryName || this.dispatcherName) {
this.searchersValues['search-station'] = this.sceneryName?.toString() || '';
currentQuery: '', this.searchersValues['search-dispatcher'] = this.dispatcherName?.toString() || '';
scrollDataLoaded: true, this.searchHistory();
scrollNoMoreData: false, }
},
showReturnButton: false,
statsCardOpen: false, mounted() {
}), if (!this.sceneryName && !this.dispatcherName) {
this.searchHistory();
setup() { }
const historyDataStatus: Ref<{ status: DataStatus; error: string | null }> = ref({ },
status: DataStatus.Loading,
error: null, methods: {
}); handleScroll(e: Event) {
const listElement = e.target as HTMLElement;
const sorterActive = ref({ id: 'timestampFrom', dir: -1 }); const scrollTop = listElement.scrollTop;
const journalFilterActive = ref({}); const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
const searchersValues = reactive([
{ id: 'search-dispatcher', value: '' }, if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
{ id: 'search-station', value: '' },
]); if (scrollTop > elementHeight * 0.85) this.addHistoryData();
},
const countFromIndex = ref(0);
const countLimit = 15; resetOptions() {
this.searchersValues['search-station'] = '';
provide('sorterActive', sorterActive); this.searchersValues['search-dispatcher'] = '';
provide('journalFilterActive', journalFilterActive); this.sorterActive.id = 'timestampFrom';
provide('searchersValues', searchersValues);
this.searchHistory();
const scrollElement: Ref<HTMLElement | null> = ref(null); },
return { searchHistory() {
store: useStore(), this.fetchHistoryData({
searchers: this.searchersValues,
historyList: ref([]) as Ref<DispatcherHistoryItem[]>, });
historyDataStatus,
this.scrollNoMoreData = false;
isDataLoading: computed(() => historyDataStatus.value.status === DataStatus.Loading), this.scrollDataLoaded = true;
isDataError: computed(() => historyDataStatus.value.status === DataStatus.Error), },
isDataInit: computed(() => historyDataStatus.value.status === DataStatus.Initialized),
async addHistoryData() {
sorterActive, this.scrollDataLoaded = false;
searchersValues,
const countFrom = this.historyList.length;
countFromIndex,
countLimit, const responseData: DispatcherHistory[] = await (
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
scrollElement, ).data;
maxCount: ref(15),
}; if (!responseData) return;
},
if (responseData.length == 0) {
computed: { this.scrollNoMoreData = true;
computedHistoryList() { return;
return this.historyList.filter( }
(doc) => doc.isOnline || (doc.currentDuration && doc.currentDuration > 10 * 60000)
); this.historyList.push(...responseData);
}, this.scrollDataLoaded = true;
}, },
activated() { async fetchHistoryData(
if (this.sceneryName || this.dispatcherName) { props: {
this.searchersValues[1].value = this.sceneryName?.toString() || ''; searchers?: JournalDispatcherSearcher;
this.searchersValues[0].value = this.dispatcherName?.toString() || ''; filter?: JournalTimetableFilter;
this.search(); } = {}
} ) {
this.dataStatus = DataStatus.Loading;
window.addEventListener('scroll', this.handleScroll);
}, const queries: string[] = [];
mounted() { const dispatcher = props.searchers?.['search-dispatcher'].trim();
if (!this.sceneryName && !this.dispatcherName) { const station = props.searchers?.['search-station'].trim();
this.search(); const dateString = props.searchers?.['search-date'].trim();
} const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
}, const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
deactivated() { if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
window.removeEventListener('scroll', this.handleScroll); if (station) queries.push(`stationName=${station}`);
}, if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
methods: { // Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
closeDispatcherStatsCard() { if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom');
this.statsCardOpen = false; else if (this.sorterActive.id == 'duration') queries.push('sortBy=currentDuration');
}, else queries.push('sortBy=timestampFrom');
navigateToScenery(name: string, isOnline: boolean) { queries.push('countLimit=30');
if (!isOnline) return;
this.currentQuery = queries.join('&');
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
}, try {
const responseData: DispatcherHistory[] = await (
isAnotherDay(prevIndex: number, currIndex: number) { await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`)
if (currIndex == 0) return true; ).data;
return ( if (!responseData) {
new Date(this.computedHistoryList[prevIndex].timestampFrom).getDate() != this.dataStatus = DataStatus.Error;
new Date(this.computedHistoryList[currIndex].timestampFrom).getDate() return;
); }
},
if (!responseData) return;
handleScroll() {
this.showReturnButton = window.scrollY > window.innerHeight; // Response data exists
this.historyList = responseData;
const element = this.$refs.scrollElement as HTMLElement;
// Stats display
if ( this.store.dispatcherStatsName =
element.getBoundingClientRect().bottom * 0.85 < window.innerHeight && this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
this.scrollDataLoaded && ? this.historyList[0].dispatcherName
!this.scrollNoMoreData && : '';
this.historyDataStatus.status == DataStatus.Loaded
) this.dataStatus = DataStatus.Loaded;
this.addHistoryData(); } catch (error) {
}, this.dataStatus = DataStatus.Error;
}
scrollToTop() { },
window.scrollTo({ top: 0 }); },
}, });
</script>
search() {
this.fetchHistoryData({ <style lang="scss" scoped>
searchers: this.searchersValues, @import '../../styles/JournalSection.scss';
}); </style>
this.scrollNoMoreData = false;
this.scrollDataLoaded = true;
},
async addHistoryData() {
this.scrollDataLoaded = false;
const countFrom = this.historyList.length;
const responseData: DispatcherHistoryItem[] = await (
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
).data;
if (!responseData) return;
if (responseData.length == 0) {
this.scrollNoMoreData = true;
return;
}
this.historyList.push(...responseData);
this.scrollDataLoaded = true;
},
async fetchHistoryData(
props: {
searchers?: JournalSearcher[];
filter?: JournalFilter;
} = {}
) {
this.historyDataStatus.status = DataStatus.Loading;
const queries: string[] = [];
const dispatcher = props.searchers?.find((s) => s.id == 'search-dispatcher')?.value.trim();
const station = props.searchers?.find((s) => s.id == 'search-station')?.value.trim();
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
if (station) queries.push(`stationName=${station}`);
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom');
else if (this.sorterActive.id == 'duration') queries.push('sortBy=currentDuration');
else queries.push('sortBy=timestampFrom');
queries.push('countLimit=15');
this.currentQuery = queries.join('&');
try {
const responseData: DispatcherHistoryItem[] = await (
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`)
).data;
if (!responseData) {
this.historyDataStatus.status = DataStatus.Error;
this.historyDataStatus.error = 'Brak danych!';
return;
}
if (!responseData) return;
// Response data exists
this.historyList = responseData;
// Stats display
this.store.dispatcherStatsName =
this.historyList.length > 0 && this.searchersValues[0].value.trim() ? this.historyList[0].dispatcherName : '';
this.historyDataStatus.status = DataStatus.Loaded;
} catch (error) {
this.historyDataStatus.status = DataStatus.Error;
this.historyDataStatus.error = 'Ups! Coś poszło nie tak!';
}
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/JournalSection.scss';
@import '../../styles/responsive.scss';
.region-badge {
padding: 0.1em 0.5em;
border-radius: 0.5em;
font-weight: bold;
&.eu {
background-color: forestgreen;
}
}
.list-wrapper {
margin-top: 1em;
}
.journal_item {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
&.online {
cursor: pointer;
}
span[data-status='true'] {
color: springgreen;
}
span[data-status='false'] {
color: salmon;
}
}
.journal_day {
position: relative;
text-align: center;
background-color: #4d4d4d;
span {
position: relative;
background-color: #4d4d4d;
z-index: 10;
padding: 0 0.5em;
}
&::after {
position: absolute;
content: '';
z-index: 0;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
height: 3px;
width: 60%;
min-width: 200px;
background-color: white;
}
}
@include smallScreen() {
.journal_item {
flex-direction: column;
span {
margin-top: 0.25em;
text-align: center;
}
}
}
</style>
@@ -0,0 +1,156 @@
<template>
<ul class="journal-list">
<!-- <transition-group name="journal-list-anim"> -->
<li v-for="item in computedDispatcherHistory" :class="{ sticky: typeof item == 'string' }">
<div v-if="typeof item == 'string'" class="journal_day">
{{ item }}
</div>
<div
v-else
class="journal_item"
:class="{ online: item.isOnline }"
@click="navigateToScenery(item.stationName, item.isOnline)"
@keydown.enter="navigateToScenery(item.stationName, item.isOnline)"
tabindex="0"
>
<span>
<b class="text--primary">{{ item.dispatcherName }}</b> &bull; <b>{{ item.stationName }}</b>
<span class="text--grayed">&nbsp;#{{ item.stationHash }}&nbsp;</span>
<span class="region-badge" :class="item.region">PL1</span>
</span>
<span>
<span :data-status="item.isOnline"> {{ item.isOnline ? $t('journal.online-since') : 'OFFLINE' }}&nbsp; </span>
<span>
{{ new Date(item.timestampFrom).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
</span>
<span v-if="item.currentDuration && item.isOnline"> ({{ calculateDuration(item.currentDuration) }}) </span>
<span v-if="item.timestampTo">
&gt;
{{ new Date(item.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
({{ $t('journal.duty-lasted') }} {{ calculateDuration(item.currentDuration!) }})
</span>
</span>
</div>
</li>
<!-- </transition-group> -->
</ul>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
export default defineComponent({
props: {
dispatcherHistory: {
type: Array as PropType<DispatcherHistory[]>,
required: true,
},
},
mixins: [dateMixin],
computed: {
computedDispatcherHistory() {
return this.dispatcherHistory.reduce((acc, historyItem, i) => {
if (this.isAnotherDay(i - 1, i)) acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
acc.push(historyItem);
return acc;
}, [] as (DispatcherHistory | string)[]);
},
},
methods: {
navigateToScenery(name: string, isOnline: boolean) {
if (!isOnline) return;
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
},
isAnotherDay(prevIndex: number, currIndex: number) {
if (currIndex == 0) return true;
return (
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
);
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/JournalSection.scss';
.region-badge {
padding: 0.1em 0.5em;
border-radius: 0.5em;
font-weight: bold;
&.eu {
background-color: forestgreen;
}
}
li.sticky {
position: sticky;
top: 0;
}
.journal_item {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
padding: 0.75em;
&.online {
cursor: pointer;
}
span[data-status='true'] {
color: springgreen;
}
span[data-status='false'] {
color: salmon;
}
}
.journal_day {
margin-bottom: 1em;
padding: 0.5em;
font-weight: bold;
background-color: #333;
span {
position: relative;
background-color: inherit;
z-index: 10;
padding-right: 1em;
font-weight: bold;
}
}
@include smallScreen() {
.journal_item {
flex-direction: column;
span {
margin-top: 0.25em;
text-align: center;
}
}
}
</style>
+189 -260
View File
@@ -1,260 +1,189 @@
<template> <template>
<div class="journal-options"> <div class="filters-options" @keydown.esc="showOptions = false">
<div class="options_wrapper"> <div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<div class="options_content">
<div class="content_select"> <button class="btn--filled btn--image" @click="showOptions = !showOptions" ref="button">
<select-box <img :src="getIcon('filter2')" alt="Open filters" />
:itemList="translatedSorterOptions" {{ $t('options.filters') }} [F]
:defaultItemIndex="0" </button>
@selected="onSorterChange"
:prefix="$t('journal.sort-prefix')" <transition name="options-anim">
/> <div class="options_wrapper" v-if="showOptions">
</div> <div class="options_content">
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
<div class="content_search"> <div class="search_content">
<div class="search-box" v-for="search in searchersValues" :key="search.id"> <div class="search" v-for="(_, propName) in searchersValues" :key="propName">
<input <label v-if="propName == 'search-date'" for="date">{{ $t('options.search-date') }}</label>
class="search-input"
:placeholder="$t(`journal.${search.id}`)" <div class="search-box">
v-model="search.value" <input
@keydown.enter="onInputSearch" v-if="propName == 'search-date'"
/> class="search-input"
id="date"
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="onInputClear(search.id)" /> type="date"
</div> min="2022-02-01"
<!-- <div class="search-box"> @keydown.enter="onSearchConfirm"
<input v-model="searchersValues[propName]"
class="search-input" />
v-model="searchedTrain"
:placeholder="$t('journal.search-train')" <input
@keydown.enter="search" v-else
/> class="search-input"
@keydown.enter="onSearchConfirm"
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearTrain" /> @focus="preventKeyDown = true"
</div> @blur="preventKeyDown = false"
:placeholder="$t(`options.${propName}`)"
<div class="search-box"> v-model="searchersValues[propName]"
<input />
class="search-input"
v-model="searchedDriver" <button class="search-exit">
:placeholder="$t('journal.search-driver')" <img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" />
@keydown.enter="search" </button>
/> </div>
</div>
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearDriver" />
</div> --> <div class="search_actions">
<button class="btn--action" @click="onResetButtonClick">
<action-button class="search-button" @click="onInputSearch"> {{ $t('options.reset-button') }}
{{ $t('journal.search') }} </button>
</action-button> <button class="btn--action" @click="onSearchButtonConfirm">
</div> {{ $t('options.search-button') }}
</div> </button>
</div>
<div class="options_filters"> </div>
<button
v-for="filter in filters" <h1 class="option-title">{{ $t('options.sort-title') }}</h1>
class="journal-filter-option btn--option" <div class="options_sorters">
:class="{ checked: journalFilterActive.id === filter.id }" <div v-for="opt in translatedSorterOptions">
:id="filter.id" <button
@click="onFilterChange(filter)" class="sort-option btn--option"
> :data-selected="opt.id == sorterActive.id"
{{ $t(`journal.filter-${filter.id}`) }} @click="onSorterChange(opt)"
</button> >
</div> {{ opt.value.toUpperCase() }}
</div> </button>
</div> </div>
</template> </div>
<script lang="ts"> <h1 class="option-title" v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1>
import { defineComponent, inject, JournalFilter, PropType } from 'vue'; <div class="options_filters">
import ActionButton from '../Global/ActionButton.vue'; <button
import SelectBox from '../Global/SelectBox.vue'; v-for="filter in filters"
class="filter-option btn--option"
export default defineComponent({ :class="{ checked: journalFilterActive.id === filter.id }"
components: { SelectBox, ActionButton }, :id="filter.id"
emits: ['onSorterChange', 'onInputChange', 'onFilterChange'], @click="onFilterChange(filter)"
props: { >
sorterOptionIds: { {{ $t(`options.filter-${filter.id}`) }}
type: Array as PropType<Array<string>>, </button>
required: true, </div>
}, </div>
</div>
filters: { </transition>
type: Array as PropType<JournalFilter[]>, </div>
default: [], </template>
},
}, <script lang="ts">
import { defineComponent, inject, Prop, PropType } from 'vue';
data: () => ({ import imageMixin from '../../mixins/imageMixin';
exitIcon: require('@/assets/icon-exit.svg'), import keyMixin from '../../mixins/keyMixin';
}), import { DataStatus } from '../../scripts/enums/DataStatus';
import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
setup() { import ActionButton from '../Global/ActionButton.vue';
return { import SelectBox from '../Global/SelectBox.vue';
searchersValues: inject('searchersValues') as {id: string; value: string}[],
sorterActive: inject('sorterActive') as { id: string | number; dir: number }, export default defineComponent({
journalFilterActive: inject('journalFilterActive') as JournalFilter, components: { SelectBox, ActionButton },
}; emits: ['onSearchConfirm', 'onOptionsReset'],
}, mixins: [imageMixin, keyMixin],
computed: { props: {
translatedSorterOptions() { sorterOptionIds: {
return this.$props.sorterOptionIds.map((id) => ({ type: Array as PropType<Array<string>>,
id, required: true,
value: this.$t(`journal.option-${id}`), },
}));
}, filters: {
}, type: Array as PropType<JournalTimetableFilter[]>,
default: [],
methods: { },
onSorterChange(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id; dataStatus: {
this.sorterActive.dir = -1; type: Number as PropType<DataStatus>,
default: DataStatus.Initialized,
this.$emit('onSorterChange'); },
}, },
onFilterChange(filter: JournalFilter) { data() {
this.journalFilterActive = filter; return {
this.$emit('onFilterChange'); showOptions: false,
}, DataStatus,
};
onInputSearch() { },
this.$emit('onInputChange');
}, setup() {
return {
onInputClear(id: string) { searchersValues: inject('searchersValues') as { [key: string]: string },
this.searchersValues.find(s => s.id == id)!.value = ""; sorterActive: inject('sorterActive') as { id: string | number; dir: number },
this.onInputSearch(); journalFilterActive: inject('journalFilterActive') as JournalTimetableFilter,
}, };
}, },
});
</script> computed: {
translatedSorterOptions() {
<style lang="scss" scoped> return this.$props.sorterOptionIds.map((id) => ({
@import '../../styles/responsive'; id,
@import '../../styles/option.scss'; value: this.$t(`options.sort-${id}`),
}));
.options { },
&_wrapper { },
display: flex;
flex-direction: column; methods: {
} // Override keyMixin function
onKeyDownFunction() {
&_content { this.showOptions = !this.showOptions;
display: flex;
flex-wrap: wrap; this.$nextTick(() => {
if (this.showOptions) (this.$refs['button'] as HTMLButtonElement)?.focus();
.content_search, });
.content_select { },
display: flex;
align-items: center; focusEnd() {
flex-wrap: wrap; console.log('focus end');
},
padding: 0.25em 0.25em 0 0;
} onSorterChange(item: { id: string | number; value: string }) {
} this.sorterActive.id = item.id;
this.sorterActive.dir = -1;
&_filters { this.$emit('onSearchConfirm');
display: flex; },
flex-wrap: wrap;
margin: 0.5em 0 0 0; onFilterChange(filter: JournalTimetableFilter) {
this.journalFilterActive = filter;
.journal-filter-option { this.$emit('onSearchConfirm');
margin: 0 0.25em 0 0; },
&#abandoned { onInputClear(id: any) {
color: salmon; this.searchersValues[id] = '';
} this.$emit('onSearchConfirm');
},
&#fulfilled {
color: lightgreen; onSearchConfirm() {
} this.$emit('onSearchConfirm');
},
&#active {
color: lightblue; onSearchButtonConfirm() {
} this.showOptions = false;
} this.$emit('onSearchConfirm');
} },
}
onResetButtonClick() {
.search { this.$emit('onOptionsReset');
&-box { },
position: relative; },
});
background: #333; </script>
border-radius: 0.5em;
min-width: 200px; <style lang="scss" scoped>
margin-right: 0.25em; @import '../../styles/filters_options.scss';
} </style>
&-input {
border: none;
min-width: 100%;
padding: 0.35em 0.5em;
}
&-exit {
position: absolute;
cursor: pointer;
top: 50%;
right: 10px;
transform: translateY(-50%);
width: 1em;
}
}
@include smallScreen() {
.journal-options {
width: 100%;
}
.options {
&_wrapper {
justify-content: center;
align-items: center;
}
&_content {
padding: 0 1em;
flex-direction: column;
.content_select {
margin: 0 auto;
padding: 0;
}
.content_search {
justify-content: center;
}
}
&_filters {
justify-content: center;
.journal-filter-option {
margin: 0.25em 0.25em;
}
}
}
.search {
&-box,
&-button {
margin: 0.5em 0 0 0;
}
&-box {
width: 100%;
}
&-button {
width: 80%;
max-width: 300px;
}
}
}
</style>
+284 -451
View File
@@ -1,451 +1,284 @@
<template> <template>
<section class="journal-timetables"> <section class="journal-timetables">
<keep-alive>
<DriverStats v-if="statsCardOpen" @close-card="closeCard" /> <div class="journal_wrapper">
</keep-alive> <JournalOptions
@on-search-confirm="searchHistory"
<div class="journal-wrapper"> @on-options-reset="resetOptions"
<div class="journal_top-bar"> :sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']"
<JournalOptions :filters="journalTimetableFilters"
@on-input-change="search" :data-status="dataStatus"
@on-filter-change="search" />
@on-sorter-change="search"
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']" <DriverStats />
:filters="journalTimetableFilters" <!-- <button @click="statsCardOpen = true">Stats</button> -->
/>
<div class="list_wrapper" @scroll="handleScroll">
<!-- <button <!-- <transition name="warning" mode="out-in"> -->
class="btn btn--option" <!-- <div :key="dataStatus"> -->
:disabled="store.driverStatsName == ''" <Loading v-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
@click="() => (statsCardOpen = !statsCardOpen)"
> <div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
<span v-if="store.driverStatsName"> {{ $t('app.error') }}
Statystyki maszynisty <b>{{ store.driverStatsName }}</b> </div>
</span>
<span v-else>Statystyki maszynisty niedostępne</span> <div v-else-if="timetableHistory.length == 0" class="journal_warning">
</button> --> {{ $t('app.no-result') }}
</div> </div>
<div class="journal-list"> <div v-else>
<div class="list-wrapper" ref="scrollElement"> <JournalTimetablesList :timetableHistory="timetableHistory" />
<transition name="warning" mode="out-in">
<div :key="historyDataStatus.status"> <button
<Loading v-if="isDataLoading || isDataInit" /> class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
<div v-else-if="isDataError" class="journal_warning error"> @click="addHistoryData"
{{ $t('app.error') }} >
</div> {{ $t('journal.load-data') }}
</button>
<div class="journal_warning" v-else-if="historyList.length == 0"> </div>
{{ $t('app.no-result') }} <!-- </div> -->
</div> <!-- </transition> -->
<ul v-else> <div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
<transition-group name="journal-list-anim"> <div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
<li v-for="(item, i) in historyList" class="journal_item" :key="item.timetableId"> </div>
<div class="journal_item-top"> </div>
<span> </section>
<span </template>
tabindex="0"
@click="navigateToTimetable(item)" <script lang="ts">
@keydown.enter="navigateToTimetable(item)" import { defineComponent, provide, reactive, Ref, ref } from 'vue';
style="cursor: pointer" import axios from 'axios';
>
<b class="text--primary">{{ item.trainCategoryCode }}&nbsp;</b> import DriverStats from './DriverStats.vue';
<b>{{ item.trainNo }}</b> import Loading from '../Global/Loading.vue';
| <span>{{ item.driverName }}</span> | import { JournalTimetableFilter, JournalTimetableSorter } from '../../types/Journal/JournalTimetablesTypes';
<span class="text--grayed">#{{ item.timetableId }}</span> import dateMixin from '../../mixins/dateMixin';
</span> import routerMixin from '../../mixins/routerMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
<div> import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
<b>{{ item.route.replace('|', ' - ') }}</b> import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
</div> import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store';
<hr style="margin: 0.25em 0" /> import JournalOptions from './JournalOptions.vue';
import { JorunalTimetableSearchType } from '../../types/Journal/JournalTimetablesTypes';
<div class="scenery-list"> import modalTrainMixin from '../../mixins/modalTrainMixin';
<span import imageMixin from '../../mixins/imageMixin';
v-for="(scenery, i) in getSceneryList(item)" import JournalTimetablesList from './JournalTimetablesList.vue';
:key="scenery.name" import { journalTimetableFilters } from '../../constants/Journal/JournalTimetablesConsts';
:class="{ confirmed: scenery.confirmed }"
> const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
{{ i > 0 ? ' > ' : '' }} {{ scenery.name }}
</span> export default defineComponent({
</div> components: { DriverStats, Loading, JournalOptions, JournalTimetablesList },
mixins: [dateMixin, routerMixin, modalTrainMixin, imageMixin],
<div class="schedule-dates">
<!-- Data odjazdu ze stacji początkowej --> name: 'JournalTimetables',
<b>{{ item.route.split('|')[0] }}:</b>
<s v-if="item.beginDate != item.scheduledBeginDate" class="text--grayed"> props: {
{{ localeTime(item.beginDate, $i18n.locale) }} timetableId: {
</s> type: String,
<span>{{ localeTime(item.scheduledBeginDate, $i18n.locale) }} </span>&bull; },
},
<!-- Data przyjazdu na stację końcową / porzucenia -->
<b v-if="(item.fulfilled && item.terminated) || !item.terminated"> data: () => ({
{{ item.route.split('|').slice(-1)[0] }}: currentQuery: '',
</b> scrollDataLoaded: true,
<i v-else>{{ $t('journal.timetable-abandoned') }} </i> scrollNoMoreData: false,
<s v-if="item.endDate != item.scheduledEndDate && item.terminated" class="text--grayed"> showReturnButton: false,
{{ localeTime(item.fulfilled ? item.endDate : item.scheduledEndDate, $i18n.locale) }} statsCardOpen: false,
</s>
<span timetableHistory: [] as TimetableHistory[],
>{{ localeTime(item.fulfilled ? item.scheduledEndDate : item.endDate, $i18n.locale) }} journalTimetableFilters,
</span>
</div> dataStatus: DataStatus.Initialized,
</span> dataErrorMessage: '',
<b DataStatus,
class="journal_item-status" }),
:class="{
fulfilled: item.fulfilled || item.currentDistance >= item.routeDistance * 0.9, setup() {
terminated: item.terminated && !item.fulfilled, const sorterActive: JournalTimetableSorter = reactive({ id: 'timetableId', dir: 1 });
active: !item.terminated, const journalFilterActive = ref(journalTimetableFilters[0]);
}"
> const searchersValues = reactive({
{{ 'search-train': '',
!item.terminated 'search-driver': '',
? $t('journal.timetable-active') 'search-author': '',
: item.fulfilled || item.currentDistance >= item.routeDistance * 0.9 'search-date': '',
? $t('journal.timetable-fulfilled') } as JorunalTimetableSearchType);
: $t('journal.timetable-abandoned')
}} const countFromIndex = ref(0);
</b> const countLimit = 15;
</div>
provide('searchersValues', searchersValues);
<div style="margin-top: 1em"> provide('sorterActive', sorterActive);
<div> provide('journalFilterActive', journalFilterActive);
{{ $t('journal.timetable-day') }} <b>{{ localeDay(item.beginDate, $i18n.locale) }}</b>
</div> const scrollElement: Ref<HTMLElement | null> = ref(null);
<!-- Nick dyżurnego --> return {
<div v-if="item.authorName"> sorterActive,
<b class="text--grayed">{{ $t('journal.dispatcher-name') }}&nbsp;</b> journalFilterActive,
<router-link searchersValues,
class="dispatcher-link"
:to="`/journal/dispatchers?dispatcherName=${item.authorName}`" countFromIndex,
>{{ item.authorName }}</router-link countLimit,
>
</div> scrollElement,
</div> store: useStore(),
};
<div style="margin-top: 1em"> },
<div>
<b>{{ $t('journal.route-length') }}</b> activated() {
{{ !item.fulfilled ? item.currentDistance + ' /' : '' }} if (this.timetableId) {
{{ item.routeDistance }} km this.searchersValues['search-train'] = `#${this.timetableId}`;
</div> this.searchHistory();
}
<div> },
<b>{{ $t('journal.station-count') }}</b>
{{ item.confirmedStopsCount }} / mounted() {
{{ item.allStopsCount }} if (!this.timetableId) this.searchHistory();
</div> },
</div>
</li> methods: {
</transition-group> handleScroll(e: Event) {
</ul> const listElement = e.target as HTMLElement;
</div> const scrollTop = listElement.scrollTop;
</transition> const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
</div>
</div> if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div> if (scrollTop > elementHeight * 0.85) this.addHistoryData();
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div> },
</div>
</section> resetOptions() {
</template> this.searchersValues['search-date'] = '';
this.searchersValues['search-driver'] = '';
<script lang="ts"> this.searchersValues['search-train'] = '';
import { computed, defineComponent, JournalFilter, JournalSearcher, provide, reactive, Ref, ref } from 'vue'; this.searchersValues['search-author'] = '';
import axios from 'axios';
this.journalFilterActive = this.journalTimetableFilters[0];
import SearchBox from '@/components/Global/SearchBox.vue'; this.sorterActive.id = 'timetableId';
import dateMixin from '@/mixins/dateMixin';
import { DataStatus } from '@/scripts/enums/DataStatus'; this.searchHistory();
},
import ActionButton from '@/components/Global/ActionButton.vue';
import JournalOptions from '@/components/JournalView/JournalOptions.vue'; searchHistory() {
this.fetchHistoryData({
import { URLs } from '@/scripts/utils/apiURLs'; searchers: this.searchersValues,
import { journalTimetableFilters } from '@/data/journalFilters'; filter: this.journalFilterActive,
import { JournalFilterType } from '@/scripts/enums/JournalFilterType'; });
import routerMixin from '@/mixins/routerMixin';
import { useStore } from '@/store/store'; this.scrollNoMoreData = false;
import DriverStats from './DriverStats.vue'; this.scrollDataLoaded = true;
import { TimetableHistory } from '@/scripts/interfaces/api/TimetablesAPIData'; },
import Loading from '../Global/Loading.vue';
async addHistoryData() {
const PROD_MODE = process.env.VUE_APP_JOURNAL_TIMETABLES_DEV != '1' || process.env.NODE_ENV === 'production'; this.scrollDataLoaded = false;
const TIMETABLES_API_URL = PROD_MODE const countFrom = this.timetableHistory.length;
? `${URLs.stacjownikAPI}/api/getTimetables`
: 'http://localhost:3001/api/getTimetables'; const responseData: TimetableHistory[] = await (
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
export default defineComponent({ ).data;
components: { SearchBox, ActionButton, JournalOptions, DriverStats, Loading },
mixins: [dateMixin, routerMixin], if (!responseData) return;
name: 'JournalTimetables', if (responseData.length == 0) {
this.scrollNoMoreData = true;
data: () => ({ return;
icons: { }
arrow: require('@/assets/icon-arrow-asc.svg'),
}, this.timetableHistory.push(...responseData);
this.scrollDataLoaded = true;
currentQuery: '', },
scrollDataLoaded: true,
scrollNoMoreData: false, async fetchHistoryData(
props: {
showReturnButton: false, searchers?: JorunalTimetableSearchType;
statsCardOpen: false, filter?: JournalTimetableFilter;
} = {}
journalTimetableFilters, ) {
}), this.dataStatus = DataStatus.Loading;
setup() { const queries: string[] = [];
const historyDataStatus: Ref<{ status: DataStatus; error: string | null }> = ref({
status: DataStatus.Loading, const driverName = props.searchers?.['search-driver'].trim();
error: null, const trainNo = props.searchers?.['search-train'].trim();
}); const authorName = props.searchers?.['search-author'].trim();
const sorterActive = ref({ id: 'timetableId', dir: -1 }); const dateString = props.searchers?.['search-date'].trim();
const journalFilterActive = ref(journalTimetableFilters[0]); const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
const searchersValues = reactive([
{ id: 'search-train', value: '' }, if (driverName) queries.push(`driverName=${driverName}`);
{ id: 'search-driver', value: '' }, if (trainNo)
]); queries.push(trainNo.startsWith('#') ? `timetableId=${trainNo.replace('#', '')}` : `trainNo=${trainNo}`);
const countFromIndex = ref(0); if (authorName) queries.push(`authorName=${authorName}`);
const countLimit = 15; if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
provide('searchersValues', searchersValues); // Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
provide('sorterActive', sorterActive); if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance');
provide('journalFilterActive', journalFilterActive); else if (this.sorterActive.id == 'total-stops') queries.push('sortBy=allStopsCount');
else if (this.sorterActive.id == 'beginDate') queries.push('sortBy=beginDate');
const scrollElement: Ref<HTMLElement | null> = ref(null); else queries.push('sortBy=timetableId');
return { queries.push('countLimit=15');
historyList: ref([]) as Ref<TimetableHistory[]>,
historyDataStatus, switch (props.filter?.id) {
case JournalFilterType.abandoned:
isDataLoading: computed(() => historyDataStatus.value.status === DataStatus.Loading), queries.push('fulfilled=0', 'terminated=1');
isDataError: computed(() => historyDataStatus.value.status === DataStatus.Error), break;
isDataInit: computed(() => historyDataStatus.value.status === DataStatus.Initialized),
case JournalFilterType.active:
sorterActive, queries.push('terminated=0');
journalFilterActive, break;
searchersValues,
case JournalFilterType.fulfilled:
countFromIndex, queries.push('fulfilled=1');
countLimit, break;
scrollElement, default:
maxCount: ref(15), break;
store: useStore(), }
};
}, this.currentQuery = queries.join('&');
activated() { try {
window.addEventListener('scroll', this.handleScroll); const responseData: TimetableHistory[] = await (
}, await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}`)
).data;
mounted() {
this.search(); if (!responseData) {
}, this.dataStatus = DataStatus.Error;
this.dataErrorMessage = 'Brak danych!';
deactivated() { return;
window.removeEventListener('scroll', this.handleScroll); }
},
if (!responseData) return;
methods: {
navigateToTimetable(historyItem: TimetableHistory) { // Response data exists
if (historyItem.terminated) return; this.timetableHistory = responseData;
this.navigateTo('/trains', { // Stats display
trainNo: historyItem.trainNo, this.store.driverStatsName =
driverName: historyItem.driverName, this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim()
}); ? this.timetableHistory[0].driverName
}, : '';
closeCard() { this.dataStatus = DataStatus.Loaded;
this.statsCardOpen = false; } catch (error) {
}, this.dataStatus = DataStatus.Error;
this.dataErrorMessage = 'Ups! Coś poszło nie tak!';
getSceneryList(historyItem: TimetableHistory) { }
return historyItem.sceneriesString },
.split('%') },
.map((name, i) => ({ name, confirmed: i < historyItem.confirmedStopsCount })); });
}, </script>
handleScroll() { <style lang="scss" scoped>
this.showReturnButton = window.scrollY > window.innerHeight; @import '../../styles/JournalSection.scss';
</style>
const element = this.$refs.scrollElement as HTMLElement;
if (
element.getBoundingClientRect().bottom * 0.85 < window.innerHeight &&
this.scrollDataLoaded &&
!this.scrollNoMoreData &&
this.historyDataStatus.status == DataStatus.Loaded
)
this.addHistoryData();
},
scrollToTop() {
window.scrollTo({ top: 0 });
},
search() {
this.fetchHistoryData({
searchers: this.searchersValues,
filter: this.journalFilterActive,
});
this.scrollNoMoreData = false;
this.scrollDataLoaded = true;
},
async addHistoryData() {
this.scrollDataLoaded = false;
const countFrom = this.historyList.length;
const responseData: TimetableHistory[] = await (
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
).data;
if (!responseData) return;
if (responseData.length == 0) {
this.scrollNoMoreData = true;
return;
}
this.historyList.push(...responseData);
this.scrollDataLoaded = true;
},
async fetchHistoryData(
props: {
searchers?: JournalSearcher[];
filter?: JournalFilter;
} = {}
) {
this.historyDataStatus.status = DataStatus.Loading;
const queries: string[] = [];
const driver = props.searchers?.find((s) => s.id == 'search-driver')?.value.trim();
const train = props.searchers?.find((s) => s.id == 'search-train')?.value.trim();
if (driver) queries.push(`driverName=${driver}`);
if (train) queries.push(`trainNo=${train}`);
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance');
else if (this.sorterActive.id == 'total-stops') queries.push('sortBy=allStopsCount');
else if (this.sorterActive.id == 'beginDate') queries.push('sortBy=beginDate');
else queries.push('sortBy=timetableId');
queries.push('countLimit=15');
switch (props.filter?.id) {
case JournalFilterType.abandoned:
queries.push('fulfilled=0', 'terminated=1');
break;
case JournalFilterType.active:
queries.push('terminated=0');
break;
case JournalFilterType.fulfilled:
queries.push('fulfilled=1');
break;
default:
break;
}
this.currentQuery = queries.join('&');
try {
const responseData: TimetableHistory[] = await (
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}`)
).data;
if (!responseData) {
this.historyDataStatus.status = DataStatus.Error;
this.historyDataStatus.error = 'Brak danych!';
return;
}
// if (responseData) {
// this.historyDataStatus.status = DataStatus.Error;
// this.historyDataStatus.error = responseData;
// return;
// }
if (!responseData) return;
// Response data exists
this.historyList = responseData;
// Stats display
this.store.driverStatsName =
this.historyList.length > 0 && this.searchersValues[1].value.trim() ? this.historyList[0].driverName : '';
this.historyDataStatus.status = DataStatus.Loaded;
} catch (error) {
this.historyDataStatus.status = DataStatus.Error;
this.historyDataStatus.error = 'Ups! Coś poszło nie tak!';
console.error(error);
}
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/JournalSection.scss';
.journal_item {
&-top {
display: flex;
justify-content: space-between;
padding: 0.2em 0;
.scenery-list {
span {
color: #adadad;
&.confirmed {
color: #a3eba3;
}
}
}
}
&-status {
&.terminated {
color: salmon;
}
&.fulfilled {
color: lightgreen;
}
&.active {
color: lightblue;
}
}
}
.dispatcher-link {
font-weight: bold;
}
</style>
@@ -0,0 +1,314 @@
<template>
<ul class="journal-list">
<li
v-for="{ timetable, sceneryList, ...item } in computedTimetableHistory"
class="journal_item"
:key="timetable.timetableId"
>
<div class="journal_item-info">
<div class="info-top">
<span
tabindex="0"
@click="showTimetable(timetable)"
@keydown.enter="showTimetable(timetable)"
style="cursor: pointer"
>
<b class="text--primary">{{ timetable.trainCategoryCode }}&nbsp;</b>
<b>{{ timetable.trainNo }}</b>
| <span>{{ timetable.driverName }}</span> |
<span class="text--grayed">#{{ timetable.timetableId }}</span>
</span>
<span>
<b class="info-date">{{ localeDay(timetable.beginDate, $i18n.locale) }}</b>
<b
class="info-status"
:class="{
fulfilled: timetable.fulfilled || timetable.currentDistance >= timetable.routeDistance * 0.9,
terminated: timetable.terminated && !timetable.fulfilled,
active: !timetable.terminated,
}"
>
{{
!timetable.terminated
? $t('journal.timetable-active')
: timetable.fulfilled || timetable.currentDistance >= timetable.routeDistance * 0.9
? $t('journal.timetable-fulfilled')
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
}}
</b>
</span>
</div>
<div class="info-route">
<b>{{ timetable.route.replace('|', ' - ') }}</b>
</div>
<hr />
<div class="scenery-list">
<span v-for="(scenery, i) in sceneryList" :key="scenery.name" :class="{ confirmed: scenery.confirmed }">
<span v-if="i > 0"> &gt;</span>
{{ scenery.name }}
<!-- Data odjazdu ze stacji początkowej -->
<span v-if="i == 0" v-html="scenery.beginDateHTML"></span>
<!-- Data przyjazdu do stacji końcowej -->
<span v-if="i == sceneryList.length - 1" v-html="scenery.endDateHTML"> </span>
</span>
</div>
<!-- Status RJ -->
<div style="margin: 0.5em 0">
<span>
<b>{{ $t('journal.route-length') }}</b>
{{ !timetable.fulfilled ? timetable.currentDistance + ' /' : '' }}
{{ timetable.routeDistance }} km
</span>
&bull;
<span>
<b>{{ $t('journal.station-count') }}</b>
{{ timetable.confirmedStopsCount }} /
{{ timetable.allStopsCount }}
</span>
</div>
<!-- Nick dyżurnego -->
<div v-if="timetable.authorName">
<b class="text--grayed">{{ $t('journal.dispatcher-name') }}&nbsp;</b>
<router-link class="dispatcher-link" :to="`/journal/dispatchers?dispatcherName=${timetable.authorName}`">
<b>{{ timetable.authorName }}</b>
</router-link>
</div>
<button
v-if="timetable.stockString"
class="btn--option btn--show"
@click="item.showStock.value = !item.showStock.value"
>
{{ $t('journal.stock-info') }}
<img :src="getIcon(`arrow-${item.showStock.value ? 'asc' : 'desc'}`)" alt="Arrow" />
</button>
<div class="info-extended" v-if="timetable.stockString && item.showStock.value">
<hr />
<div>
<span class="badge info-badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge info-badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>{{ timetable.stockLength }}m</span>
</span>
<span class="badge info-badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>{{ Math.floor(timetable.stockMass! / 1000) }}t</span>
</span>
</div>
<ul class="stock-list">
<li v-for="(car, i) in timetable.stockString.split(';')" :key="i">
<img
@error="onImageError"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${car.split(':')[0]}.png`"
:alt="car"
/>
<div>{{ car.replace(/_/g, ' ').split(':')[0] }}</div>
</li>
</ul>
</div>
</div>
</li>
</ul>
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import imageMixin from '../../mixins/imageMixin';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
export default defineComponent({
props: {
timetableHistory: {
type: Array as PropType<TimetableHistory[]>,
required: true,
},
},
mixins: [dateMixin, imageMixin, modalTrainMixin],
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
sceneryList: this.getSceneryList(timetable),
showStock: ref(false),
}));
},
},
methods: {
getSceneryList(timetable: TimetableHistory) {
return timetable.sceneriesString.split('%').map((name, i) => {
const beginDateHTML =
' (o. ' +
(timetable.beginDate != timetable.scheduledBeginDate
? `<s class='text--grayed'>${this.localeTime(timetable.beginDate, this.$i18n.locale)}</s> `
: '') +
`<span>${this.localeTime(timetable.scheduledBeginDate, this.$i18n.locale)}</span>)`;
const endDateHTML =
' (p. ' +
(timetable.endDate != timetable.scheduledEndDate && timetable.fulfilled
? `<s class='text--grayed'>${this.localeTime(
timetable.fulfilled ? timetable.endDate : timetable.scheduledEndDate,
this.$i18n.locale
)}</s> `
: '') +
`<span>${this.localeTime(
timetable.fulfilled || (timetable.terminated && !timetable.fulfilled)
? timetable.scheduledEndDate
: timetable.endDate,
this.$i18n.locale
)}</span>)`;
const abandonedDateHTML = ` (porz. ${this.localeTime(
timetable.fulfilled ? timetable.scheduledEndDate : timetable.endDate,
this.$i18n.locale
)})`;
return { name, confirmed: i < timetable.confirmedStopsCount, beginDateHTML, endDateHTML, abandonedDateHTML };
});
},
showTimetable(timetable: TimetableHistory) {
if (timetable.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString());
},
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = this.getImage('unknown.png');
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/variables.scss';
@import '../../styles/responsive.scss';
@import '../../styles/badge.scss';
@import '../../styles/JournalSection.scss';
hr {
margin: 0.25em 0;
}
.info {
&-date {
margin-right: 0.5em;
}
&-status {
padding: 0.05em 0.35em;
color: black;
&.terminated {
background-color: salmon;
}
&.fulfilled {
background-color: lightgreen;
}
&.active {
background-color: lightblue;
}
}
&-top {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
&-route {
margin: 0.25em 0;
}
&-extended {
margin-top: 0.5em;
}
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 0.5em;
margin-top: 1em;
li > div {
text-align: center;
color: #aaa;
font-size: 0.9em;
}
}
.scenery-list {
color: #adadad;
span.confirmed {
color: #a3eba3;
}
}
.btn--show {
display: flex;
margin-top: 1em;
font-weight: bold;
padding: 0.2em 0.45em;
img {
height: 1.3em;
}
}
.info-badge {
span:last-child {
color: black;
background-color: $accentCol;
}
}
@include smallScreen {
.info-top {
flex-direction: column;
span {
margin: 0.1em auto;
}
}
.info-extended {
text-align: center;
}
.info-route {
display: flex;
justify-content: center;
}
.btn--show {
margin: 1em auto 0 auto;
}
}
</style>
@@ -1,112 +1,112 @@
<template> <template>
<section class="scenery-dispatchers-history scenery-section"> <section class="scenery-dispatchers-history scenery-section">
<Loading v-if="dataStatus != 2" /> <Loading v-if="dataStatus != 2" />
<div class="list-warning" v-else-if="dispatcherHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div> <div class="list-warning" v-else-if="dispatcherHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div>
<ul class="history-list" v-else> <ul class="history-list" v-else>
<li class="list-item" v-for="historyItem in dispatcherHistoryList"> <li class="list-item" v-for="historyItem in dispatcherHistoryList">
<div> <div>
<span class="text--grayed">#{{ historyItem.stationHash }}&nbsp;</span> <router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
<b class="text--primary">{{ historyItem.dispatcherName }}</b> <span class="text--grayed">#{{ historyItem.stationHash }}&nbsp;</span>
</div> <b>{{ historyItem.dispatcherName }}</b>
</router-link>
<div v-if="historyItem.timestampTo"> </div>
<b>{{ $d(historyItem.timestampFrom) }}</b>
<div v-if="historyItem.timestampTo">
{{ timestampToString(historyItem.timestampFrom) }} <b>{{ $d(historyItem.timestampFrom) }}</b>
- {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }})
</div> {{ timestampToString(historyItem.timestampFrom) }}
- {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }})
<div class="dispatcher-online" v-else> </div>
{{ $t('journal.online-since') }}
<b>{{ timestampToString(historyItem.timestampFrom) }}</b> <div class="dispatcher-online" v-else>
({{ calculateDuration(historyItem.currentDuration) }}) {{ $t('journal.online-since') }}
<span></span> <b>{{ timestampToString(historyItem.timestampFrom) }}</b>
</div> ({{ calculateDuration(historyItem.currentDuration) }})
</li> </div>
</ul> </li>
</section> </ul>
</template> </section>
</template>
<script lang="ts">
import dateMixin from '@/mixins/dateMixin'; <script lang="ts">
import { DataStatus } from '@/scripts/enums/DataStatus'; import axios from 'axios';
import { DispatcherHistory } from '@/scripts/interfaces/api/DispatchersAPIData'; import { defineComponent, PropType } from 'vue';
import Station from '@/scripts/interfaces/Station'; import dateMixin from '../../mixins/dateMixin';
import { URLs } from '@/scripts/utils/apiURLs'; import { DataStatus } from '../../scripts/enums/DataStatus';
import axios from 'axios'; import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
import { defineComponent, PropType } from 'vue'; import Station from '../../scripts/interfaces/Station';
import Loading from '../Global/Loading.vue'; import { URLs } from '../../scripts/utils/apiURLs';
import Loading from '../Global/Loading.vue';
export default defineComponent({
name: 'SceneryDispatchersHistory', export default defineComponent({
mixins: [dateMixin], name: 'SceneryDispatchersHistory',
props: { mixins: [dateMixin],
station: { props: {
type: Object as PropType<Station>, station: {
required: true, type: Object as PropType<Station>,
}, required: true,
}, },
data() { },
return { data() {
dispatcherHistoryList: [] as DispatcherHistory[], return {
dataStatus: DataStatus.Loading, dispatcherHistoryList: [] as DispatcherHistory[],
}; dataStatus: DataStatus.Loading,
}, };
mounted() { },
this.fetchAPIData(); mounted() {
}, this.fetchAPIData();
methods: { },
async fetchAPIData(countFrom = 0, countLimit = 30) { methods: {
try { async fetchAPIData(countFrom = 0, countLimit = 30) {
const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`; try {
const historyAPIData: DispatcherHistory[] = await (await axios.get(requestString)).data; const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
const historyAPIData: DispatcherHistory[] = await (await axios.get(requestString)).data;
this.dispatcherHistoryList = historyAPIData;
this.dataStatus = DataStatus.Loaded; this.dispatcherHistoryList = historyAPIData;
this.dataStatus = DataStatus.Loaded;
console.log(this.dispatcherHistoryList); } catch (error) {
} catch (error) { console.error(error);
console.error(error); }
} },
}, },
}, components: { Loading },
components: { Loading }, });
}); </script>
</script>
<style lang="scss" scoped>
<style lang="scss" scoped> @import '../../styles/responsive.scss';
@import '../../styles/responsive.scss'; @import '../../styles/SceneryView/styles.scss';
@import '../../styles/SceneryView/styles.scss';
.history-list {
padding: 0 0.5em;
.history-list { }
padding: 0 0.5em;
} .list-item {
display: flex;
.list-item { flex-wrap: wrap;
display: flex; justify-content: space-between;
flex-wrap: wrap;
justify-content: space-between; text-align: left;
background-color: #353535;
text-align: left; padding: 0.5em;
background-color: #353535; margin: 0.5em 0;
padding: 0.5em;
margin: 0.5em 0; line-height: 1.5em;
}
line-height: 1.5em;
} .dispatcher-online {
color: springgreen;
.dispatcher-online { }
color: springgreen;
} @include smallScreen {
.history-list {
@include smallScreen { font-size: 1.1em;
.list-item { }
align-items: center; .list-item {
flex-direction: column; align-items: center;
} flex-direction: column;
} }
</style> }
</style>
+5 -16
View File
@@ -1,12 +1,8 @@
<template> <template>
<section class="info-header"> <section class="info-header">
<div class="scenery-name"> <a class="scenery-name" :href="station.generalInfo?.url">
<a v-if="station.generalInfo?.url" :href="station.generalInfo.url" target="_blank" rel="noopener noreferrer"> {{ station.name }}
{{ station.name }} </a>
</a>
<span v-else>{{ station.name }}</span>
</div>
<div class="scenery-hash" v-if="station.onlineInfo?.hash">#{{ station.onlineInfo.hash }}</div> <div class="scenery-hash" v-if="station.onlineInfo?.hash">#{{ station.onlineInfo.hash }}</div>
</section> </section>
@@ -14,8 +10,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import Station from '../../scripts/interfaces/Station';
import Station from '@/scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -33,18 +28,12 @@ export default defineComponent({
.scenery-name { .scenery-name {
font-weight: bold; font-weight: bold;
color: $accentCol;
position: relative; position: relative;
font-size: 3.5em; font-size: 3em;
padding: 0 0.5em;
text-transform: uppercase; text-transform: uppercase;
@include smallScreen() {
font-size: 2.75em;
}
} }
.scenery-hash { .scenery-hash {
+12 -41
View File
@@ -11,11 +11,6 @@
<span v-if="station.generalInfo.reqLevel > -1"> <span v-if="station.generalInfo.reqLevel > -1">
- {{ $tc('scenery.req-level', station.generalInfo.reqLevel, { lvl: station.generalInfo.reqLevel }) }} - {{ $tc('scenery.req-level', station.generalInfo.reqLevel, { lvl: station.generalInfo.reqLevel }) }}
</span> </span>
<!-- <span v-if="station.generalInfo.reqLevel > 0">
- minimum {{ station.generalInfo.reqLevel }} poziom dyżurnego
</span>
<span v-else-if="station.generalInfo.reqLevel == 0">- dla wszystkich poziomów</span> -->
</span> </span>
<span> <span>
@@ -41,12 +36,17 @@
<b> {{ $tc('scenery.authors-title', station.generalInfo.authors.length) }}: </b> <b> {{ $tc('scenery.authors-title', station.generalInfo.authors.length) }}: </b>
{{ station.generalInfo.authors.join(', ') }} {{ station.generalInfo.authors.join(', ') }}
</div> </div>
<br />
<div class="scenery-topic" v-if="station.generalInfo.url">
<a :href="station.generalInfo.url" target="_blank">
&gt; {{ $t('scenery.forum-topic', { name: station.name }) }} &lt;
</a>
</div>
</div> </div>
<div style="margin: 2em 0; height: 2px; background-color: white" /> <div style="margin: 2em 0; height: 2px; background-color: white" />
<!-- info stats -->
<!-- <scenery-info-stats :station="station" /> -->
<!-- info dispatcher --> <!-- info dispatcher -->
<scenery-info-dispatcher :station="station" :onlineFrom="onlineFrom" /> <scenery-info-dispatcher :station="station" :onlineFrom="onlineFrom" />
@@ -57,10 +57,6 @@
<!-- spawn list --> <!-- spawn list -->
<scenery-info-spawn-list :station="station" /> <scenery-info-spawn-list :station="station" />
</div> </div>
<!-- info icons -->
<!-- info routes -->
</section> </section>
</div> </div>
</template> </template>
@@ -74,8 +70,8 @@ import SceneryInfoStats from './SceneryInfo/SceneryInfoStats.vue';
import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue'; import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue';
import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue'; import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue'; import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
import Station from '../../scripts/interfaces/Station';
import Station from '@/scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -103,6 +99,7 @@ export default defineComponent({
<style lang="scss"> <style lang="scss">
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/badge.scss';
h3.section-header { h3.section-header {
margin: 0.5em 0; margin: 0.5em 0;
@@ -112,7 +109,7 @@ h3.section-header {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 1.5em; font-size: 1.2em;
img { img {
width: 1.1em; width: 1.1em;
@@ -130,7 +127,6 @@ h3.section-header {
.info-general { .info-general {
margin-top: 1em; margin-top: 1em;
font-size: 1.1em;
} }
.general-list { .general-list {
@@ -143,32 +139,7 @@ h3.section-header {
} }
} }
.badge { .scenery-topic a {
font-weight: 600; font-weight: bold;
display: inline-block;
padding: 0;
background: #585858;
margin: 0.25em;
span {
display: inline-block;
padding: 0.2em 0.4em;
}
&-none {
font-weight: 600;
padding: 0.2em 0.4em;
background: firebrick;
text-align: center;
@include smallScreen() {
font-size: 1em;
}
}
} }
</style> </style>
@@ -13,7 +13,7 @@
</router-link> </router-link>
<span class="dispatcher_likes text--primary"> <span class="dispatcher_likes text--primary">
<img :src="icons.like" alt="icon-like" /> <img :src="getIcon('like')" alt="icon-like" />
<span>{{ station.onlineInfo?.dispatcherRate || '0' }}</span> <span>{{ station.onlineInfo?.dispatcherRate || '0' }}</span>
</span> </span>
</div> </div>
@@ -35,14 +35,14 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import dateMixin from '../../../mixins/dateMixin';
import styleMixin from '@/mixins/styleMixin'; import imageMixin from '../../../mixins/imageMixin';
import Station from '@/scripts/interfaces/Station'; import routerMixin from '../../../mixins/routerMixin';
import dateMixin from '@/mixins/dateMixin'; import styleMixin from '../../../mixins/styleMixin';
import routerMixin from '@/mixins/routerMixin'; import Station from '../../../scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
mixins: [styleMixin, dateMixin, routerMixin], mixins: [styleMixin, dateMixin, routerMixin, imageMixin],
props: { props: {
station: { station: {
type: Object as () => Station, type: Object as () => Station,
@@ -54,13 +54,6 @@ export default defineComponent({
default: -1, default: -1,
}, },
}, },
data: () => ({
icons: {
spawn: require('@/assets/icon-spawn.svg'),
like: require('@/assets/icon-like.svg'),
},
}),
}); });
</script> </script>
@@ -71,6 +64,7 @@ export default defineComponent({
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5em;
.dispatcher { .dispatcher {
font-size: 2em; font-size: 2em;
@@ -89,17 +83,15 @@ export default defineComponent({
} }
&_name { &_name {
margin-right: 0.4em;
cursor: pointer; cursor: pointer;
margin-right: 0.25em;
} }
&_likes { &_likes {
img { img {
height: 0.7em; height: 0.7em;
margin-right: 0.25em; margin: 0 0.25em;
} }
margin-right: 1.5em;
} }
} }
@@ -20,7 +20,7 @@
<img <img
v-if="station.generalInfo?.SUP" v-if="station.generalInfo?.SUP"
class="icon-info" class="icon-info"
:src="require(`@/assets/icon-SUP.svg`)" :src="getIcon('SUP')"
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="require(`@/assets/icon-${station.generalInfo.signalType}.svg`)" :src="getIcon(station.generalInfo.signalType)"
: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="icons.lock" :src="getIcon('lock')"
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="icons.unavailable" :src="getIcon('unavailable')"
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="icons.abandoned" :src="getIcon('abandoned')"
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="icons.real" :src="getIcon('real')"
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="icons.unknown" :src="getIcon('unknown')"
alt="icon-unknown" alt="icon-unknown"
:title="$t('desc.unknown')" :title="$t('desc.unknown')"
/> />
@@ -77,31 +77,19 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import stationInfoMixin from '@/mixins/stationInfoMixin'; import imageMixin from '../../../mixins/imageMixin';
import stationInfoMixin from '../../../mixins/stationInfoMixin';
import Station from '@/scripts/interfaces/Station'; import styleMixin from '../../../mixins/styleMixin';
import styleMixin from '@/mixins/styleMixin'; import Station from '../../../scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
mixins: [stationInfoMixin, styleMixin], mixins: [stationInfoMixin, styleMixin, imageMixin],
props: { props: {
station: { station: {
type: Object as () => Station, type: Object as () => Station,
default: {}, default: {},
}, },
}, },
data: () => ({
icons: {
td2: require('@/assets/icon-td2.svg'),
lock: require('@/assets/icon-lock.svg'),
unavailable: require('@/assets/icon-unavailable.svg'),
unknown: require('@/assets/icon-unknown.svg'),
abandoned: require('@/assets/icon-abandoned.svg'),
real: require('@/assets/icon-real.svg'),
},
}),
}); });
</script> </script>
@@ -130,3 +118,4 @@ export default defineComponent({
} }
} }
</style> </style>
@@ -57,8 +57,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Station from '@/scripts/interfaces/Station';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import Station from '../../../scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -1,7 +1,7 @@
<template> <template>
<section class="info-spawn-list"> <section class="info-spawn-list">
<h3 class="spawn-header section-header"> <h3 class="spawn-header section-header">
<img :src="icons.spawn" alt="icon-spawn" /> <img :src="getIcon('spawn')" alt="icon-spawn" />
&nbsp;{{ $t('scenery.spawns') }} &nbsp; &nbsp;{{ $t('scenery.spawns') }} &nbsp;
<span class="text--primary">{{ station.onlineInfo?.spawns.length || '0' }}</span> <span class="text--primary">{{ station.onlineInfo?.spawns.length || '0' }}</span>
</h3> </h3>
@@ -24,22 +24,19 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Station from '@/scripts/interfaces/Station';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import imageMixin from '../../../mixins/imageMixin';
import Station from '../../../scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
mixins: [imageMixin],
props: { props: {
station: { station: {
type: Object as () => Station, type: Object as () => Station,
default: {}, default: {},
}, },
}, },
data: () => ({
icons: {
spawn: require('@/assets/icon-spawn.svg'),
},
}),
}); });
</script> </script>
@@ -1,24 +1,24 @@
<template> <template>
<section class="info-stats" :class="!station.onlineInfo ? 'no-stats' : ''"> <section class="info-stats" :class="!station.onlineInfo ? 'no-stats' : ''">
<span class="likes"> <span class="likes">
<img :src="icons.like" alt="icon-like" /> <img :src="getIcon('like')" alt="icon-like" />
<span>{{ station.onlineInfo?.dispatcherRate || '0' }}</span> <span>{{ station.onlineInfo?.dispatcherRate || '0' }}</span>
</span> </span>
<span class="users"> <span class="users">
<img :src="icons.user" alt="icon-user" /> <img :src="getIcon('user')" alt="icon-user" />
<span>{{ station.onlineInfo?.currentUsers || '0' }}</span> <span>{{ station.onlineInfo?.currentUsers || '0' }}</span>
/ /
<span>{{ station.onlineInfo?.maxUsers || '0' }}</span> <span>{{ station.onlineInfo?.maxUsers || '0' }}</span>
</span> </span>
<span class="spawns"> <span class="spawns">
<img :src="icons.spawn" alt="icon-spawn" /> <img :src="getIcon('spawn')" alt="icon-spawn" />
<span>{{ station.onlineInfo?.spawns.length || '0' }}</span> <span>{{ station.onlineInfo?.spawns.length || '0' }}</span>
</span> </span>
<span class="schedules"> <span class="schedules">
<img :src="icons.timetable" alt="icon-timetable" /> <img :src="getIcon('timetable')" alt="icon-timetable" />
<span> <span>
<span style="color: #eee">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span> <span style="color: #eee">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span>
/ /
@@ -32,25 +32,17 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import imageMixin from '../../../mixins/imageMixin';
import Station from '@/scripts/interfaces/Station'; import Station from '../../../scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
mixins: [imageMixin],
props: { props: {
station: { station: {
type: Object as () => Station, type: Object as () => Station,
default: {}, default: {},
}, },
}, },
data: () => ({
icons: {
like: require('@/assets/icon-like.svg'),
timetable: require('@/assets/icon-timetable.svg'),
user: require('@/assets/icon-user.svg'),
spawn: require('@/assets/icon-spawn.svg'),
},
}),
}); });
</script> </script>
@@ -83,7 +75,7 @@ export default defineComponent({
} }
span > img { span > img {
width: 1.2em; width: 1.2em;
margin-right: 0.5em; margin-right: 0.5em;
} }
} }
@@ -1,7 +1,7 @@
<template> <template>
<section class="info-user-list"> <section class="info-user-list">
<h3 class="user-header section-header"> <h3 class="user-header section-header">
<img :src="icons.user" alt="icon-user" /> <img :src="getIcon('user')" alt="icon-user" />
&nbsp;{{ $t('scenery.users') }} &nbsp; &nbsp;{{ $t('scenery.users') }} &nbsp;
<span class="text--primary">{{ station.onlineInfo?.currentUsers || '0' }}</span <span class="text--primary">{{ station.onlineInfo?.currentUsers || '0' }}</span
>&nbsp;/&nbsp;<span class="text--primary">{{ station.onlineInfo?.maxUsers || '0' }}</span> >&nbsp;/&nbsp;<span class="text--primary">{{ station.onlineInfo?.maxUsers || '0' }}</span>
@@ -11,10 +11,10 @@
v-for="(train, i) in computedStationTrains" v-for="(train, i) in computedStationTrains"
class="badge user" class="badge user"
:class="train.stopStatus" :class="train.stopStatus"
:key="train.trainNo + i" :key="train.trainId"
tabindex="0" tabindex="0"
@click="navigateTo('/trains', { trainNo: train.trainNo, driverName: train.driverName })" @click="selectModalTrain(train.trainId)"
@keydown.enter="navigateTo('/trains', { trainNo: train.trainNo, driverName: train.driverName })" @keydown.enter="selectModalTrain(train.trainId)"
> >
<span class="user_train">{{ train.trainNo }}</span> <span class="user_train">{{ train.trainNo }}</span>
<span class="user_name">{{ train.driverName }}</span> <span class="user_name">{{ train.driverName }}</span>
@@ -27,12 +27,16 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import routerMixin from '@/mixins/routerMixin';
import Station from '@/scripts/interfaces/Station';
import { computed, defineComponent } from 'vue'; import { computed, defineComponent } from 'vue';
import imageMixin from '../../../mixins/imageMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
import routerMixin from '../../../mixins/routerMixin';
import Station from '../../../scripts/interfaces/Station';
import { useStore } from '../../../store/store';
export default defineComponent({ export default defineComponent({
mixins: [routerMixin], mixins: [routerMixin, imageMixin, modalTrainMixin],
props: { props: {
station: { station: {
@@ -42,6 +46,8 @@ export default defineComponent({
}, },
setup(props) { setup(props) {
const store = useStore();
const computedStationTrains = computed(() => { const computedStationTrains = computed(() => {
if (!props.station) return []; if (!props.station) return [];
@@ -59,14 +65,8 @@ export default defineComponent({
}); });
}); });
return { computedStationTrains }; return { computedStationTrains, store };
}, },
data: () => ({
icons: {
user: require('@/assets/icon-user.svg'),
},
}),
}); });
</script> </script>
@@ -130,3 +130,4 @@ $disconnected: slategray;
} }
} }
</style> </style>
+189 -202
View File
@@ -2,7 +2,7 @@
<section class="scenery-timetable"> <section class="scenery-timetable">
<div class="timetable-header"> <div class="timetable-header">
<h3> <h3>
<img :src="icons.timetable" alt="icon-timetable" />&nbsp; <img :src="getIcon('timetable')" alt="icon-timetable" />&nbsp;
<span>{{ $t('scenery.timetables') }}</span> <span>{{ $t('scenery.timetables') }}</span>
&nbsp; &nbsp;
<span class="text--primary">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span> <span class="text--primary">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span>
@@ -13,181 +13,183 @@
</h3> </h3>
<div class="timetable-checkpoints" v-if="station && station.generalInfo?.checkpoints"> <div class="timetable-checkpoints" v-if="station && station.generalInfo?.checkpoints">
<button <span v-for="(cp, i) in station.generalInfo.checkpoints" :key="i">
v-for="cp in station.generalInfo.checkpoints" {{ (i > 0 && '&bull;') || '' }}
:key="cp.checkpointName"
class="checkpoint_item btn btn--text" <button
:class="{ current: selectedCheckpoint === cp.checkpointName }" :key="cp.checkpointName"
@click="selectCheckpoint(cp)" class="checkpoint_item"
> :class="{ current: selectedCheckpoint === cp.checkpointName }"
{{ cp.checkpointName }} @click="selectCheckpoint(cp)"
</button> >
{{ cp.checkpointName }}
</button>
</span>
</div> </div>
</div> </div>
<div class="timetable-list"> <div class="timetable-list">
<!-- <transition name="scenery-timetable-list-anim" mode="out-in"> -->
<!-- <div :key="store.dataStatuses.trains + selectedCheckpoint" class="scenery-timetable-list"> -->
<div style="padding-bottom: 5em" v-if="store.dataStatuses.trains == 0 && computedScheduledTrains.length == 0"> <div style="padding-bottom: 5em" v-if="store.dataStatuses.trains == 0 && computedScheduledTrains.length == 0">
<Loading /> <Loading />
</div> </div>
<span class="timetable-item empty" v-else-if="computedScheduledTrains.length == 0 && !station.onlineInfo">
{{ $t('scenery.offline') }}
</span>
<span class="timetable-item empty" v-else-if="computedScheduledTrains.length == 0"> <span class="timetable-item empty" v-else-if="computedScheduledTrains.length == 0">
{{ $t('scenery.no-timetables') }} {{ $t('scenery.no-timetables') }}
</span> </span>
<div <transition-group name="timetables-anim">
class="timetable-item" <div
v-for="(scheduledTrain, i) in computedScheduledTrains" class="timetable-item"
:key="i + 1" v-for="(scheduledTrain, i) in computedScheduledTrains"
tabindex="0" :key="scheduledTrain.trainId"
@click="navigateTo('/trains', { trainNo: scheduledTrain.trainNo, driverName: scheduledTrain.driverName })" tabindex="0"
@keydown.enter=" @click.prevent.stop="selectModalTrain(scheduledTrain.trainId)"
navigateTo('/trains', { @keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId)"
trainNo: scheduledTrain.trainNo, >
driverName: scheduledTrain.driverName, <span class="timetable-general">
}) <span class="general-info">
" <span class="info-number">
> <strong>{{ scheduledTrain.category }}</strong>
<span class="timetable-general"> {{ scheduledTrain.trainNo }}
<span class="general-info">
<span class="info-number">
<strong>{{ scheduledTrain.category }}</strong>
{{ scheduledTrain.trainNo }}
<span class="g-tooltip" v-if="scheduledTrain.stopInfo.comments"> <span class="g-tooltip" v-if="scheduledTrain.stopInfo.comments">
<img :src="icons.warning" /> <img :src="getIcon('warning')" />
<span class="content" v-html="scheduledTrain.stopInfo.comments"> </span> <span class="content" v-html="scheduledTrain.stopInfo.comments"> </span>
</span>
</span>
&nbsp;|&nbsp;
<span style="color: white">
{{ scheduledTrain.driverName }}
</span>
&nbsp;|&nbsp;
<span class="general-status">
<span :class="scheduledTrain.stopStatus">
{{ $t(`timetables.${scheduledTrain.stopStatus}`) }}
<span v-if="scheduledTrain.stopStatus == 'arriving'"> {{ scheduledTrain.prevStationName }}</span>
<span v-if="scheduledTrain.stopStatus.startsWith('departed')">{{
scheduledTrain.nextStationName
}}</span>
</span>
</span>
<div class="info-route">
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
</div>
</span>
</span>
<span class="timetable-schedule">
<span class="schedule-arrival">
<span class="arrival-time begins" v-if="scheduledTrain.stopInfo.beginsHere">
{{ $t('timetables.begins') }}
</span>
<span class="arrival-time" v-else>
<div v-if="scheduledTrain.stopInfo.arrivalDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
}}</s>
</div>
<span>
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : '' }}{{ scheduledTrain.stopInfo.arrivalDelay }})
</span> </span>
</div> </span>
</span> &nbsp;|&nbsp;
</span> <span>
{{ scheduledTrain.driverName }}
<span class="schedule-stop">
<span class="stop-time">
<span v-if="scheduledTrain.stopInfo.stopTime">
{{ scheduledTrain.stopInfo.stopTime }}
{{ scheduledTrain.stopInfo.stopType || 'pt' }}
</span> </span>
<span v-else>&nbsp;</span> <div class="info-route">
</span> <strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
</div>
<span class="arrow"></span> <ScheduledTrainStatus :scheduledTrain="scheduledTrain" />
<span class="stop-line">
{{ scheduledTrain.arrivingLine }}
{{ scheduledTrain.arrivingLine && scheduledTrain.departureLine && '&gt;' }}
{{ scheduledTrain.departureLine }}
</span> </span>
</span> </span>
<span class="schedule-departure"> <span class="timetable-schedule">
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere"> <span class="schedule-arrival">
{{ $t('timetables.terminates') }} <span class="arrival-time begins" v-if="scheduledTrain.stopInfo.beginsHere">
</span> {{ $t('timetables.begins') }}
</span>
<span class="departure-time" v-else> <span class="arrival-time" v-else>
<div v-if="scheduledTrain.stopInfo.departureDelay == 0"> <div v-if="scheduledTrain.stopInfo.arrivalDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span> <span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
}}</s>
</div> </div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
}}</s>
</div>
<span> <span>
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }} {{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : '' ({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.departureDelay }}) }}{{ scheduledTrain.stopInfo.arrivalDelay }})
</span>
</div>
</span>
</span>
<span class="schedule-stop">
<span class="stop-time">
<span v-if="scheduledTrain.stopInfo.stopTime">
{{ scheduledTrain.stopInfo.stopTime }}
{{ scheduledTrain.stopInfo.stopType || 'pt' }}
</span> </span>
</div>
<span v-else>&nbsp;</span>
</span>
<span class="arrow"></span>
<span class="stop-line">
<span>
{{ scheduledTrain.arrivingLine }}
</span>
<span></span>
<span>
{{ scheduledTrain.departureLine }}
</span>
</span>
</span>
<span class="schedule-departure">
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere">
{{ $t('timetables.terminates') }}
</span>
<span class="departure-time" v-else>
<div v-if="scheduledTrain.stopInfo.departureDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
}}</s>
</div>
<span>
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }}
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.departureDelay }})
</span>
</div>
</span>
</span> </span>
</span> </span>
</span> </div>
</div> </transition-group>
</div> </div>
<!-- </transition> -->
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import Station from '@/scripts/interfaces/Station';
import SelectBox from '../Global/SelectBox.vue'; import SelectBox from '../Global/SelectBox.vue';
import { computed, defineComponent, PropType, ref } from '@vue/runtime-core'; import { computed, defineComponent, PropType, ref } from '@vue/runtime-core';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import dateMixin from '@/mixins/dateMixin';
import routerMixin from '@/mixins/routerMixin';
import { useStore } from '@/store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import TrainModal from '../Global/TrainModal.vue';
import dateMixin from '../../mixins/dateMixin';
import routerMixin from '../../mixins/routerMixin';
import Station from '../../scripts/interfaces/Station';
import { useStore } from '../../store/store';
import imageMixin from '../../mixins/imageMixin';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import ScheduledTrain from '../../scripts/interfaces/ScheduledTrain';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetable', name: 'SceneryTimetable',
components: { SelectBox, Loading }, components: { SelectBox, Loading, TrainModal, ScheduledTrainStatus },
mixins: [dateMixin, routerMixin], mixins: [dateMixin, routerMixin, imageMixin, modalTrainMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station>, type: Object as PropType<Station>,
required: true, required: true,
}, },
timetableOnly: {
type: Boolean,
},
}, },
data: () => ({ data: () => ({
viewIcon: require('@/assets/icon-view.svg'),
listOpen: false, listOpen: false,
icons: {
warning: require('@/assets/icon-warning.svg'),
timetable: require('@/assets/icon-timetable.svg'),
},
}), }),
setup(props) { setup(props) {
@@ -251,6 +253,10 @@ export default defineComponent({
selectCheckpoint(cp: { checkpointName: string }) { selectCheckpoint(cp: { checkpointName: string }) {
this.selectedCheckpoint = cp.checkpointName; this.selectedCheckpoint = cp.checkpointName;
}, },
showTimetableOnlyView() {
this.$router.push(`${this.$route.fullPath}&timetableOnly=1`);
},
}, },
mounted() { mounted() {
@@ -267,11 +273,21 @@ export default defineComponent({
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
// .scenery-timetable { .timetables-anim-move,
// height: 85vh; .timetables-anim-enter-active,
// max-height: 900px; .timetables-anim-leave-active {
// min-height: 450px; transition: all 250ms ease;
// } }
.timetables-anim-enter-from,
.timetables-anim-leave-to {
opacity: 0;
transform: translateY(30px);
}
.timetables-anim-leave-active {
position: absolute;
}
.scenery-timetable { .scenery-timetable {
height: 100%; height: 100%;
@@ -294,7 +310,7 @@ export default defineComponent({
h3 { h3 {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 1.4em; font-size: 1.3em;
} }
} }
@@ -305,12 +321,14 @@ export default defineComponent({
&-item { &-item {
margin: 0.5em auto; margin: 0.5em auto;
padding: 0 0.5em; padding: 0.5em;
max-width: 1100px; max-width: 1100px;
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 0 0.5em; gap: 2em 0.5em;
overflow: hidden;
background: #353535; background: #353535;
@@ -325,9 +343,6 @@ export default defineComponent({
} }
&-general { &-general {
padding: 0.5rem 0;
border-radius: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -338,6 +353,10 @@ export default defineComponent({
&-schedule { &-schedule {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(30px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(30px, 1fr));
width: 100%;
max-width: 400px;
margin: 0 auto;
} }
} }
@@ -352,17 +371,15 @@ export default defineComponent({
flex-wrap: wrap; flex-wrap: wrap;
font-size: 1.1em; font-size: 1.1em;
padding: 0.75em 0; padding: 0.75em 0;
.checkpoint_item {
&.current {
font-weight: bold;
color: $accentCol;
}
&:not(:last-child)::after { button.checkpoint_item {
margin: 0 0.5em; color: #aaa;
content: '•'; display: inline;
color: white; }
}
.checkpoint_item.current {
font-weight: bold;
color: $accentCol;
} }
} }
@@ -402,7 +419,6 @@ export default defineComponent({
} }
.info-route { .info-route {
margin-top: 0.5em;
width: 100%; width: 100%;
} }
@@ -418,38 +434,6 @@ export default defineComponent({
} }
} }
.general-status {
text-align: right;
span.arriving {
color: #ccc;
}
span.departed {
color: lime;
font-weight: bold;
&-away {
font-weight: bold;
color: #5ecc5e;
}
}
span.stopped {
color: #ffa600;
font-weight: bold;
}
span.online {
color: gold;
}
span.terminated {
color: salmon;
font-weight: bold;
}
}
.schedule { .schedule {
&-arrival, &-arrival,
&-stop, &-stop,
@@ -459,23 +443,40 @@ export default defineComponent({
align-items: center; align-items: center;
margin: 0 0.3rem; margin: 0 0.3rem;
font-size: 1.1em; font-size: 1.15em;
} }
&-stop { &-stop {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-size: 0.85em; font-size: 0.9em;
padding: 0.3em 0; padding: 0.3em 0;
.stop-line { .stop-line {
margin-top: 0.25em; display: flex;
position: absolute;
span {
width: 65px;
word-break: break-all;
}
span:first-child {
text-align: right;
}
span:last-child {
text-align: left;
}
} }
.stop-time { .stop-time {
transform: translateY(-0.25em); position: absolute;
transform: translateY(-15px);
color: $accentCol;
} }
} }
} }
@@ -500,23 +501,9 @@ export default defineComponent({
} }
} }
@include smallScreen() { @include smallScreen {
.timetable { .timetable-item {
&-item { grid-template-columns: 1fr;
display: flex;
flex-direction: column;
align-items: center;
font-size: 1.05em;
}
&-general {
width: 100%;
}
&-schedule {
width: 100%;
}
} }
} }
</style> </style>
@@ -1,110 +1,112 @@
<template> <template>
<section class="scenery-timetables-history scenery-section"> <section class="scenery-timetables-history scenery-section">
<Loading v-if="dataStatus != 2" /> <Loading v-if="dataStatus != 2" />
<div class="list-warning" v-else-if="sceneryHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div> <div class="list-warning" v-else-if="sceneryHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div>
<ul class="history-list" v-else> <ul class="history-list" v-else>
<li class="list-item" v-for="historyItem in sceneryHistoryList"> <li class="list-item" v-for="historyItem in sceneryHistoryList">
<div> <div>
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b> <b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
{{ localeTime(historyItem.beginDate, $i18n.locale) }} {{ localeTime(historyItem.beginDate, $i18n.locale) }}
</div> </div>
<div>
<span class="text--grayed"> #{{ historyItem.timetableId }} </span> <div>
<b class="text--primary">&nbsp;{{ historyItem.trainCategoryCode }} {{ historyItem.trainNo }}</b> <router-link :to="`/journal/timetables?timetableId=${historyItem.timetableId}`">
<div>{{ historyItem.driverName }}</div> <span class="text--grayed"> #{{ historyItem.timetableId }} </span>
</div> <b class="text--primary">&nbsp;{{ historyItem.trainCategoryCode }} {{ historyItem.trainNo }}</b>
<div>{{ historyItem.driverName }}</div>
<div>{{ historyItem.route.replace('|', ' -> ') }}</div> </router-link>
<!-- <div>{{ historyItem.routeDistance }} km</div> --> </div>
<div>
{{ $t('scenery.timetable-author-title') }}: <div>{{ historyItem.route.replace('|', ' -> ') }}</div>
<b v-if="historyItem.authorName">{{ historyItem.authorName }}</b> <!-- <div>{{ historyItem.routeDistance }} km</div> -->
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i> <div>
</div> {{ $t('scenery.timetable-author-title') }}:
<b v-if="historyItem.authorName">{{ historyItem.authorName }}</b>
<!-- <div v-if="historyItem.authorId">{{ historyItem.authorName }}</div> --> <i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
</li> </div>
</ul>
</section> <!-- <div v-if="historyItem.authorId">{{ historyItem.authorName }}</div> -->
</template> </li>
</ul>
<script lang="ts"> </section>
import dateMixin from '@/mixins/dateMixin'; </template>
import { DataStatus } from '@/scripts/enums/DataStatus';
import { SceneryTimetableHistory, TimetableHistory } from '@/scripts/interfaces/api/TimetablesAPIData'; <script lang="ts">
import Station from '@/scripts/interfaces/Station'; import axios from 'axios';
import { URLs } from '@/scripts/utils/apiURLs'; import { defineComponent, PropType } from 'vue';
import axios from 'axios'; import dateMixin from '../../mixins/dateMixin';
import { defineComponent, PropType } from 'vue'; import { DataStatus } from '../../scripts/enums/DataStatus';
import Loading from '../Global/Loading.vue'; import { TimetableHistory, SceneryTimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import Station from '../../scripts/interfaces/Station';
export default defineComponent({ import { URLs } from '../../scripts/utils/apiURLs';
name: 'SceneryTimetablesHistory', import Loading from '../Global/Loading.vue';
mixins: [dateMixin],
props: { export default defineComponent({
station: { name: 'SceneryTimetablesHistory',
type: Object as PropType<Station>, mixins: [dateMixin],
required: true, props: {
}, station: {
}, type: Object as PropType<Station>,
data() { required: true,
return { },
sceneryHistoryList: [] as TimetableHistory[], },
dataStatus: DataStatus.Loading, data() {
}; return {
}, sceneryHistoryList: [] as TimetableHistory[],
mounted() { dataStatus: DataStatus.Loading,
this.fetchAPIData(); };
}, },
methods: { mounted() {
async fetchAPIData(countFrom = 0, countLimit = 15) { this.fetchAPIData();
try { },
const requestString = `${URLs.stacjownikAPI}/api/getSceneryTimetables?name=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`; methods: {
const historyAPIData: SceneryTimetableHistory = await (await axios.get(requestString)).data; async fetchAPIData(countFrom = 0, countLimit = 15) {
try {
this.sceneryHistoryList = historyAPIData.sceneryTimetables; const requestString = `${URLs.stacjownikAPI}/api/getSceneryTimetables?name=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
this.dataStatus = DataStatus.Loaded; const historyAPIData: SceneryTimetableHistory = await (await axios.get(requestString)).data;
} catch (error) {
console.error(error); this.sceneryHistoryList = historyAPIData.sceneryTimetables;
} this.dataStatus = DataStatus.Loaded;
}, } catch (error) {
}, console.error(error);
components: { Loading }, }
}); },
</script> },
components: { Loading },
<style lang="scss" scoped> });
@import '../../styles/responsive.scss'; </script>
@import '../../styles/SceneryView/styles.scss';
<style lang="scss" scoped>
.list-warning { @import '../../styles/responsive.scss';
padding: 1em 0.5em; @import '../../styles/SceneryView/styles.scss';
background-color: #444;
font-size: 1.2em; .list-warning {
} padding: 1em 0.5em;
background-color: #444;
.history-list { font-size: 1.2em;
padding: 0 0.5em; }
}
.history-list {
.list-item { padding: 0 0.5em;
display: grid; }
grid-template-columns: 1fr 2fr 2fr 1fr;
gap: 1em; .list-item {
align-items: center; display: grid;
grid-template-columns: 1fr 2fr 2fr 1fr;
background-color: #353535; gap: 1em;
padding: 0.5em; align-items: center;
margin: 0.5em 0;
background-color: #353535;
line-height: 1.5em; padding: 0.5em;
} margin: 0.5em 0;
@include smallScreen { line-height: 1.5em;
.list-item { }
grid-template-columns: 1fr 1fr;
font-size: 1.05em; @include smallScreen {
} .list-item {
} grid-template-columns: 1fr 1fr;
</style> }
}
</style>
@@ -0,0 +1,88 @@
<template>
<div class="general-status">
<span :class="scheduledTrain.stopStatus">
<span v-if="scheduledTrain.stopStatus == 'arriving'">
<span v-if="scheduledTrain.prevDepartureLine">({{ scheduledTrain.prevDepartureLine }})</span>
{{ scheduledTrain.prevStationName }}
&gt;<span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
{{ scheduledTrain.nextStationName || '---' }}
</span>
<span v-else-if="scheduledTrain.stopStatus == 'departed'">
&gt;&gt; <span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
{{ scheduledTrain.nextStationName }}
</span>
<span v-else-if="scheduledTrain.stopStatus == 'departed-away'">
&gt;&gt;&gt;
<span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
{{ scheduledTrain.nextStationName }}
</span>
<span v-else-if="scheduledTrain.stopStatus == 'online'">
&gt;
<span v-if="scheduledTrain.nextArrivalLine">
({{ scheduledTrain.nextArrivalLine }}) {{ scheduledTrain.nextStationName }}
</span>
<span v-else-if="!scheduledTrain.nextStationName">{{ $t('timetables.end') }}</span>
<span v-else>{{ scheduledTrain.nextStationName }}</span>
</span>
<span v-else-if="scheduledTrain.stopStatus == 'stopped'">
&gt;
<span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
{{ scheduledTrain.nextStationName }}
</span>
<span v-else-if="scheduledTrain.stopStatus == 'terminated'">X {{ $t('timetables.terminated') }}</span>
</span>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import ScheduledTrain from '../../scripts/interfaces/ScheduledTrain';
export default defineComponent({
props: {
scheduledTrain: {
type: Object as PropType<ScheduledTrain>,
required: true,
},
},
});
</script>
<style lang="scss" scoped>
.general-status {
margin-top: 0.5em;
span.arriving {
color: #ccc;
}
span.departed {
color: lime;
font-weight: bold;
&-away {
font-weight: bold;
color: #5ecc5e;
}
}
span.stopped {
color: #ffa600;
font-weight: bold;
}
span.online {
color: gold;
}
span.terminated {
color: salmon;
font-weight: bold;
}
}
</style>
+30 -70
View File
@@ -1,23 +1,12 @@
<template> <template>
<div class="filter-option option"> <button class="btn--action" :class="option.section" :data-selected="option.value" @click="handleChange">
<label> {{ $t(`filters.${option.id}`) }}
<input </button>
type="checkbox"
:name="option.name"
:defaultValue="option.defaultValue"
:id="option.id"
v-model="option.value"
@change="handleChange"
/>
<span v-if="option.id != 'troll'" :class="option.section + (option.value ? ' checked' : '')"
>{{ option.id != 'troll' ? $t(`filters.${option.id}`) : 'ARKADIA ZDRÓJ' }}
</span>
</label>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
interface FilterOption { interface FilterOption {
id: string; id: string;
@@ -34,29 +23,26 @@ export default defineComponent({
required: true, required: true,
}, },
}, },
emits: ['optionChange'],
setup() {
return {
filterStore: useStationFiltersStore(),
};
},
methods: { methods: {
handleChange() { handleChange() {
if (this.option.name == 'troll') { this.option.value = !this.option.value;
location.href = 'https://www.youtube.com/watch?v=HIcSWuKMwOw';
return;
}
this.$emit('optionChange', { this.filterStore.changeFilterValue({
name: this.option.name, name: this.option.name,
value: this.option.value, value: !this.option.value,
}); });
}, },
}, },
setup() {
return {};
},
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/option.scss';
$accessCol: #e03b07; $accessCol: #e03b07;
$controlCol: #0085ff; $controlCol: #0085ff;
$signalCol: #bf7c00; $signalCol: #bf7c00;
@@ -64,63 +50,49 @@ $statusCol: #349b32;
$saveCol: #28a826; $saveCol: #28a826;
$routesCol: #9049c0; $routesCol: #9049c0;
.option span { button {
font-size: 0.9em; width: 100%;
&.checked { padding: 0.4em;
border-radius: 0.4em;
&:focus-visible {
outline: 1px solid white;
}
&[data-selected='true'] {
&.access { &.access {
background-color: $accessCol; background-color: $accessCol;
box-shadow: 0 0 6px 1px $accessCol;
&::before {
box-shadow: 0 0 6px 1px $accessCol;
}
} }
&.control { &.control {
background-color: $controlCol; background-color: $controlCol;
box-shadow: 0 0 6px 1px $controlCol;
&::before {
box-shadow: 0 0 6px 1px $controlCol;
}
} }
&.signals { &.signals {
background-color: $signalCol; background-color: $signalCol;
box-shadow: 0 0 6px 1px $signalCol;
&::before {
box-shadow: 0 0 6px 1px $signalCol;
}
} }
&.routes { &.routes {
background-color: $routesCol; background-color: $routesCol;
box-shadow: 0 0 6px 1px $routesCol;
&::before {
box-shadow: 0 0 6px 1px $routesCol;
}
} }
&.status { &.status {
background-color: $statusCol; background-color: $statusCol;
box-shadow: 0 0 6px 1px $statusCol;
&::before {
box-shadow: 0 0 6px 1px $statusCol;
}
} }
&.save { &.save {
background-color: $saveCol; background-color: $saveCol;
box-shadow: 0 0 6px 1px $saveCol;
&::before {
box-shadow: 0 0 6px 1px $saveCol;
}
} }
&.troll { &.troll {
background-color: firebrick; background-color: firebrick;
box-shadow: 0 0 6px 1px firebrick;
&::before {
box-shadow: 0 0 6px 1px firebrick;
}
} }
&.mode { &.mode {
@@ -129,18 +101,6 @@ $routesCol: #9049c0;
font-weight: 500; font-weight: 500;
} }
&::before {
position: absolute;
content: '';
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 0.5em;
}
} }
} }
</style> </style>
+125 -119
View File
@@ -1,20 +1,35 @@
<template> <template>
<section class="filter-card" v-click-outside="closeCard"> <section class="filter-card" v-click-outside="closeCard" @keydown.esc="closeCard">
<div class="card_btn"> <div class="card_controls">
<button class="btn btn--option" @click="toggleCard"> <button class="btn--filled btn--image" @click="toggleCard">
<img class="button_icon" :src="filterIcon" alt="icon-filter" /> <img class="button_icon" :src="getIcon('filter2')" alt="filter icon" />
{{ $t('options.filters') }} {{ $t('options.filters') }} [F]
</button> </button>
<label for="scenery-search">
<input
id="scenery-search"
list="sceneries"
:placeholder="$t('sceneries.scenery-search')"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
v-model="chosenSearchScenery"
/>
<datalist id="sceneries">
<option v-for="scenery in store.stationList" :value="scenery.name"></option>
</datalist>
</label>
</div> </div>
<transition name="card-anim"> <transition name="card-anim">
<div class="card" v-if="isVisible"> <div class="card" v-if="isVisible" tabindex="0" ref="cardEl">
<div class="card_content"> <div class="card_content">
<div class="card_title flex">{{ $t('filters.title') }}</div> <div class="card_title flex">{{ $t('filters.title') }}</div>
<section class="card_options"> <section class="card_options">
<filter-option <filter-option
v-for="(option, i) in inputs.options" v-for="(option, i) in filterStore.inputs.options"
:option="option" :option="option"
:key="i" :key="i"
@optionChange="handleChange" @optionChange="handleChange"
@@ -23,7 +38,7 @@
<section class="card_timestamp" style="text-align: center"> <section class="card_timestamp" style="text-align: center">
<div>{{ $t('filters.minimum-hours-title') }}</div> <div>{{ $t('filters.minimum-hours-title') }}</div>
<span class="clock"> <span class="clock">
<button @click="subHour">-</button> <button class="btn--action" @click="subHour">-</button>
<span>{{ <span>{{
minimumHours == 0 minimumHours == 0
? $t('filters.now') ? $t('filters.now')
@@ -31,7 +46,7 @@
? minimumHours + $t('filters.hour') ? minimumHours + $t('filters.hour')
: $t('filters.no-limit') : $t('filters.no-limit')
}}</span> }}</span>
<button @click="addHour">+</button> <button class="btn--action" @click="addHour">+</button>
</span> </span>
</section> </section>
@@ -42,11 +57,13 @@
name="authors" name="authors"
v-model="authorsInputValue" v-model="authorsInputValue"
@input="handleAuthorsInput" @input="handleAuthorsInput"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
/> />
</section> </section>
<section class="card_sliders"> <section class="card_sliders">
<div class="slider" v-for="(slider, i) in inputs.sliders" :key="i"> <div class="slider" v-for="(slider, i) in filterStore.inputs.sliders" :key="i">
<input <input
class="slider-input" class="slider-input"
type="range" type="range"
@@ -65,23 +82,13 @@
</section> </section>
<section class="card_actions"> <section class="card_actions">
<div> <div class="action-buttons">
<filter-option <button class="btn--action" style="width: 100%" @click="saveFilters" :data-selected="saveOptions">
@optionChange="saveFilters" {{ $t('filters.save') }}
:option="{ </button>
id: 'save',
name: 'save', <button class="btn--action" @click="resetFilters">{{ $t('filters.reset') }}</button>
section: 'mode', <button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button>
value: saveOptions,
defaultValue: true,
}"
/>
</div>
<div>
<action-button class="outlined" @click="resetFilters">
{{ $t('filters.reset') }}
</action-button>
<action-button class="outlined" @click="closeCard">{{ $t('filters.close') }}</action-button>
</div> </div>
</section> </section>
</div> </div>
@@ -91,23 +98,22 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, inject } from '@vue/runtime-core'; import { defineComponent, inject } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import keyMixin from '../../mixins/keyMixin';
import routerMixin from '../../mixins/routerMixin';
import StorageManager from '../../scripts/managers/storageManager';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useStore } from '../../store/store';
import inputData from '@/data/options.json';
import StorageManager from '@/scripts/managers/storageManager';
import ActionButton from '../Global/ActionButton.vue'; import ActionButton from '../Global/ActionButton.vue';
import FilterOption from './FilterOption.vue'; import FilterOption from './FilterOption.vue';
import { useStore } from '@/store/store';
export default defineComponent({ export default defineComponent({
components: { ActionButton, FilterOption }, components: { ActionButton, FilterOption },
emits: ['changeFilterValue', 'invertFilters', 'resetFilters'], mixins: [imageMixin, keyMixin, routerMixin],
data: () => ({ data: () => ({
filterIcon: require('@/assets/icon-filter2.svg'),
inputs: { ...inputData },
saveOptions: false, saveOptions: false,
STORAGE_KEY: 'options_saved', STORAGE_KEY: 'options_saved',
@@ -117,15 +123,18 @@ export default defineComponent({
currentRegion: { id: '', value: '' }, currentRegion: { id: '', value: '' },
delayInputTimer: -1, delayInputTimer: -1,
chosenSearchScenery: '',
}), }),
setup() { setup() {
const isVisible = inject('isFilterCardVisible'); const isVisible = inject('isFilterCardVisible');
const store = useStore(); const store = useStore();
const filterStore = useStationFiltersStore();
return { return {
isVisible, isVisible,
store, store,
filterStore,
}; };
}, },
@@ -141,9 +150,31 @@ export default defineComponent({
this.currentRegion = this.store.region; this.currentRegion = this.store.region;
}, },
watch: {
chosenSearchScenery(value: string) {
const chosenStation = this.store.stationList.find(({ name }) => name == value);
if (chosenStation) {
this.$router.push(`/scenery?station=${chosenStation.name.replace(/ /g, '_')}`);
this.chosenSearchScenery = '';
}
},
isVisible(value: boolean) {
this.$nextTick(() => {
if (value) (this.$refs['cardEl'] as HTMLDivElement).focus();
});
},
},
methods: { methods: {
// Override keyMixin function
onKeyDownFunction() {
this.isVisible = !this.isVisible;
},
handleChange(change: { name: string; value: boolean }) { handleChange(change: { name: string; value: boolean }) {
this.$emit('changeFilterValue', { this.filterStore.changeFilterValue({
name: change.name, name: change.name,
value: !change.value, value: !change.value,
}); });
@@ -154,7 +185,7 @@ export default defineComponent({
handleInput(e: Event) { handleInput(e: Event) {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
this.$emit('changeFilterValue', { this.filterStore.changeFilterValue({
name: target.name, name: target.name,
value: target.value, value: target.value,
}); });
@@ -165,13 +196,13 @@ export default defineComponent({
handleAuthorsInput(e: Event) { handleAuthorsInput(e: Event) {
clearTimeout(this.delayInputTimer); clearTimeout(this.delayInputTimer);
this.delayInputTimer = setTimeout(() => { this.delayInputTimer = window.setTimeout(() => {
this.handleInput(e); this.handleInput(e);
}, 400); }, 400);
}, },
changeNumericFilterValue(name: string, value: number, saveToStorage = false) { changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
this.$emit('changeFilterValue', { this.filterStore.changeFilterValue({
name, name,
value, value,
}); });
@@ -191,17 +222,8 @@ export default defineComponent({
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true); this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
}, },
invertFilters() { saveFilters() {
this.inputs.options.forEach((option) => { this.saveOptions = !this.saveOptions;
option.value = !option.value;
StorageManager.setBooleanValue(option.name, option.value);
});
this.$emit('invertFilters');
},
saveFilters(change: { value }) {
this.saveOptions = change.value;
if (!this.saveOptions) { if (!this.saveOptions) {
StorageManager.unregisterStorage(this.STORAGE_KEY); StorageManager.unregisterStorage(this.STORAGE_KEY);
@@ -210,28 +232,16 @@ export default defineComponent({
StorageManager.registerStorage(this.STORAGE_KEY); StorageManager.registerStorage(this.STORAGE_KEY);
this.inputs.options.forEach((option) => StorageManager.setBooleanValue(option.name, option.value)); this.filterStore.inputs.options.forEach((option) => StorageManager.setBooleanValue(option.name, !option.value));
this.filterStore.inputs.sliders.forEach((slider) => StorageManager.setNumericValue(slider.name, slider.value));
this.inputs.sliders.forEach((slider) => StorageManager.setNumericValue(slider.name, slider.value));
}, },
resetFilters() { resetFilters() {
this.inputs.options.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, option.value);
});
this.inputs.sliders.forEach((slider) => {
slider.value = slider.defaultValue;
StorageManager.setNumericValue(slider.name, slider.value);
});
this.authorsInputValue = ''; this.authorsInputValue = '';
this.minimumHours = 0; this.minimumHours = 0;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true); this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
this.filterStore.resetFilters();
this.$emit('resetFilters');
}, },
closeCard() { closeCard() {
@@ -257,34 +267,30 @@ export default defineComponent({
&-enter-from, &-enter-from,
&-leave-to { &-leave-to {
transform: translate(-50%, -50%) scale(0.8);
opacity: 0; opacity: 0;
transform: translate(-50%, -50%) scale(0.45);
} }
} }
.card { .card {
&_btn { &_controls {
button { display: flex;
display: flex; gap: 0.5em;
align-items: center;
padding: 0.5em 1em; input {
border-radius: 0.75em 0.75em 0 0; border-radius: 0.5em 0.5em 0 0;
height: 100%;
font-weight: bold;
}
img {
width: 1.3em;
margin-right: 0.25em;
} }
} }
&_content { &_content {
display: grid; display: flex;
grid-template-rows: 70px 1fr 100px 50px auto; flex-direction: column;
min-height: 0; gap: 1em;
max-height: 100vh;
max-height: 90vh;
padding: 1em;
} }
&_title { &_title {
@@ -292,8 +298,6 @@ export default defineComponent({
font-weight: 700; font-weight: 700;
color: $accentCol; color: $accentCol;
margin: 0.5em 0;
text-align: center; text-align: center;
} }
@@ -341,32 +345,18 @@ export default defineComponent({
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 1.15em; font-size: 1.2em;
margin-top: 0.5em;
color: $accentCol; span {
font-weight: bold; min-width: 120px;
} font-weight: bold;
span {
min-width: 100px;
}
button {
border: none;
outline: none;
background: none;
padding: 0 0.45em;
cursor: pointer;
color: white;
font-size: 1.35em;
&:focus,
&:hover {
color: $accentCol; color: $accentCol;
} }
button {
padding: 0.2em 0.6em;
}
} }
} }
@@ -388,22 +378,33 @@ export default defineComponent({
input { input {
width: 100%; width: 100%;
padding: 0.5em; padding: 0.5em;
border: 1px solid white;
} }
} }
&_actions { &_actions {
margin-top: 1em; .filter-option {
max-width: 50%;
display: flex; margin: 0 auto;
flex-direction: column;
align-items: center;
button {
margin: 1em 0.25em;
} }
.option { .action-buttons {
font-size: 1.1em; display: flex;
gap: 0.5em;
width: 100%;
margin-top: 0.5em;
button {
width: 50%;
margin: 0 auto;
padding: 0.5em;
&[data-selected='true'] {
background-color: lightgreen;
color: black;
}
}
} }
} }
} }
@@ -434,8 +435,13 @@ export default defineComponent({
min-width: 25%; min-width: 25%;
max-width: 120px; max-width: 120px;
&:focus-visible ~ * {
color: gold;
}
&::-webkit-slider-thumb { &::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none;
height: 20px; height: 20px;
width: 20px; width: 20px;
+44 -47
View File
@@ -15,7 +15,7 @@
<img <img
class="sort-icon" class="sort-icon"
v-if="sorterActive.index == i" v-if="sorterActive.index == i"
:src="sorterActive.dir == 1 ? ascIcon : descIcon" :src="sorterActive.dir == 1 ? getIcon('arrow-asc') : getIcon('arrow-desc')"
alt="sort icon" alt="sort icon"
/> />
</span> </span>
@@ -23,12 +23,12 @@
<th v-for="(id, i) in headIconsIds" :key="id" @click="() => changeSorter(i + 7)"> <th v-for="(id, i) in headIconsIds" :key="id" @click="() => changeSorter(i + 7)">
<span class="header_wrapper"> <span class="header_wrapper">
<img :src="require(`@/assets/icon-${id}.svg`)" :alt="id" :title="$t(`sceneries.${id}s`)" /> <img :src="getIcon(id)" :alt="id" :title="$t(`sceneries.${id}s`)" />
<img <img
class="sort-icon" class="sort-icon"
v-if="sorterActive.index == i + 7" v-if="sorterActive.index == i + 7"
:src="sorterActive.dir == 1 ? ascIcon : descIcon" :src="sorterActive.dir == 1 ? getIcon('arrow-asc') : getIcon('arrow-desc')"
alt="sort icon" alt="sort icon"
/> />
</span> </span>
@@ -67,15 +67,15 @@
</span> </span>
<span v-else-if="station.generalInfo.availability == 'abandoned'"> <span v-else-if="station.generalInfo.availability == 'abandoned'">
<img :src="abandonedIcon" alt="non-public" :title="$t('desc.abandoned')" /> <img :src="getIcon('abandoned')" alt="non-public" :title="$t('desc.abandoned')" />
</span> </span>
<span v-else-if="station.generalInfo.availability == 'nonPublic'"> <span v-else-if="station.generalInfo.availability == 'nonPublic'">
<img :src="lockIcon" alt="non-public" :title="$t('desc.non-public')" /> <img :src="getIcon('lock')" alt="non-public" :title="$t('desc.non-public')" />
</span> </span>
<span v-else> <span v-else>
<img :src="unavailableIcon" alt="unavailable" :title="$t('desc.unavailable')" /> <img :src="getIcon('unavailable')" alt="unavailable" :title="$t('desc.unavailable')" />
</span> </span>
</span> </span>
@@ -154,7 +154,7 @@
<img <img
class="icon-info" class="icon-info"
v-if="station.generalInfo.SUP" v-if="station.generalInfo.SUP"
:src="require(`@/assets/icon-SUP.svg`)" :src="getIcon('SUP')"
alt="SUP (RASP-UZK)" alt="SUP (RASP-UZK)"
:title="$t('desc.SUP')" :title="$t('desc.SUP')"
/> />
@@ -164,7 +164,7 @@
<img <img
class="icon-info" class="icon-info"
v-if="station.generalInfo.signalType" v-if="station.generalInfo.signalType"
:src="require(`@/assets/icon-${station.generalInfo.signalType}.svg`)" :src="getIcon(station.generalInfo.signalType)"
: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}`)"
/> />
@@ -174,7 +174,7 @@
<img <img
class="icon-info" class="icon-info"
v-if="station.generalInfo && station.generalInfo.routes.sblRouteNames.length > 0" v-if="station.generalInfo && station.generalInfo.routes.sblRouteNames.length > 0"
:src="SBLIcon" :src="getIcon('SBL')"
alt="SBL" alt="SBL"
:title="$t('desc.SBL') + `${station.generalInfo.routes.sblRouteNames.join(',')}`" :title="$t('desc.SBL') + `${station.generalInfo.routes.sblRouteNames.join(',')}`"
/> />
@@ -182,7 +182,7 @@
</td> </td>
<td class="station_info" v-else> <td class="station_info" v-else>
<img class="icon-info" :src="unknownIcon" alt="icon-unknown" :title="$t('desc.unknown')" /> <img class="icon-info" :src="getIcon('unknown')" alt="icon-unknown" :title="$t('desc.unknown')" />
</td> </td>
<td class="station_users" :class="{ inactive: !station.onlineInfo }"> <td class="station_users" :class="{ inactive: !station.onlineInfo }">
@@ -222,16 +222,16 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import styleMixin from '@/mixins/styleMixin'; import { defineComponent, computed } from 'vue';
import dateMixin from '@/mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import stationInfoMixin from '@/mixins/stationInfoMixin'; import imageMixin from '../../mixins/imageMixin';
import returnBtnMixin from '@/mixins/returnBtnMixin'; import returnBtnMixin from '../../mixins/returnBtnMixin';
import stationInfoMixin from '../../mixins/stationInfoMixin';
import { DataStatus } from '@/scripts/enums/DataStatus'; import styleMixin from '../../mixins/styleMixin';
import { computed, ComputedRef, defineComponent } from '@vue/runtime-core'; import { DataStatus } from '../../scripts/enums/DataStatus';
import Station from '@/scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
import { StoreData } from '@/scripts/interfaces/StoreData'; import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useStore } from '@/store/store'; import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
export default defineComponent({ export default defineComponent({
@@ -240,61 +240,58 @@ export default defineComponent({
type: Array as () => Station[], type: Array as () => Station[],
required: true, required: true,
}, },
sorterActive: {
type: Object as () => {
index: number;
dir: number;
},
required: true,
},
setFocusedStation: { type: Function, required: true },
changeSorter: { type: Function, required: true },
}, },
mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin],
components: { Loading },
mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin],
data: () => ({ data: () => ({
likeIcon: require('@/assets/icon-like.svg'),
spawnIcon: require('@/assets/icon-spawn.svg'),
timetableIcon: require('@/assets/icon-timetable.svg'),
userIcon: require('@/assets/icon-user.svg'),
trainIcon: require('@/assets/icon-train.svg'),
SBLIcon: require('@/assets/icon-SBL.svg'),
SUPIcon: require('@/assets/icon-SUP.svg'),
lockIcon: require('@/assets/icon-lock.svg'),
unavailableIcon: require('@/assets/icon-unavailable.svg'),
unknownIcon: require('@/assets/icon-unknown.svg'),
abandonedIcon: require('@/assets/icon-abandoned.svg'),
ascIcon: require('@/assets/icon-arrow-asc.svg'),
descIcon: require('@/assets/icon-arrow-desc.svg'),
headIds: ['station', 'min-lvl', 'status', 'dispatcher', 'dispatcher-lvl', 'routes', 'general'], headIds: ['station', 'min-lvl', 'status', 'dispatcher', 'dispatcher-lvl', 'routes', 'general'],
headIconsIds: ['user', 'spawn', 'timetable'], headIconsIds: ['user', 'spawn', 'timetable'],
lastSelectedStationName: '', lastSelectedStationName: '',
}), }),
computed: {
sorterActive() {
return this.stationFiltersStore.sorterActive;
},
},
setup() { setup() {
const store = useStore(); const store = useStore();
const stationFiltersStore = useStationFiltersStore();
const isDataLoaded = computed(() => { const isDataLoaded = computed(() => {
return store.dataStatuses.sceneries != DataStatus.Loading; return store.dataStatuses.sceneries != DataStatus.Loading;
}); });
return { return {
isDataLoaded, isDataLoaded,
stationFiltersStore,
}; };
}, },
methods: { methods: {
setScenery(name: string) { setScenery(name: string) {
const station = this.stations.find((station) => station.name === name); const station = this.stations.find((station) => station.name === name);
if (!station) return; if (!station) return;
this.lastSelectedStationName = station.name; this.lastSelectedStationName = station.name;
this.$router.push({ this.$router.push({
name: 'SceneryView', name: 'SceneryView',
query: { station: station.name.replaceAll(' ', '_') }, query: { station: station.name.replaceAll(' ', '_') },
}); });
}, },
openForumSite(e: Event, url: string | undefined) { openForumSite(e: Event, url: string | undefined) {
if (!url) return; if (!url) return;
e.preventDefault(); e.preventDefault();
window.open(url, '_blank'); window.open(url, '_blank');
}, },
changeSorter(i: number) {
this.stationFiltersStore.changeSorter(i);
},
}, },
components: { Loading },
}); });
</script> </script>
@@ -303,7 +300,7 @@ export default defineComponent({
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
@import '../../styles/icons.scss'; @import '../../styles/icons.scss';
$rowCol: #4b4b4b; $rowCol: #424242;
.change-anim { .change-anim {
&-enter-active, &-enter-active,
@@ -342,7 +339,7 @@ table {
} }
thead tr { thead tr {
background-color: $primaryCol; background-color: $bgCol;
} }
thead th { thead th {
@@ -352,7 +349,7 @@ table {
min-width: 75px; min-width: 75px;
padding: 0.5em; padding: 0.5em;
background-color: $primaryCol; background-color: $bgCol;
white-space: pre-wrap; white-space: pre-wrap;
cursor: pointer; cursor: pointer;
+295 -290
View File
@@ -1,290 +1,295 @@
<template> <template>
<div class="train-info simple" tabindex="0"> <div class="train-info" tabindex="0">
<section> <section class="train-route">
<span> <div class="train_general">
<div> <span>
<span> <span class="timetable-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span>
<!-- <router-link
v-if="train.timetableData" <span class="timetable_warnings">
:to="`/journal/timetables?timetableId=${train.timetableData.timetableId}`" <span class="train-badge twr" v-if="train.timetableData?.TWR">TWR</span>
style="color: #ddd; margin-right: 0.3em" <span class="train-badge skr" v-if="train.timetableData?.SKR">SKR</span>
> </span>
#{{ train.timetableData.timetableId }} <strong v-if="train.timetableData">{{ train.timetableData.category }}&nbsp;</strong>
</router-link> --> <strong>{{ train.trainNo }}</strong>
<span>&nbsp;| {{ train.driverName }}&nbsp;</span>
<span class="timetable-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span> <b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
</span>
<span class="timetable_warnings"> </div>
<span class="warning twr" v-if="train.timetableData?.TWR">TWR</span>
<span class="warning skr" v-if="train.timetableData?.SKR">SKR</span> <div class="timetable_route" v-if="train.timetableData">
</span> <strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
<strong v-if="train.timetableData">{{ train.timetableData.category }}&nbsp;</strong> <img
<strong>{{ train.trainNo }}</strong> v-if="getSceneriesWithComments(train.timetableData).length > 0"
<span>&nbsp;| {{ train.driverName }}&nbsp;</span> class="image-warning"
</span> :src="getIcon('warning')"
:title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(train.timetableData)})`"
<img />
class="image-offline" </div>
style="height: 1em"
v-if="!train.currentStationHash" <hr style="margin: 0.25em 0" />
:src="icons.offline"
alt="offline" <div class="timetable_stops" v-if="train.timetableData">
:title="$t('trains.offline')" <span v-if="train.timetableData.followingStops.length > 2">
/> {{ $t('trains.via-title') }}
</div> <span v-html="displayStopList(train.timetableData.followingStops)"></span>
</span>
<div class="timetable_route" v-if="train.timetableData"> </div>
<strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
<img <div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
v-if="getSceneriesWithComments(train.timetableData).length > 0" <!-- <span> </span> -->
class="image-warning" <span class="timetable_progress-bar">
:src="icons.warning" <!-- {{ confirmedPercentage(train.timetableData.followingStops) }}%&nbsp; -->
:title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(train.timetableData)})`" <span class="bar-bg"></span>
/> <span
</div> class="bar-fg"
:style="{ width: `${Math.floor(confirmedPercentage(train.timetableData.followingStops))}%` }"
<hr style="margin: 0.25em 0" /> ></span>
</span>
<div class="timetable_stops" v-if="train.timetableData">
<span v-if="train.timetableData.followingStops.length > 2"> <span class="timetable_progress-distance">
{{ $t('trains.via-title') }} &nbsp; {{ currentDistance(train.timetableData.followingStops) }} km /
<span v-html="displayStopList(train.timetableData.followingStops)"></span> <span class="text--primary"> {{ train.timetableData.routeDistance }} km </span>
</span> |
</div> <span v-html="currentDelay(train.timetableData.followingStops)"></span>
</span>
<div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
<!-- <span> </span> --> <div class="train-status-badges">
<span class="timetable_progress-bar"> <div v-if="!train.currentStationHash" class="train-badge offline">{{ $t('trains.scenery-offline') }}</div>
<!-- {{ confirmedPercentage(train.timetableData.followingStops) }}%&nbsp; --> <div v-if="!train.online" class="train-badge offline">Offline {{ lastSeenMessage(train.lastSeen) }}</div>
<span class="bar-bg"></span> </div>
<span </div>
class="bar-fg"
:style="{ width: `${Math.floor(confirmedPercentage(train.timetableData.followingStops))}%` }" <div class="driver_position text--grayed" style="margin-top: 0.25em">
></span> {{ displayTrainPosition(train) }}
</span> </div>
</section>
<span>
&nbsp; {{ currentDistance(train.timetableData.followingStops) }} km / <section class="train-stats">
<span class="text--primary"> {{ train.timetableData.routeDistance }} km </span> <div>
| <img :src="train.locoURL" loading="lazy" alt="Loco image not found" @error="onImageError" />
<span v-html="currentDelay(train.timetableData.followingStops)"></span> </div>
</span>
</div> <div class="text--grayed">
{{ train.locoType }}
<div v-if="!train.online" style="color: salmon">Offline - {{ lastSeenMessage(train.lastSeen) }}</div> <span v-if="train.cars.length > 0">
&nbsp;&bull; {{ $t('trains.cars') }}:
<div class="driver_position text--grayed" style="margin-top: 0.25em"> <span class="count">{{ train.cars.length }}</span>
<span v-if="train.currentStationHash"> </span>
{{ $t('trains.current-scenery') }} <span>{{ train['currentStationName'] }}&nbsp;</span> </div>
</span>
<div>
<span v-else> <span v-for="(stat, i) in STATS.main" :key="stat.name">
{{ $t('trains.current-scenery') }} <span v-if="i > 0"> &bull; </span>
<span>{{ train['currentStationName'].replace(/.[a-zA-Z0-9]+.sc/, '') }} (offline)&nbsp;</span> <span>{{ `${~~((train as any)[stat.name] * (stat.multiplier || 1))}${stat.unit}` }} </span>
</span> </span>
</div>
<span v-if="train.signal"> </section>
{{ $t('trains.current-signal') }} <span>{{ train['signal'] }}&nbsp;</span> </div>
</span> </template>
<span v-if="train.connectedTrack"> <script lang="ts">
{{ $t('trains.current-track') }} <span>{{ train['connectedTrack'] }}&nbsp;</span> import { defineComponent } from 'vue';
</span> import imageMixin from '../../mixins/imageMixin';
import trainInfoMixin from '../../mixins/trainInfoMixin';
<span v-if="train.distance">({{ displayDistance(train.distance) }})</span> import Train from '../../scripts/interfaces/Train';
</div>
</span> export default defineComponent({
</section> props: {
train: {
<section class="train-image" style="display: flex; justify-content: center; align-items: center"> type: Object as () => Train,
<img :src="train.locoURL" loading="lazy" alt="Loco image not found" @error="onImageError" /> required: true,
},
<div class="text--grayed">
{{ train.locoType }} extended: {
<span v-if="train.cars.length > 0"> type: Boolean,
&nbsp;&bull; {{ $t('trains.cars') }}: default: true,
<span class="count">{{ train.cars.length }}</span> },
</span> },
</div>
mixins: [trainInfoMixin, imageMixin],
<div> });
<div> </script>
<span v-for="(stat, i) in STATS.main" :key="stat.name">
<span v-if="i > 0"> &bull; </span> <style lang="scss" scoped>
<span>{{ `${~~(train[stat.name] * (stat.multiplier || 1))}${stat.unit}` }} </span> @import '../../styles/responsive.scss';
</span>
</div> .image-warning {
</div> height: 1em;
</section>
</div> margin-left: 0.5em;
</template> }
<script lang="ts"> .train-stats {
import trainInfoMixin from '@/mixins/trainInfoMixin'; display: flex;
import Train from '@/scripts/interfaces/Train'; justify-content: center;
import { defineComponent } from 'vue'; align-content: center;
export default defineComponent({ flex-direction: column;
props: { text-align: center;
train: {
type: Object as () => Train, img {
required: true, margin: 0.5em 0;
}, width: 12em;
}, }
}
mixins: [trainInfoMixin],
.train-info {
data: () => ({ display: grid;
icons: { grid-template-columns: 2fr 1fr;
warning: require('@/assets/icon-warning.svg'), grid-template-rows: 1fr;
offline: require('@/assets/icon-offline.svg'),
}, padding: 1em;
}),
}); background-color: #1a1a1a;
</script> gap: 0.5em;
}
<style lang="scss" scoped>
@import '../../styles/responsive.scss'; .timetable-id {
margin-right: 0.3em;
.image-warning, color: #d2d2d2;
.image-offline { }
height: 1em;
.warning-timeout {
margin-left: 0.5em; background-color: #be3728;
}
display: inline-block;
.train-image { text-align: center;
display: flex;
flex-direction: column; width: 1.25em;
height: 1.25em;
img { border-radius: 50%;
margin: 0.5em 0; }
width: 12em;
} .timetable_stops {
} font-size: 0.75em;
}
.simple {
display: grid; .train_general {
grid-template-columns: 2fr 1fr; display: flex;
grid-template-rows: 1fr; align-items: center;
flex-wrap: wrap;
padding: 1em; }
background-color: #202020; .train-status-badges {
gap: 0.5em; display: flex;
} flex-wrap: wrap;
}
.driver_position:first-letter {
text-transform: capitalize; .train-badge {
} padding: 0.15em 0.35em;
margin-right: 0.3em;
.timetable-id {
margin-right: 0.3em; font-weight: bold;
color: #d2d2d2;
} font-size: 0.9em;
.timetable_stops { &.twr {
font-size: 0.75em; background-color: var(--clr-twr);
} }
.timetable_route { &.skr {
display: flex; background-color: var(--clr-skr);
align-items: center; }
margin-top: 0.5em; &.offline {
} background-color: #b83b2d;
}
.timetable_warnings { }
color: black;
.timetable_route {
.warning { display: flex;
padding: 0.1em 0.3em; align-items: center;
margin-right: 0.3em;
border-radius: 1em; margin-top: 0.5em;
}
font-weight: bold;
.timetable_warnings {
&.twr { color: black;
background: var(--clr-twr); }
}
.timetable_progress {
&.skr { display: flex;
background: var(--clr-skr); align-items: center;
} flex-wrap: wrap;
} }
}
.timetable_progress-bar {
.timetable_progress { position: relative;
display: flex;
align-items: center; width: 6em;
flex-wrap: wrap; height: 1em;
} margin: 0.5em 0;
.timetable_progress-bar { .bar-fg,
position: relative; .bar-bg {
position: absolute;
width: 6em; height: 1em;
height: 1em; width: 100%;
margin: 0.5em 0;
left: 0;
.bar-fg, }
.bar-bg {
position: absolute; .bar-fg {
height: 1em; background-color: springgreen;
width: 100%; }
left: 0; .bar-bg {
} background-color: #5b5b5b;
}
.bar-fg { }
background-color: springgreen;
} .timetable_progress-distance {
margin-right: 0.25em;
.bar-bg { }
background-color: #5b5b5b;
} .comments {
} display: flex;
align-items: center;
.comments {
display: flex; font-size: 0.9em;
align-items: center;
margin-top: 1em;
font-size: 0.9em;
img {
margin-top: 1em; margin-right: 0.5em;
}
img { }
margin-right: 0.5em;
} @include smallScreen() {
} .train-info {
grid-template-columns: 1fr;
@include smallScreen() { gap: 1em 0;
.simple { text-align: center;
grid-template-columns: 1fr;
gap: 1em 0; font-size: 1.15em;
text-align: center; }
font-size: 1.25em; .train-stats {
} font-size: 1.1em;
}
.info-stats {
text-align: center; .train_general {
} justify-content: center;
}
.timetable_route {
justify-content: center; .train-status-badges {
} justify-content: center;
}
.timetable_progress {
justify-content: center; .timetable_route {
} justify-content: center;
}
.comments {
flex-direction: column; .timetable_progress {
justify-content: center; justify-content: center;
}
img {
margin: 0 0 0.5em 0; .comments {
} flex-direction: column;
} justify-content: center;
}
</style> img {
margin: 0 0 0.5em 0;
}
}
}
</style>
+144 -216
View File
@@ -1,269 +1,197 @@
<template> <template>
<div class="train-options"> <div class="filters-options" @keydown.esc="showOptions = false">
<div class="options_wrapper"> <div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<div class="options_content">
<div class="content_select">
<select-box
:itemList="translatedSorterOptions"
:defaultItemIndex="0"
@selected="changeSorter"
:prefix="$t('trains.sorter-prefix')"
/>
</div>
<div class="content_search"> <button class="btn--filled btn--image" @click="toggleShowOptions" ref="button">
<div class="search-box"> <img :src="getIcon('filter2')" alt="Open filters" />
<input class="search-input" v-model="searchedTrain" :placeholder="$t('trains.search-train')" /> {{ $t('options.filters') }} [F]
</button>
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="() => (searchedTrain = '')" /> <transition name="options-anim">
<div class="options_wrapper" v-if="showOptions">
<div class="options_content">
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
<div class="search_content">
<div class="search-box">
<input
class="search-input"
ref="initFocusedElement"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
:placeholder="$t(`options.search-train`)"
v-model="searchedTrain"
/>
<button class="search-exit">
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear('train')" />
</button>
</div>
<div class="search-box">
<input
class="search-input"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
:placeholder="$t(`options.search-driver`)"
v-model="searchedDriver"
/>
<button class="search-exit">
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear('driver')" />
</button>
</div>
</div> </div>
<div class="search-box"> <h1 class="option-title">{{ $t('options.sort-title') }}</h1>
<input class="search-input" v-model="searchedDriver" :placeholder="$t('trains.search-driver')" /> <div class="options_sorters">
<div v-for="opt in translatedSorterOptions">
<button
class="sort-option btn--option"
:data-selected="opt.id == sorterActive.id"
@click="onSorterChange(opt)"
>
{{ opt.value.toUpperCase() }}
</button>
</div>
</div>
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="() => (searchedDriver = '')" /> <h1 class="option-title" v-if="trainFilterList.length != 0">{{ $t('options.filter-title') }}</h1>
<div class="options_filters">
<div class="filter-option" v-for="filter in trainFilterList">
<button class="btn--option" :data-disabled="!filter.isActive" @click="onFilterChange(filter)">
{{ $t(`options.filter-${filter.id}`) }}
</button>
</div>
<div class="filter-actions">
<button class="btn--action" @click="clearAllFilters">{{ $t('options.filter-clear') }}</button>
<button class="btn--action" @click="resetAllFilters">{{ $t('options.filter-reset') }}</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </transition>
<div class="filters">
<span
:class="{ active: filter.isActive }"
class="filter"
v-for="filter in filterList"
:key="filter.id"
tabindex="0"
@contextmenu="
(e) => {
e.preventDefault();
return false;
}
"
@click.left="toggleFilter(filter)"
@keydown.enter="toggleFilter(filter)"
@click.right="setFilterOnly(filter)"
@keydown.space="setFilterOnly(filter)"
>
{{ $t(`trains.filter-${filter.id}`) }}
</span>
<span class="filter reset-btn" @click="resetFilters" tabindex="0">
{{ $t('trains.filter-reset') }}
</span>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, inject, TrainFilter } from 'vue'; import { defineComponent, inject, PropType } from 'vue';
import { useI18n } from 'vue-i18n'; import imageMixin from '../../mixins/imageMixin';
import keyMixin from '../../mixins/keyMixin';
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
import ActionButton from '../Global/ActionButton.vue';
import SelectBox from '../Global/SelectBox.vue'; import SelectBox from '../Global/SelectBox.vue';
export default defineComponent({ export default defineComponent({
components: { SelectBox }, components: { SelectBox, ActionButton },
emits: ['changeSearchedTrain', 'changeSearchedDriver', 'changeSorter'], mixins: [imageMixin, keyMixin],
data: () => ({ props: {
exitIcon: require('@/assets/icon-exit.svg'), sorterOptionIds: {
}), type: Array as PropType<Array<string>>,
required: true,
setup() { },
const { t } = useI18n(); },
const sorterOptions = [
{
id: 'distance',
value: 'kilometraż',
},
{
id: 'progress',
value: 'przebyta trasa',
},
{
id: 'delay',
value: 'opóźnienie',
},
{
id: 'mass',
value: 'masa',
},
{
id: 'speed',
value: 'prędkość',
},
{
id: 'length',
value: 'długość',
},
];
let filterList = inject('filterList') as TrainFilter[];
const translatedSorterOptions = computed(() =>
sorterOptions.map(({ id }) => ({
id,
value: t(`trains.option-${id}`),
}))
);
data() {
return { return {
translatedSorterOptions, showOptions: false,
searchedTrain: inject('searchedTrain') as string,
searchedDriver: inject('searchedDriver') as string,
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
filterList,
}; };
}, },
setup() {
return {
searchedTrain: inject('searchedTrain') as string,
searchedDriver: inject('searchedDriver') as string,
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
trainFilterList: inject('filterList') as TrainFilter[],
};
},
computed: {
translatedSorterOptions() {
return this.$props.sorterOptionIds.map((id) => ({
id,
value: this.$t(`options.sort-${id}`),
}));
},
},
methods: { methods: {
changeSorter(item: { id: string | number; value: string }) { // Override keyMixin function
onKeyDownFunction() {
this.toggleShowOptions();
},
toggleShowOptions() {
this.showOptions = !this.showOptions;
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.id = item.id;
this.sorterActive.dir = -1; this.sorterActive.dir = -1;
}, },
toggleFilter(filter: TrainFilter) { onFilterChange(filter: TrainFilter) {
filter.isActive = !filter.isActive; filter.isActive = !filter.isActive;
}, },
setFilterOnly(filter: TrainFilter) { clearAllFilters() {
this.filterList.forEach((f) => (f.isActive = f.id == filter.id)); this.trainFilterList.forEach((filter) => {
filter.isActive = false;
});
}, },
resetFilters() { resetAllFilters() {
this.filterList.forEach((f) => (f.isActive = true)); this.trainFilterList.forEach((filter) => {
this.searchedDriver = ""; filter.isActive = true;
this.searchedTrain = ""; });
},
onInputClear(id: 'driver' | 'train') {
if (id == 'driver') this.searchedDriver = '';
if (id == 'train') this.searchedTrain = '';
}, },
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive'; @import '../../styles/filters_options.scss';
.train-options { .search_content > div {
@include smallScreen() { margin: 0.5em auto;
width: 100%;
}
} }
.options { .search_content > button {
&_wrapper {
display: flex;
}
&_content {
display: flex;
flex-wrap: wrap;
.content_search,
.content_select {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 0.25em 0.25em 0 0;
}
}
}
.search {
&-box {
position: relative;
background: #333;
border-radius: 0.5em;
min-width: 200px;
margin-right: 0.25em;
}
&-input {
border: none;
min-width: 100%;
padding: 0.35em 0.5em;
}
&-exit {
position: absolute;
cursor: pointer;
top: 50%;
right: 10px;
transform: translateY(-50%);
width: 1em;
}
}
.filters {
display: flex; display: flex;
flex-wrap: wrap; justify-content: center;
margin: 0 auto;
}
margin-top: 0.5em; .filter-option {
button {
color: white;
font-weight: bold;
@include smallScreen() { &[data-disabled='true'] {
justify-content: center; color: #888;
}
} }
} }
.filter { .filter-actions {
background: #333; display: flex;
padding: 0.2em 0.25em; gap: 0.5em;
margin: 0.25em 0.25em 0 0; width: 100%;
font-weight: bold;
cursor: pointer; margin-top: 1em;
color: gray;
&.active { button {
color: var(--clr-primary);
}
&.reset-btn {
color: salmon;
}
}
@include smallScreen() {
.journal-options {
width: 100%; width: 100%;
} }
.options {
&_wrapper {
justify-content: center;
}
&_content {
padding: 0 1em;
flex-direction: column;
.content_select {
margin: 0 auto;
padding: 0;
}
.content_search {
justify-content: center;
}
}
}
.search {
&-box,
&-button {
margin: 0.5em 0 0 0;
}
&-box {
width: 100%;
}
&-button {
width: 80%;
max-width: 300px;
}
}
} }
</style> </style>
@@ -1,159 +0,0 @@
<template>
<section class="filter-card" v-click-outside="closeCard">
<div class="card_btn">
<action-button @click="toggleCard">
<img class="button_icon" :src="filterIcon" alt="icon-filter" />
<p>{{ $t('options.filters') }}</p>
</action-button>
</div>
<transition name="card-anim">
<div class="card_content card" v-if="isVisible">
<div class="card_exit" @click="closeCard"></div>
<div class="options_wrapper">
<div class="options_content">
<div class="content_select">
<select-box
:itemList="translatedSorterOptions"
:defaultItemIndex="0"
@selected="changeSorter"
:prefix="$t('trains.sorter-prefix')"
/>
</div>
<div class="content_search">
<div class="search-box">
<input class="search-input" v-model="searchedTrain" :placeholder="$t('trains.search-train')" />
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="() => (searchedTrain = '')" />
</div>
<div class="search-box">
<input class="search-input" v-model="searchedDriver" :placeholder="$t('trains.search-driver')" />
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="() => (searchedDriver = '')" />
</div>
</div>
</div>
</div>
<section class="card_actions flex">
<action-button class="outlined">
{{ $t('filters.reset') }}
</action-button>
<action-button class="outlined" @click="closeCard">{{ $t('filters.close') }}</action-button>
</section>
</div>
</transition>
</section>
</template>
<script lang="ts">
import { defineComponent, inject } from '@vue/runtime-core';
import inputData from '@/data/options.json';
import ActionButton from '@/components/Global/ActionButton.vue';
import { sorterOptions } from '@/data/trainOptions';
import { TrainFilter, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import SelectBox from '../Global/SelectBox.vue';
export default defineComponent({
components: { ActionButton, SelectBox },
emits: ['changeFilterValue', 'invertFilters', 'resetFilters'],
data: () => ({
filterIcon: require('@/assets/icon-filter2.svg'),
exitIcon: require('@/assets/icon-exit.svg'),
inputs: { ...inputData },
}),
setup() {
const isVisible = inject('isTrainOptionsCardVisible');
const { t } = useI18n();
let filterList = inject('filterList') as TrainFilter[];
const translatedSorterOptions = computed(() =>
sorterOptions.map(({ id }) => ({
id,
value: t(`trains.option-${id}`),
}))
);
return {
translatedSorterOptions,
searchedTrain: inject('searchedTrain') as string,
searchedDriver: inject('searchedDriver') as string,
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
filterList,
isVisible,
};
},
methods: {
closeCard() {
this.isVisible = false;
},
toggleCard() {
this.isVisible = !this.isVisible;
},
changeSorter(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id;
this.sorterActive.dir = -1;
},
toggleFilter(filter: TrainFilter) {
filter.isActive = !filter.isActive;
},
setFilterOnly(filter: TrainFilter) {
this.filterList.forEach((f) => (f.isActive = f.id == filter.id));
},
resetFilters() {
this.filterList.forEach((f) => (f.isActive = true));
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive';
@import '../../styles/card';
.card-anim {
&-enter-active,
&-leave-active {
transition: all $animDuration $animType;
}
&-enter-from,
&-leave-to {
transform: translate(-50%, -50%) scale(0.85);
opacity: 0;
}
}
.card {
section {
margin: 0.5em 0;
}
&_title {
font-size: 2em;
font-weight: 700;
color: $accentCol;
margin: 0.5em 0;
text-align: center;
}
}
</style>
+21 -25
View File
@@ -60,11 +60,13 @@
<b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span> <b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span>
</div> </div>
<span v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine"> <span
v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine && !/sbl/gi.test(stop.departureLine!)"
>
{{ stop.departureLine }} {{ stop.departureLine }}
</span> </span>
<span v-else> <span v-else-if="!/sbl/gi.test(stop.departureLine!)">
{{ stop.departureLine }} / {{ stop.departureLine }} /
{{ train.timetableData!.followingStops[i + 1].arrivalLine }} {{ train.timetableData!.followingStops[i + 1].arrivalLine }}
</span> </span>
@@ -83,10 +85,11 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType } from '@vue/runtime-core'; import { computed, defineComponent, PropType } from '@vue/runtime-core';
import dateMixin from '@/mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import TrainStop from '@/scripts/interfaces/TrainStop'; import imageMixin from '../../mixins/imageMixin';
import Train from '../../scripts/interfaces/Train';
import TrainStop from '../../scripts/interfaces/TrainStop';
import StopDate from '../Global/StopDate.vue'; import StopDate from '../Global/StopDate.vue';
import Train from '@/scripts/interfaces/Train';
export default defineComponent({ export default defineComponent({
components: { StopDate }, components: { StopDate },
@@ -97,16 +100,10 @@ export default defineComponent({
}, },
}, },
mixins: [dateMixin], mixins: [dateMixin, imageMixin],
emits: ['click'], emits: ['click'],
data: () => ({
icons: {
warning: require('@/assets/icon-warning.svg'),
},
}),
setup(props) { setup(props) {
return { return {
lastConfirmed: computed(() => { lastConfirmed: computed(() => {
@@ -154,7 +151,7 @@ export default defineComponent({
onImageError(e: Event) { onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement; const imageEl = e.target as HTMLImageElement;
imageEl.src = require('@/assets/unknown.png'); imageEl.src = this.getImage('unknown.png');
}, },
}, },
}); });
@@ -179,12 +176,7 @@ $stopNameClr: #22a8d1;
} }
.train-schedule { .train-schedule {
background-color: #202020;
padding: 0 0.25em; padding: 0 0.25em;
@include smallScreen() {
font-size: 1.1em;
}
} }
.train-stock { .train-stock {
@@ -192,10 +184,11 @@ $stopNameClr: #22a8d1;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
ul.stock-list { ul.stock-list {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
overflow-x: auto; overflow: auto;
padding-bottom: 1em; padding-bottom: 1em;
li > div { li > div {
@@ -203,11 +196,15 @@ ul.stock-list {
color: #aaa; color: #aaa;
font-size: 0.9em; font-size: 0.9em;
} }
img {
max-height: 60px;
max-width: 320px;
}
} }
.schedule-wrapper { .schedule-wrapper {
overflow-y: auto; overflow-y: auto;
max-height: 500px;
width: 100%; width: 100%;
z-index: 5; z-index: 5;
@@ -278,13 +275,14 @@ ul.stop_list > li.stop {
padding: 0 0.5em; padding: 0 0.5em;
&.sbl { &.sbl {
.stop-name,
.stop-date { .stop-date {
opacity: 0.7; display: none;
} }
.stop-name { .stop-name {
background-color: #333; background: none;
color: #aaa;
padding: 0;
} }
} }
@@ -381,8 +379,6 @@ ul.stop_list > li.stop {
text-align: center; text-align: center;
flex-wrap: wrap; flex-wrap: wrap;
padding: 0.15em 0;
} }
.stop-bar { .stop-bar {
+36 -72
View File
@@ -1,29 +1,27 @@
<template> <template>
<div class="train-stats" v-click-outside="closeStats"> <div class="train-stats" v-click-outside="closeStats">
<action-button class="stats_button" @click="toggleStatsOpen"> <action-button class="stats_button" @click="toggleStatsOpen">
<img :src="statsIcon" :alt="$t('trains.stats')" /> <img :src="getIcon('stats')" :alt="$t('trains.stats')" />
<p>{{ $t("trains.stats") }}</p> <p>{{ $t('trains.stats') }}</p>
</action-button> </action-button>
<transition name="stats-anim" class="stats_wrapper" tag="div"> <transition name="stats-anim" class="stats_wrapper" tag="div">
<div class="stats-body" v-if="trainStatsOpen"> <div class="stats-body" v-if="trainStatsOpen">
<h2 class="stats-header"> <h2 class="stats-header">
<img :src="statsIcon" :alt="$t('trains.stats')" /> <img :src="getIcon('stats')" :alt="$t('trains.stats')" />
{{ $t("trains.stats") }} {{ $t('trains.stats') }}
</h2> </h2>
<div class="stats-speed"> <div class="stats-speed">
<div class="title stats-title"> <div class="title stats-title">
{{ $t("trains.stats-speed") }} {{ $t('trains.stats-speed') }}
</div>
<div class="stats-content">
{{ speedStats.min }} | {{ speedStats.avg }} | {{ speedStats.max }}
</div> </div>
<div class="stats-content">{{ speedStats.min }} | {{ speedStats.avg }} | {{ speedStats.max }}</div>
</div> </div>
<div class="stats-length"> <div class="stats-length">
<div class="title stats-title"> <div class="title stats-title">
{{ $t("trains.stats-length") }} {{ $t('trains.stats-length') }}
</div> </div>
<div class="stats-content"> <div class="stats-content">
{{ timetableStats.min }} | {{ timetableStats.avg }} | {{ timetableStats.min }} | {{ timetableStats.avg }} |
@@ -33,15 +31,11 @@
<div class="stats-categories"> <div class="stats-categories">
<div class="title stats-title"> <div class="title stats-title">
{{ $t("trains.stats-categories") }} {{ $t('trains.stats-categories') }}
</div> </div>
<div class="category-list"> <div class="category-list">
<span <span class="category" v-for="[key, value] of categoryList" :key="key">
class="category"
v-for="[key, value] of categoryList"
:key="key"
>
<span class="category-type">{{ key }}</span> <span class="category-type">{{ key }}</span>
<span class="category-count">{{ value }}</span> <span class="category-count">{{ value }}</span>
</span> </span>
@@ -49,28 +43,22 @@
<div class="special-list"> <div class="special-list">
<span class="special twr"> <span class="special twr">
<span class="special-type">{{ <span class="special-type">{{ $t('trains.stats-special-twr') }}</span>
$t("trains.stats-special-twr")
}}</span>
<span class="special-count">{{ specialTrainCount[0] }}</span> <span class="special-count">{{ specialTrainCount[0] }}</span>
</span> </span>
<span class="special skr"> <span class="special skr">
<span class="special-type">{{ <span class="special-type">{{ $t('trains.stats-special-skr') }}</span>
$t("trains.stats-special-skr")
}}</span>
<span class="special-count">{{ specialTrainCount[1] }}</span> <span class="special-count">{{ specialTrainCount[1] }}</span>
</span> </span>
</div> </div>
</div> </div>
<div class="stats-locos"> <div class="stats-locos">
<div class="title stats-title">{{ $t("trains.stats-locos") }}</div> <div class="title stats-title">{{ $t('trains.stats-locos') }}</div>
<div class="loco-list stats-content"> <div class="loco-list stats-content">
<div class="loco-item" v-for="(loco, i) in locoList" :key="i"> <div class="loco-item" v-for="(loco, i) in locoList" :key="i">{{ loco[0] }} | {{ loco[1] }}</div>
{{ loco[0] }} | {{ loco[1] }}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -79,13 +67,15 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import ActionButton from "@/components/Global/ActionButton.vue"; import { defineComponent, computed, inject } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import Train from "@/scripts/interfaces/Train"; import Train from '../../scripts/interfaces/Train';
import { computed, defineComponent, inject } from "@vue/runtime-core"; import ActionButton from '../Global/ActionButton.vue';
export default defineComponent({ export default defineComponent({
components: { ActionButton }, components: { ActionButton },
mixins: [imageMixin],
props: { props: {
trains: { trains: {
type: Array as () => Train[], type: Array as () => Train[],
@@ -95,7 +85,6 @@ export default defineComponent({
data: () => ({ data: () => ({
trainStatsOpen: false, trainStatsOpen: false,
statsIcon: require("@/assets/icon-stats.svg"),
}), }),
methods: { methods: {
@@ -110,14 +99,11 @@ export default defineComponent({
setup(props) { setup(props) {
const speedStats = computed(() => { const speedStats = computed(() => {
if (props.trains.length == 0) return { avg: "0", min: "0", max: "0" }; if (props.trains.length == 0) return { avg: '0', min: '0', max: '0' };
const trainList = props.trains.filter((train) => train.timetableData); const trainList = props.trains.filter((train) => train.timetableData);
const avg = ( const avg = (trainList.reduce((acc, train) => acc + train.speed, 0) / trainList.length).toFixed(2);
trainList.reduce((acc, train) => acc + train.speed, 0) /
trainList.length
).toFixed(2);
const minMaxSpeed = trainList.reduce((acc, train) => { const minMaxSpeed = trainList.reduce((acc, train) => {
if (!train.timetableData) return acc; if (!train.timetableData) return acc;
@@ -136,32 +122,21 @@ export default defineComponent({
}); });
const timetableStats = computed(() => { const timetableStats = computed(() => {
if (props.trains.length == 0) return { avg: "0", min: "0", max: "0" }; if (props.trains.length == 0) return { avg: '0', min: '0', max: '0' };
const activeTrainsLength = props.trains.filter( const activeTrainsLength = props.trains.filter((train) => train.timetableData).length;
(train) => train.timetableData
).length;
const avg = ( const avg = (
props.trains.reduce( props.trains.reduce((acc, train) => (train.timetableData ? acc + train.timetableData.routeDistance : acc), 0) /
(acc, train) => activeTrainsLength
train.timetableData ? acc + train.timetableData.routeDistance : acc,
0
) / activeTrainsLength
).toFixed(2); ).toFixed(2);
const minMaxDistance = props.trains.reduce((acc, train) => { const minMaxDistance = props.trains.reduce((acc, train) => {
if (!train.timetableData) return acc; if (!train.timetableData) return acc;
acc[0] = acc[0] = !acc[0] || train.timetableData.routeDistance < acc[0] ? train.timetableData.routeDistance : acc[0];
!acc[0] || train.timetableData.routeDistance < acc[0]
? train.timetableData.routeDistance
: acc[0];
acc[1] = acc[1] = !acc[1] || train.timetableData.routeDistance > acc[1] ? train.timetableData.routeDistance : acc[1];
!acc[1] || train.timetableData.routeDistance > acc[1]
? train.timetableData.routeDistance
: acc[1];
return acc; return acc;
}, [] as any); }, [] as any);
@@ -178,9 +153,7 @@ export default defineComponent({
acc.set( acc.set(
train.timetableData.category, train.timetableData.category,
acc.get(train.timetableData.category) acc.get(train.timetableData.category) ? acc.get(train.timetableData.category) + 1 : 1
? acc.get(train.timetableData.category) + 1
: 1
); );
return acc; return acc;
@@ -193,35 +166,26 @@ export default defineComponent({
const map: Map<string, number> = props.trains.reduce((acc, train) => { const map: Map<string, number> = props.trains.reduce((acc, train) => {
if (!train.timetableData || !train.locoType) return acc; if (!train.timetableData || !train.locoType) return acc;
acc.set( acc.set(train.locoType, acc.get(train.locoType) ? acc.get(train.locoType) + 1 : 1);
train.locoType,
acc.get(train.locoType) ? acc.get(train.locoType) + 1 : 1
);
return acc; return acc;
}, new Map()); }, new Map());
const sorted = [...map.entries()] const sorted = [...map.entries()].sort((a, b) => b[1] - a[1]).filter((v, i) => i < 3);
.sort((a, b) => b[1] - a[1])
.filter((v, i) => i < 3);
return sorted; return sorted;
}); });
const specialTrainCount = computed(() => { const specialTrainCount = computed(() => {
const twrList = props.trains.filter( const twrList = props.trains.filter((train) => train.timetableData && train.timetableData.TWR);
(train) => train.timetableData && train.timetableData.TWR
);
const skrList = props.trains.filter( const skrList = props.trains.filter((train) => train.timetableData && train.timetableData.SKR);
(train) => train.timetableData && train.timetableData.SKR
);
return [twrList.length, skrList.length]; return [twrList.length, skrList.length];
}); });
/* Inject list from TrainsView for category filter */ /* Inject list from TrainsView for category filter */
const chosenTrainCategories = inject("chosenTrainCategories") as string[]; const chosenTrainCategories = inject('chosenTrainCategories') as string[];
return { return {
speedStats, speedStats,
@@ -229,14 +193,14 @@ export default defineComponent({
categoryList, categoryList,
locoList, locoList,
specialTrainCount, specialTrainCount,
chosenTrainCategories chosenTrainCategories,
}; };
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../styles/responsive"; @import '../../styles/responsive';
.stats-anim { .stats-anim {
&-enter-active, &-enter-active,
@@ -370,4 +334,4 @@ export default defineComponent({
justify-content: center; justify-content: center;
} }
} }
</style> </style>
+52 -100
View File
@@ -1,9 +1,5 @@
<template> <template>
<div class="train-table" @keydown.esc="closeTimetable"> <div class="train-table">
<button class="return-btn" @click="scrollToTop" v-if="showReturnButton">
<img :src="icons.arrowAsc" alt="return arrow" />
</button>
<transition name="anim" mode="out-in"> <transition name="anim" mode="out-in">
<div :key="store.dataStatuses.trains"> <div :key="store.dataStatuses.trains">
<Loading v-if="trains.length == 0 && store.dataStatuses.trains == 0" /> <Loading v-if="trains.length == 0 && store.dataStatuses.trains == 0" />
@@ -12,17 +8,20 @@
{{ $t('trains.no-trains') }} {{ $t('trains.no-trains') }}
</div> </div>
<div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length != 0">
<b class="warning-timeout">?</b>
{{ $t('trains.timeout') }}
</div>
<ul class="train-list"> <ul class="train-list">
<li <li
class="train-row" class="train-row"
v-for="train in currentTrains" v-for="train in currentTrains"
:key="train.trainNo + train.driverId" :key="train.trainId"
@click="toggleTimetable(train)" @click.stop="selectModalTrain(train.trainId)"
@keydown.enter="toggleTimetable(train)" @keydown.enter="selectModalTrain(train.trainId)"
> >
<TrainInfo :train="train" /> <TrainInfo :train="train" />
<TrainSchedule v-if="chosenTrainId == getTrainId(train)" :train="train" ref="card-inner" tabindex="0" />
</li> </li>
</ul> </ul>
</div> </div>
@@ -31,53 +30,30 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, inject, Ref } from '@vue/runtime-core'; import { computed, defineComponent, inject, PropType, Ref } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import defaultVehicleIconsJSON from '@/data/defaultVehicleIcons.json'; import returnBtnMixin from '../../mixins/returnBtnMixin';
import Train from '../../scripts/interfaces/Train';
import Train from '@/scripts/interfaces/Train'; import { useStore } from '../../store/store';
import TrainSchedule from '@/components/TrainsView/TrainSchedule.vue';
import TrainInfo from '@/components/TrainsView/TrainInfo.vue';
import returnBtnMixin from '@/mixins/returnBtnMixin';
import { useStore } from '@/store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import TrainInfo from './TrainInfo.vue';
export default defineComponent({ export default defineComponent({
components: { components: { Loading, TrainInfo },
TrainSchedule,
TrainInfo,
Loading,
},
mixins: [returnBtnMixin],
props: { props: {
trains: { trains: {
type: Array as () => Train[], type: Array as PropType<Train[]>,
required: true, required: true,
}, },
}, },
data: () => ({ mixins: [returnBtnMixin, modalTrainMixin],
defaultLocoImage: require('@/assets/unknown.png'),
icons: {
arrowAsc: require('@/assets/icon-arrow-asc.svg'),
arrowDesc: require('@/assets/icon-arrow-desc.svg'),
},
defaultVehicleIcons: defaultVehicleIconsJSON,
chosenTrainId: null as string | null,
}),
setup(props) { setup(props) {
const store = useStore(); const store = useStore();
const searchedTrain = inject('searchedTrain') as Ref<string>; const searchedTrain = inject('searchedTrain') as Ref<string>;
const searchedDriver = inject('searchedDriver') as Ref<string>; const searchedDriver = inject('searchedDriver') as Ref<string>;
const currentTrains = computed(() => { const currentTrains = computed(() => {
return props.trains; return props.trains;
}); });
@@ -87,75 +63,32 @@ export default defineComponent({
searchedDriver, searchedDriver,
currentTrains, currentTrains,
store, store,
sorterActive: inject('sorterActive') as {
sorterActive: inject('sorterActive') as { id: string | number; dir: number }, id: string | number;
dir: number;
},
distanceLimitExceeded: computed( distanceLimitExceeded: computed(
() => props.trains.findIndex(({ timetableData }) => timetableData && timetableData.routeDistance > 200) != -1 () => props.trains.findIndex(({ timetableData }) => timetableData && timetableData.routeDistance > 200) != -1
), ),
}; };
}, },
computed: {
trainNumbersWithTimeouts() {
return this.store.trainList.filter((train) => train.isTimeout).map((train) => train.trainNo);
},
},
activated() { activated() {
const query = this.$route.query; const query = this.$route.query;
if (query.trainNo && query.driverName) { if (query.trainNo && query.driverName) {
this.searchedDriver = query.driverName.toString(); this.searchedDriver = query.driverName.toString();
this.searchedTrain = query.trainNo.toString(); this.searchedTrain = query.trainNo.toString();
setTimeout(() => { setTimeout(() => {
this.chosenTrainId = query.driverName + <string>query.trainNo; this.selectModalTrain(query.driverName! + query.trainNo!.toString());
}, 20); }, 20);
} }
}, },
deactivated() {
this.chosenTrainId = null;
},
methods: {
enter(el: HTMLElement) {
const maxHeight = getComputedStyle(el).height;
el.style.height = '0px';
getComputedStyle(el);
setTimeout(() => {
el.style.height = maxHeight;
}, 10);
},
afterEnter(el: HTMLElement) {
el.style.height = 'auto';
},
leave(el: HTMLElement) {
el.style.height = getComputedStyle(el).height;
setTimeout(() => {
el.style.height = '0px';
}, 10);
},
toggleTimetable(train: Train, state?: boolean) {
const id = this.getTrainId(train);
if (state !== undefined) {
this.chosenTrainId = state ? id : null;
return;
}
this.chosenTrainId = this.chosenTrainId && this.chosenTrainId == id ? null : id;
},
closeTimetable() {
this.chosenTrainId = null;
},
getTrainId(train: Train) {
return train.driverName + train.trainNo.toString();
},
},
}); });
</script> </script>
@@ -181,11 +114,10 @@ export default defineComponent({
text-align: center; text-align: center;
padding: 1em 0; padding: 1em 0;
margin: 1em 0;
font-size: 1.5em; font-size: 1.5em;
background: #333; background: #1a1a1a;
} }
img.train-image { img.train-image {
@@ -198,12 +130,32 @@ img.train-image {
background: var(--clr-warning); background: var(--clr-warning);
} }
.timeouts-warning {
background-color: #333;
font-weight: bold;
font-size: 1.05em;
margin-bottom: 0.5em;
padding: 0.5em;
}
.warning-timeout {
background-color: #be3728;
color: white;
display: inline-block;
text-align: center;
width: 1.25em;
height: 1.25em;
border-radius: 50%;
}
.train { .train {
&-list { &-list {
overflow: auto; overflow: auto;
margin-top: 1em;
@include smallScreen() { @include smallScreen() {
width: 100%; width: 100%;
} }
@@ -0,0 +1,28 @@
import { JournalFilterType } from "../../scripts/enums/JournalFilterType";
import { JournalTimetableFilter } from "../../types/Journal/JournalTimetablesTypes";
export const journalTimetableFilters: JournalTimetableFilter[] = [
{
id: JournalFilterType.all,
filterSection: 'timetable-status',
isActive: true,
},
{
id: JournalFilterType.active,
filterSection: 'timetable-status',
isActive: false,
},
{
id: JournalFilterType.fulfilled,
filterSection: 'timetable-status',
isActive: false,
},
{
id: JournalFilterType.abandoned,
filterSection: 'timetable-status',
isActive: false,
},
];
@@ -1,60 +1,60 @@
import { TrainFilterType } from "@/scripts/enums/TrainFilterType"; import { TrainFilterType } from '../../scripts/enums/TrainFilterType';
import { TrainFilter } from "vue"; import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
export const trainFilters: TrainFilter[] = [ export const trainFilters: TrainFilter[] = [
{ {
id: TrainFilterType.twr, id: TrainFilterType.twr,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.skr, id: TrainFilterType.skr,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.passenger, id: TrainFilterType.passenger,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.freight, id: TrainFilterType.freight,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.other, id: TrainFilterType.other,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.comments, id: TrainFilterType.comments,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.noTimetable, id: TrainFilterType.noTimetable,
isActive: true, isActive: true,
}, },
]; ];
export const sorterOptions = [ export const sorterOptions = [
{ {
id: 'distance', id: 'distance',
value: 'kilometraż', value: 'kilometraż',
}, },
{ {
id: 'progress', id: 'progress',
value: 'przebyta trasa', value: 'przebyta trasa',
}, },
{ {
id: 'delay', id: 'delay',
value: 'opóźnienie', value: 'opóźnienie',
}, },
{ {
id: 'mass', id: 'mass',
value: 'masa', value: 'masa',
}, },
{ {
id: 'speed', id: 'speed',
value: 'prędkość', value: 'prędkość',
}, },
{ {
id: 'length', id: 'length',
value: 'długość', value: 'długość',
} },
]; ];
-30
View File
@@ -1,30 +0,0 @@
import { JournalFilterType } from "@/scripts/enums/JournalFilterType";
import { JournalFilter } from "vue";
export const journalTimetableFilters: JournalFilter[] = [
{
id: JournalFilterType.all,
filterSection: "timetable-status",
isActive: true
},
{
id: JournalFilterType.active,
filterSection: "timetable-status",
isActive: false
},
{
id: JournalFilterType.fulfilled,
filterSection: "timetable-status",
isActive: false
},
{
id: JournalFilterType.abandoned,
filterSection: "timetable-status",
isActive: false
},
]
export const journalDispatcherFilters: JournalFilter[] = []
-9
View File
@@ -198,15 +198,6 @@
"section": "status", "section": "status",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
},
{
"id": "troll",
"name": "troll",
"iconName": "",
"section": "troll",
"value": true,
"defaultValue": true
} }
], ],
"sliders": [ "sliders": [
View File
+84 -57
View File
@@ -10,6 +10,12 @@
"migration-warning": "Stacjownik services will be unavailable 2/06/2022 between 1-3am (CEST time) due to the migration of API hostings!", "migration-warning": "Stacjownik services will be unavailable 2/06/2022 between 1-3am (CEST time) due to the migration of API hostings!",
"migration-confirm": "Roger that!" "migration-confirm": "Roger that!"
}, },
"update": {
"title": "New Stacjownik version is available!",
"paragraph1": "Enjoy the application and may the green signal be with you!",
"release-link": "Click here to browse version changelog (GitHub)",
"confirm-button": "Understood!"
},
"data-status": { "data-status": {
"S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!", "S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!",
"S1a-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!", "S1a-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!",
@@ -66,7 +72,52 @@
}, },
"options": { "options": {
"filters": "FILTERS", "filters": "FILTERS",
"donate": "DONATE" "donate": "DONATE",
"search-button": "Search",
"reset-button": "Reset",
"sort-title": "SORT BY:",
"filter-title": "FILTER BY:",
"search-title": "SEARCH:",
"search-train-no": "Train no. / #",
"search-train": "Train no.",
"search-driver": "Driver name",
"search-dispatcher": "Dispatcher name",
"search-station": "Scenery name",
"search-author": "Timetable author name",
"search-date": "Timetable date (CEST / GMT+2)",
"sort-mass": "mass",
"sort-speed": "speed",
"sort-length": "length",
"sort-distance": "distance",
"sort-timetable": "train no.",
"sort-progress": "route progress",
"sort-delay": "current delay",
"sort-total-stops": "total stops",
"sort-beginDate": "date",
"sort-timetableId": "timetable ID",
"sort-timestampFrom": "date",
"sort-duration": "duration",
"filter-comments": "COMMENTS",
"filter-twr": "TWR",
"filter-skr": "SKR",
"filter-passenger": "PASSENGER",
"filter-freight": "FREIGHT",
"filter-other": "OTHER",
"filter-noTimetable": "NO TIMETABLE",
"filter-reset": "RESET FILTERS",
"filter-clear": "CLEAR FILTERS",
"filter-all": "ALL ENTRIES",
"filter-abandoned": "ABANDONED",
"filter-fulfilled": "FULFILLED",
"filter-active": "ACTIVE"
}, },
"filters": { "filters": {
"endingStatus": "ENDS SOON", "endingStatus": "ENDS SOON",
@@ -110,7 +161,7 @@
"hour": "h", "hour": "h",
"no-limit": "NO LIMIT", "no-limit": "NO LIMIT",
"include-selected": "INCLUDE SELECTED", "include-selected": "INCLUDE SELECTED",
"save": "SAVE FILTERS", "save": "SAVE FILTERS",
"reset": "RESET FILTERS", "reset": "RESET FILTERS",
"close": "CLOSE FILTERS" "close": "CLOSE FILTERS"
}, },
@@ -125,7 +176,8 @@
"users": "Drivers online", "users": "Drivers online",
"spawns": "Spawns online", "spawns": "Spawns online",
"timetables": "Active timetables", "timetables": "Active timetables",
"no-stations": "No stations to show here!" "no-stations": "No stations to show here!",
"scenery-search": "Search for scenery..."
}, },
"trains": { "trains": {
"no-trains": "No trains to show here!", "no-trains": "No trains to show here!",
@@ -144,28 +196,6 @@
"current-signal": "at signal", "current-signal": "at signal",
"current-track": "on track", "current-track": "on track",
"option-mass": "mass",
"option-speed": "speed",
"option-length": "length",
"option-distance": "distance",
"option-timetable": "train no.",
"option-progress": "route progress",
"option-delay": "current delay",
"option-comments": "comments",
"filter-comments": "comments",
"filter-twr": "TWR",
"filter-skr": "SKR",
"filter-passenger": "passenger",
"filter-freight": "freight",
"filter-other": "other",
"filter-noTimetable": "no timetable",
"filter-reset": "X RESET",
"sorter-prefix": "Sort: ",
"search-train": "Train no.",
"search-driver": "Driver name",
"delayed": "Delayed: ", "delayed": "Delayed: ",
"preponed": "Ahead of schedule: ", "preponed": "Ahead of schedule: ",
"on-time": "On time", "on-time": "On time",
@@ -185,9 +215,12 @@
"comment": "Exploitation comments for: ", "comment": "Exploitation comments for: ",
"table-limit": "For performance reasons there's a limit of 10 trains shown at the same time.", "table-limit": "For performance reasons there's a limit of 10 trains shown at the same time.",
"last-seen-now": "last seen: just now", "last-seen-now": "since now",
"last-seen-min": "last seen: one minute ago", "last-seen-min": "since one minute",
"last-seen-ago": "last seen: {minutes} mins ago" "last-seen-ago": "since {minutes} minutes",
"scenery-offline": "Offline ride",
"timeout": "An error occured while trying to refresh SWDR timetable data!"
}, },
"journal": { "journal": {
"title": "DISPATCHER HISTORY", "title": "DISPATCHER HISTORY",
@@ -197,26 +230,6 @@
"section-timetables": "TIMETABLES", "section-timetables": "TIMETABLES",
"section-dispatchers": "DISPATCHERS", "section-dispatchers": "DISPATCHERS",
"search": "Search",
"search-train": "Train no.",
"search-driver": "Driver name",
"search-dispatcher": "Dispatcher name",
"search-station": "Scenery name",
"sort-prefix": "Sort: ",
"option-distance": "distance",
"option-total-stops": "total stops",
"option-beginDate": "date",
"option-timetableId": "timetable ID",
"option-timestampFrom": "date",
"option-duration": "duration",
"filter-all": "ALL ENTRIES",
"filter-abandoned": "ABANDONED",
"filter-fulfilled": "FULFILLED",
"filter-active": "ACTIVE",
"no-further-data": "No further data for current parameters", "no-further-data": "No further data for current parameters",
"loading-further-data": "Loading...", "loading-further-data": "Loading...",
@@ -231,13 +244,29 @@
"online-since": "ONLINE SINCE", "online-since": "ONLINE SINCE",
"duty-lasted": "The duty lasted", "duty-lasted": "The duty lasted",
"minutes": "{minutes} mins", "minutes": "{minutes} mins",
"hours": "{hours}h {minutes} mins" "hours": "{hours}h {minutes} mins",
"stock-info": "STOCK INFO",
"stock-length": "Length",
"stock-mass": "Mass",
"stock-max-speed": "Maximum registered speed",
"load-data": "Load further data...",
"stats-title": "DRIVING STATISTICS OF",
"stats-timetables": "TIMETABLES",
"stats-longest-timetable": "LONGEST TIMETABLE",
"stats-avg-timetable": "AVERAGE TIMETABLE LENGTH",
"stats-distance": "DISTANCE",
"stats-stations": "STATIONS"
}, },
"scenery": { "scenery": {
"users": "PLAYERS ONLINE", "users": "PLAYERS ONLINE",
"spawns": "OPEN SPAWNS", "spawns": "OPEN SPAWNS",
"timetables": "ACTIVE TIMETABLES", "timetables": "ACTIVE TIMETABLES",
"no-timetables": "No active timetables!", "no-timetables": "No active timetables!",
"offline": "Scenery is offline",
"no-users": "NO ACTIVE PLAYERS", "no-users": "NO ACTIVE PLAYERS",
"no-spawns": "NO OPEN SPAWNS", "no-spawns": "NO OPEN SPAWNS",
"no-scenery": "Oops! This scenery doesn't exist!", "no-scenery": "Oops! This scenery doesn't exist!",
@@ -258,24 +287,22 @@
"timetable-author-unknown": "Author unknown", "timetable-author-unknown": "Author unknown",
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required", "req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
"history-list-empty": "No recorded scenery history!" "history-list-empty": "No recorded scenery history!",
"forum-topic": "Official {name} forum topic"
}, },
"availability": { "availability": {
"title": "Availability", "title": "Availability",
"default": "in-game", "default": "in-game",
"nonDefault": "downloadable", "nonDefault": "additional",
"unavailable": "unavailable", "unavailable": "unavailable",
"nonPublic": "private", "nonPublic": "private",
"abandoned": "abandoned" "abandoned": "abandoned"
}, },
"timetables": { "timetables": {
"timetable-only": "Switch to timetable-only view", "timetable-only": "Switch to timetable-only view",
"online": "At station", "end": "Timetable terminates here",
"departed": "Dispatched to:", "terminated": "Timetable terminated",
"departed-away": "Departed to:",
"arriving": "Arriving from:",
"stopped": "Stopped",
"terminated": "Terminated",
"begins": "BEGINS HERE", "begins": "BEGINS HERE",
"terminates": "TERMINATES\nHERE" "terminates": "TERMINATES\nHERE"
}, },
+86 -56
View File
@@ -11,6 +11,13 @@
"migration-confirm": "Przyjąłem!" "migration-confirm": "Przyjąłem!"
}, },
"update": {
"title": "Nowa wersja Stacjownika jest dostępna!",
"paragraph1": "Miłego korzystania z aplikacji i niech S2 będzie z wami!",
"release-link": "Kliknij, aby przejrzeć listę zmian (GitHub)",
"confirm-button": "Przyjąłem!"
},
"data-status": { "data-status": {
"S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!", "S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!",
"S1a-sceneries": "<b>Sygnał S1a</b> <br> Błąd podczas pobierania danych o sceneriach online!", "S1a-sceneries": "<b>Sygnał S1a</b> <br> Błąd podczas pobierania danych o sceneriach online!",
@@ -67,7 +74,53 @@
}, },
"options": { "options": {
"filters": "FILTRY", "filters": "FILTRY",
"donate": "WESPRZYJ" "donate": "WESPRZYJ",
"search-button": "Szukaj",
"reset-button": "Zresetuj",
"sort-title": "SORTUJ WG:",
"filter-title": "FILTRUJ WG:",
"search-title": "SZUKAJ:",
"search-train-no": "Nr pociągu",
"search-train": "Nr pociągu / #",
"search-driver": "Nick maszynisty",
"search-dispatcher": "Nick dyżurnego",
"search-station": "Nazwa scenerii",
"search-author": "Nick autora rozkładu jazdy",
"search-date": "Data rozkładu jazdy (czas polski)",
"sort-distance": "kilometraż",
"sort-total-stops": "stacje",
"sort-beginDate": "data",
"sort-timetableId": "ID rozkładu",
"sort-timestampFrom": "data",
"sort-duration": "czas dyżuru",
"sort-mass": "masa",
"sort-speed": "prędkość",
"sort-length": "długość",
"sort-timetable": "nr pociągu",
"sort-progress": "przebyta trasa",
"sort-delay": "opóźnienie",
"sort-comments": "uwagi ekspl.",
"filter-comments": "UWAGI EKSPLOATACYJNE",
"filter-twr": "TWR",
"filter-skr": "PRZEKR. SKRAJNIA",
"filter-passenger": "PASAŻERSKIE",
"filter-freight": "TOWAROWE",
"filter-other": "INNE",
"filter-noTimetable": "BEZ RJ",
"filter-reset": "ZRESETUJ FILTRY",
"filter-clear": "WYŁĄCZ FILTRY",
"filter-all": "WSZYSTKIE",
"filter-abandoned": "PORZUCONE",
"filter-fulfilled": "WYPEŁNIONE",
"filter-active": "AKTYWNE"
}, },
"filters": { "filters": {
"endingStatus": "KOŃCZY", "endingStatus": "KOŃCZY",
@@ -111,7 +164,7 @@
"hour": " godz.", "hour": " godz.",
"no-limit": "BEZ LIMITU", "no-limit": "BEZ LIMITU",
"include-selected": "POKAŻ ZAZNACZONE", "include-selected": "POKAŻ ZAZNACZONE",
"save": "ZAPISZ FILTRY", "save": "ZAPISZ FILTRY",
"reset": "RESETUJ FILTRY", "reset": "RESETUJ FILTRY",
"close": "ZAMKNIJ FILTRY" "close": "ZAMKNIJ FILTRY"
}, },
@@ -126,7 +179,8 @@
"users": "Maszyniści online", "users": "Maszyniści online",
"spawns": "Otwarte spawny", "spawns": "Otwarte spawny",
"timetables": "Aktywne rozkłady jazdy", "timetables": "Aktywne rozkłady jazdy",
"no-stations": "Brak stacji do wyświetlenia!" "no-stations": "Brak stacji do wyświetlenia!",
"scenery-search": "Wyszukaj scenerię..."
}, },
"trains": { "trains": {
"no-trains": "Brak pociągów do wyświetlenia!", "no-trains": "Brak pociągów do wyświetlenia!",
@@ -145,28 +199,6 @@
"current-signal": "przy semaforze", "current-signal": "przy semaforze",
"current-track": "na szlaku", "current-track": "na szlaku",
"option-mass": "masa",
"option-speed": "prędkość",
"option-length": "długość",
"option-distance": "kilometraż",
"option-timetable": "nr pociągu",
"option-progress": "przebyta trasa",
"option-delay": "opóźnienie",
"option-comments": "uwagi ekspl.",
"filter-comments": "uwagi ekspl.",
"filter-twr": "TWR",
"filter-skr": "SKR",
"filter-passenger": "pasażerskie",
"filter-freight": "towarowe",
"filter-other": "inne",
"filter-noTimetable": "bez RJ",
"filter-reset": "X RESETUJ",
"sorter-prefix": "Sortuj: ",
"search-train": "Numer pociągu",
"search-driver": "Nick maszynisty",
"delayed": "Opóźniony: ", "delayed": "Opóźniony: ",
"preponed": "Przed czasem: ", "preponed": "Przed czasem: ",
"on-time": "Planowo", "on-time": "Planowo",
@@ -186,9 +218,13 @@
"comment": "Uwagi eksploatacyjne dla: ", "comment": "Uwagi eksploatacyjne dla: ",
"table-limit": "Dla płynności działania strony pokazanych jest tylko 10 pociągów zgodnie z wybranymi filtrami.", "table-limit": "Dla płynności działania strony pokazanych jest tylko 10 pociągów zgodnie z wybranymi filtrami.",
"last-seen-now": "ostatnio widziany: przed chwilą", "last-seen-now": "od niedawna",
"last-seen-min": "ostatnio widziany: minutę temu", "last-seen-min": "od minuty",
"last-seen-ago": "ostatnio widziany: {minutes} min. temu" "last-seen-ago": "od {minutes} minut",
"scenery-offline": "Przejazd offline",
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR"
}, },
"journal": { "journal": {
"title": "HISTORIA DYŻURÓW", "title": "HISTORIA DYŻURÓW",
@@ -198,26 +234,6 @@
"section-timetables": "ROZKŁADY JAZDY", "section-timetables": "ROZKŁADY JAZDY",
"section-dispatchers": "DYŻURNI", "section-dispatchers": "DYŻURNI",
"search": "Szukaj",
"search-train": "Numer pociągu",
"search-driver": "Nick maszynisty",
"search-dispatcher": "Nick dyżurnego",
"search-station": "Nazwa scenerii",
"sort-prefix": "Sortuj: ",
"option-distance": "kilometraż",
"option-total-stops": "stacje",
"option-beginDate": "data",
"option-timetableId": "ID rozkładu",
"option-timestampFrom": "data",
"option-duration": "czas dyżuru",
"filter-all": "WSZYSTKIE",
"filter-abandoned": "PORZUCONE",
"filter-fulfilled": "WYPEŁNIONE",
"filter-active": "AKTYWNE",
"no-further-data": "Brak dalszych wyników dla podanych parametrów", "no-further-data": "Brak dalszych wyników dla podanych parametrów",
"loading-further-data": "Ładowanie...", "loading-further-data": "Ładowanie...",
@@ -232,13 +248,29 @@
"timetable-day": "Rozkład z dnia", "timetable-day": "Rozkład z dnia",
"timetable-active": "AKTYWNY", "timetable-active": "AKTYWNY",
"timetable-fulfilled": "WYPEŁNIONY", "timetable-fulfilled": "WYPEŁNIONY",
"timetable-abandoned": "PORZUCONY" "timetable-abandoned": "PORZUCONY",
"stock-info": "INFORMACJE O SKŁADZIE",
"stock-length": "Długość",
"stock-mass": "Masa",
"stock-max-speed": "Maks. zarejestrowana prędkość",
"load-data": "Pobierz dalszą historię...",
"stats-title": "STATYSTYKI MASZYNISTY",
"stats-timetables": "ROZKŁADY JAZDY",
"stats-longest-timetable": "NAJDŁUŻSZY RJ",
"stats-avg-timetable": "ŚREDNIA DŁUGOŚĆ RJ",
"stats-distance": "DYSTANS",
"stats-stations": "STACJE"
}, },
"scenery": { "scenery": {
"users": "GRACZE ONLINE", "users": "GRACZE ONLINE",
"spawns": "OTWARTE SPAWNY", "spawns": "OTWARTE SPAWNY",
"timetables": "AKTYWNE ROZKŁADY JAZDY", "timetables": "AKTYWNE ROZKŁADY JAZDY",
"no-timetables": "Brak aktywnych rozkładów!", "no-timetables": "Brak aktywnych rozkładów!",
"offline": "Sceneria jest offline",
"no-users": "BRAK AKTYWNYCH GRACZY", "no-users": "BRAK AKTYWNYCH GRACZY",
"no-spawns": "BRAK OTWARTYCH SPAWNÓW", "no-spawns": "BRAK OTWARTYCH SPAWNÓW",
"no-scenery": "Ups! Ta sceneria nie istnieje!", "no-scenery": "Ups! Ta sceneria nie istnieje!",
@@ -259,7 +291,9 @@
"timetable-author-unknown": "Autor nieznany", "timetable-author-unknown": "Autor nieznany",
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego", "req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
"history-list-empty": "Brak historii dla tej scenerii!" "history-list-empty": "Brak historii dla tej scenerii!",
"forum-topic": "Oficjalny wątek scenerii {name}"
}, },
"availability": { "availability": {
"title": "Dostępność", "title": "Dostępność",
@@ -271,12 +305,8 @@
}, },
"timetables": { "timetables": {
"timetable-only": "Wyodrębnij rozkłady jazdy", "timetable-only": "Wyodrębnij rozkłady jazdy",
"online": "Na stacji", "end": "Koniec rozkładu jazdy",
"departed": "Odprawiony do:", "terminated": "Rozkład jazdy zakończony",
"departed-away": "Odjechał do:",
"arriving": "W drodze z:",
"stopped": "Postój",
"terminated": "Skończył bieg",
"begins": "ROZPOCZYNA\nBIEG", "begins": "ROZPOCZYNA\nBIEG",
"terminates": "KOŃCZY BIEG" "terminates": "KOŃCZY BIEG"
}, },
+2 -2
View File
@@ -2,8 +2,8 @@ import { createApp, Directive, ref } from 'vue';
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
import enLang from '@/locales/en.json'; import enLang from './locales/en.json';
import plLang from '@/locales/pl.json'; import plLang from './locales/pl.json';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
+13
View File
@@ -0,0 +1,13 @@
import { defineComponent } from 'vue';
export default defineComponent({
methods: {
getIcon(name: string, ext = 'svg') {
return new URL(`../assets/icon-${name}.${ext}`, import.meta.url).href;
},
getImage(name: string) {
return new URL(`../assets/${name}`, import.meta.url).href;
}
},
});
+26
View File
@@ -0,0 +1,26 @@
import { defineComponent } from 'vue';
export default defineComponent({
data() {
return {
preventKeyDown: false,
};
},
activated() {
window.addEventListener('keydown', this.handleKeyDown);
},
deactivated() {
window.removeEventListener('keydown', this.handleKeyDown);
},
methods: {
onKeyDownFunction() {},
handleKeyDown(e: KeyboardEvent) {
if (!e.key) return;
if (e.key.toLowerCase() == 'f' && !this.preventKeyDown && !e.ctrlKey && !e.altKey) this.onKeyDownFunction();
},
},
});
+31
View File
@@ -0,0 +1,31 @@
import { defineComponent } from 'vue';
import { useStore } from '../store/store';
export default defineComponent({
setup() {
return {
store: useStore(),
};
},
computed: {
chosenTrain() {
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId);
},
},
methods: {
selectModalTrain(trainId: string) {
this.store.chosenModalTrainId = trainId;
document.body.classList.add('no-scroll');
},
closeModal() {
this.store.chosenModalTrainId = undefined;
setTimeout(() => {
document.body.classList.remove('no-scroll');
}, 150);
},
},
});
+34 -31
View File
@@ -1,31 +1,34 @@
import { defineComponent, h } from "vue"; import { defineComponent, h } from 'vue';
import imageMixin from './imageMixin';
export default defineComponent({
data() { export default defineComponent({
return { mixins: [imageMixin],
icons: {
arrow: require('@/assets/icon-arrow-asc.svg'), data() {
}, return {
icons: {
showReturnButton: false arrow: this.getIcon('arrow-asc'),
} },
},
showReturnButton: false,
methods: { };
scrollToTop() { },
window.scrollTo({ top: 0 });
}, methods: {
scrollToTop() {
handleScroll() { window.scrollTo({ top: 0 });
this.showReturnButton = window.scrollY > window.innerHeight * 0.35; },
}
}, handleScroll() {
this.showReturnButton = window.scrollY > window.innerHeight * 0.35;
activated() { },
window.addEventListener('scroll', this.handleScroll); },
},
activated() {
deactivated() { window.addEventListener('wheel', this.handleScroll);
window.removeEventListener('scroll', this.handleScroll); },
},
}) deactivated() {
window.removeEventListener('wheel', this.handleScroll);
},
});
+24 -8
View File
@@ -1,8 +1,11 @@
import Train from '@/scripts/interfaces/Train';
import TrainStop from '@/scripts/interfaces/TrainStop';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import Train from '../scripts/interfaces/Train';
import TrainStop from '../scripts/interfaces/TrainStop';
import imageMixin from './imageMixin';
export default defineComponent({ export default defineComponent({
mixins: [imageMixin],
data: () => ({ data: () => ({
STATS: { STATS: {
main: [ main: [
@@ -55,6 +58,23 @@ export default defineComponent({
: this.$t('trains.last-seen-ago', { minutes: diffMins }); : this.$t('trains.last-seen-ago', { minutes: diffMins });
}, },
displayTrainPosition(train: Train) {
let positionString = '';
positionString += this.$t('trains.current-scenery') + ' ';
if (train.currentStationHash) positionString += train.currentStationName + ' ';
else positionString += train['currentStationName'].replace(/.[a-zA-Z0-9]+.sc/, '') + ' (offline) ';
if (train.signal) positionString += this.$t('trains.current-signal') + ' ' + train.signal + ' ';
if (train.connectedTrack) positionString += this.$t('trains.current-track') + ' ' + train.connectedTrack + ' ';
if (train.distance) positionString += `(${this.displayDistance(train.distance)})`;
return positionString.charAt(0).toUpperCase() + positionString.slice(1);
},
displayStopList(stops: TrainStop[]): string | undefined { displayStopList(stops: TrainStop[]): string | undefined {
if (!stops) return ''; if (!stops) return '';
@@ -62,11 +82,7 @@ export default defineComponent({
.reduce((acc: string[], stop: TrainStop, i: number) => { .reduce((acc: string[], stop: TrainStop, i: number) => {
if (stop.stopType.includes('ph') && !stop.stopNameRAW.includes('po.')) if (stop.stopType.includes('ph') && !stop.stopNameRAW.includes('po.'))
acc.push(`<strong style='color:${stop.confirmed ? 'springgreen' : 'white'}'>${stop.stopName}</strong>`); acc.push(`<strong style='color:${stop.confirmed ? 'springgreen' : 'white'}'>${stop.stopName}</strong>`);
else if ( else if (i > 0 && i < stops.length - 1 && !/po\.|sbl/gi.test(stop.stopNameRAW))
i > 0 &&
i < stops.length - 1 &&
!/po\.|sbl/gi.test(stop.stopNameRAW)
)
acc.push(`<span style='color:${stop.confirmed ? 'springgreen' : 'lightgray'}'>${stop.stopName}</span>`); acc.push(`<span style='color:${stop.confirmed ? 'springgreen' : 'lightgray'}'>${stop.stopName}</span>`);
return acc; return acc;
}, []) }, [])
@@ -121,7 +137,7 @@ export default defineComponent({
onImageError(e: Event) { onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement; const imageEl = e.target as HTMLImageElement;
imageEl.src = require('@/assets/unknown.png'); imageEl.src = this.getImage('unknown.png');
}, },
}, },
}); });
+12 -10
View File
@@ -1,42 +1,44 @@
import JournalDispatchersVue from '@/components/JournalView/JournalDispatchers.vue';
import JournalTimetablesVue from '@/components/JournalView/JournalTimetables.vue';
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import JournalDispatchersVue from '../components/JournalView/JournalDispatchers.vue';
import JournalTimetablesVue from '../components/JournalView/JournalTimetables.vue';
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
path: '/', path: '/',
name: 'StationsView', name: 'StationsView',
component: () => import('@/views/StationsView.vue'), component: () => import('../views/StationsView.vue'),
}, },
{ {
path: '/trains', path: '/trains',
name: 'TrainsView', name: 'TrainsView',
component: () => import('@/views/TrainsView.vue'), component: () => import('../views/TrainsView.vue'),
props: (route) => ({ train: route.query.train, driver: route.query.driver }), props: (route) => ({ train: route.query.train, driver: route.query.driver, trainId: route.query.trainId }),
}, },
{ {
path: '/scenery', path: '/scenery',
name: 'SceneryView', name: 'SceneryView',
component: () => import('@/views/SceneryView.vue'), component: () => import('../views/SceneryView.vue'),
props: true,
}, },
{ {
path: '/journal', path: '/journal',
name: 'JournalView', name: 'JournalView',
component: () => import('@/views/JournalView.vue'), component: () => import('../views/JournalView.vue'),
children: [ children: [
{ {
path: '', path: '',
redirect: '/journal/timetables', name: 'JournalTimetables',
component: JournalTimetablesVue, component: JournalTimetablesVue,
alias: '/timetables',
}, },
{ {
path: 'dispatchers', path: 'dispatchers',
name: 'JournalDispatchers',
component: JournalDispatchersVue, component: JournalDispatchersVue,
props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }), props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }),
}, },
{ {
path: 'timetables', path: 'timetables',
name: 'JournalTimetables',
component: JournalTimetablesVue, component: JournalTimetablesVue,
props: (route) => ({ props: (route) => ({
trainNo: route.query.trainNo, trainNo: route.query.trainNo,
@@ -56,7 +58,7 @@ const router = createRouter({
scrollBehavior(to, from) { scrollBehavior(to, from) {
if (to.name == 'SceneryView' && from.name) return { el: `.app_main` }; if (to.name == 'SceneryView' && from.name) return { el: `.app_main` };
if (from.name == 'SceneryView' && to.name == 'StationsView') return { el: `.last-selected`, top: 20 }; // if (from.name == 'SceneryView' && to.name == 'StationsView') return { el: `.last-selected`, top: 20 };
}, },
history: createWebHistory(), history: createWebHistory(),
routes, routes,
+1 -1
View File
@@ -1,4 +1,4 @@
export const enum DataStatus { export enum DataStatus {
Initialized = -1, Initialized = -1,
Loading = 0, Loading = 0,
Error = 1, Error = 1,
+8
View File
@@ -1,7 +1,9 @@
import TrainStop from "./TrainStop"; import TrainStop from "./TrainStop";
export default interface ScheduledTrain { export default interface ScheduledTrain {
trainId: string;
trainNo: number; trainNo: number;
driverName: string; driverName: string;
driverId: number; driverId: number;
currentStationName: string; currentStationName: string;
@@ -18,6 +20,12 @@ export default interface ScheduledTrain {
arrivingLine: string | null; arrivingLine: string | null;
departureLine: string | null; departureLine: string | null;
prevDepartureLine: string | null;
nextArrivalLine: string | null;
signal: string;
connectedTrack: string;
stopLabel: string; stopLabel: string;
stopStatus: string; stopStatus: string;
stopStatusID: number; stopStatusID: number;
+5 -4
View File
@@ -1,6 +1,6 @@
import { Availability } from "@/store/storeTypes"; import { Availability } from '../../store/storeTypes';
import ScheduledTrain from "./ScheduledTrain"; import ScheduledTrain from './ScheduledTrain';
import StationRoutes from "./StationRoutes"; import StationRoutes from './StationRoutes';
export default interface Station { export default interface Station {
name: string; name: string;
@@ -53,9 +53,10 @@ export default interface Station {
driverName: string; driverName: string;
driverId: number; driverId: number;
trainNo: number; trainNo: number;
trainId: string;
stopStatus?: string; stopStatus?: string;
}[]; }[];
scheduledTrains?: ScheduledTrain[]; scheduledTrains?: ScheduledTrain[];
} };
} }
+5 -2
View File
@@ -1,6 +1,8 @@
import TrainStop from "@/scripts/interfaces/TrainStop"; import TrainStop from './TrainStop';
export default interface Train { export default interface Train {
trainId: string;
mass: number; mass: number;
length: number; length: number;
speed: number; speed: number;
@@ -17,9 +19,10 @@ export default interface Train {
online: boolean; online: boolean;
lastSeen: number; lastSeen: number;
region: string; region: string;
cars: string[]; cars: string[];
isTimeout: boolean;
timetableData?: { timetableData?: {
timetableId: number; timetableId: number;
category: string; category: string;
@@ -1,12 +1,14 @@
export interface DispatcherHistory { export interface DispatcherHistory {
currentDuration: number; id: string;
dispatcherId: number;
dispatcherName: string; currentDuration: number;
isOnline: boolean; dispatcherId: number;
lastOnlineTimestamp: number; dispatcherName: string;
region: string; isOnline: boolean;
stationHash: string; lastOnlineTimestamp: number;
stationName: string; region: string;
timestampFrom: number; stationHash: string;
timestampTo?: number; stationName: string;
timestampFrom: number;
timestampTo?: number;
} }
+44 -35
View File
@@ -1,35 +1,44 @@
export interface TimetableHistory { export interface TimetableHistory {
timetableId: number; timetableId: number;
trainNo: number; trainNo: number;
trainCategoryCode: string; trainCategoryCode: string;
driverId: number; driverId: number;
driverName: string; driverName: string;
route: string; route: string;
twr: number; twr: number;
skr: number; skr: number;
sceneriesString: string; sceneriesString: string;
routeDistance: number; routeDistance: number;
currentDistance: number; currentDistance: number;
confirmedStopsCount: number; confirmedStopsCount: number;
allStopsCount: number; allStopsCount: number;
beginDate: string; beginDate: string;
endDate: string; endDate: string;
scheduledBeginDate: string; scheduledBeginDate: string;
scheduledEndDate: string; scheduledEndDate: string;
terminated: boolean; terminated: boolean;
fulfilled: boolean; fulfilled: boolean;
authorName?: string; authorName?: string;
authorId?: number; authorId?: number;
}
stockString?: string;
export interface SceneryTimetableHistory { stockMass?: number;
sceneryTimetables: TimetableHistory[]; stockLength?: number;
totalCount: number; maxSpeed?: number;
sceneryName: string;
} hashesString?: string;
currentSceneryName?: string;
currentSceneryHash?: string;
}
export interface SceneryTimetableHistory {
sceneryTimetables: TimetableHistory[];
totalCount: number;
sceneryName: string;
}
@@ -21,6 +21,7 @@ export default interface TrainAPIData {
lastSeen: number; lastSeen: number;
region: string; region: string;
isTimeout: boolean;
timetable?: { timetable?: {
timetableId: number; timetableId: number;
@@ -0,0 +1,41 @@
export interface Author {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
}
export interface ReleaseAPIData {
url: string;
assets_url: string;
upload_url: string;
html_url: string;
id: number;
author: Author;
node_id: string;
tag_name: string;
target_commitish: string;
name: string;
draft: boolean;
prerelease: boolean;
created_at: Date;
published_at: Date;
assets: any[];
tarball_url: string;
zipball_url: string;
body: string;
}
+7
View File
@@ -23,6 +23,13 @@ export default class StorageManager {
window.localStorage.setItem(key, val); window.localStorage.setItem(key, val);
} }
static setValue(key: string, val: any) {
if (typeof val == 'boolean') this.setBooleanValue(key, val);
else if (typeof val == 'number') this.setNumericValue(key, val);
else if (typeof val == 'string') this.setStringValue(key, val);
else this.setStringValue(key, val);
}
static removeValue(key: string) { static removeValue(key: string) {
window.localStorage.removeItem(key); window.localStorage.removeItem(key);
} }
+114 -114
View File
@@ -1,115 +1,115 @@
import { TrainFilter } from "vue"; import { TrainFilter } from "../../types/Trains/TrainOptionsTypes";
import { TrainFilterType } from "../enums/TrainFilterType"; import { TrainFilterType } from "../enums/TrainFilterType";
import Train from "../interfaces/Train"; import Train from "../interfaces/Train";
import TrainStop from "../interfaces/TrainStop"; import TrainStop from "../interfaces/TrainStop";
function confirmedPercentage(stops: TrainStop[] | undefined) { function confirmedPercentage(stops: TrainStop[] | undefined) {
if (!stops) return -1; if (!stops) return -1;
return Number(((stops.filter((stop) => stop.confirmed).length / stops.length) * 100).toFixed(0)); return Number(((stops.filter((stop) => stop.confirmed).length / stops.length) * 100).toFixed(0));
}; };
function currentDelay(stops: TrainStop[] | undefined) { function currentDelay(stops: TrainStop[] | undefined) {
if (!stops) return -Infinity; if (!stops) return -Infinity;
const delay = const delay =
stops.find((stop, i) => (i == 0 && !stop.confirmed) || (i > 0 && stops[i - 1].confirmed && !stop.confirmed)) stops.find((stop, i) => (i == 0 && !stop.confirmed) || (i > 0 && stops[i - 1].confirmed && !stop.confirmed))
?.departureDelay || 0; ?.departureDelay || 0;
return delay; return delay;
}; };
function filterTrainList(trainList: Train[], searchedTrain: string, searchedDriver: string, filters: TrainFilter[]) { function filterTrainList(trainList: Train[], searchedTrain: string, searchedDriver: string, filters: TrainFilter[]) {
return trainList.filter( return trainList.filter(
(train) => { (train) => {
const isFiltered = filters.every(f => { const isFiltered = filters.every(f => {
if (f.isActive) return true; if (f.isActive) return true;
if (!train.timetableData) return filters.find(filter => filter.id == TrainFilterType.noTimetable)!.isActive; if (!train.timetableData) return filters.find(filter => filter.id == TrainFilterType.noTimetable)!.isActive;
switch (f.id) { switch (f.id) {
case TrainFilterType.comments: case TrainFilterType.comments:
return !train.timetableData.followingStops.some(stop => stop.comments); return !train.timetableData.followingStops.some(stop => stop.comments);
case TrainFilterType.twr: case TrainFilterType.twr:
return !train.timetableData.TWR; return !train.timetableData.TWR;
case TrainFilterType.skr: case TrainFilterType.skr:
return !train.timetableData.SKR; return !train.timetableData.SKR;
case TrainFilterType.passenger: case TrainFilterType.passenger:
return !/^[AMRE]\D{2}$/.test(train.timetableData.category); return !/^[AMRE]\D{2}$/.test(train.timetableData.category);
case TrainFilterType.freight: case TrainFilterType.freight:
return !train.timetableData.category.startsWith('T'); return !train.timetableData.category.startsWith('T');
case TrainFilterType.other: case TrainFilterType.other:
return !/^[PXZL]\D{2}$/.test(train.timetableData.category); return !/^[PXZL]\D{2}$/.test(train.timetableData.category);
default: default:
return true; return true;
} }
}) })
return (searchedTrain.length > 0 ? train.trainNo.toString().startsWith(searchedTrain) : true) && return (searchedTrain.length > 0 ? train.trainNo.toString().startsWith(searchedTrain) : true) &&
(searchedDriver.length > 0 ? train.driverName.toLowerCase().startsWith(searchedDriver.toLowerCase()) : true) && isFiltered (searchedDriver.length > 0 ? train.driverName.toLowerCase().startsWith(searchedDriver.toLowerCase()) : true) && isFiltered
} }
); );
} }
function sortTrainList(trainList: Train[], sorterActive: { id: string; dir: number }) { function sortTrainList(trainList: Train[], sorterActive: { id: string; dir: number }) {
return trainList.sort((a: Train, b: Train) => { return trainList.sort((a: Train, b: Train) => {
switch (sorterActive.id) { switch (sorterActive.id) {
case 'mass': case 'mass':
if (a.mass > b.mass) return sorterActive.dir; if (a.mass > b.mass) return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'distance': case 'distance':
if ((a.timetableData?.routeDistance || -1) > (b.timetableData?.routeDistance || -1)) return sorterActive.dir; if ((a.timetableData?.routeDistance || -1) > (b.timetableData?.routeDistance || -1)) return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'progress': case 'progress':
if (confirmedPercentage(a.timetableData?.followingStops) > confirmedPercentage(b.timetableData?.followingStops)) if (confirmedPercentage(a.timetableData?.followingStops) > confirmedPercentage(b.timetableData?.followingStops))
return sorterActive.dir; return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'delay': case 'delay':
if (currentDelay(a.timetableData?.followingStops) > currentDelay(b.timetableData?.followingStops)) if (currentDelay(a.timetableData?.followingStops) > currentDelay(b.timetableData?.followingStops))
return sorterActive.dir; return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'speed': case 'speed':
if (a.speed > b.speed) return sorterActive.dir; if (a.speed > b.speed) return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'timetable': case 'timetable':
if (a.trainNo > b.trainNo) return sorterActive.dir; if (a.trainNo > b.trainNo) return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'length': case 'length':
if (a.length > b.length) return sorterActive.dir; if (a.length > b.length) return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
default: default:
break; break;
} }
return 0; return 0;
}); });
} }
export function filteredTrainList( export function filteredTrainList(
trainList: Train[], trainList: Train[],
searchedTrain: string, searchedTrain: string,
searchedDriver: string, searchedDriver: string,
sorterActive: { id: string; dir: number }, sorterActive: { id: string; dir: number },
filters: TrainFilter[] filters: TrainFilter[]
) { ) {
const filtered = filterTrainList(trainList, searchedTrain, searchedDriver, filters); const filtered = filterTrainList(trainList, searchedTrain, searchedDriver, filters);
return [...sortTrainList(filtered, sorterActive)]; return [...sortTrainList(filtered, sorterActive)];
}; };
+4 -1
View File
@@ -1,5 +1,8 @@
export const URLs = { export const URLs = {
stacjownikAPI: process.env.VUE_APP_API_DEV != 1 ? 'https://stacjownik.eu-4.evennode.com' : 'http://localhost:3000', stacjownikAPI:
import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD
? 'http://localhost:3000'
: 'https://stacjownik.eu-4.evennode.com',
stacjownikAPIDev: 'localhost:3000', stacjownikAPIDev: 'localhost:3000',
// trains: "https://api.td2.info.pl:9640/?method=getTrainsOnline", // trains: "https://api.td2.info.pl:9640/?method=getTrainsOnline",
// getTimetableURL: (trainNo: string | number, region = "eu") => `https://api.td2.info.pl:9640/?method=readFromSWDR&value=getTimetable%3B${trainNo}%3B${region}` // getTimetableURL: (trainNo: string | number, region = "eu") => `https://api.td2.info.pl:9640/?method=readFromSWDR&value=getTimetable%3B${trainNo}%3B${region}`
+23 -6
View File
@@ -117,31 +117,37 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
let prevStationName = '', let prevStationName = '',
nextStationName = ''; nextStationName = '';
let prevDepartureLine: string | null = null,
nextArrivalLine: string | null = null;
for (let i = trainStopIndex - 1; i >= 0; i--) { for (let i = trainStopIndex - 1; i >= 0; i--) {
if (/strong|podg/g.test(followingStops[i].stopName)) { if (/strong|podg/g.test(followingStops[i].stopName)) {
prevStationName = followingStops[i].stopNameRAW; prevStationName = followingStops[i].stopNameRAW.replace(/,.*/g,"");
break; break;
} }
} }
for (let i = trainStopIndex + 1; i < followingStops.length; i++) { for (let i = trainStopIndex + 1; i < followingStops.length; i++) {
if (/strong|podg/g.test(followingStops[i].stopName)) { if (/strong|podg/g.test(followingStops[i].stopName)) {
nextStationName = followingStops[i].stopNameRAW; nextStationName = followingStops[i].stopNameRAW.replace(/,.*/g,"");
break; break;
} }
} }
let departureLine: string | null = trainStop.departureLine; let departureLine: string | null = null;
let arrivingLine: string | null = trainStop.arrivalLine; let arrivingLine: string | null = null;
for (let i = trainStopIndex; i < followingStops.length; i++) { for (let i = trainStopIndex; i < followingStops.length; i++) {
const currentStop = followingStops[i]; const currentStop = followingStops[i];
if (currentStop.departureLine == null) break; if (currentStop.departureLine == null) continue;
if (!/-|_|it|sbl/gi.test(currentStop.departureLine)) { if (!/-|_|it|sbl/gi.test(currentStop.departureLine)) {
departureLine = currentStop.departureLine; departureLine = currentStop.departureLine;
nextArrivalLine = followingStops[i + 1]?.arrivalLine || null;
break; break;
} }
} }
@@ -149,16 +155,24 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
for (let i = trainStopIndex; i >= 0; i--) { for (let i = trainStopIndex; i >= 0; i--) {
const currentStop = followingStops[i]; const currentStop = followingStops[i];
if (currentStop.arrivalLine == null) break; if (currentStop.arrivalLine == null) continue;
if (!/-|_|it|sbl/gi.test(currentStop.arrivalLine)) { if (!/-|_|it|sbl/gi.test(currentStop.arrivalLine)) {
arrivingLine = currentStop.arrivalLine; arrivingLine = currentStop.arrivalLine;
prevDepartureLine = followingStops[i - 1]?.departureLine || null;
break; break;
} }
} }
return { return {
trainNo: train.trainNo, trainNo: train.trainNo,
trainId: train.trainId,
signal: train.signal,
connectedTrack: train.connectedTrack,
driverName: train.driverName, driverName: train.driverName,
driverId: train.driverId, driverId: train.driverId,
currentStationName: train.currentStationName, currentStationName: train.currentStationName,
@@ -177,5 +191,8 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
arrivingLine, arrivingLine,
departureLine, departureLine,
nextArrivalLine,
prevDepartureLine,
}; };
} }
-6
View File
@@ -1,6 +0,0 @@
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
@@ -1,292 +1,305 @@
import Station from '@/scripts/interfaces/Station'; import { defineStore } from 'pinia';
import Filter from '@/scripts/interfaces/Filter'; import inputData from '../data/options.json';
import StorageManager from './storageManager'; import Filter from '../scripts/interfaces/Filter';
import Station from '../scripts/interfaces/Station';
const sortStations = (a: Station, b: Station, sorter: { index: number; dir: number }) => { import StorageManager from '../scripts/managers/storageManager';
switch (sorter.index) {
case 1: const sortStations = (a: Station, b: Station, sorter: { index: number; dir: number }) => {
if ((a.generalInfo?.reqLevel || 0) > (b.generalInfo?.reqLevel || 0)) return sorter.dir; switch (sorter.index) {
if ((a.generalInfo?.reqLevel || 0) < (b.generalInfo?.reqLevel || 0)) return -sorter.dir; case 0:
break; return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
case 2: case 1:
if ((a.onlineInfo?.statusTimestamp || 0) > (b.onlineInfo?.statusTimestamp || 0)) return sorter.dir; if ((a.generalInfo?.reqLevel || 0) > (b.generalInfo?.reqLevel || 0)) return sorter.dir;
if ((a.onlineInfo?.statusTimestamp || 0) < (b.onlineInfo?.statusTimestamp || 0)) return -sorter.dir; if ((a.generalInfo?.reqLevel || 0) < (b.generalInfo?.reqLevel || 0)) return -sorter.dir;
break; break;
case 3: case 2:
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') > (b.onlineInfo?.dispatcherName.toLowerCase() || '')) if ((a.onlineInfo?.statusTimestamp || 0) > (b.onlineInfo?.statusTimestamp || 0)) return sorter.dir;
return sorter.dir; if ((a.onlineInfo?.statusTimestamp || 0) < (b.onlineInfo?.statusTimestamp || 0)) return -sorter.dir;
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') < (b.onlineInfo?.dispatcherName.toLowerCase() || '')) break;
return -sorter.dir;
break; case 3:
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') > (b.onlineInfo?.dispatcherName.toLowerCase() || ''))
case 4: return sorter.dir;
if ((a.onlineInfo?.dispatcherExp || 0) > (b.onlineInfo?.dispatcherExp || 0)) return sorter.dir; if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') < (b.onlineInfo?.dispatcherName.toLowerCase() || ''))
if ((a.onlineInfo?.dispatcherExp || 0) < (b.onlineInfo?.dispatcherExp || 0)) return -sorter.dir; return -sorter.dir;
break; break;
case 7: case 4:
if ((a.onlineInfo?.currentUsers || 0) > (b.onlineInfo?.currentUsers || 0)) return sorter.dir; if ((a.onlineInfo?.dispatcherExp || 0) > (b.onlineInfo?.dispatcherExp || 0)) return sorter.dir;
if ((a.onlineInfo?.currentUsers || 0) < (b.onlineInfo?.currentUsers || 0)) return -sorter.dir; if ((a.onlineInfo?.dispatcherExp || 0) < (b.onlineInfo?.dispatcherExp || 0)) return -sorter.dir;
break;
if ((a.onlineInfo?.maxUsers || 0) > (b.onlineInfo?.maxUsers || 0)) return sorter.dir;
if ((a.onlineInfo?.maxUsers || 0) < (b.onlineInfo?.maxUsers || 0)) return -sorter.dir; case 7:
break; if ((a.onlineInfo?.currentUsers || 0) > (b.onlineInfo?.currentUsers || 0)) return sorter.dir;
if ((a.onlineInfo?.currentUsers || 0) < (b.onlineInfo?.currentUsers || 0)) return -sorter.dir;
case 8:
if ((a.onlineInfo?.spawns.length || 0) > (b.onlineInfo?.spawns.length || 0)) return sorter.dir; if ((a.onlineInfo?.maxUsers || 0) > (b.onlineInfo?.maxUsers || 0)) return sorter.dir;
if ((a.onlineInfo?.spawns.length || 0) < (b.onlineInfo?.spawns.length || 0)) return -sorter.dir; if ((a.onlineInfo?.maxUsers || 0) < (b.onlineInfo?.maxUsers || 0)) return -sorter.dir;
break;
break;
case 8:
case 9: if ((a.onlineInfo?.spawns.length || 0) > (b.onlineInfo?.spawns.length || 0)) return sorter.dir;
if ((a.onlineInfo?.scheduledTrains?.length || 0) > (b.onlineInfo?.scheduledTrains?.length || 0)) if ((a.onlineInfo?.spawns.length || 0) < (b.onlineInfo?.spawns.length || 0)) return -sorter.dir;
return sorter.dir;
if ((a.onlineInfo?.scheduledTrains?.length || 0) < (b.onlineInfo?.scheduledTrains?.length || 0)) break;
return -sorter.dir;
case 9:
default: if ((a.onlineInfo?.scheduledTrains?.length || 0) > (b.onlineInfo?.scheduledTrains?.length || 0))
break; return sorter.dir;
} if ((a.onlineInfo?.scheduledTrains?.length || 0) < (b.onlineInfo?.scheduledTrains?.length || 0))
return -sorter.dir;
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
}; default:
break;
const filterStations = (station: Station, filters: Filter) => { }
const returnMode = false;
return a.name.localeCompare(b.name);
if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic']) };
return returnMode;
const filterStations = (station: Station, filters: Filter) => {
if (station.onlineInfo?.statusID == 'ending' && filters['ending']) return returnMode; const returnMode = false;
if ( if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic'])
station.onlineInfo && return returnMode;
station.onlineInfo.statusTimestamp > 0 &&
filters['onlineFromHours'] < 8 && if (station.onlineInfo?.statusID == 'ending' && filters['ending']) return returnMode;
station.onlineInfo.statusTimestamp <= Date.now() + filters['onlineFromHours'] * 3600000
) if (
return returnMode; station.onlineInfo &&
station.onlineInfo.statusTimestamp > 0 &&
if (filters['onlineFromHours'] > 0 && station.onlineInfo && station.onlineInfo.statusTimestamp <= 0) filters['onlineFromHours'] < 8 &&
return returnMode; station.onlineInfo.statusTimestamp <= Date.now() + filters['onlineFromHours'] * 3600000
if (filters['onlineFromHours'] == 8 && station.onlineInfo?.statusID != 'no-limit') return returnMode; )
return returnMode;
if (station.onlineInfo?.statusID == 'ending' && filters['endingStatus']) return returnMode;
if ( if (filters['onlineFromHours'] > 0 && station.onlineInfo && station.onlineInfo.statusTimestamp <= 0)
(station.onlineInfo?.statusID == 'not-signed' || station.onlineInfo?.statusID == 'unavailable') && return returnMode;
filters['unavailableStatus'] if (filters['onlineFromHours'] == 8 && station.onlineInfo?.statusID != 'no-limit') return returnMode;
)
return returnMode; if (station.onlineInfo?.statusID == 'ending' && filters['endingStatus']) return returnMode;
if (station.onlineInfo?.statusID == 'brb' && filters['afkStatus']) return returnMode; if (
if (station.onlineInfo?.statusID == 'no-space' && filters['noSpaceStatus']) return returnMode; (station.onlineInfo?.statusID == 'not-signed' || station.onlineInfo?.statusID == 'unavailable') &&
filters['unavailableStatus']
if (station.onlineInfo && filters['occupied']) return returnMode; )
if (!station.onlineInfo && filters['free']) return returnMode; return returnMode;
if (station.generalInfo?.availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo) if (station.onlineInfo?.statusID == 'brb' && filters['afkStatus']) return returnMode;
return returnMode; if (station.onlineInfo?.statusID == 'no-space' && filters['noSpaceStatus']) return returnMode;
if (station.generalInfo) { if (station.onlineInfo && filters['occupied']) return returnMode;
const routes = station.generalInfo.routes; if (!station.onlineInfo && filters['free']) return returnMode;
const availability = station.generalInfo.availability; if (station.generalInfo?.availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo)
return returnMode;
if (filters['abandoned'] && availability == 'abandoned') return returnMode;
if (station.generalInfo) {
if (availability == 'default' && filters['default']) return returnMode; const routes = station.generalInfo.routes;
if ( const availability = station.generalInfo.availability;
availability != 'default' &&
filters['notDefault'] && if (filters['abandoned'] && availability == 'abandoned' && !station.onlineInfo) return returnMode;
!(availability == 'abandoned' || availability == 'unavailable')
) if (availability == 'default' && filters['default']) return returnMode;
return returnMode; if (
availability != 'default' &&
if (filters['real'] && station.generalInfo.lines != '') return returnMode; filters['notDefault'] &&
if ( !(availability == 'abandoned' || availability == 'unavailable')
filters['fictional'] && )
station.generalInfo.lines == '' && return returnMode;
availability != 'abandoned' &&
availability != 'unavailable' if (filters['real'] && station.generalInfo.lines != '') return returnMode;
) if (
return returnMode; filters['fictional'] &&
station.generalInfo.lines == '' &&
if ( availability != 'abandoned' &&
station.generalInfo.reqLevel + availability != 'unavailable'
(availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned' ? 1 : 0) < )
filters['minLevel'] return returnMode;
)
return returnMode; if (
if ( station.generalInfo.reqLevel +
station.generalInfo.reqLevel + (availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned' ? 1 : 0) <
(availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned' ? 1 : 0) > filters['minLevel']
filters['maxLevel'] )
) return returnMode;
return returnMode; if (
station.generalInfo.reqLevel +
if ( (availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned' ? 1 : 0) >
filters['no-1track'] && filters['maxLevel']
(routes.oneWayCatenaryRouteNames.length != 0 || routes.oneWayNoCatenaryRouteNames.length != 0) )
) return returnMode;
return returnMode;
if ( if (
filters['no-2track'] && filters['no-1track'] &&
(routes.twoWayCatenaryRouteNames.length != 0 || routes.twoWayNoCatenaryRouteNames.length != 0) (routes.oneWayCatenaryRouteNames.length != 0 || routes.oneWayNoCatenaryRouteNames.length != 0)
) )
return returnMode; return returnMode;
if (
if (routes.oneWayCatenaryRouteNames.length < filters['minOneWayCatenary']) return returnMode; filters['no-2track'] &&
if (routes.oneWayNoCatenaryRouteNames.length < filters['minOneWay']) return returnMode; (routes.twoWayCatenaryRouteNames.length != 0 || routes.twoWayNoCatenaryRouteNames.length != 0)
)
if (routes.twoWayCatenaryRouteNames.length < filters['minTwoWayCatenary']) return returnMode; return returnMode;
if (routes.twoWayNoCatenaryRouteNames.length < filters['minTwoWay']) return returnMode;
if (routes.oneWayCatenaryRouteNames.length < filters['minOneWayCatenary']) return returnMode;
if (filters[station.generalInfo.controlType]) return returnMode; if (routes.oneWayNoCatenaryRouteNames.length < filters['minOneWay']) return returnMode;
if (filters[station.generalInfo.signalType]) return returnMode;
if (routes.twoWayCatenaryRouteNames.length < filters['minTwoWayCatenary']) return returnMode;
if ( if (routes.twoWayNoCatenaryRouteNames.length < filters['minTwoWay']) return returnMode;
filters['SPK'] &&
(station.generalInfo.controlType === 'SPK' || station.generalInfo.controlType.includes('+SPK')) if (filters[station.generalInfo.controlType]) return returnMode;
) if (filters[station.generalInfo.signalType]) return returnMode;
return returnMode;
if ( if (
filters['SCS'] && filters['SPK'] &&
(station.generalInfo.controlType === 'SCS' || station.generalInfo.controlType.includes('+SCS')) (station.generalInfo.controlType === 'SPK' || station.generalInfo.controlType.includes('+SPK'))
) )
return returnMode; return returnMode;
if ( if (
filters['SPE'] && filters['SCS'] &&
(station.generalInfo.controlType === 'SPE' || station.generalInfo.controlType.includes('+SPE')) (station.generalInfo.controlType === 'SCS' || station.generalInfo.controlType.includes('+SCS'))
) )
return returnMode; return returnMode;
if (filters['SUP'] && station.generalInfo.SUP) return returnMode; if (
filters['SPE'] &&
if ( (station.generalInfo.controlType === 'SPE' || station.generalInfo.controlType.includes('+SPE'))
filters['SCS'] && )
filters['SPK'] && return returnMode;
(station.generalInfo.controlType.includes('SPK') || station.generalInfo.controlType.includes('SCS')) if (filters['SUP'] && station.generalInfo.SUP) return returnMode;
)
return returnMode; if (
filters['SCS'] &&
if (filters['mechaniczne'] && station.generalInfo.controlType.includes('mechaniczne')) return returnMode; filters['SPK'] &&
(station.generalInfo.controlType.includes('SPK') || station.generalInfo.controlType.includes('SCS'))
if (filters['ręczne'] && station.generalInfo.controlType.includes('ręczne')) return returnMode; )
return returnMode;
if (filters['SBL'] && routes.sblRouteNames.length > 0) return returnMode;
if (filters['mechaniczne'] && station.generalInfo.controlType.includes('mechaniczne')) return returnMode;
if (
filters['authors'].length > 3 && if (filters['ręczne'] && station.generalInfo.controlType.includes('ręczne')) return returnMode;
!station.generalInfo.authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase())
) if (filters['SBL'] && routes.sblRouteNames.length > 0) return returnMode;
return returnMode;
} if (
filters['authors'].length > 3 &&
return true; !station.generalInfo.authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase())
}; )
return returnMode;
export default class StationFilterManager { }
private filterInitStates: Filter = {
default: false, return true;
notDefault: false, };
real: false,
fictional: false, const filterInitStates: Filter = {
SPK: false, default: false,
SCS: false, notDefault: false,
SPE: false, real: false,
SUP: false, fictional: false,
ręczne: false, SPK: false,
mechaniczne: false, SCS: false,
współczesna: false, SPE: false,
kształtowa: false, SUP: false,
historyczna: false, ręczne: false,
mieszana: false, mechaniczne: false,
SBL: false, współczesna: false,
minLevel: 0, kształtowa: false,
maxLevel: 20, historyczna: false,
minOneWayCatenary: 0, mieszana: false,
minOneWay: 0, SBL: false,
minTwoWayCatenary: 0, minLevel: 0,
minTwoWay: 0, maxLevel: 20,
'include-selected': false, minOneWayCatenary: 0,
'no-1track': false, minOneWay: 0,
'no-2track': false, minTwoWayCatenary: 0,
free: true, minTwoWay: 0,
occupied: false, 'include-selected': false,
ending: false, 'no-1track': false,
nonPublic: false, 'no-2track': false,
unavailable: true, free: true,
abandoned: true, occupied: false,
afkStatus: false, ending: false,
endingStatus: false, nonPublic: false,
noSpaceStatus: false, unavailable: true,
unavailableStatus: false, abandoned: true,
unsignedStatus: false, afkStatus: false,
endingStatus: false,
authors: '', noSpaceStatus: false,
unavailableStatus: false,
onlineFromHours: 0, unsignedStatus: false,
};
authors: '',
private filters: Filter = { ...this.filterInitStates };
onlineFromHours: 0,
private sorter: { index: number; dir: number } = { index: 0, dir: 1 }; };
checkFilters() { export const useStationFiltersStore = defineStore('stationFiltersStore', {
if (!StorageManager.isRegistered('options_saved')) return; state() {
return {
Object.keys(this.filterInitStates).forEach((filterKey) => { inputs: inputData,
if (StorageManager.isRegistered(filterKey)) return; filters: { ...filterInitStates },
sorterActive: { index: 0, dir: 1 },
const filterType = typeof this.filterInitStates[filterKey]; };
},
if (filterType === 'boolean')
StorageManager.setBooleanValue(filterKey, !this.filterInitStates[filterKey] as boolean); actions: {
getFilteredStationList(stationList: Station[], region: string): Station[] {
if (filterType === 'number') return stationList
StorageManager.setNumericValue(filterKey, this.filterInitStates[filterKey] as number); .map((station) => {
}); if (station.onlineInfo && station.onlineInfo.region != region) {
} delete station.onlineInfo;
}
getFilteredStationList(stationList: Station[], region: string): Station[] {
return stationList return station;
.map((station) => { })
if (station.onlineInfo && station.onlineInfo.region != region) { .filter((station) => filterStations(station, this.filters))
delete station.onlineInfo; .sort((a, b) => sortStations(a, b, this.sorterActive));
} },
return station; setupFilters() {
}) if (!StorageManager.isRegistered('options_saved')) return;
.filter((station) => filterStations(station, this.filters))
.sort((a, b) => sortStations(a, b, this.sorter)); this.inputs.options.forEach((option) => {
} if (!StorageManager.isRegistered(option.id)) return;
const savedValue = StorageManager.getBooleanValue(option.id);
changeFilterValue(filter: { name: string; value: number }) {
this.filters[filter.name] = filter.value; this.filters[option.id] = savedValue;
option.value = !savedValue;
// if(filter.name == 'authors') });
}
this.inputs.sliders.forEach((slider) => {
resetFilters() { if (!StorageManager.isRegistered(slider.name)) return;
this.filters = { ...this.filterInitStates }; const savedValue = StorageManager.getNumericValue(slider.name);
}
this.filters[slider.name] = savedValue;
invertFilters() { slider.value = savedValue;
Object.keys(this.filters).forEach((prop) => { });
if (typeof this.filters[prop] !== 'boolean') return; },
this.filters[prop] = !this.filters[prop]; changeFilterValue(filter: { name: string; value: any }) {
}); this.filters[filter.name] = filter.value;
}
if (StorageManager.isRegistered('options_saved')) StorageManager.setValue(filter.name, filter.value);
changeSorter(index: number) { },
if (index > 4 && index < 7) return;
resetFilters() {
if (index == this.sorter.index) this.sorter.dir = -1 * this.sorter.dir; this.filters = { ...filterInitStates };
else this.sorter.dir = 1;
this.inputs.options.forEach((option) => {
this.sorter.index = index; option.value = option.defaultValue;
} StorageManager.setBooleanValue(option.name, !option.defaultValue);
});
getSorter() {
return this.sorter; this.inputs.sliders.forEach((slider) => {
} slider.value = slider.defaultValue;
} StorageManager.setNumericValue(slider.name, slider.defaultValue);
});
},
changeSorter(index: number) {
if (index > 4 && index < 7) return;
if (index == this.sorterActive.index) this.sorterActive.dir = -1 * this.sorterActive.dir;
else this.sorterActive.dir = 1;
this.sorterActive.index = index;
},
},
});
+401 -389
View File
@@ -1,389 +1,401 @@
import { DataStatus } from '@/scripts/enums/DataStatus'; import axios from 'axios';
import StationAPIData from '@/scripts/interfaces/api/StationAPIData'; import { defineStore } from 'pinia';
import ScheduledTrain from '@/scripts/interfaces/ScheduledTrain'; import { io } from 'socket.io-client';
import Station from '@/scripts/interfaces/Station'; import { DataStatus } from '../scripts/enums/DataStatus';
import StationRoutes from '@/scripts/interfaces/StationRoutes'; import StationAPIData from '../scripts/interfaces/api/StationAPIData';
import Train from '@/scripts/interfaces/Train'; import ScheduledTrain from '../scripts/interfaces/ScheduledTrain';
import { URLs } from '@/scripts/utils/apiURLs'; import Station from '../scripts/interfaces/Station';
import { import StationRoutes from '../scripts/interfaces/StationRoutes';
getLocoURL, import Train from '../scripts/interfaces/Train';
getScheduledTrain, import { URLs } from '../scripts/utils/apiURLs';
getStatusID, import {
getStatusTimestamp, getLocoURL,
parseSpawns, getStatusTimestamp,
} from '@/scripts/utils/storeUtils'; getStatusID,
import axios from 'axios'; getScheduledTrain,
import { defineStore } from 'pinia'; parseSpawns,
import { io } from 'socket.io-client'; } from '../scripts/utils/storeUtils';
import { APIData, StationJSONData, StoreState } from './storeTypes'; import { APIData, StationJSONData, StoreState } from './storeTypes';
export const useStore = defineStore('store', {
state: () => export const useStore = defineStore('store', {
({ state: () =>
apiData: {} as unknown, ({
apiData: {} as unknown,
stationList: [],
trainList: [], stationList: [],
trainList: [],
sceneryData: [],
lastDispatcherStatuses: [], sceneryData: [],
lastDispatcherStatuses: [],
region: { id: 'eu', value: 'PL1' },
region: { id: 'eu', value: 'PL1' },
trainCount: 0,
stationCount: 0, trainCount: 0,
stationCount: 0,
webSocket: undefined,
webSocket: undefined,
dispatcherStatsName: '',
dispatcherStatsData: undefined, dispatcherStatsName: '',
dispatcherStatsData: undefined,
driverStatsName: '',
driverStatsData: undefined, driverStatsName: '',
driverStatsData: undefined,
dataStatuses: {
connection: DataStatus.Loading, chosenModalTrainId: undefined,
sceneries: DataStatus.Loading,
timetables: DataStatus.Loading, dataStatuses: {
dispatchers: DataStatus.Loading, connection: DataStatus.Loading,
trains: DataStatus.Loading, sceneries: DataStatus.Loading,
}, timetables: DataStatus.Loading,
dispatchers: DataStatus.Loading,
listenerLaunched: false, trains: DataStatus.Loading,
} as StoreState), },
actions: { blockScroll: false,
setTrainsOnlineData() { listenerLaunched: false,
const { trains } = this.apiData;
} as StoreState),
if (!trains) return [];
actions: {
this.trainList = trains setTrainsOnlineData() {
.filter( const { trains } = this.apiData;
(train) =>
train.region === this.region.id && (train.online || train.timetable || train.lastSeen > Date.now() - 180000) if (!trains) return [];
)
.map((train) => { this.trainList = trains
const stock = train.stockString.split(';'); .filter(
const locoType = stock ? stock[0] : train.stockString; (train) =>
train.region === this.region.id && (train.online || train.timetable || train.lastSeen > Date.now() - 180000)
const timetable = train.timetable; )
.map((train) => {
return { const stock = train.stockString.split(';');
trainNo: train.trainNo, const locoType = stock ? stock[0] : train.stockString;
mass: train.mass,
length: train.length, const timetable = train.timetable;
speed: train.speed,
region: train.region, return {
trainId: train.driverName + train.trainNo.toString(),
distance: train.distance,
signal: train.signal, trainNo: train.trainNo,
online: train.online, mass: train.mass,
driverId: train.driverId, length: train.length,
driverName: train.driverName, speed: train.speed,
currentStationName: train.currentStationName, region: train.region,
currentStationHash: train.currentStationHash,
connectedTrack: train.connectedTrack, distance: train.distance,
locoType, signal: train.signal,
locoURL: getLocoURL(locoType), online: train.online,
cars: stock.slice(1), driverId: train.driverId,
driverName: train.driverName,
lastSeen: train.lastSeen, currentStationName: train.currentStationName,
currentStationHash: train.currentStationHash,
timetableData: timetable connectedTrack: train.connectedTrack,
? { locoType,
timetableId: timetable.timetableId, locoURL: getLocoURL(locoType),
SKR: timetable.SKR, cars: stock.slice(1),
TWR: timetable.TWR,
route: timetable.route, lastSeen: train.lastSeen,
category: timetable.category, isTimeout: train.isTimeout,
followingStops: timetable.stopList,
routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance, timetableData: timetable
sceneries: timetable.sceneries, ? {
} timetableId: timetable.timetableId,
: undefined, SKR: timetable.SKR,
}; TWR: timetable.TWR,
}) as Train[]; route: timetable.route,
}, category: timetable.category,
followingStops: timetable.stopList,
getDispatcherStatus(onlineStationData: StationAPIData) { routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance,
const { dispatchers } = this.apiData; sceneries: timetable.sceneries,
}
const prevDispatcherStatus = this.lastDispatcherStatuses.find( : undefined,
(dispatcher) => dispatcher.hash === onlineStationData.stationHash };
); }) as Train[];
},
const stationStatus = !dispatchers
? undefined getDispatcherStatus(onlineStationData: StationAPIData) {
: dispatchers.find( const { dispatchers } = this.apiData;
(status: string[]) => status[0] == onlineStationData.stationHash && status[1] == this.region.id
) || -1; const prevDispatcherStatus = this.lastDispatcherStatuses.find(
(dispatcher) => dispatcher.hash === onlineStationData.stationHash
const statusTimestamp = );
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusTimestamp : getStatusTimestamp(stationStatus);
const statusID = const stationStatus = !dispatchers
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusID : getStatusID(stationStatus); ? undefined
: dispatchers.find(
return { (status: string[]) => status[0] == onlineStationData.stationHash && status[1] == this.region.id
hash: onlineStationData.stationHash, ) || -1;
statusID,
statusTimestamp, const statusTimestamp =
}; prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusTimestamp : getStatusTimestamp(stationStatus);
}, const statusID =
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusID : getStatusID(stationStatus);
getScheduledTrains(stationGeneralInfo: Station['generalInfo'], stationAPIData: StationAPIData) {
const stationName = stationAPIData.stationName.toLowerCase(); return {
hash: onlineStationData.stationHash,
stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0)); statusID,
statusTimestamp,
return this.trainList.reduce((acc: ScheduledTrain[], train) => { };
if (!train.timetableData) return acc; },
const timetable = train.timetableData; getScheduledTrains(stationGeneralInfo: Station['generalInfo'], stationAPIData: StationAPIData) {
if (!timetable.sceneries.includes(stationAPIData.stationHash)) return acc; const stationName = stationAPIData.stationName.toLowerCase();
const stopInfoIndex = timetable.followingStops.findIndex((stop) => { stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0));
const stopName = stop.stopNameRAW.toLowerCase();
return this.trainList.reduce((acc: ScheduledTrain[], train) => {
if (stationName === stopName) return true; if (!train.timetableData) return acc;
if (stopName.includes(stationName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.'))
return true; const timetable = train.timetableData;
if (!timetable.sceneries.includes(stationAPIData.stationHash)) return acc;
if (stationName.includes(stopName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.'))
return true; const stopInfoIndex = timetable.followingStops.findIndex((stop) => {
const stopName = stop.stopNameRAW.toLowerCase();
if (
stopName.includes('podg.') && if (stationName === stopName) return true;
stopName.split(', podg.')[0] && if (stopName.includes(stationName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.'))
stationName.includes(stopName.split(', podg.')[0]) return true;
)
return true; if (stationName.includes(stopName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.'))
return true;
if (
stationGeneralInfo && if (
stationGeneralInfo.checkpoints && stopName.includes('podg.') &&
stationGeneralInfo.checkpoints.length > 0 && stopName.split(', podg.')[0] &&
stationGeneralInfo.checkpoints.some((cp) => stationName.includes(stopName.split(', podg.')[0])
cp.checkpointName.toLowerCase().includes(stop.stopNameRAW.toLowerCase()) )
) return true;
)
return true; if (
stationGeneralInfo &&
return false; stationGeneralInfo.checkpoints &&
}); stationGeneralInfo.checkpoints.length > 0 &&
stationGeneralInfo.checkpoints.some((cp) =>
if (stopInfoIndex == -1) return acc; cp.checkpointName.toLowerCase().includes(stop.stopNameRAW.toLowerCase())
)
const scheduledStopTrain = getScheduledTrain(train, stopInfoIndex, stationAPIData.stationName); )
return true;
if (stationGeneralInfo?.checkpoints) {
for (const checkpoint of stationGeneralInfo.checkpoints) { return false;
const index = timetable.followingStops.findIndex( });
(stop) => stop.stopNameRAW.toLowerCase() == checkpoint.checkpointName.toLowerCase()
); if (stopInfoIndex == -1) return acc;
if (index == -1) continue; const scheduledStopTrain = getScheduledTrain(train, stopInfoIndex, stationAPIData.stationName);
const scheduledCheckpointTrain = getScheduledTrain(train, index, stationAPIData.stationName); if (stationGeneralInfo?.checkpoints) {
checkpoint.scheduledTrains.push(scheduledCheckpointTrain); for (const checkpoint of stationGeneralInfo.checkpoints) {
} const index = timetable.followingStops.findIndex(
} (stop) => stop.stopNameRAW.toLowerCase() == checkpoint.checkpointName.toLowerCase()
);
acc.push(scheduledStopTrain);
return acc; if (index == -1) continue;
}, []) as ScheduledTrain[];
}, const scheduledCheckpointTrain = getScheduledTrain(train, index, stationAPIData.stationName);
checkpoint.scheduledTrains.push(scheduledCheckpointTrain);
getStationTrains(stationAPIData: StationAPIData) { }
return this.trainList }
.filter(
(train) => acc.push(scheduledStopTrain);
train?.region === this.region.id && train.online && train.currentStationName === stationAPIData.stationName return acc;
) }, []) as ScheduledTrain[];
.map((train) => ({ driverName: train.driverName, driverId: train.driverId, trainNo: train.trainNo })); },
},
getStationTrains(stationAPIData: StationAPIData) {
setStationsOnlineInfo() { return this.trainList
const onlineStationNames: string[] = []; .filter(
const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = []; (train) =>
train?.region === this.region.id && train.online && train.currentStationName === stationAPIData.stationName
this.apiData.stations?.forEach((stationAPIData) => { )
if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return; .map((train) => ({
const station = this.stationList.find((s) => s.name === stationAPIData.stationName); driverName: train.driverName,
driverId: train.driverId,
onlineStationNames.push(stationAPIData.stationName); trainNo: train.trainNo,
trainId: train.trainId,
const dispatcherStatus = this.getDispatcherStatus(stationAPIData); }));
prevDispatcherStatuses.push(dispatcherStatus); },
const stationTrains = this.getStationTrains(stationAPIData); setStationsOnlineInfo() {
const scheduledTrains = this.getScheduledTrains(station?.generalInfo, stationAPIData); const onlineStationNames: string[] = [];
const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = [];
const onlineInfo = {
name: stationAPIData.stationName, this.apiData.stations?.forEach((stationAPIData) => {
hash: stationAPIData.stationHash, if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return;
region: stationAPIData.region, const station = this.stationList.find((s) => s.name === stationAPIData.stationName);
maxUsers: stationAPIData.maxUsers,
currentUsers: stationAPIData.currentUsers, onlineStationNames.push(stationAPIData.stationName);
spawns: parseSpawns(stationAPIData.spawnString),
dispatcherName: stationAPIData.dispatcherName, const dispatcherStatus = this.getDispatcherStatus(stationAPIData);
dispatcherRate: stationAPIData.dispatcherRate, prevDispatcherStatuses.push(dispatcherStatus);
dispatcherId: stationAPIData.dispatcherId,
dispatcherExp: stationAPIData.dispatcherExp, const stationTrains = this.getStationTrains(stationAPIData);
dispatcherIsSupporter: stationAPIData.dispatcherIsSupporter, const scheduledTrains = this.getScheduledTrains(station?.generalInfo, stationAPIData);
stationTrains,
statusTimestamp: dispatcherStatus.statusTimestamp, const onlineInfo = {
statusID: dispatcherStatus.statusID, name: stationAPIData.stationName,
scheduledTrains, hash: stationAPIData.stationHash,
}; region: stationAPIData.region,
maxUsers: stationAPIData.maxUsers,
if (!station) { currentUsers: stationAPIData.currentUsers,
this.stationList.push({ spawns: parseSpawns(stationAPIData.spawnString),
name: stationAPIData.stationName, dispatcherName: stationAPIData.dispatcherName,
onlineInfo, dispatcherRate: stationAPIData.dispatcherRate,
}); dispatcherId: stationAPIData.dispatcherId,
dispatcherExp: stationAPIData.dispatcherExp,
return; dispatcherIsSupporter: stationAPIData.dispatcherIsSupporter,
} stationTrains,
statusTimestamp: dispatcherStatus.statusTimestamp,
station.onlineInfo = { ...onlineInfo }; statusID: dispatcherStatus.statusID,
scheduledTrains,
this.stationList };
.filter((station) => !onlineStationNames.includes(station.name) && station.onlineInfo)
.forEach((offlineStation) => { if (!station) {
offlineStation.onlineInfo = undefined; this.stationList.push({
}); name: stationAPIData.stationName,
}); onlineInfo,
});
if (this.apiData.dispatchers != null) this.lastDispatcherStatuses = prevDispatcherStatuses;
}, return;
}
async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = await ( station.onlineInfo = { ...onlineInfo };
await axios.get(`${URLs.stacjownikAPI}/api/getSceneries?timestamp=${Math.floor(Date.now() / 1800000)}`)
).data; this.stationList
.filter((station) => !onlineStationNames.includes(station.name) && station.onlineInfo)
if (!sceneryData) { .forEach((offlineStation) => {
this.dataStatuses.sceneries = DataStatus.Error; offlineStation.onlineInfo = undefined;
return; });
} });
this.stationList = sceneryData.map((scenery) => ({ if (this.apiData.dispatchers != null) this.lastDispatcherStatuses = prevDispatcherStatuses;
name: scenery.name, },
generalInfo: { async fetchStationsGeneralInfo() {
...scenery, const sceneryData: StationJSONData[] = await (
authors: scenery.authors?.split(',').map((a) => a.trim()), await axios.get(`${URLs.stacjownikAPI}/api/getSceneries?timestamp=${Math.floor(Date.now() / 1800000)}`)
routes: ).data;
scenery.routes
?.split(';') if (!sceneryData) {
.filter((routeString) => routeString) this.dataStatuses.sceneries = DataStatus.Error;
.reduce( return;
(acc, routeString) => { }
const specs1 = routeString.split('_')[0];
const isInternal = specs1.startsWith('!'); this.stationList = sceneryData.map((scenery) => ({
const name = isInternal ? specs1.replace('!', '') : specs1; name: scenery.name,
const specs2 = routeString.split('_')[1].split(''); generalInfo: {
const twoWay = specs2[0] == '2'; ...scenery,
const catenary = specs2[1] == 'E'; authors: scenery.authors?.split(',').map((a) => a.trim()),
const SBL = specs2[2] == 'S'; routes:
const TWB = specs2[3] ? true : false; scenery.routes
?.split(';')
const propName = twoWay .filter((routeString) => routeString)
? catenary .reduce(
? 'twoWayCatenaryRouteNames' (acc, routeString) => {
: 'twoWayNoCatenaryRouteNames' const specs1 = routeString.split('_')[0];
: catenary const isInternal = specs1.startsWith('!');
? 'oneWayCatenaryRouteNames' const name = isInternal ? specs1.replace('!', '') : specs1;
: 'oneWayNoCatenaryRouteNames';
const specs2 = routeString.split('_')[1].split('');
acc[twoWay ? 'twoWay' : 'oneWay'].push({ const twoWay = specs2[0] == '2';
name, const catenary = specs2[1] == 'E';
SBL, const SBL = specs2[2] == 'S';
TWB, const TWB = specs2[3] ? true : false;
catenary,
isInternal, const propName = twoWay
tracks: twoWay ? 2 : 1, ? catenary
}); ? 'twoWayCatenaryRouteNames'
if (!isInternal) acc[propName].push(name); : 'twoWayNoCatenaryRouteNames'
: catenary
if (SBL) acc['sblRouteNames'].push(name); ? 'oneWayCatenaryRouteNames'
: 'oneWayNoCatenaryRouteNames';
return acc;
}, acc[twoWay ? 'twoWay' : 'oneWay'].push({
{ name,
oneWay: [], SBL,
twoWay: [], TWB,
sblRouteNames: [], catenary,
oneWayCatenaryRouteNames: [], isInternal,
oneWayNoCatenaryRouteNames: [], tracks: twoWay ? 2 : 1,
twoWayCatenaryRouteNames: [], });
twoWayNoCatenaryRouteNames: [], if (!isInternal) acc[propName].push(name);
} as StationRoutes
) || {}, if (SBL) acc['sblRouteNames'].push(name);
checkpoints: scenery.checkpoints
? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] })) return acc;
: [], },
}, {
})); oneWay: [],
}, twoWay: [],
sblRouteNames: [],
connectToWebsocket() { oneWayCatenaryRouteNames: [],
const socket = io(URLs.stacjownikAPI, { oneWayNoCatenaryRouteNames: [],
transports: ['websocket', 'polling'], twoWayCatenaryRouteNames: [],
rememberUpgrade: true, twoWayNoCatenaryRouteNames: [],
reconnection: true, } as StationRoutes
timeout: 10000 ) || {},
}); checkpoints: scenery.checkpoints
? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] }))
socket.on('connect_error', (err) => { : [],
this.dataStatuses.connection = DataStatus.Error; },
this.webSocket = undefined; }));
}); },
socket.on('UPDATE', (data: APIData) => { connectToWebsocket() {
this.apiData = data; const socket = io(URLs.stacjownikAPI, {
this.dataStatuses.connection = DataStatus.Loaded; transports: ['websocket', 'polling'],
this.setOnlineData(); rememberUpgrade: true,
}); reconnection: true,
timeout: 10000,
socket.emit('FETCH_DATA', {}, (data: APIData) => { });
this.apiData = data;
this.setOnlineData(); socket.on('connect_error', (err) => {
}); this.dataStatuses.connection = DataStatus.Error;
this.webSocket = undefined;
this.webSocket = socket; });
},
socket.on('UPDATE', (data: APIData) => {
async connectToAPI() { this.apiData = data;
await this.fetchStationsGeneralInfo(); this.dataStatuses.connection = DataStatus.Loaded;
this.setOnlineData();
this.connectToWebsocket(); });
},
socket.emit('FETCH_DATA', {}, (data: APIData) => {
async changeRegion(region: StoreState['region']) { this.apiData = data;
this.region = region; this.setOnlineData();
});
await this.setOnlineData();
}, this.webSocket = socket;
},
async setOnlineData() {
if (!this.apiData.stations) { async connectToAPI() {
this.dataStatuses.sceneries = DataStatus.Error; await this.fetchStationsGeneralInfo();
this.dataStatuses.trains = DataStatus.Error;
this.dataStatuses.dispatchers = DataStatus.Error; this.connectToWebsocket();
},
return;
} async changeRegion(region: StoreState['region']) {
this.region = region;
this.dataStatuses.sceneries = DataStatus.Loaded; await this.setOnlineData();
this.dataStatuses.trains = !this.apiData.trains ? DataStatus.Warning : DataStatus.Loaded; },
this.dataStatuses.dispatchers = !this.apiData.dispatchers ? DataStatus.Warning : DataStatus.Loaded;
async setOnlineData() {
this.setTrainsOnlineData(); if (!this.apiData.stations) {
this.setStationsOnlineInfo(); this.dataStatuses.sceneries = DataStatus.Error;
}, this.dataStatuses.trains = DataStatus.Error;
}, this.dataStatuses.dispatchers = DataStatus.Error;
});
return;
}
this.dataStatuses.sceneries = DataStatus.Loaded;
this.dataStatuses.trains = !this.apiData.trains ? DataStatus.Warning : DataStatus.Loaded;
this.dataStatuses.dispatchers = !this.apiData.dispatchers ? DataStatus.Warning : DataStatus.Loaded;
this.setTrainsOnlineData();
this.setStationsOnlineInfo();
},
},
});
+71 -68
View File
@@ -1,68 +1,71 @@
import { DataStatus } from '@/scripts/enums/DataStatus'; import { Socket } from 'socket.io-client';
import { DispatcherStatsAPIData } from '@/scripts/interfaces/api/DispatcherStatsAPIData'; import { DataStatus } from '../scripts/enums/DataStatus';
import { DriverStatsAPIData } from '@/scripts/interfaces/api/DriverStatsAPIData'; import StationAPIData from '../scripts/interfaces/api/StationAPIData';
import StationAPIData from '@/scripts/interfaces/api/StationAPIData'; import TrainAPIData from '../scripts/interfaces/api/TrainAPIData';
import TrainAPIData from '@/scripts/interfaces/api/TrainAPIData'; import Station from '../scripts/interfaces/Station';
import Station from '@/scripts/interfaces/Station'; import Train from '../scripts/interfaces/Train';
import Train from '@/scripts/interfaces/Train'; import { DispatcherStatsAPIData } from '../scripts/interfaces/api/DispatcherStatsAPIData';
import { Socket } from 'socket.io-client'; import { DriverStatsAPIData } from '../scripts/interfaces/api/DriverStatsAPIData';
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault'; export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
export interface StoreState { export interface StoreState {
stationList: Station[]; stationList: Station[];
trainList: Train[]; trainList: Train[];
apiData: APIData; apiData: APIData;
lastDispatcherStatuses: { hash: string; statusTimestamp: number; statusID: string }[]; lastDispatcherStatuses: { hash: string; statusTimestamp: number; statusID: string }[];
sceneryData: any[][]; sceneryData: any[][];
region: { id: string; value: string }; region: { id: string; value: string };
trainCount: number; trainCount: number;
stationCount: number; stationCount: number;
webSocket?: Socket; webSocket?: Socket;
dispatcherStatsName: string; dispatcherStatsName: string;
dispatcherStatsData?: DispatcherStatsAPIData; dispatcherStatsData?: DispatcherStatsAPIData;
driverStatsName: string; driverStatsName: string;
driverStatsData?: DriverStatsAPIData; driverStatsData?: DriverStatsAPIData;
dataStatuses: { chosenModalTrainId?: string;
connection: DataStatus;
sceneries: DataStatus; dataStatuses: {
timetables: DataStatus; connection: DataStatus;
dispatchers: DataStatus; sceneries: DataStatus;
trains: DataStatus; timetables: DataStatus;
}; dispatchers: DataStatus;
trains: DataStatus;
listenerLaunched: boolean; };
}
listenerLaunched: boolean;
export interface APIData { blockScroll: boolean;
stations?: StationAPIData[]; }
dispatchers?: string[][];
trains?: TrainAPIData[]; export interface APIData {
} stations?: StationAPIData[];
dispatchers?: string[][];
export interface StationJSONData { trains?: TrainAPIData[];
name: string; }
url: string;
lines: string; export interface StationJSONData {
project: string; name: string;
url: string;
reqLevel: number; lines: string;
project: string;
signalType: string;
controlType: string; reqLevel: number;
SUP: boolean; signalType: string;
controlType: string;
routes: string;
checkpoints: string | null; SUP: boolean;
authors?: string;
routes: string;
availability: Availability; checkpoints: string | null;
} authors?: string;
availability: Availability;
}
+85 -65
View File
@@ -1,65 +1,85 @@
@import 'responsive.scss'; @import 'responsive.scss';
// Animations // Animations
.warning { .warning {
&-enter-from, &-enter-from,
&-leave-to { &-leave-to {
opacity: 0; opacity: 0;
} }
&-enter-active { &-enter-active {
transition: all 150ms ease-out; transition: all 150ms 100ms ease-out;
} }
&-leave-active { &-leave-active {
transition: all 150ms ease-out; transition: all 150ms 100ms ease-out;
} }
} }
//Styles //Styles
.journal-wrapper { .list_wrapper {
width: 1350px; overflow-y: auto;
padding: 1em 0; height: 90vh;
} min-height: 550px;
.journal_warning { padding-right: 0.2em;
text-align: center; }
font-size: 1.3em;
.journal_wrapper {
&.error { max-width: 1350px;
background-color: var(--clr-error); width: 100%;
}
} padding: 1em 0;
}
.schedule-dates > * {
margin-right: 0.25em; .journal_warning {
} text-align: center;
font-size: 1.3em;
.journal_item,
.journal_warning { &.error {
background: #202020; background-color: var(--clr-error);
padding: 1em; }
margin: 1em 0; }
}
.schedule-dates > * {
.journal_top-bar { margin-right: 0.25em;
display: flex; }
justify-content: space-between;
align-items: center; .journal_item,
} .journal_warning {
background-color: #1a1a1a;
button.btn { padding: 1em;
padding: 0.5em 0.7em; margin-bottom: 1em;
} }
@include smallScreen() { .journal_top-bar {
.journal-wrapper { display: flex;
font-size: 1.25em; justify-content: space-between;
} align-items: center;
}
.journal_top-bar {
justify-content: center; .btn--load-data {
flex-wrap: wrap; padding: 0.5em 1em;
} display: flex;
} margin: 0 auto;
font-size: 1.2em;
}
@include smallScreen() {
.list_wrapper {
font-size: 1.1em;
}
.journal_top-bar {
justify-content: center;
flex-wrap: wrap;
}
}
@media (orientation: landscape) {
.list_wrapper {
font-size: 1em;
}
}
+42
View File
@@ -0,0 +1,42 @@
@import 'variables.scss';
@import 'responsive.scss';
.journal-stats {
background-color: #1a1a1a;
padding: 1em;
margin-bottom: 1em;
}
.info-stats {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
}
.stat-badge {
display: flex;
span {
background-color: $accentCol;
color: black;
font-weight: bold;
padding: 0.2em 0.5em;
}
span:first-child {
background-color: #333;
color: white;
}
}
@include smallScreen {
.journal-stats {
text-align: center;
}
.info-stats {
justify-content: center;
}
}
+28
View File
@@ -0,0 +1,28 @@
.badge {
font-weight: 600;
display: inline-block;
padding: 0;
background: #585858;
margin: 0.25em;
span {
display: inline-block;
padding: 0.2em 0.4em;
}
&-none {
font-weight: 600;
padding: 0.2em 0.4em;
background: firebrick;
text-align: center;
@include smallScreen() {
font-size: 1em;
}
}
}
-2
View File
@@ -28,8 +28,6 @@
width: 600px; width: 600px;
padding: 0.5em 1em;
@include smallScreen { @include smallScreen {
width: 100%; width: 100%;
height: 80vh; height: 80vh;

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