Compare commits

...

164 Commits

Author SHA1 Message Date
Spythere dd7d1b0bb0 Wersja 1.12.1
Wersja 1.12.1
2023-02-26 14:20:40 +01:00
Spythere 68934a89a4 bump: wersja 1.12.1 2023-02-26 14:16:13 +01:00
Spythere b88a240ec1 feature: like count historii dyżurnych 2023-02-26 14:14:32 +01:00
Spythere eaa34f3359 hotfix: dostępność grubości czcionki 2023-02-26 13:50:47 +01:00
Spythere febb22e1bc Wersja 1.12
Merge produkcyjny do wersji 1.12.0
2023-02-14 21:32:46 +01:00
Spythere 500f3c1223 dziennik RJ: wyświetlanie statów 2023-02-14 16:57:22 +01:00
Spythere 221e0c7e82 dzienniki: fix ładowania 2023-02-14 16:50:12 +01:00
Spythere ca19f7e397 hotfix: websocket 2023-02-14 16:40:15 +01:00
Spythere a71ccd3e1a bump: wersja 1.12 2023-02-14 13:52:20 +01:00
Spythere d496c70fa8 aktualizacja tłumaczenia 2023-02-14 13:51:52 +01:00
Spythere b9868ba52e dzienniki: stylistyka 2023-02-12 16:12:48 +01:00
Spythere 59bd3fa2ef design: badge poziomów 2023-02-12 12:58:23 +01:00
Spythere e14d328ed9 fix: wielkość scrollbaru 2023-02-12 00:48:18 +01:00
Spythere 36d71292bc feature: url projektów 2023-02-12 00:42:37 +01:00
Spythere 2f6e2e7402 fix: responsywność 2023-02-12 00:30:05 +01:00
Spythere e959eac6c5 hotfix 2023-02-11 03:14:43 +01:00
Spythere 8bedc4dfc6 feature: vmax szlaków 2023-02-11 03:08:24 +01:00
Spythere 73563d5db7 Wersja 1.11.2
Wersja 1.11.2
2023-01-05 22:05:34 +01:00
Spythere 3f818069cd hotfix: podświetlenie sponsorów w dzienniku RJ 2023-01-05 16:09:27 +01:00
Spythere cdf0b2a426 feature: nasłuchiwanie aktualizacji 2023-01-05 16:05:34 +01:00
Spythere c29ddeb78c fix: poziom 0 w dzienniku RJ 2023-01-05 15:59:14 +01:00
Spythere b81d98cab7 fix: filtrowanie pociągów offline 2023-01-05 15:58:17 +01:00
Spythere 0e45bca5da feature: przycisk odświeżania dzienników 2023-01-05 14:49:44 +01:00
Spythere 715e66879f feature: przejścia pomiędzy statusami ładowań 2023-01-04 14:01:25 +01:00
Spythere 1747e15dc8 bump: 1.11.2 2023-01-03 14:58:04 +01:00
Spythere 6a923a8e1d feature: sygnatura dev w stopce 2023-01-03 14:57:36 +01:00
Spythere 25a248e95e feature: animacje list 2023-01-03 14:51:19 +01:00
Spythere aa7a6b220e feature: lvl maszynisty przy dzienniku i pociągach 2023-01-02 18:30:09 +01:00
Spythere deb7b68985 Merge branch 'development' 2023-01-01 03:02:11 +01:00
Spythere 633f05f690 fix: wyświetlanie poprawnych id RJ 2023-01-01 02:57:11 +01:00
Spythere 73828867da Merge wersji dev do produkcji (1.11.1)
Wersja 1.11.1
2022-12-31 18:30:08 +01:00
Spythere 75685c1e0e bump: 1.11.1 2022-12-31 18:22:39 +01:00
Spythere 496ff95236 fix: sortowanie RJ wg id z API 2022-12-31 18:21:32 +01:00
Spythere 7e25327832 feature: lvl dyżurnego w dzienniku 2022-12-30 17:39:21 +01:00
Spythere 272c9f50f8 fix: SW cache 2022-12-30 15:45:17 +01:00
Spythere 255e07372e Merge wersji dev do produkcji (1.11)
Wersja produkcyjna 1.11.0
2022-12-26 22:58:17 +01:00
Spythere 279bbfa4db fix: responsywność 2022-12-26 20:01:10 +01:00
Spythere a5c829faf5 Fix: wskaźnik ładowania dzienników 2022-12-26 19:37:52 +01:00
Spythere 5fdfaeac5e hotfix 2022-12-26 18:52:31 +01:00
Spythere 9beb30e3d5 Tłumaczenie monitu 2022-12-26 18:51:50 +01:00
Spythere 48582e2eea lock files sync 2022-12-26 18:45:39 +01:00
Spythere 2e721fb8bf PWA: tryb offline 2022-12-26 18:43:15 +01:00
Spythere f93c1fbfec PWA: tryb offline 2022-12-26 18:43:14 +01:00
Spythere c06e7b6468 Poprawka wyświetlania sumy dystansu 2022-12-26 13:54:30 +01:00
Spythere 22a6d266cb Aktualizacja danych z API 2022-12-26 13:50:48 +01:00
Spythere 5f8a16401b Update API 2022-12-25 23:35:10 +01:00
Spythere c9be01aa29 lock files 2022-12-23 20:26:54 +01:00
Spythere 4ec058b33c Konfiguracja PWA 2022-12-23 20:25:02 +01:00
Spythere 27a5d2a406 fix: tłumaczenie komunikatu 2022-12-22 18:50:09 +01:00
Spythere 58169e26f6 Feedback i stylistyka statystyk RJ 2022-12-22 01:45:43 +01:00
Spythere fee1f4bbd5 Usprawienie podpowiedzi filtrów 2022-12-22 01:36:38 +01:00
Spythere 240817acc3 Przekierowanie do strony głównej 2022-12-21 20:32:41 +01:00
Spythere db3be87dd8 Przystosowanie pod update API 2022-12-21 20:24:48 +01:00
Spythere 1665134d6f Fix odznaczenia filtrów pociągów 2022-12-21 19:34:42 +01:00
Spythere df289ab734 Wskaźnik aktywnych filtrów pociągów online 2022-12-21 19:07:23 +01:00
Spythere f74440ba6f Pogrubienie linku dziennika w headerze 2022-12-21 18:39:40 +01:00
Spythere a25dbe9fd5 Usunięcie firebase config z html 2022-12-21 18:27:27 +01:00
Spythere 4fff136d6b Poprawki reaktywności 2022-12-21 18:24:04 +01:00
Spythere d06f2d5d2e Optymalizacja pobierania danych 2022-12-21 18:10:54 +01:00
Spythere 9f68d628d0 Wskaźnik aktywnych filtrów dziennika DR 2022-12-21 15:51:13 +01:00
Spythere d64b906dac Wskaźnik aktywnych filtrów dziennika RJ 2022-12-21 15:45:03 +01:00
Spythere f3e193e68a Cleanup 2022-12-21 15:02:41 +01:00
Spythere 5640ce9f2b Fix routingu w dzienniku RJ 2022-12-21 15:02:25 +01:00
Spythere 50100eb2f9 Nawigacja 2022-12-20 21:51:40 +01:00
Spythere e478c510b2 Fix działania reaktywności linków 2022-12-20 21:31:59 +01:00
Spythere 7ea558642f Stylistyka statystyk 2022-12-20 21:11:47 +01:00
Spythere 493145f7f2 Fix pola daty 2022-12-20 16:59:59 +01:00
Spythere 4f72535365 Setup GitHub Actions & npm 2022-12-20 16:56:12 +01:00
Spythere 8e3bf80715 Fix logiki przycisków 2022-12-20 16:44:15 +01:00
Spythere 6da586d08a Stylistyka komponentów statystyk 2022-12-20 16:41:42 +01:00
Spythere be53b9c7fb Notka o lokacji pociągu nie pojawia się przy jej braku 2022-12-20 01:41:13 +01:00
Spythere 94ed1160a1 Poprawki 2022-12-20 01:38:08 +01:00
Spythere 859d8d2631 Train modal fix 2022-12-20 00:53:03 +01:00
Spythere 5f3abd73c5 Informacja o statystykach 2022-12-19 00:44:46 +01:00
Spythere d71c8bb6f9 Bump wersji 2022-12-18 23:43:23 +01:00
Spythere a3db13d79c Github Actions 2022-12-18 20:01:15 +01:00
Spythere 8cb3da66f2 Statystyki maszynistów 2022-12-18 19:54:13 +01:00
Spythere 6e07897ac0 Fix: bug routingu dzienników 2022-12-18 03:01:13 +01:00
Spythere 726b859f5c Poprawki tabów statystyk 2022-12-18 01:28:11 +01:00
Spythere 651c60707a Rework statystyk RJ 2022-12-17 20:45:59 +01:00
Spythere d4fee84603 Rework statystyk RJ 2022-12-17 20:45:53 +01:00
Spythere 86539cdf23 1.10.10: status scenerii w dzienniku RJ 2022-12-03 09:41:46 +01:00
Spythere 69772460b8 Poprawka w działaniu sortowania wyszukiwarki scenerii 2022-11-01 18:27:27 +01:00
Spythere 6988a83355 Zmiana API 2022-10-30 23:03:47 +01:00
Spythere b6425564c8 Bump wersji 2022-10-28 13:15:28 +02:00
Spythere caf0a9b4c5 Dodano sugestie wyszukiwania istniejących użytkowników w dziennikach 2022-10-28 13:15:07 +02:00
Spythere bd5f433d6e Update paczek 2022-10-26 15:27:28 +02:00
Spythere 8d9cc721d6 Poprawki stylów 2022-10-16 23:09:46 +02:00
Spythere cceeffe49d Świecące nicki i poziomy sponsorów 2022-10-14 23:15:50 +02:00
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
99 changed files with 18214 additions and 7070 deletions
@@ -1,3 +1,6 @@
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on PR name: Deploy to Firebase Hosting on PR
'on': pull_request 'on': pull_request
jobs: jobs:
@@ -11,4 +14,4 @@ jobs:
with: with:
repoToken: '${{ secrets.GITHUB_TOKEN }}' repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}' firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
projectId: stacjownik-td2 projectId: stacjownik-td2
+1 -1
View File
@@ -1,7 +1,7 @@
.DS_Store .DS_Store
node_modules node_modules
/dist
/dev-dist /dev-dist
/dist
# local env files # local env files
.env.local .env.local
+5 -2
View File
@@ -1,7 +1,11 @@
{ {
"hosting": { "hosting": {
"public": "dist", "public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"], "ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [ "rewrites": [
{ {
"source": "**", "source": "**",
@@ -10,4 +14,3 @@
] ]
} }
} }
+4 -17
View File
@@ -5,8 +5,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, stacjownik, td2.info.pl" />
<meta name="description" content="Automatycznie odświeżana strona wyświetlająca stacje w Train Driver 2!" /> <meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
<title>Stacjownik</title> <title>Stacjownik</title>
@@ -24,21 +24,7 @@
<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@500;700&display=swap" rel="stylesheet" />
<script src="https://www.gstatic.com/firebasejs/8.1.1/firebase-app.js"></script>
<script>
const firebaseConfig = {
apiKey: 'AIzaSyBI36X2-p7vU1flxoJdCEc0noByyTe1mpw',
authDomain: 'stacjownik-td2.firebaseapp.com',
databaseURL: 'https://stacjownik-td2.firebaseio.com',
projectId: 'stacjownik-td2',
storageBucket: 'stacjownik-td2.appspot.com',
};
firebase.initializeApp(firebaseConfig);
</script>
</head> </head>
<body> <body>
@@ -46,3 +32,4 @@
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
+8609 -1019
View File
File diff suppressed because it is too large Load Diff
+37 -35
View File
@@ -1,35 +1,37 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.10.0", "version": "1.12.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"preview": "vite preview" "deploy": "yarn build && firebase deploy --only hosting",
}, "preview": "yarn build && vite preview"
"dependencies": { },
"core-js": "^3.12.1", "dependencies": {
"dotenv": "^8.6.0", "core-js": "^3.12.1",
"firebase": "^9.8.1", "dotenv": "^16.0.3",
"howler": "^2.2.1", "firebase": "^9.8.1",
"pinia": "^2.0.14", "howler": "^2.2.1",
"sass": "^1.53.0", "pinia": "^2.0.14",
"socket.io-client": "^4.4.1", "sass": "^1.53.0",
"vue": "^3.2.37", "socket.io-client": "^4.4.1",
"vue-i18n": "^9.1.6", "vue": "^3.2.37",
"vue-router": "^4.0.0-0" "vue-i18n": "^9.1.6",
}, "vue-router": "^4.0.0-0"
"devDependencies": { },
"@types/node": "^17.0.35", "devDependencies": {
"@vitejs/plugin-vue": "^3.0.0", "@types/node": "^18.11.17",
"axios": "^0.21.1", "@vitejs/plugin-vue": "^4.0.0",
"typescript": "^4.6.4", "axios": "^1.2.1",
"vite": "^3.0.0", "typescript": "^4.9.4",
"vue-tsc": "^0.38.4" "vite": "^4.0.3",
}, "vite-plugin-pwa": "^0.14.0",
"browserslist": [ "vue-tsc": "^1.0.18"
"> 1%", },
"last 2 versions", "browserslist": [
"not dead" "> 1%",
] "last 2 versions",
} "not dead"
]
}
+7 -158
View File
@@ -33,7 +33,8 @@
.route { .route {
margin: 0 0.2em; margin: 0 0.2em;
&-active { &-active,
&[data-active='true'] {
color: $accentCol; color: $accentCol;
font-weight: bold; font-weight: bold;
} }
@@ -45,7 +46,11 @@
font-size: 1rem; font-size: 1rem;
@include smallScreen() { @include smallScreen() {
font-size: calc(0.4rem + 1.4vw); font-size: calc(0.55rem + 1.1vw);
}
@include screenLandscape() {
font-size: calc(0.45rem + 0.8vw);
} }
} }
@@ -81,162 +86,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%;
+59 -99
View File
@@ -1,72 +1,19 @@
<template> <template>
<div class="app_container"> <div class="app_container">
<UpdateModal />
<transition name="modal-anim"> <transition name="modal-anim">
<keep-alive> <keep-alive>
<TrainModal v-if="store.chosenModalTrainId" /> <TrainModal v-if="store.chosenModalTrainId" />
</keep-alive> </keep-alive>
</transition> </transition>
<header class="app_header"> <UpdatePrompt />
<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"> <AppHeader :current-lang="currentLang" @change-lang="changeLang" />
<img :src="getIcon('discord', 'png')" alt="icon discord" />
</a>
</span>
</div>
<div class="header_body">
<status-indicator />
<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">{{ onlineDispatchers.length }}</span>
<span class="text--grayed"> / </span>
<span class="text--primary">{{ trainList.length }}</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>
<main class="app_main"> <main class="app_main">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive> <keep-alive exclude="JournalView">
<component :is="Component" :key="$route.path" /> <component :is="Component" :key="$route.name" />
</keep-alive> </keep-alive>
</router-view> </router-view>
</main> </main>
@@ -74,7 +21,8 @@
<footer class="app_footer"> <footer class="app_footer">
&copy; &copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a> <a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} | <a :href="releaseURL" target="_blank">v{{ VERSION }}</a> {{ new Date().getUTCFullYear() }} |
<a :href="releaseURL" target="_blank">v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}</a>
<div style="display: none">&int; ukryta taktyczna całka do programowania w HTMLu</div> <div style="display: none">&int; ukryta taktyczna całka do programowania w HTMLu</div>
</footer> </footer>
@@ -82,28 +30,33 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, provide, ref } from 'vue'; import { computed, defineComponent, KeepAlive, provide, ref, watch } from 'vue';
import Clock from './components/App/Clock.vue'; import Clock from './components/App/Clock.vue';
import packageInfo from '.././package.json'; import packageInfo from '.././package.json';
import 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 UpdateModal from './components/App/UpdateModal.vue';
import TrainModal from './components/Global/TrainModal.vue'; import TrainModal from './components/Global/TrainModal.vue';
import StorageManager from './scripts/managers/storageManager'; import StorageManager from './scripts/managers/storageManager';
import imageMixin from './mixins/imageMixin'; import imageMixin from './mixins/imageMixin';
import AppHeader from './components/App/AppHeader.vue';
import axios from 'axios';
import UpdatePrompt from './components/App/UpdatePrompt.vue';
import { VERSION } from 'vue-i18n';
import { RouterView } from 'vue-router';
import useCustomSW from './mixins/useCustomSW';
export default defineComponent({ export default defineComponent({
components: { components: {
Clock, Clock,
StatusIndicator, StatusIndicator,
SelectBox, SelectBox,
UpdateModal,
TrainModal, TrainModal,
AppHeader,
UpdatePrompt,
}, },
mixins: [imageMixin], mixins: [imageMixin],
@@ -112,6 +65,8 @@ export default defineComponent({
const store = useStore(); const store = useStore();
store.connectToAPI(); store.connectToAPI();
const { offlineReady } = useCustomSW();
const isFilterCardVisible = ref(false); const isFilterCardVisible = ref(false);
provide('isFilterCardVisible', isFilterCardVisible); provide('isFilterCardVisible', isFilterCardVisible);
@@ -127,49 +82,54 @@ 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,
options,
currentLang: 'pl', currentLang: 'pl',
releaseURL: '', releaseURL: '',
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
}), }),
created() { created() {
this.loadLang(); this.loadLang();
this.store.isOffline = !window.navigator.onLine;
window.addEventListener('offline', () => {
this.store.isOffline = true;
this.store.apiData = {
stations: [],
dispatchers: [],
trains: [],
connectedSocketCount: 0,
};
this.store.setOnlineData();
});
window.addEventListener('online', () => {
this.store.isOffline = false;
});
}, },
async mounted() { async mounted() {
this.updateStorage();
this.setReleaseURL(); this.setReleaseURL();
watch(
() => this.store.blockScroll,
(value) => {
if (value) {
document.body.classList.add('no-scroll');
return;
}
document.body.classList.remove('no-scroll');
}
);
}, },
methods: { methods: {
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;
@@ -177,18 +137,18 @@ export default defineComponent({
StorageManager.setStringValue('lang', lang); StorageManager.setStringValue('lang', lang);
}, },
setReleaseURL() { async setReleaseURL() {
const releaseURL = StorageManager.getStringValue('releaseURL'); try {
const releaseData = await (
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
).data;
this.releaseURL = releaseURL || ''; if (!releaseData) return;
},
updateStorage() { this.releaseURL = releaseData.html_url;
if (!StorageManager.isRegistered('unavailable-status')) { } catch (error) {
StorageManager.setBooleanValue('unavailable-status', true); console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
StorageManager.setBooleanValue('ending-status', true); return;
StorageManager.setBooleanValue('no-space-status', true);
StorageManager.setBooleanValue('afk-status', true);
} }
}, },
+18
View File
@@ -0,0 +1,18 @@
<svg width="144" height="147" viewBox="0 0 144 147" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_1343_19)">
<path d="M115.039 101.247C116.397 98.6665 115.405 95.4739 112.824 94.1167C110.243 92.7594 107.05 93.7514 105.693 96.3323L115.039 101.247ZM89.4447 44.0402L94.1179 46.4977L99.0329 37.1513L94.3597 34.6938L89.4447 44.0402ZM105.693 96.3323C95.7398 115.259 72.3278 122.534 53.4008 112.581L48.4858 121.927C72.5746 134.595 102.372 125.336 115.039 101.247L105.693 96.3323ZM53.4008 112.581C34.4739 102.627 27.1993 79.2155 37.1525 60.2885L27.8061 55.3735C15.1383 79.4623 24.397 109.259 48.4858 121.927L53.4008 112.581ZM37.1525 60.2885C47.1057 41.3616 70.5177 34.087 89.4447 44.0402L94.3597 34.6938C70.2709 22.026 40.4738 31.2846 27.8061 55.3735L37.1525 60.2885Z" fill="white"/>
<path d="M91.2258 38.7627L101.056 20.0698L116.15 51.8695L81.3956 57.4555L91.2258 38.7627Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_1343_19" x="18.1328" y="20.0698" width="102.017" height="115.531" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1343_19"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1343_19" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+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

+275
View File
@@ -0,0 +1,275 @@
<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">
<router-link to="/">
<img :src="getImage('stacjownik-header-logo.svg')" alt="Stacjownik" />
</router-link>
</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"
:data-active="$route.path.startsWith('/journal')"
to="/journal"
>
{{ $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;
position: relative;
@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>
-31
View File
@@ -1,31 +0,0 @@
<template>
<div class="loading">{{message}}</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
props: ["message"],
});
</script>
<style lang="scss" scoped>
.loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: calc(0.75rem + 1vw);
color: #fdc62f;
}
</style>
+14 -3
View File
@@ -161,7 +161,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
@@ -172,6 +171,7 @@ export default defineComponent({
return { return {
tooltipActive: false, tooltipActive: false,
indicator: { indicator: {
offline: false,
status: DataStatus.Loading, status: DataStatus.Loading,
message: 'data-status.S3', message: 'data-status.S3',
}, },
@@ -193,6 +193,7 @@ export default defineComponent({
return { return {
dataStatus: store.dataStatuses, dataStatus: store.dataStatuses,
store,
}; };
}, },
@@ -206,6 +207,13 @@ export default defineComponent({
const trainsDataStatus = statuses.trains; const trainsDataStatus = statuses.trains;
const dispatcherDataStatus = statuses.dispatchers; const dispatcherDataStatus = statuses.dispatchers;
if (this.store.isOffline) {
this.setSignalStatus(DataStatus.Initialized);
this.indicator.status = DataStatus.Initialized;
this.indicator.message = 'data-status.S1-offline';
return;
}
if (connectionStatus == DataStatus.Error) { if (connectionStatus == DataStatus.Error) {
this.setSignalStatus(connectionStatus); this.setSignalStatus(connectionStatus);
this.indicator.status = connectionStatus; this.indicator.status = connectionStatus;
@@ -252,6 +260,10 @@ export default defineComponent({
this.orangeLight = false; this.orangeLight = false;
this.redBottomLight = false; this.redBottomLight = false;
if (status == DataStatus.Initialized) {
this.redTopLight = true;
}
if (status == DataStatus.Loaded) { if (status == DataStatus.Loaded) {
this.greenLight = true; this.greenLight = true;
} }
@@ -291,9 +303,8 @@ export default defineComponent({
.status-indicator { .status-indicator {
position: absolute; position: absolute;
left: 50%; left: 110%;
bottom: 0; bottom: 0;
transform: translateX(12em);
z-index: 100; z-index: 100;
} }
+69
View File
@@ -0,0 +1,69 @@
<template>
<div class="update-prompt">
<transition name="prompt-anim">
<div class="prompt_content" v-if="!hidePrompt && needRefresh">
<div>{{ $t('update.title') }}</div>
<div class="prompt_actions">
<button class="btn btn--filled" @click="updateServiceWorker(true)">{{ $t('update.confirm-button') }}</button>
<button class="btn btn--filled" @click="hidePrompt = true">{{ $t('update.later-button') }}</button>
</div>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import useCustomSW from '../../mixins/useCustomSW';
const hidePrompt = ref(false);
const { needRefresh, updateServiceWorker } = useCustomSW();
</script>
<style lang="scss" scoped>
@import '../../styles/variables.scss';
.update-prompt {
position: fixed;
bottom: 0;
right: 0;
z-index: 200;
}
.prompt_content {
margin: 1em;
padding: 1em;
font-weight: bold;
background-color: black;
box-shadow: 0 0 10px 1px $accentCol;
border-radius: 1em;
}
.prompt_actions {
display: flex;
margin-top: 1em;
gap: 0.5em;
button {
width: 100%;
}
}
// Animation
.prompt-anim {
&-enter-active,
&-leave-active {
transition: all 120ms ease-in;
transform: translateY(0);
}
&-enter-from,
&-leave-to {
transform: translateY(100%);
}
}
</style>
+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;
+1 -1
View File
@@ -20,7 +20,7 @@ export default defineComponent({
.loading { .loading {
position: absolute; position: absolute;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translate(-50%, -50%);
display: flex; display: flex;
justify-content: center; justify-content: center;
+16 -10
View File
@@ -2,7 +2,6 @@
<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>
@@ -131,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;
@@ -150,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;
@@ -167,7 +171,7 @@ button.selected {
text-align: left; text-align: left;
&:focus { &:focus {
background: #555; background-color: #262626;
} }
} }
@@ -188,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 {
@@ -203,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;
@@ -218,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;
-3
View File
@@ -144,9 +144,6 @@ export default defineComponent({
} }
@include smallScreen { @include smallScreen {
.train-modal {
font-size: 1.05em;
}
.modal_content { .modal_content {
max-height: 85vh; max-height: 85vh;
+182
View File
@@ -0,0 +1,182 @@
<template>
<section class="daily-stats">
<span :data-active="data.statsStatus">
<b v-if="data.statsStatus == DataStatus.Loading">
{{ $t('app.loading') }}
</b>
<b v-else-if="data.stats.distanceSum == null">
{{ $t('journal.daily-stats-info') }}
</b>
<span>
<div v-if="data.stats.totalTimetables">
&bull;
<i18n-t keypath="journal.timetable-stats-total">
<template #count>
<b class="text--primary">
{{ data.stats.totalTimetables }}
{{ $t('journal.timetable-count', data.stats.totalTimetables) }}
</b>
</template>
<template #distance>
<b class="text--primary"> {{ data.stats.distanceSum?.toFixed(2) }} km </b>
</template>
</i18n-t>
</div>
<div v-if="data.stats.timetableId">
&bull;
<i18n-t keypath="journal.timetable-stats-longest">
<template #id>
<router-link :to="`/journal/timetables?timetableId=${data.stats.timetableId}`">
<b>{{ data.stats.timetableId }}</b>
</router-link>
</template>
<template #author>
<router-link :to="`/journal/dispatchers?dispatcherName=${data.stats.timetableAuthor}`">
<b>{{ data.stats.timetableAuthor }}</b>
</router-link>
</template>
<template #driver>
<b>{{ data.stats.timetableDriver }}</b>
</template>
<template #distance>
<b class="text--primary">{{ data.stats.timetableRouteDistance }} km</b>
</template>
</i18n-t>
</div>
<div v-if="firstPlaceDispatchers.length == 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active">
<template #dispatcher>
<router-link :to="`/journal/dispatchers?dispatcherName=${firstPlaceDispatchers[0].name}`">
<b>{{ firstPlaceDispatchers[0].name }}</b>
</router-link>
</template>
<template #count>
<b class="text--primary">
{{ firstPlaceDispatchers[0].count }}
{{ $t('journal.timetable-count', firstPlaceDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
<div v-if="firstPlaceDispatchers.length > 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-many">
<template #dispatchers>
<span v-for="(disp, i) in firstPlaceDispatchers">
<span v-if="i == firstPlaceDispatchers.length - 1"> {{ $t('general.and') }} </span>
<router-link :to="`/journal/dispatchers?dispatcherName=${disp.name}`">
<b>{{ disp.name }}</b>
</router-link>
<span v-if="i < firstPlaceDispatchers.length - 2">, </span>
</span>
</template>
<template #count>
<b class="text--primary">
{{ firstPlaceDispatchers[0].count }}
{{ $t('journal.timetable-count', firstPlaceDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
</span>
</span>
</section>
</template>
<script setup lang="ts">
import axios from 'axios';
import { computed, reactive, ref } from 'vue';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { ITimetablesDailyStats, ITimetablesDailyStatsResponse } from '../../scripts/interfaces/api/StatsAPIData';
import { URLs } from '../../scripts/utils/apiURLs';
const intervalId = ref(-1);
const data = reactive({
statsStatus: DataStatus.Loading,
stats: {
totalTimetables: 0,
distanceSum: 0,
distanceAvg: 0,
timetableAuthor: '',
timetableDriver: '',
timetableId: 0,
timetableRouteDistance: 0,
mostActiveDispatchers: [],
} as ITimetablesDailyStats,
});
const firstPlaceDispatchers = computed(() => {
if (data.stats.mostActiveDispatchers.length == 0) return [];
const maxCount = data.stats.mostActiveDispatchers[0].count;
return data.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
});
async function fetchDailyTimetableStats() {
try {
const {
distanceAvg,
distanceSum,
maxTimetable,
totalTimetables,
mostActiveDispatchers,
}: ITimetablesDailyStatsResponse = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDailyTimetableStats`)
).data;
data.stats = {
totalTimetables,
distanceSum,
distanceAvg,
timetableAuthor: maxTimetable?.authorName || '',
timetableDriver: maxTimetable?.driverName || '',
timetableId: maxTimetable?.id || 0,
timetableRouteDistance: maxTimetable?.routeDistance || 0,
mostActiveDispatchers,
};
data.statsStatus = DataStatus.Loaded;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
data.statsStatus = DataStatus.Error;
}
}
function startFetchingDailyStats() {
fetchDailyTimetableStats();
intervalId.value = setInterval(fetchDailyTimetableStats, 60000);
}
function stopFetchingDailyStats() {
clearInterval(intervalId.value);
}
defineExpose({
startFetchingDailyStats,
stopFetchingDailyStats,
});
</script>
<style lang="scss" scoped>
.daily-stats {
text-align: left;
}
.daily-stats > span[data-active='0'] {
opacity: 0.75;
}
</style>
+2 -32
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>
@@ -162,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>
-141
View File
@@ -1,141 +0,0 @@
<template>
<div class="card-dimmer" @click="closeCard"></div>
<div class="stats-card card">
<div>
<h2 class="card-title">
STATYSTYKI MASZYNISTY <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
</h2>
<div class="loading" v-if="!store.driverStatsData">Ładowanie...</div>
<div v-else>
<div class="info-stats" v-if="store.driverStatsData._sum.routeDistance != null">
<span class="stat-badge">
<span>PRZEBYTO</span>
<span>{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km</span>
</span>
<span class="stat-badge">
<span>PORZUCONO</span>
<span>
{{ (store.driverStatsData._sum.routeDistance - store.driverStatsData._sum.currentDistance).toFixed(2) }}km
</span>
</span>
<span class="stat-badge">
<span>WYPEŁNIONO</span>
<span>{{ store.driverStatsData._count.fulfilled }} RJ</span>
</span>
<span class="stat-badge">
<span>PORZUCONO</span>
<span>{{ store.driverStatsData._count._all - store.driverStatsData._count.fulfilled }} RJ</span>
</span>
<span class="stat-badge">
<span>ZATWIERDZONO</span>
<span>{{ store.driverStatsData._sum.confirmedStopsCount }} stacji</span>
</span>
<span class="stat-badge">
<span>PORZUCONO</span>
<span>
{{ store.driverStatsData._sum.allStopsCount - store.driverStatsData._sum.confirmedStopsCount }}
stacji
</span>
</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import axios from 'axios';
import { defineComponent } from 'vue';
import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store';
export default defineComponent({
emits: ['closeCard'],
setup() {
const store = useStore();
return {
store,
};
},
data() {
return {
test: Math.random(),
lastDispatcherName: '',
lastTimetables: [] as TimetableHistory[],
};
},
activated() {
this.fetchDispatcherStats();
},
methods: {
async fetchDispatcherStats() {
this.store.driverStatsData = undefined;
const statsData: DriverStatsAPIData = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`)
).data;
const recentTimetablesData: TimetableHistory[] = await (
await axios.get(`${URLs.stacjownikAPI}/api/getTimetables?driverName=${this.store.driverStatsName}`)
).data;
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>
@@ -1,425 +0,0 @@
<template>
<section class="journal-timetables">
<div class="journal-wrapper">
<div class="journal_top-bar">
<JournalOptions
@on-filter-change="search"
@on-input-change="search"
@on-sorter-change="search"
:sorter-option-ids="['timestampFrom', 'duration']"
/>
<!-- <DispatcherStats /> -->
</div>
<div class="journal-list">
<div class="list-wrapper" ref="scrollElement">
<transition name="warning" mode="out-in">
<div :key="historyDataStatus.status">
<Loading v-if="isDataLoading || isDataInit" />
<div v-else-if="isDataError" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div class="journal_warning" v-else-if="historyList.length == 0">
{{ $t('app.no-result') }}
</div>
<ul v-else>
<transition-group name="journal-list-anim">
<li v-for="(doc, i) in computedHistoryList" :key="doc.id">
<div class="journal_day" v-if="isAnotherDay(i - 1, i)">
<span>{{ new Date(doc.timestampFrom).toLocaleDateString('pl-PL') }}</span>
</div>
<div
class="journal_item"
:class="{ online: doc.isOnline }"
@click="navigateToScenery(doc.stationName, doc.isOnline)"
@keydown.enter="navigateToScenery(doc.stationName, doc.isOnline)"
tabindex="0"
>
<span>
<b class="text--primary">{{ doc.dispatcherName }}</b> &bull; <b>{{ doc.stationName }}</b>
<span class="text--grayed">&nbsp;#{{ doc.stationHash }}&nbsp;</span>
<span class="region-badge" :class="doc.region">PL1</span>
</span>
<span>
<span :data-status="doc.isOnline">
{{ doc.isOnline ? $t('journal.online-since') : 'OFFLINE' }}&nbsp;
</span>
<span>
{{ new Date(doc.timestampFrom).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
</span>
<span v-if="doc.currentDuration && doc.isOnline">
({{ calculateDuration(doc.currentDuration) }})
</span>
<span v-if="doc.timestampTo">
&gt;
{{ new Date(doc.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
({{ $t('journal.duty-lasted') }} {{ calculateDuration(doc.currentDuration!) }})
</span>
</span>
</div>
</li>
</transition-group>
</ul>
</div>
</transition>
</div>
</div>
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
</div>
</section>
</template>
<script lang="ts">
import { computed, defineComponent, JournalFilter, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios';
import ActionButton from '../../components/Global/ActionButton.vue';
import JournalOptions from '../../components/JournalView/JournalOptions.vue';
import DispatcherStats from '../../components/JournalView/DispatcherStats.vue';
import SearchBox from '../Global/SearchBox.vue';
import Loading from '../Global/Loading.vue';
import { URLs } from '../../scripts/utils/apiURLs';
import dateMixin from '../../mixins/dateMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store';
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
interface DispatcherHistoryItem {
id: string;
stationName: string;
stationHash: string;
region: string;
dispatcherName: string;
dispatcherId: number;
timestampFrom: number;
timestampTo?: number;
currentDuration?: number;
lastOnlineTimestamp: number;
isOnline: boolean;
}
type JournalDispatcherSearcher = {
[key in 'search-dispatcher' | 'search-station']: string;
};
export default defineComponent({
components: { SearchBox, ActionButton, JournalOptions, DispatcherStats, Loading },
mixins: [dateMixin],
name: 'JournalDispatchers',
props: {
sceneryName: {
type: String,
required: false,
},
dispatcherName: {
type: String,
required: false,
},
},
data: () => ({
currentQuery: '',
scrollDataLoaded: true,
scrollNoMoreData: false,
showReturnButton: false,
statsCardOpen: false,
}),
setup() {
const historyDataStatus: Ref<{ status: DataStatus; error: string | null }> = ref({
status: DataStatus.Loading,
error: null,
});
const sorterActive = ref({ id: 'timestampFrom', dir: -1 });
const journalFilterActive = ref({});
const searchersValues = reactive({
'search-dispatcher': '',
'search-station': '',
} as JournalDispatcherSearcher);
const countFromIndex = ref(0);
const countLimit = 15;
provide('sorterActive', sorterActive);
provide('journalFilterActive', journalFilterActive);
provide('searchersValues', searchersValues);
const scrollElement: Ref<HTMLElement | null> = ref(null);
return {
store: useStore(),
historyList: ref([]) as Ref<DispatcherHistoryItem[]>,
historyDataStatus,
isDataLoading: computed(() => historyDataStatus.value.status === DataStatus.Loading),
isDataError: computed(() => historyDataStatus.value.status === DataStatus.Error),
isDataInit: computed(() => historyDataStatus.value.status === DataStatus.Initialized),
sorterActive,
searchersValues,
countFromIndex,
countLimit,
scrollElement,
maxCount: ref(15),
};
},
computed: {
computedHistoryList() {
return this.historyList.filter(
(doc) => doc.isOnline || (doc.currentDuration && doc.currentDuration > 10 * 60000)
);
},
},
activated() {
if (this.sceneryName || this.dispatcherName) {
this.searchersValues['search-station'] = this.sceneryName?.toString() || '';
this.searchersValues['search-dispatcher'] = this.dispatcherName?.toString() || '';
this.search();
}
window.addEventListener('scroll', this.handleScroll);
},
mounted() {
if (!this.sceneryName && !this.dispatcherName) {
this.search();
}
},
deactivated() {
window.removeEventListener('scroll', this.handleScroll);
},
methods: {
closeDispatcherStatsCard() {
this.statsCardOpen = false;
},
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.computedHistoryList[prevIndex].timestampFrom).getDate() !=
new Date(this.computedHistoryList[currIndex].timestampFrom).getDate()
);
},
handleScroll() {
this.showReturnButton = window.scrollY > window.innerHeight;
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,
});
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?: JournalDispatcherSearcher;
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();
const dispatcher = props.searchers?.['search-dispatcher'].trim();
const station = props.searchers?.['search-station'].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['search-dispatcher'].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,183 @@
<template>
<transition-group class="journal-list" tag="ul" name="list-anim">
<li
v-for="item in computedDispatcherHistory"
:key="typeof item === 'string' ? item : item.timestampFrom + item.dispatcherId"
: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 class="item-general">
<b
v-if="item.dispatcherLevel !== null"
class="level-badge dispatcher"
:style="calculateExpStyle(item.dispatcherLevel, item.dispatcherIsSupporter)"
>
{{ item.dispatcherLevel >= 2 ? item.dispatcherLevel : 'L' }}
</b>
<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 class="like-count" v-if="item.dispatcherRate">
<img :src="getIcon('like')" alt="like icon" />
{{ item.dispatcherRate }}
</span>
</span>
<span class="item-time">
<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>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
import styleMixin from '../../mixins/styleMixin';
import imageMixin from '../../mixins/imageMixin';
export default defineComponent({
props: {
dispatcherHistory: {
type: Array as PropType<DispatcherHistory[]>,
required: true,
},
},
mixins: [dateMixin, styleMixin, imageMixin],
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/animations.scss';
@import '../../styles/responsive.scss';
@import '../../styles/badge.scss';
@import '../../styles/JournalSection.scss';
@import '../../styles/variables.scss';
li.sticky {
position: sticky;
top: 0;
}
.journal_item {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
text-align: left;
gap: 0.5em 1em;
line-height: 1.7em;
padding: 0.75em;
&.online {
cursor: pointer;
}
span[data-status='true'] {
color: springgreen;
}
span[data-status='false'] {
color: salmon;
}
}
.item-general {
display: flex;
justify-content: center;
align-items: center;
gap: 0.25em;
flex-wrap: wrap;
.level-badge {
margin-right: 0.25em;
}
}
.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;
}
}
.like-count {
display: flex;
align-items: center;
gap: 0.25em;
font-size: 1.2em;
color: $accentCol;
}
@include smallScreen {
.journal_item {
flex-direction: column;
}
}
</style>
@@ -0,0 +1,67 @@
<template>
<div class="journal-stats">
<span v-if="store.driverStatsData">
<h3>
{{ $t('journal.stats-title') }} <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
</h3>
<div class="info-stats">
<span class="stat-badge">
<span>{{ $t('journal.stats-timetables') }}</span>
<span>{{ store.driverStatsData._count.fulfilled }} / {{ store.driverStatsData._count._all }}</span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-longest-timetable') }}</span>
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-avg-timetable') }}</span>
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-distance') }}</span>
<span>
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
</span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-stations') }}</span>
<span>
{{ store.driverStatsData._sum.confirmedStopsCount }} /
{{ store.driverStatsData._sum.allStopsCount }}
</span>
</span>
</div>
</span>
<b v-else-if="store.driverStatsStatus == DataStatus.Loading">{{ $t('journal.stats-loading') }}</b>
<b v-else-if="store.driverStatsStatus == DataStatus.Error">
{{ $t('journal.stats-error ') }}
</b>
<b v-else>{{ $t('journal.driver-stats-info') }}</b>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store';
export default defineComponent({
data() {
return {
store: useStore(),
DataStatus,
};
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/JournalStats.scss';
</style>
@@ -0,0 +1,46 @@
<template>
<section class="journal-header">
<div class="journal-type-options">
<router-link class="router-link" active-class="route-active" to="/journal/timetables" exact>
{{ $t('journal.section-timetables') }}
</router-link>
&nbsp;&bull;&nbsp;
<router-link class="router-link" active-class="route-active" to="/journal/dispatchers">
{{ $t('journal.section-dispatchers') }}
</router-link>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({});
</script>
<style lang="scss" scoped>
.journal-type-options {
display: flex;
justify-content: center;
background-color: #2c2c2c;
max-width: 18em;
font-size: 1.2em;
margin: 0 auto;
border-radius: 0 0 0.5em 0.5em;
padding: 0.1em 0;
}
.journal-section > section {
height: 100%;
display: flex;
justify-content: center;
}
.router-link.active {
color: gold;
}
</style>
+285 -260
View File
@@ -1,260 +1,285 @@
<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"> <div class="actions-bar">
<select-box <button class="filter-button btn--filled btn--image" @click="showOptions = !showOptions" ref="button">
:itemList="translatedSorterOptions" <img :src="getIcon('filter2')" alt="Open filters" />
:defaultItemIndex="0" {{ $t('options.filters') }} [F]
@selected="onSorterChange" <span class="active-indicator" v-if="currentOptionsActive"></span>
:prefix="$t('journal.sort-prefix')" </button>
/>
</div> <button class="filter-button btn--filled btn--image" @click="refreshData">
<img :src="getIcon('refresh')" alt="Refresh data" />
<div class="content_search"> {{ $t('general.refresh') }}
<div class="search-box" v-for="(value, propName) in searchersValues" :key="propName"> </button>
<input </div>
class="search-input"
:placeholder="$t(`journal.${propName}`)" <datalist id="search-driver">
v-model="searchersValues[propName]" <option v-for="sugg in driverSuggestions" :value="sugg"></option>
@keydown.enter="onInputSearch" </datalist>
/>
<datalist id="search-dispatcher">
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" /> <option v-for="sugg in dispatcherSuggestions" :value="sugg"></option>
</div> </datalist>
<!-- <div class="search-box">
<input <transition name="options-anim">
class="search-input" <div class="options_wrapper" v-if="showOptions">
v-model="searchedTrain" <div class="options_content">
:placeholder="$t('journal.search-train')" <h1 class="option-title">{{ $t('options.search-title') }}</h1>
@keydown.enter="search" <div class="search_content">
/> <div class="search" v-for="(_, propName) in searchersValues" :key="propName">
<label v-if="propName == 'search-date'" for="date">{{ $t('options.search-date') }}</label>
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearTrain" />
</div> <div class="search-box">
<input
<div class="search-box"> class="search-input"
<input v-model="searchersValues[propName]"
class="search-input" @keydown.enter="onSearchConfirm"
v-model="searchedDriver" @focus="preventKeyDown = true"
:placeholder="$t('journal.search-driver')" @blur="preventKeyDown = false"
@keydown.enter="search" :placeholder="$t(`options.${propName}`)"
/> :type="propName == 'search-date' ? 'date' : 'text'"
:min="propName == 'search-date' ? '2022-02-01' : undefined"
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearDriver" /> :list="propName.toString()"
</div> --> />
<action-button class="search-button" @click="onInputSearch"> <button class="search-exit" v-if="propName != 'search-date'">
{{ $t('journal.search') }} <img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" />
</action-button> </button>
</div> </div>
</div> </div>
<div class="options_filters"> <div class="search_actions">
<button <button class="btn--action" @click="onResetButtonClick">
v-for="filter in filters" {{ $t('options.reset-button') }}
class="journal-filter-option btn--option" </button>
:class="{ checked: journalFilterActive.id === filter.id }" <button class="btn--action" @click="onSearchButtonConfirm">
:id="filter.id" {{ $t('options.search-button') }}
@click="onFilterChange(filter)" </button>
> </div>
{{ $t(`journal.filter-${filter.id}`) }} </div>
</button>
</div> <h1 class="option-title">{{ $t('options.sort-title') }}</h1>
</div> <div class="options_sorters">
</div> <div v-for="opt in translatedSorterOptions">
</template> <button
class="sort-option btn--option"
<script lang="ts"> :data-selected="opt.id == sorterActive.id"
import { defineComponent, inject, JournalFilter, PropType } from 'vue'; @click="onSorterChange(opt)"
import imageMixin from '../../mixins/imageMixin'; >
import ActionButton from '../Global/ActionButton.vue'; {{ opt.value.toUpperCase() }}
import SelectBox from '../Global/SelectBox.vue'; </button>
</div>
export default defineComponent({ </div>
components: { SelectBox, ActionButton },
emits: ['onSorterChange', 'onInputChange', 'onFilterChange'], <h1 class="option-title" v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1>
mixins: [imageMixin], <div class="options_filters">
<button
props: { v-for="filter in filters"
sorterOptionIds: { class="filter-option btn--option"
type: Array as PropType<Array<string>>, :class="{ checked: journalFilterActive.id === filter.id }"
required: true, :id="filter.id"
}, @click="onFilterChange(filter)"
>
filters: { {{ $t(`options.filter-${filter.id}`) }}
type: Array as PropType<JournalFilter[]>, </button>
default: [], </div>
}, </div>
}, </div>
</transition>
</div>
setup() { </template>
return {
searchersValues: inject('searchersValues') as {[key: string]: string}, <script lang="ts">
sorterActive: inject('sorterActive') as { id: string | number; dir: number }, import axios from 'axios';
journalFilterActive: inject('journalFilterActive') as JournalFilter, import { defineComponent, inject, PropType } from 'vue';
}; import imageMixin from '../../mixins/imageMixin';
}, import keyMixin from '../../mixins/keyMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
computed: { import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
translatedSorterOptions() { import { URLs } from '../../scripts/utils/apiURLs';
return this.$props.sorterOptionIds.map((id) => ({ import { useStore } from '../../store/store';
id, import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
value: this.$t(`journal.option-${id}`), import ActionButton from '../Global/ActionButton.vue';
})); import SelectBox from '../Global/SelectBox.vue';
},
}, export default defineComponent({
components: { SelectBox, ActionButton },
methods: { emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'],
onSorterChange(item: { id: string | number; value: string }) { mixins: [imageMixin, keyMixin],
this.sorterActive.id = item.id;
this.sorterActive.dir = -1; props: {
sorterOptionIds: {
this.$emit('onSorterChange'); type: Array as PropType<Array<string>>,
}, required: true,
},
onFilterChange(filter: JournalFilter) {
this.journalFilterActive = filter; filters: {
this.$emit('onFilterChange'); type: Array as PropType<JournalTimetableFilter[]>,
}, default: [],
},
onInputSearch() {
this.$emit('onInputChange'); dataStatus: {
}, type: Number as PropType<DataStatus>,
default: DataStatus.Initialized,
onInputClear(id: any) { },
this.searchersValues[id] = '';
this.onInputSearch(); currentOptionsActive: {
}, type: Boolean,
}, default: false,
}); },
</script> },
<style lang="scss" scoped> data() {
@import '../../styles/responsive'; return {
@import '../../styles/option.scss'; showOptions: false,
.options { driverSuggestions: [] as string[],
&_wrapper { dispatcherSuggestions: [] as string[],
display: flex;
flex-direction: column; searchTimeout: 0,
} store: useStore(),
&_content { DataStatus,
display: flex; };
flex-wrap: wrap; },
.content_search, setup() {
.content_select { return {
display: flex; searchersValues: inject('searchersValues') as { [key: string]: string },
align-items: center; sorterActive: inject('sorterActive') as { id: string | number; dir: number },
flex-wrap: wrap; journalFilterActive: inject('journalFilterActive') as JournalTimetableFilter,
};
padding: 0.25em 0.25em 0 0; },
}
} computed: {
driverStatsName() {
&_filters { return this.store.driverStatsName;
display: flex; },
flex-wrap: wrap;
margin: 0.5em 0 0 0; translatedSorterOptions() {
return this.$props.sorterOptionIds.map((id) => ({
.journal-filter-option { id,
margin: 0 0.25em 0 0; value: this.$t(`options.sort-${id}`),
}));
&#abandoned { },
color: salmon; },
}
watch: {
&#fulfilled { async driverStatsName(value: string) {
color: lightgreen; await this.fetchDriverStats();
} this.store.currentStatsTab = value ? 'driver' : 'daily';
},
&#active {
color: lightblue; async 'searchersValues.search-driver'(value: string | undefined) {
} clearTimeout(this.searchTimeout);
}
} if (!value || value == '') return;
} if (value.length < 3) return;
.search { this.startSearchTimeout('driver', value);
&-box { },
position: relative;
async 'searchersValues.search-dispatcher'(value: string | undefined) {
background: #333; if (!value || value == '') return;
border-radius: 0.5em; if (value.length < 3) return;
min-width: 200px;
margin-right: 0.25em; this.startSearchTimeout('dispatcher', value);
} },
},
&-input {
border: none; methods: {
async fetchDriverStats() {
min-width: 100%; this.store.driverStatsData = undefined;
padding: 0.35em 0.5em;
} if (!this.store.driverStatsName) {
this.store.driverStatsStatus = DataStatus.Initialized;
&-exit { return;
position: absolute; }
cursor: pointer;
try {
top: 50%; this.store.driverStatsStatus = DataStatus.Loading;
right: 10px;
transform: translateY(-50%); const statsData: DriverStatsAPIData = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`)
width: 1em; ).data;
}
} this.store.driverStatsData = statsData;
this.store.driverStatsStatus = DataStatus.Loaded;
@include smallScreen() { } catch (error) {
.journal-options { this.store.driverStatsStatus = DataStatus.Error;
width: 100%; console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/');
} }
.options { },
&_wrapper {
justify-content: center; refreshData() {
align-items: center; this.$emit('onRefreshData');
} },
&_content { startSearchTimeout(type: 'driver' | 'dispatcher', value: string) {
padding: 0 1em; if (this[`${type}Suggestions`].includes(value)) return;
flex-direction: column; window.clearTimeout(this.searchTimeout);
.content_select { this.searchTimeout = setTimeout(async () => {
margin: 0 auto; try {
padding: 0; const suggestions: string[] = await (
} await axios.get(`${URLs.stacjownikAPI}/api/get${type}Suggestions?name=${value}`)
).data;
.content_search {
justify-content: center; this[`${type}Suggestions`] = suggestions;
} } catch (error) {
} this[`${type}Suggestions`] = [];
}
&_filters { }, 450);
justify-content: center; },
.journal-filter-option { // Override keyMixin function
margin: 0.25em 0.25em; onKeyDownFunction() {
} this.showOptions = !this.showOptions;
}
} this.$nextTick(() => {
if (this.showOptions) (this.$refs['button'] as HTMLButtonElement)?.focus();
.search { });
&-box, },
&-button {
margin: 0.5em 0 0 0; focusEnd() {
} console.log('focus end');
},
&-box {
width: 100%; onSorterChange(item: { id: string | number; value: string }) {
} this.sorterActive.id = item.id;
this.sorterActive.dir = -1;
&-button { this.$emit('onSearchConfirm');
width: 80%; },
max-width: 300px;
} onFilterChange(filter: JournalTimetableFilter) {
} this.journalFilterActive = filter;
} this.$emit('onSearchConfirm');
</style> },
onInputClear(id: any) {
this.searchersValues[id] = '';
this.$emit('onSearchConfirm');
},
onSearchConfirm() {
this.$emit('onSearchConfirm');
},
onSearchButtonConfirm() {
this.showOptions = false;
this.$emit('onSearchConfirm');
},
onResetButtonClick() {
this.$emit('onOptionsReset');
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/filters_options.scss';
</style>
+112
View File
@@ -0,0 +1,112 @@
<template>
<div class="journal-stats" v-show="!store.isOffline">
<div class="tabs">
<button
v-for="tab in data.tabs"
class="btn--filled"
:data-selected="tab.name == store.currentStatsTab && areStatsOpen"
:data-inactive="tab.inactive"
@click="onTabButtonClick(tab.name)"
>
{{ $t(tab.titlePath) }}
</button>
</div>
<div class="stats-tab" v-show="areStatsOpen">
<keep-alive>
<JournalDailyStats v-if="store.currentStatsTab == 'daily'" ref="dailyStatsComp" />
<JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" />
</keep-alive>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, KeepAlive, onActivated, onDeactivated, reactive, Ref, ref, watch } from 'vue';
import { useStore } from '../../store/store';
import JournalDailyStats from './DailyStats.vue';
import JournalDriverStats from './JournalDriverStats.vue';
// Types
type TStatTab = 'daily' | 'driver';
// Variables
const store = useStore();
const dailyStatsComp: Ref<InstanceType<typeof JournalDailyStats> | null> = ref(null);
const lastDailyStatsOpen = ref(false);
const areStatsOpen = ref(false);
const lastClickedTab = ref('daily');
let data = reactive({
tabs: [
{
name: 'daily',
titlePath: 'journal.daily-stats-title',
},
{
name: 'driver',
titlePath: 'journal.driver-stats-title',
inactive: true,
},
] as { name: TStatTab; titlePath: string; inactive?: boolean }[],
});
// Methods
function onTabButtonClick(tab: TStatTab) {
if (lastClickedTab.value == tab || !areStatsOpen.value) areStatsOpen.value = !areStatsOpen.value;
if (tab == 'daily') lastDailyStatsOpen.value = areStatsOpen.value;
store.currentStatsTab = tab;
lastClickedTab.value = tab;
}
onActivated(() => {
dailyStatsComp.value?.startFetchingDailyStats();
});
onDeactivated(() => {
dailyStatsComp.value?.stopFetchingDailyStats();
});
watch(
computed(() => store.driverStatsData),
(statsData) => {
data.tabs[1].inactive = statsData ? false : true;
lastClickedTab.value = statsData ? 'driver' : 'daily';
if (statsData) areStatsOpen.value = true;
if (!statsData) areStatsOpen.value = lastDailyStatsOpen.value;
}
);
</script>
<style lang="scss" scoped>
@import '../../styles/JournalStats.scss';
@import '../../styles/variables.scss';
.tabs {
position: relative;
display: flex;
gap: 0.5em;
margin-bottom: 0.5em;
button {
font-weight: bold;
padding: 0.5em 0.75em;
&[data-inactive='true'] {
color: gray;
}
&[data-selected='true'] {
color: $accentCol;
}
}
}
</style>
@@ -1,439 +0,0 @@
<template>
<section class="journal-timetables">
<keep-alive>
<DriverStats v-if="statsCardOpen" @close-card="closeCard" />
</keep-alive>
<div class="journal-wrapper">
<div class="journal_top-bar">
<JournalOptions
@on-input-change="search"
@on-filter-change="search"
@on-sorter-change="search"
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']"
:filters="journalTimetableFilters"
/>
</div>
<div class="journal-list">
<div class="list-wrapper" ref="scrollElement">
<transition name="warning" mode="out-in">
<div :key="historyDataStatus.status">
<Loading v-if="isDataLoading || isDataInit" />
<div v-else-if="isDataError" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div class="journal_warning" v-else-if="historyList.length == 0">
{{ $t('app.no-result') }}
</div>
<ul v-else>
<transition-group name="journal-list-anim">
<li v-for="(item, i) in historyList" class="journal_item" :key="item.timetableId">
<div class="journal_item-top">
<span>
<span
tabindex="0"
@click="navigateToTimetable(item)"
@keydown.enter="navigateToTimetable(item)"
style="cursor: pointer"
>
<b class="text--primary">{{ item.trainCategoryCode }}&nbsp;</b>
<b>{{ item.trainNo }}</b>
| <span>{{ item.driverName }}</span> |
<span class="text--grayed">#{{ item.timetableId }}</span>
</span>
<div>
<b>{{ item.route.replace('|', ' - ') }}</b>
</div>
<hr style="margin: 0.25em 0" />
<div class="scenery-list">
<span
v-for="(scenery, i) in getSceneryList(item)"
:key="scenery.name"
:class="{ confirmed: scenery.confirmed }"
>
{{ i > 0 ? ' > ' : '' }} {{ scenery.name }}
</span>
</div>
<div class="schedule-dates">
<!-- Data odjazdu ze stacji początkowej -->
<b>{{ item.route.split('|')[0] }}:</b>
<s v-if="item.beginDate != item.scheduledBeginDate" class="text--grayed">
{{ localeTime(item.beginDate, $i18n.locale) }}
</s>
<span>{{ localeTime(item.scheduledBeginDate, $i18n.locale) }} </span>&bull;
<!-- Data przyjazdu na stację końcową / porzucenia -->
<b v-if="(item.fulfilled && item.terminated) || !item.terminated">
{{ item.route.split('|').slice(-1)[0] }}:
</b>
<i v-else>{{ $t('journal.timetable-abandoned') }} </i>
<s v-if="item.endDate != item.scheduledEndDate && item.terminated" class="text--grayed">
{{ localeTime(item.fulfilled ? item.endDate : item.scheduledEndDate, $i18n.locale) }}
</s>
<span
>{{ localeTime(item.fulfilled ? item.scheduledEndDate : item.endDate, $i18n.locale) }}
</span>
</div>
</span>
<b
class="journal_item-status"
:class="{
fulfilled: item.fulfilled || item.currentDistance >= item.routeDistance * 0.9,
terminated: item.terminated && !item.fulfilled,
active: !item.terminated,
}"
>
{{
!item.terminated
? $t('journal.timetable-active')
: item.fulfilled || item.currentDistance >= item.routeDistance * 0.9
? $t('journal.timetable-fulfilled')
: $t('journal.timetable-abandoned')
}}
</b>
</div>
<div style="margin-top: 1em">
<div>
{{ $t('journal.timetable-day') }} <b>{{ localeDay(item.beginDate, $i18n.locale) }}</b>
</div>
<!-- Nick dyżurnego -->
<div v-if="item.authorName">
<b class="text--grayed">{{ $t('journal.dispatcher-name') }}&nbsp;</b>
<router-link
class="dispatcher-link"
:to="`/journal/dispatchers?dispatcherName=${item.authorName}`"
>{{ item.authorName }}</router-link
>
</div>
</div>
<div style="margin-top: 1em">
<div>
<b>{{ $t('journal.route-length') }}</b>
{{ !item.fulfilled ? item.currentDistance + ' /' : '' }}
{{ item.routeDistance }} km
</div>
<div>
<b>{{ $t('journal.station-count') }}</b>
{{ item.confirmedStopsCount }} /
{{ item.allStopsCount }}
</div>
</div>
</li>
</transition-group>
</ul>
</div>
</transition>
</div>
</div>
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
</div>
</section>
</template>
<script lang="ts">
import { computed, defineComponent, JournalFilter, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios';
import DriverStats from './DriverStats.vue';
import Loading from '../Global/Loading.vue';
import { journalTimetableFilters } from '../../data/journalFilters';
import dateMixin from '../../mixins/dateMixin';
import routerMixin from '../../mixins/routerMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store';
import JournalOptions from './JournalOptions.vue';
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
type JournalTimetableSearcher = {
[key in 'search-driver' | 'search-train']: string;
};
export default defineComponent({
components: { DriverStats, Loading, JournalOptions },
mixins: [dateMixin, routerMixin],
name: 'JournalTimetables',
props: {
timetableId: {
type: String,
},
},
data: () => ({
currentQuery: '',
scrollDataLoaded: true,
scrollNoMoreData: false,
showReturnButton: false,
statsCardOpen: false,
journalTimetableFilters,
}),
setup() {
const historyDataStatus: Ref<{ status: DataStatus; error: string | null }> = ref({
status: DataStatus.Loading,
error: null,
});
const sorterActive = ref({ id: 'timetableId', dir: -1 });
const journalFilterActive = ref(journalTimetableFilters[0]);
const searchersValues = reactive({
'search-train': '',
'search-driver': '',
} as JournalTimetableSearcher);
const countFromIndex = ref(0);
const countLimit = 15;
provide('searchersValues', searchersValues);
provide('sorterActive', sorterActive);
provide('journalFilterActive', journalFilterActive);
const scrollElement: Ref<HTMLElement | null> = ref(null);
return {
historyList: ref([]) as Ref<TimetableHistory[]>,
historyDataStatus,
isDataLoading: computed(() => historyDataStatus.value.status === DataStatus.Loading),
isDataError: computed(() => historyDataStatus.value.status === DataStatus.Error),
isDataInit: computed(() => historyDataStatus.value.status === DataStatus.Initialized),
sorterActive,
journalFilterActive,
searchersValues,
countFromIndex,
countLimit,
scrollElement,
maxCount: ref(15),
store: useStore(),
};
},
activated() {
window.addEventListener('scroll', this.handleScroll);
if (this.timetableId) {
this.searchersValues['search-train'] = `#${this.timetableId}`;
this.search();
}
},
mounted() {
if (!this.timetableId) this.search();
},
deactivated() {
window.removeEventListener('scroll', this.handleScroll);
},
methods: {
navigateToTimetable(historyItem: TimetableHistory) {
if (historyItem.terminated) return;
this.navigateTo('/trains', {
trainNo: historyItem.trainNo,
driverName: historyItem.driverName,
});
},
closeCard() {
this.statsCardOpen = false;
},
getSceneryList(historyItem: TimetableHistory) {
return historyItem.sceneriesString
.split('%')
.map((name, i) => ({ name, confirmed: i < historyItem.confirmedStopsCount }));
},
handleScroll() {
this.showReturnButton = window.scrollY > window.innerHeight;
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?: JournalTimetableSearcher;
filter?: JournalFilter;
} = {}
) {
this.historyDataStatus.status = DataStatus.Loading;
const queries: string[] = [];
const driver = props.searchers?.['search-driver'].trim();
const train = props.searchers?.['search-train'].trim();
if (driver) queries.push(`driverName=${driver}`);
if (train) queries.push(train.startsWith('#') ? `timetableId=${train.replace('#', '')}` : `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) return;
// Response data exists
this.historyList = responseData;
// Stats display
this.store.driverStatsName =
this.historyList.length > 0 && this.searchersValues['search-driver'].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,331 @@
<template>
<transition-group class="journal-list" tag="ul" name="list-anim">
<li
v-for="{ timetable, sceneryList, ...item } in computedTimetableHistory"
class="journal_item"
:key="timetable.id"
>
<div class="journal_item-info">
<div class="info-general">
<span
class="general-train"
tabindex="0"
@click="showTimetable(timetable)"
@keydown.enter="showTimetable(timetable)"
style="cursor: pointer"
>
<span class="text--grayed">#{{ timetable.id }}</span>
<span>
<strong class="text--primary">
{{ timetable.trainCategoryCode }}
</strong>
<strong>&nbsp;{{ timetable.trainNo }}</strong>
</span>
&bull;
<strong
v-if="timetable.driverLevel !== null"
class="level-badge driver"
:style="calculateExpStyle(timetable.driverLevel, timetable.driverIsSupporter)"
>
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
</strong>
<strong>{{ timetable.driverName }}</strong>
</span>
<span class="general-time">
<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>
<span class="text--grayed" v-if="!timetable.fulfilled && timetable.currentSceneryName">
&bull;
<b>
{{ $t(`journal.${timetable.terminated ? 'last-seen-at' : 'currently-at'}`) }}
{{ timetable.currentSceneryName.replace(/.[a-zA-Z0-9]+.sc/, '') }}
</b>
</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>
</transition-group>
</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 styleMixin from '../../mixins/styleMixin';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
export default defineComponent({
props: {
timetableHistory: {
type: Array as PropType<TimetableHistory[]>,
required: true,
},
},
mixins: [dateMixin, imageMixin, modalTrainMixin, styleMixin],
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) return;
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/animations.scss';
@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;
}
}
&-general {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
margin-bottom: 0.5em;
}
&-route {
margin: 0.25em 0;
}
&-extended {
margin-top: 0.5em;
}
}
.general-train {
display: flex;
align-items: center;
gap: 0.25em;
}
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-general {
flex-direction: column;
}
.info-extended {
text-align: center;
}
.info-route {
display: flex;
justify-content: center;
}
.btn--show {
margin: 1em auto 0 auto;
}
}
</style>
@@ -1,116 +1,126 @@
<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="item in dispatcherHistoryList">
<div> <router-link class="item-general" :to="`/journal/dispatchers?dispatcherName=${item.dispatcherName}`">
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`"> <span class="text--grayed">#{{ item.stationHash }}&nbsp;</span>
<span class="text--grayed">#{{ historyItem.stationHash }}&nbsp;</span> <b
<b>{{ historyItem.dispatcherName }}</b> v-if="item.dispatcherLevel !== null"
</router-link> class="level-badge dispatcher"
</div> :style="calculateExpStyle(item.dispatcherLevel, item.dispatcherIsSupporter)"
>
<div v-if="historyItem.timestampTo"> {{ item.dispatcherLevel >= 2 ? item.dispatcherLevel : 'L' }}
<b>{{ $d(historyItem.timestampFrom) }}</b> </b>
{{ timestampToString(historyItem.timestampFrom) }} <b>{{ item.dispatcherName }}</b>
- {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }}) </router-link>
</div>
<div v-if="item.timestampTo">
<div class="dispatcher-online" v-else> <b>{{ $d(item.timestampFrom) }}</b>
{{ $t('journal.online-since') }}
<b>{{ timestampToString(historyItem.timestampFrom) }}</b> {{ timestampToString(item.timestampFrom) }}
({{ calculateDuration(historyItem.currentDuration) }}) - {{ timestampToString(item.timestampTo) }} ({{ calculateDuration(item.currentDuration) }})
</div> </div>
</li>
</ul> <div class="dispatcher-online" v-else>
</section> {{ $t('journal.online-since') }}
</template> <b>{{ timestampToString(item.timestampFrom) }}</b>
({{ calculateDuration(item.currentDuration) }})
<script lang="ts"> </div>
</li>
import axios from 'axios'; </ul>
import { defineComponent, PropType } from 'vue'; </section>
import dateMixin from '../../mixins/dateMixin'; </template>
import { DataStatus } from '../../scripts/enums/DataStatus';
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData'; <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 Loading from '../Global/Loading.vue'; import dateMixin from '../../mixins/dateMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
export default defineComponent({ import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
name: 'SceneryDispatchersHistory', import Station from '../../scripts/interfaces/Station';
mixins: [dateMixin], import { URLs } from '../../scripts/utils/apiURLs';
props: { import Loading from '../Global/Loading.vue';
station: { import styleMixin from '../../mixins/styleMixin';
type: Object as PropType<Station>,
required: true, export default defineComponent({
}, name: 'SceneryDispatchersHistory',
}, mixins: [dateMixin, styleMixin],
data() { props: {
return { station: {
dispatcherHistoryList: [] as DispatcherHistory[], type: Object as PropType<Station>,
dataStatus: DataStatus.Loading, required: true,
}; },
}, },
mounted() { data() {
this.fetchAPIData(); return {
}, dispatcherHistoryList: [] as DispatcherHistory[],
methods: { dataStatus: DataStatus.Loading,
async fetchAPIData(countFrom = 0, countLimit = 30) { };
try { },
const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`; activated() {
const historyAPIData: DispatcherHistory[] = await (await axios.get(requestString)).data; this.fetchAPIData();
},
this.dispatcherHistoryList = historyAPIData; methods: {
this.dataStatus = DataStatus.Loaded; async fetchAPIData(countFrom = 0, countLimit = 30) {
try {
console.log(this.dispatcherHistoryList); const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
} catch (error) { const historyAPIData: DispatcherHistory[] = await (await axios.get(requestString)).data;
console.error(error);
} this.dispatcherHistoryList = historyAPIData;
}, this.dataStatus = DataStatus.Loaded;
}, } catch (error) {
components: { Loading }, console.error(error);
}); }
</script> },
},
<style lang="scss" scoped> components: { Loading },
@import '../../styles/responsive.scss'; });
@import '../../styles/SceneryView/styles.scss'; </script>
.history-list { <style lang="scss" scoped>
padding: 0 0.5em; @import '../../styles/responsive.scss';
} @import '../../styles/SceneryView/styles.scss';
.list-item { .history-list {
display: flex; padding: 0 0.5em;
flex-wrap: wrap; }
justify-content: space-between;
.list-item {
text-align: left; display: flex;
background-color: #353535; flex-wrap: wrap;
padding: 0.5em; justify-content: space-between;
margin: 0.5em 0;
text-align: left;
line-height: 1.5em; background-color: #353535;
} padding: 0.5em;
margin: 0.5em 0;
.dispatcher-online {
color: springgreen; line-height: 1.5em;
} }
@include smallScreen { .item-general {
.history-list { display: flex;
font-size: 1.2em; align-items: center;
} flex-wrap: wrap;
.list-item { gap: 0.25em;
align-items: center; }
flex-direction: column;
} .dispatcher-online {
} color: springgreen;
</style> }
@include smallScreen {
.history-list {
font-size: 1.1em;
}
.list-item {
align-items: center;
flex-direction: column;
}
}
</style>
+3 -10
View File
@@ -1,8 +1,8 @@
<template> <template>
<section class="info-header"> <section class="info-header">
<div class="scenery-name"> <a class="scenery-name" :href="station.generalInfo?.url" target="_blank">
{{ station.name }} {{ station.name }}
</div> </a>
<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>
@@ -12,7 +12,6 @@
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: {
station: { station: {
@@ -32,14 +31,9 @@ export default defineComponent({
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 {
@@ -47,4 +41,3 @@ export default defineComponent({
font-size: 1.2em; font-size: 1.2em;
} }
</style> </style>
+22 -18
View File
@@ -1,15 +1,15 @@
<template> <template>
<div class="scenery-info"> <div class="scenery-info">
<section v-if="!timetableOnly"> <section v-if="!timetableOnly">
<div class="info-general" v-if="station.generalInfo"> <div class="scenery-info-general" v-if="station.generalInfo">
<scenery-info-icons :station="station" /> <scenery-info-icons :station="station" />
<div class="general-list"> <div class="scenery-general-list">
<span> <span>
<b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }} <b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }}
<span v-if="station.generalInfo.reqLevel > -1"> <span v-if="station.generalInfo.reqLevel > -1">
- {{ $tc('scenery.req-level', station.generalInfo.reqLevel, { lvl: station.generalInfo.reqLevel }) }} - {{ $t('scenery.req-level', { lvl: station.generalInfo.reqLevel }, station.generalInfo.reqLevel) }}
</span> </span>
</span> </span>
@@ -26,26 +26,32 @@
</span> </span>
<span v-if="station.generalInfo.project"> <span v-if="station.generalInfo.project">
&bull; <b>{{ $t('scenery.project-title') }}: </b> &bull; <b>{{ $t('scenery.project-title') }}: </b>
<b style="color: salmon">{{ station.generalInfo.project }}</b> <a
style="color: salmon; text-decoration: underline; font-weight: bold"
:href="station.generalInfo.projectUrl"
target="_blank"
>{{ station.generalInfo.project }}</a
>
</span> </span>
</div> </div>
<scenery-info-routes :station="station" /> <scenery-info-routes :station="station" />
<div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0"> <div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0">
<b> {{ $tc('scenery.authors-title', station.generalInfo.authors.length) }}: </b> <b>
{{
$t(
'scenery.authors-title',
{ authors: station.generalInfo.authors.length },
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"></div>
<!-- info dispatcher --> <!-- info dispatcher -->
<scenery-info-dispatcher :station="station" :onlineFrom="onlineFrom" /> <scenery-info-dispatcher :station="station" :onlineFrom="onlineFrom" />
@@ -72,7 +78,6 @@ 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: {
SceneryInfoDispatcher, SceneryInfoDispatcher,
@@ -109,7 +114,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;
@@ -125,12 +130,11 @@ h3.section-header {
margin-top: 1em; margin-top: 1em;
} }
.info-general { .scenery-info-general {
margin-top: 1em; margin-top: 1em;
font-size: 1.1em;
} }
.general-list { .scenery-general-list {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
@@ -1,7 +1,10 @@
<template> <template>
<section class="info-dispatcher"> <section class="info-dispatcher">
<div class="dispatcher" v-if="station.onlineInfo"> <div class="dispatcher" v-if="station.onlineInfo">
<span class="dispatcher_level" :style="calculateExpStyle(station.onlineInfo.dispatcherExp)"> <span
class="dispatcher_level"
:style="calculateExpStyle(station.onlineInfo.dispatcherExp, station.onlineInfo.dispatcherIsSupporter)"
>
{{ station.onlineInfo.dispatcherExp > 1 ? station.onlineInfo.dispatcherExp : 'L' }} {{ station.onlineInfo.dispatcherExp > 1 ? station.onlineInfo.dispatcherExp : 'L' }}
</span> </span>
@@ -64,6 +67,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;
@@ -82,17 +86,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;
} }
} }
@@ -68,7 +68,7 @@
<img <img
v-if="!station.generalInfo" v-if="!station.generalInfo"
class="icon-info" class="icon-info"
:src="getImage('unknown.png')" :src="getIcon('unknown')"
alt="icon-unknown" alt="icon-unknown"
:title="$t('desc.unknown')" :title="$t('desc.unknown')"
/> />
@@ -1,114 +1,108 @@
<template> <template>
<section class="info-routes" v-if="station.generalInfo"> <section class="info-routes" v-if="station.generalInfo">
<div class="routes one-way" v-if="station.generalInfo.routes.oneWay.length > 0"> <div class="routes one-way" v-if="station.generalInfo.routes.oneWay.length > 0">
<b>{{ $t('scenery.one-way-routes') }}</b> <b>{{ $t('scenery.one-way-routes') }}</b>
<ul class="routes-list"> <ul class="routes-list">
<li <li v-for="route in station.generalInfo.routes.oneWay">
v-for="route in station.generalInfo.routes.oneWay" <span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"> {{ route.name }}</span>
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }" <span v-if="route.speed" class="speed">{{ route.speed }}</span>
> <span v-if="route.SBL" class="sbl">SBL</span>
{{ route.name }} </li>
<b v-if="route.SBL">SBL</b> </ul>
</li> </div>
</ul>
</div> <div class="routes two-way" v-if="station.generalInfo.routes.twoWay.length > 0">
<b>{{ $t('scenery.two-way-routes') }}</b>
<div class="routes two-way" v-if="station.generalInfo.routes.twoWay.length > 0">
<b>{{ $t('scenery.two-way-routes') }}</b> <ul class="routes-list">
<li v-for="route in station.generalInfo.routes.twoWay">
<ul class="routes-list"> <span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }">{{ route.name }}</span>
<li <span v-if="route.speed" class="speed">{{ route.speed }}</span>
v-for="route in station.generalInfo.routes.twoWay" <span v-if="route.SBL" class="sbl">SBL</span>
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }" </li>
> </ul>
{{ route.name }} <b v-if="route.SBL">SBL</b> </div>
</li> </section>
</ul> </template>
</div>
<script lang="ts">
<!-- <div import { defineComponent } from 'vue';
class="route-info" import Station from '../../../scripts/interfaces/Station';
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"
v-for="route in [...station.generalInfo.routes.oneWay, ...station.generalInfo.routes.twoWay].filter( export default defineComponent({
(route) => route.name != '-' props: {
)" station: {
:key="route.name" type: Object as () => Station,
:title="`Szlak ${route.name}: ${route.isInternal ? 'wewnętrzny' : 'zewnętrzny'}, ${ default: {},
route.tracks == 2 ? 'dwutorowy' : 'jednotorowy' },
}, ${route.catenary ? 'zelektryfikowany' : 'niezelektryfikowany'} z ${route.SBL ? 'SBL' : 'PBL'} ${ },
route.TWB ? 'i blokadą dwukierunkową' : '' });
}`" </script>
> -->
<!-- <span class="track-name"> <style lang="scss" scoped>
<b>{{ route.name }}</b> .info-routes {
</span> --> display: flex;
<!-- justify-content: center;
<span class="track-specs"> flex-wrap: wrap;
{{ route.tracks }}tor
<img v-if="route.catenary" :src="icons.trackCatenary" alt="icon track catenary" /> margin: 1em 0;
<img v-else :src="icons.trackNoCatenary" alt="icon track no catenary" /> }
<img v-if="route.TWB" :src="icons.trackTWB" alt="icon track twb" /> .routes {
<img v-if="route.SBL" :src="icons.trackSBL" alt="icon track sbl" /> display: flex;
</span> --> justify-content: center;
<!-- </div> --> align-items: center;
</section> flex-wrap: wrap;
</template>
padding: 0.25em;
<script lang="ts"> }
import { defineComponent } from 'vue';
import Station from '../../../scripts/interfaces/Station'; ul.routes-list {
margin: 0.45em 0.25em;
export default defineComponent({ display: flex;
props: { justify-content: center;
station: { flex-wrap: wrap;
type: Object as () => Station,
default: {}, li {
}, margin: 0.5em 0.25em;
},
}); span {
</script> padding: 0.2em 0.25em;
background-color: #007599;
<style lang="scss" scoped> font-weight: bold;
.info-routes {
display: flex; &.no-catenary {
justify-content: center; background-color: #686868;
flex-wrap: wrap; }
margin: 1em 0; &.internal {
} text-decoration: underline;
}
.routes {
display: flex; &.speed {
justify-content: center; background-color: #404040;
align-items: center; color: #cfcfcf;
flex-wrap: wrap; }
padding: 0.25em; &.sbl {
} color: var(--clr-primary);
background-color: #404040;
ul.routes-list { }
margin: 0.45em 0.25em;
display: flex; &:last-child {
border-radius: 0 0.5em 0.5em 0;
li { }
background-color: #007599;
&:first-child {
padding: 0.2em 0.25em; border-radius: 0.5em 0 0 0.5em;
margin-left: 0.25em; }
&.no-catenary { &:only-child {
background-color: #686868; border-radius: 0.5em;
}
}
&.internal { }
text-decoration: underline; }
} }
</style>
b {
color: var(--clr-primary);
}
}
}
</style>
+162 -204
View File
@@ -13,21 +13,22 @@
</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>
@@ -36,123 +37,120 @@
{{ $t('scenery.offline') }} {{ $t('scenery.offline') }}
</span> </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="list-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.prevent.stop="selectModalTrain(scheduledTrain.trainId)" tabindex="0"
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId)" @click.prevent.stop="selectModalTrain(scheduledTrain.trainId)"
> @keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId)"
<span class="timetable-general"> >
<span class="general-info"> <span class="timetable-general">
<span class="info-number"> <span class="general-info">
<strong>{{ scheduledTrain.category }}</strong> <span class="info-number">
{{ scheduledTrain.trainNo }} <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="getIcon('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>
@@ -169,11 +167,13 @@ import Station from '../../scripts/interfaces/Station';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import modalTrainMixin from '../../mixins/modalTrainMixin'; 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, TrainModal }, components: { SelectBox, Loading, TrainModal, ScheduledTrainStatus },
mixins: [dateMixin, routerMixin, imageMixin, modalTrainMixin], mixins: [dateMixin, routerMixin, imageMixin, modalTrainMixin],
@@ -182,11 +182,14 @@ export default defineComponent({
type: Object as PropType<Station>, type: Object as PropType<Station>,
required: true, required: true,
}, },
timetableOnly: {
type: Boolean,
},
}, },
data: () => ({ data: () => ({
listOpen: false, listOpen: false,
}), }),
setup(props) { setup(props) {
@@ -250,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() {
@@ -265,12 +272,7 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
@import '../../styles/animations.scss';
// .scenery-timetable {
// height: 85vh;
// max-height: 900px;
// min-height: 450px;
// }
.scenery-timetable { .scenery-timetable {
height: 100%; height: 100%;
@@ -293,7 +295,7 @@ export default defineComponent({
h3 { h3 {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 1.4em; font-size: 1.3em;
} }
} }
@@ -304,12 +306,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;
@@ -324,9 +328,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;
@@ -337,6 +338,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;
} }
} }
@@ -351,17 +356,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;
} }
} }
@@ -401,7 +404,6 @@ export default defineComponent({
} }
.info-route { .info-route {
margin-top: 0.5em;
width: 100%; width: 100%;
} }
@@ -417,38 +419,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,
@@ -458,23 +428,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;
} }
} }
} }
@@ -484,38 +471,9 @@ export default defineComponent({
font-size: 0.85em; font-size: 0.85em;
} }
.scenery-timetable-list-anim { @include smallScreen {
&-enter-from, .timetable-item {
&-leave-to { grid-template-columns: 1fr;
opacity: 0;
}
&-enter-active {
transition: all 100ms ease-out;
}
&-leave-active {
transition: all 100ms ease-out 100ms;
}
}
@include smallScreen() {
.timetable {
&-item {
display: flex;
flex-direction: column;
align-items: center;
font-size: 1.05em;
}
&-general {
width: 100%;
}
&-schedule {
width: 100%;
}
} }
} }
</style> </style>
@@ -1,118 +1,164 @@
<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> <table v-else-if="sceneryHistoryList.length">
<ul class="history-list" v-else> <thead>
<li class="list-item" v-for="historyItem in sceneryHistoryList"> <th>{{ $t('scenery.timetables-history-id') }}</th>
<div> <th>{{ $t('scenery.timetables-history-number')}}</th>
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b> <th>{{ $t('scenery.timetables-history-route')}}</th>
{{ localeTime(historyItem.beginDate, $i18n.locale) }} <th>{{ $t('scenery.timetables-history-driver')}}</th>
</div> <th>{{ $t('scenery.timetables-history-author')}}</th>
<th>{{ $t('scenery.timetables-history-date')}}</th>
<div> </thead>
<router-link :to="`/journal/timetables?timetableId=${historyItem.timetableId}`">
<span class="text--grayed"> #{{ historyItem.timetableId }} </span> <tbody>
<b class="text--primary">&nbsp;{{ historyItem.trainCategoryCode }} {{ historyItem.trainNo }}</b> <tr v-for="historyItem in sceneryHistoryList" @click="test">
<div>{{ historyItem.driverName }}</div> <td>
</router-link> <router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">#{{ historyItem.id }}</router-link>
</div> </td>
<td>
<div>{{ historyItem.route.replace('|', ' -> ') }}</div> <b class="text--primary">{{ historyItem.trainCategoryCode }}</b> <br />
<!-- <div>{{ historyItem.routeDistance }} km</div> --> {{ historyItem.trainNo }}
<div> </td>
{{ $t('scenery.timetable-author-title') }}: <td>{{ historyItem.route.replace('|', ' -> ') }}</td>
<b v-if="historyItem.authorName">{{ historyItem.authorName }}</b> <td>{{ historyItem.driverName }}</td>
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i> <td>
</div> <router-link
v-if="historyItem.authorName"
<!-- <div v-if="historyItem.authorId">{{ historyItem.authorName }}</div> --> :to="`/journal/dispatchers?dispatcherName=${historyItem.authorName}`"
</li> >{{ historyItem.authorName }}
</ul> </router-link>
</section> <i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
</template> </td>
<td>
<script lang="ts"> <b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
{{ localeTime(historyItem.beginDate, $i18n.locale) }}
import axios from 'axios'; </td>
import { defineComponent, PropType } from 'vue'; </tr>
import dateMixin from '../../mixins/dateMixin'; </tbody>
import { DataStatus } from '../../scripts/enums/DataStatus'; </table>
import { TimetableHistory, SceneryTimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import Station from '../../scripts/interfaces/Station'; <div class="list-warning" v-else>{{ $t('scenery.history-list-empty') }}</div>
import { URLs } from '../../scripts/utils/apiURLs';
import Loading from '../Global/Loading.vue'; <!-- <ul class="history-list" v-else>
<li class="list-item" v-for="historyItem in sceneryHistoryList">
export default defineComponent({ <div>
name: 'SceneryTimetablesHistory', <b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
mixins: [dateMixin], {{ localeTime(historyItem.beginDate, $i18n.locale) }}
props: { </div>
station: {
type: Object as PropType<Station>, <div>
required: true, <router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">
}, <span class="text--grayed"> #{{ historyItem.id }} </span>
}, <b class="text--primary">&nbsp;{{ historyItem.trainCategoryCode }} {{ historyItem.trainNo }}</b>
data() { <div>{{ historyItem.driverName }}</div>
return { </router-link>
sceneryHistoryList: [] as TimetableHistory[], </div>
dataStatus: DataStatus.Loading,
}; <div>{{ historyItem.route.replace('|', ' -> ') }}</div>
}, <div>
mounted() { {{ $t('scenery.timetable-author-title') }}:
this.fetchAPIData(); <b v-if="historyItem.authorName">{{ historyItem.authorName }}</b>
}, <i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
methods: { </div>
async fetchAPIData(countFrom = 0, countLimit = 15) {
try { </li>
const requestString = `${URLs.stacjownikAPI}/api/getSceneryTimetables?name=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`; </ul> -->
const historyAPIData: SceneryTimetableHistory = await (await axios.get(requestString)).data; </section>
</template>
this.sceneryHistoryList = historyAPIData.sceneryTimetables;
this.dataStatus = DataStatus.Loaded; <script lang="ts">
} catch (error) { import axios from 'axios';
console.error(error); import { defineComponent, PropType } from 'vue';
} import dateMixin from '../../mixins/dateMixin';
}, import { DataStatus } from '../../scripts/enums/DataStatus';
}, import { TimetableHistory, SceneryTimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
components: { Loading }, import Station from '../../scripts/interfaces/Station';
}); import { URLs } from '../../scripts/utils/apiURLs';
</script> import Loading from '../Global/Loading.vue';
<style lang="scss" scoped> export default defineComponent({
@import '../../styles/responsive.scss'; name: 'SceneryTimetablesHistory',
@import '../../styles/SceneryView/styles.scss'; mixins: [dateMixin],
props: {
.list-warning { station: {
padding: 1em 0.5em; type: Object as PropType<Station>,
background-color: #444; required: true,
font-size: 1.2em; },
} },
data() {
.history-list { return {
padding: 0 0.5em; sceneryHistoryList: [] as TimetableHistory[],
} dataStatus: DataStatus.Loading,
};
.list-item { },
display: grid; activated() {
grid-template-columns: 1fr 2fr 2fr 1fr; this.fetchAPIData();
gap: 1em; },
align-items: center; methods: {
async fetchAPIData(countFrom = 0, countLimit = 15) {
background-color: #353535; try {
padding: 0.5em; const requestString = `${URLs.stacjownikAPI}/api/getSceneryTimetables?name=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
margin: 0.5em 0; const historyAPIData: SceneryTimetableHistory = await (await axios.get(requestString)).data;
line-height: 1.5em; this.sceneryHistoryList = historyAPIData.sceneryTimetables;
} this.dataStatus = DataStatus.Loaded;
} catch (error) {
@include smallScreen { console.error(error);
.history-list { }
font-size: 1.1em; },
}
.list-item { test() {
grid-template-columns: 1fr 1fr; console.log('test');
font-size: 1.05em; },
} },
} components: { Loading },
</style> });
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/SceneryView/styles.scss';
.list-warning {
padding: 1em 0.5em;
background-color: #444;
font-size: 1.2em;
}
.history-list {
padding: 0 0.5em;
}
table {
width: 100%;
border-collapse: collapse;
thead {
position: sticky;
top: 0;
background-color: #222222;
}
th {
padding: 0.5em;
}
tr {
background-color: #353535;
border: none;
}
td {
padding: 0.75em;
border-bottom: solid 5px #111;
}
}
@include smallScreen {
.list-item {
grid-template-columns: 1fr 1fr;
}
}
</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>
+127 -114
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="getIcon('filter2')" 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 sortedStationList" :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,11 +98,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, inject } from 'vue'; import { defineComponent, inject } from 'vue';
import inputData from '../../data/options.json';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import keyMixin from '../../mixins/keyMixin';
import routerMixin from '../../mixins/routerMixin';
import StorageManager from '../../scripts/managers/storageManager'; import StorageManager from '../../scripts/managers/storageManager';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import ActionButton from '../Global/ActionButton.vue'; import ActionButton from '../Global/ActionButton.vue';
@@ -103,12 +111,9 @@ import FilterOption from './FilterOption.vue';
export default defineComponent({ export default defineComponent({
components: { ActionButton, FilterOption }, components: { ActionButton, FilterOption },
emits: ['changeFilterValue', 'invertFilters', 'resetFilters'], mixins: [imageMixin, keyMixin, routerMixin],
mixins: [imageMixin],
data: () => ({ data: () => ({
inputs: { ...inputData },
saveOptions: false, saveOptions: false,
STORAGE_KEY: 'options_saved', STORAGE_KEY: 'options_saved',
@@ -118,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,
}; };
}, },
@@ -142,9 +150,39 @@ export default defineComponent({
this.currentRegion = this.store.region; this.currentRegion = this.store.region;
}, },
computed: {
sortedStationList() {
return this.store.stationList
.filter((s) => s.name.toLocaleLowerCase().includes(this.chosenSearchScenery.toLocaleLowerCase()))
.sort((s1, s2) => (s1.name > s2.name ? 1 : -1));
},
},
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,
}); });
@@ -155,7 +193,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,
}); });
@@ -172,7 +210,7 @@ export default defineComponent({
}, },
changeNumericFilterValue(name: string, value: number, saveToStorage = false) { changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
this.$emit('changeFilterValue', { this.filterStore.changeFilterValue({
name, name,
value, value,
}); });
@@ -192,17 +230,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: any }) {
this.saveOptions = change.value;
if (!this.saveOptions) { if (!this.saveOptions) {
StorageManager.unregisterStorage(this.STORAGE_KEY); StorageManager.unregisterStorage(this.STORAGE_KEY);
@@ -211,28 +240,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() {
@@ -264,28 +281,24 @@ export default defineComponent({
} }
.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 {
@@ -293,8 +306,6 @@ export default defineComponent({
font-weight: 700; font-weight: 700;
color: $accentCol; color: $accentCol;
margin: 0.5em 0;
text-align: center; text-align: center;
} }
@@ -342,32 +353,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;
}
} }
} }
@@ -389,22 +386,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;
}
}
} }
} }
} }
@@ -435,8 +443,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;
+29 -15
View File
@@ -100,7 +100,10 @@
</td> </td>
<td class="station_dispatcher-exp"> <td class="station_dispatcher-exp">
<span v-if="station.onlineInfo" :style="calculateExpStyle(station.onlineInfo.dispatcherExp)"> <span
v-if="station.onlineInfo"
:style="calculateExpStyle(station.onlineInfo.dispatcherExp, station.onlineInfo.dispatcherIsSupporter)"
>
{{ 2 > station.onlineInfo.dispatcherExp ? 'L' : station.onlineInfo.dispatcherExp }} {{ 2 > station.onlineInfo.dispatcherExp ? 'L' : station.onlineInfo.dispatcherExp }}
</span> </span>
</td> </td>
@@ -182,7 +185,7 @@
</td> </td>
<td class="station_info" v-else> <td class="station_info" v-else>
<img class="icon-info" :src="getImage('unknown.png')" 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 }">
@@ -230,6 +233,7 @@ import stationInfoMixin from '../../mixins/stationInfoMixin';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { DataStatus } from '../../scripts/enums/DataStatus';
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
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';
@@ -239,48 +243,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 },
}, },
components: { Loading },
mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin], mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin],
data: () => ({ data: () => ({
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>
@@ -289,7 +303,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,
@@ -328,7 +342,7 @@ table {
} }
thead tr { thead tr {
background-color: $primaryCol; background-color: $bgCol;
} }
thead th { thead th {
@@ -338,7 +352,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;
+312 -287
View File
@@ -1,287 +1,312 @@
<template> <template>
<div class="train-info" tabindex="0"> <div class="train-info" tabindex="0">
<section class="train-route"> <section class="train-route">
<div class="train_general"> <div class="train_general">
<span> <b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
<span class="timetable-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span> <span class="timetable-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span>
<span class="timetable_warnings"> <span class="timetable_warnings" v-if="train.timetableData?.TWR || train.timetableData?.SKR">
<span class="train-badge twr" v-if="train.timetableData?.TWR">TWR</span> <span class="train-badge twr" v-if="train.timetableData?.TWR">TWR</span>
<span class="train-badge skr" v-if="train.timetableData?.SKR">SKR</span> <span class="train-badge skr" v-if="train.timetableData?.SKR">SKR</span>
</span> </span>
<strong v-if="train.timetableData">{{ train.timetableData.category }}&nbsp;</strong>
<strong>{{ train.trainNo }}</strong> <strong>
<span>&nbsp;| {{ train.driverName }}&nbsp;</span> <span v-if="train.timetableData" class="text--primary">{{ train.timetableData.category }}&nbsp;</span>
</span> <span class="train-number">{{ train.trainNo }}</span>
</div> </strong>
<span>&bull;</span>
<div class="timetable_route" v-if="train.timetableData"> <b class="level-badge driver" :style="calculateExpStyle(train.driverLevel, train.isSupporter)">
<strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong> {{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
<img </b>
v-if="getSceneriesWithComments(train.timetableData).length > 0" <span>{{ train.driverName }}</span>
class="image-warning" </div>
:src="getIcon('warning')"
:title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(train.timetableData)})`" <div class="timetable_route" v-if="train.timetableData">
/> <strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
</div> <img
v-if="getSceneriesWithComments(train.timetableData).length > 0"
<hr style="margin: 0.25em 0" /> class="image-warning"
:src="getIcon('warning')"
<div class="timetable_stops" v-if="train.timetableData"> :title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(train.timetableData)})`"
<span v-if="train.timetableData.followingStops.length > 2"> />
{{ $t('trains.via-title') }} </div>
<span v-html="displayStopList(train.timetableData.followingStops)"></span>
</span> <hr style="margin: 0.25em 0" />
</div>
<div class="timetable_stops" v-if="train.timetableData">
<div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData"> <span v-if="train.timetableData.followingStops.length > 2">
<!-- <span> </span> --> {{ $t('trains.via-title') }}
<span class="timetable_progress-bar"> <span v-html="displayStopList(train.timetableData.followingStops)"></span>
<!-- {{ confirmedPercentage(train.timetableData.followingStops) }}%&nbsp; --> </span>
<span class="bar-bg"></span> </div>
<span
class="bar-fg" <div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
:style="{ width: `${Math.floor(confirmedPercentage(train.timetableData.followingStops))}%` }" <span class="timetable_progress-bar">
></span> <span class="bar-bg"></span>
</span> <span
class="bar-fg"
<span class="timetable_progress-distance"> :style="{ width: `${Math.floor(confirmedPercentage(train.timetableData.followingStops))}%` }"
&nbsp; {{ currentDistance(train.timetableData.followingStops) }} km / ></span>
<span class="text--primary"> {{ train.timetableData.routeDistance }} km </span> </span>
|
<span v-html="currentDelay(train.timetableData.followingStops)"></span> <span class="timetable_progress-distance">
</span> &nbsp; {{ currentDistance(train.timetableData.followingStops) }} km /
<span class="text--primary"> {{ train.timetableData.routeDistance }} km </span>
<div class="train-status-badges"> |
<div v-if="!train.currentStationHash" class="train-badge offline">{{ $t('trains.scenery-offline') }}</div> <span v-html="currentDelay(train.timetableData.followingStops)"></span>
<div v-if="!train.online" class="train-badge offline">Offline {{ lastSeenMessage(train.lastSeen) }}</div> </span>
</div>
</div> <div class="train-status-badges">
<div v-if="!train.currentStationHash" class="train-badge offline">{{ $t('trains.scenery-offline') }}</div>
<div class="driver_position text--grayed" style="margin-top: 0.25em"> <div v-if="!train.online" class="train-badge offline">Offline {{ lastSeenMessage(train.lastSeen) }}</div>
{{ displayTrainPosition(train) }} </div>
</div> </div>
</section>
<div class="driver_position text--grayed" style="margin-top: 0.25em">
<section class="train-stats"> {{ displayTrainPosition(train) }}
<div> </div>
<img :src="train.locoURL" loading="lazy" alt="Loco image not found" @error="onImageError" /> </section>
</div>
<section class="train-stats">
<div class="text--grayed"> <div>
{{ train.locoType }} <img :src="train.locoURL" loading="lazy" alt="Loco image not found" @error="onImageError" />
<span v-if="train.cars.length > 0"> </div>
&nbsp;&bull; {{ $t('trains.cars') }}:
<span class="count">{{ train.cars.length }}</span> <div class="text--grayed">
</span> {{ train.locoType }}
</div> <span v-if="train.cars.length > 0">
&nbsp;&bull; {{ $t('trains.cars') }}:
<div> <span class="count">{{ train.cars.length }}</span>
<span v-for="(stat, i) in STATS.main" :key="stat.name"> </span>
<span v-if="i > 0"> &bull; </span> </div>
<span>{{ `${~~((train as any)[stat.name] * (stat.multiplier || 1))}${stat.unit}` }} </span>
</span> <div>
</div> <span v-for="(stat, i) in STATS.main" :key="stat.name">
</section> <span v-if="i > 0"> &bull; </span>
</div> <span>{{ `${~~((train as any)[stat.name] * (stat.multiplier || 1))}${stat.unit}` }} </span>
</template> </span>
</div>
<script lang="ts"> </section>
import { defineComponent } from 'vue'; </div>
import imageMixin from '../../mixins/imageMixin'; </template>
import trainInfoMixin from '../../mixins/trainInfoMixin';
import Train from '../../scripts/interfaces/Train'; <script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({ import imageMixin from '../../mixins/imageMixin';
props: { import styleMixin from '../../mixins/styleMixin';
train: { import trainInfoMixin from '../../mixins/trainInfoMixin';
type: Object as () => Train, import Train from '../../scripts/interfaces/Train';
required: true,
}, export default defineComponent({
props: {
extended: { train: {
type: Boolean, type: Object as () => Train,
default: true, required: true,
}, },
},
extended: {
mixins: [trainInfoMixin, imageMixin], type: Boolean,
}); default: true,
</script> },
},
<style lang="scss" scoped>
@import '../../styles/responsive.scss'; mixins: [trainInfoMixin, imageMixin, styleMixin],
});
.image-warning { </script>
height: 1em;
<style lang="scss" scoped>
margin-left: 0.5em; @import '../../styles/responsive.scss';
} @import '../../styles/badge.scss';
.train-stats {
display: flex; .image-warning {
justify-content: center; height: 1em;
align-content: center;
margin-left: 0.5em;
flex-direction: column; }
text-align: center;
.train-stats {
img { display: flex;
margin: 0.5em 0; justify-content: center;
width: 12em; align-content: center;
}
} flex-direction: column;
text-align: center;
.train-info {
display: grid; img {
grid-template-columns: 2fr 1fr; margin: 0.5em 0;
grid-template-rows: 1fr; width: 12em;
}
padding: 1em; }
background-color: #1a1a1a; .train-info {
gap: 0.5em; display: grid;
} grid-template-columns: 2fr 1fr;
grid-template-rows: 1fr;
.timetable-id {
margin-right: 0.3em; padding: 1em;
color: #d2d2d2;
} background-color: #1a1a1a;
gap: 0.5em;
.timetable_stops { }
font-size: 0.75em;
} .timetable-id {
color: #d2d2d2;
.train_general { }
display: flex;
align-items: center; .warning-timeout {
flex-wrap: wrap; background-color: #be3728;
}
.train-status-badges { display: inline-block;
display: flex; text-align: center;
flex-wrap: wrap;
} padding: 0 0.25em;
}
.train-badge {
padding: 0.15em 0.35em; .timetable_stops {
margin-right: 0.3em; font-size: 0.75em;
}
font-weight: bold;
.train_general {
font-size: 0.9em; display: flex;
align-items: center;
&.twr { flex-wrap: wrap;
background-color: var(--clr-twr);
} gap: 0.25em;
margin-right: 1.5em;
&.skr { }
background-color: var(--clr-skr); .train-status-badges {
} display: flex;
flex-wrap: wrap;
&.offline {
background-color: #b83b2d; gap: 0.25em;
} }
}
.train-badge {
.timetable_route { padding: 0.1em 0.2em;
display: flex; border-radius: 0.2em;
align-items: center; font-weight: bold;
margin-top: 0.5em; font-size: 0.9em;
}
&.twr {
.timetable_warnings { background-color: var(--clr-twr);
color: black; }
}
&.skr {
.timetable_progress { background-color: var(--clr-skr);
display: flex; }
align-items: center;
flex-wrap: wrap; &.offline {
} background-color: #9c362b;
}
.timetable_progress-bar { }
position: relative;
.train-driver {
width: 6em; &.supporter {
height: 1em; color: orange;
margin: 0.5em 0; text-shadow: orange 0 0 5px;
}
.bar-fg, }
.bar-bg {
position: absolute; .timetable_route {
height: 1em; display: flex;
width: 100%; align-items: center;
left: 0; margin-top: 0.5em;
} }
.bar-fg { .timetable_warnings {
background-color: springgreen; display: flex;
} gap: 0.2em;
.bar-bg { color: black;
background-color: #5b5b5b; }
}
} .timetable_progress {
display: flex;
.timetable_progress-distance { align-items: center;
margin-right: 0.25em; flex-wrap: wrap;
} }
.comments { .timetable_progress-bar {
display: flex; position: relative;
align-items: center;
width: 6em;
font-size: 0.9em; height: 1em;
margin: 0.5em 0;
margin-top: 1em;
.bar-fg,
img { .bar-bg {
margin-right: 0.5em; position: absolute;
} height: 1em;
} width: 100%;
@include smallScreen() { left: 0;
.train-info { }
grid-template-columns: 1fr;
gap: 1em 0; .bar-fg {
text-align: center; background-color: springgreen;
}
font-size: 1.15em;
} .bar-bg {
background-color: #5b5b5b;
.train-stats { }
font-size: 1.1em; }
img { .timetable_progress-distance {
display: none; margin-right: 0.25em;
} }
}
.comments {
.train_general { display: flex;
justify-content: center; align-items: center;
}
font-size: 0.9em;
.train-status-badges {
justify-content: center; margin-top: 1em;
}
img {
.timetable_route { margin-right: 0.5em;
justify-content: center; }
} }
.timetable_progress { @include smallScreen() {
justify-content: center; .train-info {
} grid-template-columns: 1fr;
gap: 1em 0;
.comments { text-align: center;
flex-direction: column;
justify-content: center; font-size: 1.15em;
}
img {
margin: 0 0 0.5em 0; .train-stats {
} font-size: 1.1em;
} }
}
</style> .train_general {
justify-content: center;
}
.train-status-badges {
justify-content: center;
}
.timetable_route {
justify-content: center;
}
.timetable_progress {
justify-content: center;
}
.comments {
flex-direction: column;
justify-content: center;
img {
margin: 0 0 0.5em 0;
}
}
}
</style>
+153 -212
View File
@@ -1,267 +1,208 @@
<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="filter-button 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]
<span class="active-indicator" v-if="currentOptionsActive"></span>
</button>
<img class="search-exit" :src="getIcon('exit')" 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="getIcon('exit')" 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-inactive="!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 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],
mixins: [imageMixin],
setup() { props: {
const { t } = useI18n(); sorterOptionIds: {
type: Array as PropType<Array<string>>,
required: true,
},
const sorterOptions = [ currentOptionsActive: {
{ type: Boolean,
id: 'distance', default: false,
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, lastSelectedFilter: null as TrainFilter | null,
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) {
// if (this.lastSelectedFilter?.id === filter.id)
// this.trainFilterList.forEach((tf) => (tf.isActive = filter.id === tf.id));
filter.isActive = !filter.isActive; filter.isActive = !filter.isActive;
this.lastSelectedFilter = filter;
}, },
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,144 +0,0 @@
<template>
<section class="filter-card" v-click-outside="closeCard">
<div class="card_btn">
<action-button @click="toggleCard">
<img class="button_icon" :src="getIcon('filter2')" 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="getIcon('exit')" 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="getIcon('exit')" 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 inputData from "../../data/options.json";
import { TrainFilter, computed, defineComponent, inject } from 'vue';
import { useI18n } from 'vue-i18n';
import SelectBox from '../Global/SelectBox.vue';
import ActionButton from '../Global/ActionButton.vue';
import { sorterOptions } from '../../data/trainOptions';
import imageMixin from "../../mixins/imageMixin";
export default defineComponent({
components: { ActionButton, SelectBox },
emits: ['changeFilterValue', 'invertFilters', 'resetFilters'],
mixins: [imageMixin],
data: () => ({
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 {
section {
margin: 0.5em 0;
}
&_title {
font-size: 2em;
font-weight: 700;
color: $accentCol;
margin: 0.5em 0;
text-align: center;
}
}
</style>
+12 -5
View File
@@ -60,7 +60,9 @@
<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 && !/sbl/gi.test(stop.departureLine!)"> <span
v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine && !/sbl/gi.test(stop.departureLine!)"
>
{{ stop.departureLine }} {{ stop.departureLine }}
</span> </span>
@@ -87,6 +89,7 @@ import dateMixin from '../../mixins/dateMixin';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import Train from '../../scripts/interfaces/Train'; import Train from '../../scripts/interfaces/Train';
import TrainStop from '../../scripts/interfaces/TrainStop'; import TrainStop from '../../scripts/interfaces/TrainStop';
import { useStore } from '../../store/store';
import StopDate from '../Global/StopDate.vue'; import StopDate from '../Global/StopDate.vue';
export default defineComponent({ export default defineComponent({
@@ -104,6 +107,8 @@ export default defineComponent({
setup(props) { setup(props) {
return { return {
store: useStore(),
lastConfirmed: computed(() => { lastConfirmed: computed(() => {
return props.train.timetableData!.followingStops.findIndex( return props.train.timetableData!.followingStops.findIndex(
(stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed && !stops[i + 1]?.stopped (stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed && !stops[i + 1]?.stopped
@@ -175,10 +180,6 @@ $stopNameClr: #22a8d1;
.train-schedule { .train-schedule {
padding: 0 0.25em; padding: 0 0.25em;
@include smallScreen() {
font-size: 1.1em;
}
} }
.train-stock { .train-stock {
@@ -198,6 +199,11 @@ 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 {
@@ -421,3 +427,4 @@ ul.stop_list > li.stop {
} }
} }
</style> </style>
+54 -56
View File
@@ -2,13 +2,21 @@
<div class="train-table"> <div class="train-table">
<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" /> <div class="table-info" v-if="store.isOffline">
{{ $t('app.offline') }}
</div>
<div class="table-info no-trains" v-if="trains.length == 0 && store.dataStatuses.trains != 0"> <Loading v-else-if="trains.length == 0 && store.dataStatuses.trains == 0" />
<div class="table-info no-trains" v-else-if="trains.length == 0 && store.dataStatuses.trains != 0">
{{ $t('trains.no-trains') }} {{ $t('trains.no-trains') }}
</div> </div>
<ul class="train-list"> <!-- <div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length == 0">
<b class="warning-timeout">?</b>
{{ $t('trains.timeout') }}
</div> -->
<transition-group name="list-anim" tag="ul" class="train-list" v-else>
<li <li
class="train-row" class="train-row"
v-for="train in currentTrains" v-for="train in currentTrains"
@@ -18,46 +26,37 @@
> >
<TrainInfo :train="train" /> <TrainInfo :train="train" />
</li> </li>
</ul> </transition-group>
</div> </div>
</transition> </transition>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, inject, Ref, computed } from 'vue'; import { computed, defineComponent, inject, PropType, Ref } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import returnBtnMixin from '../../mixins/returnBtnMixin'; import returnBtnMixin from '../../mixins/returnBtnMixin';
import Train from '../../scripts/interfaces/Train'; import Train from '../../scripts/interfaces/Train';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import TrainModal from '../Global/TrainModal.vue';
import TrainInfo from './TrainInfo.vue'; import TrainInfo from './TrainInfo.vue';
import TrainSchedule from './TrainSchedule.vue';
export default defineComponent({ export default defineComponent({
components: { components: { Loading, TrainInfo },
TrainSchedule,
TrainInfo,
Loading,
TrainModal,
},
mixins: [returnBtnMixin, modalTrainMixin],
props: { props: {
trains: { trains: {
type: Array as () => Train[], type: Array as PropType<Train[]>,
required: true, required: true,
}, },
}, },
mixins: [returnBtnMixin, modalTrainMixin],
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;
}); });
@@ -67,58 +66,38 @@ 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.selectModalTrain(query.driverName + <string>query.trainNo); this.selectModalTrain(query.driverName! + query.trainNo!.toString());
}, 20); }, 20);
} }
}, },
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);
},
},
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/animations.scss';
.anim { .anim {
&-enter-from, &-enter-from,
@@ -139,11 +118,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 {
@@ -156,12 +134,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; position: relative;
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,64 @@
import { TrainFilter } from "vue"; import { TrainFilterType } from '../../scripts/enums/TrainFilterType';
import { TrainFilterType } from "../scripts/enums/TrainFilterType"; 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: 'id',
value: 'przebyta trasa', value: 'id rozkładu',
}, },
{ {
id: 'delay', id: 'progress',
value: 'opóźnienie', value: 'przebyta trasa',
}, },
{ {
id: 'mass', id: 'delay',
value: 'masa', value: 'opóźnienie',
}, },
{ {
id: 'speed', id: 'mass',
value: 'prędkość', value: 'masa',
}, },
{ {
id: 'length', id: 'speed',
value: 'długość', value: 'prędkość',
} },
]; {
id: 'length',
value: 'długość',
},
];
-30
View File
@@ -1,30 +0,0 @@
import { JournalFilter } from "vue";
import { JournalFilterType } from "../scripts/enums/JournalFilterType";
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": [
+108 -58
View File
@@ -1,4 +1,8 @@
{ {
"general": {
"and": " and ",
"refresh": "REFRESH"
},
"app": { "app": {
"sceneries": "SCENERIES", "sceneries": "SCENERIES",
"trains": "TRAINS", "trains": "TRAINS",
@@ -8,15 +12,18 @@
"error": "An error occured while loading data!", "error": "An error occured while loading data!",
"no-result": "No results for current search!", "no-result": "No results for current search!",
"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!",
"offline": "App is in the offline mode!"
}, },
"update": { "update": {
"title": "New Stacjownik version is available!", "title": "New version of the app is available!",
"paragraph1": "Enjoy the application and may the green signal be with you!", "paragraph1": "Enjoy the application and may the green signal be with you!",
"release-link": "Click here to browse version changelog (GitHub)", "release-link": "Click here to browse version changelog (GitHub)",
"confirm-button": "Understood!" "confirm-button": "UPDATE NOW",
"later-button": "LATER"
}, },
"data-status": { "data-status": {
"S1-offline": "<b>S1 signal</b> <br> The app is working in offline mode!",
"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!",
"S2": "<b>S2 signal</b> <br> All data loaded successfully!", "S2": "<b>S2 signal</b> <br> All data loaded successfully!",
@@ -72,7 +79,53 @@
}, },
"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-id": "timetable id",
"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",
@@ -116,7 +169,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"
}, },
@@ -131,7 +184,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!",
@@ -150,28 +204,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",
@@ -195,7 +227,8 @@
"last-seen-min": "since one minute", "last-seen-min": "since one minute",
"last-seen-ago": "since {minutes} minutes", "last-seen-ago": "since {minutes} minutes",
"scenery-offline": "Offline ride" "scenery-offline": "Offline ride",
"timeout": "An error occured while trying to refresh SWDR timetable data!"
}, },
"journal": { "journal": {
"title": "DISPATCHER HISTORY", "title": "DISPATCHER HISTORY",
@@ -205,26 +238,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...",
@@ -239,7 +252,41 @@
"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...",
"last-seen-at": "Last seen at",
"currently-at": "Currently at",
"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",
"timetable-stats-total": "Today, dispatchers made so far {count} with total distance of {distance}",
"timetable-stats-longest": "The longest timetable today is #{id} made by {author} for {driver} - {distance}",
"timetable-stats-most-active": "The most active dispatcher today is {dispatcher} who created {count}",
"timetable-stats-most-active-many": "The most active dispatchers today are {dispatchers} who created {count} each",
"timetable-count": "timetable | timetables",
"daily-stats-title": "DAILY STATS",
"daily-stats-info": "Today's statistics are unavailable yet!",
"driver-stats-title": "DRIVER STATS",
"driver-stats-info": "Enter a proper nickname into filters [F] to see user's driving statistics!",
"stats-loading": "Fetching statistics...",
"stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/"
}, },
"scenery": { "scenery": {
"users": "PLAYERS ONLINE", "users": "PLAYERS ONLINE",
@@ -266,6 +313,13 @@
"timetable-author-title": "Issued by", "timetable-author-title": "Issued by",
"timetable-author-unknown": "Author unknown", "timetable-author-unknown": "Author unknown",
"timetables-history-id": "ID",
"timetables-history-number": "Number",
"timetables-history-route": "Route",
"timetables-history-driver": "Driver",
"timetables-history-author": "TT author",
"timetables-history-date": "Date",
"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!",
@@ -281,12 +335,8 @@
}, },
"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"
}, },
+107 -55
View File
@@ -1,4 +1,8 @@
{ {
"general": {
"and": " oraz ",
"refresh": "ODŚWIEŻ"
},
"app": { "app": {
"sceneries": "SCENERIE", "sceneries": "SCENERIE",
"trains": "POCIĄGI", "trains": "POCIĄGI",
@@ -8,17 +12,20 @@
"error": "Wystąpił problem z załadowaniem danych!", "error": "Wystąpił problem z załadowaniem danych!",
"no-result": "Brak wyników o podanych kryteriach!", "no-result": "Brak wyników o podanych kryteriach!",
"migration-warning": "Usługi Stacjownika będą niedostępne w godzinach 1:00-3:00 2 czerwca 2022r. z powodu migracji hostingów API!", "migration-warning": "Usługi Stacjownika będą niedostępne w godzinach 1:00-3:00 2 czerwca 2022r. z powodu migracji hostingów API!",
"migration-confirm": "Przyjąłem!" "migration-confirm": "Przyjąłem!",
"offline": "Aplikacja w trybie offline!"
}, },
"update": { "update": {
"title": "Nowa wersja Stacjownika jest dostępna!", "title": "Nowa wersja Stacjownika jest dostępna!",
"paragraph1": "Miłego korzystania z aplikacji i niech S2 będzie z wami!", "paragraph1": "Miłego korzystania z aplikacji i niech S2 będzie z wami!",
"release-link": "Kliknij, aby przejrzeć listę zmian (GitHub)", "release-link": "Kliknij, aby przejrzeć listę zmian (GitHub)",
"confirm-button": "Przyjąłem!" "confirm-button": "ZAKTUALIZUJ",
"later-button": "PÓŹNIEJ"
}, },
"data-status": { "data-status": {
"S1-offline": "<b>Sygnał S1</b> <br> Aplikacja działa w trybie offline!",
"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!",
"S2": "<b>Sygnał S2</b> <br> Pomyślnie załadowano dane!", "S2": "<b>Sygnał S2</b> <br> Pomyślnie załadowano dane!",
@@ -74,7 +81,54 @@
}, },
"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-id": "id rozkładu",
"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",
@@ -118,7 +172,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"
}, },
@@ -133,7 +187,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!",
@@ -152,28 +207,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",
@@ -197,7 +230,9 @@
"last-seen-min": "od minuty", "last-seen-min": "od minuty",
"last-seen-ago": "od {minutes} minut", "last-seen-ago": "od {minutes} minut",
"scenery-offline": "Przejazd offline" "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",
@@ -207,26 +242,6 @@
"section-timetables": "ROZKŁADY JAZDY", "section-timetables": "ROZKŁADY JAZDY",
"section-dispatchers": "DYŻURNI", "section-dispatchers": "DYŻURNI",
"search": "Szukaj",
"search-train": "Nr 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...",
@@ -241,7 +256,41 @@
"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",
"last-seen-at": "Ostatnio widziany na: ",
"currently-at": "Obecnie na scenerii: ",
"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",
"timetable-stats-total": "Dyżurni stworzyli dziś {count} o łącznym dystansie {distance}",
"timetable-stats-longest": "Najdłuższym rozkładem jazdy jest dzisiaj #{id} stworzony przez dyżurnego {author} dla maszynisty {driver} - {distance}",
"timetable-stats-most-active": "Dzisiejszym najaktywniejszym dyżurnym jest {dispatcher}, który stworzył {count}",
"timetable-stats-most-active-many": "Dzisiejszymi najaktywniejszymi dyżurnymi są {dispatchers}, którzy stworzyli po {count}",
"timetable-count": "rozkład jazdy | rozkładów jazdy",
"daily-stats-title": "STATYSTYKI DNIA",
"daily-stats-info": "Dzisiejsze statystyki nie są jeszcze dostępne!",
"driver-stats-title": "STATYSTYKI GRACZA",
"driver-stats-info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!",
"stats-loading": "Pobieranie statystyk...",
"stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk! :/"
}, },
"scenery": { "scenery": {
"users": "GRACZE ONLINE", "users": "GRACZE ONLINE",
@@ -268,6 +317,13 @@
"timetable-author-title": "Wydany przez", "timetable-author-title": "Wydany przez",
"timetable-author-unknown": "Autor nieznany", "timetable-author-unknown": "Autor nieznany",
"timetables-history-id": "ID",
"timetables-history-number": "Numer",
"timetables-history-route": "Trasa",
"timetables-history-driver": "Maszynista",
"timetables-history-author": "Autor RJ",
"timetables-history-date": "Data",
"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!",
@@ -283,12 +339,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"
}, },
+11
View File
@@ -7,9 +7,11 @@ import plLang from './locales/pl.json';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import { registerSW } from 'virtual:pwa-register';
const i18n = createI18n({ const i18n = createI18n({
locale: 'pl', locale: 'pl',
legacy: false,
fallbackLocale: 'pl', fallbackLocale: 'pl',
messages: { messages: {
en: enLang, en: enLang,
@@ -18,6 +20,15 @@ const i18n = createI18n({
enableLegacy: false, enableLegacy: false,
}); });
registerSW({
onRegistered(r) {
r &&
setInterval(() => {
r.update();
}, 60 * 60 * 1000);
},
});
const clickOutsideDirective: Directive = { const clickOutsideDirective: Directive = {
mounted(el, binding) { mounted(el, binding) {
el.clickOutsideEvent = (event: Event) => { el.clickOutsideEvent = (event: Event) => {
+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 -30
View File
@@ -1,30 +1,31 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStore } from '../store/store'; import { useStore } from '../store/store';
export default defineComponent({ export default defineComponent({
setup() { data() {
return { return {
store: useStore(), store: useStore(),
}; };
}, },
mounted() { computed: {
console.log('Mixin mounted'); chosenTrain() {
}, return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId);
},
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');
methods: { },
selectModalTrain(trainId: string) {
this.store.chosenModalTrainId = trainId; closeModal() {
}, this.store.chosenModalTrainId = undefined;
closeModal() { setTimeout(() => {
this.store.chosenModalTrainId = undefined; document.body.classList.remove('no-scroll');
}, }, 150);
}, },
}); },
});
+34 -34
View File
@@ -1,34 +1,34 @@
import { defineComponent, h } from 'vue'; import { defineComponent, h } from 'vue';
import imageMixin from './imageMixin'; import imageMixin from './imageMixin';
export default defineComponent({ export default defineComponent({
mixins: [imageMixin], mixins: [imageMixin],
data() { data() {
return { return {
icons: { icons: {
arrow: this.getIcon('arrow-asc'), arrow: this.getIcon('arrow-asc'),
}, },
showReturnButton: false, showReturnButton: false,
}; };
}, },
methods: { methods: {
scrollToTop() { scrollToTop() {
window.scrollTo({ top: 0 }); window.scrollTo({ top: 0 });
}, },
handleScroll() { handleScroll() {
this.showReturnButton = window.scrollY > window.innerHeight * 0.35; this.showReturnButton = window.scrollY > window.innerHeight * 0.35;
}, },
}, },
activated() { activated() {
window.addEventListener('scroll', this.handleScroll); window.addEventListener('wheel', this.handleScroll);
}, },
deactivated() { deactivated() {
window.removeEventListener('scroll', this.handleScroll); window.removeEventListener('wheel', this.handleScroll);
}, },
}); });
+14 -8
View File
@@ -4,11 +4,17 @@ export default defineComponent({
methods: { methods: {
calculateExpStyle(exp: number, isSupporter = false): string { calculateExpStyle(exp: number, isSupporter = false): string {
const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666'; const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666';
const fontColor = exp > 15 || exp == -1 ? 'white' : 'black'; const fontColor = exp > 14 || exp == -1 ? 'white' : 'black';
const boxShadow = isSupporter ? `box-shadow: 0 0 10px 2px ${bgColor};` : ''; const boxShadow = isSupporter ? `box-shadow: 0 0 6px 2px ${bgColor};` : '';
return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow}`; return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow};`;
},
calculateTextExpStyle(exp: number, isSupporter = false): string {
const textColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 75%, 50%)`) : '#666';
return `color: ${textColor}; ${isSupporter ? 'text-shadow: 0 0 6px ' + textColor : ''};`;
}, },
statusClasses(occupiedTo: string) { statusClasses(occupiedTo: string) {
@@ -41,6 +47,6 @@ export default defineComponent({
} }
return className; return className;
} },
} },
}) });
+13
View File
@@ -0,0 +1,13 @@
import { useRegisterSW } from 'virtual:pwa-register/vue';
export default () => {
const { needRefresh, updateServiceWorker, offlineReady } = useRegisterSW({
immediate: true,
});
return {
needRefresh,
updateServiceWorker,
offlineReady,
};
};
+21 -31
View File
@@ -1,6 +1,6 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import JournalDispatchersVue from '../components/JournalView/JournalDispatchers.vue'; import JournalDispatchersVue from '../views/JournalDispatchers.vue';
import JournalTimetablesVue from '../components/JournalView/JournalTimetables.vue'; import JournalTimetablesVue from '../views/JournalTimetables.vue';
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
@@ -12,42 +12,32 @@ const routes: Array<RouteRecordRaw> = [
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', redirect: '/journal/timetables'
component: () => import('../views/JournalView.vue'), },
children: [ {
{ path: '/journal/timetables',
path: '', name: 'JournalTimetables',
name: 'JournalTimetables', component: JournalTimetablesVue,
component: JournalTimetablesVue, props: (route) => ({
alias: '/timetables', trainNo: route.query.trainNo,
}, driverName: route.query.driverName,
{ timetableId: route.query.timetableId,
path: 'dispatchers', }),
name: 'JournalDispatchers', },
component: JournalDispatchersVue, {
props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }), path: '/journal/dispatchers',
}, name: 'JournalDispatchers',
{ component: JournalDispatchersVue,
path: 'timetables', props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }),
name: 'JournalTimetables',
component: JournalTimetablesVue,
props: (route) => ({
trainNo: route.query.trainNo,
driverName: route.query.driverName,
timetableId: route.query.timetableId,
}),
},
],
}, },
{ {
path: '/:catchAll(.*)', path: '/:catchAll(.*)',
@@ -59,7 +49,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,
+6
View File
@@ -20,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;
+3 -2
View File
@@ -8,12 +8,13 @@ export default interface Station {
generalInfo?: { generalInfo?: {
name: string; name: string;
url: string; url: string;
reqLevel: number; reqLevel: number;
// supportersOnly: boolean; // supportersOnly: boolean;
lines: string; lines: string;
project: string; project: string;
projectUrl?: string;
signalType: string; signalType: string;
controlType: string; controlType: string;
+30 -27
View File
@@ -1,27 +1,30 @@
export default interface StationRoutes { export default interface StationRoutes {
oneWay: oneWay: {
{ name: string;
name: string; catenary: boolean;
catenary: boolean; SBL: boolean;
SBL: boolean; TWB: boolean;
TWB: boolean; isInternal: boolean;
isInternal: boolean; tracks: number;
tracks: number; speed: number;
}[]; length: number;
}[];
twoWay: {
name: string; twoWay: {
catenary: boolean; name: string;
SBL: boolean; catenary: boolean;
TWB: boolean; SBL: boolean;
isInternal: boolean; TWB: boolean;
tracks: number; isInternal: boolean;
}[]; tracks: number;
speed: number;
/* [catenary, noCatenary] */ length: number;
oneWayCatenaryRouteNames: string[]; }[];
oneWayNoCatenaryRouteNames: string[];
twoWayCatenaryRouteNames: string[]; /* [catenary, noCatenary] */
twoWayNoCatenaryRouteNames: string[]; oneWayCatenaryRouteNames: string[];
sblRouteNames: string[]; oneWayNoCatenaryRouteNames: string[];
} twoWayCatenaryRouteNames: string[];
twoWayNoCatenaryRouteNames: string[];
sblRouteNames: string[];
}
+4 -1
View File
@@ -12,6 +12,7 @@ export default interface Train {
driverId: number; driverId: number;
trainNo: number; trainNo: number;
driverName: string; driverName: string;
driverLevel: number;
currentStationName: string; currentStationName: string;
currentStationHash: string; currentStationHash: string;
locoURL: string; locoURL: string;
@@ -19,9 +20,11 @@ export default interface Train {
online: boolean; online: boolean;
lastSeen: number; lastSeen: number;
region: string; region: string;
cars: string[]; cars: string[];
isTimeout: boolean;
isSupporter: boolean;
timetableData?: { timetableData?: {
timetableId: number; timetableId: number;
category: string; category: string;
+1 -1
View File
@@ -1,4 +1,4 @@
export default interface TrainStop { export default interface TrainStop {
stopName: string; stopName: string;
stopNameRAW: string; stopNameRAW: string;
stopType: string; stopType: string;
@@ -1,12 +1,17 @@
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; dispatcherLevel: number | null;
stationHash: string; dispatcherRate: number;
stationName: string; dispatcherIsSupporter: boolean;
timestampFrom: number; isOnline: boolean;
timestampTo?: number; lastOnlineTimestamp: number;
} region: string;
stationHash: string;
stationName: string;
timestampFrom: number;
timestampTo?: number;
}
@@ -0,0 +1,30 @@
import { TimetableHistory } from './TimetablesAPIData';
export interface ITimetablesDailyStats {
totalTimetables: number;
distanceSum: number;
distanceAvg: number;
timetableId: number;
timetableAuthor: string;
timetableDriver: string;
timetableRouteDistance: number;
mostActiveDispatchers: {
name: string;
count: number;
}[];
}
export interface ITimetablesDailyStatsResponse {
totalTimetables: number;
distanceSum: number;
distanceAvg: number;
maxTimetable: TimetableHistory | null;
mostActiveDispatchers: {
name: string;
count: number;
}[];
}
+50 -35
View File
@@ -1,35 +1,50 @@
export interface TimetableHistory { export interface TimetableHistory {
timetableId: number; id: number;
trainNo: number;
trainCategoryCode: string; timetableId: number;
driverId: number; trainNo: number;
driverName: string; trainCategoryCode: string;
route: string;
twr: number; driverId: number;
skr: number; driverName: string;
sceneriesString: string; driverLevel: number | null;
driverIsSupporter: boolean;
routeDistance: number;
currentDistance: number; route: string;
twr: number;
confirmedStopsCount: number; skr: number;
allStopsCount: number; sceneriesString: string;
beginDate: string; routeDistance: number;
endDate: string; currentDistance: number;
scheduledBeginDate: string; confirmedStopsCount: number;
scheduledEndDate: string; allStopsCount: number;
terminated: boolean; beginDate: string;
fulfilled: boolean; endDate: string;
authorName?: string; scheduledBeginDate: string;
authorId?: number; scheduledEndDate: string;
}
terminated: boolean;
export interface SceneryTimetableHistory { fulfilled: boolean;
sceneryTimetables: TimetableHistory[];
totalCount: number; authorName?: string;
sceneryName: string; authorId?: number;
}
stockString?: string;
stockMass?: number;
stockLength?: number;
maxSpeed?: number;
hashesString?: string;
currentSceneryName?: string;
currentSceneryHash?: string;
}
export interface SceneryTimetableHistory {
sceneryTimetables: TimetableHistory[];
totalCount: number;
sceneryName: string;
}
@@ -13,6 +13,7 @@ export default interface TrainAPIData {
driverName: string; driverName: string;
driverId: number; driverId: number;
driverIsSupporter: boolean; driverIsSupporter: boolean;
driverLevel?: number;
currentStationName: string; currentStationName: string;
currentStationHash: string; currentStationHash: string;
@@ -21,6 +22,7 @@ export default interface TrainAPIData {
lastSeen: number; lastSeen: number;
region: string; region: string;
isTimeout: boolean;
timetable?: { timetable?: {
timetableId: number; timetableId: number;
+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);
} }
+120 -115
View File
@@ -1,115 +1,120 @@
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 (
return (searchedTrain.length > 0 ? train.trainNo.toString().startsWith(searchedTrain) : true) && (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) &&
} (!train.timetableData ? !train.online : true) &&
isFiltered
); );
} });
}
function sortTrainList(trainList: Train[], sorterActive: { id: string; dir: number }) {
return trainList.sort((a: Train, b: Train) => { function sortTrainList(trainList: Train[], sorterActive: { id: string; dir: number }) {
switch (sorterActive.id) { return trainList.sort((a: Train, b: Train) => {
case 'mass': switch (sorterActive.id) {
if (a.mass > b.mass) return sorterActive.dir; case 'id':
return -sorterActive.dir; if ((a.timetableData?.timetableId || -1) > (b.timetableData?.timetableId || -1)) return sorterActive.dir;
case 'distance': return -sorterActive.dir;
if ((a.timetableData?.routeDistance || -1) > (b.timetableData?.routeDistance || -1)) return sorterActive.dir;
case 'mass':
return -sorterActive.dir; if (a.mass > b.mass) return sorterActive.dir;
return -sorterActive.dir;
case 'progress':
if (confirmedPercentage(a.timetableData?.followingStops) > confirmedPercentage(b.timetableData?.followingStops)) case 'distance':
return sorterActive.dir; if ((a.timetableData?.routeDistance || -1) > (b.timetableData?.routeDistance || -1)) return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'delay': case 'progress':
if (currentDelay(a.timetableData?.followingStops) > currentDelay(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 'speed': case 'delay':
if (a.speed > b.speed) return sorterActive.dir; if (currentDelay(a.timetableData?.followingStops) > currentDelay(b.timetableData?.followingStops))
return -sorterActive.dir; return sorterActive.dir;
case 'timetable': return -sorterActive.dir;
if (a.trainNo > b.trainNo) return sorterActive.dir;
return -sorterActive.dir; case 'speed':
if (a.speed > b.speed) return sorterActive.dir;
case 'length': return -sorterActive.dir;
if (a.length > b.length) return sorterActive.dir;
return -sorterActive.dir; case 'timetable':
if (a.trainNo > b.trainNo) return sorterActive.dir;
default: return -sorterActive.dir;
break;
} case 'length':
if (a.length > b.length) return sorterActive.dir;
return 0; return -sorterActive.dir;
});
} default:
break;
export function filteredTrainList( }
trainList: Train[],
searchedTrain: string, return 0;
searchedDriver: string, });
sorterActive: { id: string; dir: number }, }
filters: TrainFilter[]
) { export function filteredTrainList(
trainList: Train[],
const filtered = filterTrainList(trainList, searchedTrain, searchedDriver, filters); searchedTrain: string,
return [...sortTrainList(filtered, sorterActive)]; searchedDriver: string,
}; sorterActive: { id: string; dir: number },
filters: TrainFilter[]
) {
const filtered = filterTrainList(trainList, searchedTrain, searchedDriver, filters);
return [...sortTrainList(filtered, sorterActive)];
}
+1 -5
View File
@@ -1,9 +1,5 @@
export const URLs = { export const URLs = {
stacjownikAPI: stacjownikAPI:
import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD ? 'http://localhost:3000' : 'https://spythere.pl',
? 'http://localhost:3000'
: 'https://stacjownik.eu-4.evennode.com',
stacjownikAPIDev: 'localhost:3000', stacjownikAPIDev: 'localhost:3000',
// 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}`
}; };
+22 -7
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,10 +155,12 @@ 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;
} }
} }
@@ -160,7 +168,11 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
return { return {
trainNo: train.trainNo, trainNo: train.trainNo,
trainId: train.trainId, 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,
@@ -179,5 +191,8 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
arrivingLine, arrivingLine,
departureLine, departureLine,
nextArrivalLine,
prevDepartureLine,
}; };
} }
@@ -1,292 +1,308 @@
import Filter from '../interfaces/Filter'; import { defineStore } from 'pinia';
import Station from '../interfaces/Station'; 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) { import { useStore } from './store';
case 1:
if ((a.generalInfo?.reqLevel || 0) > (b.generalInfo?.reqLevel || 0)) return sorter.dir; 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) {
break; case 0:
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
case 2:
if ((a.onlineInfo?.statusTimestamp || 0) > (b.onlineInfo?.statusTimestamp || 0)) return sorter.dir; 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;
break; if ((a.generalInfo?.reqLevel || 0) < (b.generalInfo?.reqLevel || 0)) return -sorter.dir;
break;
case 3:
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') > (b.onlineInfo?.dispatcherName.toLowerCase() || '')) case 2:
return sorter.dir; if ((a.onlineInfo?.statusTimestamp || 0) > (b.onlineInfo?.statusTimestamp || 0)) return sorter.dir;
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') < (b.onlineInfo?.dispatcherName.toLowerCase() || '')) if ((a.onlineInfo?.statusTimestamp || 0) < (b.onlineInfo?.statusTimestamp || 0)) return -sorter.dir;
return -sorter.dir; break;
break;
case 3:
case 4: if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') > (b.onlineInfo?.dispatcherName.toLowerCase() || ''))
if ((a.onlineInfo?.dispatcherExp || 0) > (b.onlineInfo?.dispatcherExp || 0)) return sorter.dir; return sorter.dir;
if ((a.onlineInfo?.dispatcherExp || 0) < (b.onlineInfo?.dispatcherExp || 0)) return -sorter.dir; if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') < (b.onlineInfo?.dispatcherName.toLowerCase() || ''))
break; return -sorter.dir;
break;
case 7:
if ((a.onlineInfo?.currentUsers || 0) > (b.onlineInfo?.currentUsers || 0)) return sorter.dir; 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?.dispatcherExp || 0) < (b.onlineInfo?.dispatcherExp || 0)) return -sorter.dir;
if ((a.onlineInfo?.maxUsers || 0) > (b.onlineInfo?.maxUsers || 0)) return sorter.dir; break;
if ((a.onlineInfo?.maxUsers || 0) < (b.onlineInfo?.maxUsers || 0)) return -sorter.dir;
break; case 7:
if ((a.onlineInfo?.currentUsers || 0) > (b.onlineInfo?.currentUsers || 0)) return sorter.dir;
case 8: if ((a.onlineInfo?.currentUsers || 0) < (b.onlineInfo?.currentUsers || 0)) return -sorter.dir;
if ((a.onlineInfo?.spawns.length || 0) > (b.onlineInfo?.spawns.length || 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;
if ((a.onlineInfo?.maxUsers || 0) < (b.onlineInfo?.maxUsers || 0)) return -sorter.dir;
break; break;
case 9: case 8:
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?.spawns.length || 0) < (b.onlineInfo?.spawns.length || 0)) return -sorter.dir;
if ((a.onlineInfo?.scheduledTrains?.length || 0) < (b.onlineInfo?.scheduledTrains?.length || 0))
return -sorter.dir; break;
default: case 9:
break; if ((a.onlineInfo?.scheduledTrains?.length || 0) > (b.onlineInfo?.scheduledTrains?.length || 0))
} return sorter.dir;
if ((a.onlineInfo?.scheduledTrains?.length || 0) < (b.onlineInfo?.scheduledTrains?.length || 0))
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name); return -sorter.dir;
};
default:
const filterStations = (station: Station, filters: Filter) => { break;
const returnMode = false; }
if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic']) return a.name.localeCompare(b.name);
return returnMode; };
if (station.onlineInfo?.statusID == 'ending' && filters['ending']) return returnMode; const filterStations = (station: Station, filters: Filter, isOffline = false) => {
const returnMode = false;
if (
station.onlineInfo && if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic'])
station.onlineInfo.statusTimestamp > 0 && return returnMode;
filters['onlineFromHours'] < 8 &&
station.onlineInfo.statusTimestamp <= Date.now() + filters['onlineFromHours'] * 3600000 if (station.onlineInfo?.statusID == 'ending' && filters['ending']) return returnMode;
)
return returnMode; if (
station.onlineInfo &&
if (filters['onlineFromHours'] > 0 && station.onlineInfo && station.onlineInfo.statusTimestamp <= 0) station.onlineInfo.statusTimestamp > 0 &&
return returnMode; filters['onlineFromHours'] < 8 &&
if (filters['onlineFromHours'] == 8 && station.onlineInfo?.statusID != 'no-limit') return returnMode; station.onlineInfo.statusTimestamp <= Date.now() + filters['onlineFromHours'] * 3600000
)
if (station.onlineInfo?.statusID == 'ending' && filters['endingStatus']) return returnMode; return returnMode;
if (
(station.onlineInfo?.statusID == 'not-signed' || station.onlineInfo?.statusID == 'unavailable') && if (filters['onlineFromHours'] > 0 && station.onlineInfo && station.onlineInfo.statusTimestamp <= 0)
filters['unavailableStatus'] return returnMode;
) if (filters['onlineFromHours'] == 8 && station.onlineInfo?.statusID != 'no-limit') return returnMode;
return returnMode;
if (station.onlineInfo?.statusID == 'brb' && filters['afkStatus']) return returnMode; if (station.onlineInfo?.statusID == 'ending' && filters['endingStatus']) return returnMode;
if (station.onlineInfo?.statusID == 'no-space' && filters['noSpaceStatus']) return returnMode; if (
(station.onlineInfo?.statusID == 'not-signed' || station.onlineInfo?.statusID == 'unavailable') &&
if (station.onlineInfo && filters['occupied']) return returnMode; filters['unavailableStatus']
if (!station.onlineInfo && filters['free']) return returnMode; )
if (station.generalInfo?.availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo) return returnMode;
return returnMode; if (station.onlineInfo?.statusID == 'brb' && filters['afkStatus']) return returnMode;
if (station.onlineInfo?.statusID == 'no-space' && filters['noSpaceStatus']) return returnMode;
if (station.generalInfo) {
const routes = station.generalInfo.routes; if (station.onlineInfo && filters['occupied']) return returnMode;
const availability = station.generalInfo.availability; if (!station.onlineInfo && filters['free']) return returnMode;
if (station.generalInfo?.availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo)
if (filters['abandoned'] && availability == 'abandoned') return returnMode; return returnMode;
if (availability == 'default' && filters['default']) return returnMode; if (station.generalInfo) {
if ( const routes = station.generalInfo.routes;
availability != 'default' && const availability = station.generalInfo.availability;
filters['notDefault'] &&
!(availability == 'abandoned' || availability == 'unavailable') if (filters['abandoned'] && availability == 'abandoned' && !station.onlineInfo) return returnMode;
)
return returnMode; if (availability == 'default' && filters['default']) return returnMode;
if (
if (filters['real'] && station.generalInfo.lines != '') return returnMode; availability != 'default' &&
if ( filters['notDefault'] &&
filters['fictional'] && !(availability == 'abandoned' || availability == 'unavailable')
station.generalInfo.lines == '' && )
availability != 'abandoned' && return returnMode;
availability != 'unavailable'
) if (filters['real'] && station.generalInfo.lines != '') return returnMode;
return returnMode; if (
filters['fictional'] &&
if ( station.generalInfo.lines == '' &&
station.generalInfo.reqLevel + availability != 'abandoned' &&
(availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned' ? 1 : 0) < availability != 'unavailable'
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['maxLevel'] filters['minLevel']
) )
return returnMode; return returnMode;
if (
if ( station.generalInfo.reqLevel +
filters['no-1track'] && (availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned' ? 1 : 0) >
(routes.oneWayCatenaryRouteNames.length != 0 || routes.oneWayNoCatenaryRouteNames.length != 0) filters['maxLevel']
) )
return returnMode; return returnMode;
if (
filters['no-2track'] && if (
(routes.twoWayCatenaryRouteNames.length != 0 || routes.twoWayNoCatenaryRouteNames.length != 0) filters['no-1track'] &&
) (routes.oneWayCatenaryRouteNames.length != 0 || routes.oneWayNoCatenaryRouteNames.length != 0)
return returnMode; )
return returnMode;
if (routes.oneWayCatenaryRouteNames.length < filters['minOneWayCatenary']) return returnMode; if (
if (routes.oneWayNoCatenaryRouteNames.length < filters['minOneWay']) return returnMode; filters['no-2track'] &&
(routes.twoWayCatenaryRouteNames.length != 0 || routes.twoWayNoCatenaryRouteNames.length != 0)
if (routes.twoWayCatenaryRouteNames.length < filters['minTwoWayCatenary']) return returnMode; )
if (routes.twoWayNoCatenaryRouteNames.length < filters['minTwoWay']) return returnMode; return returnMode;
if (filters[station.generalInfo.controlType]) return returnMode; if (routes.oneWayCatenaryRouteNames.length < filters['minOneWayCatenary']) return returnMode;
if (filters[station.generalInfo.signalType]) return returnMode; if (routes.oneWayNoCatenaryRouteNames.length < filters['minOneWay']) return returnMode;
if ( if (routes.twoWayCatenaryRouteNames.length < filters['minTwoWayCatenary']) return returnMode;
filters['SPK'] && if (routes.twoWayNoCatenaryRouteNames.length < filters['minTwoWay']) return returnMode;
(station.generalInfo.controlType === 'SPK' || station.generalInfo.controlType.includes('+SPK'))
) if (filters[station.generalInfo.controlType]) return returnMode;
return returnMode; if (filters[station.generalInfo.signalType]) return returnMode;
if (
filters['SCS'] && if (
(station.generalInfo.controlType === 'SCS' || station.generalInfo.controlType.includes('+SCS')) filters['SPK'] &&
) (station.generalInfo.controlType === 'SPK' || station.generalInfo.controlType.includes('+SPK'))
return returnMode; )
if ( return returnMode;
filters['SPE'] && if (
(station.generalInfo.controlType === 'SPE' || station.generalInfo.controlType.includes('+SPE')) filters['SCS'] &&
) (station.generalInfo.controlType === 'SCS' || station.generalInfo.controlType.includes('+SCS'))
return returnMode; )
if (filters['SUP'] && station.generalInfo.SUP) return returnMode; return returnMode;
if (
if ( filters['SPE'] &&
filters['SCS'] && (station.generalInfo.controlType === 'SPE' || station.generalInfo.controlType.includes('+SPE'))
filters['SPK'] && )
(station.generalInfo.controlType.includes('SPK') || station.generalInfo.controlType.includes('SCS')) return returnMode;
) if (filters['SUP'] && station.generalInfo.SUP) return returnMode;
return returnMode;
if (
if (filters['mechaniczne'] && station.generalInfo.controlType.includes('mechaniczne')) return returnMode; filters['SCS'] &&
filters['SPK'] &&
if (filters['ręczne'] && station.generalInfo.controlType.includes('ręczne')) return returnMode; (station.generalInfo.controlType.includes('SPK') || station.generalInfo.controlType.includes('SCS'))
)
if (filters['SBL'] && routes.sblRouteNames.length > 0) return returnMode; return returnMode;
if ( if (filters['mechaniczne'] && station.generalInfo.controlType.includes('mechaniczne')) return returnMode;
filters['authors'].length > 3 &&
!station.generalInfo.authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase()) if (filters['ręczne'] && station.generalInfo.controlType.includes('ręczne')) return returnMode;
)
return returnMode; if (filters['SBL'] && routes.sblRouteNames.length > 0) return returnMode;
}
if (
return true; filters['authors'].length > 3 &&
}; !station.generalInfo.authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase())
)
export default class StationFilterManager { return returnMode;
private filterInitStates: Filter = { }
default: false,
notDefault: false, return true;
real: false, };
fictional: false,
SPK: false, const filterInitStates: Filter = {
SCS: false, default: false,
SPE: false, notDefault: false,
SUP: false, real: false,
ręczne: false, fictional: false,
mechaniczne: false, SPK: false,
współczesna: false, SCS: false,
kształtowa: false, SPE: false,
historyczna: false, SUP: false,
mieszana: false, ręczne: false,
SBL: false, mechaniczne: false,
minLevel: 0, współczesna: false,
maxLevel: 20, kształtowa: false,
minOneWayCatenary: 0, historyczna: false,
minOneWay: 0, mieszana: false,
minTwoWayCatenary: 0, SBL: false,
minTwoWay: 0, minLevel: 0,
'include-selected': false, maxLevel: 20,
'no-1track': false, minOneWayCatenary: 0,
'no-2track': false, minOneWay: 0,
free: true, minTwoWayCatenary: 0,
occupied: false, minTwoWay: 0,
ending: false, 'include-selected': false,
nonPublic: false, 'no-1track': false,
unavailable: true, 'no-2track': false,
abandoned: true, free: true,
afkStatus: false, occupied: false,
endingStatus: false, ending: false,
noSpaceStatus: false, nonPublic: false,
unavailableStatus: false, unavailable: true,
unsignedStatus: false, abandoned: true,
afkStatus: false,
authors: '', endingStatus: false,
noSpaceStatus: false,
onlineFromHours: 0, unavailableStatus: false,
}; unsignedStatus: false,
private filters: Filter = { ...this.filterInitStates }; authors: '',
private sorter: { index: number; dir: number } = { index: 0, dir: 1 }; onlineFromHours: 0,
};
checkFilters() {
if (!StorageManager.isRegistered('options_saved')) return; export const useStationFiltersStore = defineStore('stationFiltersStore', {
state() {
Object.keys(this.filterInitStates).forEach((filterKey) => { return {
if (StorageManager.isRegistered(filterKey)) return; inputs: inputData,
filters: { ...filterInitStates },
const filterType = typeof this.filterInitStates[filterKey]; sorterActive: { index: 0, dir: 1 },
store: useStore(),
if (filterType === 'boolean') };
StorageManager.setBooleanValue(filterKey, !this.filterInitStates[filterKey] as boolean); },
if (filterType === 'number') actions: {
StorageManager.setNumericValue(filterKey, this.filterInitStates[filterKey] as number); getFilteredStationList(stationList: Station[], region: string): Station[] {
}); return stationList
} .map((station) => {
if (station.onlineInfo && station.onlineInfo.region != region) {
getFilteredStationList(stationList: Station[], region: string): Station[] { delete station.onlineInfo;
return stationList }
.map((station) => {
if (station.onlineInfo && station.onlineInfo.region != region) { return station;
delete station.onlineInfo; })
} .filter((station) => filterStations(station, this.filters, this.store.isOffline))
.sort((a, b) => sortStations(a, b, this.sorterActive));
return station; },
})
.filter((station) => filterStations(station, this.filters)) setupFilters() {
.sort((a, b) => sortStations(a, b, this.sorter)); if (!StorageManager.isRegistered('options_saved')) return;
}
this.inputs.options.forEach((option) => {
changeFilterValue(filter: { name: string; value: number }) { if (!StorageManager.isRegistered(option.id)) return;
this.filters[filter.name] = filter.value; const savedValue = StorageManager.getBooleanValue(option.id);
// if(filter.name == 'authors') this.filters[option.id] = savedValue;
} option.value = !savedValue;
});
resetFilters() {
this.filters = { ...this.filterInitStates }; this.inputs.sliders.forEach((slider) => {
} if (!StorageManager.isRegistered(slider.name)) return;
const savedValue = StorageManager.getNumericValue(slider.name);
invertFilters() {
Object.keys(this.filters).forEach((prop) => { this.filters[slider.name] = savedValue;
if (typeof this.filters[prop] !== 'boolean') return; slider.value = savedValue;
});
this.filters[prop] = !this.filters[prop]; },
});
} changeFilterValue(filter: { name: string; value: any }) {
this.filters[filter.name] = filter.value;
changeSorter(index: number) {
if (index > 4 && index < 7) return; if (StorageManager.isRegistered('options_saved')) StorageManager.setValue(filter.name, filter.value);
},
if (index == this.sorter.index) this.sorter.dir = -1 * this.sorter.dir;
else this.sorter.dir = 1; resetFilters() {
this.filters = { ...filterInitStates };
this.sorter.index = index;
} this.inputs.options.forEach((option) => {
option.value = option.defaultValue;
getSorter() { StorageManager.setBooleanValue(option.name, !option.defaultValue);
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;
},
},
});
+421 -398
View File
@@ -1,398 +1,421 @@
import axios from 'axios'; import axios from 'axios';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { io } from 'socket.io-client'; import { io } from 'socket.io-client';
import { DataStatus } from '../scripts/enums/DataStatus'; import { DataStatus } from '../scripts/enums/DataStatus';
import StationAPIData from '../scripts/interfaces/api/StationAPIData'; import StationAPIData from '../scripts/interfaces/api/StationAPIData';
import ScheduledTrain from '../scripts/interfaces/ScheduledTrain'; import ScheduledTrain from '../scripts/interfaces/ScheduledTrain';
import Station from '../scripts/interfaces/Station'; import Station from '../scripts/interfaces/Station';
import StationRoutes from '../scripts/interfaces/StationRoutes'; import StationRoutes from '../scripts/interfaces/StationRoutes';
import Train from '../scripts/interfaces/Train'; import Train from '../scripts/interfaces/Train';
import { URLs } from '../scripts/utils/apiURLs'; import { URLs } from '../scripts/utils/apiURLs';
import { import {
getLocoURL, getLocoURL,
getStatusTimestamp, getStatusTimestamp,
getStatusID, getStatusID,
getScheduledTrain, getScheduledTrain,
parseSpawns, parseSpawns,
} from '../scripts/utils/storeUtils'; } from '../scripts/utils/storeUtils';
import { APIData, StationJSONData, StoreState } from './storeTypes'; import { APIData, StationJSONData, StoreState } from './storeTypes';
export const useStore = defineStore('store', { export const useStore = defineStore('store', {
state: () => state: () =>
({ ({
apiData: {} as unknown, apiData: {} as unknown,
stationList: [], stationList: [],
trainList: [], trainList: [],
routesList: [],
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: '', isOffline: false,
dispatcherStatsData: undefined,
dispatcherStatsName: '',
driverStatsName: '', dispatcherStatsData: undefined,
driverStatsData: undefined,
driverStatsName: '',
chosenModalTrainId: undefined, driverStatsData: undefined,
driverStatsStatus: DataStatus.Initialized,
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: { currentStatsTab: 'daily',
setTrainsOnlineData() {
const { trains } = this.apiData; blockScroll: false,
listenerLaunched: false,
if (!trains) return []; } as StoreState),
this.trainList = trains actions: {
.filter( setTrainsOnlineData() {
(train) => const { trains } = this.apiData;
train.region === this.region.id && (train.online || train.timetable || train.lastSeen > Date.now() - 180000)
) if (!trains) return [];
.map((train) => {
const stock = train.stockString.split(';'); this.trainList = trains
const locoType = stock ? stock[0] : train.stockString; .filter(
(train) =>
const timetable = train.timetable; train.region === this.region.id && (train.online || train.timetable || train.lastSeen > Date.now() - 180000)
)
return { .map((train) => {
trainId: train.driverName + train.trainNo.toString(), const stock = train.stockString.split(';');
const locoType = stock ? stock[0] : train.stockString;
trainNo: train.trainNo,
mass: train.mass, const timetable = train.timetable;
length: train.length,
speed: train.speed, return {
region: train.region, trainId: train.driverName + train.trainNo.toString(),
distance: train.distance, trainNo: train.trainNo,
signal: train.signal, mass: train.mass,
online: train.online, length: train.length,
driverId: train.driverId, speed: train.speed,
driverName: train.driverName, region: train.region,
currentStationName: train.currentStationName,
currentStationHash: train.currentStationHash, distance: train.distance,
connectedTrack: train.connectedTrack, signal: train.signal,
locoType, online: train.online,
locoURL: getLocoURL(locoType), driverId: train.driverId,
cars: stock.slice(1), driverName: train.driverName,
currentStationName: train.currentStationName,
lastSeen: train.lastSeen, currentStationHash: train.currentStationHash,
connectedTrack: train.connectedTrack,
timetableData: timetable locoType,
? { locoURL: getLocoURL(locoType),
timetableId: timetable.timetableId, cars: stock.slice(1),
SKR: timetable.SKR,
TWR: timetable.TWR, lastSeen: train.lastSeen,
route: timetable.route, isTimeout: train.isTimeout,
category: timetable.category,
followingStops: timetable.stopList, isSupporter: train.driverIsSupporter,
routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance, driverLevel: train.driverLevel,
sceneries: timetable.sceneries,
} timetableData: timetable
: undefined, ? {
}; timetableId: timetable.timetableId,
}) as Train[]; SKR: timetable.SKR,
}, TWR: timetable.TWR,
route: timetable.route,
getDispatcherStatus(onlineStationData: StationAPIData) { category: timetable.category,
const { dispatchers } = this.apiData; followingStops: timetable.stopList,
routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance,
const prevDispatcherStatus = this.lastDispatcherStatuses.find( sceneries: timetable.sceneries,
(dispatcher) => dispatcher.hash === onlineStationData.stationHash }
); : undefined,
} as Train;
const stationStatus = !dispatchers });
? undefined },
: dispatchers.find(
(status: string[]) => status[0] == onlineStationData.stationHash && status[1] == this.region.id getDispatcherStatus(onlineStationData: StationAPIData) {
) || -1; const { dispatchers } = this.apiData;
const statusTimestamp = const prevDispatcherStatus = this.lastDispatcherStatuses.find(
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusTimestamp : getStatusTimestamp(stationStatus); (dispatcher) => dispatcher.hash === onlineStationData.stationHash
const statusID = );
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusID : getStatusID(stationStatus);
const stationStatus = !dispatchers
return { ? undefined
hash: onlineStationData.stationHash, : dispatchers.find(
statusID, (status: string[]) => status[0] == onlineStationData.stationHash && status[1] == this.region.id
statusTimestamp, ) || -1;
};
}, const statusTimestamp =
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusTimestamp : getStatusTimestamp(stationStatus);
getScheduledTrains(stationGeneralInfo: Station['generalInfo'], stationAPIData: StationAPIData) { const statusID =
const stationName = stationAPIData.stationName.toLowerCase(); prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusID : getStatusID(stationStatus);
stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0)); return {
hash: onlineStationData.stationHash,
return this.trainList.reduce((acc: ScheduledTrain[], train) => { statusID,
if (!train.timetableData) return acc; statusTimestamp,
};
const timetable = train.timetableData; },
if (!timetable.sceneries.includes(stationAPIData.stationHash)) return acc;
getScheduledTrains(stationGeneralInfo: Station['generalInfo'], stationAPIData: StationAPIData) {
const stopInfoIndex = timetable.followingStops.findIndex((stop) => { const stationName = stationAPIData.stationName.toLowerCase();
const stopName = stop.stopNameRAW.toLowerCase();
stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0));
if (stationName === stopName) return true;
if (stopName.includes(stationName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.')) return this.trainList.reduce((acc: ScheduledTrain[], train) => {
return true; if (!train.timetableData) return acc;
if (stationName.includes(stopName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.')) const timetable = train.timetableData;
return true; if (!timetable.sceneries.includes(stationAPIData.stationHash)) return acc;
if ( const stopInfoIndex = timetable.followingStops.findIndex((stop) => {
stopName.includes('podg.') && const stopName = stop.stopNameRAW.toLowerCase();
stopName.split(', podg.')[0] &&
stationName.includes(stopName.split(', podg.')[0]) if (stationName === stopName) return true;
) if (stopName.includes(stationName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.'))
return true; return true;
if ( if (stationName.includes(stopName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.'))
stationGeneralInfo && return true;
stationGeneralInfo.checkpoints &&
stationGeneralInfo.checkpoints.length > 0 && if (
stationGeneralInfo.checkpoints.some((cp) => stopName.includes('podg.') &&
cp.checkpointName.toLowerCase().includes(stop.stopNameRAW.toLowerCase()) stopName.split(', podg.')[0] &&
) stationName.includes(stopName.split(', podg.')[0])
) )
return true; return true;
return false; if (
}); stationGeneralInfo &&
stationGeneralInfo.checkpoints &&
if (stopInfoIndex == -1) return acc; stationGeneralInfo.checkpoints.length > 0 &&
stationGeneralInfo.checkpoints.some((cp) =>
const scheduledStopTrain = getScheduledTrain(train, stopInfoIndex, stationAPIData.stationName); cp.checkpointName.toLowerCase().includes(stop.stopNameRAW.toLowerCase())
)
if (stationGeneralInfo?.checkpoints) { )
for (const checkpoint of stationGeneralInfo.checkpoints) { return true;
const index = timetable.followingStops.findIndex(
(stop) => stop.stopNameRAW.toLowerCase() == checkpoint.checkpointName.toLowerCase() return false;
); });
if (index == -1) continue; if (stopInfoIndex == -1) return acc;
const scheduledCheckpointTrain = getScheduledTrain(train, index, stationAPIData.stationName); const scheduledStopTrain = getScheduledTrain(train, stopInfoIndex, stationAPIData.stationName);
checkpoint.scheduledTrains.push(scheduledCheckpointTrain);
} if (stationGeneralInfo?.checkpoints) {
} for (const checkpoint of stationGeneralInfo.checkpoints) {
const index = timetable.followingStops.findIndex(
acc.push(scheduledStopTrain); (stop) => stop.stopNameRAW.toLowerCase() == checkpoint.checkpointName.toLowerCase()
return acc; );
}, []) as ScheduledTrain[];
}, if (index == -1) continue;
getStationTrains(stationAPIData: StationAPIData) { const scheduledCheckpointTrain = getScheduledTrain(train, index, stationAPIData.stationName);
return this.trainList checkpoint.scheduledTrains.push(scheduledCheckpointTrain);
.filter( }
(train) => }
train?.region === this.region.id && train.online && train.currentStationName === stationAPIData.stationName
) acc.push(scheduledStopTrain);
.map((train) => ({ return acc;
driverName: train.driverName, }, []) as ScheduledTrain[];
driverId: train.driverId, },
trainNo: train.trainNo,
trainId: train.trainId, getStationTrains(stationAPIData: StationAPIData) {
})); return this.trainList
}, .filter(
(train) =>
setStationsOnlineInfo() { train?.region === this.region.id && train.online && train.currentStationName === stationAPIData.stationName
const onlineStationNames: string[] = []; )
const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = []; .map((train) => ({
driverName: train.driverName,
this.apiData.stations?.forEach((stationAPIData) => { driverId: train.driverId,
if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return; trainNo: train.trainNo,
const station = this.stationList.find((s) => s.name === stationAPIData.stationName); trainId: train.trainId,
}));
onlineStationNames.push(stationAPIData.stationName); },
const dispatcherStatus = this.getDispatcherStatus(stationAPIData); setStationsOnlineInfo() {
prevDispatcherStatuses.push(dispatcherStatus); const onlineStationNames: string[] = [];
const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = [];
const stationTrains = this.getStationTrains(stationAPIData);
const scheduledTrains = this.getScheduledTrains(station?.generalInfo, stationAPIData); if (this.isOffline) {
this.stationList.forEach((station) => {
const onlineInfo = { station.onlineInfo = undefined;
name: stationAPIData.stationName, });
hash: stationAPIData.stationHash,
region: stationAPIData.region, return;
maxUsers: stationAPIData.maxUsers, }
currentUsers: stationAPIData.currentUsers,
spawns: parseSpawns(stationAPIData.spawnString), this.apiData.stations?.forEach((stationAPIData) => {
dispatcherName: stationAPIData.dispatcherName, if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return;
dispatcherRate: stationAPIData.dispatcherRate, const station = this.stationList.find((s) => s.name === stationAPIData.stationName);
dispatcherId: stationAPIData.dispatcherId,
dispatcherExp: stationAPIData.dispatcherExp, onlineStationNames.push(stationAPIData.stationName);
dispatcherIsSupporter: stationAPIData.dispatcherIsSupporter,
stationTrains, const dispatcherStatus = this.getDispatcherStatus(stationAPIData);
statusTimestamp: dispatcherStatus.statusTimestamp, prevDispatcherStatuses.push(dispatcherStatus);
statusID: dispatcherStatus.statusID,
scheduledTrains, const stationTrains = this.getStationTrains(stationAPIData);
}; const scheduledTrains = this.getScheduledTrains(station?.generalInfo, stationAPIData);
if (!station) { const onlineInfo = {
this.stationList.push({ name: stationAPIData.stationName,
name: stationAPIData.stationName, hash: stationAPIData.stationHash,
onlineInfo, region: stationAPIData.region,
}); maxUsers: stationAPIData.maxUsers,
currentUsers: stationAPIData.currentUsers,
return; spawns: parseSpawns(stationAPIData.spawnString),
} dispatcherName: stationAPIData.dispatcherName,
dispatcherRate: stationAPIData.dispatcherRate,
station.onlineInfo = { ...onlineInfo }; dispatcherId: stationAPIData.dispatcherId,
dispatcherExp: stationAPIData.dispatcherExp,
this.stationList dispatcherIsSupporter: stationAPIData.dispatcherIsSupporter,
.filter((station) => !onlineStationNames.includes(station.name) && station.onlineInfo) stationTrains,
.forEach((offlineStation) => { statusTimestamp: dispatcherStatus.statusTimestamp,
offlineStation.onlineInfo = undefined; statusID: dispatcherStatus.statusID,
}); scheduledTrains,
}); };
if (this.apiData.dispatchers != null) this.lastDispatcherStatuses = prevDispatcherStatuses; if (!station) {
}, this.stationList.push({
name: stationAPIData.stationName,
async fetchStationsGeneralInfo() { onlineInfo,
const sceneryData: StationJSONData[] = await ( });
await axios.get(`${URLs.stacjownikAPI}/api/getSceneries?timestamp=${Math.floor(Date.now() / 1800000)}`)
).data; return;
}
if (!sceneryData) {
this.dataStatuses.sceneries = DataStatus.Error; station.onlineInfo = { ...onlineInfo };
return;
} this.stationList
.filter((station) => !onlineStationNames.includes(station.name) && station.onlineInfo)
this.stationList = sceneryData.map((scenery) => ({ .forEach((offlineStation) => {
name: scenery.name, offlineStation.onlineInfo = undefined;
});
generalInfo: { });
...scenery,
authors: scenery.authors?.split(',').map((a) => a.trim()), if (this.apiData.dispatchers != null) this.lastDispatcherStatuses = prevDispatcherStatuses;
routes: },
scenery.routes
?.split(';') async fetchStationsGeneralInfo() {
.filter((routeString) => routeString) const sceneryData: StationJSONData[] = await (
.reduce( await axios.get(`${URLs.stacjownikAPI}/api/getSceneries?timestamp=${Math.floor(Date.now() / 1800000)}`)
(acc, routeString) => { ).data;
const specs1 = routeString.split('_')[0];
const isInternal = specs1.startsWith('!'); if (!sceneryData) {
const name = isInternal ? specs1.replace('!', '') : specs1; this.dataStatuses.sceneries = DataStatus.Error;
return;
const specs2 = routeString.split('_')[1].split(''); }
const twoWay = specs2[0] == '2';
const catenary = specs2[1] == 'E'; this.stationList = sceneryData.map((scenery) => {
const SBL = specs2[2] == 'S'; return {
const TWB = specs2[3] ? true : false; name: scenery.name,
const propName = twoWay generalInfo: {
? catenary ...scenery,
? 'twoWayCatenaryRouteNames' authors: scenery.authors?.split(',').map((a) => a.trim()),
: 'twoWayNoCatenaryRouteNames' routes:
: catenary scenery.routes
? 'oneWayCatenaryRouteNames' ?.split(';')
: 'oneWayNoCatenaryRouteNames'; .filter((routeString) => routeString)
.reduce(
acc[twoWay ? 'twoWay' : 'oneWay'].push({ (acc, routeString) => {
name, const specs1 = routeString.split('_')[0];
SBL, const isInternal = specs1.startsWith('!');
TWB, const name = isInternal ? specs1.replace('!', '') : specs1;
catenary,
isInternal, const specs2 = routeString.split('_')[1].split('');
tracks: twoWay ? 2 : 1, const twoWay = specs2[0] == '2';
}); const catenary = specs2[1] == 'E';
if (!isInternal) acc[propName].push(name); const SBL = specs2[2] == 'S';
const TWB = specs2[3] ? true : false;
if (SBL) acc['sblRouteNames'].push(name); const speed = Number(routeString.split(':')[1]) || 0;
const length = Number(routeString.split(':')[2]) || 0;
return acc;
}, const propName = twoWay
{ ? catenary
oneWay: [], ? 'twoWayCatenaryRouteNames'
twoWay: [], : 'twoWayNoCatenaryRouteNames'
sblRouteNames: [], : catenary
oneWayCatenaryRouteNames: [], ? 'oneWayCatenaryRouteNames'
oneWayNoCatenaryRouteNames: [], : 'oneWayNoCatenaryRouteNames';
twoWayCatenaryRouteNames: [],
twoWayNoCatenaryRouteNames: [], acc[twoWay ? 'twoWay' : 'oneWay'].push({
} as StationRoutes name,
) || {}, SBL,
checkpoints: scenery.checkpoints TWB,
? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] })) catenary,
: [], isInternal,
}, tracks: twoWay ? 2 : 1,
})); length,
}, speed,
});
connectToWebsocket() { if (!isInternal) acc[propName].push(name);
const socket = io(URLs.stacjownikAPI, {
transports: ['websocket', 'polling'], if (SBL) acc['sblRouteNames'].push(name);
rememberUpgrade: true,
reconnection: true, return acc;
timeout: 10000, },
}); {
oneWay: [],
socket.on('connect_error', (err) => { twoWay: [],
this.dataStatuses.connection = DataStatus.Error; sblRouteNames: [],
this.webSocket = undefined; oneWayCatenaryRouteNames: [],
}); oneWayNoCatenaryRouteNames: [],
twoWayCatenaryRouteNames: [],
socket.on('UPDATE', (data: APIData) => { twoWayNoCatenaryRouteNames: [],
this.apiData = data; } as StationRoutes
this.dataStatuses.connection = DataStatus.Loaded; ) || {},
this.setOnlineData(); checkpoints: scenery.checkpoints
}); ? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] }))
: [],
socket.emit('FETCH_DATA', {}, (data: APIData) => { },
this.apiData = data; };
this.setOnlineData(); });
}); },
this.webSocket = socket; connectToWebsocket() {
}, const socket = io(URLs.stacjownikAPI, {
// transports: ['websocket', 'polling'],
async connectToAPI() { rememberUpgrade: true,
await this.fetchStationsGeneralInfo(); reconnection: true,
});
this.connectToWebsocket();
}, socket.on('connect_error', (err) => {
this.dataStatuses.connection = DataStatus.Error;
async changeRegion(region: StoreState['region']) { });
this.region = region;
socket.on('UPDATE', (data: APIData) => {
await this.setOnlineData(); this.apiData = data;
}, this.dataStatuses.connection = DataStatus.Loaded;
this.setOnlineData();
async setOnlineData() { });
if (!this.apiData.stations) {
this.dataStatuses.sceneries = DataStatus.Error; socket.emit('FETCH_DATA', {}, (data: APIData) => {
this.dataStatuses.trains = DataStatus.Error; this.dataStatuses.connection = DataStatus.Loaded;
this.dataStatuses.dispatchers = DataStatus.Error;
this.apiData = data;
return; this.setOnlineData();
} });
this.dataStatuses.sceneries = DataStatus.Loaded; this.webSocket = socket;
this.dataStatuses.trains = !this.apiData.trains ? DataStatus.Warning : DataStatus.Loaded; },
this.dataStatuses.dispatchers = !this.apiData.dispatchers ? DataStatus.Warning : DataStatus.Loaded;
async connectToAPI() {
this.setTrainsOnlineData(); await this.fetchStationsGeneralInfo();
this.setStationsOnlineInfo();
}, this.connectToWebsocket();
}, },
});
async changeRegion(region: StoreState['region']) {
this.region = region;
await this.setOnlineData();
},
async setOnlineData() {
if (!this.apiData.stations) {
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();
},
},
});
+78 -71
View File
@@ -1,71 +1,78 @@
import { Socket } from 'socket.io-client';
import { Socket } from 'socket.io-client'; import { DataStatus } from '../scripts/enums/DataStatus';
import { DataStatus } from '../scripts/enums/DataStatus'; import StationAPIData from '../scripts/interfaces/api/StationAPIData';
import { DispatcherStatsAPIData } from '../scripts/interfaces/api/DispatcherStatsAPIData'; import TrainAPIData from '../scripts/interfaces/api/TrainAPIData';
import { DriverStatsAPIData } from '../scripts/interfaces/api/DriverStatsAPIData'; import Station from '../scripts/interfaces/Station';
import StationAPIData from '../scripts/interfaces/api/StationAPIData'; import Train from '../scripts/interfaces/Train';
import TrainAPIData from '../scripts/interfaces/api/TrainAPIData'; import { DispatcherStatsAPIData } from '../scripts/interfaces/api/DispatcherStatsAPIData';
import Station from '../scripts/interfaces/Station'; import { DriverStatsAPIData } from '../scripts/interfaces/api/DriverStatsAPIData';
import Train from '../scripts/interfaces/Train';
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; isOffline: boolean;
dispatcherStatsName: string; dispatcherStatsName: string;
dispatcherStatsData?: DispatcherStatsAPIData; dispatcherStatsData?: DispatcherStatsAPIData;
driverStatsName: string; driverStatsName: string;
driverStatsData?: DriverStatsAPIData; driverStatsData?: DriverStatsAPIData;
driverStatsStatus: DataStatus;
chosenModalTrainId?: string;
chosenModalTrainId?: string;
dataStatuses: {
connection: DataStatus; currentStatsTab: 'daily' | 'driver';
sceneries: DataStatus;
timetables: DataStatus; dataStatuses: {
dispatchers: DataStatus; connection: DataStatus;
trains: DataStatus; sceneries: DataStatus;
}; timetables: DataStatus;
dispatchers: DataStatus;
listenerLaunched: boolean; trains: DataStatus;
} };
export interface APIData { listenerLaunched: boolean;
stations?: StationAPIData[]; blockScroll: boolean;
dispatchers?: string[][]; }
trains?: TrainAPIData[];
} export interface APIData {
stations?: StationAPIData[];
export interface StationJSONData { dispatchers?: string[][];
name: string; trains?: TrainAPIData[];
url: string; connectedSocketCount: number;
lines: string; }
project: string;
export interface StationJSONData {
reqLevel: number; name: string;
url: string;
signalType: string; lines: string;
controlType: string; project: string;
projectUrl: string;
SUP: boolean;
reqLevel: number;
routes: string;
checkpoints: string | null; signalType: string;
authors?: string; controlType: string;
availability: Availability; SUP: boolean;
}
routes: string;
checkpoints: string | null;
authors?: string;
availability: Availability;
}
+80 -65
View File
@@ -1,65 +1,80 @@
@import 'responsive.scss'; @import 'responsive.scss';
@import 'animations.scss';
// Animations //Styles
.warning {
&-enter-from, .list_wrapper {
&-leave-to { overflow-y: auto;
opacity: 0; height: 90vh;
} min-height: 550px;
&-enter-active { padding-right: 0.2em;
transition: all 150ms ease-out; }
}
.journal-list {
&-leave-active { position: relative;
transition: all 150ms ease-out; }
}
} .journal_wrapper {
max-width: 1350px;
//Styles width: 100%;
.journal-wrapper { margin: 0 auto;
width: 1350px;
padding: 1em 0; padding: 1em 0;
} }
.journal_warning { .journal_warning {
text-align: center; text-align: center;
font-size: 1.3em; font-size: 1.3em;
&.error { &.error {
background-color: var(--clr-error); background-color: var(--clr-error);
} }
} }
.schedule-dates > * { .schedule-dates > * {
margin-right: 0.25em; margin-right: 0.25em;
} }
.journal_item, .journal_item,
.journal_warning { .journal_warning {
background: #202020; background-color: #1a1a1a;
padding: 1em; padding: 1em;
margin: 1em 0; margin-bottom: 1em;
} }
.journal_top-bar { .journal_top-bar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} gap: 0.5em;
button.btn { position: relative;
padding: 0.5em 0.7em;
} margin-bottom: 0.5em;
}
@include smallScreen() {
.journal-wrapper { .btn--load-data {
font-size: 1.25em; padding: 0.5em 1em;
} display: flex;
margin: 0 auto;
.journal_top-bar {
justify-content: center; font-size: 1.2em;
flex-wrap: wrap; }
}
} @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;
}
}
+50
View File
@@ -0,0 +1,50 @@
@import 'variables.scss';
@import 'responsive.scss';
.stats-tab {
background-color: #1a1a1a;
box-shadow: 0 0 5px 1px $accentCol;
padding: 1em;
display: flex;
align-items: flex-end;
margin-bottom: 0.5em;
width: 100%;
}
.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;
}
}
+31
View File
@@ -0,0 +1,31 @@
.list-anim-move,
.list-anim-enter-active,
.list-anim-leave-active {
transition: all 250ms ease;
}
.list-anim-enter-from,
.list-anim-leave-to {
opacity: 0;
transform: translateY(30px);
}
.list-anim-leave-active {
position: absolute;
width: 100%;
}
.status-anim {
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-active {
transition: all 100ms ease-out;
}
&-leave-active {
transition: all 100ms ease-out;
}
}
+58 -28
View File
@@ -1,28 +1,58 @@
.badge { .badge {
font-weight: 600; font-weight: 600;
display: inline-block; display: inline-block;
padding: 0; padding: 0;
background: #585858; background: #585858;
margin: 0.25em; margin: 0.25em;
span { span {
display: inline-block; display: inline-block;
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
} }
&-none { &-none {
font-weight: 600; font-weight: 600;
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
background: firebrick; background: firebrick;
text-align: center; text-align: center;
@include smallScreen() { @include smallScreen() {
font-size: 1em; font-size: 1em;
} }
} }
} }
.level-badge {
display: flex;
justify-content: center;
align-items: center;
font-size: 0.9em;
&.driver {
border-radius: 50%;
width: 1.7em;
height: 1.7em;
}
&.dispatcher {
border-radius: 0.25em;
width: 1.6em;
height: 1.6em;
}
}
.region-badge {
padding: 0 0.5em;
border-radius: 0.5em;
font-weight: bold;
&.eu {
background-color: forestgreen;
}
}
-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;
+165
View File
@@ -0,0 +1,165 @@
@import 'responsive.scss';
@import 'variables.scss';
@import 'search_box.scss';
.filters-options {
margin-bottom: 0.5em;
}
.actions-bar {
display: flex;
gap: 0.5em;
}
.filter-button .active-indicator {
width: 7px;
height: 7px;
background-color: lightgreen;
border-radius: 50%;
margin-left: 10px;
}
h1.option-title {
position: relative;
font-size: 1.1em;
margin: 0.7em 0 0.25em 0;
&::before {
content: '';
position: absolute;
top: -4px;
width: 50%;
height: 2px;
background-color: white;
border-radius: 2px;
}
}
.options-anim {
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(10px);
}
&-enter-active,
&-leave-active {
transition: all 150ms ease;
}
}
.bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10;
}
.options_wrapper {
position: absolute;
background-color: $bgCol;
box-shadow: 0 5px 10px 2px #0f0f0f;
width: 97%;
max-width: 500px;
padding: 1em;
z-index: 100;
}
.options_sorters {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 0.25em 0.25em 0 0;
}
.options_filters {
display: flex;
flex-wrap: wrap;
margin: 0.5em 0 0 0;
}
.sort-option,
.filter-option {
margin: 0.25em 0.25em 0.25em 0;
}
.sort-option[data-selected='true'] {
color: $accentCol;
font-weight: bold;
}
.filter-option {
&#abandoned {
color: salmon;
}
&#fulfilled {
color: lightgreen;
}
&#active {
color: lightblue;
}
}
.search_content {
.search {
margin: 0.5em auto;
}
.search_actions {
display: flex;
gap: 0.5em;
margin: 1em 0;
width: 100%;
button {
width: 100%;
}
}
.search-box {
.search-exit {
position: absolute;
transform: translateY(-50%);
top: 50%;
right: 0;
}
}
}
@include smallScreen() {
h1 {
text-align: center;
&::before {
width: 75%;
left: 50%;
transform: translateX(-50%);
}
}
.options_wrapper {
font-size: 1.1em;
max-width: 100%;
}
.filter-option,
.sort-option {
margin: 0.25em 0.25em;
}
.options_filters,
.options_sorters {
justify-content: center;
}
}
+104 -73
View File
@@ -3,6 +3,7 @@
--clr-secondary: #2f2f2f; --clr-secondary: #2f2f2f;
--clr-bg: #4d4d4d; --clr-bg: #4d4d4d;
--clr-bg2: #1b1b1b;
--clr-accent: #1085b3; --clr-accent: #1085b3;
--clr-accent2: #ff3d5d; --clr-accent2: #ff3d5d;
@@ -12,6 +13,26 @@
--clr-error: #df3e3e; --clr-error: #df3e3e;
--clr-warning: #c59429; --clr-warning: #c59429;
font-size: 16px;
}
::-webkit-scrollbar {
width: 15px;
height: 15px;
background-color: transparent;
&-track {
background-color: #333;
}
&-thumb {
background-color: #666;
}
&-corner {
background-color: #333;
}
} }
html { html {
@@ -24,29 +45,16 @@ body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: 'Quicksand', sans-serif; font-family: 'Quicksand', sans-serif;
font-weight: 500;
overflow-y: scroll; overflow-y: scroll;
}
*:focus-visible { &.no-scroll {
outline: 1px solid white; overflow-y: hidden;
outline-offset: 1px; padding-right: 15px;
}
:root { @include smallScreen() {
font-size: 16px; padding: 0;
} }
::-webkit-scrollbar {
width: 0.5rem;
height: 0.5rem;
&-track {
background: #222;
}
&-thumb {
border-radius: 1rem;
background: #777;
} }
} }
@@ -74,22 +82,9 @@ body {
transition: opacity 0.3s; transition: opacity 0.3s;
padding: 0.25em; padding: 0.25em;
// @include smallScreen() {
// right: 0;
// left: 0;
// &::after {
// left: 75%;
// }
// }
} }
&:hover > .content { &:hover > .content {
// @include smallScreen() {
// display: none;
// }
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
} }
@@ -98,19 +93,18 @@ body {
button, button,
input, input,
select { select {
// font-family: "Open Sans", sans-serif;
border: none; border: none;
font-family: 'Quicksand', sans-serif; font-family: 'Quicksand', sans-serif;
font-size: 1em; font-size: 1em;
} }
input { input {
border: 1px solid white;
background: none; background: none;
color: white; color: white;
font-size: 1em; font-size: 1em;
padding: 0.15em; background-color: #333;
padding: 0.15em 0.5em;
outline: none; outline: none;
@@ -129,6 +123,14 @@ input {
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
*:focus {
outline: none;
}
*:focus-visible {
outline: 1px solid $accentCol;
}
.title { .title {
color: $accentCol; color: $accentCol;
font-weight: 600; font-weight: 600;
@@ -182,54 +184,68 @@ ul {
} }
} }
.btn { button {
background: none;
cursor: pointer; cursor: pointer;
font-size: 1em; color: white;
background: none;
&--text { display: flex;
color: white; align-items: center;
transition: color 0.3s; justify-content: center;
&:hover:not(:disabled), padding: 0.25em 0.5em;
&:focus:not(:disabled) {
color: $accentCol;
}
&.checked { transition: all 100ms ease;
color: var(--clr-primary);
font-weight: bold; &[data-disabled='true'] {
} user-select: none;
pointer-events: none;
opacity: 0.85;
} }
&--image { &[data-inactive='true'] {
color: white; opacity: 0.55;
transition: color 0.3s;
} }
}
&--option { button.btn--filled {
cursor: pointer; background-color: #1a1a1a;
border-radius: 0.25em;
color: white; &:hover {
background-color: #333; background-color: #2a2a2a;
border-radius: 0.25em;
padding: 0.25em 0.5em;
&:hover:not(:disabled) {
background-color: #3c3c3c;
}
&.checked {
color: var(--clr-primary);
font-weight: bold;
background-color: #3c3c3c;
}
} }
}
&:disabled { button.btn--action {
opacity: 0.65; background-color: #424242;
border-radius: 0.25em;
&:hover {
background-color: #555;
}
}
button.btn--option {
color: white;
background-color: #333;
&.checked {
color: var(--clr-primary);
font-weight: bold;
background-color: #3c3c3c;
}
}
button.btn--image {
font-weight: bold;
padding: 0.35em 0.75em;
img {
width: 1.5em;
margin-right: 0.5em;
vertical-align: middle;
} }
} }
@@ -274,3 +290,18 @@ ul {
transform: translateX(-50%); transform: translateX(-50%);
} }
} }
@include smallScreen {
::-webkit-scrollbar {
width: 0.5em;
height: 0.5em;
&-track {
background-color: #222;
}
&-thumb {
background-color: #777;
}
}
}
+16 -11
View File
@@ -1,18 +1,23 @@
@mixin smallScreen() { @mixin smallScreen() {
@media only screen and (max-width: 700px) { @media only screen and (max-width: 700px) {
@content; @content;
} }
} }
@mixin midScreen() { @mixin midScreen() {
@media only screen and (max-width: 1150px) { @media only screen and (max-width: 1150px) {
@content; @content;
} }
}
@mixin screenLandscape() {
@media only screen and (orientation: landscape) and (max-device-height: 450px) {
@content;
}
} }
@mixin bigScreen() { @mixin bigScreen() {
@media only screen and (min-width: 2000px) { @media only screen and (min-width: 2000px) {
@content; @content;
} }
} }
+52
View File
@@ -0,0 +1,52 @@
@import 'responsive.scss';
.search {
label {
display: block;
color: #ccc;
margin-bottom: 0.25em;
}
&-box {
position: relative;
display: flex;
border-radius: 0.5em;
min-width: 200px;
margin-right: 0.25em;
}
&-input {
border: none;
background-color: #424242;
padding: 0.35em 0.5em;
width: 100%;
}
&-exit {
background-color: #424242;
img {
vertical-align: middle;
height: 1.3em;
}
}
&-button {
width: 80%;
max-width: 300px;
}
@include smallScreen {
&-box,
&-button {
margin: 0.5em 0 0 0;
}
&-box {
width: 100%;
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
$primaryCol: #2c2c2c; $primaryCol: #2c2c2c;
$secondaryCol: #01e733; $secondaryCol: #01e733;
$bgCol: #4d4d4d; $bgCol: #1d1d1d;
$bgLigtherCol: #5b5b5b; $bgLigtherCol: #5b5b5b;
$errorCol: #ff1919; $errorCol: #ff1919;
@@ -0,0 +1,8 @@
export type JournalDispatcherSearcher = {
[key in 'search-dispatcher' | 'search-station' | 'search-date']: string;
};
export interface JournalDispatcherSorter {
id: 'timestampFrom' | 'duration';
dir: -1 | 1;
}
@@ -0,0 +1,18 @@
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
export type JournalTimetableSearchKey = 'search-driver' | 'search-train' | 'search-date' | 'search-dispatcher';
export type JournalTimetableSearchType = {
[key in JournalTimetableSearchKey]: string;
};
export interface JournalTimetableFilter {
id: JournalFilterType;
filterSection: string;
isActive: boolean;
}
export interface JournalTimetableSorter {
id: 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
dir: -1 | 1;
}
+6
View File
@@ -0,0 +1,6 @@
import { TrainFilterType } from "../../scripts/enums/TrainFilterType";
export interface TrainFilter {
id: TrainFilterType;
isActive: boolean;
}
+289
View File
@@ -0,0 +1,289 @@
<template>
<section class="journal-timetables">
<JournalHeader />
<div class="journal_wrapper">
<JournalOptions
@on-search-confirm="fetchHistoryData"
@on-options-reset="resetOptions"
@on-refresh-data="fetchHistoryData"
:sorter-option-ids="['timestampFrom', 'duration']"
:data-status="dataStatus"
:current-options-active="currentOptionsActive"
/>
<div class="list_wrapper" @scroll="handleScroll">
<transition name="status-anim" mode="out-in">
<div :key="dataStatus">
<div class="journal_warning" v-if="store.isOffline">
{{ $t('app.offline') }}
</div>
<Loading v-else-if="dataStatus == DataStatus.Loading" />
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div class="journal_warning" v-else-if="historyList.length == 0">
{{ $t('app.no-result') }}
</div>
<div v-else>
<JournalDispatchersList :dispatcherHistory="computedHistoryList" />
<button
class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded && computedHistoryList.length > 15"
@click="addHistoryData"
>
{{ $t('journal.load-data') }}
</button>
</div>
</div>
</transition>
<div class="journal_warning" v-if="scrollNoMoreData">
{{ $t('journal.no-further-data') }}
</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">
{{ $t('journal.loading-further-data') }}
</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios';
import ActionButton from '../components/Global/ActionButton.vue';
import JournalOptions from '../components/JournalView/JournalOptions.vue';
import DispatcherStats from '../components/JournalView/DispatcherStats.vue';
import SearchBox from '../components/Global/SearchBox.vue';
import Loading from '../components/Global/Loading.vue';
import { URLs } from '../scripts/utils/apiURLs';
import { DataStatus } from '../scripts/enums/DataStatus';
import { useStore } from '../store/store';
import JournalDispatchersList from '../components/JournalView/JournalDispatchersList.vue';
import { JournalDispatcherSearcher, JournalDispatcherSorter } from '../types/Journal/JournalDispatcherTypes';
import { DispatcherHistory } from '../scripts/interfaces/api/DispatchersAPIData';
import JournalHeader from '../components/JournalView/JournalHeader.vue';
import { LocationQuery } from 'vue-router';
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
export default defineComponent({
components: {
SearchBox,
ActionButton,
JournalOptions,
DispatcherStats,
Loading,
JournalDispatchersList,
JournalHeader,
},
name: 'JournalDispatchers',
props: {
sceneryName: {
type: String,
required: false,
},
dispatcherName: {
type: String,
required: false,
},
},
data: () => ({
currentQuery: '',
currentQueryArray: [] as string[],
scrollDataLoaded: true,
scrollNoMoreData: false,
showReturnButton: false,
statsCardOpen: false,
currentOptionsActive: false,
dataStatus: DataStatus.Loading,
DataStatus,
historyList: [] as DispatcherHistory[],
}),
setup() {
const sorterActive: JournalDispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
const journalFilterActive = ref({});
const searchersValues = reactive({
'search-dispatcher': '',
'search-station': '',
'search-date': '',
} as JournalDispatcherSearcher);
const countFromIndex = ref(0);
const countLimit = 15;
provide('sorterActive', sorterActive);
provide('journalFilterActive', journalFilterActive);
provide('searchersValues', searchersValues);
const scrollElement: Ref<HTMLElement | null> = ref(null);
return {
store: useStore(),
sorterActive,
searchersValues,
countFromIndex,
countLimit,
scrollElement,
maxCount: ref(15),
};
},
watch: {
currentQueryArray(q: string[]) {
this.currentOptionsActive =
q.length > 2 || q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1] != 'timestampFrom');
},
},
computed: {
computedHistoryList() {
return this.historyList.filter(
(doc) => doc.isOnline || (doc.currentDuration && doc.currentDuration > 10 * 60000)
);
},
},
beforeRouteUpdate(to, _) {
this.handleQueries(to.query);
this.fetchHistoryData();
},
activated() {
this.handleQueries(this.$route.query);
this.fetchHistoryData();
},
methods: {
handleScroll(e: Event) {
const listElement = e.target as HTMLElement;
const scrollTop = listElement.scrollTop;
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
},
handleQueries(query: LocationQuery) {
if ('sceneryName' in query) this.searchersValues['search-station'] = `${query.sceneryName}`;
if ('dispatcherName' in query) this.searchersValues['search-dispatcher'] = `${query.dispatcherName}`;
},
setSearchers(date: string, station: string, dispatcher: string) {
this.searchersValues['search-date'] = date;
this.searchersValues['search-station'] = station;
this.searchersValues['search-dispatcher'] = dispatcher;
},
resetOptions() {
this.setSearchers('', '', '');
this.sorterActive.id = 'timestampFrom';
this.fetchHistoryData();
},
async addHistoryData() {
this.scrollDataLoaded = false;
const countFrom = this.historyList.length;
const responseData: DispatcherHistory[] = 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() {
const queries: string[] = [];
const dispatcher = this.searchersValues['search-dispatcher'].trim();
const station = this.searchersValues['search-station'].trim();
const dateString = this.searchersValues['search-date'].trim();
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
if (station) queries.push(`stationName=${station}`);
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
// 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=30');
if (this.currentQuery != queries.join('&')) this.dataStatus = DataStatus.Loading;
this.currentQuery = queries.join('&');
this.currentQueryArray = queries;
try {
const responseData: DispatcherHistory[] = await (
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`)
).data;
if (!responseData) {
this.dataStatus = DataStatus.Error;
return;
}
if (!responseData) return;
// Response data exists
this.historyList = responseData;
// Stats display
this.store.dispatcherStatsName =
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
? this.historyList[0].dispatcherName
: '';
this.dataStatus = DataStatus.Loaded;
} catch (error) {
this.dataStatus = DataStatus.Error;
}
this.scrollNoMoreData = false;
this.scrollDataLoaded = true;
},
},
});
</script>
<style lang="scss" scoped>
@import '../styles/JournalSection.scss';
</style>
+304
View File
@@ -0,0 +1,304 @@
<template>
<section class="journal-timetables">
<JournalHeader />
<div class="journal_wrapper">
<JournalOptions
@on-search-confirm="fetchHistoryData"
@on-options-reset="resetOptions"
@on-refresh-data="fetchHistoryData"
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']"
:filters="journalTimetableFilters"
:currentOptionsActive="currentOptionsActive"
:data-status="dataStatus"
/>
<JournalStats />
<div class="list_wrapper" @scroll="handleScroll">
<transition name="status-anim" mode="out-in">
<div :key="dataStatus">
<div class="journal_warning" v-if="store.isOffline">
{{ $t('app.offline') }}
</div>
<Loading v-else-if="dataStatus == DataStatus.Loading" />
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div v-else-if="timetableHistory.length == 0" class="journal_warning">
{{ $t('app.no-result') }}
</div>
<div v-else>
<JournalTimetablesList :timetableHistory="timetableHistory" />
<button
class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
@click="addHistoryData"
>
{{ $t('journal.load-data') }}
</button>
</div>
</div>
</transition>
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios';
import DriverStats from '../components/JournalView/JournalDriverStats.vue';
import Loading from '../components/Global/Loading.vue';
import { JournalTimetableSorter } from '../types/Journal/JournalTimetablesTypes';
import dateMixin from '../mixins/dateMixin';
import routerMixin from '../mixins/routerMixin';
import { DataStatus } from '../scripts/enums/DataStatus';
import { JournalFilterType } from '../scripts/enums/JournalFilterType';
import { TimetableHistory } from '../scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '../scripts/utils/apiURLs';
import { useStore } from '../store/store';
import JournalOptions from '../components/JournalView/JournalOptions.vue';
import { JournalTimetableSearchType } from '../types/Journal/JournalTimetablesTypes';
import modalTrainMixin from '../mixins/modalTrainMixin';
import imageMixin from '../mixins/imageMixin';
import JournalTimetablesList from '../components/JournalView/JournalTimetablesList.vue';
import { journalTimetableFilters } from '../constants/Journal/JournalTimetablesConsts';
import JournalStats from '../components/JournalView/JournalStats.vue';
import JournalHeader from '../components/JournalView/JournalHeader.vue';
import { LocationQuery } from 'vue-router';
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
export default defineComponent({
components: { DriverStats, Loading, JournalOptions, JournalTimetablesList, JournalStats, JournalHeader },
mixins: [dateMixin, routerMixin, modalTrainMixin, imageMixin],
name: 'JournalTimetables',
props: {
timetableId: {
type: String,
},
},
data: () => ({
currentQuery: '',
currentQueryArray: [] as string[],
scrollDataLoaded: true,
scrollNoMoreData: false,
showReturnButton: false,
statsCardOpen: false,
currentOptionsActive: false,
timetableHistory: [] as TimetableHistory[],
journalTimetableFilters,
dataStatus: DataStatus.Loading,
dataErrorMessage: '',
DataStatus,
}),
setup() {
const sorterActive: JournalTimetableSorter = reactive({ id: 'timetableId', dir: 1 });
const journalFilterActive = ref(journalTimetableFilters[0]);
const searchersValues = reactive({
'search-train': '',
'search-driver': '',
'search-dispatcher': '',
'search-date': '',
} as JournalTimetableSearchType);
const countFromIndex = ref(0);
const countLimit = 15;
provide('searchersValues', searchersValues);
provide('sorterActive', sorterActive);
provide('journalFilterActive', journalFilterActive);
const scrollElement: Ref<HTMLElement | null> = ref(null);
return {
sorterActive,
journalFilterActive,
searchersValues,
countFromIndex,
countLimit,
scrollElement,
store: useStore(),
};
},
watch: {
currentQueryArray(q: string[]) {
this.currentOptionsActive = q.length >= 2 || q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1]);
},
},
// Handle route updates for route-links
beforeRouteUpdate(to, _) {
this.handleQueries(to.query);
this.fetchHistoryData();
},
activated() {
this.handleQueries(this.$route.query);
this.fetchHistoryData();
},
methods: {
handleScroll(e: Event) {
const listElement = e.target as HTMLElement;
const scrollTop = listElement.scrollTop;
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
},
handleQueries(query: LocationQuery) {
if ('timetableId' in query) this.searchersValues['search-train'] = `#${query.timetableId}`;
},
setSearchers(date: string, driver: string, train: string, dispatcher: string) {
this.searchersValues['search-date'] = date;
this.searchersValues['search-driver'] = driver;
this.searchersValues['search-train'] = train;
this.searchersValues['search-dispatcher'] = dispatcher;
},
resetOptions() {
this.setSearchers('', '', '', '');
this.journalFilterActive = this.journalTimetableFilters[0];
this.sorterActive.id = 'timetableId';
this.fetchHistoryData();
},
async addHistoryData() {
this.scrollDataLoaded = false;
const countFrom = this.timetableHistory.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.timetableHistory.push(...responseData);
this.scrollDataLoaded = true;
},
async fetchHistoryData() {
// if(this.dataStatus == DataStatus.Loading) return;
const queries: string[] = [];
const driverName = this.searchersValues['search-driver'].trim();
const trainNo = this.searchersValues['search-train'].trim();
const authorName = this.searchersValues['search-dispatcher'].trim();
const dateString = this.searchersValues['search-date'].trim();
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
if (driverName) queries.push(`driverName=${driverName}`);
if (trainNo)
queries.push(trainNo.startsWith('#') ? `timetableId=${trainNo.replace('#', '')}` : `trainNo=${trainNo}`);
if (authorName) queries.push(`authorName=${authorName}`);
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
// 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 (this.journalFilterActive.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;
}
if (this.currentQuery != queries.join('&')) this.dataStatus = DataStatus.Loading;
this.currentQuery = queries.join('&');
this.currentQueryArray = queries;
try {
const responseData: TimetableHistory[] = await (
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}`)
).data;
if (!responseData) {
this.dataStatus = DataStatus.Error;
this.dataErrorMessage = 'Brak danych!';
return;
}
if (!responseData) return;
// Response data exists
this.timetableHistory = responseData;
// Stats display
this.store.driverStatsName =
this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim()
? this.timetableHistory[0].driverName
: '';
this.dataStatus = DataStatus.Loaded;
} catch (error) {
this.dataStatus = DataStatus.Error;
this.dataErrorMessage = 'Ups! Coś poszło nie tak!';
}
this.scrollNoMoreData = false;
this.scrollDataLoaded = true;
},
},
});
</script>
<style lang="scss" scoped>
@import '../styles/JournalSection.scss';
</style>
-77
View File
@@ -1,77 +0,0 @@
<template>
<section class="journal-view">
<div class="journal-type-options">
<router-link class="router-link" active-class="route-active" to="/journal/timetables" exact>
{{ $t('journal.section-timetables') }}
</router-link>
&nbsp;&bull;&nbsp;
<router-link class="router-link" active-class="route-active" to="/journal/dispatchers">
{{ $t('journal.section-dispatchers') }}
</router-link>
</div>
<div class="journal-section">
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" :key="$route.path" />
</keep-alive>
</router-view>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import JournalDispatchers from '../components/JournalView/JournalDispatchers.vue';
import JournalTimetables from '../components/JournalView/JournalTimetables.vue';
export default defineComponent({
components: { JournalDispatchers, JournalTimetables },
setup() {
const journalTypeChosen = ref('journalTimetables');
return {
activeJournalComponent: journalTypeChosen,
};
},
methods: {
changeJournalType(type: string) {
this.activeJournalComponent = type;
},
},
activated() {
const query = this.$route.query;
if (query.view == 'dispatchers') this.activeJournalComponent = 'journalDispatchers';
},
});
</script>
<style lang="scss" scoped>
.journal-type-options {
display: flex;
justify-content: center;
background-color: #2c2c2c;
max-width: 18em;
font-size: 1.2em;
margin: 0 auto;
border-radius: 0 0 0.5em 0.5em;
padding: 0.1em 0;
}
.journal-section > section {
height: 100%;
display: flex;
justify-content: center;
}
.router-link.active {
color: gold;
}
</style>
+28 -10
View File
@@ -8,16 +8,16 @@
</action-button> </action-button>
</div> </div>
<div class="scenery-wrapper" v-if="stationInfo" ref="card-wrapper"> <div class="scenery-wrapper" v-if="stationInfo" ref="card-wrapper" :data-timetable-only="timetableOnly">
<div class="scenery-left"> <div class="scenery-left" v-if="!timetableOnly">
<div class="scenery-actions"> <div class="scenery-actions">
<button v-if="!timetableOnly" class="back-btn btn" :title="$t('scenery.return-btn')" @click="navigateTo('/')"> <button class="back-btn btn" :title="$t('scenery.return-btn')" @click="navigateTo('/')">
<img :src="getIcon('back')" alt="Back to scenery" /> <img :src="getIcon('back')" alt="Back to scenery" />
</button> </button>
</div> </div>
<SceneryHeader :station="stationInfo" /> <SceneryHeader :station="stationInfo" />
<SceneryInfo :station="stationInfo" :timetableOnly="timetableOnly" /> <SceneryInfo :station="stationInfo" />
</div> </div>
<div class="scenery-right"> <div class="scenery-right">
@@ -33,7 +33,12 @@
</div> </div>
<keep-alive> <keep-alive>
<component :is="currentViewCompontent" :station="stationInfo" :key="currentViewCompontent"></component> <component
:is="currentViewCompontent"
:station="stationInfo"
:timetableOnly="timetableOnly"
:key="currentViewCompontent"
></component>
</keep-alive> </keep-alive>
</div> </div>
</div> </div>
@@ -41,7 +46,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from 'vue'; import { computed, defineComponent, PropType } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import routerMixin from '../mixins/routerMixin'; import routerMixin from '../mixins/routerMixin';
import { useStore } from '../store/store'; import { useStore } from '../store/store';
@@ -68,7 +73,9 @@ export default defineComponent({
SceneryTimetablesHistory, SceneryTimetablesHistory,
SceneryDispatchersHistory, SceneryDispatchersHistory,
}, },
mixins: [routerMixin, imageMixin], mixins: [routerMixin, imageMixin],
data: () => ({ data: () => ({
viewModes: [ viewModes: [
{ {
@@ -89,17 +96,22 @@ export default defineComponent({
currentViewCompontent: 'SceneryTimetable', currentViewCompontent: 'SceneryTimetable',
onlineFrom: -1, onlineFrom: -1,
}), }),
activated() { activated() {
this.loadSelectedCheckpoint(); this.loadSelectedCheckpoint();
}, },
setup() { setup() {
const route = useRoute(); const route = useRoute();
const store = useStore(); const store = useStore();
const timetableOnly = computed(() => (route.query['timetable_only'] == '1' ? true : false));
const timetableOnly = computed(() => (route.query['timetableOnly'] == '1' ? true : false));
const isComponentVisible = computed(() => route.path === '/scenery'); const isComponentVisible = computed(() => route.path === '/scenery');
const stationInfo = computed(() => { const stationInfo = computed(() => {
return store.stationList.find((station) => station.name === route.query.station?.toString().replace(/_/g, ' ')); return store.stationList.find((station) => station.name === route.query.station?.toString().replace(/_/g, ' '));
}); });
return { return {
timetableOnly, timetableOnly,
isComponentVisible, isComponentVisible,
@@ -111,11 +123,13 @@ export default defineComponent({
setViewMode(componentName: string) { setViewMode(componentName: string) {
this.currentViewCompontent = componentName; this.currentViewCompontent = componentName;
}, },
loadSelectedCheckpoint() { loadSelectedCheckpoint() {
if (!this.stationInfo?.generalInfo?.checkpoints) return; if (!this.stationInfo?.generalInfo?.checkpoints) return;
if (this.stationInfo.generalInfo.checkpoints.length == 0) return; if (this.stationInfo.generalInfo.checkpoints.length == 0) return;
this.selectedCheckpoint = this.stationInfo.generalInfo.checkpoints[0].checkpointName; this.selectedCheckpoint = this.stationInfo.generalInfo.checkpoints[0].checkpointName;
}, },
selectCheckpoint(cp: { checkpointName: string }) { selectCheckpoint(cp: { checkpointName: string }) {
this.selectedCheckpoint = cp.checkpointName; this.selectedCheckpoint = cp.checkpointName;
}, },
@@ -169,8 +183,12 @@ button.back-btn {
max-width: 1700px; max-width: 1700px;
margin: 1rem 0; margin: 1rem 0;
text-align: center; text-align: center;
&[data-timetable-only='true'] {
grid-template-columns: 1fr;
max-width: 1000px;
}
} }
.scenery-left { .scenery-left {
@@ -209,15 +227,15 @@ button.back-btn {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-wrap: wrap;
gap: 0.75em;
.btn { .btn {
margin: 0.5em;
padding: 0.5em; padding: 0.5em;
box-shadow: 0 0 10px 4px #242424; box-shadow: 0 0 10px 4px #242424;
&[data-checked='true'] { &[data-checked='true'] {
color: var(--clr-primary); color: var(--clr-primary);
font-weight: bold;
} }
} }
} }
+28 -72
View File
@@ -3,39 +3,23 @@
<div class="wrapper"> <div class="wrapper">
<div class="body"> <div class="body">
<div class="options-bar"> <div class="options-bar">
<StationFilterCard <StationFilterCard :showCard="filterCardOpen" :exit="(filterCardOpen = false)" ref="filterCardRef" />
:showCard="filterCardOpen"
:exit="closeCard"
@changeFilterValue="changeFilterValue"
@invertFilters="invertFilters"
@resetFilters="resetFilters"
ref="filterCardRef"
/>
</div> </div>
<StationTable <StationTable :stations="computedStationList" />
:stations="computedStations"
:sorterActive="filterManager.getSorter()"
:setFocusedStation="setFocusedStation"
:changeSorter="changeSorter"
/>
</div> </div>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue';
import inputData from '../data/options.json';
import { computed, ComputedRef, defineComponent, reactive } from 'vue';
import { useStore } from '../store/store';
import StationFilterManager from '../scripts/managers/stationFilterManager';
import Station from '../scripts/interfaces/Station';
import StorageManager from '../scripts/managers/storageManager'; import StorageManager from '../scripts/managers/storageManager';
import StationTable from '../components/StationsView/StationTable.vue'; import StationTable from '../components/StationsView/StationTable.vue';
import StationFilterCard from '../components/StationsView/StationFilterCard.vue'; import StationFilterCard from '../components/StationsView/StationFilterCard.vue';
import SelectBox from '../components/Global/SelectBox.vue'; import SelectBox from '../components/Global/SelectBox.vue';
import { useStationFiltersStore } from '../store/stationFiltersStore';
import { useStore } from '../store/store';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -43,70 +27,42 @@ export default defineComponent({
StationFilterCard, StationFilterCard,
SelectBox, SelectBox,
}, },
data: () => ({ data: () => ({
filterCardOpen: false, filterCardOpen: false,
modalHidden: true, modalHidden: true,
STORAGE_KEY: 'options_saved', STORAGE_KEY: 'options_saved',
inputs: inputData, focusedStationName: '',
}), }),
setup() { setup() {
const store = useStore();
const filterManager = reactive(new StationFilterManager());
const focusedStationName = '';
const computedStations: ComputedRef<Station[]> = computed(
() => filterManager.getFilteredStationList(store.stationList, store.region.id)
// .filter((station) => !station.onlineInfo || station.onlineInfo.region == store.region.id)
);
return { return {
computedStations, filterStore: useStationFiltersStore(),
filterManager, store: useStore(),
focusedStationName,
}; };
}, },
mounted() {
if (!StorageManager.isRegistered(this.STORAGE_KEY)) return;
this.filterManager.checkFilters(); computed: {
computedStationList() {
const list = this.filterStore.getFilteredStationList(this.store.stationList, this.store.region.id);
this.inputs.options.forEach((option) => { return list;
const value = StorageManager.getBooleanValue(option.name); },
this.changeFilterValue({ name: option.name, value: value ? 0 : 1 });
option.value = value;
});
this.inputs.sliders.forEach((slider) => {
const value = StorageManager.getNumericValue(slider.name);
this.changeFilterValue({ name: slider.name, value });
slider.value = value;
});
}, },
methods: {
toggleCardsState(name: string): void { mounted() {
if (name == 'filter') { this.filterStore.setupFilters();
this.filterCardOpen = !this.filterCardOpen; // this.filterStore.inputs.options.forEach((option) => {
} // const value = StorageManager.getBooleanValue(option.name);
}, // option.value = value;
changeSorter(index: number) { // this.filterStore.changeFilterValue({ name: option.name, value: value });
this.filterManager.changeSorter(index); // });
},
changeFilterValue(filter: { name: string; value: number }) { // this.filterStore.inputs.sliders.forEach((slider) => {
this.filterManager.changeFilterValue(filter); // const value = StorageManager.getNumericValue(slider.name);
}, // slider.value = value;
resetFilters() { // this.filterStore.changeFilterValue({ name: slider.name, value: value });
this.filterManager.resetFilters(); // });
},
invertFilters() {
this.filterManager.invertFilters();
},
closeCard() {
this.filterCardOpen = false;
},
setFocusedStation(name: string) {
this.focusedStationName = this.focusedStationName == name ? '' : name;
},
}, },
}); });
</script> </script>
+36 -27
View File
@@ -1,9 +1,10 @@
<template> <template>
<section class="trains-view"> <section class="trains-view">
<div class="wrapper"> <div class="trains_wrapper">
<div class="options-bar"> <TrainOptions
<train-options /> :sorter-option-ids="['distance', 'id', 'progress', 'delay', 'mass', 'speed', 'length']"
</div> :current-options-active="currentOptionsActive"
/>
<TrainTable :trains="computedTrains" /> <TrainTable :trains="computedTrains" />
</div> </div>
@@ -11,14 +12,16 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, ComputedRef, defineComponent, provide, reactive, ref, TrainFilter } from 'vue'; import { computed, ComputedRef, defineComponent, provide, reactive, ref, watch } from 'vue';
import TrainOptions from '../components/TrainsView/TrainOptions.vue'; import TrainOptions from '../components/TrainsView/TrainOptions.vue';
import TrainStats from '../components/TrainsView/TrainStats.vue'; import TrainStats from '../components/TrainsView/TrainStats.vue';
import TrainTable from '../components/TrainsView/TrainTable.vue'; import TrainTable from '../components/TrainsView/TrainTable.vue';
import { trainFilters } from '../data/trainOptions'; import { trainFilters } from '../constants/Trains/TrainOptionsConsts';
import modalTrainMixin from '../mixins/modalTrainMixin';
import Train from '../scripts/interfaces/Train'; import Train from '../scripts/interfaces/Train';
import { filteredTrainList } from '../scripts/managers/trainFilterManager'; import { filteredTrainList } from '../scripts/managers/trainFilterManager';
import { useStore } from '../store/store'; import { useStore } from '../store/store';
import { TrainFilter } from '../types/Trains/TrainOptionsTypes';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -27,6 +30,8 @@ export default defineComponent({
TrainOptions, TrainOptions,
}, },
mixins: [modalTrainMixin],
props: { props: {
train: { train: {
type: String, type: String,
@@ -37,6 +42,11 @@ export default defineComponent({
type: String, type: String,
required: false, required: false,
}, },
trainId: {
type: String,
required: false,
},
}, },
data: () => ({ data: () => ({
@@ -45,10 +55,12 @@ export default defineComponent({
setup() { setup() {
const store = useStore(); const store = useStore();
const initTrainFilters = [...trainFilters.map((f) => ({ ...f }))];
const sorterActive = ref({ id: 'distance', dir: -1 }); const sorterActive = reactive({ id: 'distance', dir: -1 });
const filterList = reactive([...trainFilters]) as TrainFilter[]; const filterList = reactive([...trainFilters]) as TrainFilter[];
const isTrainOptionsCardVisible = ref(false);
const currentOptionsActive = ref(false);
const searchedDriver = ref(''); const searchedDriver = ref('');
const searchedTrain = ref(''); const searchedTrain = ref('');
@@ -57,16 +69,15 @@ export default defineComponent({
provide('searchedDriver', searchedDriver); provide('searchedDriver', searchedDriver);
provide('sorterActive', sorterActive); provide('sorterActive', sorterActive);
provide('filterList', filterList); provide('filterList', filterList);
provide('isTrainOptionsCardVisible', isTrainOptionsCardVisible);
const computedTrains: ComputedRef<Train[]> = computed(() => { const computedTrains: ComputedRef<Train[]> = computed(() => {
return filteredTrainList( return filteredTrainList(store.trainList, searchedTrain.value, searchedDriver.value, sorterActive, filterList);
store.trainList, });
searchedTrain.value,
searchedDriver.value, watch([searchedTrain, searchedDriver, sorterActive, filterList], ([sT, sD, sA, fL]) => {
sorterActive.value, const areFiltersActive = fL.some((f, i) => f.isActive !== initTrainFilters[i].isActive);
filterList
); currentOptionsActive.value = sT.length > 0 || sD.length > 0 || sA.id != 'distance' || areFiltersActive;
}); });
return { return {
@@ -74,6 +85,8 @@ export default defineComponent({
searchedTrain, searchedTrain,
searchedDriver, searchedDriver,
sorterActive, sorterActive,
store,
currentOptionsActive,
}; };
}, },
@@ -82,10 +95,12 @@ export default defineComponent({
this.searchedTrain = this.train; this.searchedTrain = this.train;
this.searchedDriver = this.driver || ''; this.searchedDriver = this.driver || '';
} }
// if (this.train) {
// this.searchedTrain = this.train; this.$nextTick(() => {
// if(this.x) this.searchedDriver = this.x; if (this.trainId) {
// } this.selectModalTrain(this.trainId);
}
});
}, },
}); });
</script> </script>
@@ -98,14 +113,8 @@ export default defineComponent({
position: relative; position: relative;
} }
.wrapper { .trains_wrapper {
margin: 1rem auto; margin: 1rem auto;
max-width: 1350px; max-width: 1350px;
} }
@include smallScreen {
.options-bar {
font-size: 1.25em;
}
}
</style> </style>
-30
View File
@@ -1,30 +0,0 @@
import { ComponentCustomProperties } from 'vue'
import { Store } from 'vuex'
import { JournalFilterType } from './scripts/enums/JournalFilterType';
import { TrainFilterType } from './scripts/enums/TrainFilterType';
declare module '@vue/runtime-core' {
// declare your own store states
interface State {
count: number
}
// provide typings for `this.$store`
interface ComponentCustomProperties {
$store: Store<State>
}
// Train filter for TrainView
interface TrainFilter {
id: TrainFilterType;
isActive: boolean;
}
interface JournalFilter {
id: JournalFilterType;
filterSection: string;
isActive: boolean;
}
}
+1
View File
@@ -14,6 +14,7 @@
"ESNext", "ESNext",
"DOM" "DOM"
], ],
"types": ["vite/client", "vite-plugin-pwa/client"],
"skipLibCheck": true "skipLibCheck": true
}, },
"include": [ "include": [
+47 -27
View File
@@ -1,34 +1,54 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({ export default defineConfig({
plugins: [vue()], server: {
port: 5001,
},
plugins: [
vue(),
VitePWA({
registerType: 'prompt',
workbox: {
globPatterns: ['**/*.{js,css,html,png,svg,jpg}'],
runtimeCaching: [
{
urlPattern: new RegExp('^https://spythere.pl/api/getSceneries', 'i'),
handler: 'NetworkFirst',
options: {
cacheName: 'sceneries-cache',
expiration: {
maxEntries: 1,
maxAgeSeconds: 60 * 60 * 24 * 7, // <== 7 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: /^https:\/\/rj.td2.info.pl\/dist\/img\/thumbnails\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'images-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 60,
},
cacheableResponse: {
statuses: [0, 200, 404],
},
},
},
],
},
devOptions: {
enabled: true,
},
}),
],
}); });
// PWA
// VitePWA({
// registerType: 'autoUpdate',
// workbox: {
// globPatterns: ['**/*.{js,css,html,png,svg,img}'],
// runtimeCaching: [
// {
// urlPattern: new RegExp('^https://stacjownik.eu-4.evennode.com/api/getSceneries'),
// handler: 'NetworkFirst',
// options: {
// cacheName: 'sceneries-cache',
// expiration: {
// maxEntries: 200,
// maxAgeSeconds: 60 * 60 * 24 * 60, // <== 60 days
// },
// cacheableResponse: {
// statuses: [0, 200],
// },
// },
// },
// ],
// },
// devOptions: {
// enabled: true,
// },
// }),
+3607 -1157
View File
File diff suppressed because it is too large Load Diff