Compare commits

..

131 Commits

Author SHA1 Message Date
Spythere 20cd393e05 Merge pull request #84 from Spythere/development
Wersja 1.23.0
2024-03-24 01:30:03 +01:00
Spythere 31e65c09d6 hotfix: podgląd pojazdów 2024-03-24 00:05:39 +01:00
Spythere fb2348e774 hotfixy designu 2024-03-23 23:55:18 +01:00
Spythere 1ec75bda70 poprawki do popupów 2024-03-23 16:47:57 +01:00
Spythere 6b6b837dde bump: v1.23.0 2024-03-23 00:01:15 +01:00
Spythere 66a02d76bd dodano odnośnik do dziennika RJ maszynisty 2024-03-23 00:00:52 +01:00
Spythere c7162dbd14 dodano dymki kontekstowe oraz podgląd pojazdu 2024-03-22 23:41:43 +01:00
Spythere 1cfe073bab Merge pull request #83 from Spythere/development
Wersja 1.22.3
2024-03-17 18:38:27 +01:00
Spythere e3b72c81ea bump: 1.22.3 2024-03-17 18:37:58 +01:00
Spythere 5552995564 fix: duplikujące się aktywne RJ scenerii 2024-03-17 18:37:45 +01:00
Spythere 623d5dd2ce fix: RJ scenerii offline 2024-03-17 17:33:19 +01:00
Spythere 6992b998a8 Merge pull request #82 from Spythere/development
Wersja 1.22.2
2024-03-17 16:47:52 +01:00
Spythere 669975c68e hotfixy 2024-03-17 16:42:35 +01:00
Spythere 084823de44 fix pobierania danych 2024-03-16 22:13:38 +01:00
Spythere f62d6982e5 Merge pull request #81 from Spythere/development
Wersja 1.22.1
2024-03-12 16:51:26 +01:00
Spythere 1c9b54b578 bump: 1.22.1 2024-03-11 23:50:00 +01:00
Spythere 0f4e5ee5f3 fix: niepoprawne miniaturki pojazdów 2024-03-11 23:49:45 +01:00
Spythere 29b5e715fa hotfixy wyglądu 2024-03-11 23:43:10 +01:00
Spythere 91a18b51a0 Merge pull request #80 from Spythere/development
Wersja 1.22.0
2024-03-06 22:49:46 +01:00
Spythere 241648ec49 asdek: filtry 2024-03-06 18:33:30 +01:00
Spythere ed7d93e7fc fixy filtrowania; ogólne 2024-03-06 18:12:20 +01:00
Spythere 436e3e63f9 hotfixy 2024-03-05 15:27:42 +01:00
Spythere 17ebdace82 fix filtrowania ocenami 2024-03-04 21:13:12 +01:00
Spythere 20826d905d poprawki tłumaczeń 2024-03-04 21:10:43 +01:00
Spythere 41b1ab398c bump: 1.22.0 2024-03-04 20:32:05 +01:00
Spythere 03465a1487 poprawki 2024-03-04 20:31:54 +01:00
Spythere a19fdbc19d hotfix 2024-03-04 18:18:37 +01:00
Spythere 032f82acd2 animacje statusów listy scenerii; fixy tłumaczeń 2024-03-04 18:06:47 +01:00
Spythere b4a9d4ca3b brakujące tłumaczenia 2024-03-04 17:50:45 +01:00
Spythere 986c7ba95e asdek 2024-03-04 17:46:09 +01:00
Spythere 17f6f9c8ef poprawki designu scenerii 2024-03-03 22:17:15 +01:00
Spythere 40bbdbe4fa sortowanie po liczbie szlaków i ocenie dyżurnego 2024-03-03 21:44:39 +01:00
Spythere 9f5d882119 poprawki tabelki scenerii 2024-03-03 20:30:05 +01:00
Spythere 6f45663c6c design stock listy 2024-03-03 19:04:17 +01:00
Spythere a7861b361d design statystyk pociągu 2024-03-03 15:10:29 +01:00
Spythere 5f8d7480d1 fix designu 2024-03-03 14:46:37 +01:00
Spythere e222dc63eb scroller sponsorów projektu w modalu 2024-03-03 14:31:04 +01:00
Spythere 9c2f0ac797 bump: 1.21.1 2024-03-02 23:21:13 +01:00
Spythere e33ba4af90 design szlaków na liście scenerii 2024-03-02 23:16:17 +01:00
Spythere 7b868a9f28 brakujące tłumaczenia 2024-03-02 22:38:53 +01:00
Spythere 2a18ba4368 poprawki szerokości 2024-03-02 22:19:41 +01:00
Spythere fcbd6d0883 migracja http clienta do apiStore 2024-03-02 16:13:33 +01:00
Spythere 20fc4aba5b padding scrollbaru 2024-03-01 19:12:23 +01:00
Spythere 76ca0d1786 cleanup kodu 2024-03-01 19:07:21 +01:00
Spythere 7e3c150815 usprawnienia miniaturek pojazdów 2024-03-01 19:07:00 +01:00
Spythere c8d56ec442 vite config dev 2024-02-29 13:31:48 +01:00
Spythere 5c4c486643 Wersja 1.21.0
Wersja 1.21.0
2024-02-12 15:14:50 +01:00
Spythere 755c729a9b brakujące tłumaczenia; poprawki 2024-02-12 14:58:59 +01:00
Spythere 3ac8d60c5c filtry aktywnych RJ 2024-02-11 15:30:19 +01:00
Spythere dcff3b088f poprawki filtrowania statusów 2024-02-10 23:11:13 +01:00
Spythere 90b2099955 checkpointy; hotfixy 2024-02-10 22:42:35 +01:00
Spythere fc0c04ec9d bump: 1.21.0 2024-02-10 01:31:08 +01:00
Spythere 41b335555a wyświetlanie RJ dla scenerii offline 2024-02-10 01:30:43 +01:00
Spythere 60f7b3bbb5 Wersja 1.20.5
Wersja 1.20.5
2024-02-03 14:36:57 +01:00
Spythere eaefe955a7 bump: 1.20.5 2024-02-02 21:14:29 +01:00
Spythere edaa4f2684 cleanup 2024-02-02 21:14:16 +01:00
Spythere 30fce3787b usprawnienia pobierania danych; statusy SWDR na semaforze 2024-02-02 21:13:21 +01:00
Spythere 4716f1c7a4 Wersja 1.20.4
Wersja 1.20.4
2024-01-30 16:44:55 +01:00
Spythere bb7ccf98fd bump wersji 2024-01-30 16:41:27 +01:00
Spythere c06d75b981 lock fix; linting 2024-01-30 14:00:38 +01:00
Spythere c7da8477fa przywrócenie komunikacji po WS (test) 2024-01-30 13:58:47 +01:00
Spythere e43f1e0819 Wersja 1.20.3
Wersja 1.20.3
2024-01-26 13:59:23 +01:00
Spythere f130e6900b hotfix: brak komentarzy dla ostatniej stacji w RJ 2024-01-26 13:54:47 +01:00
Spythere db205915be bump wersji 2024-01-26 13:39:04 +01:00
Spythere 05c38e10e3 hotfix postojów pt w SRJP 2024-01-26 13:38:45 +01:00
Spythere a8f683a585 mock data 2024-01-26 13:38:28 +01:00
Spythere 68f6fc8a42 Wersja 1.20.2
Wersja 1.20.2
2024-01-24 20:37:35 +01:00
Spythere 6d3b32cd7d bump: 1.20.2 2024-01-24 20:35:49 +01:00
Spythere fadecc9d2c fix literówki w tłumaczeniu 2024-01-24 20:35:15 +01:00
Spythere 50602cb6db Merge pull request #74 from Spythere/development
- rozbudowany szczegółówy RJ pociągu
- hotfixy do pobierania danych z API
2024-01-13 17:54:17 +01:00
Spythere 186ce81819 hotfix: filtrowanie aktywnych rj do odpowiednich regionów 2024-01-13 17:28:16 +01:00
Spythere f836a075b0 hotfix: pobieranie historii RJ nieznanych scenerii 2024-01-13 15:41:40 +01:00
Spythere 9acf3c740c dodano wybór z listy autorów w filtrach 2024-01-06 17:40:43 +01:00
Spythere bc1c1bd3d2 filtrowanie ukrytych szlaków 2024-01-06 14:47:20 +01:00
Spythere 2348277b95 poprawki do SRJP 2024-01-06 14:10:59 +01:00
Spythere cd5f489df7 bump wersji: 1.20.1 2024-01-06 14:06:50 +01:00
Spythere f74962222b przywrócenie SRJP bez pokazywania dod. informacji 2024-01-06 14:05:40 +01:00
Spythere e7f651d2b9 poprawki ułożenia elementów progress bara SRJP, elektryfikacja szlaku 2024-01-02 15:44:48 +01:00
Spythere 4862328090 rozbudowany szczegółówy RJ pociągu 2024-01-01 22:49:19 +01:00
Spythere 87631d1f74 Merge pull request #73 from Spythere/development
Wersja 1.20
2023-12-23 14:08:08 +01:00
Spythere 86bb9fcc2e hotfix redirectu do zakładki dziennika 2023-12-22 16:39:42 +01:00
Spythere b85e3bfe1d ukryto ikonę pragotronu 2023-12-22 16:16:19 +01:00
Spythere dd15072813 linting 2023-12-22 16:13:45 +01:00
Spythere 2f8376c996 dodane nowe statystyki dnia; poprawki bugów 2023-12-22 15:46:47 +01:00
Spythere 514723cf74 poprawki typów; ułożenie elementów w karcie filtrów scenerii 2023-12-21 22:16:03 +01:00
Spythere 0995ce15bc poprawki filtrów dziennia DR 2023-12-21 22:11:06 +01:00
Spythere 3b3c3bda31 poprawki wskaźników ładowania 2023-12-21 19:53:34 +01:00
Spythere 2027b85450 lokalne fonty; poprawki offline i cachingu pwa 2023-12-21 19:27:27 +01:00
Spythere 0c6b55146f bump: 1.20.0 2023-12-20 23:56:18 +01:00
Spythere 3c728e3cfa poprawki requestów statystyk; organizacja kodu 2023-12-20 23:55:42 +01:00
Spythere adce339392 statystyki dr (c.d.) + tłumaczenia 2023-12-19 22:11:17 +01:00
Spythere 00a4a840b0 statystyki DR 2023-12-18 16:00:18 +01:00
Spythere 1e705ea496 filtry URL dziennika DR 2023-12-17 19:51:52 +01:00
Spythere e8ed36df16 reaktywne filtry URL w dzienniku RJ 2023-12-17 16:10:13 +01:00
Spythere f4be32aa39 Statystyki DR (wip) 2023-12-16 17:49:54 +01:00
Spythere e0d3d2585d zmiana wyglądu statystyk dzienników 2023-12-14 18:42:13 +01:00
Spythere ebfaf06a44 Merge pull request #72 from Spythere/development
Wersja 1.19.4
2023-12-11 13:02:05 +01:00
Spythere 5a651aedf8 imports hotfix 2023-12-11 12:58:41 +01:00
Spythere b66af014b9 lock sync 2023-12-10 15:29:29 +01:00
Spythere 634c9e1514 bump wersji: 1.19.4 2023-12-10 15:23:54 +01:00
Spythere c4132a9be2 cleanup http c.d. 2023-12-10 15:23:27 +01:00
Spythere 82a9a9165f cleanup http 2023-12-10 15:22:33 +01:00
Spythere fcac03c0a4 Merge pull request #71 from Spythere/development
Wersja 1.19.3
2023-12-10 00:56:21 +01:00
Spythere 39c3cf2329 restrukturyzacja storów 2023-12-09 16:18:23 +01:00
Spythere 59f4a0cb66 zmniejszenie czasu odpytywania z serwera 2023-12-08 20:00:17 +01:00
Spythere e2b42d16a4 poprawki scrollBehavior 2023-12-08 18:55:06 +01:00
Spythere e23663ed28 fix: brak ładowania danych o historii scenerii przy bezpośrednim wejściu z poz. URL 2023-12-08 18:15:49 +01:00
Spythere dc7846c31e bump wersji: 1.19.3 2023-12-08 17:15:16 +01:00
Spythere d875433d56 ulepszone zapamiętywanie zakładek statystyk w dzienniku 2023-12-08 17:14:49 +01:00
Spythere 71e5044cb4 poprawki designu dziennika RJ 2023-12-08 16:20:17 +01:00
Spythere e83aa40f82 aktualizacja endpointu API statystyk 2023-12-08 16:16:14 +01:00
Spythere d1c0e0b898 Merge pull request #70 from Spythere/development
Wersja 1.19.2
2023-12-07 16:29:53 +01:00
Spythere 26a7c69886 bump: 1.19.2 2023-12-07 16:18:21 +01:00
Spythere 0dc2c505db poprawki do braku pokazywania się niezapisanych scenerii 2023-12-07 16:16:06 +01:00
Spythere 188857d335 fix: rozkłady jazdy pokazują się na innych serwerach 2023-12-06 20:29:28 +01:00
Spythere 3dbbb3b4f9 Merge pull request #69 from Spythere/development
Wersja 1.19.1
2023-12-02 23:04:55 +01:00
Spythere 07a77c463b bump 1.19.1 2023-12-02 23:04:24 +01:00
Spythere 1a8e2231dd przejście z WS na komunikację http 2023-12-02 23:02:08 +01:00
Spythere f630d272ca Merge pull request #68 from Spythere/development
Wersja 1.19.0
2023-12-02 18:31:58 +01:00
Spythere a381cf806c wyróżnienie nicku donatora w widoku scenerii 2023-12-02 18:15:52 +01:00
Spythere 0023ab8cfd tłumaczenia; poprawki dropdown 2023-12-02 18:01:01 +01:00
Spythere a392940d52 tłumaczenia 2023-12-02 16:15:37 +01:00
Spythere 74984ad653 dodano wyróżnienie nicku wspierającego w Dzienniku 2023-12-02 16:11:29 +01:00
Spythere a8991434bc tłumaczenia statystyk pociągów 2023-12-02 16:00:28 +01:00
Spythere 825610b4c2 hotfix; aktualizacja paczek 2023-12-01 21:40:04 +01:00
Spythere fef3991206 bump: 1.19.0 2023-12-01 21:27:13 +01:00
Spythere a21cb31a0a ikona przy informacji offline pociągu 2023-12-01 21:26:57 +01:00
Spythere 23a954a0d1 statystyki ruchu online 2023-12-01 21:19:04 +01:00
Spythere b7bc9625b8 dodatkowe opcje darowizny 2023-12-01 16:11:56 +01:00
Spythere bc58196804 cleanup 2023-12-01 14:55:13 +01:00
Spythere 6550d6973e zmiana API do pobierania informacji o wspierających 2023-12-01 14:54:55 +01:00
109 changed files with 15754 additions and 6449 deletions
-5
View File
@@ -50,11 +50,6 @@
name="twitter:image"
content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg"
/>
<link
href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=swap"
rel="stylesheet"
/>
</head>
<body>
+199 -319
View File
@@ -1,12 +1,12 @@
{
"name": "stacjownik",
"version": "1.18.4",
"version": "1.20.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "stacjownik",
"version": "1.18.4",
"version": "1.20.3",
"dependencies": {
"core-js": "^3.32.2",
"dotenv": "^16.3.1",
@@ -14,7 +14,7 @@
"howler": "^2.2.4",
"pinia": "^2.1.6",
"sass": "^1.67.0",
"socket.io-client": "^4.7.2",
"socket.io-client": "^4.7.4",
"vue": "^3.3.4",
"vue-i18n": "^9.4.1",
"vue-router": "^4.2.4"
@@ -102,13 +102,13 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
"integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
"integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/highlight": "^7.18.6"
"@babel/highlight": "^7.23.4",
"chalk": "^2.4.2"
},
"engines": {
"node": ">=6.9.0"
@@ -156,14 +156,14 @@
}
},
"node_modules/@babel/generator": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz",
"integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==",
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz",
"integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.20.7",
"@babel/types": "^7.23.5",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
},
"engines": {
@@ -292,11 +292,10 @@
}
},
"node_modules/@babel/helper-environment-visitor": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
"integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -315,27 +314,25 @@
}
},
"node_modules/@babel/helper-function-name": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
"integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.18.10",
"@babel/types": "^7.19.0"
"@babel/template": "^7.22.15",
"@babel/types": "^7.23.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-hoist-variables": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
"integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
"integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
@@ -474,34 +471,31 @@
}
},
"node_modules/@babel/helper-split-export-declaration": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
"integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.19.4",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
"integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
"integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -548,14 +542,13 @@
}
},
"node_modules/@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
"integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"engines": {
@@ -563,11 +556,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.7.tgz",
"integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==",
"dev": true,
"license": "MIT",
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz",
"integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==",
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -1740,35 +1731,33 @@
}
},
"node_modules/@babel/template": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
"integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==",
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/parser": "^7.20.7",
"@babel/types": "^7.20.7"
"@babel/code-frame": "^7.22.13",
"@babel/parser": "^7.22.15",
"@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.20.10",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.10.tgz",
"integrity": "sha512-oSf1juCgymrSez8NI4A2sr4+uB/mFd9MXplYGPEBnfAuWmmyeVcHa6xLPiaRBcXkcb/28bgxmQLTVwFKE1yfsg==",
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz",
"integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.7",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.19.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.20.7",
"@babel/types": "^7.20.7",
"@babel/code-frame": "^7.23.5",
"@babel/generator": "^7.23.5",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.23.5",
"@babel/types": "^7.23.5",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -1777,14 +1766,13 @@
}
},
"node_modules/@babel/types": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz",
"integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==",
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz",
"integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.19.4",
"@babel/helper-validator-identifier": "^7.19.1",
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
@@ -2856,8 +2844,7 @@
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
"license": "MIT"
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"node_modules/@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3",
@@ -3241,18 +3228,6 @@
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-core/node_modules/@babel/parser": {
"version": "7.22.16",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz",
"integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==",
"license": "MIT",
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz",
@@ -3281,18 +3256,6 @@
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-sfc/node_modules/@babel/parser": {
"version": "7.22.16",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz",
"integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==",
"license": "MIT",
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@vue/compiler-sfc/node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
@@ -3311,42 +3274,6 @@
"node": ">=12"
}
},
"node_modules/@vue/compiler-sfc/node_modules/nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/@vue/compiler-sfc/node_modules/postcss": {
"version": "8.4.20",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz",
"integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
@@ -3464,18 +3391,6 @@
"magic-string": "^0.30.0"
}
},
"node_modules/@vue/reactivity-transform/node_modules/@babel/parser": {
"version": "7.22.16",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz",
"integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==",
"license": "MIT",
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@vue/reactivity-transform/node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
@@ -3666,11 +3581,10 @@
}
},
"node_modules/axios": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
"integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@@ -3942,9 +3856,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001503",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001503.tgz",
"integrity": "sha512-Sf9NiF+wZxPfzv8Z3iS0rXM1Do+iOy2Lxvib38glFX+08TCYYYGR5fRJXk4d77C4AYwhUjgYgMsMudbh2TqCKw==",
"version": "1.0.30001565",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz",
"integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==",
"dev": true,
"funding": [
{
@@ -3959,15 +3873,13 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
]
},
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@@ -3982,7 +3894,6 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^1.9.0"
},
@@ -3995,7 +3906,6 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "1.1.3"
}
@@ -4004,15 +3914,13 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/chalk/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
@@ -4022,7 +3930,6 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^3.0.0"
},
@@ -4498,10 +4405,9 @@
}
},
"node_modules/engine.io-client": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz",
"integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==",
"license": "MIT",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
@@ -4514,7 +4420,6 @@
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
@@ -4629,7 +4534,6 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
@@ -5182,9 +5086,9 @@
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"dev": true,
"funding": [
{
@@ -5192,7 +5096,6 @@
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
@@ -5246,6 +5149,19 @@
"dev": true,
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -6083,8 +5999,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/js-yaml": {
"version": "4.1.0",
@@ -6434,7 +6349,6 @@
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"dev": true,
"funding": [
{
"type": "github",
@@ -6816,10 +6730,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.30",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz",
"integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==",
"dev": true,
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
@@ -6834,7 +6747,6 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
@@ -7477,11 +7389,10 @@
}
},
"node_modules/semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
@@ -7651,10 +7562,9 @@
}
},
"node_modules/socket.io-client": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
"integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
"license": "MIT",
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz",
"integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
@@ -7669,7 +7579,6 @@
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
@@ -8292,11 +8201,10 @@
"dev": true
},
"node_modules/vite": {
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
"integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==",
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.18.10",
"postcss": "^8.4.27",
@@ -8872,7 +8780,6 @@
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
@@ -9009,12 +8916,13 @@
}
},
"@babel/code-frame": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
"integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
"integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
"dev": true,
"requires": {
"@babel/highlight": "^7.18.6"
"@babel/highlight": "^7.23.4",
"chalk": "^2.4.2"
}
},
"@babel/compat-data": {
@@ -9047,13 +8955,14 @@
}
},
"@babel/generator": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz",
"integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==",
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz",
"integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==",
"dev": true,
"requires": {
"@babel/types": "^7.20.7",
"@babel/types": "^7.23.5",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
}
},
@@ -9146,9 +9055,9 @@
}
},
"@babel/helper-environment-visitor": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
"integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true
},
"@babel/helper-explode-assignable-expression": {
@@ -9161,22 +9070,22 @@
}
},
"@babel/helper-function-name": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
"integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"requires": {
"@babel/template": "^7.18.10",
"@babel/types": "^7.19.0"
"@babel/template": "^7.22.15",
"@babel/types": "^7.23.0"
}
},
"@babel/helper-hoist-variables": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
"integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
"integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
"dev": true,
"requires": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.22.5"
}
},
"@babel/helper-member-expression-to-functions": {
@@ -9273,24 +9182,24 @@
}
},
"@babel/helper-split-export-declaration": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
"integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dev": true,
"requires": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.22.5"
}
},
"@babel/helper-string-parser": {
"version": "7.19.4",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
"integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
"integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"dev": true
},
"@babel/helper-validator-identifier": {
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true
},
"@babel/helper-validator-option": {
@@ -9323,21 +9232,20 @@
}
},
"@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
"integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
}
},
"@babel/parser": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.7.tgz",
"integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==",
"dev": true
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz",
"integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ=="
},
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
"version": "7.18.6",
@@ -10074,42 +9982,42 @@
}
},
"@babel/template": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
"integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==",
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/parser": "^7.20.7",
"@babel/types": "^7.20.7"
"@babel/code-frame": "^7.22.13",
"@babel/parser": "^7.22.15",
"@babel/types": "^7.22.15"
}
},
"@babel/traverse": {
"version": "7.20.10",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.10.tgz",
"integrity": "sha512-oSf1juCgymrSez8NI4A2sr4+uB/mFd9MXplYGPEBnfAuWmmyeVcHa6xLPiaRBcXkcb/28bgxmQLTVwFKE1yfsg==",
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz",
"integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.7",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.19.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.20.7",
"@babel/types": "^7.20.7",
"@babel/code-frame": "^7.23.5",
"@babel/generator": "^7.23.5",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.23.5",
"@babel/types": "^7.23.5",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz",
"integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==",
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz",
"integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==",
"dev": true,
"requires": {
"@babel/helper-string-parser": "^7.19.4",
"@babel/helper-validator-identifier": "^7.19.1",
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
}
},
@@ -11151,13 +11059,6 @@
"@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
},
"dependencies": {
"@babel/parser": {
"version": "7.22.16",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz",
"integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA=="
}
}
},
"@vue/compiler-dom": {
@@ -11186,11 +11087,6 @@
"source-map-js": "^1.0.2"
},
"dependencies": {
"@babel/parser": {
"version": "7.22.16",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz",
"integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA=="
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
@@ -11203,21 +11099,6 @@
"requires": {
"@jridgewell/sourcemap-codec": "^1.4.15"
}
},
"nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="
},
"postcss": {
"version": "8.4.20",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz",
"integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==",
"requires": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
}
}
}
},
@@ -11303,11 +11184,6 @@
"magic-string": "^0.30.0"
},
"dependencies": {
"@babel/parser": {
"version": "7.22.16",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz",
"integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA=="
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
@@ -11450,9 +11326,9 @@
"dev": true
},
"axios": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
"integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"dev": true,
"requires": {
"follow-redirects": "^1.15.0",
@@ -11628,9 +11504,9 @@
"dev": true
},
"caniuse-lite": {
"version": "1.0.30001503",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001503.tgz",
"integrity": "sha512-Sf9NiF+wZxPfzv8Z3iS0rXM1Do+iOy2Lxvib38glFX+08TCYYYGR5fRJXk4d77C4AYwhUjgYgMsMudbh2TqCKw==",
"version": "1.0.30001565",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz",
"integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==",
"dev": true
},
"chalk": {
@@ -12007,9 +11883,9 @@
}
},
"engine.io-client": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz",
"integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
@@ -12508,9 +12384,9 @@
"dev": true
},
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"dev": true
},
"form-data": {
@@ -12548,6 +12424,12 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"optional": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -13354,8 +13236,7 @@
"nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"dev": true
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA=="
},
"napi-build-utils": {
"version": "1.0.2",
@@ -13602,10 +13483,9 @@
}
},
"postcss": {
"version": "8.4.30",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz",
"integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==",
"dev": true,
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"requires": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
@@ -14046,9 +13926,9 @@
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true
},
"serialize-javascript": {
@@ -14163,9 +14043,9 @@
"dev": true
},
"socket.io-client": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
"integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz",
"integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
@@ -14614,9 +14494,9 @@
"dev": true
},
"vite": {
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
"integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==",
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
"dev": true,
"requires": {
"esbuild": "^0.18.10",
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "stacjownik",
"version": "1.18.6",
"version": "1.23.0",
"private": true,
"scripts": {
"dev": "vite",
@@ -18,27 +18,27 @@
"howler": "^2.2.4",
"pinia": "^2.1.6",
"sass": "^1.67.0",
"socket.io-client": "^4.7.2",
"socket.io-client": "^4.7.4",
"vue": "^3.3.4",
"vue-i18n": "^9.4.1",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@types/node": "^20.6.2",
"@vite-pwa/assets-generator": "^0.0.10",
"@vitejs/plugin-vue": "^4.3.4",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.4.0",
"axios": "^1.5.0",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"prettier": "^3.0.3",
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vite-plugin-pwa": "^0.16.5",
"vue-tsc": "^1.8.11",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"@rushstack/eslint-patch": "^1.3.3"
"vue-tsc": "^1.8.11"
},
"browserslist": [
"> 1%",
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
<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"/>
<path d="M10.5573 31.3793L9.43741 28.0694C9.35445 27.8592 9.26596 27.6131 9.17195 27.3311C9.07793 27.0435 8.98391 26.7338 8.8899 26.402C8.80694 26.7393 8.71845 27.0518 8.62444 27.3394C8.53042 27.6269 8.44193 27.8758 8.35898 28.086L7.24736 31.3793H10.5573ZM15.0121 36H12.8386C12.5953 36 12.3989 35.9447 12.2496 35.8341C12.1003 35.7179 11.9869 35.5714 11.9095 35.3944L11.1961 33.2873H6.6003L5.88688 35.3944C5.82604 35.5493 5.71544 35.6903 5.55505 35.8175C5.4002 35.9392 5.20664 36 4.97436 36H2.78431L7.46305 23.9133H10.3333L15.0121 36ZM22.643 26.3688C22.5601 26.5015 22.4716 26.6011 22.3775 26.6674C22.2891 26.7338 22.1729 26.767 22.0291 26.767C21.9019 26.767 21.7637 26.7283 21.6143 26.6508C21.4706 26.5679 21.3046 26.4766 21.1166 26.3771C20.9341 26.2775 20.724 26.189 20.4861 26.1116C20.2483 26.0287 19.9773 25.9872 19.6732 25.9872C19.1478 25.9872 18.7551 26.1006 18.4952 26.3273C18.2408 26.5485 18.1136 26.8499 18.1136 27.2315C18.1136 27.4749 18.191 27.6767 18.3459 27.8371C18.5007 27.9975 18.7026 28.1357 18.9515 28.2519C19.2059 28.368 19.4934 28.4759 19.8142 28.5754C20.1405 28.6694 20.4723 28.7773 20.8097 28.8989C21.147 29.0151 21.4761 29.1533 21.7969 29.3137C22.1231 29.4741 22.4107 29.6787 22.6596 29.9276C22.914 30.1765 23.1186 30.4806 23.2735 30.8401C23.4283 31.1941 23.5058 31.6227 23.5058 32.1259C23.5058 32.6845 23.409 33.2071 23.2154 33.6938C23.0218 34.1805 22.7398 34.6063 22.3693 34.9713C22.0042 35.3308 21.5508 35.6156 21.0088 35.8258C20.4723 36.0304 19.8612 36.1327 19.1754 36.1327C18.7994 36.1327 18.415 36.094 18.0223 36.0166C17.6352 35.9392 17.2591 35.8313 16.8941 35.6931C16.5291 35.5493 16.1862 35.3806 15.8655 35.187C15.5447 34.9935 15.2654 34.7778 15.0276 34.54L15.8572 33.2293C15.9235 33.1352 16.0093 33.0578 16.1143 32.997C16.225 32.9306 16.3439 32.8974 16.4711 32.8974C16.637 32.8974 16.8029 32.95 16.9688 33.0551C17.1402 33.1601 17.331 33.2763 17.5412 33.4035C17.7569 33.5307 18.003 33.6468 18.2795 33.7519C18.556 33.857 18.8823 33.9095 19.2584 33.9095C19.7672 33.9095 20.1626 33.7989 20.4447 33.5777C20.7267 33.3509 20.8677 32.9942 20.8677 32.5075C20.8677 32.2255 20.7903 31.996 20.6355 31.819C20.4806 31.642 20.276 31.4955 20.0216 31.3793C19.7727 31.2632 19.4879 31.1609 19.1671 31.0724C18.8464 30.9839 18.5173 30.8871 18.18 30.7821C17.8426 30.6714 17.5135 30.5387 17.1928 30.3839C16.872 30.2235 16.5844 30.0161 16.33 29.7617C16.0812 29.5018 15.8793 29.181 15.7245 28.7994C15.5696 28.4123 15.4922 27.9367 15.4922 27.3725C15.4922 26.9191 15.5834 26.4766 15.7659 26.0452C15.9484 25.6139 16.2167 25.2295 16.5706 24.8922C16.9246 24.5548 17.3587 24.2866 17.873 24.0875C18.3874 23.8829 18.9763 23.7805 19.64 23.7805C20.0105 23.7805 20.37 23.811 20.7184 23.8718C21.0724 23.9271 21.407 24.0128 21.7222 24.129C22.0374 24.2396 22.3305 24.3751 22.6015 24.5354C22.8781 24.6903 23.1242 24.8673 23.3398 25.0664L22.643 26.3688ZM36.1186 29.9525C36.1186 30.8263 35.9665 31.6337 35.6623 32.3748C35.3637 33.1104 34.9406 33.7491 34.3931 34.2911C33.8456 34.8276 33.1847 35.2479 32.4105 35.552C31.6417 35.8507 30.7873 36 29.8471 36H25.1518V23.9133H29.8471C30.7873 23.9133 31.6417 24.0654 32.4105 24.3695C33.1847 24.6737 33.8456 25.094 34.3931 25.6305C34.9406 26.1669 35.3637 26.8057 35.6623 27.5468C35.9665 28.2823 36.1186 29.0842 36.1186 29.9525ZM33.2483 29.9525C33.2483 29.3552 33.1709 28.816 33.016 28.3348C32.8612 27.8537 32.6372 27.4472 32.3441 27.1154C32.0565 26.778 31.7026 26.5209 31.2823 26.3439C30.8619 26.1614 30.3836 26.0701 29.8471 26.0701H27.9723V33.8431H29.8471C30.3836 33.8431 30.8619 33.7547 31.2823 33.5777C31.7026 33.3952 32.0565 33.138 32.3441 32.8062C32.6372 32.4688 32.8612 32.0596 33.016 31.5784C33.1709 31.0973 33.2483 30.5553 33.2483 29.9525ZM40.594 26.0701V28.8906H44.3934V30.9646H40.594V33.8431H45.5547V36H37.7735V23.9133H45.5547V26.0701H40.594ZM50.0882 28.8077H50.5361C50.9509 28.8077 51.2496 28.6777 51.4321 28.4178L54.153 24.4691C54.3134 24.2589 54.4849 24.1151 54.6674 24.0377C54.8554 23.9547 55.0877 23.9133 55.3642 23.9133H57.8031L54.2194 28.7994C53.965 29.1368 53.6912 29.3801 53.3981 29.5294C53.6083 29.6068 53.7991 29.7147 53.9705 29.8529C54.142 29.9912 54.3024 30.1709 54.4517 30.3922L58.1018 36H55.5965C55.4361 36 55.2978 35.9889 55.1817 35.9668C55.0711 35.9447 54.9743 35.9115 54.8913 35.8673C54.8084 35.823 54.7365 35.7705 54.6757 35.7097C54.6148 35.6433 54.5568 35.5686 54.5015 35.4857L51.7639 31.2798C51.6643 31.1249 51.5371 31.0171 51.3823 30.9563C51.233 30.8899 51.0284 30.8567 50.7684 30.8567H50.0882V36H47.2843V23.9133H50.0882V28.8077Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

+56
View File
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Warstwa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 159 38.8" style="enable-background:new 0 0 159 38.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#00A962;}
.st1{enable-background:new ;}
.st2{fill:#FFFFFF;}
.st3{fill:#1E3A33;}
.st4{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
</style>
<path class="st0" d="M139.6,38.7H19.4C8.7,38.7,0,30.1,0,19.3l0,0C0,8.6,8.7-0.1,19.4-0.1h120.2c10.7,0,19.4,8.7,19.4,19.4l0,0
C159,30.1,150.3,38.7,139.6,38.7z"/>
<g class="st1">
<path class="st2" d="M48.9,23.7c0,0.5-0.4,0.9-0.9,0.9c-0.5,0-0.9-0.4-0.9-0.9v-9.9c0-0.6,0.4-1,1-1h3.7c2.5,0,3.6,1.9,3.6,3.7
s-1,3.7-3.6,3.7h-2.9V23.7z M48.9,14.5v4.1h2.8c1.3,0,1.9-1,1.9-2c0-1-0.5-2.1-1.8-2.1H48.9z"/>
<path class="st2" d="M56.3,20.5c0-2.5,1.9-4.1,4.1-4.1c2.3,0,4.2,1.6,4.2,4.1c0,2.5-1.9,4.2-4.2,4.2C58.2,24.7,56.3,23,56.3,20.5z
M62.9,20.5c0-1.5-1.1-2.4-2.4-2.4c-1.3,0-2.4,1-2.4,2.4c0,1.5,1.1,2.5,2.4,2.5C61.8,23,62.9,22,62.9,20.5z"/>
<path class="st2" d="M66.6,21.9c0.4-0.2,0.7,0.1,0.9,0.3c0.3,0.6,0.9,1,1.7,1c0.8,0,1.4-0.4,1.4-1c0-0.5-0.5-0.7-1.1-0.9l-1.1-0.3
c-1.7-0.5-2.3-1.4-2.2-2.7c0.1-1.2,1.4-2.1,2.8-2.1c1.1,0,2,0.3,2.5,1.2c0.2,0.4,0.1,0.8-0.2,1c-0.3,0.2-0.6,0.2-1-0.1
c-0.4-0.4-1-0.5-1.4-0.5c-0.4,0-0.9,0.2-1.1,0.4c-0.1,0.2-0.2,0.4-0.1,0.7c0.1,0.3,0.6,0.5,1,0.6l1.2,0.3c1.7,0.4,2.1,1.5,2.1,2.3
c0,1.5-1.3,2.4-3.2,2.4c-1.2,0-2.5-0.6-2.9-1.8C66,22.5,66.2,22.1,66.6,21.9z"/>
<path class="st2" d="M74.6,18h-0.8c-0.4,0-0.7-0.3-0.7-0.7c0-0.4,0.3-0.7,0.7-0.7h0.8v-2.2c0-0.5,0.4-0.9,0.9-0.9
c0.5,0,0.9,0.4,0.9,0.9v2.2h1c0.4,0,0.7,0.3,0.7,0.7c0,0.4-0.3,0.7-0.7,0.7h-1v5.7c0,0.5-0.4,0.9-0.9,0.9c-0.5,0-0.9-0.4-0.9-0.9
V18z"/>
<path class="st2" d="M85.7,23.4c-0.5,0.9-1.7,1.3-2.7,1.3c-2.1,0-4-1.6-4-4.1c0-2.5,1.9-4.1,4-4.1c1,0,2.1,0.4,2.7,1.3v-0.3
c0-0.5,0.4-0.9,0.9-0.9c0.5,0,0.9,0.4,0.9,0.9v6.3c0,0.5-0.4,0.9-0.9,0.9c-0.5,0-0.9-0.4-0.9-0.9V23.4z M83.2,23
c1.3,0,2.4-0.9,2.4-2.5c0-1.6-1.3-2.4-2.4-2.4c-1.3,0-2.4,1-2.4,2.4C80.8,22,81.9,23,83.2,23z"/>
<path class="st2" d="M95.1,16.6c0.5,0,0.9,0.3,1,0.8l1.6,4.6l1.7-4.8c0.2-0.5,0.6-0.7,1.1-0.6c0.5,0.1,0.7,0.6,0.6,1.1l-2.4,6.3
c-0.2,0.5-0.5,0.7-0.9,0.7h-0.1c-0.4,0-0.7-0.2-0.9-0.7L95.1,19l-1.7,4.9c-0.2,0.5-0.5,0.7-0.9,0.7h-0.1c-0.4,0-0.7-0.2-0.9-0.7
l-2.4-6.3c-0.2-0.4,0.1-1,0.6-1.1c0.5-0.1,0.9,0.1,1.1,0.6l1.7,4.8l1.6-4.6C94.2,16.8,94.6,16.6,95.1,16.6L95.1,16.6z"/>
<path class="st2" d="M112.9,23.2c0.3,0.3,0.3,0.9,0,1.2c-0.4,0.3-0.9,0.3-1.2,0l-3.2-3.5v2.9c0,0.5-0.4,0.9-0.9,0.9
c-0.5,0-0.8-0.4-0.8-0.9V13.1c0-0.5,0.4-0.9,0.8-0.9c0.5,0,0.9,0.4,0.9,0.9v6.1l2.4-2.4c0.3-0.3,0.9-0.3,1.2,0
c0.3,0.4,0.3,0.9,0,1.2l-2,2.1L112.9,23.2z"/>
<path class="st2" d="M120.8,23.4c-0.5,0.9-1.7,1.3-2.7,1.3c-2.1,0-4-1.6-4-4.1c0-2.5,1.9-4.1,4-4.1c1,0,2.1,0.4,2.7,1.3v-0.3
c0-0.5,0.4-0.9,0.8-0.9c0.5,0,0.9,0.4,0.9,0.9v6.3c0,0.5-0.4,0.9-0.9,0.9c-0.5,0-0.8-0.4-0.8-0.9V23.4z M118.4,23
c1.3,0,2.4-0.9,2.4-2.5c0-1.6-1.3-2.4-2.4-2.4c-1.3,0-2.4,1-2.4,2.4C115.9,22,117,23,118.4,23z"/>
<path class="st2" d="M130.3,16.6c0.5,0,0.9,0.3,1,0.8l1.6,4.6l1.7-4.8c0.2-0.5,0.6-0.7,1.1-0.6c0.5,0.1,0.7,0.6,0.6,1.1l-2.4,6.3
c-0.2,0.5-0.5,0.7-0.9,0.7h-0.1c-0.4,0-0.7-0.2-0.9-0.7l-1.7-4.9l-1.7,4.9c-0.2,0.5-0.5,0.7-0.9,0.7h-0.1c-0.4,0-0.7-0.2-0.9-0.7
l-2.4-6.3c-0.2-0.4,0.1-1,0.6-1.1c0.5-0.1,0.9,0.1,1.1,0.6l1.7,4.8l1.6-4.6C129.3,16.8,129.7,16.6,130.3,16.6L130.3,16.6z"/>
<path class="st2" d="M143.6,26.1c0,0.2,0.2,0.5,0.6,0.5c0.1,0,0.4,0,0.5-0.4c0.1-0.4,0.3-0.5,0.7-0.5c0.4,0.1,0.6,0.4,0.5,0.8
c-0.2,0.8-0.9,1.4-1.7,1.4c-1.3,0-1.9-0.8-1.9-1.7c0-0.2,0.1-0.6,0.2-0.8l0.5-1c-0.5,0.2-1,0.3-1.6,0.3c-2.3,0-4.1-1.6-4.1-4.2
c0-2.5,1.8-4.1,4.1-4.1c2.2,0,3.9,1.5,4,4c0,0.5-0.4,0.8-0.8,0.8h-5.5c0.2,1.2,1.2,2,2.4,2c0.8,0,1.3-0.3,1.9-0.8
c0.3-0.2,0.8-0.4,1.1-0.1c0.3,0.3,0.2,0.7,0,1.1C143.5,25.9,143.6,26.1,143.6,26.1z M143.7,19.9c-0.2-1.2-1.1-2-2.3-2
c-1.2,0-2.2,0.7-2.3,2H143.7z"/>
</g>
<g>
<path class="st3" d="M21.6,26.7c-4.9,0-7.4-2.6-8.5-4.9c-1.2-2.4-1.4-5-1.4-6c0-2.2,1.8-3.8,4.3-3.8h7c0.7,0,1.3,0.6,1.3,1.3
c0,0.7-0.6,1.3-1.3,1.3h-7c-0.5,0-1,0.1-1.3,0.4c-0.3,0.2-0.4,0.4-0.4,0.8c0,1.9,0.5,8.3,7.3,8.3c5.8,0,7.3-4.6,7.9-8.1
c0-0.2,0-0.3,0.1-0.4c0.2-0.9,0.4-1.7,0.8-2.3C31,12.4,31.9,12,33,12c0.5,0,0.9,0.2,1.2,0.5c0.5,0.4,0.9,1.3,1.2,4.1
c0.1,1.4,0.1,2.6,0.1,2.7c0,0.7-0.6,1.3-1.3,1.3l0,0c-0.7,0-1.3-0.6-1.3-1.3c0,0,0-1.1-0.1-2.3c-0.1-1-0.2-1.7-0.3-2.1
c-0.1,0.3-0.2,0.9-0.3,1.1c0,0.1,0,0.2-0.1,0.4c-0.2,1.2-0.7,3.7-2.1,5.9c-0.8,1.3-1.9,2.3-3.2,3.1C25.4,26.3,23.6,26.7,21.6,26.7z
"/>
<ellipse class="st4" cx="26.7" cy="13.3" rx="1.3" ry="1.3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

+2 -2
View File
@@ -1,4 +1,4 @@
<svg width="160" height="150" viewBox="0 0 160 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.163 139L80 12.4204L149.837 139H80H10.163Z" stroke="white" stroke-width="12"/>
<path d="M85.4488 50.3354V80.6619C85.4488 83.8784 85.2898 87.0418 84.9717 90.1522C84.6536 93.2273 84.2294 96.4968 83.6992 99.9606H74.8451C74.315 96.4968 73.8908 93.2273 73.5727 90.1522C73.2546 87.0418 73.0955 83.8784 73.0955 80.6619V50.3354H85.4488ZM71.0808 119.789C71.0808 118.694 71.2752 117.651 71.664 116.661C72.0882 115.672 72.6537 114.823 73.3606 114.117C74.1029 113.41 74.9689 112.844 75.9585 112.42C76.9482 111.996 78.0086 111.784 79.1396 111.784C80.2354 111.784 81.278 111.996 82.2677 112.42C83.2574 112.844 84.1057 113.41 84.8126 114.117C85.5195 114.823 86.085 115.672 86.5092 116.661C86.9333 117.651 87.1454 118.694 87.1454 119.789C87.1454 120.921 86.9333 121.981 86.5092 122.971C86.085 123.925 85.5195 124.756 84.8126 125.462C84.1057 126.169 83.2574 126.717 82.2677 127.106C81.278 127.53 80.2354 127.742 79.1396 127.742C78.0086 127.742 76.9482 127.53 75.9585 127.106C74.9689 126.717 74.1029 126.169 73.3606 125.462C72.6537 124.756 72.0882 123.925 71.664 122.971C71.2752 121.981 71.0808 120.921 71.0808 119.789Z" fill="#FFFBFB"/>
<path d="M10.163 139L80 12.4204L149.837 139H80H10.163Z" stroke="salmon" stroke-width="15"/>
<path d="M85.4488 50.3354V80.6619C85.4488 83.8784 85.2898 87.0418 84.9717 90.1522C84.6536 93.2273 84.2294 96.4968 83.6992 99.9606H74.8451C74.315 96.4968 73.8908 93.2273 73.5727 90.1522C73.2546 87.0418 73.0955 83.8784 73.0955 80.6619V50.3354H85.4488ZM71.0808 119.789C71.0808 118.694 71.2752 117.651 71.664 116.661C72.0882 115.672 72.6537 114.823 73.3606 114.117C74.1029 113.41 74.9689 112.844 75.9585 112.42C76.9482 111.996 78.0086 111.784 79.1396 111.784C80.2354 111.784 81.278 111.996 82.2677 112.42C83.2574 112.844 84.1057 113.41 84.8126 114.117C85.5195 114.823 86.085 115.672 86.5092 116.661C86.9333 117.651 87.1454 118.694 87.1454 119.789C87.1454 120.921 86.9333 121.981 86.5092 122.971C86.085 123.925 85.5195 124.756 84.8126 125.462C84.1057 126.169 83.2574 126.717 82.2677 127.106C81.278 127.53 80.2354 127.742 79.1396 127.742C78.0086 127.742 76.9482 127.53 75.9585 127.106C74.9689 126.717 74.1029 126.169 73.3606 125.462C72.6537 124.756 72.0882 123.925 71.664 122.971C71.2752 121.981 71.0808 120.921 71.0808 119.789Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

-71
View File
@@ -1,71 +0,0 @@
@import './styles/responsive.scss';
@import './styles/variables.scss';
@import './styles/global.scss';
@import './styles/animations.scss';
.route {
margin: 0 0.2em;
&-active,
&[data-active='true'] {
color: $accentCol;
font-weight: bold;
}
}
// APP
#app {
color: white;
font-size: 1rem;
overflow-x: hidden;
@include smallScreen() {
font-size: calc(0.65rem + 0.8vw);
}
@include screenLandscape() {
font-size: calc(0.45rem + 0.8vw);
}
}
// CONTAINER
.app_container {
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-columns: 100%;
min-height: 100vh;
}
.app_main {
padding: 0 0.5em;
}
.warning {
background-color: firebrick;
text-align: center;
padding: 0.5em 0.4em;
max-width: 1100px;
margin: 0 auto;
border-radius: 0 0 1em 1em;
}
// FOOTER
footer.app_footer {
max-width: 100%;
padding: 0.5em;
img {
width: 1.1em;
vertical-align: text-bottom;
}
z-index: 10;
background: #111;
color: white;
text-align: center;
vertical-align: middle;
}
+170 -48
View File
@@ -1,5 +1,7 @@
<template>
<div class="app_container">
<div class="app_container" v-cloak>
<PopUp />
<transition name="modal-anim">
<keep-alive>
<TrainModal v-if="store.chosenModalTrainId" />
@@ -10,7 +12,7 @@
<main class="app_main">
<router-view v-slot="{ Component }">
<keep-alive exclude="JournalView,SceneryView">
<keep-alive exclude="SceneryView">
<component :is="Component" :key="$route.name" />
</keep-alive>
</router-view>
@@ -33,30 +35,37 @@
<script lang="ts">
import { defineComponent, watch } from 'vue';
import axios from 'axios';
import { version } from '.././package.json';
import { useMainStore } from './store/mainStore';
import Clock from './components/App/Clock.vue';
import packageInfo from '.././package.json';
import { regions } from './data/options.json';
import { useStore } from './store/mainStore';
import StatusIndicator from './components/App/StatusIndicator.vue';
import TrainModal from './components/Global/TrainModal.vue';
import AppHeader from './components/App/AppHeader.vue';
import axios from 'axios';
import TrainModal from './components/TrainsView/TrainModal.vue';
import StorageManager from './managers/storageManager';
import PopUp from './components/PopUp/PopUp.vue';
import { useApiStore } from './store/apiStore';
import { Status } from './typings/common';
import { usePopupStore } from './store/popupStore';
const STORAGE_VERSION_KEY = 'app_version';
export default defineComponent({
components: {
Clock,
StatusIndicator,
AppHeader,
TrainModal,
AppHeader
PopUp
},
data: () => ({
VERSION: packageInfo.version,
store: useStore(),
VERSION: version,
store: useMainStore(),
apiStore: useApiStore(),
popupStore: usePopupStore(),
currentLang: 'pl',
releaseURL: '',
@@ -64,28 +73,37 @@ export default defineComponent({
}),
created() {
this.loadLang();
this.store.connectToAPI();
this.store.isOffline = !window.navigator.onLine;
window.addEventListener('offline', () => {
this.store.isOffline = true;
this.store.activeData.activeSceneries = [];
this.store.activeData.trains = [];
this.store.activeData.connectedSocketCount = 0;
this.store.setStatuses();
});
window.addEventListener('online', () => {
this.store.isOffline = false;
});
this.init();
},
async mounted() {
this.setReleaseURL();
window.addEventListener('focus', () => {
if (Date.now() - this.apiStore.lastFetchData.getTime() < 15000) return;
this.apiStore.fetchActiveData();
});
// popup handling
window.addEventListener('mousemove', (e: MouseEvent) => {
e.stopPropagation();
const targetEl = e
.composedPath()
.find((p) => p instanceof HTMLElement && p.getAttribute('data-popup-key'));
if (!targetEl || !(targetEl instanceof HTMLElement)) {
if (this.popupStore.currentPopupComponent != null) this.popupStore.onPopUpHide();
return;
}
const popupComponentKey = targetEl.getAttribute('data-popup-key');
const popupContent = targetEl.getAttribute('data-popup-content');
if (popupComponentKey && popupContent)
this.popupStore.onPopUpShow(e, popupComponentKey, popupContent);
else if (this.popupStore.currentPopupComponent != null) this.popupStore.onPopUpHide();
});
watch(
() => this.store.blockScroll,
@@ -96,23 +114,56 @@ export default defineComponent({
);
},
watch: {
'$route.query.region': {
immediate: true,
handler(regionQuery: string) {
if (regionQuery) {
this.store.region.id =
regions.find(
(reg) =>
reg.id == regionQuery.toLocaleLowerCase() ||
reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase()
)?.id || 'eu';
}
}
}
},
methods: {
init() {
this.loadLang();
this.setReleaseURL();
this.setupOfflineHandling();
this.checkAppVersion();
this.apiStore.setupAPIData();
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
},
checkAppVersion() {
if (import.meta.env.DEV) {
this.store.isNewUpdate = true;
return;
}
const storageVersion = StorageManager.getStringValue(STORAGE_VERSION_KEY);
if (storageVersion === undefined || storageVersion != version) {
this.store.isNewUpdate = true;
StorageManager.setStringValue(STORAGE_VERSION_KEY, version);
}
},
setupOfflineHandling() {
this.store.isOffline = !window.navigator.onLine;
if (this.store.isOffline) this.handleOfflineMode();
window.addEventListener('offline', this.handleOfflineMode);
window.addEventListener('online', this.handleOnlineMode);
},
handleOfflineMode() {
this.store.isOffline = true;
this.apiStore.activeData = undefined;
this.apiStore.dataStatuses.connection = Status.Data.Offline;
},
handleOnlineMode() {
this.store.isOffline = false;
this.apiStore.connectToAPI();
},
changeLang(lang: string) {
this.$i18n.locale = lang;
this.currentLang = lang;
@@ -156,4 +207,75 @@ export default defineComponent({
});
</script>
<style lang="scss" src="./App.scss"></style>
<style lang="scss">
@import './styles/global';
@import './styles/animations';
.route {
margin: 0 0.2em;
&-active,
&[data-active='true'] {
color: $accentCol;
font-weight: bold;
}
}
// APP
#app {
color: white;
font-size: 1rem;
overflow-x: hidden;
@include smallScreen() {
font-size: calc(0.65rem + 0.8vw);
}
@include screenLandscape() {
font-size: calc(0.45rem + 0.8vw);
}
}
// CONTAINER
.app_container {
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-columns: 100%;
min-height: 100vh;
position: relative;
}
.app_main {
padding: 0 0.5em;
}
.warning {
background-color: firebrick;
text-align: center;
padding: 0.5em 0.4em;
max-width: 1100px;
margin: 0 auto;
border-radius: 0 0 1em 1em;
}
// FOOTER
footer.app_footer {
max-width: 100%;
padding: 0.5em;
img {
width: 1.1em;
vertical-align: text-bottom;
}
z-index: 10;
background: #111;
color: white;
text-align: center;
vertical-align: middle;
}
</style>
+4 -4
View File
@@ -68,7 +68,7 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../../store/mainStore';
import { useMainStore } from '../../store/mainStore';
import StatusIndicator from './StatusIndicator.vue';
import Clock from './Clock.vue';
import RegionDropdown from '../Global/RegionDropdown.vue';
@@ -84,7 +84,7 @@ export default defineComponent({
setup() {
return {
store: useStore()
store: useMainStore()
};
},
@@ -100,8 +100,8 @@ export default defineComponent({
},
onlineDispatchersCount() {
return this.store.onlineSceneryList.filter(
(scenery) => scenery.region == this.store.region.id
return this.store.activeSceneryList.filter(
(scenery) => scenery.region == this.store.region.id && scenery.dispatcherId != -1
).length;
},
+96 -104
View File
@@ -36,11 +36,11 @@
<circle id="Ellipse 18" cx="15" cy="17" r="7" fill="#393838" />
</g>
<g v-if="greenLight" filter="url(#filter0_d_843_28)">
<g v-if="indicator.lights.greenLight" filter="url(#filter0_d_843_28)">
<circle cx="15" cy="17" r="7" fill="#00FF0A" />
</g>
<g v-if="greenBlinkLight" filter="url(#filter0_d_843_28)">
<g v-if="indicator.lights.greenBlinkLight" filter="url(#filter0_d_843_28)">
<circle cx="15" cy="17" r="7" fill="#00FF0A" />
<animate
@@ -52,14 +52,14 @@
/>
</g>
<g v-if="redTopLight" filter="url(#filter1_d_843_28)">
<g v-if="indicator.lights.redTopLight" filter="url(#filter1_d_843_28)">
<circle cx="15" cy="36" r="7" fill="#F40000" />
</g>
<g v-if="orangeLight" filter="url(#filter2_d_843_28)">
<g v-if="indicator.lights.orangeLight" filter="url(#filter2_d_843_28)">
<circle cx="15" cy="55" r="7" fill="#FFB800" />
</g>
<g v-if="redBottomLight" filter="url(#filter3_d_843_28)">
<g v-if="indicator.lights.redBottomLight" filter="url(#filter3_d_843_28)">
<circle cx="15" cy="74" r="7" fill="#F40000" />
<animate
@@ -186,7 +186,11 @@
</svg>
<transition name="tooltip-anim">
<div v-html="$t(indicator.message)" class="indicator-tooltip" v-if="tooltipActive"></div>
<div
v-html="$t('data-status.' + indicator.message)"
class="indicator-tooltip"
v-if="tooltipActive"
></div>
</transition>
</div>
</div>
@@ -194,124 +198,112 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { StoreState } from '../../store/typings';
import { useStore } from '../../store/mainStore';
import { Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
import { APIDataStatus } from '../../typings/api';
interface Indicator {
// status: Status.Data;
message: string;
lights: {
greenLight: boolean;
greenBlinkLight: boolean;
redTopLight: boolean;
orangeLight: boolean;
redBottomLight: boolean;
};
}
export default defineComponent({
data() {
return {
tooltipActive: false,
indicator: {
offline: false,
status: Status.Data.Loading,
message: 'data-status.S3'
},
greenLight: false,
greenBlinkLight: false,
redTopLight: false,
orangeLight: false,
redBottomLight: false
apiStore: useApiStore()
};
},
mounted() {
this.setSignalStatus(Status.Data.Loading);
},
setup() {
const store = useStore();
return {
dataStatus: store.dataStatuses,
store
};
},
watch: {
dataStatus: {
deep: true,
handler(statuses: StoreState['dataStatuses']) {
const connectionStatus = statuses.connection;
const sceneryDataStatus = statuses.sceneries;
const trainsDataStatus = statuses.trains;
const dispatcherDataStatus = statuses.dispatchers;
if (this.store.isOffline) {
this.setSignalStatus(Status.Data.Initialized);
this.indicator.status = Status.Data.Initialized;
this.indicator.message = 'data-status.S1-offline';
return;
}
if (connectionStatus == Status.Data.Error) {
this.setSignalStatus(connectionStatus);
this.indicator.status = connectionStatus;
this.indicator.message = 'data-status.S1a-connection';
return;
}
if (sceneryDataStatus == Status.Data.Error) {
this.setSignalStatus(sceneryDataStatus);
this.indicator.status = sceneryDataStatus;
this.indicator.message = 'data-status.S1a-sceneries';
return;
}
if (trainsDataStatus == Status.Data.Warning) {
this.setSignalStatus(trainsDataStatus);
this.indicator.status = trainsDataStatus;
this.indicator.message = 'data-status.S5-trains';
return;
}
if (dispatcherDataStatus == Status.Data.Warning) {
this.setSignalStatus(dispatcherDataStatus);
this.indicator.status = dispatcherDataStatus;
this.indicator.message = 'data-status.S5-dispatchers';
return;
}
if (sceneryDataStatus == Status.Data.Loaded) {
this.setSignalStatus(Status.Data.Loaded);
this.indicator.status = Status.Data.Loaded;
this.indicator.message = 'data-status.S2';
}
}
}
},
methods: {
setSignalStatus(status: Status.Data) {
this.greenLight = false;
this.greenBlinkLight = false;
this.redTopLight = false;
this.orangeLight = false;
this.redBottomLight = false;
setLights(message: string) {
let lights = {
greenBlinkLight: false,
greenLight: false,
orangeLight: false,
redBottomLight: false,
redTopLight: false
};
if (status == Status.Data.Initialized) {
this.redTopLight = true;
switch (message) {
case 'S3':
lights.greenBlinkLight = true;
break;
case 'S2':
lights.greenLight = true;
break;
case 'S1-offline':
lights.redTopLight = true;
break;
case 'S1a-connection':
case 'S1a-sceneries':
lights.redTopLight = true;
lights.redBottomLight = true;
break;
case 'S5-dispatchers':
case 'S5-trains':
lights.orangeLight = true;
break;
default:
break;
}
if (status == Status.Data.Loaded) {
this.greenLight = true;
return lights;
}
},
computed: {
indicator(): Indicator {
const dataStatuses = this.apiStore.dataStatuses;
const swdrStatuses = this.apiStore.activeData?.apiStatuses;
let message = 'S3';
switch (dataStatuses.connection) {
case Status.Data.Loading:
message = 'S3';
break;
case Status.Data.Loaded:
message = 'S2';
break;
case Status.Data.Offline:
message = 'S1-offline';
break;
case Status.Data.Error:
message = 'S1a-connection';
break;
default:
break;
}
if (status == Status.Data.Warning) {
this.orangeLight = true;
if (swdrStatuses?.dispatchersAPI == APIDataStatus.WARNING) {
message = 'S5-dispatchers';
}
if (status == Status.Data.Error) {
this.redTopLight = true;
this.redBottomLight = true;
if (swdrStatuses?.trainsAPI == APIDataStatus.WARNING) {
message = 'S5-trains';
}
if (status == Status.Data.Loading) {
this.greenBlinkLight = true;
if (swdrStatuses?.stationsAPI == APIDataStatus.WARNING) {
message = 'S1a-sceneries';
}
return {
lights: this.setLights(message),
message
};
}
}
});
+48
View File
@@ -0,0 +1,48 @@
<template>
<AnimatedModal :is-open="mainStore.isNewUpdate" @toggle-modal="toggleModal">
<div class="modal_content">
<h1 class="header">Aktualizacja Stacjownika</h1>
<h2>wersja {{ version }}</h2>
<b>Co nowego?</b>
<p>
<ul>
<li>test</li>
</ul>
</p>
</div>
</AnimatedModal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
import { version } from '../../../package.json';
import AnimatedModal from '../Global/AnimatedModal.vue';
export default defineComponent({
components: { AnimatedModal },
data() {
return {
mainStore: useMainStore(),
version: version
};
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
}
}
});
</script>
<style lang="scss" scoped>
.modal_content {
text-align: center;
padding: 1em;
height: 80vh;
min-height: 550px;
}
</style>
+2 -2
View File
@@ -12,7 +12,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../../store/mainStore';
import { useMainStore } from '../../store/mainStore';
export default defineComponent({
emits: ['toggleModal'],
@@ -23,7 +23,7 @@ export default defineComponent({
data() {
return {
store: useStore()
store: useMainStore()
};
},
+184 -112
View File
@@ -1,142 +1,157 @@
<template>
<div class="donation-modal" @keydown.esc="toggleModal(false)">
<button
class="modal_button action btn--image"
ref="btn"
@click="toggleModal(true)"
@focus="toggleModal(false)"
>
<img src="/images/icon-dollar.svg" alt="dollar donation icon" />
<span>{{ $t('donations.button-title') }}</span>
</button>
<AnimatedModal
class="donation-modal"
:isOpen="isModalOpen"
@toggleModal="toggleModal"
@keydown.esc="toggleModal(false)"
>
<div class="modal_content">
<div class="modal_main">
<h1 v-html="$t('donations.header')"></h1>
<div class="donators-slider" v-if="donatorList.length != 0">
<span v-html="$t('donations.donator-title', { count: donatorList.length })"></span>
<AnimatedModal :isOpen="isModalOpen" @toggleModal="toggleModal">
<div class="modal_content">
<div class="modal_main">
<h1 v-html="$t('donations.header')"></h1>
<br />
<p v-html="$t('donations.p1')"></p>
<br />
<i18n-t keypath="donations.p2" tag="p">
<template v-slot:b1>
<b>{{ $t('donations.p2-b1') }}</b>
</template>
<template v-slot:b2>
<b>{{ $t('donations.p2-b2') }}</b>
</template>
<template v-slot:b3>
<b>{{ $t('donations.p2-b3') }}</b>
</template>
<template v-slot:link>
<a href="https://discord.gg/x2mpNN3svk" target="_blank">
{{ $t('donations.p2-a1') }}
</a>
</template>
</i18n-t>
<br />
<p v-html="$t('donations.p3')"></p>
<br />
<i18n-t keypath="donations.p4" tag="p">
<template v-slot:img>
<transition mode="out-in" name="slider-anim" class="current-name">
<span :key="displayingName">
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
</template>
<template v-slot:b1>
<b>{{ $t('donations.p4-b1') }}</b>
</template>
<template v-slot:b2>
<b>{{ $t('donations.p4-b2') }}</b>
</template>
</i18n-t>
<br />
<i
v-html="$t('donations.p5')"
style="display: flex; justify-content: flex-end; text-align: right"
>
</i>
{{ displayingName }}
</span>
</transition>
</div>
<br />
<p v-html="$t('donations.p1')"></p>
<br />
<i18n-t keypath="donations.p2" tag="p">
<template v-slot:b1>
<b>{{ $t('donations.p2-b1') }}</b>
</template>
<template v-slot:b2>
<b>{{ $t('donations.p2-b2') }}</b>
</template>
<template v-slot:b3>
<b>{{ $t('donations.p2-b3') }}</b>
</template>
<template v-slot:link>
<a class="discord" href="https://discord.gg/x2mpNN3svk" target="_blank">
{{ $t('donations.p2-a1') }}
</a>
</template>
</i18n-t>
<br />
<p v-html="$t('donations.p3')"></p>
<br />
<i18n-t keypath="donations.p4" tag="p">
<template v-slot:img>
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
</template>
<div class="modal_actions">
<button class="modal_button exit btn--image" @click="toggleModal(false)">
<img src="/images/icon-exit.svg" alt="dollar donation icon" />
{{ $t('donations.action-exit') }}
</button>
<template v-slot:b1>
<b>{{ $t('donations.p4-b1') }}</b>
</template>
<form action="https://www.paypal.com/donate" method="post">
<input type="hidden" name="hosted_button_id" value="EDB3SKFAHXFTW" />
<button class="modal_button action btn--image" @click="toggleModal(false)">
<img src="/images/icon-dollar.svg" alt="dollar donation icon" />
{{ $t('donations.action-confirm') }}
</button>
</form>
</div>
<template v-slot:b2>
<b>{{ $t('donations.p4-b2') }}</b>
</template>
</i18n-t>
<br />
<i
v-html="$t('donations.p5')"
style="display: flex; justify-content: flex-end; text-align: right"
>
</i>
</div>
</AnimatedModal>
</div>
<div class="modal_actions">
<a
class="modal-action a-button btn--image coffee"
href="https://buycoffee.to/spythere"
target="_blank"
>
<img src="/images/icon-coffee.png" width="20" alt="buycoffee.to donation" />
{{ $t('donations.action-buycoffee') }}
</a>
<a
class="modal-action a-button btn--image paypal"
href="https://www.paypal.com/donate/?hosted_button_id=EDB3SKFAHXFTW"
target="_blank"
>
<img src="/images/icon-dollar.svg" alt="paypal donation" />
{{ $t('donations.action-paypal') }}
</a>
<button class="modal-action btn--image exit" @click="toggleModal(false)">
<img src="/images/icon-exit.svg" alt="dollar donation icon" />
{{ $t('donations.action-exit') }}
</button>
</div>
</div>
</AnimatedModal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import AnimatedModal from './AnimatedModal.vue';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
components: { AnimatedModal },
props: {
isModalOpen: Boolean
},
emits: ['toggleModal'],
watch: {
isModalOpen(b: boolean) {
this.running = b;
this.lastUpdate = Date.now();
}
},
created() {
this.runUpdate();
},
data() {
return {
apiStore: useApiStore(),
displayingIndex: 0,
lastUpdate: 0,
running: false
};
},
computed: {
displayingName() {
return this.donatorList[this.displayingIndex];
},
donatorList() {
return this.apiStore.donatorsData.slice().sort(() => Math.sign(Math.random() * -2 + 1));
}
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
},
runUpdate() {
if (Date.now() >= this.lastUpdate + 2000 && this.running) {
this.displayingIndex = (this.displayingIndex + 1) % this.donatorList.length;
this.lastUpdate = Date.now();
}
window.requestAnimationFrame(this.runUpdate);
}
},
components: { AnimatedModal }
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
.modal_button {
&.action {
$btnColor: #254069;
background-color: $btnColor;
&:hover {
background-color: lighten($btnColor, 10%);
}
}
&.exit {
$btnColor: crimson;
background-color: $btnColor;
&:hover {
background-color: lighten($btnColor, 10%);
}
}
@include smallScreen {
span {
display: none;
}
}
}
.modal-logo {
position: absolute;
top: 0;
left: 50%;
width: 6em;
transform: translate(-50%, -50%);
}
.modal_content {
display: grid;
grid-template-rows: 1fr auto;
@@ -157,13 +172,14 @@ export default defineComponent({
text-align: justify;
}
a {
a.discord {
text-decoration: underline;
}
}
.modal_main {
overflow: auto;
overflow-x: hidden;
img {
max-height: 20px;
@@ -174,11 +190,67 @@ export default defineComponent({
.modal_actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(13em, 1fr));
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.5em;
form button {
width: 100%;
}
}
.modal_actions > .modal-action {
&.paypal {
$btnColor: #254069;
background-color: $btnColor;
&:hover {
background-color: lighten($btnColor, 5%);
}
}
&.coffee {
$btnColor: #009255;
background-color: $btnColor;
&:hover {
background-color: lighten($btnColor, 5%);
}
}
&.exit {
$btnColor: #686868;
background-color: $btnColor;
&:hover {
background-color: lighten($btnColor, 5%);
}
}
}
.donators-slider {
text-align: center;
line-height: 30px;
.current-name {
backface-visibility: hidden;
display: block;
font-weight: bold;
word-wrap: break-word;
color: var(--clr-donator);
}
}
.slider-anim {
&-move,
&-enter-active,
&-leave-active {
transition: all 150ms ease-in-out;
}
&-enter-from,
&-leave-to {
opacity: 0;
}
}
</style>
+17 -4
View File
@@ -30,7 +30,7 @@
<script lang="ts">
import { defineComponent, Ref, ref } from 'vue';
import { regions as regionsJSON } from '../../data/options.json';
import { useStore } from '../../store/mainStore';
import { useMainStore } from '../../store/mainStore';
interface Item {
id: string;
@@ -41,7 +41,7 @@ interface Item {
export default defineComponent({
data() {
return {
store: useStore(),
store: useMainStore(),
selectedItemIndex: 0,
listOpen: false
};
@@ -60,6 +60,19 @@ export default defineComponent({
handler(regionId) {
this.selectedItemIndex = this.regionList.findIndex((reg) => reg.id == regionId);
}
},
'$route.query.region': {
immediate: true,
handler(regionQuery: string) {
if (regionQuery) {
this.store.region.id =
regionsJSON.find(
(reg) =>
reg.id == regionQuery.toLocaleLowerCase() ||
reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase()
)?.id || 'eu';
}
}
}
},
@@ -70,8 +83,8 @@ export default defineComponent({
regionList() {
return regionsJSON.map((region) => {
const regionStationCount = this.store.onlineSceneryList.filter(
(scenery) => scenery.region == region.id
const regionStationCount = this.store.activeSceneryList.filter(
(scenery) => scenery.region == region.id && scenery.dispatcherId != -1
).length;
const regionTrainCount =
+4 -1
View File
@@ -61,6 +61,9 @@ export default defineComponent({
case Status.ActiveDispatcher.UNKNOWN:
return 'unknown';
case Status.ActiveDispatcher.FREE:
return 'free';
default:
if (this.dispatcherTimestamp != null && this.dispatcherStatus >= Date.now() + 25500000)
return 'no-limit';
@@ -83,7 +86,7 @@ $online: #09a116;
$unknown: #b93c3c;
.status-badge {
border-radius: 1rem;
border-radius: 1em;
font-weight: 500;
padding: 0.2em 0.55em;
+74 -14
View File
@@ -1,7 +1,24 @@
<template>
<div class="stock-list">
<ul>
<li v-for="(stockName, i) in trainStockList" :key="i">
<div v-if="tractionOnly">
<p>
{{ computedStockList[0].split(':')[0].split('_').splice(0, 2).join(' ') }}
{{ computedStockList[0].split(':')[1] }}
</p>
<img
class="traction-only"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${computedStockList[0].split(':')[0]}${
/^EN/.test(computedStockList[0]) ? 'rb' : ''
}.png`"
@error="onImageError($event, computedStockList[0])"
width="300"
height="60"
/>
</div>
<ul v-else>
<li v-for="(stockName, i) in computedStockList" :key="i">
<p>
{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }}
{{ stockName.split(':')[1] }}
@@ -9,39 +26,55 @@
<span>
<img
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${
/^EN/.test(stockName) ? 'rb' : ''
}.png`"
@error="onImageError($event, stockName)"
@click.stop="() => {}"
width="400"
height="60"
/>
<!-- /// Manualne dodawanie miniaturek członów dla kibelków /// -->
<img
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
"
@click.stop="() => {}"
/>
<img
class="train-thumbnail"
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
v-if="/^EN71/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
"
@click.stop="() => {}"
/>
<img
class="train-thumbnail"
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}ra.png`"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-ra.png')
"
@click.stop="() => {}"
/>
<!-- /// -->
</span>
</li>
</ul>
@@ -50,31 +83,53 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { useStore } from '../../store/mainStore';
import { API } from '../../typings/api';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
props: {
trainStockList: {
type: Array as PropType<string[]>,
required: true
},
tractionOnly: {
type: Boolean,
required: false
}
},
data() {
return {
store: useStore()
apiStore: useApiStore()
};
},
computed: {
computedStockList() {
return this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList;
}
},
methods: {
onImageError(event: Event, stockName: string) {
const fallbackName =
Object.keys(this.store.rollingStockData!.info).find((type) => {
return this.store.rollingStockData!.info[type as keyof API.RollingStock.Info].find(
(v) => v[0] === stockName.split(':')[0]
);
}) || 'vehicle-unknown';
let fallbackName = '';
const isLoco = /.-\d{3}/.test(stockName);
if (isLoco) {
fallbackName += 'loco-';
fallbackName += /^\d?EN\d{2}/.test(stockName)
? 'ezt'
: /^SN\d{2}/.test(stockName)
? 'szt'
: /^\d?E/.test(stockName)
? 'e'
: 's';
} else {
const isCarPassenger = /(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(stockName);
fallbackName += 'car-';
fallbackName += isCarPassenger ? 'passenger' : 'cargo';
}
(event.target as HTMLImageElement).src = `/images/icon-${fallbackName}.png`;
}
@@ -99,6 +154,7 @@ export default defineComponent({
ul > li > span {
display: flex;
align-items: flex-end;
cursor: crosshair;
}
img {
@@ -107,10 +163,14 @@ img {
height: auto;
}
img.traction-only {
max-width: 100%;
}
p {
text-align: center;
color: #aaa;
font-size: 0.9em;
font-size: 0.95em;
margin-bottom: 1em;
}
</style>
-123
View File
@@ -1,123 +0,0 @@
<template>
<span class="stop-date">
<span
class="date arrival"
v-if="!stop.beginsHere"
:class="{
delayed: stop.arrivalDelay > 0 && (stop.confirmed || stop.stopped),
preponed: stop.arrivalDelay < 0 && (stop.confirmed || stop.stopped),
'on-time': stop.arrivalDelay == 0 && stop.confirmed
}"
>
<span v-if="stop.arrivalDelay != 0 && (stop.confirmed || stop.stopped)">
<s>{{ timestampToString(stop.arrivalTimestamp) }}</s>
{{ timestampToString(stop.arrivalRealTimestamp) }}
({{ stop.arrivalDelay > 0 ? '+' : '' }}{{ stop.arrivalDelay }})
</span>
<span v-else>
{{ timestampToString(stop.arrivalTimestamp) }}
</span>
</span>
<span
class="date stop"
v-if="stop.stopTime || stop.stopped"
:class="stop.stopType.replace(', ', '-')"
>
{{ stop.stopTime }} {{ stop.stopType == '' ? 'pt' : stop.stopType }}
</span>
<span
class="date departure"
v-if="!stop.terminatesHere && (stop.stopTime != 0 || stop.stopped)"
:class="{
delayed: stop.departureDelay > 0 && stop.confirmed,
preponed: stop.departureDelay < 0 && stop.confirmed
}"
>
<span v-if="stop.departureDelay != 0 && stop.confirmed">
<s>{{ timestampToString(stop.departureTimestamp) }}</s>
{{ timestampToString(stop.departureRealTimestamp) }}
({{ stop.departureDelay > 0 ? '+' : '' }}{{ stop.departureDelay }})
</span>
<span v-else>
{{ timestampToString(stop.departureTimestamp) }}
</span>
</span>
</span>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { TrainStop } from '../../store/typings';
export default defineComponent({
mixins: [dateMixin],
props: {
stop: {
type: Object as PropType<TrainStop>,
required: true
}
},
setup() {
return {};
}
});
</script>
<style lang="scss" scoped>
$preponedClr: lime;
$delayedClr: salmon;
$dateClr: #525151;
$stopExchangeClr: #db8e29;
$stopDefaultClr: #252525;
.stop-date {
display: flex;
align-items: center;
.date {
background: $dateClr;
padding: 0.3em 0.5em;
}
.stop {
&.ph,
&.ph-pm,
&.pm {
background: $stopExchangeClr;
}
background: $stopDefaultClr;
}
.arrival,
.departure {
&.delayed {
s {
color: #999;
}
span {
color: $delayedClr;
}
}
&.preponed {
s {
color: #999;
}
span {
color: $preponedClr;
}
}
}
}
</style>
-85
View File
@@ -1,85 +0,0 @@
<template>
<img class="train-thumbnail" :src="placeholderUrl" v-if="isNotFound" />
<img
class="train-thumbnail"
v-else
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${name.split(':')[0]}${
stockType == 'loco-ezt' ? 'rb' : ''
}.png`"
@error="onImageError"
@load="onImageLoad"
width="220"
height="60"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../../store/mainStore';
import { API } from '../../typings/api';
export default defineComponent({
props: {
name: {
type: String,
required: true
},
onlyFirstSegment: {
type: Boolean,
default: false
}
},
data() {
return {
store: useStore(),
isNotFound: false,
isLoaded: false
};
},
computed: {
url() {
return `https://rj.td2.info.pl/dist/img/thumbnails/${this.name.split(':')[0]}.png`;
},
placeholderUrl() {
return `/images/icon-${this.stockType}.png`;
},
stockType() {
if (!this.store.rollingStockData) return 'vehicle-unknown';
return (
Object.keys(this.store.rollingStockData.info).find((type) => {
return this.store.rollingStockData?.info[type as keyof API.RollingStock.Info].find(
(v) => v[0] === this.name.split(':')[0]
);
}) || 'vehicle-unknown'
);
}
},
methods: {
onImageError() {
this.isNotFound = true;
this.isLoaded = false;
},
onImageLoad() {
this.isNotFound = false;
this.isLoaded = true;
}
}
});
</script>
<style lang="scss" scoped>
.train-thumbnail {
width: auto;
height: auto;
max-height: 60px;
}
</style>
-241
View File
@@ -1,241 +0,0 @@
<template>
<section class="daily-stats">
<span :data-active="statsStatus">
<b v-if="statsStatus == Status.Data.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.maxTimetable">
&bull;
<i18n-t keypath="journal.timetable-stats-longest">
<template #id>
<router-link :to="`/journal/timetables?timetableId=${stats.maxTimetable.id}`">
<b>{{ stats.maxTimetable.id }}</b>
</router-link>
</template>
<template #author>
<router-link
:to="`/journal/dispatchers?dispatcherName=${stats.maxTimetable.authorName}`"
>
<b>{{ stats.maxTimetable.authorName }}</b>
</router-link>
</template>
<template #driver>
<b class="text--primary">{{ stats.maxTimetable.driverName }}</b>
</template>
<template #distance>
<b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
</template>
</i18n-t>
</div>
<div v-if="topDispatchers.length == 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-dr">
<template #dispatcher>
<router-link :to="`/journal/dispatchers?dispatcherName=${topDispatchers[0].name}`">
<b>{{ topDispatchers[0].name }}</b>
</router-link>
</template>
<template #count>
<b class="text--primary">
{{ topDispatchers[0].count }}
{{ $t('journal.timetable-count', topDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
<div v-if="topDispatchers.length > 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-dr-many">
<template #dispatchers>
<span v-for="(disp, i) in topDispatchers" :key="i">
<span v-if="i == topDispatchers.length - 1"> {{ $t('general.and') }} </span>
<router-link :to="`/journal/dispatchers?dispatcherName=${disp.name}`">
<b>{{ disp.name }}</b>
</router-link>
<span v-if="i < topDispatchers.length - 2">, </span>
</span>
</template>
<template #count>
<b class="text--primary">
{{ topDispatchers[0].count }}
{{ $t('journal.timetable-count', topDispatchers[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 { URLs } from '../../scripts/utils/apiURLs';
import { API } from '../../typings/api';
import { Status } from '../../typings/common';
export default defineComponent({
mixins: [dateMixin],
emits: ['toggleStatsOpen'],
data() {
return {
Status,
statsStatus: Status.Data.Loading,
intervalId: -1,
stats: {} as API.DailyStats.Response
};
},
activated() {
this.startFetchingDailyStats();
this.$emit('toggleStatsOpen', true);
},
deactivated() {
this.stopFetchingDailyStats();
},
computed: {
topDispatchers() {
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: API.DailyStats.Response = 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.stats = res;
this.statsStatus = Status.Data.Loaded;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
this.statsStatus = Status.Data.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 {
h3 {
text-align: center;
}
}
</style>
@@ -1,165 +0,0 @@
<template>
<div class="stats_container" v-click-outside="() => (cardVisible = false)">
<button class="stats_button" @click="toggleCard">
Statystyki dyżurnego {{ store.dispatcherStatsName }}
</button>
<div class="stats_card" v-if="store.dispatcherStatsName && cardVisible">
<div>
<Loading v-if="!store.dispatcherStatsData" />
<div class="loading" v-else-if="!store.dispatcherStatsData._count._all">
Ten dyżurny nie ma jeszcze szczegółowych statystyk!
</div>
<div v-else>
<h3>STATYSTYKI WYSTAWIONYCH ROZKŁADÓW</h3>
<div class="info-stats" v-if="store.dispatcherStatsData._count._all">
<span class="stat-badge">
<span>LICZBA</span>
<span>{{ store.dispatcherStatsData._count._all }}</span>
</span>
<span class="stat-badge">
<span>SUMA (KM)</span>
<span>{{ store.dispatcherStatsData._sum.routeDistance.toFixed(2) }}km</span>
</span>
<span class="stat-badge">
<span>NAJDŁUŻSZY</span>
<span>{{ store.dispatcherStatsData._max.routeDistance.toFixed(2) }}km</span>
</span>
<span class="stat-badge">
<span>ŚREDNIO</span>
<span>{{ store.dispatcherStatsData._avg.routeDistance.toFixed(2) }}km</span>
</span>
</div>
<h3>OSTATNIE WYSTAWIONE ROZKŁADY</h3>
<div class="last-timetables">
<div class="timetable-row" v-for="timetable in timetables" :key="timetable.id">
#{{ timetable.timetableId }} |
<b>{{ timetable.trainCategoryCode }} {{ timetable.trainNo }}</b> |
{{ timetable.driverName }} ({{ timetable.routeDistance }}km)
<div>{{ timetable.route.replace('|', ' > ') }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import axios from 'axios';
import { defineComponent } from 'vue';
import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue';
import { API } from '../../typings/api';
export default defineComponent({
components: { Loading },
setup() {
const store = useStore();
return {
store
};
},
data() {
return {
cardVisible: false,
lastDispatcherName: '',
timetables: [] as API.TimetableHistory.Response
};
},
methods: {
toggleCard() {
if (!this.store.dispatcherStatsName) return;
this.cardVisible = !this.cardVisible;
if (this.cardVisible) this.fetchDispatcherStats();
},
async fetchDispatcherStats() {
if (this.lastDispatcherName != this.store.dispatcherStatsName) {
this.store.dispatcherStatsData = undefined;
}
const statsData: API.DispatcherStats.Response = await (
await axios.get(
`${URLs.stacjownikAPI}/api/getDispatcherInfo?name=${this.store.dispatcherStatsName}`
)
).data;
const timetables: API.TimetableHistory.Response = await (
await axios.get(
`${URLs.stacjownikAPI}/api/getTimetables?authorName=${this.store.dispatcherStatsName}`
)
).data;
this.timetables = timetables;
this.store.dispatcherStatsData = statsData;
this.lastDispatcherName = this.store.dispatcherStatsName;
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/variables.scss';
.stats_container {
position: relative;
}
.stats_card {
position: absolute;
z-index: 999;
top: 120%;
right: 0;
width: 500px;
max-width: 97vw;
min-height: 100px;
overflow: auto;
border-radius: 1em 0 1em 1em;
background-color: #222222f1;
box-shadow: 0 3px 10px 5px #131313;
padding: 1em 0.5em;
}
.last-timetables {
max-height: 400px;
margin: 0.5em 0;
}
.timetable-row {
width: 95%;
margin: 0.5em auto;
padding: 0.5em;
background-color: #4d4d4d;
}
h2.card-title {
font-size: 1.8em;
}
h3 {
margin-top: 1em;
}
h2,
h3 {
text-align: center;
}
.last-timetables {
overflow-y: auto;
}
</style>
@@ -0,0 +1,271 @@
<template>
<section class="daily-stats">
<span :data-active="statsStatus">
<span class="stats-list">
<h3>
{{ $t('journal.daily-stats.title') }}
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
</h3>
<hr class="header-separator" />
<b v-if="statsStatus == Status.Data.Loading">
{{ $t('app.loading') }}
</b>
<b class="text--error" v-else-if="statsStatus == Status.Data.Error">
{{ $t('journal.stats-error') }}
</b>
<b v-else-if="topDispatchers.length == 0">
{{ $t('journal.daily-stats.info') }}
</b>
<div v-else>
<div v-if="stats.totalTimetables">
&bull;
<i18n-t keypath="journal.daily-stats.total">
<template #count>
<b class="text--primary">
{{ stats.totalTimetables }}
{{ $t('journal.daily-stats.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.maxTimetable">
&bull;
<i18n-t keypath="journal.daily-stats.longest">
<template #id>
<router-link :to="`/journal/timetables?search-train=%23${stats.maxTimetable.id}`">
<b>{{ stats.maxTimetable.id }}</b>
</router-link>
</template>
<template #author>
<router-link
:to="`/journal/timetables?search-dispatcher=${stats.maxTimetable.authorName}`"
>
<b>{{ stats.maxTimetable.authorName }}</b>
</router-link>
</template>
<template #driver>
<b class="text--primary">{{ stats.maxTimetable.driverName }}</b>
</template>
<template #distance>
<b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
</template>
</i18n-t>
</div>
<div v-if="topDispatchers.length == 1">
&bull;
<i18n-t keypath="journal.daily-stats.most-active-dr">
<template #dispatcher>
<router-link
:to="`/journal/dispatchers?search-dispatcher=${topDispatchers[0].name}`"
>
<b>{{ topDispatchers[0].name }}</b>
</router-link>
</template>
<template #count>
<b class="text--primary">
{{ topDispatchers[0].count }}
{{ $t('journal.daily-stats.count', topDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
<div v-if="topDispatchers.length > 1">
&bull;
<i18n-t keypath="journal.daily-stats.most-active-dr-many">
<template #dispatchers>
<span v-for="(disp, i) in topDispatchers" :key="i">
<span v-if="i == topDispatchers.length - 1"> {{ $t('general.and') }} </span>
<router-link :to="`/journal/dispatchers?search-dispatcher=${disp.name}`">
<b>{{ disp.name }}</b>
</router-link>
<span v-if="i < topDispatchers.length - 2">, </span>
</span>
</template>
<template #count>
<b class="text--primary">
{{ topDispatchers[0].count }}
{{ $t('journal.daily-stats.count', topDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
<div v-if="stats.longestDuties.length > 0">
&bull;
<i18n-t keypath="journal.daily-stats.longest-duties">
<template #dispatcher>
<router-link
:to="`/journal/dispatchers?search-dispatcher=${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.daily-stats.most-active-driver">
<template #driver>
<router-link
:to="`/journal/timetables?search-driver=${stats.mostActiveDrivers[0].name}`"
>
<b>{{ stats.mostActiveDrivers[0].name }}</b>
</router-link>
</template>
<template #distance>
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
</template>
</i18n-t>
</div>
<hr class="section-separator" />
<div class="stats-badges">
<span
class="stat-badge"
v-for="key in [
'rippedSwitches',
'derailments',
'skippedStopSignals',
'radioStops',
'kills'
]"
:key="key"
>
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
<span>{{
Object.entries(stats.globalDiff).find(([k, v]) => k == key)?.[1] || '--'
}}</span>
</span>
</div>
</div>
</span>
</span>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { API } from '../../typings/api';
import { Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
name: 'journal-daily-stats',
mixins: [dateMixin],
// emits: ['toggleStatsOpen'],
data() {
return {
Status,
statsStatus: Status.Data.Loading,
intervalId: -1,
stats: {} as API.DailyStats.Response,
apiStore: useApiStore()
};
},
activated() {
this.startFetchingDailyStats();
// this.$emit('toggleStatsOpen', true);
},
deactivated() {
this.stopFetchingDailyStats();
},
computed: {
topDispatchers() {
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: API.DailyStats.Response = await (
await this.apiStore.client!.get('api/getDailyStats')
).data;
this.stats = res;
this.statsStatus = Status.Data.Loaded;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
this.statsStatus = Status.Data.Error;
}
},
startFetchingDailyStats() {
this.fetchDailyTimetableStats();
if (this.intervalId != -1) return;
this.intervalId = window.setInterval(this.fetchDailyTimetableStats, 60000);
},
stopFetchingDailyStats() {
clearInterval(this.intervalId);
this.intervalId = -1;
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/JournalStats.scss';
@import '../../styles/badge.scss';
.daily-stats {
text-align: left;
}
.daily-stats > span[data-active='0'] {
opacity: 0.75;
}
.stats-list a {
text-decoration: underline;
}
.stats-badges {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
}
@include smallScreen {
h3 {
text-align: center;
}
}
</style>
@@ -0,0 +1,85 @@
<template>
<div class="journal-stats dispatcher" v-if="dispatcherName && stats">
<span class="loading" v-if="!stats.issuedTimetables && !stats.services">
{{ $t('journal.dispatcher-stats.empty') }}
</span>
<span v-else>
<h3>
<i18n-t keypath="journal.dispatcher-stats.title">
<template #name>
<span class="text--primary">{{ dispatcherName.toUpperCase() }}</span>
</template>
</i18n-t>
</h3>
<hr class="header-separator" />
<div class="info-stats">
<span class="stat-badge" v-if="stats.services">
<span>{{ $t('journal.dispatcher-stats.services-count') }}</span>
<span>{{ stats.services.count }}</span>
</span>
<span class="stat-badge" v-if="stats.services">
<span>{{ $t('journal.dispatcher-stats.service-max') }}</span>
<span>{{ calculateDuration(stats.services.durationMax) }}</span>
</span>
<span class="stat-badge" v-if="stats.services">
<span>{{ $t('journal.dispatcher-stats.service-avg') }}</span>
<span>{{ calculateDuration(stats.services.durationAvg) }}</span>
</span>
</div>
<hr class="section-separator" />
<div class="info-stats">
<span class="stat-badge" v-if="stats.issuedTimetables">
<span>{{ $t('journal.dispatcher-stats.timetables-count') }}</span>
<span>{{ stats.issuedTimetables.count }}</span>
</span>
<span class="stat-badge" v-if="stats.issuedTimetables">
<span>{{ $t('journal.dispatcher-stats.timetables-sum') }}</span>
<span>{{ stats.issuedTimetables.distanceSum.toFixed(2) }}km</span>
</span>
<span class="stat-badge" v-if="stats.issuedTimetables">
<span>{{ $t('journal.dispatcher-stats.timetables-max') }}</span>
<span>{{ stats.issuedTimetables.distanceMax.toFixed(2) }}km</span>
</span>
<span class="stat-badge" v-if="stats.issuedTimetables">
<span>{{ $t('journal.dispatcher-stats.timetables-avg') }}</span>
<span>{{ stats.issuedTimetables.distanceAvg.toFixed(2) }}km</span>
</span>
</div>
</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import dateMixin from '../../../mixins/dateMixin';
import { useMainStore } from '../../../store/mainStore';
export default defineComponent({
name: 'journal-dispatcher-stats',
mixins: [dateMixin],
setup() {
const store = useMainStore();
return {
stats: store.dispatcherStatsData,
dispatcherName: store.dispatcherStatsName
};
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/JournalStats.scss';
</style>
@@ -0,0 +1,256 @@
<template>
<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 == Status.Data.Loading" />
<div v-else-if="dataStatus == Status.Data.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="dispatchers-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?search-station=${historyItem.stationName}`"
>
<b>{{ historyItem.stationName }}</b>
</router-link>
</td>
<td>#{{ historyItem.stationHash }}</td>
<td>
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
<b
v-if="isDonator(historyItem.dispatcherName)"
class="text--donator"
:title="$t('donations.dispatcher-message')"
>
{{ historyItem.dispatcherName }}
</b>
<b v-else>
{{ 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>
<AddDataButton
:list="dispatcherHistory"
:scrollDataLoaded="scrollDataLoaded"
:scrollNoMoreData="scrollNoMoreData"
@addHistoryData="addHistoryData"
/>
</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>
</transition>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { regions } from '../../../data/options.json';
import { useMainStore } from '../../../store/mainStore';
import { API } from '../../../typings/api';
import { Status } from '../../../typings/common';
import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue';
import dateMixin from '../../../mixins/dateMixin';
import donatorMixin from '../../../mixins/donatorMixin';
import styleMixin from '../../../mixins/styleMixin';
export default defineComponent({
components: { Loading, AddDataButton },
mixins: [dateMixin, styleMixin, donatorMixin],
props: {
dispatcherHistory: {
type: Array as PropType<API.DispatcherHistory.Response>,
required: true
},
scrollNoMoreData: {
type: Boolean
},
scrollDataLoaded: {
type: Boolean
},
addHistoryData: {
type: Function as PropType<() => void>
},
dataStatus: {
type: Number as PropType<Status.Data>
}
},
data() {
return {
Status,
store: useMainStore(),
regions
};
},
computed: {
computedDispatcherHistory() {
return this.dispatcherHistory.reduce(
(acc, historyItem, i) => {
if (this.isAnotherDay(i - 1, i))
acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
acc.push(historyItem);
return acc;
},
[] as (API.DispatcherHistory.Data | 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.dispatchers-table {
--_bg-table: #111;
--_bg-head: #101010;
--_bg-row: #2f2f2f;
width: 100%;
border-collapse: collapse;
position: relative;
text-align: center;
margin-bottom: 1em;
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;
}
}
}
.text {
&--online {
color: springgreen;
}
&--offline {
color: #ddd;
}
}
</style>
@@ -1,249 +0,0 @@
<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 == Status.Data.Loading" />
<div v-else-if="dataStatus == Status.Data.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>
<AddDataButton
:list="dispatcherHistory"
:scrollDataLoaded="scrollDataLoaded"
:scrollNoMoreData="scrollNoMoreData"
@addHistoryData="addHistoryData"
/>
</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 styleMixin from '../../mixins/styleMixin';
import { useStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue';
import { regions } from '../../data/options.json';
import AddDataButton from '../Global/AddDataButton.vue';
import { API } from '../../typings/api';
import { Status } from '../../typings/common';
export default defineComponent({
components: { Loading, AddDataButton },
mixins: [dateMixin, styleMixin],
props: {
dispatcherHistory: {
type: Array as PropType<API.DispatcherHistory.Response>,
required: true
},
scrollNoMoreData: {
type: Boolean
},
scrollDataLoaded: {
type: Boolean
},
addHistoryData: {
type: Function as PropType<() => void>
},
dataStatus: {
type: Number as PropType<Status.Data>
}
},
data() {
return {
Status,
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 (API.DispatcherHistory.Data | 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;
margin-bottom: 1em;
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;
}
}
}
.text {
&--online {
color: springgreen;
}
&--offline {
color: #ddd;
}
}
</style>
+48 -46
View File
@@ -1,6 +1,6 @@
<template>
<div class="filters-options" @keydown.esc="showOptions = false">
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<div class="filters-options dropdown" @keydown.esc="showOptions = false">
<div class="dropdown_background" v-if="showOptions" @click="showOptions = false"></div>
<div class="actions-bar">
<button
@@ -27,13 +27,13 @@
<option v-for="(sugg, i) in dispatcherSuggestions" :key="i" :value="sugg"></option>
</datalist>
<transition name="options-anim">
<div class="options_wrapper" v-if="showOptions">
<transition name="dropdown-anim">
<div class="dropdown_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">{{
<label v-if="propName == 'search-date'" for="search-date">{{
$t(`options.search-${optionsType}-date`)
}}</label>
@@ -41,12 +41,13 @@
<input
class="search-input"
v-model="searchersValues[propName]"
@keydown.enter="onSearchConfirm"
@keydown.enter="searchConfirm"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
:placeholder="$t(`options.${propName}`)"
:type="propName == 'search-date' ? 'date' : 'text'"
:min="propName == 'search-date' ? '2022-02-01' : undefined"
:id="`${propName}`"
:list="propName.toString()"
/>
@@ -110,14 +111,12 @@
</template>
<script lang="ts">
import axios from 'axios';
import { defineComponent, inject, PropType } from 'vue';
import keyMixin from '../../mixins/keyMixin';
import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/mainStore';
import { useMainStore } from '../../store/mainStore';
import { Journal } from './typings';
import { API } from '../../typings/api';
import { Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'],
@@ -158,7 +157,8 @@ export default defineComponent({
dispatcherSuggestions: [] as string[],
searchTimeout: 0,
store: useStore(),
store: useMainStore(),
apiStore: useApiStore(),
JournalFilterSection: Journal.FilterSection
};
@@ -182,12 +182,6 @@ export default defineComponent({
},
watch: {
async 'store.driverStatsName'() {
await this.fetchDriverStats();
// if (value) this.store.currentStatsTab = 'driver';
},
async 'searchersValues.search-driver'(value: string | undefined) {
clearTimeout(this.searchTimeout);
@@ -206,29 +200,34 @@ export default defineComponent({
},
methods: {
async fetchDriverStats() {
this.store.driverStatsData = undefined;
// filters & sorters from URL params
handleRouteParams() {
this.$router.push({
query: {
...this.$route.query,
'sorter-active':
this.sorterOptionIds.indexOf(`${this.sorterActive.id}`) != 0
? this.sorterActive.id
: undefined,
...Object.keys(this.searchersValues).reduce(
(acc, k) => {
const searchVal = this.searchersValues[k as Journal.TimetableSearchKey];
if (!this.store.driverStatsName) {
this.store.driverStatsStatus = Status.Data.Initialized;
return;
}
acc[k] = searchVal || undefined;
try {
this.store.driverStatsStatus = Status.Data.Loading;
const statsData: API.DriverStats.Response = await (
await axios.get(
`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`
return acc;
},
{} as { [k: string]: string | undefined }
),
...this.filterList?.reduce(
(acc, f) => {
if (f.isActive) acc[f.filterSection] = f.default ? undefined : f.id;
return acc;
},
{} as { [k: string]: string | undefined }
)
).data;
this.store.driverStatsData = statsData;
this.store.driverStatsStatus = Status.Data.Loaded;
} catch (error) {
this.store.driverStatsStatus = Status.Data.Error;
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/');
}
}
});
},
refreshData() {
@@ -240,17 +239,17 @@ export default defineComponent({
window.clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(async () => {
this.searchTimeout = window.setTimeout(async () => {
try {
const suggestions: string[] = await (
await axios.get(`${URLs.stacjownikAPI}/api/get${type}Suggestions?name=${value}`)
await this.apiStore.client!.get(`api/get${type}Suggestions?name=${value}`)
).data;
this[`${type}Suggestions`] = suggestions;
} catch (error) {
this[`${type}Suggestions`] = [];
}
}, 450);
}, 250);
},
// Override keyMixin function
@@ -265,7 +264,7 @@ export default defineComponent({
onSorterChange(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id;
this.sorterActive.dir = -1;
this.$emit('onSearchConfirm');
this.searchConfirm();
},
onFilterChange(filter: Journal.TimetableFilter) {
@@ -275,30 +274,33 @@ export default defineComponent({
.forEach((f) => (f.isActive = false));
filter.isActive = true;
this.$emit('onSearchConfirm');
this.searchConfirm();
},
onInputClear(id: any) {
this.searchersValues[id] = '';
this.$emit('onSearchConfirm');
this.searchConfirm();
},
onSearchConfirm() {
searchConfirm() {
this.$emit('onSearchConfirm');
this.handleRouteParams();
},
onSearchButtonConfirm() {
this.showOptions = false;
this.$emit('onSearchConfirm');
this.searchConfirm();
},
onResetButtonClick() {
this.$emit('onOptionsReset');
this.handleRouteParams();
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/filters_options.scss';
@import '../../styles/dropdown.scss';
@import '../../styles/dropdown_filters.scss';
</style>
+62 -99
View File
@@ -1,122 +1,85 @@
<template>
<div class="journal-stats" v-if="!store.isOffline">
<div class="tabs">
<div
class="journal-stats dropdown"
v-if="!mainStore.isOffline"
@keydown.esc="currentStatsTab = null"
>
<div
class="dropdown_background"
v-if="currentStatsTab !== null"
@click="currentStatsTab = null"
></div>
<div class="actions-bar">
<button
v-for="tab in data.tabs"
:key="tab.name"
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)"
v-for="button in statsButtons"
:key="button.tab"
class="btn--filled btn--image"
:data-selected="button.tab == currentStatsTab"
:data-disabled="button.disabled"
:disabled="button.disabled"
@click="onTabButtonClick(button.tab)"
>
{{ $t(tab.titlePath) }}
<img
v-if="button.iconName"
:src="`/images/icon-${button.iconName}.svg`"
:alt="button.iconName"
/>
{{ $t(button.localeKey) }}
</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>
<transition name="dropdown-anim">
<div class="dropdown_wrapper" v-if="currentStatsTab !== null">
<keep-alive>
<component :is="currentStatsTab" :key="currentStatsTab"></component>
</keep-alive>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, Ref, ref, watch } from 'vue';
import { useStore } from '../../store/mainStore';
import JournalDailyStats from './DailyStats.vue';
import JournalDriverStats from './JournalDriverStats.vue';
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { useMainStore } from '../../store/mainStore';
import StorageManager from '../../managers/storageManager';
import { Journal } from './typings';
import JournalDailyStats from './JournalDailyStats.vue';
import JournalDispatcherStats from '../JournalView/JournalDispatchers/JournalDispatcherStats.vue';
import JournalDriverStats from '../JournalView/JournalTimetables/JournalDriverStats.vue';
// 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,
export default defineComponent({
components: { JournalDailyStats, JournalDriverStats, JournalDispatcherStats },
props: {
statsButtons: {
type: Array as PropType<Journal.StatsButton[]>,
required: true
}
] as { name: TStatTab; titlePath: string; inactive?: boolean }[]
});
},
data() {
return {
Journal,
mainStore: useMainStore(),
currentStatsTab: null as Journal.StatsTab | null
};
},
// Methods
function onTabButtonClick(tab: TStatTab) {
if (lastClickedTab.value == tab || !lastClickedTab.value || !areStatsOpen.value)
areStatsOpen.value = !areStatsOpen.value;
methods: {
onTabButtonClick(tab: Journal.StatsTab) {
this.currentStatsTab = tab == this.currentStatsTab ? null : tab;
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';
StorageManager.setStringValue('journalStatsTab', this.currentStatsTab ?? '');
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/JournalStats.scss';
@import '../../styles/dropdown.scss';
@import '../../styles/dropdown_filters.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;
}
}
.dropdown_wrapper {
max-width: 100%;
}
</style>
@@ -1,14 +1,19 @@
<template>
<div class="journal-stats">
<span v-if="store.driverStatsData">
<div class="journal-stats driver" v-if="store.driverStatsData">
<span>
<h3>
{{ $t('journal.stats-title') }}
<span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
<i18n-t keypath="journal.driver-stats.title">
<template #name>
<span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
</template>
</i18n-t>
</h3>
<hr class="header-separator" />
<div class="info-stats">
<span class="stat-badge">
<span>{{ $t('journal.stats-timetables') }}</span>
<span>{{ $t('journal.driver-stats.timetables') }}</span>
<span
>{{ store.driverStatsData._count.fulfilled }} /
{{ store.driverStatsData._count._all }}</span
@@ -16,17 +21,17 @@
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-longest-timetable') }}</span>
<span>{{ $t('journal.driver-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>{{ $t('journal.driver-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>{{ $t('journal.driver-stats.distance') }}</span>
<span>
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
@@ -34,7 +39,7 @@
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-stations') }}</span>
<span>{{ $t('journal.driver-stats.stations') }}</span>
<span>
{{ store.driverStatsData._sum.confirmedStopsCount }} /
{{ store.driverStatsData._sum.allStopsCount }}
@@ -42,26 +47,20 @@
</span>
</div>
</span>
<b v-else-if="store.driverStatsStatus == Status.Data.Loading">{{
$t('journal.stats-loading')
}}</b>
<b v-else-if="store.driverStatsStatus == Status.Data.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 { useStore } from '../../store/mainStore';
import { Status } from '../../typings/common';
import { useMainStore } from '../../../store/mainStore';
import { Status } from '../../../typings/common';
export default defineComponent({
name: 'journal-driver-stats',
data() {
return {
store: useStore(),
store: useMainStore(),
Status: Status
};
}
@@ -69,5 +68,5 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@import '../../styles/JournalStats.scss';
@import '../../../styles/JournalStats.scss';
</style>
@@ -42,7 +42,7 @@ import { defineComponent, PropType } from 'vue';
import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue';
import TimetableHistoryList from './TimetableHistoryList.vue';
import { useStore } from '../../../store/mainStore';
import { useMainStore } from '../../../store/mainStore';
import { Status } from '../../../typings/common';
import { API } from '../../../typings/api';
@@ -71,7 +71,7 @@ export default defineComponent({
data() {
return {
Status,
store: useStore()
store: useMainStore()
};
}
});
@@ -53,7 +53,6 @@
</button>
</div>
<!-- <StockList :trainStockList="currentHistoryIndex == 0 ? timetable.stockString : stockHistory[currentHistoryIndex].stockString).split(';')" /> -->
<StockList
:trainStockList="
(currentHistoryIndex == 0
@@ -63,15 +62,6 @@
"
/>
<!-- <ul class="stock-list">
<li
v-for="(stockName, i) in (currentHistoryIndex == 0 ? timetable.stockString : stockHistory[currentHistoryIndex].stockString).split(';')"
:key="i"
>
<div>{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }} {{ stockName.split(':')[1] }}</div>
<TrainThumbnail :name="stockName" />
</li>
</ul> -->
</div>
</template>
@@ -1,11 +1,6 @@
<template>
<div class="item-general">
<span
class="general-train"
tabindex="0"
@click.stop="showTimetable(timetable, $event.currentTarget)"
@keydown.enter="showTimetable(timetable, $event.currentTarget)"
>
<span class="general-train">
<span class="text--grayed">#{{ timetable.id }}</span>
<span class="badges" v-if="timetable.skr || timetable.twr">
@@ -28,7 +23,26 @@
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
</strong>
<strong>{{ timetable.driverName }}</strong>
<strong
v-if="isDonator(timetable.driverName)"
class="text--donator"
:title="$t('donations.driver-message')"
>
{{ timetable.driverName }}
</strong>
<strong v-else>
{{ timetable.driverName }}
</strong>
<button
v-if="timetable.terminated == false"
class="btn--image btn--action btn-timetable"
@click.stop="showTimetable(timetable, $event.currentTarget)"
>
<img src="/images/icon-train.svg" alt="" />
{{ $t('journal.timetable-online-button') }}
</button>
</span>
<span class="general-time">
@@ -63,13 +77,14 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { API } from '../../../typings/api';
import dateMixin from '../../../mixins/dateMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
import styleMixin from '../../../mixins/styleMixin';
import { API } from '../../../typings/api';
import donatorMixin from '../../../mixins/donatorMixin';
export default defineComponent({
mixins: [dateMixin, modalTrainMixin, styleMixin],
mixins: [dateMixin, modalTrainMixin, styleMixin, donatorMixin],
props: {
timetable: {
@@ -100,16 +115,17 @@ export default defineComponent({
gap: 0.5em;
margin-bottom: 0.5em;
@include smallScreen() {
justify-content: center;
}
}
.info-date {
margin-right: 0.5em;
}
.badges {
display: flex;
gap: 0.25em;
}
.info-badge {
padding: 0.05em 0.35em;
color: black;
@@ -131,7 +147,24 @@ export default defineComponent({
cursor: pointer;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 0.25em;
}
.btn-timetable {
display: inline-block;
padding: 0.1em 0.4em;
margin-left: 0.5em;
img {
vertical-align: top;
}
}
@include smallScreen {
.item-general {
justify-content: center;
}
}
</style>
@@ -5,7 +5,7 @@
v-for="{ timetable, showExtraInfo } in computedTimetableHistory"
class="journal_item"
:key="timetable.id"
@click="showExtraInfo.value = !showExtraInfo.value"
@click.stop.prevent="showExtraInfo.value = !showExtraInfo.value"
>
<div class="journal_item-info">
<!-- General -->
@@ -46,6 +46,8 @@ import TimetableExtra from './TimetableExtra.vue';
import { API } from '../../../typings/api';
export default defineComponent({
components: { TimetableGeneral, TimetableStops, TimetableStatus, TimetableExtra },
props: {
timetableHistory: {
type: Array as PropType<API.TimetableHistory.Response>,
@@ -59,9 +61,7 @@ export default defineComponent({
showExtraInfo: ref(false)
}));
}
},
methods: {},
components: { TimetableGeneral, TimetableStops, TimetableStatus, TimetableExtra }
}
});
</script>
+32 -14
View File
@@ -1,12 +1,5 @@
export namespace Journal {
export type DispatcherSearcher = {
[key in 'search-dispatcher' | 'search-station' | 'search-date']: string;
};
export interface DispatcherSorter {
id: 'timestampFrom' | 'duration';
dir: -1 | 1;
}
export type DispatcherSearchKey = 'search-dispatcher' | 'search-station' | 'search-date';
export type TimetableSearchKey =
| 'search-driver'
@@ -19,11 +12,29 @@ export namespace Journal {
[key in TimetableSearchKey]: string;
};
export type DispatcherSearchType = {
[key in DispatcherSearchKey]: string;
};
export type TimetableSorterKey = 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
export type DispatcherSorterKey = 'timestampFrom' | 'duration';
export interface DispatcherSorter {
id: DispatcherSorterKey;
dir: -1 | 1;
}
export interface TimetableSorter {
id: TimetableSorterKey;
dir: 'asc' | 'desc';
}
export const enum TimetableFilterId {
ALL_STATUSES = 'all-statuses',
ACTIVE = 'active',
FULFILLED = 'fulfilled',
ABANDONED = 'abandoned',
ALL = 'all',
ALL_SPECIALS = 'all-specials',
TWR = 'twr',
SKR = 'skr',
TWR_SKR = 'twr-skr'
@@ -31,19 +42,26 @@ export namespace Journal {
export enum FilterSection {
TIMETABLE_STATUS = 'timetable-status',
TWRSKR = 'twrskr'
SPECIAL = 'special'
}
export interface TimetableFilter {
id: TimetableFilterId;
filterSection: string;
isActive: boolean;
default: boolean;
}
export type TimetableSorterKey = 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
export enum StatsTab {
DRIVER_STATS = 'journal-driver-stats',
DISPATCHER_STATS = 'journal-dispatcher-stats',
DAILY_STATS = 'journal-daily-stats'
}
export interface TimetableSorter {
id: TimetableSorterKey;
dir: 'asc' | 'desc';
export interface StatsButton {
tab: StatsTab;
localeKey: string;
iconName: string;
disabled: boolean;
}
}
+39
View File
@@ -0,0 +1,39 @@
<template>
<div class="popup-content">
<img src="/images/icon-diamond.svg" alt="" />
<span>{{ popupStore.currentPopupContent }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { usePopupStore } from '../../store/popupStore';
export default defineComponent({
data() {
return {
popupStore: usePopupStore()
};
}
});
</script>
<style lang="scss" scoped>
.popup-content {
gap: 0.5em;
padding: 0.5em;
border-radius: 0.25em;
width: 100%;
background-color: #333;
box-shadow: 0 0 10px 2px #aaa;
}
img {
vertical-align: middle;
height: 1em;
margin-right: 0.5em;
}
</style>
+55
View File
@@ -0,0 +1,55 @@
<template>
<div class="popup" v-show="popupStore.currentPopupComponent" ref="preview">
<component v-if="popupStore.currentPopupComponent" :is="popupStore.currentPopupComponent" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import DonatorPopUp from './DonatorPopUp.vue';
import TrainCommentsPopUp from './TrainCommentsPopUp.vue';
import VehiclePreviewPopUp from './VehiclePreviewPopUp.vue';
import { usePopupStore } from '../../store/popupStore';
export default defineComponent({
components: { DonatorPopUp, TrainCommentsPopUp, VehiclePreviewPopUp },
data() {
return {
popupStore: usePopupStore()
};
},
watch: {
'popupStore.popupPosition': {
deep: true,
handler(val: typeof this.popupStore.popupPosition) {
const previewEl = this.$refs['preview'] as HTMLElement;
previewEl.style.top = `${val.y}px`;
previewEl.style.left = `${val.x}px`;
previewEl.style.transform = 'translateY(1.5rem)';
this.$nextTick(() => {
const isOutside =
val.y + previewEl.getBoundingClientRect().height > window.innerHeight + window.scrollY;
// previewEl.style.transform = `translate(-${~~((val.x / window.innerWidth) * 100)}%, calc(${isOutside ? '-100% - 1.5rem' : '1.5rem'}))`;
previewEl.style.transform = `translate(-${~~((val.x / window.innerWidth) * 100)}%, calc(${
isOutside ? '-100% - 1.5rem' : '1.5rem'
}))`;
});
}
}
}
});
</script>
<style lang="scss" scoped>
.popup {
position: absolute;
z-index: 250;
max-width: 400px;
text-align: center;
}
</style>
@@ -0,0 +1,38 @@
<template>
<div class="popup-content">
<span>{{ popupStore.currentPopupContent }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { usePopupStore } from '../../store/popupStore';
export default defineComponent({
data() {
return {
popupStore: usePopupStore()
};
}
});
</script>
<style lang="scss" scoped>
.popup-content {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5em;
padding: 0.25em 0.5em;
border-radius: 0.25em;
width: 100%;
background-color: #333;
box-shadow: 0 0 5px 2px #aaa;
}
img {
height: 1em;
}
</style>
@@ -0,0 +1,86 @@
<template>
<div class="popup-content">
<div v-if="imageState == 'loading'" class="loading-info">
{{ $t('vehicle-preview.loading') }}
</div>
<div v-if="imageState == 'error'">{{ $t('vehicle-preview.error') }}</div>
<img
v-if="popupStore.currentPopupContent"
@load="onImageLoad"
@error="onImageError"
@click="popupStore.onPopUpHide"
width="300"
height="176"
class="rounded-md w-full h-auto"
:src="`https://spythere.github.io/api/td2/images/${popupStore.currentPopupContent}--300px.jpg`"
/>
<div class="vehicle-name" v-if="imageState != 'error'">
{{ popupStore.currentPopupContent.replace(/_/g, ' ') }}
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { usePopupStore } from '../../store/popupStore';
export default defineComponent({
data() {
return {
popupStore: usePopupStore(),
imageState: 'loading'
};
},
mounted() {
this.imageState = 'loading';
},
methods: {
onImageLoad() {
this.imageState = 'loaded';
},
onImageError(e: Event) {
this.imageState = 'error';
(e.target as HTMLElement).style.display = 'none';
}
}
});
</script>
<style lang="scss" scoped>
.popup-content {
// min-w-[300px] min-h-[200px] p-2 bg-slate-800 rounded-md
width: 300px;
min-height: 200px;
background-color: #333;
box-shadow: 0 0 10px 2px #aaa;
padding: 0.5em;
border-radius: 0.5em;
}
.loading-info {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
img {
width: 100%;
height: auto;
}
.vehicle-name {
text-align: center;
margin-top: 0.5em;
color: #ccc;
text-wrap: wrap;
}
</style>
@@ -19,7 +19,9 @@
<tr v-for="historyItem in historyList" :key="historyItem.id">
<td>#{{ historyItem.stationHash }}</td>
<td>
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
<b>{{ historyItem.dispatcherName }}</b>
</router-link>
</td>
@@ -33,6 +35,8 @@
>
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b>
<b v-else>?</b>
</td>
<td class="text--primary">
<b>{{ historyItem.dispatcherRate }}</b>
@@ -66,17 +70,16 @@
</template>
<script lang="ts">
import axios from 'axios';
import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import Station from '../../scripts/interfaces/Station';
import { URLs } from '../../scripts/utils/apiURLs';
import Loading from '../Global/Loading.vue';
import styleMixin from '../../mixins/styleMixin';
import listObserverMixin from '../../mixins/listObserverMixin';
import { OnlineScenery } from '../../store/typings';
import { ActiveScenery } from '../../store/typings';
import { API } from '../../typings/api';
import { Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
name: 'SceneryDispatchersHistory',
@@ -84,12 +87,10 @@ export default defineComponent({
components: { Loading },
props: {
station: {
type: Object as PropType<Station>,
required: true
type: Object as PropType<Station>
},
onlineScenery: {
type: Object as PropType<OnlineScenery>,
required: false
type: Object as PropType<ActiveScenery>
}
},
@@ -97,7 +98,8 @@ export default defineComponent({
return {
historyList: [] as API.DispatcherHistory.Response,
dataStatus: Status.Data.Loading,
DataStatus: Status.Data
DataStatus: Status.Data,
apiStore: useApiStore()
};
},
@@ -113,12 +115,20 @@ export default defineComponent({
countFrom = 0,
countLimit = 30
): Promise<API.DispatcherHistory.Response | null> {
if (!this.station && !this.onlineScenery) {
this.dataStatus = Status.Data.Loaded;
return null;
}
try {
this.dataStatus = Status.Data.Loading;
const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
const requestString = `api/getDispatchers?stationName=${
this.station?.name || this.onlineScenery?.name
}&countFrom=${countFrom}&countLimit=${countLimit}`;
const historyAPIData: API.DispatcherHistory.Response = await (
await axios.get(requestString)
await this.apiStore.client!.get(requestString)
).data;
this.dataStatus = Status.Data.Loaded;
@@ -130,7 +140,9 @@ export default defineComponent({
}
},
navigateToHistory() {
this.$router.push(`/journal/dispatchers?sceneryName=${this.station.name}`);
this.$router.push(
`/journal/dispatchers?search-station=${this.station?.name || this.onlineScenery?.name}`
);
}
}
});
+11 -9
View File
@@ -1,11 +1,11 @@
<template>
<section class="info-header">
<a class="scenery-name" :href="station.generalInfo?.url" target="_blank">
{{ station.name }}
<a class="scenery-name" :href="station?.generalInfo?.url" target="_blank">
{{ stationName.replace(/_/g, ' ') }}
</a>
<div class="scenery-abbrev">
{{ $t('scenery.abbrev') }} <b>{{ station.generalInfo?.abbr }}</b>
<div class="scenery-abbrev" v-if="station?.generalInfo?.abbr">
{{ $t('scenery.abbrev') }} <b>{{ station.generalInfo.abbr }}</b>
</div>
<div class="scenery-hash" v-if="onlineScenery?.hash">#{{ onlineScenery.hash }}</div>
@@ -15,18 +15,21 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import Station from '../../scripts/interfaces/Station';
import { OnlineScenery } from '../../store/typings';
import { ActiveScenery } from '../../store/typings';
export default defineComponent({
props: {
station: {
type: Object as PropType<Station>,
type: Object as PropType<Station>
},
stationName: {
type: String,
required: true
},
onlineScenery: {
type: Object as PropType<OnlineScenery>,
required: false
type: Object as PropType<ActiveScenery>
}
}
});
@@ -58,4 +61,3 @@ export default defineComponent({
font-size: 1.2em;
}
</style>
../../store/storeTypes
+7 -9
View File
@@ -1,10 +1,10 @@
<template>
<div class="scenery-info">
<section>
<div class="scenery-info-general" v-if="station.generalInfo">
<div class="scenery-info-general">
<SceneryInfoIcons :station="station" />
<div class="scenery-general-list">
<div class="scenery-general-list" v-if="station?.generalInfo">
<span>
<b>{{ $t('availability.title') }}:</b>
{{ $t(`availability.${station.generalInfo.availability}`) }}
@@ -46,11 +46,11 @@
</span>
</div>
<SceneryInfoRoutes :station="station" />
<SceneryInfoRoutes v-if="station" :station="station" />
<div
class="scenery-authors"
v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0"
v-if="station?.generalInfo?.authors && station.generalInfo.authors.length > 0"
>
<b>
{{
@@ -90,7 +90,7 @@ import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue';
import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
import Station from '../../scripts/interfaces/Station';
import { OnlineScenery } from '../../store/typings';
import { ActiveScenery } from '../../store/typings';
export default defineComponent({
components: {
@@ -102,13 +102,11 @@ export default defineComponent({
},
props: {
station: {
type: Object as PropType<Station>,
required: true
type: Object as PropType<Station>
},
onlineScenery: {
type: Object as PropType<OnlineScenery>,
required: false
type: Object as PropType<ActiveScenery>
}
}
});
@@ -1,31 +1,45 @@
<template>
<section class="info-dispatcher">
<div class="dispatcher" v-if="onlineScenery">
<div class="info-top" v-if="onlineScenery && onlineScenery.dispatcherExp != -1">
<span
class="dispatcher_level"
class="dispatcher-level"
:style="calculateExpStyle(onlineScenery.dispatcherExp, onlineScenery.dispatcherIsSupporter)"
>
{{ onlineScenery.dispatcherExp > 1 ? onlineScenery.dispatcherExp : 'L' }}
</span>
<router-link
class="dispatcher_name"
:to="`/journal/dispatchers?dispatcherName=${onlineScenery.dispatcherName}`"
class="dispatcher-name"
:to="`/journal/dispatchers?search-dispatcher=${onlineScenery.dispatcherName}`"
>
{{ onlineScenery.dispatcherName }}
<span
class="text--donator"
v-if="isDonator(onlineScenery.dispatcherName)"
:title="$t('donations.dispatcher-message')"
>
{{ onlineScenery.dispatcherName }}
</span>
<span v-else>{{ onlineScenery.dispatcherName }}</span>
</router-link>
</div>
<span class="dispatcher_likes text--primary">
<div class="info-bottom">
<span
class="dispatcher-likes text--primary"
v-if="onlineScenery && onlineScenery.dispatcherExp != -1"
>
<img src="/images/icon-like.svg" alt="Likes count icon" />
<span>{{ onlineScenery?.dispatcherRate || '0' }}</span>
</span>
</div>
<StationStatusBadge
:isOnline="onlineScenery ? true : false"
:dispatcherStatus="onlineScenery?.dispatcherStatus"
:dispatcherTimestamp="onlineScenery?.dispatcherTimestamp"
/>
<span class="dispatcher-badge">
<StationStatusBadge
:isOnline="onlineScenery ? true : false"
:dispatcherStatus="onlineScenery?.dispatcherStatus"
:dispatcherTimestamp="onlineScenery?.dispatcherTimestamp"
/>
</span>
</div>
</section>
</template>
@@ -35,13 +49,14 @@ import dateMixin from '../../../mixins/dateMixin';
import routerMixin from '../../../mixins/routerMixin';
import styleMixin from '../../../mixins/styleMixin';
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
import { OnlineScenery } from '../../../store/typings';
import { ActiveScenery } from '../../../store/typings';
import donatorMixin from '../../../mixins/donatorMixin';
export default defineComponent({
mixins: [styleMixin, dateMixin, routerMixin],
mixins: [styleMixin, dateMixin, routerMixin, donatorMixin],
props: {
onlineScenery: {
type: Object as PropType<OnlineScenery>,
type: Object as PropType<ActiveScenery>,
required: false
}
},
@@ -51,45 +66,46 @@ export default defineComponent({
<style lang="scss" scoped>
.info-dispatcher {
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8em;
}
flex-wrap: wrap;
.info-top {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5em;
}
.info-bottom {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5em;
.dispatcher {
font-size: 2em;
margin-top: 0.5em;
}
&_level {
display: inline-block;
margin-right: 0.3em;
background: firebrick;
.dispatcher-level {
background: firebrick;
border-radius: 0.1em;
border-radius: 0.1em;
width: 1.5em;
height: 1.5em;
line-height: 1.5em;
font-weight: bold;
}
width: 1.5em;
height: 1.5em;
line-height: 1.5em;
font-weight: bold;
}
&_name {
cursor: pointer;
margin-right: 0.25em;
}
.dispatcher-likes {
display: flex;
gap: 0.25em;
&_likes {
img {
height: 0.7em;
margin: 0 0.25em;
}
}
}
.status-badge {
font-size: 1.25em;
margin: 0.5em 0.25em;
img {
width: 1em;
}
}
.dispatcher-badge {
font-size: 0.7em;
}
</style>
@@ -1,76 +1,87 @@
<template>
<section class="info-icons">
<span v-if="!station || !station.generalInfo">
<img
class="icon-info"
src="/images/icon-unknown.svg"
alt="icon-unknown"
:title="$t('sceneries.info.unknown')"
/>
</span>
<span
v-if="station.generalInfo && station.generalInfo.reqLevel >= 0"
v-if="station?.generalInfo && station?.generalInfo.reqLevel >= 0"
class="scenery-icon icon-info level"
:style="calculateExpStyle(station.generalInfo.reqLevel)"
:style="calculateExpStyle(station?.generalInfo.reqLevel)"
>
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
{{ station?.generalInfo.reqLevel >= 2 ? station?.generalInfo.reqLevel : 'L' }}
</span>
<span
v-if="station.generalInfo"
v-if="station?.generalInfo"
class="scenery-icon icon-info"
:class="station.generalInfo.controlType.replace('+', '-')"
:title="$t('desc.control-type') + $t(`controls.${station.generalInfo.controlType}`)"
v-html="getControlTypeAbbrev(station.generalInfo.controlType)"
:class="station?.generalInfo.controlType.replace('+', '-')"
:title="
$t('sceneries.info.control-type') + $t(`controls.${station?.generalInfo.controlType}`)
"
v-html="getControlTypeAbbrev(station?.generalInfo.controlType)"
>
</span>
<img
v-if="station.generalInfo?.SUP"
class="icon-info"
src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)"
:title="$t('desc.SUP')"
/>
<img
v-if="station.generalInfo?.signalType"
v-if="station?.generalInfo?.signalType"
class="icon-info"
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
:alt="station.generalInfo.signalType"
:title="$t('desc.signals-type') + $t(`signals.${station.generalInfo.signalType}`)"
:title="$t('sceneries.info.signals-type') + $t(`signals.${station.generalInfo.signalType}`)"
/>
<img
v-if="station.generalInfo?.availability == 'nonPublic'"
v-if="station?.generalInfo?.availability == 'nonPublic'"
class="icon-info"
src="/images/icon-lock.svg"
alt="Non-public scenery"
:title="$t('desc.non-public')"
:title="$t('sceneries.info.non-public')"
/>
<img
v-if="station.generalInfo?.availability == 'unavailable'"
v-if="station?.generalInfo?.availability == 'unavailable'"
class="icon-info"
src="/images/icon-unavailable.svg"
alt="Unavailable scenery"
:title="$t('desc.unavailable')"
:title="$t('sceneries.info.unavailable')"
/>
<img
v-if="station.generalInfo?.availability == 'abandoned'"
v-if="station?.generalInfo?.availability == 'abandoned'"
class="icon-info"
src="/images/icon-abandoned.svg"
alt="Abandoned scenery"
:title="$t('desc.abandoned')"
:title="$t('sceneries.info.abandoned')"
/>
<img
v-if="station.generalInfo?.lines"
v-if="station?.generalInfo?.SUP"
class="icon-info"
src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)"
:title="$t('sceneries.info.SUP')"
/>
<img
v-if="station?.generalInfo?.ASDEK"
class="icon-info"
src="/images/icon-ASDEK.svg"
alt="dSAT ASDEK"
:title="$t('sceneries.info.ASDEK')"
/>
<img
v-if="station?.generalInfo?.lines"
class="icon-info"
src="/images/icon-real.svg"
alt="real scenery"
:title="`${$t('desc.real')} ${station.generalInfo.lines}`"
/>
<img
v-if="!station.generalInfo"
class="icon-info"
src="/images/icon-unknown.svg"
alt="icon-unknown"
:title="$t('desc.unknown')"
:title="`${$t('sceneries.info.real')} ${station.generalInfo.lines}`"
/>
</section>
</template>
@@ -85,8 +96,7 @@ export default defineComponent({
mixins: [stationInfoMixin, styleMixin],
props: {
station: {
type: Object as PropType<Station>,
required: true
type: Object as PropType<Station>
}
}
});
@@ -1,41 +1,49 @@
<template>
<section class="info-routes" v-if="station.generalInfo">
<div class="routes one-way" v-if="station.generalInfo.routes.oneWay.length > 0">
<div class="routes one-way" v-if="oneWayRoutes.length > 0">
<b>{{ $t('scenery.one-way-routes') }}</b>
<ul class="routes-list">
<li
v-for="route in station.generalInfo.routes.oneWay"
:key="route.name"
@click="setActiveShowLength(route.name)"
v-for="route in oneWayRoutes"
:key="route.routeName"
@click="setActiveShowLength(route.routeName)"
>
<span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }">
{{ route.name }}</span
<span :class="{ 'no-catenary': !route.isElectric, internal: route.isInternal }">
{{ route.routeName }}</span
>
<span v-if="route.speed" class="speed">
{{ activeShowLength.includes(route.name) ? route.length + 'm' : route.speed }}
<span v-if="route.routeSpeed" class="speed">
{{
activeShowLength.includes(route.routeName)
? route.routeLength + 'm'
: route.routeSpeed
}}
</span>
<span v-if="route.SBL" class="sbl">SBL</span>
<span v-if="route.isRouteSBL" class="sbl">SBL</span>
</li>
</ul>
</div>
<div class="routes two-way" v-if="station.generalInfo.routes.twoWay.length > 0">
<div class="routes two-way" v-if="twoWayRoutes.length > 0">
<b>{{ $t('scenery.two-way-routes') }}</b>
<ul class="routes-list">
<li
v-for="route in station.generalInfo.routes.twoWay"
:key="route.name"
@click="setActiveShowLength(route.name)"
v-for="route in twoWayRoutes"
:key="route.routeName"
@click="setActiveShowLength(route.routeName)"
>
<span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }">{{
route.name
<span :class="{ 'no-catenary': !route.isElectric, internal: route.isInternal }">{{
route.routeName
}}</span>
<span v-if="route.speed" class="speed">
{{ activeShowLength.includes(route.name) ? route.length + 'm' : route.speed }}
<span v-if="route.routeSpeed" class="speed">
{{
activeShowLength.includes(route.routeName)
? route.routeLength + 'm'
: route.routeSpeed
}}
</span>
<span v-if="route.SBL" class="sbl">SBL</span>
<span v-if="route.isRouteSBL" class="sbl">SBL</span>
</li>
</ul>
</div>
@@ -66,6 +74,16 @@ export default defineComponent({
return {
activeShowLength: [] as string[]
};
},
computed: {
oneWayRoutes() {
return this.station.generalInfo?.routes.single ?? [];
},
twoWayRoutes() {
return this.station.generalInfo?.routes.double ?? [];
}
}
});
</script>
@@ -30,12 +30,12 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { OnlineScenery } from '../../../store/typings';
import { ActiveScenery } from '../../../store/typings';
export default defineComponent({
props: {
onlineScenery: {
type: Object as PropType<OnlineScenery>,
type: Object as PropType<ActiveScenery>,
required: false
}
},
@@ -32,14 +32,14 @@
import { PropType, defineComponent } from 'vue';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
import routerMixin from '../../../mixins/routerMixin';
import { OnlineScenery } from '../../../store/typings';
import { ActiveScenery } from '../../../store/typings';
export default defineComponent({
mixins: [routerMixin, modalTrainMixin],
props: {
onlineScenery: {
type: Object as PropType<OnlineScenery>,
type: Object as PropType<ActiveScenery>,
required: false
}
}
+30 -25
View File
@@ -6,21 +6,21 @@
<span>{{ $t('scenery.timetables') }}</span>
<span>
<span class="text--primary">{{ onlineScenery?.scheduledTrainCount.all || 0 }}</span>
<span class="text--primary">{{ onlineScenery?.scheduledTrainCount.all ?? 0 }}</span>
<span> / </span>
<span class="text--grayed">
{{ onlineScenery?.scheduledTrainCount.confirmed || '0' }}
{{ onlineScenery?.scheduledTrainCount.confirmed ?? 0 }}
</span>
</span>
<span class="header_links">
<a
<span class="header_links" v-if="station">
<!-- <a
:href="`https://pragotron-td2.web.app/board?name=${station.name}`"
target="_blank"
:title="$t('scenery.pragotron-link')"
>
<img src="/images/icon-pragotron.svg" alt="icon-pragotron" />
</a>
</a> -->
<a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
<img src="/images/icon-tablice.ico" alt="icon-tablice" />
@@ -33,12 +33,12 @@
{{ (i > 0 && '&bull;') || '' }}
<button
:key="cp.checkpointName"
:key="cp"
class="checkpoint_item"
:class="{ current: chosenCheckpoint === cp.checkpointName }"
:class="{ current: chosenCheckpoint === cp }"
@click="setCheckpoint(cp)"
>
{{ cp.checkpointName }}
{{ cp }}
</button>
</span>
</div>
@@ -48,7 +48,7 @@
<transition-group name="list-anim">
<div
style="padding-bottom: 5em"
v-if="store.dataStatuses.trains == 0 && computedScheduledTrains.length == 0"
v-if="apiStore.dataStatuses.connection == 0 && computedScheduledTrains.length == 0"
key="list-loading"
>
<Loading />
@@ -74,7 +74,7 @@
class="timetable-item"
v-else
v-for="scheduledTrain in computedScheduledTrains"
:key="scheduledTrain.trainId"
:key="scheduledTrain.trainId + scheduledTrain.stopInfo.arrivalTimestamp"
tabindex="0"
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
@@ -187,10 +187,11 @@ import Loading from '../Global/Loading.vue';
import dateMixin from '../../mixins/dateMixin';
import routerMixin from '../../mixins/routerMixin';
import Station from '../../scripts/interfaces/Station';
import { useStore } from '../../store/mainStore';
import { useMainStore } from '../../store/mainStore';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import { OnlineScenery } from '../../store/typings';
import { ActiveScenery } from '../../store/typings';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
name: 'SceneryTimetable',
@@ -201,12 +202,10 @@ export default defineComponent({
props: {
station: {
type: Object as PropType<Station>,
required: true
type: Object as PropType<Station>
},
onlineScenery: {
type: Object as PropType<OnlineScenery>,
required: false
type: Object as PropType<ActiveScenery>
}
},
@@ -226,36 +225,41 @@ export default defineComponent({
const route = useRoute();
const currentURL = computed(() => `${location.origin}${route.fullPath}`);
const store = useStore();
const apiStore = useApiStore();
const mainStore = useMainStore();
const chosenCheckpoint = ref(
props.station?.generalInfo?.checkpoints?.length == 0
? ''
: props.station?.generalInfo?.checkpoints[0].checkpointName || null
: props.station?.generalInfo?.checkpoints[0] ?? null
);
return {
currentURL,
chosenCheckpoint,
store
apiStore,
mainStore
};
},
computed: {
tabliceZbiorczeHref() {
let url = `https://tablice-td2.web.app/?station=${this.station.name}`;
let url = `https://tablice-td2.web.app/?station=${this.station!.name}`;
if (this.chosenCheckpoint) url += `&checkpoint=${this.chosenCheckpoint}`;
return url;
},
computedScheduledTrains() {
if (!this.station) return [];
return (
this.onlineScenery?.scheduledTrains
?.filter(
(train) =>
train.checkpointName.toLocaleLowerCase() ==
(this.chosenCheckpoint || this.station.name).toLocaleLowerCase()
(this.chosenCheckpoint || this.station!.name).toLocaleLowerCase() &&
train.region == this.mainStore.region.id
)
.sort((a, b) => {
if (a.stopStatusID > b.stopStatusID) return 1;
@@ -272,12 +276,13 @@ export default defineComponent({
methods: {
loadSelectedOption() {
this.chosenCheckpoint =
this.station.generalInfo?.checkpoints[0]?.checkpointName || this.station.name;
if (!this.station) return;
this.chosenCheckpoint = this.station.generalInfo?.checkpoints[0] ?? this.station.name;
},
setCheckpoint(cp: { checkpointName: string }) {
this.chosenCheckpoint = cp.checkpointName;
setCheckpoint(cp: string) {
this.chosenCheckpoint = cp;
}
}
});
@@ -28,7 +28,7 @@
<tbody>
<tr v-for="historyItem in historyList" :key="historyItem.id">
<td>
<router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">
<router-link :to="`/journal/timetables?search-train=%23${historyItem.id}`">
#{{ historyItem.id }}
</router-link>
</td>
@@ -37,11 +37,16 @@
{{ historyItem.trainNo }}
</td>
<td>{{ historyItem.route.replace('|', ' -> ') }}</td>
<td>{{ historyItem.driverName }}</td>
<td>
<router-link :to="`/journal/timetables?search-driver=${historyItem.driverName}`">
{{ historyItem.driverName }}
</router-link>
</td>
<td>
<router-link
v-if="historyItem.authorName"
:to="`/journal/timetables?authorName=${historyItem.authorName}`"
:to="`/journal/timetables?search-dispatcher=${historyItem.authorName}`"
>{{ historyItem.authorName }}
</router-link>
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
@@ -63,35 +68,33 @@
</template>
<script lang="ts">
import axios from 'axios';
import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import Station from '../../scripts/interfaces/Station';
import { URLs } from '../../scripts/utils/apiURLs';
import Loading from '../Global/Loading.vue';
import listObserverMixin from '../../mixins/listObserverMixin';
import { OnlineScenery } from '../../store/typings';
import { ActiveScenery } from '../../store/typings';
import { API } from '../../typings/api';
import { Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
name: 'SceneryTimetablesHistory',
mixins: [dateMixin, listObserverMixin],
props: {
station: {
type: Object as PropType<Station>,
required: true
type: Object as PropType<Station>
},
onlineScenery: {
type: Object as PropType<OnlineScenery>,
required: false
type: Object as PropType<ActiveScenery>
}
},
data() {
return {
historyList: [] as API.TimetableHistory.Response,
apiStore: useApiStore(),
dataStatus: Status.Data.Loading,
DataStatus: Status.Data
};
@@ -102,11 +105,20 @@ export default defineComponent({
},
methods: {
async fetchAPIData(countFrom = 0, countLimit = 15) {
try {
const requestString = `${URLs.stacjownikAPI}/api/getTimetables?issuedFrom=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
async fetchAPIData() {
if (!this.station && !this.onlineScenery) {
this.dataStatus = Status.Data.Loaded;
return;
}
const response: API.TimetableHistory.Response = await (await axios.get(requestString)).data;
try {
const response: API.TimetableHistory.Response = await (
await this.apiStore.client!.get('api/getTimetables', {
params: {
issuedFrom: this.station?.name || this.onlineScenery?.name
}
})
).data;
this.historyList = response;
@@ -117,7 +129,12 @@ export default defineComponent({
},
navigateToHistory() {
this.$router.push(`/journal/timetables?issuedFrom=${this.station.name}`);
this.$router.push({
path: '/journal/timetables',
query: {
'search-issuedFrom': this.station?.name || this.onlineScenery?.name
}
});
}
},
components: { Loading }
+163 -137
View File
@@ -60,8 +60,9 @@
</div>
</section>
<section class="card_timestamp" style="text-align: center">
<div>{{ $t('filters.minimum-hours-title') }}</div>
<section class="card_timestamp">
<h3 class="section-header">{{ $t('filters.minimum-hours-title') }}</h3>
<span class="clock">
<button class="btn--action" @click="subHour">-</button>
<span>{{
@@ -75,16 +76,27 @@
</span>
</section>
<datalist id="authors">
<option v-for="(author, i) in authors" :key="i" :value="author"></option>
</datalist>
<section class="card_authors-search">
<input
type="text"
:placeholder="$t('filters.authors-search')"
name="authors"
v-model="authorsInputValue"
@input="handleAuthorsInput"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
/>
<h3 class="section-header">{{ $t('filters.authors-search') }}</h3>
<form action="javascript:void(0);" @submit="handleAuthorsInput">
<input
type="text"
id="author"
list="authors"
name="authors"
:placeholder="$t('filters.authors-placeholder')"
v-model="authorsInputValue"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
/>
<button class="btn--action">{{ $t('filters.authors-button-title') }}</button>
</form>
</section>
<section class="card_sliders">
@@ -139,7 +151,7 @@ import { defineComponent, inject } from 'vue';
import keyMixin from '../../mixins/keyMixin';
import routerMixin from '../../mixins/routerMixin';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useStore } from '../../store/mainStore';
import { useMainStore } from '../../store/mainStore';
import FilterOption from './FilterOption.vue';
import StorageManager from '../../managers/storageManager';
@@ -163,7 +175,7 @@ export default defineComponent({
setup() {
const isVisible = inject('isFilterCardVisible');
const store = useStore();
const store = useMainStore();
const filterStore = useStationFiltersStore();
return {
@@ -196,6 +208,19 @@ export default defineComponent({
currentOptionsActive() {
return true;
},
authors() {
return this.store.stationList
.reduce((acc, station) => {
station.generalInfo?.authors?.forEach((author) => {
if (author.trim() != '' && !acc.includes(author.toLocaleLowerCase()))
acc.push(author.toLocaleLowerCase());
});
return acc;
}, [] as string[])
.sort((a, b) => a.localeCompare(b));
}
},
@@ -230,12 +255,10 @@ export default defineComponent({
if (this.saveOptions) StorageManager.setStringValue(target.name, target.value);
},
handleAuthorsInput(e: Event) {
clearTimeout(this.delayInputTimer);
handleAuthorsInput() {
this.filterStore.changeFilterValue('authors', this.authorsInputValue);
this.delayInputTimer = window.setTimeout(() => {
this.handleInput(e);
}, 400);
if (this.saveOptions) StorageManager.setStringValue('authors', this.authorsInputValue);
},
changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
@@ -297,136 +320,139 @@ export default defineComponent({
@import '../../styles/card.scss';
@import '../../styles/animations.scss';
h3.section-header {
text-align: center;
margin: 0.5em 0;
}
.card {
display: grid;
grid-template-rows: 1fr auto;
}
&_info {
background-color: #111;
padding: 0.5em;
.card_info {
background-color: #111;
padding: 0.5em;
}
.card_controls {
display: flex;
gap: 0.5em;
input {
border-radius: 0.5em 0.5em 0 0;
height: 100%;
}
}
.card_content {
padding: 1em 0.5em;
display: flex;
flex-direction: column;
gap: 1em;
overflow: auto;
}
.card_title {
font-size: 2em;
font-weight: 700;
color: $accentCol;
text-align: center;
}
.card_regions {
display: flex;
justify-content: center;
label > input {
display: none;
}
&_controls {
label > span {
padding: 0.25em 0.5em;
margin: 0 0.25em;
cursor: pointer;
background-color: gray;
&.checked {
background-color: seagreen;
}
}
}
.card_timestamp {
display: flex;
flex-direction: column;
justify-content: center;
.clock {
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2em;
text-align: center;
span {
min-width: 120px;
font-weight: bold;
color: $accentCol;
}
button {
padding: 0.2em 0.6em;
}
}
}
.card_authors-search {
margin: 1em 0;
form {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.5em;
width: 100%;
margin-top: 1em;
}
input {
width: 70%;
max-width: 400px;
padding: 0.5em;
outline: 1px solid white;
}
}
.card_actions {
width: 100%;
padding: 0.5em;
.filter-option {
max-width: 50%;
margin: 0 auto;
}
.action-buttons {
display: flex;
gap: 0.5em;
input {
border-radius: 0.5em 0.5em 0 0;
height: 100%;
}
}
&_content {
padding: 1em 0.5em;
display: flex;
flex-direction: column;
gap: 1em;
overflow: auto;
}
&_title {
font-size: 2em;
font-weight: 700;
color: $accentCol;
text-align: center;
}
&_regions {
display: flex;
justify-content: center;
label > input {
display: none;
}
label > span {
padding: 0.25em 0.5em;
margin: 0 0.25em;
cursor: pointer;
background-color: gray;
&.checked {
background-color: seagreen;
}
}
}
&_timestamp {
display: flex;
flex-direction: column;
justify-content: center;
.clock {
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2em;
margin-top: 0.5em;
span {
min-width: 120px;
font-weight: bold;
color: $accentCol;
}
button {
padding: 0.2em 0.6em;
}
}
}
&_modes {
display: flex;
justify-content: center;
.option {
margin: 0 1em;
}
}
&_authors-search {
display: inline-block;
margin: 0 auto;
width: 60%;
min-width: 240px;
input {
width: 100%;
padding: 0.5em;
border: 1px solid white;
}
}
&_actions {
width: 100%;
padding: 0.5em;
.filter-option {
max-width: 50%;
margin-top: 0.5em;
button {
width: 50%;
margin: 0 auto;
}
padding: 0.5em;
.action-buttons {
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;
}
&[data-selected='true'] {
background-color: forestgreen;
}
}
}
@@ -447,7 +473,7 @@ export default defineComponent({
.section-inputs {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
margin: 1em 0;
}
+406 -385
View File
@@ -1,304 +1,318 @@
<template>
<section class="station_table">
<div class="table_wrapper">
<table>
<thead>
<tr>
<th
v-for="headerName in headIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-text"
<transition name="status-anim" mode="out-in">
<div class="table_wrapper" :key="apiStore.dataStatuses.connection">
<table>
<thead>
<tr>
<th
v-for="headerName in headIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-text"
:class="headerName"
>
<span class="header_wrapper">
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
<img
class="sort-icon"
v-if="sorterActive.headerName == headerName"
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
<th
v-for="headerName in headIconsIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-image"
:class="headerName"
>
<span class="header_wrapper">
<img
:src="`/images/icon-${headerName}.svg`"
:alt="headerName"
:title="$t(`sceneries.headers.${headerName}`)"
/>
<img
class="sort-icon"
v-if="sorterActive.headerName == headerName"
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="station in stations"
:class="{ 'last-selected': lastSelectedStationName == station.name }"
:key="station.name"
@click.left="setScenery(station.name)"
@click.right="openForumSite($event, station.generalInfo?.url)"
@keydown.enter="setScenery(station.name)"
@keydown.space="openForumSite($event, station.generalInfo?.url)"
tabindex="0"
>
<span class="header_wrapper">
<div v-html="$t(`sceneries.${headerName}`)"></div>
<td class="station-name" :class="station.generalInfo?.availability">
<b v-if="station.generalInfo?.project" style="color: salmon">{{
station.generalInfo.project
}}</b>
{{ station.name }}
</td>
<img
class="sort-icon"
v-if="sorterActive.headerName == headerName"
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
<td class="station-level">
<span v-if="station.generalInfo">
<span
v-if="
station.generalInfo.reqLevel > -1 &&
station.generalInfo.availability != 'nonPublic' &&
station.generalInfo.availability != 'unavailable'
"
:style="calculateExpStyle(station.generalInfo.reqLevel)"
>
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
</span>
<span v-else-if="station.generalInfo.availability == 'abandoned'">
<img
src="/images/icon-abandoned.svg"
alt="non-public"
:title="$t('sceneries.info.abandoned')"
/>
</span>
<span v-else-if="station.generalInfo.availability == 'nonPublic'">
<img
src="/images/icon-lock.svg"
alt="non-public"
:title="$t('sceneries.info.non-public')"
/>
</span>
<span v-else>
<img
src="/images/icon-unavailable.svg"
alt="unavailable"
:title="$t('sceneries.info.unavailable')"
/>
</span>
</span>
<span v-else> ? </span>
</td>
<td class="station-status">
<StationStatusBadge
:isOnline="station.onlineInfo ? true : false"
:dispatcherStatus="station.onlineInfo?.dispatcherStatus"
/>
</span>
</th>
</td>
<th
v-for="headerName in headIconsIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-image"
>
<span class="header_wrapper">
<img
:src="`/images/icon-${headerName}.svg`"
:alt="headerName"
:title="$t(`sceneries.${headerName}`)"
/>
<td class="station-dispatcher-name">
<span v-if="station.onlineInfo?.dispatcherName">
<b
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
@click.stop="openDonationModal"
data-popup-key="DonatorPopUp"
:data-popup-content="$t('donations.dispatcher-message')"
>
<img src="/images/icon-diamond.svg" alt="" />
{{ station.onlineInfo.dispatcherName }}
</b>
<img
class="sort-icon"
v-if="sorterActive.headerName == headerName"
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
</tr>
</thead>
<div v-else>
{{ station.onlineInfo.dispatcherName }}
</div>
</span>
</td>
<tbody>
<tr
class="station"
:class="{ 'last-selected': lastSelectedStationName == station.name }"
v-for="(station, i) in stations"
:key="i + station.name"
@click.left="setScenery(station.name)"
@click.right="openForumSite($event, station.generalInfo?.url)"
@keydown.enter="setScenery(station.name)"
@keydown.space="openForumSite($event, station.generalInfo?.url)"
tabindex="0"
>
<td class="station_name" :class="station.generalInfo?.availability">
<b v-if="station.generalInfo?.project" style="color: salmon">{{
station.generalInfo.project
}}</b>
{{ station.name }}
</td>
<td class="station_level">
<span v-if="station.generalInfo">
<td class="station-dispatcher-exp">
<span
v-if="
station.generalInfo.reqLevel > -1 &&
station.generalInfo.availability != 'nonPublic' &&
station.generalInfo.availability != 'unavailable'
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
:style="
calculateExpStyle(
station.onlineInfo.dispatcherExp,
station.onlineInfo.dispatcherIsSupporter
)
"
:style="calculateExpStyle(station.generalInfo.reqLevel)"
>
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
{{
station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp
}}
</span>
</td>
<span v-else-if="station.generalInfo.availability == 'abandoned'">
<img
src="/images/icon-abandoned.svg"
alt="non-public"
:title="$t('desc.abandoned')"
/>
</span>
<td class="station-tracks">
<div v-if="station.generalInfo">
<span
v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0"
class="track catenary"
:title="`${$t('sceneries.info.single-track-routes-catenary')}${
station.generalInfo.routes.singleElectrifiedNames.length
}`"
>
{{ station.generalInfo.routes.singleElectrifiedNames.length }}
</span>
<span v-else-if="station.generalInfo.availability == 'nonPublic'">
<img
src="/images/icon-lock.svg"
alt="non-public"
:title="$t('desc.non-public')"
/>
</span>
<span v-else>
<img
src="/images/icon-unavailable.svg"
alt="unavailable"
:title="$t('desc.unavailable')"
/>
</span>
</span>
<span v-else> ? </span>
</td>
<td class="station_status">
<StationStatusBadge
:isOnline="station.onlineInfo ? true : false"
:dispatcherStatus="station.onlineInfo?.dispatcherStatus"
/>
</td>
<td class="station_dispatcher-name">
<span v-if="station.onlineInfo?.dispatcherName">
<b
v-if="store.donatorsData.includes(station.onlineInfo.dispatcherName)"
:title="$t('donations.dispatcher-message')"
@click.stop="openDonationModal"
>
<img src="/images/icon-diamond.svg" alt="" />
{{ station.onlineInfo.dispatcherName }}
</b>
<div v-else>
{{ station.onlineInfo.dispatcherName }}
<span
v-if="station.generalInfo.routes.singleOtherNames.length != 0"
class="track no-catenary"
:title="`${$t('sceneries.info.single-track-routes-other')}${
station.generalInfo.routes.singleOtherNames.length
}`"
>
{{ station.generalInfo.routes.singleOtherNames.length }}
</span>
</div>
</span>
</td>
</td>
<td class="station_dispatcher-exp">
<span
v-if="station.onlineInfo"
:style="
calculateExpStyle(
station.onlineInfo.dispatcherExp,
station.onlineInfo.dispatcherIsSupporter
)
"
>
{{ station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp }}
</span>
</td>
<td class="station-tracks">
<div v-if="station.generalInfo">
<span
v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0"
class="track catenary"
:title="`${$t('sceneries.info.double-track-routes-catenary')}${
station.generalInfo.routes.doubleElectrifiedNames.length
}`"
>
{{ station.generalInfo.routes.doubleElectrifiedNames.length }}
</span>
<td class="station_tracks twoway">
<span
v-if="
station.generalInfo &&
station.generalInfo.routes.twoWayCatenaryRouteNames.length > 0
"
class="track catenary"
:title="`Liczba zelektryfikowanych szlaków dwutorowych: ${station.generalInfo.routes.twoWayCatenaryRouteNames.length}`"
>
{{ station.generalInfo.routes.twoWayCatenaryRouteNames.length }}
</span>
<span
v-if="station.generalInfo.routes.doubleOtherNames.length != 0"
class="track no-catenary"
:title="`${$t('sceneries.info.double-track-routes-other')}${
station.generalInfo.routes.doubleOtherNames.length
}`"
>
{{ station.generalInfo.routes.doubleOtherNames.length }}
</span>
</div>
</td>
<span
v-if="
station.generalInfo &&
station.generalInfo.routes.twoWayNoCatenaryRouteNames.length > 0
"
class="track no-catenary"
:title="`Liczba niezelektryfikowanych szlaków dwutorowych: ${station.generalInfo.routes.twoWayNoCatenaryRouteNames.length}`"
>
{{ station.generalInfo.routes.twoWayNoCatenaryRouteNames.length }}
</span>
<td class="station-info">
<span
v-if="station.generalInfo?.signalType"
class="scenery-icon icon-info"
:class="station.generalInfo?.controlType.replace('+', '-')"
:title="
$t('sceneries.info.control-type') +
$t(`controls.${station.generalInfo?.controlType}`)
"
v-html="getControlTypeAbbrev(station.generalInfo.controlType)"
>
</span>
<span class="separator"></span>
<span
v-if="
station.generalInfo &&
station.generalInfo.routes.oneWayCatenaryRouteNames.length > 0
"
class="track catenary"
:title="`Liczba zelektryfikowanych szlaków jednotorowych: ${station.generalInfo.routes.oneWayCatenaryRouteNames.length}`"
>
{{ station.generalInfo.routes.oneWayCatenaryRouteNames.length }}
</span>
<span
v-if="
station.generalInfo &&
station.generalInfo.routes.oneWayNoCatenaryRouteNames.length > 0
"
class="track no-catenary"
:title="`Liczba niezelektryfikowanych szlaków jednotorowych: ${station.generalInfo.routes.oneWayNoCatenaryRouteNames.length}`"
>
{{ station.generalInfo.routes.oneWayNoCatenaryRouteNames.length }}
</span>
</td>
<td class="station_info" v-if="station.generalInfo">
<span
class="scenery-icon icon-info"
:class="station.generalInfo.controlType.replace('+', '-')"
:title="$t('desc.control-type') + $t(`controls.${station.generalInfo.controlType}`)"
v-html="getControlTypeAbbrev(station.generalInfo.controlType)"
>
</span>
<span>
<img
v-if="station.generalInfo?.signalType"
class="icon-info"
v-if="station.generalInfo.SUP"
src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)"
:title="$t('desc.SUP')"
/>
</span>
<span>
<img
class="icon-info"
v-if="station.generalInfo.signalType"
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
:alt="station.generalInfo.signalType"
:title="$t('desc.signals-type') + $t(`signals.${station.generalInfo.signalType}`)"
:title="
$t('sceneries.info.signals-type') +
$t(`signals.${station.generalInfo.signalType}`)
"
/>
</span>
<span>
<img
v-if="station.generalInfo?.SUP"
class="icon-info"
v-if="station.generalInfo && station.generalInfo.routes.sblRouteNames.length > 0"
src="/images/icon-SBL.svg"
alt="SBL"
:title="$t('desc.SBL') + `${station.generalInfo.routes.sblRouteNames.join(',')}`"
src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)"
:title="$t('sceneries.info.SUP')"
/>
</span>
</td>
<td class="station_info" v-else>
<img
class="icon-info"
src="/images/icon-unknown.svg"
alt="icon-unknown"
:title="$t('desc.unknown')"
/>
</td>
<img
v-if="station.generalInfo?.ASDEK"
class="icon-info"
src="/images/icon-ASDEK.svg"
alt="dSAT ASDEK"
:title="$t('sceneries.info.ASDEK')"
/>
<td class="station_users" :class="{ inactive: !station.onlineInfo }">
<span>{{ station.onlineInfo?.currentUsers || 0 }}</span>
/
<span>{{ station.onlineInfo?.maxUsers || 0 }}</span>
</td>
<img
v-if="!station.generalInfo"
class="icon-info"
src="/images/icon-unknown.svg"
alt="icon-unknown"
:title="$t('sceneries.info.unknown')"
/>
</td>
<td class="station_spawns" :class="{ inactive: !station.onlineInfo }">
<span>{{ station.onlineInfo?.spawns.length || 0 }}</span>
</td>
<td class="station-users" :class="{ inactive: !station.onlineInfo }">
<span class="text--primary">{{ station.onlineInfo?.currentUsers ?? '-' }}</span>
/
<span class="text--primary">{{ station.onlineInfo?.maxUsers ?? '-' }}</span>
</td>
<td
class="station_schedules all"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.all }}
</td>
<td class="station-likes" :class="{ inactive: !station.onlineInfo }">
<span>{{ station.onlineInfo?.dispatcherRate ?? '-' }}</span>
</td>
<td
class="station_schedules unconfirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed }}
</td>
<td class="station-spawns" :class="{ inactive: !station.onlineInfo }">
<span>{{ station.onlineInfo?.spawns.length ?? '-' }}</span>
</td>
<td
class="station_schedules confirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.confirmed }}
</td>
</tr>
</tbody>
</table>
</div>
<td
class="station-schedules all"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }}
</td>
<Loading v-if="!isDataLoaded && stations.length == 0" />
<td
class="station-schedules unconfirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }}
</td>
<div class="no-stations" v-else-if="stations.length == 0">
{{ $t('sceneries.no-stations') }}
</div>
<td
class="station-schedules confirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
</td>
</tr>
</tbody>
</table>
<Loading
v-if="apiStore.dataStatuses.connection == Status.Loading && stations.length == 0"
/>
<div class="no-stations" v-else-if="stations.length == 0">
{{ $t('sceneries.no-stations') }}
</div>
</div>
</transition>
</section>
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from 'vue';
import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import stationInfoMixin from '../../mixins/stationInfoMixin';
import styleMixin from '../../mixins/styleMixin';
import Station from '../../scripts/interfaces/Station';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useStore } from '../../store/mainStore';
import { useMainStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue';
import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames';
import StationStatusBadge from '../Global/StationStatusBadge.vue';
import { Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
import { usePopupStore } from '../../store/popupStore';
export default defineComponent({
props: {
@@ -325,17 +339,18 @@ export default defineComponent({
},
setup() {
const store = useStore();
const mainStore = useMainStore();
const apiStore = useApiStore();
const popupStore = usePopupStore();
const stationFiltersStore = useStationFiltersStore();
const isDataLoaded = computed(() => {
return store.dataStatuses.sceneries != Status.Data.Loading;
});
return {
isDataLoaded,
Status: Status.Data,
stationFiltersStore,
store
mainStore,
apiStore,
popupStore
};
},
@@ -357,7 +372,8 @@ export default defineComponent({
openDonationModal(e: Event) {
this.$emit('toggleDonationModal', true);
this.store.modalLastClickedTarget = e.target;
this.mainStore.modalLastClickedTarget = e.target;
this.popupStore.currentPopupComponent = null;
},
openForumSite(e: Event, url: string | undefined) {
@@ -367,7 +383,7 @@ export default defineComponent({
},
changeSorter(headerName: HeadIdsTypes) {
if (headerName == 'general' || headerName == 'routes') return;
if (headerName == 'general') return;
this.stationFiltersStore.changeSorter(headerName);
}
@@ -396,19 +412,27 @@ $rowCol: #424242;
.table_wrapper {
overflow: auto;
overflow-y: hidden;
font-weight: 500;
height: 90vh;
min-height: 550px;
}
.no-stations {
text-align: center;
font-size: 1.5em;
padding: 1em;
margin: 1em 0;
background: #333;
}
table {
white-space: nowrap;
border-collapse: collapse;
// min-width: 1350px;
table-layout: fixed;
width: 100%;
@include smallScreen() {
min-width: auto;
}
min-width: 1250px;
white-space: wrap;
thead tr {
background-color: $bgCol;
@@ -418,12 +442,41 @@ table {
position: sticky;
top: 0;
&.header-text {
min-width: 140px;
&.station {
width: 12em;
}
&.min-lvl {
width: 4em;
}
&.status {
width: 10em;
}
&.dispatcher {
width: 12em;
}
&.dispatcher-lvl {
width: 6em;
}
&.routes-double,
&.routes-single {
width: 7em;
}
&.general {
width: 11em;
}
&.header-image {
min-width: 60px;
width: 3.5em;
&.user {
width: 5em;
}
}
padding: 0.5em 0.25em;
@@ -448,7 +501,7 @@ table {
}
}
tr.station {
tr {
background-color: $rowCol;
&:nth-child(even) {
@@ -462,10 +515,15 @@ tr.station {
}
td {
padding: 0.25em 1em;
padding: 0.15em 0;
text-align: center;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
&.inactive {
opacity: 0.2;
}
@include smallScreen() {
margin: 0;
@@ -475,118 +533,95 @@ tr.station {
}
}
td.station {
&_name {
font-weight: bold;
.station-name {
font-weight: bold;
max-width: 200px;
&.default {
color: $accentCol;
}
&.nonPublic {
color: #bebebe;
}
&.unavailable {
font-weight: 500;
color: #bebebe;
}
&.default {
color: $accentCol;
}
&_level,
&_dispatcher-exp {
span {
display: block;
width: 2em;
height: 2em;
line-height: 2em;
margin: 0 auto;
}
img {
width: 2em;
border-radius: 50%;
}
&.nonPublic {
color: #bebebe;
}
// &_dispatcher-name {
// position: relative;
// }
&.unavailable {
font-weight: 500;
color: #bebebe;
}
}
&_dispatcher-name img {
.station-level,
.station-dispatcher-exp {
span {
display: block;
width: 2em;
height: 2em;
line-height: 2em;
margin: 0 auto;
}
img {
width: 2em;
border-radius: 50%;
}
}
.station-dispatcher-name {
img {
max-width: 1.35em;
vertical-align: text-bottom;
}
&_level {
span {
background-color: #888;
border-radius: 50%;
}
}
&_info {
display: flex;
align-items: center;
justify-content: center;
/* Images */
.icon-info {
display: flex;
justify-content: center;
align-items: center;
width: 32px;
height: 32px;
font-size: 12px;
margin: 0 0.2em;
outline: 2px solid #444;
border-radius: 0.5em;
@include smallScreen() {
width: 24px;
height: 24px;
font-size: 10px;
}
}
}
&_tracks {
.no-catenary {
background-color: #939393;
}
.catenary {
background-color: #009dce;
}
.track {
margin: 0 0.35em;
padding: 0.35em;
font-size: 1.05em;
white-space: pre-wrap;
}
}
&_users,
&_spawns,
&_schedules {
&.inactive {
opacity: 0.2;
}
}
}
.station_users {
.station-level {
span {
color: gold;
background-color: #888;
border-radius: 50%;
}
}
.station_schedules {
.station-info {
.icon-info {
vertical-align: middle;
line-height: 2.5em;
width: 2.5em;
height: 2.5em;
font-size: 0.8em;
margin: 0 3px;
outline: 2px solid #2b2b2b;
border-radius: 5px;
}
}
.station-tracks {
.no-catenary {
background-color: #939393;
}
.catenary {
background-color: #009dce;
}
.separator {
background-color: #b3b3b3;
padding: 2px;
}
.track {
display: inline-block;
text-align: center;
width: 1.3em;
padding: 0.35em 0;
font-size: 1.1em;
margin: 0 0.2em;
}
}
.station-schedules {
&.all {
color: gold;
}
@@ -599,18 +634,4 @@ td.station {
color: lime;
}
}
.separator {
border-left: 3px solid #b3b3b3;
}
.no-stations {
text-align: center;
font-size: 1.5em;
padding: 1em;
margin: 1em 0;
background: #333;
}
</style>
+4 -3
View File
@@ -16,6 +16,8 @@ export interface Filter {
SPE: boolean;
SUP: boolean;
noSUP: boolean;
ASDEK: boolean;
noASDEK: boolean;
ręczne: boolean;
'ręczne+SPK': boolean;
'ręczne+SCS': boolean;
@@ -42,14 +44,13 @@ export interface Filter {
nonPublic: boolean;
unavailable: boolean;
abandoned: boolean;
endingStatus: boolean;
afkStatus: boolean;
noSpaceStatus: boolean;
unavailableStatus: boolean;
unsignedStatus: boolean;
authors: string;
onlineFromHours: number;
withActiveTimetables: boolean;
withoutActiveTimetables: boolean;
}
+172
View File
@@ -0,0 +1,172 @@
<template>
<span class="stop-label" :data-sbl="stop.isSBL">
<span class="name" v-html="stop.nameHtml"></span>
<span
v-if="stop.position != 'begin'"
class="date arrival"
:data-status="
stop.arrivalDelay > 0 && stop.status != 'unconfirmed'
? 'delayed'
: stop.arrivalDelay < 0 && stop.status != 'unconfirmed'
? 'preponed'
: stop.arrivalDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
"
>
<span v-if="stop.arrivalDelay != 0 && stop.status != 'unconfirmed'">
<s>{{ timestampToString(stop.arrivalScheduled) }}</s>
{{ timestampToString(stop.arrivalReal) }}
({{ stop.arrivalDelay > 0 ? '+' : '' }}{{ stop.arrivalDelay }})
</span>
<span v-else>
{{ timestampToString(stop.arrivalScheduled) }}
</span>
</span>
<span
v-if="
stop.duration ||
(stop.status == 'stopped' &&
stop.position != 'begin' &&
stop.departureDelay != stop.arrivalDelay)
"
class="date stop"
:data-stop-types="stop.type.replace(', ', '-')"
:data-stop-status="
stop.departureDelay - stop.arrivalDelay > 0 && !stop.duration ? 'delayed' : ''
"
>
{{ stop.duration || stop.departureDelay - stop.arrivalDelay }}
{{ stop.type == '' ? 'pt' : stop.type }}
</span>
<span
v-if="
stop.position != 'end' &&
(stop.duration != 0 || stop.status == 'stopped' || stop.departureDelay != stop.arrivalDelay)
"
class="date departure"
:data-status="
stop.departureDelay > 0 && stop.status == 'confirmed'
? 'delayed'
: stop.departureDelay < 0 && stop.status == 'confirmed'
? 'preponed'
: stop.departureDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
"
>
<span v-if="stop.departureDelay != 0 && stop.status == 'confirmed'">
<s>{{ timestampToString(stop.departureScheduled) }}</s>
{{ timestampToString(stop.departureReal) }}
({{ stop.departureDelay > 0 ? '+' : '' }}{{ stop.departureDelay }})
</span>
<span v-else>
{{ timestampToString(stop.departureScheduled) }}
</span>
</span>
</span>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { TrainScheduleStop } from './TrainSchedule.vue';
export default defineComponent({
mixins: [dateMixin],
props: {
stop: {
type: Object as PropType<TrainScheduleStop>,
required: true
}
}
});
</script>
<style lang="scss" scoped>
$preponedClr: lime;
$delayedClr: salmon;
$dateClr: #525151;
$stopExchangeClr: #db8e29;
$stopDefaultClr: #252525;
$stopNameClr: #22a8d1;
.stop-label {
display: flex;
flex-wrap: wrap;
align-items: center;
&[data-sbl='true'] {
.date {
display: none;
}
.name {
background: none;
color: #aaa;
padding: 0;
}
}
.name {
background: $stopNameClr;
padding: 0.3em 0.5em;
display: flex;
align-items: center;
&.misc {
background: gray;
}
}
.date {
background: $dateClr;
padding: 0.3em 0.5em;
}
.stop {
&[data-stop-types='ph'],
&[data-stop-types='ph-pm'],
&[data-stop-types='pm'] {
background: $stopExchangeClr;
}
background: $stopDefaultClr;
&[data-stop-status='delayed'] {
color: $delayedClr;
}
}
.arrival,
.departure {
&[data-status='delayed'] {
s {
color: #999;
}
span {
color: $delayedClr;
}
}
&[data-status='preponed'] {
s {
color: #999;
}
span {
color: $preponedClr;
}
}
}
}
</style>
+68 -39
View File
@@ -35,26 +35,38 @@
<div class="train-driver">
<b
v-if="store.donatorsData.includes(train.driverName)"
:title="$t('donations.driver-message')"
v-if="apiStore.donatorsData.includes(train.driverName)"
data-popup-key="DonatorPopUp"
:data-popup-content="$t('donations.driver-message')"
>
{{ train.driverName }}
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
</b>
<span v-else>{{ train.driverName }}</span>
<button
class="btn--image btn--action btn-timetable"
@click="navigateToJournal"
v-if="extended"
>
<img src="/images/icon-train.svg" alt="" />
{{ $t('trains.journal-button') }}
</button>
</div>
</div>
<div class="general-timetable" v-if="train.timetableData">
<strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
<img
<span
v-if="getSceneriesWithComments(train.timetableData).length > 0"
class="image-warning"
src="/images/icon-warning.svg"
:title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
data-popup-key="TrainCommentsPopUp"
:data-popup-content="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
train.timetableData
)})`"
/>
>
<img class="image-warning" src="/images/icon-warning.svg" />
</span>
</div>
<hr style="margin: 0.25em 0" />
@@ -80,10 +92,12 @@
<div class="status-badges">
<div v-if="!train.currentStationHash" class="train-badge offline">
<img src="/images/icon-offline.svg" alt="" />
{{ $t('trains.scenery-offline') }}
</div>
<div v-if="!train.online" class="train-badge offline">
<img src="/images/icon-offline.svg" alt="" />
Offline {{ lastSeenMessage(train.lastSeen) }}
</div>
</div>
@@ -95,23 +109,20 @@
</section>
<section class="train-stats">
<TrainThumbnail :name="train.locoType" :onlyFirstSegment="true" />
<div class="text--grayed">
{{ train.locoType }}
<span v-if="train.stockList.length > 1">
&nbsp;&bull; {{ $t('trains.cars') }}:
<span class="count">{{ train.stockList.length - 1 }}</span>
</span>
</div>
<StockList :trainStockList="train.stockList" :tractionOnly="true" />
<div>
<span v-for="(stat, i) in STATS.main" :key="stat.name">
<span v-if="i > 0"> &bull; </span>
<span
>{{ `${~~((train as any)[stat.name] * (stat.multiplier || 1))}${stat.unit}` }}
<span>{{ train.speed }}km/h</span>
<div>
<span> {{ train.length }}m</span>
&bull;
<span> {{ (train.mass / 1000).toFixed(1) }}t</span>
<span v-if="train.stockList.length > 1">
&bull;
{{ $t('trains.cars') }}: {{ train.stockList.length - 1 }}
</span>
</span>
</div>
</div>
</section>
</div>
@@ -123,12 +134,15 @@ import styleMixin from '../../mixins/styleMixin';
import trainInfoMixin from '../../mixins/trainInfoMixin';
import Train from '../../scripts/interfaces/Train';
import ProgressBar from '../Global/ProgressBar.vue';
import TrainThumbnail from '../Global/TrainThumbnail.vue';
import { useStore } from '../../store/mainStore';
import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore';
import StockList from '../Global/StockList.vue';
import { usePopupStore } from '../../store/popupStore';
import modalTrainMixin from '../../mixins/modalTrainMixin';
export default defineComponent({
mixins: [trainInfoMixin, styleMixin],
components: { ProgressBar, TrainThumbnail },
mixins: [trainInfoMixin, styleMixin, modalTrainMixin],
components: { ProgressBar, StockList },
props: {
train: {
@@ -136,34 +150,41 @@ export default defineComponent({
required: true
},
extended: {
type: Boolean,
default: true
type: Boolean
}
},
data() {
return {
store: useStore()
store: useMainStore(),
apiStore: useApiStore(),
popupStore: usePopupStore()
};
},
methods: {
navigateToJournal() {
this.$router.push({
path: '/journal/timetables',
query: {
'search-driver': this.train.driverName
}
});
this.closeModal();
}
}
});
</script>
<!-- Global style for TrainThumbnail -->
<style lang="scss">
.train-stats .train-thumbnail {
max-width: 100%;
}
</style>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/badge.scss';
.image-warning {
height: 1em;
margin-left: 0.5em;
vertical-align: middle;
}
.train-stats {
@@ -174,7 +195,7 @@ export default defineComponent({
flex-direction: column;
text-align: center;
gap: 0.25em;
line-height: 1.5em;
}
.train-info {
@@ -193,6 +214,12 @@ export default defineComponent({
vertical-align: text-bottom;
}
.btn-timetable {
display: inline-block;
padding: 0.25em;
margin-left: 0.5em;
}
.timetable-id {
color: #d2d2d2;
}
@@ -237,6 +264,10 @@ export default defineComponent({
flex-wrap: wrap;
gap: 0.25em;
img {
height: 15px;
}
}
.general-timetable {
@@ -264,8 +295,6 @@ export default defineComponent({
grid-template-columns: 1fr;
gap: 1em 0;
text-align: center;
font-size: 1.15em;
}
.general-info,
@@ -6,7 +6,7 @@
<img src="/images/icon-exit.svg" alt="close card" />
</button>
<TrainInfo :train="chosenTrain" :extended="false" ref="trainInfo" />
<TrainInfo :train="chosenTrain" :extended="true" ref="trainInfo" />
<TrainSchedule :train="chosenTrain" tabindex="0" />
</div>
</div>
@@ -15,19 +15,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import trainInfoMixin from '../../mixins/trainInfoMixin';
import TrainInfo from '../TrainsView/TrainInfo.vue';
import TrainSchedule from '../TrainsView/TrainSchedule.vue';
import TrainInfo from './TrainInfo.vue';
import TrainSchedule from './TrainSchedule.vue';
export default defineComponent({
components: { TrainInfo, TrainSchedule },
mixins: [trainInfoMixin, modalTrainMixin],
data() {
return {
isTopBarVisible: false
};
},
mixins: [modalTrainMixin],
activated() {
const contentEl = this.$refs['content'] as HTMLElement;
+8 -5
View File
@@ -1,6 +1,6 @@
<template>
<div class="filters-options" @keydown.esc="showOptions = false">
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<div class="dropdown" @keydown.esc="showOptions = false">
<div class="dropdown_background" v-if="showOptions" @click="showOptions = false"></div>
<button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button">
<img src="/images/icon-filter2.svg" alt="Open filters icon" />
@@ -8,8 +8,8 @@
<span class="active-indicator" v-if="currentOptionsActive"></span>
</button>
<transition name="options-anim">
<div class="options_wrapper" v-if="showOptions">
<transition name="dropdown-anim">
<div class="dropdown_wrapper" v-if="showOptions">
<div class="options_content">
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
<div class="search_content">
@@ -87,6 +87,8 @@
</button>
</div>
</div>
<div tabindex="0" @focus="showOptions = false"></div>
</div>
</transition>
</div>
@@ -184,7 +186,8 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@import '../../styles/filters_options.scss';
@import '../../styles/dropdown.scss';
@import '../../styles/dropdown_filters.scss';
.search_content > div {
margin: 0.5em auto;
+379 -271
View File
@@ -2,83 +2,126 @@
<div class="train-schedule" @click="toggleShowState">
<StockList :trainStockList="train.stockList" />
<!-- <div class="train-stock"> -->
<!-- <ul>
<li v-for="(stockName, i) in train.stockList" :key="i">
<p>{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }} {{ stockName.split(':')[1] }}</p>
<TrainThumbnail :name="stockName" />
</li>
</ul> -->
<!-- </div> -->
<div class="schedule-wrapper" v-if="train.timetableData">
<ul class="stop_list">
<li
v-for="(stop, i) in train.timetableData.followingStops"
<div class="stops">
<div
v-for="(stop, i) in scheduleStops"
:key="i"
class="stop"
:class="addClasses(stop, i)"
:data-status="stop.status"
:data-position="stop.position"
:data-delayed="stop.departureDelay > 0"
:data-stop-type="stop.type"
:data-minor-stop-active="stop.isActive"
:data-last-confirmed="stop.isLastConfirmed"
x
>
<span class="stop_info">
<div class="indicator"></div>
<div class="progress-bar"></div>
<div class="stop-bar"></div>
<span class="distance" v-if="stop.stopDistance">
{{ Math.floor(stop.stopDistance) }}
<span class="distance">
{{ stop.distance ? stop.distance.toFixed(1) : '' }}
</span>
<span class="stop-name" v-html="stop.stopName"> </span>
<div class="progress">
<div class="line line_node line_node-top"></div>
<div class="node"></div>
<div class="line line_node line_node-bottom"></div>
</div>
<StopDate :stop="stop" />
<StopLabel :stop="stop" />
</span>
<div class="stop_line" v-if="i < train.timetableData!.followingStops.length - 1">
<div class="progress-bar"></div>
<div class="stop_line">
<!-- Grid placeholder -->
<div></div>
<div v-if="stop.comments" style="color: salmon">
<b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span>
<div class="progress">
<div class="line line_connection" v-if="i < scheduleStops.length - 1"></div>
</div>
<span
v-if="
stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine &&
!/sbl/gi.test(stop.departureLine!)
"
>
{{ stop.departureLine }}
</span>
<div class="bottom-line-info">
<div class="info-comments" v-if="stop.comments" style="color: salmon">
<img src="/images/icon-warning.svg" alt="icon-warning" width="20" />
<b v-html="stop.comments"></b>
</div>
<span v-else-if="!/sbl/gi.test(stop.departureLine!)">
{{ stop.departureLine }} /
{{ train.timetableData!.followingStops[i + 1].arrivalLine }}
</span>
</div>
<!-- Routes -->
<span
v-if="
stop.departureLine &&
stop.departureLine == scheduleStops[i + 1]?.arrivalLine &&
!/sbl/gi.test(stop.departureLine)
"
>
{{ stop.departureLine }}
</span>
<div class="stop_line" v-else>
<div v-if="stop.comments" style="color: salmon">
<b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span>
<span v-else-if="stop.departureLine && !/sbl/gi.test(stop.departureLine)">
<div>{{ stop.departureLine }}</div>
<div
class="scenery-change-name"
v-if="
i < scheduleStops.length - 1 &&
stop.sceneryName != scheduleStops[i + 1].sceneryName
"
>
{{ scheduleStops[i + 1].sceneryName }}
</div>
<div>
{{ scheduleStops[i + 1].arrivalLine }}
</div>
</span>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import Train from '../../scripts/interfaces/Train';
import { useStore } from '../../store/mainStore';
import StopDate from '../Global/StopDate.vue';
import StopLabel from './StopLabel.vue';
import StockList from '../Global/StockList.vue';
import { TrainStop } from '../../store/typings';
import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore';
export interface TrainScheduleStop {
nameHtml: string;
nameRaw: string;
status: 'confirmed' | 'unconfirmed' | 'stopped';
type: string;
position: 'begin' | 'end' | 'en-route';
arrivalScheduled: number;
arrivalReal: number;
departureScheduled: number;
departureReal: number;
departureDelay: number;
arrivalDelay: number;
duration: number | null;
isActive: boolean;
isLastConfirmed: boolean;
isSBL: boolean;
sceneryName: string | null;
sceneryHash: string;
distance: number;
arrivalLine: string | null;
departureLine: string | null;
comments: string | null;
}
export default defineComponent({
components: { StopDate, StockList },
components: { StopLabel, StockList },
props: {
train: {
type: Object as PropType<Train>,
@@ -90,62 +133,95 @@ export default defineComponent({
emits: ['click'],
setup(props) {
data() {
return {
store: useStore(),
lastConfirmed: computed(() => {
return props.train.timetableData!.followingStops.findIndex(
(stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed && !stops[i + 1]?.stopped
);
}),
activeMinorStops: computed(() => {
const lastMajorConfirmed = props.train.timetableData!.followingStops.findIndex(
(stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed
);
const activeMinorStopList: number[] = [];
if (lastMajorConfirmed + 1 >= props.train.timetableData!.followingStops.length)
return activeMinorStopList;
for (
let i = lastMajorConfirmed + 1;
i < props.train.timetableData!.followingStops.length;
i++
) {
if (/po\.|sbl/gi.test(props.train.timetableData!.followingStops[i].stopNameRAW))
activeMinorStopList.push(i);
else break;
}
return activeMinorStopList;
})
store: useMainStore(),
apiStore: useApiStore()
};
},
computed: {
scheduleStops(): TrainScheduleStop[] {
let currentSceneryIndex = 0;
return (
this.train.timetableData?.followingStops.map((stop, i, arr) => {
if (
i > 0 &&
stop.arrivalLine &&
stop.arrivalLine != arr[i - 1].departureLine &&
!/sbl/gi.test(stop.arrivalLine)
)
currentSceneryIndex++;
return {
nameHtml: stop.stopName,
nameRaw: stop.stopNameRAW,
arrivalScheduled: stop.arrivalTimestamp,
arrivalReal: stop.arrivalRealTimestamp,
departureScheduled: stop.departureTimestamp,
departureReal: stop.departureRealTimestamp,
departureDelay: stop.departureDelay,
arrivalDelay: stop.arrivalDelay,
duration: stop.stopTime,
comments: stop.comments ?? null,
arrivalLine: stop.arrivalLine,
departureLine: stop.departureLine,
type: stop.stopType,
distance: stop.stopDistance,
isActive: this.activeMinorStops.includes(i),
isLastConfirmed: this.lastConfirmed === i && !stop.terminatesHere,
isSBL: /sbl/gi.test(stop.stopName),
position: stop.beginsHere ? 'begin' : stop.terminatesHere ? 'end' : 'en-route',
sceneryHash: '',
sceneryName: this.train.timetableData!.sceneryNames[currentSceneryIndex],
status: stop.confirmed ? 'confirmed' : stop.stopped ? 'stopped' : 'unconfirmed'
};
}) ?? []
);
},
lastConfirmed() {
return this.train.timetableData?.followingStops.findIndex(
(stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed && !stops[i + 1]?.stopped
);
},
activeMinorStops() {
if (!this.train.timetableData) return [];
const lastMajorConfirmed = this.train.timetableData.followingStops.findIndex(
(stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed
);
const activeMinorStopList: number[] = [];
if (lastMajorConfirmed + 1 >= this.train.timetableData.followingStops.length)
return activeMinorStopList;
for (
let i = lastMajorConfirmed + 1;
i < this.train.timetableData!.followingStops.length;
i++
) {
if (/po\.|sbl/gi.test(this.train.timetableData!.followingStops[i].stopNameRAW))
activeMinorStopList.push(i);
else break;
}
return activeMinorStopList;
}
},
methods: {
toggleShowState() {
this.$emit('click');
},
addClasses(stop: TrainStop, index: number) {
return {
confirmed: stop.confirmed,
stopped: stop.stopped,
begin: stop.beginsHere,
end: stop.terminatesHere,
delayed: stop.departureDelay > 0,
sbl: /sbl/gi.test(stop.stopName),
[stop.stopType.replaceAll(', ', '-')]:
stop.stopType.match(new RegExp('ph|pm|pt')) && !stop.confirmed && !stop.beginsHere,
'minor-stop-active': this.activeMinorStops.includes(index),
'last-confirmed': index == this.lastConfirmed && !stop.terminatesHere
};
},
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = '/images/icon-unknown.png';
}
}
});
@@ -155,17 +231,18 @@ export default defineComponent({
@import '../../styles/responsive.scss';
$barClr: #b1b1b1;
$confirmedClr: #18d818;
$confirmedClr: #4ae24a;
$stoppedClr: #f55f31;
$haltClr: #f8bb36;
$stopNameClr: #22a8d1;
$blinkAnim: 0.5s ease-in-out alternate infinite blink;
@keyframes blink {
from {
background-color: $barClr;
border-color: $barClr;
}
to {
background-color: $confirmedClr;
border-color: $confirmedClr;
}
}
@@ -181,216 +258,247 @@ $stopNameClr: #22a8d1;
margin-top: 1em;
}
.progress-bar {
position: absolute;
z-index: 10;
top: -1px;
left: -17px;
height: 100%;
width: 3px;
background-color: $barClr;
}
.stop-name {
background: $stopNameClr;
padding: 0.3em 0.5em;
display: flex;
align-items: center;
&.misc {
background: gray;
}
}
.stop-comment {
background: forestgreen;
padding: 0.3em 0.5em;
max-width: 250px;
overflow: hidden;
white-space: nowrap;
width: 2em;
cursor: pointer;
&:hover {
text-overflow: ellipsis;
width: 100%;
}
img {
width: 1em;
}
span {
font-size: 0.8em;
}
}
ul.stop_list {
margin-left: 2.5em;
}
ul.stop_list > li.stop {
position: relative;
.stops {
display: flex;
flex-direction: column;
overflow-y: hidden;
gap: 5px;
padding: 0 0.5em;
padding: 5px 0;
}
&.sbl {
.stop-date {
display: none;
}
.stop-name {
background: none;
color: #aaa;
padding: 0;
}
}
&[class*='ph'] > .stop_info > .indicator {
border-color: $stopNameClr;
}
&[class*='pt'] > .stop_info > .indicator {
border-color: #818181;
}
&.begin {
.stop_info > .indicator {
.stop {
// Begin stop
&[data-position='begin'] {
.node {
border-color: lightgreen;
}
.stop_info > .progress-bar {
background: lightgreen;
.line_node-top {
display: none;
}
}
&.end {
.stop_info > .indicator {
// End stop
&[data-position='end'] {
.node {
border-color: salmon;
}
.stop_info > .progress-bar {
background: salmon;
.line_node-bottom {
display: none;
}
}
&.minor-stop-active {
.stop_info > .progress-bar {
animation: 0.5s ease-in-out alternate infinite blink;
}
.stop_line > .progress-bar {
animation: 0.5s ease-in-out alternate infinite blink;
}
// Stop types
&[data-stop-type*='pt'] .node {
border-color: #818181;
}
&.last-confirmed {
.stop_line > .progress-bar {
animation: 0.5s ease-in-out alternate infinite blink;
}
&[data-stop-type*='ph'] .node {
border-color: $haltClr;
}
&.confirmed {
.stop_info {
> .progress-bar {
background-color: $confirmedClr;
}
> .indicator {
border-color: $confirmedClr;
}
&[data-minor-stop-active='true'] {
.progress > .line {
animation: $blinkAnim;
}
.stop_line > .progress-bar {
background-color: $confirmedClr;
}
}
&.stopped {
.stop_info {
> .indicator {
border-color: $stoppedClr;
}
> .stop-bar {
background: $stoppedClr;
& + div {
.progress > .line_node-top {
animation: $blinkAnim;
}
}
}
.stop_line {
font-size: 0.8em;
color: #ccc;
// Last confirmed outpost / checkpoint
&[data-last-confirmed='true'] {
.progress > .line_connection {
animation: $blinkAnim;
}
padding: 0.35em 0;
.progress > .line_node-bottom {
animation: $blinkAnim;
}
position: relative;
.line-segment {
color: $barClr;
font-weight: 500;
& + div {
.progress > .line_node-top {
animation: $blinkAnim;
}
}
}
.stop_info {
display: flex;
position: relative;
text-align: center;
flex-wrap: wrap;
// Confirmed status
&[data-status='confirmed'] {
.progress > .node {
border-color: $confirmedClr;
}
.progress > .line {
border-left: 2px solid $confirmedClr;
border-right: 2px solid $confirmedClr;
}
}
.stop-bar {
// Stopped status
&[data-status='stopped'] {
.progress > .node {
border-color: $stoppedClr;
}
.progress > .line_node {
border-color: $stoppedClr;
}
}
// Unused so far
&[data-track-count-departure='2'] {
.progress > .line {
width: 6px;
}
}
&[data-track-count-arrival='2'] {
.progress > .line_node-top {
width: 6px;
}
}
&[data-track-count-arrival='1'] {
.progress > .line_node-top {
width: 4px;
}
}
&[data-electrified-departure] {
.stop_line > .line-speed > .speed-departure {
color: #00c1c7;
}
}
&[data-electrified-arrival] {
.stop_line > .line-speed > .speed-next-arrival {
color: #00c1c7;
}
}
}
.stop_info,
.stop_line {
display: grid;
grid-template-columns: 30px 40px auto 1fr;
}
.line-speed {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #9b9b9b;
gap: 10px;
}
.stop_info {
position: relative;
text-align: center;
}
.stop_line {
font-size: 0.8em;
color: #ccc;
margin-top: 5px;
}
.distance {
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75em;
}
.progress {
position: relative;
& > .node {
position: absolute;
top: 0;
left: -17px;
z-index: 10;
width: 3px;
height: 100%;
}
.distance {
position: absolute;
top: 50%;
transform: translate(-100%, -50%);
margin-left: -1.75rem;
font-size: 0.75em;
color: #d6d6d6;
}
.indicator {
position: absolute;
z-index: 11;
top: 50%;
left: -1rem;
transform: translate(-47%, -50%);
left: 50%;
transform: translate(-50%, -50%);
z-index: 15;
text-align: right;
width: 15px;
height: 15px;
background: var(--clr-secondary);
border: 3px solid $barClr;
background-color: var(--clr-secondary);
border: 4px solid $barClr;
border-radius: 100%;
}
& > .line {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, 0);
z-index: 10;
height: 100%;
// background-color: $barClr;
border-left: 2px solid $barClr;
border-right: 2px solid $barClr;
&.line_connection {
transform: translate(-50%, -6px);
height: calc(100% + 12px);
// height: calc(100% + 0.25em);
}
&.line_node-top {
top: 0;
height: 50%;
}
&.line_node-bottom {
top: 50%;
height: 50%;
}
&.line_stop {
border-color: $stoppedClr;
z-index: 11;
}
}
}
.info-comments {
display: flex;
align-items: center;
gap: 0.25em;
margin: 0.25em 0;
img {
height: 1.2em;
}
}
.bottom-line-info {
.scenery-change-name {
position: relative;
margin: 0.25em 0;
&::before {
content: '';
position: absolute;
height: 2px;
width: 30px;
background-color: #aaa;
top: 50%;
right: calc(100% + 5px);
transform: translate(0, -50%);
}
}
}
</style>
+282
View File
@@ -0,0 +1,282 @@
<template>
<div class="dropdown" @keydown.esc="showOptions = false" @focusout="showOptions = false">
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button">
<img src="/images/icon-stats.svg" alt="Open filters icon" />
{{ $t('train-stats.stats-button') }}
</button>
<transition name="dropdown-anim">
<div class="dropdown_wrapper" v-if="showOptions">
<h1 class="text--primary">
<img src="/images/icon-stats.svg" alt="Open filters icon" />
{{ $t('train-stats.title') }}
</h1>
<hr style="margin: 0.5em 0" />
<div v-if="apiStore.dataStatuses.connection == Status.Loaded && regionTrains.length > 0">
<div class="top-list general">
<transition-group tag="ul" name="stats-anim">
<li class="badge" key="timetable-count">
<span>{{ $t('train-stats.timetable-count') }}</span>
<span>
<b>{{ regionTrainsWithTT.length }}</b>
</span>
</li>
<li class="badge" key="avg-speed">
<span>{{ $t('train-stats.avg-speed') }}</span>
<span>
<b>{{ stats.avgSpeed.toFixed(1) }} km/h</b>
</span>
</li>
<li class="badge" key="avg-distance">
<span>{{ $t('train-stats.avg-timetable') }}</span>
<span>
<b>{{ stats.avgDistance.toFixed(1) }} km</b>
</span>
</li>
</transition-group>
</div>
<div class="top-list categories">
<h3>{{ $t('train-stats.top-categories') }}</h3>
<transition-group tag="ul" name="stats-anim">
<li class="badge" v-for="top in stats.topCategories" :key="top.name">
<span>{{ top.name }}</span>
<span>{{ top.count }}</span>
</li>
</transition-group>
<span class="no-data" v-if="stats.topCategories.length == 0">
{{ $t('train-stats.no-timetables') }}
</span>
</div>
<div class="top-list vehicles">
<h3>{{ $t('train-stats.top-vehicles') }}</h3>
<transition-group tag="ul" name="stats-anim">
<li class="badge" v-for="top in stats.topVehicles" :key="top.name">
<span>{{ top.name }}</span>
<span>{{ top.count }}</span>
</li>
</transition-group>
<span class="no-data" v-if="stats.topVehicles.length == 0">
{{ $t('train-stats.no-vehicles') }}
</span>
</div>
<div class="top-list vehicle-types">
<h3>{{ $t('train-stats.top-units') }}</h3>
<transition-group tag="ul" name="stats-anim">
<li class="badge" v-for="top in stats.topUnits.slice(0, 7)" :key="top.name">
<span>{{ top.name }}</span>
<span>{{ top.count }}</span>
</li>
</transition-group>
<span class="no-data" v-if="stats.topUnits.length == 0">
{{ $t('train-stats.no-units') }}
</span>
</div>
</div>
<div v-else-if="apiStore.dataStatuses.connection != Status.Loaded">
{{ $t('train-stats.stats-loading') }}
</div>
<div class="no-data" v-else>
{{ $t('train-stats.no-stats') }}
</div>
</div>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
import { Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
interface ITop {
name: string;
count: number;
}
interface IStats {
timetableCount: number;
avgSpeed: number;
avgDistance: number;
topCategories: ITop[];
topVehicles: ITop[];
topUnits: ITop[];
}
function compareTop(top1: ITop, top2: ITop) {
return Math.sign(top2.count - top1.count) || top1.name.localeCompare(top2.name, 'pl-PL');
}
export default defineComponent({
data() {
return {
showOptions: false,
store: useMainStore(),
apiStore: useApiStore(),
Status: Status.Data
};
},
computed: {
regionTrains() {
return this.store.trainList.filter((train) => train.region == this.store.region.id);
},
regionTrainsWithTT() {
return this.regionTrains.filter((train) => train.timetableData);
},
stats() {
const stats = this.regionTrains.reduce(
(acc, train, i, arr) => {
// AVG SPEED
acc.avgSpeed += train.speed / arr.length;
// TOP VEHICLES
const locoType = train.locoType.split('-')[0];
const topVehicle = acc.topVehicles.find((top) => top.name == locoType);
if (!topVehicle) acc.topVehicles.push({ name: locoType, count: 1 });
else topVehicle.count++;
// TOP UNITS
const unitType = train.locoType;
const topUnit = acc.topUnits.find((top) => top.name == unitType);
if (!topUnit) acc.topUnits.push({ name: unitType, count: 1 });
else topUnit.count++;
if (train.timetableData !== undefined) {
acc.timetableCount++;
// AVG DISTANCE
acc.avgDistance += train.timetableData.routeDistance;
// TOP CATEGORIES
const topCategory = acc.topCategories.find(
(top) => top.name == train.timetableData!.category
);
if (!topCategory)
acc.topCategories.push({ name: train.timetableData!.category, count: 1 });
else topCategory.count++;
}
if (i == arr.length - 1 && acc.timetableCount != 0) {
acc.avgDistance /= acc.timetableCount;
}
return acc;
},
{
timetableCount: 0,
avgDistance: 0,
avgSpeed: 0,
topCategories: [],
topUnits: [],
topVehicles: []
} as IStats
);
stats.topCategories.sort(compareTop);
stats.topUnits.sort(compareTop);
stats.topVehicles.sort(compareTop);
return stats;
}
},
methods: {
toggleShowOptions() {
this.showOptions = !this.showOptions;
this.$nextTick(() => {
if (this.showOptions) (this.$refs['button'] as HTMLButtonElement)?.focus();
});
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/dropdown.scss';
@import '../../styles/badge.scss';
@import '../../styles/responsive.scss';
h1 img {
vertical-align: text-bottom;
}
h3 {
margin: 0.5em 0;
}
.no-data {
font-size: 1.1em;
color: #ccc;
}
.top-list ul {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
// @include smallScreen {
// justify-content: center;
// }
}
.badge {
margin: 0;
& > span:first-child {
background-color: $accentCol;
color: black;
}
}
.dropdown_wrapper {
max-width: 600px;
}
.stats-anim {
&-move,
&-enter-active,
&-leave-active {
transition: all 250ms ease;
}
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateX(5px);
}
&-leave-active {
position: absolute;
}
}
@include smallScreen {
h1,
.no-data {
text-align: center;
}
}
</style>
+13 -13
View File
@@ -1,17 +1,13 @@
<template>
<transition name="status-anim" mode="out-in" tag="div" class="train-table">
<div :key="store.dataStatuses.trains">
<div class="table-info" key="offline" v-if="store.isOffline">
<div :key="apiStore.dataStatuses.connection">
<div class="table-warning" key="offline" v-if="store.isOffline">
{{ $t('app.offline') }}
</div>
<Loading v-else-if="trains.length == 0 && store.dataStatuses.trains == 0" key="loading" />
<Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" />
<div
class="table-info"
key="no-trains"
v-else-if="trains.length == 0 && store.dataStatuses.trains != 0"
>
<div class="table-warning" key="no-trains" v-else-if="trains.length == 0">
{{ $t('trains.no-trains') }}
</div>
@@ -24,7 +20,7 @@
@click.stop="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
>
<TrainInfo :train="train" />
<TrainInfo :train="train" :extended="false" />
</li>
</transition-group>
</div>
@@ -35,10 +31,11 @@
import { defineComponent, inject, PropType, Ref } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import Train from '../../scripts/interfaces/Train';
import { useStore } from '../../store/mainStore';
import { useMainStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue';
import TrainInfo from './TrainInfo.vue';
import { Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
components: { Loading, TrainInfo },
@@ -53,7 +50,8 @@ export default defineComponent({
mixins: [modalTrainMixin],
setup() {
const store = useStore();
const store = useMainStore();
const apiStore = useApiStore();
const searchedTrain = inject('searchedTrain') as Ref<string>;
const searchedDriver = inject('searchedDriver') as Ref<string>;
@@ -61,6 +59,8 @@ export default defineComponent({
searchedTrain,
searchedDriver,
store,
apiStore,
Status: Status.Data,
sorterActive: inject('sorterActive') as {
id: string | number;
dir: number;
@@ -72,7 +72,7 @@ export default defineComponent({
dataStatus() {
if (this.store.isOffline) return Status.Data.Offline;
if (this.trains.length == 0 && this.store.dataStatuses.trains == Status.Data.Loading)
if (this.trains.length == 0 && this.apiStore.dataStatuses.connection == Status.Data.Loading)
return Status.Data.Loading;
return Status.Data.Loaded;
@@ -105,7 +105,7 @@ export default defineComponent({
overflow-x: hidden;
}
.table-info {
.table-warning {
text-align: center;
padding: 1em 0;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+31 -2
View File
@@ -1,13 +1,14 @@
{
"optionSections": [
"status",
"timetables",
"reality",
"package-access",
"access",
"control",
"addons",
"blockades",
"signals",
"status"
"addons"
],
"options": [
@@ -138,6 +139,20 @@
"value": true,
"defaultValue": true
},
{
"id": "ASDEK",
"name": "ASDEK",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "noASDEK",
"name": "noASDEK",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "SBL",
"name": "SBL",
@@ -228,6 +243,20 @@
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "withActiveTimetables",
"name": "withActiveTimetables",
"section": "timetables",
"value": true,
"defaultValue": true
},
{
"id": "withoutActiveTimetables",
"name": "withoutActiveTimetables",
"section": "timetables",
"value": true,
"defaultValue": true
}
],
"sliders": [
+19
View File
@@ -0,0 +1,19 @@
import enLang from './locales/en.json';
import plLang from './locales/pl.json';
import { createI18n } from 'vue-i18n';
const i18n = createI18n({
locale: 'pl',
legacy: false,
warnHtmlMessage: false,
fallbackLocale: 'pl',
messages: {
en: enLang,
pl: plLang
},
enableLegacy: false
});
export default i18n;
+121 -56
View File
@@ -2,6 +2,7 @@
"donations": {
"button-title": "TOSS A COIN",
"header": "Toss a coin to Stacjownik!",
"donator-title": "Project is supported by more than <b>{count}</b> people, including:",
"p1": "<b>Hello o7!</b> This is Spythere, the creator of Stacjownik, Pojazdownik and several other applications that enhance the gameplay of Train Driver 2!",
"p2": "{b1} is a completely free tool, created and continuously developed for the Train Driver 2 simulator community since 2020. However, a part of the project is sustained solely through my private financial contribution. Features such as {b2} or {b3} (operating on my {link} - to which you are warmly invited) must function on a dedicated server where they can collect and process data, and then display it on the website.",
"p2-b1": "Stacjownik",
@@ -9,12 +10,13 @@
"p2-b3": "Stacjobot (Stacjownik bot)",
"p2-a1": "Discord server",
"p3": "<b>If you have the means and would like to support my work, I would be grateful for any financial assistance that could help cover at least some of the server costs and further enhance the capabilities of the application!</b>",
"p4": "Every person who decides to contribute at least {b1} for the development of Stacjownik, will receive (upon a personal request) {img}{b2} of username in the \"Sceneries\" and \"Trains\" tabs of the application, as well as on my Discord server (after verifying the payment author, preferably by providing the username directly with the payment).",
"p4": "Every person who decides to contribute at least {b1} (in case of PayPal it must be a payment including additional transaction fees) for the development of Stacjownik, will receive (upon a personal request) {img}{b2} of username in the app and on my Discord server (after verifying the payment author, preferably by providing the username directly with the payment).",
"p4-b1": "5 PLN",
"p4-b2": "a symbolic highlight",
"p5": "Thank you and enjoy the app!<br />~ Spythere",
"action-exit": "Maybe next time...",
"action-confirm": "DONATE!",
"action-paypal": "DONATE WITH PAYPAL",
"action-buycoffee": "BUY ME A COFFEE!",
"dispatcher-message": "Dispatcher supporting the Stacjownik project!",
"driver-message": "Driver supporting the Stacjownik project!"
},
@@ -46,6 +48,10 @@
"confirm-button": "UPDATE NOW",
"later-button": "LATER"
},
"vehicle-preview": {
"loading": "Loading preview...",
"error": "Oops! The vehicle preview seems to be missing! :/"
},
"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!",
@@ -56,20 +62,6 @@
"S5-dispatchers": "<b>S5 signal</b> <br> Cannot load dispatchers status data!",
"S5-trains": "<b>S5 signal</b> <br> Cannot load online trains data!"
},
"desc": {
"control-type": "Control type: ",
"signals-type": "Signals type: ",
"SBL": "This scenery has automatic block signalling (ABS/SBL) system on following routes: ",
"SUP": "Requires the SUP application (level crossing remote control simulator)",
"TWB-all": "This scenery has two-way route blockade on all routes",
"TWB-routes": "This scenery has two-way route blockade on following routes: ",
"default": "This scenery is available by default",
"non-public": "This scenery is not public",
"unknown": "This scenery isn't recognizable right now",
"unavailable": "This scenery is unavailable",
"abandoned": "This scenery is no longer supported by its creators",
"real": "Scenery with real lines: "
},
"signals": {
"title": "Signal type",
"współczesna": "modern",
@@ -143,7 +135,8 @@
"filter-withComments": "COMMENTS",
"filter-twr": "HIGH RISK CARGO",
"filter-skr": "EXCEEDED GAUGE",
"filter-twr-skr": "ALL TYPES",
"filter-twr-skr": "BOTH TYPES",
"filter-all-specials": "ALL",
"filter-common": "NO WARNINGS",
"filter-passenger": "PASSENGER",
"filter-freight": "FREIGHT",
@@ -155,9 +148,9 @@
"filter-clear": "CLEAR FILTERS",
"filter-section-timetable-status": "TIMETABLE STATUS",
"filter-section-twrskr": "WARNINGS",
"filter-section-special": "SPECIAL TYPE",
"filter-all": "ALL ENTRIES",
"filter-all-statuses": "ALL",
"filter-abandoned": "ABANDONED",
"filter-fulfilled": "FULFILLED",
"filter-active": "ACTIVE"
@@ -174,7 +167,8 @@
"signals": "SIGNALLING",
"addons": "ADDITIONAL PROGRAMS",
"blockades": "BLOCK SIGNALLING",
"status": "ONLINE STATUS"
"status": "ONLINE STATUS",
"timetables": "ACTIVE TIMETABLES"
},
"all-available": "ALL AVAILABLE",
@@ -208,6 +202,9 @@
"SUP": "SUP (RASP-UZK)",
"noSUP": "WITHOUT SUP",
"ASDEK": "ASDEK",
"noASDEK": "NO ASDEK",
"SBL": "AUTOMATIC (SBL)",
"PBL": "SEMIAUTOMATIC (PBL)",
@@ -217,6 +214,10 @@
"historical": "HISTORICAL",
"free": "FREE",
"occupied": "OCCUPIED",
"withActiveTimetables": "ACTIVE",
"withoutActiveTimetables": "NO ACTIVE",
"sliders": {
"min-lvl": "MIN. REQUIRED DISPATCHER LEVEL",
"max-lvl": "MAX. REQUIRED DISPATCHER LEVEL",
@@ -225,7 +226,10 @@
"routes-2t-cat": "MIN. CATENARY DOUBLE TRACK ROUTES",
"routes-2t-other": "MIN. OTHER DOUBLE TRACK ROUTES"
},
"authors-search": "Search by author (other filters apply)",
"authors-search": "SEARCH BY AUTHOR NAME (other filters apply):",
"authors-placeholder": "Enter the author nickname...",
"authors-button-title": "Search",
"minimum-hours-title": "SHOW ONLY SCENERIES UNTIL:",
"now": "NOW",
"hour": "h",
@@ -236,18 +240,41 @@
"close": "CLOSE FILTERS"
},
"sceneries": {
"station": "Station",
"min-lvl": "Min. dispatcher\nlevel",
"status": "Status",
"dispatcher": "Dispatcher",
"dispatcher-lvl": "Dispatcher\nlevel",
"routes": "Routes\ndouble / single",
"general": "General info",
"user": "Drivers online",
"spawn": "Spawns online",
"timetableAll": "Active timetables",
"timetableConfirmed": "Confirmed timetables",
"timetableUnconfirmed": "Unconfirmed timetables",
"headers": {
"station": "Scenery",
"min-lvl": "Scenery\nlevel",
"status": "Status",
"dispatcher": "Dispatcher",
"dispatcher-lvl": "Dispatcher\nlevel",
"routes-single": "1-track\nroutes",
"routes-double": "2-track\nroutes",
"general": "General info",
"user": "Drivers online",
"like": "Dispatcher rating",
"spawn": "Spawns online",
"timetableAll": "Active timetables",
"timetableConfirmed": "Confirmed timetables",
"timetableUnconfirmed": "Unconfirmed timetables"
},
"info": {
"control-type": "Control type: ",
"signals-type": "Signals type: ",
"SBL": "This scenery has automatic block signalling (ABS/SBL) system on following routes: ",
"SUP": "Requires the SUP program (level crossing remote control)",
"ASDEK": "Requires the ASDEK program (defect detection of moving rolling stock)",
"TWB-all": "This scenery has two-way route blockade on all routes",
"TWB-routes": "This scenery has two-way route blockade on following routes: ",
"default": "This scenery is available by default",
"non-public": "This scenery is not public",
"unavailable": "This scenery is unavailable",
"abandoned": "This scenery is no longer supported by its creators",
"unknown": "This scenery isn't recognizable right now",
"real": "Scenery with real lines: ",
"double-track-routes-catenary": "Electrified double-track routes count: ",
"single-track-routes-catenary": "Electrified single-track routes count: ",
"double-track-routes-other": "Not electrified double-track routes count: ",
"single-track-routes-other": "Not electrified single-track routes count: "
},
"no-stations": "No stations to show here!",
"scenery-search": "Search for scenery..."
},
@@ -292,7 +319,24 @@
"last-seen-ago": "since {minutes} minutes",
"scenery-offline": "Offline ride",
"timeout": "An error occured while trying to refresh SWDR timetable data!"
"timeout": "An error occured while trying to refresh SWDR timetable data!",
"journal-button": "DRIVER'S JOURNAL"
},
"train-stats": {
"stats-button": "STATISTICS",
"title": "STATISTICS ONLINE",
"timetable-count": "ACTIVE TIMETABLES",
"avg-speed": "AVG SPEED",
"avg-timetable": "AVG TIMETABLE",
"top-categories": "Timetable categories",
"top-vehicles": "Vehicles online",
"top-units": "Common units online",
"stats-loading": "Loading...",
"no-timetables": "No active timetables in this region!",
"no-vehicles": "No active vehicles in this region!",
"no-units": "No active units in this region!",
"no-stats": "No statistics available for the current region!"
},
"journal": {
"title": "DISPATCHER HISTORY",
@@ -313,6 +357,7 @@
"timetable-active": "ACTIVE",
"timetable-fulfilled": "FULFILLED",
"timetable-abandoned": "ABANDONED",
"timetable-online-button": "ONLINE TIMETABLE",
"online-since": "ONLINE SINCE",
"duty-lasted": "The duty lasted",
@@ -331,29 +376,49 @@
"last-seen-at": "Last seen at",
"currently-at": "Currently at",
"stats-title": "DRIVING STATISTICS OF",
"driver-stats": {
"button": "DRIVER STATS",
"title": "{name}'s DRIVER STATS",
"info": "Enter a proper nickname into filters [F] to see user's driving statistics!",
"timetables": "TIMETABLES",
"longest-timetable": "LONGEST TIMETABLE",
"avg-timetable": "AVERAGE TIMETABLE LENGTH",
"distance": "DISTANCE",
"stations": "STATIONS"
},
"stats-timetables": "TIMETABLES",
"stats-longest-timetable": "LONGEST TIMETABLE",
"stats-avg-timetable": "AVERAGE TIMETABLE LENGTH",
"stats-distance": "DISTANCE",
"stats-stations": "STATIONS",
"daily-stats": {
"button": "DAILY STATS",
"title": "STATS OF THE DAY",
"info": "Today's statistics are unavailable yet!",
"total": "Issued timetables: {count} (total distance: {distance})",
"longest": "The longest timetable: #{id} (made by {author} for {driver}, distance: {distance})",
"most-active-dr": "The most active dispatcher: {dispatcher} (created {count})",
"most-active-dr-many": "The most active dispatchers: {dispatchers} (created {count} each)",
"most-active-driver": "The most active driver: {driver} (total driven distance: {distance})",
"longest-duties": "The longest service: {dispatcher} at {station} (duration: {duration})",
"count": "timetable | timetables",
"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})",
"rippedSwitches": "RIPPED SWITCHES",
"derailments": "DERAILMENTS",
"skippedStopSignals": "SKIPPED STOP SIGNALS",
"radioStops": "RADIOSTOPS",
"kills": "KILLS"
},
"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!",
"dispatcher-stats": {
"button": "DISPATCHER STATS",
"title": "{name}'s DISPATCHER STATS",
"empty": "This user has no statistics saved yet!",
"info": "Enter a proper nickname into filters [F] to see user's dispatcher statistics!",
"services-count": "SERVICES",
"service-max": "MAX SERVICE DURATION",
"service-avg": "AVG SERVICE DURATION",
"timetables-count": "ISSUED TIMETABLES",
"timetables-sum": "TIMETABLES DISTANCE SUM",
"timetables-max": "LONGEST TIMETABLE",
"timetables-avg": "AVG TIMETABLE DISTANCE"
},
"stats-loading": "Fetching statistics...",
"stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/",
@@ -389,8 +454,8 @@
"two-way-routes": "Two way routes",
"option-active-timetables": "Active timetables",
"option-timetables-history": "Timetables history",
"option-dispatchers-history": "Dispatchers history",
"option-timetables-history": "Timetables history PL1",
"option-dispatchers-history": "Dispatchers history PL1",
"timetable-author-title": "Issued by",
"timetable-author-unknown": "Author unknown",
-434
View File
@@ -1,434 +0,0 @@
{
"general": {
"and": " oraz ",
"refresh": "ODŚWIEŻ",
"TWR": "Towar niebezpieczny wysokiego ryzyka",
"SKR": "Przekroczona skrajnia"
},
"app": {
"sceneries": "SCENERIE",
"trains": "POCIĄGI",
"journal": "DZIENNIK",
"loading": "Pobieranie danych...",
"support": "Wspomóż projekt",
"error": "Wystąpił problem z załadowaniem danych!",
"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-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": {
"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-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!",
"S3": "<b>Sygnał S3</b> <br> Pobieranie danych...",
"S5-timetables": "<b>Sygnał S5</b> <br> Rozkłady jazdy mogą być niekompletne!",
"S5-dispatchers": "<b>Sygnał S5</b> <br> Błąd podczas pobierania danych o statusach dyżurnych ruchu!",
"S5-trains": "<b>Sygnał S5</b> <br> Błąd podczas pobierania danych o pociągach online!"
},
"desc": {
"control-type": "Sterowanie: ",
"signals-type": "Sygnalizacja: ",
"SBL": "Sceneria posiada SBL na szlakach: ",
"SUP": "Wymaga programu SUP do kontroli systemu RASP-UZK",
"TWB-all": "Sceneria posiada blokadę dwukierunkową na wszystkich szlakach",
"TWB-routes": "Sceneria posiada blokadę dwukierunkową na szlakach: ",
"default": "Sceneria dostępna domyślnie w paczce z grą",
"non-public": "Sceneria niepubliczna",
"unavailable": "Sceneria niedostępna",
"unknown": "Nieznana sceneria",
"real": "Sceneria z realnymi liniami kolejowymi: ",
"abandoned": "Sceneria wycofana z rozgrywki"
},
"signals": {
"title": "Sygnalizacja",
"współczesna": "współczesna",
"mieszana": "mieszana",
"kształtowa": "kształtowa",
"historyczna": "historyczna"
},
"controls": {
"title": "Sterowanie",
"SPK": "SPK",
"SCS": "SCS",
"SCS-SPK": "SCS/SPK",
"SPE": "SPE",
"ręczne": "ręczne",
"ręczne+SPK": "ręczne z SPK",
"ręczne+SCS": "ręczne z SCS",
"mechaniczne": "mechaniczne",
"mechaniczne+SPK": "mechaniczne z SPK",
"mechaniczne+SCS": "mechaniczne z SCS"
},
"status": {
"online": "DO ",
"free": "WOLNA",
"ending": "KOŃCZY",
"not-signed": "NIEZALOGOWANY",
"no-limit": "BEZ LIMITU",
"unavailable": "NIEDOSTĘPNY",
"brb": "Z/W",
"no-space": "BRAK MIEJSCA",
"unknown": "NIEZNANY"
},
"options": {
"filters": "FILTRY",
"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": {
"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",
"afkStatus": "Z/W",
"noSpaceStatus": "BRAK MIEJSCA",
"unavailableStatus": "NIEDOSTĘPNY",
"title": "FILTRUJ STACJE",
"default": "DOMYŚLNA",
"not-default": "POZA PACZKĄ",
"real": "REALNA",
"fictional": "FIKCYJNA",
"unavailable": "NIEDOSTĘPNA",
"non-public": "NIEPUBLICZNA",
"abandoned": "WYCOFANA",
"SPK": "SPK",
"SPK-R": "SPK + RĘCZNE",
"SPK-M": "SPK + MECH.",
"SCS": "SCS",
"SCS-R": "SCS + RĘCZNE",
"SCS-M": "SCS + MECH.",
"SPE": "SPE",
"manual": "RĘCZNE",
"SUP": "SUP (RASP-UZK)",
"noSUP": "BEZ SUP",
"SBL": "SAMOCZYNNA",
"PBL": "PÓŁSAMOCZYNNA",
"mechanical": "MECHANICZNE",
"modern": "WSPÓŁCZESNA",
"semaphores": "KSZTAŁTOWA",
"mixed": "MIESZANA",
"historical": "HISTORYCZNA",
"free": "WOLNA",
"occupied": "ZAJĘTA",
"sliders": {
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
"routes-1t-cat": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
"routes-1t-other": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
"routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
"routes-2t-other": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
},
"authors-search": "Szukaj autora (uwzględnia inne filtry)",
"minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:",
"now": "TERAZ",
"hour": " godz.",
"no-limit": "BEZ LIMITU",
"include-selected": "POKAŻ ZAZNACZONE",
"save": "ZAPAMIĘTAJ FILTRY",
"reset": "RESETUJ FILTRY",
"close": "ZAMKNIJ FILTRY"
},
"sceneries": {
"station": "Stacja",
"abbr": "Skrót\nposterunku",
"min-lvl": "Min. poziom\ndyżurnego",
"status": "Status",
"dispatcher": "Dyżurny",
"dispatcher-lvl": "Poziom\ndyżurnego",
"routes": "Szlaki\n2tor / 1tor",
"general": "Informacje\nogólne",
"user": "Maszyniści online",
"spawn": "Otwarte spawny",
"timetableAll": "Aktywne rozkłady jazdy",
"timetableConfirmed": "Zatwierdzone rozkłady jazdy",
"timetableUnconfirmed": "Niezatwierdzone rozkłady jazdy",
"no-stations": "Brak stacji do wyświetlenia!",
"scenery-search": "Wyszukaj scenerię..."
},
"trains": {
"no-trains": "Brak pociągów do wyświetlenia!",
"loading": "Pobieranie danych o pociągach...",
"offline": "Przejazd offline",
"stats": "STATYSTYKI RUCHU",
"stats-speed": "PRĘDKOŚCI POCIĄGÓW (MIN, ŚR, MAX) [km/h]",
"stats-length": "DŁUGOŚCI ROZKŁADÓW (MIN, ŚR, MAX) [km]",
"stats-categories": "KATEGORIE RJ",
"stats-special-twr": "WYSOKIEGO RYZYKA",
"stats-special-skr": "PRZEKROCZONA SKRAJNIA",
"stats-locos": "NAJCZĘSTSZE JEDNOSTKI",
"current-scenery": "na scenerii",
"current-signal": "przy semaforze",
"current-track": "na szlaku",
"delayed": "Opóźniony: ",
"preponed": "Przed czasem: ",
"on-time": "Planowo",
"route-progress": "Postęp: ",
"detailed-timetable": "Szczegółowy rozkład jazdy pociągu ",
"via-title": "Przez: ",
"no-timetable": "brak rozkładu jazdy",
"distance-exceeded": "Uwaga! Z powodu wewnętrznego błędu serwera TD2, rozkłady jazdy o kilometrażu powyżej 200km mogą być niepoprawne!",
"cars": "Wagony",
"EZT": "EZT",
"SZT": "SZT",
"loco-electric": "Elektrowóz",
"loco-diesel": "Spalinowóz",
"timetable-comments": "Pociąg z uwagami eksploatacyjnymi",
"comment": "Uwagi eksploatacyjne dla: ",
"table-limit": "Dla płynności działania strony pokazanych jest tylko 10 pociągów zgodnie z wybranymi filtrami.",
"last-seen-now": "od niedawna",
"last-seen-min": "od minuty",
"last-seen-ago": "od {minutes} minut",
"scenery-offline": "Przejazd offline",
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR"
},
"journal": {
"title": "HISTORIA DYŻURÓW",
"loading": "Ładowanie historii dyżurów...",
"no-history": "Brak historii dyżurów dla tej scenerii!",
"data-refreshed-at": "Dane odświeżone o",
"section-timetables": "ROZKŁADY JAZDY",
"section-dispatchers": "DYŻURNI",
"no-further-data": "Brak dalszych wyników dla podanych parametrów",
"loading-further-data": "Ładowanie...",
"online-since": "ONLINE OD",
"duty-lasted": "Dyżur trwał",
"hours": "{value} godz.",
"minutes": "{value} min.",
"seconds": "{value} sek.",
"route-length": "Kilometraż:",
"station-count": "Stacje:",
"dispatcher-name": "Autor",
"timetable-day": "Rozkład z dnia",
"timetable-active": "AKTYWNY",
"timetable-fulfilled": "WYPEŁNIONY",
"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": {
"users": "GRACZE ONLINE",
"spawns": "OTWARTE SPAWNY",
"timetables": "AKTYWNE ROZKŁADY JAZDY",
"no-timetables": "Brak aktywnych rozkładów!",
"offline": "Sceneria jest offline",
"no-users": "BRAK AKTYWNYCH GRACZY",
"no-spawns": "BRAK OTWARTYCH SPAWNÓW",
"no-scenery": "Ups! Ta sceneria nie istnieje!",
"return-btn": "Wróć na stronę główną",
"history-btn": "Przejdź do widoku historii dyżurnych ruchu",
"info-btn": "Wróć do widoku scenerii",
"authors-title": "Autor scenerii | Autorzy scenerii",
"abbrev": "Skrót posterunku:",
"lines-title": "Rzeczywiste linie",
"project-title": "Projekt",
"one-way-routes": "Szlaki jednotorowe",
"two-way-routes": "Szlaki dwutorowe",
"option-active-timetables": "Aktywne rozkłady jazdy",
"option-timetables-history": "Historia rozkładów",
"option-dispatchers-history": "Historia dyżurów",
"timetable-author-title": "Wydany przez",
"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",
"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": {
"title": "Dostępność",
"default": "w paczce",
"nonDefault": "poza paczką",
"unavailable": "niedostępna",
"nonPublic": "niepubliczna",
"abandoned": "wycofana"
},
"timetables": {
"timetable-only": "Wyodrębnij rozkłady jazdy",
"end": "Koniec rozkładu jazdy",
"terminated": "Rozkład jazdy zakończony",
"begins": "ROZPOCZYNA\nBIEG",
"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": {
"title": "DZIENNIK ROZKŁADÓW JAZDY"
}
}
+116 -54
View File
@@ -2,6 +2,7 @@
"donations": {
"button-title": "GROSZA DAJ",
"header": "Grosza daj Stacjownikowi!",
"donator-title": "Projekt ma już ponad <b>{count}</b> wspierających, w tym:",
"p1": "<b>Hej o7!</b> Z tej strony Spythere, twórca Stacjownika, Pojazdownika oraz kilku innych aplikacji wspomagających rozgrywkę symulatora Train Driver 2!",
"p2": "{b1} to narzędzie całkowicie darmowe, tworzone i rozwijane dla społeczności symulatora TD2 nieprzerwanie od 2020 roku. Jednakże, część projektu jest podtrzymywana wyłącznie dzięki mojemu prywatnemu wkładowi finansowemu. Funkcje takie jak {b2} czy też {b3} działający na moim {link} (na który serdeczne zapraszam) muszą działać na wydzielonym serwerze, gdzie będą mogły zbierać i przetwarzać dane, aby następnie pokazać je na stronie.",
"p2-b1": "Stacjownik",
@@ -9,12 +10,13 @@
"p2-b3": "Stacjobot",
"p2-a1": "serwerze Discord",
"p3": "<b>Jeśli masz możliwość i chcesz wspomóc moją twórczość dla symulatora, będę wdzięczny za wszelkie pomoce finansowe, które pozwolą na choćby częściowe pokrycie kosztów serwera oraz dalsze rozwijanie możliwości aplikacji!</b>",
"p4": "Każda osoba, która postanowi przelać co najmniej {b1} na rozwój Stacjownika, otrzyma na życzenie symboliczne {img}{b2} nicku użytkownika w zakładkach \"Scenerie\" i \"Pociągi\" aplikacji i moim serwerze Discord (po zweryfikowaniu autora płatności, najlepiej poprzez podanie nicku bezpośrednio przy niej).",
"p4": "Każda osoba, która postanowi przelać co najmniej {b1} (w przypadku PayPala musi być to opłata brutto z dodatkową opłatą za transakcję) na rozwój Stacjownika, otrzyma na życzenie symboliczne {img}{b2} nicku użytkownika na stronie i moim serwerze Discord (po zweryfikowaniu autora płatności, najlepiej poprzez podanie nicku bezpośrednio przy niej).",
"p4-b1": "5 złotych",
"p4-b2": "wyróżnienie",
"p5": "Dzięki i miłego korzystania z aplikacji! <br />~ Spythere",
"action-exit": "Może kiedy indziej...",
"action-confirm": "WSPOMÓŻ!",
"action-paypal": "PRZELEJ PAYPALEM",
"action-buycoffee": "POSTAW KAWĘ!",
"dispatcher-message": "Dyżurny wspierający projekt Stacjownika!",
"driver-message": "Maszynista wspierający projekt Stacjownika!"
},
@@ -36,6 +38,10 @@
"footer": {
"discord": "Serwer Discord Stacjownika"
},
"vehicle-preview": {
"loading": "Ładowanie podglądu...",
"error": "Ups! Nie znaleziono podglądu pojazdu! :/"
},
"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!",
@@ -46,18 +52,6 @@
"S5-dispatchers": "<b>Sygnał S5</b> <br> Błąd podczas pobierania danych o statusach dyżurnych ruchu!",
"S5-trains": "<b>Sygnał S5</b> <br> Błąd podczas pobierania danych o pociągach online!"
},
"desc": {
"control-type": "Sterowanie:",
"signals-type": "Sygnalizacja:",
"SBL": "Sceneria posiada SBL na szlakach:",
"SUP": "Wymaga programu SUP do kontroli systemu RASP-UZK",
"default": "Sceneria dostępna domyślnie w paczce z grą",
"non-public": "Sceneria niepubliczna",
"unavailable": "Sceneria niedostępna",
"unknown": "Nieznana sceneria",
"real": "Sceneria z realnymi liniami kolejowymi:",
"abandoned": "Sceneria wycofana z rozgrywki"
},
"signals": {
"title": "Sygnalizacja",
"współczesna": "współczesna",
@@ -132,7 +126,8 @@
"filter-noComments": "BEZ UWAG",
"filter-twr": "WYS. RYZYKA",
"filter-skr": "SKRAJNIA",
"filter-twr-skr": "WSZYSTKIE",
"filter-twr-skr": "TWR/SKR",
"filter-all-statuses": "WSZYSTKIE",
"filter-common": "ZWYKŁE",
"filter-passenger": "PASAŻERSKIE",
"filter-freight": "TOWAROWE",
@@ -144,9 +139,9 @@
"filter-clear": "WYŁĄCZ FILTRY",
"filter-section-timetable-status": "STATUS ROZKŁADU JAZDY",
"filter-section-twrskr": "UWAGI",
"filter-section-special": "TYPY SPECJALNE",
"filter-all": "WSZYSTKIE",
"filter-all-specials": "WSZYSTKIE",
"filter-abandoned": "PORZUCONE",
"filter-fulfilled": "WYPEŁNIONE",
"filter-active": "AKTYWNE"
@@ -163,7 +158,8 @@
"signals": "TYP SYGNALIZACJI",
"addons": "DODATKOWE PROGRAMY",
"blockades": "BLOKADY LINIOWE",
"status": "STATUS ONLINE"
"status": "STATUS ONLINE",
"timetables": "AKTYWNE ROZKŁADY JAZDY"
},
"all-available": "WSZYSTKIE DOSTĘPNE",
@@ -195,6 +191,9 @@
"SUP": "SUP (RASP-UZK)",
"noSUP": "BEZ SUP",
"ASDEK": "ASDEK",
"noASDEK": "BEZ ASDEK-a",
"SBL": "SAMOCZYNNA",
"PBL": "PÓŁSAMOCZYNNA",
@@ -207,6 +206,9 @@
"free": "WOLNA",
"occupied": "ZAJĘTA",
"withActiveTimetables": "AKTYWNE",
"withoutActiveTimetables": "BEZ AKTYWNYCH",
"sliders": {
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
@@ -216,7 +218,9 @@
"routes-2t-other": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
},
"authors-search": "Szukaj autora (uwzględnia inne filtry)",
"authors-search": "SZUKAJ AUTORA (uwzględnia inne filtry):",
"authors-placeholder": "Wpisz nick autora...",
"authors-button-title": "Szukaj",
"minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:",
"now": "TERAZ",
"hour": " godz.",
@@ -227,19 +231,39 @@
"close": "ZAMKNIJ FILTRY"
},
"sceneries": {
"station": "Stacja",
"abbr": "Skrót\nposterunku",
"min-lvl": "Min. poziom\ndyżurnego",
"status": "Status",
"dispatcher": "Dyżurny",
"dispatcher-lvl": "Poziom\ndyżurnego",
"routes": "Szlaki\n2tor / 1tor",
"general": "Informacje\nogólne",
"user": "Maszyniści online",
"spawn": "Otwarte spawny",
"timetableAll": "Aktywne rozkłady jazdy",
"timetableConfirmed": "Zatwierdzone rozkłady jazdy",
"timetableUnconfirmed": "Niezatwierdzone rozkłady jazdy",
"headers": {
"station": "Sceneria",
"min-lvl": "Poziom\nscenerii",
"status": "Status",
"dispatcher": "Dyżurny",
"dispatcher-lvl": "Poziom\ndyżurnego",
"routes-single": "Szlaki\n1-torowe",
"routes-double": "Szlaki\n2-torowe",
"general": "Informacje\nogólne",
"user": "Maszyniści online",
"like": "Ocena dyżurnego",
"spawn": "Otwarte spawny",
"timetableAll": "Aktywne rozkłady jazdy",
"timetableConfirmed": "Zatwierdzone rozkłady jazdy",
"timetableUnconfirmed": "Niezatwierdzone rozkłady jazdy"
},
"info": {
"control-type": "Sterowanie: ",
"signals-type": "Sygnalizacja: ",
"SBL": "Sceneria posiada SBL na szlakach: ",
"SUP": "Wymaga programu SUP do kontroli systemu RASP-UZK",
"ASDEK": "Wymaga programu ASDEK do detekcji stanów awaryjnych taboru w ruchu",
"default": "Sceneria dostępna domyślnie w paczce z grą",
"non-public": "Sceneria niepubliczna",
"unavailable": "Sceneria niedostępna",
"abandoned": "Sceneria wycofana z rozgrywki",
"unknown": "Nieznana sceneria",
"real": "Sceneria z realnymi liniami kolejowymi: ",
"double-track-routes-catenary": "Liczba zelektryfikowanych szlaków dwutorowych: ",
"single-track-routes-catenary": "Liczba zelektryfikowanych szlaków jednotorowych: ",
"double-track-routes-other": "Liczba niezelektryfikowanych szlaków dwutorowych: ",
"single-track-routes-other": "Liczba niezelektryfikowanych szlaków jednotorowych: "
},
"no-stations": "Brak stacji do wyświetlenia!",
"scenery-search": "Wyszukaj scenerię..."
},
@@ -275,7 +299,24 @@
"scenery-offline": "Przejazd offline",
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR"
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR",
"journal-button": "DZIENNIK MASZYNISTY"
},
"train-stats": {
"stats-button": "STATYSTYKI",
"title": "STATYSTYKI ONLINE",
"timetable-count": "AKTYWNE RJ",
"avg-speed": "ŚREDNIA PRĘDKOŚĆ",
"avg-timetable": "ŚREDNI RJ",
"top-categories": "Kategorie RJ",
"top-vehicles": "Pojazdy w grze",
"top-units": "Najczęstsze jednostki w grze",
"stats-loading": "Ładowanie...",
"no-timetables": "Brak aktywnych rozkładów jazdy na tym serwerze!",
"no-vehicles": "Brak aktywnych pojazdów na tym serwerze!",
"no-units": "Brak aktywnych jednostek na tym serwerze!",
"no-stats": "Brak statystyk online dla wybranego serwera!"
},
"journal": {
"title": "HISTORIA DYŻURÓW",
@@ -302,6 +343,7 @@
"timetable-active": "AKTYWNY",
"timetable-fulfilled": "WYPEŁNIONY",
"timetable-abandoned": "PORZUCONY",
"timetable-online-button": "RJ ONLINE",
"stock-info": "DODATKOWE INFORMACJE",
"stock-length": "Długość",
@@ -310,31 +352,51 @@
"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",
"driver-stats": {
"button": "STAT. MASZYNISTY",
"title": "STATYSTYKI MASZYNISTY {name}",
"info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!",
"timetables": "ROZKŁADY JAZDY",
"longest-timetable": "NAJDŁUŻSZY RJ",
"avg-timetable": "ŚREDNIA DŁUGOŚĆ RJ",
"distance": "DYSTANS",
"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})",
"daily-stats": {
"button": "STATYSTYKI DNIA",
"title": "STATYSTYKI DNIA",
"info": "Dzisiejsze statystyki nie są jeszcze dostępne!",
"total": "Stworzone rozkłady jazdy: {count} (łączny dystans: {distance})",
"longest": "Najdłuższy rozkład jazdy: #{id} (stworzony przez dyżurnego {author} dla maszynisty {driver} o dystansie {distance})",
"most-active-dr": "Najaktywniejszy dyżurny: {dispatcher} (stworzył {count})",
"most-active-dr-many": "Najaktywniejsi dyżurni: {dispatchers} (stworzyli po {count})",
"most-active-driver": "Najaktywniejszy maszynista: {driver} (łączny przejechany dystans: {distance})",
"longest-duties": "Najdłuższa służba: {dispatcher} na scenerii {station} (czas trwania: {duration})",
"count": "rozkład jazdy | rozkładów jazdy",
"timetable-count": "rozkład jazdy | rozkładów jazdy",
"rippedSwitches": "ROZPRUTE ZWROTNICE",
"derailments": "WYKOLEJENIA",
"skippedStopSignals": "POMINIĘTE S1",
"radioStops": "RADIOSTOPY",
"kills": "POTRĄCENIA"
},
"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!",
"dispatcher-stats": {
"button": "STATYSTYKI DYŻURNEGO",
"title": "STATYSTYKI DYŻURNEGO {name}",
"info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki dyżurnego!",
"services-count": "DYŻURY",
"service-max": "MAKS. CZAS DYŻURU",
"service-avg": "ŚREDNI CZAS DYŻURU",
"timetables-count": "WYSTAWIONE RJ",
"timetables-sum": "SUMA WYSTAWIONYCH RJ",
"timetables-max": "NAJDŁUŻSZY WYSTAWIONY RJ",
"timetables-avg": "ŚREDNIA WYSTAWIONYCH RJ"
},
"stats-loading": "Pobieranie statystyk...",
"stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk!",
@@ -370,8 +432,8 @@
"two-way-routes": "Szlaki dwutorowe",
"option-active-timetables": "Aktywne rozkłady jazdy",
"option-timetables-history": "Historia rozkładów",
"option-dispatchers-history": "Historia dyżurów",
"option-timetables-history": "Historia rozkładów PL1",
"option-dispatchers-history": "Historia dyżurów PL1",
"timetable-author-title": "Wydany przez",
"timetable-author-unknown": "Autor nieznany",
+2 -16
View File
@@ -2,26 +2,12 @@ import { createApp, Directive, ref } from 'vue';
import App from './App.vue';
import router from './router';
import enLang from './locales/en.json';
import plLang from './locales/pl.json';
import i18n from './i18n';
import { createI18n } from 'vue-i18n';
import { createPinia } from 'pinia';
import useCustomSW from './mixins/useCustomSW';
const i18n = createI18n({
locale: 'pl',
legacy: false,
warnHtmlMessage: false,
fallbackLocale: 'pl',
messages: {
en: enLang,
pl: plLang
},
enableLegacy: false
});
// SW
// Service worker
useCustomSW();
const clickOutsideDirective: Directive = {
+16
View File
@@ -0,0 +1,16 @@
import { defineComponent } from 'vue';
import { useApiStore } from '../store/apiStore';
export default defineComponent({
data() {
return {
apiStore: useApiStore()
};
},
methods: {
isDonator(name: string) {
return this.apiStore.donatorsData.includes(name);
}
}
});
-2
View File
@@ -10,8 +10,6 @@ export default defineComponent({
mountObserver(actionFunction: () => void, target: Element) {
this.observer = new IntersectionObserver(
(entries) => {
console.log(entries);
if (entries[0].intersectionRatio > 0.5) actionFunction();
},
{ threshold: 0.2 }
+5 -2
View File
@@ -1,10 +1,12 @@
import { defineComponent } from 'vue';
import { useStore } from '../store/mainStore';
import { useMainStore } from '../store/mainStore';
import { usePopupStore } from '../store/popupStore';
export default defineComponent({
data() {
return {
store: useStore()
store: useMainStore(),
popupStore: usePopupStore()
};
},
@@ -23,6 +25,7 @@ export default defineComponent({
closeModal() {
this.store.chosenModalTrainId = undefined;
this.popupStore.currentPopupComponent = null;
setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus();
-5
View File
@@ -148,11 +148,6 @@ export default defineComponent({
if (distance < 1000) return `${distance}m`;
return `${(distance / 1000).toPrecision(2)}km`;
},
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = '/images/icon-unknown.png';
}
}
});
+7 -13
View File
@@ -1,6 +1,4 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import JournalDispatchersVue from '../views/JournalDispatchers.vue';
import JournalTimetablesVue from '../views/JournalTimetables.vue';
const routes: Array<RouteRecordRaw> = [
{
@@ -18,7 +16,8 @@ const routes: Array<RouteRecordRaw> = [
props: (route) => ({
train: route.query.train,
driver: route.query.driver,
trainId: route.query.trainId
trainId: route.query.trainId,
region: route.query.region
})
},
{
@@ -37,20 +36,17 @@ const routes: Array<RouteRecordRaw> = [
{
path: '/journal/timetables',
name: 'JournalTimetables',
component: JournalTimetablesVue,
component: () => import('../views/JournalTimetables.vue'),
props: (route) => ({
trainNo: route.query.trainNo,
driverName: route.query.driverName,
timetableId: route.query.timetableId
region: route.query.region
})
},
{
path: '/journal/dispatchers',
name: 'JournalDispatchers',
component: JournalDispatchersVue,
component: () => import('../views/JournalDispatchers.vue'),
props: (route) => ({
sceneryName: route.query.sceneryName,
dispatcherName: route.query.dispatcherName
region: route.query.region
})
},
{
@@ -61,12 +57,10 @@ const routes: Array<RouteRecordRaw> = [
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
if (to.name == 'SceneryView' && from.name && from.query['view'] === undefined)
if (to.name == 'SceneryView' && from.name !== to.name && from.query['view'] === undefined)
return { el: `.app_main` };
if (savedPosition) return savedPosition;
// if (from.name == 'SceneryView' && to.name == 'StationsView') return { el: `.last-selected`, top: 20 };
},
history: createWebHistory(),
routes
+3 -1
View File
@@ -4,12 +4,14 @@ export const headIds = [
'status',
'dispatcher',
'dispatcher-lvl',
'routes',
'routes-single',
'routes-double',
'general'
] as const;
export const headIconsIds = [
'user',
'like',
'spawn',
'timetableAll',
'timetableUnconfirmed',
+6 -7
View File
@@ -1,5 +1,5 @@
import { Availability, OnlineScenery, ScheduledTrain } from '../../store/typings';
import StationRoutes from './StationRoutes';
import { Availability, ActiveScenery } from '../../store/typings';
import { StationRoutes } from './StationRoutes';
export default interface Station {
name: string;
@@ -8,6 +8,7 @@ export default interface Station {
name: string;
url: string;
abbr: string;
hash?: string;
reqLevel: number;
// supportersOnly: boolean;
@@ -20,16 +21,14 @@ export default interface Station {
controlType: string;
SUP: boolean;
ASDEK: boolean;
authors?: string[];
availability: Availability;
routes: StationRoutes;
checkpoints: {
checkpointName: string;
scheduledTrains: ScheduledTrain[];
}[];
checkpoints: string[];
};
onlineInfo?: OnlineScenery;
onlineInfo?: ActiveScenery;
}
+9 -27
View File
@@ -1,30 +1,12 @@
export default interface StationRoutes {
oneWay: {
name: string;
catenary: boolean;
SBL: boolean;
TWB: boolean;
isInternal: boolean;
tracks: number;
speed: number;
length: number;
}[];
import { StationRoutesInfo } from '../../store/typings';
twoWay: {
name: string;
catenary: boolean;
SBL: boolean;
TWB: boolean;
isInternal: boolean;
tracks: number;
speed: number;
length: number;
}[];
export interface StationRoutes {
single: StationRoutesInfo[];
double: StationRoutesInfo[];
/* [catenary, noCatenary] */
oneWayCatenaryRouteNames: string[];
oneWayNoCatenaryRouteNames: string[];
twoWayCatenaryRouteNames: string[];
twoWayNoCatenaryRouteNames: string[];
sblRouteNames: string[];
singleElectrifiedNames: string[];
singleOtherNames: string[];
doubleElectrifiedNames: string[];
doubleOtherNames: string[];
sblNames: string[];
}
+1
View File
@@ -33,5 +33,6 @@ export default interface Train {
SKR: boolean;
routeDistance: number;
sceneries: string[];
sceneryNames: string[];
};
}
-7
View File
@@ -1,7 +0,0 @@
export const URLs = {
stacjownikAPI:
import.meta.env.VITE_APP_API_DEV === '1' && !import.meta.env.PROD
? 'http://localhost:3001'
: 'https://stacjownik.spythere.eu',
stacjownikAPIDev: 'localhost:3000'
};
@@ -54,12 +54,30 @@ export const sortStations = (
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
break;
case 'routes-single':
diff =
(a.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
(b.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
break;
case 'routes-double':
diff =
(a.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
(b.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
break;
case 'user':
diff =
(b.onlineInfo ? b.onlineInfo.currentUsers : -1) -
(a.onlineInfo ? a.onlineInfo.currentUsers : -1);
break;
case 'like':
diff =
(a.onlineInfo ? a.onlineInfo.dispatcherRate : -Infinity) -
(b.onlineInfo ? b.onlineInfo.dispatcherRate : -Infinity);
break;
case 'spawn':
diff =
(a.onlineInfo ? a.onlineInfo.spawns.length : -1) -
@@ -93,26 +111,41 @@ export const sortStations = (
};
export const filterStations = (station: Station, filters: Filter) => {
if (!station.onlineInfo && filters['free']) return false;
if (filters['free'] && (!station.onlineInfo || station.onlineInfo.dispatcherId == -1))
return false;
if (station.onlineInfo) {
const { dispatcherStatus } = station.onlineInfo;
const isEnding = dispatcherStatus == Status.ActiveDispatcher.ENDING && filters['endingStatus'];
const excludeEnding =
dispatcherStatus == Status.ActiveDispatcher.ENDING && filters['endingStatus'];
const isNotSigned =
const excludeNotSigned =
(dispatcherStatus == Status.ActiveDispatcher.NOT_LOGGED_IN ||
dispatcherStatus == Status.ActiveDispatcher.UNAVAILABLE) &&
filters['unavailableStatus'];
const isAFK = dispatcherStatus == Status.ActiveDispatcher.AFK && filters['afkStatus'];
const excludeAFK = dispatcherStatus == Status.ActiveDispatcher.AFK && filters['afkStatus'];
const isNoSpace =
const excludeNoSpace =
dispatcherStatus == Status.ActiveDispatcher.NO_SPACE && filters['noSpaceStatus'];
const isOccupied = station.onlineInfo && filters['occupied'];
const excludeOccupied = filters['occupied'] && dispatcherStatus != Status.ActiveDispatcher.FREE;
if (isEnding || isNotSigned || isAFK || isNoSpace || isOccupied) return false;
const excludeActiveTTs =
(dispatcherStatus == Status.ActiveDispatcher.FREE ||
station.onlineInfo.scheduledTrainCount.all != 0) &&
filters['withActiveTimetables'];
if (
excludeEnding ||
excludeAFK ||
excludeNoSpace ||
excludeNotSigned ||
excludeOccupied ||
excludeActiveTTs
)
return false;
if (
filters['onlineFromHours'] > 0 &&
@@ -121,6 +154,12 @@ export const filterStations = (station: Station, filters: Filter) => {
return false;
}
const excludeNoActiveTTs =
filters['withoutActiveTimetables'] &&
(!station.onlineInfo || station.onlineInfo.scheduledTrainCount.all == 0);
if (excludeNoActiveTTs) return false;
if (
(station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) &&
filters['nonPublic']
@@ -128,7 +167,7 @@ export const filterStations = (station: Station, filters: Filter) => {
return false;
if (station.generalInfo) {
const { routes, availability, controlType, lines, reqLevel, signalType, SUP, authors } =
const { routes, availability, controlType, lines, reqLevel, signalType, SUP, ASDEK, authors } =
station.generalInfo;
if (availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo)
@@ -155,21 +194,21 @@ export const filterStations = (station: Station, filters: Filter) => {
if (
filters['no-1track'] &&
(routes.oneWayCatenaryRouteNames.length != 0 || routes.oneWayNoCatenaryRouteNames.length != 0)
(routes.singleElectrifiedNames.length != 0 || routes.singleOtherNames.length != 0)
)
return false;
if (
filters['no-2track'] &&
(routes.twoWayCatenaryRouteNames.length != 0 || routes.twoWayNoCatenaryRouteNames.length != 0)
(routes.doubleElectrifiedNames.length != 0 || routes.doubleOtherNames.length != 0)
)
return false;
if (routes.oneWayCatenaryRouteNames.length < filters['minOneWayCatenary']) return false;
if (routes.oneWayNoCatenaryRouteNames.length < filters['minOneWay']) return false;
if (routes.singleElectrifiedNames.length < filters['minOneWayCatenary']) return false;
if (routes.singleOtherNames.length < filters['minOneWay']) return false;
if (routes.twoWayCatenaryRouteNames.length < filters['minTwoWayCatenary']) return false;
if (routes.twoWayNoCatenaryRouteNames.length < filters['minTwoWay']) return false;
if (routes.doubleElectrifiedNames.length < filters['minTwoWayCatenary']) return false;
if (routes.doubleOtherNames.length < filters['minTwoWay']) return false;
if (filters[controlType]) return false;
if (filters[signalType]) return false;
@@ -177,8 +216,11 @@ export const filterStations = (station: Station, filters: Filter) => {
if (filters['SUP'] && SUP) return false;
if (filters['noSUP'] && !SUP) return false;
if (filters['SBL'] && routes.sblRouteNames.length > 0) return false;
if (filters['PBL'] && routes.sblRouteNames.length == 0) return false;
if (filters['ASDEK'] && ASDEK) return false;
if (filters['noASDEK'] && !ASDEK) return false;
if (filters['SBL'] && routes.sblNames.length > 0) return false;
if (filters['PBL'] && routes.sblNames.length == 0) return false;
if (
filters['authors'].length > 3 &&
+115
View File
@@ -0,0 +1,115 @@
import { defineStore } from 'pinia';
import { API } from '../typings/api';
import { Status } from '../typings/common';
import { StationJSONData } from './typings';
import axios, { AxiosInstance } from 'axios';
// Update seconds cron for active data scheduler
const UPDATE_SECONDS = [3, 23, 43];
export enum APIMode {
PRODUCTION = 0,
DEV = 1,
MOCK = 2
}
export const useApiStore = defineStore('apiStore', {
state: () => ({
dataStatuses: {
connection: Status.Data.Loading,
sceneries: Status.Data.Loading
},
activeData: undefined as API.ActiveData.Response | undefined,
donatorsData: [] as API.Donators.Response,
sceneryData: [] as StationJSONData[],
lastFetchData: new Date(),
client: undefined as AxiosInstance | undefined,
activeDataScheduler: undefined as number | undefined
}),
actions: {
async setupAPIData() {
let baseURL = 'https://stacjownik.spythere.eu';
switch (import.meta.env.VITE_API_MODE) {
case 'development':
baseURL = 'http://localhost:3001';
break;
case 'mocking':
baseURL = 'http://localhost:3123';
break;
default:
break;
}
this.client = axios.create({
baseURL
});
this.connectToAPI();
},
async connectToAPI() {
// Static data
this.fetchDonatorsData();
this.fetchStationsGeneralInfo();
// Active data schedueler
this.fetchActiveData();
this.setupActiveDataFetcher();
},
async setupActiveDataFetcher() {
if (this.activeDataScheduler) return;
this.activeDataScheduler = window.setInterval(() => {
this.fetchActiveData();
}, 25000);
},
async fetchActiveData() {
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
try {
const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData');
this.activeData = response.data;
this.lastFetchData = new Date();
this.dataStatuses.connection = Status.Data.Loaded;
console.log('Fetching active data at ' + new Date().toLocaleTimeString('pl-PL'));
} catch (error) {
this.dataStatuses.connection = Status.Data.Error;
console.error('Ups! Wystąpił błąd podczas pobierania danych online:', error);
}
},
async fetchDonatorsData() {
try {
const response = await this.client!.get<API.Donators.Response>('api/getDonators');
this.donatorsData = response.data;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania informacji o donatorach:', error);
}
},
async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = (
await this.client!.get<StationJSONData[]>('api/getSceneries')
).data;
if (!sceneryData) {
this.dataStatuses.sceneries = Status.Data.Error;
return;
}
this.dataStatuses.sceneries = Status.Data.Loaded;
this.sceneryData = sceneryData;
}
}
});
+155 -216
View File
@@ -1,42 +1,24 @@
import axios from 'axios';
import { defineStore } from 'pinia';
import { io } from 'socket.io-client';
import StationRoutes from '../scripts/interfaces/StationRoutes';
import Train from '../scripts/interfaces/Train';
import { URLs } from '../scripts/utils/apiURLs';
import { parseSpawns, getScheduledTrains, getStationTrains } from './utils';
import { OnlineScenery, ScheduledTrain, StationJSONData, StoreState } from './typings';
import { ActiveScenery, ScheduledTrain, StoreState } from './typings';
import packageInfo from '../../package.json';
import { Websocket, API, GithubAPI } from '../typings/api';
import { Status } from '../typings/common';
import Station from '../scripts/interfaces/Station';
import { useApiStore } from './apiStore';
import { StationRoutes } from '../scripts/interfaces/StationRoutes';
export const useStore = defineStore('store', {
export const useMainStore = defineStore('store', {
state: () =>
({
activeData: {} as unknown,
rollingStockData: undefined,
donatorsData: [],
stationList: [],
regionOnlineCounters: [],
routesList: [],
sceneryData: [],
lastDispatcherStatuses: [],
region: { id: 'eu', value: 'PL1' },
trainCount: 0,
stationCount: 0,
webSocket: undefined,
isOffline: false,
isNewUpdate: false,
dispatcherStatsName: '',
dispatcherStatsData: undefined,
dispatcherStatsStatus: Status.Data.Initialized,
driverStatsName: '',
driverStatsData: undefined,
@@ -44,31 +26,15 @@ export const useStore = defineStore('store', {
chosenModalTrainId: undefined,
dataStatuses: {
connection: Status.Data.Loading,
sceneries: Status.Data.Loading,
timetables: Status.Data.Loading,
dispatchers: Status.Data.Loading,
trains: Status.Data.Loading
},
currentStatsTab: null,
blockScroll: false,
listenerLaunched: false,
modalLastClickedTarget: null,
tooltip: {
content: '',
visible: false,
x: 0,
y: 0
}
modalLastClickedTarget: null
}) as StoreState,
getters: {
trainList(): Train[] {
return (this.activeData?.trains ?? [])
const apiStore = useApiStore();
return (apiStore.activeData?.trains ?? [])
.filter((train) => train.timetable || train.online)
.map((train) => {
const stock = train.stockString.split(';');
@@ -76,6 +42,14 @@ export const useStore = defineStore('store', {
const timetable = train.timetable;
const sceneryNames =
train.timetable?.sceneries?.map(
(sceneryHash) =>
this.activeSceneryList.find((st) => st.hash === sceneryHash)?.name ??
apiStore.sceneryData.find((sd) => sd.hash === sceneryHash)?.name ??
sceneryHash
) ?? [];
return {
trainId: train.driverName + train.trainNo.toString(),
@@ -87,7 +61,7 @@ export const useStore = defineStore('store', {
distance: train.distance,
signal: train.signal,
online: train.online,
online: Boolean(train.online),
driverId: train.driverId,
driverName: train.driverName,
currentStationName: train.currentStationName,
@@ -111,41 +85,70 @@ export const useStore = defineStore('store', {
category: timetable.category,
followingStops: timetable.stopList,
routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance,
sceneries: timetable.sceneries
sceneries: timetable.sceneries,
sceneryNames: sceneryNames.reverse()
}
: undefined
} as Train;
});
},
onlineSceneryList(state): OnlineScenery[] {
if (state.isOffline) return [];
if (!state.activeData?.activeSceneries) return [];
activeSceneryList(state): ActiveScenery[] {
const apiStore = useApiStore();
return state.activeData?.activeSceneries.reduce((list, scenery) => {
if (state.isOffline) return [];
if (!apiStore.activeData?.activeSceneries) return [];
const offlineActiveSceneries = this.trainList.reduce((acc, train) => {
if (!train.timetableData) return acc;
train.timetableData.sceneryNames.forEach((name) => {
if (
acc.findIndex((v) => v.name == name && v.region == train.region) != -1 ||
apiStore.activeData?.activeSceneries?.findIndex(
(sc) =>
sc.stationName === name &&
sc.region == train.region &&
Date.now() - sc.lastSeen < 1000 * 60 * 2
) != -1
)
return acc;
acc.push({
name: name,
hash: '',
region: train.region,
maxUsers: 0,
currentUsers: 0,
spawns: [],
dispatcherName: '',
dispatcherRate: 0,
dispatcherId: -1,
dispatcherExp: -1,
dispatcherIsSupporter: false,
scheduledTrains: [],
stationTrains: [],
dispatcherStatus: Status.ActiveDispatcher.FREE,
dispatcherTimestamp: -1,
isOnline: false,
scheduledTrainCount: {
all: 0,
confirmed: 0,
unconfirmed: 0
}
});
});
return acc;
}, [] as ActiveScenery[]);
const onlineActiveSceneries = apiStore.activeData?.activeSceneries.reduce((list, scenery) => {
if (scenery.isOnline !== 1 && Date.now() - scenery.lastSeen > 1000 * 60 * 2) return list;
if (scenery.dispatcherStatus == Status.ActiveDispatcher.UNKNOWN) return list;
const station = this.stationList.find((s) => s.name === scenery.stationName);
const scheduledTrains = getScheduledTrains(this.trainList, scenery, station?.generalInfo);
const stationTrains = getStationTrains(
this.trainList,
scheduledTrains,
this.region.id,
scenery
);
// Remove checkpoint duplicates
const uniqueScheduledTrains = scheduledTrains.reduce(
(uniqueList, sTrain) =>
uniqueList.find((v) => v.trainId === sTrain.trainId)
? uniqueList
: [...uniqueList, sTrain],
[] as ScheduledTrain[]
);
const dispatcherTimestamp =
scenery.dispatcherStatus == Status.ActiveDispatcher.NO_LIMIT
? Date.now() + 25500000
@@ -165,173 +168,109 @@ export const useStore = defineStore('store', {
dispatcherId: scenery.dispatcherId,
dispatcherExp: scenery.dispatcherExp,
dispatcherIsSupporter: scenery.dispatcherIsSupporter,
scheduledTrains: scheduledTrains,
stationTrains: stationTrains,
dispatcherStatus: scenery.dispatcherStatus,
dispatcherTimestamp: dispatcherTimestamp,
isOnline: scenery.isOnline == 1,
scheduledTrains: [],
stationTrains: [],
scheduledTrainCount: {
all: uniqueScheduledTrains.length,
confirmed: uniqueScheduledTrains.filter((train) => train.stopInfo.confirmed).length,
unconfirmed: uniqueScheduledTrains.filter((train) => !train.stopInfo.confirmed).length
all: 0,
confirmed: 0,
unconfirmed: 0
}
});
return list;
}, [] as OnlineScenery[]);
}
},
actions: {
async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = await (
await axios.get(`${URLs.stacjownikAPI}/api/getSceneries`)
).data;
}, [] as ActiveScenery[]);
if (!sceneryData) {
this.dataStatuses.sceneries = Status.Data.Error;
return;
const allActiveSceneries = [...onlineActiveSceneries, ...offlineActiveSceneries];
for (let i = 0, n = allActiveSceneries.length; i < n; i++) {
const scenery = allActiveSceneries[i];
const station = this.stationList.find((s) => s.name === scenery.name);
const scheduledTrains = getScheduledTrains(
this.trainList,
station?.generalInfo,
scenery.name,
scenery.region
);
const stationTrains = getStationTrains(
this.trainList,
scheduledTrains,
this.region.id,
scenery.name
);
// Remove checkpoint duplicates
const uniqueScheduledTrains = scheduledTrains.reduce(
(uniqueList, sTrain) =>
uniqueList.find((v) => v.trainId === sTrain.trainId)
? uniqueList
: [...uniqueList, sTrain],
[] as ScheduledTrain[]
);
scenery.scheduledTrains = scheduledTrains;
scenery.stationTrains = stationTrains;
scenery.scheduledTrainCount = {
all: uniqueScheduledTrains.length,
confirmed: uniqueScheduledTrains.filter((train) => train.stopInfo.confirmed).length,
unconfirmed: uniqueScheduledTrains.filter((train) => !train.stopInfo.confirmed).length
};
}
this.stationList = sceneryData.map((scenery) => {
return allActiveSceneries;
},
stationList(): Station[] {
const apiStore = useApiStore();
return apiStore.sceneryData.map((scenery) => {
const routes = scenery.routesInfo.reduce(
(acc, route) => {
if (route.hidden) return acc;
const tracksKey = route.routeTracks == 2 ? 'double' : 'single';
const isElectric = route.isElectric;
const routesKey: keyof StationRoutes = `${tracksKey}${
!isElectric ? 'Other' : 'Electrified'
}Names`;
if (!route.isInternal) acc[routesKey].push(route.routeName);
if (route.isRouteSBL) acc['sblNames'].push(route.routeName);
acc[tracksKey].push(route);
return acc;
},
{
single: [],
singleElectrifiedNames: [],
singleOtherNames: [],
double: [],
doubleElectrifiedNames: [],
doubleOtherNames: [],
sblNames: []
} as StationRoutes
);
return {
name: scenery.name,
generalInfo: {
...scenery,
authors: scenery.authors?.split(',').map((a) => a.trim()),
routes:
scenery.routesInfo.reduce(
(acc, route) => {
const propName: keyof StationRoutes = `${
route.routeTracks == 2 ? 'twoWay' : 'oneWay'
}${route.isElectric ? '' : 'No'}CatenaryRouteNames`;
acc[route.routeTracks == 2 ? 'twoWay' : 'oneWay'].push({
name: route.routeName,
SBL: route.isRouteSBL,
TWB: false,
catenary: route.isElectric,
isInternal: route.isInternal,
tracks: route.routeTracks,
length: route.routeLength,
speed: route.routeSpeed
});
if (!route.isInternal) acc[propName].push(route.routeName);
if (route.isRouteSBL) acc['sblRouteNames'].push(route.routeName);
return acc;
},
{
oneWay: [],
twoWay: [],
sblRouteNames: [],
oneWayCatenaryRouteNames: [],
oneWayNoCatenaryRouteNames: [],
twoWayCatenaryRouteNames: [],
twoWayNoCatenaryRouteNames: []
} as StationRoutes
) || {},
checkpoints: scenery.checkpoints
? scenery.checkpoints
.split(';')
.map((sub) => ({ checkpointName: sub, scheduledTrains: [] }))
: []
routes: routes,
checkpoints: scenery.checkpoints?.split(';') ?? []
}
};
});
},
async connectToWebsocket() {
if (import.meta.env.VITE_APP_WS_DEV === '1') {
const mockWebsocketData = await import('../data/mockWebsocketData.json');
this.dataStatuses.connection = Status.Data.Loaded;
this.activeData = mockWebsocketData as any;
this.setStatuses();
console.warn('Stacjownik działa w trybie mockowania danych z WS');
return;
}
const socket = io(URLs.stacjownikAPI, {
transports: ['websocket', 'polling'],
rememberUpgrade: true,
reconnection: true
});
socket.emit('CONNECTION', { version: packageInfo.version });
socket.on('connect_error', () => {
this.dataStatuses.connection = Status.Data.Error;
});
socket.on('UPDATE', (data: Websocket.ActiveData) => {
this.activeData = data;
this.dataStatuses.connection = Status.Data.Loaded;
this.setStatuses();
});
socket.emit('FETCH_DATA', { version: packageInfo.version }, (data: Websocket.ActiveData) => {
this.dataStatuses.connection = Status.Data.Loaded;
this.activeData = data;
this.setStatuses();
});
this.webSocket = socket;
},
async connectToAPI() {
this.connectToWebsocket();
this.fetchStockInfoData();
this.fetchDonatorsData();
this.fetchStationsGeneralInfo();
},
async changeRegion(region: StoreState['region']) {
this.region = region;
},
async fetchStockInfoData() {
try {
this.rollingStockData = (
await axios.get<API.RollingStock.Response>(
'https://raw.githubusercontent.com/Spythere/api/main/td2/data/stockInfo.json'
)
).data;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania informacji o taborze z API:', error);
}
},
async fetchDonatorsData() {
try {
const response = await axios.get<GithubAPI.Donators.Response>(
'https://raw.githubusercontent.com/Spythere/api/main/td2/data/donators.json'
);
if (response.data) this.donatorsData = response.data;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania informacji o donatorach:', error);
}
},
async setStatuses() {
if (!this.activeData.activeSceneries) {
this.dataStatuses.sceneries = Status.Data.Error;
this.dataStatuses.trains = Status.Data.Error;
this.dataStatuses.dispatchers = Status.Data.Error;
return;
}
this.dataStatuses.sceneries = Status.Data.Loaded;
this.dataStatuses.trains = !this.activeData.trains ? Status.Data.Warning : Status.Data.Loaded;
this.dataStatuses.dispatchers = Status.Data.Loaded;
}
}
});
+37
View File
@@ -0,0 +1,37 @@
import { defineStore } from 'pinia';
export const popupKeys = ['DonatorPopUp', 'TrainCommentsPopUp', 'VehiclePreviewPopUp'] as const;
export type PopUp = (typeof popupKeys)[number];
const isPopUp = (v: any): v is PopUp => popupKeys.includes(v);
export const usePopupStore = defineStore('popupStore', {
state: () => ({
popupPosition: { x: 0, y: 0 },
currentPopupComponent: null as PopUp | null,
currentPopupContent: '',
donatorPopupVisible: false
}),
actions: {
onPopUpShow(e: MouseEvent, componentKey: string, value?: string) {
if (!isPopUp(componentKey)) return;
this.popupPosition.x = e.pageX;
this.popupPosition.y = e.pageY;
this.currentPopupComponent = componentKey;
this.currentPopupContent = value ?? '';
},
onPopUpMove(e: MouseEvent) {
this.popupPosition.x = e.pageX;
this.popupPosition.y = e.pageY;
},
onPopUpHide() {
this.currentPopupComponent = null;
this.currentPopupContent = '';
}
}
});
+23 -6
View File
@@ -1,7 +1,7 @@
import { defineStore } from 'pinia';
import inputData from '../data/options.json';
import { useStore } from './mainStore';
import { filterStations, sortStations } from '../scripts/utils/filterUtils';
import { useMainStore } from './mainStore';
import { filterStations, sortStations } from '../scripts/utils/stationFilterUtils';
import { HeadIdsTypes } from '../scripts/data/stationHeaderNames';
import StorageManager from '../managers/storageManager';
import { Filter } from '../components/StationsView/typings';
@@ -16,6 +16,8 @@ const filterInitStates: Filter = {
SPE: false,
SUP: false,
noSUP: false,
ASDEK: false,
noASDEK: false,
ręczne: false,
'ręczne+SPK': false,
'ręczne+SCS': false,
@@ -48,6 +50,8 @@ const filterInitStates: Filter = {
noSpaceStatus: false,
unavailableStatus: false,
unsignedStatus: false,
withActiveTimetables: false,
withoutActiveTimetables: false,
authors: '',
@@ -70,14 +74,27 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
},
filteredStationList: (state) => {
const store = useStore();
return store.stationList
.map((station) => ({
const store = useMainStore();
const savedStationNames = store.stationList.map((s) => s.name);
const onlineUnsavedStations = store.activeSceneryList
.filter((os) => !savedStationNames.includes(os.name) && os.region == store.region.id)
.map((os) => ({
name: os.name,
generalInfo: undefined,
onlineInfo: os
}));
return [
...onlineUnsavedStations,
...store.stationList.map((station) => ({
...station,
onlineInfo: store.onlineSceneryList.find(
// append to 'onlineInfo' object for filtering legacy reasons - to optimize later (hopefully)
onlineInfo: store.activeSceneryList.find(
(os) => os.name == station.name && os.region == store.region.id
)
}))
]
.filter((station) => filterStations(station, state.filters))
.sort((a, b) => sortStations(a, b, state.sorterActive));
}
+8 -45
View File
@@ -1,6 +1,4 @@
import { Socket } from 'socket.io-client';
import Station from '../scripts/interfaces/Station';
import { API, GithubAPI, Websocket } from '../typings/api';
import { API } from '../typings/api';
import { Status } from '../typings/common';
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
@@ -12,57 +10,17 @@ export interface RegionCounters {
}
export interface StoreState {
stationList: Station[];
activeData: Websocket.ActiveData;
rollingStockData?: API.RollingStock.Response;
donatorsData: GithubAPI.Donators.Response;
regionOnlineCounters: RegionCounters[];
lastDispatcherStatuses: {
hash: string;
statusTimestamp: number;
statusID: Status.ActiveDispatcher;
}[];
sceneryData: any[][];
region: { id: string; value: string };
trainCount: number;
stationCount: number;
webSocket?: Socket;
isOffline: boolean;
isNewUpdate: boolean;
dispatcherStatsName: string;
dispatcherStatsData?: API.DispatcherStats.Response;
driverStatsName: string;
driverStatsData?: API.DriverStats.Response;
driverStatsStatus: Status.Data;
chosenModalTrainId?: string;
currentStatsTab: 'daily' | 'driver' | null;
dataStatuses: {
connection: Status.Data;
sceneries: Status.Data;
timetables: Status.Data;
dispatchers: Status.Data;
trains: Status.Data;
};
listenerLaunched: boolean;
blockScroll: boolean;
modalLastClickedTarget: EventTarget | null;
tooltip: {
visible: boolean;
x: number;
y: number;
content: string;
};
}
export interface StationRoutesInfo {
@@ -73,6 +31,7 @@ export interface StationRoutesInfo {
routeLength: number;
routeSpeed: number;
routeTracks: number;
hidden?: boolean;
}
export interface StationJSONData {
@@ -82,6 +41,7 @@ export interface StationJSONData {
lines: string;
project: string;
projectUrl: string;
hash: string;
reqLevel: number;
@@ -89,6 +49,7 @@ export interface StationJSONData {
controlType: string;
SUP: boolean;
ASDEK: boolean;
// routes: string;
routesInfo: StationRoutesInfo[];
@@ -99,7 +60,7 @@ export interface StationJSONData {
availability: Availability;
}
export interface OnlineScenery {
export interface ActiveScenery {
name: string;
hash: string;
region: string;
@@ -166,6 +127,8 @@ export interface ScheduledTrain {
stopLabel: string;
stopStatus: StopStatus;
stopStatusID: number;
region: string;
}
export enum StopStatus {
+39 -83
View File
@@ -1,6 +1,5 @@
import Station from '../scripts/interfaces/Station';
import Train from '../scripts/interfaces/Train';
import { API } from '../typings/api';
import { ScheduledTrain, StationTrain, StopStatus, TrainStop } from './typings';
export function getLocoURL(locoType: string): string {
@@ -102,51 +101,33 @@ export function getCheckpointTrain(
let prevStationName = '',
nextStationName = '';
let prevDepartureLine: string | null = null,
nextArrivalLine: string | null = null;
for (let i = trainStopIndex - 1; i >= 0; i--) {
if (/strong|podg/g.test(followingStops[i].stopName)) {
prevStationName = followingStops[i].stopNameRAW.replace(/,.*/g, '');
break;
}
}
for (let i = trainStopIndex + 1; i < followingStops.length; i++) {
if (/strong|podg/g.test(followingStops[i].stopName)) {
nextStationName = followingStops[i].stopNameRAW.replace(/,.*/g, '');
break;
}
}
let departureLine: string | null = null;
let arrivingLine: string | null = null;
for (let i = trainStopIndex; i < followingStops.length; i++) {
const currentStop = followingStops[i];
let prevDepartureLine: string | null = null,
nextArrivalLine: string | null = null;
if (currentStop.departureLine == null) continue;
for (let i = trainStopIndex; i >= 0; i--) {
const stop = followingStops[i];
if (!/-|_|it|sbl/gi.test(currentStop.departureLine)) {
departureLine = currentStop.departureLine;
nextArrivalLine = followingStops[i + 1]?.arrivalLine || null;
if (/strong|podg\.|pe\./g.test(stop.stopName) && !prevStationName && i <= trainStopIndex - 1)
prevStationName = stop.stopNameRAW.replace(/,.*/g, '');
break;
if (stop.arrivalLine != null && !arrivingLine && !/-|_|it|sbl/gi.test(stop.arrivalLine)) {
arrivingLine = stop.arrivalLine;
prevDepartureLine = followingStops[i - 1]?.departureLine || null;
}
}
for (let i = trainStopIndex; i >= 0; i--) {
const currentStop = followingStops[i];
for (let i = trainStopIndex; i < followingStops.length; i++) {
const stop = followingStops[i];
if (currentStop.arrivalLine == null) continue;
if (/strong|podg\.|pe\./g.test(stop.stopName) && !nextStationName && i > trainStopIndex)
nextStationName = stop.stopNameRAW.replace(/,.*/g, '');
if (!/-|_|it|sbl/gi.test(currentStop.arrivalLine)) {
arrivingLine = currentStop.arrivalLine;
prevDepartureLine = followingStops[i - 1]?.departureLine || null;
break;
if (stop.departureLine && !departureLine && !/-|_|it|sbl/gi.test(stop.departureLine)) {
departureLine = stop.departureLine;
nextArrivalLine = followingStops[i + 1]?.arrivalLine || null;
}
}
@@ -175,8 +156,10 @@ export function getCheckpointTrain(
stopStatus: trainStopStatus.stopStatus,
stopStatusID: trainStopStatus.stopStatusID,
arrivingLine,
departureLine,
region: train.region,
arrivingLine: arrivingLine,
departureLine: departureLine,
nextArrivalLine,
prevDepartureLine
@@ -185,58 +168,33 @@ export function getCheckpointTrain(
export function getScheduledTrains(
trainList: Train[],
sceneryData: API.ActiveSceneries.Data,
stationGeneralInfo: Station['generalInfo']
stationGeneralInfo: Station['generalInfo'],
stationName: string,
region: string
// sceneryData: API.ActiveSceneries.Data,
): ScheduledTrain[] {
const stationNameLower = sceneryData.stationName.toLocaleLowerCase();
stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0));
// stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0));
return trainList.reduce((acc: ScheduledTrain[], train) => {
if (!train.timetableData) return acc;
if (train.region != region) return acc;
const timetable = train.timetableData;
if (!timetable.sceneries.includes(sceneryData.stationHash)) return acc;
if (!timetable.sceneryNames.includes(stationName)) return acc;
const stopInfoIndex = timetable.followingStops.findIndex((stop) => {
const stopNameLower = stop.stopNameRAW.toLocaleLowerCase();
return (
stationNameLower == stopNameLower ||
(!/(po\.|podg\.)/.test(stopNameLower) && stopNameLower.includes(stationNameLower)) ||
(!/(po\.|podg\.)/.test(stationNameLower) && stationNameLower.includes(stopNameLower)) ||
(stopNameLower.split(', podg.')[0] !== undefined &&
stationNameLower.startsWith(stopNameLower.split(', podg.')[0]))
);
});
const checkpoints = [stationName];
if (stationGeneralInfo?.checkpoints) checkpoints.push(...stationGeneralInfo.checkpoints);
const checkpointScheduledTrains: ScheduledTrain[] = [];
if (stopInfoIndex != -1) {
checkpointScheduledTrains.push(
getCheckpointTrain(train, stopInfoIndex, sceneryData.stationName)
);
}
stationGeneralInfo?.checkpoints?.forEach((checkpoint) => {
// if (checkpoint.checkpointName.toLocaleLowerCase() == stationNameLower) return;
for (let i = 0; i < timetable.followingStops.length; i++) {
if (
checkpointScheduledTrains.findIndex(
(cpTrain) =>
cpTrain.checkpointName.toLocaleLowerCase() ==
checkpoint.checkpointName.toLocaleLowerCase()
) != -1
)
return;
const index = timetable.followingStops.findIndex(
(stop) => stop.stopNameRAW.toLowerCase() == checkpoint.checkpointName.toLowerCase()
);
if (index > -1)
checkpointScheduledTrains.push(getCheckpointTrain(train, index, sceneryData.stationName));
});
new RegExp(`^(${checkpoints.join('|')})$`, 'i').test(
timetable.followingStops[i].stopNameRAW
)
) {
checkpointScheduledTrains.push(getCheckpointTrain(train, i, stationName));
}
}
acc.push(...checkpointScheduledTrains);
return acc;
@@ -247,14 +205,12 @@ export function getStationTrains(
trainList: Train[],
scheduledTrainList: ScheduledTrain[],
region: string,
sceneryData: API.ActiveSceneries.Data
stationName: string
): StationTrain[] {
return trainList
.filter(
(train) =>
train?.region === region &&
train.online &&
train.currentStationName === sceneryData.stationName
train?.region === region && train.online && train.currentStationName === stationName
)
.map((train) => ({
driverName: train.driverName,
+3 -4
View File
@@ -5,12 +5,13 @@
overflow-y: auto;
height: 90vh;
min-height: 550px;
margin-top: 0.5em;
padding-right: 0.2em;
}
.journal_wrapper {
max-width: 1350px;
max-width: 1500px;
width: 100%;
margin: 0 auto;
@@ -24,7 +25,7 @@
text-align: end;
padding: 0.25em;
margin: 0.5em 0;
margin-top: 0.5em;
}
.journal_warning {
@@ -53,9 +54,7 @@
justify-content: space-between;
align-items: center;
gap: 0.5em;
position: relative;
margin-bottom: 0.5em;
}
.btn--load-data {
+15 -4
View File
@@ -2,24 +2,35 @@
@import 'responsive.scss';
.stats-tab {
position: absolute;
right: 0;
z-index: 99;
transform: translateY(1em);
width: 100%;
background-color: #1a1a1a;
box-shadow: 0 0 5px 1px $accentCol;
padding: 1em;
display: flex;
align-items: flex-end;
}
margin-bottom: 0.5em;
hr.header-separator {
margin-bottom: 1em;
}
width: 100%;
hr.section-separator {
margin: 1em 0;
}
.info-stats {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.5em;
margin-top: 1em;
}
.stat-badge {
+1 -2
View File
@@ -1,4 +1,4 @@
$animDuration: 150ms;
$animDuration: 95ms;
$animType: ease-in-out;
// List animation
@@ -72,7 +72,6 @@ $animType: ease-in-out;
&-enter-from,
&-leave-to {
transform: translateY(-25%);
opacity: 0;
}
}
+5 -1
View File
@@ -76,7 +76,11 @@
}
.train-badge {
padding: 0.1em 0.2em;
display: flex;
align-items: center;
gap: 0.5em;
padding: 0.1em 0.3em;
border-radius: 0.2em;
font-weight: bold;
+48
View File
@@ -0,0 +1,48 @@
@import 'responsive.scss';
@import 'variables.scss';
.dropdown-anim {
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(10px);
}
&-enter-active,
&-leave-active {
transition: all 150ms ease;
}
}
.dropdown_background {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10;
}
.dropdown_wrapper {
position: absolute;
left: 0;
top: calc(100% + 0.5em);
background-color: $bgCol;
// box-shadow: 0 5px 10px 2px #0f0f0f;
box-shadow: 0 0 5px 1px $accentCol;
width: 100%;
max-width: 550px;
padding: 1em;
z-index: 100;
}
@include smallScreen {
.dropdown_wrapper {
font-size: 1.1em;
max-width: 100%;
}
}
@@ -1,11 +1,6 @@
@import 'search_box.scss';
@import 'responsive.scss';
@import 'variables.scss';
@import 'search_box.scss';
.filters-options {
margin-bottom: 0.5em;
position: relative;
}
.actions-bar {
display: flex;
@@ -29,42 +24,6 @@ h1.option-title {
}
}
.options-anim {
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(10px);
}
&-enter-active,
&-leave-active {
transition: all 150ms ease;
}
}
.bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10;
}
.options_wrapper {
position: absolute;
background-color: $bgCol;
box-shadow: 0 5px 10px 2px #0f0f0f;
width: 100%;
max-width: 550px;
padding: 1em;
z-index: 100;
}
.options_sorters {
display: flex;
align-items: center;
@@ -93,6 +52,7 @@ h1.option-title {
.sort-option[data-selected='true'] {
color: $accentCol;
font-weight: bold;
}
.filter-option {
@@ -146,11 +106,6 @@ h1.option-title {
}
}
.options_wrapper {
font-size: 1.1em;
max-width: 100%;
}
.options_filters,
.options_sorters {
justify-content: center;
+49
View File
@@ -0,0 +1,49 @@
@font-face {
font-family: 'Quicksand';
src:
url('/fonts/Quicksand-Bold.woff2') format('woff2'),
url('/fonts/Quicksand-Bold.woff') format('woff');
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Quicksand';
src:
url('/fonts/Quicksand-SemiBold.woff2') format('woff2'),
url('/fonts/Quicksand-SemiBold.woff') format('woff');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Quicksand';
src:
url('/fonts/Quicksand-Medium.woff2') format('woff2'),
url('/fonts/Quicksand-Medium.woff') format('woff');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Quicksand';
src:
url('/fonts/Quicksand-Regular.woff2') format('woff2'),
url('/fonts/Quicksand-Regular.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Quicksand';
src:
url('/fonts/Quicksand-Light.woff2') format('woff2'),
url('/fonts/Quicksand-Light.woff') format('woff');
font-weight: 300;
font-style: normal;
font-display: swap;
}
+53 -38
View File
@@ -1,3 +1,7 @@
@import 'fonts';
@import 'variables';
@import 'responsive';
:root {
--clr-primary: #ffc014;
--clr-secondary: #2f2f2f;
@@ -11,17 +15,20 @@
--clr-skr: #ff5100;
--clr-twr: #ffbb00;
--clr-error: #df3e3e;
--clr-error: #fa3636;
--clr-warning: #c59429;
--clr-honorable: #f47fff;
--clr-donator: #f7a4ff;
--no-scroll-padding: 17px;
--max-container-width: 1700px;
font-size: 16px;
}
::-webkit-scrollbar {
width: 15px;
height: 15px;
width: var(--no-scroll-padding);
height: var(--no-scroll-padding);
background-color: transparent;
&-track {
@@ -44,11 +51,14 @@ body {
padding: 0;
font-family: 'Quicksand', sans-serif;
font-weight: 500;
text-rendering: optimizeLegibility !important;
-webkit-font-smoothing: antialiased !important;
overflow-y: scroll;
&.no-scroll {
overflow-y: hidden;
padding-right: 15px;
padding-right: var(--no-scroll-padding);
@include smallScreen() {
padding: 0;
@@ -158,13 +168,18 @@ ul {
color: #ccc;
}
&--honorable {
color: var(--clr-honorable);
text-shadow: var(--clr-honorable) 0 0 10px;
&--error {
color: var(--clr-error);
}
&--donator {
color: var(--clr-donator);
text-shadow: var(--clr-donator) 0 0 10px;
}
}
button {
button,
a.a-button {
cursor: pointer;
color: white;
background: none;
@@ -182,46 +197,46 @@ button {
&[data-disabled='true'] {
user-select: none;
pointer-events: none;
opacity: 0.85;
opacity: 0.7;
}
}
button.btn--filled {
background-color: #1a1a1a;
&.btn--filled {
background-color: #1a1a1a;
&:hover {
background-color: #2a2a2a;
&:hover {
background-color: #2a2a2a;
}
}
}
button.btn--action {
background-color: #424242;
border-radius: 0.25em;
&.btn--action {
background-color: #424242;
border-radius: 0.25em;
&:hover {
background-color: #555;
&:hover {
background-color: #555;
}
}
}
button.btn--option {
color: white;
background-color: #333;
&.btn--option {
color: white;
background-color: #333;
&.checked {
color: var(--clr-primary);
&.checked {
color: var(--clr-primary);
font-weight: bold;
background-color: #3c3c3c;
}
}
&.btn--image {
font-weight: bold;
padding: 0.35em 0.75em;
background-color: #3c3c3c;
}
}
button.btn--image {
font-weight: bold;
padding: 0.35em 0.75em;
img {
width: 1.5em;
vertical-align: middle;
img {
width: 1.35em;
vertical-align: middle;
}
}
}

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