Compare commits

...

68 Commits

Author SHA1 Message Date
Spythere aecbcf62df Aktualizacja odnośnika do changelogu 2022-10-02 00:54:05 +02:00
Spythere 2a817365a6 Tłumaczenie statystyk maszynistów 2022-10-02 00:40:10 +02:00
Spythere ecf3a00cab Statystyki maszynistów 2022-10-01 15:55:10 +02:00
Spythere beb2f3c0d4 Tłumaczenie 2022-10-01 13:16:40 +02:00
Spythere a65b09981b Poprawki responsywności 2022-09-30 14:56:49 +02:00
Spythere 4ec544e8a9 Dodano informację o timeoucie SWDRa 2022-09-30 00:00:36 +02:00
Spythere 7e108c5183 Bump wersji 2022-09-29 19:41:55 +02:00
Spythere 72361b157e Tłumaczenie PL 2022-09-29 19:41:26 +02:00
Spythere 1cc4d76e4d Poprawki filtrów 2022-09-29 19:40:15 +02:00
Spythere 846d4d0547 Filtry scenerii 2022-09-29 19:27:54 +02:00
Spythere 751cadd218 Poprawki stylistyczne 2022-09-28 16:36:26 +02:00
Spythere 3b44adff44 Poprawki responsywności 2022-09-27 19:36:34 +02:00
Spythere 29a02dd98f Poprawki responsywności; dodano wyszukiwanie scenerii 2022-09-27 18:58:46 +02:00
Spythere c5e68c4d03 Bump wersji 2022-09-27 14:52:47 +02:00
Spythere 95f7c2a4d9 Poprawki 2022-09-27 14:52:24 +02:00
Spythere 84412822ff Zmiana hostingu API 2022-09-26 00:31:55 +02:00
Spythere 42bb056e66 Poprawki dostępności searchboxów 2022-09-25 23:30:37 +02:00
Spythere 053e9d2b6a Update package-lock 2022-09-25 19:44:56 +02:00
Spythere c729d75541 Poprawki dostępności (c.d.) 2022-09-23 23:01:09 +02:00
Spythere a9b72d0b7a Poprawki dostępności 2022-09-23 22:58:23 +02:00
Spythere 95a027f284 Filtrowanie po nicku autora RJ w dzienniku 2022-09-23 22:39:38 +02:00
Spythere dbba83b28b Dodano id pociągu jako parametr 2022-09-22 19:09:28 +02:00
Spythere 65abe550f5 Poprawki list dzienników 2022-09-22 17:16:10 +02:00
Spythere 531108c25a Wygląd filtrów 2022-09-22 15:08:22 +02:00
Spythere bcf750d451 Wywoływanie filtrów za pomocą klawisza F 2022-09-22 14:57:03 +02:00
Spythere 0a8bfe4c52 Poprawki; usunięto github workflows 2022-09-22 14:15:53 +02:00
Spythere 0f19bc767a Poprawki wyglądu; cleanup kodu 2022-09-22 13:59:19 +02:00
Spythere 8eb0266874 Merge branch 'development' 2022-09-15 12:38:57 +02:00
Spythere ae5b5ff965 Responsywność i ułożenie opcji filtrów 2022-09-15 12:38:36 +02:00
Spythere 3a0c4bc151 Aktualizacja 1.10.4
Aktualizacja Stacjownika do wersji 1.10.4
2022-09-11 14:06:59 +02:00
Spythere 4f5fcb3189 Bump wersji 2022-09-11 13:59:08 +02:00
Spythere 3a2978bbe3 Usprawniono działanie listy dziennika dyżurnych 2022-09-11 02:00:58 +02:00
Spythere a81cc4559b Poprawki w filtrach i ustawieniach dzienników 2022-09-10 22:49:56 +02:00
Spythere 065143c359 JournalTimetables: dodano resetowanie filtrów 2022-09-10 18:22:00 +02:00
Spythere 1661881127 Poprawki w stylach 2022-09-10 18:12:07 +02:00
Spythere 93aa889414 Cleanup kodu 2022-09-10 17:57:43 +02:00
Spythere 2a131ab1fb Poprawiono tłumaczenie 2022-09-10 15:14:36 +02:00
Spythere 387f42985a Poprawiono filtrowanie datą 2022-09-10 15:10:39 +02:00
Spythere 6c83ce90bf Dodano filtrowanie po dacie w opcjach 2022-09-09 00:23:18 +02:00
Spythere 3d519e874f Opcje filtrów: tłumaczenia 2022-09-08 23:24:58 +02:00
Spythere 99cdb3442a Opcje filtrów: animacja i poprawki 2022-09-08 23:15:54 +02:00
Spythere a6c0fe86c8 Poprawki filtrów 2022-09-08 12:47:30 +02:00
Spythere 828421efe0 Filtry aktywnych pociągów 2022-09-08 12:21:27 +02:00
Spythere 21bacb1c95 Filtry dzienników; poprawki stylistyczne 2022-09-07 20:37:58 +02:00
Spythere 0d9a3f4b4f Rozszerzone opcje filtrów dzienników 2022-09-06 12:44:18 +02:00
Spythere 76b8534d63 Poprawki responsywności selectboxów 2022-09-06 00:26:49 +02:00
Spythere 0821fd708e Stylistyka informacji o składzie 2022-09-05 23:44:36 +02:00
Spythere b0a9939446 Cleanup kodu; poprawki funkcjonalności 2022-09-05 23:32:27 +02:00
Spythere 2a64b8f10d Dodatkowe informacje i poprawki wyglądu dziennika RJ 2022-09-04 17:12:44 +02:00
Spythere dc1c457ea4 Fix: wykrywanie scrolla dzienników 2022-09-04 16:46:44 +02:00
Spythere 1f95bc5230 Tłumaczenie i poprawki do wersji 1.10.3 2022-09-04 01:27:12 +02:00
Spythere 5a06920e5b Dodano tłumaczenie; poprawki 2022-09-04 01:25:27 +02:00
Spythere ee0d9e7ed4 Wersja 1.10.3
Wersja 1.10.3
2022-09-04 01:14:24 +02:00
Spythere 30ad3ad4f2 Bump wersji 2022-09-04 01:12:04 +02:00
Spythere c2bd5a8a1b Poprawiono mobilny scroll bar 2022-09-04 01:10:56 +02:00
Spythere 7101d0972d Przywrócono ikonę pociągu mobilnego widoku aktywnych RJ 2022-09-04 01:06:30 +02:00
Spythere 82bbfcdf70 Dokończenie widoku dziennika RJ 2022-09-04 01:04:04 +02:00
Spythere b90ac6c09e Zmiany w wyglądzie i funkcjonalnościach dziennika RJ 2022-09-03 00:11:42 +02:00
Spythere 76d0ff88f1 Zmiany w designie dziennika rozkładów jazdy 2022-09-01 01:56:16 +02:00
Spythere 951afcedeb Bump wersji 2022-08-29 19:12:56 +02:00
Spythere 96de3f0dcc Scroll lock przy otwartym modalu 2022-08-29 19:12:19 +02:00
Spythere 03950eef66 Bump wersji 2022-08-27 20:19:03 +02:00
Spythere 6dd8cb2dad Cleanup c.d. 2022-08-27 14:05:35 +02:00
Spythere aae51d4139 Hotfix 2022-08-27 14:04:02 +02:00
Spythere 9994a541b1 Cleanup 2022-08-27 14:02:42 +02:00
Spythere bc3a603ba2 Poprawiono sortowanie stacji 2022-08-27 13:44:04 +02:00
Spythere 7857377cab Merge branch 'development' 2022-08-09 00:01:40 +02:00
Spythere 0034f43be4 Fix: zła ikonka przy nieznanej scenerii 2022-08-08 23:59:55 +02:00
69 changed files with 5394 additions and 5200 deletions
@@ -1,20 +0,0 @@
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on merge
'on':
push:
branches:
- master
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci && npm run build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
channelId: live
projectId: stacjownik-td2
@@ -1,14 +0,0 @@
name: Deploy to Firebase Hosting on PR
'on': pull_request
jobs:
build_and_preview:
if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci && npm run build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
projectId: stacjownik-td2
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.10.2-alpha", "version": "1.10.4",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "stacjownik", "name": "stacjownik",
"version": "1.10.2-alpha", "version": "1.10.4",
"dependencies": { "dependencies": {
"core-js": "^3.12.1", "core-js": "^3.12.1",
"dotenv": "^8.6.0", "dotenv": "^8.6.0",
+2 -1
View File
@@ -1,10 +1,11 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.10.0", "version": "1.10.6",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"deploy": "yarn build && firebase deploy --only hosting",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
+1 -157
View File
@@ -45,7 +45,7 @@
font-size: 1rem; font-size: 1rem;
@include smallScreen() { @include smallScreen() {
font-size: calc(0.4rem + 1.4vw); font-size: calc(0.55rem + 1vw);
} }
} }
@@ -81,162 +81,6 @@
border-radius: 0 0 1em 1em; border-radius: 0 0 1em 1em;
} }
// Error icon
.wip-alert {
padding: 0 0.5em;
text-align: center;
}
.icon-error {
width: 13em;
margin: 0.5em 0;
}
// HEADER
.app_header {
display: flex;
justify-content: center;
position: relative;
background-color: $primaryCol;
}
.header {
&_body {
max-width: 21em;
}
&_container {
display: flex;
justify-content: center;
position: relative;
width: 1350px;
padding: 0.5em 0.3em 0 0.3em;
border-radius: 0 0 1em 1em;
}
&_brand {
img {
width: 100%;
}
}
&_info {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
max-width: 100%;
font-size: 1.2em;
}
&_links {
display: flex;
justify-content: center;
border-radius: 0.7em;
font-size: 1.25em;
padding: 0.5em;
}
&_icons {
position: absolute;
right: 0;
top: 0;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
padding: 0.5em 0.5em;
@include smallScreen() {
right: auto;
left: 0.75em;
padding: 0;
align-items: center;
}
}
}
// ICONS
.icons {
position: relative;
&-top {
img {
width: 2.5em;
cursor: pointer;
}
margin-bottom: 0.5em;
}
&-bottom {
display: flex;
a {
margin-left: 0.6em;
user-select: none;
}
img {
width: 1.9em;
}
@include smallScreen() {
flex-direction: column;
a {
margin: 0.25em 0;
}
}
}
}
// COUNTER
.info_counter {
display: flex;
justify-content: center;
align-items: center;
span {
margin: 0 0.15em;
}
img {
width: 1.35em;
}
}
// REGION SELECTION
.info_region {
color: white;
font-weight: bold;
display: flex;
justify-content: flex-end;
.select-box_content button {
background-color: transparent;
font-weight: bold;
padding: 0.1em 0.5em;
color: paleturquoise;
}
.options {
font-size: 0.9em;
}
.arrow {
padding: 0;
}
}
// FOOTER // FOOTER
footer.app_footer { footer.app_footer {
max-width: 100%; max-width: 100%;
+28 -98
View File
@@ -1,72 +1,17 @@
<template> <template>
<div class="app_container"> <div class="app_container">
<UpdateModal />
<transition name="modal-anim"> <transition name="modal-anim">
<keep-alive> <keep-alive>
<TrainModal v-if="store.chosenModalTrainId" /> <TrainModal v-if="store.chosenModalTrainId" />
</keep-alive> </keep-alive>
</transition> </transition>
<header class="app_header"> <AppHeader :current-lang="currentLang" @change-lang="changeLang" />
<div class="header_container">
<div class="header_icons">
<span class="icons-top">
<img :src="getIcon('pl')" alt="icon-pl" @click="changeLang('en')" v-if="currentLang == 'pl'" />
<img :src="getIcon('en', 'jpg')" alt="icon-en" @click="changeLang('pl')" v-else />
</span>
<span class="icons-bottom">
<a href="https://www.paypal.com/paypalme/spythere" target="_blank">
<img :src="getIcon('dollar')" alt="icon paypal" />
</a>
<a href="https://discord.gg/x2mpNN3svk" target="_blank">
<img :src="getIcon('discord', 'png')" alt="icon discord" />
</a>
</span>
</div>
<div class="header_body">
<status-indicator />
<span class="header_brand">
<img :src="getImage('stacjownik-header-logo.svg')" alt="Stacjownik" />
</span>
<span class="header_info">
<Clock />
<div class="info_counter">
<img :src="getIcon('dispatcher')" alt="icon dispatcher" />
<span class="text--primary">{{ onlineDispatchers.length }}</span>
<span class="text--grayed"> / </span>
<span class="text--primary">{{ trainList.length }}</span>
<img :src="getIcon('train')" alt="icon train" />
</div>
<span class="info_region">
<SelectBox :itemList="computedRegions" :defaultItemIndex="0" @selected="changeRegion" />
</span>
</span>
<span class="header_links">
<router-link class="route" active-class="route-active" to="/" exact>
{{ $t('app.sceneries') }}
</router-link>
/
<router-link class="route" active-class="route-active" to="/trains">{{ $t('app.trains') }}</router-link>
/
<router-link class="route" active-class="route-active" to="/journal/timetables">
{{ $t('app.journal') }}
</router-link>
</span>
</div>
</div>
</header>
<main class="app_main"> <main class="app_main">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive> <keep-alive>
<component :is="Component" :key="$route.path" /> <component :is="Component" :key="$route.name" />
</keep-alive> </keep-alive>
</router-view> </router-view>
</main> </main>
@@ -82,28 +27,28 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, provide, ref } from 'vue'; import { computed, defineComponent, provide, ref, watch } from 'vue';
import Clock from './components/App/Clock.vue'; import Clock from './components/App/Clock.vue';
import packageInfo from '.././package.json'; import packageInfo from '.././package.json';
import options from './data/options.json';
import StatusIndicator from './components/App/StatusIndicator.vue'; import StatusIndicator from './components/App/StatusIndicator.vue';
import SelectBox from './components/Global/SelectBox.vue'; import SelectBox from './components/Global/SelectBox.vue';
import { useStore } from './store/store'; import { useStore } from './store/store';
import UpdateModal from './components/App/UpdateModal.vue';
import TrainModal from './components/Global/TrainModal.vue'; import TrainModal from './components/Global/TrainModal.vue';
import StorageManager from './scripts/managers/storageManager'; import StorageManager from './scripts/managers/storageManager';
import imageMixin from './mixins/imageMixin'; import imageMixin from './mixins/imageMixin';
import AppHeader from './components/App/AppHeader.vue';
import axios from 'axios';
export default defineComponent({ export default defineComponent({
components: { components: {
Clock, Clock,
StatusIndicator, StatusIndicator,
SelectBox, SelectBox,
UpdateModal,
TrainModal, TrainModal,
AppHeader,
}, },
mixins: [imageMixin], mixins: [imageMixin],
@@ -127,30 +72,8 @@ export default defineComponent({
}; };
}, },
computed: {
trainList() {
return this.store.trainList.filter((train) => train.online);
},
computedRegions() {
return this.options.regions.map((region) => {
const regionStationCount =
this.store.apiData.stations?.filter((station) => station.region == region.id && station.isOnline).length || 0;
const regionTrainCount =
this.store.apiData.trains?.filter((train) => train.region == region.id && train.online).length || 0;
return {
id: region.id,
value: `${region.value} <div class='text--grayed'>${regionStationCount} / ${regionTrainCount}</div>`,
selectedValue: region.value,
};
});
},
},
data: () => ({ data: () => ({
VERSION: packageInfo.version, VERSION: packageInfo.version,
options,
currentLang: 'pl', currentLang: 'pl',
releaseURL: '', releaseURL: '',
@@ -161,15 +84,22 @@ export default defineComponent({
}, },
async mounted() { async mounted() {
this.updateStorage();
this.setReleaseURL(); this.setReleaseURL();
watch(
() => this.store.blockScroll,
(value) => {
if (value) {
document.body.classList.add('no-scroll');
return;
}
document.body.classList.remove('no-scroll');
}
);
}, },
methods: { methods: {
changeRegion(region: { id: string; value: string }) {
this.store.changeRegion(region);
},
changeLang(lang: string) { changeLang(lang: string) {
this.$i18n.locale = lang; this.$i18n.locale = lang;
this.currentLang = lang; this.currentLang = lang;
@@ -177,18 +107,18 @@ export default defineComponent({
StorageManager.setStringValue('lang', lang); StorageManager.setStringValue('lang', lang);
}, },
setReleaseURL() { async setReleaseURL() {
const releaseURL = StorageManager.getStringValue('releaseURL'); try {
const releaseData = await (
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
).data;
this.releaseURL = releaseURL || ''; if (!releaseData) return;
},
updateStorage() { this.releaseURL = releaseData.html_url;
if (!StorageManager.isRegistered('unavailable-status')) { } catch (error) {
StorageManager.setBooleanValue('unavailable-status', true); console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
StorageManager.setBooleanValue('ending-status', true); return;
StorageManager.setBooleanValue('no-space-status', true);
StorageManager.setBooleanValue('afk-status', true);
} }
}, },
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" ?><svg enable-background="new 0 0 32 32" id="Glyph" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M27.414,24.586l-5.077-5.077C23.386,17.928,24,16.035,24,14c0-5.514-4.486-10-10-10S4,8.486,4,14 s4.486,10,10,10c2.035,0,3.928-0.614,5.509-1.663l5.077,5.077c0.78,0.781,2.048,0.781,2.828,0 C28.195,26.633,28.195,25.367,27.414,24.586z M7,14c0-3.86,3.14-7,7-7s7,3.14,7,7s-3.14,7-7,7S7,17.86,7,14z" id="XMLID_223_" fill="white" /></svg>

After

Width:  |  Height:  |  Size: 546 B

+266
View File
@@ -0,0 +1,266 @@
<template>
<header class="app_header">
<div class="header_container">
<div class="header_icons">
<span class="icons-top">
<img :src="getIcon('pl')" alt="icon-pl" @click="changeLang('en')" v-if="currentLang == 'pl'" />
<img :src="getIcon('en', 'jpg')" alt="icon-en" @click="changeLang('pl')" v-else />
</span>
<span class="icons-bottom">
<a href="https://www.paypal.com/paypalme/spythere" target="_blank">
<img :src="getIcon('dollar')" alt="icon paypal" />
</a>
<a href="https://discord.gg/x2mpNN3svk" target="_blank">
<img :src="getIcon('discord', 'png')" alt="icon discord" />
</a>
</span>
</div>
<div class="header_body">
<StatusIndicator />
<span class="header_brand">
<img :src="getImage('stacjownik-header-logo.svg')" alt="Stacjownik" />
</span>
<span class="header_info">
<Clock />
<div class="info_counter">
<img :src="getIcon('dispatcher')" alt="icon dispatcher" />
<span class="text--primary">{{ onlineDispatchersCount }}</span>
<span class="text--grayed"> / </span>
<span class="text--primary">{{ onlineTrainsCount }}</span>
<img :src="getIcon('train')" alt="icon train" />
</div>
<span class="info_region">
<SelectBox :itemList="computedRegions" :defaultItemIndex="0" @selected="changeRegion" />
</span>
</span>
<span class="header_links">
<router-link class="route" active-class="route-active" to="/" exact>
{{ $t('app.sceneries') }}
</router-link>
/
<router-link class="route" active-class="route-active" to="/trains">{{ $t('app.trains') }}</router-link>
/
<router-link class="route" active-class="route-active" to="/journal/timetables">
{{ $t('app.journal') }}
</router-link>
</span>
</div>
</div>
</header>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../../store/store';
import options from '../../data/options.json';
import imageMixin from '../../mixins/imageMixin';
import SelectBox from '../Global/SelectBox.vue';
import StatusIndicator from './StatusIndicator.vue';
import Clock from './Clock.vue';
export default defineComponent({
emits: ["changeLang"],
mixins: [imageMixin],
props: {
currentLang: {
type: String,
required: true,
},
},
setup() {
return {
store: useStore(),
};
},
methods: {
changeRegion(region: {
id: string;
value: string;
}) {
this.store.changeRegion(region);
},
changeLang(lang: string) {
this.$emit("changeLang", lang);
},
},
computed: {
onlineTrainsCount() {
return this.store.trainList.filter((train) => train.online).length;
},
onlineDispatchersCount() {
return this.store.stationList.filter((station) => station.onlineInfo && station.onlineInfo.region == this.store.region.id).length;
},
computedRegions() {
return options.regions.map((region) => {
const regionStationCount = this.store.apiData.stations?.filter((station) => station.region == region.id && station.isOnline).length || 0;
const regionTrainCount = this.store.apiData.trains?.filter((train) => train.region == region.id && train.online).length || 0;
return {
id: region.id,
value: `${region.value} <div class='text--grayed'>${regionStationCount} / ${regionTrainCount}</div>`,
selectedValue: region.value,
};
});
},
},
components: { SelectBox, StatusIndicator, Clock }
});
</script>
<style lang="scss" scoped>
@import '../../styles/variables.scss';
@import '../../styles/responsive.scss';
// HEADER
.app_header {
display: flex;
justify-content: center;
position: relative;
background-color: $primaryCol;
}
.header {
&_body {
max-width: 21em;
@include smallScreen {
max-width: 18em;
}
}
&_container {
display: flex;
justify-content: center;
position: relative;
width: 1350px;
padding: 0.5em 0.3em 0 0.3em;
border-radius: 0 0 1em 1em;
}
&_brand {
display: flex;
img {
width: 100%;
margin: 0 auto;
}
}
&_info {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
max-width: 100%;
font-size: 1.2em;
}
&_links {
display: flex;
justify-content: center;
border-radius: 0.7em;
font-size: 1.25em;
padding: 0.5em;
}
&_icons {
position: absolute;
right: 0;
top: 0;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
padding: 0.5em 0.5em;
@include smallScreen() {
right: auto;
left: 0.75em;
padding: 0;
align-items: center;
}
}
}
// ICONS
.icons {
position: relative;
&-top {
img {
width: 2.5em;
cursor: pointer;
}
margin-bottom: 0.5em;
}
&-bottom {
display: flex;
a {
margin-left: 0.6em;
user-select: none;
}
img {
width: 1.9em;
}
@include smallScreen() {
flex-direction: column;
a {
margin: 0.25em 0;
}
}
}
}
// COUNTER
.info_counter {
display: flex;
justify-content: center;
align-items: center;
span {
margin: 0 0.15em;
}
img {
width: 1.35em;
}
}
// REGION SELECTION
.info_region {
color: white;
font-weight: bold;
display: flex;
justify-content: flex-end;
.select-box_content button {
background-color: transparent;
font-weight: bold;
padding: 0.1em 0.5em;
color: paleturquoise;
}
.options {
font-size: 0.9em;
}
}
</style>
+1 -42
View File
@@ -1,5 +1,5 @@
<template> <template>
<button class="action-btn"> <button class="action-btn btn--filled">
<div class="button_content"> <div class="button_content">
<slot></slot> <slot></slot>
</div> </div>
@@ -16,47 +16,6 @@ export default defineComponent({});
@import "../../styles/variables"; @import "../../styles/variables";
@import "../../styles/responsive"; @import "../../styles/responsive";
.action-btn {
background: #333;
border: none;
color: #bdbdbd;
font-size: 1em;
font-weight: 500;
padding: 0.35em 0.65em;
cursor: pointer;
transition: all 0.3s;
&.outlined {
border: 1px solid white;
}
img {
width: 1.25em;
vertical-align: middle;
margin-right: 0.35em;
}
p {
font-size: 1em;
overflow: hidden;
}
&.open {
color: $accentCol;
border: none;
}
&:hover,
&:focus {
color: $accentCol;
background: #5c5c5c;
}
}
.button_content { .button_content {
display: flex; display: flex;
justify-content: center; justify-content: center;
+16 -10
View File
@@ -2,7 +2,6 @@
<div class="select-box"> <div class="select-box">
<div class="select-box_content"> <div class="select-box_content">
<button class="selected" @click="toggleBox"> <button class="selected" @click="toggleBox">
<span class="text--primary">{{ prefix }}</span>
<span>{{ computedSelectedItem.selectedValue || computedSelectedItem.value }}</span> <span>{{ computedSelectedItem.selectedValue || computedSelectedItem.value }}</span>
</button> </button>
@@ -131,13 +130,14 @@ export default defineComponent({
.select-box { .select-box {
position: relative; position: relative;
width: auto;
} }
.arrow { .arrow {
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 0; right: 0;
padding: 0.5em; padding: 0;
img { img {
vertical-align: middle; vertical-align: middle;
@@ -150,13 +150,17 @@ export default defineComponent({
} }
button.selected { button.selected {
background: #333; background-color: transparent;
color: white; color: paleturquoise;
font-size: 1em; font-size: 1em;
font-weight: bold;
padding: 0.1em 0.5em;
margin-right: 2em;
display: flex;
padding: 0.35em 0.5em;
margin-right: 1.4em;
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
@@ -167,7 +171,7 @@ button.selected {
text-align: left; text-align: left;
&:focus { &:focus {
background: #555; background-color: #262626;
} }
} }
@@ -188,8 +192,9 @@ ul.options {
height: auto; height: auto;
z-index: 100; z-index: 100;
width: 100%; width: 100%;
font-size: 0.9em;
} }
li.option { li.option {
@@ -203,6 +208,7 @@ li.option {
appearance: none; appearance: none;
border: none; border: none;
outline: none; outline: none;
background: none;
&:focus + span { &:focus + span {
color: $accentCol; color: $accentCol;
@@ -218,11 +224,11 @@ li.option {
position: relative; position: relative;
display: inline-block; display: inline-block;
background-color: hsla(0, 0%, 15%, 0.95); background-color: #262626f2;
&:hover, &:hover,
&:focus { &:focus {
background-color: hsla(0, 0%, 20%, 0.95); background-color: #333333f2;
} }
padding: 0.5em 0; padding: 0.5em 0;
-3
View File
@@ -144,9 +144,6 @@ export default defineComponent({
} }
@include smallScreen { @include smallScreen {
.train-modal {
font-size: 1.05em;
}
.modal_content { .modal_content {
max-height: 85vh; max-height: 85vh;
+2 -32
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="stats_container" v-click-outside="() => (cardVisible = false)"> <div class="stats_container" v-click-outside="() => (cardVisible = false)">
<button class="stats_button btn btn--option" @click="toggleCard"> <button class="stats_button" @click="toggleCard">
Statystyki dyżurnego {{ store.dispatcherStatsName }} Statystyki dyżurnego {{ store.dispatcherStatsName }}
</button> </button>
@@ -14,6 +14,7 @@
<div v-else> <div v-else>
<h3>STATYSTYKI WYSTAWIONYCH ROZKŁADÓW</h3> <h3>STATYSTYKI WYSTAWIONYCH ROZKŁADÓW</h3>
<div class="info-stats" v-if="store.dispatcherStatsData._count._all"> <div class="info-stats" v-if="store.dispatcherStatsData._count._all">
<span class="stat-badge"> <span class="stat-badge">
<span>LICZBA</span> <span>LICZBA</span>
@@ -162,42 +163,11 @@ h3 {
text-align: center; text-align: center;
} }
.info-stats {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin-top: 1em;
}
.last-timetables { .last-timetables {
overflow-y: auto; overflow-y: auto;
} }
.stat-badge {
margin-right: 0.5em;
padding-bottom: 1em;
span {
padding: 0.25em 0.3em;
}
span:first-child {
background-color: #4d4d4d;
}
span:last-child {
background-color: $accentCol;
color: black;
font-weight: bold;
}
}
@include smallScreen() {
.stats_card {
text-align: center;
left: 50%;
transform: translateX(-50%);
border-radius: 0 0 1em 1em;
}
}
</style> </style>
+31 -78
View File
@@ -1,59 +1,47 @@
<template> <template>
<div class="card-dimmer" @click="closeCard"></div> <div class="journal-stats" v-if="store.driverStatsData?._sum.routeDistance != null">
<h1>
<div class="stats-card card">
<div>
<h2 class="card-title">
STATYSTYKI MASZYNISTY <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span> STATYSTYKI MASZYNISTY <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
</h2> </h1>
<div class="loading" v-if="!store.driverStatsData">Ładowanie...</div> <div class="info-stats">
<div v-else>
<div class="info-stats" v-if="store.driverStatsData._sum.routeDistance != null">
<span class="stat-badge"> <span class="stat-badge">
<span>PRZEBYTO</span> <span>{{ $t('journal.stats-timetables') }}</span>
<span>{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km</span> <span>{{ store.driverStatsData._count.fulfilled }} / {{ store.driverStatsData._count._all }}</span>
</span> </span>
<span class="stat-badge"> <span class="stat-badge">
<span>PORZUCONO</span> <span>{{ $t('journal.stats-longest-timetable') }}</span>
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-avg-timetable') }}</span>
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-distance') }}</span>
<span> <span>
{{ (store.driverStatsData._sum.routeDistance - store.driverStatsData._sum.currentDistance).toFixed(2) }}km {{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
</span> </span>
</span> </span>
<span class="stat-badge"> <span class="stat-badge">
<span>WYPEŁNIONO</span> <span>{{ $t('journal.stats-stations') }}</span>
<span>{{ store.driverStatsData._count.fulfilled }} RJ</span>
</span>
<span class="stat-badge">
<span>PORZUCONO</span>
<span>{{ store.driverStatsData._count._all - store.driverStatsData._count.fulfilled }} RJ</span>
</span>
<span class="stat-badge">
<span>ZATWIERDZONO</span>
<span>{{ store.driverStatsData._sum.confirmedStopsCount }} stacji</span>
</span>
<span class="stat-badge">
<span>PORZUCONO</span>
<span> <span>
{{ store.driverStatsData._sum.allStopsCount - store.driverStatsData._sum.confirmedStopsCount }} {{ store.driverStatsData._sum.confirmedStopsCount }} /
stacji {{ store.driverStatsData._sum.allStopsCount }}
</span> </span>
</span> </span>
</div> </div>
</div> </div>
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import axios from 'axios'; import axios from 'axios';
import { defineComponent } from 'vue'; import { computed, defineComponent, ref } from 'vue';
import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData'; import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData'; import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '../../scripts/utils/apiURLs'; import { URLs } from '../../scripts/utils/apiURLs';
@@ -66,6 +54,7 @@ export default defineComponent({
const store = useStore(); const store = useStore();
return { return {
store, store,
driverStatsName: computed(() => store.driverStatsName),
}; };
}, },
@@ -78,64 +67,28 @@ export default defineComponent({
}; };
}, },
activated() { watch: {
driverStatsName(value: string) {
this.fetchDispatcherStats(); this.fetchDispatcherStats();
}, },
},
methods: { methods: {
async fetchDispatcherStats() { async fetchDispatcherStats() {
this.store.driverStatsData = undefined; this.store.driverStatsData = undefined;
if (!this.store.driverStatsName) return;
const statsData: DriverStatsAPIData = await ( const statsData: DriverStatsAPIData = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`) await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`)
).data; ).data;
const recentTimetablesData: TimetableHistory[] = await (
await axios.get(`${URLs.stacjownikAPI}/api/getTimetables?driverName=${this.store.driverStatsName}`)
).data;
this.store.driverStatsData = statsData; this.store.driverStatsData = statsData;
this.lastTimetables = recentTimetablesData || [];
},
closeCard() {
this.$emit('closeCard');
}, },
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/JournalStats.scss';
.timetable-row {
display: grid;
grid-template-columns: 4fr 1fr 1fr 2fr 2fr;
gap: 0.2em;
margin: 0.5em 0;
text-align: center;
span {
min-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
background-color: #4d4d4d;
padding: 0.5em 0.2em;
}
@include smallScreen() {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
span {
padding: 0.2em 0.3em;
}
grid-template-columns: 1fr;
background-color: #4d4d4d;
}
}
</style> </style>
+68 -229
View File
@@ -1,24 +1,19 @@
<template> <template>
<section class="journal-timetables"> <section class="journal-timetables">
<div class="journal-wrapper"> <div class="journal_wrapper">
<div class="journal_top-bar">
<JournalOptions <JournalOptions
@on-filter-change="search" @on-search-confirm="searchHistory"
@on-input-change="search" @on-options-reset="resetOptions"
@on-sorter-change="search"
:sorter-option-ids="['timestampFrom', 'duration']" :sorter-option-ids="['timestampFrom', 'duration']"
:data-status="dataStatus"
/> />
<!-- <DispatcherStats /> --> <div class="list_wrapper" @scroll="handleScroll">
</div> <!-- <transition name="warning" mode="out-in"> -->
<!-- <div :key="dataStatus"> -->
<Loading v-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
<div class="journal-list"> <div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
<div class="list-wrapper" ref="scrollElement">
<transition name="warning" mode="out-in">
<div :key="historyDataStatus.status">
<Loading v-if="isDataLoading || isDataInit" />
<div v-else-if="isDataError" class="journal_warning error">
{{ $t('app.error') }} {{ $t('app.error') }}
</div> </div>
@@ -26,60 +21,34 @@
{{ $t('app.no-result') }} {{ $t('app.no-result') }}
</div> </div>
<ul v-else> <div v-else>
<transition-group name="journal-list-anim"> <JournalDispatchersList :dispatcherHistory="computedHistoryList" />
<li v-for="(doc, i) in computedHistoryList" :key="doc.id">
<div class="journal_day" v-if="isAnotherDay(i - 1, i)">
<span>{{ new Date(doc.timestampFrom).toLocaleDateString('pl-PL') }}</span>
</div>
<div <button
class="journal_item" class="btn btn--option btn--load-data"
:class="{ online: doc.isOnline }" v-if="!scrollNoMoreData && scrollDataLoaded && computedHistoryList.length > 15"
@click="navigateToScenery(doc.stationName, doc.isOnline)" @click="addHistoryData"
@keydown.enter="navigateToScenery(doc.stationName, doc.isOnline)"
tabindex="0"
> >
<span> {{ $t('journal.load-data') }}
<b class="text--primary">{{ doc.dispatcherName }}</b> &bull; <b>{{ doc.stationName }}</b> </button>
<span class="text--grayed">&nbsp;#{{ doc.stationHash }}&nbsp;</span> </div>
<span class="region-badge" :class="doc.region">PL1</span> <!-- </div>
</span> </transition> -->
<span>
<span :data-status="doc.isOnline">
{{ doc.isOnline ? $t('journal.online-since') : 'OFFLINE' }}&nbsp;
</span>
<span>
{{ new Date(doc.timestampFrom).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
</span>
<span v-if="doc.currentDuration && doc.isOnline"> <div class="journal_warning" v-if="scrollNoMoreData">
({{ calculateDuration(doc.currentDuration) }}) {{ $t('journal.no-further-data') }}
</span>
<span v-if="doc.timestampTo">
&gt;
{{ new Date(doc.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
({{ $t('journal.duty-lasted') }} {{ calculateDuration(doc.currentDuration!) }})
</span>
</span>
</div>
</li>
</transition-group>
</ul>
</div>
</transition>
</div>
</div> </div>
<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') }}</div> {{ $t('journal.loading-further-data') }}
</div>
</div>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, JournalFilter, provide, reactive, Ref, ref } from 'vue'; import { defineComponent, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios'; import axios from 'axios';
import ActionButton from '../../components/Global/ActionButton.vue'; import ActionButton from '../../components/Global/ActionButton.vue';
@@ -89,38 +58,17 @@ import SearchBox from '../Global/SearchBox.vue';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import { URLs } from '../../scripts/utils/apiURLs'; import { URLs } from '../../scripts/utils/apiURLs';
import dateMixin from '../../mixins/dateMixin';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import JournalDispatchersList from './JournalDispatchersList.vue';
import { JournalDispatcherSearcher, JournalDispatcherSorter } from '../../types/Journal/JournalDispatcherTypes';
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`; const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
interface DispatcherHistoryItem {
id: string;
stationName: string;
stationHash: string;
region: string;
dispatcherName: string;
dispatcherId: number;
timestampFrom: number;
timestampTo?: number;
currentDuration?: number;
lastOnlineTimestamp: number;
isOnline: boolean;
}
type JournalDispatcherSearcher = {
[key in 'search-dispatcher' | 'search-station']: string;
};
export default defineComponent({ export default defineComponent({
components: { SearchBox, ActionButton, JournalOptions, DispatcherStats, Loading }, components: { SearchBox, ActionButton, JournalOptions, DispatcherStats, Loading, JournalDispatchersList },
mixins: [dateMixin],
name: 'JournalDispatchers', name: 'JournalDispatchers',
props: { props: {
@@ -142,19 +90,21 @@ export default defineComponent({
showReturnButton: false, showReturnButton: false,
statsCardOpen: false, statsCardOpen: false,
dataStatus: DataStatus.Initialized,
DataStatus,
historyList: [] as DispatcherHistory[],
}), }),
setup() { setup() {
const historyDataStatus: Ref<{ status: DataStatus; error: string | null }> = ref({ const sorterActive: JournalDispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
status: DataStatus.Loading,
error: null,
});
const sorterActive = ref({ id: 'timestampFrom', dir: -1 });
const journalFilterActive = ref({}); const journalFilterActive = ref({});
const searchersValues = reactive({ const searchersValues = reactive({
'search-dispatcher': '', 'search-dispatcher': '',
'search-station': '', 'search-station': '',
'search-date': '',
} as JournalDispatcherSearcher); } as JournalDispatcherSearcher);
const countFromIndex = ref(0); const countFromIndex = ref(0);
@@ -169,13 +119,6 @@ export default defineComponent({
return { return {
store: useStore(), store: useStore(),
historyList: ref([]) as Ref<DispatcherHistoryItem[]>,
historyDataStatus,
isDataLoading: computed(() => historyDataStatus.value.status === DataStatus.Loading),
isDataError: computed(() => historyDataStatus.value.status === DataStatus.Error),
isDataInit: computed(() => historyDataStatus.value.status === DataStatus.Initialized),
sorterActive, sorterActive,
searchersValues, searchersValues,
@@ -199,61 +142,36 @@ export default defineComponent({
if (this.sceneryName || this.dispatcherName) { if (this.sceneryName || this.dispatcherName) {
this.searchersValues['search-station'] = this.sceneryName?.toString() || ''; this.searchersValues['search-station'] = this.sceneryName?.toString() || '';
this.searchersValues['search-dispatcher'] = this.dispatcherName?.toString() || ''; this.searchersValues['search-dispatcher'] = this.dispatcherName?.toString() || '';
this.search(); this.searchHistory();
} }
window.addEventListener('scroll', this.handleScroll);
}, },
mounted() { mounted() {
if (!this.sceneryName && !this.dispatcherName) { if (!this.sceneryName && !this.dispatcherName) {
this.search(); this.searchHistory();
} }
}, },
deactivated() {
window.removeEventListener('scroll', this.handleScroll);
},
methods: { methods: {
closeDispatcherStatsCard() { handleScroll(e: Event) {
this.statsCardOpen = false; const listElement = e.target as HTMLElement;
const scrollTop = listElement.scrollTop;
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
}, },
navigateToScenery(name: string, isOnline: boolean) { resetOptions() {
if (!isOnline) return; this.searchersValues['search-station'] = '';
this.searchersValues['search-dispatcher'] = '';
this.sorterActive.id = 'timestampFrom';
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`); this.searchHistory();
}, },
isAnotherDay(prevIndex: number, currIndex: number) { searchHistory() {
if (currIndex == 0) return true;
return (
new Date(this.computedHistoryList[prevIndex].timestampFrom).getDate() !=
new Date(this.computedHistoryList[currIndex].timestampFrom).getDate()
);
},
handleScroll() {
this.showReturnButton = window.scrollY > window.innerHeight;
const element = this.$refs.scrollElement as HTMLElement;
if (
element.getBoundingClientRect().bottom * 0.85 < window.innerHeight &&
this.scrollDataLoaded &&
!this.scrollNoMoreData &&
this.historyDataStatus.status == DataStatus.Loaded
)
this.addHistoryData();
},
scrollToTop() {
window.scrollTo({ top: 0 });
},
search() {
this.fetchHistoryData({ this.fetchHistoryData({
searchers: this.searchersValues, searchers: this.searchersValues,
}); });
@@ -267,7 +185,7 @@ export default defineComponent({
const countFrom = this.historyList.length; const countFrom = this.historyList.length;
const responseData: DispatcherHistoryItem[] = await ( const responseData: DispatcherHistory[] = await (
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}&countFrom=${countFrom}`) await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
).data; ).data;
@@ -285,39 +203,39 @@ export default defineComponent({
async fetchHistoryData( async fetchHistoryData(
props: { props: {
searchers?: JournalDispatcherSearcher; searchers?: JournalDispatcherSearcher;
filter?: JournalFilter; filter?: JournalTimetableFilter;
} = {} } = {}
) { ) {
this.historyDataStatus.status = DataStatus.Loading; this.dataStatus = DataStatus.Loading;
const queries: string[] = []; const queries: string[] = [];
// const dispatcher = props.searchers?.find((s) => s.id == 'search-dispatcher')?.value.trim();
// const station = props.searchers?.find((s) => s.id == 'search-station')?.value.trim();
const dispatcher = props.searchers?.['search-dispatcher'].trim(); const dispatcher = props.searchers?.['search-dispatcher'].trim();
const station = props.searchers?.['search-station'].trim(); const station = props.searchers?.['search-station'].trim();
const dateString = props.searchers?.['search-date'].trim();
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`); if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
if (station) queries.push(`stationName=${station}`); if (station) queries.push(`stationName=${station}`);
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance']; // Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom'); if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom');
else if (this.sorterActive.id == 'duration') queries.push('sortBy=currentDuration'); else if (this.sorterActive.id == 'duration') queries.push('sortBy=currentDuration');
else queries.push('sortBy=timestampFrom'); else queries.push('sortBy=timestampFrom');
queries.push('countLimit=15'); queries.push('countLimit=30');
this.currentQuery = queries.join('&'); this.currentQuery = queries.join('&');
try { try {
const responseData: DispatcherHistoryItem[] = await ( const responseData: DispatcherHistory[] = await (
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`) await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`)
).data; ).data;
if (!responseData) { if (!responseData) {
this.historyDataStatus.status = DataStatus.Error; this.dataStatus = DataStatus.Error;
this.historyDataStatus.error = 'Brak danych!';
return; return;
} }
@@ -332,10 +250,9 @@ export default defineComponent({
? this.historyList[0].dispatcherName ? this.historyList[0].dispatcherName
: ''; : '';
this.historyDataStatus.status = DataStatus.Loaded; this.dataStatus = DataStatus.Loaded;
} catch (error) { } catch (error) {
this.historyDataStatus.status = DataStatus.Error; this.dataStatus = DataStatus.Error;
this.historyDataStatus.error = 'Ups! Coś poszło nie tak!';
} }
}, },
}, },
@@ -344,82 +261,4 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/JournalSection.scss'; @import '../../styles/JournalSection.scss';
@import '../../styles/responsive.scss';
.region-badge {
padding: 0.1em 0.5em;
border-radius: 0.5em;
font-weight: bold;
&.eu {
background-color: forestgreen;
}
}
.list-wrapper {
margin-top: 1em;
}
.journal_item {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
&.online {
cursor: pointer;
}
span[data-status='true'] {
color: springgreen;
}
span[data-status='false'] {
color: salmon;
}
}
.journal_day {
position: relative;
text-align: center;
background-color: #4d4d4d;
span {
position: relative;
background-color: #4d4d4d;
z-index: 10;
padding: 0 0.5em;
}
&::after {
position: absolute;
content: '';
z-index: 0;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
height: 3px;
width: 60%;
min-width: 200px;
background-color: white;
}
}
@include smallScreen() {
.journal_item {
flex-direction: column;
span {
margin-top: 0.25em;
text-align: center;
}
}
}
</style> </style>
@@ -0,0 +1,156 @@
<template>
<ul class="journal-list">
<!-- <transition-group name="journal-list-anim"> -->
<li v-for="item in computedDispatcherHistory" :class="{ sticky: typeof item == 'string' }">
<div v-if="typeof item == 'string'" class="journal_day">
{{ item }}
</div>
<div
v-else
class="journal_item"
:class="{ online: item.isOnline }"
@click="navigateToScenery(item.stationName, item.isOnline)"
@keydown.enter="navigateToScenery(item.stationName, item.isOnline)"
tabindex="0"
>
<span>
<b class="text--primary">{{ item.dispatcherName }}</b> &bull; <b>{{ item.stationName }}</b>
<span class="text--grayed">&nbsp;#{{ item.stationHash }}&nbsp;</span>
<span class="region-badge" :class="item.region">PL1</span>
</span>
<span>
<span :data-status="item.isOnline"> {{ item.isOnline ? $t('journal.online-since') : 'OFFLINE' }}&nbsp; </span>
<span>
{{ new Date(item.timestampFrom).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
</span>
<span v-if="item.currentDuration && item.isOnline"> ({{ calculateDuration(item.currentDuration) }}) </span>
<span v-if="item.timestampTo">
&gt;
{{ new Date(item.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
({{ $t('journal.duty-lasted') }} {{ calculateDuration(item.currentDuration!) }})
</span>
</span>
</div>
</li>
<!-- </transition-group> -->
</ul>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
export default defineComponent({
props: {
dispatcherHistory: {
type: Array as PropType<DispatcherHistory[]>,
required: true,
},
},
mixins: [dateMixin],
computed: {
computedDispatcherHistory() {
return this.dispatcherHistory.reduce((acc, historyItem, i) => {
if (this.isAnotherDay(i - 1, i)) acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
acc.push(historyItem);
return acc;
}, [] as (DispatcherHistory | string)[]);
},
},
methods: {
navigateToScenery(name: string, isOnline: boolean) {
if (!isOnline) return;
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
},
isAnotherDay(prevIndex: number, currIndex: number) {
if (currIndex == 0) return true;
return (
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
);
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/JournalSection.scss';
.region-badge {
padding: 0.1em 0.5em;
border-radius: 0.5em;
font-weight: bold;
&.eu {
background-color: forestgreen;
}
}
li.sticky {
position: sticky;
top: 0;
}
.journal_item {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
padding: 0.75em;
&.online {
cursor: pointer;
}
span[data-status='true'] {
color: springgreen;
}
span[data-status='false'] {
color: salmon;
}
}
.journal_day {
margin-bottom: 1em;
padding: 0.5em;
font-weight: bold;
background-color: #333;
span {
position: relative;
background-color: inherit;
z-index: 10;
padding-right: 1em;
font-weight: bold;
}
}
@include smallScreen() {
.journal_item {
flex-direction: column;
span {
margin-top: 0.25em;
text-align: center;
}
}
}
</style>
+112 -183
View File
@@ -1,80 +1,101 @@
<template> <template>
<div class="journal-options"> <div class="filters-options" @keydown.esc="showOptions = false">
<div class="options_wrapper"> <div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<button class="btn--filled btn--image" @click="showOptions = !showOptions" ref="button">
<img :src="getIcon('filter2')" alt="Open filters" />
{{ $t('options.filters') }} [F]
</button>
<transition name="options-anim">
<div class="options_wrapper" v-if="showOptions">
<div class="options_content"> <div class="options_content">
<div class="content_select"> <h1 class="option-title">{{ $t('options.search-title') }}</h1>
<select-box <div class="search_content">
:itemList="translatedSorterOptions" <div class="search" v-for="(_, propName) in searchersValues" :key="propName">
:defaultItemIndex="0" <label v-if="propName == 'search-date'" for="date">{{ $t('options.search-date') }}</label>
@selected="onSorterChange"
:prefix="$t('journal.sort-prefix')"
/>
</div>
<div class="content_search">
<div class="search-box" v-for="(value, propName) in searchersValues" :key="propName">
<input
class="search-input"
:placeholder="$t(`journal.${propName}`)"
v-model="searchersValues[propName]"
@keydown.enter="onInputSearch"
/>
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" />
</div>
<!-- <div class="search-box">
<input
class="search-input"
v-model="searchedTrain"
:placeholder="$t('journal.search-train')"
@keydown.enter="search"
/>
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearTrain" />
</div>
<div class="search-box"> <div class="search-box">
<input <input
v-if="propName == 'search-date'"
class="search-input" class="search-input"
v-model="searchedDriver" id="date"
:placeholder="$t('journal.search-driver')" type="date"
@keydown.enter="search" min="2022-02-01"
@keydown.enter="onSearchConfirm"
v-model="searchersValues[propName]"
/> />
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearDriver" /> <input
</div> --> v-else
class="search-input"
@keydown.enter="onSearchConfirm"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
:placeholder="$t(`options.${propName}`)"
v-model="searchersValues[propName]"
/>
<action-button class="search-button" @click="onInputSearch"> <button class="search-exit">
{{ $t('journal.search') }} <img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" />
</action-button> </button>
</div> </div>
</div> </div>
<div class="search_actions">
<button class="btn--action" @click="onResetButtonClick">
{{ $t('options.reset-button') }}
</button>
<button class="btn--action" @click="onSearchButtonConfirm">
{{ $t('options.search-button') }}
</button>
</div>
</div>
<h1 class="option-title">{{ $t('options.sort-title') }}</h1>
<div class="options_sorters">
<div v-for="opt in translatedSorterOptions">
<button
class="sort-option btn--option"
:data-selected="opt.id == sorterActive.id"
@click="onSorterChange(opt)"
>
{{ opt.value.toUpperCase() }}
</button>
</div>
</div>
<h1 class="option-title" v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1>
<div class="options_filters"> <div class="options_filters">
<button <button
v-for="filter in filters" v-for="filter in filters"
class="journal-filter-option btn--option" class="filter-option btn--option"
:class="{ checked: journalFilterActive.id === filter.id }" :class="{ checked: journalFilterActive.id === filter.id }"
:id="filter.id" :id="filter.id"
@click="onFilterChange(filter)" @click="onFilterChange(filter)"
> >
{{ $t(`journal.filter-${filter.id}`) }} {{ $t(`options.filter-${filter.id}`) }}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</transition>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, inject, JournalFilter, PropType } from 'vue'; import { defineComponent, inject, Prop, PropType } from 'vue';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import keyMixin from '../../mixins/keyMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
import ActionButton from '../Global/ActionButton.vue'; import ActionButton from '../Global/ActionButton.vue';
import SelectBox from '../Global/SelectBox.vue'; import SelectBox from '../Global/SelectBox.vue';
export default defineComponent({ export default defineComponent({
components: { SelectBox, ActionButton }, components: { SelectBox, ActionButton },
emits: ['onSorterChange', 'onInputChange', 'onFilterChange'], emits: ['onSearchConfirm', 'onOptionsReset'],
mixins: [imageMixin], mixins: [imageMixin, keyMixin],
props: { props: {
sorterOptionIds: { sorterOptionIds: {
@@ -83,17 +104,28 @@ export default defineComponent({
}, },
filters: { filters: {
type: Array as PropType<JournalFilter[]>, type: Array as PropType<JournalTimetableFilter[]>,
default: [], default: [],
}, },
dataStatus: {
type: Number as PropType<DataStatus>,
default: DataStatus.Initialized,
},
}, },
data() {
return {
showOptions: false,
DataStatus,
};
},
setup() { setup() {
return { return {
searchersValues: inject('searchersValues') as {[key: string]: string}, searchersValues: inject('searchersValues') as { [key: string]: string },
sorterActive: inject('sorterActive') as { id: string | number; dir: number }, sorterActive: inject('sorterActive') as { id: string | number; dir: number },
journalFilterActive: inject('journalFilterActive') as JournalFilter, journalFilterActive: inject('journalFilterActive') as JournalTimetableFilter,
}; };
}, },
@@ -101,160 +133,57 @@ export default defineComponent({
translatedSorterOptions() { translatedSorterOptions() {
return this.$props.sorterOptionIds.map((id) => ({ return this.$props.sorterOptionIds.map((id) => ({
id, id,
value: this.$t(`journal.option-${id}`), value: this.$t(`options.sort-${id}`),
})); }));
}, },
}, },
methods: { methods: {
// Override keyMixin function
onKeyDownFunction() {
this.showOptions = !this.showOptions;
this.$nextTick(() => {
if (this.showOptions) (this.$refs['button'] as HTMLButtonElement)?.focus();
});
},
focusEnd() {
console.log('focus end');
},
onSorterChange(item: { id: string | number; value: string }) { onSorterChange(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id; this.sorterActive.id = item.id;
this.sorterActive.dir = -1; this.sorterActive.dir = -1;
this.$emit('onSearchConfirm');
this.$emit('onSorterChange');
}, },
onFilterChange(filter: JournalFilter) { onFilterChange(filter: JournalTimetableFilter) {
this.journalFilterActive = filter; this.journalFilterActive = filter;
this.$emit('onFilterChange'); this.$emit('onSearchConfirm');
},
onInputSearch() {
this.$emit('onInputChange');
}, },
onInputClear(id: any) { onInputClear(id: any) {
this.searchersValues[id] = ''; this.searchersValues[id] = '';
this.onInputSearch(); this.$emit('onSearchConfirm');
},
onSearchConfirm() {
this.$emit('onSearchConfirm');
},
onSearchButtonConfirm() {
this.showOptions = false;
this.$emit('onSearchConfirm');
},
onResetButtonClick() {
this.$emit('onOptionsReset');
}, },
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive'; @import '../../styles/filters_options.scss';
@import '../../styles/option.scss';
.options {
&_wrapper {
display: flex;
flex-direction: column;
}
&_content {
display: flex;
flex-wrap: wrap;
.content_search,
.content_select {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 0.25em 0.25em 0 0;
}
}
&_filters {
display: flex;
flex-wrap: wrap;
margin: 0.5em 0 0 0;
.journal-filter-option {
margin: 0 0.25em 0 0;
&#abandoned {
color: salmon;
}
&#fulfilled {
color: lightgreen;
}
&#active {
color: lightblue;
}
}
}
}
.search {
&-box {
position: relative;
background: #333;
border-radius: 0.5em;
min-width: 200px;
margin-right: 0.25em;
}
&-input {
border: none;
min-width: 100%;
padding: 0.35em 0.5em;
}
&-exit {
position: absolute;
cursor: pointer;
top: 50%;
right: 10px;
transform: translateY(-50%);
width: 1em;
}
}
@include smallScreen() {
.journal-options {
width: 100%;
}
.options {
&_wrapper {
justify-content: center;
align-items: center;
}
&_content {
padding: 0 1em;
flex-direction: column;
.content_select {
margin: 0 auto;
padding: 0;
}
.content_search {
justify-content: center;
}
}
&_filters {
justify-content: center;
.journal-filter-option {
margin: 0.25em 0.25em;
}
}
}
.search {
&-box,
&-button {
margin: 0.5em 0 0 0;
}
&-box {
width: 100%;
}
&-button {
width: 80%;
max-width: 300px;
}
}
}
</style> </style>
+89 -244
View File
@@ -1,158 +1,59 @@
<template> <template>
<section class="journal-timetables"> <section class="journal-timetables">
<keep-alive>
<DriverStats v-if="statsCardOpen" @close-card="closeCard" />
</keep-alive>
<div class="journal-wrapper"> <div class="journal_wrapper">
<div class="journal_top-bar">
<JournalOptions <JournalOptions
@on-input-change="search" @on-search-confirm="searchHistory"
@on-filter-change="search" @on-options-reset="resetOptions"
@on-sorter-change="search"
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']" :sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']"
:filters="journalTimetableFilters" :filters="journalTimetableFilters"
:data-status="dataStatus"
/> />
</div>
<div class="journal-list"> <DriverStats />
<div class="list-wrapper" ref="scrollElement"> <!-- <button @click="statsCardOpen = true">Stats</button> -->
<transition name="warning" mode="out-in">
<div :key="historyDataStatus.status">
<Loading v-if="isDataLoading || isDataInit" />
<div v-else-if="isDataError" class="journal_warning error"> <div class="list_wrapper" @scroll="handleScroll">
<!-- <transition name="warning" mode="out-in"> -->
<!-- <div :key="dataStatus"> -->
<Loading v-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }} {{ $t('app.error') }}
</div> </div>
<div class="journal_warning" v-else-if="historyList.length == 0"> <div v-else-if="timetableHistory.length == 0" class="journal_warning">
{{ $t('app.no-result') }} {{ $t('app.no-result') }}
</div> </div>
<ul v-else> <div v-else>
<transition-group name="journal-list-anim"> <JournalTimetablesList :timetableHistory="timetableHistory" />
<li v-for="(item, i) in historyList" class="journal_item" :key="item.timetableId">
<div class="journal_item-top"> <button
<span> class="btn btn--option btn--load-data"
<span v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
tabindex="0" @click="addHistoryData"
@click="navigateToTimetable(item)"
@keydown.enter="navigateToTimetable(item)"
style="cursor: pointer"
> >
<b class="text--primary">{{ item.trainCategoryCode }}&nbsp;</b> {{ $t('journal.load-data') }}
<b>{{ item.trainNo }}</b> </button>
| <span>{{ item.driverName }}</span> |
<span class="text--grayed">#{{ item.timetableId }}</span>
</span>
<div>
<b>{{ item.route.replace('|', ' - ') }}</b>
</div>
<hr style="margin: 0.25em 0" />
<div class="scenery-list">
<span
v-for="(scenery, i) in getSceneryList(item)"
:key="scenery.name"
:class="{ confirmed: scenery.confirmed }"
>
{{ i > 0 ? ' > ' : '' }} {{ scenery.name }}
</span>
</div>
<div class="schedule-dates">
<!-- Data odjazdu ze stacji początkowej -->
<b>{{ item.route.split('|')[0] }}:</b>
<s v-if="item.beginDate != item.scheduledBeginDate" class="text--grayed">
{{ localeTime(item.beginDate, $i18n.locale) }}
</s>
<span>{{ localeTime(item.scheduledBeginDate, $i18n.locale) }} </span>&bull;
<!-- Data przyjazdu na stację końcową / porzucenia -->
<b v-if="(item.fulfilled && item.terminated) || !item.terminated">
{{ item.route.split('|').slice(-1)[0] }}:
</b>
<i v-else>{{ $t('journal.timetable-abandoned') }} </i>
<s v-if="item.endDate != item.scheduledEndDate && item.terminated" class="text--grayed">
{{ localeTime(item.fulfilled ? item.endDate : item.scheduledEndDate, $i18n.locale) }}
</s>
<span
>{{ localeTime(item.fulfilled ? item.scheduledEndDate : item.endDate, $i18n.locale) }}
</span>
</div>
</span>
<b
class="journal_item-status"
:class="{
fulfilled: item.fulfilled || item.currentDistance >= item.routeDistance * 0.9,
terminated: item.terminated && !item.fulfilled,
active: !item.terminated,
}"
>
{{
!item.terminated
? $t('journal.timetable-active')
: item.fulfilled || item.currentDistance >= item.routeDistance * 0.9
? $t('journal.timetable-fulfilled')
: $t('journal.timetable-abandoned')
}}
</b>
</div>
<div style="margin-top: 1em">
<div>
{{ $t('journal.timetable-day') }} <b>{{ localeDay(item.beginDate, $i18n.locale) }}</b>
</div>
<!-- Nick dyżurnego -->
<div v-if="item.authorName">
<b class="text--grayed">{{ $t('journal.dispatcher-name') }}&nbsp;</b>
<router-link
class="dispatcher-link"
:to="`/journal/dispatchers?dispatcherName=${item.authorName}`"
>{{ item.authorName }}</router-link
>
</div>
</div>
<div style="margin-top: 1em">
<div>
<b>{{ $t('journal.route-length') }}</b>
{{ !item.fulfilled ? item.currentDistance + ' /' : '' }}
{{ item.routeDistance }} km
</div>
<div>
<b>{{ $t('journal.station-count') }}</b>
{{ item.confirmedStopsCount }} /
{{ item.allStopsCount }}
</div>
</div>
</li>
</transition-group>
</ul>
</div>
</transition>
</div>
</div> </div>
<!-- </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">{{ $t('journal.loading-further-data') }}</div> <div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
</div> </div>
</div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, JournalFilter, provide, reactive, Ref, ref } from 'vue'; import { defineComponent, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios'; import axios from 'axios';
import DriverStats from './DriverStats.vue'; import DriverStats from './DriverStats.vue';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import { journalTimetableFilters } from '../../data/journalFilters'; import { JournalTimetableFilter, JournalTimetableSorter } from '../../types/Journal/JournalTimetablesTypes';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import routerMixin from '../../mixins/routerMixin'; import routerMixin from '../../mixins/routerMixin';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { DataStatus } from '../../scripts/enums/DataStatus';
@@ -161,16 +62,17 @@ import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData
import { URLs } from '../../scripts/utils/apiURLs'; import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import JournalOptions from './JournalOptions.vue'; import JournalOptions from './JournalOptions.vue';
import { JorunalTimetableSearchType } from '../../types/Journal/JournalTimetablesTypes';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import imageMixin from '../../mixins/imageMixin';
import JournalTimetablesList from './JournalTimetablesList.vue';
import { journalTimetableFilters } from '../../constants/Journal/JournalTimetablesConsts';
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`; const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
type JournalTimetableSearcher = {
[key in 'search-driver' | 'search-train']: string;
};
export default defineComponent({ export default defineComponent({
components: { DriverStats, Loading, JournalOptions }, components: { DriverStats, Loading, JournalOptions, JournalTimetablesList },
mixins: [dateMixin, routerMixin], mixins: [dateMixin, routerMixin, modalTrainMixin, imageMixin],
name: 'JournalTimetables', name: 'JournalTimetables',
@@ -188,22 +90,25 @@ export default defineComponent({
showReturnButton: false, showReturnButton: false,
statsCardOpen: false, statsCardOpen: false,
timetableHistory: [] as TimetableHistory[],
journalTimetableFilters, journalTimetableFilters,
dataStatus: DataStatus.Initialized,
dataErrorMessage: '',
DataStatus,
}), }),
setup() { setup() {
const historyDataStatus: Ref<{ status: DataStatus; error: string | null }> = ref({ const sorterActive: JournalTimetableSorter = reactive({ id: 'timetableId', dir: 1 });
status: DataStatus.Loading,
error: null,
});
const sorterActive = ref({ id: 'timetableId', dir: -1 });
const journalFilterActive = ref(journalTimetableFilters[0]); const journalFilterActive = ref(journalTimetableFilters[0]);
const searchersValues = reactive({ const searchersValues = reactive({
'search-train': '', 'search-train': '',
'search-driver': '', 'search-driver': '',
} as JournalTimetableSearcher); 'search-author': '',
'search-date': '',
} as JorunalTimetableSearchType);
const countFromIndex = ref(0); const countFromIndex = ref(0);
const countLimit = 15; const countLimit = 15;
@@ -215,13 +120,6 @@ export default defineComponent({
const scrollElement: Ref<HTMLElement | null> = ref(null); const scrollElement: Ref<HTMLElement | null> = ref(null);
return { return {
historyList: ref([]) as Ref<TimetableHistory[]>,
historyDataStatus,
isDataLoading: computed(() => historyDataStatus.value.status === DataStatus.Loading),
isDataError: computed(() => historyDataStatus.value.status === DataStatus.Error),
isDataInit: computed(() => historyDataStatus.value.status === DataStatus.Initialized),
sorterActive, sorterActive,
journalFilterActive, journalFilterActive,
searchersValues, searchersValues,
@@ -230,67 +128,45 @@ export default defineComponent({
countLimit, countLimit,
scrollElement, scrollElement,
maxCount: ref(15),
store: useStore(), store: useStore(),
}; };
}, },
activated() { activated() {
window.addEventListener('scroll', this.handleScroll);
if (this.timetableId) { if (this.timetableId) {
this.searchersValues['search-train'] = `#${this.timetableId}`; this.searchersValues['search-train'] = `#${this.timetableId}`;
this.search(); this.searchHistory();
} }
}, },
mounted() { mounted() {
if (!this.timetableId) this.search(); if (!this.timetableId) this.searchHistory();
},
deactivated() {
window.removeEventListener('scroll', this.handleScroll);
}, },
methods: { methods: {
navigateToTimetable(historyItem: TimetableHistory) { handleScroll(e: Event) {
if (historyItem.terminated) return; const listElement = e.target as HTMLElement;
const scrollTop = listElement.scrollTop;
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
this.navigateTo('/trains', { if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
trainNo: historyItem.trainNo,
driverName: historyItem.driverName, if (scrollTop > elementHeight * 0.85) this.addHistoryData();
});
}, },
closeCard() { resetOptions() {
this.statsCardOpen = false; this.searchersValues['search-date'] = '';
this.searchersValues['search-driver'] = '';
this.searchersValues['search-train'] = '';
this.searchersValues['search-author'] = '';
this.journalFilterActive = this.journalTimetableFilters[0];
this.sorterActive.id = 'timetableId';
this.searchHistory();
}, },
getSceneryList(historyItem: TimetableHistory) { searchHistory() {
return historyItem.sceneriesString
.split('%')
.map((name, i) => ({ name, confirmed: i < historyItem.confirmedStopsCount }));
},
handleScroll() {
this.showReturnButton = window.scrollY > window.innerHeight;
const element = this.$refs.scrollElement as HTMLElement;
if (
element.getBoundingClientRect().bottom * 0.85 < window.innerHeight &&
this.scrollDataLoaded &&
!this.scrollNoMoreData &&
this.historyDataStatus.status == DataStatus.Loaded
)
this.addHistoryData();
},
scrollToTop() {
window.scrollTo({ top: 0 });
},
search() {
this.fetchHistoryData({ this.fetchHistoryData({
searchers: this.searchersValues, searchers: this.searchersValues,
filter: this.journalFilterActive, filter: this.journalFilterActive,
@@ -303,7 +179,7 @@ export default defineComponent({
async addHistoryData() { async addHistoryData() {
this.scrollDataLoaded = false; this.scrollDataLoaded = false;
const countFrom = this.historyList.length; const countFrom = this.timetableHistory.length;
const responseData: TimetableHistory[] = await ( const responseData: TimetableHistory[] = await (
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}&countFrom=${countFrom}`) await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
@@ -316,25 +192,33 @@ export default defineComponent({
return; return;
} }
this.historyList.push(...responseData); this.timetableHistory.push(...responseData);
this.scrollDataLoaded = true; this.scrollDataLoaded = true;
}, },
async fetchHistoryData( async fetchHistoryData(
props: { props: {
searchers?: JournalTimetableSearcher; searchers?: JorunalTimetableSearchType;
filter?: JournalFilter; filter?: JournalTimetableFilter;
} = {} } = {}
) { ) {
this.historyDataStatus.status = DataStatus.Loading; this.dataStatus = DataStatus.Loading;
const queries: string[] = []; const queries: string[] = [];
const driver = props.searchers?.['search-driver'].trim(); const driverName = props.searchers?.['search-driver'].trim();
const train = props.searchers?.['search-train'].trim(); const trainNo = props.searchers?.['search-train'].trim();
const authorName = props.searchers?.['search-author'].trim();
if (driver) queries.push(`driverName=${driver}`); const dateString = props.searchers?.['search-date'].trim();
if (train) queries.push(train.startsWith('#') ? `timetableId=${train.replace('#', '')}` : `trainNo=${train}`); const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
if (driverName) queries.push(`driverName=${driverName}`);
if (trainNo)
queries.push(trainNo.startsWith('#') ? `timetableId=${trainNo.replace('#', '')}` : `trainNo=${trainNo}`);
if (authorName) queries.push(`authorName=${authorName}`);
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance']; // Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance'); if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance');
@@ -369,28 +253,26 @@ export default defineComponent({
).data; ).data;
if (!responseData) { if (!responseData) {
this.historyDataStatus.status = DataStatus.Error; this.dataStatus = DataStatus.Error;
this.historyDataStatus.error = 'Brak danych!'; this.dataErrorMessage = 'Brak danych!';
return; return;
} }
if (!responseData) return; if (!responseData) return;
// Response data exists // Response data exists
this.historyList = responseData; this.timetableHistory = responseData;
// Stats display // Stats display
this.store.driverStatsName = this.store.driverStatsName =
this.historyList.length > 0 && this.searchersValues['search-driver'].trim() this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim()
? this.historyList[0].driverName ? this.timetableHistory[0].driverName
: ''; : '';
this.historyDataStatus.status = DataStatus.Loaded; this.dataStatus = DataStatus.Loaded;
} catch (error) { } catch (error) {
this.historyDataStatus.status = DataStatus.Error; this.dataStatus = DataStatus.Error;
this.historyDataStatus.error = 'Ups! Coś poszło nie tak!'; this.dataErrorMessage = 'Ups! Coś poszło nie tak!';
console.error(error);
} }
}, },
}, },
@@ -399,41 +281,4 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/JournalSection.scss'; @import '../../styles/JournalSection.scss';
.journal_item {
&-top {
display: flex;
justify-content: space-between;
padding: 0.2em 0;
.scenery-list {
span {
color: #adadad;
&.confirmed {
color: #a3eba3;
}
}
}
}
&-status {
&.terminated {
color: salmon;
}
&.fulfilled {
color: lightgreen;
}
&.active {
color: lightblue;
}
}
}
.dispatcher-link {
font-weight: bold;
}
</style> </style>
@@ -0,0 +1,314 @@
<template>
<ul class="journal-list">
<li
v-for="{ timetable, sceneryList, ...item } in computedTimetableHistory"
class="journal_item"
:key="timetable.timetableId"
>
<div class="journal_item-info">
<div class="info-top">
<span
tabindex="0"
@click="showTimetable(timetable)"
@keydown.enter="showTimetable(timetable)"
style="cursor: pointer"
>
<b class="text--primary">{{ timetable.trainCategoryCode }}&nbsp;</b>
<b>{{ timetable.trainNo }}</b>
| <span>{{ timetable.driverName }}</span> |
<span class="text--grayed">#{{ timetable.timetableId }}</span>
</span>
<span>
<b class="info-date">{{ localeDay(timetable.beginDate, $i18n.locale) }}</b>
<b
class="info-status"
:class="{
fulfilled: timetable.fulfilled || timetable.currentDistance >= timetable.routeDistance * 0.9,
terminated: timetable.terminated && !timetable.fulfilled,
active: !timetable.terminated,
}"
>
{{
!timetable.terminated
? $t('journal.timetable-active')
: timetable.fulfilled || timetable.currentDistance >= timetable.routeDistance * 0.9
? $t('journal.timetable-fulfilled')
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
}}
</b>
</span>
</div>
<div class="info-route">
<b>{{ timetable.route.replace('|', ' - ') }}</b>
</div>
<hr />
<div class="scenery-list">
<span v-for="(scenery, i) in sceneryList" :key="scenery.name" :class="{ confirmed: scenery.confirmed }">
<span v-if="i > 0"> &gt;</span>
{{ scenery.name }}
<!-- Data odjazdu ze stacji początkowej -->
<span v-if="i == 0" v-html="scenery.beginDateHTML"></span>
<!-- Data przyjazdu do stacji końcowej -->
<span v-if="i == sceneryList.length - 1" v-html="scenery.endDateHTML"> </span>
</span>
</div>
<!-- Status RJ -->
<div style="margin: 0.5em 0">
<span>
<b>{{ $t('journal.route-length') }}</b>
{{ !timetable.fulfilled ? timetable.currentDistance + ' /' : '' }}
{{ timetable.routeDistance }} km
</span>
&bull;
<span>
<b>{{ $t('journal.station-count') }}</b>
{{ timetable.confirmedStopsCount }} /
{{ timetable.allStopsCount }}
</span>
</div>
<!-- Nick dyżurnego -->
<div v-if="timetable.authorName">
<b class="text--grayed">{{ $t('journal.dispatcher-name') }}&nbsp;</b>
<router-link class="dispatcher-link" :to="`/journal/dispatchers?dispatcherName=${timetable.authorName}`">
<b>{{ timetable.authorName }}</b>
</router-link>
</div>
<button
v-if="timetable.stockString"
class="btn--option btn--show"
@click="item.showStock.value = !item.showStock.value"
>
{{ $t('journal.stock-info') }}
<img :src="getIcon(`arrow-${item.showStock.value ? 'asc' : 'desc'}`)" alt="Arrow" />
</button>
<div class="info-extended" v-if="timetable.stockString && item.showStock.value">
<hr />
<div>
<span class="badge info-badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge info-badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>{{ timetable.stockLength }}m</span>
</span>
<span class="badge info-badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>{{ Math.floor(timetable.stockMass! / 1000) }}t</span>
</span>
</div>
<ul class="stock-list">
<li v-for="(car, i) in timetable.stockString.split(';')" :key="i">
<img
@error="onImageError"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${car.split(':')[0]}.png`"
:alt="car"
/>
<div>{{ car.replace(/_/g, ' ').split(':')[0] }}</div>
</li>
</ul>
</div>
</div>
</li>
</ul>
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import imageMixin from '../../mixins/imageMixin';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
export default defineComponent({
props: {
timetableHistory: {
type: Array as PropType<TimetableHistory[]>,
required: true,
},
},
mixins: [dateMixin, imageMixin, modalTrainMixin],
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
sceneryList: this.getSceneryList(timetable),
showStock: ref(false),
}));
},
},
methods: {
getSceneryList(timetable: TimetableHistory) {
return timetable.sceneriesString.split('%').map((name, i) => {
const beginDateHTML =
' (o. ' +
(timetable.beginDate != timetable.scheduledBeginDate
? `<s class='text--grayed'>${this.localeTime(timetable.beginDate, this.$i18n.locale)}</s> `
: '') +
`<span>${this.localeTime(timetable.scheduledBeginDate, this.$i18n.locale)}</span>)`;
const endDateHTML =
' (p. ' +
(timetable.endDate != timetable.scheduledEndDate && timetable.fulfilled
? `<s class='text--grayed'>${this.localeTime(
timetable.fulfilled ? timetable.endDate : timetable.scheduledEndDate,
this.$i18n.locale
)}</s> `
: '') +
`<span>${this.localeTime(
timetable.fulfilled || (timetable.terminated && !timetable.fulfilled)
? timetable.scheduledEndDate
: timetable.endDate,
this.$i18n.locale
)}</span>)`;
const abandonedDateHTML = ` (porz. ${this.localeTime(
timetable.fulfilled ? timetable.scheduledEndDate : timetable.endDate,
this.$i18n.locale
)})`;
return { name, confirmed: i < timetable.confirmedStopsCount, beginDateHTML, endDateHTML, abandonedDateHTML };
});
},
showTimetable(timetable: TimetableHistory) {
if (timetable.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString());
},
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = this.getImage('unknown.png');
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/variables.scss';
@import '../../styles/responsive.scss';
@import '../../styles/badge.scss';
@import '../../styles/JournalSection.scss';
hr {
margin: 0.25em 0;
}
.info {
&-date {
margin-right: 0.5em;
}
&-status {
padding: 0.05em 0.35em;
color: black;
&.terminated {
background-color: salmon;
}
&.fulfilled {
background-color: lightgreen;
}
&.active {
background-color: lightblue;
}
}
&-top {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
&-route {
margin: 0.25em 0;
}
&-extended {
margin-top: 0.5em;
}
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 0.5em;
margin-top: 1em;
li > div {
text-align: center;
color: #aaa;
font-size: 0.9em;
}
}
.scenery-list {
color: #adadad;
span.confirmed {
color: #a3eba3;
}
}
.btn--show {
display: flex;
margin-top: 1em;
font-weight: bold;
padding: 0.2em 0.45em;
img {
height: 1.3em;
}
}
.info-badge {
span:last-child {
color: black;
background-color: $accentCol;
}
}
@include smallScreen {
.info-top {
flex-direction: column;
span {
margin: 0.1em auto;
}
}
.info-extended {
text-align: center;
}
.info-route {
display: flex;
justify-content: center;
}
.btn--show {
margin: 1em auto 0 auto;
}
}
</style>
@@ -31,7 +31,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import axios from 'axios'; import axios from 'axios';
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
@@ -67,8 +66,6 @@ export default defineComponent({
this.dispatcherHistoryList = historyAPIData; this.dispatcherHistoryList = historyAPIData;
this.dataStatus = DataStatus.Loaded; this.dataStatus = DataStatus.Loaded;
console.log(this.dispatcherHistoryList);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@@ -105,7 +102,7 @@ export default defineComponent({
@include smallScreen { @include smallScreen {
.history-list { .history-list {
font-size: 1.2em; font-size: 1.1em;
} }
.list-item { .list-item {
align-items: center; align-items: center;
@@ -113,4 +110,3 @@ export default defineComponent({
} }
} }
</style> </style>
+3 -10
View File
@@ -1,8 +1,8 @@
<template> <template>
<section class="info-header"> <section class="info-header">
<div class="scenery-name"> <a class="scenery-name" :href="station.generalInfo?.url">
{{ station.name }} {{ station.name }}
</div> </a>
<div class="scenery-hash" v-if="station.onlineInfo?.hash">#{{ station.onlineInfo.hash }}</div> <div class="scenery-hash" v-if="station.onlineInfo?.hash">#{{ station.onlineInfo.hash }}</div>
</section> </section>
@@ -12,7 +12,6 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
props: { props: {
station: { station: {
@@ -32,14 +31,9 @@ export default defineComponent({
position: relative; position: relative;
font-size: 3.5em; font-size: 3em;
padding: 0 0.5em;
text-transform: uppercase; text-transform: uppercase;
@include smallScreen() {
font-size: 2.75em;
}
} }
.scenery-hash { .scenery-hash {
@@ -47,4 +41,3 @@ export default defineComponent({
font-size: 1.2em; font-size: 1.2em;
} }
</style> </style>
+1 -2
View File
@@ -109,7 +109,7 @@ h3.section-header {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 1.5em; font-size: 1.2em;
img { img {
width: 1.1em; width: 1.1em;
@@ -127,7 +127,6 @@ h3.section-header {
.info-general { .info-general {
margin-top: 1em; margin-top: 1em;
font-size: 1.1em;
} }
.general-list { .general-list {
@@ -64,6 +64,7 @@ export default defineComponent({
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5em;
.dispatcher { .dispatcher {
font-size: 2em; font-size: 2em;
@@ -82,17 +83,15 @@ export default defineComponent({
} }
&_name { &_name {
margin-right: 0.4em;
cursor: pointer; cursor: pointer;
margin-right: 0.25em;
} }
&_likes { &_likes {
img { img {
height: 0.7em; height: 0.7em;
margin-right: 0.25em; margin: 0 0.25em;
} }
margin-right: 1.5em;
} }
} }
@@ -68,7 +68,7 @@
<img <img
v-if="!station.generalInfo" v-if="!station.generalInfo"
class="icon-info" class="icon-info"
:src="getImage('unknown.png')" :src="getIcon('unknown')"
alt="icon-unknown" alt="icon-unknown"
:title="$t('desc.unknown')" :title="$t('desc.unknown')"
/> />
+27 -15
View File
@@ -10,18 +10,27 @@
<span class="text--grayed"> <span class="text--grayed">
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }} {{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }}
</span> </span>
<!--
<button class="btn--image" v-if="!timetableOnly">
<a :href="`${$route.path}?station=${$route.query.station}&timetableOnly=1`">
<img :src="getIcon('view')" alt="View image" />
</a>
</button> -->
</h3> </h3>
<div class="timetable-checkpoints" v-if="station && station.generalInfo?.checkpoints"> <div class="timetable-checkpoints" v-if="station && station.generalInfo?.checkpoints">
<span v-for="(cp, i) in station.generalInfo.checkpoints" :key="i">
{{ (i > 0 && '&bull;') || '' }}
<button <button
v-for="cp in station.generalInfo.checkpoints"
:key="cp.checkpointName" :key="cp.checkpointName"
class="checkpoint_item btn btn--text" class="checkpoint_item"
:class="{ current: selectedCheckpoint === cp.checkpointName }" :class="{ current: selectedCheckpoint === cp.checkpointName }"
@click="selectCheckpoint(cp)" @click="selectCheckpoint(cp)"
> >
{{ cp.checkpointName }} {{ cp.checkpointName }}
</button> </button>
</span>
</div> </div>
</div> </div>
@@ -182,11 +191,14 @@ export default defineComponent({
type: Object as PropType<Station>, type: Object as PropType<Station>,
required: true, required: true,
}, },
timetableOnly: {
type: Boolean,
},
}, },
data: () => ({ data: () => ({
listOpen: false, listOpen: false,
}), }),
setup(props) { setup(props) {
@@ -250,6 +262,10 @@ export default defineComponent({
selectCheckpoint(cp: { checkpointName: string }) { selectCheckpoint(cp: { checkpointName: string }) {
this.selectedCheckpoint = cp.checkpointName; this.selectedCheckpoint = cp.checkpointName;
}, },
showTimetableOnlyView() {
this.$router.push(`${this.$route.fullPath}&timetableOnly=1`);
},
}, },
mounted() { mounted() {
@@ -293,7 +309,7 @@ export default defineComponent({
h3 { h3 {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 1.4em; font-size: 1.3em;
} }
} }
@@ -351,17 +367,15 @@ export default defineComponent({
flex-wrap: wrap; flex-wrap: wrap;
font-size: 1.1em; font-size: 1.1em;
padding: 0.75em 0; padding: 0.75em 0;
.checkpoint_item {
&.current { button.checkpoint_item {
font-weight: bold; color: #aaa;
color: $accentCol; display: inline;
} }
&:not(:last-child)::after { .checkpoint_item.current {
margin: 0 0.5em; font-weight: bold;
content: '•'; color: $accentCol;
color: white;
}
} }
} }
@@ -505,8 +519,6 @@ export default defineComponent({
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
font-size: 1.05em;
} }
&-general { &-general {
@@ -33,7 +33,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import axios from 'axios'; import axios from 'axios';
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
@@ -106,13 +105,8 @@ export default defineComponent({
} }
@include smallScreen { @include smallScreen {
.history-list {
font-size: 1.1em;
}
.list-item { .list-item {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
font-size: 1.05em;
} }
} }
</style> </style>
+23 -63
View File
@@ -1,23 +1,12 @@
<template> <template>
<div class="filter-option option"> <button class="btn--action" :class="option.section" :data-selected="option.value" @click="handleChange">
<label> {{ $t(`filters.${option.id}`) }}
<input </button>
type="checkbox"
:name="option.name"
:defaultValue="option.defaultValue"
:id="option.id"
v-model="option.value"
@change="handleChange"
/>
<span v-if="option.id != 'troll'" :class="option.section + (option.value ? ' checked' : '')"
>{{ option.id != 'troll' ? $t(`filters.${option.id}`) : 'ARKADIA ZDRÓJ' }}
</span>
</label>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
interface FilterOption { interface FilterOption {
id: string; id: string;
@@ -34,29 +23,26 @@ export default defineComponent({
required: true, required: true,
}, },
}, },
emits: ['optionChange'],
setup() {
return {
filterStore: useStationFiltersStore(),
};
},
methods: { methods: {
handleChange() { handleChange() {
if (this.option.name == 'troll') { this.option.value = !this.option.value;
location.href = 'https://www.youtube.com/watch?v=HIcSWuKMwOw';
return;
}
this.$emit('optionChange', { this.filterStore.changeFilterValue({
name: this.option.name, name: this.option.name,
value: this.option.value, value: !this.option.value,
}); });
}, },
}, },
setup() {
return {};
},
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/option.scss';
$accessCol: #e03b07; $accessCol: #e03b07;
$controlCol: #0085ff; $controlCol: #0085ff;
$signalCol: #bf7c00; $signalCol: #bf7c00;
@@ -64,64 +50,50 @@ $statusCol: #349b32;
$saveCol: #28a826; $saveCol: #28a826;
$routesCol: #9049c0; $routesCol: #9049c0;
.option span { button {
font-size: 0.9em; width: 100%;
&.checked { padding: 0.4em;
border-radius: 0.4em;
&:focus-visible {
outline: 1px solid white;
}
&[data-selected='true'] {
&.access { &.access {
background-color: $accessCol; background-color: $accessCol;
&::before {
box-shadow: 0 0 6px 1px $accessCol; box-shadow: 0 0 6px 1px $accessCol;
} }
}
&.control { &.control {
background-color: $controlCol; background-color: $controlCol;
&::before {
box-shadow: 0 0 6px 1px $controlCol; box-shadow: 0 0 6px 1px $controlCol;
} }
}
&.signals { &.signals {
background-color: $signalCol; background-color: $signalCol;
&::before {
box-shadow: 0 0 6px 1px $signalCol; box-shadow: 0 0 6px 1px $signalCol;
} }
}
&.routes { &.routes {
background-color: $routesCol; background-color: $routesCol;
&::before {
box-shadow: 0 0 6px 1px $routesCol; box-shadow: 0 0 6px 1px $routesCol;
} }
}
&.status { &.status {
background-color: $statusCol; background-color: $statusCol;
&::before {
box-shadow: 0 0 6px 1px $statusCol; box-shadow: 0 0 6px 1px $statusCol;
} }
}
&.save { &.save {
background-color: $saveCol; background-color: $saveCol;
&::before {
box-shadow: 0 0 6px 1px $saveCol; box-shadow: 0 0 6px 1px $saveCol;
} }
}
&.troll { &.troll {
background-color: firebrick; background-color: firebrick;
&::before {
box-shadow: 0 0 6px 1px firebrick; box-shadow: 0 0 6px 1px firebrick;
} }
}
&.mode { &.mode {
background-color: lightgreen; background-color: lightgreen;
@@ -129,18 +101,6 @@ $routesCol: #9049c0;
font-weight: 500; font-weight: 500;
} }
&::before {
position: absolute;
content: '';
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 0.5em;
}
} }
} }
</style> </style>
+115 -110
View File
@@ -1,20 +1,35 @@
<template> <template>
<section class="filter-card" v-click-outside="closeCard"> <section class="filter-card" v-click-outside="closeCard" @keydown.esc="closeCard">
<div class="card_btn"> <div class="card_controls">
<button class="btn btn--option" @click="toggleCard"> <button class="btn--filled btn--image" @click="toggleCard">
<img class="button_icon" :src="getIcon('filter2')" alt="icon-filter" /> <img class="button_icon" :src="getIcon('filter2')" alt="filter icon" />
{{ $t('options.filters') }} {{ $t('options.filters') }} [F]
</button> </button>
<label for="scenery-search">
<input
id="scenery-search"
list="sceneries"
:placeholder="$t('sceneries.scenery-search')"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
v-model="chosenSearchScenery"
/>
<datalist id="sceneries">
<option v-for="scenery in store.stationList" :value="scenery.name"></option>
</datalist>
</label>
</div> </div>
<transition name="card-anim"> <transition name="card-anim">
<div class="card" v-if="isVisible"> <div class="card" v-if="isVisible" tabindex="0" ref="cardEl">
<div class="card_content"> <div class="card_content">
<div class="card_title flex">{{ $t('filters.title') }}</div> <div class="card_title flex">{{ $t('filters.title') }}</div>
<section class="card_options"> <section class="card_options">
<filter-option <filter-option
v-for="(option, i) in inputs.options" v-for="(option, i) in filterStore.inputs.options"
:option="option" :option="option"
:key="i" :key="i"
@optionChange="handleChange" @optionChange="handleChange"
@@ -23,7 +38,7 @@
<section class="card_timestamp" style="text-align: center"> <section class="card_timestamp" style="text-align: center">
<div>{{ $t('filters.minimum-hours-title') }}</div> <div>{{ $t('filters.minimum-hours-title') }}</div>
<span class="clock"> <span class="clock">
<button @click="subHour">-</button> <button class="btn--action" @click="subHour">-</button>
<span>{{ <span>{{
minimumHours == 0 minimumHours == 0
? $t('filters.now') ? $t('filters.now')
@@ -31,7 +46,7 @@
? minimumHours + $t('filters.hour') ? minimumHours + $t('filters.hour')
: $t('filters.no-limit') : $t('filters.no-limit')
}}</span> }}</span>
<button @click="addHour">+</button> <button class="btn--action" @click="addHour">+</button>
</span> </span>
</section> </section>
@@ -42,11 +57,13 @@
name="authors" name="authors"
v-model="authorsInputValue" v-model="authorsInputValue"
@input="handleAuthorsInput" @input="handleAuthorsInput"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
/> />
</section> </section>
<section class="card_sliders"> <section class="card_sliders">
<div class="slider" v-for="(slider, i) in inputs.sliders" :key="i"> <div class="slider" v-for="(slider, i) in filterStore.inputs.sliders" :key="i">
<input <input
class="slider-input" class="slider-input"
type="range" type="range"
@@ -65,23 +82,13 @@
</section> </section>
<section class="card_actions"> <section class="card_actions">
<div> <div class="action-buttons">
<filter-option <button class="btn--action" style="width: 100%" @click="saveFilters" :data-selected="saveOptions">
@optionChange="saveFilters" {{ $t('filters.save') }}
:option="{ </button>
id: 'save',
name: 'save', <button class="btn--action" @click="resetFilters">{{ $t('filters.reset') }}</button>
section: 'mode', <button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button>
value: saveOptions,
defaultValue: true,
}"
/>
</div>
<div>
<action-button class="outlined" @click="resetFilters">
{{ $t('filters.reset') }}
</action-button>
<action-button class="outlined" @click="closeCard">{{ $t('filters.close') }}</action-button>
</div> </div>
</section> </section>
</div> </div>
@@ -91,11 +98,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, inject } from 'vue'; import { defineComponent, inject } from 'vue';
import inputData from '../../data/options.json';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import keyMixin from '../../mixins/keyMixin';
import routerMixin from '../../mixins/routerMixin';
import StorageManager from '../../scripts/managers/storageManager'; import StorageManager from '../../scripts/managers/storageManager';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import ActionButton from '../Global/ActionButton.vue'; import ActionButton from '../Global/ActionButton.vue';
@@ -103,12 +111,9 @@ import FilterOption from './FilterOption.vue';
export default defineComponent({ export default defineComponent({
components: { ActionButton, FilterOption }, components: { ActionButton, FilterOption },
emits: ['changeFilterValue', 'invertFilters', 'resetFilters'], mixins: [imageMixin, keyMixin, routerMixin],
mixins: [imageMixin],
data: () => ({ data: () => ({
inputs: { ...inputData },
saveOptions: false, saveOptions: false,
STORAGE_KEY: 'options_saved', STORAGE_KEY: 'options_saved',
@@ -118,15 +123,18 @@ export default defineComponent({
currentRegion: { id: '', value: '' }, currentRegion: { id: '', value: '' },
delayInputTimer: -1, delayInputTimer: -1,
chosenSearchScenery: '',
}), }),
setup() { setup() {
const isVisible = inject('isFilterCardVisible'); const isVisible = inject('isFilterCardVisible');
const store = useStore(); const store = useStore();
const filterStore = useStationFiltersStore();
return { return {
isVisible, isVisible,
store, store,
filterStore,
}; };
}, },
@@ -142,9 +150,31 @@ export default defineComponent({
this.currentRegion = this.store.region; this.currentRegion = this.store.region;
}, },
watch: {
chosenSearchScenery(value: string) {
const chosenStation = this.store.stationList.find(({ name }) => name == value);
if (chosenStation) {
this.$router.push(`/scenery?station=${chosenStation.name.replace(/ /g, '_')}`);
this.chosenSearchScenery = '';
}
},
isVisible(value: boolean) {
this.$nextTick(() => {
if (value) (this.$refs['cardEl'] as HTMLDivElement).focus();
});
},
},
methods: { methods: {
// Override keyMixin function
onKeyDownFunction() {
this.isVisible = !this.isVisible;
},
handleChange(change: { name: string; value: boolean }) { handleChange(change: { name: string; value: boolean }) {
this.$emit('changeFilterValue', { this.filterStore.changeFilterValue({
name: change.name, name: change.name,
value: !change.value, value: !change.value,
}); });
@@ -155,7 +185,7 @@ export default defineComponent({
handleInput(e: Event) { handleInput(e: Event) {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
this.$emit('changeFilterValue', { this.filterStore.changeFilterValue({
name: target.name, name: target.name,
value: target.value, value: target.value,
}); });
@@ -172,7 +202,7 @@ export default defineComponent({
}, },
changeNumericFilterValue(name: string, value: number, saveToStorage = false) { changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
this.$emit('changeFilterValue', { this.filterStore.changeFilterValue({
name, name,
value, value,
}); });
@@ -192,17 +222,8 @@ export default defineComponent({
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true); this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
}, },
invertFilters() { saveFilters() {
this.inputs.options.forEach((option) => { this.saveOptions = !this.saveOptions;
option.value = !option.value;
StorageManager.setBooleanValue(option.name, option.value);
});
this.$emit('invertFilters');
},
saveFilters(change: { value: any }) {
this.saveOptions = change.value;
if (!this.saveOptions) { if (!this.saveOptions) {
StorageManager.unregisterStorage(this.STORAGE_KEY); StorageManager.unregisterStorage(this.STORAGE_KEY);
@@ -211,28 +232,16 @@ export default defineComponent({
StorageManager.registerStorage(this.STORAGE_KEY); StorageManager.registerStorage(this.STORAGE_KEY);
this.inputs.options.forEach((option) => StorageManager.setBooleanValue(option.name, option.value)); this.filterStore.inputs.options.forEach((option) => StorageManager.setBooleanValue(option.name, !option.value));
this.filterStore.inputs.sliders.forEach((slider) => StorageManager.setNumericValue(slider.name, slider.value));
this.inputs.sliders.forEach((slider) => StorageManager.setNumericValue(slider.name, slider.value));
}, },
resetFilters() { resetFilters() {
this.inputs.options.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, option.value);
});
this.inputs.sliders.forEach((slider) => {
slider.value = slider.defaultValue;
StorageManager.setNumericValue(slider.name, slider.value);
});
this.authorsInputValue = ''; this.authorsInputValue = '';
this.minimumHours = 0; this.minimumHours = 0;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true); this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
this.filterStore.resetFilters();
this.$emit('resetFilters');
}, },
closeCard() { closeCard() {
@@ -264,28 +273,24 @@ export default defineComponent({
} }
.card { .card {
&_btn { &_controls {
button {
display: flex; display: flex;
align-items: center; gap: 0.5em;
padding: 0.5em 1em; input {
border-radius: 0.75em 0.75em 0 0; border-radius: 0.5em 0.5em 0 0;
height: 100%;
font-weight: bold;
}
img {
width: 1.3em;
margin-right: 0.25em;
} }
} }
&_content { &_content {
display: grid; display: flex;
grid-template-rows: 70px 1fr 100px 50px auto; flex-direction: column;
min-height: 0; gap: 1em;
max-height: 100vh;
max-height: 90vh;
padding: 1em;
} }
&_title { &_title {
@@ -293,8 +298,6 @@ export default defineComponent({
font-weight: 700; font-weight: 700;
color: $accentCol; color: $accentCol;
margin: 0.5em 0;
text-align: center; text-align: center;
} }
@@ -342,31 +345,17 @@ export default defineComponent({
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 1.15em; font-size: 1.2em;
margin-top: 0.5em;
color: $accentCol;
font-weight: bold;
}
span { span {
min-width: 100px; min-width: 120px;
font-weight: bold;
color: $accentCol;
} }
button { button {
border: none; padding: 0.2em 0.6em;
outline: none;
background: none;
padding: 0 0.45em;
cursor: pointer;
color: white;
font-size: 1.35em;
&:focus,
&:hover {
color: $accentCol;
} }
} }
} }
@@ -389,22 +378,33 @@ export default defineComponent({
input { input {
width: 100%; width: 100%;
padding: 0.5em; padding: 0.5em;
border: 1px solid white;
} }
} }
&_actions { &_actions {
margin-top: 1em; .filter-option {
max-width: 50%;
display: flex; margin: 0 auto;
flex-direction: column;
align-items: center;
button {
margin: 1em 0.25em;
} }
.option { .action-buttons {
font-size: 1.1em; display: flex;
gap: 0.5em;
width: 100%;
margin-top: 0.5em;
button {
width: 50%;
margin: 0 auto;
padding: 0.5em;
&[data-selected='true'] {
background-color: lightgreen;
color: black;
}
}
} }
} }
} }
@@ -435,8 +435,13 @@ export default defineComponent({
min-width: 25%; min-width: 25%;
max-width: 120px; max-width: 120px;
&:focus-visible ~ * {
color: gold;
}
&::-webkit-slider-thumb { &::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none;
height: 20px; height: 20px;
width: 20px; width: 20px;
+25 -14
View File
@@ -182,7 +182,7 @@
</td> </td>
<td class="station_info" v-else> <td class="station_info" v-else>
<img class="icon-info" :src="getImage('unknown.png')" alt="icon-unknown" :title="$t('desc.unknown')" /> <img class="icon-info" :src="getIcon('unknown')" alt="icon-unknown" :title="$t('desc.unknown')" />
</td> </td>
<td class="station_users" :class="{ inactive: !station.onlineInfo }"> <td class="station_users" :class="{ inactive: !station.onlineInfo }">
@@ -230,6 +230,7 @@ import stationInfoMixin from '../../mixins/stationInfoMixin';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { DataStatus } from '../../scripts/enums/DataStatus';
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
@@ -239,48 +240,58 @@ export default defineComponent({
type: Array as () => Station[], type: Array as () => Station[],
required: true, required: true,
}, },
sorterActive: {
type: Object as () => {
index: number;
dir: number;
},
required: true,
},
setFocusedStation: { type: Function, required: true },
changeSorter: { type: Function, required: true },
}, },
components: { Loading },
mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin], mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin],
data: () => ({ data: () => ({
headIds: ['station', 'min-lvl', 'status', 'dispatcher', 'dispatcher-lvl', 'routes', 'general'], headIds: ['station', 'min-lvl', 'status', 'dispatcher', 'dispatcher-lvl', 'routes', 'general'],
headIconsIds: ['user', 'spawn', 'timetable'], headIconsIds: ['user', 'spawn', 'timetable'],
lastSelectedStationName: '', lastSelectedStationName: '',
}), }),
computed: {
sorterActive() {
return this.stationFiltersStore.sorterActive;
},
},
setup() { setup() {
const store = useStore(); const store = useStore();
const stationFiltersStore = useStationFiltersStore();
const isDataLoaded = computed(() => { const isDataLoaded = computed(() => {
return store.dataStatuses.sceneries != DataStatus.Loading; return store.dataStatuses.sceneries != DataStatus.Loading;
}); });
return { return {
isDataLoaded, isDataLoaded,
stationFiltersStore,
}; };
}, },
methods: { methods: {
setScenery(name: string) { setScenery(name: string) {
const station = this.stations.find((station) => station.name === name); const station = this.stations.find((station) => station.name === name);
if (!station) return; if (!station) return;
this.lastSelectedStationName = station.name; this.lastSelectedStationName = station.name;
this.$router.push({ this.$router.push({
name: 'SceneryView', name: 'SceneryView',
query: { station: station.name.replaceAll(' ', '_') }, query: { station: station.name.replaceAll(' ', '_') },
}); });
}, },
openForumSite(e: Event, url: string | undefined) { openForumSite(e: Event, url: string | undefined) {
if (!url) return; if (!url) return;
e.preventDefault(); e.preventDefault();
window.open(url, '_blank'); window.open(url, '_blank');
}, },
changeSorter(i: number) {
this.stationFiltersStore.changeSorter(i);
},
}, },
components: { Loading },
}); });
</script> </script>
@@ -289,7 +300,7 @@ export default defineComponent({
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
@import '../../styles/icons.scss'; @import '../../styles/icons.scss';
$rowCol: #4b4b4b; $rowCol: #424242;
.change-anim { .change-anim {
&-enter-active, &-enter-active,
@@ -328,7 +339,7 @@ table {
} }
thead tr { thead tr {
background-color: $primaryCol; background-color: $bgCol;
} }
thead th { thead th {
@@ -338,7 +349,7 @@ table {
min-width: 75px; min-width: 75px;
padding: 0.5em; padding: 0.5em;
background-color: $primaryCol; background-color: $bgCol;
white-space: pre-wrap; white-space: pre-wrap;
cursor: pointer; cursor: pointer;
+12 -4
View File
@@ -12,6 +12,7 @@
<strong v-if="train.timetableData">{{ train.timetableData.category }}&nbsp;</strong> <strong v-if="train.timetableData">{{ train.timetableData.category }}&nbsp;</strong>
<strong>{{ train.trainNo }}</strong> <strong>{{ train.trainNo }}</strong>
<span>&nbsp;| {{ train.driverName }}&nbsp;</span> <span>&nbsp;| {{ train.driverName }}&nbsp;</span>
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
</span> </span>
</div> </div>
@@ -148,6 +149,17 @@ export default defineComponent({
color: #d2d2d2; color: #d2d2d2;
} }
.warning-timeout {
background-color: #be3728;
display: inline-block;
text-align: center;
width: 1.25em;
height: 1.25em;
border-radius: 50%;
}
.timetable_stops { .timetable_stops {
font-size: 0.75em; font-size: 0.75em;
} }
@@ -253,10 +265,6 @@ export default defineComponent({
.train-stats { .train-stats {
font-size: 1.1em; font-size: 1.1em;
img {
display: none;
}
} }
.train_general { .train_general {
+136 -206
View File
@@ -1,267 +1,197 @@
<template> <template>
<div class="train-options"> <div class="filters-options" @keydown.esc="showOptions = false">
<div class="options_wrapper"> <div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<button class="btn--filled btn--image" @click="toggleShowOptions" ref="button">
<img :src="getIcon('filter2')" alt="Open filters" />
{{ $t('options.filters') }} [F]
</button>
<transition name="options-anim">
<div class="options_wrapper" v-if="showOptions">
<div class="options_content"> <div class="options_content">
<div class="content_select"> <h1 class="option-title">{{ $t('options.search-title') }}</h1>
<select-box <div class="search_content">
:itemList="translatedSorterOptions" <div class="search-box">
:defaultItemIndex="0" <input
@selected="changeSorter" class="search-input"
:prefix="$t('trains.sorter-prefix')" ref="initFocusedElement"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
:placeholder="$t(`options.search-train`)"
v-model="searchedTrain"
/> />
</div> <button class="search-exit">
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear('train')" />
<div class="content_search"> </button>
<div class="search-box">
<input class="search-input" v-model="searchedTrain" :placeholder="$t('trains.search-train')" />
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedTrain = '')" />
</div> </div>
<div class="search-box"> <div class="search-box">
<input class="search-input" v-model="searchedDriver" :placeholder="$t('trains.search-driver')" /> <input
class="search-input"
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedDriver = '')" /> @focus="preventKeyDown = true"
</div> @blur="preventKeyDown = false"
</div> :placeholder="$t(`options.search-driver`)"
v-model="searchedDriver"
/>
<button class="search-exit">
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear('driver')" />
</button>
</div> </div>
</div> </div>
<div class="filters"> <h1 class="option-title">{{ $t('options.sort-title') }}</h1>
<span <div class="options_sorters">
:class="{ active: filter.isActive }" <div v-for="opt in translatedSorterOptions">
class="filter" <button
v-for="filter in filterList" class="sort-option btn--option"
:key="filter.id" :data-selected="opt.id == sorterActive.id"
tabindex="0" @click="onSorterChange(opt)"
@contextmenu="
(e) => {
e.preventDefault();
return false;
}
"
@click.left="toggleFilter(filter)"
@keydown.enter="toggleFilter(filter)"
@click.right="setFilterOnly(filter)"
@keydown.space="setFilterOnly(filter)"
> >
{{ $t(`trains.filter-${filter.id}`) }} {{ opt.value.toUpperCase() }}
</span> </button>
<span class="filter reset-btn" @click="resetFilters" tabindex="0">
{{ $t('trains.filter-reset') }}
</span>
</div> </div>
</div> </div>
<h1 class="option-title" v-if="trainFilterList.length != 0">{{ $t('options.filter-title') }}</h1>
<div class="options_filters">
<div class="filter-option" v-for="filter in trainFilterList">
<button class="btn--option" :data-disabled="!filter.isActive" @click="onFilterChange(filter)">
{{ $t(`options.filter-${filter.id}`) }}
</button>
</div>
<div class="filter-actions">
<button class="btn--action" @click="clearAllFilters">{{ $t('options.filter-clear') }}</button>
<button class="btn--action" @click="resetAllFilters">{{ $t('options.filter-reset') }}</button>
</div>
</div>
</div>
</div>
</transition>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, inject, TrainFilter } from 'vue'; import { defineComponent, inject, PropType } from 'vue';
import { useI18n } from 'vue-i18n';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import keyMixin from '../../mixins/keyMixin';
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
import ActionButton from '../Global/ActionButton.vue';
import SelectBox from '../Global/SelectBox.vue'; import SelectBox from '../Global/SelectBox.vue';
export default defineComponent({ export default defineComponent({
components: { SelectBox }, components: { SelectBox, ActionButton },
emits: ['changeSearchedTrain', 'changeSearchedDriver', 'changeSorter'], mixins: [imageMixin, keyMixin],
mixins: [imageMixin],
setup() { props: {
const { t } = useI18n(); sorterOptionIds: {
type: Array as PropType<Array<string>>,
const sorterOptions = [ required: true,
{ },
id: 'distance', },
value: 'kilometraż',
},
{
id: 'progress',
value: 'przebyta trasa',
},
{
id: 'delay',
value: 'opóźnienie',
},
{
id: 'mass',
value: 'masa',
},
{
id: 'speed',
value: 'prędkość',
},
{
id: 'length',
value: 'długość',
},
];
let filterList = inject('filterList') as TrainFilter[];
const translatedSorterOptions = computed(() =>
sorterOptions.map(({ id }) => ({
id,
value: t(`trains.option-${id}`),
}))
);
data() {
return { return {
translatedSorterOptions, showOptions: false,
searchedTrain: inject('searchedTrain') as string,
searchedDriver: inject('searchedDriver') as string,
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
filterList,
}; };
}, },
setup() {
return {
searchedTrain: inject('searchedTrain') as string,
searchedDriver: inject('searchedDriver') as string,
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
trainFilterList: inject('filterList') as TrainFilter[],
};
},
computed: {
translatedSorterOptions() {
return this.$props.sorterOptionIds.map((id) => ({
id,
value: this.$t(`options.sort-${id}`),
}));
},
},
methods: { methods: {
changeSorter(item: { id: string | number; value: string }) { // Override keyMixin function
onKeyDownFunction() {
this.toggleShowOptions();
},
toggleShowOptions() {
this.showOptions = !this.showOptions;
this.$nextTick(() => {
if (this.showOptions) (this.$refs['button'] as HTMLButtonElement)?.focus();
});
},
onSorterChange(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id; this.sorterActive.id = item.id;
this.sorterActive.dir = -1; this.sorterActive.dir = -1;
}, },
toggleFilter(filter: TrainFilter) { onFilterChange(filter: TrainFilter) {
filter.isActive = !filter.isActive; filter.isActive = !filter.isActive;
}, },
setFilterOnly(filter: TrainFilter) { clearAllFilters() {
this.filterList.forEach((f) => (f.isActive = f.id == filter.id)); this.trainFilterList.forEach((filter) => {
filter.isActive = false;
});
}, },
resetFilters() { resetAllFilters() {
this.filterList.forEach((f) => (f.isActive = true)); this.trainFilterList.forEach((filter) => {
this.searchedDriver = ""; filter.isActive = true;
this.searchedTrain = ""; });
},
onInputClear(id: 'driver' | 'train') {
if (id == 'driver') this.searchedDriver = '';
if (id == 'train') this.searchedTrain = '';
}, },
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive'; @import '../../styles/filters_options.scss';
.train-options { .search_content > div {
@include smallScreen() { margin: 0.5em auto;
width: 100%;
}
} }
.options { .search_content > button {
&_wrapper {
display: flex; display: flex;
}
&_content {
display: flex;
flex-wrap: wrap;
.content_search,
.content_select {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 0.25em 0.25em 0 0;
}
}
}
.search {
&-box {
position: relative;
background: #333;
border-radius: 0.5em;
min-width: 200px;
margin-right: 0.25em;
}
&-input {
border: none;
min-width: 100%;
padding: 0.35em 0.5em;
}
&-exit {
position: absolute;
cursor: pointer;
top: 50%;
right: 10px;
transform: translateY(-50%);
width: 1em;
}
}
.filters {
display: flex;
flex-wrap: wrap;
margin-top: 0.5em;
@include smallScreen() {
justify-content: center; justify-content: center;
} margin: 0 auto;
} }
.filter { .filter-option {
background: #333; button {
padding: 0.2em 0.25em; color: white;
margin: 0.25em 0.25em 0 0;
font-weight: bold; font-weight: bold;
cursor: pointer; &[data-disabled='true'] {
color: gray; color: #888;
&.active {
color: var(--clr-primary);
} }
&.reset-btn {
color: salmon;
} }
} }
@include smallScreen() { .filter-actions {
.journal-options { display: flex;
gap: 0.5em;
width: 100%; width: 100%;
}
.options {
&_wrapper {
justify-content: center;
}
&_content { margin-top: 1em;
padding: 0 1em;
flex-direction: column; button {
.content_select {
margin: 0 auto;
padding: 0;
}
.content_search {
justify-content: center;
}
}
}
.search {
&-box,
&-button {
margin: 0.5em 0 0 0;
}
&-box {
width: 100%; width: 100%;
} }
&-button {
width: 80%;
max-width: 300px;
}
}
} }
</style> </style>
@@ -1,144 +0,0 @@
<template>
<section class="filter-card" v-click-outside="closeCard">
<div class="card_btn">
<action-button @click="toggleCard">
<img class="button_icon" :src="getIcon('filter2')" alt="icon-filter" />
<p>{{ $t('options.filters') }}</p>
</action-button>
</div>
<transition name="card-anim">
<div class="card_content card" v-if="isVisible">
<div class="card_exit" @click="closeCard"></div>
<div class="options_wrapper">
<div class="options_content">
<div class="content_select">
<select-box
:itemList="translatedSorterOptions"
:defaultItemIndex="0"
@selected="changeSorter"
:prefix="$t('trains.sorter-prefix')"
/>
</div>
<div class="content_search">
<div class="search-box">
<input class="search-input" v-model="searchedTrain" :placeholder="$t('trains.search-train')" />
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedTrain = '')" />
</div>
<div class="search-box">
<input class="search-input" v-model="searchedDriver" :placeholder="$t('trains.search-driver')" />
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedDriver = '')" />
</div>
</div>
</div>
</div>
<section class="card_actions flex">
<action-button class="outlined">
{{ $t('filters.reset') }}
</action-button>
<action-button class="outlined" @click="closeCard">{{ $t('filters.close') }}</action-button>
</section>
</div>
</transition>
</section>
</template>
<script lang="ts">
import inputData from "../../data/options.json";
import { TrainFilter, computed, defineComponent, inject } from 'vue';
import { useI18n } from 'vue-i18n';
import SelectBox from '../Global/SelectBox.vue';
import ActionButton from '../Global/ActionButton.vue';
import { sorterOptions } from '../../data/trainOptions';
import imageMixin from "../../mixins/imageMixin";
export default defineComponent({
components: { ActionButton, SelectBox },
emits: ['changeFilterValue', 'invertFilters', 'resetFilters'],
mixins: [imageMixin],
data: () => ({
inputs: { ...inputData },
}),
setup() {
const isVisible = inject('isTrainOptionsCardVisible');
const { t } = useI18n();
let filterList = inject('filterList') as TrainFilter[];
const translatedSorterOptions = computed(() =>
sorterOptions.map(({ id }) => ({
id,
value: t(`trains.option-${id}`),
}))
);
return {
translatedSorterOptions,
searchedTrain: inject('searchedTrain') as string,
searchedDriver: inject('searchedDriver') as string,
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
filterList,
isVisible,
};
},
methods: {
closeCard() {
this.isVisible = false;
},
toggleCard() {
this.isVisible = !this.isVisible;
},
changeSorter(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id;
this.sorterActive.dir = -1;
},
toggleFilter(filter: TrainFilter) {
filter.isActive = !filter.isActive;
},
setFilterOnly(filter: TrainFilter) {
this.filterList.forEach((f) => (f.isActive = f.id == filter.id));
},
resetFilters() {
this.filterList.forEach((f) => (f.isActive = true));
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive';
@import '../../styles/card';
.card {
section {
margin: 0.5em 0;
}
&_title {
font-size: 2em;
font-weight: 700;
color: $accentCol;
margin: 0.5em 0;
text-align: center;
}
}
</style>
+8 -5
View File
@@ -60,7 +60,9 @@
<b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span> <b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span>
</div> </div>
<span v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine && !/sbl/gi.test(stop.departureLine!)"> <span
v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine && !/sbl/gi.test(stop.departureLine!)"
>
{{ stop.departureLine }} {{ stop.departureLine }}
</span> </span>
@@ -175,10 +177,6 @@ $stopNameClr: #22a8d1;
.train-schedule { .train-schedule {
padding: 0 0.25em; padding: 0 0.25em;
@include smallScreen() {
font-size: 1.1em;
}
} }
.train-stock { .train-stock {
@@ -198,6 +196,11 @@ ul.stock-list {
color: #aaa; color: #aaa;
font-size: 0.9em; font-size: 0.9em;
} }
img {
max-height: 60px;
max-width: 320px;
}
} }
.schedule-wrapper { .schedule-wrapper {
+44 -50
View File
@@ -8,6 +8,11 @@
{{ $t('trains.no-trains') }} {{ $t('trains.no-trains') }}
</div> </div>
<div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length != 0">
<b class="warning-timeout">?</b>
{{ $t('trains.timeout') }}
</div>
<ul class="train-list"> <ul class="train-list">
<li <li
class="train-row" class="train-row"
@@ -25,39 +30,30 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, inject, Ref, computed } from 'vue'; import { computed, defineComponent, inject, PropType, Ref } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import returnBtnMixin from '../../mixins/returnBtnMixin'; import returnBtnMixin from '../../mixins/returnBtnMixin';
import Train from '../../scripts/interfaces/Train'; import Train from '../../scripts/interfaces/Train';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import TrainModal from '../Global/TrainModal.vue';
import TrainInfo from './TrainInfo.vue'; import TrainInfo from './TrainInfo.vue';
import TrainSchedule from './TrainSchedule.vue';
export default defineComponent({ export default defineComponent({
components: { components: { Loading, TrainInfo },
TrainSchedule,
TrainInfo,
Loading,
TrainModal,
},
mixins: [returnBtnMixin, modalTrainMixin],
props: { props: {
trains: { trains: {
type: Array as () => Train[], type: Array as PropType<Train[]>,
required: true, required: true,
}, },
}, },
mixins: [returnBtnMixin, modalTrainMixin],
setup(props) { setup(props) {
const store = useStore(); const store = useStore();
const searchedTrain = inject('searchedTrain') as Ref<string>; const searchedTrain = inject('searchedTrain') as Ref<string>;
const searchedDriver = inject('searchedDriver') as Ref<string>; const searchedDriver = inject('searchedDriver') as Ref<string>;
const currentTrains = computed(() => { const currentTrains = computed(() => {
return props.trains; return props.trains;
}); });
@@ -67,53 +63,32 @@ export default defineComponent({
searchedDriver, searchedDriver,
currentTrains, currentTrains,
store, store,
sorterActive: inject('sorterActive') as {
sorterActive: inject('sorterActive') as { id: string | number; dir: number }, id: string | number;
dir: number;
},
distanceLimitExceeded: computed( distanceLimitExceeded: computed(
() => props.trains.findIndex(({ timetableData }) => timetableData && timetableData.routeDistance > 200) != -1 () => props.trains.findIndex(({ timetableData }) => timetableData && timetableData.routeDistance > 200) != -1
), ),
}; };
}, },
computed: {
trainNumbersWithTimeouts() {
return this.store.trainList.filter((train) => train.isTimeout).map((train) => train.trainNo);
},
},
activated() { activated() {
const query = this.$route.query; const query = this.$route.query;
if (query.trainNo && query.driverName) { if (query.trainNo && query.driverName) {
this.searchedDriver = query.driverName.toString(); this.searchedDriver = query.driverName.toString();
this.searchedTrain = query.trainNo.toString(); this.searchedTrain = query.trainNo.toString();
setTimeout(() => { setTimeout(() => {
this.selectModalTrain(query.driverName + <string>query.trainNo); this.selectModalTrain(query.driverName! + query.trainNo!.toString());
}, 20); }, 20);
} }
}, },
methods: {
enter(el: HTMLElement) {
const maxHeight = getComputedStyle(el).height;
el.style.height = '0px';
getComputedStyle(el);
setTimeout(() => {
el.style.height = maxHeight;
}, 10);
},
afterEnter(el: HTMLElement) {
el.style.height = 'auto';
},
leave(el: HTMLElement) {
el.style.height = getComputedStyle(el).height;
setTimeout(() => {
el.style.height = '0px';
}, 10);
},
},
}); });
</script> </script>
@@ -139,11 +114,10 @@ export default defineComponent({
text-align: center; text-align: center;
padding: 1em 0; padding: 1em 0;
margin: 1em 0;
font-size: 1.5em; font-size: 1.5em;
background: #333; background: #1a1a1a;
} }
img.train-image { img.train-image {
@@ -156,12 +130,32 @@ img.train-image {
background: var(--clr-warning); background: var(--clr-warning);
} }
.timeouts-warning {
background-color: #333;
font-weight: bold;
font-size: 1.05em;
margin-bottom: 0.5em;
padding: 0.5em;
}
.warning-timeout {
background-color: #be3728;
color: white;
display: inline-block;
text-align: center;
width: 1.25em;
height: 1.25em;
border-radius: 50%;
}
.train { .train {
&-list { &-list {
overflow: auto; overflow: auto;
margin-top: 1em;
@include smallScreen() { @include smallScreen() {
width: 100%; width: 100%;
} }
@@ -0,0 +1,28 @@
import { JournalFilterType } from "../../scripts/enums/JournalFilterType";
import { JournalTimetableFilter } from "../../types/Journal/JournalTimetablesTypes";
export const journalTimetableFilters: JournalTimetableFilter[] = [
{
id: JournalFilterType.all,
filterSection: 'timetable-status',
isActive: true,
},
{
id: JournalFilterType.active,
filterSection: 'timetable-status',
isActive: false,
},
{
id: JournalFilterType.fulfilled,
filterSection: 'timetable-status',
isActive: false,
},
{
id: JournalFilterType.abandoned,
filterSection: 'timetable-status',
isActive: false,
},
];
@@ -1,5 +1,5 @@
import { TrainFilter } from "vue"; import { TrainFilterType } from '../../scripts/enums/TrainFilterType';
import { TrainFilterType } from "../scripts/enums/TrainFilterType"; import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
export const trainFilters: TrainFilter[] = [ export const trainFilters: TrainFilter[] = [
{ {
@@ -56,5 +56,5 @@ export const sorterOptions = [
{ {
id: 'length', id: 'length',
value: 'długość', value: 'długość',
} },
]; ];
-30
View File
@@ -1,30 +0,0 @@
import { JournalFilter } from "vue";
import { JournalFilterType } from "../scripts/enums/JournalFilterType";
export const journalTimetableFilters: JournalFilter[] = [
{
id: JournalFilterType.all,
filterSection: "timetable-status",
isActive: true
},
{
id: JournalFilterType.active,
filterSection: "timetable-status",
isActive: false
},
{
id: JournalFilterType.fulfilled,
filterSection: "timetable-status",
isActive: false
},
{
id: JournalFilterType.abandoned,
filterSection: "timetable-status",
isActive: false
},
]
export const journalDispatcherFilters: JournalFilter[] = []
-9
View File
@@ -198,15 +198,6 @@
"section": "status", "section": "status",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
},
{
"id": "troll",
"name": "troll",
"iconName": "",
"section": "troll",
"value": true,
"defaultValue": true
} }
], ],
"sliders": [ "sliders": [
+65 -47
View File
@@ -72,7 +72,52 @@
}, },
"options": { "options": {
"filters": "FILTERS", "filters": "FILTERS",
"donate": "DONATE" "donate": "DONATE",
"search-button": "Search",
"reset-button": "Reset",
"sort-title": "SORT BY:",
"filter-title": "FILTER BY:",
"search-title": "SEARCH:",
"search-train-no": "Train no. / #",
"search-train": "Train no.",
"search-driver": "Driver name",
"search-dispatcher": "Dispatcher name",
"search-station": "Scenery name",
"search-author": "Timetable author name",
"search-date": "Timetable date (CEST / GMT+2)",
"sort-mass": "mass",
"sort-speed": "speed",
"sort-length": "length",
"sort-distance": "distance",
"sort-timetable": "train no.",
"sort-progress": "route progress",
"sort-delay": "current delay",
"sort-total-stops": "total stops",
"sort-beginDate": "date",
"sort-timetableId": "timetable ID",
"sort-timestampFrom": "date",
"sort-duration": "duration",
"filter-comments": "COMMENTS",
"filter-twr": "TWR",
"filter-skr": "SKR",
"filter-passenger": "PASSENGER",
"filter-freight": "FREIGHT",
"filter-other": "OTHER",
"filter-noTimetable": "NO TIMETABLE",
"filter-reset": "RESET FILTERS",
"filter-clear": "CLEAR FILTERS",
"filter-all": "ALL ENTRIES",
"filter-abandoned": "ABANDONED",
"filter-fulfilled": "FULFILLED",
"filter-active": "ACTIVE"
}, },
"filters": { "filters": {
"endingStatus": "ENDS SOON", "endingStatus": "ENDS SOON",
@@ -116,7 +161,7 @@
"hour": "h", "hour": "h",
"no-limit": "NO LIMIT", "no-limit": "NO LIMIT",
"include-selected": "INCLUDE SELECTED", "include-selected": "INCLUDE SELECTED",
"save": "SAVE FILTERS", "save": "SAVE FILTERS",
"reset": "RESET FILTERS", "reset": "RESET FILTERS",
"close": "CLOSE FILTERS" "close": "CLOSE FILTERS"
}, },
@@ -131,7 +176,8 @@
"users": "Drivers online", "users": "Drivers online",
"spawns": "Spawns online", "spawns": "Spawns online",
"timetables": "Active timetables", "timetables": "Active timetables",
"no-stations": "No stations to show here!" "no-stations": "No stations to show here!",
"scenery-search": "Search for scenery..."
}, },
"trains": { "trains": {
"no-trains": "No trains to show here!", "no-trains": "No trains to show here!",
@@ -150,28 +196,6 @@
"current-signal": "at signal", "current-signal": "at signal",
"current-track": "on track", "current-track": "on track",
"option-mass": "mass",
"option-speed": "speed",
"option-length": "length",
"option-distance": "distance",
"option-timetable": "train no.",
"option-progress": "route progress",
"option-delay": "current delay",
"option-comments": "comments",
"filter-comments": "comments",
"filter-twr": "TWR",
"filter-skr": "SKR",
"filter-passenger": "passenger",
"filter-freight": "freight",
"filter-other": "other",
"filter-noTimetable": "no timetable",
"filter-reset": "X RESET",
"sorter-prefix": "Sort: ",
"search-train": "Train no.",
"search-driver": "Driver name",
"delayed": "Delayed: ", "delayed": "Delayed: ",
"preponed": "Ahead of schedule: ", "preponed": "Ahead of schedule: ",
"on-time": "On time", "on-time": "On time",
@@ -195,7 +219,8 @@
"last-seen-min": "since one minute", "last-seen-min": "since one minute",
"last-seen-ago": "since {minutes} minutes", "last-seen-ago": "since {minutes} minutes",
"scenery-offline": "Offline ride" "scenery-offline": "Offline ride",
"timeout": "An error occured while trying to refresh SWDR timetable data!"
}, },
"journal": { "journal": {
"title": "DISPATCHER HISTORY", "title": "DISPATCHER HISTORY",
@@ -205,26 +230,6 @@
"section-timetables": "TIMETABLES", "section-timetables": "TIMETABLES",
"section-dispatchers": "DISPATCHERS", "section-dispatchers": "DISPATCHERS",
"search": "Search",
"search-train": "Train no. / #",
"search-driver": "Driver name",
"search-dispatcher": "Dispatcher name",
"search-station": "Scenery name",
"sort-prefix": "Sort: ",
"option-distance": "distance",
"option-total-stops": "total stops",
"option-beginDate": "date",
"option-timetableId": "timetable ID",
"option-timestampFrom": "date",
"option-duration": "duration",
"filter-all": "ALL ENTRIES",
"filter-abandoned": "ABANDONED",
"filter-fulfilled": "FULFILLED",
"filter-active": "ACTIVE",
"no-further-data": "No further data for current parameters", "no-further-data": "No further data for current parameters",
"loading-further-data": "Loading...", "loading-further-data": "Loading...",
@@ -239,7 +244,20 @@
"online-since": "ONLINE SINCE", "online-since": "ONLINE SINCE",
"duty-lasted": "The duty lasted", "duty-lasted": "The duty lasted",
"minutes": "{minutes} mins", "minutes": "{minutes} mins",
"hours": "{hours}h {minutes} mins" "hours": "{hours}h {minutes} mins",
"stock-info": "STOCK INFO",
"stock-length": "Length",
"stock-mass": "Mass",
"stock-max-speed": "Maximum registered speed",
"load-data": "Load further data...",
"stats-timetables": "TIMETABLES",
"stats-longest-timetable": "LONGEST TIMETABLE",
"stats-avg-timetable": "AVERAGE TIMETABLE LENGTH",
"stats-distance": "DISTANCE",
"stats-stations": "STATIONS"
}, },
"scenery": { "scenery": {
"users": "PLAYERS ONLINE", "users": "PLAYERS ONLINE",
+67 -47
View File
@@ -74,7 +74,53 @@
}, },
"options": { "options": {
"filters": "FILTRY", "filters": "FILTRY",
"donate": "WESPRZYJ" "donate": "WESPRZYJ",
"search-button": "Szukaj",
"reset-button": "Zresetuj",
"sort-title": "SORTUJ WG:",
"filter-title": "FILTRUJ WG:",
"search-title": "SZUKAJ:",
"search-train-no": "Nr pociągu",
"search-train": "Nr pociągu / #",
"search-driver": "Nick maszynisty",
"search-dispatcher": "Nick dyżurnego",
"search-station": "Nazwa scenerii",
"search-author": "Nick autora rozkładu jazdy",
"search-date": "Data rozkładu jazdy (czas polski)",
"sort-distance": "kilometraż",
"sort-total-stops": "stacje",
"sort-beginDate": "data",
"sort-timetableId": "ID rozkładu",
"sort-timestampFrom": "data",
"sort-duration": "czas dyżuru",
"sort-mass": "masa",
"sort-speed": "prędkość",
"sort-length": "długość",
"sort-timetable": "nr pociągu",
"sort-progress": "przebyta trasa",
"sort-delay": "opóźnienie",
"sort-comments": "uwagi ekspl.",
"filter-comments": "UWAGI EKSPLOATACYJNE",
"filter-twr": "TWR",
"filter-skr": "PRZEKR. SKRAJNIA",
"filter-passenger": "PASAŻERSKIE",
"filter-freight": "TOWAROWE",
"filter-other": "INNE",
"filter-noTimetable": "BEZ RJ",
"filter-reset": "ZRESETUJ FILTRY",
"filter-clear": "WYŁĄCZ FILTRY",
"filter-all": "WSZYSTKIE",
"filter-abandoned": "PORZUCONE",
"filter-fulfilled": "WYPEŁNIONE",
"filter-active": "AKTYWNE"
}, },
"filters": { "filters": {
"endingStatus": "KOŃCZY", "endingStatus": "KOŃCZY",
@@ -118,7 +164,7 @@
"hour": " godz.", "hour": " godz.",
"no-limit": "BEZ LIMITU", "no-limit": "BEZ LIMITU",
"include-selected": "POKAŻ ZAZNACZONE", "include-selected": "POKAŻ ZAZNACZONE",
"save": "ZAPISZ FILTRY", "save": "ZAPISZ FILTRY",
"reset": "RESETUJ FILTRY", "reset": "RESETUJ FILTRY",
"close": "ZAMKNIJ FILTRY" "close": "ZAMKNIJ FILTRY"
}, },
@@ -133,7 +179,8 @@
"users": "Maszyniści online", "users": "Maszyniści online",
"spawns": "Otwarte spawny", "spawns": "Otwarte spawny",
"timetables": "Aktywne rozkłady jazdy", "timetables": "Aktywne rozkłady jazdy",
"no-stations": "Brak stacji do wyświetlenia!" "no-stations": "Brak stacji do wyświetlenia!",
"scenery-search": "Wyszukaj scenerię..."
}, },
"trains": { "trains": {
"no-trains": "Brak pociągów do wyświetlenia!", "no-trains": "Brak pociągów do wyświetlenia!",
@@ -152,28 +199,6 @@
"current-signal": "przy semaforze", "current-signal": "przy semaforze",
"current-track": "na szlaku", "current-track": "na szlaku",
"option-mass": "masa",
"option-speed": "prędkość",
"option-length": "długość",
"option-distance": "kilometraż",
"option-timetable": "nr pociągu",
"option-progress": "przebyta trasa",
"option-delay": "opóźnienie",
"option-comments": "uwagi ekspl.",
"filter-comments": "uwagi ekspl.",
"filter-twr": "TWR",
"filter-skr": "SKR",
"filter-passenger": "pasażerskie",
"filter-freight": "towarowe",
"filter-other": "inne",
"filter-noTimetable": "bez RJ",
"filter-reset": "X RESETUJ",
"sorter-prefix": "Sortuj: ",
"search-train": "Numer pociągu",
"search-driver": "Nick maszynisty",
"delayed": "Opóźniony: ", "delayed": "Opóźniony: ",
"preponed": "Przed czasem: ", "preponed": "Przed czasem: ",
"on-time": "Planowo", "on-time": "Planowo",
@@ -197,7 +222,9 @@
"last-seen-min": "od minuty", "last-seen-min": "od minuty",
"last-seen-ago": "od {minutes} minut", "last-seen-ago": "od {minutes} minut",
"scenery-offline": "Przejazd offline" "scenery-offline": "Przejazd offline",
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR"
}, },
"journal": { "journal": {
"title": "HISTORIA DYŻURÓW", "title": "HISTORIA DYŻURÓW",
@@ -207,26 +234,6 @@
"section-timetables": "ROZKŁADY JAZDY", "section-timetables": "ROZKŁADY JAZDY",
"section-dispatchers": "DYŻURNI", "section-dispatchers": "DYŻURNI",
"search": "Szukaj",
"search-train": "Nr pociągu / #",
"search-driver": "Nick maszynisty",
"search-dispatcher": "Nick dyżurnego",
"search-station": "Nazwa scenerii",
"sort-prefix": "Sortuj: ",
"option-distance": "kilometraż",
"option-total-stops": "stacje",
"option-beginDate": "data",
"option-timetableId": "ID rozkładu",
"option-timestampFrom": "data",
"option-duration": "czas dyżuru",
"filter-all": "WSZYSTKIE",
"filter-abandoned": "PORZUCONE",
"filter-fulfilled": "WYPEŁNIONE",
"filter-active": "AKTYWNE",
"no-further-data": "Brak dalszych wyników dla podanych parametrów", "no-further-data": "Brak dalszych wyników dla podanych parametrów",
"loading-further-data": "Ładowanie...", "loading-further-data": "Ładowanie...",
@@ -241,7 +248,20 @@
"timetable-day": "Rozkład z dnia", "timetable-day": "Rozkład z dnia",
"timetable-active": "AKTYWNY", "timetable-active": "AKTYWNY",
"timetable-fulfilled": "WYPEŁNIONY", "timetable-fulfilled": "WYPEŁNIONY",
"timetable-abandoned": "PORZUCONY" "timetable-abandoned": "PORZUCONY",
"stock-info": "INFORMACJE O SKŁADZIE",
"stock-length": "Długość",
"stock-mass": "Masa",
"stock-max-speed": "Maks. zarejestrowana prędkość",
"load-data": "Pobierz dalszą historię...",
"stats-timetables": "ROZKŁADY JAZDY",
"stats-longest-timetable": "NAJDŁUŻSZY RJ",
"stats-avg-timetable": "ŚREDNIA DŁUGOŚĆ RJ",
"stats-distance": "DYSTANS",
"stats-stations": "STACJE"
}, },
"scenery": { "scenery": {
"users": "GRACZE ONLINE", "users": "GRACZE ONLINE",
+26
View File
@@ -0,0 +1,26 @@
import { defineComponent } from 'vue';
export default defineComponent({
data() {
return {
preventKeyDown: false,
};
},
activated() {
window.addEventListener('keydown', this.handleKeyDown);
},
deactivated() {
window.removeEventListener('keydown', this.handleKeyDown);
},
methods: {
onKeyDownFunction() {},
handleKeyDown(e: KeyboardEvent) {
if (!e.key) return;
if (e.key.toLowerCase() == 'f' && !this.preventKeyDown && !e.ctrlKey && !e.altKey) this.onKeyDownFunction();
},
},
});
+5 -4
View File
@@ -8,10 +8,6 @@ export default defineComponent({
}; };
}, },
mounted() {
console.log('Mixin mounted');
},
computed: { computed: {
chosenTrain() { chosenTrain() {
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId); return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId);
@@ -21,10 +17,15 @@ export default defineComponent({
methods: { methods: {
selectModalTrain(trainId: string) { selectModalTrain(trainId: string) {
this.store.chosenModalTrainId = trainId; this.store.chosenModalTrainId = trainId;
document.body.classList.add('no-scroll');
}, },
closeModal() { closeModal() {
this.store.chosenModalTrainId = undefined; this.store.chosenModalTrainId = undefined;
setTimeout(() => {
document.body.classList.remove('no-scroll');
}, 150);
}, },
}, },
}); });
+2 -2
View File
@@ -25,10 +25,10 @@ export default defineComponent({
}, },
activated() { activated() {
window.addEventListener('scroll', this.handleScroll); window.addEventListener('wheel', this.handleScroll);
}, },
deactivated() { deactivated() {
window.removeEventListener('scroll', this.handleScroll); window.removeEventListener('wheel', this.handleScroll);
}, },
}); });
+2 -3
View File
@@ -12,13 +12,12 @@ const routes: Array<RouteRecordRaw> = [
path: '/trains', path: '/trains',
name: 'TrainsView', name: 'TrainsView',
component: () => import('../views/TrainsView.vue'), component: () => import('../views/TrainsView.vue'),
props: (route) => ({ train: route.query.train, driver: route.query.driver }), props: (route) => ({ train: route.query.train, driver: route.query.driver, trainId: route.query.trainId }),
}, },
{ {
path: '/scenery', path: '/scenery',
name: 'SceneryView', name: 'SceneryView',
component: () => import('../views/SceneryView.vue'), component: () => import('../views/SceneryView.vue'),
props: true,
}, },
{ {
path: '/journal', path: '/journal',
@@ -59,7 +58,7 @@ const router = createRouter({
scrollBehavior(to, from) { scrollBehavior(to, from) {
if (to.name == 'SceneryView' && from.name) return { el: `.app_main` }; if (to.name == 'SceneryView' && from.name) return { el: `.app_main` };
if (from.name == 'SceneryView' && to.name == 'StationsView') return { el: `.last-selected`, top: 20 }; // if (from.name == 'SceneryView' && to.name == 'StationsView') return { el: `.last-selected`, top: 20 };
}, },
history: createWebHistory(), history: createWebHistory(),
routes, routes,
+1 -1
View File
@@ -1,4 +1,4 @@
export const enum DataStatus { export enum DataStatus {
Initialized = -1, Initialized = -1,
Loading = 0, Loading = 0,
Error = 1, Error = 1,
+2 -1
View File
@@ -19,9 +19,10 @@ export default interface Train {
online: boolean; online: boolean;
lastSeen: number; lastSeen: number;
region: string; region: string;
cars: string[]; cars: string[];
isTimeout: boolean;
timetableData?: { timetableData?: {
timetableId: number; timetableId: number;
category: string; category: string;
@@ -1,4 +1,6 @@
export interface DispatcherHistory { export interface DispatcherHistory {
id: string;
currentDuration: number; currentDuration: number;
dispatcherId: number; dispatcherId: number;
dispatcherName: string; dispatcherName: string;
@@ -26,6 +26,15 @@ export interface TimetableHistory {
authorName?: string; authorName?: string;
authorId?: number; authorId?: number;
stockString?: string;
stockMass?: number;
stockLength?: number;
maxSpeed?: number;
hashesString?: string;
currentSceneryName?: string;
currentSceneryHash?: string;
} }
export interface SceneryTimetableHistory { export interface SceneryTimetableHistory {
@@ -21,6 +21,7 @@ export default interface TrainAPIData {
lastSeen: number; lastSeen: number;
region: string; region: string;
isTimeout: boolean;
timetable?: { timetable?: {
timetableId: number; timetableId: number;
+7
View File
@@ -23,6 +23,13 @@ export default class StorageManager {
window.localStorage.setItem(key, val); window.localStorage.setItem(key, val);
} }
static setValue(key: string, val: any) {
if (typeof val == 'boolean') this.setBooleanValue(key, val);
else if (typeof val == 'number') this.setNumericValue(key, val);
else if (typeof val == 'string') this.setStringValue(key, val);
else this.setStringValue(key, val);
}
static removeValue(key: string) { static removeValue(key: string) {
window.localStorage.removeItem(key); window.localStorage.removeItem(key);
} }
+1 -1
View File
@@ -1,4 +1,4 @@
import { TrainFilter } from "vue"; import { TrainFilter } from "../../types/Trains/TrainOptionsTypes";
import { TrainFilterType } from "../enums/TrainFilterType"; import { TrainFilterType } from "../enums/TrainFilterType";
import Train from "../interfaces/Train"; import Train from "../interfaces/Train";
import TrainStop from "../interfaces/TrainStop"; import TrainStop from "../interfaces/TrainStop";
@@ -1,9 +1,14 @@
import Filter from '../interfaces/Filter'; import { defineStore } from 'pinia';
import Station from '../interfaces/Station'; import inputData from '../data/options.json';
import StorageManager from './storageManager'; import Filter from '../scripts/interfaces/Filter';
import Station from '../scripts/interfaces/Station';
import StorageManager from '../scripts/managers/storageManager';
const sortStations = (a: Station, b: Station, sorter: { index: number; dir: number }) => { const sortStations = (a: Station, b: Station, sorter: { index: number; dir: number }) => {
switch (sorter.index) { switch (sorter.index) {
case 0:
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
case 1: case 1:
if ((a.generalInfo?.reqLevel || 0) > (b.generalInfo?.reqLevel || 0)) return sorter.dir; if ((a.generalInfo?.reqLevel || 0) > (b.generalInfo?.reqLevel || 0)) return sorter.dir;
if ((a.generalInfo?.reqLevel || 0) < (b.generalInfo?.reqLevel || 0)) return -sorter.dir; if ((a.generalInfo?.reqLevel || 0) < (b.generalInfo?.reqLevel || 0)) return -sorter.dir;
@@ -50,7 +55,7 @@ const sortStations = (a: Station, b: Station, sorter: { index: number; dir: numb
break; break;
} }
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name); return a.name.localeCompare(b.name);
}; };
const filterStations = (station: Station, filters: Filter) => { const filterStations = (station: Station, filters: Filter) => {
@@ -91,7 +96,7 @@ const filterStations = (station: Station, filters: Filter) => {
const routes = station.generalInfo.routes; const routes = station.generalInfo.routes;
const availability = station.generalInfo.availability; const availability = station.generalInfo.availability;
if (filters['abandoned'] && availability == 'abandoned') return returnMode; if (filters['abandoned'] && availability == 'abandoned' && !station.onlineInfo) return returnMode;
if (availability == 'default' && filters['default']) return returnMode; if (availability == 'default' && filters['default']) return returnMode;
if ( if (
@@ -183,8 +188,7 @@ const filterStations = (station: Station, filters: Filter) => {
return true; return true;
}; };
export default class StationFilterManager { const filterInitStates: Filter = {
private filterInitStates: Filter = {
default: false, default: false,
notDefault: false, notDefault: false,
real: false, real: false,
@@ -224,28 +228,18 @@ export default class StationFilterManager {
authors: '', authors: '',
onlineFromHours: 0, onlineFromHours: 0,
};
export const useStationFiltersStore = defineStore('stationFiltersStore', {
state() {
return {
inputs: inputData,
filters: { ...filterInitStates },
sorterActive: { index: 0, dir: 1 },
}; };
},
private filters: Filter = { ...this.filterInitStates }; actions: {
private sorter: { index: number; dir: number } = { index: 0, dir: 1 };
checkFilters() {
if (!StorageManager.isRegistered('options_saved')) return;
Object.keys(this.filterInitStates).forEach((filterKey) => {
if (StorageManager.isRegistered(filterKey)) return;
const filterType = typeof this.filterInitStates[filterKey];
if (filterType === 'boolean')
StorageManager.setBooleanValue(filterKey, !this.filterInitStates[filterKey] as boolean);
if (filterType === 'number')
StorageManager.setNumericValue(filterKey, this.filterInitStates[filterKey] as number);
});
}
getFilteredStationList(stationList: Station[], region: string): Station[] { getFilteredStationList(stationList: Station[], region: string): Station[] {
return stationList return stationList
.map((station) => { .map((station) => {
@@ -256,37 +250,56 @@ export default class StationFilterManager {
return station; return station;
}) })
.filter((station) => filterStations(station, this.filters)) .filter((station) => filterStations(station, this.filters))
.sort((a, b) => sortStations(a, b, this.sorter)); .sort((a, b) => sortStations(a, b, this.sorterActive));
} },
changeFilterValue(filter: { name: string; value: number }) { setupFilters() {
if (!StorageManager.isRegistered('options_saved')) return;
this.inputs.options.forEach((option) => {
if (!StorageManager.isRegistered(option.id)) return;
const savedValue = StorageManager.getBooleanValue(option.id);
this.filters[option.id] = savedValue;
option.value = !savedValue;
});
this.inputs.sliders.forEach((slider) => {
if (!StorageManager.isRegistered(slider.name)) return;
const savedValue = StorageManager.getNumericValue(slider.name);
this.filters[slider.name] = savedValue;
slider.value = savedValue;
});
},
changeFilterValue(filter: { name: string; value: any }) {
this.filters[filter.name] = filter.value; this.filters[filter.name] = filter.value;
// if(filter.name == 'authors') if (StorageManager.isRegistered('options_saved')) StorageManager.setValue(filter.name, filter.value);
} },
resetFilters() { resetFilters() {
this.filters = { ...this.filterInitStates }; this.filters = { ...filterInitStates };
}
invertFilters() { this.inputs.options.forEach((option) => {
Object.keys(this.filters).forEach((prop) => { option.value = option.defaultValue;
if (typeof this.filters[prop] !== 'boolean') return; StorageManager.setBooleanValue(option.name, !option.defaultValue);
this.filters[prop] = !this.filters[prop];
}); });
}
this.inputs.sliders.forEach((slider) => {
slider.value = slider.defaultValue;
StorageManager.setNumericValue(slider.name, slider.defaultValue);
});
},
changeSorter(index: number) { changeSorter(index: number) {
if (index > 4 && index < 7) return; if (index > 4 && index < 7) return;
if (index == this.sorter.index) this.sorter.dir = -1 * this.sorter.dir; if (index == this.sorterActive.index) this.sorterActive.dir = -1 * this.sorterActive.dir;
else this.sorter.dir = 1; else this.sorterActive.dir = 1;
this.sorter.index = index; this.sorterActive.index = index;
} },
},
getSorter() { });
return this.sorter;
}
}
+4 -1
View File
@@ -17,6 +17,7 @@ import {
} from '../scripts/utils/storeUtils'; } from '../scripts/utils/storeUtils';
import { APIData, StationJSONData, StoreState } from './storeTypes'; import { APIData, StationJSONData, StoreState } from './storeTypes';
export const useStore = defineStore('store', { export const useStore = defineStore('store', {
state: () => state: () =>
({ ({
@@ -51,7 +52,9 @@ export const useStore = defineStore('store', {
trains: DataStatus.Loading, trains: DataStatus.Loading,
}, },
blockScroll: false,
listenerLaunched: false, listenerLaunched: false,
} as StoreState), } as StoreState),
actions: { actions: {
@@ -93,6 +96,7 @@ export const useStore = defineStore('store', {
cars: stock.slice(1), cars: stock.slice(1),
lastSeen: train.lastSeen, lastSeen: train.lastSeen,
isTimeout: train.isTimeout,
timetableData: timetable timetableData: timetable
? { ? {
@@ -395,4 +399,3 @@ export const useStore = defineStore('store', {
}, },
}, },
}); });
+3 -3
View File
@@ -1,12 +1,11 @@
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { DataStatus } from '../scripts/enums/DataStatus'; import { DataStatus } from '../scripts/enums/DataStatus';
import { DispatcherStatsAPIData } from '../scripts/interfaces/api/DispatcherStatsAPIData';
import { DriverStatsAPIData } from '../scripts/interfaces/api/DriverStatsAPIData';
import StationAPIData from '../scripts/interfaces/api/StationAPIData'; import StationAPIData from '../scripts/interfaces/api/StationAPIData';
import TrainAPIData from '../scripts/interfaces/api/TrainAPIData'; import TrainAPIData from '../scripts/interfaces/api/TrainAPIData';
import Station from '../scripts/interfaces/Station'; import Station from '../scripts/interfaces/Station';
import Train from '../scripts/interfaces/Train'; import Train from '../scripts/interfaces/Train';
import { DispatcherStatsAPIData } from '../scripts/interfaces/api/DispatcherStatsAPIData';
import { DriverStatsAPIData } from '../scripts/interfaces/api/DriverStatsAPIData';
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault'; export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
@@ -42,6 +41,7 @@ export interface StoreState {
}; };
listenerLaunched: boolean; listenerLaunched: boolean;
blockScroll: boolean;
} }
export interface APIData { export interface APIData {
+30 -10
View File
@@ -8,18 +8,28 @@
} }
&-enter-active { &-enter-active {
transition: all 150ms ease-out; transition: all 150ms 100ms ease-out;
} }
&-leave-active { &-leave-active {
transition: all 150ms ease-out; transition: all 150ms 100ms ease-out;
} }
} }
//Styles //Styles
.journal-wrapper { .list_wrapper {
width: 1350px; overflow-y: auto;
height: 90vh;
min-height: 550px;
padding-right: 0.2em;
}
.journal_wrapper {
max-width: 1350px;
width: 100%;
padding: 1em 0; padding: 1em 0;
} }
@@ -38,9 +48,9 @@
.journal_item, .journal_item,
.journal_warning { .journal_warning {
background: #202020; background-color: #1a1a1a;
padding: 1em; padding: 1em;
margin: 1em 0; margin-bottom: 1em;
} }
.journal_top-bar { .journal_top-bar {
@@ -49,13 +59,17 @@
align-items: center; align-items: center;
} }
button.btn { .btn--load-data {
padding: 0.5em 0.7em; padding: 0.5em 1em;
display: flex;
margin: 0 auto;
font-size: 1.2em;
} }
@include smallScreen() { @include smallScreen() {
.journal-wrapper { .list_wrapper {
font-size: 1.25em; font-size: 1.1em;
} }
.journal_top-bar { .journal_top-bar {
@@ -63,3 +77,9 @@ button.btn {
flex-wrap: wrap; flex-wrap: wrap;
} }
} }
@media (orientation: landscape) {
.list_wrapper {
font-size: 1em;
}
}
+42
View File
@@ -0,0 +1,42 @@
@import 'variables.scss';
@import 'responsive.scss';
.journal-stats {
background-color: #1a1a1a;
padding: 1em;
margin-bottom: 1em;
}
.info-stats {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
}
.stat-badge {
display: flex;
span {
background-color: $accentCol;
color: black;
font-weight: bold;
padding: 0.2em 0.5em;
}
span:first-child {
background-color: #333;
color: white;
}
}
@include smallScreen {
.journal-stats {
text-align: center;
}
.info-stats {
justify-content: center;
}
}
-2
View File
@@ -28,8 +28,6 @@
width: 600px; width: 600px;
padding: 0.5em 1em;
@include smallScreen { @include smallScreen {
width: 100%; width: 100%;
height: 80vh; height: 80vh;
+153
View File
@@ -0,0 +1,153 @@
@import 'responsive.scss';
@import 'variables.scss';
@import 'search_box.scss';
h1.option-title {
position: relative;
font-size: 1.1em;
margin: 0.7em 0 0.25em 0;
&::before {
content: '';
position: absolute;
top: -4px;
width: 50%;
height: 2px;
background-color: white;
border-radius: 2px;
}
}
.options-anim {
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(10px);
}
&-enter-active,
&-leave-active {
transition: all 150ms ease;
}
}
.bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10;
}
.filters-options {
position: relative;
margin-bottom: 0.5em;
}
.options_wrapper {
position: absolute;
background-color: $bgCol;
box-shadow: 0 5px 10px 2px #0f0f0f;
width: 100%;
max-width: 500px;
padding: 1em;
z-index: 100;
}
.options_sorters {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 0.25em 0.25em 0 0;
}
.options_filters {
display: flex;
flex-wrap: wrap;
margin: 0.5em 0 0 0;
}
.sort-option,
.filter-option {
margin: 0.25em 0.25em 0.25em 0;
}
.sort-option[data-selected='true'] {
color: $accentCol;
font-weight: bold;
}
.filter-option {
&#abandoned {
color: salmon;
}
&#fulfilled {
color: lightgreen;
}
&#active {
color: lightblue;
}
}
.search_content {
.search {
margin: 0.5em auto;
}
.search_actions {
display: flex;
gap: 0.5em;
margin: 1em 0;
width: 100%;
button {
width: 100%;
}
}
.search-box {
.search-exit {
position: absolute;
transform: translateY(-50%);
top: 50%;
right: 0;
}
}
}
@include smallScreen() {
h1 {
text-align: center;
&::before {
width: 75%;
left: 50%;
transform: translateX(-50%);
}
}
.options_wrapper {
font-size: 1.1em;
max-width: 100%;
}
.filter-option,
.sort-option {
margin: 0.25em 0.25em;
}
.options_filters,
.options_sorters {
justify-content: center;
}
}
+84 -52
View File
@@ -3,6 +3,7 @@
--clr-secondary: #2f2f2f; --clr-secondary: #2f2f2f;
--clr-bg: #4d4d4d; --clr-bg: #4d4d4d;
--clr-bg2: #1b1b1b;
--clr-accent: #1085b3; --clr-accent: #1085b3;
--clr-accent2: #ff3d5d; --clr-accent2: #ff3d5d;
@@ -12,6 +13,24 @@
--clr-error: #df3e3e; --clr-error: #df3e3e;
--clr-warning: #c59429; --clr-warning: #c59429;
font-size: 16px;
}
::-webkit-scrollbar {
width: 1rem;
height: 1rem;
background-color: transparent;
&-track {
border-radius: 0.5em;
background-color: #333;
}
&-thumb {
border-radius: 0.5em;
background-color: #666;
}
} }
html { html {
@@ -25,28 +44,14 @@ body {
padding: 0; padding: 0;
font-family: 'Quicksand', sans-serif; font-family: 'Quicksand', sans-serif;
overflow-y: scroll; overflow-y: scroll;
}
*:focus-visible { &.no-scroll {
outline: 1px solid white; overflow-y: hidden;
outline-offset: 1px; padding-right: 1rem;
}
:root { @include smallScreen() {
font-size: 16px; padding: 0;
}
::-webkit-scrollbar {
width: 0.5rem;
height: 0.5rem;
&-track {
background: #222;
} }
&-thumb {
border-radius: 1rem;
background: #777;
} }
} }
@@ -105,12 +110,12 @@ select {
} }
input { input {
border: 1px solid white;
background: none; background: none;
color: white; color: white;
font-size: 1em; font-size: 1em;
padding: 0.15em; background-color: #333;
padding: 0.15em 0.5em;
outline: none; outline: none;
@@ -129,6 +134,14 @@ input {
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
*:focus {
outline: none;
}
*:focus-visible {
outline: 1px solid $accentCol;
}
.title { .title {
color: $accentCol; color: $accentCol;
font-weight: 600; font-weight: 600;
@@ -182,54 +195,58 @@ ul {
} }
} }
.btn { button {
cursor: pointer;
color: white;
background: none; background: none;
cursor: pointer;
font-size: 1em;
&--text { display: flex;
color: white; align-items: center;
transition: color 0.3s; justify-content: center;
&:hover:not(:disabled), padding: 0.25em 0.5em;
&:focus:not(:disabled) {
color: $accentCol; transition: all 100ms ease;
}
button.btn--filled {
background-color: #333;
border-radius: 0.25em;
&:hover {
background-color: #2a2a2a;
} }
}
&.checked { button.btn--action {
color: var(--clr-primary); background-color: #424242;
font-weight: bold; border-radius: 0.25em;
}
&:hover {
background-color: #555;
} }
}
&--image { button.btn--option {
color: white;
transition: color 0.3s;
}
&--option {
cursor: pointer;
color: white; color: white;
background-color: #333; background-color: #333;
border-radius: 0.25em;
padding: 0.25em 0.5em;
&:hover:not(:disabled) {
background-color: #3c3c3c;
}
&.checked { &.checked {
color: var(--clr-primary); color: var(--clr-primary);
font-weight: bold; font-weight: bold;
background-color: #3c3c3c; background-color: #3c3c3c;
} }
} }
&:disabled { button.btn--image {
opacity: 0.65; font-weight: bold;
padding: 0.35em 0.75em;
img {
width: 1.5em;
margin-right: 0.5em;
vertical-align: middle;
} }
} }
@@ -274,3 +291,18 @@ ul {
transform: translateX(-50%); transform: translateX(-50%);
} }
} }
@include smallScreen {
::-webkit-scrollbar {
width: 0.5em;
height: 0.5em;
&-track {
background-color: #222;
}
&-thumb {
background-color: #777;
}
}
}
+52
View File
@@ -0,0 +1,52 @@
@import 'responsive.scss';
.search {
label {
display: block;
color: #ccc;
margin-bottom: 0.25em;
}
&-box {
position: relative;
display: flex;
border-radius: 0.5em;
min-width: 200px;
margin-right: 0.25em;
}
&-input {
border: none;
background-color: #424242;
padding: 0.35em 0.5em;
width: 100%;
}
&-exit {
background-color: #424242;
img {
vertical-align: middle;
height: 1.3em;
}
}
&-button {
width: 80%;
max-width: 300px;
}
@include smallScreen {
&-box,
&-button {
margin: 0.5em 0 0 0;
}
&-box {
width: 100%;
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
$primaryCol: #2c2c2c; $primaryCol: #2c2c2c;
$secondaryCol: #01e733; $secondaryCol: #01e733;
$bgCol: #4d4d4d; $bgCol: #1d1d1d;
$bgLigtherCol: #5b5b5b; $bgLigtherCol: #5b5b5b;
$errorCol: #ff1919; $errorCol: #ff1919;
@@ -0,0 +1,8 @@
export type JournalDispatcherSearcher = {
[key in 'search-dispatcher' | 'search-station' | 'search-date']: string;
};
export interface JournalDispatcherSorter {
id: 'timestampFrom' | 'duration';
dir: -1 | 1;
}
@@ -0,0 +1,16 @@
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
export type JorunalTimetableSearchType = {
[key in 'search-driver' | 'search-train' | 'search-date' | 'search-author']: string;
};
export interface JournalTimetableFilter {
id: JournalFilterType;
filterSection: string;
isActive: boolean;
}
export interface JournalTimetableSorter {
id: 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
dir: -1 | 1;
}
+6
View File
@@ -0,0 +1,6 @@
import { TrainFilterType } from "../../scripts/enums/TrainFilterType";
export interface TrainFilter {
id: TrainFilterType;
isActive: boolean;
}
+28 -10
View File
@@ -8,16 +8,16 @@
</action-button> </action-button>
</div> </div>
<div class="scenery-wrapper" v-if="stationInfo" ref="card-wrapper"> <div class="scenery-wrapper" v-if="stationInfo" ref="card-wrapper" :data-timetable-only="timetableOnly">
<div class="scenery-left"> <div class="scenery-left" v-if="!timetableOnly">
<div class="scenery-actions"> <div class="scenery-actions">
<button v-if="!timetableOnly" class="back-btn btn" :title="$t('scenery.return-btn')" @click="navigateTo('/')"> <button class="back-btn btn" :title="$t('scenery.return-btn')" @click="navigateTo('/')">
<img :src="getIcon('back')" alt="Back to scenery" /> <img :src="getIcon('back')" alt="Back to scenery" />
</button> </button>
</div> </div>
<SceneryHeader :station="stationInfo" /> <SceneryHeader :station="stationInfo" />
<SceneryInfo :station="stationInfo" :timetableOnly="timetableOnly" /> <SceneryInfo :station="stationInfo" />
</div> </div>
<div class="scenery-right"> <div class="scenery-right">
@@ -33,7 +33,12 @@
</div> </div>
<keep-alive> <keep-alive>
<component :is="currentViewCompontent" :station="stationInfo" :key="currentViewCompontent"></component> <component
:is="currentViewCompontent"
:station="stationInfo"
:timetableOnly="timetableOnly"
:key="currentViewCompontent"
></component>
</keep-alive> </keep-alive>
</div> </div>
</div> </div>
@@ -41,7 +46,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from 'vue'; import { computed, defineComponent, PropType } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import routerMixin from '../mixins/routerMixin'; import routerMixin from '../mixins/routerMixin';
import { useStore } from '../store/store'; import { useStore } from '../store/store';
@@ -68,7 +73,9 @@ export default defineComponent({
SceneryTimetablesHistory, SceneryTimetablesHistory,
SceneryDispatchersHistory, SceneryDispatchersHistory,
}, },
mixins: [routerMixin, imageMixin], mixins: [routerMixin, imageMixin],
data: () => ({ data: () => ({
viewModes: [ viewModes: [
{ {
@@ -89,17 +96,22 @@ export default defineComponent({
currentViewCompontent: 'SceneryTimetable', currentViewCompontent: 'SceneryTimetable',
onlineFrom: -1, onlineFrom: -1,
}), }),
activated() { activated() {
this.loadSelectedCheckpoint(); this.loadSelectedCheckpoint();
}, },
setup() { setup() {
const route = useRoute(); const route = useRoute();
const store = useStore(); const store = useStore();
const timetableOnly = computed(() => (route.query['timetable_only'] == '1' ? true : false));
const timetableOnly = computed(() => (route.query['timetableOnly'] == '1' ? true : false));
const isComponentVisible = computed(() => route.path === '/scenery'); const isComponentVisible = computed(() => route.path === '/scenery');
const stationInfo = computed(() => { const stationInfo = computed(() => {
return store.stationList.find((station) => station.name === route.query.station?.toString().replace(/_/g, ' ')); return store.stationList.find((station) => station.name === route.query.station?.toString().replace(/_/g, ' '));
}); });
return { return {
timetableOnly, timetableOnly,
isComponentVisible, isComponentVisible,
@@ -111,11 +123,13 @@ export default defineComponent({
setViewMode(componentName: string) { setViewMode(componentName: string) {
this.currentViewCompontent = componentName; this.currentViewCompontent = componentName;
}, },
loadSelectedCheckpoint() { loadSelectedCheckpoint() {
if (!this.stationInfo?.generalInfo?.checkpoints) return; if (!this.stationInfo?.generalInfo?.checkpoints) return;
if (this.stationInfo.generalInfo.checkpoints.length == 0) return; if (this.stationInfo.generalInfo.checkpoints.length == 0) return;
this.selectedCheckpoint = this.stationInfo.generalInfo.checkpoints[0].checkpointName; this.selectedCheckpoint = this.stationInfo.generalInfo.checkpoints[0].checkpointName;
}, },
selectCheckpoint(cp: { checkpointName: string }) { selectCheckpoint(cp: { checkpointName: string }) {
this.selectedCheckpoint = cp.checkpointName; this.selectedCheckpoint = cp.checkpointName;
}, },
@@ -169,8 +183,12 @@ button.back-btn {
max-width: 1700px; max-width: 1700px;
margin: 1rem 0; margin: 1rem 0;
text-align: center; text-align: center;
&[data-timetable-only='true'] {
grid-template-columns: 1fr;
max-width: 1000px;
}
} }
.scenery-left { .scenery-left {
@@ -209,15 +227,15 @@ button.back-btn {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-wrap: wrap;
gap: 0.75em;
.btn { .btn {
margin: 0.5em;
padding: 0.5em; padding: 0.5em;
box-shadow: 0 0 10px 4px #242424; box-shadow: 0 0 10px 4px #242424;
&[data-checked='true'] { &[data-checked='true'] {
color: var(--clr-primary); color: var(--clr-primary);
font-weight: bold;
} }
} }
} }
+29 -73
View File
@@ -3,39 +3,23 @@
<div class="wrapper"> <div class="wrapper">
<div class="body"> <div class="body">
<div class="options-bar"> <div class="options-bar">
<StationFilterCard <StationFilterCard :showCard="filterCardOpen" :exit="(filterCardOpen = false)" ref="filterCardRef" />
:showCard="filterCardOpen"
:exit="closeCard"
@changeFilterValue="changeFilterValue"
@invertFilters="invertFilters"
@resetFilters="resetFilters"
ref="filterCardRef"
/>
</div> </div>
<StationTable <StationTable :stations="computedStationList" />
:stations="computedStations"
:sorterActive="filterManager.getSorter()"
:setFocusedStation="setFocusedStation"
:changeSorter="changeSorter"
/>
</div> </div>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue';
import inputData from '../data/options.json';
import { computed, ComputedRef, defineComponent, reactive } from 'vue';
import { useStore } from '../store/store';
import StationFilterManager from '../scripts/managers/stationFilterManager';
import Station from '../scripts/interfaces/Station';
import StorageManager from '../scripts/managers/storageManager'; import StorageManager from '../scripts/managers/storageManager';
import StationTable from '../components/StationsView/StationTable.vue'; import StationTable from '../components/StationsView/StationTable.vue';
import StationFilterCard from '../components/StationsView/StationFilterCard.vue'; import StationFilterCard from '../components/StationsView/StationFilterCard.vue';
import SelectBox from '../components/Global/SelectBox.vue'; import SelectBox from '../components/Global/SelectBox.vue';
import { useStationFiltersStore } from '../store/stationFiltersStore';
import { useStore } from '../store/store';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -43,70 +27,42 @@ export default defineComponent({
StationFilterCard, StationFilterCard,
SelectBox, SelectBox,
}, },
data: () => ({ data: () => ({
filterCardOpen: false, filterCardOpen: false,
modalHidden: true, modalHidden: true,
STORAGE_KEY: 'options_saved', STORAGE_KEY: 'options_saved',
inputs: inputData, focusedStationName: '',
}), }),
setup() { setup() {
const store = useStore();
const filterManager = reactive(new StationFilterManager());
const focusedStationName = '';
const computedStations: ComputedRef<Station[]> = computed(
() => filterManager.getFilteredStationList(store.stationList, store.region.id)
// .filter((station) => !station.onlineInfo || station.onlineInfo.region == store.region.id)
);
return { return {
computedStations, filterStore: useStationFiltersStore(),
filterManager, store: useStore(),
focusedStationName,
}; };
}, },
computed: {
computedStationList() {
const list = this.filterStore.getFilteredStationList(this.store.stationList, this.store.region.id);
return list;
},
},
mounted() { mounted() {
if (!StorageManager.isRegistered(this.STORAGE_KEY)) return; this.filterStore.setupFilters();
// this.filterStore.inputs.options.forEach((option) => {
// const value = StorageManager.getBooleanValue(option.name);
// option.value = value;
// this.filterStore.changeFilterValue({ name: option.name, value: value });
// });
this.filterManager.checkFilters(); // this.filterStore.inputs.sliders.forEach((slider) => {
// const value = StorageManager.getNumericValue(slider.name);
this.inputs.options.forEach((option) => { // slider.value = value;
const value = StorageManager.getBooleanValue(option.name); // this.filterStore.changeFilterValue({ name: slider.name, value: value });
this.changeFilterValue({ name: option.name, value: value ? 0 : 1 }); // });
option.value = value;
});
this.inputs.sliders.forEach((slider) => {
const value = StorageManager.getNumericValue(slider.name);
this.changeFilterValue({ name: slider.name, value });
slider.value = value;
});
},
methods: {
toggleCardsState(name: string): void {
if (name == 'filter') {
this.filterCardOpen = !this.filterCardOpen;
}
},
changeSorter(index: number) {
this.filterManager.changeSorter(index);
},
changeFilterValue(filter: { name: string; value: number }) {
this.filterManager.changeFilterValue(filter);
},
resetFilters() {
this.filterManager.resetFilters();
},
invertFilters() {
this.filterManager.invertFilters();
},
closeCard() {
this.filterCardOpen = false;
},
setFocusedStation(name: string) {
this.focusedStationName = this.focusedStationName == name ? '' : name;
},
}, },
}); });
</script> </script>
+21 -19
View File
@@ -1,9 +1,7 @@
<template> <template>
<section class="trains-view"> <section class="trains-view">
<div class="wrapper"> <div class="trains_wrapper">
<div class="options-bar"> <TrainOptions :sorter-option-ids="['distance', 'progress', 'delay', 'mass', 'speed', 'length']" />
<train-options />
</div>
<TrainTable :trains="computedTrains" /> <TrainTable :trains="computedTrains" />
</div> </div>
@@ -11,14 +9,16 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, ComputedRef, defineComponent, provide, reactive, ref, TrainFilter } from 'vue'; import { computed, ComputedRef, defineComponent, provide, reactive, ref } from 'vue';
import TrainOptions from '../components/TrainsView/TrainOptions.vue'; import TrainOptions from '../components/TrainsView/TrainOptions.vue';
import TrainStats from '../components/TrainsView/TrainStats.vue'; import TrainStats from '../components/TrainsView/TrainStats.vue';
import TrainTable from '../components/TrainsView/TrainTable.vue'; import TrainTable from '../components/TrainsView/TrainTable.vue';
import { trainFilters } from '../data/trainOptions'; import { trainFilters } from '../constants/Trains/TrainOptionsConsts';
import modalTrainMixin from '../mixins/modalTrainMixin';
import Train from '../scripts/interfaces/Train'; import Train from '../scripts/interfaces/Train';
import { filteredTrainList } from '../scripts/managers/trainFilterManager'; import { filteredTrainList } from '../scripts/managers/trainFilterManager';
import { useStore } from '../store/store'; import { useStore } from '../store/store';
import { TrainFilter } from '../types/Trains/TrainOptionsTypes';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -27,6 +27,8 @@ export default defineComponent({
TrainOptions, TrainOptions,
}, },
mixins: [modalTrainMixin],
props: { props: {
train: { train: {
type: String, type: String,
@@ -37,6 +39,11 @@ export default defineComponent({
type: String, type: String,
required: false, required: false,
}, },
trainId: {
type: String,
required: false,
},
}, },
data: () => ({ data: () => ({
@@ -48,7 +55,6 @@ export default defineComponent({
const sorterActive = ref({ id: 'distance', dir: -1 }); const sorterActive = ref({ id: 'distance', dir: -1 });
const filterList = reactive([...trainFilters]) as TrainFilter[]; const filterList = reactive([...trainFilters]) as TrainFilter[];
const isTrainOptionsCardVisible = ref(false);
const searchedDriver = ref(''); const searchedDriver = ref('');
const searchedTrain = ref(''); const searchedTrain = ref('');
@@ -57,7 +63,6 @@ export default defineComponent({
provide('searchedDriver', searchedDriver); provide('searchedDriver', searchedDriver);
provide('sorterActive', sorterActive); provide('sorterActive', sorterActive);
provide('filterList', filterList); provide('filterList', filterList);
provide('isTrainOptionsCardVisible', isTrainOptionsCardVisible);
const computedTrains: ComputedRef<Train[]> = computed(() => { const computedTrains: ComputedRef<Train[]> = computed(() => {
return filteredTrainList( return filteredTrainList(
@@ -74,6 +79,7 @@ export default defineComponent({
searchedTrain, searchedTrain,
searchedDriver, searchedDriver,
sorterActive, sorterActive,
store,
}; };
}, },
@@ -82,10 +88,12 @@ export default defineComponent({
this.searchedTrain = this.train; this.searchedTrain = this.train;
this.searchedDriver = this.driver || ''; this.searchedDriver = this.driver || '';
} }
// if (this.train) {
// this.searchedTrain = this.train; this.$nextTick(() => {
// if(this.x) this.searchedDriver = this.x; if (this.trainId) {
// } this.selectModalTrain(this.trainId);
}
});
}, },
}); });
</script> </script>
@@ -98,14 +106,8 @@ export default defineComponent({
position: relative; position: relative;
} }
.wrapper { .trains_wrapper {
margin: 1rem auto; margin: 1rem auto;
max-width: 1350px; max-width: 1350px;
} }
@include smallScreen {
.options-bar {
font-size: 1.25em;
}
}
</style> </style>
-30
View File
@@ -1,30 +0,0 @@
import { ComponentCustomProperties } from 'vue'
import { Store } from 'vuex'
import { JournalFilterType } from './scripts/enums/JournalFilterType';
import { TrainFilterType } from './scripts/enums/TrainFilterType';
declare module '@vue/runtime-core' {
// declare your own store states
interface State {
count: number
}
// provide typings for `this.$store`
interface ComponentCustomProperties {
$store: Store<State>
}
// Train filter for TrainView
interface TrainFilter {
id: TrainFilterType;
isActive: boolean;
}
interface JournalFilter {
id: JournalFilterType;
filterSection: string;
isActive: boolean;
}
}
+1
View File
@@ -14,6 +14,7 @@
"ESNext", "ESNext",
"DOM" "DOM"
], ],
"types": ["vite/client"],
"skipLibCheck": true "skipLibCheck": true
}, },
"include": [ "include": [
+1001 -906
View File
File diff suppressed because it is too large Load Diff