Compare commits

..

307 Commits

Author SHA1 Message Date
Spythere ea5c9e0028 Merge do wersji 1.17.0
Wersja 1.17.0
2023-09-06 15:43:52 +02:00
Spythere eb7c2d7132 revamp postoju i strzałek w RJ scenerii 2023-09-05 16:48:26 +02:00
Spythere ee7c50f59b szybkie filtry (wip) 2023-09-05 16:10:38 +02:00
Spythere 439f59fedc poprawki filtrów scenerii 2023-09-05 15:40:32 +02:00
Spythere c47d839ce3 poprawki kolorów 2023-09-04 18:50:12 +02:00
Spythere f77c13cbcf srjp: poprawki dostępności modalu 2023-09-04 18:37:52 +02:00
Spythere dbbbd33100 poprawki dzienników 2023-09-03 19:24:32 +02:00
Spythere 14d13360a8 dziennik dr: animacje 2023-09-03 18:47:01 +02:00
Spythere dc862252ba dziennik dr: kolumna regionów 2023-09-03 18:34:45 +02:00
Spythere e5fe727ccd aktualizacja URLi 2023-09-03 17:58:55 +02:00
Spythere e836bbed0c szybkie filtry (wip) 2023-09-02 19:36:49 +02:00
Spythere d4438fd215 station filters active indicator 2023-09-02 18:53:23 +02:00
Spythere 1550849360 dzienniki 2023-09-02 18:47:01 +02:00
Spythere 9d1dc4ffca bump 1.17 2023-08-31 22:46:41 +02:00
Spythere 0397fa788d zmiana wyglądu listy dzienników 2023-08-31 22:45:14 +02:00
Spythere 6e5696b0a6 daily stats hotfix 2023-08-31 21:46:05 +02:00
Spythere 4537341a57 odświeżony wygląd dziennika RJ 2023-08-30 20:31:40 +02:00
Spythere c35c74bd4a Merge pull request #52
fix: data wcześniejszego przyjazdu dla 0pt
2023-08-27 23:07:01 +02:00
Spythere 25735c5e6e fix: data wcześniejszego przyjazdu dla 0pt 2023-08-27 22:45:36 +02:00
Spythere 41e60bc69e Merge do wersji 1.16.3 2023-07-06 01:54:21 +02:00
Spythere 933bdecb3c bump: 1.16.3 2023-07-02 14:50:55 +02:00
Spythere 10e183d96b zamiana infinite scrolla na przyciski 2023-07-02 14:50:18 +02:00
Spythere 5429d39f5e tłumaczenia historii dr scenerii 2023-07-01 23:39:08 +02:00
Spythere ff31e7f903 bump: 1.16.2.1 2023-07-01 23:29:48 +02:00
Spythere 91f4c6bc57 Merge do wersji 1.16.2 2023-07-01 01:25:10 +02:00
Spythere c133eb060b bump: 1.16.2 2023-07-01 01:20:04 +02:00
Spythere 7ffc169d8a hotfix 2023-07-01 01:19:50 +02:00
Spythere 1b85cc5f58 poprawki dziennika rj / dr 2023-06-30 20:50:03 +02:00
Spythere 72ff857fff dodatkowe info o postojach w dzienniku RJ 2023-06-27 02:52:41 +02:00
Spythere 96d64e77fc feature: nieskończona lista historii dr/rj scenerii 2023-06-24 13:46:37 +02:00
Spythere 6ceae3f161 revamp tabeli historii dyżurów 2023-06-23 14:19:28 +02:00
Spythere 8e8e27658c Merge do wersji 1.16.1
Wersja 1.16.1
2023-06-22 19:27:39 +02:00
Spythere 9b6ace394a bump v.1.16.1 2023-06-21 18:36:21 +02:00
Spythere 6cfeaa91bf responsywność dailyStats 2023-06-21 18:35:23 +02:00
Spythere 08b208aeaa fix tłumaczeń 2023-06-21 18:31:16 +02:00
Spythere a089b5275b hotfix daily stats 2023-06-21 18:30:02 +02:00
Spythere 8425cd4371 przyjazdy/odjazdy stacji pośrednich RJ w dzienniku 2023-06-21 18:26:16 +02:00
Spythere dbdc517b87 fix tłumaczeń 2023-06-21 17:32:36 +02:00
Spythere e271358a27 fix timetable id 2023-06-21 17:19:31 +02:00
Spythere 66262e3fcd dodatkowe statystyki dnia 2023-06-21 17:16:02 +02:00
Spythere 5b2b6bdea2 bump 1.16.0.1 2023-06-16 01:17:33 +02:00
Spythere c8587de6d9 npm update 2023-06-15 15:28:32 +02:00
Spythere 1f376085f2 feature: info o elektryfikacji spawnu na scenerii 2023-06-15 15:28:12 +02:00
Spythere f28600a7fa Merge do wersji 1.16.0
Wersja 1.16.0
2023-06-12 01:33:45 +02:00
Spythere d59ead87e6 bump v1.16.0 2023-06-12 01:21:49 +02:00
Spythere 34d91bc800 poprawki w pokazywaniu statystyk 2023-06-12 01:19:31 +02:00
Spythere cf9991d8a0 layout filtrów dzienników 2023-06-12 00:51:17 +02:00
Spythere 4ffb79d62b poprawki pobierania danych 2023-06-11 21:47:50 +02:00
Spythere d9f5edb4fe poprawki tłumaczeń 2023-06-11 21:47:22 +02:00
Spythere 1b2112430a feature: długości szlaków po kliknięciu 2023-06-08 23:35:57 +02:00
Spythere 0a972a23ef fix: asynchroniczność pobierania danych z API 2023-06-04 13:35:53 +02:00
Spythere 6d52724d06 zapamiętywanie stanu statystyk dnia 2023-06-04 12:19:46 +02:00
Spythere 99415c35d3 rozbudowane filtry dziennika RJ 2023-06-04 12:06:15 +02:00
Spythere c3f687d439 hotfixy dzienników 2023-06-04 01:45:58 +02:00
Spythere 266edfd6e6 reorder typów danych 2023-06-04 00:33:43 +02:00
Spythere d32d5ad91b poprawki dzienników 2023-06-03 18:55:44 +02:00
Spythere c3481470cb optymalizacja zapytań; filtr scenerii pocz. 2023-06-03 15:49:15 +02:00
Spythere 57e88b9abc Merge do wersji 1.15.1
Wersja 1.15.1
2023-06-02 20:09:43 +02:00
Spythere 44ebf53798 poprawki pwa 2023-06-02 20:06:25 +02:00
Spythere 145dc72b6b pwa: zmiana na autoUpdate 2023-06-02 19:41:33 +02:00
Spythere b7f3761940 Merge do wersji 1.15.0
Wersja 1.15.0
2023-06-02 01:16:37 +02:00
Spythere ea7c49dfb3 bump: 1.15.0 2023-06-02 01:10:01 +02:00
Spythere 5d6785813a fix: odznaki TWR/SKR; dodano do dziennika 2023-06-02 01:07:44 +02:00
Spythere a0054aed14 fix: optymalizacja zapytań historii RJ scenerii 2023-06-02 00:51:03 +02:00
Spythere 471e6f5216 Wersja 1.14.3
Wersja 1.14.3
2023-05-18 02:42:27 +02:00
Spythere a617eef00e bump 1.14.3 2023-05-18 02:38:13 +02:00
Spythere 38e700ecd6 migracja z route na routeNames 2023-05-18 02:37:27 +02:00
Spythere da1be0e10a Wersja 1.14.2 2023-05-17 02:24:05 +02:00
Spythere f49bb12948 bump 1.14.2 2023-05-17 02:21:13 +02:00
Spythere 02673a3d70 poprawki filtrów scenerii 2023-05-16 14:27:31 +02:00
Spythere 4ddc7345df poprawki filtrów RJ 2023-05-16 02:40:08 +02:00
Spythere 5d822684c0 feature: rozszerzone filtry RJ 2023-05-14 15:05:51 +02:00
Spythere 69fa15c70a v1.14.1
v1.14.1
2023-04-13 19:25:30 +02:00
Spythere 9192067388 1.14.1 2023-04-13 19:21:36 +02:00
Spythere 2b41e5b857 hotfix 2023-04-13 19:21:24 +02:00
Spythere 674680ff14 1.14: hotfixy
1.14 hotfixy
2023-04-13 19:15:38 +02:00
Spythere 475bd2ff10 przeniesienie skrótów do widoku scenerii 2023-04-13 19:11:44 +02:00
Spythere 074d1eb155 Hotfix tłumaczeń 2023-04-13 18:50:00 +02:00
Spythere 378393de89 Wersja 1.14.0
Wersja 1.14.0
2023-04-13 15:45:39 +02:00
Spythere 03e61083a7 tłumaczenie stopki 2023-04-13 15:37:34 +02:00
Spythere 0b746fce8c hotfix headera 2023-04-13 15:32:41 +02:00
Spythere 5883e710be bump wersji: 1.14 2023-04-13 15:26:22 +02:00
Spythere 3d0695a17b Poprawki w headerze i stopce 2023-04-13 15:25:39 +02:00
Spythere 4adb76eeb0 feature: podpisy status indicatorów dla RJ 2023-04-11 00:33:14 +02:00
Spythere 4c41076519 feature: skrót posterunku w tabelce 2023-04-09 01:37:11 +02:00
Spythere 77f61d17fd hotfix: wygląd badge'a 2023-04-09 00:19:53 +02:00
Spythere 032a84cbcf feature: historia zmian w zestawieniu pociągów 2023-03-20 22:39:23 +01:00
Spythere de9851ebcc Wersja 1.13.0
Wersja 1.13.0
2023-03-16 01:31:18 +01:00
Spythere ff78eba927 hotfixy filtrów 2023-03-15 18:15:01 +01:00
Spythere e4c5f6a322 filtry pociągów 2023-03-15 17:56:27 +01:00
Spythere 0a78761928 hotfix tłumaczeń 2023-03-15 15:34:36 +01:00
Spythere 4843043c29 bump wersji: 1.13.0 2023-03-15 15:20:28 +01:00
Spythere 9e1df1fb61 filtry pociągów 2023-03-15 15:19:50 +01:00
Spythere 021474cfb0 fix: tłumaczenia; hotfixy 2023-03-14 23:13:37 +01:00
Spythere 7d0e68862c filtry scenerii 2023-03-14 21:42:39 +01:00
Spythere 653d45dfc6 scenerie: remake filtrów 2023-03-13 22:26:00 +01:00
Spythere 4a4e1240a4 widok scenerii: odnośniki do tablic przy aktywnych RJ 2023-03-10 16:59:16 +01:00
Spythere 14ca48a90d hotfix 2023-03-10 16:30:36 +01:00
Spythere a02f9804b1 dziennik RJ: dodatkowe info 2023-03-10 16:30:31 +01:00
Spythere c5efc6fbac bump: wersja 1.12.2 2023-03-10 02:03:37 +01:00
Spythere cacd0a1e4e fix: data stworzenia RJ 2023-03-10 01:53:19 +01:00
Spythere 50375099ab update url api 2023-03-10 01:42:10 +01:00
Spythere 6af67ec741 fix: tłumaczenia filtrów 2023-03-09 14:35:03 +01:00
Spythere c64112c86a dziennik RJ: dodatkowe informacje 2023-03-09 14:28:16 +01:00
Spythere 0434702d3b update: URL api 2023-03-09 13:41:59 +01:00
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
Spythere c09fc81886 Aktualizacja 1.10.0
Aktualizacja aplikacji do wersji 1.10.0
2022-07-24 00:00:18 +02:00
Spythere 30f72d518d Bump wersji 1.10.0 (prod) 2022-07-23 23:46:17 +02:00
Spythere 9b86e07152 Poprawki responsywności 2022-07-23 23:45:53 +02:00
Spythere 4e0fb5dc01 Fix reaktywności SRJP 2022-07-19 23:32:16 +02:00
Spythere a392991030 PWA: wyłączono funkcję 2022-07-19 22:56:27 +02:00
Spythere ff7ca27fe6 Dodano informację o offline 2022-07-19 14:45:46 +02:00
Spythere 94cd7aaa60 Bump wersji (dev) 2022-07-16 23:21:31 +02:00
Spythere 843289d8d7 Poprawki URL 2022-07-16 17:21:08 +02:00
Spythere 66cae68e19 Update package lock 2022-07-16 17:12:32 +02:00
Spythere b38e50396a package lock, gitignore 2022-07-16 17:10:25 +02:00
Spythere 7888e59117 Poprawki po migracji 2022-07-16 17:07:57 +02:00
Spythere 46e700583d Migracja na Vite 2022-07-16 16:12:31 +02:00
Spythere fc56c38c45 Poprawki stylistyczne modalu aktualizacji; link do najnowszego wydania w stopce 2022-07-16 12:41:24 +02:00
Spythere 9594e2c21a Bump wersji 2022-07-16 00:56:41 +02:00
Spythere a8bab5283b Modal aktualizacji aplikacji 2022-07-16 00:56:25 +02:00
Spythere 1cc799706c Globalny TrainModal; animacja przejścia 2022-07-16 00:27:37 +02:00
Spythere 5ee8f72652 Poprawka semantyki wersji 2022-07-15 15:36:06 +02:00
Spythere 942f883b91 Dodano modal aktualizacji 2022-07-15 15:32:12 +02:00
Spythere 54b47d44e5 PWA: odświeżanie przy wykryciu aktualizacji 2022-07-14 21:25:17 +02:00
Spythere f9aaf21f7a Test PWA 2022-07-14 20:52:53 +02:00
Spythere d79705ca5c Test cachingu scenerii 2022-07-14 20:44:43 +02:00
Spythere 55c64d5f0a Caching service workera 2022-07-14 17:50:17 +02:00
Spythere 4ca1c7bb9c Dodano wsparcie PWA 2022-07-14 14:57:44 +02:00
Spythere abc8fda98e Poprawka kolorów 2022-07-14 14:13:55 +02:00
Spythere aaec23d210 Poprawiono wygląd modalu RJ 2022-07-13 16:34:38 +02:00
Spythere 0af7b68138 Poprawki w widoku RJ 2022-07-12 18:45:18 +02:00
Spythere ae24eaf8e4 Poprawka w info o pozycji pociągu 2022-07-12 13:37:18 +02:00
Spythere f73a07daee Poprawiono wygląd posterunków SBL w widoku RJ 2022-07-12 13:25:16 +02:00
Spythere 89f5bf2e95 Dodano górny pasek z informacjami dla widoku RJ 2022-07-12 13:15:49 +02:00
Spythere 8137c1ff95 Widok pociągów: nowe odznaki statusów 2022-07-12 12:06:27 +02:00
Spythere 4b0d9b887e Historia dr scenerii: dodano odnośniki do dziennika 2022-07-12 11:20:19 +02:00
Spythere 506064cf9a Sceneria: przeniesiono link do wątku forum 2022-07-12 11:14:56 +02:00
Spythere 825e25434a Poprawki tłumaczenia 2022-07-12 11:02:49 +02:00
Spythere 32c601d50a Historia RJ scenerii: dodano odnośniki do dziennika 2022-07-12 10:59:29 +02:00
Spythere b88a96237e Dziennik: dodano filtrowanie po ID rozkładu 2022-07-12 10:54:48 +02:00
Spythere 6c724440d7 Bump wersji: 1.9.9 -> 1.9.10 2022-07-11 18:11:26 +02:00
Spythere 71016e63bb Bump wersji: 1.9.9 -> 1.9.9.1 2022-07-11 18:06:07 +02:00
Spythere fb85352ce3 Modal widoku pociągu 2022-07-11 18:04:07 +02:00
158 changed files with 23740 additions and 26538 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:
+1
View File
@@ -1,5 +1,6 @@
.DS_Store .DS_Store
node_modules node_modules
/dev-dist
/dist /dist
# local env files # local env files
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
+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 @@
] ]
} }
} }
+5 -24
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,31 +24,12 @@
<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>
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please
enable it to continue.</strong
>
</noscript>
<div id="app"></div> <div id="app"></div>
<!-- built files will be auto injected --> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
+5452 -14427
View File
File diff suppressed because it is too large Load Diff
+16 -20
View File
@@ -1,37 +1,33 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.9.9", "version": "1.17.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite",
"build": "vue-cli-service build", "build": "vue-tsc --noEmit && vite build",
"deploy-prod": "npm run build && firebase deploy --only hosting:prod", "deploy": "yarn build && firebase deploy --only hosting",
"deploy-dev": "npm run build && firebase deploy --only hosting:dev" "preview": "yarn build && vite preview"
}, },
"dependencies": { "dependencies": {
"core-js": "^3.12.1", "core-js": "^3.12.1",
"dotenv": "^8.6.0", "dotenv": "^16.0.3",
"firebase": "^9.8.1", "firebase": "^9.8.1",
"howler": "^2.2.1", "howler": "^2.2.1",
"pinia": "^2.0.14", "pinia": "^2.0.14",
"sass": "^1.53.0",
"socket.io-client": "^4.4.1", "socket.io-client": "^4.4.1",
"vue": "^3.2.34", "vue": "^3.2.37",
"vue-i18n": "^9.1.6", "vue-i18n": "^9.1.6",
"vue-router": "^4.0.0-0", "vue-router": "^4.0.0-0"
"vuex": "^4.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^17.0.35", "@types/node": "^18.11.17",
"@vue/cli-plugin-babel": "^5.0.4", "@vitejs/plugin-vue": "^4.0.0",
"@vue/cli-plugin-router": "^5.0.4", "axios": "^1.2.1",
"@vue/cli-plugin-typescript": "^5.0.4", "typescript": "^4.9.4",
"@vue/cli-plugin-vuex": "^5.0.4", "vite": "^4.0.3",
"@vue/cli-service": "^5.0.4", "vite-plugin-pwa": "^0.14.0",
"@vue/compiler-sfc": "^3.1.0", "vue-tsc": "^1.0.18"
"axios": "^0.21.1",
"sass": "^1.32.13",
"sass-loader": "^8.0.2",
"typescript": "^4.7.3"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

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

After

Width:  |  Height:  |  Size: 215 B

+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow:
+6 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "", "name": "Stacjownik TD2",
"short_name": "", "short_name": "Stacjownik",
"icons": [ "icons": [
{ {
"src": "/android-chrome-192x192.png", "src": "/android-chrome-192x192.png",
@@ -13,7 +13,8 @@
"type": "image/png" "type": "image/png"
} }
], ],
"theme_color": "#ffffff", "theme_color": "#ffc014",
"background_color": "#ffffff", "background_color": "#4d4d4d",
"display": "standalone" "display": "standalone",
"start_url": "."
} }
+28 -163
View File
@@ -1,7 +1,6 @@
@import './styles/responsive.scss'; @import './styles/responsive.scss';
@import './styles/variables.scss'; @import './styles/variables.scss';
@import './styles/global.scss'; @import './styles/global.scss';
@import './styles/scenery_status.scss';
// VUE ROUTE CHANGE ANIMATION // VUE ROUTE CHANGE ANIMATION
.view-anim { .view-anim {
@@ -17,22 +16,40 @@
} }
} }
.modal-anim {
&-enter-active,
&-leave-active {
transition: all $animDuration $animType;
}
&-enter-from,
&-leave-to {
transform: translateY(-25%);
opacity: 0;
}
}
.route { .route {
margin: 0 0.2em; margin: 0 0.2em;
&-active { &-active,
&[data-active='true'] {
color: $accentCol; color: $accentCol;
font-weight: bold; font-weight: bold;
} }
} }
// APP // APP
.app { #app {
color: white; color: white;
font-size: 1rem; font-size: 1rem;
@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);
} }
} }
@@ -40,8 +57,8 @@
.app_container { .app_container {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
height: 100vh;
min-height: 800px; min-height: 100vh;
header { header {
flex: 0 0 auto; flex: 0 0 auto;
@@ -68,168 +85,16 @@
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%;
padding: 0.5em; padding: 0.5em;
img {
width: 1.1em;
vertical-align: text-bottom;
}
z-index: 10; z-index: 10;
background: #111; background: #111;
+88 -137
View File
@@ -1,110 +1,74 @@
<template> <template>
<div class="app"> <div class="app_container">
<div class="app_container"> <transition name="modal-anim">
<!-- <div class="wip-alert"> <keep-alive>
<img class="icon-error" :src="iconError" alt="error" /> <TrainModal v-if="store.chosenModalTrainId" />
<h2>Stacjownik tymczasowo nieaktywny!</h2> </keep-alive>
<p>Absolutny zakaz wjazdu!</p> </transition>
</div> -->
<header class="app_header">
<div class="header_container">
<div class="header_icons">
<span class="icons-top">
<img :src="icons.pl" alt="icon-pl" @click="changeLang('en')" v-if="currentLang == 'pl'" />
<img :src="icons.en" alt="icon-en" @click="changeLang('pl')" v-else />
</span>
<span class="icons-bottom">
<a href="https://www.paypal.com/paypalme/spythere" target="_blank">
<img :src="icons.dollar" alt="icon paypal" />
</a>
<a href="https://discord.gg/x2mpNN3svk" target="_blank"> <UpdatePrompt />
<img :src="icons.discord" alt="icon discord" />
</a>
</span>
</div>
<div class="header_body"> <AppHeader :current-lang="currentLang" @change-lang="changeLang" />
<status-indicator />
<span class="header_brand">
<img :src="brand_logo" alt="Stacjownik" />
</span>
<span class="header_info"> <main class="app_main">
<Clock /> <router-view v-slot="{ Component }">
<keep-alive exclude="JournalView">
<component :is="Component" :key="$route.name" />
</keep-alive>
</router-view>
</main>
<div class="info_counter"> <footer class="app_footer">
<img src="@/assets/icon-dispatcher.svg" alt="icon dispatcher" /> &copy;
<span class="text--primary">{{ onlineDispatchers.length }}</span> <a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
<span class="text--grayed"> / </span> {{ new Date().getUTCFullYear() }} |
<span class="text--primary">{{ trainList.length }}</span> <a :href="releaseURL" target="_blank">v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}</a>
<img src="@/assets/icon-train.svg" alt="icon train" /> <br />
</div> <a href="https://discord.gg/x2mpNN3svk"><img :src="getIcon('discord', 'png')" alt="">&nbsp;<b>{{ $t('footer.discord') }}</b></a>
<span class="info_region"> <div style="display: none">&int; ukryta taktyczna całka do programowania w HTMLu</div>
<SelectBox :itemList="computedRegions" :defaultItemIndex="0" @selected="changeRegion" /> </footer>
</span>
</span>
<span class="header_links">
<router-link class="route" active-class="route-active" to="/" exact>
{{ $t('app.sceneries') }}
</router-link>
/
<router-link class="route" active-class="route-active" to="/trains">{{ $t('app.trains') }}</router-link>
/
<router-link class="route" active-class="route-active" to="/journal">
{{ $t('app.journal') }}
</router-link>
</span>
</div>
</div>
</header>
<main class="app_main">
<router-view v-slot="{ Component }">
<!-- <transition name="view-anim" mode="out-in"> -->
<keep-alive>
<component :is="Component" :key="$route.path" />
</keep-alive>
</router-view>
</main>
<footer class="app_footer">
&copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} | v{{ VERSION }}
<div style="display: none">&int; ukryta taktyczna całka do programowania w HTMLu</div>
</footer>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, provide, ref } from 'vue'; import { computed, defineComponent, KeepAlive, provide, ref, watch } from 'vue';
import Clock from '@/components/App/Clock.vue'; import Clock from './components/App/Clock.vue';
import StorageManager from '@/scripts/managers/storageManager';
import packageInfo from '.././package.json'; import packageInfo from '.././package.json';
import options from '@/data/options.json';
import StatusIndicator from '@/components/App/StatusIndicator.vue'; import StatusIndicator from './components/App/StatusIndicator.vue';
import SelectBox from '@/components/Global/SelectBox.vue'; import SelectBox from './components/Global/SelectBox.vue';
import { useStore } from './store/store'; import { useStore } from './store/store';
import TrainModal from './components/Global/TrainModal.vue';
import StorageManager from './scripts/managers/storageManager';
import imageMixin from './mixins/imageMixin';
import AppHeader from './components/App/AppHeader.vue';
import axios from 'axios';
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,
TrainModal,
AppHeader,
UpdatePrompt,
}, },
mixins: [imageMixin],
setup() { setup() {
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);
@@ -120,73 +84,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,
updateModalVisible: false,
hasReleaseNotes: false,
options,
currentLang: 'pl', currentLang: 'pl',
releaseURL: '',
brand_logo: require('@/assets/stacjownik-header-logo.svg'), isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
icons: {
en: require('@/assets/icon-en.jpg'),
pl: require('@/assets/icon-pl.svg'),
error: require('@/assets/icon-error.svg'),
dollar: require('@/assets/icon-dollar.svg'),
dispatcher: require('@/assets/icon-dispatcher.svg'),
train: require('@/assets/icon-train.svg'),
discord: require('@/assets/icon-discord.png'),
},
}), }),
created() { created() {
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() {
if (StorageManager.getStringValue('version') != this.VERSION) { this.setReleaseURL();
StorageManager.setStringValue('version', this.VERSION);
if (this.hasReleaseNotes) StorageManager.setBooleanValue('version_notes_read', false); watch(
} () => this.store.blockScroll,
(value) => {
if (value) {
document.body.classList.add('no-scroll');
return;
}
this.updateModalVisible = this.hasReleaseNotes && !StorageManager.getBooleanValue('version_notes_read'); document.body.classList.remove('no-scroll');
}
this.updateToNewestVersion(); );
}, },
methods: { methods: {
toggleUpdateModal() {
this.updateModalVisible = !this.updateModalVisible;
StorageManager.setBooleanValue('version_notes_read', true);
},
changeRegion(region: { id: string; value: string }) {
this.store.changeRegion(region);
},
changeLang(lang: string) { changeLang(lang: string) {
this.$i18n.locale = lang; this.$i18n.locale = lang;
this.currentLang = lang; this.currentLang = lang;
@@ -194,12 +139,18 @@ export default defineComponent({
StorageManager.setStringValue('lang', lang); StorageManager.setStringValue('lang', lang);
}, },
updateToNewestVersion() { async setReleaseURL() {
if (!StorageManager.isRegistered('unavailable-status')) { try {
StorageManager.setBooleanValue('unavailable-status', true); const releaseData = await (
StorageManager.setBooleanValue('ending-status', true); await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
StorageManager.setBooleanValue('no-space-status', true); ).data;
StorageManager.setBooleanValue('afk-status', true);
if (!releaseData) return;
this.releaseURL = releaseData.html_url;
} catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
return;
} }
}, },
+18 -16
View File
@@ -1,18 +1,20 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="60" height="60" fill="#898989"/> <rect y="-0.00012207" width="60" height="60" fill="#898989"/>
<path d="M30.5 6.04878H35.2195" stroke="#BFBFBF"/> <path d="M29.0126 32.4897V10.2511V9.52028H30.4337V10.2511V57.234H29.0126V32.4897Z" fill="#BFBFBF"/>
<path d="M27.9024 4.00303C25.2115 4.10008 24.2403 6.24494 24 7.41767H32.0488C31.8486 6.16406 30.5934 3.90598 27.9024 4.00303Z" fill="black"/> <path d="M26.955 29.3992V32.9949L29.7672 36.9105" stroke="black" stroke-width="0.61183"/>
<path d="M33.0244 29.6688V5.47793V4.68292H34.4878V5.47793V56.5854H33.0244V32.5H27.5V28.5V28.0163L28.5 28V31.5L31.9268 31.5447H33.0244V29.6688Z" fill="#BFBFBF"/> <rect x="29.0051" y="34.0686" width="1.42857" height="22.8196" fill="white"/>
<path d="M28.1463 29.2683C30.8373 29.1712 31.8085 27.0264 32.0488 25.8537H24C24.2002 27.1073 25.4554 29.3654 28.1463 29.2683Z" fill="black"/> <rect x="29.0051" y="34.0686" width="1.42857" height="5.18627" fill="#FF0000"/>
<path d="M32.0488 25.8537V7.86993V7.41464H24V25.8537H32.0488Z" fill="black"/> <rect x="29.0051" y="54.8137" width="1.42857" height="5.18627" fill="#FF0000"/>
<path d="M25 26V29.5L33.8781 44.9756" stroke="black"/> <rect x="29.0051" y="44.4412" width="1.42857" height="5.18627" fill="#FF0000"/>
<rect x="33.0244" y="31.5447" width="1.46341" height="25.0407" fill="white"/> <rect x="27.8749" y="31.8649" width="3.75" height="2.17823" fill="white"/>
<rect x="33.0244" y="31.5447" width="1.46341" height="5.69106" fill="#FF0000"/> <path d="M33.5 28.5111V8.61545V8.11176H26V28.5111H33.5Z" fill="black"/>
<rect x="33.0244" y="42.9268" width="1.46341" height="5.69106" fill="#FF0000"/> <path d="M29.6364 5.00276C27.1289 5.09112 26.2239 7.044 26 8.11176H33.5C33.3134 6.97036 32.1438 4.91439 29.6364 5.00276Z" fill="black"/>
<rect x="33.0244" y="54.3089" width="1.46341" height="5.69106" fill="#FF0000"/> <path d="M29.8636 31.6201C32.3711 31.5317 33.2761 29.5789 33.5 28.5111H26C26.1865 29.6525 27.3561 31.7085 29.8636 31.6201Z" fill="black"/>
<ellipse cx="27.9024" cy="7.40022" rx="1.46341" ry="1.40022" fill="#212121"/> <ellipse cx="29.887" cy="11.8168" rx="1.38696" ry="1.28474" fill="#212121"/>
<ellipse cx="27.9024" cy="11.8343" rx="1.46341" ry="1.40022" fill="#212121"/> <ellipse cx="29.887" cy="8.0135" rx="1.38696" ry="1.28474" fill="#212121"/>
<ellipse cx="27.9024" cy="16.2683" rx="1.46341" ry="1.40022" fill="#FF0000"/> <ellipse cx="29.887" cy="15.6151" rx="1.38696" ry="1.28474" fill="#212121"/>
<ellipse cx="27.9024" cy="20.7023" rx="1.46341" ry="1.40022" fill="#212121"/> <ellipse cx="29.887" cy="19.6834" rx="1.38696" ry="1.28474" fill="#212121"/>
<ellipse cx="27.9024" cy="25.1364" rx="1.46341" ry="1.40022" fill="#212121"/> <ellipse cx="29.887" cy="23.7518" rx="1.38696" ry="1.28474" fill="#212121"/>
<ellipse cx="29.887" cy="27.8201" rx="1.38696" ry="1.28474" fill="#00FF0A"/>
<ellipse cx="29.887" cy="19.769" rx="1.38696" ry="1.28474" fill="#00FF0A"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

+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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.75 3.75H22.5V1.25H20V3.75H10V1.25H7.5V3.75H6.25C4.875 3.75 3.75 4.875 3.75 6.25V23.75C3.75 25.125 4.875 26.25 6.25 26.25H23.75C25.125 26.25 26.25 25.125 26.25 23.75V6.25C26.25 4.875 25.125 3.75 23.75 3.75ZM23.75 23.75H6.25V11.25H23.75V23.75ZM6.25 8.75V6.25H23.75V8.75H6.25ZM8.75 13.75H21.25V16.25H8.75V13.75ZM8.75 18.75H17.5V21.25H8.75V18.75Z" fill="#F2E147"/>
</svg>

After

Width:  |  Height:  |  Size: 477 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.75 3.75H22.5V1.25H20V3.75H10V1.25H7.5V3.75H6.25C4.875 3.75 3.75 4.875 3.75 6.25V23.75C3.75 25.125 4.875 26.25 6.25 26.25H23.75C25.125 26.25 26.25 25.125 26.25 23.75V6.25C26.25 4.875 25.125 3.75 23.75 3.75ZM23.75 23.75H6.25V11.25H23.75V23.75ZM6.25 8.75V6.25H23.75V8.75H6.25ZM8.75 13.75H21.25V16.25H8.75V13.75ZM8.75 18.75H17.5V21.25H8.75V18.75Z" fill="#66FF6C"/>
</svg>

After

Width:  |  Height:  |  Size: 477 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.75 3.75H22.5V1.25H20V3.75H10V1.25H7.5V3.75H6.25C4.875 3.75 3.75 4.875 3.75 6.25V23.75C3.75 25.125 4.875 26.25 6.25 26.25H23.75C25.125 26.25 26.25 25.125 26.25 23.75V6.25C26.25 4.875 25.125 3.75 23.75 3.75ZM23.75 23.75H6.25V11.25H23.75V23.75ZM6.25 8.75V6.25H23.75V8.75H6.25ZM8.75 13.75H21.25V16.25H8.75V13.75ZM8.75 18.75H17.5V21.25H8.75V18.75Z" fill="#898989"/>
</svg>

After

Width:  |  Height:  |  Size: 477 B

+237
View File
@@ -0,0 +1,237 @@
<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>
</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="g-tooltip">
<b class="text--primary">{{ factorU }}U</b>
<div class="content">Test</div>
</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;
},
factorU() {
return this.onlineDispatchersCount == 0 ? '-' : (this.onlineTrainsCount / this.onlineDispatchersCount).toFixed(2);
},
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 {
position: relative;
max-width: 20em;
}
&_container {
display: flex;
justify-content: center;
border-radius: 0 0 1em 1em;
@include smallScreen {
position: relative;
margin-top: 0.5em;
}
}
&_brand {
display: flex;
img {
width: 100%;
margin: 0 auto;
}
}
&_info {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
font-size: 1.15em;
}
&_links {
display: flex;
justify-content: center;
border-radius: 0.7em;
font-size: 1.25em;
padding: 0.5em;
}
&_icons {
position: absolute;
right: 0;
top: 0;
padding: 0.5em;
@include smallScreen {
transform: translateX(85%);
}
}
}
// ICONS
.icons-top {
img {
width: 2.5em;
cursor: pointer;
}
}
// 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/runtime-core";
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>
+28 -23
View File
@@ -161,20 +161,17 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { DataStatus } from '@/scripts/enums/DataStatus'; import { defineComponent } from 'vue';
import { StoreData } from '@/scripts/interfaces/StoreData'; import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '@/store/store'; import { useStore } from '../../store/store';
import { StoreState } from '@/store/storeTypes'; import { StoreState } from '../../scripts/interfaces/store/storeTypes';
import { computed, defineComponent, watch } from 'vue';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
icons: {
statusIndicator: require('@/assets/signal-status-indicator.svg'),
},
tooltipActive: false, tooltipActive: false,
indicator: { indicator: {
offline: false,
status: DataStatus.Loading, status: DataStatus.Loading,
message: 'data-status.S3', message: 'data-status.S3',
}, },
@@ -196,6 +193,7 @@ export default defineComponent({
return { return {
dataStatus: store.dataStatuses, dataStatus: store.dataStatuses,
store,
}; };
}, },
@@ -209,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;
@@ -255,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;
} }
@@ -294,10 +303,11 @@ export default defineComponent({
.status-indicator { .status-indicator {
position: absolute; position: absolute;
left: 50%;
bottom: 0; bottom: 0;
transform: translateX(12em); right: 0;
z-index: 100; z-index: 100;
transform: translateX(1.5em);
} }
.indicator { .indicator {
@@ -322,7 +332,7 @@ export default defineComponent({
background-color: #171717; background-color: #171717;
border-radius: 0.75em; border-radius: 0.75em;
min-width: 13em; width: 13em;
text-align: center; text-align: center;
overflow: none; overflow: none;
@@ -346,22 +356,16 @@ export default defineComponent({
} }
@include midScreen() { @include midScreen() {
left: 50%; left: auto;
top: 100%; right: 200%;
transform: translate(-50%, 0);
margin-left: 0;
margin-top: 0.75em;
&::before { &::before {
border-left: 10px solid transparent;
border-right: 10px solid transparent; border-right: 10px solid transparent;
border-bottom: 10px solid #171717; border-left: 12px solid #171717;
right: 0;
left: auto;
top: 0; transform: translate(100%, -50%);
left: 50%;
transform: translate(-50%, -100%);
} }
} }
@@ -371,3 +375,4 @@ export default defineComponent({
} }
} }
</style> </style>
+168
View File
@@ -0,0 +1,168 @@
<template>
<transition name="modal-anim">
<section class="update-modal card" v-if="releaseData && modalOpen">
<h2 class="modal_header text--primary">
<img :src="getImage('stacjownik-header-logo.svg')" alt="stacjownik logo" />
{{ releaseData.tag_name }}
</h2>
<div class="horizontal"></div>
<div class="modal_content">
<h3>{{ $t('update.title') }}</h3>
<a :href="releaseData.html_url" target="_blank">{{ $t('update.release-link') }}</a>
<br />
<br />
<p>{{ $t('update.paragraph1') }}</p>
<!-- <div class="modal_changelog" v-html="markdownReleaseBody"></div> -->
</div>
<div class="modal_actions">
<button class="btn btn--option" @click="modalOpen = false">{{ $t('update.confirm-button') }}</button>
</div>
</section>
</transition>
</template>
<script lang="ts">
import axios from 'axios';
import { defineComponent } from 'vue';
import packageInfo from '../../../package.json';
import imageMixin from '../../mixins/imageMixin';
import { ReleaseAPIData } from '../../scripts/interfaces/github_api/ReleaseAPIData';
import StorageManager from '../../scripts/managers/storageManager';
import { useStore } from '../../store/store';
const GH_LASTEST_RELEASE_URL = 'https://api.github.com/repos/Spythere/stacjownik/releases/latest';
export default defineComponent({
mixins: [imageMixin],
mounted() {
this.fetchReleases();
},
data() {
return {
modalOpen: false,
releaseData: null as ReleaseAPIData | null,
};
},
setup() {
return {
store: useStore()
}
},
methods: {
async fetchReleases() {
const storedVersion = StorageManager.getStringValue('appVersion');
const appVersion = packageInfo.version;
// Zmiana
if (appVersion != storedVersion) {
StorageManager.setStringValue('appVersion', appVersion);
// Znajdź changelog na GitHubie, jeśli jest pokaż modal
try {
const releaseData: ReleaseAPIData = await (await axios.get(GH_LASTEST_RELEASE_URL)).data;
if (!releaseData) return;
const lastReleaseVersion = releaseData.tag_name.slice(1);
if (lastReleaseVersion == appVersion) {
this.releaseData = releaseData;
this.modalOpen = true;
StorageManager.setStringValue('releaseURL', releaseData.html_url);
}
} catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
}
}
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/card.scss';
@import '../../styles/responsive.scss';
.modal-anim {
&-enter-active,
&-leave-active {
transition: all $animDuration $animType;
}
&-enter-from,
&-leave-to {
opacity: 0;
transform: translate(-50%, -50%) scale(0.45);
}
}
.update-modal {
text-align: center;
background-color: var(--clr-secondary);
padding: 1em;
}
.horizontal {
margin: 1em 0;
height: 2px;
width: 100%;
background-color: white;
}
.modal_header {
font-size: 1.6em;
img {
width: 50%;
vertical-align: text-top;
}
}
.modal_content {
font-size: 1.1em;
a {
text-decoration: underline;
}
}
.modal_actions {
margin-top: 2em;
button {
color: white;
padding: 0.5em;
font-size: 1.2em;
background-color: black;
}
}
.modal_changelog {
font-size: 0.8em;
margin-top: 2em;
}
@include smallScreen {
.update-modal {
height: auto;
max-width: 95%;
}
}
</style>
+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>
-47
View File
@@ -1,47 +0,0 @@
<template>
<section
class="updates card"
v-if="cardOpen"
>
<h2>Ostatnie aktualizacje w Stacjowniku</h2>
<p>Tutaj będą pojawiać się informacje o kolejnych nowościach na stronie :)</p>
<ul>
<li
v-for="(update, i) in updates"
:key="i"
>
<div>{{update.date}}</div>
<div>
<span
v-for="(line, l) in content"
:key="l"
>{{line}}</span>
</div>
</li>
</ul>
</section>
</template>
<script>
import { defineComponent } from "@vue/runtime-core";
export default defineComponent({
data() {
return {
updates: {
date: "08/08/20",
content: [
"Lekko odświeżono wygląd strony, dodano nowy widok z pociągami online",
"Dodano animacje zmieniania widoków (zakładek)",
"Dodano przycisk zamykający kartę z filtrami",
],
},
};
},
});
</script>
<style lang="scss" scoped>
</style>
+1 -42
View File
@@ -1,5 +1,5 @@
<template> <template>
<button class="action-btn"> <button class="action-btn btn--filled">
<div class="button_content"> <div class="button_content">
<slot></slot> <slot></slot>
</div> </div>
@@ -16,47 +16,6 @@ export default defineComponent({});
@import "../../styles/variables"; @import "../../styles/variables";
@import "../../styles/responsive"; @import "../../styles/responsive";
.action-btn {
background: #333;
border: none;
color: #bdbdbd;
font-size: 1em;
font-weight: 500;
padding: 0.35em 0.65em;
cursor: pointer;
transition: all 0.3s;
&.outlined {
border: 1px solid white;
}
img {
width: 1.25em;
vertical-align: middle;
margin-right: 0.35em;
}
p {
font-size: 1em;
overflow: hidden;
}
&.open {
color: $accentCol;
border: none;
}
&:hover,
&:focus {
color: $accentCol;
background: #5c5c5c;
}
}
.button_content { .button_content {
display: flex; display: flex;
justify-content: center; justify-content: center;
+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;
+62
View File
@@ -0,0 +1,62 @@
<template>
<div class="progress-bar">
<span class="bar-bg"></span>
<span class="bar-fg" :style="{ width: `${~~progressPercent}%`, backgroundColor: bgColor }"></span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
progressPercent: {
type: Number,
required: true,
},
progressType: {
type: String,
required: false,
},
},
computed: {
bgColor() {
switch (this.progressType) {
case 'abandoned':
return 'salmon';
default:
return 'springgreen';
}
},
},
});
</script>
<style lang="scss" scoped>
.progress-bar {
position: relative;
width: 6em;
height: 1em;
margin: 0.5em 0;
.bar-fg,
.bar-bg {
position: absolute;
height: 1em;
width: 100%;
left: 0;
}
.bar-fg {
background-color: springgreen;
}
.bar-bg {
background-color: #5b5b5b;
}
}
</style>
+5 -5
View File
@@ -9,7 +9,7 @@
<img <img
class="search-exit" class="search-exit"
:src="exitIcon" :src="getIcon('exit')"
alt="exit-icon" alt="exit-icon"
@click="clearValue" @click="clearValue"
/> />
@@ -18,11 +18,11 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, watch } from "vue"; import { defineComponent, ref, watch } from "vue";
import imageMixin from "../../mixins/imageMixin";
export default defineComponent({ export default defineComponent({
data: () => ({ mixins: [imageMixin],
exitIcon: require("@/assets/icon-exit.svg"),
}),
emits: ["update:searchedValue", "clearValue"], emits: ["update:searchedValue", "clearValue"],
props: { props: {
searchedValue: { searchedValue: {
@@ -59,7 +59,7 @@ export default defineComponent({
emit("clearValue"); emit("clearValue");
}; };
const updateValue = (e) => { const updateValue = (e: any) => {
if (!props.updateOnInput && e.keyCode == 13) if (!props.updateOnInput && e.keyCode == 13)
emit("update:searchedValue", compSearchedValue.value); emit("update:searchedValue", compSearchedValue.value);
}; };
+19 -40
View File
@@ -1,9 +1,12 @@
<template> <template>
<div class="select-box" > <div class="select-box">
<div class="select-box_content"> <div class="select-box_content">
<button class="selected" @click="toggleBox"> <button class="selected" @click="toggleBox">
<span class="text--primary">{{ prefix }}</span>
<span>{{ computedSelectedItem.selectedValue || computedSelectedItem.value }}</span> <span>{{ computedSelectedItem.selectedValue || computedSelectedItem.value }}</span>
<div class="arrow">
<img :src="listOpen ? getIcon('arrow-asc') : getIcon('arrow-desc')" alt="arrow-icon" />
</div>
</button> </button>
<ul class="options" :ref="(el) => (listRef = el as Element)"> <ul class="options" :ref="(el) => (listRef = el as Element)">
@@ -22,15 +25,12 @@
</li> </li>
</ul> </ul>
</div> </div>
<div class="arrow">
<img :src="listOpen ? ascIcon : descIcon" alt="arrow-icon" />
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, Ref, ref } from '@vue/runtime-core'; import { defineComponent, Ref, ref, computed } from 'vue';
import imageMixin from '../../mixins/imageMixin';
interface Item { interface Item {
id: string; id: string;
@@ -40,6 +40,7 @@ interface Item {
export default defineComponent({ export default defineComponent({
emits: ['selected'], emits: ['selected'],
mixins: [imageMixin],
props: { props: {
itemList: { itemList: {
@@ -58,11 +59,6 @@ export default defineComponent({
}, },
}, },
data: () => ({
ascIcon: require('@/assets/icon-arrow-asc.svg'),
descIcon: require('@/assets/icon-arrow-desc.svg'),
}),
setup(props) { setup(props) {
let listRef: Ref<Element | null> = ref(null); let listRef: Ref<Element | null> = ref(null);
let buttonRef: Ref<HTMLButtonElement | null> = ref(null); let buttonRef: Ref<HTMLButtonElement | null> = ref(null);
@@ -133,44 +129,25 @@ export default defineComponent({
} }
.select-box { .select-box {
position: relative; display: flex;
align-items: center;
} }
.arrow { .arrow {
position: absolute;
top: 50%;
right: 0;
padding: 0.5em;
img { img {
vertical-align: middle; vertical-align: middle;
width: 1.35em; width: 1.35em;
} }
transform: translateY(-50%);
pointer-events: none;
} }
button.selected { button.selected {
background: #333; color: paleturquoise;
color: white;
font-size: 1em; font-weight: bold;
padding: 0.1em 0.5em;
padding: 0.35em 0.5em;
margin-right: 1.4em;
width: 100%;
cursor: pointer;
border: none;
outline: none;
text-align: left;
&:focus { &:focus {
background: #555; background-color: #262626;
} }
} }
@@ -191,8 +168,9 @@ ul.options {
height: auto; height: auto;
z-index: 100; z-index: 100;
width: 100%; width: 100%;
font-size: 0.9em;
} }
li.option { li.option {
@@ -206,6 +184,7 @@ li.option {
appearance: none; appearance: none;
border: none; border: none;
outline: none; outline: none;
background: none;
&:focus + span { &:focus + span {
color: $accentCol; color: $accentCol;
@@ -221,11 +200,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;
@@ -0,0 +1,90 @@
<template>
<span class="status-badge" :class="statusID" v-if="isOnline">
{{ $t(`status.${statusID}`) }}
{{ statusID == 'online' ? timestampToString(statusTimestamp!) : '' }}
</span>
<span class="status-badge free" v-else>
{{ $t('status.free') }}
</span>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin';
export default defineComponent({
props: {
statusID: {
type: String,
},
statusTimestamp: {
type: Number,
},
isOnline: {
type: Boolean,
},
},
mixins: [dateMixin],
});
</script>
<style lang="scss" scoped>
$free: #8a8a8a;
$ending: #e6c300;
$no-limit: #117fc9;
$unav: #ff3d5d;
$brb: #e6a100;
$no-space: #222;
$online: #09a116;
$unknown: rgb(185, 60, 60);
.status-badge {
border-radius: 1rem;
font-weight: 500;
padding: 0.2em 0.55em;
background-color: $online;
&.free {
background-color: $free;
font-size: 0.95em;
}
&.ending {
background-color: $ending;
color: black;
font-size: 0.9em;
}
&.no-limit {
background-color: $no-limit;
font-size: 0.85em;
}
&.not-signed,
&.unavailable {
background-color: $unav;
font-size: 0.85em;
}
&.brb {
background-color: $brb;
color: black;
font-size: 0.95em;
}
&.no-space {
background-color: $no-space;
border: 1px solid white;
color: white;
font-size: 0.85em;
}
&.unknown {
background-color: $unknown;
font-size: 0.95em;
}
}
</style>
+7 -7
View File
@@ -4,12 +4,12 @@
class="date arrival" class="date arrival"
v-if="!stop.beginsHere" v-if="!stop.beginsHere"
:class="{ :class="{
delayed: stop.arrivalDelay > 0 && stop.confirmed, delayed: stop.arrivalDelay > 0 && (stop.confirmed || stop.stopped),
preponed: stop.arrivalDelay < 0 && stop.confirmed, preponed: stop.arrivalDelay < 0 && (stop.confirmed || stop.stopped),
'on-time': stop.arrivalDelay == 0 && stop.confirmed, 'on-time': stop.arrivalDelay == 0 && stop.confirmed,
}" }"
> >
<span v-if="stop.arrivalDelay != 0 && stop.confirmed"> <span v-if="stop.arrivalDelay != 0 && (stop.confirmed || stop.stopped)">
<s>{{ timestampToString(stop.arrivalTimestamp) }}</s> <s>{{ timestampToString(stop.arrivalTimestamp) }}</s>
{{ timestampToString(stop.arrivalRealTimestamp) }} {{ timestampToString(stop.arrivalRealTimestamp) }}
({{ stop.arrivalDelay > 0 ? '+' : '' }}{{ stop.arrivalDelay }}) ({{ stop.arrivalDelay > 0 ? '+' : '' }}{{ stop.arrivalDelay }})
@@ -20,13 +20,13 @@
</span> </span>
</span> </span>
<span class="date stop" v-if="stop.stopTime" :class="stop.stopType.replace(', ', '-')"> <span class="date stop" v-if="stop.stopTime || stop.stopped" :class="stop.stopType.replace(', ', '-')">
{{ stop.stopTime }} {{ stop.stopType == '' ? 'pt' : stop.stopType }} {{ stop.stopTime }} {{ stop.stopType == '' ? 'pt' : stop.stopType }}
</span> </span>
<span <span
class="date departure" class="date departure"
v-if="!stop.terminatesHere && stop.stopTime != 0" v-if="!stop.terminatesHere && (stop.stopTime != 0 || stop.stopped)"
:class="{ :class="{
delayed: stop.departureDelay > 0 && stop.confirmed, delayed: stop.departureDelay > 0 && stop.confirmed,
preponed: stop.departureDelay < 0 && stop.confirmed, preponed: stop.departureDelay < 0 && stop.confirmed,
@@ -47,9 +47,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import dateMixin from '@/mixins/dateMixin';
import TrainStop from '@/scripts/interfaces/TrainStop';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import TrainStop from '../../scripts/interfaces/TrainStop';
export default defineComponent({ export default defineComponent({
mixins: [dateMixin], mixins: [dateMixin],
+152
View File
@@ -0,0 +1,152 @@
<template>
<div class="train-modal" v-if="chosenTrain" @keydown.esc="closeModal">
<div class="modal_background" @click="closeModal"></div>
<div class="modal_content" ref="content" tabindex="0">
<button class="btn exit" @click="closeModal">
<img :src="getIcon('exit')" alt="close card" />
</button>
<TrainInfo :train="chosenTrain" :extended="false" ref="trainInfo" />
<TrainSchedule :train="chosenTrain" tabindex="0" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import trainInfoMixin from '../../mixins/trainInfoMixin';
import { useStore } from '../../store/store';
import TrainInfo from '../TrainsView/TrainInfo.vue';
import TrainSchedule from '../TrainsView/TrainSchedule.vue';
export default defineComponent({
components: { TrainInfo, TrainSchedule },
mixins: [trainInfoMixin, modalTrainMixin, imageMixin],
data() {
return {
isTopBarVisible: false,
};
},
setup() {
const store = useStore();
return {
store,
};
},
activated() {
const contentEl = this.$refs['content'] as HTMLElement;
this.$nextTick(() => {
contentEl.focus();
});
},
methods: {
handleContentScroll(e: Event) {
const trainInfoCompHeight: number = (this.$refs['trainInfo'] as any).$el.getBoundingClientRect().height;
const posTop = (e.target as HTMLElement).scrollTop;
this.isTopBarVisible = posTop > trainInfoCompHeight;
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/card.scss';
.top-info-bar-anim {
&-enter-active,
&-leave-active {
transition: all 150ms ease-in-out;
}
&-enter-from,
&-leave-to {
transform: translate(-50%, -50%) scale(0.8);
opacity: 0;
}
}
.exit {
position: absolute;
top: 0;
right: 0;
margin: 0.5em 1em;
padding: 0.25em;
z-index: 201;
img {
width: 1.5rem;
vertical-align: middle;
}
}
.train-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
color: white;
z-index: 200;
display: flex;
justify-content: center;
text-align: left;
}
.modal_background {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.55);
}
.modal_content {
position: relative;
overflow-y: scroll;
margin-top: 1em;
width: 95vw;
max-height: 96vh;
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e;
}
@include midScreen {
.exit {
margin: 0.5em;
img {
width: 1.75rem;
}
}
}
@include smallScreen {
.modal_content {
max-height: 85vh;
}
}
</style>
+250
View File
@@ -0,0 +1,250 @@
<template>
<section class="daily-stats">
<span :data-active="statsStatus">
<b v-if="statsStatus == DataStatus.Loading">
{{ $t('app.loading') }}
</b>
<b v-else-if="stats.distanceSum == null">
{{ $t('journal.daily-stats-info') }}
</b>
<span class="stats-list" v-else>
<h3>
{{ $t('journal.daily-stats-title') }}
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
</h3>
<hr style="margin-bottom: 0.5em" />
<div v-if="stats.totalTimetables">
&bull;
<i18n-t keypath="journal.timetable-stats-total">
<template #count>
<b class="text--primary">
{{ stats.totalTimetables }}
{{ $t('journal.timetable-count', stats.totalTimetables) }}
</b>
</template>
<template #distance>
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
</template>
</i18n-t>
</div>
<div v-if="stats.timetableId">
&bull;
<i18n-t keypath="journal.timetable-stats-longest">
<template #id>
<router-link :to="`/journal/timetables?timetableId=${stats.timetableId}`">
<b>{{ stats.timetableId }}</b>
</router-link>
</template>
<template #author>
<router-link :to="`/journal/dispatchers?dispatcherName=${stats.timetableAuthor}`">
<b>{{ stats.timetableAuthor }}</b>
</router-link>
</template>
<template #driver>
<b class="text--primary">{{ stats.timetableDriver }}</b>
</template>
<template #distance>
<b class="text--primary">{{ stats.timetableRouteDistance }} km</b>
</template>
</i18n-t>
</div>
<div v-if="firstPlaceDispatchers.length == 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-dr">
<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-dr-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>
<div v-if="stats.longestDuties.length > 0">
&bull;
<i18n-t keypath="journal.timetable-stats-longest-duties">
<template #dispatcher>
<router-link :to="`/journal/dispatchers?dispatcherName=${stats.longestDuties[0].name}`">
<b>{{ stats.longestDuties[0].name }}</b>
</router-link>
</template>
<template #station>{{ stats.longestDuties[0].station }}</template>
<template #duration>
{{ calculateDuration(stats.longestDuties[0].duration) }}
</template>
</i18n-t>
</div>
<div v-if="stats.mostActiveDrivers.length > 0">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-driver">
<template #driver>
<b class="text--primary">{{ stats.mostActiveDrivers[0].name }}</b>
</template>
<template #distance>
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
</template>
</i18n-t>
</div>
</span>
</span>
</section>
</template>
<script lang="ts">
import axios from 'axios';
import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { ITimetablesDailyStats, ITimetablesDailyStatsResponse } from '../../scripts/interfaces/api/StatsAPIData';
import { URLs } from '../../scripts/utils/apiURLs';
export default defineComponent({
mixins: [dateMixin],
emits: ['toggleStatsOpen'],
data() {
return {
DataStatus,
statsStatus: DataStatus.Loading,
intervalId: -1,
stats: {
totalTimetables: 0,
distanceSum: 0,
distanceAvg: 0,
timetableAuthor: '',
timetableDriver: '',
timetableId: 0,
timetableRouteDistance: 0,
longestDuties: [],
mostActiveDrivers: [],
mostActiveDispatchers: [],
} as ITimetablesDailyStats,
};
},
activated() {
this.startFetchingDailyStats();
this.$emit('toggleStatsOpen', true);
},
deactivated() {
this.stopFetchingDailyStats();
},
computed: {
firstPlaceDispatchers() {
if (this.stats.mostActiveDispatchers.length == 0) return [];
const maxCount = this.stats.mostActiveDispatchers[0].count;
return this.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
},
},
methods: {
async fetchDailyTimetableStats() {
try {
const res: ITimetablesDailyStatsResponse = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDailyTimetableStats`)
).data;
this.stats = {
totalTimetables: res.totalTimetables,
distanceSum: res.distanceSum,
distanceAvg: res.distanceAvg,
timetableAuthor: res.maxTimetable?.authorName || '',
timetableDriver: res.maxTimetable?.driverName || '',
timetableId: res.maxTimetable?.id || 0,
timetableRouteDistance: res.maxTimetable?.routeDistance || 0,
mostActiveDispatchers: res.mostActiveDispatchers,
mostActiveDrivers: res.mostActiveDrivers,
longestDuties: res.longestDuties,
};
this.statsStatus = DataStatus.Loaded;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
this.statsStatus = DataStatus.Error;
}
},
startFetchingDailyStats() {
this.fetchDailyTimetableStats();
if (this.intervalId != -1) return;
this.intervalId = setInterval(this.fetchDailyTimetableStats, 60000);
},
stopFetchingDailyStats() {
clearInterval(this.intervalId);
this.intervalId = -1;
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
.daily-stats {
text-align: left;
}
.daily-stats > span[data-active='0'] {
opacity: 0.75;
}
.stats-list a {
text-decoration: underline;
}
@include smallScreen {
.daily-stats {
text-align: justify;
}
h3 {
text-align: center;
}
}
</style>
+7 -36
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="stats_container" v-click-outside="() => (cardVisible = false)"> <div class="stats_container" v-click-outside="() => (cardVisible = false)">
<button class="stats_button btn btn--option" @click="toggleCard"> <button class="stats_button" @click="toggleCard">
Statystyki dyżurnego {{ store.dispatcherStatsName }} Statystyki dyżurnego {{ store.dispatcherStatsName }}
</button> </button>
@@ -14,6 +14,7 @@
<div v-else> <div v-else>
<h3>STATYSTYKI WYSTAWIONYCH ROZKŁADÓW</h3> <h3>STATYSTYKI WYSTAWIONYCH ROZKŁADÓW</h3>
<div class="info-stats" v-if="store.dispatcherStatsData._count._all"> <div class="info-stats" v-if="store.dispatcherStatsData._count._all">
<span class="stat-badge"> <span class="stat-badge">
<span>LICZBA</span> <span>LICZBA</span>
@@ -48,12 +49,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { DispatcherStatsAPIData } from '@/scripts/interfaces/api/DispatcherStatsAPIData';
import { TimetableHistory } from '@/scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '@/scripts/utils/apiURLs';
import { useStore } from '@/store/store';
import axios from 'axios'; import axios from 'axios';
import { computed, defineComponent } from 'vue'; import { computed, defineComponent } from 'vue';
import { DispatcherStatsAPIData } from '../../scripts/interfaces/api/DispatcherStatsAPIData';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
export default defineComponent({ export default defineComponent({
@@ -161,42 +163,11 @@ h3 {
text-align: center; text-align: center;
} }
.info-stats {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin-top: 1em;
}
.last-timetables { .last-timetables {
overflow-y: auto; overflow-y: auto;
} }
.stat-badge {
margin-right: 0.5em;
padding-bottom: 1em;
span {
padding: 0.25em 0.3em;
}
span:first-child {
background-color: #4d4d4d;
}
span:last-child {
background-color: $accentCol;
color: black;
font-weight: bold;
}
}
@include smallScreen() {
.stats_card {
text-align: center;
left: 50%;
transform: translateX(-50%);
border-radius: 0 0 1em 1em;
}
}
</style> </style>
-140
View File
@@ -1,140 +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 { DriverStatsAPIData } from '@/scripts/interfaces/api/DriverStatsAPIData';
import { TimetableHistory } from '@/scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '@/scripts/utils/apiURLs';
import { useStore } from '@/store/store';
import axios from 'axios';
import { defineComponent } from 'vue';
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, JournalSearcher, provide, reactive, Ref, ref, watch } from 'vue';
import axios from 'axios';
import SearchBox from '@/components/Global/SearchBox.vue';
import dateMixin from '@/mixins/dateMixin';
import { DataStatus } from '@/scripts/enums/DataStatus';
import ActionButton from '@/components/Global/ActionButton.vue';
import JournalOptions from '@/components/JournalView/JournalOptions.vue';
import DispatcherStats from '@/components/JournalView/DispatcherStats.vue';
import { URLs } from '@/scripts/utils/apiURLs';
import { useStore } from '@/store/store';
import { DispatcherStatsAPIData } from '@/scripts/interfaces/api/DispatcherStatsAPIData';
import Loading from '../Global/Loading.vue';
import { useRoute, useRouter } from 'vue-router';
const PROD_MODE = process.env.VUE_APP_JORUNAL_DISPATCHERS_DEV != '1' || process.env.NODE_ENV === 'production';
const DISPATCHERS_API_URL = (PROD_MODE ? `${URLs.stacjownikAPI}/api` : 'http://localhost:3001/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;
}
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: () => ({
icons: {
arrow: require('@/assets/icon-arrow-asc.svg'),
},
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([
{ id: 'search-dispatcher', value: '' },
{ id: 'search-station', value: '' },
]);
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[1].value = this.sceneryName?.toString() || '';
this.searchersValues[0].value = 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?: JournalSearcher[];
filter?: JournalFilter;
} = {}
) {
this.historyDataStatus.status = DataStatus.Loading;
const queries: string[] = [];
const dispatcher = props.searchers?.find((s) => s.id == 'search-dispatcher')?.value.trim();
const station = props.searchers?.find((s) => s.id == 'search-station')?.value.trim();
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
if (station) queries.push(`stationName=${station}`);
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom');
else if (this.sorterActive.id == 'duration') queries.push('sortBy=currentDuration');
else queries.push('sortBy=timestampFrom');
queries.push('countLimit=15');
this.currentQuery = queries.join('&');
try {
const responseData: DispatcherHistoryItem[] = await (
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`)
).data;
if (!responseData) {
this.historyDataStatus.status = DataStatus.Error;
this.historyDataStatus.error = 'Brak danych!';
return;
}
if (!responseData) return;
// Response data exists
this.historyList = responseData;
// Stats display
this.store.dispatcherStatsName =
this.historyList.length > 0 && this.searchersValues[0].value.trim() ? this.historyList[0].dispatcherName : '';
this.historyDataStatus.status = DataStatus.Loaded;
} catch (error) {
this.historyDataStatus.status = DataStatus.Error;
this.historyDataStatus.error = 'Ups! Coś poszło nie tak!';
}
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/JournalSection.scss';
@import '../../styles/responsive.scss';
.region-badge {
padding: 0.1em 0.5em;
border-radius: 0.5em;
font-weight: bold;
&.eu {
background-color: forestgreen;
}
}
.list-wrapper {
margin-top: 1em;
}
.journal_item {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
&.online {
cursor: pointer;
}
span[data-status='true'] {
color: springgreen;
}
span[data-status='false'] {
color: salmon;
}
}
.journal_day {
position: relative;
text-align: center;
background-color: #4d4d4d;
span {
position: relative;
background-color: #4d4d4d;
z-index: 10;
padding: 0 0.5em;
}
&::after {
position: absolute;
content: '';
z-index: 0;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
height: 3px;
width: 60%;
min-width: 200px;
background-color: white;
}
}
@include smallScreen() {
.journal_item {
flex-direction: column;
span {
margin-top: 0.25em;
text-align: center;
}
}
}
</style>
@@ -0,0 +1,239 @@
<template>
<div>
<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="dispatcherHistory.length == 0">
{{ $t('app.no-result') }}
</div>
<div v-else>
<table class="scenery-history-table">
<thead>
<th>{{ $t('journal.history-name') }}</th>
<th>{{ $t('journal.history-hash') }}</th>
<th>{{ $t('journal.history-dispatcher') }}</th>
<th>{{ $t('journal.history-level') }}</th>
<th>{{ $t('journal.history-rate') }}</th>
<th>{{ $t('journal.history-region') }}</th>
<th>{{ $t('journal.history-date') }}</th>
</thead>
<tbody>
<transition-group name="list-anim">
<tr v-for="historyItem in dispatcherHistory" :key="historyItem.id">
<td>
<router-link :to="`/journal/dispatchers?sceneryName=${historyItem.stationName}`">
<b>{{ historyItem.stationName }}</b>
</router-link>
</td>
<td>#{{ historyItem.stationHash }}</td>
<td>
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
<b>{{ historyItem.dispatcherName }}</b>
</router-link>
</td>
<td>
<b
v-if="historyItem.dispatcherLevel !== null"
class="level-badge dispatcher"
:style="calculateExpStyle(historyItem.dispatcherLevel, historyItem.dispatcherIsSupporter)"
>
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b>
</td>
<td class="text--primary">
<b>{{ historyItem.dispatcherRate }}</b>
</td>
<td>
<b class="region-badge" :aria-describedby="historyItem.region">{{
regions.find((r) => r.id == historyItem.region)?.value || '???'
}}</b>
</td>
<td style="min-width: 200px" class="time">
<span v-if="historyItem.timestampTo" class="text--offline">
<b>{{ $d(historyItem.timestampFrom) }}</b>
{{ timestampToString(historyItem.timestampFrom) }}
- {{ timestampToString(historyItem.timestampTo) }} ({{
calculateDuration(historyItem.currentDuration)
}})
</span>
<span class="dispatcher-online" v-else>
<b class="text--online">
<router-link :to="`/scenery?station=${historyItem.stationName}`">{{
$t('journal.online-since')
}}</router-link>
{{ timestampToString(historyItem.timestampFrom) }}
</b>
({{ calculateDuration(historyItem.currentDuration) }})
</span>
</td>
</tr>
</transition-group>
</tbody>
</table>
<button
class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded && dispatcherHistory.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>
</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';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue';
import { regions } from '../../data/options.json';
export default defineComponent({
components: { Loading },
mixins: [dateMixin, styleMixin, imageMixin],
props: {
dispatcherHistory: {
type: Array as PropType<DispatcherHistory[]>,
required: true,
},
scrollNoMoreData: {
type: Boolean,
},
scrollDataLoaded: {
type: Boolean,
},
addHistoryData: {
type: Function as PropType<() => void>,
},
dataStatus: {
type: Number as PropType<DataStatus>,
},
},
data() {
return {
DataStatus,
store: useStore(),
regions,
};
},
computed: {
computedDispatcherHistory() {
console.log(this.dispatcherHistory.length);
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/variables.scss';
@import '../../styles/JournalSection.scss';
table.scenery-history-table {
--_bg-table: #111;
--_bg-head: #101010;
--_bg-row: #2f2f2f;
width: 100%;
border-collapse: collapse;
position: relative;
text-align: center;
thead {
position: sticky;
top: 0;
background-color: var(--_bg-head);
}
th {
padding: 0.5em;
}
tr {
background-color: var(--_bg-row);
border-bottom: 2px solid black;
&:last-child {
border: none;
}
}
td {
padding: 0.75em;
.level-badge {
margin: 0 auto;
}
}
@media screen and (max-width: 550px) {
font-size: 0.9em;
}
}
.text {
&--online {
color: springgreen;
}
&--offline {
color: #ddd;
}
}
</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>
+242 -202
View File
@@ -1,78 +1,122 @@
<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">
<select-box
:itemList="translatedSorterOptions"
:defaultItemIndex="0"
@selected="onSorterChange"
:prefix="$t('journal.sort-prefix')"
/>
</div>
<div class="content_search"> <div class="actions-bar">
<div class="search-box" v-for="search in searchersValues" :key="search.id"> <button class="filter-button btn--filled btn--image" @click="showOptions = !showOptions" ref="button">
<input <img :src="getIcon('filter2')" alt="Open filters" />
class="search-input" {{ $t('options.filters') }} [F]
:placeholder="$t(`journal.${search.id}`)" <span class="active-indicator" v-if="currentOptionsActive"></span>
v-model="search.value" </button>
@keydown.enter="onInputSearch"
/>
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="onInputClear(search.id)" /> <button class="filter-button btn--filled btn--image" @click="refreshData">
</div> <img :src="getIcon('refresh')" alt="Refresh data" />
<!-- <div class="search-box"> {{ $t('general.refresh') }}
<input </button>
class="search-input"
v-model="searchedTrain"
:placeholder="$t('journal.search-train')"
@keydown.enter="search"
/>
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearTrain" />
</div>
<div class="search-box">
<input
class="search-input"
v-model="searchedDriver"
:placeholder="$t('journal.search-driver')"
@keydown.enter="search"
/>
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearDriver" />
</div> -->
<action-button class="search-button" @click="onInputSearch">
{{ $t('journal.search') }}
</action-button>
</div>
</div>
<div class="options_filters">
<button
v-for="filter in filters"
class="journal-filter-option btn--option"
:class="{ checked: journalFilterActive.id === filter.id }"
:id="filter.id"
@click="onFilterChange(filter)"
>
{{ $t(`journal.filter-${filter.id}`) }}
</button>
</div>
</div> </div>
<datalist id="search-driver">
<option v-for="sugg in driverSuggestions" :value="sugg"></option>
</datalist>
<datalist id="search-dispatcher">
<option v-for="sugg in dispatcherSuggestions" :value="sugg"></option>
</datalist>
<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" v-for="(_, propName) in searchersValues" :key="propName">
<label v-if="propName == 'search-date'" for="date">{{ $t(`options.search-${optionsType}-date`) }}</label>
<div class="search-box">
<input
class="search-input"
v-model="searchersValues[propName]"
@keydown.enter="onSearchConfirm"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
:placeholder="$t(`options.${propName}`)"
:type="propName == 'search-date' ? 'date' : 'text'"
:min="propName == 'search-date' ? '2022-02-01' : undefined"
:list="propName.toString()"
/>
<button class="search-exit" v-if="propName != 'search-date'">
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" />
</button>
</div>
</div>
</div>
<h1 class="option-title">{{ $t('options.sort-title') }}</h1>
<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>
<h1 class="option-title" v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1>
<div class="options_filter-sections" v-if="filters.length != 0 && filterList">
<section class="filter-section" v-for="section in JournalFilterSection">
<p>{{ $t(`options.filter-section-${section}`) }}</p>
<div class="options_filters">
<button
v-for="filter in filterList.filter((f) => f.filterSection == section)"
class="filter-option btn--option"
:class="{ checked: filter.isActive }"
:id="filter.id"
@click="onFilterChange(filter)"
>
{{ $t(`options.filter-${filter.id}`) }}
</button>
</div>
</section>
</div>
<div class="options_actions">
<button class="btn--action" @click="onResetButtonClick">
{{ $t('options.reset-button') }}
</button>
<button class="btn--action" @click="onSearchButtonConfirm">
{{ $t('options.search-button') }}
</button>
</div>
</div>
</div>
</transition>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, inject, JournalFilter, PropType } from 'vue'; import axios from 'axios';
import { defineComponent, inject, PropType } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import keyMixin from '../../mixins/keyMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store';
import ActionButton from '../Global/ActionButton.vue'; import ActionButton from '../Global/ActionButton.vue';
import SelectBox from '../Global/SelectBox.vue'; import SelectBox from '../Global/SelectBox.vue';
import { JournalFilterSection } from '../../scripts/enums/JournalFilterType';
import { JournalFilter } from '../../scripts/types/JournalTimetablesTypes';
export default defineComponent({ export default defineComponent({
components: { SelectBox, ActionButton }, components: { SelectBox, ActionButton },
emits: ['onSorterChange', 'onInputChange', 'onFilterChange'], emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'],
mixins: [imageMixin, keyMixin],
props: { props: {
sorterOptionIds: { sorterOptionIds: {
type: Array as PropType<Array<string>>, type: Array as PropType<Array<string>>,
@@ -83,178 +127,174 @@ export default defineComponent({
type: Array as PropType<JournalFilter[]>, type: Array as PropType<JournalFilter[]>,
default: [], default: [],
}, },
dataStatus: {
type: Number as PropType<DataStatus>,
default: DataStatus.Initialized,
},
currentOptionsActive: {
type: Boolean,
default: false,
},
optionsType: {
type: String,
required: true,
},
}, },
data: () => ({ data() {
exitIcon: require('@/assets/icon-exit.svg'), return {
}), showOptions: false,
JournalFilterSection,
driverSuggestions: [] as string[],
dispatcherSuggestions: [] as string[],
searchTimeout: 0,
store: useStore(),
DataStatus,
};
},
setup() { setup() {
return { return {
searchersValues: inject('searchersValues') as {id: string; value: string}[], searchersValues: inject('searchersValues') as { [key: string]: string },
sorterActive: inject('sorterActive') as { id: string | number; dir: number }, sorterActive: inject('sorterActive') as { id: string | number; dir: number },
journalFilterActive: inject('journalFilterActive') as JournalFilter, // journalFilterActive: inject('journalFilterActive') as JournalFilter,
filterList: inject('filterList') as JournalFilter[] | undefined,
}; };
}, },
computed: { computed: {
driverStatsName() {
return this.store.driverStatsName;
},
translatedSorterOptions() { translatedSorterOptions() {
return this.$props.sorterOptionIds.map((id) => ({ return this.$props.sorterOptionIds.map((id) => ({
id, id,
value: this.$t(`journal.option-${id}`), value: this.$t(`options.sort-${id}`),
})); }));
}, },
}, },
watch: {
async driverStatsName(value: string) {
await this.fetchDriverStats();
// if (value) this.store.currentStatsTab = 'driver';
},
async 'searchersValues.search-driver'(value: string | undefined) {
clearTimeout(this.searchTimeout);
if (!value || value == '') return;
if (value.length < 3) return;
this.startSearchTimeout('driver', value);
},
async 'searchersValues.search-dispatcher'(value: string | undefined) {
if (!value || value == '') return;
if (value.length < 3) return;
this.startSearchTimeout('dispatcher', value);
},
},
methods: { methods: {
async fetchDriverStats() {
this.store.driverStatsData = undefined;
if (!this.store.driverStatsName) {
this.store.driverStatsStatus = DataStatus.Initialized;
return;
}
try {
this.store.driverStatsStatus = DataStatus.Loading;
const statsData: DriverStatsAPIData = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`)
).data;
this.store.driverStatsData = statsData;
this.store.driverStatsStatus = DataStatus.Loaded;
} catch (error) {
this.store.driverStatsStatus = DataStatus.Error;
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/');
}
},
refreshData() {
this.$emit('onRefreshData');
},
startSearchTimeout(type: 'driver' | 'dispatcher', value: string) {
if (this[`${type}Suggestions`].includes(value)) return;
window.clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(async () => {
try {
const suggestions: string[] = await (
await axios.get(`${URLs.stacjownikAPI}/api/get${type}Suggestions?name=${value}`)
).data;
this[`${type}Suggestions`] = suggestions;
} catch (error) {
this[`${type}Suggestions`] = [];
}
}, 450);
},
// Override keyMixin function
onKeyDownFunction() {
this.showOptions = !this.showOptions;
this.$nextTick(() => {
if (this.showOptions) (this.$refs['button'] as HTMLButtonElement)?.focus();
});
},
onSorterChange(item: { id: string | number; value: string }) { 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;
this.$emit('onSearchConfirm');
this.$emit('onSorterChange');
}, },
onFilterChange(filter: JournalFilter) { onFilterChange(filter: JournalFilter) {
this.journalFilterActive = filter; // this.journalFilterActive = filter;
this.$emit('onFilterChange'); this.filterList?.filter((f) => f.filterSection === filter.filterSection).forEach((f) => (f.isActive = false));
filter.isActive = true;
this.$emit('onSearchConfirm');
}, },
onInputSearch() { onInputClear(id: any) {
this.$emit('onInputChange'); this.searchersValues[id] = '';
this.$emit('onSearchConfirm');
}, },
onInputClear(id: string) { onSearchConfirm() {
this.searchersValues.find(s => s.id == id)!.value = ""; this.$emit('onSearchConfirm');
this.onInputSearch(); },
onSearchButtonConfirm() {
this.showOptions = false;
this.$emit('onSearchConfirm');
},
onResetButtonClick() {
this.$emit('onOptionsReset');
}, },
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive'; @import '../../styles/filters_options.scss';
@import '../../styles/option.scss';
.options {
&_wrapper {
display: flex;
flex-direction: column;
}
&_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;
}
}
&_filters {
display: flex;
flex-wrap: wrap;
margin: 0.5em 0 0 0;
.journal-filter-option {
margin: 0 0.25em 0 0;
&#abandoned {
color: salmon;
}
&#fulfilled {
color: lightgreen;
}
&#active {
color: lightblue;
}
}
}
}
.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;
}
}
@include smallScreen() {
.journal-options {
width: 100%;
}
.options {
&_wrapper {
justify-content: center;
align-items: center;
}
&_content {
padding: 0 1em;
flex-direction: column;
.content_select {
margin: 0 auto;
padding: 0;
}
.content_search {
justify-content: center;
}
}
&_filters {
justify-content: center;
.journal-filter-option {
margin: 0.25em 0.25em;
}
}
}
.search {
&-box,
&-button {
margin: 0.5em 0 0 0;
}
&-box {
width: 100%;
}
&-button {
width: 80%;
max-width: 300px;
}
}
}
</style> </style>
+118
View File
@@ -0,0 +1,118 @@
<template>
<div class="journal-stats" v-if="!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"
:data-disabled="tab.inactive"
:disabled="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'" @toggleStatsOpen="toggleStatsOpen" />
<JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" />
</keep-alive>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, KeepAlive, onMounted, reactive, Ref, ref, watch } from 'vue';
import { useStore } from '../../store/store';
import JournalDailyStats from './DailyStats.vue';
import JournalDriverStats from './JournalDriverStats.vue';
import StorageManager from '../../scripts/managers/storageManager';
// Types
type TStatTab = 'daily' | 'driver';
// Variables
const store = useStore();
const lastDailyStatsOpen = ref(false);
const areStatsOpen = ref(false);
const lastClickedTab: Ref<'daily' | 'driver' | null> = ref(null);
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') {
StorageManager.setBooleanValue('dailyStatsOpen', areStatsOpen.value);
lastDailyStatsOpen.value = areStatsOpen.value;
}
store.currentStatsTab = tab;
lastClickedTab.value = tab;
if (areStatsOpen.value == false) store.currentStatsTab = null;
}
function toggleStatsOpen(open: boolean) {
areStatsOpen.value = open;
}
watch(
computed(() => store.driverStatsData),
(statsData) => {
store.currentStatsTab = statsData ? 'driver' : lastClickedTab.value;
areStatsOpen.value = statsData ? true : lastClickedTab.value !== null;
}
);
onMounted(() => {
if (StorageManager.getBooleanValue('dailyStatsOpen')) {
areStatsOpen.value = true;
store.currentStatsTab = 'daily';
}
});
</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,451 +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"
/>
<!-- <button
class="btn btn--option"
:disabled="store.driverStatsName == ''"
@click="() => (statsCardOpen = !statsCardOpen)"
>
<span v-if="store.driverStatsName">
Statystyki maszynisty <b>{{ store.driverStatsName }}</b>
</span>
<span v-else>Statystyki maszynisty niedostępne</span>
</button> -->
</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, JournalSearcher, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios';
import SearchBox from '@/components/Global/SearchBox.vue';
import dateMixin from '@/mixins/dateMixin';
import { DataStatus } from '@/scripts/enums/DataStatus';
import ActionButton from '@/components/Global/ActionButton.vue';
import JournalOptions from '@/components/JournalView/JournalOptions.vue';
import { URLs } from '@/scripts/utils/apiURLs';
import { journalTimetableFilters } from '@/data/journalFilters';
import { JournalFilterType } from '@/scripts/enums/JournalFilterType';
import routerMixin from '@/mixins/routerMixin';
import { useStore } from '@/store/store';
import DriverStats from './DriverStats.vue';
import { TimetableHistory } from '@/scripts/interfaces/api/TimetablesAPIData';
import Loading from '../Global/Loading.vue';
const PROD_MODE = process.env.VUE_APP_JOURNAL_TIMETABLES_DEV != '1' || process.env.NODE_ENV === 'production';
const TIMETABLES_API_URL = PROD_MODE
? `${URLs.stacjownikAPI}/api/getTimetables`
: 'http://localhost:3001/api/getTimetables';
export default defineComponent({
components: { SearchBox, ActionButton, JournalOptions, DriverStats, Loading },
mixins: [dateMixin, routerMixin],
name: 'JournalTimetables',
data: () => ({
icons: {
arrow: require('@/assets/icon-arrow-asc.svg'),
},
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([
{ id: 'search-train', value: '' },
{ id: 'search-driver', value: '' },
]);
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);
},
mounted() {
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?: JournalSearcher[];
filter?: JournalFilter;
} = {}
) {
this.historyDataStatus.status = DataStatus.Loading;
const queries: string[] = [];
const driver = props.searchers?.find((s) => s.id == 'search-driver')?.value.trim();
const train = props.searchers?.find((s) => s.id == 'search-train')?.value.trim();
if (driver) queries.push(`driverName=${driver}`);
if (train) queries.push(`trainNo=${train}`);
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance');
else if (this.sorterActive.id == 'total-stops') queries.push('sortBy=allStopsCount');
else if (this.sorterActive.id == 'beginDate') queries.push('sortBy=beginDate');
else queries.push('sortBy=timetableId');
queries.push('countLimit=15');
switch (props.filter?.id) {
case JournalFilterType.abandoned:
queries.push('fulfilled=0', 'terminated=1');
break;
case JournalFilterType.active:
queries.push('terminated=0');
break;
case JournalFilterType.fulfilled:
queries.push('fulfilled=1');
break;
default:
break;
}
this.currentQuery = queries.join('&');
try {
const responseData: TimetableHistory[] = await (
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}`)
).data;
if (!responseData) {
this.historyDataStatus.status = DataStatus.Error;
this.historyDataStatus.error = 'Brak danych!';
return;
}
// if (responseData) {
// this.historyDataStatus.status = DataStatus.Error;
// this.historyDataStatus.error = responseData;
// return;
// }
if (!responseData) return;
// Response data exists
this.historyList = responseData;
// Stats display
this.store.driverStatsName =
this.historyList.length > 0 && this.searchersValues[1].value.trim() ? this.historyList[0].driverName : '';
this.historyDataStatus.status = DataStatus.Loaded;
} catch (error) {
this.historyDataStatus.status = DataStatus.Error;
this.historyDataStatus.error = 'Ups! Coś poszło nie tak!';
console.error(error);
}
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/JournalSection.scss';
.journal_item {
&-top {
display: flex;
justify-content: space-between;
padding: 0.2em 0;
.scenery-list {
span {
color: #adadad;
&.confirmed {
color: #a3eba3;
}
}
}
}
&-status {
&.terminated {
color: salmon;
}
&.fulfilled {
color: lightgreen;
}
&.active {
color: lightblue;
}
}
}
.dispatcher-link {
font-weight: bold;
}
</style>
@@ -0,0 +1,549 @@
<template>
<div class="journal-list">
<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>
<transition-group tag="ul" name="list-anim">
<li
v-for="{ timetable, stockHistoryComp, stops, showExtraInfo, ...item } in computedTimetableHistory"
class="journal_item"
:key="timetable.id"
@click="showExtraInfo.value = !showExtraInfo.value"
>
<div class="journal_item-info">
<div class="info-general">
<span
class="general-train"
tabindex="0"
@click.stop="showTimetable(timetable, $event.currentTarget)"
@keydown.enter="showTimetable(timetable, $event.currentTarget)"
>
<span class="text--grayed">#{{ timetable.id }}</span>
<span class="badges" v-if="timetable.skr || timetable.twr">
<span class="train-badge twr" v-if="timetable.twr" :title="$t('general.TWR')">TWR</span>
<span class="train-badge skr" v-if="timetable.skr" :title="$t('general.SKR')">SKR</span>
</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"
>{{
new Date(timetable.createdAt).getTime() - new Date(timetable.beginDate).getTime() < 0
? localeDateTime(timetable.createdAt, $i18n.locale)
: localeDateTime(timetable.beginDate, $i18n.locale)
}}
</b>
<b
class="info-badge"
:class="{
fulfilled: timetable.fulfilled,
terminated: timetable.terminated && !timetable.fulfilled,
active: !timetable.terminated,
}"
>
{{
!timetable.terminated
? $t('journal.timetable-active')
: timetable.fulfilled
? $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 />
<!-- Spis postojów -->
<div class="stop-list" v-if="showExtraInfo.value == true">
<span
v-for="(stop, i) in stops.filter((_, i) =>
!showExtraInfo.value ? i == 0 || i == stops.length - 1 : true
)"
class="stop-list-item"
:key="stop.stopName"
:data-confirmed="stop.confirmed"
>
<span v-if="i > 0">
&gt;
<span v-if="!showExtraInfo.value && i == 1 && stops.length > 2">
... (+{{ stops.length - 2 }}) &gt;
</span>
</span>
<span class="stop-name">{{ stop.stopName }}</span>
<span v-html="stop.html"></span>
</span>
</div>
<!-- Status RJ -->
<div class="info-status" style="margin: 0.5em 0">
<ProgressBar
:progressPercent="~~((timetable.currentDistance / timetable.routeDistance) * 100)"
:progressType="!timetable.fulfilled && timetable.terminated ? 'abandoned' : ''"
/>
<span>
<span :style="{ color: timetable.fulfilled ? 'lightgreen' : timetable.terminated ? 'salmon' : '' }">
{{ timetable.currentDistance + ' km' }}
</span>
<span> / </span>
<span class="text--primary">{{ timetable.routeDistance }} km</span>
|
<span class="text--grayed">{{ timetable.confirmedStopsCount }}/{{ timetable.allStopsCount }}</span>
</span>
<span class="text--grayed" v-if="timetable.currentSceneryName">
<b>
{{ $t(`journal.${timetable.terminated ? 'last-seen-at' : 'currently-at'}`) }}
{{ timetable.currentSceneryName.replace(/.[a-zA-Z0-9]+.sc/, '') }}
<span v-if="timetable.currentLocation[0] || timetable.currentLocation[1]">&lpar;</span>
<span v-if="timetable.currentLocation[1]">
{{ $t('journal.timetable-location-route') }} {{ timetable.currentLocation[1] }}
</span>
<span v-else-if="timetable.currentLocation[0]">
{{ $t('journal.timetable-location-signal') }} {{ timetable.currentLocation[0] }}
</span>
<span v-if="timetable.currentLocation[0] || timetable.currentLocation[1]">&rpar;</span>
</b>
</span>
</div>
<button class="btn--option btn--show">
{{ $t('journal.stock-info') }}
<img :src="getIcon(`arrow-${showExtraInfo.value ? 'asc' : 'desc'}`)" alt="Arrow" />
</button>
<!-- Dodatkowe informacje -->
<div class="info-extended" v-if="timetable.stockString && timetable.stockMass && showExtraInfo.value">
<hr />
<div class="stock-specs">
<span class="badge specs-badge">
<span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetable.authorName }}</span>
</span>
</div>
<div class="stock-specs">
<span class="badge specs-badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge specs-badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{
item.currentHistoryIndex.value == 0
? timetable.stockLength
: stockHistoryComp[item.currentHistoryIndex.value].stockLength || timetable.stockLength
}}m
</span>
</span>
<span class="badge specs-badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor(
(item.currentHistoryIndex.value == 0
? timetable.stockMass!
: stockHistoryComp[item.currentHistoryIndex.value].stockMass || timetable.stockMass) /
1000
)
}}t
</span>
</span>
</div>
<!-- Historia zmian w składzie -->
<div class="stock-history" v-if="stockHistoryComp.length > 1">
<button
class="btn--action"
v-for="(sh, i) in stockHistoryComp"
:data-checked="i == item.currentHistoryIndex.value"
@click.stop="item.currentHistoryIndex.value = i"
>
{{ sh.updatedAt }}
</button>
</div>
<ul class="stock-list">
<li
v-for="(car, i) in (item.currentHistoryIndex.value == 0
? timetable.stockString
: stockHistoryComp[item.currentHistoryIndex.value].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>
<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>
</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 { DataStatus } from '../../scripts/enums/DataStatus';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue';
import ProgressBar from '../Global/ProgressBar.vue';
export default defineComponent({
components: { ProgressBar, Loading },
mixins: [dateMixin, imageMixin, modalTrainMixin, styleMixin],
props: {
timetableHistory: {
type: Array as PropType<TimetableHistory[]>,
required: true,
},
scrollNoMoreData: {
type: Boolean,
},
scrollDataLoaded: {
type: Boolean,
},
addHistoryData: {
type: Function as PropType<() => void>,
},
dataStatus: {
type: Number as PropType<DataStatus>,
},
},
data() {
return {
DataStatus,
store: useStore(),
};
},
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
stockHistoryComp: timetable.stockHistory
.slice()
.reverse()
.map((h) => {
const historyData = h.split('@');
return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
hour: '2-digit',
minute: '2-digit',
}),
stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined,
};
}),
showExtraInfo: ref(false),
stops: this.getTimetableStops(timetable),
currentHistoryIndex: ref(0),
}));
},
},
methods: {
getTimetableStops(timetable: TimetableHistory) {
const stopNames = timetable.sceneriesString.split('%');
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.endDate, this.$i18n.locale)}</s>`
: ''
} <span>${this.localeTime(timetable.scheduledEndDate, this.$i18n.locale)}</span>)`;
return stopNames.map((stopName, i) => {
const confirmed = i < timetable.confirmedStopsCount;
if (i == 0) return { stopName, html: beginDateHTML, confirmed };
if (i == stopNames.length - 1) return { stopName, html: endDateHTML, confirmed };
const departureDateScheduled = this.stringToDate(timetable.checkpointDeparturesScheduled?.at(i));
const departureDateReal = this.stringToDate(timetable.checkpointDepartures?.at(i));
const arrivalDateScheduled = this.stringToDate(timetable.checkpointArrivalsScheduled?.at(i));
const arrivalDateReal = this.stringToDate(timetable.checkpointArrivals?.at(i));
const arrivalHTML =
(arrivalDateReal && arrivalDateScheduled && arrivalDateReal?.getTime() != arrivalDateScheduled?.getTime()
? `<s class="text--grayed">${this.parseDateToTimeString(arrivalDateScheduled)}</s> `
: '') + this.parseDateToTimeString(arrivalDateReal || arrivalDateScheduled);
const departureHTML =
(departureDateReal &&
departureDateScheduled &&
departureDateReal?.getTime() != departureDateScheduled?.getTime()
? `<s class="text--grayed">${this.parseDateToTimeString(departureDateScheduled)}</s> `
: '') + this.parseDateToTimeString(departureDateReal || departureDateScheduled);
let html = `${arrivalHTML}${departureHTML ? ` / ${departureHTML}` : ''}`;
if (html) html = ` (${html})`;
return { stopName, html, confirmed };
});
},
showTimetable(timetable: TimetableHistory, target: EventTarget | null) {
if (timetable?.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target);
},
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';
.journal_item {
cursor: pointer;
}
hr {
margin: 0.25em 0;
}
.info {
&-date {
margin-right: 0.5em;
}
&-badge {
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;
}
&-status {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
}
}
.general-train {
cursor: pointer;
display: flex;
flex-wrap: wrap;
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;
}
li > img {
vertical-align: text-bottom;
max-height: 60px;
}
}
.stock-specs {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
.specs-badge {
margin: 0;
span:last-child {
color: black;
background-color: $accentCol;
}
}
}
// badge.scss
.badges {
display: flex;
gap: 0.25em;
}
.stock-history {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
button[data-checked='true'] {
color: $accentCol;
}
}
.stop-list {
word-wrap: break-word;
gap: 0.25em;
font-size: 0.95em;
color: #adadad;
&-item[data-confirmed='true'] {
color: lightgreen;
.stop-name {
font-weight: bold;
}
}
}
.btn--show {
display: flex;
font-weight: bold;
padding: 0.2em 0.45em;
img {
height: 1.3em;
}
}
@include smallScreen {
.journal_item-info {
text-align: center;
}
.info-route {
display: flex;
justify-content: center;
}
.info-status {
justify-content: center;
}
.btn--show {
margin: 1em auto 0 auto;
}
.info-general,
.general-train,
.stock-specs,
.stock-history {
justify-content: center;
}
}
</style>
@@ -1,76 +1,119 @@
<template> <template>
<section class="scenery-dispatchers-history scenery-section"> <section class="scenery-table-section">
<Loading v-if="dataStatus != 2" /> <Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
<div class="no-history" v-else-if="historyList.length == 0">{{ $t('scenery.history-list-empty') }}</div>
<div class="list-warning" v-else-if="dispatcherHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div> <table class="scenery-history-table" v-else="historyList.length">
<thead>
<th>{{ $t('scenery.dispatchers-history-hash') }}</th>
<th>{{ $t('scenery.dispatchers-history-dispatcher') }}</th>
<th>{{ $t('scenery.dispatchers-history-level') }}</th>
<th>{{ $t('scenery.dispatchers-history-rate') }}</th>
<th>{{ $t('scenery.dispatchers-history-date') }}</th>
</thead>
<ul class="history-list" v-else> <tbody>
<li class="list-item" v-for="historyItem in dispatcherHistoryList"> <tr v-for="historyItem in historyList">
<div> <td>#{{ historyItem.stationHash }}</td>
<span class="text--grayed">#{{ historyItem.stationHash }}&nbsp;</span> <td>
<b class="text--primary">{{ historyItem.dispatcherName }}</b> <router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
</div> <b>{{ historyItem.dispatcherName }}</b>
</router-link>
</td>
<td>
<b
v-if="historyItem.dispatcherLevel !== null"
class="level-badge dispatcher"
:style="calculateExpStyle(historyItem.dispatcherLevel, historyItem.dispatcherIsSupporter)"
>
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b>
</td>
<td class="text--primary">
<b>{{ historyItem.dispatcherRate }}</b>
</td>
<td style="min-width: 300px">
<div v-if="historyItem.timestampTo">
<b>{{ $d(historyItem.timestampFrom) }}</b>
<div v-if="historyItem.timestampTo"> {{ timestampToString(historyItem.timestampFrom) }}
<b>{{ $d(historyItem.timestampFrom) }}</b> - {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }})
</div>
{{ timestampToString(historyItem.timestampFrom) }} <div class="dispatcher-online" v-else>
- {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }}) {{ $t('journal.online-since') }}
</div> <b>{{ timestampToString(historyItem.timestampFrom) }}</b>
({{ calculateDuration(historyItem.currentDuration) }})
<div class="dispatcher-online" v-else> </div>
{{ $t('journal.online-since') }} </td>
<b>{{ timestampToString(historyItem.timestampFrom) }}</b> </tr>
({{ calculateDuration(historyItem.currentDuration) }}) </tbody>
<span></span> </table>
</div>
</li>
</ul>
</section> </section>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
{{ $t('scenery.bottom-info') }}
</button>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import dateMixin from '@/mixins/dateMixin';
import { DataStatus } from '@/scripts/enums/DataStatus';
import { DispatcherHistory } from '@/scripts/interfaces/api/DispatchersAPIData';
import Station from '@/scripts/interfaces/Station';
import { URLs } from '@/scripts/utils/apiURLs';
import axios from 'axios'; import axios from 'axios';
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
import Station from '../../scripts/interfaces/Station';
import { URLs } from '../../scripts/utils/apiURLs';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import styleMixin from '../../mixins/styleMixin';
import listObserverMixin from '../../mixins/listObserverMixin';
export default defineComponent({ export default defineComponent({
name: 'SceneryDispatchersHistory', name: 'SceneryDispatchersHistory',
mixins: [dateMixin], mixins: [dateMixin, styleMixin, listObserverMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station>, type: Object as PropType<Station>,
required: true, required: true,
}, },
}, },
data() { data() {
return { return {
dispatcherHistoryList: [] as DispatcherHistory[], historyList: [] as DispatcherHistory[],
dataStatus: DataStatus.Loading, dataStatus: DataStatus.Loading,
DataStatus,
}; };
}, },
mounted() {
this.fetchAPIData(); async activated() {
// if (this.historyList.length == 0) {
const fetchedHistory = await this.fetchAPIData();
if (fetchedHistory) this.historyList = fetchedHistory;
// }
}, },
methods: { methods: {
async fetchAPIData(countFrom = 0, countLimit = 30) { async fetchAPIData(countFrom = 0, countLimit = 30): Promise<DispatcherHistory[] | null> {
try { try {
this.dataStatus = DataStatus.Loading;
const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`; const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
const historyAPIData: DispatcherHistory[] = await (await axios.get(requestString)).data; const historyAPIData: DispatcherHistory[] = await (await axios.get(requestString)).data;
this.dispatcherHistoryList = historyAPIData;
this.dataStatus = DataStatus.Loaded; this.dataStatus = DataStatus.Loaded;
return historyAPIData;
console.log(this.dispatcherHistoryList);
} catch (error) { } catch (error) {
this.dataStatus = DataStatus.Error;
console.error(error); console.error(error);
return null;
} }
}, },
navigateToHistory() {
this.$router.push(`/journal/dispatchers?sceneryName=${this.station.name}`);
},
}, },
components: { Loading }, components: { Loading },
}); });
@@ -78,24 +121,10 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/SceneryView/styles.scss'; @import '../../styles/sceneryViewTables.scss';
.level-badge {
.history-list { margin: 0 auto;
padding: 0 0.5em;
}
.list-item {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
text-align: left;
background-color: #353535;
padding: 0.5em;
margin: 0.5em 0;
line-height: 1.5em;
} }
.dispatcher-online { .dispatcher-online {
@@ -103,10 +132,12 @@ export default defineComponent({
} }
@include smallScreen { @include smallScreen {
.history-list {
font-size: 1.1em;
}
.list-item { .list-item {
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
} }
} }
</style> </style>
+17 -16
View File
@@ -1,11 +1,11 @@
<template> <template>
<section class="info-header"> <section class="info-header">
<div class="scenery-name"> <a class="scenery-name" :href="station.generalInfo?.url" target="_blank">
<a v-if="station.generalInfo?.url" :href="station.generalInfo.url" target="_blank" rel="noopener noreferrer"> {{ station.name }}
{{ station.name }} </a>
</a>
<span v-else>{{ station.name }}</span> <div class="scenery-abbrev">
{{ $t('scenery.abbrev') }} <b>{{ station.generalInfo?.abbr }}</b>
</div> </div>
<div class="scenery-hash" v-if="station.onlineInfo?.hash">#{{ station.onlineInfo.hash }}</div> <div class="scenery-hash" v-if="station.onlineInfo?.hash">#{{ station.onlineInfo.hash }}</div>
@@ -14,8 +14,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import Station from '../../scripts/interfaces/Station';
import Station from '@/scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -31,24 +30,26 @@ export default defineComponent({
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
.info-header {
margin-top: 1em;
}
.scenery-name { .scenery-name {
font-weight: bold; font-weight: bold;
color: $accentCol; font-size: 3em;
position: relative;
font-size: 3.5em;
padding: 0 0.5em;
text-transform: uppercase; text-transform: uppercase;
}
@include smallScreen() { .scenery-abbrev {
font-size: 2.75em; font-size: 1.3em;
} color: #aaa;
} }
.scenery-hash { .scenery-hash {
margin-top: 0.5em;
color: #aaa; color: #aaa;
font-size: 1.2em; font-size: 1.2em;
} }
</style> </style>
+27 -50
View File
@@ -1,21 +1,16 @@
<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 v-if="station.generalInfo.reqLevel > 0">
- minimum {{ station.generalInfo.reqLevel }} poziom dyżurnego
</span>
<span v-else-if="station.generalInfo.reqLevel == 0">- dla wszystkich poziomów</span> -->
</span> </span>
<span> <span>
@@ -31,22 +26,34 @@
</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>
</div> </div>
<div style="margin: 2em 0; height: 2px; background-color: white" /> <div style="margin: 2em 0; height: 2px; background-color: white"></div>
<!-- info stats -->
<!-- <scenery-info-stats :station="station" /> -->
<!-- info dispatcher --> <!-- info dispatcher -->
<scenery-info-dispatcher :station="station" :onlineFrom="onlineFrom" /> <scenery-info-dispatcher :station="station" :onlineFrom="onlineFrom" />
@@ -57,10 +64,6 @@
<!-- spawn list --> <!-- spawn list -->
<scenery-info-spawn-list :station="station" /> <scenery-info-spawn-list :station="station" />
</div> </div>
<!-- info icons -->
<!-- info routes -->
</section> </section>
</div> </div>
</template> </template>
@@ -74,8 +77,7 @@ import SceneryInfoStats from './SceneryInfo/SceneryInfoStats.vue';
import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue'; import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue';
import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue'; import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue'; import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
import Station from '../../scripts/interfaces/Station';
import Station from '@/scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -103,6 +105,7 @@ export default defineComponent({
<style lang="scss"> <style lang="scss">
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/badge.scss';
h3.section-header { h3.section-header {
margin: 0.5em 0; margin: 0.5em 0;
@@ -112,7 +115,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;
@@ -128,12 +131,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;
@@ -143,32 +145,7 @@ h3.section-header {
} }
} }
.badge { .scenery-topic a {
font-weight: 600; font-weight: bold;
display: inline-block;
padding: 0;
background: #585858;
margin: 0.25em;
span {
display: inline-block;
padding: 0.2em 0.4em;
}
&-none {
font-weight: 600;
padding: 0.2em 0.4em;
background: firebrick;
text-align: center;
@include smallScreen() {
font-size: 1em;
}
}
} }
</style> </style>
@@ -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>
@@ -13,54 +16,41 @@
</router-link> </router-link>
<span class="dispatcher_likes text--primary"> <span class="dispatcher_likes text--primary">
<img :src="icons.like" alt="icon-like" /> <img :src="getIcon('like')" alt="icon-like" />
<span>{{ station.onlineInfo?.dispatcherRate || '0' }}</span> <span>{{ station.onlineInfo?.dispatcherRate || '0' }}</span>
</span> </span>
</div> </div>
<span class="status-badge" v-if="station.onlineInfo && onlineFrom > 0"> <StationStatusBadge
OD {{ new Date(onlineFrom).toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }) }} :statusID="station.onlineInfo?.statusID"
</span> :isOnline="station.onlineInfo ? true : false"
:statusTimestamp="station.onlineInfo?.statusTimestamp"
<span class="status-badge" v-if="station.onlineInfo" :class="station.onlineInfo.statusID"> />
{{ $t(`status.${station.onlineInfo.statusID}`) }}
{{ station.onlineInfo.statusID == 'online' ? timestampToString(station.onlineInfo.statusTimestamp) : '' }}
</span>
<span class="status-badge free" v-else>
{{ $t('status.free') }}
</span>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import dateMixin from '../../../mixins/dateMixin';
import styleMixin from '@/mixins/styleMixin'; import imageMixin from '../../../mixins/imageMixin';
import Station from '@/scripts/interfaces/Station'; import routerMixin from '../../../mixins/routerMixin';
import dateMixin from '@/mixins/dateMixin'; import styleMixin from '../../../mixins/styleMixin';
import routerMixin from '@/mixins/routerMixin'; import Station from '../../../scripts/interfaces/Station';
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
export default defineComponent({ export default defineComponent({
mixins: [styleMixin, dateMixin, routerMixin], mixins: [styleMixin, dateMixin, routerMixin, imageMixin],
props: { props: {
station: { station: {
type: Object as () => Station, type: Object as () => Station,
default: {}, default: {},
},
onlineFrom: {
type: Number,
default: -1,
},
}, },
components: { StationStatusBadge }
onlineFrom: {
type: Number,
default: -1,
},
},
data: () => ({
icons: {
spawn: require('@/assets/icon-spawn.svg'),
like: require('@/assets/icon-like.svg'),
},
}),
}); });
</script> </script>
@@ -71,6 +61,7 @@ export default defineComponent({
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5em;
.dispatcher { .dispatcher {
font-size: 2em; font-size: 2em;
@@ -89,17 +80,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;
} }
} }
@@ -109,3 +98,4 @@ export default defineComponent({
} }
} }
</style> </style>
@@ -20,7 +20,7 @@
<img <img
v-if="station.generalInfo?.SUP" v-if="station.generalInfo?.SUP"
class="icon-info" class="icon-info"
:src="require(`@/assets/icon-SUP.svg`)" :src="getIcon('SUP')"
alt="SUP (RASP-UZK)" alt="SUP (RASP-UZK)"
:title="$t('desc.SUP')" :title="$t('desc.SUP')"
/> />
@@ -28,7 +28,7 @@
<img <img
v-if="station.generalInfo?.signalType" v-if="station.generalInfo?.signalType"
class="icon-info" class="icon-info"
:src="require(`@/assets/icon-${station.generalInfo.signalType}.svg`)" :src="getIcon(station.generalInfo.signalType)"
:alt="station.generalInfo.signalType" :alt="station.generalInfo.signalType"
:title="$t('desc.signals-type') + $t(`signals.${station.generalInfo.signalType}`)" :title="$t('desc.signals-type') + $t(`signals.${station.generalInfo.signalType}`)"
/> />
@@ -36,7 +36,7 @@
<img <img
v-if="station.generalInfo?.availability == 'nonPublic'" v-if="station.generalInfo?.availability == 'nonPublic'"
class="icon-info" class="icon-info"
:src="icons.lock" :src="getIcon('lock')"
alt="Non-public scenery" alt="Non-public scenery"
:title="$t('desc.non-public')" :title="$t('desc.non-public')"
/> />
@@ -44,7 +44,7 @@
<img <img
v-if="station.generalInfo?.availability == 'unavailable'" v-if="station.generalInfo?.availability == 'unavailable'"
class="icon-info" class="icon-info"
:src="icons.unavailable" :src="getIcon('unavailable')"
alt="Unavailable scenery" alt="Unavailable scenery"
:title="$t('desc.unavailable')" :title="$t('desc.unavailable')"
/> />
@@ -52,7 +52,7 @@
<img <img
v-if="station.generalInfo?.availability == 'abandoned'" v-if="station.generalInfo?.availability == 'abandoned'"
class="icon-info" class="icon-info"
:src="icons.abandoned" :src="getIcon('abandoned')"
alt="Abandoned scenery" alt="Abandoned scenery"
:title="$t('desc.abandoned')" :title="$t('desc.abandoned')"
/> />
@@ -60,7 +60,7 @@
<img <img
v-if="station.generalInfo?.lines" v-if="station.generalInfo?.lines"
class="icon-info" class="icon-info"
:src="icons.real" :src="getIcon('real')"
alt="real scenery" alt="real scenery"
:title="`${$t('desc.real')} ${station.generalInfo.lines}`" :title="`${$t('desc.real')} ${station.generalInfo.lines}`"
/> />
@@ -68,7 +68,7 @@
<img <img
v-if="!station.generalInfo" v-if="!station.generalInfo"
class="icon-info" class="icon-info"
:src="icons.unknown" :src="getIcon('unknown')"
alt="icon-unknown" alt="icon-unknown"
:title="$t('desc.unknown')" :title="$t('desc.unknown')"
/> />
@@ -77,31 +77,19 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import stationInfoMixin from '@/mixins/stationInfoMixin'; import imageMixin from '../../../mixins/imageMixin';
import stationInfoMixin from '../../../mixins/stationInfoMixin';
import Station from '@/scripts/interfaces/Station'; import styleMixin from '../../../mixins/styleMixin';
import styleMixin from '@/mixins/styleMixin'; import Station from '../../../scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
mixins: [stationInfoMixin, styleMixin], mixins: [stationInfoMixin, styleMixin, imageMixin],
props: { props: {
station: { station: {
type: Object as () => Station, type: Object as () => Station,
default: {}, default: {},
}, },
}, },
data: () => ({
icons: {
td2: require('@/assets/icon-td2.svg'),
lock: require('@/assets/icon-lock.svg'),
unavailable: require('@/assets/icon-unavailable.svg'),
unknown: require('@/assets/icon-unknown.svg'),
abandoned: require('@/assets/icon-abandoned.svg'),
real: require('@/assets/icon-real.svg'),
},
}),
}); });
</script> </script>
@@ -130,3 +118,4 @@ export default defineComponent({
} }
} }
</style> </style>
@@ -4,12 +4,12 @@
<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" @click="setActiveShowLength(route.name)">
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">
> {{ activeShowLength.includes(route.name) ? route.length + 'm' : route.speed }}
{{ route.name }} </span>
<b v-if="route.SBL">SBL</b> <span v-if="route.SBL" class="sbl">SBL</span>
</li> </li>
</ul> </ul>
</div> </div>
@@ -18,47 +18,21 @@
<b>{{ $t('scenery.two-way-routes') }}</b> <b>{{ $t('scenery.two-way-routes') }}</b>
<ul class="routes-list"> <ul class="routes-list">
<li <li v-for="(route, i) in station.generalInfo.routes.twoWay" @click="setActiveShowLength(route.name)">
v-for="route in station.generalInfo.routes.twoWay" <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">
> {{ activeShowLength.includes(route.name) ? route.length + 'm' : route.speed }}
{{ route.name }} <b v-if="route.SBL">SBL</b> </span>
<span v-if="route.SBL" class="sbl">SBL</span>
</li> </li>
</ul> </ul>
</div> </div>
<!-- <div
class="route-info"
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"
v-for="route in [...station.generalInfo.routes.oneWay, ...station.generalInfo.routes.twoWay].filter(
(route) => route.name != '-'
)"
:key="route.name"
:title="`Szlak ${route.name}: ${route.isInternal ? 'wewnętrzny' : 'zewnętrzny'}, ${
route.tracks == 2 ? 'dwutorowy' : 'jednotorowy'
}, ${route.catenary ? 'zelektryfikowany' : 'niezelektryfikowany'} z ${route.SBL ? 'SBL' : 'PBL'} ${
route.TWB ? 'i blokadą dwukierunkową' : ''
}`"
> -->
<!-- <span class="track-name">
<b>{{ route.name }}</b>
</span> -->
<!--
<span class="track-specs">
{{ route.tracks }}tor
<img v-if="route.catenary" :src="icons.trackCatenary" alt="icon track catenary" />
<img v-else :src="icons.trackNoCatenary" alt="icon track no catenary" />
<img v-if="route.TWB" :src="icons.trackTWB" alt="icon track twb" />
<img v-if="route.SBL" :src="icons.trackSBL" alt="icon track sbl" />
</span> -->
<!-- </div> -->
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import Station from '@/scripts/interfaces/Station';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import Station from '../../../scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -67,6 +41,19 @@ export default defineComponent({
default: {}, default: {},
}, },
}, },
methods: {
setActiveShowLength(name: string) {
if (this.activeShowLength.includes(name)) this.activeShowLength.splice(this.activeShowLength.indexOf(name), 1);
else this.activeShowLength.push(name);
},
},
data() {
return {
activeShowLength: [] as string[],
};
},
}); });
</script> </script>
@@ -91,23 +78,51 @@ export default defineComponent({
ul.routes-list { ul.routes-list {
margin: 0.45em 0.25em; margin: 0.45em 0.25em;
display: flex; display: flex;
justify-content: center;
flex-wrap: wrap;
li { li {
background-color: #007599; margin: 0.5em 0.25em;
cursor: pointer;
padding: 0.2em 0.25em; user-select: none;
margin-left: 0.25em; -moz-user-select: none;
-webkit-user-select: none;
&.no-catenary { span {
background-color: #686868; padding: 0.2em 0.25em;
} background-color: #007599;
font-weight: bold;
&.internal { &.no-catenary {
text-decoration: underline; background-color: #686868;
} }
b { &.internal {
color: var(--clr-primary); text-decoration: underline;
}
&.speed {
background-color: #404040;
color: #cfcfcf;
}
&.sbl {
color: var(--clr-primary);
background-color: #404040;
}
&:last-child {
border-radius: 0 0.5em 0.5em 0;
}
&:first-child {
border-radius: 0.5em 0 0 0.5em;
}
&:only-child {
border-radius: 0.5em;
}
} }
} }
} }
@@ -1,7 +1,7 @@
<template> <template>
<section class="info-spawn-list"> <section class="info-spawn-list">
<h3 class="spawn-header section-header"> <h3 class="spawn-header section-header">
<img :src="icons.spawn" alt="icon-spawn" /> <img :src="getIcon('spawn')" alt="icon-spawn" />
&nbsp;{{ $t('scenery.spawns') }} &nbsp; &nbsp;{{ $t('scenery.spawns') }} &nbsp;
<span class="text--primary">{{ station.onlineInfo?.spawns.length || '0' }}</span> <span class="text--primary">{{ station.onlineInfo?.spawns.length || '0' }}</span>
</h3> </h3>
@@ -9,8 +9,9 @@
<span v-if="station.onlineInfo"> <span v-if="station.onlineInfo">
<span <span
class="badge spawn" class="badge spawn"
v-for="(spawn, i) in station.onlineInfo.spawns" v-for="(spawn, i) in sortedSpawns"
:key="spawn.spawnName + station.onlineInfo?.dispatcherName + i" :key="spawn.spawnName + station.onlineInfo?.dispatcherName + i"
:data-electrified="spawn.isElectrified"
> >
<span class="spawn_name">{{ spawn.spawnName }}</span> <span class="spawn_name">{{ spawn.spawnName }}</span>
<span class="spawn_length">{{ spawn.spawnLength }}m</span> <span class="spawn_length">{{ spawn.spawnLength }}m</span>
@@ -24,10 +25,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Station from '@/scripts/interfaces/Station';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import imageMixin from '../../../mixins/imageMixin';
import Station from '../../../scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
mixins: [imageMixin],
props: { props: {
station: { station: {
type: Object as () => Station, type: Object as () => Station,
@@ -35,11 +39,11 @@ export default defineComponent({
}, },
}, },
data: () => ({ computed: {
icons: { sortedSpawns() {
spawn: require('@/assets/icon-spawn.svg'), return this.station.onlineInfo?.spawns.sort((s1, s2) => (s1.spawnLength < s2.spawnLength ? 1 : -1));
}, },
}), },
}); });
</script> </script>
@@ -47,9 +51,15 @@ export default defineComponent({
@import '../../../styles/variables.scss'; @import '../../../styles/variables.scss';
.spawn { .spawn {
color: white;
&_length { &_length {
background: $accentCol; background-color: #404040;
color: black; color: #cfcfcf;
}
&[data-electrified='true'] > &_name {
background-color: #007599;
} }
} }
</style> </style>
@@ -1,24 +1,24 @@
<template> <template>
<section class="info-stats" :class="!station.onlineInfo ? 'no-stats' : ''"> <section class="info-stats" :class="!station.onlineInfo ? 'no-stats' : ''">
<span class="likes"> <span class="likes">
<img :src="icons.like" alt="icon-like" /> <img :src="getIcon('like')" alt="icon-like" />
<span>{{ station.onlineInfo?.dispatcherRate || '0' }}</span> <span>{{ station.onlineInfo?.dispatcherRate || '0' }}</span>
</span> </span>
<span class="users"> <span class="users">
<img :src="icons.user" alt="icon-user" /> <img :src="getIcon('user')" alt="icon-user" />
<span>{{ station.onlineInfo?.currentUsers || '0' }}</span> <span>{{ station.onlineInfo?.currentUsers || '0' }}</span>
/ /
<span>{{ station.onlineInfo?.maxUsers || '0' }}</span> <span>{{ station.onlineInfo?.maxUsers || '0' }}</span>
</span> </span>
<span class="spawns"> <span class="spawns">
<img :src="icons.spawn" alt="icon-spawn" /> <img :src="getIcon('spawn')" alt="icon-spawn" />
<span>{{ station.onlineInfo?.spawns.length || '0' }}</span> <span>{{ station.onlineInfo?.spawns.length || '0' }}</span>
</span> </span>
<span class="schedules"> <span class="schedules">
<img :src="icons.timetable" alt="icon-timetable" /> <img :src="getIcon('timetable')" alt="icon-timetable" />
<span> <span>
<span style="color: #eee">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span> <span style="color: #eee">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span>
/ /
@@ -32,25 +32,17 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import imageMixin from '../../../mixins/imageMixin';
import Station from '@/scripts/interfaces/Station'; import Station from '../../../scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
mixins: [imageMixin],
props: { props: {
station: { station: {
type: Object as () => Station, type: Object as () => Station,
default: {}, default: {},
}, },
}, },
data: () => ({
icons: {
like: require('@/assets/icon-like.svg'),
timetable: require('@/assets/icon-timetable.svg'),
user: require('@/assets/icon-user.svg'),
spawn: require('@/assets/icon-spawn.svg'),
},
}),
}); });
</script> </script>
@@ -1,7 +1,7 @@
<template> <template>
<section class="info-user-list"> <section class="info-user-list">
<h3 class="user-header section-header"> <h3 class="user-header section-header">
<img :src="icons.user" alt="icon-user" /> <img :src="getIcon('user')" alt="icon-user" />
&nbsp;{{ $t('scenery.users') }} &nbsp; &nbsp;{{ $t('scenery.users') }} &nbsp;
<span class="text--primary">{{ station.onlineInfo?.currentUsers || '0' }}</span <span class="text--primary">{{ station.onlineInfo?.currentUsers || '0' }}</span
>&nbsp;/&nbsp;<span class="text--primary">{{ station.onlineInfo?.maxUsers || '0' }}</span> >&nbsp;/&nbsp;<span class="text--primary">{{ station.onlineInfo?.maxUsers || '0' }}</span>
@@ -11,10 +11,10 @@
v-for="(train, i) in computedStationTrains" v-for="(train, i) in computedStationTrains"
class="badge user" class="badge user"
:class="train.stopStatus" :class="train.stopStatus"
:key="train.trainNo + i" :key="train.trainId"
tabindex="0" tabindex="0"
@click="navigateTo('/trains', { trainNo: train.trainNo, driverName: train.driverName })" @click.prevent="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="navigateTo('/trains', { trainNo: train.trainNo, driverName: train.driverName })" @keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
> >
<span class="user_train">{{ train.trainNo }}</span> <span class="user_train">{{ train.trainNo }}</span>
<span class="user_name">{{ train.driverName }}</span> <span class="user_name">{{ train.driverName }}</span>
@@ -27,12 +27,15 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import routerMixin from '@/mixins/routerMixin';
import Station from '@/scripts/interfaces/Station';
import { computed, defineComponent } from 'vue'; import { computed, defineComponent } from 'vue';
import imageMixin from '../../../mixins/imageMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
import routerMixin from '../../../mixins/routerMixin';
import Station from '../../../scripts/interfaces/Station';
import { useStore } from '../../../store/store';
export default defineComponent({ export default defineComponent({
mixins: [routerMixin], mixins: [routerMixin, imageMixin, modalTrainMixin],
props: { props: {
station: { station: {
@@ -42,6 +45,8 @@ export default defineComponent({
}, },
setup(props) { setup(props) {
const store = useStore();
const computedStationTrains = computed(() => { const computedStationTrains = computed(() => {
if (!props.station) return []; if (!props.station) return [];
@@ -59,14 +64,8 @@ export default defineComponent({
}); });
}); });
return { computedStationTrains }; return { computedStationTrains, store };
}, },
data: () => ({
icons: {
user: require('@/assets/icon-user.svg'),
},
}),
}); });
</script> </script>
+212 -269
View File
@@ -2,192 +2,205 @@
<section class="scenery-timetable"> <section class="scenery-timetable">
<div class="timetable-header"> <div class="timetable-header">
<h3> <h3>
<img :src="icons.timetable" alt="icon-timetable" />&nbsp; <img :src="getIcon('timetable')" alt="icon-timetable" />
<span>{{ $t('scenery.timetables') }}</span> <span>{{ $t('scenery.timetables') }}</span>
&nbsp;
<span class="text--primary">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span> <span>
<span>&nbsp;/&nbsp;</span> <span class="text--primary">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span>
<span class="text--grayed"> <span> / </span>
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }} <span class="text--grayed">
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }}
</span>
</span>
<span class="header_links">
<a
:href="`https://pragotron-td2.web.app/board?name=${station.name}`"
target="_blank"
:title="$t('scenery.pragotron-link')"
>
<img :src="getIcon('pragotron')" alt="icon-pragotron" />
</a>
<a
:href="`https://tablice-td2.web.app/?station=${station.name}`"
target="_blank"
:title="$t('scenery.tablice-link')"
>
<img :src="getIcon('tablice', 'ico')" alt="icon-tablice" />
</a>
</span> </span>
</h3> </h3>
<div class="timetable-checkpoints" v-if="station && station.generalInfo?.checkpoints"> <div class="timetable-checkpoints" v-if="station?.generalInfo?.checkpoints">
<button <span v-for="(cp, i) in station.generalInfo.checkpoints" :key="i">
v-for="cp in station.generalInfo.checkpoints" {{ (i > 0 && '&bull;') || '' }}
:key="cp.checkpointName"
class="checkpoint_item btn btn--text" <button
:class="{ current: selectedCheckpoint === cp.checkpointName }" :key="cp.checkpointName"
@click="selectCheckpoint(cp)" class="checkpoint_item"
> :class="{ current: selectedCheckpoint === cp.checkpointName }"
{{ cp.checkpointName }} @click="selectCheckpoint(cp)"
</button> >
{{ cp.checkpointName }}
</button>
</span>
</div> </div>
</div> </div>
<div class="timetable-list"> <div class="timetable-list">
<!-- <transition name="scenery-timetable-list-anim" mode="out-in"> -->
<!-- <div :key="store.dataStatuses.trains + selectedCheckpoint" class="scenery-timetable-list"> -->
<div style="padding-bottom: 5em" v-if="store.dataStatuses.trains == 0 && computedScheduledTrains.length == 0"> <div style="padding-bottom: 5em" v-if="store.dataStatuses.trains == 0 && computedScheduledTrains.length == 0">
<Loading /> <Loading />
</div> </div>
<span class="timetable-item empty" v-else-if="computedScheduledTrains.length == 0 && !station.onlineInfo">
{{ $t('scenery.offline') }}
</span>
<span class="timetable-item empty" v-else-if="computedScheduledTrains.length == 0"> <span class="timetable-item empty" v-else-if="computedScheduledTrains.length == 0">
{{ $t('scenery.no-timetables') }} {{ $t('scenery.no-timetables') }}
</span> </span>
<div <transition-group name="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="navigateTo('/trains', { trainNo: scheduledTrain.trainNo, driverName: scheduledTrain.driverName })" tabindex="0"
@keydown.enter=" @click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
navigateTo('/trains', { @keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
trainNo: scheduledTrain.trainNo, >
driverName: scheduledTrain.driverName, <span class="timetable-general">
}) <span class="general-info">
" <span class="info-number">
> <strong>{{ scheduledTrain.category }}</strong>
<span class="timetable-general"> {{ scheduledTrain.trainNo }}
<span class="general-info">
<span class="info-number">
<strong>{{ scheduledTrain.category }}</strong>
{{ scheduledTrain.trainNo }}
<span class="g-tooltip" v-if="scheduledTrain.stopInfo.comments"> <span class="g-tooltip" v-if="scheduledTrain.stopInfo.comments">
<img :src="icons.warning" /> <img :src="getIcon('warning')" />
<span class="content" v-html="scheduledTrain.stopInfo.comments"> </span> <span class="content" v-html="scheduledTrain.stopInfo.comments"> </span>
</span>
</span>
&nbsp;|&nbsp;
<span style="color: white">
{{ scheduledTrain.driverName }}
</span>
&nbsp;|&nbsp;
<span class="general-status">
<span :class="scheduledTrain.stopStatus">
{{ $t(`timetables.${scheduledTrain.stopStatus}`) }}
<span v-if="scheduledTrain.stopStatus == 'arriving'"> {{ scheduledTrain.prevStationName }}</span>
<span v-if="scheduledTrain.stopStatus.startsWith('departed')">{{
scheduledTrain.nextStationName
}}</span>
</span>
</span>
<div class="info-route">
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
</div>
</span>
</span>
<span class="timetable-schedule">
<span class="schedule-arrival">
<span class="arrival-time begins" v-if="scheduledTrain.stopInfo.beginsHere">
{{ $t('timetables.begins') }}
</span>
<span class="arrival-time" v-else>
<div v-if="scheduledTrain.stopInfo.arrivalDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
}}</s>
</div>
<span>
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : '' }}{{ scheduledTrain.stopInfo.arrivalDelay }})
</span> </span>
</div> </span>
</span> &nbsp;|&nbsp;
</span> <span>
{{ scheduledTrain.driverName }}
<span class="schedule-stop">
<span class="stop-time">
<span v-if="scheduledTrain.stopInfo.stopTime">
{{ scheduledTrain.stopInfo.stopTime }}
{{ scheduledTrain.stopInfo.stopType || 'pt' }}
</span> </span>
<span v-else>&nbsp;</span> <div class="info-route">
</span> <strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
</div>
<span class="arrow"></span> <ScheduledTrainStatus :scheduledTrain="scheduledTrain" />
<span class="stop-line">
{{ scheduledTrain.arrivingLine }}
{{ scheduledTrain.arrivingLine && scheduledTrain.departureLine && '&gt;' }}
{{ scheduledTrain.departureLine }}
</span> </span>
</span> </span>
<span class="schedule-departure"> <span class="timetable-schedule">
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere"> <span class="schedule-arrival">
{{ $t('timetables.terminates') }} <span class="arrival-time begins" v-if="scheduledTrain.stopInfo.beginsHere">
</span> {{ $t('timetables.begins') }}
</span>
<span class="departure-time" v-else> <span class="arrival-time" v-else>
<div v-if="scheduledTrain.stopInfo.departureDelay == 0"> <div v-if="scheduledTrain.stopInfo.arrivalDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span> <span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
}}</s>
</div> </div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
}}</s>
</div>
<span> <span>
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }} {{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : '' ({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.departureDelay }}) }}{{ scheduledTrain.stopInfo.arrivalDelay }})
</span> </span>
</div> </div>
</span>
</span>
<span class="schedule-stop">
<span class="stop-connection">
{{ scheduledTrain.arrivingLine }}
</span>
<span class="stop-time">
{{ scheduledTrain.stopInfo.stopTime || '' }}
{{ scheduledTrain.stopInfo.stopTime ? scheduledTrain.stopInfo.stopType || 'pt' : '' }}
</span>
<span class="stop-connection">
{{ scheduledTrain.departureLine }}
</span>
</span>
<span class="schedule-departure">
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere">
{{ $t('timetables.terminates') }}
</span>
<span class="departure-time" v-else>
<div v-if="scheduledTrain.stopInfo.departureDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
}}</s>
</div>
<span>
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }}
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.departureDelay }})
</span>
</div>
</span>
</span> </span>
</span> </span>
</span> </div>
</div> </transition-group>
</div> </div>
<!-- </transition> -->
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import Station from '@/scripts/interfaces/Station';
import SelectBox from '../Global/SelectBox.vue'; import SelectBox from '../Global/SelectBox.vue';
import { computed, defineComponent, PropType, ref } from '@vue/runtime-core'; import { computed, defineComponent, PropType, ref } from '@vue/runtime-core';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import dateMixin from '@/mixins/dateMixin';
import routerMixin from '@/mixins/routerMixin';
import { useStore } from '@/store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import TrainModal from '../Global/TrainModal.vue';
import dateMixin from '../../mixins/dateMixin';
import routerMixin from '../../mixins/routerMixin';
import Station from '../../scripts/interfaces/Station';
import { useStore } from '../../store/store';
import imageMixin from '../../mixins/imageMixin';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetable', name: 'SceneryTimetable',
components: { SelectBox, Loading }, components: { SelectBox, Loading, TrainModal, ScheduledTrainStatus },
mixins: [dateMixin, routerMixin], mixins: [dateMixin, routerMixin, imageMixin, modalTrainMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station>, type: Object as PropType<Station>,
required: true, required: true,
}, },
timetableOnly: {
type: Boolean,
},
}, },
data: () => ({ data: () => ({
viewIcon: require('@/assets/icon-view.svg'),
listOpen: false, listOpen: false,
icons: {
warning: require('@/assets/icon-warning.svg'),
timetable: require('@/assets/icon-timetable.svg'),
},
}), }),
setup(props) { setup(props) {
@@ -251,6 +264,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() {
@@ -266,12 +283,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%;
@@ -280,24 +292,36 @@ export default defineComponent({
} }
.timetable-header { .timetable-header {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 99; z-index: 99;
background-color: #181818; background-color: #181818;
padding: 0.5em;
img {
width: 25px;
vertical-align: middle;
}
h3 { h3 {
display: flex; display: flex;
justify-content: center;
flex-wrap: wrap;
align-items: center; align-items: center;
font-size: 1.4em;
gap: 0.5em;
font-size: 1.3em;
} }
} }
.header_links {
display: flex;
gap: 0.5em;
margin-left: 0.5em;
}
.timetable { .timetable {
&-count { &-count {
margin-left: 0.5em; margin-left: 0.5em;
@@ -305,12 +329,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(300px, 1fr));
gap: 0 0.5em; gap: 1.2em 0.5em;
overflow: hidden;
background: #353535; background: #353535;
@@ -325,9 +351,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,7 +360,13 @@ export default defineComponent({
&-schedule { &-schedule {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(30px, 1fr)); grid-template-columns: repeat(3, 1fr);
gap: 0.2em;
align-items: center;
width: 100%;
max-width: 400px;
margin: 0 auto;
} }
} }
@@ -351,45 +380,17 @@ export default defineComponent({
flex-wrap: wrap; flex-wrap: wrap;
font-size: 1.1em; font-size: 1.1em;
padding: 0.75em 0;
.checkpoint_item {
&.current {
font-weight: bold;
color: $accentCol;
}
&:not(:last-child)::after { margin-top: 0.5em;
margin: 0 0.5em;
content: '•'; button.checkpoint_item {
color: white; color: #aaa;
} display: inline;
} }
}
.arrow { .checkpoint_item.current {
border: solid white; font-weight: bold;
border-width: 0 2px 2px 0; color: $accentCol;
display: inline-block;
padding: 2px;
margin-left: 50px;
position: relative;
transform: rotate(-45deg);
&::before {
content: '';
position: absolute;
display: block;
width: 55px;
height: 3px;
top: 4px;
left: 4px;
transform: translate(-100%, -1px) rotate(45deg);
transform-origin: right bottom;
background: white;
} }
} }
@@ -402,7 +403,6 @@ export default defineComponent({
} }
.info-route { .info-route {
margin-top: 0.5em;
width: 100%; width: 100%;
} }
@@ -418,64 +418,36 @@ 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,
&-departure { &-departure {
display: flex; font-size: 1.15em;
justify-content: center;
align-items: center;
margin: 0 0.3rem;
font-size: 1.1em;
} }
&-stop { &-stop {
position: relative; display: grid;
display: flex; grid-template-columns: repeat(3, 1fr);
flex-direction: column; gap: 0.5em;
font-size: 0.85em; align-items: end;
padding: 0.3em 0; .stop-connection {
font-size: 0.95em;
.stop-line {
margin-top: 0.25em;
} }
.stop-time { .stop-time {
transform: translateY(-0.25em); position: relative;
inline-size: max-content;
align-self: center;
font-size: 0.9em;
color: $accentCol;
&::after {
content: '\027F6';
display: block;
font-size: 2.2em;
line-height: 0.65em;
}
} }
} }
} }
@@ -485,38 +457,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,74 +1,104 @@
<template> <template>
<section class="scenery-timetables-history scenery-section"> <section class="scenery-table-section">
<Loading v-if="dataStatus != 2" /> <Loading v-if="dataStatus != DataStatus.Loaded" />
<div class="no-history" v-else-if="historyList.length == 0">{{ $t('scenery.history-list-empty') }}</div>
<div class="list-warning" v-else-if="sceneryHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div> <table class="scenery-history-table" v-else>
<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>
<div> <th>{{ $t('scenery.timetables-history-date') }}</th>
<span class="text--grayed"> #{{ historyItem.timetableId }} </span> </thead>
<b class="text--primary">&nbsp;{{ historyItem.trainCategoryCode }} {{ historyItem.trainNo }}</b>
<div>{{ historyItem.driverName }}</div>
</div>
<div>{{ historyItem.route.replace('|', ' -> ') }}</div> <tbody>
<!-- <div>{{ historyItem.routeDistance }} km</div> --> <tr v-for="historyItem in historyList">
<div> <td>
{{ $t('scenery.timetable-author-title') }}: <router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">#{{ historyItem.id }}</router-link>
<b v-if="historyItem.authorName">{{ historyItem.authorName }}</b> </td>
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i> <td>
</div> <b class="text--primary">{{ historyItem.trainCategoryCode }}</b> <br />
{{ historyItem.trainNo }}
<!-- <div v-if="historyItem.authorId">{{ historyItem.authorName }}</div> --> </td>
</li> <td>{{ historyItem.route.replace('|', ' -> ') }}</td>
</ul> <td>{{ historyItem.driverName }}</td>
<td>
<router-link
v-if="historyItem.authorName"
:to="`/journal/timetables?authorName=${historyItem.authorName}`"
>{{ historyItem.authorName }}
</router-link>
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
</td>
<td>
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
{{ localeTime(historyItem.beginDate, $i18n.locale) }}
</td>
</tr>
</tbody>
</table>
</section> </section>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
{{ $t('scenery.bottom-info') }}
</button>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import dateMixin from '@/mixins/dateMixin';
import { DataStatus } from '@/scripts/enums/DataStatus';
import { SceneryTimetableHistory, TimetableHistory } from '@/scripts/interfaces/api/TimetablesAPIData';
import Station from '@/scripts/interfaces/Station';
import { URLs } from '@/scripts/utils/apiURLs';
import axios from 'axios'; import axios from 'axios';
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { TimetableHistory, SceneryTimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import Station from '../../scripts/interfaces/Station';
import { URLs } from '../../scripts/utils/apiURLs';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import listObserverMixin from '../../mixins/listObserverMixin';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetablesHistory', name: 'SceneryTimetablesHistory',
mixins: [dateMixin], mixins: [dateMixin, listObserverMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station>, type: Object as PropType<Station>,
required: true, required: true,
}, },
}, },
data() { data() {
return { return {
sceneryHistoryList: [] as TimetableHistory[], historyList: [] as TimetableHistory[],
dataStatus: DataStatus.Loading, dataStatus: DataStatus.Loading,
DataStatus,
}; };
}, },
mounted() {
this.fetchAPIData(); async activated() {
const fetchedHistory = await this.fetchAPIData();
if (fetchedHistory) this.historyList = fetchedHistory.timetables;
}, },
methods: { methods: {
async fetchAPIData(countFrom = 0, countLimit = 15) { async fetchAPIData(countFrom = 0, countLimit = 15): Promise<SceneryTimetableHistory | null> {
try { try {
const requestString = `${URLs.stacjownikAPI}/api/getSceneryTimetables?name=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`; const requestString = `${URLs.stacjownikAPI}/api/getIssuedTimetables?name=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
const historyAPIData: SceneryTimetableHistory = await (await axios.get(requestString)).data; const historyAPIData: SceneryTimetableHistory = await (await axios.get(requestString)).data;
this.sceneryHistoryList = historyAPIData.sceneryTimetables;
this.dataStatus = DataStatus.Loaded; this.dataStatus = DataStatus.Loaded;
return historyAPIData;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return null;
} }
}, },
navigateToHistory() {
this.$router.push(`/journal/timetables?issuedFrom=${this.station.name}`);
},
}, },
components: { Loading }, components: { Loading },
}); });
@@ -76,35 +106,5 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/SceneryView/styles.scss'; @import '../../styles/sceneryViewTables.scss';
.list-warning {
padding: 1em 0.5em;
background-color: #444;
font-size: 1.2em;
}
.history-list {
padding: 0 0.5em;
}
.list-item {
display: grid;
grid-template-columns: 1fr 2fr 2fr 1fr;
gap: 1em;
align-items: center;
background-color: #353535;
padding: 0.5em;
margin: 0.5em 0;
line-height: 1.5em;
}
@include smallScreen {
.list-item {
grid-template-columns: 1fr 1fr;
font-size: 1.05em;
}
}
</style> </style>
@@ -0,0 +1,113 @@
<template>
<div class="general-status">
<span :class="computedScheduledTrain.stopStatus" :title="computedScheduledTrain.stopStatusDescription">
{{ computedScheduledTrain.stopStatusIndicator }}
</span>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { ScheduledTrain, StopStatus } from '../../scripts/interfaces/ScheduledTrain';
interface ScheduledTrainComp extends ScheduledTrain {
stopStatusIndicator: string;
stopStatusDescription: string;
}
export default defineComponent({
props: {
scheduledTrain: {
type: Object as PropType<ScheduledTrain>,
required: true,
},
},
computed: {
computedScheduledTrain(): ScheduledTrainComp {
const { prevDepartureLine, prevStationName, stopStatus, nextArrivalLine, nextStationName } = this.scheduledTrain;
const prevDepartureIndicator = prevDepartureLine ? `(${prevDepartureLine}) ${prevStationName}` : '---';
const nextArrivalIndicator = nextArrivalLine ? `(${nextArrivalLine}) ${nextStationName}` : '---';
let stopStatusDescription = '',
stopStatusIndicator = '';
switch (stopStatus) {
case StopStatus.arriving:
stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`;
stopStatusDescription = this.$t('timetables.desc-arriving', { prevStationName, prevDepartureLine });
break;
case StopStatus.online:
case StopStatus.stopped:
stopStatusIndicator = nextArrivalLine
? `${this.$t('timetables.to')}: ${nextArrivalIndicator}`
: `${this.$t('timetables.desc-end')}`;
stopStatusDescription = nextArrivalLine
? this.$t(`timetables.desc-${stopStatus}`, { nextStationName, nextArrivalLine })
: '';
break;
case StopStatus.departed:
stopStatusIndicator = `${this.$t('timetables.to')}: ${nextArrivalIndicator}`;
stopStatusDescription = this.$t('timetables.desc-departed', { nextStationName, nextArrivalLine });
break;
case StopStatus['departed-away']:
stopStatusIndicator = `${this.$t('timetables.to')}: ${nextArrivalIndicator}`;
stopStatusDescription = this.$t('timetables.desc-departed-away', { nextStationName, nextArrivalLine });
break;
case StopStatus.terminated:
stopStatusIndicator = `X ${this.$t('timetables.desc-terminated')}`;
stopStatusDescription = this.$t('timetables.desc-terminated');
break;
default:
break;
}
return {
...this.scheduledTrain,
stopStatusDescription,
stopStatusIndicator,
};
},
},
});
</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>
+61 -108
View File
@@ -1,23 +1,15 @@
<template> <template>
<div class="filter-option option"> <label @dblclick="handleDbClick">
<label> <input v-model="option.value" type="checkbox" :class="option.section" :name="option.id" />
<input <span>
type="checkbox" {{ $t(`filters.${option.id}`) }}
:name="option.name" </span>
:defaultValue="option.defaultValue" </label>
: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,113 +26,74 @@ export default defineComponent({
required: true, required: true,
}, },
}, },
emits: ['optionChange'],
methods: {
handleChange() {
if (this.option.name == 'troll') {
location.href = 'https://www.youtube.com/watch?v=HIcSWuKMwOw';
return;
}
this.$emit('optionChange', { setup() {
name: this.option.name, return {
value: this.option.value, filterStore: useStationFiltersStore(),
}); };
},
watch: {
'option.value'() {
this.filterStore.changeFilterValue(this.option.name, !this.option.value);
}, },
}, },
setup() {
return {}; methods: {
handleDbClick(e: Event) {
e.preventDefault();
this.filterStore.lastClickedFilterId = this.option.id;
this.option.value = true;
this.filterStore.inputs.options
.filter((option) => {
return option.section == this.option.section && option.id != this.option.id;
})
.forEach((option) => {
option.value = !this.option.value;
});
},
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/option.scss'; @import '../../styles/variables.scss';
$accessCol: #e03b07; label {
$controlCol: #0085ff; position: relative;
$signalCol: #bf7c00; user-select: none;
$statusCol: #349b32; -webkit-user-select: none;
$saveCol: #28a826; -moz-user-select: none;
$routesCol: #9049c0;
.option span { span {
font-size: 0.9em; cursor: pointer;
&.checked { display: inline-block;
&.access { width: 100%;
background-color: $accessCol; text-align: center;
padding: 0.25em;
background-color: #444;
}
&::before { span:hover {
box-shadow: 0 0 6px 1px $accessCol; background-color: #555;
} }
input[type='checkbox'] {
cursor: pointer;
position: absolute;
opacity: 0;
&:checked + span {
background-color: forestgreen;
font-weight: bold;
} }
&.control { &:focus-visible + span {
background-color: $controlCol; outline: 1px solid $accentCol;
&::before {
box-shadow: 0 0 6px 1px $controlCol;
}
}
&.signals {
background-color: $signalCol;
&::before {
box-shadow: 0 0 6px 1px $signalCol;
}
}
&.routes {
background-color: $routesCol;
&::before {
box-shadow: 0 0 6px 1px $routesCol;
}
}
&.status {
background-color: $statusCol;
&::before {
box-shadow: 0 0 6px 1px $statusCol;
}
}
&.save {
background-color: $saveCol;
&::before {
box-shadow: 0 0 6px 1px $saveCol;
}
}
&.troll {
background-color: firebrick;
&::before {
box-shadow: 0 0 6px 1px firebrick;
}
}
&.mode {
background-color: lightgreen;
color: black;
font-weight: 500;
}
&::before {
position: absolute;
content: '';
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 0.5em;
} }
} }
} }
</style> </style>
+213 -150
View File
@@ -1,29 +1,74 @@
<template> <template>
<section class="filter-card" v-click-outside="closeCard"> <section class="filter-card" v-click-outside="closeCard" @keydown.esc="closeCard">
<div class="card_btn"> <div class="card_controls">
<button class="btn btn--option" @click="toggleCard"> <button class="btn--filled btn--image" @click="toggleCard">
<img class="button_icon" :src="filterIcon" alt="icon-filter" /> <img class="button_icon" :src="getIcon('filter2')" alt="filter icon" />
{{ $t('options.filters') }} {{ $t('options.filters') }} [F]
<span class="active-indicator" v-if="!filterStore.areFiltersAtDefault"></span>
</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>
<p class="card_info" v-html="$t('filters.desc')"></p>
<section class="card_options"> <section class="card_options">
<filter-option <!-- QUICK ACTIONS (TODO) -->
v-for="(option, i) in inputs.options" <!-- <div class="quick-actions">
:option="option" <h3 class="text--primary">{{ $t('filters.sections.quick') }}</h3>
:key="i" <hr />
@optionChange="handleChange"
/> <div>
<button class="btn--action" style="width: 100%" @click="filterStore.handleQuickAction('all-available')">
{{ $t('filters.all-available') }}
</button>
<button class="btn--action" style="width: 100%" @click="filterStore.handleQuickAction('all-free')">
{{ $t('filters.all-free') }}
</button>
</div>
</div> -->
<div class="option-section" v-for="section in filterStore.inputs.optionSections">
<h3 class="text--primary">
{{ $t(`filters.sections.${section}`) }}
<button @click="filterStore.resetSectionOptions(section)">RESET</button>
</h3>
<hr />
<div class="section-inputs">
<FilterOption
v-for="(option, i) in filterStore.inputs.options.filter((o) => o.section == section)"
:option="option"
:key="i"
/>
</div>
</div>
</section> </section>
<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 +76,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 +87,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"
@@ -63,51 +110,47 @@
</div> </div>
</div> </div>
</section> </section>
<section class="card_actions">
<div>
<filter-option
@optionChange="saveFilters"
:option="{
id: 'save',
name: 'save',
section: 'mode',
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>
</section>
</div> </div>
<section class="card_actions">
<div class="action-buttons">
<button class="btn--action" style="width: 100%" @click="saveFilters" :data-selected="saveOptions">
{{ $t('filters.save') }}
</button>
<button
class="btn--action"
@click="resetFilters"
:disabled="filterStore.areFiltersAtDefault"
:data-disabled="filterStore.areFiltersAtDefault"
>
{{ $t('filters.reset') }}
</button>
<button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button>
</div>
</section>
</div> </div>
</transition> </transition>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, inject } from '@vue/runtime-core'; import { defineComponent, inject } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import keyMixin from '../../mixins/keyMixin';
import routerMixin from '../../mixins/routerMixin';
import StorageManager from '../../scripts/managers/storageManager';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useStore } from '../../store/store';
import inputData from '@/data/options.json';
import StorageManager from '@/scripts/managers/storageManager';
import ActionButton from '../Global/ActionButton.vue'; import ActionButton from '../Global/ActionButton.vue';
import FilterOption from './FilterOption.vue'; import FilterOption from './FilterOption.vue';
import { useStore } from '@/store/store';
export default defineComponent({ export default defineComponent({
components: { ActionButton, FilterOption }, components: { ActionButton, FilterOption },
emits: ['changeFilterValue', 'invertFilters', 'resetFilters'], mixins: [imageMixin, keyMixin, routerMixin],
data: () => ({ data: () => ({
filterIcon: require('@/assets/icon-filter2.svg'),
inputs: { ...inputData },
saveOptions: false, saveOptions: false,
STORAGE_KEY: 'options_saved', STORAGE_KEY: 'options_saved',
@@ -117,15 +160,18 @@ export default defineComponent({
currentRegion: { id: '', value: '' }, currentRegion: { id: '', value: '' },
delayInputTimer: -1, delayInputTimer: -1,
chosenSearchScenery: '',
}), }),
setup() { setup() {
const isVisible = inject('isFilterCardVisible'); const isVisible = inject('isFilterCardVisible');
const store = useStore(); const store = useStore();
const filterStore = useStationFiltersStore();
return { return {
isVisible, isVisible,
store, store,
filterStore,
}; };
}, },
@@ -141,23 +187,45 @@ export default defineComponent({
this.currentRegion = this.store.region; this.currentRegion = this.store.region;
}, },
methods: { computed: {
handleChange(change: { name: string; value: boolean }) { sortedStationList() {
this.$emit('changeFilterValue', { return this.store.stationList
name: change.name, .filter((s) => s.name.toLocaleLowerCase().includes(this.chosenSearchScenery.toLocaleLowerCase()))
value: !change.value, .sort((s1, s2) => (s1.name > s2.name ? 1 : -1));
}); },
if (this.saveOptions) StorageManager.setBooleanValue(change.name, change.value); currentOptionsActive() {
return true;
},
},
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: {
// Override keyMixin function
onKeyDownFunction() {
this.isVisible = !this.isVisible;
}, },
handleInput(e: Event) { handleInput(e: Event) {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
this.$emit('changeFilterValue', { this.filterStore.changeFilterValue(target.name, target.value);
name: target.name,
value: target.value,
});
if (this.saveOptions) StorageManager.setStringValue(target.name, target.value); if (this.saveOptions) StorageManager.setStringValue(target.name, target.value);
}, },
@@ -165,17 +233,13 @@ export default defineComponent({
handleAuthorsInput(e: Event) { handleAuthorsInput(e: Event) {
clearTimeout(this.delayInputTimer); clearTimeout(this.delayInputTimer);
this.delayInputTimer = setTimeout(() => { this.delayInputTimer = window.setTimeout(() => {
this.handleInput(e); this.handleInput(e);
}, 400); }, 400);
}, },
changeNumericFilterValue(name: string, value: number, saveToStorage = false) { changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
this.$emit('changeFilterValue', { this.filterStore.changeFilterValue(name, value);
name,
value,
});
if (this.saveOptions && saveToStorage) StorageManager.setNumericValue(name, value); if (this.saveOptions && saveToStorage) StorageManager.setNumericValue(name, value);
}, },
@@ -191,17 +255,8 @@ export default defineComponent({
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true); this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
}, },
invertFilters() { saveFilters() {
this.inputs.options.forEach((option) => { this.saveOptions = !this.saveOptions;
option.value = !option.value;
StorageManager.setBooleanValue(option.name, option.value);
});
this.$emit('invertFilters');
},
saveFilters(change: { value }) {
this.saveOptions = change.value;
if (!this.saveOptions) { if (!this.saveOptions) {
StorageManager.unregisterStorage(this.STORAGE_KEY); StorageManager.unregisterStorage(this.STORAGE_KEY);
@@ -210,28 +265,16 @@ export default defineComponent({
StorageManager.registerStorage(this.STORAGE_KEY); StorageManager.registerStorage(this.STORAGE_KEY);
this.inputs.options.forEach((option) => StorageManager.setBooleanValue(option.name, option.value)); this.filterStore.inputs.options.forEach((option) => StorageManager.setBooleanValue(option.name, !option.value));
this.filterStore.inputs.sliders.forEach((slider) => StorageManager.setNumericValue(slider.name, slider.value));
this.inputs.sliders.forEach((slider) => StorageManager.setNumericValue(slider.name, slider.value));
}, },
resetFilters() { resetFilters() {
this.inputs.options.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, option.value);
});
this.inputs.sliders.forEach((slider) => {
slider.value = slider.defaultValue;
StorageManager.setNumericValue(slider.name, slider.value);
});
this.authorsInputValue = ''; this.authorsInputValue = '';
this.minimumHours = 0; this.minimumHours = 0;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true); this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
this.filterStore.resetFilters();
this.$emit('resetFilters');
}, },
closeCard() { closeCard() {
@@ -257,34 +300,38 @@ export default defineComponent({
&-enter-from, &-enter-from,
&-leave-to { &-leave-to {
transform: translate(-50%, -50%) scale(0.8);
opacity: 0; opacity: 0;
transform: translate(-50%, -50%) scale(0.45);
} }
} }
.card { .card {
&_btn { display: grid;
button { grid-template-rows: 1fr auto;
display: flex;
align-items: center;
padding: 0.5em 1em; &_info {
border-radius: 0.75em 0.75em 0 0; background-color: #111;
padding: 0.5em;
}
font-weight: bold; &_controls {
} display: flex;
gap: 0.5em;
img { input {
width: 1.3em; border-radius: 0.5em 0.5em 0 0;
margin-right: 0.25em; height: 100%;
} }
} }
&_content { &_content {
display: grid; padding: 1em 0.5em;
grid-template-rows: 70px 1fr 100px 50px auto;
min-height: 0; display: flex;
max-height: 100vh; flex-direction: column;
gap: 1em;
overflow: auto;
} }
&_title { &_title {
@@ -292,23 +339,9 @@ export default defineComponent({
font-weight: 700; font-weight: 700;
color: $accentCol; color: $accentCol;
margin: 0.5em 0;
text-align: center; text-align: center;
} }
&_options {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-rows: repeat(4, 1fr);
gap: 0.5em;
@include smallScreen() {
grid-template-columns: repeat(auto-fit, minmax(8em, 1fr));
grid-template-rows: auto;
}
}
&_regions { &_regions {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -341,32 +374,18 @@ export default defineComponent({
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 1.15em; font-size: 1.2em;
margin-top: 0.5em;
color: $accentCol; span {
font-weight: bold; min-width: 120px;
} font-weight: bold;
span {
min-width: 100px;
}
button {
border: none;
outline: none;
background: none;
padding: 0 0.45em;
cursor: pointer;
color: white;
font-size: 1.35em;
&:focus,
&:hover {
color: $accentCol; color: $accentCol;
} }
button {
padding: 0.2em 0.6em;
}
} }
} }
@@ -388,26 +407,65 @@ export default defineComponent({
input { input {
width: 100%; width: 100%;
padding: 0.5em; padding: 0.5em;
border: 1px solid white;
} }
} }
&_actions { &_actions {
margin-top: 1em; width: 100%;
padding: 0.5em;
display: flex; .filter-option {
flex-direction: column; max-width: 50%;
align-items: center; margin: 0 auto;
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: forestgreen;
}
}
} }
} }
} }
.option-section h3 {
display: flex;
align-items: center;
margin-bottom: 0.25em;
gap: 0.5em;
button {
padding: 0.15em;
color: coral;
}
}
.section-inputs {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.5em;
margin: 1em 0;
}
.quick-actions div {
display: flex;
margin: 1em 0;
gap: 1em;
}
.slider { .slider {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -434,8 +492,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;
+100 -83
View File
@@ -1,34 +1,40 @@
<template> <template>
<section class="station_table"> <section class="station_table">
<button class="return-btn" @click="scrollToTop" v-if="showReturnButton">
<img :src="icons.arrow" alt="return arrow" />
</button>
<div class="table_wrapper"> <div class="table_wrapper">
<table> <table>
<thead> <thead>
<tr> <tr>
<th v-for="(id, i) in headIds" :key="id" @click="() => changeSorter(i)"> <th
v-for="(headerName, i) in headIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-text"
>
<span class="header_wrapper"> <span class="header_wrapper">
<div v-html="$t(`sceneries.${id}`)"></div> <div v-html="$t(`sceneries.${headerName}`)"></div>
<img <img
class="sort-icon" class="sort-icon"
v-if="sorterActive.index == i" v-if="sorterActive.headerName == headerName"
:src="sorterActive.dir == 1 ? ascIcon : descIcon" :src="sorterActive.dir == 1 ? getIcon('arrow-asc') : getIcon('arrow-desc')"
alt="sort icon" alt="sort icon"
/> />
</span> </span>
</th> </th>
<th v-for="(id, i) in headIconsIds" :key="id" @click="() => changeSorter(i + 7)"> <th
v-for="(headerName, i) in headIconsIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-image"
>
<span class="header_wrapper"> <span class="header_wrapper">
<img :src="require(`@/assets/icon-${id}.svg`)" :alt="id" :title="$t(`sceneries.${id}s`)" /> <img :src="getIcon(headerName)" :alt="headerName" :title="$t(`sceneries.${headerName}`)" />
<img <img
class="sort-icon" class="sort-icon"
v-if="sorterActive.index == i + 7" v-if="sorterActive.headerName == headerName"
:src="sorterActive.dir == 1 ? ascIcon : descIcon" :src="sorterActive.dir == 1 ? getIcon('arrow-asc') : getIcon('arrow-desc')"
alt="sort icon" alt="sort icon"
/> />
</span> </span>
@@ -67,15 +73,15 @@
</span> </span>
<span v-else-if="station.generalInfo.availability == 'abandoned'"> <span v-else-if="station.generalInfo.availability == 'abandoned'">
<img :src="abandonedIcon" alt="non-public" :title="$t('desc.abandoned')" /> <img :src="getIcon('abandoned')" alt="non-public" :title="$t('desc.abandoned')" />
</span> </span>
<span v-else-if="station.generalInfo.availability == 'nonPublic'"> <span v-else-if="station.generalInfo.availability == 'nonPublic'">
<img :src="lockIcon" alt="non-public" :title="$t('desc.non-public')" /> <img :src="getIcon('lock')" alt="non-public" :title="$t('desc.non-public')" />
</span> </span>
<span v-else> <span v-else>
<img :src="unavailableIcon" alt="unavailable" :title="$t('desc.unavailable')" /> <img :src="getIcon('unavailable')" alt="unavailable" :title="$t('desc.unavailable')" />
</span> </span>
</span> </span>
@@ -83,16 +89,11 @@
</td> </td>
<td class="station_status"> <td class="station_status">
<span class="status-badge" :class="station.onlineInfo.statusID" v-if="station.onlineInfo"> <StationStatusBadge
{{ $t(`status.${station.onlineInfo.statusID}`) }} :statusID="station.onlineInfo?.statusID"
{{ :isOnline="station.onlineInfo ? true : false"
station.onlineInfo.statusID == 'online' ? timestampToString(station.onlineInfo.statusTimestamp) : '' :statusTimestamp="station.onlineInfo?.statusTimestamp"
}} />
</span>
<span class="status-badge free" v-else>
{{ $t('status.free') }}
</span>
</td> </td>
<td class="station_dispatcher-name"> <td class="station_dispatcher-name">
@@ -100,7 +101,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>
@@ -154,7 +158,7 @@
<img <img
class="icon-info" class="icon-info"
v-if="station.generalInfo.SUP" v-if="station.generalInfo.SUP"
:src="require(`@/assets/icon-SUP.svg`)" :src="getIcon('SUP')"
alt="SUP (RASP-UZK)" alt="SUP (RASP-UZK)"
:title="$t('desc.SUP')" :title="$t('desc.SUP')"
/> />
@@ -164,7 +168,7 @@
<img <img
class="icon-info" class="icon-info"
v-if="station.generalInfo.signalType" v-if="station.generalInfo.signalType"
:src="require(`@/assets/icon-${station.generalInfo.signalType}.svg`)" :src="getIcon(station.generalInfo.signalType)"
:alt="station.generalInfo.signalType" :alt="station.generalInfo.signalType"
:title="$t('desc.signals-type') + $t(`signals.${station.generalInfo.signalType}`)" :title="$t('desc.signals-type') + $t(`signals.${station.generalInfo.signalType}`)"
/> />
@@ -174,7 +178,7 @@
<img <img
class="icon-info" class="icon-info"
v-if="station.generalInfo && station.generalInfo.routes.sblRouteNames.length > 0" v-if="station.generalInfo && station.generalInfo.routes.sblRouteNames.length > 0"
:src="SBLIcon" :src="getIcon('SBL')"
alt="SBL" alt="SBL"
:title="$t('desc.SBL') + `${station.generalInfo.routes.sblRouteNames.join(',')}`" :title="$t('desc.SBL') + `${station.generalInfo.routes.sblRouteNames.join(',')}`"
/> />
@@ -182,30 +186,36 @@
</td> </td>
<td class="station_info" v-else> <td class="station_info" v-else>
<img class="icon-info" :src="unknownIcon" alt="icon-unknown" :title="$t('desc.unknown')" /> <img class="icon-info" :src="getIcon('unknown')" alt="icon-unknown" :title="$t('desc.unknown')" />
</td> </td>
<td class="station_users" :class="{ inactive: !station.onlineInfo }"> <td class="station_users" :class="{ inactive: !station.onlineInfo }">
<span> <span>
<span class="highlight">{{ station.onlineInfo?.currentUsers || '0' }}</span> <span class="highlight">{{ station.onlineInfo?.currentUsers || 0 }}</span>
/ /
<span>{{ station.onlineInfo?.maxUsers || '0' }}</span> <span class="highlight">{{ station.onlineInfo?.maxUsers || 0 }}</span>
</span> </span>
</td> </td>
<td class="station_spawns" :class="{ inactive: !station.onlineInfo }"> <td class="station_spawns" :class="{ inactive: !station.onlineInfo }">
<span class="highlight">{{ station.onlineInfo?.spawns.length || '0' }}</span> <span>{{ station.onlineInfo?.spawns.length || 0 }}</span>
</td> </td>
<td class="station_schedules" :class="{ inactive: !station.onlineInfo }"> <td class="station_schedules" style="width: 30px" :class="{ inactive: !station.onlineInfo }">
<span> <span class="highlight">
<span class="highlight"> {{ station.onlineInfo?.scheduledTrains?.length || 0 }}
{{ station.onlineInfo?.scheduledTrains?.length || '0' }} </span>
</span> </td>
/
<span style="color: #bbb"> <td class="station_schedules" style="width: 30px" :class="{ inactive: !station.onlineInfo }">
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }} <span style="color: #ccc">
</span> {{ station.onlineInfo?.scheduledTrains?.filter((train) => !train.stopInfo.confirmed).length || 0 }}
</span>
</td>
<td class="station_schedules" style="width: 30px" :class="{ inactive: !station.onlineInfo }">
<span style="color: #66ff6c">
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || 0 }}
</span> </span>
</td> </td>
</tr> </tr>
@@ -222,17 +232,19 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import styleMixin from '@/mixins/styleMixin'; import { defineComponent, computed } from 'vue';
import dateMixin from '@/mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import stationInfoMixin from '@/mixins/stationInfoMixin'; import imageMixin from '../../mixins/imageMixin';
import returnBtnMixin from '@/mixins/returnBtnMixin'; import returnBtnMixin from '../../mixins/returnBtnMixin';
import stationInfoMixin from '../../mixins/stationInfoMixin';
import { DataStatus } from '@/scripts/enums/DataStatus'; import styleMixin from '../../mixins/styleMixin';
import { computed, ComputedRef, defineComponent } from '@vue/runtime-core'; import { DataStatus } from '../../scripts/enums/DataStatus';
import Station from '@/scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
import { StoreData } from '@/scripts/interfaces/StoreData'; import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useStore } from '@/store/store'; import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames';
import StationStatusBadge from '../Global/StationStatusBadge.vue';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -240,61 +252,60 @@ export default defineComponent({
type: Array as () => Station[], type: Array as () => Station[],
required: true, required: true,
}, },
sorterActive: {
type: Object as () => {
index: number;
dir: number;
},
required: true,
},
setFocusedStation: { type: Function, required: true },
changeSorter: { type: Function, required: true },
}, },
mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin],
components: { Loading, StationStatusBadge },
mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin],
data: () => ({ data: () => ({
likeIcon: require('@/assets/icon-like.svg'), headIconsIds,
spawnIcon: require('@/assets/icon-spawn.svg'), headIds,
timetableIcon: require('@/assets/icon-timetable.svg'),
userIcon: require('@/assets/icon-user.svg'),
trainIcon: require('@/assets/icon-train.svg'),
SBLIcon: require('@/assets/icon-SBL.svg'),
SUPIcon: require('@/assets/icon-SUP.svg'),
lockIcon: require('@/assets/icon-lock.svg'),
unavailableIcon: require('@/assets/icon-unavailable.svg'),
unknownIcon: require('@/assets/icon-unknown.svg'),
abandonedIcon: require('@/assets/icon-abandoned.svg'),
ascIcon: require('@/assets/icon-arrow-asc.svg'),
descIcon: require('@/assets/icon-arrow-desc.svg'),
headIds: ['station', 'min-lvl', 'status', 'dispatcher', 'dispatcher-lvl', 'routes', 'general'],
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(headerName: HeadIdsTypes) {
if (headerName == 'general' || headerName == 'routes') return;
this.stationFiltersStore.changeSorter(headerName);
},
}, },
components: { Loading },
}); });
</script> </script>
@@ -303,7 +314,7 @@ export default defineComponent({
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
@import '../../styles/icons.scss'; @import '../../styles/icons.scss';
$rowCol: #4b4b4b; $rowCol: #424242;
.change-anim { .change-anim {
&-enter-active, &-enter-active,
@@ -342,17 +353,23 @@ table {
} }
thead tr { thead tr {
background-color: $primaryCol; background-color: $bgCol;
} }
thead th { thead th {
position: sticky; position: sticky;
top: 0; top: 0;
min-width: 75px; &.header-text {
min-width: 140px;
}
padding: 0.5em; &.header-image {
background-color: $primaryCol; min-width: 60px;
}
padding: 0.5em 0.25em;
background-color: $bgCol;
white-space: pre-wrap; white-space: pre-wrap;
cursor: pointer; cursor: pointer;
+131 -163
View File
@@ -1,103 +1,70 @@
<template> <template>
<div class="train-info simple" tabindex="0"> <div class="train-info">
<section> <section class="train-route">
<span> <div class="train_general">
<div> <b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
<span> <span class="timetable-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span>
<!-- <router-link
v-if="train.timetableData"
:to="`/journal/timetables?timetableId=${train.timetableData.timetableId}`"
style="color: #ddd; margin-right: 0.3em"
>
#{{ train.timetableData.timetableId }}
</router-link> -->
<span class="timetable-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span> <span class="timetable_warnings" v-if="train.timetableData?.TWR || train.timetableData?.SKR">
<span class="train-badge twr" v-if="train.timetableData?.TWR" :title="$t('general.TWR')">TWR</span>
<span class="train-badge skr" v-if="train.timetableData?.SKR" :title="$t('general.SKR')">SKR</span>
</span>
<span class="timetable_warnings"> <strong>
<span class="warning twr" v-if="train.timetableData?.TWR">TWR</span> <span v-if="train.timetableData" class="text--primary">{{ train.timetableData.category }}&nbsp;</span>
<span class="warning skr" v-if="train.timetableData?.SKR">SKR</span> <span class="train-number">{{ train.trainNo }}</span>
</span> </strong>
<strong v-if="train.timetableData">{{ train.timetableData.category }}&nbsp;</strong> <span>&bull;</span>
<strong>{{ train.trainNo }}</strong> <b class="level-badge driver" :style="calculateExpStyle(train.driverLevel, train.isSupporter)">
<span>&nbsp;| {{ train.driverName }}&nbsp;</span> {{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
</span> </b>
<span>{{ train.driverName }}</span>
</div>
<img <div class="timetable_route" v-if="train.timetableData">
class="image-offline" <strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
style="height: 1em" <img
v-if="!train.currentStationHash" v-if="getSceneriesWithComments(train.timetableData).length > 0"
:src="icons.offline" class="image-warning"
alt="offline" :src="getIcon('warning')"
:title="$t('trains.offline')" :title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(train.timetableData)})`"
/> />
</div>
<hr style="margin: 0.25em 0" />
<div class="timetable_stops" v-if="train.timetableData">
<span v-if="train.timetableData.followingStops.length > 2">
{{ $t('trains.via-title') }}
<span v-html="displayStopList(train.timetableData.followingStops)"></span>
</span>
</div>
<div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
<ProgressBar :progressPercent="confirmedPercentage(train.timetableData.followingStops)" />
<span class="timetable_progress-distance">
&nbsp; {{ currentDistance(train.timetableData.followingStops) }} km /
<span class="text--primary"> {{ train.timetableData.routeDistance }} km </span>
|
<span v-html="currentDelay(train.timetableData.followingStops)"></span>
</span>
<div class="train-status-badges">
<div v-if="!train.currentStationHash" class="train-badge offline">{{ $t('trains.scenery-offline') }}</div>
<div v-if="!train.online" class="train-badge offline">Offline {{ lastSeenMessage(train.lastSeen) }}</div>
</div> </div>
</div>
<div class="timetable_route" v-if="train.timetableData"> <div class="driver_position text--grayed" style="margin-top: 0.25em">
<strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong> {{ displayTrainPosition(train) }}
<img </div>
v-if="getSceneriesWithComments(train.timetableData).length > 0"
class="image-warning"
:src="icons.warning"
:title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(train.timetableData)})`"
/>
</div>
<hr style="margin: 0.25em 0" />
<div class="timetable_stops" v-if="train.timetableData">
<span v-if="train.timetableData.followingStops.length > 2">
{{ $t('trains.via-title') }}
<span v-html="displayStopList(train.timetableData.followingStops)"></span>
</span>
</div>
<div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
<!-- <span> </span> -->
<span class="timetable_progress-bar">
<!-- {{ confirmedPercentage(train.timetableData.followingStops) }}%&nbsp; -->
<span class="bar-bg"></span>
<span
class="bar-fg"
:style="{ width: `${Math.floor(confirmedPercentage(train.timetableData.followingStops))}%` }"
></span>
</span>
<span>
&nbsp; {{ currentDistance(train.timetableData.followingStops) }} km /
<span class="text--primary"> {{ train.timetableData.routeDistance }} km </span>
|
<span v-html="currentDelay(train.timetableData.followingStops)"></span>
</span>
</div>
<div v-if="!train.online" style="color: salmon">Offline - {{ lastSeenMessage(train.lastSeen) }}</div>
<div class="driver_position text--grayed" style="margin-top: 0.25em">
<span v-if="train.currentStationHash">
{{ $t('trains.current-scenery') }} <span>{{ train['currentStationName'] }}&nbsp;</span>
</span>
<span v-else>
{{ $t('trains.current-scenery') }}
<span>{{ train['currentStationName'].replace(/.[a-zA-Z0-9]+.sc/, '') }} (offline)&nbsp;</span>
</span>
<span v-if="train.signal">
{{ $t('trains.current-signal') }} <span>{{ train['signal'] }}&nbsp;</span>
</span>
<span v-if="train.connectedTrack">
{{ $t('trains.current-track') }} <span>{{ train['connectedTrack'] }}&nbsp;</span>
</span>
<span v-if="train.distance">({{ displayDistance(train.distance) }})</span>
</div>
</span>
</section> </section>
<section class="train-image" style="display: flex; justify-content: center; align-items: center"> <section class="train-stats">
<img :src="train.locoURL" loading="lazy" alt="Loco image not found" @error="onImageError" /> <div>
<img :src="train.locoURL" loading="lazy" alt="Loco image not found" @error="onImageError" />
</div>
<div class="text--grayed"> <div class="text--grayed">
{{ train.locoType }} {{ train.locoType }}
@@ -108,21 +75,22 @@
</div> </div>
<div> <div>
<div> <span v-for="(stat, i) in STATS.main" :key="stat.name">
<span v-for="(stat, i) in STATS.main" :key="stat.name"> <span v-if="i > 0"> &bull; </span>
<span v-if="i > 0"> &bull; </span> <span>{{ `${~~((train as any)[stat.name] * (stat.multiplier || 1))}${stat.unit}` }} </span>
<span>{{ `${~~(train[stat.name] * (stat.multiplier || 1))}${stat.unit}` }} </span> </span>
</span>
</div>
</div> </div>
</section> </section>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import trainInfoMixin from '@/mixins/trainInfoMixin';
import Train from '@/scripts/interfaces/Train';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import styleMixin from '../../mixins/styleMixin';
import trainInfoMixin from '../../mixins/trainInfoMixin';
import Train from '../../scripts/interfaces/Train';
import ProgressBar from '../Global/ProgressBar.vue';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -130,32 +98,33 @@ export default defineComponent({
type: Object as () => Train, type: Object as () => Train,
required: true, required: true,
}, },
}, extended: {
type: Boolean,
mixins: [trainInfoMixin], default: true,
data: () => ({
icons: {
warning: require('@/assets/icon-warning.svg'),
offline: require('@/assets/icon-offline.svg'),
}, },
}), },
mixins: [trainInfoMixin, imageMixin, styleMixin],
components: { ProgressBar },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/badge.scss';
.image-warning, .image-warning {
.image-offline {
height: 1em; height: 1em;
margin-left: 0.5em; margin-left: 0.5em;
} }
.train-image { .train-stats {
display: flex; display: flex;
justify-content: center;
align-content: center;
flex-direction: column; flex-direction: column;
text-align: center;
img { img {
margin: 0.5em 0; margin: 0.5em 0;
@@ -163,29 +132,56 @@ export default defineComponent({
} }
} }
.simple { .train-info {
display: grid; display: grid;
grid-template-columns: 2fr 1fr; grid-template-columns: 2fr 1fr;
grid-template-rows: 1fr; grid-template-rows: 1fr;
padding: 1em; padding: 1em;
background-color: #202020;
background-color: #1a1a1a;
gap: 0.5em; gap: 0.5em;
} }
.driver_position:first-letter { .timetable-id {
text-transform: capitalize; color: #d2d2d2;
} }
.timetable-id { .warning-timeout {
margin-right: 0.3em; background-color: #be3728;
color: #d2d2d2;
display: inline-block;
text-align: center;
padding: 0 0.25em;
} }
.timetable_stops { .timetable_stops {
font-size: 0.75em; font-size: 0.75em;
} }
.train_general {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
margin-right: 1.5em;
}
.train-status-badges {
display: flex;
flex-wrap: wrap;
gap: 0.25em;
}
.train-driver {
&.supporter {
color: orange;
text-shadow: orange 0 0 5px;
}
}
.timetable_route { .timetable_route {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -194,23 +190,8 @@ export default defineComponent({
} }
.timetable_warnings { .timetable_warnings {
color: black; display: flex;
gap: 0.25em;
.warning {
padding: 0.1em 0.3em;
margin-right: 0.3em;
border-radius: 1em;
font-weight: bold;
&.twr {
background: var(--clr-twr);
}
&.skr {
background: var(--clr-skr);
}
}
} }
.timetable_progress { .timetable_progress {
@@ -219,29 +200,8 @@ export default defineComponent({
flex-wrap: wrap; flex-wrap: wrap;
} }
.timetable_progress-bar { .timetable_progress-distance {
position: relative; margin-right: 0.25em;
width: 6em;
height: 1em;
margin: 0.5em 0;
.bar-fg,
.bar-bg {
position: absolute;
height: 1em;
width: 100%;
left: 0;
}
.bar-fg {
background-color: springgreen;
}
.bar-bg {
background-color: #5b5b5b;
}
} }
.comments { .comments {
@@ -258,16 +218,24 @@ export default defineComponent({
} }
@include smallScreen() { @include smallScreen() {
.simple { .train-info {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 1em 0; gap: 1em 0;
text-align: center; text-align: center;
font-size: 1.25em; font-size: 1.15em;
} }
.info-stats { .train-stats {
text-align: center; font-size: 1.1em;
}
.train_general {
justify-content: center;
}
.train-status-badges {
justify-content: center;
} }
.timetable_route { .timetable_route {
+169 -212
View File
@@ -1,269 +1,226 @@
<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="exitIcon" alt="exit-icon" @click="() => (searchedTrain = '')" /> <transition name="options-anim">
<div class="options_wrapper" v-if="showOptions">
<div class="options_content">
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
<div class="search_content">
<div class="search-box">
<input
class="search-input"
ref="initFocusedElement"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
:placeholder="$t(`options.search-train`)"
v-model="searchedTrain"
/>
<button class="search-exit">
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear('train')" />
</button>
</div>
<div class="search-box">
<input
class="search-input"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
:placeholder="$t(`options.search-driver`)"
v-model="searchedDriver"
/>
<button class="search-exit">
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear('driver')" />
</button>
</div>
</div> </div>
<div class="search-box"> <h1 class="option-title">{{ $t('options.sort-title') }}</h1>
<input class="search-input" v-model="searchedDriver" :placeholder="$t('trains.search-driver')" /> <div class="options_sorters">
<button
v-for="opt in translatedSorterOptions"
class="sort-option btn--option"
:data-selected="opt.id == sorterActive.id"
@click="onSorterChange(opt)"
>
{{ opt.value.toUpperCase() }}
</button>
</div>
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="() => (searchedDriver = '')" /> <h1 class="option-title" v-if="trainFilterList.length != 0">{{ $t('options.filter-title') }}</h1>
<div class="options_filters">
<div v-for="section in Object.keys(TrainFilterSection)">
<button
class="btn--option"
v-for="filter in trainFilterList.filter((f) => f.section == section)"
:data-inactive="!filter.isActive"
@click="onFilterChange(filter)"
>
{{ $t(`options.filter-${filter.id}`) }}
</button>
</div>
</div>
<div class="filter-actions">
<div></div>
<button class="btn--action" @click="resetAllFilters">{{ $t('options.filter-reset') }}</button>
</div> </div>
</div> </div>
</div> </div>
</div> </transition>
<div class="filters">
<span
:class="{ active: filter.isActive }"
class="filter"
v-for="filter in filterList"
:key="filter.id"
tabindex="0"
@contextmenu="
(e) => {
e.preventDefault();
return false;
}
"
@click.left="toggleFilter(filter)"
@keydown.enter="toggleFilter(filter)"
@click.right="setFilterOnly(filter)"
@keydown.space="setFilterOnly(filter)"
>
{{ $t(`trains.filter-${filter.id}`) }}
</span>
<span class="filter reset-btn" @click="resetFilters" tabindex="0">
{{ $t('trains.filter-reset') }}
</span>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, inject, TrainFilter } from 'vue'; import { defineComponent, inject, PropType } from 'vue';
import { useI18n } from 'vue-i18n'; import imageMixin from '../../mixins/imageMixin';
import keyMixin from '../../mixins/keyMixin';
import ActionButton from '../Global/ActionButton.vue';
import SelectBox from '../Global/SelectBox.vue'; import SelectBox from '../Global/SelectBox.vue';
import { TrainFilterSection } from '../../scripts/enums/TrainFilterType';
import { TrainFilter } from '../../scripts/interfaces/Trains/TrainFilter';
export default defineComponent({ export default defineComponent({
components: { SelectBox }, components: { SelectBox, ActionButton },
emits: ['changeSearchedTrain', 'changeSearchedDriver', 'changeSorter'], mixins: [imageMixin, keyMixin],
data: () => ({ props: {
exitIcon: require('@/assets/icon-exit.svg'), sorterOptionIds: {
}), type: Array as PropType<Array<string>>,
required: true,
},
setup() { currentOptionsActive: {
const { t } = useI18n(); type: Boolean,
default: false,
const sorterOptions = [ },
{ },
id: 'distance',
value: 'kilometraż',
},
{
id: 'progress',
value: 'przebyta trasa',
},
{
id: 'delay',
value: 'opóźnienie',
},
{
id: 'mass',
value: 'masa',
},
{
id: 'speed',
value: 'prędkość',
},
{
id: 'length',
value: 'długość',
},
];
let filterList = inject('filterList') as TrainFilter[];
const translatedSorterOptions = computed(() =>
sorterOptions.map(({ id }) => ({
id,
value: t(`trains.option-${id}`),
}))
);
data() {
return { return {
translatedSorterOptions, showOptions: false,
searchedTrain: inject('searchedTrain') as string, lastSelectedFilter: null as TrainFilter | null,
searchedDriver: inject('searchedDriver') as string, TrainFilterSection,
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;
@include smallScreen() {
justify-content: center;
}
} }
.filter { .options_sorters {
background: #333; display: flex;
padding: 0.2em 0.25em; grid-template-columns: repeat(3, 1fr);
margin: 0.25em 0.25em 0 0;
font-weight: bold;
cursor: pointer;
color: gray;
&.active {
color: var(--clr-primary);
}
&.reset-btn {
color: salmon;
}
} }
@include smallScreen() { .options_filters > div {
.journal-options { display: flex;
width: 100%;
gap: 0.5em;
button {
width: 100%; width: 100%;
} color: springgreen;
.options { font-weight: bold;
&_wrapper {
justify-content: center;
}
&_content { &[data-inactive='true'] {
padding: 0 1em; color: #aaa;
flex-direction: column;
.content_select {
margin: 0 auto;
padding: 0;
}
.content_search {
justify-content: center;
}
} }
} }
}
.search { .filter-actions {
&-box, display: flex;
&-button { gap: 0.5em;
margin: 0.5em 0 0 0; width: 100%;
}
&-box { margin-top: 1em;
width: 100%;
}
&-button { > * {
width: 80%; width: 100%;
max-width: 300px;
}
} }
} }
</style> </style>
@@ -1,159 +0,0 @@
<template>
<section class="filter-card" v-click-outside="closeCard">
<div class="card_btn">
<action-button @click="toggleCard">
<img class="button_icon" :src="filterIcon" alt="icon-filter" />
<p>{{ $t('options.filters') }}</p>
</action-button>
</div>
<transition name="card-anim">
<div class="card_content card" v-if="isVisible">
<div class="card_exit" @click="closeCard"></div>
<div class="options_wrapper">
<div class="options_content">
<div class="content_select">
<select-box
:itemList="translatedSorterOptions"
:defaultItemIndex="0"
@selected="changeSorter"
:prefix="$t('trains.sorter-prefix')"
/>
</div>
<div class="content_search">
<div class="search-box">
<input class="search-input" v-model="searchedTrain" :placeholder="$t('trains.search-train')" />
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="() => (searchedTrain = '')" />
</div>
<div class="search-box">
<input class="search-input" v-model="searchedDriver" :placeholder="$t('trains.search-driver')" />
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="() => (searchedDriver = '')" />
</div>
</div>
</div>
</div>
<section class="card_actions flex">
<action-button class="outlined">
{{ $t('filters.reset') }}
</action-button>
<action-button class="outlined" @click="closeCard">{{ $t('filters.close') }}</action-button>
</section>
</div>
</transition>
</section>
</template>
<script lang="ts">
import { defineComponent, inject } from '@vue/runtime-core';
import inputData from '@/data/options.json';
import ActionButton from '@/components/Global/ActionButton.vue';
import { sorterOptions } from '@/data/trainOptions';
import { TrainFilter, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import SelectBox from '../Global/SelectBox.vue';
export default defineComponent({
components: { ActionButton, SelectBox },
emits: ['changeFilterValue', 'invertFilters', 'resetFilters'],
data: () => ({
filterIcon: require('@/assets/icon-filter2.svg'),
exitIcon: require('@/assets/icon-exit.svg'),
inputs: { ...inputData },
}),
setup() {
const isVisible = inject('isTrainOptionsCardVisible');
const { t } = useI18n();
let filterList = inject('filterList') as TrainFilter[];
const translatedSorterOptions = computed(() =>
sorterOptions.map(({ id }) => ({
id,
value: t(`trains.option-${id}`),
}))
);
return {
translatedSorterOptions,
searchedTrain: inject('searchedTrain') as string,
searchedDriver: inject('searchedDriver') as string,
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
filterList,
isVisible,
};
},
methods: {
closeCard() {
this.isVisible = false;
},
toggleCard() {
this.isVisible = !this.isVisible;
},
changeSorter(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id;
this.sorterActive.dir = -1;
},
toggleFilter(filter: TrainFilter) {
filter.isActive = !filter.isActive;
},
setFilterOnly(filter: TrainFilter) {
this.filterList.forEach((f) => (f.isActive = f.id == filter.id));
},
resetFilters() {
this.filterList.forEach((f) => (f.isActive = true));
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive';
@import '../../styles/card';
.card-anim {
&-enter-active,
&-leave-active {
transition: all $animDuration $animType;
}
&-enter-from,
&-leave-to {
transform: translate(-50%, -50%) scale(0.85);
opacity: 0;
}
}
.card {
section {
margin: 0.5em 0;
}
&_title {
font-size: 2em;
font-weight: 700;
color: $accentCol;
margin: 0.5em 0;
text-align: center;
}
}
</style>
+24 -25
View File
@@ -60,11 +60,13 @@
<b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span> <b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span>
</div> </div>
<span v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine"> <span
v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine && !/sbl/gi.test(stop.departureLine!)"
>
{{ stop.departureLine }} {{ stop.departureLine }}
</span> </span>
<span v-else> <span v-else-if="!/sbl/gi.test(stop.departureLine!)">
{{ stop.departureLine }} / {{ stop.departureLine }} /
{{ train.timetableData!.followingStops[i + 1].arrivalLine }} {{ train.timetableData!.followingStops[i + 1].arrivalLine }}
</span> </span>
@@ -83,10 +85,12 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType } from '@vue/runtime-core'; import { computed, defineComponent, PropType } from '@vue/runtime-core';
import dateMixin from '@/mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import TrainStop from '@/scripts/interfaces/TrainStop'; import imageMixin from '../../mixins/imageMixin';
import Train from '../../scripts/interfaces/Train';
import TrainStop from '../../scripts/interfaces/TrainStop';
import { useStore } from '../../store/store';
import StopDate from '../Global/StopDate.vue'; import StopDate from '../Global/StopDate.vue';
import Train from '@/scripts/interfaces/Train';
export default defineComponent({ export default defineComponent({
components: { StopDate }, components: { StopDate },
@@ -97,18 +101,14 @@ export default defineComponent({
}, },
}, },
mixins: [dateMixin], mixins: [dateMixin, imageMixin],
emits: ['click'], emits: ['click'],
data: () => ({
icons: {
warning: require('@/assets/icon-warning.svg'),
},
}),
setup(props) { setup(props) {
return { return {
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
@@ -154,7 +154,7 @@ export default defineComponent({
onImageError(e: Event) { onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement; const imageEl = e.target as HTMLImageElement;
imageEl.src = require('@/assets/unknown.png'); imageEl.src = this.getImage('unknown.png');
}, },
}, },
}); });
@@ -179,12 +179,7 @@ $stopNameClr: #22a8d1;
} }
.train-schedule { .train-schedule {
background-color: #202020;
padding: 0 0.25em; padding: 0 0.25em;
@include smallScreen() {
font-size: 1.1em;
}
} }
.train-stock { .train-stock {
@@ -192,10 +187,11 @@ $stopNameClr: #22a8d1;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
ul.stock-list { ul.stock-list {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
overflow-x: auto; overflow: auto;
padding-bottom: 1em; padding-bottom: 1em;
li > div { li > div {
@@ -203,11 +199,14 @@ ul.stock-list {
color: #aaa; color: #aaa;
font-size: 0.9em; font-size: 0.9em;
} }
img {
max-height: 60px;
}
} }
.schedule-wrapper { .schedule-wrapper {
overflow-y: auto; overflow-y: auto;
max-height: 500px;
width: 100%; width: 100%;
z-index: 5; z-index: 5;
@@ -278,13 +277,14 @@ ul.stop_list > li.stop {
padding: 0 0.5em; padding: 0 0.5em;
&.sbl { &.sbl {
.stop-name,
.stop-date { .stop-date {
opacity: 0.7; display: none;
} }
.stop-name { .stop-name {
background-color: #333; background: none;
color: #aaa;
padding: 0;
} }
} }
@@ -381,8 +381,6 @@ ul.stop_list > li.stop {
text-align: center; text-align: center;
flex-wrap: wrap; flex-wrap: wrap;
padding: 0.15em 0;
} }
.stop-bar { .stop-bar {
@@ -428,3 +426,4 @@ ul.stop_list > li.stop {
} }
} }
</style> </style>
+35 -71
View File
@@ -1,29 +1,27 @@
<template> <template>
<div class="train-stats" v-click-outside="closeStats"> <div class="train-stats" v-click-outside="closeStats">
<action-button class="stats_button" @click="toggleStatsOpen"> <action-button class="stats_button" @click="toggleStatsOpen">
<img :src="statsIcon" :alt="$t('trains.stats')" /> <img :src="getIcon('stats')" :alt="$t('trains.stats')" />
<p>{{ $t("trains.stats") }}</p> <p>{{ $t('trains.stats') }}</p>
</action-button> </action-button>
<transition name="stats-anim" class="stats_wrapper" tag="div"> <transition name="stats-anim" class="stats_wrapper" tag="div">
<div class="stats-body" v-if="trainStatsOpen"> <div class="stats-body" v-if="trainStatsOpen">
<h2 class="stats-header"> <h2 class="stats-header">
<img :src="statsIcon" :alt="$t('trains.stats')" /> <img :src="getIcon('stats')" :alt="$t('trains.stats')" />
{{ $t("trains.stats") }} {{ $t('trains.stats') }}
</h2> </h2>
<div class="stats-speed"> <div class="stats-speed">
<div class="title stats-title"> <div class="title stats-title">
{{ $t("trains.stats-speed") }} {{ $t('trains.stats-speed') }}
</div>
<div class="stats-content">
{{ speedStats.min }} | {{ speedStats.avg }} | {{ speedStats.max }}
</div> </div>
<div class="stats-content">{{ speedStats.min }} | {{ speedStats.avg }} | {{ speedStats.max }}</div>
</div> </div>
<div class="stats-length"> <div class="stats-length">
<div class="title stats-title"> <div class="title stats-title">
{{ $t("trains.stats-length") }} {{ $t('trains.stats-length') }}
</div> </div>
<div class="stats-content"> <div class="stats-content">
{{ timetableStats.min }} | {{ timetableStats.avg }} | {{ timetableStats.min }} | {{ timetableStats.avg }} |
@@ -33,15 +31,11 @@
<div class="stats-categories"> <div class="stats-categories">
<div class="title stats-title"> <div class="title stats-title">
{{ $t("trains.stats-categories") }} {{ $t('trains.stats-categories') }}
</div> </div>
<div class="category-list"> <div class="category-list">
<span <span class="category" v-for="[key, value] of categoryList" :key="key">
class="category"
v-for="[key, value] of categoryList"
:key="key"
>
<span class="category-type">{{ key }}</span> <span class="category-type">{{ key }}</span>
<span class="category-count">{{ value }}</span> <span class="category-count">{{ value }}</span>
</span> </span>
@@ -49,28 +43,22 @@
<div class="special-list"> <div class="special-list">
<span class="special twr"> <span class="special twr">
<span class="special-type">{{ <span class="special-type">{{ $t('trains.stats-special-twr') }}</span>
$t("trains.stats-special-twr")
}}</span>
<span class="special-count">{{ specialTrainCount[0] }}</span> <span class="special-count">{{ specialTrainCount[0] }}</span>
</span> </span>
<span class="special skr"> <span class="special skr">
<span class="special-type">{{ <span class="special-type">{{ $t('trains.stats-special-skr') }}</span>
$t("trains.stats-special-skr")
}}</span>
<span class="special-count">{{ specialTrainCount[1] }}</span> <span class="special-count">{{ specialTrainCount[1] }}</span>
</span> </span>
</div> </div>
</div> </div>
<div class="stats-locos"> <div class="stats-locos">
<div class="title stats-title">{{ $t("trains.stats-locos") }}</div> <div class="title stats-title">{{ $t('trains.stats-locos') }}</div>
<div class="loco-list stats-content"> <div class="loco-list stats-content">
<div class="loco-item" v-for="(loco, i) in locoList" :key="i"> <div class="loco-item" v-for="(loco, i) in locoList" :key="i">{{ loco[0] }} | {{ loco[1] }}</div>
{{ loco[0] }} | {{ loco[1] }}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -79,13 +67,15 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import ActionButton from "@/components/Global/ActionButton.vue"; import { defineComponent, computed, inject } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import Train from "@/scripts/interfaces/Train"; import Train from '../../scripts/interfaces/Train';
import { computed, defineComponent, inject } from "@vue/runtime-core"; import ActionButton from '../Global/ActionButton.vue';
export default defineComponent({ export default defineComponent({
components: { ActionButton }, components: { ActionButton },
mixins: [imageMixin],
props: { props: {
trains: { trains: {
type: Array as () => Train[], type: Array as () => Train[],
@@ -95,7 +85,6 @@ export default defineComponent({
data: () => ({ data: () => ({
trainStatsOpen: false, trainStatsOpen: false,
statsIcon: require("@/assets/icon-stats.svg"),
}), }),
methods: { methods: {
@@ -110,14 +99,11 @@ export default defineComponent({
setup(props) { setup(props) {
const speedStats = computed(() => { const speedStats = computed(() => {
if (props.trains.length == 0) return { avg: "0", min: "0", max: "0" }; if (props.trains.length == 0) return { avg: '0', min: '0', max: '0' };
const trainList = props.trains.filter((train) => train.timetableData); const trainList = props.trains.filter((train) => train.timetableData);
const avg = ( const avg = (trainList.reduce((acc, train) => acc + train.speed, 0) / trainList.length).toFixed(2);
trainList.reduce((acc, train) => acc + train.speed, 0) /
trainList.length
).toFixed(2);
const minMaxSpeed = trainList.reduce((acc, train) => { const minMaxSpeed = trainList.reduce((acc, train) => {
if (!train.timetableData) return acc; if (!train.timetableData) return acc;
@@ -136,32 +122,21 @@ export default defineComponent({
}); });
const timetableStats = computed(() => { const timetableStats = computed(() => {
if (props.trains.length == 0) return { avg: "0", min: "0", max: "0" }; if (props.trains.length == 0) return { avg: '0', min: '0', max: '0' };
const activeTrainsLength = props.trains.filter( const activeTrainsLength = props.trains.filter((train) => train.timetableData).length;
(train) => train.timetableData
).length;
const avg = ( const avg = (
props.trains.reduce( props.trains.reduce((acc, train) => (train.timetableData ? acc + train.timetableData.routeDistance : acc), 0) /
(acc, train) => activeTrainsLength
train.timetableData ? acc + train.timetableData.routeDistance : acc,
0
) / activeTrainsLength
).toFixed(2); ).toFixed(2);
const minMaxDistance = props.trains.reduce((acc, train) => { const minMaxDistance = props.trains.reduce((acc, train) => {
if (!train.timetableData) return acc; if (!train.timetableData) return acc;
acc[0] = acc[0] = !acc[0] || train.timetableData.routeDistance < acc[0] ? train.timetableData.routeDistance : acc[0];
!acc[0] || train.timetableData.routeDistance < acc[0]
? train.timetableData.routeDistance
: acc[0];
acc[1] = acc[1] = !acc[1] || train.timetableData.routeDistance > acc[1] ? train.timetableData.routeDistance : acc[1];
!acc[1] || train.timetableData.routeDistance > acc[1]
? train.timetableData.routeDistance
: acc[1];
return acc; return acc;
}, [] as any); }, [] as any);
@@ -178,9 +153,7 @@ export default defineComponent({
acc.set( acc.set(
train.timetableData.category, train.timetableData.category,
acc.get(train.timetableData.category) acc.get(train.timetableData.category) ? acc.get(train.timetableData.category) + 1 : 1
? acc.get(train.timetableData.category) + 1
: 1
); );
return acc; return acc;
@@ -193,35 +166,26 @@ export default defineComponent({
const map: Map<string, number> = props.trains.reduce((acc, train) => { const map: Map<string, number> = props.trains.reduce((acc, train) => {
if (!train.timetableData || !train.locoType) return acc; if (!train.timetableData || !train.locoType) return acc;
acc.set( acc.set(train.locoType, acc.get(train.locoType) ? acc.get(train.locoType) + 1 : 1);
train.locoType,
acc.get(train.locoType) ? acc.get(train.locoType) + 1 : 1
);
return acc; return acc;
}, new Map()); }, new Map());
const sorted = [...map.entries()] const sorted = [...map.entries()].sort((a, b) => b[1] - a[1]).filter((v, i) => i < 3);
.sort((a, b) => b[1] - a[1])
.filter((v, i) => i < 3);
return sorted; return sorted;
}); });
const specialTrainCount = computed(() => { const specialTrainCount = computed(() => {
const twrList = props.trains.filter( const twrList = props.trains.filter((train) => train.timetableData && train.timetableData.TWR);
(train) => train.timetableData && train.timetableData.TWR
);
const skrList = props.trains.filter( const skrList = props.trains.filter((train) => train.timetableData && train.timetableData.SKR);
(train) => train.timetableData && train.timetableData.SKR
);
return [twrList.length, skrList.length]; return [twrList.length, skrList.length];
}); });
/* Inject list from TrainsView for category filter */ /* Inject list from TrainsView for category filter */
const chosenTrainCategories = inject("chosenTrainCategories") as string[]; const chosenTrainCategories = inject('chosenTrainCategories') as string[];
return { return {
speedStats, speedStats,
@@ -229,14 +193,14 @@ export default defineComponent({
categoryList, categoryList,
locoList, locoList,
specialTrainCount, specialTrainCount,
chosenTrainCategories chosenTrainCategories,
}; };
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../styles/responsive"; @import '../../styles/responsive';
.stats-anim { .stats-anim {
&-enter-active, &-enter-active,
+52 -108
View File
@@ -1,83 +1,59 @@
<template> <template>
<div class="train-table" @keydown.esc="closeTimetable"> <div class="train-table">
<button class="return-btn" @click="scrollToTop" v-if="showReturnButton">
<img :src="icons.arrowAsc" alt="return arrow" />
</button>
<transition name="anim" mode="out-in"> <transition name="anim" mode="out-in">
<div :key="store.dataStatuses.trains"> <div :key="store.dataStatuses.trains">
<Loading v-if="trains.length == 0 && store.dataStatuses.trains == 0" /> <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"> <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"
:key="train.trainNo + train.driverId" :key="train.trainId"
@click="toggleTimetable(train)" tabindex="0"
@keydown.enter="toggleTimetable(train)" @click.stop="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
> >
<TrainInfo :train="train" /> <TrainInfo :train="train" />
<TrainSchedule v-if="chosenTrainId == getTrainId(train)" :train="train" ref="card-inner" tabindex="0" />
</li> </li>
</ul> </transition-group>
</div> </div>
</transition> </transition>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, inject, Ref } from '@vue/runtime-core'; import { computed, defineComponent, inject, PropType, Ref } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import defaultVehicleIconsJSON from '@/data/defaultVehicleIcons.json'; import returnBtnMixin from '../../mixins/returnBtnMixin';
import Train from '../../scripts/interfaces/Train';
import Train from '@/scripts/interfaces/Train'; import { useStore } from '../../store/store';
import TrainSchedule from '@/components/TrainsView/TrainSchedule.vue';
import TrainInfo from '@/components/TrainsView/TrainInfo.vue';
import returnBtnMixin from '@/mixins/returnBtnMixin';
import { useStore } from '@/store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import TrainInfo from './TrainInfo.vue';
export default defineComponent({ export default defineComponent({
components: { components: { Loading, TrainInfo },
TrainSchedule,
TrainInfo,
Loading,
},
mixins: [returnBtnMixin],
props: { props: {
trains: { trains: {
type: Array as () => Train[], type: Array as PropType<Train[]>,
required: true, required: true,
}, },
}, },
data: () => ({ mixins: [returnBtnMixin, modalTrainMixin],
defaultLocoImage: require('@/assets/unknown.png'),
icons: {
arrowAsc: require('@/assets/icon-arrow-asc.svg'),
arrowDesc: require('@/assets/icon-arrow-desc.svg'),
},
defaultVehicleIcons: defaultVehicleIconsJSON,
chosenTrainId: null as string | null,
}),
setup(props) { setup(props) {
const store = useStore(); const store = useStore();
const searchedTrain = inject('searchedTrain') as Ref<string>; const searchedTrain = inject('searchedTrain') as Ref<string>;
const searchedDriver = inject('searchedDriver') as Ref<string>; const searchedDriver = inject('searchedDriver') as Ref<string>;
const currentTrains = computed(() => { const currentTrains = computed(() => {
return props.trains; return props.trains;
}); });
@@ -87,80 +63,29 @@ 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;
distanceLimitExceeded: computed( dir: number;
() => props.trains.findIndex(({ timetableData }) => timetableData && timetableData.routeDistance > 200) != -1 },
),
}; };
}, },
activated() { activated() {
const query = this.$route.query; const query = this.$route.query;
if (query.trainNo && query.driverName) { if (query.trainNo && query.driverName) {
this.searchedDriver = query.driverName.toString(); this.searchedDriver = query.driverName.toString();
this.searchedTrain = query.trainNo.toString(); this.searchedTrain = query.trainNo.toString();
setTimeout(() => { setTimeout(() => {
this.chosenTrainId = query.driverName + <string>query.trainNo; this.selectModalTrain(query.driverName! + query.trainNo!.toString());
}, 20); }, 20);
} }
}, },
deactivated() {
this.chosenTrainId = null;
},
methods: {
enter(el: HTMLElement) {
const maxHeight = getComputedStyle(el).height;
el.style.height = '0px';
getComputedStyle(el);
setTimeout(() => {
el.style.height = maxHeight;
}, 10);
},
afterEnter(el: HTMLElement) {
el.style.height = 'auto';
},
leave(el: HTMLElement) {
el.style.height = getComputedStyle(el).height;
setTimeout(() => {
el.style.height = '0px';
}, 10);
},
toggleTimetable(train: Train, state?: boolean) {
const id = this.getTrainId(train);
if (state !== undefined) {
this.chosenTrainId = state ? id : null;
return;
}
this.chosenTrainId = this.chosenTrainId && this.chosenTrainId == id ? null : id;
},
closeTimetable() {
this.chosenTrainId = null;
},
getTrainId(train: Train) {
return train.driverName + train.trainNo.toString();
},
},
}); });
</script> </script>
<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,
@@ -181,11 +106,10 @@ export default defineComponent({
text-align: center; text-align: center;
padding: 1em 0; padding: 1em 0;
margin: 1em 0;
font-size: 1.5em; font-size: 1.5em;
background: #333; background: #1a1a1a;
} }
img.train-image { img.train-image {
@@ -198,11 +122,31 @@ 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,46 @@
import { JournalFilterSection, JournalFilterType } from '../../scripts/enums/JournalFilterType';
import { JournalFilter } from '../../scripts/types/JournalTimetablesTypes';
export const journalTimetableFilters: JournalFilter[] = [
{
id: JournalFilterType.ALL,
filterSection: JournalFilterSection.TIMETABLE_STATUS,
isActive: true,
},
{
id: JournalFilterType.ACTIVE,
filterSection: JournalFilterSection.TIMETABLE_STATUS,
isActive: false,
},
{
id: JournalFilterType.FULFILLED,
filterSection: JournalFilterSection.TIMETABLE_STATUS,
isActive: false,
},
{
id: JournalFilterType.ABANDONED,
filterSection: JournalFilterSection.TIMETABLE_STATUS,
isActive: false,
},
{
id: JournalFilterType.TWR_SKR,
filterSection: JournalFilterSection.TWRSKR,
isActive: true,
},
{
id: JournalFilterType.TWR,
filterSection: JournalFilterSection.TWRSKR,
isActive: false,
},
{
id: JournalFilterType.SKR,
filterSection: JournalFilterSection.TWRSKR,
isActive: false,
},
];
@@ -0,0 +1,89 @@
import { TrainFilterSection, TrainFilterType } from '../../scripts/enums/TrainFilterType';
import { TrainFilter } from '../../scripts/interfaces/Trains/TrainFilter';
export const trainFilters: TrainFilter[] = [
{
id: TrainFilterType.twr,
section: TrainFilterSection.TRAIN_TYPE,
isActive: true,
},
{
id: TrainFilterType.skr,
section: TrainFilterSection.TRAIN_TYPE,
isActive: true,
},
{
id: TrainFilterType.common,
section: TrainFilterSection.TRAIN_TYPE,
isActive: true,
},
{
id: TrainFilterType.passenger,
section: TrainFilterSection.TIMETABLE_TYPE,
isActive: true,
},
{
id: TrainFilterType.freight,
section: TrainFilterSection.TIMETABLE_TYPE,
isActive: true,
},
{
id: TrainFilterType.other,
section: TrainFilterSection.TIMETABLE_TYPE,
isActive: true,
},
{
id: TrainFilterType.withComments,
section: TrainFilterSection.COMMENTS,
isActive: true,
},
{
id: TrainFilterType.noComments,
section: TrainFilterSection.COMMENTS,
isActive: true,
},
{
id: TrainFilterType.withTimetable,
section: TrainFilterSection.TIMETABLE,
isActive: true,
},
{
id: TrainFilterType.noTimetable,
section: TrainFilterSection.TIMETABLE,
isActive: true,
},
];
export const sorterOptions = [
{
id: 'distance',
value: 'kilometraż',
},
{
id: 'id',
value: 'id rozkładu',
},
{
id: 'progress',
value: 'przebyta trasa',
},
{
id: 'delay',
value: 'opóźnienie',
},
{
id: 'mass',
value: 'masa',
},
{
id: 'speed',
value: 'prędkość',
},
{
id: 'length',
value: 'długość',
},
];
-30
View File
@@ -1,30 +0,0 @@
import { JournalFilterType } from "@/scripts/enums/JournalFilterType";
import { JournalFilter } from "vue";
export const journalTimetableFilters: JournalFilter[] = [
{
id: JournalFilterType.all,
filterSection: "timetable-status",
isActive: true
},
{
id: JournalFilterType.active,
filterSection: "timetable-status",
isActive: false
},
{
id: JournalFilterType.fulfilled,
filterSection: "timetable-status",
isActive: false
},
{
id: JournalFilterType.abandoned,
filterSection: "timetable-status",
isActive: false
},
]
export const journalDispatcherFilters: JournalFilter[] = []
+65 -55
View File
@@ -1,41 +1,38 @@
{ {
"optionSections": ["reality", "package-access", "access", "control", "addons", "blockades", "signals", "status"],
"options": [ "options": [
{
"id": "default",
"name": "default",
"iconName": "td2",
"section": "access",
"value": true,
"defaultValue": true
},
{
"id": "not-default",
"name": "notDefault",
"iconName": "",
"section": "access",
"value": true,
"defaultValue": true
},
{ {
"id": "real", "id": "real",
"name": "real", "name": "real",
"iconName": "lock", "section": "reality",
"section": "access",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
}, },
{ {
"id": "fictional", "id": "fictional",
"name": "fictional", "name": "fictional",
"iconName": "user", "section": "reality",
"section": "access", "value": true,
"defaultValue": true
},
{
"id": "default",
"name": "default",
"section": "package-access",
"value": true,
"defaultValue": true
},
{
"id": "not-default",
"name": "notDefault",
"section": "package-access",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
}, },
{ {
"id": "non-public", "id": "non-public",
"name": "nonPublic", "name": "nonPublic",
"iconName": "user",
"section": "access", "section": "access",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -43,7 +40,6 @@
{ {
"id": "unavailable", "id": "unavailable",
"name": "unavailable", "name": "unavailable",
"iconName": "user",
"section": "access", "section": "access",
"value": false, "value": false,
"defaultValue": false "defaultValue": false
@@ -51,7 +47,6 @@
{ {
"id": "abandoned", "id": "abandoned",
"name": "abandoned", "name": "abandoned",
"iconName": "user",
"section": "access", "section": "access",
"value": false, "value": false,
"defaultValue": false "defaultValue": false
@@ -59,7 +54,6 @@
{ {
"id": "SPK", "id": "SPK",
"name": "SPK", "name": "SPK",
"iconName": "SPK",
"section": "control", "section": "control",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -67,7 +61,6 @@
{ {
"id": "SCS", "id": "SCS",
"name": "SCS", "name": "SCS",
"iconName": "SCS",
"section": "control", "section": "control",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -75,15 +68,21 @@
{ {
"id": "SPE", "id": "SPE",
"name": "SPE", "name": "SPE",
"iconName": "SPE", "section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPK-M",
"name": "mechaniczne+SPK",
"section": "control", "section": "control",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
}, },
{ {
"id": "manual", "id": "SCS-M",
"name": "ręczne", "name": "mechaniczne+SCS",
"iconName": "ręczne",
"section": "control", "section": "control",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -91,7 +90,27 @@
{ {
"id": "mechanical", "id": "mechanical",
"name": "mechaniczne", "name": "mechaniczne",
"iconName": "mechaniczne", "section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPK-R",
"name": "ręczne+SPK",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SCS-R",
"name": "ręczne+SCS",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "manual",
"name": "ręczne",
"section": "control", "section": "control",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -99,23 +118,34 @@
{ {
"id": "SUP", "id": "SUP",
"name": "SUP", "name": "SUP",
"iconName": "SUP", "section": "addons",
"section": "control", "value": true,
"defaultValue": true
},
{
"id": "noSUP",
"name": "noSUP",
"section": "addons",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
}, },
{ {
"id": "SBL", "id": "SBL",
"name": "SBL", "name": "SBL",
"iconName": "SBL", "section": "blockades",
"section": "routes", "value": true,
"defaultValue": true
},
{
"id": "PBL",
"name": "PBL",
"section": "blockades",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
}, },
{ {
"id": "modern", "id": "modern",
"name": "współczesna", "name": "współczesna",
"iconName": "współczesna",
"section": "signals", "section": "signals",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -123,7 +153,6 @@
{ {
"id": "semaphores", "id": "semaphores",
"name": "kształtowa", "name": "kształtowa",
"iconName": "kształtowa",
"section": "signals", "section": "signals",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -131,7 +160,6 @@
{ {
"id": "mixed", "id": "mixed",
"name": "mieszana", "name": "mieszana",
"iconName": "mieszana",
"section": "signals", "section": "signals",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -139,7 +167,6 @@
{ {
"id": "historical", "id": "historical",
"name": "historyczna", "name": "historyczna",
"iconName": "historyczna",
"section": "signals", "section": "signals",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -148,7 +175,6 @@
{ {
"id": "free", "id": "free",
"name": "free", "name": "free",
"iconName": "",
"section": "status", "section": "status",
"value": false, "value": false,
@@ -157,7 +183,6 @@
{ {
"id": "occupied", "id": "occupied",
"name": "occupied", "name": "occupied",
"iconName": "",
"section": "status", "section": "status",
"value": true, "value": true,
@@ -166,7 +191,6 @@
{ {
"id": "endingStatus", "id": "endingStatus",
"name": "endingStatus", "name": "endingStatus",
"iconName": "",
"section": "status", "section": "status",
"value": true, "value": true,
@@ -175,7 +199,6 @@
{ {
"id": "afkStatus", "id": "afkStatus",
"name": "afkStatus", "name": "afkStatus",
"iconName": "",
"section": "status", "section": "status",
"value": true, "value": true,
@@ -184,7 +207,6 @@
{ {
"id": "noSpaceStatus", "id": "noSpaceStatus",
"name": "noSpaceStatus", "name": "noSpaceStatus",
"iconName": "",
"section": "status", "section": "status",
"value": true, "value": true,
@@ -193,20 +215,10 @@
{ {
"id": "unavailableStatus", "id": "unavailableStatus",
"name": "unavailableStatus", "name": "unavailableStatus",
"iconName": "",
"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": [
@@ -263,7 +275,6 @@
{ {
"id": "include-selected", "id": "include-selected",
"name": "include-selected", "name": "include-selected",
"iconName": "",
"section": "mode", "section": "mode",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -271,7 +282,6 @@
{ {
"id": "save", "id": "save",
"name": "save", "name": "save",
"iconName": "",
"section": "mode", "section": "mode",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
-60
View File
@@ -1,60 +0,0 @@
import { TrainFilterType } from "@/scripts/enums/TrainFilterType";
import { TrainFilter } from "vue";
export const trainFilters: TrainFilter[] = [
{
id: TrainFilterType.twr,
isActive: true,
},
{
id: TrainFilterType.skr,
isActive: true,
},
{
id: TrainFilterType.passenger,
isActive: true,
},
{
id: TrainFilterType.freight,
isActive: true,
},
{
id: TrainFilterType.other,
isActive: true,
},
{
id: TrainFilterType.comments,
isActive: true,
},
{
id: TrainFilterType.noTimetable,
isActive: true,
},
];
export const sorterOptions = [
{
id: 'distance',
value: 'kilometraż',
},
{
id: 'progress',
value: 'przebyta trasa',
},
{
id: 'delay',
value: 'opóźnienie',
},
{
id: 'mass',
value: 'masa',
},
{
id: 'speed',
value: 'prędkość',
},
{
id: 'length',
value: 'długość',
}
];
View File
+218 -72
View File
@@ -1,4 +1,10 @@
{ {
"general": {
"and": " and ",
"refresh": "REFRESH",
"TWR": "High risk freight train",
"SKR": "Train with exceeded gauge"
},
"app": { "app": {
"sceneries": "SCENERIES", "sceneries": "SCENERIES",
"trains": "TRAINS", "trains": "TRAINS",
@@ -8,9 +14,21 @@
"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!"
},
"footer": {
"discord": "Stacjownik Discord server"
},
"update": {
"title": "New version of the app is available!",
"paragraph1": "Enjoy the application and may the green signal be with you!",
"release-link": "Click here to browse version changelog (GitHub)",
"confirm-button": "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!",
@@ -22,7 +40,7 @@
"desc": { "desc": {
"control-type": "Control type: ", "control-type": "Control type: ",
"signals-type": "Signals type: ", "signals-type": "Signals type: ",
"SBL": "This scenery has automatic line blockade system on following routes: ", "SBL": "This scenery has automatic block signalling (ABS/SBL) system on following routes: ",
"SUP": "Requires the SUP application (level crossing remote control simulator)", "SUP": "Requires the SUP application (level crossing remote control simulator)",
"TWB-all": "This scenery has two-way route blockade on all routes", "TWB-all": "This scenery has two-way route blockade on all routes",
"TWB-routes": "This scenery has two-way route blockade on following routes: ", "TWB-routes": "This scenery has two-way route blockade on following routes: ",
@@ -66,17 +84,90 @@
}, },
"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-issuedFrom": "Origin scenery name",
"search-timetables-date": "Timetable date (UTC+2 / CEST)",
"search-dispatchers-date": "Service date (UTC+2 / CEST)",
"search-date": "Date (UTC+2 / CEST)",
"sort-mass": "mass",
"sort-speed": "speed",
"sort-length": "length",
"sort-routeDistance": "route distance",
"sort-timetable": "train no.",
"sort-progress": "route progress",
"sort-delay": "current delay",
"sort-id": "timetable id",
"sort-allStopsCount": "total stops",
"sort-beginDate": "date",
"sort-timetableId": "timetable ID",
"sort-timestampFrom": "date",
"sort-duration": "duration",
"filter-noComments": "NO COMMENTS",
"filter-withComments": "COMMENTS",
"filter-twr": "HIGH RISK CARGO",
"filter-skr": "EXCEEDED GAUGE",
"filter-twr-skr": "ALL TYPES",
"filter-common": "NO WARNINGS",
"filter-passenger": "PASSENGER",
"filter-freight": "FREIGHT",
"filter-other": "OTHER",
"filter-noTimetable": "NO TIMETABLE",
"filter-withTimetable": "TIMETABLE",
"filter-reset": "RESET FILTERS",
"filter-clear": "CLEAR FILTERS",
"filter-section-timetable-status": "TIMETABLE STATUS",
"filter-section-twrskr": "WARNINGS",
"filter-all": "ALL ENTRIES",
"filter-abandoned": "ABANDONED",
"filter-fulfilled": "FULFILLED",
"filter-active": "ACTIVE"
}, },
"filters": { "filters": {
"desc": " &bull; Left mouse click: select / unselect chosen filter <br /> &bull; Double left click: unselect all filters but chosen from a <b class='text--primary'>group</b> <br /> &bull; <span style='color: coral'>RESET</span>: reset all filters from a <b class='text--primary'>group</b>",
"sections": {
"quick": "QUICK FILTERS",
"reality": "SCENERY REALITY",
"package-access": "IN-GAME AVAILABILITY",
"access": "GENERAL AVAILABILITY",
"control": "CONTROLS",
"signals": "SIGNALLING",
"addons": "ADDITIONAL PROGRAMS",
"blockades": "BLOCK SIGNALLING",
"status": "ONLINE STATUS"
},
"all-available": "ALL AVAILABLE",
"all-free": "CURRENTLY FREE",
"endingStatus": "ENDS SOON", "endingStatus": "ENDS SOON",
"afkStatus": "AFK", "afkStatus": "AFK",
"noSpaceStatus": "NO SPACE", "noSpaceStatus": "NO SPACE",
"unavailableStatus": "UNAVAILABLE", "unavailableStatus": "UNAVAILABLE",
"title": "STATION FILTER", "title": "STATION FILTERS",
"default": "DEFAULT", "default": "IN-GAME",
"not-default": "OTHER", "not-default": "ADDITIONAL",
"real": "REAL", "real": "REAL",
"fictional": "FICTIONAL", "fictional": "FICTIONAL",
"unavailable": "UNSUPPORTED", "unavailable": "UNSUPPORTED",
@@ -84,12 +175,22 @@
"abandoned": "ABANDONED", "abandoned": "ABANDONED",
"SPK": "SPK", "SPK": "SPK",
"SPK-R": "SPK + MANUAL",
"SPK-M": "SPK + MECH.",
"SCS": "SCS", "SCS": "SCS",
"SCS-R": "SCS + MANUAL",
"SCS-M": "SCS + MECH.",
"SPE": "SPE", "SPE": "SPE",
"manual": "MANUAL", "manual": "MANUAL",
"mechanical": "MECHANICAL", "mechanical": "MECHANICAL",
"SUP": "SUP",
"SBL": "SBL", "SUP": "SUP (RASP-UZK)",
"noSUP": "WITHOUT SUP",
"SBL": "AUTOMATIC (SBL)",
"PBL": "SEMIAUTOMATIC (PBL)",
"modern": "MODERN", "modern": "MODERN",
"semaphores": "SEMAPHORES", "semaphores": "SEMAPHORES",
"mixed": "MIXED", "mixed": "MIXED",
@@ -110,7 +211,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": "REMEMBER FILTERS",
"reset": "RESET FILTERS", "reset": "RESET FILTERS",
"close": "CLOSE FILTERS" "close": "CLOSE FILTERS"
}, },
@@ -122,10 +223,13 @@
"dispatcher-lvl": "Dispatcher\nlevel", "dispatcher-lvl": "Dispatcher\nlevel",
"routes": "Routes\ndouble / single", "routes": "Routes\ndouble / single",
"general": "General info", "general": "General info",
"users": "Drivers online", "user": "Drivers online",
"spawns": "Spawns online", "spawn": "Spawns online",
"timetables": "Active timetables", "timetableAll": "Active timetables",
"no-stations": "No stations to show here!" "timetableConfirmed": "Confirmed timetables",
"timetableUnconfirmed": "Unconfirmed timetables",
"no-stations": "No stations to show here!",
"scenery-search": "Search for scenery..."
}, },
"trains": { "trains": {
"no-trains": "No trains to show here!", "no-trains": "No trains to show here!",
@@ -144,28 +248,6 @@
"current-signal": "at signal", "current-signal": "at signal",
"current-track": "on track", "current-track": "on track",
"option-mass": "mass",
"option-speed": "speed",
"option-length": "length",
"option-distance": "distance",
"option-timetable": "train no.",
"option-progress": "route progress",
"option-delay": "current delay",
"option-comments": "comments",
"filter-comments": "comments",
"filter-twr": "TWR",
"filter-skr": "SKR",
"filter-passenger": "passenger",
"filter-freight": "freight",
"filter-other": "other",
"filter-noTimetable": "no timetable",
"filter-reset": "X RESET",
"sorter-prefix": "Sort: ",
"search-train": "Train no.",
"search-driver": "Driver name",
"delayed": "Delayed: ", "delayed": "Delayed: ",
"preponed": "Ahead of schedule: ", "preponed": "Ahead of schedule: ",
"on-time": "On time", "on-time": "On time",
@@ -185,44 +267,28 @@
"comment": "Exploitation comments for: ", "comment": "Exploitation comments for: ",
"table-limit": "For performance reasons there's a limit of 10 trains shown at the same time.", "table-limit": "For performance reasons there's a limit of 10 trains shown at the same time.",
"last-seen-now": "last seen: just now", "last-seen-now": "since now",
"last-seen-min": "last seen: one minute ago", "last-seen-min": "since one minute",
"last-seen-ago": "last seen: {minutes} mins ago" "last-seen-ago": "since {minutes} minutes",
"scenery-offline": "Offline ride",
"timeout": "An error occured while trying to refresh SWDR timetable data!"
}, },
"journal": { "journal": {
"title": "DISPATCHER HISTORY", "title": "DISPATCHER HISTORY",
"loading": "Loading dispatcher history data...", "loading": "Loading dispatcher history data...",
"no-history": "No dispatcher history found!", "no-history": "No dispatcher history found!",
"data-refreshed-at": "Data refreshed at",
"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...",
"route-length": "Route length:", "route-length": "Route length:",
"station-count": "Stations:", "station-count": "Stations:",
"dispatcher-name": "Created by", "dispatcher-name": "Author",
"timetable-day": "Timetable created at", "timetable-day": "Timetable created at",
"timetable-active": "ACTIVE", "timetable-active": "ACTIVE",
"timetable-fulfilled": "FULFILLED", "timetable-fulfilled": "FULFILLED",
@@ -230,14 +296,65 @@
"online-since": "ONLINE SINCE", "online-since": "ONLINE SINCE",
"duty-lasted": "The duty lasted", "duty-lasted": "The duty lasted",
"minutes": "{minutes} mins",
"hours": "{hours}h {minutes} mins" "hours": "{value} hour | {value} hours",
"minutes": "{value} min | {value} mins",
"seconds": "{value} s",
"stock-info": "EXTRA INFO",
"stock-length": "Length",
"stock-mass": "Mass",
"stock-max-speed": "Max. 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-title": "Daily stats on {date}",
"timetable-stats-total": "Issued timetables: {count} (total distance: {distance})",
"timetable-stats-longest": "The longest timetable: #{id} (made by {author} for {driver}, distance: {distance})",
"timetable-stats-most-active-dr": "The most active dispatcher: {dispatcher} (created {count})",
"timetable-stats-most-active-dr-many": "The most active dispatchers: {dispatchers} (created {count} each)",
"timetable-stats-most-active-driver": "The most active driver: {driver} (total driven distance: {distance})",
"timetable-stats-longest-duties": "The longest service: {dispatcher} at {station} (duration: {duration})",
"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! :/",
"timetable-location-signal": "signal:",
"timetable-location-route": "route:",
"history-name": "Scenery name",
"history-hash": "Hash",
"history-dispatcher": "Dispatcher",
"history-level": "Level",
"history-rate": "Rate",
"history-region": "Region",
"history-date": "Service date"
}, },
"scenery": { "scenery": {
"users": "PLAYERS ONLINE", "users": "PLAYERS ONLINE",
"spawns": "OPEN SPAWNS", "spawns": "OPEN SPAWNS",
"timetables": "ACTIVE TIMETABLES", "timetables": "ACTIVE TIMETABLES",
"no-timetables": "No active timetables!", "no-timetables": "No active timetables!",
"offline": "Scenery is offline",
"no-users": "NO ACTIVE PLAYERS", "no-users": "NO ACTIVE PLAYERS",
"no-spawns": "NO OPEN SPAWNS", "no-spawns": "NO OPEN SPAWNS",
"no-scenery": "Oops! This scenery doesn't exist!", "no-scenery": "Oops! This scenery doesn't exist!",
@@ -245,39 +362,68 @@
"history-btn": "View the dispatcher history", "history-btn": "View the dispatcher history",
"info-btn": "Return to the scenery view", "info-btn": "Return to the scenery view",
"authors-title": "Scenery author | Scenery authors", "authors-title": "Scenery author | Scenery authors",
"abbrev": "Station symbol:",
"lines-title": "Real lines", "lines-title": "Real lines",
"project-title": "Project name", "project-title": "Project name",
"one-way-routes": "One way routes", "one-way-routes": "One way routes",
"two-way-routes": "Two way routes", "two-way-routes": "Two way routes",
"option-active-timetables": "Active timetables", "option-active-timetables": "Active timetables",
"option-timetables-history": "Scenery timetables history", "option-timetables-history": "Timetables history",
"option-dispatchers-history": "Scenery dispatchers history", "option-dispatchers-history": "Dispatchers history",
"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",
"dispatchers-history-hash": "Hash",
"dispatchers-history-dispatcher": "Dispatcher",
"dispatchers-history-level": "Level",
"dispatchers-history-rate": "Rate",
"dispatchers-history-date": "Service 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!",
"forum-topic": "Official {name} forum topic",
"pragotron-link": "Timetable pallet board (beta)",
"tablice-link": "Timetable summary board (by Thundo)",
"bottom-info": "Show full history in the Journal tab"
}, },
"availability": { "availability": {
"title": "Availability", "title": "Availability",
"default": "in-game", "default": "in-game",
"nonDefault": "downloadable", "nonDefault": "additional",
"unavailable": "unavailable", "unavailable": "unavailable",
"nonPublic": "private", "nonPublic": "private",
"abandoned": "abandoned" "abandoned": "abandoned"
}, },
"timetables": { "timetables": {
"timetable-only": "Switch to timetable-only view", "timetable-only": "Switch to timetable-only view",
"online": "At station", "end": "Timetable terminates here",
"departed": "Dispatched to:", "terminated": "Timetable terminated",
"departed-away": "Departed to:",
"arriving": "Arriving from:",
"stopped": "Stopped",
"terminated": "Terminated",
"begins": "BEGINS HERE", "begins": "BEGINS HERE",
"terminates": "TERMINATES\nHERE" "terminates": "TERMINATES\nHERE",
"from": "FROM",
"to": "TO",
"desc-arriving": "The train is not here yet. It's going to come from: {prevStationName} (szlak {prevDepartureLine})",
"desc-online": "The train is at the station. It's going to leave to: {nextStationName} (szlak {nextArrivalLine})",
"desc-stopped": "The train is at the station and is stopped. It's going to leave towards: {nextStationName} (szlak {nextArrivalLine})",
"desc-next-arrival": "Leaves towards: {nextStationName} (szlak {nextArrivalLine})",
"desc-departed": "The train is at the station and it's been departed. Leaves towards: {nextStationName} (szlak {nextArrivalLine})",
"desc-departed-away": "The train has been departed to: {nextStationName} (szlak {nextArrivalLine})",
"desc-end": "The train terminates here",
"desc-terminated": "The train has been terminated"
}, },
"history": { "history": {
"title": "TIMETABLE JOURNAL", "title": "TIMETABLE JOURNAL",
+218 -70
View File
@@ -1,4 +1,10 @@
{ {
"general": {
"and": " oraz ",
"refresh": "ODŚWIEŻ",
"TWR": "Towar niebezpieczny wysokiego ryzyka",
"SKR": "Przekroczona skrajnia"
},
"app": { "app": {
"sceneries": "SCENERIE", "sceneries": "SCENERIE",
"trains": "POCIĄGI", "trains": "POCIĄGI",
@@ -8,10 +14,21 @@
"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!"
},
"footer": {
"discord": "Serwer Discord Stacjownika"
},
"update": {
"title": "Nowa wersja Stacjownika jest dostępna!",
"paragraph1": "Miłego korzystania z aplikacji i niech S2 będzie z wami!",
"release-link": "Kliknij, aby przejrzeć listę zmian (GitHub)",
"confirm-button": "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!",
@@ -67,9 +84,83 @@
}, },
"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-issuedFrom": "Sceneria początkowa",
"search-timetables-date": "Data rozkładu jazdy (UTC+2 / CEST)",
"search-dispatchers-date": "Data służby (UTC+2 / CEST)",
"search-date": "Data (UTC+2 / CEST)",
"sort-routeDistance": "kilometraż",
"sort-allStopsCount": "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-withComments": "UWAGI EKSPLOATACYJNE",
"filter-noComments": "BEZ UWAG",
"filter-twr": "WYS. RYZYKA",
"filter-skr": "SKRAJNIA",
"filter-twr-skr": "WSZYSTKIE",
"filter-common": "ZWYKŁE",
"filter-passenger": "PASAŻERSKIE",
"filter-freight": "TOWAROWE",
"filter-other": "INNE",
"filter-noTimetable": "BEZ RJ",
"filter-withTimetable": "ROZKŁAD JAZDY",
"filter-reset": "ZRESETUJ FILTRY",
"filter-clear": "WYŁĄCZ FILTRY",
"filter-section-timetable-status": "STATUS ROZKŁADU JAZDY",
"filter-section-twrskr": "UWAGI",
"filter-all": "WSZYSTKIE",
"filter-abandoned": "PORZUCONE",
"filter-fulfilled": "WYPEŁNIONE",
"filter-active": "AKTYWNE"
}, },
"filters": { "filters": {
"desc": " &bull; Kliknięcie: zaznaczenie / odznaczenie filtru <br /> &bull; Podwójne kliknięcie: odznaczenie reszty filtrów z <b class='text--primary'>grupy</b> <br /> &bull; <span style='color: coral'>RESET</span>: zresetowanie filtrów z <b class='text--primary'>grupy</b>",
"sections": {
"quick": "SZYBKIE FILTRY",
"reality": "FIKCYJNOŚĆ SCENERII",
"package-access": "DOSTĘPNOŚĆ W PACZCE",
"access": "DOSTĘPNOŚĆ OGÓLNA",
"control": "TYP STEROWANIA",
"signals": "TYP SYGNALIZACJI",
"addons": "DODATKOWE PROGRAMY",
"blockades": "BLOKADY LINIOWE",
"status": "STATUS ONLINE"
},
"all-available": "WSZYSTKIE DOSTĘPNE",
"all-free": "WSZYSTKIE WOLNE",
"endingStatus": "KOŃCZY", "endingStatus": "KOŃCZY",
"afkStatus": "Z/W", "afkStatus": "Z/W",
"noSpaceStatus": "BRAK MIEJSCA", "noSpaceStatus": "BRAK MIEJSCA",
@@ -85,18 +176,29 @@
"abandoned": "WYCOFANA", "abandoned": "WYCOFANA",
"SPK": "SPK", "SPK": "SPK",
"SPK-R": "SPK + RĘCZNE",
"SPK-M": "SPK + MECH.",
"SCS": "SCS", "SCS": "SCS",
"SCS-R": "SCS + RĘCZNE",
"SCS-M": "SCS + MECH.",
"SPE": "SPE", "SPE": "SPE",
"manual": "RĘCZNE", "manual": "RĘCZNE",
"SUP": "SUP",
"SBL": "SBL", "SUP": "SUP (RASP-UZK)",
"noSUP": "BEZ SUP",
"SBL": "SAMOCZYNNA",
"PBL": "PÓŁSAMOCZYNNA",
"mechanical": "MECHANICZNE", "mechanical": "MECHANICZNE",
"modern": "WSPÓŁCZESNA", "modern": "WSPÓŁCZESNA",
"semaphores": "KSZTAŁTOWA", "semaphores": "KSZTAŁTOWA",
"mixed": "MIESZANA", "mixed": "MIESZANA",
"historical": "HISTORYCZNA", "historical": "HISTORYCZNA",
"free": "WOLNA", "free": "WOLNA",
"occupied": "ZAJĘTA", "occupied": "ZAJĘTA",
"sliders": { "sliders": {
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO", "min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO", "max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
@@ -105,28 +207,33 @@
"routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)", "routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
"routes-2t-other": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)" "routes-2t-other": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
}, },
"authors-search": "Szukaj autora (uwzględnia inne filtry)", "authors-search": "Szukaj autora (uwzględnia inne filtry)",
"minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:", "minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:",
"now": "TERAZ", "now": "TERAZ",
"hour": " godz.", "hour": " godz.",
"no-limit": "BEZ LIMITU", "no-limit": "BEZ LIMITU",
"include-selected": "POKAŻ ZAZNACZONE", "include-selected": "POKAŻ ZAZNACZONE",
"save": "ZAPISZ FILTRY", "save": "ZAPAMIĘTAJ FILTRY",
"reset": "RESETUJ FILTRY", "reset": "RESETUJ FILTRY",
"close": "ZAMKNIJ FILTRY" "close": "ZAMKNIJ FILTRY"
}, },
"sceneries": { "sceneries": {
"station": "Stacja", "station": "Stacja",
"abbr": "Skrót\nposterunku",
"min-lvl": "Min. poziom\ndyżurnego", "min-lvl": "Min. poziom\ndyżurnego",
"status": "Status", "status": "Status",
"dispatcher": "Dyżurny", "dispatcher": "Dyżurny",
"dispatcher-lvl": "Poziom\ndyżurnego", "dispatcher-lvl": "Poziom\ndyżurnego",
"routes": "Szlaki\n2tor / 1tor", "routes": "Szlaki\n2tor / 1tor",
"general": "Informacje\nogólne", "general": "Informacje\nogólne",
"users": "Maszyniści online", "user": "Maszyniści online",
"spawns": "Otwarte spawny", "spawn": "Otwarte spawny",
"timetables": "Aktywne rozkłady jazdy", "timetableAll": "Aktywne rozkłady jazdy",
"no-stations": "Brak stacji do wyświetlenia!" "timetableConfirmed": "Zatwierdzone rozkłady jazdy",
"timetableUnconfirmed": "Niezatwierdzone rozkłady jazdy",
"no-stations": "Brak stacji do wyświetlenia!",
"scenery-search": "Wyszukaj scenerię..."
}, },
"trains": { "trains": {
"no-trains": "Brak pociągów do wyświetlenia!", "no-trains": "Brak pociągów do wyświetlenia!",
@@ -145,28 +252,6 @@
"current-signal": "przy semaforze", "current-signal": "przy semaforze",
"current-track": "na szlaku", "current-track": "na szlaku",
"option-mass": "masa",
"option-speed": "prędkość",
"option-length": "długość",
"option-distance": "kilometraż",
"option-timetable": "nr pociągu",
"option-progress": "przebyta trasa",
"option-delay": "opóźnienie",
"option-comments": "uwagi ekspl.",
"filter-comments": "uwagi ekspl.",
"filter-twr": "TWR",
"filter-skr": "SKR",
"filter-passenger": "pasażerskie",
"filter-freight": "towarowe",
"filter-other": "inne",
"filter-noTimetable": "bez RJ",
"filter-reset": "X RESETUJ",
"sorter-prefix": "Sortuj: ",
"search-train": "Numer pociągu",
"search-driver": "Nick maszynisty",
"delayed": "Opóźniony: ", "delayed": "Opóźniony: ",
"preponed": "Przed czasem: ", "preponed": "Przed czasem: ",
"on-time": "Planowo", "on-time": "Planowo",
@@ -186,80 +271,135 @@
"comment": "Uwagi eksploatacyjne dla: ", "comment": "Uwagi eksploatacyjne dla: ",
"table-limit": "Dla płynności działania strony pokazanych jest tylko 10 pociągów zgodnie z wybranymi filtrami.", "table-limit": "Dla płynności działania strony pokazanych jest tylko 10 pociągów zgodnie z wybranymi filtrami.",
"last-seen-now": "ostatnio widziany: przed chwilą", "last-seen-now": "od niedawna",
"last-seen-min": "ostatnio widziany: minutę temu", "last-seen-min": "od minuty",
"last-seen-ago": "ostatnio widziany: {minutes} min. temu" "last-seen-ago": "od {minutes} minut",
"scenery-offline": "Przejazd offline",
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR"
}, },
"journal": { "journal": {
"title": "HISTORIA DYŻURÓW", "title": "HISTORIA DYŻURÓW",
"loading": "Ładowanie historii dyżurów...", "loading": "Ładowanie historii dyżurów...",
"no-history": "Brak historii dyżurów dla tej scenerii!", "no-history": "Brak historii dyżurów dla tej scenerii!",
"data-refreshed-at": "Dane odświeżone o",
"section-timetables": "ROZKŁADY JAZDY", "section-timetables": "ROZKŁADY JAZDY",
"section-dispatchers": "DYŻURNI", "section-dispatchers": "DYŻURNI",
"search": "Szukaj",
"search-train": "Numer pociągu",
"search-driver": "Nick maszynisty",
"search-dispatcher": "Nick dyżurnego",
"search-station": "Nazwa scenerii",
"sort-prefix": "Sortuj: ",
"option-distance": "kilometraż",
"option-total-stops": "stacje",
"option-beginDate": "data",
"option-timetableId": "ID rozkładu",
"option-timestampFrom": "data",
"option-duration": "czas dyżuru",
"filter-all": "WSZYSTKIE",
"filter-abandoned": "PORZUCONE",
"filter-fulfilled": "WYPEŁNIONE",
"filter-active": "AKTYWNE",
"no-further-data": "Brak dalszych wyników dla podanych parametrów", "no-further-data": "Brak dalszych wyników dla podanych parametrów",
"loading-further-data": "Ładowanie...", "loading-further-data": "Ładowanie...",
"online-since": "ONLINE OD", "online-since": "ONLINE OD",
"duty-lasted": "Dyżur trwał", "duty-lasted": "Dyżur trwał",
"minutes": "{minutes} min.", "hours": "{value} godz.",
"hours": "{hours} godz. {minutes} min.", "minutes": "{value} min.",
"seconds": "{value} sek.",
"route-length": "Kilometraż:", "route-length": "Kilometraż:",
"station-count": "Stacje:", "station-count": "Stacje:",
"dispatcher-name": "Wystawiony przez dyżurnego", "dispatcher-name": "Autor",
"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": "DODATKOWE INFORMACJE",
"stock-length": "Długość",
"stock-mass": "Masa",
"stock-max-speed": "Prędkość maks.",
"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": "Stworzone rozkłady jazdy: {count} (łączny dystans: {distance})",
"timetable-stats-longest": "Najdłuższy rozkład jazdy: #{id} (stworzony przez dyżurnego {author} dla maszynisty {driver} o dystansie {distance})",
"timetable-stats-most-active-dr": "Najaktywniejszy dyżurny: {dispatcher} (stworzył {count})",
"timetable-stats-most-active-dr-many": "Najaktywniejsi dyżurni: {dispatchers} (stworzyli po {count})",
"timetable-stats-most-active-driver": "Najaktywniejszy maszynista: {driver} (łączny przejechany dystans: {distance})",
"timetable-stats-longest-duties": "Najdłuższa służba: {dispatcher} na scenerii {station} (czas trwania: {duration})",
"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! :/",
"timetable-location-signal": "semafor:",
"timetable-location-route": "szlak:",
"history-name": "Sceneria",
"history-hash": "Hash",
"history-dispatcher": "Dyżurny",
"history-level": "Poziom",
"history-rate": "Ocena",
"history-region": "Region",
"history-date": "Data służby"
}, },
"scenery": { "scenery": {
"users": "GRACZE ONLINE", "users": "GRACZE ONLINE",
"spawns": "OTWARTE SPAWNY", "spawns": "OTWARTE SPAWNY",
"timetables": "AKTYWNE ROZKŁADY JAZDY", "timetables": "AKTYWNE ROZKŁADY JAZDY",
"no-timetables": "Brak aktywnych rozkładów!", "no-timetables": "Brak aktywnych rozkładów!",
"offline": "Sceneria jest offline",
"no-users": "BRAK AKTYWNYCH GRACZY", "no-users": "BRAK AKTYWNYCH GRACZY",
"no-spawns": "BRAK OTWARTYCH SPAWNÓW", "no-spawns": "BRAK OTWARTYCH SPAWNÓW",
"no-scenery": "Ups! Ta sceneria nie istnieje!", "no-scenery": "Ups! Ta sceneria nie istnieje!",
"return-btn": "Wróć na stronę główną", "return-btn": "Wróć na stronę główną",
"history-btn": "Przejdź do widoku historii dyżurnych ruchu", "history-btn": "Przejdź do widoku historii dyżurnych ruchu",
"info-btn": "Wróc do widoku scenerii", "info-btn": "Wróć do widoku scenerii",
"authors-title": "Autor scenerii | Autorzy scenerii", "authors-title": "Autor scenerii | Autorzy scenerii",
"abbrev": "Skrót posterunku:",
"lines-title": "Rzeczywiste linie", "lines-title": "Rzeczywiste linie",
"project-title": "Projekt", "project-title": "Projekt",
"one-way-routes": "Szlaki jednotorowe", "one-way-routes": "Szlaki jednotorowe",
"two-way-routes": "Szlaki dwutorowe", "two-way-routes": "Szlaki dwutorowe",
"option-active-timetables": "Aktywne rozkłady jazdy", "option-active-timetables": "Aktywne rozkłady jazdy",
"option-timetables-history": "Historia rozkładów scenerii", "option-timetables-history": "Historia rozkładów",
"option-dispatchers-history": "Historia dyżurów scenerii", "option-dispatchers-history": "Historia dyżurów",
"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",
"dispatchers-history-hash": "Hash",
"dispatchers-history-dispatcher": "Dyżurny",
"dispatchers-history-level": "Poziom",
"dispatchers-history-rate": "Ocena",
"dispatchers-history-date": "Data służby",
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego", "req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
"history-list-empty": "Brak historii dla tej scenerii!" "history-list-empty": "Brak historii dla tej scenerii!",
"forum-topic": "Oficjalny wątek scenerii {name}",
"pragotron-link": "Paletowa tablica informacyjna (beta)",
"tablice-link": "Tablica informacyjna zbiorcza (autorstwa Thundo)",
"bottom-info": "Pokaż pełną historię w zakładce Dziennika"
}, },
"availability": { "availability": {
"title": "Dostępność", "title": "Dostępność",
@@ -271,14 +411,22 @@
}, },
"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",
"from": "Z",
"to": "DO",
"desc-arriving": "Pociągu nie ma jeszcze na tej scenerii. Przyjedzie z: {prevStationName} (szlak {prevDepartureLine})",
"desc-online": "Pociąg jest na tej scenerii. Odjedzie do: {nextStationName} (szlak {nextArrivalLine})",
"desc-stopped": "Pociąg jest na tej scenerii i odbywa postój. Odjedzie do: {nextStationName} (szlak {nextArrivalLine})",
"desc-next-arrival": "Odjeżdża do: {nextStationName} (szlak {nextArrivalLine})",
"desc-departed": "Pociąg jest na tej scenerii i został odprawiony. Odjeżdża do: {nextStationName} (szlak {nextArrivalLine})",
"desc-departed-away": "Pociąg został odprawiony i odjechał do: {nextStationName} (szlak {nextArrivalLine})",
"desc-end": "Pociąg kończy bieg",
"desc-terminated": "Pociąg skończył bieg"
}, },
"history": { "history": {
"title": "DZIENNIK ROZKŁADÓW JAZDY" "title": "DZIENNIK ROZKŁADÓW JAZDY"
+4 -2
View File
@@ -2,14 +2,16 @@ import { createApp, Directive, ref } from 'vue';
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
import enLang from '@/locales/en.json'; import enLang from './locales/en.json';
import plLang from '@/locales/pl.json'; import plLang from './locales/pl.json';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
const i18n = createI18n({ const i18n = createI18n({
locale: 'pl', locale: 'pl',
legacy: false,
warnHtmlMessage: false,
fallbackLocale: 'pl', fallbackLocale: 'pl',
messages: { messages: {
en: enLang, en: enLang,
+31 -4
View File
@@ -21,6 +21,13 @@ export default defineComponent({
}); });
}, },
localeDateTime(dateString: string, locale: string) {
return new Date(dateString).toLocaleString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
timeStyle: 'short',
dateStyle: 'medium'
});
},
localeTime(dateString: string, locale: string) { localeTime(dateString: string, locale: string) {
return new Date(dateString).toLocaleTimeString(locale == 'pl' ? 'pl-PL' : 'en-GB', { return new Date(dateString).toLocaleTimeString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
hour: '2-digit', hour: '2-digit',
@@ -28,6 +35,19 @@ export default defineComponent({
}); });
}, },
stringToDate(dateString?: string) {
return dateString ? new Date(dateString) : null;
},
parseDateToTimeString(date: Date | null) {
return (
date?.toLocaleTimeString('pl-PL', {
hour: '2-digit',
minute: '2-digit',
}) || ''
);
},
timestampToString(timestamp: number | null) { timestampToString(timestamp: number | null) {
return timestamp return timestamp
? new Date(timestamp).toLocaleTimeString('pl-PL', { ? new Date(timestamp).toLocaleTimeString('pl-PL', {
@@ -37,14 +57,21 @@ export default defineComponent({
: ''; : '';
}, },
calculateDuration(timestampMs: number) { calculateDuration(timestampMs: number, showSeconds = false) {
const secondsTotal = Math.floor(timestampMs / 1000);
const minsTotal = Math.round(timestampMs / 60000); const minsTotal = Math.round(timestampMs / 60000);
const hoursTotal = Math.floor(minsTotal / 60); const hoursTotal = Math.floor(minsTotal / 60);
const minsInHour = minsTotal % 60; const minsInHour = minsTotal % 60;
return minsTotal > 60 return minsTotal >= 60
? this.$t('journal.hours', { hours: hoursTotal, minutes: minsInHour }) ? `${this.$t('journal.hours', { value: hoursTotal }, hoursTotal)} ${this.$t(
: this.$t('journal.minutes', { minutes: minsTotal }); 'journal.minutes',
{ value: minsInHour },
minsInHour
)}`
: showSeconds && secondsTotal <= 60
? this.$t('journal.seconds', { value: secondsTotal }, secondsTotal)
: this.$t('journal.minutes', { value: minsTotal }, minsTotal);
}, },
}, },
}); });
+13
View File
@@ -0,0 +1,13 @@
import { defineComponent } from 'vue';
export default defineComponent({
methods: {
getIcon(name: string, ext = 'svg') {
return new URL(`../assets/icon-${name}.${ext}`, import.meta.url).href;
},
getImage(name: string) {
return new URL(`../assets/${name}`, import.meta.url).href;
}
},
});
+26
View File
@@ -0,0 +1,26 @@
import { defineComponent } from 'vue';
export default defineComponent({
data() {
return {
preventKeyDown: false,
};
},
activated() {
window.addEventListener('keydown', this.handleKeyDown);
},
deactivated() {
window.removeEventListener('keydown', this.handleKeyDown);
},
methods: {
onKeyDownFunction() {},
handleKeyDown(e: KeyboardEvent) {
if (!e.key) return;
if (e.key.toLowerCase() == 'f' && !this.preventKeyDown && !e.ctrlKey && !e.altKey) this.onKeyDownFunction();
},
},
});
+26
View File
@@ -0,0 +1,26 @@
import { defineComponent } from 'vue';
export default defineComponent({
data: () => ({
observer: null as IntersectionObserver | null,
observerTarget: null as Element | null,
}),
methods: {
mountObserver(actionFunction: () => void, target: Element) {
this.observer = new IntersectionObserver((entries) => {
console.log(entries);
if (entries[0].intersectionRatio > 0.5) actionFunction();
}, { threshold: 0.2 });
this.observer.observe(target);
},
unmountObserver() {
if (!this.observerTarget) return;
this.observer?.unobserve(this.observerTarget);
},
},
});
+33
View File
@@ -0,0 +1,33 @@
import { Ref, defineComponent } from 'vue';
import { useStore } from '../store/store';
export default defineComponent({
data() {
return {
store: useStore(),
};
},
computed: {
chosenTrain() {
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId);
},
},
methods: {
selectModalTrain(trainId: string, target?: EventTarget | null) {
this.store.chosenModalTrainId = trainId;
document.body.classList.add('no-scroll');
if (target) this.store.modalLastClickedTarget = target;
},
closeModal() {
this.store.chosenModalTrainId = undefined;
setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus();
document.body.classList.remove('no-scroll');
}, 150);
},
},
});
+26 -23
View File
@@ -1,31 +1,34 @@
import { defineComponent, h } from "vue"; import { defineComponent, h } from 'vue';
import imageMixin from './imageMixin';
export default defineComponent({ export default defineComponent({
data() { mixins: [imageMixin],
return {
icons: {
arrow: require('@/assets/icon-arrow-asc.svg'),
},
showReturnButton: false data() {
} return {
icons: {
arrow: this.getIcon('arrow-asc'),
},
showReturnButton: false,
};
},
methods: {
scrollToTop() {
window.scrollTo({ top: 0 });
}, },
methods: { handleScroll() {
scrollToTop() { this.showReturnButton = window.scrollY > window.innerHeight * 0.35;
window.scrollTo({ top: 0 });
},
handleScroll() {
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);
}, },
}) });
+12 -6
View File
@@ -5,10 +5,16 @@ export default defineComponent({
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;
} },
} },
}) });
+24 -8
View File
@@ -1,8 +1,11 @@
import Train from '@/scripts/interfaces/Train';
import TrainStop from '@/scripts/interfaces/TrainStop';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import Train from '../scripts/interfaces/Train';
import TrainStop from '../scripts/interfaces/TrainStop';
import imageMixin from './imageMixin';
export default defineComponent({ export default defineComponent({
mixins: [imageMixin],
data: () => ({ data: () => ({
STATS: { STATS: {
main: [ main: [
@@ -55,6 +58,23 @@ export default defineComponent({
: this.$t('trains.last-seen-ago', { minutes: diffMins }); : this.$t('trains.last-seen-ago', { minutes: diffMins });
}, },
displayTrainPosition(train: Train) {
let positionString = '';
positionString += this.$t('trains.current-scenery') + ' ';
if (train.currentStationHash) positionString += train.currentStationName + ' ';
else positionString += train['currentStationName'].replace(/.[a-zA-Z0-9]+.sc/, '') + ' (offline) ';
if (train.signal) positionString += this.$t('trains.current-signal') + ' ' + train.signal + ' ';
if (train.connectedTrack) positionString += this.$t('trains.current-track') + ' ' + train.connectedTrack + ' ';
if (train.distance) positionString += `(${this.displayDistance(train.distance)})`;
return positionString.charAt(0).toUpperCase() + positionString.slice(1);
},
displayStopList(stops: TrainStop[]): string | undefined { displayStopList(stops: TrainStop[]): string | undefined {
if (!stops) return ''; if (!stops) return '';
@@ -62,11 +82,7 @@ export default defineComponent({
.reduce((acc: string[], stop: TrainStop, i: number) => { .reduce((acc: string[], stop: TrainStop, i: number) => {
if (stop.stopType.includes('ph') && !stop.stopNameRAW.includes('po.')) if (stop.stopType.includes('ph') && !stop.stopNameRAW.includes('po.'))
acc.push(`<strong style='color:${stop.confirmed ? 'springgreen' : 'white'}'>${stop.stopName}</strong>`); acc.push(`<strong style='color:${stop.confirmed ? 'springgreen' : 'white'}'>${stop.stopName}</strong>`);
else if ( else if (i > 0 && i < stops.length - 1 && !/po\.|sbl/gi.test(stop.stopNameRAW))
i > 0 &&
i < stops.length - 1 &&
!/po\.|sbl/gi.test(stop.stopNameRAW)
)
acc.push(`<span style='color:${stop.confirmed ? 'springgreen' : 'lightgray'}'>${stop.stopName}</span>`); acc.push(`<span style='color:${stop.confirmed ? 'springgreen' : 'lightgray'}'>${stop.stopName}</span>`);
return acc; return acc;
}, []) }, [])
@@ -121,7 +137,7 @@ export default defineComponent({
onImageError(e: Event) { onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement; const imageEl = e.target as HTMLImageElement;
imageEl.src = require('@/assets/unknown.png'); imageEl.src = this.getImage('unknown.png');
}, },
}, },
}); });
+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,
};
};
+24 -31
View File
@@ -1,50 +1,43 @@
import JournalDispatchersVue from '@/components/JournalView/JournalDispatchers.vue';
import JournalTimetablesVue from '@/components/JournalView/JournalTimetables.vue';
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import JournalDispatchersVue from '../views/JournalDispatchers.vue';
import JournalTimetablesVue from '../views/JournalTimetables.vue';
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
path: '/', path: '/',
name: 'StationsView', name: 'StationsView',
component: () => import('@/views/StationsView.vue'), component: () => import('../views/StationsView.vue'),
}, },
{ {
path: '/trains', path: '/trains',
name: 'TrainsView', name: 'TrainsView',
component: () => import('@/views/TrainsView.vue'), component: () => import('../views/TrainsView.vue'),
props: (route) => ({ train: route.query.train, driver: route.query.driver }), props: (route) => ({ train: route.query.train, driver: route.query.driver, trainId: route.query.trainId }),
}, },
{ {
path: '/scenery', path: '/scenery',
name: 'SceneryView', name: 'SceneryView',
component: () => import('@/views/SceneryView.vue'), component: () => import('../views/SceneryView.vue'),
props: true,
}, },
{ {
path: '/journal', path: '/journal',
name: 'JournalView', redirect: '/journal/timetables'
component: () => import('@/views/JournalView.vue'), },
children: [ {
{ path: '/journal/timetables',
path: '', name: 'JournalTimetables',
redirect: '/journal/timetables', component: JournalTimetablesVue,
component: JournalTimetablesVue, props: (route) => ({
}, trainNo: route.query.trainNo,
{ driverName: route.query.driverName,
path: 'dispatchers', timetableId: route.query.timetableId,
component: JournalDispatchersVue, }),
props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }), },
}, {
{ path: '/journal/dispatchers',
path: 'timetables', name: 'JournalDispatchers',
component: JournalTimetablesVue, component: JournalDispatchersVue,
props: (route) => ({ props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }),
trainNo: route.query.trainNo,
driverName: route.query.driverName,
timetableId: route.query.timetableId,
}),
},
],
}, },
{ {
path: '/:catchAll(.*)', path: '/:catchAll(.*)',
@@ -56,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,

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