Compare commits
358 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65c1ab809f | |||
| e7c8ba62d7 | |||
| 38a9f1987f | |||
| f90dfd3cc8 | |||
| 9b765c7fdd | |||
| 0f7e3e8820 | |||
| 1735444176 | |||
| 1d95b26e9c | |||
| 86fbaa2510 | |||
| b7db3edd9b | |||
| 72fa9523e8 | |||
| 7b07a43715 | |||
| 448c6e387e | |||
| 527c929b53 | |||
| b622df19f6 | |||
| 03e69b315c | |||
| f2c11bf2cf | |||
| 92c73b9ed9 | |||
| acc15619a9 | |||
| 3705325a9a | |||
| 1655aa2c94 | |||
| f38ad8fa81 | |||
| 1a7801259f | |||
| abd1c8b684 | |||
| 7f315b549e | |||
| 329c85b858 | |||
| dcef8cdac8 | |||
| 298f8a5f23 | |||
| 51d952ffee | |||
| 83b22e5978 | |||
| 87ad7b8ede | |||
| 440e11bdd9 | |||
| 84ecd3c175 | |||
| 72b3aef045 | |||
| 36ae24fdaf | |||
| 41e3d018e6 | |||
| d9faa486d2 | |||
| 89dc265e1b | |||
| 200e994ae6 | |||
| 150b7749ae | |||
| 0f8932b53c | |||
| 1365140802 | |||
| ce8bbe4c67 | |||
| 1d49de1c6b | |||
| b8574f9ea1 | |||
| ecced14cca | |||
| 212a87126d | |||
| 41e50b8207 | |||
| 565b0dfd8c | |||
| 40a0b47984 | |||
| ccca1c8752 | |||
| cf51045343 | |||
| 23a8b9e8d4 | |||
| b34f8229cc | |||
| f1eee97d46 | |||
| d93be0b9be | |||
| 5190eed7ee | |||
| a6f284270e | |||
| 08422caa96 | |||
| 3a70d8f6a6 | |||
| e3e5eb3460 | |||
| 1819569234 | |||
| 052ca08f01 | |||
| b01b2f8360 | |||
| bda369d13b | |||
| a8cac9ebe9 | |||
| 0d55a10ec2 | |||
| fa7b1c1629 | |||
| c99b5df4aa | |||
| 0b435c95a0 | |||
| 5d32145f13 | |||
| cb6ea1edb2 | |||
| 6a3974f899 | |||
| 2cbeef7611 | |||
| 43be04826d | |||
| d9986da354 | |||
| ac2269c5a5 | |||
| 6957120b3b | |||
| c0b892da97 | |||
| 907b75f12b | |||
| 47f824bef0 | |||
| 2d3e830cf9 | |||
| c888b3d386 | |||
| 645a70ef9c | |||
| 1cd93f09c4 | |||
| 6b4231496e | |||
| b72ee13bdb | |||
| ce053a5a82 | |||
| b08e39ae1a | |||
| dc27500237 | |||
| 47193181e5 | |||
| 7bbabdd7bf | |||
| 200318def7 | |||
| f335ca8fc2 | |||
| 15e599fe3c | |||
| bd25914ed4 | |||
| 01ea259381 | |||
| aea26fa538 | |||
| 28d78cd2bc | |||
| a021deae96 | |||
| 8840576796 | |||
| 5018e21736 | |||
| a7fa1dfb6d | |||
| a3558c0b30 | |||
| ee159fd582 | |||
| 35c9fb7ef1 | |||
| e24097c240 | |||
| 01cbebd019 | |||
| 3a5ef7e025 | |||
| c78a5b4d67 | |||
| 023de9f7b8 | |||
| 1024e44cc0 | |||
| 580d404d4a | |||
| 6d1ef26ac1 | |||
| bf9799e0c3 | |||
| 1d13e31d79 | |||
| 16f272bd7d | |||
| 23ca33264c | |||
| 324ca3de4d | |||
| e0548e593c | |||
| 2727350837 | |||
| 6c3af0a8d3 | |||
| e784202a36 | |||
| c24f691693 | |||
| 3aeabd63c9 | |||
| 4c79376318 | |||
| bc1c446c37 | |||
| fba335d0c7 | |||
| b4e536da40 | |||
| 8cde8e6323 | |||
| d7a9e93978 | |||
| 69aa62e77f | |||
| 4b842627fb | |||
| 5ffc63a815 | |||
| 87f7ff58e8 | |||
| 8b6944a8e5 | |||
| cfeeb8fddd | |||
| 89f7fd3c53 | |||
| 86259988c9 | |||
| 7b5ef18ad6 | |||
| d784042691 | |||
| d0e482aa4f | |||
| 3bf1db52b4 | |||
| 8e713a5c6e | |||
| af6eb35b67 | |||
| 1e6ab1c2d1 | |||
| fd4849bd5e | |||
| bc0f4c5d3f | |||
| 8909a0cd40 | |||
| a2602aeefe | |||
| 37ad9b2787 | |||
| 0b4ad679b3 | |||
| dd0d7897cf | |||
| 1453dbda01 | |||
| 4af856b833 | |||
| 182b46a377 | |||
| bb5fc395d2 | |||
| a91a00f88a | |||
| c8d481a952 | |||
| 03ff4d8648 | |||
| 23767801d5 | |||
| 310261fb59 | |||
| 742754ceef | |||
| b9bb9dc201 | |||
| 611927f96f | |||
| 2e191f355e | |||
| f974643e37 | |||
| 02afe2bf33 | |||
| ebdffc6241 | |||
| 911c051af3 | |||
| 7ab16960ca | |||
| 43c7b8b024 | |||
| 32cf7745e8 | |||
| a68c5020d9 | |||
| bf88caa704 | |||
| ea5d681943 | |||
| d346e049e0 | |||
| feb2027c16 | |||
| 06d0fabc99 | |||
| 3c74580bed | |||
| 02d234a21b | |||
| d9dc44063f | |||
| 5929bbdccb | |||
| f73c3f4aec | |||
| a44ad5c89d | |||
| c30c2206ce | |||
| 128f3c32b4 | |||
| 0fdcd82754 | |||
| 1e75ff517f | |||
| b278c20480 | |||
| fd28eb4609 | |||
| a602358241 | |||
| 5a09543a22 | |||
| f952a7c491 | |||
| adf4d88cb2 | |||
| 34f2a69863 | |||
| b2930f6a9e | |||
| edcaff2183 | |||
| 010ab08701 | |||
| 16b3bb3683 | |||
| 93e242c0f5 | |||
| 861206a5ab | |||
| a47399fe1b | |||
| 8e196c8279 | |||
| be55bac9fe | |||
| c5e53057eb | |||
| 4ba5d544af | |||
| 22b6177560 | |||
| 9b6c6ee756 | |||
| 829059d35b | |||
| b56e114ef9 | |||
| 71b4cc3bdb | |||
| 8cc773ffb5 | |||
| 427b4c03e4 | |||
| 46dc43d652 | |||
| 6435d12090 | |||
| e41b8cfa98 | |||
| bc81bb2a38 | |||
| e6c064d15d | |||
| 4d1df5165c | |||
| 43ac2be3e7 | |||
| 75c4e56183 | |||
| 931f6b9fbd | |||
| 21fa1f8699 | |||
| 877ef50a97 | |||
| 933be53630 | |||
| eef4103960 | |||
| 9fafbe2c7f | |||
| 666ba07307 | |||
| b63328f97c | |||
| 342127d541 | |||
| c6ab0d21de | |||
| da4476bdf0 | |||
| a950b4bef4 | |||
| 5aa9297ec5 | |||
| 0af49befc6 | |||
| 4da0ab475b | |||
| 1fa5934784 | |||
| 5fb1a87b41 | |||
| 8a5687cc01 | |||
| c5fe929b9a | |||
| 5787deeaf8 | |||
| 130732921b | |||
| 1b2cd34e86 | |||
| 17bda9e6e7 | |||
| c66ff8feed | |||
| 027cdee25a | |||
| 435cfb3b3f | |||
| 425241c8e7 | |||
| f24f961d52 | |||
| 4718eeeaaf | |||
| 931fd7b21b | |||
| bb79c5033a | |||
| ee290788dc | |||
| a87d1060d3 | |||
| 1804d6d0f0 | |||
| 77250e30c7 | |||
| c5aefd03b8 | |||
| 2ec4694bd3 | |||
| 729f66bcdb | |||
| b746843086 | |||
| cbbd06fecd | |||
| 11e99b6af0 | |||
| 31f4a2e5b2 | |||
| 22514c3263 | |||
| 0df673467c | |||
| 6377e13809 | |||
| 13fa633db4 | |||
| dd9661551c | |||
| 495012a5ca | |||
| 3cfccb1bb4 | |||
| d2a8cdb2b0 | |||
| c33b5ef8c1 | |||
| 52d1771c21 | |||
| cac4345683 | |||
| 6fd9e21213 | |||
| 421add1ec1 | |||
| 4ac92198b7 | |||
| 5105229eef | |||
| 2ec02080c3 | |||
| 95b2a696e1 | |||
| 091e94e396 | |||
| 43c939bf01 | |||
| 8285d5c579 | |||
| 547248b478 | |||
| e1b9b37ac8 | |||
| c8ec28292b | |||
| 3daf800a89 | |||
| 69d9be0bb3 | |||
| f53f3a18fe | |||
| fac8fced3e | |||
| a3d9e68c8a | |||
| b09761de58 | |||
| 8ac2c68660 | |||
| 4177c6e5f4 | |||
| b8f135a454 | |||
| f0863b2459 | |||
| 55b4732992 | |||
| 7b3dcea89e | |||
| f4b0c39185 | |||
| 275d602f97 | |||
| c93514fdf0 | |||
| 0861d92e4b | |||
| bdfd73f4be | |||
| df86364c51 | |||
| 631bb20c61 | |||
| bed79ed2d0 | |||
| 2a07471e12 | |||
| cfe188d0dc | |||
| d9865be83e | |||
| 9155fd9f8d | |||
| 4674bf886e | |||
| 289fd310df | |||
| b04797052f | |||
| 7079f20791 | |||
| c244275aee | |||
| fbc9785341 | |||
| 9fd02c2336 | |||
| c031dd55c1 | |||
| b0870699a4 | |||
| a8da634b0e | |||
| 8920b1e5e8 | |||
| 4fa1c05831 | |||
| ecef2d5ee4 | |||
| 1749871d08 | |||
| 5545616706 | |||
| b35bb03868 | |||
| b9521918cb | |||
| 7ad17fc2c5 | |||
| a80144cb1c | |||
| 1227cdb94a | |||
| b33594fd6f | |||
| 9f8656e590 | |||
| 81cd165fe7 | |||
| 41e4b45599 | |||
| 462dd7dd7a | |||
| 9837ae97e1 | |||
| a818cd980b | |||
| 573ebc233b | |||
| aae47c6abd | |||
| 24c9b62162 | |||
| 481d43b6d8 | |||
| 4969a433cc | |||
| 8a2b453dc6 | |||
| 86d178ef56 | |||
| 7769477508 | |||
| 551b60c733 | |||
| 80a5b56785 | |||
| 6bd62f13a1 | |||
| 42591f6e76 | |||
| 4ca0f09e75 | |||
| 02c3629c00 | |||
| 9c4c806f0e | |||
| 58d6a97762 | |||
| cd71c78eb4 | |||
| 300e70dcfe | |||
| 09b31f7914 | |||
| c29c3c6abe |
@@ -0,0 +1,23 @@
|
|||||||
|
name: Build & Deploy to VPS
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
PROJECT_NAME: stacjownik-td2
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Build the app
|
||||||
|
run: yarn && yarn build
|
||||||
|
- name: Setup SSH key for connection with the server
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.VPS_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa
|
||||||
|
- name: Send new files
|
||||||
|
run: rsync -avP -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa -p 2022" ./dist/ ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }}:/var/www/$PROJECT_NAME --delete
|
||||||
@@ -33,6 +33,10 @@ node_modules
|
|||||||
|
|
||||||
# Env
|
# Env
|
||||||
.env
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
.fake
|
.fake
|
||||||
.ionide
|
.ionide
|
||||||
|
|
||||||
|
# api-mock
|
||||||
|
/api-mock/endpoints/
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { mkdir, writeFile } from 'fs/promises';
|
||||||
|
|
||||||
|
async function fetchJSONEndpointData(url, fileName) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url);
|
||||||
|
const data = await res.json();
|
||||||
|
await writeFile(`./endpoints/${fileName}`, JSON.stringify(data));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (!existsSync('endpoints')) await mkdir('endpoints');
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
['getActiveData', 'getDonators', 'getSceneries', 'getVehicles'].map((endpointName) =>
|
||||||
|
fetchJSONEndpointData(
|
||||||
|
`https://stacjownik.spythere.eu/api/${endpointName}`,
|
||||||
|
`${endpointName}.json`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).then(() => {
|
||||||
|
console.log('Endpoints downloaded!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import path from 'path';
|
||||||
|
import { cwd } from 'process';
|
||||||
|
import cors from 'cors';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
|
||||||
|
app.get('/api/getActiveData', (_, res) => {
|
||||||
|
res.sendFile(path.join(cwd(), 'endpoints', 'getActiveData.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/getSceneries', (_, res) => {
|
||||||
|
res.sendFile(path.join(cwd(), 'endpoints', 'getSceneries.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/getVehiclesData', (_, res) => {
|
||||||
|
res.sendFile(path.join(cwd(), 'endpoints', 'getVehiclesData.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/getDonators', (_, res) => {
|
||||||
|
res.sendFile(path.join(cwd(), 'endpoints', 'getDonators.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(3123, () => {
|
||||||
|
console.log('Mocking API server...');
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "api-mock",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node index.js",
|
||||||
|
"fetch": "node fetchEndpoints.js"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.18.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
accepts@~1.3.8:
|
||||||
|
version "1.3.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
|
||||||
|
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
|
||||||
|
dependencies:
|
||||||
|
mime-types "~2.1.34"
|
||||||
|
negotiator "0.6.3"
|
||||||
|
|
||||||
|
array-flatten@1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
|
||||||
|
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
|
||||||
|
|
||||||
|
body-parser@1.20.3:
|
||||||
|
version "1.20.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6"
|
||||||
|
integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==
|
||||||
|
dependencies:
|
||||||
|
bytes "3.1.2"
|
||||||
|
content-type "~1.0.5"
|
||||||
|
debug "2.6.9"
|
||||||
|
depd "2.0.0"
|
||||||
|
destroy "1.2.0"
|
||||||
|
http-errors "2.0.0"
|
||||||
|
iconv-lite "0.4.24"
|
||||||
|
on-finished "2.4.1"
|
||||||
|
qs "6.13.0"
|
||||||
|
raw-body "2.5.2"
|
||||||
|
type-is "~1.6.18"
|
||||||
|
unpipe "1.0.0"
|
||||||
|
|
||||||
|
bytes@3.1.2:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||||
|
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
||||||
|
|
||||||
|
call-bind@^1.0.7:
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
|
||||||
|
integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==
|
||||||
|
dependencies:
|
||||||
|
es-define-property "^1.0.0"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
get-intrinsic "^1.2.4"
|
||||||
|
set-function-length "^1.2.1"
|
||||||
|
|
||||||
|
content-disposition@0.5.4:
|
||||||
|
version "0.5.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
|
||||||
|
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
|
||||||
|
dependencies:
|
||||||
|
safe-buffer "5.2.1"
|
||||||
|
|
||||||
|
content-type@~1.0.4, content-type@~1.0.5:
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
|
||||||
|
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
|
||||||
|
|
||||||
|
cookie-signature@1.0.6:
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||||
|
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
|
||||||
|
|
||||||
|
cookie@0.6.0:
|
||||||
|
version "0.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
|
||||||
|
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
|
||||||
|
|
||||||
|
cors@^2.8.5:
|
||||||
|
version "2.8.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
|
||||||
|
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
|
||||||
|
dependencies:
|
||||||
|
object-assign "^4"
|
||||||
|
vary "^1"
|
||||||
|
|
||||||
|
debug@2.6.9:
|
||||||
|
version "2.6.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
|
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||||
|
dependencies:
|
||||||
|
ms "2.0.0"
|
||||||
|
|
||||||
|
define-data-property@^1.1.4:
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
|
||||||
|
integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
|
||||||
|
dependencies:
|
||||||
|
es-define-property "^1.0.0"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
gopd "^1.0.1"
|
||||||
|
|
||||||
|
depd@2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
|
||||||
|
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
|
||||||
|
|
||||||
|
destroy@1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
|
||||||
|
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
|
||||||
|
|
||||||
|
ee-first@1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||||
|
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
|
||||||
|
|
||||||
|
encodeurl@~1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||||
|
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
|
||||||
|
|
||||||
|
encodeurl@~2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
|
||||||
|
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
||||||
|
|
||||||
|
es-define-property@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
|
||||||
|
integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
|
||||||
|
dependencies:
|
||||||
|
get-intrinsic "^1.2.4"
|
||||||
|
|
||||||
|
es-errors@^1.3.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||||
|
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||||
|
|
||||||
|
escape-html@~1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||||
|
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
|
||||||
|
|
||||||
|
etag@~1.8.1:
|
||||||
|
version "1.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||||
|
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
||||||
|
|
||||||
|
express@^4.18.3:
|
||||||
|
version "4.21.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915"
|
||||||
|
integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==
|
||||||
|
dependencies:
|
||||||
|
accepts "~1.3.8"
|
||||||
|
array-flatten "1.1.1"
|
||||||
|
body-parser "1.20.3"
|
||||||
|
content-disposition "0.5.4"
|
||||||
|
content-type "~1.0.4"
|
||||||
|
cookie "0.6.0"
|
||||||
|
cookie-signature "1.0.6"
|
||||||
|
debug "2.6.9"
|
||||||
|
depd "2.0.0"
|
||||||
|
encodeurl "~2.0.0"
|
||||||
|
escape-html "~1.0.3"
|
||||||
|
etag "~1.8.1"
|
||||||
|
finalhandler "1.3.1"
|
||||||
|
fresh "0.5.2"
|
||||||
|
http-errors "2.0.0"
|
||||||
|
merge-descriptors "1.0.3"
|
||||||
|
methods "~1.1.2"
|
||||||
|
on-finished "2.4.1"
|
||||||
|
parseurl "~1.3.3"
|
||||||
|
path-to-regexp "0.1.10"
|
||||||
|
proxy-addr "~2.0.7"
|
||||||
|
qs "6.13.0"
|
||||||
|
range-parser "~1.2.1"
|
||||||
|
safe-buffer "5.2.1"
|
||||||
|
send "0.19.0"
|
||||||
|
serve-static "1.16.2"
|
||||||
|
setprototypeof "1.2.0"
|
||||||
|
statuses "2.0.1"
|
||||||
|
type-is "~1.6.18"
|
||||||
|
utils-merge "1.0.1"
|
||||||
|
vary "~1.1.2"
|
||||||
|
|
||||||
|
finalhandler@1.3.1:
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019"
|
||||||
|
integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==
|
||||||
|
dependencies:
|
||||||
|
debug "2.6.9"
|
||||||
|
encodeurl "~2.0.0"
|
||||||
|
escape-html "~1.0.3"
|
||||||
|
on-finished "2.4.1"
|
||||||
|
parseurl "~1.3.3"
|
||||||
|
statuses "2.0.1"
|
||||||
|
unpipe "~1.0.0"
|
||||||
|
|
||||||
|
forwarded@0.2.0:
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||||
|
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
|
||||||
|
|
||||||
|
fresh@0.5.2:
|
||||||
|
version "0.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||||
|
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
|
||||||
|
|
||||||
|
function-bind@^1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||||
|
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||||
|
|
||||||
|
get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
|
||||||
|
integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
has-proto "^1.0.1"
|
||||||
|
has-symbols "^1.0.3"
|
||||||
|
hasown "^2.0.0"
|
||||||
|
|
||||||
|
gopd@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
|
||||||
|
integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
|
||||||
|
dependencies:
|
||||||
|
get-intrinsic "^1.1.3"
|
||||||
|
|
||||||
|
has-property-descriptors@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
|
||||||
|
integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
|
||||||
|
dependencies:
|
||||||
|
es-define-property "^1.0.0"
|
||||||
|
|
||||||
|
has-proto@^1.0.1:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd"
|
||||||
|
integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==
|
||||||
|
|
||||||
|
has-symbols@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
|
||||||
|
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
|
||||||
|
|
||||||
|
hasown@^2.0.0:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
|
||||||
|
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||||
|
dependencies:
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
|
||||||
|
http-errors@2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
|
||||||
|
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
|
||||||
|
dependencies:
|
||||||
|
depd "2.0.0"
|
||||||
|
inherits "2.0.4"
|
||||||
|
setprototypeof "1.2.0"
|
||||||
|
statuses "2.0.1"
|
||||||
|
toidentifier "1.0.1"
|
||||||
|
|
||||||
|
iconv-lite@0.4.24:
|
||||||
|
version "0.4.24"
|
||||||
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||||
|
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||||
|
dependencies:
|
||||||
|
safer-buffer ">= 2.1.2 < 3"
|
||||||
|
|
||||||
|
inherits@2.0.4:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
|
|
||||||
|
ipaddr.js@1.9.1:
|
||||||
|
version "1.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||||
|
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
|
||||||
|
|
||||||
|
media-typer@0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
|
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
|
||||||
|
|
||||||
|
merge-descriptors@1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
|
||||||
|
integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
|
||||||
|
|
||||||
|
methods@~1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||||
|
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
|
||||||
|
|
||||||
|
mime-db@1.52.0:
|
||||||
|
version "1.52.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||||
|
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||||
|
|
||||||
|
mime-types@~2.1.24, mime-types@~2.1.34:
|
||||||
|
version "2.1.35"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||||
|
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||||
|
dependencies:
|
||||||
|
mime-db "1.52.0"
|
||||||
|
|
||||||
|
mime@1.6.0:
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||||
|
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||||
|
|
||||||
|
ms@2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
|
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
|
||||||
|
|
||||||
|
ms@2.1.3:
|
||||||
|
version "2.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||||
|
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||||
|
|
||||||
|
negotiator@0.6.3:
|
||||||
|
version "0.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
||||||
|
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
||||||
|
|
||||||
|
object-assign@^4:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||||
|
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
||||||
|
|
||||||
|
object-inspect@^1.13.1:
|
||||||
|
version "1.13.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff"
|
||||||
|
integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==
|
||||||
|
|
||||||
|
on-finished@2.4.1:
|
||||||
|
version "2.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
||||||
|
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
|
||||||
|
dependencies:
|
||||||
|
ee-first "1.1.1"
|
||||||
|
|
||||||
|
parseurl@~1.3.3:
|
||||||
|
version "1.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||||
|
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
|
||||||
|
|
||||||
|
path-to-regexp@0.1.10:
|
||||||
|
version "0.1.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b"
|
||||||
|
integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
|
||||||
|
|
||||||
|
proxy-addr@~2.0.7:
|
||||||
|
version "2.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
|
||||||
|
integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
|
||||||
|
dependencies:
|
||||||
|
forwarded "0.2.0"
|
||||||
|
ipaddr.js "1.9.1"
|
||||||
|
|
||||||
|
qs@6.13.0:
|
||||||
|
version "6.13.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
|
||||||
|
integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
|
||||||
|
dependencies:
|
||||||
|
side-channel "^1.0.6"
|
||||||
|
|
||||||
|
range-parser@~1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||||
|
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||||
|
|
||||||
|
raw-body@2.5.2:
|
||||||
|
version "2.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
|
||||||
|
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
|
||||||
|
dependencies:
|
||||||
|
bytes "3.1.2"
|
||||||
|
http-errors "2.0.0"
|
||||||
|
iconv-lite "0.4.24"
|
||||||
|
unpipe "1.0.0"
|
||||||
|
|
||||||
|
safe-buffer@5.2.1:
|
||||||
|
version "5.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||||
|
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||||
|
|
||||||
|
"safer-buffer@>= 2.1.2 < 3":
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||||
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
|
||||||
|
send@0.19.0:
|
||||||
|
version "0.19.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
|
||||||
|
integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==
|
||||||
|
dependencies:
|
||||||
|
debug "2.6.9"
|
||||||
|
depd "2.0.0"
|
||||||
|
destroy "1.2.0"
|
||||||
|
encodeurl "~1.0.2"
|
||||||
|
escape-html "~1.0.3"
|
||||||
|
etag "~1.8.1"
|
||||||
|
fresh "0.5.2"
|
||||||
|
http-errors "2.0.0"
|
||||||
|
mime "1.6.0"
|
||||||
|
ms "2.1.3"
|
||||||
|
on-finished "2.4.1"
|
||||||
|
range-parser "~1.2.1"
|
||||||
|
statuses "2.0.1"
|
||||||
|
|
||||||
|
serve-static@1.16.2:
|
||||||
|
version "1.16.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296"
|
||||||
|
integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==
|
||||||
|
dependencies:
|
||||||
|
encodeurl "~2.0.0"
|
||||||
|
escape-html "~1.0.3"
|
||||||
|
parseurl "~1.3.3"
|
||||||
|
send "0.19.0"
|
||||||
|
|
||||||
|
set-function-length@^1.2.1:
|
||||||
|
version "1.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
|
||||||
|
integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
|
||||||
|
dependencies:
|
||||||
|
define-data-property "^1.1.4"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
get-intrinsic "^1.2.4"
|
||||||
|
gopd "^1.0.1"
|
||||||
|
has-property-descriptors "^1.0.2"
|
||||||
|
|
||||||
|
setprototypeof@1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||||
|
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||||
|
|
||||||
|
side-channel@^1.0.6:
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
|
||||||
|
integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
|
||||||
|
dependencies:
|
||||||
|
call-bind "^1.0.7"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
get-intrinsic "^1.2.4"
|
||||||
|
object-inspect "^1.13.1"
|
||||||
|
|
||||||
|
statuses@2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
||||||
|
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
|
||||||
|
|
||||||
|
toidentifier@1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||||
|
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||||
|
|
||||||
|
type-is@~1.6.18:
|
||||||
|
version "1.6.18"
|
||||||
|
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||||
|
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
|
||||||
|
dependencies:
|
||||||
|
media-typer "0.3.0"
|
||||||
|
mime-types "~2.1.24"
|
||||||
|
|
||||||
|
unpipe@1.0.0, unpipe@~1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
|
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
||||||
|
|
||||||
|
utils-merge@1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||||
|
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||||
|
|
||||||
|
vary@^1, vary@~1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||||
|
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
||||||
@@ -22,6 +22,65 @@
|
|||||||
|
|
||||||
<link rel="icon" href="favicon.ico" />
|
<link rel="icon" href="favicon.ico" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/fa/css/fontawesome.css" />
|
||||||
|
<link rel="stylesheet" href="/fa/css/brands.css" />
|
||||||
|
<link rel="stylesheet" href="/fa/css/regular.css" />
|
||||||
|
<link rel="stylesheet" href="/fa/css/solid.css" />
|
||||||
|
|
||||||
|
<!-- Preloads -->
|
||||||
|
<link rel="preload" href="fonts/Quicksand-Bold.woff2" as="font" type="font/woff2" crossorigin />
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/fonts/Quicksand-Light.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/fonts/Quicksand-Medium.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/fonts/Quicksand-Regular.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/fonts/Quicksand-SemiBold.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link rel="preload" as="image" href="/images/stacjownik-header-logo.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-dispatcher.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-train.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-arrow-desc.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-pojazdownik.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-stats.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-filter2.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-user.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-like.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-gnr.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-spawn.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-timetableAll.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-timetableUnconfirmed.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-timetableConfirmed.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-discord.png" />
|
||||||
|
|
||||||
|
<link rel="prefetch" as="image" href="/images/icon-arrow-asc.svg" />
|
||||||
|
<link rel="prefetch" as="image" href="/images/icon-diamond.svg" />
|
||||||
|
|
||||||
<!-- Static OpenGraph meta -->
|
<!-- Static OpenGraph meta -->
|
||||||
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
|
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
|
||||||
<meta property="og:url" content="https://stacjownik-td2.web.app/" />
|
<meta property="og:url" content="https://stacjownik-td2.web.app/" />
|
||||||
@@ -31,10 +90,12 @@
|
|||||||
property="og:description"
|
property="og:description"
|
||||||
content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2"
|
content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<meta
|
<meta
|
||||||
property="og:image"
|
property="og:image"
|
||||||
content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg"
|
content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1200" />
|
||||||
<meta property="og:image:height" content="630" />
|
<meta property="og:image:height" content="630" />
|
||||||
<meta property="og:site_name" content="Stacjownik" />
|
<meta property="og:site_name" content="Stacjownik" />
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "stacjownik",
|
"name": "stacjownik",
|
||||||
"version": "1.26.1",
|
"version": "1.31.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --mode staging",
|
||||||
|
"dev:mock": "vite --mode development & yarn --cwd ./api-mock start",
|
||||||
|
"dev:fetch": "yarn --cwd ./api-mock fetch",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"deploy:prod": "yarn build && firebase deploy --only hosting",
|
"deploy:prod": "yarn build && firebase deploy --only hosting",
|
||||||
"deploy:dev": "yarn build && firebase hosting:channel:deploy dev --expires 7d",
|
"deploy:dev": "yarn build && firebase hosting:channel:deploy dev --expires 7d",
|
||||||
@@ -14,27 +16,27 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-js": "^3.32.2",
|
"core-js": "^3.42.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^17.2.2",
|
||||||
"pinia": "^2.1.6",
|
"pinia": "^3.0.2",
|
||||||
"sass": "^1.67.0",
|
"sass": "^1.87.0",
|
||||||
"showdown": "^2.1.0",
|
"showdown": "^2.1.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-i18n": "^9.4.1",
|
"vue-i18n": "^11.1.3",
|
||||||
"vue-router": "^4.4.0"
|
"vue-router": "^4.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.14.12",
|
"@types/node": "^24.3.1",
|
||||||
"@types/showdown": "^2.0.6",
|
"@types/showdown": "^2.0.6",
|
||||||
"@vite-pwa/assets-generator": "^0.2.4",
|
"@vite-pwa/assets-generator": "^1.0.0",
|
||||||
"@vitejs/plugin-vue": "^5.1.0",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.9.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.3.4",
|
"vite": "^7.1.4",
|
||||||
"vite-plugin-pwa": "^0.20.0",
|
"vite-plugin-pwa": "^1.0.0",
|
||||||
"vue-tsc": "^2.0.28"
|
"vue-tsc": "^3.0.6"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2024 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-style-family-classic: 'Font Awesome 6 Free';
|
||||||
|
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Free';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
.far,
|
||||||
|
.fa-regular {
|
||||||
|
font-weight: 400; }
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2024 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-style-family-classic: 'Font Awesome 6 Free';
|
||||||
|
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Free';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
.fas,
|
||||||
|
.fa-solid {
|
||||||
|
font-weight: 900; }
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-cz" viewBox="0 0 640 480">
|
||||||
|
<path fill="#fff" d="M0 0h640v240H0z"/>
|
||||||
|
<path fill="#d7141a" d="M0 240h640v240H0z"/>
|
||||||
|
<path fill="#11457e" d="M360 240 0 0v480z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 225 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-de" viewBox="0 0 640 480">
|
||||||
|
<path fill="#fc0" d="M0 320h640v160H0z"/>
|
||||||
|
<path fill="#000001" d="M0 0h640v160H0z"/>
|
||||||
|
<path fill="red" d="M0 160h640v160H0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 221 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-gb" viewBox="0 0 640 480">
|
||||||
|
<path fill="#012169" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#FFF" d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0z"/>
|
||||||
|
<path fill="#C8102E" d="m424 281 216 159v40L369 281zm-184 20 6 35L54 480H0zM640 0v3L391 191l2-44L590 0zM0 0l239 176h-60L0 42z"/>
|
||||||
|
<path fill="#FFF" d="M241 0v480h160V0zM0 160v160h640V160z"/>
|
||||||
|
<path fill="#C8102E" d="M0 193v96h640v-96zM273 0v480h96V0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 504 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-it" viewBox="0 0 640 480">
|
||||||
|
<g fill-rule="evenodd" stroke-width="1pt">
|
||||||
|
<path fill="#fff" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#009246" d="M0 0h213.3v480H0z"/>
|
||||||
|
<path fill="#ce2b37" d="M426.7 0H640v480H426.7z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 289 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-pl" viewBox="0 0 640 480">
|
||||||
|
<g fill-rule="evenodd">
|
||||||
|
<path fill="#fff" d="M640 480H0V0h640z"/>
|
||||||
|
<path fill="#dc143c" d="M640 480H0V240h640z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 219 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ru" viewBox="0 0 640 480">
|
||||||
|
<path fill="#fff" d="M0 0h640v160H0z"/>
|
||||||
|
<path fill="#0039a6" d="M0 160h640v160H0z"/>
|
||||||
|
<path fill="#d52b1e" d="M0 320h640v160H0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 225 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-se" viewBox="0 0 640 480">
|
||||||
|
<path fill="#005293" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#fecb00" d="M176 0v192H0v96h176v192h96V288h368v-96H272V0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 209 B |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-sk" viewBox="0 0 640 480">
|
||||||
|
<path fill="#ee1c25" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#0b4ea2" d="M0 0h640v320H0z"/>
|
||||||
|
<path fill="#fff" d="M0 0h640v160H0z"/>
|
||||||
|
<path fill="#fff" d="M233 370.8c-43-20.7-104.6-61.9-104.6-143.2 0-81.4 4-118.4 4-118.4h201.3s3.9 37 3.9 118.4S276 350 233 370.8"/>
|
||||||
|
<path fill="#ee1c25" d="M233 360c-39.5-19-96-56.8-96-131.4s3.6-108.6 3.6-108.6h184.8s3.5 34 3.5 108.6C329 303.3 272.5 341 233 360"/>
|
||||||
|
<path fill="#fff" d="M241.4 209c10.7.2 31.6.6 50.1-5.6 0 0-.4 6.7-.4 14.4s.5 14.4.5 14.4c-17-5.7-38.1-5.8-50.2-5.7v41.2h-16.8v-41.2c-12-.1-33.1 0-50.1 5.7 0 0 .5-6.7.5-14.4s-.5-14.4-.5-14.4c18.5 6.2 39.4 5.8 50 5.6v-25.9c-9.7 0-23.7.4-39.6 5.7 0 0 .5-6.6.5-14.4 0-7.7-.5-14.4-.5-14.4 15.9 5.3 29.9 5.8 39.6 5.7-.5-16.4-5.3-37-5.3-37s9.9.7 13.8.7 13.8-.7 13.8-.7-4.8 20.6-5.3 37c9.7.1 23.7-.4 39.6-5.7 0 0-.5 6.7-.5 14.4s.5 14.4.5 14.4a119 119 0 0 0-39.7-5.7v26z"/>
|
||||||
|
<path fill="#0b4ea2" d="M233 263.3c-19.9 0-30.5 27.5-30.5 27.5s-6-13-22.2-13c-11 0-19 9.7-24.2 18.8 20 31.7 51.9 51.3 76.9 63.4 25-12 57-31.7 76.9-63.4-5.2-9-13.2-18.8-24.2-18.8-16.2 0-22.2 13-22.2 13S253 263.3 233 263.3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ua" viewBox="0 0 640 480">
|
||||||
|
<g fill-rule="evenodd" stroke-width="1pt">
|
||||||
|
<path fill="gold" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#0057b8" d="M0 0h640v240H0z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 232 B |
@@ -0,0 +1,15 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
||||||
|
<path d="M12 9v4" />
|
||||||
|
<path d="M12 17h.01" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 345 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="144" height="144" viewBox="0 0 144 144" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M82.7143 61.3284L118.429 7L22 74.9104H68.4286L36.2857 137L122 61.3284H82.7143Z" fill="#FFF500"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 213 B |
|
Before Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,47 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="512" height="512" rx="256" fill="black"/>
|
||||||
|
<rect x="329.454" y="340.498" width="7.7497" height="147.239" rx="3.87485" transform="rotate(90 329.454 340.498)" fill="white"/>
|
||||||
|
<rect x="308" y="320" width="5" height="103" rx="2.5" transform="rotate(90 308 320)" fill="white"/>
|
||||||
|
<rect x="366.263" y="367.622" width="11.6246" height="213.496" rx="5.81228" transform="rotate(90 366.263 367.622)" fill="white"/>
|
||||||
|
<g filter="url(#filter0_d_1067_42)">
|
||||||
|
<rect width="18.2931" height="124.137" rx="9.14654" transform="matrix(0.688736 0.725012 -0.688736 0.725012 212.498 294)" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<g filter="url(#filter1_d_1067_42)">
|
||||||
|
<rect width="19.6916" height="124.137" rx="9.84578" transform="matrix(-0.688736 0.725012 0.688736 0.725012 303.502 294)" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<g filter="url(#filter2_d_1067_42)">
|
||||||
|
<path d="M147.893 304.935H121.015C109.69 304.935 101.233 302.498 95.6422 297.624C90.195 292.607 87.4713 285.01 87.4713 274.832V194.628C87.4713 184.307 90.195 176.71 95.6422 171.836C101.233 166.962 109.69 164.525 121.015 164.525H178.856V187.318H115.854C114.134 187.318 113.274 188.178 113.274 189.898V279.562C113.274 281.283 114.134 282.143 115.854 282.143H154.559C156.279 282.143 157.139 281.283 157.139 279.562V245.589L159.719 249.674H138.002V228.387H181.436V274.832C181.436 285.01 178.641 292.607 173.051 297.624C167.603 302.498 159.217 304.935 147.893 304.935ZM282.921 265.371V166.46H304.853V303H287.006L222.284 204.734L226.585 203.874V303H204.867V166.46H222.499L287.006 264.511L282.921 265.371ZM358.59 303H333.218V166.46L391.059 165.6C402.527 165.313 411.199 167.894 417.077 173.341C423.097 178.788 426.108 186.672 426.108 196.994V218.711C426.108 227.025 423.814 233.978 419.227 239.568C414.783 245.159 408.189 248.671 399.445 250.104V245.159C403.889 246.019 407.401 247.739 409.981 250.319C412.561 252.9 414.783 256.842 416.647 262.146L431.053 303H405.68L389.339 254.19C388.909 253.043 388.479 252.255 388.049 251.824C387.762 251.251 386.973 250.964 385.683 250.964H354.505L358.59 246.879V303ZM358.59 183.232V233.117L354.935 229.247H397.295C399.301 229.247 400.305 228.315 400.305 226.452V190.328C400.305 188.464 399.301 187.533 397.295 187.533H354.935L358.59 183.232Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_1067_42" x="101.768" y="268.962" width="148.561" height="153.339" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset/>
|
||||||
|
<feGaussianBlur stdDeviation="14.4611"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1067_42"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1067_42" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="filter1_d_1067_42" x="264.99" y="269.259" width="148.96" height="153.759" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset/>
|
||||||
|
<feGaussianBlur stdDeviation="14.4611"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1067_42"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1067_42" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="filter2_d_1067_42" x="85.2844" y="161.245" width="352.33" height="149.158" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dx="2.18692" dy="1.09346"/>
|
||||||
|
<feGaussianBlur stdDeviation="2.18692"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.5375 0 0 0 0 0.5375 0 0 0 0 0.5375 0 0 0 1 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1067_42"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1067_42" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,60 @@
|
|||||||
|
<svg width="79" height="127" viewBox="0 0 79 127" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="icon-loading">
|
||||||
|
<g id="Rectangle 37">
|
||||||
|
<rect id="Rectangle 38" x="36.9999" y="79" width="6" height="16" fill="#FF0000"/>
|
||||||
|
<rect id="Rectangle 40" x="36.9999" y="111" width="6" height="16" fill="#FF0000"/>
|
||||||
|
<rect id="Rectangle 39" x="36.9999" y="95" width="6" height="16" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<g id="Group 8">
|
||||||
|
<path id="Vector 15" d="M59.5018 41.0003H23.0018C-1.49817 41.0003 -0.498171 79.5003 23.0018 79.5003H59.5018C83.0018 79.5003 84.0018 41.0003 59.5018 41.0003Z" fill="#3F3E3E"/>
|
||||||
|
<g id="Group 51">
|
||||||
|
<circle id="light-left" cx="22.9999" cy="60" r="9" fill="#FF1313" fill-opacity="1"/>
|
||||||
|
|
||||||
|
<animate
|
||||||
|
attributeType="XML"
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.25;1;1;0.25;0.25"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="Group 51">
|
||||||
|
<circle id="light-right" cx="57.9999" cy="60" r="9" fill="#FF1313" fill-opacity="1"/>
|
||||||
|
|
||||||
|
<animate
|
||||||
|
attributeType="XML"
|
||||||
|
attributeName="opacity"
|
||||||
|
values="1;0.25;0.25;1;1"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
|
||||||
|
</g>
|
||||||
|
<g id="Group 52">
|
||||||
|
<rect id="Rectangle 36" x="37.9999" y="10" width="4" height="31" fill="#525252"/>
|
||||||
|
<g id="Vector 23">
|
||||||
|
<path id="Rectangle 28" d="M4.09756 32.3241L10.9579 29.2933L14.1908 36.611L7.33047 39.6418L3.42724 36.9932L4.09756 32.3241Z" fill="#FF0000"/>
|
||||||
|
<path id="Rectangle 30" d="M10.9579 29.2933L20.105 25.2522L23.3379 32.5698L14.1908 36.611L12.5743 32.9521L10.9579 29.2933Z" fill="white"/>
|
||||||
|
<path id="Rectangle 34" d="M20.105 25.2522L29.2521 21.211L32.485 28.5287L23.3379 32.5698L21.7214 28.911L20.105 25.2522Z" fill="#FF0000"/>
|
||||||
|
<path id="Rectangle 35" d="M47.5463 13.1288L56.6934 9.08762L59.9263 16.4053L50.7792 20.4464L49.1627 16.7876L47.5463 13.1288Z" fill="#FF0000"/>
|
||||||
|
<path id="Rectangle 31" d="M29.2521 21.211L38.3992 17.1699L41.6321 24.4876L32.485 28.5287L30.8685 24.8699L29.2521 21.211Z" fill="white"/>
|
||||||
|
<path id="Rectangle 32" d="M38.3992 17.1699L47.5463 13.1288L50.7792 20.4464L41.6321 24.4876L40.0156 20.8287L38.3992 17.1699Z" fill="white"/>
|
||||||
|
<path id="Rectangle 33" d="M56.6934 9.08762L65.8404 5.04649L69.0734 12.3642L59.9263 16.4053L58.3098 12.7465L56.6934 9.08762Z" fill="white"/>
|
||||||
|
<path id="Rectangle 29" d="M73.1581 1.81359L65.8405 5.04649L69.0734 12.3642L76.391 9.13126L76.604 4.6642L73.1581 1.81359Z" fill="#FF0000"/>
|
||||||
|
</g>
|
||||||
|
<g id="Vector 24">
|
||||||
|
<path id="Rectangle 28_2" d="M6.36567 3.47438L13.3598 6.18222L10.4714 13.6426L3.47731 10.9348L2.59012 6.30195L6.36567 3.47438Z" fill="#FF0000"/>
|
||||||
|
<path id="Rectangle 30_2" d="M13.3597 6.18222L22.6852 9.79268L19.7969 17.2531L10.4714 13.6426L11.9156 9.91241L13.3597 6.18222Z" fill="white"/>
|
||||||
|
<path id="Rectangle 34_2" d="M22.6853 9.79268L32.0108 13.4031L29.1224 20.8635L19.7969 17.2531L21.2411 13.5229L22.6853 9.79268Z" fill="#FF0000"/>
|
||||||
|
<path id="Rectangle 35_2" d="M50.6617 20.6241L59.9872 24.2345L57.0989 31.6949L47.7734 28.0844L49.2176 24.3542L50.6617 20.6241Z" fill="#FF0000"/>
|
||||||
|
<path id="Rectangle 31_2" d="M32.0107 13.4031L41.3362 17.0136L38.4479 24.474L29.1224 20.8635L30.5666 17.1333L32.0107 13.4031Z" fill="white"/>
|
||||||
|
<path id="Rectangle 32_2" d="M41.3363 17.0136L50.6618 20.6241L47.7734 28.0844L38.4479 24.474L39.8921 20.7438L41.3363 17.0136Z" fill="white"/>
|
||||||
|
<path id="Rectangle 33_2" d="M59.9872 24.2345L69.3127 27.845L66.4243 35.3054L57.0988 31.6949L58.543 27.9647L59.9872 24.2345Z" fill="white"/>
|
||||||
|
<path id="Rectangle 29_2" d="M76.7731 30.7333L69.3127 27.845L66.4243 35.3054L73.8847 38.1937L77.194 35.1856L76.7731 30.7333Z" fill="#FF0000"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="39" height="23" viewBox="0 0 39 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="39" height="23" fill="#FF0F0F"/>
|
|
||||||
<rect width="39" height="11.5" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 199 B |
@@ -0,0 +1,24 @@
|
|||||||
|
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="path-1-inside-1_102_63" fill="white">
|
||||||
|
<path d="M0 250C0 111.929 111.929 6.10352e-05 250 6.10352e-05C388.071 6.10352e-05 500 111.929 500 250C500 388.071 388.071 500 250 500C111.929 500 0 388.071 0 250Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M0 250C0 111.929 111.929 6.10352e-05 250 6.10352e-05C388.071 6.10352e-05 500 111.929 500 250C500 388.071 388.071 500 250 500C111.929 500 0 388.071 0 250Z" fill="#242424"/>
|
||||||
|
<path d="M0 222.821C0 84.7503 111.929 -27.1785 250 -27.1785C388.071 -27.1785 500 84.7503 500 222.821V250C500 126.939 388.071 27.1787 250 27.1787C111.929 27.1787 0 126.939 0 250V222.821ZM500 277.179C500 415.25 388.071 527.179 250 527.179C111.929 527.179 0 415.25 0 277.179V250C0 373.061 111.929 472.821 250 472.821C388.071 472.821 500 373.061 500 250V277.179ZM0 500V6.10352e-05V500ZM500 6.10352e-05V500V6.10352e-05Z" fill="#FFD600" mask="url(#path-1-inside-1_102_63)"/>
|
||||||
|
<path d="M210.369 301.604C210.369 301.604 210.369 341.807 210.369 364.846C210.369 387.885 202.798 417.491 171.591 417.491C140.385 417.491 132.813 417.491 132.813 417.491L132.812 78.125L250.754 78.125C274.312 78.125 294.504 80.9665 311.331 86.6494C328.311 92.1788 342.232 99.8585 353.093 109.689C364.107 119.519 372.214 131.115 377.415 144.478C382.616 157.84 385.217 172.278 385.217 187.791C385.217 204.533 382.54 219.892 377.186 233.869C371.832 247.846 363.648 259.827 352.634 269.81C341.62 279.794 327.623 287.627 310.643 293.31C293.816 298.839 273.853 301.604 250.754 301.604L210.369 301.604ZM210.369 242.854L250.754 242.854C270.946 242.854 285.479 238.016 294.351 228.34C303.224 218.663 307.66 205.147 307.66 187.791C307.66 180.111 306.512 173.123 304.218 166.825C301.923 160.528 298.405 155.152 293.663 150.698C289.074 146.09 283.184 142.558 275.995 140.1C268.958 137.643 260.544 136.414 250.754 136.414L210.369 136.414L210.369 242.854Z" fill="url(#paint0_linear_102_63)"/>
|
||||||
|
<path d="M239.215 301.604C239.215 301.604 239.215 341.807 239.215 364.846C239.215 387.885 231.643 417.491 200.437 417.491C169.231 417.491 161.659 417.491 161.659 417.491L161.658 78.125L279.6 78.125C303.158 78.125 323.35 80.9665 340.177 86.6494C357.157 92.1788 371.077 99.8585 381.938 109.689C392.952 119.519 401.06 131.115 406.261 144.478C411.462 157.84 414.062 172.278 414.062 187.791C414.062 204.533 411.385 219.892 406.031 233.869C400.677 247.846 392.493 259.827 381.479 269.81C370.465 279.794 356.468 287.627 339.488 293.31C322.662 298.839 302.699 301.604 279.6 301.604L239.215 301.604ZM239.215 242.854L279.6 242.854C299.792 242.854 314.325 238.016 323.197 228.34C332.069 218.663 336.505 205.147 336.505 187.791C336.505 180.111 335.358 173.123 333.064 166.825C330.769 160.528 327.251 155.152 322.509 150.698C317.919 146.09 312.03 142.558 304.84 140.1C297.804 137.643 289.39 136.414 279.6 136.414L239.215 136.414L239.215 242.854Z" fill="url(#paint1_linear_102_63)"/>
|
||||||
|
<path d="M210.685 301.604C210.685 301.604 210.685 341.807 210.685 364.846C210.685 387.885 203.082 417.491 171.749 417.491C140.416 417.491 132.813 417.491 132.813 417.491L132.812 78.125L251.233 78.125C274.887 78.125 295.161 80.9665 312.057 86.6494C329.105 92.1788 343.083 99.8585 353.988 109.689C365.046 119.519 373.187 131.115 378.409 144.478C383.631 157.84 386.242 172.278 386.242 187.791C386.242 204.533 383.555 219.892 378.179 233.869C372.803 247.846 364.586 259.827 353.527 269.81C342.468 279.794 328.414 287.627 311.365 293.31C294.47 298.839 274.426 301.604 251.233 301.604L210.685 301.604ZM210.685 242.854L251.233 242.854C271.508 242.854 286.099 238.016 295.008 228.34C303.916 218.663 308.37 205.147 308.37 187.791C308.37 180.111 307.218 173.123 304.914 166.825C302.611 160.528 299.078 155.152 294.316 150.698C289.709 146.09 283.795 142.558 276.576 140.1C269.511 137.643 261.063 136.414 251.233 136.414L210.685 136.414L210.685 242.854Z" fill="url(#paint2_radial_102_63)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_102_63" x1="259.015" y1="78.125" x2="259.015" y2="417.491" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.135417" stop-color="#FFD600"/>
|
||||||
|
<stop offset="1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear_102_63" x1="287.86" y1="78.125" x2="287.86" y2="417.491" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.135417" stop-color="#FFD600"/>
|
||||||
|
<stop offset="1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint2_radial_102_63" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(259.527 247.808) rotate(0.36307) scale(345.948 325.206)">
|
||||||
|
<stop offset="0.484375" stop-color="white"/>
|
||||||
|
<stop offset="1"/>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="144" height="144" viewBox="0 0 144 144" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="9.20437" y="2.4661" width="36.5457" height="57.8287" rx="16.7449" transform="matrix(0.869001 -0.494811 0.505207 0.862998 50.006 87.4256)" stroke="white" stroke-width="13.3959"/>
|
||||||
|
<rect x="9.20437" y="2.4661" width="36.5457" height="57.8287" rx="16.7449" transform="matrix(0.869001 -0.494811 0.505207 0.862998 14.9599 29.6039)" stroke="white" stroke-width="13.3959"/>
|
||||||
|
<path d="M65.1133 58.145L79.8524 84.3103" stroke="white" stroke-width="10.0469" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 611 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="512" height="512" rx="256" fill="#151414"/>
|
||||||
|
<path d="M72.4253 291.986V279.965H120.201C123.283 279.965 124.824 278.424 124.824 275.342V264.246C124.824 261.266 123.54 259.571 120.971 259.16L90.9189 252.995C78.5898 250.529 72.4253 242.259 72.4253 228.183V219.553C72.4253 202.292 81.0557 193.662 98.3164 193.662H133.608L143.934 201.675V213.696H99.2411C96.1588 213.696 94.6177 215.237 94.6177 218.32V228.337C94.6177 231.214 95.9019 232.909 98.4705 233.423L128.523 239.433C140.852 241.899 147.016 250.17 147.016 264.246V274.109C147.016 291.37 138.386 300 121.125 300H82.7509L72.4253 291.986ZM167.651 300V193.662H219.433C236.694 193.662 245.324 202.292 245.324 219.553V237.122C245.324 249.964 240.546 257.978 230.991 261.163L248.406 295.377L245.786 300H226.676L207.874 263.013H189.843V300H167.651ZM189.843 242.978H218.508C221.591 242.978 223.132 241.437 223.132 238.355V218.32C223.132 215.237 221.591 213.696 218.508 213.696H189.843V242.978ZM262.96 274.109V253.766H285.153V275.342C285.153 278.424 286.694 279.965 289.776 279.965H310.736C313.818 279.965 315.359 278.424 315.359 275.342V213.696H286.386V193.662H337.551V274.109C337.551 291.37 328.921 300 311.66 300H288.852C271.591 300 262.96 291.37 262.96 274.109ZM361.948 300V193.662H413.731C430.991 193.662 439.622 202.292 439.622 219.553V240.204C439.622 257.465 430.991 266.095 413.731 266.095H384.141V300H361.948ZM384.141 246.06H412.806C415.888 246.06 417.429 244.519 417.429 241.437V218.32C417.429 215.237 415.888 213.696 412.806 213.696H384.141V246.06Z" fill="white"/>
|
||||||
|
<path d="M304.958 332.848V322.831H348.418V332.848H332.236V376H321.14V332.848H304.958ZM356.61 376V322.831H376.799C391.285 322.831 398.529 330.074 398.529 344.561V354.27C398.529 368.757 391.285 376 376.799 376H356.61ZM367.706 365.983H377.415C384.093 365.983 387.432 362.643 387.432 355.965V342.866C387.432 336.187 384.093 332.848 377.415 332.848H367.706V365.983ZM407.35 376V358.662C407.35 351.624 410.432 347.489 416.597 346.256L430.852 343.405C432.136 343.148 432.779 342.3 432.779 340.862V335.16C432.779 333.619 432.008 332.848 430.467 332.848H408.891V326.838L414.054 322.831H430.929C439.56 322.831 443.875 327.146 443.875 335.776V340.785C443.875 347.823 440.792 351.958 434.628 353.191L420.372 356.042C419.088 356.299 418.446 357.147 418.446 358.585V365.983H443.875V376H407.35Z" fill="#E63E3E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -13,7 +13,7 @@
|
|||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"theme_color": "#ffc014",
|
"theme_color": "#4d4d4d",
|
||||||
"background_color": "#4d4d4d",
|
"background_color": "#4d4d4d",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"start_url": "."
|
"start_url": "."
|
||||||
|
|||||||
@@ -5,39 +5,26 @@
|
|||||||
@toggle-card="() => (isUpdateCardOpen = false)"
|
@toggle-card="() => (isUpdateCardOpen = false)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AppWelcomeCard :is-card-open="isWelcomeCardOpen" @toggle-card="closeWelcomeCard" />
|
||||||
|
|
||||||
<Tooltip />
|
<Tooltip />
|
||||||
|
|
||||||
<transition name="modal-anim">
|
<AppHeader />
|
||||||
<keep-alive>
|
|
||||||
<TrainModal />
|
|
||||||
</keep-alive>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<AppHeader :current-lang="currentLang" @change-lang="changeLang" />
|
|
||||||
|
|
||||||
<main class="app_main">
|
<main class="app_main">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<keep-alive exclude="SceneryView">
|
<keep-alive>
|
||||||
<component :is="Component" :key="$route.name" />
|
<component :is="Component" :key="$route.name" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="app_footer">
|
<AppFooter
|
||||||
©
|
:version="VERSION"
|
||||||
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
|
:is-on-production-host="isOnProductionHost"
|
||||||
{{ new Date().getUTCFullYear() }} |
|
:is-update-card-open="isUpdateCardOpen"
|
||||||
<button class="btn--text" @click="() => (isUpdateCardOpen = true)">
|
@open-update-card="() => (isUpdateCardOpen = true)"
|
||||||
v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}
|
/>
|
||||||
</button>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<a href="https://discord.gg/x2mpNN3svk">
|
|
||||||
<img src="/images/icon-discord.png" alt="" /> <b>{{ $t('footer.discord') }}</b>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div style="display: none">∫ ukryta taktyczna całka do programowania w HTMLu</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -45,7 +32,7 @@
|
|||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { version } from '.././package.json';
|
import { version } from '../package.json';
|
||||||
import { Status } from './typings/common';
|
import { Status } from './typings/common';
|
||||||
import { useMainStore } from './store/mainStore';
|
import { useMainStore } from './store/mainStore';
|
||||||
import { useApiStore } from './store/apiStore';
|
import { useApiStore } from './store/apiStore';
|
||||||
@@ -54,21 +41,25 @@ import { useTooltipStore } from './store/tooltipStore';
|
|||||||
import Clock from './components/App/Clock.vue';
|
import Clock from './components/App/Clock.vue';
|
||||||
import StatusIndicator from './components/App/StatusIndicator.vue';
|
import StatusIndicator from './components/App/StatusIndicator.vue';
|
||||||
import AppHeader from './components/App/AppHeader.vue';
|
import AppHeader from './components/App/AppHeader.vue';
|
||||||
import TrainModal from './components/TrainsView/TrainModal.vue';
|
|
||||||
import Tooltip from './components/Tooltip/Tooltip.vue';
|
import Tooltip from './components/Tooltip/Tooltip.vue';
|
||||||
import UpdateCard from './components/App/UpdateCard.vue';
|
import UpdateCard from './components/App/UpdateCard.vue';
|
||||||
|
|
||||||
import StorageManager from './managers/storageManager';
|
import StorageManager from './managers/storageManager';
|
||||||
|
import AppFooter from './components/App/AppFooter.vue';
|
||||||
|
import AppWelcomeCard from './components/App/AppWelcomeCard.vue';
|
||||||
|
|
||||||
const STORAGE_VERSION_KEY = 'app_version';
|
const STORAGE_VERSION_KEY = 'app_version';
|
||||||
|
const WELCOME_CARD_SEEN_KEY = 'welcome_card_seen';
|
||||||
|
const MIGRATE_INFO_CARD_SEEN_KEY = 'migrate_info_card_seen';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
Clock,
|
Clock,
|
||||||
StatusIndicator,
|
StatusIndicator,
|
||||||
AppHeader,
|
AppHeader,
|
||||||
TrainModal,
|
AppFooter,
|
||||||
UpdateCard,
|
UpdateCard,
|
||||||
|
AppWelcomeCard,
|
||||||
Tooltip
|
Tooltip
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -79,9 +70,9 @@ export default defineComponent({
|
|||||||
tooltipStore: useTooltipStore(),
|
tooltipStore: useTooltipStore(),
|
||||||
|
|
||||||
isUpdateCardOpen: false,
|
isUpdateCardOpen: false,
|
||||||
|
isWelcomeCardOpen: false,
|
||||||
|
|
||||||
currentLang: 'pl',
|
isOnProductionHost: /(stacjownik-td2)(\.web\.app|\.spythere\.eu)/.test(location.hostname)
|
||||||
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
@@ -90,6 +81,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
window.addEventListener('mousemove', (e: MouseEvent) => this.tooltipStore.handle(e));
|
window.addEventListener('mousemove', (e: MouseEvent) => this.tooltipStore.handle(e));
|
||||||
|
window.addEventListener('mousedown', () => this.tooltipStore.hide());
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -99,13 +91,34 @@ export default defineComponent({
|
|||||||
this.loadLang();
|
this.loadLang();
|
||||||
this.setupOfflineHandling();
|
this.setupOfflineHandling();
|
||||||
this.checkAppVersion();
|
this.checkAppVersion();
|
||||||
|
this.handleQueries();
|
||||||
|
this.handleMigrateInfo();
|
||||||
|
|
||||||
this.apiStore.setupAPIData();
|
this.apiStore.setupAPIData();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleQueries() {
|
||||||
|
const query = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
if (query.get('welcomeCard') == '1') {
|
||||||
|
this.isWelcomeCardOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.get('migrateCard') == '1') {
|
||||||
|
this.store.isMigrateInfoCardOpen = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async checkAppVersion() {
|
async checkAppVersion() {
|
||||||
|
const isWelcomeCardSeen = StorageManager.getBooleanValue(WELCOME_CARD_SEEN_KEY);
|
||||||
const storageVersion = StorageManager.getStringValue(STORAGE_VERSION_KEY);
|
const storageVersion = StorageManager.getStringValue(STORAGE_VERSION_KEY);
|
||||||
|
|
||||||
|
if (isWelcomeCardSeen == false && storageVersion == '') {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isWelcomeCardOpen = true;
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const releaseData = await (
|
const releaseData = await (
|
||||||
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
|
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
|
||||||
@@ -152,18 +165,18 @@ export default defineComponent({
|
|||||||
this.apiStore.connectToAPI();
|
this.apiStore.connectToAPI();
|
||||||
},
|
},
|
||||||
|
|
||||||
changeLang(lang: string) {
|
handleMigrateInfo() {
|
||||||
this.$i18n.locale = lang;
|
if (location.hostname != 'stacjownik-td2.web.app') return;
|
||||||
this.currentLang = lang;
|
if (StorageManager.getBooleanValue(MIGRATE_INFO_CARD_SEEN_KEY) === true) return;
|
||||||
|
|
||||||
StorageManager.setStringValue('lang', lang);
|
this.store.isMigrateInfoCardOpen = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
loadLang() {
|
loadLang() {
|
||||||
const storageLang = StorageManager.getStringValue('lang');
|
const storageLang = StorageManager.getStringValue('lang');
|
||||||
|
|
||||||
if (storageLang) {
|
if (storageLang) {
|
||||||
this.changeLang(storageLang);
|
this.store.changeLocale(storageLang);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,42 +184,34 @@ export default defineComponent({
|
|||||||
|
|
||||||
const naviLanguage = window.navigator.language.toString();
|
const naviLanguage = window.navigator.language.toString();
|
||||||
|
|
||||||
if (naviLanguage.startsWith('en')) {
|
if (!naviLanguage.startsWith('pl')) {
|
||||||
this.changeLang('en');
|
this.store.changeLocale('en');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeWelcomeCard() {
|
||||||
|
this.isWelcomeCardOpen = false;
|
||||||
|
StorageManager.setBooleanValue(WELCOME_CARD_SEEN_KEY, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
closeMigrateInfoCard() {
|
||||||
|
this.store.isMigrateInfoCardOpen = false;
|
||||||
|
StorageManager.setBooleanValue(MIGRATE_INFO_CARD_SEEN_KEY, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import './styles/global';
|
@use './styles/animations';
|
||||||
@import './styles/animations';
|
@use './styles/global';
|
||||||
|
|
||||||
.route {
|
|
||||||
margin: 0 0.2em;
|
|
||||||
|
|
||||||
&-active,
|
|
||||||
&[data-active='true'] {
|
|
||||||
color: $accentCol;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// APP
|
// APP
|
||||||
#app {
|
#app {
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 1rem;
|
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
font-size: 1em;
|
||||||
@include smallScreen() {
|
|
||||||
font-size: calc(0.65rem + 0.85vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
@include screenLandscape() {
|
|
||||||
font-size: calc(0.45rem + 0.8vw);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CONTAINER
|
// CONTAINER
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<footer class="app_footer">
|
||||||
|
©
|
||||||
|
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
|
||||||
|
{{ new Date().getUTCFullYear() }} |
|
||||||
|
<button class="btn--text" @click="openUpdateCard">
|
||||||
|
v{{ version }}{{ isOnProductionHost ? '' : 'dev' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style="display: none">∫ ukryta taktyczna całka do programowania w HTMLu</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emits: ['openUpdateCard'],
|
||||||
|
props: {
|
||||||
|
isUpdateCardOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
version: String,
|
||||||
|
isOnProductionHost: Boolean
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
openUpdateCard() {
|
||||||
|
this.$emit('openUpdateCard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,24 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="app_header">
|
<header class="app_header">
|
||||||
<div class="header_container">
|
<div class="header_container">
|
||||||
<div class="header_icons">
|
|
||||||
<span class="icons-top">
|
|
||||||
<img
|
|
||||||
src="/images/icon-pl.svg"
|
|
||||||
alt="icon-pl"
|
|
||||||
@click="changeLang('en')"
|
|
||||||
v-if="currentLang == 'pl'"
|
|
||||||
/>
|
|
||||||
<img src="/images/icon-en.jpg" alt="icon-en" @click="changeLang('pl')" v-else />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header_body">
|
<div class="header_body">
|
||||||
<StatusIndicator />
|
<StatusIndicator />
|
||||||
|
|
||||||
<span class="header_brand">
|
<span class="header_brand">
|
||||||
<router-link to="/">
|
<router-link to="/">
|
||||||
<img src="/images/stacjownik-header-logo.svg" alt="Stacjownik" />
|
<img
|
||||||
|
v-if="isChristmas"
|
||||||
|
src="/images/stacjownik-header-logo-christmas.svg"
|
||||||
|
alt="Stacjownik logo (christmas)"
|
||||||
|
/>
|
||||||
|
<img v-else src="/images/stacjownik-header-logo.svg" alt="Stacjownik logo" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -40,17 +33,17 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="header_links">
|
<span class="header_links">
|
||||||
<router-link class="route" active-class="route-active" to="/" exact>
|
<router-link class="route-link" active-class="route-link-active" to="/" exact>
|
||||||
{{ $t('app.sceneries') }}
|
{{ $t('app.sceneries') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
/
|
/
|
||||||
<router-link class="route" active-class="route-active" to="/trains">{{
|
<router-link class="route-link" active-class="route-link-active" to="/trains">{{
|
||||||
$t('app.trains')
|
$t('app.trains')
|
||||||
}}</router-link>
|
}}</router-link>
|
||||||
/
|
/
|
||||||
<router-link
|
<router-link
|
||||||
class="route"
|
class="route-link"
|
||||||
active-class="route-active"
|
active-class="route-link-active"
|
||||||
:data-active="$route.path.startsWith('/journal')"
|
:data-active="$route.path.startsWith('/journal')"
|
||||||
to="/journal"
|
to="/journal"
|
||||||
>
|
>
|
||||||
@@ -69,13 +62,7 @@ import Clock from './Clock.vue';
|
|||||||
import RegionDropdown from '../Global/RegionDropdown.vue';
|
import RegionDropdown from '../Global/RegionDropdown.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: ['changeLang'],
|
components: { StatusIndicator, Clock, RegionDropdown },
|
||||||
props: {
|
|
||||||
currentLang: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
@@ -83,12 +70,6 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
|
||||||
changeLang(lang: string) {
|
|
||||||
this.$emit('changeLang', lang);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
onlineTrainsCount() {
|
onlineTrainsCount() {
|
||||||
return this.store.trainList.filter((train) => train.region == this.store.region.id).length;
|
return this.store.trainList.filter((train) => train.region == this.store.region.id).length;
|
||||||
@@ -98,14 +79,19 @@ export default defineComponent({
|
|||||||
return this.store.activeSceneryList.filter(
|
return this.store.activeSceneryList.filter(
|
||||||
(scenery) => scenery.region == this.store.region.id && scenery.dispatcherId != -1
|
(scenery) => scenery.region == this.store.region.id && scenery.dispatcherId != -1
|
||||||
).length;
|
).length;
|
||||||
|
},
|
||||||
|
|
||||||
|
isChristmas() {
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
return date.getUTCMonth() == 11 && date.getUTCDate() >= 6 && date.getUTCDate() <= 31;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
components: { StatusIndicator, Clock, RegionDropdown }
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/variables.scss';
|
@use '../../styles/responsive';
|
||||||
@import '../../styles/responsive.scss';
|
|
||||||
|
|
||||||
// HEADER
|
// HEADER
|
||||||
.app_header {
|
.app_header {
|
||||||
@@ -113,7 +99,7 @@ export default defineComponent({
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: $primaryCol;
|
background-color: #2c2c2c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@@ -128,7 +114,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
border-radius: 0 0 1em 1em;
|
border-radius: 0 0 1em 1em;
|
||||||
|
|
||||||
@include smallScreen {
|
@include responsive.smallScreen {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
}
|
}
|
||||||
@@ -167,20 +153,12 @@ export default defineComponent({
|
|||||||
|
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
|
|
||||||
@include smallScreen {
|
@include responsive.smallScreen {
|
||||||
transform: translateX(85%);
|
transform: translateX(85%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ICONS
|
|
||||||
.icons-top {
|
|
||||||
img {
|
|
||||||
width: 2.5em;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// COUNTER
|
// COUNTER
|
||||||
.info_counter {
|
.info_counter {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<template>
|
||||||
|
<Card :is-open="props.isCardOpen">
|
||||||
|
<div class="body-content">
|
||||||
|
<h1>{{ $t('welcome.title') }}</h1>
|
||||||
|
|
||||||
|
<div class="language-select">
|
||||||
|
<button :data-active="$i18n.locale == 'pl'" @click="store.changeLocale('pl')">
|
||||||
|
<FlagIcon :language-id="0" width="2.5em" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button :data-active="$i18n.locale == 'en'" @click="store.changeLocale('en')">
|
||||||
|
<FlagIcon :language-id="1" width="2.5em" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="app-description">
|
||||||
|
<i18n-t keypath="welcome.app-desc" tag="p">
|
||||||
|
<template v-slot:b1>
|
||||||
|
<b>{{ $t('welcome.app-desc-b1') }}</b>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:link>
|
||||||
|
<a href="https://td2.info.pl/" class="link" target="_blank">Train Driver 2</a>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="tabs">
|
||||||
|
<div class="tab-description">
|
||||||
|
<h2 class="text--primary">{{ $t('welcome.sceneries-header') }}</h2>
|
||||||
|
<hr />
|
||||||
|
<i18n-t keypath="welcome.sceneries-desc" tag="p">
|
||||||
|
<template v-slot:b1>
|
||||||
|
<b>{{ $t('welcome.sceneries-desc-b1') }}</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-description">
|
||||||
|
<h2 class="text--primary">{{ $t('welcome.trains-header') }}</h2>
|
||||||
|
<hr />
|
||||||
|
<i18n-t keypath="welcome.trains-desc" tag="p">
|
||||||
|
<template v-slot:b1>
|
||||||
|
<b>{{ $t('welcome.trains-desc-b1') }}</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-description">
|
||||||
|
<h2 class="text--primary">{{ $t('welcome.journal-header') }}</h2>
|
||||||
|
<hr />
|
||||||
|
<i18n-t keypath="welcome.journal-desc" tag="p">
|
||||||
|
<template v-slot:b1>
|
||||||
|
<b>{{ $t('welcome.journal-desc-b1') }}</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="other-apps">
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ $t('welcome.other-apps') }}
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<div class="apps-grid">
|
||||||
|
<a class="app-item" href="https://pojazdownik-td2.web.app/" target="_blank">
|
||||||
|
<img src="/images/icon-pojazdownik.svg" alt="pojazdownik app logo" />
|
||||||
|
<h3 class="text--primary">Pojazdownik</h3>
|
||||||
|
<p>{{ $t('welcome.pojazdownik-desc') }}</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="app-item" href="https://generator-td2.web.app/" target="_blank">
|
||||||
|
<img src="/images/icon-gnr.svg" alt="generator app logo" />
|
||||||
|
<h3 class="text--primary">GeneraTOR</h3>
|
||||||
|
<p>{{ $t('welcome.generator-desc') }}</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="app-item" href="https://srjp-td2.web.app/" target="_blank">
|
||||||
|
<img src="/images/icon-srjp.svg" alt="srjp app logo" />
|
||||||
|
<h3 class="text--primary">Rozkładownik</h3>
|
||||||
|
<p>{{ $t('welcome.srjp-desc') }}</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="bottom-info">
|
||||||
|
<i18n-t keypath="welcome.donation-info" tag="div" class="donation-info">
|
||||||
|
<template v-slot:icon1>
|
||||||
|
<img src="/images/icon-diamond.svg" alt="diamond icon" width="25" />
|
||||||
|
<span class="text--donator"> {{ $t('welcome.donation-info-icon1-text') }}</span>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
|
||||||
|
<i18n-t keypath="welcome.discord-info" tag="div" class="discord-info">
|
||||||
|
<template v-slot:discord>
|
||||||
|
<a href="https://discord.gg/x2mpNN3svk" class="link" target="_blank">
|
||||||
|
<b class="text--discord">{{ $t('welcome.discord-info-link-text') }}</b>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
|
||||||
|
<div class="bottom-text">
|
||||||
|
<i>{{ $t('welcome.bottom-text') }}</i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bottom-actions">
|
||||||
|
<button class="btn btn--action" @click="toggleCard(false)">
|
||||||
|
{{ $t('welcome.button-confirm') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Card from '../Global/Card.vue';
|
||||||
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
import FlagIcon from '../Global/FlagIcon.vue';
|
||||||
|
|
||||||
|
const store = useMainStore();
|
||||||
|
|
||||||
|
const emit = defineEmits(['toggleCard']);
|
||||||
|
const props = defineProps({
|
||||||
|
isCardOpen: Boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleCard(state: boolean) {
|
||||||
|
emit('toggleCard', state);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.body-content {
|
||||||
|
max-width: 800px;
|
||||||
|
min-height: 900px;
|
||||||
|
padding: 1em 0.5em;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.link {
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 0.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-select {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
|
||||||
|
button[data-active='false'] ::v-deep(img) {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-description {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-description {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.other-apps {
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 1em 0;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apps-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apps-grid > a.app-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
padding: 1em;
|
||||||
|
background-color: #2b2b2b;
|
||||||
|
transition: background-color 100ms ease-in-out;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #3b3b3b;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 2.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.donation-info {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-info {
|
||||||
|
margin-top: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-text {
|
||||||
|
margin: 1em 0;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1em;
|
||||||
|
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,9 +6,7 @@
|
|||||||
import { computed, defineComponent, ref } from 'vue';
|
import { computed, defineComponent, ref } from 'vue';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'VueClock',
|
name: 'VueClock',
|
||||||
data: () => ({
|
data: () => ({ timestamp: Date.now() }),
|
||||||
timestamp: Date.now()
|
|
||||||
}),
|
|
||||||
setup() {
|
setup() {
|
||||||
let timestamp = ref(Date.now());
|
let timestamp = ref(Date.now());
|
||||||
|
|
||||||
@@ -28,8 +26,6 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
|
||||||
|
|
||||||
.clock {
|
.clock {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<Card :is-open="isOpen" @toggle-card="toggleCard">
|
||||||
|
<div class="body-content">
|
||||||
|
<div class="content-top">
|
||||||
|
<img src="/images/icon-loading.svg" alt="loading" height="125" />
|
||||||
|
<h1>{{ t('migrate-info.header-text') }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p v-html="t('migrate-info.paragraph-1-html')"></p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a class="new-link" href="https://stacjownik-td2.spythere.eu/" target="_blank">
|
||||||
|
{{ t('migrate-info.paragraph-2-link-text') }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ t('migrate-info.paragraph-3-text') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="info-bottom" v-html="t('migrate-info.paragraph-4-html')"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-actions">
|
||||||
|
<button class="btn btn--action" @click="toggleCard">PRZYJĄŁEM!</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import Card from '../Global/Card.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
isOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['toggleCard']);
|
||||||
|
|
||||||
|
function toggleCard() {
|
||||||
|
emit('toggleCard');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.body-content {
|
||||||
|
max-width: 800px;
|
||||||
|
min-height: 500px;
|
||||||
|
padding: 1em 0.5em;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
padding: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.new-link {
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: var(--clr-primary);
|
||||||
|
color: transparent;
|
||||||
|
|
||||||
|
background: var(--clr-primary);
|
||||||
|
background: linear-gradient(90deg, var(--clr-primary), #ffffff);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
|
||||||
|
text-shadow: var(--clr-primary) 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-bottom {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -310,7 +310,7 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
// INDICATOR TOOLTIP ANIMATION
|
// INDICATOR TOOLTIP ANIMATION
|
||||||
.tooltip-anim {
|
.tooltip-anim {
|
||||||
@@ -379,7 +379,7 @@ export default defineComponent({
|
|||||||
content: '';
|
content: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@include midScreen() {
|
@include responsive.midScreen() {
|
||||||
left: auto;
|
left: auto;
|
||||||
right: 200%;
|
right: 200%;
|
||||||
|
|
||||||
@@ -393,7 +393,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen() {
|
@include responsive.smallScreen{
|
||||||
min-width: 8em;
|
min-width: 8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
|
<Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
|
||||||
<div class="content">
|
<div class="content" tabindex="0" ref="content">
|
||||||
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
|
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
|
||||||
|
|
||||||
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
|
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
|
||||||
@@ -13,7 +13,14 @@
|
|||||||
<p class="bottom-info">
|
<p class="bottom-info">
|
||||||
{{ $t('update.info-1') }}
|
{{ $t('update.info-1') }}
|
||||||
<br />
|
<br />
|
||||||
<span v-html="$t('update.info-2')"></span>
|
|
||||||
|
<i18n-t keypath="update.info-2">
|
||||||
|
<template v-slot:link>
|
||||||
|
<a href="https://github.com/Spythere/stacjownik" target="_blank">{{
|
||||||
|
$t('update.info-2-link-text')
|
||||||
|
}}</a>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -51,7 +58,7 @@ export default defineComponent({
|
|||||||
watch: {
|
watch: {
|
||||||
isUpdateCardOpen(val: boolean) {
|
isUpdateCardOpen(val: boolean) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (val) (this.$refs['confirm-btn'] as HTMLElement).focus();
|
if (val) (this.$refs['content'] as HTMLElement).focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -73,21 +80,24 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/variables';
|
|
||||||
|
|
||||||
::v-deep(h1) {
|
::v-deep(h1) {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: $accentCol;
|
color: var(--clr-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(h2) {
|
::v-deep(h2) {
|
||||||
padding: 0.25em 0;
|
margin-top: 1em;
|
||||||
|
padding: 0.5em 0;
|
||||||
border-bottom: 1px solid #aaa;
|
border-bottom: 1px solid #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep(h3) {
|
||||||
|
padding: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
::v-deep(ul) {
|
::v-deep(ul) {
|
||||||
list-style: initial;
|
list-style: disc;
|
||||||
padding: 1em;
|
padding: 0 1.5em;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +117,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin: 0 auto;
|
margin: 0.5em auto;
|
||||||
padding: 0.5em 0.75em;
|
padding: 0.5em 0.75em;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
@@ -119,5 +129,6 @@ p.bottom-info {
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div class="driver-not-found">
|
||||||
|
<h2>⦻ {{ $t('trains.driver-not-found-header') }}</h2>
|
||||||
|
|
||||||
|
<p class="text--grayed">
|
||||||
|
{{ $t('trains.driver-not-found-desc-1') }} <br />
|
||||||
|
{{ $t('trains.driver-not-found-desc-2') }}
|
||||||
|
<router-link to="/journal/timetables"
|
||||||
|
>{{ $t('trains.driver-not-found-journal') }} </router-link
|
||||||
|
>!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p v-if="props.trainId && otherDriverTrains.length > 0">
|
||||||
|
<i18n-t keypath="trains.driver-not-found-others">
|
||||||
|
<template v-slot:driver>
|
||||||
|
<b>{{ otherDriverTrains[0].driverName }}</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="other-driver-trains">
|
||||||
|
<template v-for="(train, i) in otherDriverTrains">
|
||||||
|
<router-link :to="`/driver?trainId=${train.id}`">
|
||||||
|
{{ train.trainNo }}
|
||||||
|
| {{ regions.find((r) => r.id == train.region)?.name ?? 'PL1' }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 1em">
|
||||||
|
<router-link to="/"><< {{ $t('trains.driver-not-found-return') }}</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
import { regions } from '../../data/options.json';
|
||||||
|
|
||||||
|
const mainStore = useMainStore();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
trainId: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const otherDriverTrains = computed(() => {
|
||||||
|
return mainStore.trainList.filter(
|
||||||
|
(train) =>
|
||||||
|
train.driverId == Number(props.trainId?.split('|')[0]) &&
|
||||||
|
(train.timetableData || train.online || train.lastSeen >= Date.now() - 60000)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.driver-not-found {
|
||||||
|
background-color: var(--clr-view-bg);
|
||||||
|
text-align: center;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 0.5em 0.5em;
|
||||||
|
|
||||||
|
p {
|
||||||
|
padding: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.other-driver-trains {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="driver-top-actions">
|
||||||
|
<div class="actions-container">
|
||||||
|
<div class="actions actions-left">
|
||||||
|
<button class="a-button btn--filled btn--image" @click="routerReturn">
|
||||||
|
<img src="/images/icon-back.svg" alt="train icon" />
|
||||||
|
<span>
|
||||||
|
{{ t('trains.driver-return-link') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions actions-right">
|
||||||
|
<a
|
||||||
|
class="a-button btn--filled btn--image"
|
||||||
|
:href="`https://srjp-td2.web.app/?id=${chosenTrain.id}`"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span class="hidable">
|
||||||
|
{{ t('trains.driver-srjp-link') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<img src="/images/icon-srjp.svg" alt="srjp icon" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<router-link
|
||||||
|
:to="`/profile?playerId=${chosenTrain.driverId}`"
|
||||||
|
class="a-button btn--filled btn--image"
|
||||||
|
>
|
||||||
|
<span class="hidable">
|
||||||
|
{{ t('trains.driver-profile-link') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<img src="/images/icon-user.svg" alt="user icon" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { Train } from '../../typings/common';
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
chosenTrain: {
|
||||||
|
type: Object as PropType<Train>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function routerReturn() {
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
|
.actions-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-container > .actions > .a-button {
|
||||||
|
padding: 0.5em;
|
||||||
|
border-radius: 0.5em 0.5em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include responsive.smallScreen {
|
||||||
|
span.hidable {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
<template>
|
||||||
|
<div class="driver-train-card">
|
||||||
|
<TrainInfo :train="chosenTrain" :extended="true" />
|
||||||
|
|
||||||
|
<!-- Train action buttons -->
|
||||||
|
<div class="train-stock-actions">
|
||||||
|
<button class="btn btn--action" style="margin: 1em 0" @click="copyStockToClipboard()">
|
||||||
|
<i class="fa-regular fa-copy"></i> {{ i18n.t('trains.stock-copy') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn--action" style="margin: 1em 0" @click="toggleNumberPropositions()">
|
||||||
|
<i class="fa-regular fa-lightbulb"></i> {{ i18n.t('trains.number-propositions') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Proposed numbers container -->
|
||||||
|
<transition name="view-anim" class="propositions-container">
|
||||||
|
<div v-if="arePropositionsVisible">
|
||||||
|
<h3 style="margin-bottom: 0.5em">{{ i18n.t('trains.number-propositions-header') }}</h3>
|
||||||
|
|
||||||
|
<div class="categories-select">
|
||||||
|
<button
|
||||||
|
v-for="(category, i) in availableCategories"
|
||||||
|
class="btn btn--option btn--action"
|
||||||
|
@click="selectCategory(i)"
|
||||||
|
:class="{ checked: i == chosenCategoryIndex }"
|
||||||
|
>
|
||||||
|
{{ category }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="numberPropositions.length > 0" class="propositions-numbers">
|
||||||
|
<div v-if="chosenCategory">
|
||||||
|
<b>{{ chosenCategory }} </b> -
|
||||||
|
{{ i18n.t(`categories.${chosenCategory.slice(0, 2)}`) }}
|
||||||
|
({{ i18n.t(`categories.${chosenCategory.slice(2)}`) }})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="chosenCategoryRules">
|
||||||
|
<span v-if="chosenCategoryRules[0]"
|
||||||
|
>{{ i18n.t('trains.number-propositions-third-number') }}
|
||||||
|
<b class="text--primary">{{ chosenCategoryRules[0] }}</b> •
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
>{{
|
||||||
|
i18n.t('trains.number-propositions-last-nums', {
|
||||||
|
count: chosenCategoryRules[1].length
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
<b class="text--primary">{{ chosenCategoryRules[1] }}</b> -
|
||||||
|
<b class="text--primary">{{ chosenCategoryRules[2] }}</b></span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 0.5em">
|
||||||
|
<b>{{ i18n.t('trains.number-propositions-title') }} </b>
|
||||||
|
<i>{{ numberPropositions.join(', ') }}</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="no-propositions" v-else>{{ i18n.t('trains.number-propositions-empty') }}</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<StockList :trainStockList="chosenTrain.stockList" />
|
||||||
|
<TrainSchedule :train="chosenTrain" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { PropType, ref } from 'vue';
|
||||||
|
import { Train } from '../../typings/common';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
|
||||||
|
import StockList from '../Global/StockList.vue';
|
||||||
|
import TrainSchedule from '../TrainsView/TrainSchedule.vue';
|
||||||
|
import TrainInfo from '../TrainsView/TrainInfo.vue';
|
||||||
|
|
||||||
|
import rulesJSON from '../../data/trainNumberRules.json';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
const apiStore = useApiStore();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const arePropositionsVisible = ref(false);
|
||||||
|
const chosenCategoryIndex = ref(0);
|
||||||
|
|
||||||
|
const numberPropositions = ref<string[]>([]);
|
||||||
|
const chosenCategoryRules = ref<any[]>([]);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
chosenTrain: {
|
||||||
|
type: Object as PropType<Train>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function copyStockToClipboard() {
|
||||||
|
const stockString = props.chosenTrain.stockList.join(';');
|
||||||
|
|
||||||
|
if (!stockString) {
|
||||||
|
alert(i18n.t('trains.stock-clipboard-failure'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(stockString)
|
||||||
|
.then(() => {
|
||||||
|
prompt(i18n.t('trains.stock-clipboard-success'), stockString);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert(i18n.t('trains.stock-clipboard-failure'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleNumberPropositions() {
|
||||||
|
arePropositionsVisible.value = !arePropositionsVisible.value;
|
||||||
|
|
||||||
|
if (arePropositionsVisible.value) generateNumberPropositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCategory(i: number) {
|
||||||
|
chosenCategoryIndex.value = i;
|
||||||
|
|
||||||
|
generateNumberPropositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateNumberPropositions() {
|
||||||
|
const categoryCode = chosenCategory.value?.slice(0, 2);
|
||||||
|
const trainNoStr = props.chosenTrain.trainNo.toString();
|
||||||
|
|
||||||
|
// Get category rules
|
||||||
|
const rules = categoryCode
|
||||||
|
? ((rulesJSON.categoriesRules as any)[categoryCode] as any[])
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (!categoryCode || !rules) {
|
||||||
|
numberPropositions.value.length = 0;
|
||||||
|
chosenCategoryRules.value.length = 0;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [thirdNumber, minRange, maxRange] = rules;
|
||||||
|
|
||||||
|
const propositionsArr: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
let generatedNumStr = '';
|
||||||
|
|
||||||
|
generatedNumStr += trainNoStr.at(0) ?? Math.floor(Math.random() * 10);
|
||||||
|
generatedNumStr += trainNoStr.at(1) ?? Math.floor(Math.random() * 10);
|
||||||
|
|
||||||
|
// Third number
|
||||||
|
generatedNumStr += thirdNumber ?? '';
|
||||||
|
|
||||||
|
// Remaining numbers
|
||||||
|
const rangeNums = minRange?.length ?? 3;
|
||||||
|
|
||||||
|
const randRange = Math.floor(
|
||||||
|
Math.random() * (Number(maxRange) - Number(minRange)) + Number(minRange)
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
const leadingZeros = new Array(Math.abs(randRange.toString().length - rangeNums))
|
||||||
|
.fill('0')
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
generatedNumStr += `${leadingZeros}${randRange}`;
|
||||||
|
|
||||||
|
const isNumberTaken =
|
||||||
|
apiStore.activeData?.trains?.some((t) => t.trainNo.toString() == generatedNumStr) ?? false;
|
||||||
|
|
||||||
|
if (!isNumberTaken) {
|
||||||
|
propositionsArr.push(generatedNumStr);
|
||||||
|
} else {
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number(randRange) > Number(maxRange)) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
numberPropositions.value = propositionsArr;
|
||||||
|
chosenCategoryRules.value = rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chosenCategory = computed(() => {
|
||||||
|
return availableCategories.value.at(chosenCategoryIndex.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableCategories = computed(() => {
|
||||||
|
const stockList = props.chosenTrain.stockList;
|
||||||
|
const headVehicle = stockList.at(0)?.split('-')[0] ?? '';
|
||||||
|
|
||||||
|
let availableCategories: string[] = [];
|
||||||
|
let categoryTraction = 'E';
|
||||||
|
|
||||||
|
let vehicleTypesSet = new Set<string>();
|
||||||
|
let wagonsNamesSet = new Set<string>();
|
||||||
|
let cargoNamesSet = new Set<string>();
|
||||||
|
|
||||||
|
for (const stockName of stockList) {
|
||||||
|
const [vehicleName, ...cargoList] = stockName.split(':');
|
||||||
|
|
||||||
|
const vehicleData = apiStore.vehiclesData?.vehicles.find((v) => v.name == vehicleName);
|
||||||
|
|
||||||
|
if (!vehicleData) continue;
|
||||||
|
|
||||||
|
vehicleTypesSet.add(vehicleData.type);
|
||||||
|
|
||||||
|
if (vehicleData.type.startsWith('wagon-')) wagonsNamesSet.add(vehicleData.name.split('_')[0]);
|
||||||
|
|
||||||
|
if (cargoList !== undefined) cargoList.forEach((c) => cargoNamesSet.add(c.split('_')[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let vehicleTypesArr = [...vehicleTypesSet];
|
||||||
|
let wagonsNamesArr = [...wagonsNamesSet];
|
||||||
|
|
||||||
|
// Traction
|
||||||
|
if (vehicleTypesArr[0] == 'loco-electric') categoryTraction = 'E';
|
||||||
|
else if (vehicleTypesArr[0] == 'loco-diesel') categoryTraction = 'S';
|
||||||
|
else if (vehicleTypesArr[0] == 'unit-electric') categoryTraction = 'J';
|
||||||
|
else categoryTraction = 'M';
|
||||||
|
|
||||||
|
// EMU / DMU - M*, R*, P*
|
||||||
|
if (vehicleTypesArr.length == 1 && (categoryTraction == 'J' || categoryTraction == 'M')) {
|
||||||
|
availableCategories.push('MO', 'MP', 'MM', 'RO', 'RP', 'RA', 'RM', 'PW');
|
||||||
|
}
|
||||||
|
// Only locos (up to 3) - LT, LP, LS
|
||||||
|
else if (stockList.length <= 3 && vehicleTypesArr.every((v) => v.startsWith('loco-'))) {
|
||||||
|
if (/^(EU|ET|201E|4E|SU|ST|M62|CTLR4C)/.test(headVehicle)) availableCategories.push('LT');
|
||||||
|
if (/^(EU|EP|SU|SP)/.test(headVehicle)) availableCategories.push('LP');
|
||||||
|
if (/^(SM)/.test(headVehicle)) availableCategories.push('LS');
|
||||||
|
}
|
||||||
|
// Only locos (more than 3) - TH
|
||||||
|
else if (stockList.length > 3 && vehicleTypesArr.every((v) => v.startsWith('loco-'))) {
|
||||||
|
availableCategories.push('TH');
|
||||||
|
}
|
||||||
|
// Loco(s) + passenger only wagons - M*, R*, E*, P*
|
||||||
|
else if (vehicleTypesArr.every((v) => v.startsWith('loco-') || v == 'wagon-passenger')) {
|
||||||
|
availableCategories.push('EI', 'EC', 'EN', 'MO', 'MP', 'MM', 'RO', 'RP', 'RA', 'RM', 'PW');
|
||||||
|
}
|
||||||
|
// Loco(s) + cargo only / mixed wagons - T*, Z*
|
||||||
|
else {
|
||||||
|
if (wagonsNamesArr.every((v) => /^(627Z|412Z)/.test(v)))
|
||||||
|
availableCategories.push('TC', 'TD', 'TS');
|
||||||
|
else if (stockList.slice(1).every((v) => /PKPE/.test(v))) {
|
||||||
|
availableCategories.push('ZU', 'ZN');
|
||||||
|
} else if (wagonsNamesArr.length < 3 || cargoNamesSet.size < 3) {
|
||||||
|
availableCategories.push('TM', 'TG', 'TS', 'TK');
|
||||||
|
} else {
|
||||||
|
availableCategories.push('TN', 'TR', 'TS', 'TK');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableCategories.map((c) => `${c}${categoryTraction}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
computed(() => `${props.chosenTrain.trainNo}`),
|
||||||
|
() => {
|
||||||
|
chosenCategoryIndex.value = 0;
|
||||||
|
generateNumberPropositions();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
|
.driver-train-card {
|
||||||
|
padding: 1em;
|
||||||
|
background-color: var(--clr-view-bg);
|
||||||
|
border-radius: 0 0 0.5em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.train-stock-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propositions-container {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding: 0.5em;
|
||||||
|
background-color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-select {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(-0.5em);
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.propositions-numbers {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-propositions {
|
||||||
|
margin-top: 1em;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include responsive.smallScreen {
|
||||||
|
.propositions-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-select {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button class="action-btn btn--filled">
|
|
||||||
<div class="button_content">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../styles/variables';
|
|
||||||
@import '../../styles/responsive';
|
|
||||||
|
|
||||||
.button_content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -38,5 +38,3 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -81,13 +81,8 @@ export default defineComponent({
|
|||||||
|
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
box-shadow: 0 0 15px 10px #0e0e0e;
|
box-shadow: 0 0 15px 10px #0e0e0e;
|
||||||
|
border-radius: 1em;
|
||||||
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen {
|
|
||||||
.card {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<transition mode="out-in" name="slider-anim" class="current-name">
|
<transition mode="out-in" name="slider-anim" class="current-name">
|
||||||
<span :key="displayingName">
|
<span :key="displayingName">
|
||||||
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
|
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
|
||||||
{{ displayingName }}
|
<span class="text--donator">{{ displayingName }}</span>
|
||||||
</span>
|
</span>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-slot:b2>
|
<template v-slot:b2>
|
||||||
<b>{{ $t('donations.p4-b2') }}</b>
|
<b class="text--donator">{{ $t('donations.p4-b2') }}</b>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
<br />
|
<br />
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions-container">
|
||||||
<a
|
<a
|
||||||
class="action a-button btn--image coffee"
|
class="action a-button btn--image coffee"
|
||||||
href="https://buycoffee.to/spythere"
|
href="https://buycoffee.to/spythere"
|
||||||
@@ -150,8 +150,6 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 1fr auto;
|
grid-template-rows: 1fr auto;
|
||||||
@@ -187,7 +185,7 @@ a.discord {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
@@ -198,32 +196,28 @@ a.discord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions > .action {
|
.actions-container > .action {
|
||||||
&.paypal {
|
&.paypal {
|
||||||
$btnColor: #254069;
|
background-color: #254069;
|
||||||
|
|
||||||
background-color: $btnColor;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: lighten($btnColor, 5%);
|
background-color: #2f5185;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.coffee {
|
&.coffee {
|
||||||
$btnColor: #009255;
|
background-color: #009255;
|
||||||
background-color: $btnColor;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: lighten($btnColor, 5%);
|
background-color: #00a35f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.exit {
|
&.exit {
|
||||||
$btnColor: #686868;
|
background-color: #686868;
|
||||||
background-color: $btnColor;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: lighten($btnColor, 5%);
|
background-color: #8d8d8d;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flag-icon">
|
||||||
|
<img
|
||||||
|
:src="languageFlagSrc"
|
||||||
|
alt="language flag"
|
||||||
|
:style="{
|
||||||
|
width: width
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { getLanguageNameById } from '../../utils/languageUtils';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
languageId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
width: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const languageFlagSrc = computed(
|
||||||
|
() => `/images/flags/${getLanguageNameById(props.languageId)}.svg`
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flag-icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-icon img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -43,7 +43,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
width: 6em;
|
width: 6em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
margin: 0.5em 0;
|
|
||||||
|
|
||||||
.bar-fg,
|
.bar-fg,
|
||||||
.bar-bg {
|
.bar-bg {
|
||||||
|
|||||||
@@ -120,8 +120,6 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/variables.scss';
|
|
||||||
|
|
||||||
.region-dropdown {
|
.region-dropdown {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -162,7 +160,7 @@ ul.options {
|
|||||||
|
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
z-index: 100;
|
z-index: 150;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
@@ -182,7 +180,7 @@ li.option {
|
|||||||
background: none;
|
background: none;
|
||||||
|
|
||||||
&:focus + span {
|
&:focus + span {
|
||||||
color: $accentCol;
|
color: var(--clr-primary);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,12 @@
|
|||||||
@keypress="updateValue"
|
@keypress="updateValue"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<img class="search-exit" src="/images/icon-exit.svg" alt="exit-icon" @click="clearSearchValue" />
|
<img
|
||||||
|
class="search-exit"
|
||||||
|
src="/images/icon-exit.svg"
|
||||||
|
alt="exit-icon"
|
||||||
|
@click="clearSearchValue"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -17,21 +22,10 @@ import { defineComponent, ref, watch } from 'vue';
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: ['update:searchedValue', 'clearValue'],
|
emits: ['update:searchedValue', 'clearValue'],
|
||||||
props: {
|
props: {
|
||||||
searchedValue: {
|
searchedValue: { type: String, required: true },
|
||||||
type: String,
|
updateOnInput: { type: Boolean, default: true },
|
||||||
required: true
|
titleToTranslate: { type: String, required: true },
|
||||||
},
|
clearValue: { type: Function }
|
||||||
updateOnInput: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
titleToTranslate: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
clearValue: {
|
|
||||||
type: Function
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
@@ -56,17 +50,13 @@ export default defineComponent({
|
|||||||
emit('update:searchedValue', compSearchedValue.value);
|
emit('update:searchedValue', compSearchedValue.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return { compSearchedValue, updateValue, clearSearchValue };
|
||||||
compSearchedValue,
|
|
||||||
updateValue,
|
|
||||||
clearSearchValue
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive';
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
&-box {
|
&-box {
|
||||||
@@ -78,7 +68,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
margin: 0.5em 0 0.5em 0.5em;
|
margin: 0.5em 0 0.5em 0.5em;
|
||||||
|
|
||||||
@include smallScreen() {
|
@include responsive.smallScreen{
|
||||||
width: 85%;
|
width: 85%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,9 @@ import { Status } from '../../typings/common';
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
dispatcherStatus: {
|
dispatcherStatus: { type: Number as PropType<Status.ActiveDispatcher | number> },
|
||||||
type: Number as PropType<Status.ActiveDispatcher | number>
|
dispatcherTimestamp: { type: Number as PropType<number | null> },
|
||||||
},
|
isOnline: { type: Boolean }
|
||||||
dispatcherTimestamp: {
|
|
||||||
type: Number as PropType<number | null>
|
|
||||||
},
|
|
||||||
isOnline: {
|
|
||||||
type: Boolean
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mixins: [dateMixin],
|
mixins: [dateMixin],
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="stock-list">
|
<div class="list-wrapper">
|
||||||
<ul>
|
<ul class="stock-list">
|
||||||
<li
|
<li v-for="({ images, imagesFallbacks, vehicleString }, i) in thumbnailNames">
|
||||||
v-for="(
|
<VehicleThumbnail
|
||||||
{ vehicleName, vehicleCargo, images, imagesFallbacks, vehicleString }, i
|
:key="i"
|
||||||
) in thumbnailNames"
|
:vehicle-string="vehicleString"
|
||||||
:key="i"
|
:images="images"
|
||||||
>
|
:image-fallbacks="imagesFallbacks"
|
||||||
<div class="stock-text">
|
/>
|
||||||
<p>{{ vehicleName.replace(/_/g, ' ') }}</p>
|
|
||||||
<small v-if="vehicleCargo">({{ vehicleCargo }})</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
<VehicleThumbnail
|
|
||||||
v-for="(thumbnailImage, imageIndex) in images"
|
|
||||||
:vehicle-name="vehicleString"
|
|
||||||
:img-name="thumbnailImage"
|
|
||||||
:fallback-name="imagesFallbacks[imageIndex]"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,20 +22,12 @@ export default defineComponent({
|
|||||||
components: { VehicleThumbnail },
|
components: { VehicleThumbnail },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
trainStockList: {
|
trainStockList: { type: Array as PropType<string[]>, required: true },
|
||||||
type: Array as PropType<string[]>,
|
tractionOnly: { type: Boolean, required: false }
|
||||||
required: true
|
|
||||||
},
|
|
||||||
tractionOnly: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return { apiStore: useApiStore() };
|
||||||
apiStore: useApiStore()
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
@@ -59,13 +39,12 @@ export default defineComponent({
|
|||||||
return (this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList)
|
return (this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList)
|
||||||
.filter((v) => v.length != 0)
|
.filter((v) => v.length != 0)
|
||||||
.map((vehicleString) => {
|
.map((vehicleString) => {
|
||||||
const [vehicleName, vehicleCargo] = vehicleString.split(':');
|
const [vehicleName] = vehicleString.split(':');
|
||||||
|
|
||||||
const vehicleThumbnailData = {
|
const vehicleThumbnailData = {
|
||||||
images: [] as string[],
|
images: [] as string[],
|
||||||
imagesFallbacks: [] as string[],
|
imagesFallbacks: [] as string[],
|
||||||
vehicleName,
|
vehicleName,
|
||||||
vehicleCargo,
|
|
||||||
vehicleString
|
vehicleString
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -159,50 +138,20 @@ export default defineComponent({
|
|||||||
return vehicleThumbnailData;
|
return vehicleThumbnailData;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onImageError(event: Event, fallbackImage: string) {
|
|
||||||
(event.target as HTMLImageElement).src = `/images/${fallbackImage}.png`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.stock-list {
|
.list-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stock-list ul {
|
.stock-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul > li > span {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
cursor: crosshair;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-height: 60px;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.traction-only {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stock-text {
|
|
||||||
text-align: center;
|
|
||||||
color: #aaa;
|
|
||||||
font-size: 0.9em;
|
|
||||||
margin-bottom: 0.25em;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,37 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="vehicle-thumbnail">
|
<div class="vehicle-thumbnail" :data-load-status="imgStatus" ref="thumbRef">
|
||||||
<img
|
<div class="stock-text">
|
||||||
ref="imgRef"
|
<div>{{ vehicleName }}</div>
|
||||||
:src="`https://static.spythere.eu/thumbnails/v2/${imgName}.png`"
|
<small v-if="vehicleCargo">({{ vehicleCargo }})</small>
|
||||||
height="60"
|
</div>
|
||||||
loading="lazy"
|
|
||||||
data-tooltip-type="VehiclePreviewTooltip"
|
<div class="stock-images">
|
||||||
:data-tooltip-content="vehicleName"
|
<img
|
||||||
:data-load-status="imgStatus"
|
v-for="(thumbnailImage, imageIndex) in images"
|
||||||
@error="onImageError"
|
:src="`https://stacjownik.spythere.eu/static/thumbnails/${thumbnailImage}.png`"
|
||||||
@load="onImageLoad"
|
height="70"
|
||||||
/>
|
loading="lazy"
|
||||||
|
data-tooltip-type="VehiclePreviewTooltip"
|
||||||
|
:data-tooltip-content="vehicleString"
|
||||||
|
@error="onImageError($event, imageFallbacks[imageIndex])"
|
||||||
|
@load="onImageLoad"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, Ref, ref } from 'vue';
|
import { computed, PropType, Ref, ref } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
vehicleName: { type: String, required: true },
|
vehicleString: { type: String, required: true },
|
||||||
imgName: { type: String, required: true },
|
images: { type: Object as PropType<string[]>, required: true },
|
||||||
fallbackName: { type: String, required: true },
|
imageFallbacks: { type: Object as PropType<string[]>, required: true }
|
||||||
placeholderName: String
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const imgRef = ref(null) as Ref<HTMLElement | null>;
|
const thumbRef = ref(null) as Ref<HTMLElement | null>;
|
||||||
|
|
||||||
const imgStatus = ref('loading');
|
const imgStatus = ref('loading');
|
||||||
|
|
||||||
function onImageError(event: Event) {
|
const vehicleName = computed(() => props.vehicleString.split(':')[0].replace(/_/g, ' '));
|
||||||
console.log('error');
|
const vehicleCargo = computed(() => props.vehicleString.split(':')[1]);
|
||||||
|
|
||||||
(event.target as HTMLImageElement).src = `/images/${props.fallbackName}.png`;
|
function onImageError(event: Event, fallbackImage: string) {
|
||||||
|
(event.target as HTMLImageElement).src = `/images/${fallbackImage}.png`;
|
||||||
imgStatus.value = 'error';
|
imgStatus.value = 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,22 +45,37 @@ function onImageLoad() {
|
|||||||
imgStatus.value = 'loaded';
|
imgStatus.value = 'loaded';
|
||||||
}
|
}
|
||||||
|
|
||||||
imgRef.value!.style.opacity = '1';
|
if (thumbRef.value) thumbRef.value.style.opacity = '1';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.vehicle-thumbnail {
|
.vehicle-thumbnail {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 100ms ease-in-out;
|
transition: opacity 100ms ease-in-out;
|
||||||
|
|
||||||
&[data-load-status='loading'] {
|
&[data-load-status='loading'] {
|
||||||
min-height: 60px;
|
min-height: 70px;
|
||||||
min-width: 150px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stock-text {
|
||||||
|
max-width: 90%;
|
||||||
|
text-align: center;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.85em;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-images {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-end;
|
||||||
|
cursor: crosshair;
|
||||||
|
|
||||||
|
padding: 0.5em 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,69 +1,72 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="daily-stats">
|
<section class="daily-stats">
|
||||||
<span :data-active="statsStatus">
|
<span :data-active="apiStore.dataStatuses.dailyStatsData">
|
||||||
<span class="stats-list">
|
<h3>
|
||||||
<h3>
|
{{ $t('journal.daily-stats.title') }}
|
||||||
{{ $t('journal.daily-stats.title') }}
|
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
|
||||||
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
|
</h3>
|
||||||
</h3>
|
|
||||||
|
|
||||||
<hr class="header-separator" />
|
<hr class="header-separator" />
|
||||||
|
|
||||||
<b v-if="statsStatus == Status.Data.Loading">
|
<b v-if="apiStore.dataStatuses.dailyStatsData == Status.Data.Loading">
|
||||||
{{ $t('app.loading') }}
|
{{ $t('app.loading') }}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
<b class="text--error" v-else-if="statsStatus == Status.Data.Error">
|
<b class="text--error" v-else-if="apiStore.dataStatuses.dailyStatsData == Status.Data.Error">
|
||||||
{{ $t('journal.stats-error') }}
|
{{ $t('journal.stats-error') }}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
<b v-else-if="topDispatchers.length == 0">
|
<b v-else-if="topDispatchers.length == 0">
|
||||||
{{ $t('journal.daily-stats.info') }}
|
{{ $t('journal.daily-stats.info') }}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else-if="apiStore.dailyStatsData">
|
||||||
<div v-if="stats.totalTimetables">
|
<ul class="stats-list">
|
||||||
•
|
<li v-if="apiStore.dailyStatsData.totalTimetables">
|
||||||
<i18n-t keypath="journal.daily-stats.total">
|
<i18n-t keypath="journal.daily-stats.total">
|
||||||
<template #count>
|
<template #count>
|
||||||
<b class="text--primary">
|
<b class="text--primary">
|
||||||
{{ stats.totalTimetables }}
|
{{ apiStore.dailyStatsData.totalTimetables }}
|
||||||
{{ $t('journal.daily-stats.count', stats.totalTimetables) }}
|
{{ $t('journal.daily-stats.count', apiStore.dailyStatsData.totalTimetables) }}
|
||||||
</b>
|
</b>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #distance>
|
<template #distance>
|
||||||
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
|
<b class="text--primary">
|
||||||
|
{{ apiStore.dailyStatsData.distanceSum?.toFixed(2) }} km</b
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</li>
|
||||||
|
|
||||||
<div v-if="stats.maxTimetable">
|
<li v-if="apiStore.dailyStatsData.maxTimetable">
|
||||||
•
|
|
||||||
<i18n-t keypath="journal.daily-stats.longest">
|
<i18n-t keypath="journal.daily-stats.longest">
|
||||||
<template #id>
|
<template #id>
|
||||||
<router-link :to="`/journal/timetables?search-train=%23${stats.maxTimetable.id}`">
|
<router-link
|
||||||
<b>{{ stats.maxTimetable.id }}</b>
|
:to="`/journal/timetables?search-train=%23${apiStore.dailyStatsData.maxTimetable.id}`"
|
||||||
|
>
|
||||||
|
<b>{{ apiStore.dailyStatsData.maxTimetable.id }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
<template #author>
|
<template #author>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/journal/timetables?search-dispatcher=${stats.maxTimetable.authorName}`"
|
:to="`/journal/timetables?search-dispatcher=${apiStore.dailyStatsData.maxTimetable.authorName}`"
|
||||||
>
|
>
|
||||||
<b>{{ stats.maxTimetable.authorName }}</b>
|
<b>{{ apiStore.dailyStatsData.maxTimetable.authorName }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
<template #driver>
|
<template #driver>
|
||||||
<b class="text--primary">{{ stats.maxTimetable.driverName }}</b>
|
<b class="text--primary">{{ apiStore.dailyStatsData.maxTimetable.driverName }}</b>
|
||||||
</template>
|
</template>
|
||||||
<template #distance>
|
<template #distance>
|
||||||
<b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
|
<b class="text--primary"
|
||||||
|
>{{ apiStore.dailyStatsData.maxTimetable.routeDistance }} km</b
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</li>
|
||||||
|
|
||||||
<div v-if="topDispatchers.length == 1">
|
<li v-if="topDispatchers.length == 1">
|
||||||
•
|
|
||||||
<i18n-t keypath="journal.daily-stats.most-active-dr">
|
<i18n-t keypath="journal.daily-stats.most-active-dr">
|
||||||
<template #dispatcher>
|
<template #dispatcher>
|
||||||
<router-link
|
<router-link
|
||||||
@@ -79,10 +82,9 @@
|
|||||||
</b>
|
</b>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</li>
|
||||||
|
|
||||||
<div v-if="topDispatchers.length > 1">
|
<li v-if="topDispatchers.length > 1">
|
||||||
•
|
|
||||||
<i18n-t keypath="journal.daily-stats.most-active-dr-many">
|
<i18n-t keypath="journal.daily-stats.most-active-dr-many">
|
||||||
<template #dispatchers>
|
<template #dispatchers>
|
||||||
<span v-for="(disp, i) in topDispatchers" :key="i">
|
<span v-for="(disp, i) in topDispatchers" :key="i">
|
||||||
@@ -103,167 +105,128 @@
|
|||||||
</b>
|
</b>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</li>
|
||||||
|
|
||||||
<div v-if="stats.longestDuties.length > 0">
|
<li v-if="apiStore.dailyStatsData.longestDuties.length > 0">
|
||||||
•
|
|
||||||
<i18n-t keypath="journal.daily-stats.longest-duties">
|
<i18n-t keypath="journal.daily-stats.longest-duties">
|
||||||
<template #dispatcher>
|
<template #dispatcher>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/journal/dispatchers?search-dispatcher=${stats.longestDuties[0].name}`"
|
:to="`/journal/dispatchers?search-dispatcher=${apiStore.dailyStatsData.longestDuties[0].name}`"
|
||||||
>
|
>
|
||||||
<b>{{ stats.longestDuties[0].name }}</b>
|
<b>{{ apiStore.dailyStatsData.longestDuties[0].name }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #station>{{ stats.longestDuties[0].station }}</template>
|
<template #station>{{ apiStore.dailyStatsData.longestDuties[0].station }}</template>
|
||||||
|
|
||||||
<template #duration>
|
<template #duration>
|
||||||
{{ calculateDuration(stats.longestDuties[0].duration) }}
|
{{ humanizeDuration(apiStore.dailyStatsData.longestDuties[0].duration) }}
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</li>
|
||||||
|
|
||||||
<div v-if="stats.mostActiveDrivers.length > 0">
|
<li v-if="apiStore.dailyStatsData.mostActiveDrivers.length > 0">
|
||||||
•
|
|
||||||
<i18n-t keypath="journal.daily-stats.most-active-driver">
|
<i18n-t keypath="journal.daily-stats.most-active-driver">
|
||||||
<template #driver>
|
<template #driver>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/journal/timetables?search-driver=${stats.mostActiveDrivers[0].name}`"
|
:to="`/journal/timetables?search-driver=${apiStore.dailyStatsData.mostActiveDrivers[0].name}`"
|
||||||
>
|
>
|
||||||
<b>{{ stats.mostActiveDrivers[0].name }}</b>
|
<b>{{ apiStore.dailyStatsData.mostActiveDrivers[0].name }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
<template #distance>
|
<template #distance>
|
||||||
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
|
<b class="text--primary"
|
||||||
|
>{{ apiStore.dailyStatsData.mostActiveDrivers[0].distance.toFixed(2) }} km</b
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<hr class="section-separator" />
|
<hr class="section-separator" />
|
||||||
|
|
||||||
<div class="stats-badges">
|
<div class="stats-badges">
|
||||||
<span
|
<span
|
||||||
class="stat-badge"
|
class="badge stat-badge"
|
||||||
v-for="key in [
|
v-for="key in [
|
||||||
'rippedSwitches',
|
'rippedSwitches',
|
||||||
'derailments',
|
'derailments',
|
||||||
'skippedStopSignals',
|
'skippedStopSignals',
|
||||||
'radioStops',
|
'radioStops',
|
||||||
'kills'
|
'kills'
|
||||||
]"
|
]"
|
||||||
:key="key"
|
:key="key"
|
||||||
>
|
>
|
||||||
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
|
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
|
||||||
<span>{{
|
<span>
|
||||||
Object.entries(stats.globalDiff).find(([k, v]) => k == key)?.[1] || '--'
|
{{
|
||||||
}}</span>
|
Object.entries(apiStore.dailyStatsData.globalDiff).find(
|
||||||
|
([k, v]) => k == key
|
||||||
|
)?.[1] || '--'
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { computed, onMounted } from 'vue';
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
|
||||||
|
|
||||||
import { API } from '../../typings/api';
|
|
||||||
import { Status } from '../../typings/common';
|
|
||||||
import { useApiStore } from '../../store/apiStore';
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import { Status } from '../../typings/common';
|
||||||
|
import { humanizeDuration } from '../../composables/time';
|
||||||
|
|
||||||
export default defineComponent({
|
onMounted(() => {
|
||||||
name: 'journal-daily-stats',
|
apiStore.fetchDailyStats();
|
||||||
|
});
|
||||||
|
|
||||||
mixins: [dateMixin],
|
const apiStore = useApiStore();
|
||||||
// emits: ['toggleStatsOpen'],
|
|
||||||
|
|
||||||
data() {
|
const topDispatchers = computed(() => {
|
||||||
return {
|
if (!apiStore.dailyStatsData || apiStore.dailyStatsData.mostActiveDispatchers.length == 0)
|
||||||
Status,
|
return [];
|
||||||
statsStatus: Status.Data.Loading,
|
|
||||||
intervalId: -1,
|
|
||||||
|
|
||||||
stats: {} as API.DailyStats.Response,
|
const maxCount = apiStore.dailyStatsData.mostActiveDispatchers[0].count;
|
||||||
apiStore: useApiStore()
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
activated() {
|
return apiStore.dailyStatsData.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
@use '../../styles/animations';
|
||||||
@import '../../styles/JournalStats.scss';
|
@use '../../styles/journal-stats';
|
||||||
@import '../../styles/badge.scss';
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
.daily-stats {
|
.daily-stats {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.daily-stats > span[data-active='0'] {
|
.daily-stats > span[data-active='0'] {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.stats-list {
|
||||||
|
list-style: disc;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.stats-list a {
|
.stats-list a {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stats-list > li {
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
.stats-badges {
|
.stats-badges {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen {
|
@include responsive.smallScreen {
|
||||||
h3 {
|
h3 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<li class="dispatcher-history-entry">
|
<li class="dispatcher-history-entry">
|
||||||
<div class="entry-info">
|
<div class="entry-info">
|
||||||
<span>
|
<span class="entry-info-left">
|
||||||
<span>
|
<div class="station-info">
|
||||||
<router-link :to="`/journal/dispatchers?search-station=${entry.stationName}`">
|
<router-link :to="`/journal/dispatchers?search-station=${entry.stationName}`">
|
||||||
<b>{{ entry.stationName }}</b>
|
<b>{{ entry.stationName }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<b class="text--grayed"> #{{ entry.stationHash }}</b>
|
<b class="text--grayed"> #{{ entry.stationHash }}</b>
|
||||||
</span>
|
•
|
||||||
•
|
<b
|
||||||
<b
|
v-if="entry.dispatcherLevel !== null"
|
||||||
v-if="entry.dispatcherLevel !== null"
|
class="level-badge dispatcher"
|
||||||
class="level-badge dispatcher"
|
:style="calculateExpStyle(entry.dispatcherLevel, entry.dispatcherIsSupporter)"
|
||||||
:style="calculateExpStyle(entry.dispatcherLevel, entry.dispatcherIsSupporter)"
|
>
|
||||||
>
|
{{ entry.dispatcherLevel >= 2 ? entry.dispatcherLevel : 'L' }}
|
||||||
{{ entry.dispatcherLevel >= 2 ? entry.dispatcherLevel : 'L' }}
|
</b>
|
||||||
</b>
|
|
||||||
<b style="margin-left: 5px">
|
|
||||||
<span
|
<span
|
||||||
v-if="apiStore.donatorsData.includes(entry.dispatcherName)"
|
v-if="apiStore.donatorsData.includes(entry.dispatcherName)"
|
||||||
data-tooltip-type="DonatorTooltip"
|
data-tooltip-type="DonatorTooltip"
|
||||||
@@ -37,7 +36,11 @@
|
|||||||
>
|
>
|
||||||
{{ entry.dispatcherName }}
|
{{ entry.dispatcherName }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</b>
|
|
||||||
|
<span class="dispatcher-language" v-if="entry.dispatcherLanguageId != null">
|
||||||
|
<FlagIcon :language-id="entry.dispatcherLanguageId" width="1.75em" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span v-if="entry.timestampTo">
|
<span v-if="entry.timestampTo">
|
||||||
@@ -118,28 +121,20 @@ import dateMixin from '../../../mixins/dateMixin';
|
|||||||
import styleMixin from '../../../mixins/styleMixin';
|
import styleMixin from '../../../mixins/styleMixin';
|
||||||
import { useApiStore } from '../../../store/apiStore';
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
||||||
|
import FlagIcon from '../../Global/FlagIcon.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
entry: {
|
entry: { type: Object as PropType<API.DispatcherHistory.Data>, required: true },
|
||||||
type: Object as PropType<API.DispatcherHistory.Data>,
|
showExtraInfo: { type: Boolean, required: true }
|
||||||
required: true
|
|
||||||
},
|
|
||||||
showExtraInfo: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
components: { StationStatusBadge },
|
components: { StationStatusBadge, FlagIcon },
|
||||||
mixins: [dateMixin, styleMixin],
|
mixins: [dateMixin, styleMixin],
|
||||||
emits: ['toggleShowExtraInfo'],
|
emits: ['toggleShowExtraInfo'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return { regions, apiStore: useApiStore() };
|
||||||
regions,
|
|
||||||
apiStore: useApiStore()
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -151,8 +146,8 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../../styles/responsive.scss';
|
@use '../../../styles/responsive';
|
||||||
@import '../../../styles/badge.scss';
|
@use '../../../styles/badge';
|
||||||
|
|
||||||
.region-badge {
|
.region-badge {
|
||||||
padding: 0 0.25em;
|
padding: 0 0.25em;
|
||||||
@@ -173,6 +168,11 @@ export default defineComponent({
|
|||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dispatcher-language {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.entry-info {
|
.entry-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -194,6 +194,15 @@ export default defineComponent({
|
|||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.station-info {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.status-list {
|
.status-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -207,11 +216,15 @@ export default defineComponent({
|
|||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen {
|
@include responsive.smallScreen {
|
||||||
.entry-info {
|
.entry-info {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.station-info {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,48 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="status-anim" mode="out-in">
|
<div>
|
||||||
<div :key="dataStatus">
|
<div class="journal_warning" v-if="store.isOffline">
|
||||||
<div class="journal_warning" v-if="store.isOffline">
|
{{ $t('app.offline') }}
|
||||||
{{ $t('app.offline') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<ul v-else class="journal-list">
|
|
||||||
<transition-group name="list-anim">
|
|
||||||
<JournalDispatcherEntry
|
|
||||||
v-for="entry in dispatcherHistory"
|
|
||||||
:key="entry.id"
|
|
||||||
:entry="entry"
|
|
||||||
:onToggleShowExtraInfo="toggleExtraInfo"
|
|
||||||
:showExtraInfo="extraInfoIndexes.includes(entry.id)"
|
|
||||||
/>
|
|
||||||
</transition-group>
|
|
||||||
|
|
||||||
<AddDataButton
|
|
||||||
:list="dispatcherHistory"
|
|
||||||
:scrollDataLoaded="scrollDataLoaded"
|
|
||||||
:scrollNoMoreData="scrollNoMoreData"
|
|
||||||
@addHistoryData="addHistoryData"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="journal_warning" v-if="scrollNoMoreData">
|
|
||||||
{{ $t('journal.no-further-data') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="journal_warning" v-else-if="!scrollDataLoaded">
|
|
||||||
{{ $t('journal.loading-further-data') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
|
||||||
|
<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>
|
||||||
|
<transition-group name="list-anim" class="journal-list" tag="ul">
|
||||||
|
<JournalDispatcherEntry
|
||||||
|
v-for="entry in dispatcherHistory"
|
||||||
|
:key="entry.id"
|
||||||
|
:entry="entry"
|
||||||
|
:onToggleShowExtraInfo="toggleExtraInfo"
|
||||||
|
:showExtraInfo="extraInfoIndexes.includes(entry.id)"
|
||||||
|
/>
|
||||||
|
</transition-group>
|
||||||
|
|
||||||
|
<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>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -85,6 +83,15 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'$route.query': {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
this.extraInfoIndexes.length = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
toggleExtraInfo(id: number) {
|
toggleExtraInfo(id: number) {
|
||||||
const existingIdx = this.extraInfoIndexes.indexOf(id);
|
const existingIdx = this.extraInfoIndexes.indexOf(id);
|
||||||
@@ -97,13 +104,5 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../../styles/variables.scss';
|
@use '../../../styles/journal-section';
|
||||||
@import '../../../styles/JournalSection.scss';
|
|
||||||
|
|
||||||
.journal-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5em;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="journal-header">
|
<section class="journal-header">
|
||||||
<div class="journal-type-options">
|
<div class="journal-type-options">
|
||||||
<router-link class="router-link" active-class="route-active" to="/journal/timetables" exact>
|
<router-link
|
||||||
|
class="route-link"
|
||||||
|
active-class="route-link-active"
|
||||||
|
to="/journal/timetables"
|
||||||
|
exact
|
||||||
|
>
|
||||||
{{ $t('journal.section-timetables') }}
|
{{ $t('journal.section-timetables') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
•
|
•
|
||||||
<router-link class="router-link" active-class="route-active" to="/journal/dispatchers">
|
<router-link class="route-link" active-class="route-link-active" to="/journal/dispatchers">
|
||||||
{{ $t('journal.section-dispatchers') }}
|
{{ $t('journal.section-dispatchers') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,8 +44,4 @@ export default defineComponent({});
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-link.active {
|
|
||||||
color: gold;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="filters-options dropdown" @keydown.esc="showOptions = false">
|
<div class="dropdown filters-options" @keydown.esc="showOptions = false">
|
||||||
<div class="dropdown_background" v-if="showOptions" @click="showOptions = false"></div>
|
<div class="dropdown_background" v-if="showOptions" @click="showOptions = false"></div>
|
||||||
|
|
||||||
<div class="actions-bar">
|
<div class="actions-bar">
|
||||||
@@ -33,31 +33,56 @@
|
|||||||
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
|
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
|
||||||
<div class="search_content">
|
<div class="search_content">
|
||||||
<div class="search" v-for="(_, propName) in searchersValues" :key="propName">
|
<div class="search" v-for="(_, propName) in searchersValues" :key="propName">
|
||||||
<label v-if="propName == 'search-date'" for="search-date">{{
|
<!-- Train category select -->
|
||||||
$t(`options.search-${optionsType}-date`)
|
<div v-if="propName.toString() == 'select-categoryCode'">
|
||||||
}}</label>
|
<label for="journalCategoryCode">{{ $t(`options.${propName}`) }}</label>
|
||||||
|
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<input
|
<select
|
||||||
class="search-input"
|
class="search-input"
|
||||||
v-model="searchersValues[propName]"
|
name="journalCategoryCode"
|
||||||
@keydown.enter="searchConfirm"
|
id="journalCategoryCode"
|
||||||
@focus="preventKeyDown = true"
|
v-model="searchersValues[propName]"
|
||||||
@blur="preventKeyDown = false"
|
>
|
||||||
:placeholder="$t(`options.${propName}`)"
|
<option value="">...</option>
|
||||||
:type="propName == 'search-date' ? 'date' : 'text'"
|
<option v-for="categoryName in allCategories" :value="categoryName">
|
||||||
:min="propName == 'search-date' ? '2022-02-01' : undefined"
|
{{ categoryName }} - {{ getCategoryExplanation(categoryName) }}
|
||||||
:id="`${propName}`"
|
</option>
|
||||||
:list="propName.toString()"
|
</select>
|
||||||
/>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="search-exit" v-if="propName != 'search-date'">
|
<!-- Other inputs -->
|
||||||
<img
|
<div v-else>
|
||||||
src="/images/icon-exit.svg"
|
<label v-if="propName == 'search-date-from'" for="search-date">{{
|
||||||
alt="exit-icon"
|
$t(`options.search-${optionsType}-date`)
|
||||||
@click="onInputClear(propName)"
|
}}</label>
|
||||||
|
|
||||||
|
<div class="search-box">
|
||||||
|
<input
|
||||||
|
class="search-input"
|
||||||
|
v-model="searchersValues[propName]"
|
||||||
|
@keydown.enter="searchConfirm"
|
||||||
|
@focus="preventKeyDown = true"
|
||||||
|
@blur="preventKeyDown = false"
|
||||||
|
:placeholder="$t(`options.${propName}`)"
|
||||||
|
:type="propName.toString().startsWith('search-date') ? 'date' : 'text'"
|
||||||
|
:min="propName.toString().startsWith('search-date') ? '2022-02-01' : undefined"
|
||||||
|
:id="`${propName}`"
|
||||||
|
:list="propName.toString()"
|
||||||
/>
|
/>
|
||||||
</button>
|
|
||||||
|
<button
|
||||||
|
class="btn btn--action search-exit"
|
||||||
|
v-if="!propName.toString().startsWith('search-date')"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/icon-exit.svg"
|
||||||
|
alt="exit-icon"
|
||||||
|
@click="onInputClear(propName)"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,10 +142,12 @@ import { useMainStore } from '../../store/mainStore';
|
|||||||
import { Journal } from './typings';
|
import { Journal } from './typings';
|
||||||
import { Status } from '../../typings/common';
|
import { Status } from '../../typings/common';
|
||||||
import { useApiStore } from '../../store/apiStore';
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import { allCategories } from '../../data/trainNumberRules.json';
|
||||||
|
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'],
|
emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'],
|
||||||
mixins: [keyMixin],
|
mixins: [keyMixin, trainCategoryMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
sorterOptionIds: {
|
sorterOptionIds: {
|
||||||
@@ -152,6 +179,7 @@ export default defineComponent({
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showOptions: false,
|
showOptions: false,
|
||||||
|
allCategories,
|
||||||
|
|
||||||
driverSuggestions: [] as string[],
|
driverSuggestions: [] as string[],
|
||||||
dispatcherSuggestions: [] as string[],
|
dispatcherSuggestions: [] as string[],
|
||||||
@@ -188,14 +216,14 @@ export default defineComponent({
|
|||||||
if (!value || value == '') return;
|
if (!value || value == '') return;
|
||||||
if (value.length < 3) return;
|
if (value.length < 3) return;
|
||||||
|
|
||||||
this.startSearchTimeout('driver', value);
|
if (this.showOptions) this.startSearchTimeout('driver', value);
|
||||||
},
|
},
|
||||||
|
|
||||||
async 'searchersValues.search-dispatcher'(value: string | undefined) {
|
async 'searchersValues.search-dispatcher'(value: string | undefined) {
|
||||||
if (!value || value == '') return;
|
if (!value || value == '') return;
|
||||||
if (value.length < 3) return;
|
if (value.length < 3) return;
|
||||||
|
|
||||||
this.startSearchTimeout('dispatcher', value);
|
if (this.showOptions) this.startSearchTimeout('dispatcher', value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -283,7 +311,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
searchConfirm() {
|
searchConfirm() {
|
||||||
this.$emit('onSearchConfirm');
|
|
||||||
this.handleRouteParams();
|
this.handleRouteParams();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -301,6 +328,11 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/dropdown';
|
@use '../../styles/dropdown';
|
||||||
@import '../../styles/dropdown_filters';
|
@use '../../styles/dropdown-filters';
|
||||||
|
|
||||||
|
.filters-options > .dropdown_wrapper {
|
||||||
|
height: calc(100vh - 19em);
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,84 +2,72 @@
|
|||||||
<div
|
<div
|
||||||
class="journal-stats dropdown"
|
class="journal-stats dropdown"
|
||||||
v-if="!mainStore.isOffline"
|
v-if="!mainStore.isOffline"
|
||||||
@keydown.esc="currentStatsTab = null"
|
@keydown.esc="isDropdownOpen = false"
|
||||||
>
|
>
|
||||||
<div
|
<div class="dropdown_background" v-if="isDropdownOpen" @click="isDropdownOpen = false"></div>
|
||||||
class="dropdown_background"
|
|
||||||
v-if="currentStatsTab !== null"
|
|
||||||
@click="currentStatsTab = null"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div class="actions-bar">
|
<div class="actions-bar">
|
||||||
|
<button class="btn--filled btn--image" @click="toggleDropdown">
|
||||||
|
<img :src="`/images/icon-stats.svg`" alt="stats icon" />
|
||||||
|
{{ $t('journal.daily-stats.button') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-for="button in statsButtons"
|
|
||||||
:key="button.tab"
|
|
||||||
class="btn--filled btn--image"
|
class="btn--filled btn--image"
|
||||||
:data-selected="button.tab == currentStatsTab"
|
:data-disabled="chosenPlayerId == -1"
|
||||||
:data-disabled="button.disabled"
|
@click="navigateToProfile"
|
||||||
:disabled="button.disabled"
|
|
||||||
@click="onTabButtonClick(button.tab)"
|
|
||||||
>
|
>
|
||||||
<img
|
<img :src="`/images/icon-user.svg`" alt="user icon" />
|
||||||
v-if="button.iconName"
|
{{ $t('profile.journal-button') }}
|
||||||
:src="`/images/icon-${button.iconName}.svg`"
|
|
||||||
:alt="button.iconName"
|
|
||||||
/>
|
|
||||||
{{ $t(button.localeKey) }}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition name="dropdown-anim">
|
<transition name="dropdown-anim">
|
||||||
<div class="dropdown_wrapper" v-if="currentStatsTab !== null">
|
<div class="dropdown_wrapper" v-if="isDropdownOpen">
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<component :is="currentStatsTab" :key="currentStatsTab"></component>
|
<JournalDailyStats />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, PropType } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useMainStore } from '../../store/mainStore';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
import StorageManager from '../../managers/storageManager';
|
|
||||||
import { Journal } from './typings';
|
|
||||||
import JournalDailyStats from './JournalDailyStats.vue';
|
import JournalDailyStats from './JournalDailyStats.vue';
|
||||||
import JournalDispatcherStats from '../JournalView/JournalDispatchers/JournalDispatcherStats.vue';
|
import { useRouter } from 'vue-router';
|
||||||
import JournalDriverStats from '../JournalView/JournalTimetables/JournalDriverStats.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
const router = useRouter();
|
||||||
components: { JournalDailyStats, JournalDriverStats, JournalDispatcherStats },
|
|
||||||
props: {
|
|
||||||
statsButtons: {
|
|
||||||
type: Array as PropType<Journal.StatsButton[]>,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
Journal,
|
|
||||||
mainStore: useMainStore(),
|
|
||||||
currentStatsTab: null as Journal.StatsTab | null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
const props = defineProps({
|
||||||
onTabButtonClick(tab: Journal.StatsTab) {
|
chosenPlayerId: {
|
||||||
this.currentStatsTab = tab == this.currentStatsTab ? null : tab;
|
type: Number,
|
||||||
|
required: true
|
||||||
StorageManager.setStringValue('journalStatsTab', this.currentStatsTab ?? '');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mainStore = useMainStore();
|
||||||
|
const isDropdownOpen = ref(false);
|
||||||
|
|
||||||
|
function toggleDropdown() {
|
||||||
|
isDropdownOpen.value = !isDropdownOpen.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToProfile() {
|
||||||
|
if (props.chosenPlayerId == -1) return;
|
||||||
|
|
||||||
|
router.push(`/profile?playerId=${props.chosenPlayerId}`);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/dropdown.scss';
|
@use '../../styles/dropdown';
|
||||||
@import '../../styles/dropdown_filters.scss';
|
@use '../../styles/dropdown-filters';
|
||||||
@import '../../styles/variables.scss';
|
|
||||||
|
|
||||||
.dropdown_wrapper {
|
.dropdown_wrapper {
|
||||||
max-width: 100%;
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
max-width: 700px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,340 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="details-actions">
|
||||||
|
<button class="btn--action" @click="toggleExtraInfo">
|
||||||
|
<b>{{ $t('journal.entry-details') }}</b>
|
||||||
|
<img :src="`/images/icon-arrow-${showExtraInfo ? 'asc' : 'desc'}.svg`" alt="Arrow icon" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<router-link
|
||||||
|
v-if="driverRouteLocation !== null"
|
||||||
|
class="a-button btn--action btn-timetable"
|
||||||
|
:to="driverRouteLocation"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-train.svg" alt="train icon" />
|
||||||
|
<b>{{ $t('journal.timetable-online-button') }}</b>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details-body" v-if="showExtraInfo">
|
||||||
|
<div class="g-separator"></div>
|
||||||
|
|
||||||
|
<div v-if="timetableDetails">
|
||||||
|
<EntryStops :timetable="timetableDetails" />
|
||||||
|
|
||||||
|
<div class="g-separator"></div>
|
||||||
|
|
||||||
|
<div class="timetable-specs">
|
||||||
|
<span class="badge specs-badge" v-if="timetableDetails.authorName">
|
||||||
|
<span>{{ $t('journal.dispatcher-name') }}</span>
|
||||||
|
<span>{{ timetableDetails.authorName }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="badge specs-badge" v-if="timetableDetails.trainMaxSpeed">
|
||||||
|
<span>{{ $t('journal.stock-timetable-speed') }}</span>
|
||||||
|
<span> {{ timetableDetails.trainMaxSpeed }}km/h </span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="badge specs-badge" v-if="timetableDetails.maxSpeed">
|
||||||
|
<span>{{ $t('journal.stock-max-speed') }}</span>
|
||||||
|
<span>{{ timetableDetails.maxSpeed }}km/h</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stock-dangers" v-if="timetableDetails.warningNotes">
|
||||||
|
<div class="g-separator"></div>
|
||||||
|
|
||||||
|
<b>{{ $t('journal.stock-dangers') }}:</b>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li v-if="timetableDetails.twr">
|
||||||
|
<b class="text--primary">{{ $t('warnings.TWR') }} (TWR)</b>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li v-if="timetableDetails.skr">
|
||||||
|
<b class="text--primary">{{ $t('warnings.SKR') }}</b>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li v-if="timetableDetails.hasDangerousCargo">
|
||||||
|
<b class="text--primary">{{ $t('warnings.TN') }}</b>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li v-if="timetableDetails.hasExtraDeliveries">
|
||||||
|
<b class="text--primary">{{ $t('warnings.PN') }}</b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="dangers-notes" v-if="timetableDetails.warningNotes">
|
||||||
|
<h4>{{ $t('warnings.header-title') }}</h4>
|
||||||
|
<p>
|
||||||
|
<i>{{ timetableDetails.warningNotes }}</i>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Historia zmian w składzie -->
|
||||||
|
<div v-if="timetableDetails.stockString || stockHistory.length != 0">
|
||||||
|
<div class="g-separator"></div>
|
||||||
|
|
||||||
|
<b>{{ $t('journal.stock-preview') }}:</b>
|
||||||
|
|
||||||
|
<div class="stock-specs" style="margin-top: 0.5em">
|
||||||
|
<span class="badge specs-badge" v-if="timetableDetails.stockLength">
|
||||||
|
<span>{{ $t('journal.stock-length') }}</span>
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
currentHistoryIndex == 0
|
||||||
|
? timetableDetails.stockLength
|
||||||
|
: stockHistory[currentHistoryIndex].stockLength || timetableDetails.stockLength
|
||||||
|
}}m
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="badge specs-badge" v-if="timetableDetails.stockMass">
|
||||||
|
<span>{{ $t('journal.stock-mass') }}</span>
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
Math.floor(
|
||||||
|
(currentHistoryIndex == 0
|
||||||
|
? timetableDetails.stockMass
|
||||||
|
: stockHistory[currentHistoryIndex].stockMass || timetableDetails.stockMass) /
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
}}t
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stock-history">
|
||||||
|
<button class="btn btn--action" @click="copyStockToClipboard()">
|
||||||
|
<i class="fa-regular fa-copy"></i> {{ $t('journal.stock-copy') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-for="(sh, i) in stockHistory"
|
||||||
|
:key="i"
|
||||||
|
class="btn--action"
|
||||||
|
:data-checked="i == currentHistoryIndex"
|
||||||
|
@click.stop="currentHistoryIndex = i"
|
||||||
|
>
|
||||||
|
{{ sh.updatedAt }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="timetableDetails.stockString" style="margin-top: 1em">
|
||||||
|
<StockList
|
||||||
|
:trainStockList="
|
||||||
|
(currentHistoryIndex == 0
|
||||||
|
? timetableDetails.stockString
|
||||||
|
: stockHistory[currentHistoryIndex].stockString
|
||||||
|
).split(';')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, PropType, ref } from 'vue';
|
||||||
|
import { RouteLocationRaw } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import StockList from '../../Global/StockList.vue';
|
||||||
|
import EntryStops from './EntryStops.vue';
|
||||||
|
import { API } from '../../../typings/api';
|
||||||
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const apiStore = useApiStore();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
showExtraInfo: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
timetableEntry: {
|
||||||
|
type: Object as PropType<API.TimetableHistory.DataShort>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['toggleExtraInfo']);
|
||||||
|
const currentHistoryIndex = ref(0);
|
||||||
|
|
||||||
|
const timetableDetails = ref<API.TimetableHistory.Data | null>(null);
|
||||||
|
|
||||||
|
const stockHistory = computed(() => {
|
||||||
|
return (
|
||||||
|
timetableDetails.value?.stockHistory
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((h) => {
|
||||||
|
const historyData = h.split('@');
|
||||||
|
return {
|
||||||
|
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(i18n.locale.value, {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}),
|
||||||
|
stockString: historyData[1],
|
||||||
|
stockMass: Number(historyData[2]) || undefined,
|
||||||
|
stockLength: Number(historyData[3]) || undefined
|
||||||
|
};
|
||||||
|
}) ?? []
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const driverRouteLocation = computed<RouteLocationRaw | null>(() => {
|
||||||
|
if (props.timetableEntry.terminated) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'DriverView',
|
||||||
|
query: {
|
||||||
|
trainId: `${props.timetableEntry.driverId}|${props.timetableEntry.trainNo}|eu`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchTimetableDetails() {
|
||||||
|
try {
|
||||||
|
const responseData = await apiStore.client!.get<API.TimetableHistory.Response>(
|
||||||
|
'api/getTimetables',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
timetableId: props.timetableEntry.id,
|
||||||
|
returnType: 'detailed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!responseData || responseData.data.length != 1) {
|
||||||
|
timetableDetails.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timetableDetails.value = responseData.data[0];
|
||||||
|
} catch (error) {
|
||||||
|
// this.dataStatus = Status.Data.Error;
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleExtraInfo() {
|
||||||
|
if (props.showExtraInfo == false) {
|
||||||
|
await fetchTimetableDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
emits('toggleExtraInfo', timetableDetails.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyStockToClipboard() {
|
||||||
|
if (!timetableDetails.value) return;
|
||||||
|
|
||||||
|
const currentStockString =
|
||||||
|
stockHistory.value[currentHistoryIndex.value]?.stockString ??
|
||||||
|
timetableDetails.value.stockString;
|
||||||
|
|
||||||
|
if (!currentStockString) {
|
||||||
|
alert(i18n.t('journal.stock-clipboard-failure'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(currentStockString)
|
||||||
|
.then(() => {
|
||||||
|
prompt(i18n.t('journal.stock-clipboard-success'), currentStockString);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert(i18n.t('journal.stock-clipboard-failure'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use '../../../styles/responsive';
|
||||||
|
@use '../../../styles/badge';
|
||||||
|
|
||||||
|
.details-body {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin-top: 1em;
|
||||||
|
|
||||||
|
button img {
|
||||||
|
height: 1.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-history {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin-top: 1em;
|
||||||
|
|
||||||
|
button[data-checked='true'] {
|
||||||
|
color: var(--clr-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-specs,
|
||||||
|
.stock-specs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.specs-badge {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
span:first-child {
|
||||||
|
color: white;
|
||||||
|
background-color: #666;
|
||||||
|
border-radius: 0.25em 0 0 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:last-child {
|
||||||
|
color: black;
|
||||||
|
background-color: var(--clr-primary);
|
||||||
|
border-radius: 0 0.25em 0.25em 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-dangers ul {
|
||||||
|
list-style: disc;
|
||||||
|
padding-left: 1em;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dangers-notes {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
max-height: 200px;
|
||||||
|
max-width: 500px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include responsive.smallScreen {
|
||||||
|
.timetable-specs {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,9 +3,40 @@
|
|||||||
<span class="general-train">
|
<span class="general-train">
|
||||||
<span class="text--grayed">#{{ timetable.id }}</span>
|
<span class="text--grayed">#{{ timetable.id }}</span>
|
||||||
|
|
||||||
<span class="badges" v-if="timetable.skr || timetable.twr">
|
<span
|
||||||
<span class="train-badge twr" v-if="timetable.twr" :title="$t('general.TWR')">TWR</span>
|
class="train-badge twr"
|
||||||
<span class="train-badge skr" v-if="timetable.skr" :title="$t('general.SKR')">SKR</span>
|
v-if="timetable.twr"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="$t('warnings.TWR')"
|
||||||
|
>
|
||||||
|
TWR
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="train-badge skr"
|
||||||
|
v-if="timetable.skr"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="$t('warnings.SKR')"
|
||||||
|
>
|
||||||
|
SKR
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="train-badge tn"
|
||||||
|
v-if="timetable.hasDangerousCargo"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="$t('warnings.TN')"
|
||||||
|
>
|
||||||
|
TN
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="train-badge pn"
|
||||||
|
v-if="timetable.hasExtraDeliveries"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="$t('warnings.PN')"
|
||||||
|
>
|
||||||
|
PN
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
@@ -27,18 +58,23 @@
|
|||||||
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
|
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
|
||||||
</strong>
|
</strong>
|
||||||
|
|
||||||
<strong
|
<router-link
|
||||||
v-if="apiStore.donatorsData.includes(timetable.driverName)"
|
v-if="apiStore.donatorsData.includes(timetable.driverName)"
|
||||||
class="text--donator"
|
class="text--donator"
|
||||||
data-tooltip-type="DonatorTooltip"
|
data-tooltip-type="DonatorTooltip"
|
||||||
:data-tooltip-content="$t('donations.driver-message')"
|
:data-tooltip-content="$t('donations.driver-message')"
|
||||||
|
:to="`/journal/timetables?search-driver=${timetable.driverName}`"
|
||||||
>
|
>
|
||||||
{{ timetable.driverName }}
|
<strong>{{ timetable.driverName }}</strong>
|
||||||
</strong>
|
</router-link>
|
||||||
|
|
||||||
<strong v-else>
|
<router-link v-else :to="`/journal/timetables?search-driver=${timetable.driverName}`">
|
||||||
{{ timetable.driverName }}
|
<strong>{{ timetable.driverName }}</strong>
|
||||||
</strong>
|
</router-link>
|
||||||
|
|
||||||
|
<div v-if="timetable.driverLanguageId != null">
|
||||||
|
<FlagIcon :language-id="timetable.driverLanguageId" width="1.75em" />
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="general-time">
|
<span class="general-time">
|
||||||
@@ -51,7 +87,7 @@
|
|||||||
</b>
|
</b>
|
||||||
|
|
||||||
<b
|
<b
|
||||||
class="info-badge"
|
class="timetable-status-badge"
|
||||||
:class="{
|
:class="{
|
||||||
fulfilled: timetable.fulfilled,
|
fulfilled: timetable.fulfilled,
|
||||||
terminated: timetable.terminated && !timetable.fulfilled,
|
terminated: timetable.terminated && !timetable.fulfilled,
|
||||||
@@ -66,15 +102,6 @@
|
|||||||
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
|
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
|
||||||
}}
|
}}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="timetable.terminated == false"
|
|
||||||
class="btn--action btn-timetable"
|
|
||||||
@click.stop="showTimetable(timetable, $event.currentTarget)"
|
|
||||||
>
|
|
||||||
<img src="/images/icon-train.svg" alt="train icon" />
|
|
||||||
<b>{{ $t('journal.timetable-online-button') }}</b>
|
|
||||||
</button>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -84,13 +111,14 @@ import { PropType, defineComponent } from 'vue';
|
|||||||
|
|
||||||
import { API } from '../../../typings/api';
|
import { API } from '../../../typings/api';
|
||||||
import dateMixin from '../../../mixins/dateMixin';
|
import dateMixin from '../../../mixins/dateMixin';
|
||||||
import modalTrainMixin from '../../../mixins/modalTrainMixin';
|
|
||||||
import styleMixin from '../../../mixins/styleMixin';
|
import styleMixin from '../../../mixins/styleMixin';
|
||||||
import { useApiStore } from '../../../store/apiStore';
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
|
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
|
||||||
|
import FlagIcon from '../../Global/FlagIcon.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [dateMixin, modalTrainMixin, styleMixin, trainCategoryMixin],
|
components: { FlagIcon },
|
||||||
|
mixins: [dateMixin, styleMixin, trainCategoryMixin],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -100,24 +128,16 @@ export default defineComponent({
|
|||||||
|
|
||||||
props: {
|
props: {
|
||||||
timetable: {
|
timetable: {
|
||||||
type: Object as PropType<API.TimetableHistory.Data>,
|
type: Object as PropType<API.TimetableHistory.DataShort>,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) {
|
|
||||||
if (timetable?.terminated) return;
|
|
||||||
|
|
||||||
this.selectModalTrainById(`${timetable.driverName}${timetable.trainNo}`, target);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../../styles/responsive';
|
@use '../../../styles/responsive';
|
||||||
@import '../../../styles/badge';
|
@use '../../../styles/badge';
|
||||||
|
|
||||||
.item-general {
|
.item-general {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -137,7 +157,6 @@ export default defineComponent({
|
|||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
line-height: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.general-time {
|
.general-time {
|
||||||
@@ -152,23 +171,6 @@ export default defineComponent({
|
|||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-badge {
|
|
||||||
padding: 0.05em 0.35em;
|
|
||||||
color: black;
|
|
||||||
|
|
||||||
&.terminated {
|
|
||||||
background-color: salmon;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.fulfilled {
|
|
||||||
background-color: lightgreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: lightblue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-timetable {
|
.btn-timetable {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0.2em 0.5em;
|
padding: 0.2em 0.5em;
|
||||||
@@ -178,8 +180,9 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen {
|
@include responsive.smallScreen {
|
||||||
.item-general {
|
.item-general {
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="item-status" style="margin: 0.5em 0">
|
<div class="entry-status" style="margin: 0.5em 0">
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
:progressPercent="~~((timetable.currentDistance / timetable.routeDistance) * 100)"
|
:progressPercent="~~((timetable.currentDistance / timetable.routeDistance) * 100)"
|
||||||
:progressType="!timetable.fulfilled && timetable.terminated ? 'abandoned' : ''"
|
:progressType="!timetable.fulfilled && timetable.terminated ? 'abandoned' : ''"
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="text--grayed" v-if="timetable.currentSceneryName">
|
<span class="entry-location" v-if="timetable.currentSceneryName">
|
||||||
<b>
|
<b>
|
||||||
{{ $t(`journal.${timetable.terminated ? 'last-seen-at' : 'currently-at'}`) }}
|
{{ $t(`journal.${timetable.terminated ? 'last-seen-at' : 'currently-at'}`) }}
|
||||||
{{ timetable.currentSceneryName.replace(/.[a-zA-Z0-9]+.sc/, '') }}
|
{{ timetable.currentSceneryName.replace(/.[a-zA-Z0-9]+.sc/, '') }}
|
||||||
@@ -51,7 +51,7 @@ export default defineComponent({
|
|||||||
components: { ProgressBar },
|
components: { ProgressBar },
|
||||||
props: {
|
props: {
|
||||||
timetable: {
|
timetable: {
|
||||||
type: Object as PropType<API.TimetableHistory.Data>,
|
type: Object as PropType<API.TimetableHistory.DataShort>,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,16 +59,21 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../../styles/responsive.scss';
|
@use '../../../styles/responsive';
|
||||||
|
|
||||||
.item-status {
|
.entry-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
|
|
||||||
@include smallScreen() {
|
@include responsive.smallScreen{
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entry-location {
|
||||||
|
text-align: center;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
<template>
|
||||||
|
<div class="entry-stops">
|
||||||
|
<ul class="stop-list">
|
||||||
|
<li v-for="(stop, i) in timetableStops" :key="stop.stopName">
|
||||||
|
<span class="stop-label" :data-confirmed="stop.isConfirmed">
|
||||||
|
<span v-if="i > 0">></span>
|
||||||
|
|
||||||
|
<span class="stop-name">{{ stop.stopName }}</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="stop-date"
|
||||||
|
v-if="stop.scheduledArrivalTimestamp != 0"
|
||||||
|
:data-delayed="
|
||||||
|
stop.isConfirmed && stop.arrivalTimestamp - stop.scheduledArrivalTimestamp > 0
|
||||||
|
"
|
||||||
|
:data-preponed="
|
||||||
|
stop.isConfirmed &&
|
||||||
|
stop.arrivalTimestamp != 0 &&
|
||||||
|
stop.arrivalTimestamp - stop.scheduledArrivalTimestamp < 0
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="stop.isConfirmed && stop.arrivalTimestamp - stop.scheduledArrivalTimestamp != 0"
|
||||||
|
>
|
||||||
|
p. <s>{{ timestampToString(stop.scheduledArrivalTimestamp) }}</s>
|
||||||
|
{{ timestampToString(stop.arrivalTimestamp) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else>p. {{ timestampToString(stop.scheduledArrivalTimestamp) }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="stop-time"
|
||||||
|
v-if="stop.stopTime > 0"
|
||||||
|
:data-stop-ph="stop.stopType.includes('ph')"
|
||||||
|
:data-stop-pt="stop.stopType.includes('pt')"
|
||||||
|
:data-stop-pm="stop.stopType.includes('pm')"
|
||||||
|
>
|
||||||
|
/<span>{{ stop.stopTime }} {{ stop.stopType }}</span
|
||||||
|
>/
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="stop-date"
|
||||||
|
v-if="
|
||||||
|
stop.scheduledDepartureTimestamp != 0 &&
|
||||||
|
stop.scheduledArrivalTimestamp != stop.scheduledDepartureTimestamp
|
||||||
|
"
|
||||||
|
:data-delayed="
|
||||||
|
stop.isConfirmed && stop.departureTimestamp - stop.scheduledDepartureTimestamp > 0
|
||||||
|
"
|
||||||
|
:data-preponed="
|
||||||
|
stop.isConfirmed &&
|
||||||
|
stop.departureTimestamp != 0 &&
|
||||||
|
stop.departureTimestamp - stop.scheduledDepartureTimestamp < 0
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
stop.isConfirmed && stop.departureTimestamp - stop.scheduledDepartureTimestamp != 0
|
||||||
|
"
|
||||||
|
>
|
||||||
|
o. <s>{{ timestampToString(stop.scheduledDepartureTimestamp) }}</s>
|
||||||
|
{{ timestampToString(stop.departureTimestamp) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else>o. {{ timestampToString(stop.scheduledDepartureTimestamp) }}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="timetable-path-list" v-if="timetablePathDetails">
|
||||||
|
<li
|
||||||
|
v-for="(pathData, i) in timetablePathDetails"
|
||||||
|
:data-visited="pathData.isVisited"
|
||||||
|
:data-next-visited="
|
||||||
|
i < timetablePathDetails.length - 1 && timetablePathDetails[i + 1].isVisited
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span v-if="i > 0" class="path-arrow">></span>
|
||||||
|
<span class="path-arrival" v-if="pathData.arrival">{{ pathData.arrival }}</span>
|
||||||
|
<b class="path-scenery">{{ pathData.sceneryName }}</b>
|
||||||
|
<span class="path-departure" v-if="pathData.departure">{{ pathData.departure }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { PropType, defineComponent } from 'vue';
|
||||||
|
import dateMixin from '../../../mixins/dateMixin';
|
||||||
|
import { API } from '../../../typings/api';
|
||||||
|
|
||||||
|
interface ITimetableStopDetails {
|
||||||
|
stopName: string;
|
||||||
|
stopComments: string | null;
|
||||||
|
stopTime: number;
|
||||||
|
stopType: string;
|
||||||
|
arrivalTimestamp: number;
|
||||||
|
scheduledArrivalTimestamp: number;
|
||||||
|
departureTimestamp: number;
|
||||||
|
scheduledDepartureTimestamp: number;
|
||||||
|
isConfirmed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
mixins: [dateMixin],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
timetable: {
|
||||||
|
type: Object as PropType<API.TimetableHistory.Data>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
timetablePathDetails() {
|
||||||
|
if (!this.timetable.path || this.timetable.path == '') return null;
|
||||||
|
|
||||||
|
return this.timetable.path.split(';').map((pathEl, i) => {
|
||||||
|
const [arrival, name, departure] = pathEl.split(',');
|
||||||
|
const sceneryName = name.split(' ').slice(0, -1).join(' ');
|
||||||
|
const sceneryHash = name.split(' ').pop()?.replace('.sc', '') ?? '';
|
||||||
|
const isVisited = this.timetable.visitedSceneries.includes(sceneryHash);
|
||||||
|
|
||||||
|
return {
|
||||||
|
arrival,
|
||||||
|
sceneryName,
|
||||||
|
sceneryHash,
|
||||||
|
departure,
|
||||||
|
isVisited,
|
||||||
|
isVisitedOffline:
|
||||||
|
!isVisited &&
|
||||||
|
this.timetable.visitedSceneries.includes(`${sceneryName} ${sceneryHash}.sc`)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
timetableStops(): ITimetableStopDetails[] {
|
||||||
|
const timetable = this.timetable;
|
||||||
|
|
||||||
|
const stopNames = timetable.sceneriesString.split('%');
|
||||||
|
|
||||||
|
return stopNames.reduce<ITimetableStopDetails[]>((acc, stopName, i, arr) => {
|
||||||
|
const arrivalDate =
|
||||||
|
i == arr.length - 1
|
||||||
|
? (timetable.checkpointArrivals.at(i) ?? timetable.endDate)
|
||||||
|
: timetable.checkpointArrivals.at(i);
|
||||||
|
|
||||||
|
const scheduledArrivalDate =
|
||||||
|
i == arr.length - 1
|
||||||
|
? (timetable.checkpointArrivalsScheduled.at(i) ?? timetable.scheduledEndDate)
|
||||||
|
: timetable.checkpointArrivalsScheduled.at(i);
|
||||||
|
|
||||||
|
const departureDate =
|
||||||
|
i == 0
|
||||||
|
? (timetable.checkpointDepartures.at(i) ?? timetable.beginDate)
|
||||||
|
: timetable.checkpointDepartures.at(i);
|
||||||
|
|
||||||
|
const scheduledDepartureDate =
|
||||||
|
i == 0
|
||||||
|
? (timetable.checkpointDeparturesScheduled.at(i) ?? timetable.scheduledBeginDate)
|
||||||
|
: timetable.checkpointDeparturesScheduled.at(i);
|
||||||
|
|
||||||
|
const stopTime = Number(timetable.checkpointStopTypes.at(i)?.split(',')[0]) || 0;
|
||||||
|
const stopType = timetable.checkpointStopTypes.at(i)?.split(',').slice(1).join(',') || 'pt';
|
||||||
|
const stopComments = timetable.checkpointComments.at(i) ?? null;
|
||||||
|
|
||||||
|
acc.push({
|
||||||
|
stopName,
|
||||||
|
stopTime,
|
||||||
|
stopType,
|
||||||
|
stopComments,
|
||||||
|
arrivalTimestamp: this.dateStringToTimestamp(arrivalDate),
|
||||||
|
scheduledArrivalTimestamp: this.dateStringToTimestamp(scheduledArrivalDate),
|
||||||
|
departureTimestamp: this.dateStringToTimestamp(departureDate),
|
||||||
|
scheduledDepartureTimestamp: this.dateStringToTimestamp(scheduledDepartureDate),
|
||||||
|
isConfirmed: i < timetable.confirmedStopsCount
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use '../../../styles/badge';
|
||||||
|
|
||||||
|
.entry-stops {
|
||||||
|
word-wrap: break-word;
|
||||||
|
gap: 0.25em;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
padding: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-label {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: center;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&[data-confirmed='true'] > .stop-name {
|
||||||
|
color: lightgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-confirmed='true'] > .stop-date:not([data-preponed='true']):not([data-delayed='true']) {
|
||||||
|
color: lightgreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-name {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ccc;
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-date {
|
||||||
|
color: #ccc;
|
||||||
|
|
||||||
|
s {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-delayed='true'] {
|
||||||
|
color: salmon;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-preponed='true'] {
|
||||||
|
color: mediumspringgreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-time {
|
||||||
|
&[data-stop-pt='true'] span {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-stop-ph='true'] span,
|
||||||
|
&[data-stop-pm='true'] span {
|
||||||
|
color: gold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-path-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em 0;
|
||||||
|
padding: 0.5em 0;
|
||||||
|
color: #ccc;
|
||||||
|
|
||||||
|
li > .path-scenery:first-child,
|
||||||
|
li > .path-arrival:nth-child(2) {
|
||||||
|
border-radius: 0.5em 0 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
li > :last-child {
|
||||||
|
border-radius: 0 0.5em 0.5em 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-scenery {
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
background-color: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-arrival,
|
||||||
|
.path-departure {
|
||||||
|
padding: 0.25em;
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #4e4e4e;
|
||||||
|
min-width: 25px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-arrow {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-path-list > li[data-visited='true'] {
|
||||||
|
.path-arrival,
|
||||||
|
.path-scenery,
|
||||||
|
.path-arrow {
|
||||||
|
color: lightgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-next-visited='true'] .path-departure {
|
||||||
|
color: lightgreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="journal-stats driver" v-if="store.driverStatsData">
|
|
||||||
<span>
|
|
||||||
<h3>
|
|
||||||
<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.driver-stats.timetables') }}</span>
|
|
||||||
<span
|
|
||||||
>{{ store.driverStatsData._count.fulfilled }} /
|
|
||||||
{{ store.driverStatsData._count._all }}</span
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="stat-badge">
|
|
||||||
<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.driver-stats.avg-timetable') }}</span>
|
|
||||||
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>{{ $t('journal.driver-stats.distance') }}</span>
|
|
||||||
<span>
|
|
||||||
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
|
|
||||||
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>{{ $t('journal.driver-stats.stations') }}</span>
|
|
||||||
<span>
|
|
||||||
{{ store.driverStatsData._sum.confirmedStopsCount }} /
|
|
||||||
{{ store.driverStatsData._sum.allStopsCount }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import { useMainStore } from '../../../store/mainStore';
|
|
||||||
import { Status } from '../../../typings/common';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'journal-driver-stats',
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
store: useMainStore(),
|
|
||||||
Status: Status
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../../../styles/JournalStats.scss';
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<li class="timetable-history-entry">
|
||||||
|
<!-- General -->
|
||||||
|
<EntryGeneral :timetable="timetableEntry" />
|
||||||
|
|
||||||
|
<!-- Route -->
|
||||||
|
<div class="entry-route">
|
||||||
|
<b>{{ timetableEntry.route.replace('|', ' - ') }}</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div style="cursor: pointer">
|
||||||
|
<!-- Status -->
|
||||||
|
<EntryStatus :timetable="timetableEntry" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Extra -->
|
||||||
|
<EntryDetails
|
||||||
|
:timetableEntry="timetableEntry"
|
||||||
|
:show-extra-info="showExtraInfo"
|
||||||
|
@toggle-extra-info="toggleExtraInfo"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
import { API } from '../../../typings/api';
|
||||||
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
|
|
||||||
|
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
|
||||||
|
import dateMixin from '../../../mixins/dateMixin';
|
||||||
|
import styleMixin from '../../../mixins/styleMixin';
|
||||||
|
|
||||||
|
import EntryGeneral from './EntryGeneral.vue';
|
||||||
|
import EntryStatus from './EntryStatus.vue';
|
||||||
|
import EntryDetails from './EntryDetails.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
timetableEntry: {
|
||||||
|
type: Object as PropType<API.TimetableHistory.DataShort>,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
showExtraInfo: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
components: { EntryDetails, EntryGeneral, EntryStatus },
|
||||||
|
mixins: [trainCategoryMixin, dateMixin, styleMixin],
|
||||||
|
emits: ['toggleShowExtraInfo'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
apiStore: useApiStore()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggleExtraInfo(data: API.TimetableHistory.Data | null) {
|
||||||
|
this.$emit('toggleShowExtraInfo', data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use '../../../styles/responsive';
|
||||||
|
|
||||||
|
.timetable-history-entry {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-route {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include responsive.smallScreen {
|
||||||
|
.entry-route {
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,62 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<transition name="status-anim" mode="out-in">
|
<div class="journal_warning" v-if="store.isOffline">
|
||||||
<div :key="dataStatus">
|
{{ $t('app.offline') }}
|
||||||
<div class="journal_warning" v-if="store.isOffline">
|
</div>
|
||||||
{{ $t('app.offline') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Loading v-else-if="dataStatus == Status.Data.Loading" />
|
<Loading v-else-if="dataStatus == Status.Data.Loading" />
|
||||||
|
|
||||||
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
|
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
|
||||||
{{ $t('app.error') }}
|
{{ $t('app.error') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="timetableHistory.length == 0" class="journal_warning">
|
<div v-else-if="timetableHistory.length == 0" class="journal_warning">
|
||||||
{{ $t('app.no-result') }}
|
{{ $t('app.no-result') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<ul class="journal-list">
|
<transition-group name="list-anim" class="journal-list" tag="ul">
|
||||||
<transition-group name="list-anim">
|
<JournalTimetableEntry
|
||||||
<li
|
v-for="(timetableEntry, i) in timetableHistory"
|
||||||
v-for="{ timetable, showExtraInfo } in computedTimetableHistory"
|
:key="timetableEntry.id"
|
||||||
class="journal_item"
|
:timetableEntry="timetableEntry"
|
||||||
:key="timetable.id"
|
:onToggleShowExtraInfo="toggleExtraInfo"
|
||||||
@click="showExtraInfo.value = !showExtraInfo.value"
|
:showExtraInfo="extraInfoIndexes.includes(timetableEntry.id)"
|
||||||
>
|
/>
|
||||||
<div class="journal_item-info">
|
</transition-group>
|
||||||
<!-- General -->
|
|
||||||
<TimetableGeneral :timetable="timetable" />
|
|
||||||
<!-- Route -->
|
|
||||||
<span class="item-route">
|
|
||||||
<b>{{ timetable.route.replace('|', ' - ') }}</b>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<hr />
|
<AddDataButton
|
||||||
<!-- Stops -->
|
:list="timetableHistory"
|
||||||
<TimetableStops :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
|
:scrollDataLoaded="scrollDataLoaded"
|
||||||
<!-- Status -->
|
:scrollNoMoreData="scrollNoMoreData"
|
||||||
<TimetableStatus :timetable="timetable" />
|
@addHistoryData="addHistoryData"
|
||||||
|
/>
|
||||||
<!-- Extra -->
|
</div>
|
||||||
<TimetableDetails :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</transition-group>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<AddDataButton
|
|
||||||
:list="timetableHistory"
|
|
||||||
: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-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
|
||||||
|
|
||||||
<div class="journal_warning" v-else-if="!scrollDataLoaded">
|
<div class="journal_warning" v-else-if="!scrollDataLoaded">
|
||||||
{{ $t('journal.loading-further-data') }}
|
{{ $t('journal.loading-further-data') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -64,33 +42,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType, ref } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
|
||||||
import Loading from '../../Global/Loading.vue';
|
import Loading from '../../Global/Loading.vue';
|
||||||
import AddDataButton from '../../Global/AddDataButton.vue';
|
import AddDataButton from '../../Global/AddDataButton.vue';
|
||||||
|
import JournalTimetableEntry from './JournalTimetableEntry.vue';
|
||||||
|
|
||||||
import { useMainStore } from '../../../store/mainStore';
|
import { useMainStore } from '../../../store/mainStore';
|
||||||
import { Status } from '../../../typings/common';
|
import { Status } from '../../../typings/common';
|
||||||
import { API } from '../../../typings/api';
|
import { API } from '../../../typings/api';
|
||||||
|
|
||||||
import TimetableGeneral from './TimetableGeneral.vue';
|
|
||||||
import TimetableStops from './TimetableStops.vue';
|
|
||||||
import TimetableStatus from './TimetableStatus.vue';
|
|
||||||
import TimetableDetails from './TimetableDetails.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
Loading,
|
Loading,
|
||||||
AddDataButton,
|
AddDataButton,
|
||||||
TimetableDetails,
|
JournalTimetableEntry
|
||||||
TimetableGeneral,
|
|
||||||
TimetableStatus,
|
|
||||||
TimetableStops
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ['toggleExtraInfo'],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
timetableHistory: {
|
timetableHistory: {
|
||||||
type: Array as PropType<API.TimetableHistory.Response>,
|
type: Array as PropType<API.TimetableHistory.ResponseShort>,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
scrollNoMoreData: {
|
scrollNoMoreData: {
|
||||||
@@ -104,6 +77,10 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
dataStatus: {
|
dataStatus: {
|
||||||
type: Number as PropType<Status.Data>
|
type: Number as PropType<Status.Data>
|
||||||
|
},
|
||||||
|
extraInfoIndexes: {
|
||||||
|
type: Object as PropType<number[]>,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -114,22 +91,20 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
methods: {
|
||||||
computedTimetableHistory() {
|
toggleExtraInfo(data: API.TimetableHistory.Data | null) {
|
||||||
return this.timetableHistory.map((timetable) => ({
|
this.$emit('toggleExtraInfo', data);
|
||||||
timetable,
|
|
||||||
showExtraInfo: ref(false)
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../../styles/JournalSection.scss';
|
@use '../../../styles/animations';
|
||||||
@import '../../../styles/animations.scss';
|
@use '../../../styles/journal-section';
|
||||||
|
@use '../../../styles/responsive';
|
||||||
|
|
||||||
@include smallScreen {
|
@include responsive.smallScreen {
|
||||||
.journal_item-info {
|
.journal_item-info {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="details-actions">
|
|
||||||
<button class="btn--action">
|
|
||||||
<b>{{ $t('journal.stock-info') }}</b>
|
|
||||||
<img :src="`/images/icon-arrow-${showExtraInfo ? 'asc' : 'desc'}.svg`" alt="Arrow icon" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="details-body" v-if="timetable.stockString && timetable.stockMass && showExtraInfo">
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<div class="stock-specs">
|
|
||||||
<span class="badge">
|
|
||||||
<span>{{ $t('journal.dispatcher-name') }}</span>
|
|
||||||
<span>{{ timetable.authorName }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stock-specs">
|
|
||||||
<span class="badge">
|
|
||||||
<span>{{ $t('journal.stock-max-speed') }}</span>
|
|
||||||
<span>{{ timetable.maxSpeed }}km/h</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="badge">
|
|
||||||
<span>{{ $t('journal.stock-length') }}</span>
|
|
||||||
<span>
|
|
||||||
{{
|
|
||||||
currentHistoryIndex == 0
|
|
||||||
? timetable.stockLength
|
|
||||||
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength
|
|
||||||
}}m
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="badge">
|
|
||||||
<span>{{ $t('journal.stock-mass') }}</span>
|
|
||||||
<span>
|
|
||||||
{{
|
|
||||||
Math.floor(
|
|
||||||
(currentHistoryIndex == 0
|
|
||||||
? timetable.stockMass!
|
|
||||||
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
|
|
||||||
)
|
|
||||||
}}t
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Historia zmian w składzie -->
|
|
||||||
<div class="stock-history" v-if="stockHistory.length > 1">
|
|
||||||
<button
|
|
||||||
v-for="(sh, i) in stockHistory"
|
|
||||||
:key="i"
|
|
||||||
class="btn--action"
|
|
||||||
:data-checked="i == currentHistoryIndex"
|
|
||||||
@click.stop="currentHistoryIndex = i"
|
|
||||||
>
|
|
||||||
{{ sh.updatedAt }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<StockList
|
|
||||||
:trainStockList="
|
|
||||||
(currentHistoryIndex == 0
|
|
||||||
? timetable.stockString
|
|
||||||
: stockHistory[currentHistoryIndex].stockString
|
|
||||||
).split(';')
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { PropType, defineComponent } from 'vue';
|
|
||||||
import StockList from '../../Global/StockList.vue';
|
|
||||||
import { API } from '../../../typings/api';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { StockList },
|
|
||||||
props: {
|
|
||||||
showExtraInfo: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
timetable: {
|
|
||||||
type: Object as PropType<API.TimetableHistory.Data>,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
currentHistoryIndex: 0
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
stockHistory() {
|
|
||||||
return this.timetable.stockHistory
|
|
||||||
.slice()
|
|
||||||
.reverse()
|
|
||||||
.map((h) => {
|
|
||||||
const historyData = h.split('@');
|
|
||||||
return {
|
|
||||||
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
}),
|
|
||||||
stockString: historyData[1],
|
|
||||||
stockMass: Number(historyData[2]) || undefined,
|
|
||||||
stockLength: Number(historyData[3]) || undefined
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onImageError(e: Event) {
|
|
||||||
const imageEl = e.target as HTMLImageElement;
|
|
||||||
imageEl.src = '/images/icon-unknown.png';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../../../styles/variables.scss';
|
|
||||||
@import '../../../styles/responsive.scss';
|
|
||||||
@import '../../../styles/badge.scss';
|
|
||||||
|
|
||||||
.details-body {
|
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-actions {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
button img {
|
|
||||||
height: 1.25em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stock-history {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5em;
|
|
||||||
margin-top: 1em;
|
|
||||||
|
|
||||||
button[data-checked='true'] {
|
|
||||||
color: $accentCol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stock-specs {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5em;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
span:last-child {
|
|
||||||
color: black;
|
|
||||||
background-color: $accentCol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.stock-list {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
padding-bottom: 0.5em;
|
|
||||||
|
|
||||||
li > div {
|
|
||||||
margin: 1em 0;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
color: #aaa;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include smallScreen() {
|
|
||||||
.stock-specs {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-actions {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="timetable-stops">
|
|
||||||
<div class="stop-list">
|
|
||||||
<span
|
|
||||||
v-for="(stop, i) in timetableStops.filter((_, i) =>
|
|
||||||
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
|
|
||||||
)"
|
|
||||||
class="stop-list-item"
|
|
||||||
:key="stop.stopName"
|
|
||||||
:data-confirmed="stop.confirmed"
|
|
||||||
>
|
|
||||||
<span v-if="i > 0">
|
|
||||||
>
|
|
||||||
<span v-if="!showExtraInfo && i == 1 && timetableStops.length > 2">
|
|
||||||
... (+{{ timetableStops.length - 2 }}) >
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="stop-name">{{ stop.stopName }}</span>
|
|
||||||
<span v-html="stop.html"></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="path-details" v-if="showExtraInfo && timetablePathDetails">
|
|
||||||
<span
|
|
||||||
v-for="(pathData, i) in timetablePathDetails"
|
|
||||||
:data-visited="pathData.isVisited"
|
|
||||||
:data-next-visited="
|
|
||||||
i < timetablePathDetails.length - 1 && timetablePathDetails[i + 1].isVisited
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<span class="path-arrival" v-if="pathData.arrival">/ {{ pathData.arrival }} → </span>
|
|
||||||
<b class="path-scenery">{{ pathData.sceneryName }}</b>
|
|
||||||
<span class="path-departure" v-if="pathData.departure">
|
|
||||||
→ {{ pathData.departure }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { PropType, defineComponent } from 'vue';
|
|
||||||
import dateMixin from '../../../mixins/dateMixin';
|
|
||||||
import { API } from '../../../typings/api';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
mixins: [dateMixin],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
showExtraInfo: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
|
|
||||||
timetable: {
|
|
||||||
type: Object as PropType<API.TimetableHistory.Data>,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
timetablePathDetails() {
|
|
||||||
if (!this.timetable.path || this.timetable.path == '') return null;
|
|
||||||
|
|
||||||
return this.timetable.path.split(';').map((pathEl, i) => {
|
|
||||||
const [arrival, name, departure] = pathEl.split(',');
|
|
||||||
const sceneryName = name.split(' ').slice(0, -1).join(' ');
|
|
||||||
const sceneryHash = name.split(' ').pop()?.replace('.sc', '') ?? '';
|
|
||||||
|
|
||||||
return {
|
|
||||||
arrival,
|
|
||||||
sceneryName,
|
|
||||||
sceneryHash,
|
|
||||||
departure,
|
|
||||||
isVisited: this.timetable.visitedSceneries?.includes(sceneryHash) ?? false
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
timetableStops() {
|
|
||||||
const timetable = this.timetable;
|
|
||||||
|
|
||||||
const stopNames = timetable.sceneriesString.split('%');
|
|
||||||
|
|
||||||
const beginDateHTML = ` (o. ${
|
|
||||||
timetable.beginDate != timetable.scheduledBeginDate
|
|
||||||
? `<s class="text--grayed">${this.localeTime(timetable.beginDate, this.$i18n.locale)}</s>`
|
|
||||||
: ''
|
|
||||||
} <span>${this.localeTime(timetable.scheduledBeginDate, this.$i18n.locale)}</span>)`;
|
|
||||||
|
|
||||||
const endDateHTML = ` (p. ${
|
|
||||||
timetable.endDate != timetable.scheduledEndDate && timetable.fulfilled
|
|
||||||
? `<s class="text--grayed">${this.localeTime(timetable.endDate, this.$i18n.locale)}</s>`
|
|
||||||
: ''
|
|
||||||
} <span>${this.localeTime(timetable.scheduledEndDate, this.$i18n.locale)}</span>)`;
|
|
||||||
|
|
||||||
return stopNames.map((stopName, i) => {
|
|
||||||
const confirmed = i < timetable.confirmedStopsCount;
|
|
||||||
if (i == 0) return { stopName, html: beginDateHTML, confirmed };
|
|
||||||
if (i == stopNames.length - 1) return { stopName, html: endDateHTML, confirmed };
|
|
||||||
|
|
||||||
const departureDateScheduled = this.stringToDate(
|
|
||||||
timetable.checkpointDeparturesScheduled?.at(i)
|
|
||||||
);
|
|
||||||
const departureDateReal = this.stringToDate(timetable.checkpointDepartures?.at(i));
|
|
||||||
const arrivalDateScheduled = this.stringToDate(
|
|
||||||
timetable.checkpointArrivalsScheduled?.at(i)
|
|
||||||
);
|
|
||||||
const arrivalDateReal = this.stringToDate(timetable.checkpointArrivals?.at(i));
|
|
||||||
const arrivalHTML =
|
|
||||||
(arrivalDateReal &&
|
|
||||||
arrivalDateScheduled &&
|
|
||||||
arrivalDateReal?.getTime() != arrivalDateScheduled?.getTime()
|
|
||||||
? `<s class="text--grayed">${this.parseDateToTimeString(arrivalDateScheduled)}</s> `
|
|
||||||
: '') + this.parseDateToTimeString(arrivalDateReal || arrivalDateScheduled);
|
|
||||||
const departureHTML =
|
|
||||||
(departureDateReal &&
|
|
||||||
departureDateScheduled &&
|
|
||||||
departureDateReal?.getTime() != departureDateScheduled?.getTime()
|
|
||||||
? `<s class="text--grayed">${this.parseDateToTimeString(departureDateScheduled)}</s> `
|
|
||||||
: '') + this.parseDateToTimeString(departureDateReal || departureDateScheduled);
|
|
||||||
let html = `${arrivalHTML}${departureHTML ? ` / ${departureHTML}` : ''}`;
|
|
||||||
if (html) html = ` (${html})`;
|
|
||||||
return { stopName, html, confirmed };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.timetable-stops {
|
|
||||||
word-wrap: break-word;
|
|
||||||
gap: 0.25em;
|
|
||||||
font-size: 0.95em;
|
|
||||||
color: #adadad;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stop-list {
|
|
||||||
&-item[data-confirmed='true'] {
|
|
||||||
color: lightgreen;
|
|
||||||
|
|
||||||
.stop-name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.path-details {
|
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.path-details > span[data-visited='true'] {
|
|
||||||
.path-arrival,
|
|
||||||
.path-scenery {
|
|
||||||
color: lightgreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-next-visited='true'] .path-departure {
|
|
||||||
color: lightgreen;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,14 +1,21 @@
|
|||||||
export namespace Journal {
|
export namespace Journal {
|
||||||
export type DispatcherSearchKey = 'search-dispatcher' | 'search-station' | 'search-date';
|
export type DispatcherSearchKey =
|
||||||
|
| 'search-duty-id'
|
||||||
|
| 'search-dispatcher'
|
||||||
|
| 'search-station'
|
||||||
|
| 'search-date-from'
|
||||||
|
| 'search-date-to';
|
||||||
|
|
||||||
export type TimetableSearchKey =
|
export type TimetableSearchKey =
|
||||||
| 'search-driver'
|
| 'search-driver'
|
||||||
| 'search-train'
|
| 'search-train'
|
||||||
| 'search-date'
|
| 'search-date-from'
|
||||||
| 'search-dispatcher'
|
| 'search-dispatcher'
|
||||||
|
| 'search-includesScenery'
|
||||||
| 'search-issuedFrom'
|
| 'search-issuedFrom'
|
||||||
| 'search-terminatingAt'
|
| 'search-terminatingAt'
|
||||||
| 'search-via';
|
| 'search-via'
|
||||||
|
| 'select-categoryCode';
|
||||||
|
|
||||||
export type TimetableSearchType = {
|
export type TimetableSearchType = {
|
||||||
[key in TimetableSearchKey]: string;
|
[key in TimetableSearchKey]: string;
|
||||||
@@ -19,7 +26,7 @@ export namespace Journal {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TimetableSorterKey = 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
|
export type TimetableSorterKey = 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
|
||||||
export type DispatcherSorterKey = 'timestampFrom' | 'duration';
|
export type DispatcherSorterKey = 'timestampFrom' | 'currentDuration';
|
||||||
|
|
||||||
export interface DispatcherSorter {
|
export interface DispatcherSorter {
|
||||||
id: DispatcherSorterKey;
|
id: DispatcherSorterKey;
|
||||||
@@ -39,6 +46,8 @@ export namespace Journal {
|
|||||||
ALL_SPECIALS = 'all-specials',
|
ALL_SPECIALS = 'all-specials',
|
||||||
TWR = 'twr',
|
TWR = 'twr',
|
||||||
SKR = 'skr',
|
SKR = 'skr',
|
||||||
|
PN = 'pn',
|
||||||
|
TN = 'tn',
|
||||||
TWR_SKR = 'twr-skr'
|
TWR_SKR = 'twr-skr'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,16 +63,14 @@ export namespace Journal {
|
|||||||
default: boolean;
|
default: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum StatsTab {
|
export interface TimetableStopDetails {
|
||||||
DRIVER_STATS = 'journal-driver-stats',
|
stopName: string;
|
||||||
DISPATCHER_STATS = 'journal-dispatcher-stats',
|
arrivalTimestamp: number;
|
||||||
DAILY_STATS = 'journal-daily-stats'
|
scheduledArrivalTimestamp: number;
|
||||||
}
|
departureTimestamp: number;
|
||||||
|
scheduledDepartureTimestamp: number;
|
||||||
export interface StatsButton {
|
stopTime: number;
|
||||||
tab: StatsTab;
|
stopType: string;
|
||||||
localeKey: string;
|
isConfirmed: boolean;
|
||||||
iconName: string;
|
|
||||||
disabled: boolean;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
<template>
|
||||||
|
<section class="profile-history-list">
|
||||||
|
<div class="list-header">
|
||||||
|
<div class="history-menu">
|
||||||
|
<button
|
||||||
|
v-for="(filterState, filterKey) in activeFilterTypes"
|
||||||
|
class="menu-btn btn--option"
|
||||||
|
:data-active="filterState"
|
||||||
|
@click="toggleFilter(filterKey)"
|
||||||
|
>
|
||||||
|
{{ t(`profile.filters.${filterKey}`) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-list-box">
|
||||||
|
<Loading v-if="journalDataStatus == Status.Data.Loading" />
|
||||||
|
|
||||||
|
<div v-else-if="combinedJournal.length == 0" class="no-recent-history">
|
||||||
|
{{ t('profile.list.no-recent-history') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<router-link
|
||||||
|
v-else
|
||||||
|
v-for="entry in combinedJournal"
|
||||||
|
:to="
|
||||||
|
'trainNo' in entry.value
|
||||||
|
? `/journal/timetables?search-train=%23${entry.value.id}`
|
||||||
|
: `/journal/dispatchers?search-duty-id=${entry.value.id}`
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- Date -->
|
||||||
|
<div class="entry-top-date">
|
||||||
|
<img
|
||||||
|
v-if="entry.type == 'Dispatcher'"
|
||||||
|
src="/images/icon-user.svg"
|
||||||
|
width="25"
|
||||||
|
alt="user icon"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-else-if="entry.type == 'Timetable'"
|
||||||
|
src="/images/icon-train.svg"
|
||||||
|
width="25"
|
||||||
|
alt="train icon"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img v-else src="/images/icon-timetable.svg" width="25" alt="timetable icon" />
|
||||||
|
|
||||||
|
<b
|
||||||
|
class="timestamp-indicator"
|
||||||
|
:data-online="
|
||||||
|
'isOnline' in entry.value
|
||||||
|
? entry.value.isOnline
|
||||||
|
: !entry.value.terminated && entry.type != 'IssuedTimetable'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ dateToLocaleString(entry.date, { dateStyle: 'long', timeStyle: 'short' }) }}
|
||||||
|
<span v-if="'timestampTo' in entry.value && entry.value.timestampTo">
|
||||||
|
-
|
||||||
|
<span v-if="new Date(entry.value.timestampTo).getDay() == entry.date.getDay()">{{
|
||||||
|
dateToLocaleString(new Date(entry.value.timestampTo), {
|
||||||
|
timeStyle: 'short'
|
||||||
|
})
|
||||||
|
}}</span>
|
||||||
|
<span v-else>{{
|
||||||
|
dateToLocaleString(new Date(entry.value.timestampTo), {
|
||||||
|
dateStyle: 'long',
|
||||||
|
timeStyle: 'short'
|
||||||
|
})
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timetables -->
|
||||||
|
<div v-if="'trainNo' in entry.value">
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ entry.value.trainCategoryCode }}
|
||||||
|
</b>
|
||||||
|
{{ ' ' }}
|
||||||
|
<b>{{ entry.value.trainNo }}</b>
|
||||||
|
<b class="text--grayed" v-if="entry.type == 'IssuedTimetable'">
|
||||||
|
{{ ' ' }} {{ t('profile.list.for') }}: {{ entry.value.driverName }}
|
||||||
|
</b>
|
||||||
|
{{ ' ' }}
|
||||||
|
<b>{{ entry.value.route.replace('|', ' > ') }}</b>
|
||||||
|
{{ ' ' }}
|
||||||
|
<b class="text--primary">{{ entry.value.currentDistance }} km</b>
|
||||||
|
<b> / {{ entry.value.routeDistance }} km</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dispatchers -->
|
||||||
|
<div v-else>
|
||||||
|
<b class="text--primary">{{ entry.value.stationName }}</b>
|
||||||
|
{{ ' - ' }}
|
||||||
|
<b class="timestamp-indicator" :data-online="entry.value.isOnline">
|
||||||
|
<span v-if="entry.value.isOnline">{{ t('profile.list.online-since') }}: </span>
|
||||||
|
<span>{{
|
||||||
|
humanizeDuration((entry.value.timestampTo || Date.now()) - entry.value.timestampFrom)
|
||||||
|
}}</span>
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onActivated, onDeactivated, onMounted, reactive, ref } from 'vue';
|
||||||
|
import { dateToLocaleString, humanizeDuration } from '../../composables/time';
|
||||||
|
import { API } from '../../typings/api';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { Status } from '../../typings/common';
|
||||||
|
import Loading from '../Global/Loading.vue';
|
||||||
|
|
||||||
|
type JournalEntryType = 'Timetable' | 'Dispatcher' | 'IssuedTimetable';
|
||||||
|
|
||||||
|
interface JournalEntry {
|
||||||
|
type: JournalEntryType;
|
||||||
|
date: Date;
|
||||||
|
value: API.TimetableHistory.DataShort | API.DispatcherHistory.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
playerName: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const apiStore = useApiStore();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const playerId = ref(-1);
|
||||||
|
const playerJournal = ref<API.PlayerJournal.Data | null>(null);
|
||||||
|
const journalDataStatus = ref(Status.Data.Initialized);
|
||||||
|
|
||||||
|
const intervalId = ref(-1);
|
||||||
|
|
||||||
|
const activeFilterTypes = reactive<Record<JournalEntryType, boolean>>({
|
||||||
|
Timetable: true,
|
||||||
|
Dispatcher: true,
|
||||||
|
IssuedTimetable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchPlayerJournal();
|
||||||
|
intervalId.value = setInterval(fetchPlayerJournal, 30000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
clearInterval(intervalId.value);
|
||||||
|
intervalId.value = -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const combinedJournal = computed<JournalEntry[]>(() => {
|
||||||
|
if (!playerJournal.value || !props.playerName) return [];
|
||||||
|
|
||||||
|
const list = [
|
||||||
|
...playerJournal.value.timetables,
|
||||||
|
...playerJournal.value.duties,
|
||||||
|
...playerJournal.value.issuedTimetables
|
||||||
|
]
|
||||||
|
.reduce<JournalEntry[]>((acc, v) => {
|
||||||
|
// Timetable or dispatcher type
|
||||||
|
if ('trainNo' in v) {
|
||||||
|
const isIssued = v.authorName == props.playerName;
|
||||||
|
|
||||||
|
if (!isIssued && !activeFilterTypes['Timetable']) return acc;
|
||||||
|
if (isIssued && !activeFilterTypes['IssuedTimetable']) return acc;
|
||||||
|
|
||||||
|
acc.push({
|
||||||
|
date: new Date(v.createdAt),
|
||||||
|
type: isIssued ? 'IssuedTimetable' : 'Timetable',
|
||||||
|
value: v
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!activeFilterTypes['Dispatcher']) return acc;
|
||||||
|
|
||||||
|
acc.push({
|
||||||
|
date: new Date(v.timestampFrom),
|
||||||
|
type: 'Dispatcher',
|
||||||
|
value: v
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, [])
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.date.getTime() - b.date.getTime() > 0 ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleFilter(filterType: JournalEntryType) {
|
||||||
|
const toggledState = !activeFilterTypes[filterType];
|
||||||
|
|
||||||
|
// Prevent switching off all filters at the same time (at least one must be active)
|
||||||
|
if (
|
||||||
|
toggledState === false &&
|
||||||
|
Object.values(activeFilterTypes).filter((v) => v === false).length ==
|
||||||
|
Object.values(activeFilterTypes).length - 1
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
activeFilterTypes[filterType] = toggledState;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPlayerJournal() {
|
||||||
|
const queryPlayerId = Number(route.query.playerId) || -1;
|
||||||
|
|
||||||
|
if (!apiStore.client || !queryPlayerId) return;
|
||||||
|
|
||||||
|
if (queryPlayerId != playerId.value) {
|
||||||
|
journalDataStatus.value = Status.Data.Loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
playerId.value = queryPlayerId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiStore.client.get<API.PlayerJournal.Data>('api/getPlayerJournal', {
|
||||||
|
params: {
|
||||||
|
playerId: queryPlayerId,
|
||||||
|
dateScope: '30d'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
playerJournal.value = response.data;
|
||||||
|
journalDataStatus.value = Status.Data.Loaded;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
journalDataStatus.value = Status.Data.Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
|
.profile-history-list {
|
||||||
|
overflow-y: scroll;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
& > h3 {
|
||||||
|
padding: 0.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-menu {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1em;
|
||||||
|
background-color: var(--clr-tile);
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-btn {
|
||||||
|
padding: 0.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #aaa;
|
||||||
|
|
||||||
|
&[data-active='true'] {
|
||||||
|
color: var(--clr-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list-box {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list-box > a {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25em;
|
||||||
|
|
||||||
|
background-color: var(--clr-bg-light);
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
text-align: initial;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-recent-history {
|
||||||
|
padding: 1em;
|
||||||
|
font-size: 1.25em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-top-date {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp-indicator {
|
||||||
|
color: #ccc;
|
||||||
|
|
||||||
|
&[data-online='true'] {
|
||||||
|
color: var(--clr-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include responsive.midScreen {
|
||||||
|
.profile-history-list {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="player-avatar">
|
||||||
|
<img
|
||||||
|
v-if="avatarId"
|
||||||
|
class="player-avatar-image"
|
||||||
|
ref="avatarImageRef"
|
||||||
|
:src="`https://td2.info.pl/index.php?action=dlattach;attach=${avatarId};type=avatar`"
|
||||||
|
alt="player image"
|
||||||
|
@load="onAvatarLoadSuccess"
|
||||||
|
@error="onAvatarLoadError"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="avatarLoadingStatus == Status.Data.Error || avatarId == 0"
|
||||||
|
class="img-placeholder"
|
||||||
|
height="100"
|
||||||
|
src="/images/default-avatar.jpg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Loading v-else-if="avatarLoadingStatus == Status.Data.Loading || avatarId === undefined" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Status } from '../../typings/common';
|
||||||
|
import Loading from '../Global/Loading.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
avatarId: {
|
||||||
|
type: Number
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const avatarImageRef = ref<HTMLImageElement | null>(null);
|
||||||
|
const avatarLoadingStatus = ref<Status.Data>(Status.Data.Loading);
|
||||||
|
|
||||||
|
function onAvatarLoadSuccess() {
|
||||||
|
if (!avatarImageRef.value) return;
|
||||||
|
|
||||||
|
avatarLoadingStatus.value = Status.Data.Loaded;
|
||||||
|
avatarImageRef.value.style.opacity = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAvatarLoadError() {
|
||||||
|
if (!avatarImageRef.value) return;
|
||||||
|
|
||||||
|
avatarLoadingStatus.value = Status.Data.Error;
|
||||||
|
avatarImageRef.value.src = '/images/default-avatar.jpg';
|
||||||
|
avatarImageRef.value.style.opacity = '1';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.player-avatar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
min-height: 110px;
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
top: 50%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.player-avatar-image {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<section class="profile-recent-stats">
|
||||||
|
<h2 class="stats-header">
|
||||||
|
<img src="/images/icon-stats.svg" width="30" alt="stats icon" />
|
||||||
|
{{ t('profile.recent-stats.header') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="month-stats-box">
|
||||||
|
<div class="month-stat">
|
||||||
|
<div><img src="/images/icon-train.svg" width="30" alt="train icon" /></div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text--primary">{{ playerInfo.driverStatsLastMonth.countAll }}</h3>
|
||||||
|
</div>
|
||||||
|
<div>{{ t('profile.recent-stats.timetables') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="month-stat">
|
||||||
|
<div><img src="/images/icon-spawn.svg" width="30" alt="spawn icon" /></div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text--primary">
|
||||||
|
{{ playerInfo.driverStatsLastMonth.currentDistanceTotal?.toFixed(2) || 0 }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div>{{ t('profile.recent-stats.distance') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="month-stat">
|
||||||
|
<div><img src="/images/icon-user.svg" width="30" alt="user icon" /></div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text--primary">
|
||||||
|
{{ playerInfo.dispatcherStatsLastMonth.services?.count || 0 }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div>{{ t('profile.recent-stats.duties') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="month-stat">
|
||||||
|
<div><img src="/images/icon-timetable.svg" width="30" alt="timetable icon" /></div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text--primary">
|
||||||
|
{{ playerInfo.dispatcherStatsLastMonth.issuedTimetables?.count || 0 }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div>{{ t('profile.recent-stats.created-timetables') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
import { API } from '../../typings/api';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
playerInfo: {
|
||||||
|
type: Object as PropType<API.PlayerInfo.Data>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
|
.profile-recent-stats {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-header {
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-stats-box {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 0.5em;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-stat {
|
||||||
|
background-color: var(--clr-bg-light);
|
||||||
|
border-radius: 0.5em;
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div:nth-child(3) {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include responsive.smallScreen {
|
||||||
|
.month-stats-box {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
<template>
|
||||||
|
<section class="profile-summary">
|
||||||
|
<div class="player-info">
|
||||||
|
<div class="info-main">
|
||||||
|
<ProfilePlayerAvatar :avatarId="playerTD2Info?.avatar" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 class="player-name-header" :class="{ 'text--donator': isPlayerDonator }">
|
||||||
|
<a :href="`https://td2.info.pl/profile/?u=${route.query.playerId}`" target="_blank">
|
||||||
|
<img
|
||||||
|
v-if="isPlayerDonator"
|
||||||
|
src="/images/icon-diamond.svg"
|
||||||
|
width="25"
|
||||||
|
alt="diamond icon"
|
||||||
|
/>
|
||||||
|
{{ playerName }}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="player-badges">
|
||||||
|
<div class="badge-container" v-if="playerInfo.driverStats.driverLevel != null">
|
||||||
|
<span
|
||||||
|
class="level-badge driver"
|
||||||
|
:style="calculateExpStyles(playerInfo.driverStats.driverLevel)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
playerInfo.driverStats.driverLevel > 1 ? playerInfo.driverStats.driverLevel : 'L'
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
{{ t('profile.stats.driver') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badge-container" v-if="playerInfo.dispatcherStats.dispatcherLevel != null">
|
||||||
|
<span
|
||||||
|
class="level-badge dispatcher"
|
||||||
|
:style="calculateExpStyles(playerInfo.dispatcherStats.dispatcherLevel)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
playerInfo.dispatcherStats.dispatcherLevel > 1
|
||||||
|
? playerInfo.dispatcherStats.dispatcherLevel
|
||||||
|
: 'L'
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
{{ t('profile.stats.dispatcher') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="player-journal-links">
|
||||||
|
<router-link
|
||||||
|
class="a-button btn--action"
|
||||||
|
:to="`/journal/timetables?search-driver=${playerInfo.driverStats.driverName}`"
|
||||||
|
>
|
||||||
|
{{ t('profile.stats.timetables-journal') }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<router-link
|
||||||
|
class="a-button btn--action"
|
||||||
|
:to="`/journal/dispatchers?search-dispatcher=${playerInfo.dispatcherStats.dispatcherName}`"
|
||||||
|
>
|
||||||
|
{{ t('profile.stats.dispatchers-journal') }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="a-button btn--action"
|
||||||
|
:href="`https://td2.info.pl/profile/?u=${route.query.playerId}`"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ t('profile.stats.forum-profile') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current activity -->
|
||||||
|
<div
|
||||||
|
class="player-activity"
|
||||||
|
v-if="activeDispatches.length > 0 || activeTrains.length > 0"
|
||||||
|
>
|
||||||
|
<div class="info-activity" v-if="activeDispatches.length > 0">
|
||||||
|
<router-link
|
||||||
|
v-for="d in activeDispatches"
|
||||||
|
class="dispatcher-badge"
|
||||||
|
:to="`/scenery?station=${d.stationName}`"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-user.svg" width="25" alt="user icon" />
|
||||||
|
<b>{{ d.stationName }} ({{ getRegionNameById(d.region) }})</b>
|
||||||
|
<StationStatusBadge :isOnline="true" :dispatcherStatus="d.dispatcherStatus" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-activity" v-if="activeTrains.length > 0">
|
||||||
|
<router-link
|
||||||
|
v-for="d in activeTrains"
|
||||||
|
:to="`/driver?trainId=${d.id}`"
|
||||||
|
class="driver-badge"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-train.svg" width="25" alt="train icon" />
|
||||||
|
<span v-if="d.timetable" class="text--primary">{{ d.timetable.category }}</span>
|
||||||
|
<span>{{ d.trainNo }}</span>
|
||||||
|
•
|
||||||
|
<span>{{ d.currentStationName }} ({{ getRegionNameById(d.region) }})</span>
|
||||||
|
•
|
||||||
|
<span class="text--grayed">{{ d.stockString.split(';')[0] }}</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="player-stats">
|
||||||
|
<div class="stats-driver">
|
||||||
|
<h3 class="stats-header">
|
||||||
|
<img src="/images/icon-train.svg" width="30" alt="train icon" />
|
||||||
|
{{ t('profile.stats.header-driver') }}
|
||||||
|
</h3>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div v-if="playerInfo.driverStats.countAll > 0">
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ playerInfo.driverStats.countFulfilled }} /
|
||||||
|
{{ playerInfo.driverStats.countAll }} ({{
|
||||||
|
getCountPercentage(
|
||||||
|
playerInfo.driverStats.countFulfilled,
|
||||||
|
playerInfo.driverStats.countAll,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
}}%)
|
||||||
|
</b>
|
||||||
|
- {{ t('profile.stats.fulfilled-timetables') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ playerInfo.driverStats.currentDistanceTotal?.toFixed(2) }} /
|
||||||
|
{{ playerInfo.driverStats.routeDistanceTotal?.toFixed(2) }} ({{
|
||||||
|
getCountPercentage(
|
||||||
|
playerInfo.driverStats.currentDistanceTotal || 0,
|
||||||
|
playerInfo.driverStats.routeDistanceTotal || 0,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
}}%)
|
||||||
|
</b>
|
||||||
|
- {{ t('profile.stats.route-distance') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ playerInfo.driverStats.confirmedStopsTotal }} /
|
||||||
|
{{ playerInfo.driverStats.allStopsTotal }} ({{
|
||||||
|
getCountPercentage(
|
||||||
|
playerInfo.driverStats.confirmedStopsTotal || 0,
|
||||||
|
playerInfo.driverStats.allStopsTotal || 0,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
}}%)
|
||||||
|
</b>
|
||||||
|
- {{ t('profile.stats.confirmed-stops') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">{{ playerInfo.driverStats.routeDistanceMax || 0 }}km</b> -
|
||||||
|
{{ t('profile.stats.longest-timetable') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ playerInfo.driverStats.routeDistanceAvg?.toFixed(2) || 0 }}km
|
||||||
|
</b>
|
||||||
|
- {{ t('profile.stats.avg-timetable-length') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text--grayed" v-else>
|
||||||
|
{{ t('profile.stats.no-timetable-stats') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="stats-dispatcher"
|
||||||
|
v-if="playerInfo.dispatcherStats && playerInfo.dispatcherStats.services?.count"
|
||||||
|
>
|
||||||
|
<h3 class="stats-header">
|
||||||
|
<img src="/images/icon-user.svg" width="30" alt="user icon" />
|
||||||
|
{{ t('profile.stats.header-dispatcher') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">{{ playerInfo.dispatcherStats.services.count }}</b> -
|
||||||
|
{{ t('profile.stats.duties-count') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">{{
|
||||||
|
humanizeDuration(playerInfo.dispatcherStats.services.durationMax)
|
||||||
|
}}</b>
|
||||||
|
- {{ t('profile.stats.longest-duty') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="playerInfo.dispatcherStats.issuedTimetables">
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">{{ playerInfo.dispatcherStats.issuedTimetables.count }}</b>
|
||||||
|
- {{ t('profile.stats.created-timetables-count') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ playerInfo.dispatcherStats.issuedTimetables.distanceMax }}km
|
||||||
|
</b>
|
||||||
|
- {{ t('profile.stats.longest-created-timetable') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ playerInfo.dispatcherStats.issuedTimetables.distanceSum.toFixed(2) }}km
|
||||||
|
</b>
|
||||||
|
- {{ t('profile.stats.created-timetables-length-sum') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text--grayed" v-else>
|
||||||
|
{{ t('profile.stats.no-dispatcher-stats') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onMounted, PropType, ref } from 'vue';
|
||||||
|
import { API, Td2API } from '../../typings/api';
|
||||||
|
import { calculateExpStyles } from '../../composables/badge';
|
||||||
|
import { getCountPercentage } from '../../utils/calcUtils';
|
||||||
|
import { humanizeDuration } from '../../composables/time';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import StationStatusBadge from '../Global/StationStatusBadge.vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import ProfilePlayerAvatar from './ProfilePlayerAvatar.vue';
|
||||||
|
import { getRegionNameById } from '../../utils/regionUtils';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const apiStore = useApiStore();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
playerInfo: {
|
||||||
|
type: Object as PropType<API.PlayerInfo.Data>,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
playerName: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const playerTD2Info = ref<Td2API.UsersInfoByName.UserInfo | null>(null);
|
||||||
|
|
||||||
|
const isPlayerDonator = computed(() =>
|
||||||
|
props.playerName ? apiStore.donatorsData.includes(props.playerName) : false
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTD2Data();
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeDispatches = computed(() => {
|
||||||
|
if (!props.playerName) return [];
|
||||||
|
if (!apiStore.activeData || !apiStore.activeData.activeSceneries) return [];
|
||||||
|
|
||||||
|
return apiStore.activeData.activeSceneries.filter(
|
||||||
|
(sc) =>
|
||||||
|
sc.dispatcherName == props.playerName && (sc.lastSeen >= Date.now() - 60000 || sc.isOnline)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeTrains = computed(() => {
|
||||||
|
if (!props.playerName) return [];
|
||||||
|
if (!apiStore.activeData || !apiStore.activeData.trains) return [];
|
||||||
|
|
||||||
|
return apiStore.activeData.trains.filter(
|
||||||
|
(t) => t.driverName == props.playerName && (t.lastSeen >= Date.now() - 60000 || t.online)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchTD2Data() {
|
||||||
|
if (!props.playerName) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get<Td2API.UsersInfoByName.Response>('https://api.td2.info.pl', {
|
||||||
|
params: {
|
||||||
|
method: 'getUsersInfoByName',
|
||||||
|
name: props.playerName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success && response.data.message.length == 1) {
|
||||||
|
playerTD2Info.value = response.data.message[0];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use '../../styles/badge';
|
||||||
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
|
.profile-summary {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-name-header {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-badges {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
& > .level-badge {
|
||||||
|
font-size: 1.15em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-journal-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-activity {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1em;
|
||||||
|
margin-top: 1em;
|
||||||
|
|
||||||
|
.dispatcher-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.driver-badge {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
gap: 0.25em;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-info,
|
||||||
|
.player-stats > div {
|
||||||
|
background-color: var(--clr-tile);
|
||||||
|
border-radius: 0.5em;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include responsive.midScreen {
|
||||||
|
.player-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include responsive.smallScreen {
|
||||||
|
.player-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -96,6 +96,7 @@ export default defineComponent({
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
historyList: [] as API.DispatcherHistory.Response,
|
historyList: [] as API.DispatcherHistory.Response,
|
||||||
|
lastStationName: '',
|
||||||
dataStatus: Status.Data.Loading,
|
dataStatus: Status.Data.Loading,
|
||||||
DataStatus: Status.Data,
|
DataStatus: Status.Data,
|
||||||
apiStore: useApiStore()
|
apiStore: useApiStore()
|
||||||
@@ -103,10 +104,10 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async activated() {
|
async activated() {
|
||||||
// if (this.historyList.length == 0) {
|
this.historyList.length = 0;
|
||||||
|
|
||||||
const fetchedHistory = await this.fetchAPIData();
|
const fetchedHistory = await this.fetchAPIData();
|
||||||
if (fetchedHistory) this.historyList = fetchedHistory;
|
if (fetchedHistory) this.historyList = fetchedHistory;
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -148,8 +149,9 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
@use '../../styles/responsive';
|
||||||
@import '../../styles/sceneryViewTables.scss';
|
@use '../../styles/scenery-history-table';
|
||||||
|
@use '../../styles/badge';
|
||||||
|
|
||||||
.scenery-dispatchers-history {
|
.scenery-dispatchers-history {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -194,7 +196,7 @@ export default defineComponent({
|
|||||||
color: springgreen;
|
color: springgreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen {
|
@include responsive.smallScreen {
|
||||||
.journal-list > div {
|
.journal-list > div {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-header">
|
<section class="info-header">
|
||||||
|
<button
|
||||||
|
class="btn btn-return"
|
||||||
|
:title="$t('scenery.return-btn')"
|
||||||
|
@click="onReturnButtonClick"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-back.svg" alt="return button" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<a class="scenery-name" :href="station?.generalInfo?.url" target="_blank">
|
<a class="scenery-name" :href="station?.generalInfo?.url" target="_blank">
|
||||||
{{ stationName.replace(/_/g, ' ') }}
|
{{ stationName.replace(/_/g, ' ') }}
|
||||||
</a>
|
</a>
|
||||||
@@ -12,40 +20,64 @@
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { PropType, defineComponent } from 'vue';
|
import { onMounted, PropType, ref } from 'vue';
|
||||||
import { ActiveScenery, Station } from '../../typings/common';
|
import { ActiveScenery, Station } from '../../typings/common';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
export default defineComponent({
|
const route = useRoute();
|
||||||
props: {
|
const router = useRouter();
|
||||||
station: {
|
|
||||||
type: Object as PropType<Station>
|
|
||||||
},
|
|
||||||
|
|
||||||
stationName: {
|
const prevPath = ref('/');
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
|
|
||||||
onlineScenery: {
|
onMounted(() => {
|
||||||
type: Object as PropType<ActiveScenery>
|
prevPath.value = (route.meta['prevPath'] as string) ?? '/';
|
||||||
}
|
});
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
station: {
|
||||||
|
type: Object as PropType<Station>
|
||||||
|
},
|
||||||
|
|
||||||
|
stationName: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
onlineScenery: {
|
||||||
|
type: Object as PropType<ActiveScenery>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onReturnButtonClick() {
|
||||||
|
router.push(prevPath.value);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/variables.scss';
|
@use '../../styles/responsive';
|
||||||
@import '../../styles/responsive.scss';
|
@use 'sass:color';
|
||||||
|
|
||||||
.info-header {
|
.btn-return {
|
||||||
margin-top: 1em;
|
$bgColor: #2b2b2b;
|
||||||
|
background-color: $bgColor;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: color.adjust($color: $bgColor, $lightness: 15%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenery-name {
|
.scenery-name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 3em;
|
font-size: 3em;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,4 +91,10 @@ export default defineComponent({
|
|||||||
color: #aaa;
|
color: #aaa;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include responsive.smallScreen {
|
||||||
|
.scenery-name {
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,71 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="scenery-info">
|
<div class="scenery-info">
|
||||||
<section>
|
<section>
|
||||||
<div class="scenery-info-general">
|
<SceneryInfoIcons :station="station" />
|
||||||
<SceneryInfoIcons :station="station" />
|
<SceneryInfoGeneral :station="station" />
|
||||||
|
<SceneryInfoRoutes v-if="station" :station="station" />
|
||||||
|
<SceneryInfoAuthors :station="station" />
|
||||||
|
|
||||||
<div class="scenery-general-list" v-if="station?.generalInfo">
|
<div style="margin: 1em 0; height: 2px; background-color: white"></div>
|
||||||
<span>
|
|
||||||
<b>{{ $t('availability.title') }}:</b>
|
|
||||||
{{ $t(`availability.${station.generalInfo.availability}`) }}
|
|
||||||
|
|
||||||
<span v-if="station.generalInfo.reqLevel > -1">
|
|
||||||
-
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
'scenery.req-level',
|
|
||||||
{ lvl: station.generalInfo.reqLevel },
|
|
||||||
station.generalInfo.reqLevel
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
• <b>{{ $t('controls.title') }}:</b>
|
|
||||||
{{ $t(`controls.${station.generalInfo.controlType}`) }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
• <b>{{ $t('signals.title') }}:</b>
|
|
||||||
{{ $t(`signals.${station.generalInfo.signalType}`) }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-if="station.generalInfo.lines">
|
|
||||||
• <b>{{ $t('scenery.lines-title') }}:</b> {{ station.generalInfo.lines }}
|
|
||||||
</span>
|
|
||||||
<span v-if="station.generalInfo.project">
|
|
||||||
• <b>{{ $t('scenery.project-title') }}: </b>
|
|
||||||
<a
|
|
||||||
style="color: salmon; text-decoration: underline; font-weight: bold"
|
|
||||||
:href="station.generalInfo.projectUrl"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{{ station.generalInfo.project }}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SceneryInfoRoutes v-if="station" :station="station" />
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="scenery-authors"
|
|
||||||
v-if="station?.generalInfo?.authors && station.generalInfo.authors.length > 0"
|
|
||||||
>
|
|
||||||
<b>
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
'scenery.authors-title',
|
|
||||||
{ authors: station.generalInfo.authors.length },
|
|
||||||
station.generalInfo.authors.length
|
|
||||||
)
|
|
||||||
}}:
|
|
||||||
</b>
|
|
||||||
{{ station.generalInfo.authors.join(', ') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin: 2em 0; height: 2px; background-color: white"></div>
|
|
||||||
|
|
||||||
<!-- info dispatcher -->
|
<!-- info dispatcher -->
|
||||||
<SceneryInfoDispatcher :onlineScenery="onlineScenery" />
|
<SceneryInfoDispatcher :onlineScenery="onlineScenery" />
|
||||||
@@ -89,12 +30,17 @@ import SceneryInfoIcons from './SceneryInfo/SceneryInfoIcons.vue';
|
|||||||
import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue';
|
import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue';
|
||||||
import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
|
import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
|
||||||
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
|
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
|
||||||
|
import SceneryInfoGeneral from './SceneryInfo/SceneryInfoGeneral.vue';
|
||||||
|
import SceneryInfoAuthors from './SceneryInfo/SceneryInfoAuthors.vue';
|
||||||
|
|
||||||
import { ActiveScenery, Station } from '../../typings/common';
|
import { ActiveScenery, Station } from '../../typings/common';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
SceneryInfoDispatcher,
|
SceneryInfoDispatcher,
|
||||||
|
SceneryInfoGeneral,
|
||||||
SceneryInfoIcons,
|
SceneryInfoIcons,
|
||||||
|
SceneryInfoAuthors,
|
||||||
SceneryInfoUserList,
|
SceneryInfoUserList,
|
||||||
SceneryInfoSpawnList,
|
SceneryInfoSpawnList,
|
||||||
SceneryInfoRoutes
|
SceneryInfoRoutes
|
||||||
@@ -111,20 +57,8 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
@use '../../styles/responsive';
|
||||||
@import '../../styles/badge.scss';
|
|
||||||
|
|
||||||
h3.section-header {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
padding: 0.3em;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-lists {
|
.info-lists {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -134,20 +68,6 @@ h3.section-header {
|
|||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenery-info-general {
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenery-general-list {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
span {
|
|
||||||
margin: 0 0.15em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenery-topic a {
|
.scenery-topic a {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="scenery-authors"
|
||||||
|
v-if="station?.generalInfo?.authors && station.generalInfo.authors.length > 0"
|
||||||
|
>
|
||||||
|
<b>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'scenery.authors-title',
|
||||||
|
{ authors: station.generalInfo.authors.length },
|
||||||
|
station.generalInfo.authors.length
|
||||||
|
)
|
||||||
|
}}:
|
||||||
|
</b>
|
||||||
|
{{ station.generalInfo.authors.join(', ') }}
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
import { Station } from '../../../typings/common';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
station: {
|
||||||
|
type: Object as PropType<Station>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -8,10 +8,7 @@
|
|||||||
{{ onlineScenery.dispatcherExp > 1 ? onlineScenery.dispatcherExp : 'L' }}
|
{{ onlineScenery.dispatcherExp > 1 ? onlineScenery.dispatcherExp : 'L' }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<router-link
|
<router-link class="dispatcher-name" :to="`/profile?playerId=${onlineScenery.dispatcherId}`">
|
||||||
class="dispatcher-name"
|
|
||||||
:to="`/journal/dispatchers?search-dispatcher=${onlineScenery.dispatcherName}`"
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
class="text--donator"
|
class="text--donator"
|
||||||
v-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)"
|
v-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)"
|
||||||
@@ -21,6 +18,8 @@
|
|||||||
</span>
|
</span>
|
||||||
<span v-else>{{ onlineScenery.dispatcherName }}</span>
|
<span v-else>{{ onlineScenery.dispatcherName }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<FlagIcon :languageId="onlineScenery.dispatcherLanguageId" width="1.25em" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-bottom">
|
<div class="info-bottom">
|
||||||
@@ -51,9 +50,11 @@ import styleMixin from '../../../mixins/styleMixin';
|
|||||||
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
||||||
import { ActiveScenery } from '../../../typings/common';
|
import { ActiveScenery } from '../../../typings/common';
|
||||||
import { useApiStore } from '../../../store/apiStore';
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
|
import FlagIcon from '../../Global/FlagIcon.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [styleMixin, dateMixin, routerMixin],
|
mixins: [styleMixin, dateMixin, routerMixin],
|
||||||
|
components: { StationStatusBadge, FlagIcon },
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -66,8 +67,7 @@ export default defineComponent({
|
|||||||
type: Object as PropType<ActiveScenery>,
|
type: Object as PropType<ActiveScenery>,
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
components: { StationStatusBadge }
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<section class="info-general">
|
||||||
|
<div v-if="station?.generalInfo === undefined">
|
||||||
|
<b>{{ $t('scenery.no-data') }}</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<span>
|
||||||
|
<b>{{ $t('availability.title') }}:</b>
|
||||||
|
{{ $t(`availability.${station.generalInfo.availability}`) }}
|
||||||
|
|
||||||
|
<span v-if="station.generalInfo.reqLevel > -1">
|
||||||
|
-
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'scenery.req-level',
|
||||||
|
{ lvl: station.generalInfo.reqLevel },
|
||||||
|
station.generalInfo.reqLevel
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
• <b>{{ $t('controls.title') }}:</b>
|
||||||
|
{{ $t(`controls.${station.generalInfo.controlType}`) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
• <b>{{ $t('signals.title') }}:</b>
|
||||||
|
{{ $t(`signals.${station.generalInfo.signalType}`) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="station.generalInfo.lines">
|
||||||
|
• <b>{{ $t('scenery.lines-title') }}:</b> {{ station.generalInfo.lines }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="station.generalInfo.project">
|
||||||
|
• <b>{{ $t('scenery.project-title') }}: </b>
|
||||||
|
<a
|
||||||
|
style="color: salmon; text-decoration: underline; font-weight: bold"
|
||||||
|
:href="station.generalInfo.projectUrl"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ station.generalInfo.project }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="additionalTools.length != 0">
|
||||||
|
• <b>{{ $t('scenery.additional-tools-title') }}: </b>
|
||||||
|
{{ additionalTools.join(', ') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
import { Station } from '../../../typings/common';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
station: {
|
||||||
|
type: Object as PropType<Station>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
additionalTools() {
|
||||||
|
if (this.$props.station?.generalInfo === undefined) return [];
|
||||||
|
let tools = [];
|
||||||
|
|
||||||
|
if (this.$props.station.generalInfo.SUP) tools.push('SUP');
|
||||||
|
if (this.$props.station.generalInfo.ASDEK) tools.push('ASDEK');
|
||||||
|
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.info-general {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
div {
|
||||||
|
margin: 0 0.15em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -17,25 +17,6 @@
|
|||||||
{{ station?.generalInfo.reqLevel >= 2 ? station?.generalInfo.reqLevel : 'L' }}
|
{{ station?.generalInfo.reqLevel >= 2 ? station?.generalInfo.reqLevel : 'L' }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="station?.generalInfo"
|
|
||||||
class="scenery-icon icon-info"
|
|
||||||
:class="station?.generalInfo.controlType.replace('+', '-')"
|
|
||||||
:title="
|
|
||||||
$t('sceneries.info.control-type') + $t(`controls.${station?.generalInfo.controlType}`)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<img
|
|
||||||
v-if="station?.generalInfo?.signalType"
|
|
||||||
class="icon-info"
|
|
||||||
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
|
|
||||||
:alt="station.generalInfo.signalType"
|
|
||||||
:title="$t('sceneries.info.signals-type') + $t(`signals.${station.generalInfo.signalType}`)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-if="station?.generalInfo?.availability == 'nonPublic'"
|
v-if="station?.generalInfo?.availability == 'nonPublic'"
|
||||||
class="icon-info"
|
class="icon-info"
|
||||||
@@ -60,6 +41,33 @@
|
|||||||
:title="$t('sceneries.info.abandoned')"
|
:title="$t('sceneries.info.abandoned')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="station?.generalInfo"
|
||||||
|
class="scenery-icon icon-info"
|
||||||
|
:class="station?.generalInfo.controlType.replace('+', '-')"
|
||||||
|
:title="
|
||||||
|
$t('sceneries.info.control-type') + $t(`controls.${station?.generalInfo.controlType}`)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="station?.generalInfo?.signalType"
|
||||||
|
class="icon-info"
|
||||||
|
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
|
||||||
|
:alt="station.generalInfo.signalType"
|
||||||
|
:title="$t('sceneries.info.signals-type') + $t(`signals.${station.generalInfo.signalType}`)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="station?.generalInfo?.lines"
|
||||||
|
class="icon-info"
|
||||||
|
src="/images/icon-real.svg"
|
||||||
|
alt="real scenery"
|
||||||
|
:title="`${$t('sceneries.info.real')} ${station.generalInfo.lines}`"
|
||||||
|
/>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-if="station?.generalInfo?.SUP"
|
v-if="station?.generalInfo?.SUP"
|
||||||
class="icon-info"
|
class="icon-info"
|
||||||
@@ -75,14 +83,6 @@
|
|||||||
alt="dSAT ASDEK"
|
alt="dSAT ASDEK"
|
||||||
:title="$t('sceneries.info.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('sceneries.info.real')} ${station.generalInfo.lines}`"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../../styles/icons.scss';
|
@use '../../../styles/icons';
|
||||||
|
|
||||||
.info-icons {
|
.info-icons {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -118,6 +118,7 @@ export default defineComponent({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
width: 3em;
|
width: 3em;
|
||||||
|
height: 3em;
|
||||||
margin: 0.25em;
|
margin: 0.25em;
|
||||||
|
|
||||||
border: 2px solid #4e4e4e;
|
border: 2px solid #4e4e4e;
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-routes" v-if="station.generalInfo">
|
<section class="info-routes" v-if="station.generalInfo">
|
||||||
<div class="routes one-way" v-if="oneWayRoutes.length > 0">
|
<div class="routes one-way" v-if="oneWayRoutes.length > 0">
|
||||||
<b>{{ $t('scenery.one-way-routes') }}</b>
|
<button
|
||||||
|
class="routes-btn"
|
||||||
|
@click="toggleRoutesVisibility('single')"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="`${showInternalSingleRoutes ? $t('scenery.btn-hide-internal-routes') : $t('scenery.btn-show-internal-routes')}`"
|
||||||
|
>
|
||||||
|
<b>{{ $t('scenery.one-way-routes') }}</b>
|
||||||
|
<i class="fa-solid" :class="`${showInternalSingleRoutes ? 'fa-eye' : 'fa-eye-slash'}`"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<ul class="routes-list">
|
<ul class="routes-list">
|
||||||
<li
|
<li v-for="route in oneWayRoutes" :key="route.routeName">
|
||||||
v-for="route in oneWayRoutes"
|
|
||||||
:key="route.routeName"
|
|
||||||
@click="setActiveShowLength(route.routeName)"
|
|
||||||
>
|
|
||||||
<span :class="{ 'no-catenary': !route.isElectric, internal: route.isInternal }">
|
<span :class="{ 'no-catenary': !route.isElectric, internal: route.isInternal }">
|
||||||
{{ route.routeName }}</span
|
{{ route.routeName }}</span
|
||||||
>
|
>
|
||||||
<span v-if="route.routeSpeed" class="speed">
|
<span v-if="route.routeSpeed" class="speed">
|
||||||
{{
|
{{ route.routeSpeed }}
|
||||||
activeShowLength.includes(route.routeName)
|
</span>
|
||||||
? route.routeLength + 'm'
|
<span v-if="route.routeLength" class="length">
|
||||||
: route.routeSpeed
|
{{ (route.routeLength / 1000).toFixed(1) + 'km' }}
|
||||||
}}
|
|
||||||
</span>
|
</span>
|
||||||
<span v-if="route.isRouteSBL" class="sbl">SBL</span>
|
<span v-if="route.isRouteSBL" class="sbl">SBL</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -25,23 +28,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="routes two-way" v-if="twoWayRoutes.length > 0">
|
<div class="routes two-way" v-if="twoWayRoutes.length > 0">
|
||||||
<b>{{ $t('scenery.two-way-routes') }}</b>
|
<button
|
||||||
|
class="routes-btn"
|
||||||
|
@click="toggleRoutesVisibility('double')"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="`${showInternalDoubleRoutes ? $t('scenery.btn-hide-internal-routes') : $t('scenery.btn-show-internal-routes')}`"
|
||||||
|
>
|
||||||
|
<b>{{ $t('scenery.two-way-routes') }}</b>
|
||||||
|
<i class="fa-solid" :class="`${showInternalDoubleRoutes ? 'fa-eye' : 'fa-eye-slash'}`"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<ul class="routes-list">
|
<ul class="routes-list">
|
||||||
<li
|
<li v-for="route in twoWayRoutes" :key="route.routeName">
|
||||||
v-for="route in twoWayRoutes"
|
<span :class="{ 'no-catenary': !route.isElectric, internal: route.isInternal }">
|
||||||
:key="route.routeName"
|
{{ route.routeName }}
|
||||||
@click="setActiveShowLength(route.routeName)"
|
</span>
|
||||||
>
|
|
||||||
<span :class="{ 'no-catenary': !route.isElectric, internal: route.isInternal }">{{
|
|
||||||
route.routeName
|
|
||||||
}}</span>
|
|
||||||
<span v-if="route.routeSpeed" class="speed">
|
<span v-if="route.routeSpeed" class="speed">
|
||||||
{{
|
<span>{{ route.routeSpeed }}</span>
|
||||||
activeShowLength.includes(route.routeName)
|
<span v-if="route.routeSpeedExit && route.routeSpeedExit != route.routeSpeed">
|
||||||
? route.routeLength + 'm'
|
| {{ route.routeSpeedExit }}
|
||||||
: route.routeSpeed
|
</span>
|
||||||
}}
|
</span>
|
||||||
|
<span v-if="route.routeLength" class="length">
|
||||||
|
{{ (route.routeLength / 1000).toFixed(1) + 'km' }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="route.isRouteSBL" class="sbl">SBL</span>
|
<span v-if="route.isRouteSBL" class="sbl">SBL</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -53,6 +62,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PropType, defineComponent } from 'vue';
|
import { PropType, defineComponent } from 'vue';
|
||||||
import { Station } from '../../../typings/common';
|
import { Station } from '../../../typings/common';
|
||||||
|
import StorageManager from '../../../managers/storageManager';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -62,27 +72,50 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
data() {
|
||||||
setActiveShowLength(name: string) {
|
return {
|
||||||
if (this.activeShowLength.includes(name))
|
showInternalSingleRoutes: false,
|
||||||
this.activeShowLength.splice(this.activeShowLength.indexOf(name), 1);
|
showInternalDoubleRoutes: false
|
||||||
else this.activeShowLength.push(name);
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
if (StorageManager.getBooleanValue('showInternalDoubleRoutes')) {
|
||||||
|
this.showInternalDoubleRoutes = StorageManager.getBooleanValue('showInternalDoubleRoutes');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StorageManager.getBooleanValue('showInternalSingleRoutes')) {
|
||||||
|
this.showInternalSingleRoutes = StorageManager.getBooleanValue('showInternalSingleRoutes');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
methods: {
|
||||||
return {
|
toggleRoutesVisibility(type: 'single' | 'double') {
|
||||||
activeShowLength: [] as string[]
|
if (type == 'double') {
|
||||||
};
|
this.showInternalDoubleRoutes = !this.showInternalDoubleRoutes;
|
||||||
|
StorageManager.setBooleanValue('showInternalDoubleRoutes', this.showInternalDoubleRoutes);
|
||||||
|
} else {
|
||||||
|
this.showInternalSingleRoutes = !this.showInternalSingleRoutes;
|
||||||
|
StorageManager.setBooleanValue('showInternalSingleRoutes', this.showInternalSingleRoutes);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
oneWayRoutes() {
|
oneWayRoutes() {
|
||||||
return this.station.generalInfo?.routes.single ?? [];
|
return (
|
||||||
|
this.station.generalInfo?.routes.single
|
||||||
|
.filter((r) => !r.isInternal || r.isInternal == this.showInternalSingleRoutes)
|
||||||
|
.sort((r1, r2) => r1.routeName.localeCompare(r2.routeName)) ?? []
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
twoWayRoutes() {
|
twoWayRoutes() {
|
||||||
return this.station.generalInfo?.routes.double ?? [];
|
return (
|
||||||
|
this.station.generalInfo?.routes.double
|
||||||
|
.filter((r) => !r.isInternal || r.isInternal == this.showInternalDoubleRoutes)
|
||||||
|
.sort((r1, r2) => r1.routeName.localeCompare(r2.routeName)) ?? []
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -90,22 +123,29 @@ export default defineComponent({
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.info-routes {
|
.info-routes {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
|
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.routes {
|
.routes {
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.routes > button.routes-btn {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ul.routes-list {
|
ul.routes-list {
|
||||||
margin: 0.45em 0.25em;
|
margin: 0.45em 0.25em;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -120,8 +160,8 @@ ul.routes-list {
|
|||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
|
||||||
span {
|
& > span {
|
||||||
padding: 0.2em 0.25em;
|
padding: 0.2em;
|
||||||
background-color: #007599;
|
background-color: #007599;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
@@ -138,6 +178,10 @@ ul.routes-list {
|
|||||||
color: #cfcfcf;
|
color: #cfcfcf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.length {
|
||||||
|
background-color: #303030;
|
||||||
|
color: #cfcfcf;
|
||||||
|
}
|
||||||
&.sbl {
|
&.sbl {
|
||||||
color: var(--clr-primary);
|
color: var(--clr-primary);
|
||||||
background-color: #404040;
|
background-color: #404040;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-spawn-list">
|
<section class="info-spawn-list">
|
||||||
<h3 class="spawn-header section-header">
|
<h3 class="spawn-header">
|
||||||
<img src="/images/icon-spawn.svg" alt="Open spawns icon" />
|
<img src="/images/icon-spawn.svg" alt="Open spawns icon" />
|
||||||
{{ $t('scenery.spawns') }}
|
{{ $t('scenery.spawns') }}
|
||||||
<span class="text--primary">{{ onlineScenery?.spawns.length || '0' }}</span>
|
<span class="text--primary">{{ onlineScenery?.spawns.length || '0' }}</span>
|
||||||
@@ -53,12 +53,23 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../../styles/variables.scss';
|
@use '../../../styles/badge';
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h3.spawn-header {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding: 0.3em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
.spawns-anim {
|
.spawns-anim {
|
||||||
&-move,
|
&-move,
|
||||||
&-enter-active,
|
&-enter-active,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-user-list">
|
<section class="info-user-list">
|
||||||
<h3 class="user-header section-header">
|
<h3 class="user-header">
|
||||||
<img src="/images/icon-user.svg" alt="Users icon" />
|
<img src="/images/icon-user.svg" alt="Users icon" />
|
||||||
{{ $t('scenery.users') }}
|
{{ $t('scenery.users') }}
|
||||||
<span class="text--primary">{{ onlineScenery?.stationTrains?.length || 0 }}</span
|
<span class="text--primary">{{ onlineScenery?.stationTrains?.length || 0 }}</span
|
||||||
@@ -15,14 +15,37 @@
|
|||||||
<li
|
<li
|
||||||
v-for="{ train, status } in stationTrains"
|
v-for="{ train, status } in stationTrains"
|
||||||
class="badge user"
|
class="badge user"
|
||||||
tabindex="0"
|
|
||||||
:key="train.id"
|
:key="train.id"
|
||||||
:data-status="status"
|
:data-status="status"
|
||||||
@click.prevent="selectModalTrain(train, $event.currentTarget)"
|
|
||||||
@keydown.enter="selectModalTrain(train, $event.currentTarget)"
|
|
||||||
>
|
>
|
||||||
<span class="user_train">{{ train.trainNo }}</span>
|
<router-link
|
||||||
<span class="user_name">{{ train.driverName }}</span>
|
:to="train.driverRouteLocation"
|
||||||
|
data-tooltip-type="TrainInfoTooltip"
|
||||||
|
:data-tooltip-content="train.id"
|
||||||
|
>
|
||||||
|
<span class="user_train"> {{ train.trainNo }}</span>
|
||||||
|
<span class="user_name">
|
||||||
|
{{ train.driverName }}
|
||||||
|
<i
|
||||||
|
v-if="
|
||||||
|
train.timetableData != undefined &&
|
||||||
|
(train.lastSeen <= Date.now() - 60000 || !train.online)
|
||||||
|
"
|
||||||
|
class="fa-solid fa-user-slash"
|
||||||
|
style="color: lightcoral"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="$t('app.tooltip-driver-offline')"
|
||||||
|
></i>
|
||||||
|
|
||||||
|
<i
|
||||||
|
v-if="train.currentStationName.indexOf('.sc') != -1"
|
||||||
|
class="fa-solid fa-ban"
|
||||||
|
style="color: lightcoral"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="$t('app.tooltip-scenery-offline')"
|
||||||
|
></i>
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</section>
|
</section>
|
||||||
@@ -30,14 +53,13 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PropType, defineComponent } from 'vue';
|
import { PropType, defineComponent } from 'vue';
|
||||||
import modalTrainMixin from '../../../mixins/modalTrainMixin';
|
|
||||||
import routerMixin from '../../../mixins/routerMixin';
|
import routerMixin from '../../../mixins/routerMixin';
|
||||||
import { ActiveScenery, Station, StopStatus } from '../../../typings/common';
|
import { ActiveScenery, Station } from '../../../typings/common';
|
||||||
import { getTrainStopStatus } from '../utils';
|
import { getTrainStopStatus } from '../utils';
|
||||||
import { useMainStore } from '../../../store/mainStore';
|
import { useMainStore } from '../../../store/mainStore';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [routerMixin, modalTrainMixin],
|
mixins: [routerMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
onlineScenery: {
|
onlineScenery: {
|
||||||
@@ -65,11 +87,17 @@ export default defineComponent({
|
|||||||
const stop = train.timetableData?.followingStops.find(
|
const stop = train.timetableData?.followingStops.find(
|
||||||
(stop) =>
|
(stop) =>
|
||||||
stop.stopNameRAW.toLowerCase() == name.toLowerCase() ||
|
stop.stopNameRAW.toLowerCase() == name.toLowerCase() ||
|
||||||
this.station?.generalInfo?.checkpoints.includes(stop.stopNameRAW)
|
this.station?.generalInfo?.checkpoints.includes(stop.stopNameRAW) ||
|
||||||
|
this.onlineScenery?.missingCheckpoints.includes(stop.stopNameRAW)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sceneryName =
|
||||||
|
train.currentStationName.indexOf('.sc') != -1
|
||||||
|
? train.currentStationName.split(' ').slice(0, -1).join(' ')
|
||||||
|
: train.currentStationName;
|
||||||
|
|
||||||
const status = stop
|
const status = stop
|
||||||
? getTrainStopStatus(stop, train.currentStationName, this.onlineScenery!.name)
|
? getTrainStopStatus(stop, sceneryName, this.onlineScenery!.name)
|
||||||
: 'no-timetable';
|
: 'no-timetable';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -83,6 +111,8 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use '../../../styles/badge';
|
||||||
|
|
||||||
$no-timetable: #aaa;
|
$no-timetable: #aaa;
|
||||||
$departed: springgreen;
|
$departed: springgreen;
|
||||||
$stopped: #ffa600;
|
$stopped: #ffa600;
|
||||||
@@ -90,6 +120,17 @@ $online: gold;
|
|||||||
$terminated: salmon;
|
$terminated: salmon;
|
||||||
$disconnected: slategray;
|
$disconnected: slategray;
|
||||||
|
|
||||||
|
h3.user-header {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding: 0.3em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
.info-user-list {
|
.info-user-list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -99,38 +140,27 @@ ul {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user {
|
.user {
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&_train {
|
|
||||||
color: black;
|
|
||||||
background-color: $no-timetable;
|
|
||||||
|
|
||||||
transition: background-color 200ms;
|
|
||||||
-ms-transition: background-color 200ms;
|
|
||||||
-webkit-transition: background-color 200ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-status='no-timetable'] .user_train {
|
&[data-status='no-timetable'] .user_train {
|
||||||
background-color: $no-timetable;
|
background-color: $no-timetable;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-status='departed'] > &_train {
|
&[data-status='departed'] .user_train {
|
||||||
background-color: $departed;
|
background-color: $departed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-status='stopped'] > &_train {
|
&[data-status='stopped'] .user_train {
|
||||||
background-color: $stopped;
|
background-color: $stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-status='online'] > &_train {
|
&[data-status='online'] .user_train {
|
||||||
background-color: $online;
|
background-color: $online;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-status='terminated'] > &_train {
|
&[data-status='terminated'] .user_train {
|
||||||
background-color: $terminated;
|
background-color: $terminated;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-status='disconnected'] > &_train {
|
&[data-status='disconnected'] .user_train {
|
||||||
background-color: $disconnected;
|
background-color: $disconnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +169,16 @@ ul {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user_train {
|
||||||
|
color: black;
|
||||||
|
background-color: $no-timetable;
|
||||||
|
|
||||||
|
transition: background-color 200ms;
|
||||||
|
-ms-transition: background-color 200ms;
|
||||||
|
-webkit-transition: background-color 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
.users-anim {
|
.users-anim {
|
||||||
&-move,
|
&-move,
|
||||||
&-enter-active,
|
&-enter-active,
|
||||||
|
|||||||
@@ -13,26 +13,58 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="header_links" v-if="station">
|
<span class="header_links" v-if="station && onlineScenery">
|
||||||
<a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
|
<a
|
||||||
|
:href="generatorHref"
|
||||||
|
target="_blank"
|
||||||
|
data-tooltip-type="HtmlTooltip"
|
||||||
|
:data-tooltip-content="`<b>${$t('scenery.gnr-link')}</b>`"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-gnr.svg" alt="GeneraTOR app icon" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
:href="pragotronHref"
|
||||||
|
target="_blank"
|
||||||
|
data-tooltip-type="HtmlTooltip"
|
||||||
|
:data-tooltip-content="`<b>${$t('scenery.pragotron-link')}</b>`"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-pragotron.svg" alt="icon-pragotron" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
:href="tabliceZbiorczeHref"
|
||||||
|
target="_blank"
|
||||||
|
data-tooltip-type="HtmlTooltip"
|
||||||
|
:data-tooltip-content="`<b>${$t('scenery.tablice-link')}</b>`"
|
||||||
|
>
|
||||||
<img src="/images/icon-tablice.ico" alt="icon-tablice" />
|
<img src="/images/icon-tablice.ico" alt="icon-tablice" />
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="timetable-checkpoints" v-if="station?.generalInfo?.checkpoints">
|
<div class="timetable-checkpoints" v-if="station?.generalInfo?.checkpoints">
|
||||||
<span v-for="(cp, i) in station.generalInfo.checkpoints" :key="i">
|
<template v-for="(ch, i) in station.generalInfo.checkpoints" :key="i">
|
||||||
{{ (i > 0 && '•') || '' }}
|
<template v-if="i > 0">•</template>
|
||||||
|
<router-link
|
||||||
<button
|
class="checkpoint-item"
|
||||||
:key="cp"
|
:class="{ current: chosenCheckpoint === ch }"
|
||||||
class="checkpoint_item"
|
:to="`/scenery?station=${station.name}&checkpoint=${ch}`"
|
||||||
:class="{ current: chosenCheckpoint === cp }"
|
>{{ ch }}</router-link
|
||||||
@click="setCheckpoint(cp)"
|
|
||||||
>
|
>
|
||||||
{{ cp }}
|
</template>
|
||||||
</button>
|
</div>
|
||||||
</span>
|
|
||||||
|
<div class="timetable-checkpoints" v-else-if="onlineScenery">
|
||||||
|
<template v-for="(ch, i) in onlineScenery.missingCheckpoints" :key="i">
|
||||||
|
<template v-if="i > 0">•</template>
|
||||||
|
<router-link
|
||||||
|
class="checkpoint-item"
|
||||||
|
:class="{ current: chosenCheckpoint === ch }"
|
||||||
|
:to="`/scenery?station=${onlineScenery.name}&checkpoint=${ch}`"
|
||||||
|
>{{ ch }}</router-link
|
||||||
|
>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,34 +94,76 @@
|
|||||||
{{ $t('scenery.no-timetables') }}
|
{{ $t('scenery.no-timetables') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<router-link
|
||||||
class="timetable-item"
|
class="timetable-item"
|
||||||
v-else
|
v-else
|
||||||
v-for="(row, i) in sceneryTimetables"
|
v-for="(row, i) in sceneryTimetables"
|
||||||
:key="row.train.id + i"
|
:key="row.train.id + i"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click.prevent.stop="selectModalTrain(row.train, $event.currentTarget)"
|
:to="row.train.driverRouteLocation"
|
||||||
@keydown.enter.prevent="selectModalTrain(row.train, $event.currentTarget)"
|
|
||||||
>
|
>
|
||||||
<span class="timetable-general">
|
<span class="timetable-general">
|
||||||
<span class="general-info">
|
<span class="general-info">
|
||||||
<span>
|
<div class="info-train">
|
||||||
<b
|
<!-- Cargo warnings & details badges -->
|
||||||
|
<span
|
||||||
|
class="train-badge twr"
|
||||||
|
v-if="row.train.timetableData!.twr"
|
||||||
data-tooltip-type="BaseTooltip"
|
data-tooltip-type="BaseTooltip"
|
||||||
:data-tooltip-content="getCategoryExplanation(row.train.timetableData!.category)"
|
:data-tooltip-content="$t('warnings.TWR')"
|
||||||
class="text--primary tooltip-help"
|
>
|
||||||
|
TWR
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="train-badge tn"
|
||||||
|
v-if="row.train.timetableData!.hasDangerousCargo"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="$t('warnings.TN')"
|
||||||
|
>
|
||||||
|
TN
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="train-badge pn"
|
||||||
|
v-if="row.train.timetableData!.hasExtraDeliveries"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="$t('warnings.PN')"
|
||||||
|
>
|
||||||
|
PN
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Train info -->
|
||||||
|
<span
|
||||||
|
data-tooltip-type="TrainInfoTooltip"
|
||||||
|
:data-tooltip-content="row.train.id"
|
||||||
|
class="tooltip-help"
|
||||||
|
>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ row.train.timetableData!.category }}
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<b> {{ row.train.trainNo }}</b>
|
||||||
|
•
|
||||||
|
{{ row.train.driverName }}
|
||||||
|
|
||||||
|
<i
|
||||||
|
class="fa-solid fa-user-slash"
|
||||||
|
style="color: salmon"
|
||||||
|
v-if="!row.train.online && row.train.lastSeen <= Date.now() - 60000"
|
||||||
|
></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Train stop comments -->
|
||||||
|
<span
|
||||||
|
v-if="row.checkpointStop.comments"
|
||||||
|
class="stop-comments-icon"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="row.checkpointStop.comments"
|
||||||
>
|
>
|
||||||
{{ row.train.timetableData!.category }}
|
|
||||||
</b>
|
|
||||||
<b> {{ row.train.trainNo }}</b>
|
|
||||||
<span v-if="row.checkpointStop.comments" :title="row.checkpointStop.comments">
|
|
||||||
<img src="/images/icon-warning.svg" />
|
<img src="/images/icon-warning.svg" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
•
|
|
||||||
<span>
|
|
||||||
{{ row.train.driverName }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="info-route">
|
<div class="info-route">
|
||||||
<strong>{{ row.train.timetableData!.route.replace('|', ' - ') }}</strong>
|
<strong>{{ row.train.timetableData!.route.replace('|', ' - ') }}</strong>
|
||||||
@@ -127,7 +201,7 @@
|
|||||||
|
|
||||||
<span class="schedule-stop">
|
<span class="schedule-stop">
|
||||||
<span class="stop-connection">
|
<span class="stop-connection">
|
||||||
{{ row.arrivingLine }}
|
{{ row.currentElement.arrivalRouteExt }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="stop-time">
|
<span class="stop-time">
|
||||||
@@ -136,7 +210,7 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="stop-connection">
|
<span class="stop-connection">
|
||||||
{{ row.departureLine }}
|
{{ row.currentElement.departureRouteExt }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -165,7 +239,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</router-link>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -178,21 +252,20 @@ import { useRoute } from 'vue-router';
|
|||||||
import Loading from '../Global/Loading.vue';
|
import Loading from '../Global/Loading.vue';
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
import dateMixin from '../../mixins/dateMixin';
|
||||||
import routerMixin from '../../mixins/routerMixin';
|
import routerMixin from '../../mixins/routerMixin';
|
||||||
import { useMainStore } from '../../store/mainStore';
|
|
||||||
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
|
||||||
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
|
|
||||||
import { useApiStore } from '../../store/apiStore';
|
|
||||||
import { ActiveScenery, Station } from '../../typings/common';
|
|
||||||
import { SceneryTimetableRow } from './typings';
|
|
||||||
import { getTrainStopStatus, stopStatusPriority } from './utils';
|
|
||||||
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
|
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
|
||||||
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
|
||||||
|
import { SceneryTimetableRow } from './typings';
|
||||||
|
import { ActiveScenery, Station, TooltipTrainInfo, Train } from '../../typings/common';
|
||||||
|
import { getTrainStopStatus, stopStatusPriority } from './utils';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'SceneryTimetable',
|
name: 'SceneryTimetable',
|
||||||
|
|
||||||
components: { Loading, ScheduledTrainStatus },
|
components: { Loading, ScheduledTrainStatus },
|
||||||
|
|
||||||
mixins: [dateMixin, routerMixin, modalTrainMixin, trainCategoryMixin],
|
mixins: [dateMixin, routerMixin, trainCategoryMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
station: {
|
station: {
|
||||||
@@ -211,6 +284,12 @@ export default defineComponent({
|
|||||||
this.loadSelectedOption();
|
this.loadSelectedOption();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
currentURL() {
|
||||||
|
this.loadSelectedOption();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const currentURL = computed(() => `${location.origin}${route.fullPath}`);
|
const currentURL = computed(() => `${location.origin}${route.fullPath}`);
|
||||||
@@ -220,6 +299,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const chosenCheckpoint = ref(
|
const chosenCheckpoint = ref(
|
||||||
props.station?.generalInfo?.checkpoints[0] ??
|
props.station?.generalInfo?.checkpoints[0] ??
|
||||||
|
props.onlineScenery?.missingCheckpoints[0] ??
|
||||||
props.station?.name ??
|
props.station?.name ??
|
||||||
route.query['station']?.toString() ??
|
route.query['station']?.toString() ??
|
||||||
''
|
''
|
||||||
@@ -241,6 +321,17 @@ export default defineComponent({
|
|||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
pragotronHref() {
|
||||||
|
let url = `https://pragotron-td2.web.app/board?name=${this.station!.name}®ion=${this.mainStore.region.id}`;
|
||||||
|
if (this.chosenCheckpoint) url += `&checkpoint=${this.chosenCheckpoint}`;
|
||||||
|
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
|
||||||
|
generatorHref() {
|
||||||
|
return `https://generator-td2.web.app/?sceneryId=${this.onlineScenery!.name}|${this.onlineScenery!.region}`;
|
||||||
|
},
|
||||||
|
|
||||||
sceneryTimetables(): SceneryTimetableRow[] {
|
sceneryTimetables(): SceneryTimetableRow[] {
|
||||||
if (!this.onlineScenery) return [];
|
if (!this.onlineScenery) return [];
|
||||||
|
|
||||||
@@ -264,12 +355,9 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
checkpointStop: ct.checkpointStop,
|
checkpointStop: ct.checkpointStop,
|
||||||
train: ct.train,
|
train: ct.train,
|
||||||
prevDepartureLine: ct.previousSceneryElement?.departureRouteExt ?? null,
|
prevElement: ct.previousSceneryElement,
|
||||||
nextArrivalLine: ct.nextSceneryElement?.arrivalRouteExt ?? null,
|
nextElement: ct.nextSceneryElement,
|
||||||
departureLine: ct.timetablePathElement.departureRouteExt ?? null,
|
currentElement: ct.timetablePathElement,
|
||||||
arrivingLine: ct.timetablePathElement.arrivalRouteExt ?? null,
|
|
||||||
prevStationName: ct.previousSceneryElement?.stationName ?? null,
|
|
||||||
nextStationName: ct.nextSceneryElement?.stationName ?? null,
|
|
||||||
status: trainStopStatus
|
status: trainStopStatus
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -290,9 +378,30 @@ export default defineComponent({
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
loadSelectedOption() {
|
loadSelectedOption() {
|
||||||
if (!this.station) return;
|
const queryCheckpoint = this.$route.query['checkpoint']?.toString();
|
||||||
|
|
||||||
this.chosenCheckpoint = this.station.generalInfo?.checkpoints[0] ?? this.station.name;
|
let checkpointsListRef: string[] | null = null;
|
||||||
|
let sceneryName = '';
|
||||||
|
|
||||||
|
if (this.station && this.station.generalInfo) {
|
||||||
|
checkpointsListRef = this.station.generalInfo.checkpoints;
|
||||||
|
sceneryName = this.station.name;
|
||||||
|
} else if (this.onlineScenery) {
|
||||||
|
checkpointsListRef = this.onlineScenery.missingCheckpoints;
|
||||||
|
sceneryName = this.onlineScenery.name;
|
||||||
|
} else if (this.station) {
|
||||||
|
this.chosenCheckpoint = this.station.name;
|
||||||
|
sceneryName = this.station.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkpointsListRef) {
|
||||||
|
this.chosenCheckpoint =
|
||||||
|
checkpointsListRef.find(
|
||||||
|
(ch) => ch.toLocaleLowerCase() === queryCheckpoint?.toLocaleLowerCase()
|
||||||
|
) ??
|
||||||
|
checkpointsListRef[0] ??
|
||||||
|
sceneryName;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setCheckpoint(cp: string) {
|
setCheckpoint(cp: string) {
|
||||||
@@ -303,9 +412,9 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
@use '../../styles/responsive';
|
||||||
@import '../../styles/variables.scss';
|
@use '../../styles/animations';
|
||||||
@import '../../styles/animations.scss';
|
@use '../../styles/badge';
|
||||||
|
|
||||||
.scenery-timetable {
|
.scenery-timetable {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -340,7 +449,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
.header_links {
|
.header_links {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5em;
|
gap: 0.25em;
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +471,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
background: #353535;
|
background: #353535;
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
&.empty {
|
&.empty {
|
||||||
@@ -392,45 +500,63 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timetable-list {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetable-checkpoints {
|
.timetable-checkpoints {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
|
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
button.checkpoint_item {
|
.checkpoint-item {
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
display: inline;
|
display: inline;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkpoint_item.current {
|
&.current {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: $accentCol;
|
color: var(--clr-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timetable-list {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.general-info {
|
.general-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.info-number {
|
.info-train {
|
||||||
color: $accentCol;
|
display: flex;
|
||||||
}
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
.info-route {
|
.info-train > .train-badge {
|
||||||
width: 100%;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
.info-number {
|
||||||
width: 1.1em;
|
color: var(--clr-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-route {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-comments-icon > img {
|
||||||
|
width: 1.3em;
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.schedule {
|
.schedule {
|
||||||
@@ -455,7 +581,7 @@ export default defineComponent({
|
|||||||
align-self: center;
|
align-self: center;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
|
||||||
color: $accentCol;
|
color: var(--clr-primary);
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '\027F6';
|
content: '\027F6';
|
||||||
@@ -472,7 +598,7 @@ export default defineComponent({
|
|||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen {
|
@include responsive.smallScreen {
|
||||||
.timetable-item {
|
.timetable-item {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,36 +40,28 @@
|
|||||||
<span>
|
<span>
|
||||||
{{ $t('scenery.timetable-issued-date') }}
|
{{ $t('scenery.timetable-issued-date') }}
|
||||||
<b>
|
<b>
|
||||||
{{
|
{{ parseCreatedDate(timetableHistory, $i18n.locale) }}
|
||||||
localeDateTime(
|
|
||||||
timetableHistory.createdAt > timetableHistory.beginDate
|
|
||||||
? timetableHistory.beginDate
|
|
||||||
: timetableHistory.createdAt,
|
|
||||||
$i18n.locale
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</b></span
|
|
||||||
>
|
|
||||||
<span v-if="timetableHistory.authorName">
|
|
||||||
{{ $t('scenery.timetable-issued-by') }}
|
|
||||||
<b>
|
|
||||||
<router-link
|
|
||||||
:to="`/journal/timetables?search-dispatcher=${timetableHistory.authorName}`"
|
|
||||||
>
|
|
||||||
{{ timetableHistory.authorName }}
|
|
||||||
</router-link>
|
|
||||||
</b>
|
</b>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{{ $t('scenery.timetable-issued-for') }}
|
{{ $t('scenery.timetable-issued-for') }}
|
||||||
<b>
|
<router-link
|
||||||
<router-link
|
class="journal-link"
|
||||||
:to="`/journal/timetables?search-driver=${timetableHistory.driverName}`"
|
:to="`/journal/timetables?search-driver=${timetableHistory.driverName}`"
|
||||||
>
|
>
|
||||||
{{ timetableHistory.driverName }}
|
{{ timetableHistory.driverName }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</b>
|
</span>
|
||||||
|
|
||||||
|
<span v-if="timetableHistory.authorName">
|
||||||
|
{{ $t('scenery.timetable-issued-by') }}
|
||||||
|
<router-link
|
||||||
|
class="journal-link"
|
||||||
|
:to="`/journal/timetables?search-dispatcher=${timetableHistory.authorName}`"
|
||||||
|
>
|
||||||
|
{{ timetableHistory.authorName }}
|
||||||
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
@@ -106,7 +98,7 @@ import { useApiStore } from '../../store/apiStore';
|
|||||||
import routerMixin from '../../mixins/routerMixin';
|
import routerMixin from '../../mixins/routerMixin';
|
||||||
import { useMainStore } from '../../store/mainStore';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
|
||||||
const historyModeList = ['via', 'issuedFrom', 'terminatingAt'] as const;
|
const historyModeList = ['includesScenery', 'issuedFrom', 'via', 'terminatingAt'] as const;
|
||||||
type HistoryMode = (typeof historyModeList)[number];
|
type HistoryMode = (typeof historyModeList)[number];
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -123,7 +115,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
historyList: [] as API.TimetableHistory.Response,
|
historyList: [] as API.TimetableHistory.ResponseShort,
|
||||||
historyModeList,
|
historyModeList,
|
||||||
|
|
||||||
apiStore: useApiStore(),
|
apiStore: useApiStore(),
|
||||||
@@ -131,17 +123,19 @@ export default defineComponent({
|
|||||||
dataStatus: Status.Data.Loading,
|
dataStatus: Status.Data.Loading,
|
||||||
DataStatus: Status.Data,
|
DataStatus: Status.Data,
|
||||||
|
|
||||||
checkedHistoryMode: 'via' as HistoryMode
|
checkedHistoryMode: 'includesScenery' as HistoryMode
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
async activated() {
|
async activated() {
|
||||||
|
this.checkedHistoryMode = 'includesScenery';
|
||||||
this.fetchAPIData();
|
this.fetchAPIData();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async fetchAPIData() {
|
async fetchAPIData() {
|
||||||
const stationName = this.$route.query['station'];
|
const stationName = this.$route.query['station'];
|
||||||
|
this.dataStatus = Status.Data.Loading;
|
||||||
|
|
||||||
if (!stationName) {
|
if (!stationName) {
|
||||||
this.historyList = [];
|
this.historyList = [];
|
||||||
@@ -152,9 +146,10 @@ export default defineComponent({
|
|||||||
const requestFilters: Record<string, any> = {};
|
const requestFilters: Record<string, any> = {};
|
||||||
requestFilters[this.checkedHistoryMode] = stationName.toString();
|
requestFilters[this.checkedHistoryMode] = stationName.toString();
|
||||||
requestFilters.countLimit = 30;
|
requestFilters.countLimit = 30;
|
||||||
|
requestFilters['returnType'] = 'short';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response: API.TimetableHistory.Response = await (
|
const response: API.TimetableHistory.ResponseShort = await (
|
||||||
await this.apiStore.client!.get('api/getTimetables', {
|
await this.apiStore.client!.get('api/getTimetables', {
|
||||||
params: requestFilters
|
params: requestFilters
|
||||||
})
|
})
|
||||||
@@ -165,12 +160,12 @@ export default defineComponent({
|
|||||||
this.dataStatus = Status.Data.Loaded;
|
this.dataStatus = Status.Data.Loaded;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
this.dataStatus = Status.Data.Error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
checkHistoryMode(mode: HistoryMode) {
|
checkHistoryMode(mode: HistoryMode) {
|
||||||
this.checkedHistoryMode = mode;
|
this.checkedHistoryMode = mode;
|
||||||
this.dataStatus = Status.Data.Loading;
|
|
||||||
this.fetchAPIData();
|
this.fetchAPIData();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -181,6 +176,18 @@ export default defineComponent({
|
|||||||
[`search-${this.checkedHistoryMode}`]: this.station?.name || this.onlineScenery?.name
|
[`search-${this.checkedHistoryMode}`]: this.station?.name || this.onlineScenery?.name
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
parseCreatedDate(timetable: API.TimetableHistory.DataShort, locale: string) {
|
||||||
|
const createdDate =
|
||||||
|
timetable.createdAt > timetable.beginDate
|
||||||
|
? new Date(timetable.beginDate)
|
||||||
|
: new Date(timetable.createdAt);
|
||||||
|
|
||||||
|
return createdDate.toLocaleString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
|
||||||
|
timeStyle: 'short',
|
||||||
|
dateStyle: 'medium'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: { Loading }
|
components: { Loading }
|
||||||
@@ -188,8 +195,8 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
@use '../../styles/responsive';
|
||||||
@import '../../styles/sceneryViewTables.scss';
|
@use '../../styles/scenery-history-table';
|
||||||
|
|
||||||
.scenery-timetables-history {
|
.scenery-timetables-history {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -215,7 +222,15 @@ export default defineComponent({
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 0.35em;
|
padding: 0.35em;
|
||||||
min-width: 120px;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-link {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #eee;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--clr-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="general-status">
|
<div class="general-status">
|
||||||
<span
|
<router-link
|
||||||
|
v-if="computedScheduledTrain.stationNameHref"
|
||||||
|
:to="`/scenery?station=${computedScheduledTrain.stationNameHref}`"
|
||||||
:class="computedScheduledTrain.status"
|
:class="computedScheduledTrain.status"
|
||||||
:title="computedScheduledTrain.stopStatusDescription"
|
v-html="computedScheduledTrain.stopStatusIndicator"
|
||||||
>
|
>
|
||||||
{{ computedScheduledTrain.stopStatusIndicator }}
|
</router-link>
|
||||||
</span>
|
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
:class="computedScheduledTrain.status"
|
||||||
|
v-html="computedScheduledTrain.stopStatusIndicator"
|
||||||
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -24,57 +31,67 @@ export default defineComponent({
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
computedScheduledTrain() {
|
computedScheduledTrain() {
|
||||||
const { prevDepartureLine, prevStationName, nextArrivalLine, nextStationName, status } =
|
const { status, prevElement, currentElement, nextElement } = this.sceneryTimetableRow;
|
||||||
this.sceneryTimetableRow;
|
|
||||||
|
|
||||||
const prevDepartureIndicator = prevDepartureLine
|
let stopStatusIndicator = '';
|
||||||
? `(${prevDepartureLine}) ${prevStationName}`
|
let stationNameHref = '';
|
||||||
: '---';
|
|
||||||
const nextArrivalIndicator = nextArrivalLine
|
|
||||||
? `(${nextArrivalLine}) ${nextStationName}`
|
|
||||||
: '---';
|
|
||||||
|
|
||||||
let stopStatusDescription = '',
|
|
||||||
stopStatusIndicator = '';
|
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case StopStatus.ARRIVING:
|
case StopStatus.ARRIVING:
|
||||||
stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`;
|
if (prevElement) {
|
||||||
stopStatusDescription = this.$t('timetables.desc-arriving', {
|
stopStatusIndicator = this.$t('timetables.desc-arriving', {
|
||||||
prevStationName,
|
prevStationName: prevElement?.stationName ?? '',
|
||||||
prevDepartureLine
|
prevDepartureLine: prevElement?.departureRouteExt ?? ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stationNameHref = prevElement?.stationName ?? '';
|
||||||
|
} else {
|
||||||
|
stopStatusIndicator = this.$t('timetables.desc-beginning');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case StopStatus.ONLINE:
|
case StopStatus.ONLINE:
|
||||||
case StopStatus.STOPPED:
|
case StopStatus.STOPPED:
|
||||||
stopStatusIndicator = nextArrivalLine
|
stopStatusIndicator = nextElement?.arrivalRouteExt
|
||||||
? `${this.$t('timetables.to')}: ${nextArrivalIndicator}`
|
? this.$t(`timetables.desc-${status}`, {
|
||||||
: `${this.$t('timetables.desc-end')}`;
|
nextStationName: nextElement?.stationName,
|
||||||
stopStatusDescription = nextArrivalLine
|
nextArrivalLine: nextElement?.arrivalRouteExt
|
||||||
? this.$t(`timetables.desc-${status}`, { nextStationName, nextArrivalLine })
|
})
|
||||||
: '';
|
: this.$t(`timetables.desc-end`);
|
||||||
|
|
||||||
|
stationNameHref = nextElement?.stationName ?? '';
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case StopStatus.DEPARTED:
|
case StopStatus.DEPARTED:
|
||||||
stopStatusIndicator = `${this.$t('timetables.to')}: ${nextArrivalIndicator}`;
|
if (!nextElement?.stationName) {
|
||||||
stopStatusDescription = this.$t('timetables.desc-departed', {
|
stopStatusIndicator = this.$t('timetables.desc-departed-ends', {
|
||||||
nextStationName,
|
nextStationName: currentElement.stationName
|
||||||
nextArrivalLine
|
});
|
||||||
});
|
|
||||||
|
stationNameHref = nextElement?.stationName ?? '';
|
||||||
|
} else {
|
||||||
|
stopStatusIndicator = this.$t('timetables.desc-departed', {
|
||||||
|
nextStationName: nextElement?.stationName ?? currentElement.stationName,
|
||||||
|
nextArrivalLine: nextElement?.arrivalRouteExt
|
||||||
|
});
|
||||||
|
|
||||||
|
stationNameHref = nextElement?.stationName ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case StopStatus.DEPARTED_AWAY:
|
case StopStatus.DEPARTED_AWAY:
|
||||||
stopStatusIndicator = `${this.$t('timetables.to')}: ${nextArrivalIndicator}`;
|
stopStatusIndicator = this.$t('timetables.desc-departed-away', {
|
||||||
stopStatusDescription = this.$t('timetables.desc-departed-away', {
|
nextStationName: nextElement?.stationName,
|
||||||
nextStationName,
|
nextArrivalLine: nextElement?.arrivalRouteExt
|
||||||
nextArrivalLine
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stationNameHref = nextElement?.stationName ?? '';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case StopStatus.TERMINATED:
|
case StopStatus.TERMINATED:
|
||||||
stopStatusIndicator = `X ${this.$t('timetables.desc-terminated')}`;
|
stopStatusIndicator = this.$t('timetables.desc-terminated');
|
||||||
stopStatusDescription = this.$t('timetables.desc-terminated');
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -82,10 +99,18 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...this.sceneryTimetableRow,
|
...this.sceneryTimetableRow,
|
||||||
stopStatusDescription,
|
stationNameHref,
|
||||||
stopStatusIndicator
|
stopStatusIndicator
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
navigateToScenery(sceneryName?: string) {
|
||||||
|
if (!sceneryName) return;
|
||||||
|
|
||||||
|
this.$router.push(`/scenery?station=${sceneryName}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -94,32 +119,28 @@ export default defineComponent({
|
|||||||
.general-status {
|
.general-status {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
|
|
||||||
span.arriving {
|
& > .arriving {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.departed {
|
& > .departed {
|
||||||
color: lime;
|
color: lime;
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
&-away {
|
&-away {
|
||||||
font-weight: bold;
|
|
||||||
color: #5ecc5e;
|
color: #5ecc5e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span.stopped {
|
& > .stopped {
|
||||||
color: #ffa600;
|
color: #ffa600;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
span.online {
|
& > .online {
|
||||||
color: gold;
|
color: gold;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.terminated {
|
& > .terminated {
|
||||||
color: salmon;
|
color: salmon;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { StopStatus, Train, TrainStop } from '../../typings/common';
|
import { StopStatus, TimetablePathElement, Train, TrainStop } from '../../typings/common';
|
||||||
|
|
||||||
export interface SceneryTimetableRow {
|
export interface SceneryTimetableRow {
|
||||||
checkpointStop: TrainStop;
|
checkpointStop: TrainStop;
|
||||||
train: Train;
|
train: Train;
|
||||||
prevDepartureLine: string | null;
|
prevElement: TimetablePathElement | null;
|
||||||
nextArrivalLine: string | null;
|
nextElement: TimetablePathElement | null;
|
||||||
departureLine: string | null;
|
currentElement: TimetablePathElement;
|
||||||
arrivingLine: string | null;
|
|
||||||
prevStationName: string | null;
|
|
||||||
nextStationName: string | null;
|
|
||||||
status: StopStatus;
|
status: StopStatus;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,8 +66,6 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/variables.scss';
|
|
||||||
|
|
||||||
label {
|
label {
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@@ -98,7 +96,7 @@ label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible + span {
|
&:focus-visible + span {
|
||||||
outline: 1px solid $accentCol;
|
outline: 1px solid var(--clr-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,30 +6,11 @@
|
|||||||
<p>[F] {{ $t('options.filters') }}</p>
|
<p>[F] {{ $t('options.filters') }}</p>
|
||||||
<span class="active-indicator" v-if="changedFilters.length != 0"></span>
|
<span class="active-indicator" v-if="changedFilters.length != 0"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<label for="scenery-search">
|
|
||||||
<input
|
|
||||||
id="scenery-search"
|
|
||||||
list="sceneries"
|
|
||||||
:placeholder="$t('sceneries.scenery-search')"
|
|
||||||
@focus="preventKeyDown = true"
|
|
||||||
@blur="preventKeyDown = false"
|
|
||||||
v-model="chosenSearchScenery"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<datalist id="sceneries">
|
|
||||||
<option
|
|
||||||
v-for="scenery in sortedStationList"
|
|
||||||
:key="scenery.name"
|
|
||||||
:value="scenery.name"
|
|
||||||
></option>
|
|
||||||
</datalist>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition name="card-anim">
|
<transition name="card-anim">
|
||||||
<div class="card" v-if="isVisible" tabindex="0" ref="cardRef" @keydown.r="resetFilters">
|
<div class="card" v-if="isVisible" ref="cardRef" @keydown.r="resetFilters">
|
||||||
<div class="card_content" @scroll="onScroll" ref="cardContentRef">
|
<div class="card_content" tabindex="0" @scroll="onScroll" ref="cardContentRef">
|
||||||
<div class="card_title flex">{{ $t('filters.title') }}</div>
|
<div class="card_title flex">{{ $t('filters.title') }}</div>
|
||||||
<p class="card_info" v-html="$t('filters.desc')"></p>
|
<p class="card_info" v-html="$t('filters.desc')"></p>
|
||||||
|
|
||||||
@@ -40,13 +21,77 @@
|
|||||||
<template v-else>{{ $t('filters.no-changed-filters') }}</template>
|
<template v-else>{{ $t('filters.no-changed-filters') }}</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="card_input-search">
|
||||||
|
<datalist id="sceneries">
|
||||||
|
<option
|
||||||
|
v-for="scenery in sortedStationList"
|
||||||
|
:key="scenery.name"
|
||||||
|
:value="scenery.name"
|
||||||
|
></option>
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<input
|
||||||
|
v-model="chosenSearchScenery"
|
||||||
|
id="scenery-search"
|
||||||
|
list="sceneries"
|
||||||
|
:placeholder="$t('filters.sceneries-placeholder')"
|
||||||
|
@change="handleSceneriesInput"
|
||||||
|
@focus="preventKeyDown = true"
|
||||||
|
@blur="preventKeyDown = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button class="btn--action" @click="handleSceneriesInput">
|
||||||
|
{{ $t('filters.search-button-title') }}
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card_input-search">
|
||||||
|
<input
|
||||||
|
v-model="filters['lines']"
|
||||||
|
id="line-numbers-search"
|
||||||
|
:placeholder="$t('filters.line-numbers-placeholder')"
|
||||||
|
@focus="preventKeyDown = true"
|
||||||
|
@blur="preventKeyDown = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button class="btn--action btn--image" @click="resetLineNumbersInput">
|
||||||
|
<img src="/images/icon-exit.svg" alt="reset line numbers search" />
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card_input-search">
|
||||||
|
<select id="author" name="authors" v-model="filters['authors']">
|
||||||
|
<option value="">{{ $t('filters.authors-placeholder') }}</option>
|
||||||
|
<option v-for="(author, i) in authorsOptions" :key="i" :value="author">
|
||||||
|
{{ author }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button class="btn--action btn--image" @click="resetAuthorsInput">
|
||||||
|
<img src="/images/icon-exit.svg" alt="reset authors search" />
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card_input-search">
|
||||||
|
<select id="projects" name="projects" v-model="filters['projects']">
|
||||||
|
<option value="">{{ $t('filters.projects-placeholder') }}</option>
|
||||||
|
<option v-for="(project, i) in projectsOptions" :key="i" :value="project">
|
||||||
|
{{ project }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button class="btn--action btn--image" @click="resetProjectsInput">
|
||||||
|
<img src="/images/icon-exit.svg" alt="reset projects search" />
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="card_options">
|
<section class="card_options">
|
||||||
<div
|
<div
|
||||||
class="option-section"
|
class="option-section"
|
||||||
v-for="(sectionFilters, sectionKey) in filtersSections"
|
v-for="(sectionFilters, sectionKey) in filtersSections"
|
||||||
:key="sectionKey"
|
:key="sectionKey"
|
||||||
>
|
>
|
||||||
<h3 class="text--primary">
|
<h3 class="section-header">
|
||||||
<span class="active-indicator" v-if="!areSectionFiltersDefault(sectionKey)"></span>
|
<span class="active-indicator" v-if="!areSectionFiltersDefault(sectionKey)"></span>
|
||||||
{{ $t(`filters.sections.${sectionKey}`) }}
|
{{ $t(`filters.sections.${sectionKey}`) }}
|
||||||
<button @click="resetSectionFilters(sectionKey)">RESET</button>
|
<button @click="resetSectionFilters(sectionKey)">RESET</button>
|
||||||
@@ -57,16 +102,15 @@
|
|||||||
<div class="section-filters">
|
<div class="section-filters">
|
||||||
<label
|
<label
|
||||||
v-for="filterKey in sectionFilters"
|
v-for="filterKey in sectionFilters"
|
||||||
@click="() => (filters[filterKey] = !filters[filterKey])"
|
|
||||||
@dblclick="setSingleSectionFilter(sectionKey, filterKey)"
|
@dblclick="setSingleSectionFilter(sectionKey, filterKey)"
|
||||||
:for="filterKey"
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
type="checkbox"
|
||||||
:checked="filters[filterKey]"
|
:checked="filters[filterKey]"
|
||||||
v-model="filters[filterKey]"
|
v-model="filters[filterKey]"
|
||||||
type="checkbox"
|
|
||||||
:class="sectionKey"
|
:class="sectionKey"
|
||||||
:name="filterKey"
|
:name="filterKey"
|
||||||
|
:id="filterKey"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{{ $t(`filters.${filterKey}`) }}
|
{{ $t(`filters.${filterKey}`) }}
|
||||||
@@ -77,7 +121,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card_timestamp">
|
<section class="card_timestamp">
|
||||||
<h3 class="section-header">{{ $t('filters.minimum-hours-title') }}</h3>
|
<h3 class="hours-section-header">{{ $t('filters.minimum-hours-title') }}</h3>
|
||||||
|
|
||||||
<span class="clock">
|
<span class="clock">
|
||||||
<button class="btn--action" @click="subHour">-</button>
|
<button class="btn--action" @click="subHour">-</button>
|
||||||
@@ -92,31 +136,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card_authors-search">
|
|
||||||
<h3 class="section-header">{{ $t('filters.authors-search') }}</h3>
|
|
||||||
|
|
||||||
<datalist id="authors" name="authors">
|
|
||||||
<option v-for="(author, i) in authorsHint" :key="i" :value="author"></option>
|
|
||||||
</datalist>
|
|
||||||
|
|
||||||
<form action="javascript:void(0);" @submit="handleAuthorsInput">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="author"
|
|
||||||
list="authors"
|
|
||||||
name="authors"
|
|
||||||
v-model="authors"
|
|
||||||
:placeholder="$t('filters.authors-placeholder')"
|
|
||||||
@focus="preventKeyDown = true"
|
|
||||||
@blur="preventKeyDown = false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button class="btn--action">{{ $t('filters.authors-button-title') }}</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card_sliders">
|
<section class="card_sliders">
|
||||||
<div class="slider" v-for="(slider, i) in initSliders" :key="i">
|
<div class="slider" v-for="(slider, i) in sliderStates" :key="i">
|
||||||
<input
|
<input
|
||||||
class="slider-input"
|
class="slider-input"
|
||||||
type="range"
|
type="range"
|
||||||
@@ -125,7 +146,7 @@
|
|||||||
:min="slider.minRange"
|
:min="slider.minRange"
|
||||||
:max="slider.maxRange"
|
:max="slider.maxRange"
|
||||||
:step="slider.step"
|
:step="slider.step"
|
||||||
v-model="filters[slider.id]"
|
v-model.number="filters[slider.id]"
|
||||||
/>
|
/>
|
||||||
<span class="slider-value">{{ filters[slider.id] }}</span>
|
<span class="slider-value">{{ filters[slider.id] }}</span>
|
||||||
<div class="slider-content">
|
<div class="slider-content">
|
||||||
@@ -173,7 +194,7 @@ import StorageManager from '../../managers/storageManager';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
filtersSections,
|
filtersSections,
|
||||||
initSliders,
|
sliderStates,
|
||||||
initFilters,
|
initFilters,
|
||||||
getChangedFilters
|
getChangedFilters
|
||||||
} from '../../managers/stationFilterManager';
|
} from '../../managers/stationFilterManager';
|
||||||
@@ -192,10 +213,9 @@ export default defineComponent({
|
|||||||
saveOptions: false,
|
saveOptions: false,
|
||||||
|
|
||||||
filtersSections,
|
filtersSections,
|
||||||
initSliders,
|
sliderStates,
|
||||||
|
|
||||||
minimumHours: 0,
|
minimumHours: 0,
|
||||||
authors: '',
|
|
||||||
|
|
||||||
currentRegion: { id: '', value: '' },
|
currentRegion: { id: '', value: '' },
|
||||||
|
|
||||||
@@ -250,13 +270,11 @@ export default defineComponent({
|
|||||||
.sort((s1, s2) => (s1.name > s2.name ? 1 : -1));
|
.sort((s1, s2) => (s1.name > s2.name ? 1 : -1));
|
||||||
},
|
},
|
||||||
|
|
||||||
currentOptionsActive() {
|
authorsOptions() {
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
authorsHint() {
|
|
||||||
return this.store.stationList
|
return this.store.stationList
|
||||||
.reduce((acc, station) => {
|
.reduce((acc, station) => {
|
||||||
|
if (station.generalInfo?.hidden === true) return acc;
|
||||||
|
|
||||||
station.generalInfo?.authors?.forEach((author) => {
|
station.generalInfo?.authors?.forEach((author) => {
|
||||||
if (author.trim() != '' && !acc.includes(author.toLocaleLowerCase()))
|
if (author.trim() != '' && !acc.includes(author.toLocaleLowerCase()))
|
||||||
acc.push(author.toLocaleLowerCase());
|
acc.push(author.toLocaleLowerCase());
|
||||||
@@ -265,24 +283,28 @@ export default defineComponent({
|
|||||||
return acc;
|
return acc;
|
||||||
}, [] as string[])
|
}, [] as string[])
|
||||||
.sort((a, b) => a.localeCompare(b));
|
.sort((a, b) => a.localeCompare(b));
|
||||||
|
},
|
||||||
|
|
||||||
|
projectsOptions() {
|
||||||
|
return this.store.stationList
|
||||||
|
.reduce((acc, station) => {
|
||||||
|
if (!station.generalInfo || !station.generalInfo.project || station.generalInfo.hidden)
|
||||||
|
return acc;
|
||||||
|
if (!acc.includes(station.generalInfo.project.trim()))
|
||||||
|
acc.push(station.generalInfo.project.trim());
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, [] as string[])
|
||||||
|
.sort((a, b) => a.localeCompare(b));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
chosenSearchScenery(value: string) {
|
|
||||||
const chosenStation = this.store.stationList.find(({ name }) => name == value);
|
|
||||||
|
|
||||||
if (chosenStation) {
|
|
||||||
this.$router.push(`/scenery?station=${chosenStation.name.replace(/ /g, '_')}`);
|
|
||||||
this.chosenSearchScenery = '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
isVisible(value: boolean) {
|
isVisible(value: boolean) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (value) {
|
if (value) {
|
||||||
(this.$refs['cardRef'] as HTMLDivElement).focus();
|
|
||||||
(this.$refs['cardContentRef'] as HTMLDivElement).scrollTop = this.scrollTop;
|
(this.$refs['cardContentRef'] as HTMLDivElement).scrollTop = this.scrollTop;
|
||||||
|
(this.$refs['cardContentRef'] as HTMLDivElement).focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -298,9 +320,28 @@ export default defineComponent({
|
|||||||
this.scrollTop = (e.target as HTMLElement).scrollTop;
|
this.scrollTop = (e.target as HTMLElement).scrollTop;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleAuthorsInput() {
|
resetAuthorsInput() {
|
||||||
this.filters['authors'] = this.authors;
|
this.filters['authors'] = '';
|
||||||
// if (this.saveOptions) StorageManager.setStringValue('authors', target.value);
|
},
|
||||||
|
|
||||||
|
resetProjectsInput() {
|
||||||
|
this.filters['projects'] = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
resetLineNumbersInput() {
|
||||||
|
this.filters['lines'] = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSceneriesInput() {
|
||||||
|
const chosenStation = this.store.stationList.find(
|
||||||
|
({ name }) => name == this.chosenSearchScenery
|
||||||
|
);
|
||||||
|
|
||||||
|
if (chosenStation) {
|
||||||
|
this.$router.push(`/scenery?station=${chosenStation.name.replace(/ /g, '_')}`);
|
||||||
|
this.chosenSearchScenery = '';
|
||||||
|
this.isVisible = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
subHour() {
|
subHour() {
|
||||||
@@ -329,9 +370,10 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
resetFilters() {
|
resetFilters() {
|
||||||
|
if (this.preventKeyDown) return;
|
||||||
|
|
||||||
// Reset local model values
|
// Reset local model values
|
||||||
this.minimumHours = 0;
|
this.minimumHours = 0;
|
||||||
this.authors = '';
|
|
||||||
|
|
||||||
// Reset global filters
|
// Reset global filters
|
||||||
Object.keys(this.filters).forEach((filterKey) => {
|
Object.keys(this.filters).forEach((filterKey) => {
|
||||||
@@ -353,7 +395,8 @@ export default defineComponent({
|
|||||||
|
|
||||||
setSingleSectionFilter(sectionKey: StationFilterSection, chosenKey: string) {
|
setSingleSectionFilter(sectionKey: StationFilterSection, chosenKey: string) {
|
||||||
filtersSections[sectionKey].forEach((filterKey) => {
|
filtersSections[sectionKey].forEach((filterKey) => {
|
||||||
if (filterKey != chosenKey) this.filters[filterKey] = initFilters[filterKey];
|
if (typeof this.filters[filterKey] === 'boolean')
|
||||||
|
this.filters[filterKey] = filterKey != chosenKey;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -369,12 +412,19 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive';
|
@use '../../styles/responsive';
|
||||||
@import '../../styles/card';
|
@use '../../styles/card';
|
||||||
@import '../../styles/animations';
|
@use '../../styles/animations';
|
||||||
@import '../../styles/variables';
|
|
||||||
|
|
||||||
h3.section-header {
|
h3.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
gap: 0.5em;
|
||||||
|
color: var(--clr-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3.hours-section-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
@@ -382,6 +432,7 @@ h3.section-header {
|
|||||||
.card {
|
.card {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 1fr auto;
|
grid-template-rows: 1fr auto;
|
||||||
|
padding: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card_info {
|
.card_info {
|
||||||
@@ -401,11 +452,6 @@ h3.section-header {
|
|||||||
.card_controls {
|
.card_controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
|
|
||||||
input {
|
|
||||||
border-radius: 0.5em 0.5em 0 0;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card_content {
|
.card_content {
|
||||||
@@ -421,7 +467,7 @@ h3.section-header {
|
|||||||
.card_title {
|
.card_title {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: $accentCol;
|
color: var(--clr-primary);
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -442,7 +488,7 @@ h3.section-header {
|
|||||||
span {
|
span {
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: $accentCol;
|
color: var(--clr-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@@ -451,23 +497,20 @@ h3.section-header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card_authors-search {
|
.card_input-search {
|
||||||
margin: 1em 0;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
form {
|
button {
|
||||||
display: flex;
|
height: 100%;
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5em;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input,
|
||||||
width: 70%;
|
select {
|
||||||
max-width: 400px;
|
width: 100%;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
outline: 1px solid white;
|
border: 1px solid #aaa;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,7 +555,7 @@ h3.section-header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible + span {
|
&:focus-visible + span {
|
||||||
outline: 1px solid $accentCol;
|
outline: 1px solid var(--clr-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,12 +582,6 @@ h3.section-header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.option-section h3 {
|
.option-section h3 {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0.25em;
|
|
||||||
|
|
||||||
gap: 0.5em;
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 0.15em;
|
padding: 0.15em;
|
||||||
color: coral;
|
color: coral;
|
||||||
@@ -552,15 +589,17 @@ h3.section-header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 50px 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
|
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
|
||||||
&-value {
|
&-value {
|
||||||
color: $accentCol;
|
color: var(--clr-primary);
|
||||||
padding: 0.1em 0.2em;
|
padding: 0.1em 0.2em;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-input {
|
&-input {
|
||||||
@@ -587,13 +626,14 @@ h3.section-header {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
background: white;
|
background: white;
|
||||||
border: 4px solid $accentCol;
|
border: 3px solid var(--clr-primary);
|
||||||
|
background-color: #333;
|
||||||
|
|
||||||
@include smallScreen() {
|
@include responsive.smallScreen {
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
margin-top: -5px;
|
margin-top: -5px;
|
||||||
border: 3px solid $accentCol;
|
border: 3px solid var(--clr-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,14 +644,14 @@ h3.section-header {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
background: white;
|
background: white;
|
||||||
border: 4px solid $accentCol;
|
border: 4px solid var(--clr-primary);
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
@include smallScreen() {
|
@include responsive.smallScreen {
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
border: 3px solid $accentCol;
|
border: 3px solid var(--clr-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,18 +681,23 @@ h3.section-header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen {
|
@include responsive.smallScreen {
|
||||||
.card_controls > button.card-button > p {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
&-input {
|
&-input {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card_controls > button > p {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||