chore: refreshed project

This commit is contained in:
2024-08-02 02:02:31 +02:00
parent a10f12e568
commit 03e4330ab5
27 changed files with 2977 additions and 1104 deletions
-2
View File
@@ -1,2 +0,0 @@
VITE_STACJOWNIK_API_URL=https://stacjownik.spythere.pl
VITE_USE_MOCK_DATA=0
+17
View File
@@ -0,0 +1,17 @@
/* eslint-env node */
module.exports = {
root: true,
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier/skip-formatting",
],
rules: {
"vue/multi-word-component-names": "off",
},
parserOptions: {
ecmaVersion: "latest",
},
};
+1
View File
@@ -26,3 +26,4 @@ dist-ssr
.firebase .firebase
mockData/ mockData/
.env
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}
+18 -9
View File
@@ -1,24 +1,33 @@
{ {
"name": "pragotron-td2", "name": "pragotron-td2",
"private": true, "private": true,
"version": "0.4.0", "version": "0.5.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"preview": "vite preview", "preview": "vite preview",
"deploy": "yarn build && firebase deploy --only hosting" "deploy": "yarn build && firebase deploy --only hosting",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"sass": "^1.55.0", "axios": "^1.6.2",
"vue": "^3.2.41", "pinia": "^2.1.7",
"vue-router": "4" "sass": "^1.69.5",
"vue": "^3.3.11",
"vue-router": "4.2.5"
}, },
"devDependencies": { "devDependencies": {
"@types/vue-router": "^2.0.0", "@types/vue-router": "^2.0.0",
"@vitejs/plugin-vue": "^3.2.0", "@vitejs/plugin-vue": "^4.5.2",
"typescript": "^4.6.4", "@vue/eslint-config-prettier": "^8.0.0",
"vite": "^3.2.1", "@vue/eslint-config-typescript": "^12.0.0",
"vue-tsc": "^1.0.9" "eslint": "^8.55.0",
"eslint-plugin-vue": "^9.19.2",
"prettier": "^3.1.1",
"typescript": "^5.3.3",
"vite": "^5.0.7",
"vue-tsc": "^1.8.25"
} }
} }
+3
View File
@@ -0,0 +1,3 @@
<svg width="800px" height="800px" viewBox="0 0 32 32" id="i-options" xmlns="http://www.w3.org/2000/svg" fill="white" stroke="white" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M28 6 L4 6 M28 16 L4 16 M28 26 L4 26 M24 3 L24 9 M8 13 L8 19 M20 23 L20 29" />
</svg>

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.
+31 -46
View File
@@ -1,13 +1,9 @@
<template> <template>
<div class="app_content"> <div class="app_content">
<nav class="navbar"> <Navbar :version="version" />
<router-link to="/">
Pragotron TD2 <span class="text--accent">v{{ VERSION }}</span> <sup>by Spythere</sup>
</router-link>
<!-- <button v-else class="back-btn btn--text" @click="selectedStation = null">&lt; powrót</button> -->
</nav>
<main> <main>
<button @click="testAudio">test audio</button>
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive> <keep-alive>
<component :is="Component" :key="$route.path"></component> <component :is="Component" :key="$route.path"></component>
@@ -18,27 +14,37 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from '@vue/runtime-core'; import { defineComponent } from 'vue';
import PragotronVue from './views/PragotronView.vue';
import IStationData from './types/ISceneryData';
import packageInfo from '../package.json'; import packageInfo from '../package.json';
import { useApiStore } from './stores/apiStore';
import Navbar from './components/Navbar.vue';
export default defineComponent({ export default defineComponent({
components: { components: { Navbar },
PragotronVue,
},
data: () => ({ data: () => ({
onlineStations: [] as IStationData[], version: packageInfo.version,
dataLoaded: false, apiStore: useApiStore()
VERSION: packageInfo.version,
}), }),
async mounted() { async mounted() {
this.dataLoaded = true; this.apiStore.fetchSceneriesData();
this.apiStore.fetchActiveData();
setInterval(() => {
this.apiStore.fetchActiveData();
}, 30000);
}, },
methods: {
testAudio() {
const audio = new Audio('../public/pragotron.mp3');
audio.play();
audio.loop = true;
}
}
}); });
</script> </script>
@@ -48,45 +54,24 @@ export default defineComponent({
.app_content { .app_content {
text-align: center; text-align: center;
display: flex; display: grid;
flex-direction: column; grid-template-rows: auto 1fr;
min-height: 100vh; min-height: 100vh;
overflow-x: hidden; overflow-x: hidden;
} }
nav {
flex: 0 1 40px;
font-size: 1.35em;
padding: 0.25em;
display: flex;
align-items: center;
background-color: $accentBg;
sup {
font-size: 0.8em;
color: $dimmedText;
}
button {
padding: 0;
}
}
main { main {
flex: 1 1 auto; padding: 1em;
overflow-x: hidden;
display: flex;
justify-content: center;
align-items: center;
} }
a { a {
text-decoration: none; text-decoration: none;
color: white; color: white;
a:hover {
color: gold;
}
} }
</style> </style>
+104
View File
@@ -0,0 +1,104 @@
<template>
<div class="dropdown" v-click-outside="() => (store.optionsOpen = false)">
<button class="btn--image" @click="store.optionsOpen = !store.optionsOpen">
<img src="/options.svg" alt="options" />
</button>
<transition name="dropdown-anim">
<div class="dropdown-body" v-if="store.optionsOpen">
<h3>Opcje</h3>
<hr />
<div style="margin: 0.5em 0">
<label>
<input type="checkbox" v-model="store.filters.nonPassenger" />
Relacje niepasażerskie
</label>
<label>
<input type="checkbox" v-model="store.filters.terminating" />
Relacje kończące bieg
</label>
</div>
<div v-if="isPragotronOpen">
<label for="checkpoint">
Posterunek:
<select id="checkpoint" v-model="store.selectedCheckpointName">
<option v-for="cp in store.selectedStation?.stationCheckpoints" :value="cp" :key="cp">
{{ cp }}
</option>
</select>
</label>
</div>
<div tabindex="0" @focus="() => (store.optionsOpen = false)"></div>
</div>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../stores/mainStore';
export default defineComponent({
data: () => ({
store: useMainStore()
}),
computed: {
isPragotronOpen() {
return this.$route.path == '/board';
}
}
});
</script>
<style lang="scss" scoped>
img {
max-width: 2em;
}
h3 {
font-size: 1.2em;
margin: 0;
}
.dropdown-bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 100;
}
.dropdown-body {
position: absolute;
top: 100%;
right: 0;
padding: 0.25em;
transform: translateY(0.5em);
width: 500px;
max-width: calc(100% - 0.5em);
z-index: 105;
background-color: #000000e1;
}
.dropdown-anim {
&-enter-active,
&-leave-active {
transition: all 90ms ease-out;
}
&-enter-from,
&-leave-to {
transform: translateY(20px);
opacity: 0;
}
}
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<nav class="navbar">
<div class="navbar-body">
<router-link class="brand" to="/">
Pragotron TD2 <span class="text--accent">v{{ version }}</span> <sup>by Spythere</sup>
</router-link>
<div class="options">
<Dropdown />
</div>
</div>
</nav>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../stores/mainStore';
import Dropdown from './Dropdown.vue';
export default defineComponent({
components: { Dropdown },
props: {
version: String
},
data() {
return {
store: useMainStore()
};
}
});
</script>
<style lang="scss" scoped>
@import '../styles.scss';
nav.navbar {
background-color: $accentBg;
padding: 0 0.5em;
sup {
font-size: 0.8em;
color: $dimmedText;
}
}
.navbar-body {
padding: 0.25em;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
margin: 0 auto;
max-width: 1400px;
}
.brand {
font-size: 1.25em;
}
</style>
+129 -1
View File
@@ -1 +1,129 @@
["", "Aleksandrów Kujawski","Arkadia Zdrój","Babimost","Bargowice","Bargowice Zachód","Horz Zdrój","Bełchów","Blaszki","Borki","Brakowice","Buczek","Buk","Bystra Woda","Cenorzyce Nowe","Chełmik Wołowski","Chlorkowice","Cis","Czerepy","Czermin","Dobrzyca DTA","Dobrzyca DTB","Dobrzyca DTC","Dobrzyniec","Dobrzyniec Mącice","Drzewko","Dziewoszyce","Falewo","Glinnik","Grabów Miasto","Grabów Wieś","Góra Włodowska","Głogowo","Głębce","Głęboszów","Imielin","Jordanowo","Karszynek","Kcynia","Kieły","Kolsko","Kowalewo","Krzemienice","Krzęcz","Kszęty","Kudowa Zdrój","Głowno","Domaniewice","Ozorków","Chociszew","Skrzynki","Wykno","Żywiec","Węgierska Górka","Łodygowice","Wilkowice Bystra","BB Leszczyny","Legno","Lewków","Ligota Grabowska","Ligota Trzeszcze","Lisiczki","Lisków","TEFAMA","Lisków Miasto","Lublinek","Lutol Suchy","Luzino","Lębork","Milówka","Modlinków","Motławy","Naterki","Okoń Główny","Orniki","Otwocko","Parów","Piaskowo","Pilichowice","Poreńsk","Radostowice","Radowice","Radzikowo","Rajcza","Razemsko","Rebrowo Dolne","Redlin Sudecki","Santok Zdrój","Sieniawka","Skawce","Sowi Bór","Sroka","Stare Lipowo","Przęsy","Starzynki","Stefanowo","Stryków","Strączki","Sulechów","Szadek","Sól","Tarkowo","Tartakowo","Testowo","Trawniczki","Tłoki","Wełtawa","Wielichowo Główne","Wielichowo Główne gt","Wielichowo Wieś","Wijewo","Wilczyca","Witaszyczki","Witonia","Wodnica","Wola","Wola Nowska","Wschodna","Zgierz","Zgierz Kontrewers","Zwardoń","Łask","Łaskarzew","Łebnino","Łęczyca","Żerniki","Żory"] [
"",
"Aleksandrów Kujawski",
"Arkadia Zdrój",
"Babimost",
"Bargowice",
"Bargowice Zachód",
"Horz Zdrój",
"Bełchów",
"Blaszki",
"Borki",
"Brakowice",
"Buczek",
"Buk",
"Bystra Woda",
"Cenorzyce Nowe",
"Chełmik Wołowski",
"Chlorkowice",
"Cis",
"Czerepy",
"Czermin",
"Dobrzyca DTA",
"Dobrzyca DTB",
"Dobrzyca DTC",
"Dobrzyniec",
"Dobrzyniec Mącice",
"Drzewko",
"Dziewoszyce",
"Falewo",
"Glinnik",
"Grabów Miasto",
"Grabów Wieś",
"Góra Włodowska",
"Głogowo",
"Głębce",
"Głęboszów",
"Imielin",
"Jordanowo",
"Karszynek",
"Kcynia",
"Kieły",
"Kolsko",
"Kowalewo",
"Krzemienice",
"Krzęcz",
"Kszęty",
"Kudowa Zdrój",
"Głowno",
"Domaniewice",
"Ozorków",
"Chociszew",
"Skrzynki",
"Wykno",
"Żywiec",
"Węgierska Górka",
"Łodygowice",
"Wilkowice Bystra",
"BB Leszczyny",
"Legno",
"Lewków",
"Ligota Grabowska",
"Ligota Trzeszcze",
"Lisiczki",
"Lisków",
"TEFAMA",
"Lisków Miasto",
"Lublinek",
"Lutol Suchy",
"Luzino",
"Lębork",
"Milówka",
"Modlinków",
"Motławy",
"Naterki",
"Okoń Główny",
"Orniki",
"Otwocko",
"Parów",
"Piaskowo",
"Pilichowice",
"Poreńsk",
"Radostowice",
"Radowice",
"Radzikowo",
"Rajcza",
"Razemsko",
"Rebrowo Dolne",
"Redlin Sudecki",
"Santok Zdrój",
"Sieniawka",
"Skawce",
"Sowi Bór",
"Sroka",
"Stare Lipowo",
"Przęsy",
"Starzynki",
"Stefanowo",
"Stryków",
"Strączki",
"Sulechów",
"Szadek",
"Sól",
"Tarkowo",
"Tartakowo",
"Testowo",
"Trawniczki",
"Tłoki",
"Wełtawa",
"Wielichowo Główne",
"Wielichowo Główne gt",
"Wielichowo Wieś",
"Wijewo",
"Wilczyca",
"Witaszyczki",
"Witonia",
"Wodnica",
"Wola",
"Wola Nowska",
"Wschodna",
"Zgierz",
"Zgierz Kontrewers",
"Zwardoń",
"Łask",
"Łaskarzew",
"Łebnino",
"Łęczyca",
"Żerniki",
"Żory"
]
-2
View File
@@ -1,2 +0,0 @@
{
}
+7
View File
@@ -0,0 +1,7 @@
import axios from 'axios';
const http = axios.create({
baseURL: 'https://stacjownik.spythere.eu'
});
export default http;
+20 -2
View File
@@ -1,6 +1,24 @@
import { createApp } from 'vue'; import { createApp, Directive } from 'vue';
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
import { createPinia } from 'pinia';
createApp(App).use(router).mount('#app'); const pinia = createPinia();
const clickOutsideDirective: Directive = {
mounted(el, binding) {
el.clickOutsideEvent = (event: Event) => {
if (!(el == event.target || el.contains(event.target))) {
binding.value();
}
};
document.addEventListener('click', el.clickOutsideEvent);
}
};
createApp(App)
.use(router)
.use(pinia)
.directive('click-outside', clickOutsideDirective)
.mount('#app');
+4 -4
View File
@@ -6,18 +6,18 @@ import PragotronView from './views/PragotronView.vue';
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {
path: '/', path: '/',
component: HomeView, component: HomeView
}, },
{ {
path: '/board', path: '/board',
component: PragotronView, component: PragotronView,
props: (route) => ({ stationName: route.query.name }), props: (route) => ({ stationName: route.query.name, region: route.query.region })
}, }
]; ];
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes, routes
}); });
export default router; export default router;
+53
View File
@@ -0,0 +1,53 @@
import { defineStore } from 'pinia';
import { API } from '../typings/api';
import http from '../http';
export enum DataStatus {
LOADING = 0,
LOADED = 1,
ERROR = 2
}
export const useApiStore = defineStore('api', {
state() {
return {
activeData: undefined as API.ActiveData.Response | undefined,
stationData: undefined as API.Sceneries.Response | undefined,
dataStatuses: {
activeData: DataStatus.LOADING,
stationData: DataStatus.LOADING
}
};
},
actions: {
async fetchActiveData() {
try {
const response = (await http.get<API.ActiveData.Response | undefined>('api/getActiveData'))
.data;
this.dataStatuses.activeData = DataStatus.LOADED;
this.activeData = response;
} catch (error) {
this.dataStatuses.activeData = DataStatus.ERROR;
console.error('Wystąpił błąd podczas pobierania danych:', error);
}
},
async fetchSceneriesData() {
try {
const response = (await http.get<API.Sceneries.Response | undefined>('api/getSceneries'))
.data;
this.dataStatuses.stationData = DataStatus.LOADED;
this.stationData = response;
} catch (error) {
this.dataStatuses.stationData = DataStatus.ERROR;
console.error('Wystąpił błąd podczas pobierania danych:', error);
}
}
}
});
+52
View File
@@ -0,0 +1,52 @@
import { defineStore } from 'pinia';
import ISceneryData from '../types/ISceneryData';
import { useApiStore } from './apiStore';
export enum Region {
PL1 = 'eu',
PL2 = 'cae',
CZE = 'usw',
DE = 'us ',
ENG = 'ru'
}
export const regionNames = {
[Region.PL1]: 'PL1',
[Region.PL2]: 'PL2',
[Region.CZE]: 'CZE',
[Region.DE]: 'DE',
[Region.ENG]: 'ENG'
};
export const useMainStore = defineStore('main', {
state() {
return {
region: Region.PL1,
optionsOpen: false,
filters: {
nonPassenger: true,
terminating: true
},
selectedStationName: '',
selectedCheckpointName: ''
};
},
getters: {
selectedStation(state): ISceneryData | undefined {
const apiStore = useApiStore();
const station = apiStore.stationData?.find(({ name }) => name == state.selectedStationName);
if (!station) return undefined;
return {
stationName: station.name,
stationCheckpoints:
station.checkpoints && station.checkpoints.length > 0
? station.checkpoints.split(';')
: [station.name],
nameAbbreviation: ''
};
}
}
});
+67 -7
View File
@@ -1,16 +1,16 @@
@import url('https://fonts.googleapis.com/css2?family=Monda:wght@400;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Monda:wght@400;700&display=swap');
@import 'theme.scss'; @import 'theme.scss';
body, body,
html { html {
background: $primaryBg; background: $primaryBg;
min-height: 100vh; min-height: 100vh;
color: $primaryText; color: $primaryText;
padding: 0; padding: 0;
margin: 0; margin: 0;
font-size: 16px;
} }
*, *,
@@ -19,22 +19,82 @@ html {
font-family: 'Monda', sans-serif; font-family: 'Monda', sans-serif;
} }
input,
select,
option,
label {
font-size: inherit;
font-family: inherit;
}
ul { ul {
list-style: none; list-style: none;
padding: 0;
} }
button { button {
cursor: pointer;
color: white;
font-size: 1em;
&.btn--text {
background: none; background: none;
border: none; border: none;
outline: none; outline: none;
cursor: pointer;
font-size: 1em;
color: white;
background-color: #1b1b1b;
&:hover {
background-color: #252525;
} }
&:focus-visible { &:focus-visible {
color: $accentText; color: $accentText;
} }
&.btn--image {
display: flex;
background: none;
img {
vertical-align: middle;
}
&:focus-visible {
outline: 1px solid $accentText;
}
}
}
// Input radio
.g-selector {
label {
background-color: #202020;
cursor: pointer;
display: flex;
justify-content: center;
&:hover {
background-color: #2b2b2b;
}
span {
width: 100%;
padding: 0.25em 0.5em;
}
input[type='radio'] {
opacity: 0;
width: 1px;
height: 1px;
position: absolute;
&:checked + span {
color: $accentText;
}
&:focus-visible + span {
outline: 1px solid white;
}
}
}
} }
+4 -4
View File
@@ -5,7 +5,7 @@ interface ITableRowValues {
// routeTo, routeVia, date1, date2, date3, date4 // routeTo, routeVia, date1, date2, date3, date4
currentRowIndexes: [number, number, number, number, number, number]; currentRowIndexes: [number, number, number, number, number, number];
dateDigits: string[], dateDigits: string[];
} }
export interface ITableRow { export interface ITableRow {
@@ -20,9 +20,9 @@ export interface ITableRow {
arrivalTimestamp: number; arrivalTimestamp: number;
departureTimestamp: number; departureTimestamp: number;
delayMinutes: number, delayMinutes: number;
date?: Date, date?: Date;
dateDigits: string[], dateDigits: string[];
tableValues: ITableRowValues; tableValues: ITableRowValues;
} }
+136
View File
@@ -0,0 +1,136 @@
export namespace API {
export namespace ActiveData {
export interface Response {
activeSceneries: ActiveScenery[];
trains: ActiveTrain[];
}
export interface ActiveScenery {
dispatcherId: number;
dispatcherName: string;
dispatcherIsSupporter: boolean;
stationName: string;
stationHash: string;
region: string;
maxUsers: number;
currentUsers: number;
spawn: number;
lastSeen: number;
dispatcherExp: number;
nameFromHeader: string;
spawnString: string | null;
networkConnectionString: string;
isOnline: number;
dispatcherRate: number;
dispatcherStatus: number;
}
export interface ActiveTrain {
trainNo: number;
mass: number;
length: number;
speed: number;
stockString: string;
signal: string;
distance: number;
connectedTrack: string;
driverName: string;
driverId: number;
driverIsSupporter: boolean;
driverLevel?: number;
currentStationName: string;
currentStationHash?: string;
online: number;
lastSeen: number;
region: string;
isTimeout: boolean;
timetable?: Timetable;
}
export interface TimetableStop {
stopName: string;
stopNameRAW: string;
stopType: string;
stopDistance: number;
pointId: string;
mainStop: boolean;
arrivalLine: string | null;
arrivalTimestamp: number;
arrivalRealTimestamp: number;
arrivalDelay: number;
departureLine: string | null;
departureTimestamp: number;
departureRealTimestamp: number;
departureDelay: number;
comments?: any;
beginsHere: boolean;
terminatesHere: boolean;
confirmed: number;
stopped: number;
stopTime: number | null;
}
export interface Timetable {
timetableId: number;
category: string;
route: string;
stopList: TimetableStop[];
TWR: boolean;
SKR: boolean;
sceneries: string[];
}
}
export namespace Sceneries {
export type Response = Scenery[];
export interface Scenery {
createdAt: string;
updatedAt?: string;
id: number;
name: string;
SUP: boolean;
authors: string;
availability: string;
backupJSON: any;
checkpoints?: string;
controlType: string;
lines?: string;
project?: string;
reqLevel: number;
routes?: string;
routesInfo: RoutesInfo[];
signalType: string;
supportersOnly?: boolean;
url?: string;
projectUrl?: string;
hash?: string;
abbr: string;
hidden: boolean;
}
export interface RoutesInfo {
routeName: string;
isElectric: boolean;
isInternal: boolean;
isRouteSBL: boolean;
routeSpeed: number;
routeLength: number;
routeTracks: number;
}
}
}
View File
+96 -39
View File
@@ -1,75 +1,132 @@
<template> <template>
<div class="home-view"> <div class="home-view">
<div class="scenery-selector"> <div>
<h1 style="margin: 0">Wybierz scenerię, aby otworzyć widok pragotronu</h1> <h1 style="margin: 0">Wybierz region i scenerię, aby otworzyć widok pragotronu</h1>
<p style="margin: 0.5em; color: #ccc">Widoczne jedynie scenerie aktywne na serwerze PL1</p>
<ul class="scenery-list" v-if="dataLoaded && onlineStations.length > 0"> <div class="region-selector g-selector">
<li v-for="(stationName, i) in onlineStations"> <label v-for="region in regions" :key="region">
<span v-if="i > 0">&bull;</span> <input
<button class="btn--text" @click="handleClick(stationName)"> type="radio"
{{ stationName }} name="region"
@change="changeRegion(region)"
:checked="mainStore.region == region"
/>
<span>{{ regionNames[region] }}</span>
</label>
</div>
</div>
<div class="scenery-selector">
<!-- <p style="margin: 0.5em; color: #ccc">Widoczne jedynie scenerie aktywne na serwerze PL1</p> -->
<transition name="list-anim" tag="div" mode="out-in">
<h3 v-if="apiStore.dataStatuses.activeData == DataStatus.LOADING">
Ładowanie listy aktywnych scenerii...
</h3>
<h3 v-else-if="sceneriesOnline.length == 0">Brak aktywnych scenerii</h3>
<ul v-else class="scenery-list">
<li v-for="station in sceneriesOnline" :key="station.stationName">
<button @click="handleClick(station.stationName)">
{{ station.stationName }}
</button> </button>
</li> </li>
</ul> </ul>
</transition>
<h3 v-else-if="onlineStations.length == 0">Brak aktywnych scenerii</h3>
<h3 v-else>Ładowanie listy aktywnych scenerii...</h3>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { IOnlineStationsResponse } from '../types/IOnlineStationsResponse'; import { DataStatus, useApiStore } from '../stores/apiStore';
import { Region, useMainStore, regionNames } from '../stores/mainStore';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
onlineStations: [] as string[], apiStore: useApiStore(),
dataLoaded: false, mainStore: useMainStore(),
regions: [Region.PL1, Region.PL2, Region.CZE, Region.DE, Region.ENG],
DataStatus: DataStatus,
regionNames
}; };
}, },
async mounted() { computed: {
const stationsAPIResponse: IOnlineStationsResponse = await ( sceneriesOnline() {
await fetch('https://api.td2.info.pl/?method=getStationsOnline') return (
).json(); this.apiStore.activeData?.activeSceneries
.filter((station) => {
this.dataLoaded = true; return station.region == this.mainStore.region && station.isOnline;
}, [])
this.onlineStations = stationsAPIResponse.message .sort((s1, s2) => s1.stationName.localeCompare(s2.stationName)) || []
.reduce((acc, station) => { );
if (station.region != 'eu') return acc; }
if (!station.isOnline) return acc;
acc.push(station.stationName);
return acc;
}, [] as string[])
.sort((s1, s2) => (s1 > s2 ? 1 : -1));
}, },
methods: { methods: {
handleClick(stationName: string) { handleClick(stationName: string) {
this.$router.push(`/board?name=${stationName}`); this.$router.push({
// this.selectedStation = station; path: '/board',
}, query: {
name: stationName,
region: this.mainStore.region
}
});
}, },
changeRegion(region: Region) {
this.mainStore.region = region;
}
}
}); });
</script> </script>
<style ;ang="scss" scoped> <style lang="scss" scoped>
.home-view {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 3em;
}
.region-selector {
display: grid;
justify-content: center;
grid-template-columns: repeat(5, 1fr);
max-width: 500px;
gap: 0.5em;
margin: 1em auto;
}
.scenery-selector > div {
position: relative;
}
ul.scenery-list { ul.scenery-list {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5em;
padding: 1em;
margin: 0 auto;
font-size: 1.3em; font-size: 1.3em;
max-width: 1000px; max-width: 1000px;
} }
</style>
// List animation
.list-anim-enter-active,
.list-anim-leave-active {
transition: all 90ms ease;
}
.list-anim-enter-from,
.list-anim-leave-to {
opacity: 0;
transform: translateY(20px);
}
</style>
+123 -166
View File
@@ -1,30 +1,10 @@
<template> <template>
<div class="pragotron" ref="pragotron"> <div class="pragotron">
<div class="pragotron_content"> <div class="pragotron_content">
<div class="filters"> <div class="wrapper" ref="pragotron">
<div>
<label>
<input type="checkbox" v-model="includeNonPassenger" />
Relacje niepasażerskie
</label>
<label>
<input type="checkbox" v-model="includeArrivals" />
Relacje kończące bieg
</label>
</div>
<div>
<label for="checkpoint">
Posterunek:
<select id="checkpoint" v-model="selectedCheckpointName">
<option v-for="cp in selectedStation?.stationCheckpoints" :value="cp">{{ cp }}</option>
</select>
</label>
</div>
</div>
<div class="wrapper">
<div class="top-pane"> <div class="top-pane">
<span class="title"> <span class="title">
<div>{{ selectedCheckpointName.toUpperCase() }}</div> <div>{{ mainStore.selectedCheckpointName.toUpperCase() }}</div>
</span> </span>
<div class="headers"> <div class="headers">
<span>GODZ.</span> <span>GODZ.</span>
@@ -35,7 +15,7 @@
</div> </div>
</div> </div>
<div class="table"> <div class="table">
<div class="row" v-for="(departure, i) in filledTable" :key="i"> <div class="row" v-for="(departure, i) in departureTable" :key="i">
<div class="row-content"> <div class="row-content">
<span class="departure-date"> <span class="departure-date">
<transition name="slot-anim" mode="out-in"> <transition name="slot-anim" mode="out-in">
@@ -62,7 +42,9 @@
</span> </span>
<span class="train-class"> <span class="train-class">
<transition name="slot-anim" mode="out-in"> <transition name="slot-anim" mode="out-in">
<div class="slider-slot" :key="departure.trainNumber">{{ departure.trainNumber }}</div> <div class="slider-slot" :key="departure.trainNumber">
{{ departure.trainNumber }}
</div>
</transition> </transition>
</span> </span>
<span class="route-via"> <span class="route-via">
@@ -97,15 +79,9 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import stationAbbrevsJSON from '../data/stationAbbrevs.json';
import routeValues from '../data/routeValues.json';
import { ITimetableStop, ITrainResponse } from '../types/ITrainResponse';
import { ITableRow } from '../types/ITableRow'; import { ITableRow } from '../types/ITableRow';
import { ISceneryResponse } from '../types/ISceneryReponse'; import { useMainStore } from '../stores/mainStore';
import ISceneryData from '../types/ISceneryData'; import { useApiStore } from '../stores/apiStore';
const stationAbbrevs: { [key: string]: string } = stationAbbrevsJSON;
const departureInfoEmptyObj: ITableRow = { const departureInfoEmptyObj: ITableRow = {
timetableId: -1, timetableId: -1,
@@ -130,29 +106,32 @@ const departureInfoEmptyObj: ITableRow = {
currentRowIndexes: [0, 0, 0, 0, 0, 0], currentRowIndexes: [0, 0, 0, 0, 0, 0],
dateDigits: ['', '', '', ''], dateDigits: ['', '', '', '']
}, }
}; };
export default defineComponent({ export default defineComponent({
props: { props: {
stationName: { stationName: {
type: String, type: String,
required: true, required: true
}, },
region: {
type: String,
default: 'pl1'
}
}, },
data: () => ({ data: () => ({
currentStationName: '', mainStore: useMainStore(),
sceneriesInfo: [] as ISceneryData[], apiStore: useApiStore(),
includeNonPassenger: true, includeNonPassenger: true,
includeArrivals: true, includeArrivals: true,
apiTrainData: [] as ITrainResponse[],
isAnimationRunning: true, isAnimationRunning: true,
intervalIndex: 0, // intervalIndex: 0,
lastRefreshTime: 0, lastRefreshTime: 0,
@@ -163,15 +142,11 @@ export default defineComponent({
currentRouteIndex: 0, currentRouteIndex: 0,
currentDateDigitIndex: 0, currentDateDigitIndex: 0,
currentRowAnimating: 0, currentRowAnimating: 0
stationAbbrevs: stationAbbrevs as { [key: string]: string },
selectedCheckpointName: '',
}), }),
async created() { async created() {
await this.fetchSceneryInfo(); // this.selectDefaultCheckpoint();
this.selectDefaultCheckpoint();
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
this.resizeTable(); this.resizeTable();
@@ -179,15 +154,12 @@ export default defineComponent({
}, },
activated() { activated() {
this.mainStore.selectedStationName = this.stationName;
this.resizeTable(); this.resizeTable();
this.selectDefaultCheckpoint(); this.selectDefaultCheckpoint();
this.shuffleRoutes(); this.shuffleRoutes();
this.fetchDepartureList();
this.intervalIndex = setInterval(() => {
this.fetchDepartureList();
}, 30000);
this.isAnimationRunning = true; this.isAnimationRunning = true;
requestAnimationFrame(this.update); requestAnimationFrame(this.update);
@@ -195,24 +167,69 @@ export default defineComponent({
deactivated() { deactivated() {
this.isAnimationRunning = false; this.isAnimationRunning = false;
},
clearInterval(this.intervalIndex); watch: {
filledTable: {
deep: true,
handler(value) {
for (let i = 0; i < this.departureTable.length; i++) {
if (i <= value.length - 1) {
const updateInfo = value[i];
const existingInfo = this.departureTable[i];
this.departureTable[i] = { ...updateInfo };
this.departureTable[i].tableValues.routeTo = existingInfo.routeTo;
this.departureTable[i].tableValues.routeVia = existingInfo.routeVia;
// this.departureTable[i].dateDigits = [...existingInfo.tableValues.dateDigits];
this.departureTable[i].tableValues.dateDigits = [
...existingInfo.tableValues.dateDigits
];
this.departureTable[i].tableValues.currentRowIndexes = [
...existingInfo.tableValues.currentRowIndexes
];
} else {
this.departureTable[i] = {
...this.departureTable[i],
timetableId: -1,
routeTo: '',
routeVia: '',
trainNumber: '',
date: new Date(0),
dateDigits: ['', '', '', ''],
arrivalTimestamp: 0,
departureTimestamp: 0,
checkpointName: '',
delayMinutes: 0
};
}
}
}
},
'apiStore.activeData'(_val, prevVal) {
if (prevVal == undefined) {
this.selectDefaultCheckpoint();
}
}
}, },
computed: { computed: {
selectedStation() {
return this.sceneriesInfo.find(({ stationName }) => stationName == this.stationName.replace(/_/g, ' '));
},
filledTable() { filledTable() {
const filteredData = this.apiTrainData const filteredData = this.apiStore.activeData?.trains
.reduce((list, train, i) => { .reduce((list, train) => {
if (!train.timetable) return list; if (!train.timetable) return list;
const timetable = train.timetable; const timetable = train.timetable;
const stopInfo: ITimetableStop | undefined = timetable.stopList.find( const stopInfo = timetable.stopList.find(
(sp) => sp.stopNameRAW.toLowerCase() == this.selectedCheckpointName.toLowerCase() (sp) =>
sp.stopNameRAW.toLowerCase() == this.mainStore.selectedCheckpointName.toLowerCase()
); );
if (!stopInfo || stopInfo.confirmed) return list; if (!stopInfo || stopInfo.confirmed) return list;
@@ -234,7 +251,12 @@ export default defineComponent({
const date = departureLine ? new Date(departureTimestamp) : new Date(arrivalTimestamp); const date = departureLine ? new Date(departureTimestamp) : new Date(arrivalTimestamp);
// [HH, MM, SS] - nienawidzę dat w JavaScripcie // [HH, MM, SS] - nienawidzę dat w JavaScripcie
const dateArray = date.toLocaleString('pl-PL').split(', ')[1].split(':') || ['', '', '', '']; const dateArray = date.toLocaleString('pl-PL').split(', ')[1].split(':') || [
'',
'',
'',
''
];
// [H,H,M,M] - ZABIJCIE MNIE BŁAGAM // [H,H,M,M] - ZABIJCIE MNIE BŁAGAM
const dateDigits = [...dateArray[0].split(''), ...dateArray[1].split('')]; const dateDigits = [...dateArray[0].split(''), ...dateArray[1].split('')];
@@ -249,7 +271,7 @@ export default defineComponent({
date, date,
dateDigits, dateDigits,
delayMinutes: departureDelay, delayMinutes: departureDelay,
checkpointName: this.selectedCheckpointName.toLowerCase(), checkpointName: this.mainStore.selectedCheckpointName.toLowerCase(),
arrivalTimestamp, arrivalTimestamp,
departureTimestamp, departureTimestamp,
@@ -258,8 +280,8 @@ export default defineComponent({
routeTo: '', routeTo: '',
routeVia: '', routeVia: '',
dateDigits: ['', '', '', ''], dateDigits: ['', '', '', ''],
currentRowIndexes: [0, 0, 0, 0, 0, 0], currentRowIndexes: [0, 0, 0, 0, 0, 0]
}, }
}); });
if (!this.departureRoutes.includes(routeVia)) this.departureRoutes.push(routeVia); if (!this.departureRoutes.includes(routeVia)) this.departureRoutes.push(routeVia);
@@ -269,77 +291,40 @@ export default defineComponent({
}, [] as ITableRow[]) }, [] as ITableRow[])
.filter( .filter(
(dep) => (dep) =>
(this.includeNonPassenger || !/^[T|L|Z|P]/g.test(dep.trainNumber)) && (this.mainStore.filters.nonPassenger || !/^[T|L|Z|P]/g.test(dep.trainNumber)) &&
(this.includeArrivals || dep.departureTimestamp) (this.mainStore.filters.terminating || dep.departureTimestamp)
) )
.sort((dep1, dep2) => (dep1.date?.getTime() || 0) - (dep2.date?.getTime() || 0)); .sort((dep1, dep2) => (dep1.date?.getTime() || 0) - (dep2.date?.getTime() || 0));
for (let i = 0; i < this.departureTable.length; i++) { return filteredData;
if (i <= filteredData.length - 1) {
const updateInfo = filteredData[i];
const existingInfo = this.departureTable[i];
this.departureTable[i] = { ...updateInfo };
this.departureTable[i].tableValues.routeTo = existingInfo.routeTo;
this.departureTable[i].tableValues.routeVia = existingInfo.routeVia;
// this.departureTable[i].dateDigits = [...existingInfo.tableValues.dateDigits];
this.departureTable[i].tableValues.dateDigits = [...existingInfo.tableValues.dateDigits];
this.departureTable[i].tableValues.currentRowIndexes = [...existingInfo.tableValues.currentRowIndexes];
} else {
this.departureTable[i] = {
...this.departureTable[i],
timetableId: -1,
routeTo: '',
routeVia: '',
trainNumber: '',
date: new Date(0),
dateDigits: ['', '', '', ''],
arrivalTimestamp: 0,
departureTimestamp: 0,
checkpointName: '',
delayMinutes: 0,
};
} }
}
return this.departureTable;
},
}, },
methods: { methods: {
async fetchSceneryInfo() {
const sceneryInfoRes: ISceneryResponse[] = await (
await fetch(`${import.meta.env.VITE_STACJOWNIK_API_URL}/api/getSceneries`)
).json();
this.sceneriesInfo = sceneryInfoRes.map((stationData) => ({
stationName: stationData.name,
stationCheckpoints:
stationData.checkpoints?.length > 0 ? stationData.checkpoints.split(';') : [stationData.name],
nameAbbreviation: '',
}));
},
resizeTable() { resizeTable() {
const elRef = this.$refs['pragotron'] as HTMLElement; const elRef = this.$refs['pragotron'] as HTMLElement;
if (!elRef) return; if (!elRef) return;
const scale = Math.min(window.innerWidth / elRef.clientWidth, window.innerHeight / elRef.clientHeight, 1); const scale = Math.min(
window.innerWidth / elRef.clientWidth,
window.innerHeight / elRef.clientHeight,
1
);
// elRef.style.width = `${window.innerWidth - 10}px`;
// elRef.style.height = `${(window.innerWidth - 10) / 2}px`;
elRef.style.transform = `scale(${scale})`; elRef.style.transform = `scale(${scale})`;
}, },
selectDefaultCheckpoint() { selectDefaultCheckpoint() {
this.selectedCheckpointName = this.selectedStation?.stationCheckpoints[0] || this.stationName; this.mainStore.selectedCheckpointName =
this.mainStore.selectedStation?.stationCheckpoints[0] || this.stationName;
}, },
abbrevStationName(name: string) { abbrevStationName(name: string) {
return (stationAbbrevs[name] || name).toUpperCase(); // return (stationAbbrevs[name] || name).toUpperCase();
return name.toUpperCase();
}, },
update(time: number) { update(time: number) {
@@ -356,15 +341,7 @@ export default defineComponent({
requestAnimationFrame(this.update); requestAnimationFrame(this.update);
}, },
// d = 0 -> time = time
// d = time -> time2 = time2-time
updateTableRows() { updateTableRows() {
// const isAnimating =
// dep.tableValues.routeTo.toLowerCase() != dep.routeTo.toLowerCase() ||
// dep.tableValues.routeVia.toLowerCase() != dep.routeVia.toLowerCase() ||
// !dep.tableValues.dateDigits.every((dd, i) => dd == dep.dateDigits[i]);
// console.log(isAnimating);
for (let i = 0; i < this.departureTable.length; i++) { for (let i = 0; i < this.departureTable.length; i++) {
const dep = this.departureTable[i]; const dep = this.departureTable[i];
@@ -384,7 +361,8 @@ export default defineComponent({
dep.tableValues.dateDigits.forEach((digit, j) => { dep.tableValues.dateDigits.forEach((digit, j) => {
if (dep.dateDigits[j] != digit) { if (dep.dateDigits[j] != digit) {
dep.tableValues.dateDigits[j] = this.dateDigits[dep.tableValues.currentRowIndexes[j + 2]]; dep.tableValues.dateDigits[j] =
this.dateDigits[dep.tableValues.currentRowIndexes[j + 2]];
dep.tableValues.currentRowIndexes[j + 2] = dep.tableValues.currentRowIndexes[j + 2] =
(dep.tableValues.currentRowIndexes[j + 2] + 1) % this.dateDigits.length; (dep.tableValues.currentRowIndexes[j + 2] + 1) % this.dateDigits.length;
} }
@@ -393,24 +371,9 @@ export default defineComponent({
}, },
shuffleRoutes() { shuffleRoutes() {
for (let i = 0; i < 25; i++) {
const randIndex = Math.floor(Math.random() * routeValues.length);
const randRoute = routeValues[randIndex];
}
this.departureRoutes.sort(() => Math.random() - 0.5); this.departureRoutes.sort(() => Math.random() - 0.5);
}, }
}
async fetchDepartureList() {
const trainsAPIResponse: ITrainResponse[] = await (
await fetch(`${import.meta.env.VITE_STACJOWNIK_API_URL}/api/getActiveTrainList`)
).json();
if (!trainsAPIResponse) return;
this.apiTrainData = trainsAPIResponse;
},
},
}); });
</script> </script>
@@ -443,24 +406,28 @@ export default defineComponent({
/* ************** */ /* ************** */
.pragotron { .pragotron_content {
padding: 1em;
will-change: transform;
}
.filters {
display: flex; display: flex;
justify-content: space-between; justify-content: center;
padding: 0.25em 0; padding: 1em;
gap: 0.5em;
} }
.wrapper { .wrapper {
width: 1200px;
height: 650px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 1400px;
min-height: 700px;
padding: 2em;
transform-origin: top;
}
.top-pane > .headers,
.row-content {
display: grid;
grid-template-columns: 1fr 1fr 2fr 2fr 1fr;
gap: 0 10px;
padding: 0 10px;
} }
.top-pane { .top-pane {
@@ -479,11 +446,6 @@ export default defineComponent({
} }
.headers { .headers {
display: grid;
grid-template-columns: 1fr 1fr 2fr 2fr 1fr;
gap: 0 10px;
padding: 0 10px;
text-align: center; text-align: center;
font-size: 1.35em; font-size: 1.35em;
@@ -501,16 +463,11 @@ export default defineComponent({
.row { .row {
&-content { &-content {
display: grid;
grid-template-columns: 1fr 1fr 2fr 2fr 1fr;
gap: 0 10px;
padding: 0 10px;
height: 100%; height: 100%;
align-items: center; align-items: center;
color: white; color: white;
font-size: 1.2em; font-size: 1.3em;
background: #1a1a1a; background: #1a1a1a;
+3 -3
View File
@@ -1,7 +1,7 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module '*.vue' { declare module '*.vue' {
import type { DefineComponent } from 'vue' import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any> const component: DefineComponent<{}, {}, any>;
export default component export default component;
} }
+1643 -422
View File
File diff suppressed because it is too large Load Diff