Merge pull request #161 from Spythere/development

v1.34.0
This commit is contained in:
Spythere
2026-04-19 01:08:00 +02:00
committed by GitHub
41 changed files with 1988 additions and 1656 deletions
+1 -2
View File
@@ -15,13 +15,12 @@ pnpm-debug.log*
# Editor directories and files # Editor directories and files
.idea .idea
.vscode
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
node_modules .vscode/settings.json
*.log *.log
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "esbenp.prettier-vscode"]
}
Vendored
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.33.0", "version": "1.34.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -26,12 +26,12 @@
"vue-router": "^4.4.0" "vue-router": "^4.4.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.3.1", "@tsconfig/node24": "^24.0.4",
"@types/node": "^24.12.0",
"@types/showdown": "^2.0.6", "@types/showdown": "^2.0.6",
"@vite-pwa/assets-generator": "^1.0.0", "@vite-pwa/assets-generator": "^1.0.0",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"axios": "^1.9.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^7.1.4", "vite": "^7.1.4",
+9 -6
View File
@@ -30,7 +30,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import axios from 'axios';
import { version } from '../package.json'; import { version } from '../package.json';
import { Status } from './typings/common'; import { Status } from './typings/common';
@@ -114,11 +113,15 @@ export default defineComponent({
} }
try { try {
const releaseData = await ( const response = await fetch(
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest') 'https://api.github.com/repos/Spythere/stacjownik/releases/latest'
).data; );
if (!releaseData) return; if (!response.ok) {
throw new Error('Failed to fetch release data from repository!');
}
const releaseData = await response.json();
this.store.appUpdate = { this.store.appUpdate = {
version, version,
@@ -130,7 +133,7 @@ export default defineComponent({
(storageVersion != '' && storageVersion != version && this.isOnProductionHost) || (storageVersion != '' && storageVersion != version && this.isOnProductionHost) ||
import.meta.env.VITE_UPDATE_TEST === 'test'; import.meta.env.VITE_UPDATE_TEST === 'test';
} catch (error) { } catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`); console.error(error);
} }
StorageManager.setStringValue(STORAGE_VERSION_KEY, version); StorageManager.setStringValue(STORAGE_VERSION_KEY, version);
+3 -1
View File
@@ -8,6 +8,7 @@
:images="images" :images="images"
:image-fallbacks="imagesFallbacks" :image-fallbacks="imagesFallbacks"
:show-previews="showPreviews" :show-previews="showPreviews"
:thumbnail-size="thumbnailSize"
/> />
</li> </li>
</ul> </ul>
@@ -25,7 +26,8 @@ export default defineComponent({
props: { props: {
trainStockList: { type: Array as PropType<string[]>, required: true }, trainStockList: { type: Array as PropType<string[]>, required: true },
tractionOnly: { type: Boolean, required: false }, tractionOnly: { type: Boolean, required: false },
showPreviews: { type: Boolean } showPreviews: { type: Boolean },
thumbnailSize: { type: Number }
}, },
data() { data() {
+4 -3
View File
@@ -9,7 +9,7 @@
<img <img
v-for="(thumbnailImage, imageIndex) in images" v-for="(thumbnailImage, imageIndex) in images"
:src="`https://stacjownik.spythere.eu/static/thumbnails/${thumbnailImage}.png`" :src="`https://stacjownik.spythere.eu/static/thumbnails/${thumbnailImage}.png`"
height="70" :height="thumbnailSize || 70"
loading="lazy" loading="lazy"
:data-crosshair-cursor="showPreviews" :data-crosshair-cursor="showPreviews"
:data-tooltip-type="showPreviews ? 'VehiclePreviewTooltip' : ''" :data-tooltip-type="showPreviews ? 'VehiclePreviewTooltip' : ''"
@@ -28,7 +28,8 @@ const props = defineProps({
vehicleString: { type: String, required: true }, vehicleString: { type: String, required: true },
images: { type: Object as PropType<string[]>, required: true }, images: { type: Object as PropType<string[]>, required: true },
imageFallbacks: { type: Object as PropType<string[]>, required: true }, imageFallbacks: { type: Object as PropType<string[]>, required: true },
showPreviews: { type: Boolean } showPreviews: { type: Boolean },
thumbnailSize: { type: Number }
}); });
const thumbRef = ref(null) as Ref<HTMLElement | null>; const thumbRef = ref(null) as Ref<HTMLElement | null>;
@@ -67,7 +68,7 @@ function onImageLoad() {
max-width: 90%; max-width: 90%;
text-align: center; text-align: center;
color: #aaa; color: #aaa;
font-size: 0.85em; font-size: 0.8em;
margin: 0 auto; margin: 0 auto;
padding: 0.25em 0; padding: 0.25em 0;
} }
+11 -4
View File
@@ -269,9 +269,9 @@ export default defineComponent({
this.searchTimeout = window.setTimeout(async () => { this.searchTimeout = window.setTimeout(async () => {
try { try {
const suggestions: string[] = await ( const suggestions: string[] = await this.apiStore.client.get(
await this.apiStore.client!.get(`api/get${type}Suggestions?name=${value}`) `api/get${type}Suggestions?name=${value}`
).data; );
this[`${type}Suggestions`] = suggestions; this[`${type}Suggestions`] = suggestions;
} catch (error) { } catch (error) {
@@ -336,10 +336,17 @@ export default defineComponent({
display: grid; display: grid;
grid-template-rows: 1fr auto; grid-template-rows: 1fr auto;
overflow: hidden; overflow: hidden;
max-height: 530px; max-height: calc(100% - 4.5em);
top: 3.5em;
padding: 1em 0;
} }
.options_content { .options_content {
overflow: auto; overflow: auto;
padding: 0 1em;
}
.options_actions {
padding: 0 1em;
} }
</style> </style>
@@ -69,5 +69,6 @@ function navigateToProfile() {
left: auto; left: auto;
right: 0; right: 0;
max-width: 700px; max-width: 700px;
top: 3.5em;
} }
</style> </style>
@@ -201,22 +201,20 @@ const driverRouteLocation = computed<RouteLocationRaw | null>(() => {
async function fetchTimetableDetails() { async function fetchTimetableDetails() {
try { try {
const responseData = await apiStore.client!.get<API.TimetableHistory.Response>( const responseData = await apiStore.client.get<API.TimetableHistory.Response>(
'api/getTimetables', 'api/getTimetables',
{ {
params: { timetableId: props.timetableEntry.id,
timetableId: props.timetableEntry.id, returnType: 'detailed'
returnType: 'detailed'
}
} }
); );
if (!responseData || responseData.data.length != 1) { if (!responseData || responseData.length != 1) {
timetableDetails.value = null; timetableDetails.value = null;
return; return;
} }
timetableDetails.value = responseData.data[0]; timetableDetails.value = responseData[0];
} catch (error) { } catch (error) {
// this.dataStatus = Status.Data.Error; // this.dataStatus = Status.Data.Error;
console.error(error); console.error(error);
+2 -1
View File
@@ -15,7 +15,8 @@ export namespace Journal {
| 'search-issuedFrom' | 'search-issuedFrom'
| 'search-terminatingAt' | 'search-terminatingAt'
| 'search-via' | 'search-via'
| 'select-categoryCode'; | 'select-categoryCode'
| 'search-headUnit';
export type TimetableSearchType = { export type TimetableSearchType = {
[key in TimetableSearchKey]: string; [key in TimetableSearchKey]: string;
@@ -127,9 +127,8 @@ export default defineComponent({
this.station?.name || this.onlineScenery?.name this.station?.name || this.onlineScenery?.name
}&countFrom=${countFrom}&countLimit=${countLimit}`; }&countFrom=${countFrom}&countLimit=${countLimit}`;
const historyAPIData: API.DispatcherHistory.Response = await ( const historyAPIData: API.DispatcherHistory.Response =
await this.apiStore.client!.get(requestString) await this.apiStore.client.get(requestString);
).data;
this.dataStatus = Status.Data.Loaded; this.dataStatus = Status.Data.Loaded;
return historyAPIData; return historyAPIData;
@@ -29,7 +29,8 @@
<i <i
v-if=" v-if="
train.timetableData != undefined && train.timetableData != undefined &&
(train.lastSeen <= Date.now() - 60000 || !train.online) train.lastSeen <= Date.now() - 60000 &&
!train.online
" "
class="fa-solid fa-user-slash" class="fa-solid fa-user-slash"
style="color: lightcoral" style="color: lightcoral"
@@ -210,7 +210,7 @@
</div> </div>
<div class="item-stock-list" v-if="showStockThumbnails"> <div class="item-stock-list" v-if="showStockThumbnails">
<StockList :trainStockList="row.train.stockList" /> <StockList :trainStockList="row.train.stockList" :thumbnailSize="45" />
</div> </div>
</router-link> </router-link>
</transition-group> </transition-group>
@@ -348,7 +348,7 @@ const tabliceZbiorczeHref = computed(() => {
}); });
const pragotronHref = computed(() => { const pragotronHref = computed(() => {
let url = `https://pragotron-td2.web.app/board?name=${props.station!.name}&region=${mainStore.region.id}`; let url = `https://pragotron-td2.spythere.eu/board?name=${props.station!.name}&region=${mainStore.region.id}`;
if (props.chosenCheckpoint) url += `&checkpoint=${props.chosenCheckpoint}`; if (props.chosenCheckpoint) url += `&checkpoint=${props.chosenCheckpoint}`;
return url; return url;
@@ -149,11 +149,12 @@ export default defineComponent({
requestFilters['returnType'] = 'short'; requestFilters['returnType'] = 'short';
try { try {
const response: API.TimetableHistory.ResponseShort = await ( const response: API.TimetableHistory.ResponseShort = await this.apiStore.client.get(
await this.apiStore.client!.get('api/getTimetables', { 'api/getTimetables',
params: requestFilters requestFilters
}) );
).data;
console.log(response);
this.historyList = response; this.historyList = response;
@@ -0,0 +1,204 @@
<template>
<div class="scenery-top-list">
<h2 class="header">{{ t('scenery.top-list.header') }}</h2>
<div class="top-actions">
<div class="actions-modes">
<button
v-for="mode in availableModes"
:class="`btn btn--option ${mode == currentListMode ? 'checked' : ''}`"
@click="selectListMode(mode)"
>
{{ t(`scenery.top-list.mode-${mode}`) }}
</button>
</div>
<div class="actions-scopes">
<button
v-for="scope in availableScopes"
:class="`btn btn--option ${scope == currentListScope ? 'checked' : ''}`"
@click="selectListScope(scope)"
>
{{ t(`scenery.top-list.scope-${scope}`) }}
</button>
</div>
</div>
<div class="rating-list-wrapper">
<Loading v-if="listState == Status.Data.Loading" />
<div v-else-if="listState == Status.Data.Error">Ups, coś poszło nie tak...</div>
<ul v-else>
<li v-for="(value, i) in bestScoreList">
<div>
{{ t('scenery.top-list.place', i + 1) }} -
<router-link :to="`/profile?playerId=${value.dispatcherId}`">{{
value.dispatcherName
}}</router-link>
</div>
<div>
<b class="text--primary" v-if="currentListMode == 'dutyCount'">{{
t('scenery.top-list.duty-count', value.value)
}}</b>
<b class="text--primary" v-else-if="currentListMode == 'dispatcherRating'">{{
t('scenery.top-list.dispatcher-rating', value.value)
}}</b>
<b class="text--primary" v-else>
{{ t('scenery.top-list.duration') }}
{{ humanizeDuration(value.value) }}
</b>
</div>
</li>
</ul>
</div>
</div>
</template>
<script lang="ts" setup>
import { onActivated, PropType, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useApiStore } from '../../store/apiStore';
import { Station, ActiveScenery, Status } from '../../typings/common';
import Loading from '../Global/Loading.vue';
import { humanizeDuration } from '../../composables/time';
interface SceneryBestScoreItem {
dispatcherName: string;
dispatcherId: number;
value: number;
}
const { t } = useI18n();
const apiStore = useApiStore();
defineOptions({
name: 'SceneryTopList'
});
const props = defineProps({
station: {
type: Object as PropType<Station>
},
onlineScenery: {
type: Object as PropType<ActiveScenery>
}
});
const availableModes = ['dutyCount', 'dispatcherRating', 'dutyDuration'] as const;
const availableScopes = ['name', 'hash'] as const;
type ListMode = (typeof availableModes)[number];
type ListScope = (typeof availableScopes)[number];
const currentListMode = ref<ListMode>('dutyCount');
const currentListScope = ref<ListScope>('name');
const listState = ref<Status.Data>(Status.Data.Loading);
const bestScoreList = ref<SceneryBestScoreItem[]>([]);
onActivated(() => {
fetchTopDispatchersList();
});
function selectListMode(mode: ListMode) {
currentListMode.value = mode;
fetchTopDispatchersList();
}
function selectListScope(scope: ListScope) {
currentListScope.value = scope;
fetchTopDispatchersList();
}
async function fetchTopDispatchersList() {
const searchedStationValue =
currentListScope.value == 'name'
? props.station?.name
: apiStore.sceneryData.find((sc) => sc.name == props.station!.name)?.hash;
bestScoreList.value = [];
if (!searchedStationValue) {
listState.value = Status.Data.Loaded;
return;
}
try {
listState.value = Status.Data.Loading;
const response: SceneryBestScoreItem[] = await apiStore.client.get(`api/getSceneryBestScores`, {
[currentListScope.value]: searchedStationValue,
type: currentListMode.value,
countLimit: 40
});
bestScoreList.value = response;
listState.value = Status.Data.Loaded;
} catch (error) {
listState.value = Status.Data.Error;
console.error(error);
}
}
</script>
<style lang="scss" scoped>
.scenery-top-list {
display: grid;
grid-template-rows: auto auto 1fr;
overflow: hidden;
gap: 1em;
}
.top-actions {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.5em 1.5em;
}
.actions-modes,
.actions-scopes {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
font-weight: bold;
button {
font-weight: bold;
}
}
.rating-list-wrapper {
overflow: auto;
}
.rating-list-wrapper > ul {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
align-items: center;
gap: 0.65em;
padding-right: 0.5em;
}
.rating-list-wrapper > ul > li {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 0.25em;
background-color: #2b2b2b;
height: 100%;
line-height: 1.5em;
a {
font-weight: bold;
}
}
</style>
+13 -5
View File
@@ -18,23 +18,31 @@ export function getTrainStopStatus(
return StopStatus.TERMINATED; return StopStatus.TERMINATED;
} }
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == sceneryName) { if (
!stopInfo.terminatesHere &&
stopInfo.confirmed &&
currentStationName.startsWith(sceneryName)
) {
return StopStatus.DEPARTED; return StopStatus.DEPARTED;
} }
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != sceneryName) { if (
!stopInfo.terminatesHere &&
stopInfo.confirmed &&
!currentStationName.startsWith(sceneryName)
) {
return StopStatus.DEPARTED_AWAY; return StopStatus.DEPARTED_AWAY;
} }
if (currentStationName == sceneryName && !stopInfo.stopped) { if (currentStationName.startsWith(sceneryName) && !stopInfo.stopped) {
return StopStatus.ONLINE; return StopStatus.ONLINE;
} }
if (currentStationName == sceneryName && stopInfo.stopped) { if (currentStationName.startsWith(sceneryName) && stopInfo.stopped) {
return StopStatus.STOPPED; return StopStatus.STOPPED;
} }
if (currentStationName != sceneryName) { if (!currentStationName.startsWith(sceneryName)) {
return StopStatus.ARRIVING; return StopStatus.ARRIVING;
} }
@@ -278,6 +278,10 @@ export default defineComponent({
color: #ccc; color: #ccc;
} }
.dropdown_wrapper {
top: 2.5em;
}
@include responsive.smallScreen { @include responsive.smallScreen {
.stats-title { .stats-title {
text-align: center; text-align: center;
@@ -286,5 +290,9 @@ export default defineComponent({
.filter-button > span { .filter-button > span {
display: none; display: none;
} }
.no-data {
text-align: center;
}
} }
</style> </style>
@@ -210,6 +210,10 @@ export default defineComponent({
@use '../../styles/dropdown'; @use '../../styles/dropdown';
@use '../../styles/dropdown-filters'; @use '../../styles/dropdown-filters';
.dropdown_wrapper {
top: 2.5em;
}
.search_content > div { .search_content > div {
margin: 0.5em auto; margin: 0.5em auto;
} }
+2 -1
View File
@@ -250,9 +250,10 @@ h3 {
.dropdown_wrapper { .dropdown_wrapper {
max-width: 600px; max-width: 600px;
top: 2.5em;
} }
@include responsive.smallScreen{ @include responsive.smallScreen {
.no-data { .no-data {
text-align: center; text-align: center;
} }
+23
View File
@@ -0,0 +1,23 @@
export class HttpClient {
constructor(private readonly baseURL: string) {}
async get<T>(url: string, params?: Record<string, any>): Promise<T> {
const absoluteURL = new URL(this.baseURL + '/' + url);
if (params) {
Object.keys(params).forEach((key) => {
if (params[key] === undefined) return;
absoluteURL.searchParams.append(key, params[key]);
});
}
const data = await fetch(absoluteURL);
if (!data.ok) {
throw new Error(`Cannot fetch ${absoluteURL}: ${data.statusText}`);
}
return data.json();
}
}
+23
View File
@@ -3,12 +3,35 @@ import plLang from './locales/pl.json';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
function customRule(choice: number, choicesLength: number) {
if (choice === 0) {
return 0;
}
const teen = choice > 10 && choice < 20;
const endsWithOne = choice % 10 === 1;
if (!teen && endsWithOne) {
return 1;
}
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
return 2;
}
return choicesLength < 4 ? 2 : 3;
}
const i18n = createI18n({ const i18n = createI18n({
locale: 'pl', locale: 'pl',
legacy: false, legacy: false,
warnHtmlMessage: false, warnHtmlMessage: false,
fallbackLocale: 'pl', fallbackLocale: 'pl',
pluralizationRules: {
pl: customRule
},
messages: { messages: {
en: enLang, en: enLang,
pl: plLang pl: plLang
+16 -1
View File
@@ -199,6 +199,7 @@
"search-date-from": "Date (UTC+2 / CEST)", "search-date-from": "Date (UTC+2 / CEST)",
"search-date-to": "Date (UTC+2 / CEST)", "search-date-to": "Date (UTC+2 / CEST)",
"select-categoryCode": "Train category", "select-categoryCode": "Train category",
"search-headUnit": "Traction unit (e.g. EP09, ET22-401)",
"sort-mass": "mass", "sort-mass": "mass",
"sort-speed": "speed", "sort-speed": "speed",
"sort-length": "length", "sort-length": "length",
@@ -573,6 +574,7 @@
"option-active-timetables": "Active timetables", "option-active-timetables": "Active timetables",
"option-timetables-history": "Timetables history PL1", "option-timetables-history": "Timetables history PL1",
"option-dispatchers-history": "Dispatchers history PL1", "option-dispatchers-history": "Dispatchers history PL1",
"option-top-list": "Scenery records",
"btn-show-timetable-thumbnails": "Show rolling stock thumbnails", "btn-show-timetable-thumbnails": "Show rolling stock thumbnails",
"btn-hide-timetable-thumbnails": "Hide rolling stock thumbnails", "btn-hide-timetable-thumbnails": "Hide rolling stock thumbnails",
"timetable-includesScenery": "ALL TIMETABLES", "timetable-includesScenery": "ALL TIMETABLES",
@@ -592,7 +594,20 @@
"tablice-link": "Timetable summary board <br> (by Thundo)", "tablice-link": "Timetable summary board <br> (by Thundo)",
"bottom-info": "Show full history in the Journal tab", "bottom-info": "Show full history in the Journal tab",
"btn-show-internal-routes": "Show internal routes", "btn-show-internal-routes": "Show internal routes",
"btn-hide-internal-routes": "Hide internal routes" "btn-hide-internal-routes": "Hide internal routes",
"top-list": {
"header": "RECORDS ON THE SCENERY (PL1)",
"mode-dutyCount": "DUTIES",
"mode-dispatcherRating": "RATING",
"mode-dutyDuration": "DUTY DURATION",
"scope-name": "GENERAL",
"scope-hash": "CURRENT HASH",
"place": "{n}. place",
"dispatcher-rating": "Rating: {n}",
"duty-count": "No duties | 1 duty | Duties: {n}",
"duration": "Duration:"
}
}, },
"availability": { "availability": {
"title": "Availability", "title": "Availability",
+16 -1
View File
@@ -196,6 +196,7 @@
"search-date-from": "Data (UTC+2 / CEST)", "search-date-from": "Data (UTC+2 / CEST)",
"search-date-to": "Data (UTC+2 / CEST)", "search-date-to": "Data (UTC+2 / CEST)",
"select-categoryCode": "Kategoria pociągu", "select-categoryCode": "Kategoria pociągu",
"search-headUnit": "Pojazd trakcyjny (np. EP09, ET22-137)",
"sort-routeDistance": "kilometraż", "sort-routeDistance": "kilometraż",
"sort-allStopsCount": "stacje", "sort-allStopsCount": "stacje",
"sort-beginDate": "data", "sort-beginDate": "data",
@@ -559,6 +560,7 @@
"option-active-timetables": "Aktywne rozkłady jazdy", "option-active-timetables": "Aktywne rozkłady jazdy",
"option-timetables-history": "Historia rozkładów PL1", "option-timetables-history": "Historia rozkładów PL1",
"option-dispatchers-history": "Historia dyżurów PL1", "option-dispatchers-history": "Historia dyżurów PL1",
"option-top-list": "Rekordy scenerii",
"btn-show-timetable-thumbnails": "Pokazuj podglądy składów", "btn-show-timetable-thumbnails": "Pokazuj podglądy składów",
"btn-hide-timetable-thumbnails": "Ukrywaj podglądy składów", "btn-hide-timetable-thumbnails": "Ukrywaj podglądy składów",
"timetable-includesScenery": "WSZYSTKIE RJ", "timetable-includesScenery": "WSZYSTKIE RJ",
@@ -578,7 +580,20 @@
"tablice-link": "Tablica informacyjna zbiorcza <br> (autorstwa Thundo)", "tablice-link": "Tablica informacyjna zbiorcza <br> (autorstwa Thundo)",
"bottom-info": "Pokaż pełną historię w zakładce Dziennika", "bottom-info": "Pokaż pełną historię w zakładce Dziennika",
"btn-show-internal-routes": "Pokazuj szlaki wewnętrzne", "btn-show-internal-routes": "Pokazuj szlaki wewnętrzne",
"btn-hide-internal-routes": "Ukrywaj szlaki wewnętrzne" "btn-hide-internal-routes": "Ukrywaj szlaki wewnętrzne",
"top-list": {
"header": "REKORDY NA SCENERII (PL1)",
"mode-dutyCount": "DYŻURY",
"mode-dispatcherRating": "OCENA",
"mode-dutyDuration": "CZAS DYŻURU",
"scope-name": "OGÓLNIE",
"scope-hash": "OBECNY HASH",
"place": "{n}. miejsce",
"dispatcher-rating": "Ocena: {n}",
"duty-count": "Brak dyżurów | 1 dyżur | Dyżury: {n}",
"duration": "Czas:"
}
}, },
"availability": { "availability": {
"title": "Dostępność", "title": "Dostępność",
+24 -32
View File
@@ -2,7 +2,20 @@ import { defineStore } from 'pinia';
import { API } from '../typings/api'; import { API } from '../typings/api';
import { Status } from '../typings/common'; import { Status } from '../typings/common';
import { StationJSONData } from './typings'; import { StationJSONData } from './typings';
import axios, { AxiosInstance } from 'axios'; import { HttpClient } from '../http';
let baseURL = 'https://stacjownik.spythere.eu';
switch (import.meta.env.VITE_API_MODE) {
case 'development':
baseURL = 'http://localhost:3001';
break;
case 'mocking':
baseURL = 'http://localhost:3123';
break;
default:
break;
}
export const useApiStore = defineStore('apiStore', { export const useApiStore = defineStore('apiStore', {
state: () => ({ state: () => ({
@@ -25,30 +38,13 @@ export const useApiStore = defineStore('apiStore', {
nextUpdateTime: 0, nextUpdateTime: 0,
nextDataCheckTime: 0, nextDataCheckTime: 0,
client: undefined as AxiosInstance | undefined, client: new HttpClient(baseURL),
activeDataScheduler: undefined as number | undefined activeDataScheduler: undefined as number | undefined
}), }),
actions: { actions: {
async setupAPIData() { async setupAPIData() {
let baseURL = 'https://stacjownik.spythere.eu';
switch (import.meta.env.VITE_API_MODE) {
case 'development':
baseURL = 'http://localhost:3001';
break;
case 'mocking':
baseURL = 'http://localhost:3123';
break;
default:
break;
}
this.client = axios.create({
baseURL
});
this.connectToAPI(); this.connectToAPI();
}, },
@@ -82,9 +78,9 @@ export const useApiStore = defineStore('apiStore', {
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading; if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
try { try {
const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData'); const response = await this.client.get<API.ActiveData.Response>('api/getActiveData');
this.activeData = response.data; this.activeData = response;
this.dataStatuses.connection = Status.Data.Loaded; this.dataStatuses.connection = Status.Data.Loaded;
} catch (error) { } catch (error) {
this.dataStatuses.connection = Status.Data.Error; this.dataStatuses.connection = Status.Data.Error;
@@ -94,9 +90,9 @@ export const useApiStore = defineStore('apiStore', {
async fetchDonatorsData() { async fetchDonatorsData() {
try { try {
const response = await this.client!.get<API.Donators.Response>('api/getDonators'); const response = await this.client.get<API.Donators.Response>('api/getDonators');
this.donatorsData = response.data; this.donatorsData = response;
} catch (error) { } catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania informacji o donatorach:', error); console.error('Ups! Wystąpił błąd podczas pobierania informacji o donatorach:', error);
} }
@@ -104,9 +100,7 @@ export const useApiStore = defineStore('apiStore', {
async fetchStationsGeneralInfo() { async fetchStationsGeneralInfo() {
try { try {
const sceneryData: StationJSONData[] = ( const sceneryData = await this.client.get<StationJSONData[]>(`api/getSceneries`);
await this.client!.get<StationJSONData[]>(`api/getSceneries`)
).data;
this.dataStatuses.sceneries = Status.Data.Loaded; this.dataStatuses.sceneries = Status.Data.Loaded;
this.sceneryData = sceneryData; this.sceneryData = sceneryData;
@@ -118,10 +112,10 @@ export const useApiStore = defineStore('apiStore', {
async fetchVehiclesInfo() { async fetchVehiclesInfo() {
try { try {
const response = await this.client!.get<API.VehiclesData.Response>('api/getVehiclesData'); const response = await this.client.get<API.VehiclesData.Response>('api/getVehiclesData');
this.vehiclesData = response.data; this.vehiclesData = response;
this.dataStatuses.vehicles = response.data ? Status.Data.Loaded : Status.Data.Warning; this.dataStatuses.vehicles = response ? Status.Data.Loaded : Status.Data.Warning;
} catch (error) { } catch (error) {
this.dataStatuses.vehicles = Status.Data.Error; this.dataStatuses.vehicles = Status.Data.Error;
console.error('Ups! Wystąpił błąd podczas pobierania informacji o pojazdach:', error); console.error('Ups! Wystąpił błąd podczas pobierania informacji o pojazdach:', error);
@@ -130,9 +124,7 @@ export const useApiStore = defineStore('apiStore', {
async fetchDailyStats() { async fetchDailyStats() {
try { try {
const res: API.DailyStats.Response = await ( const res = await this.client.get<API.DailyStats.Response>('api/getDailyStats');
await this.client!.get('api/getDailyStats')
).data;
this.dailyStatsData = res; this.dailyStatsData = res;
+2 -2
View File
@@ -78,14 +78,14 @@ h1.option-title {
display: flex; display: flex;
gap: 0.5em; gap: 0.5em;
width: 100%; width: 100%;
margin-top: 0.5em; margin-top: 1em;
button { button {
width: 100%; width: 100%;
} }
} }
@include responsive.smallScreen{ @include responsive.smallScreen {
h1 { h1 {
text-align: center; text-align: center;
+1 -2
View File
@@ -26,7 +26,7 @@
.dropdown_wrapper { .dropdown_wrapper {
position: absolute; position: absolute;
left: 0; left: 0;
top: calc(100% + 0.5em); top: 0;
background-color: var(--clr-bg3); background-color: var(--clr-bg3);
box-shadow: 0 0 5px 1px var(--clr-primary); box-shadow: 0 0 5px 1px var(--clr-primary);
@@ -34,7 +34,6 @@
width: 100%; width: 100%;
max-width: 550px; max-width: 550px;
max-height: 750px;
overflow: auto; overflow: auto;
padding: 1em; padding: 1em;
+2 -3
View File
@@ -24,8 +24,8 @@
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 1em 0; padding: 1em 0;
position: relative;
} }
.journal_refreshed-date { .journal_refreshed-date {
@@ -57,7 +57,6 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 0.5em; gap: 0.5em;
position: relative;
} }
.btn--load-data { .btn--load-data {
@@ -68,7 +67,7 @@
font-size: 1.2em; font-size: 1.2em;
} }
@include responsive.smallScreen{ @include responsive.smallScreen {
.journal_top-bar { .journal_top-bar {
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
-1
View File
@@ -15,7 +15,6 @@
gap: 0.25em; gap: 0.25em;
min-width: 200px; min-width: 200px;
margin-right: 0.25em;
} }
&-input { &-input {
+3 -1
View File
@@ -253,8 +253,10 @@ export namespace API {
pn?: number; pn?: number;
tn?: number; tn?: number;
returnType?: 'all' | 'short' | 'detailed'; headUnitName?: string;
headUnitType?: string;
returnType?: 'all' | 'short' | 'detailed';
sortBy?: Journal.TimetableSorter['id']; sortBy?: Journal.TimetableSorter['id'];
} }
+8 -6
View File
@@ -217,9 +217,10 @@ export default defineComponent({
this.scrollDataLoaded = false; this.scrollDataLoaded = false;
this.currentQueryParams['countFrom'] = this.historyList.length; this.currentQueryParams['countFrom'] = this.historyList.length;
const responseData: API.DispatcherHistory.Response = await ( const responseData: API.DispatcherHistory.Response = await await this.apiStore.client.get(
await this.apiStore.client!.get(`api/getDispatchers`, { params: this.currentQueryParams }) `api/getDispatchers`,
).data; this.currentQueryParams
);
if (!responseData) return; if (!responseData) return;
@@ -276,9 +277,10 @@ export default defineComponent({
this.currentQueryParams = queryParams; this.currentQueryParams = queryParams;
try { try {
const responseData: API.DispatcherHistory.Response = await ( const responseData: API.DispatcherHistory.Response = await this.apiStore.client.get(
await this.apiStore.client!.get(`api/getDispatchers`, { params: this.currentQueryParams }) `api/getDispatchers`,
).data; this.currentQueryParams
);
if (!responseData) { if (!responseData) {
this.dataStatus = Status.Data.Error; this.dataStatus = Status.Data.Error;
+25 -14
View File
@@ -173,8 +173,9 @@ export default defineComponent({
'search-issuedFrom': '', 'search-issuedFrom': '',
'search-via': '', 'search-via': '',
'search-terminatingAt': '', 'search-terminatingAt': '',
'select-categoryCode': '', 'search-headUnit': '',
'search-date-from': '' 'search-date-from': '',
'select-categoryCode': ''
} as Journal.TimetableSearchType); } as Journal.TimetableSearchType);
const countFromIndex = ref(0); const countFromIndex = ref(0);
@@ -277,11 +278,10 @@ export default defineComponent({
this.currentQueryParams['countFrom'] = this.timetableHistory.length; this.currentQueryParams['countFrom'] = this.timetableHistory.length;
const responseData: API.TimetableHistory.Response = await ( const responseData: API.TimetableHistory.Response = await this.apiStore.client.get(
await this.apiStore.client!.get('api/getTimetables', { 'api/getTimetables',
params: this.currentQueryParams this.currentQueryParams
}) );
).data;
if (!responseData) return; if (!responseData) return;
@@ -297,6 +297,8 @@ export default defineComponent({
async fetchHistoryData() { async fetchHistoryData() {
this.extraInfoIndexes.length = 0; this.extraInfoIndexes.length = 0;
const queryParams: API.TimetableHistory.QueryParams = {};
const driverName = this.searchersValues['search-driver'].trim() || undefined; const driverName = this.searchersValues['search-driver'].trim() || undefined;
const trainNo = this.searchersValues['search-train'].trim() || undefined; const trainNo = this.searchersValues['search-train'].trim() || undefined;
const authorName = this.searchersValues['search-dispatcher'].trim() || undefined; const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
@@ -306,6 +308,7 @@ export default defineComponent({
const via = this.searchersValues['search-via'].trim() || undefined; const via = this.searchersValues['search-via'].trim() || undefined;
const terminatingAt = this.searchersValues['search-terminatingAt'].trim() || undefined; const terminatingAt = this.searchersValues['search-terminatingAt'].trim() || undefined;
const categoryCode = this.searchersValues['select-categoryCode'].trim() || undefined; const categoryCode = this.searchersValues['select-categoryCode'].trim() || undefined;
const headUnit = this.searchersValues['search-headUnit'].trim() || undefined;
let dateFromISO: string | undefined = undefined; let dateFromISO: string | undefined = undefined;
let dateToISO: string | undefined = undefined; let dateToISO: string | undefined = undefined;
@@ -321,8 +324,6 @@ export default defineComponent({
dateToISO = dateTo.toISOString(); dateToISO = dateTo.toISOString();
} }
const queryParams: API.TimetableHistory.QueryParams = {};
this.filterList this.filterList
.filter((f) => f.isActive) .filter((f) => f.isActive)
.forEach((f) => { .forEach((f) => {
@@ -394,17 +395,27 @@ export default defineComponent({
queryParams['sortBy'] = queryParams['sortBy'] =
this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined; this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined;
// Head unit params
if (headUnit) {
const [headUnitName, headUnitNumber] = headUnit.split('-');
if (headUnitNumber && !isNaN(Number(headUnitNumber))) {
queryParams['headUnitName'] = `${headUnitName}-${headUnitNumber}`;
} else {
queryParams['headUnitType'] = headUnitName;
}
}
if (JSON.stringify(this.currentQueryParams) != JSON.stringify(queryParams)) if (JSON.stringify(this.currentQueryParams) != JSON.stringify(queryParams))
this.dataStatus = Status.Data.Loading; this.dataStatus = Status.Data.Loading;
this.currentQueryParams = queryParams; this.currentQueryParams = queryParams;
try { try {
const responseData: API.TimetableHistory.ResponseShort = await ( const responseData: API.TimetableHistory.ResponseShort = await this.apiStore.client.get(
await this.apiStore.client!.get('api/getTimetables', { 'api/getTimetables',
params: this.currentQueryParams this.currentQueryParams
}) );
).data;
if (!responseData) { if (!responseData) {
this.dataStatus = Status.Data.Error; this.dataStatus = Status.Data.Error;
+20 -24
View File
@@ -40,7 +40,6 @@ import Loading from '../components/Global/Loading.vue';
import ProfileSummary from '../components/PlayerProfileView/ProfileSummary.vue'; import ProfileSummary from '../components/PlayerProfileView/ProfileSummary.vue';
import ProfileRecentStats from '../components/PlayerProfileView/ProfileRecentStats.vue'; import ProfileRecentStats from '../components/PlayerProfileView/ProfileRecentStats.vue';
import ProfileHistoryList from '../components/PlayerProfileView/ProfileHistoryList.vue'; import ProfileHistoryList from '../components/PlayerProfileView/ProfileHistoryList.vue';
import axios from 'axios';
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
@@ -71,29 +70,28 @@ onDeactivated(() => {
}); });
async function fetchPlayerInfo(playerId: number) { async function fetchPlayerInfo(playerId: number) {
return apiStore.client!.get<API.PlayerInfo.Data>('api/getPlayerInfo', { return apiStore.client.get<API.PlayerInfo.Data>('api/getPlayerInfo', {
params: { playerId
playerId
}
}); });
} }
async function fetchPlayerJournal(playerId: number) { async function fetchPlayerJournal(playerId: number) {
return apiStore.client!.get<API.PlayerJournal.Data>('api/getPlayerJournal', { return apiStore.client.get<API.PlayerJournal.Data>('api/getPlayerJournal', {
params: { playerId,
playerId, dateScope: '30d'
dateScope: '30d'
}
}); });
} }
async function fetchPlayerTd2Info(playerName: string) { async function fetchPlayerTd2Info(playerName: string): Promise<Td2API.UsersInfoByName.Response> {
return axios.get<Td2API.UsersInfoByName.Response>('https://api.td2.info.pl', { const response = await fetch(
params: { `https://api.td2.info.pl?method=getUsersInfoByName&name=${playerName}`
method: 'getUsersInfoByName', );
name: playerName
} if (!response.ok) {
}); throw new Error('fetchPlayerTd2Info: could not fetch data');
}
return response.json();
} }
async function fetchPlayerData() { async function fetchPlayerData() {
@@ -116,23 +114,21 @@ async function fetchPlayerData() {
const playerInfoResp = await fetchPlayerInfo(playerId.value); const playerInfoResp = await fetchPlayerInfo(playerId.value);
playerName.value = playerName.value =
playerInfoResp.data.driverStats.driverName || playerInfoResp.driverStats.driverName || playerInfoResp.dispatcherStats.dispatcherName || '';
playerInfoResp.data.dispatcherStats.dispatcherName ||
'';
if (!playerName.value) { if (!playerName.value) {
router.push('/'); router.push('/');
return; return;
} }
playerInfo.value = playerName.value ? playerInfoResp.data : undefined; playerInfo.value = playerName.value ? playerInfoResp : undefined;
playerInfoStatus.value = Status.Data.Loaded; playerInfoStatus.value = Status.Data.Loaded;
if (playerName.value) { if (playerName.value) {
const playerTD2InfoResp = await fetchPlayerTd2Info(playerName.value); const playerTD2InfoResp = await fetchPlayerTd2Info(playerName.value);
if (playerTD2InfoResp.data.success && playerTD2InfoResp.data.message.length == 1) { if (playerTD2InfoResp.success && playerTD2InfoResp.message.length == 1) {
playerTD2Info.value = playerTD2InfoResp.data.message[0]; playerTD2Info.value = playerTD2InfoResp.message[0];
} }
} }
} catch (error) { } catch (error) {
@@ -144,7 +140,7 @@ async function fetchPlayerData() {
try { try {
const playerJournalResp = await fetchPlayerJournal(playerId.value); const playerJournalResp = await fetchPlayerJournal(playerId.value);
playerJournal.value = playerJournalResp.data; playerJournal.value = playerJournalResp;
playerJournalStatus.value = Status.Data.Loaded; playerJournalStatus.value = Status.Data.Loaded;
} catch (error) { } catch (error) {
playerJournal.value = undefined; playerJournal.value = undefined;
+6 -1
View File
@@ -58,6 +58,7 @@ import SceneryDispatchersHistory from '../components/SceneryView/SceneryDispatch
import { useApiStore } from '../store/apiStore'; import { useApiStore } from '../store/apiStore';
import { Status } from '../typings/common'; import { Status } from '../typings/common';
import SceneryTopList from '../components/SceneryView/SceneryTopList.vue';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@@ -89,6 +90,10 @@ const viewModes = [
{ {
id: 'scenery.option-dispatchers-history', id: 'scenery.option-dispatchers-history',
component: SceneryDispatchersHistory component: SceneryDispatchersHistory
},
{
id: 'scenery.option-top-list',
component: SceneryTopList
} }
]; ];
@@ -184,7 +189,7 @@ function setViewMode(componentName: string) {
background-color: #181818; background-color: #181818;
border-radius: 0.5em; border-radius: 0.5em;
padding: 0.5em; padding: 1em;
} }
.scenery-left { .scenery-left {
+1 -5
View File
@@ -116,13 +116,10 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@use '../styles/responsive'; @use '../styles/responsive';
.trains-view {
position: relative;
}
.trains_wrapper { .trains_wrapper {
margin: 1rem auto; margin: 1rem auto;
max-width: var(--max-container-width); max-width: var(--max-container-width);
position: relative;
} }
.trains_topbar { .trains_topbar {
@@ -130,7 +127,6 @@ export default defineComponent({
align-items: center; align-items: center;
gap: 0.5em; gap: 0.5em;
position: relative;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"noUncheckedIndexedAccess": false,
"verbatimModuleSyntax": null,
"paths": {
"@/*": ["./src/*"]
},
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
}
}
+4 -17
View File
@@ -1,24 +1,11 @@
{ {
"compilerOptions": { "files": [],
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"types": ["vite/client", "vite-plugin-pwa/client"],
"skipLibCheck": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [ "references": [
{ {
"path": "./tsconfig.node.json" "path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
} }
] ]
} }
+9 -6
View File
@@ -1,9 +1,12 @@
// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
{ {
"extends": "@tsconfig/node24/tsconfig.json",
"include": ["vite.config.*", "eslint.config.*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "module": "preserve",
"module": "nodenext", "moduleResolution": "bundler",
"moduleResolution": "nodenext", "types": ["node", "vite/client", "vite-plugin-pwa/client"],
"allowSyntheticDefaultImports": true "noEmit": true,
}, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
"include": ["vite.config.ts"] }
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import { VitePWA } from 'vite-plugin-pwa'; import { VitePWA } from 'vite-plugin-pwa';
import path from 'path'; import { fileURLToPath } from 'node:url';
export default defineConfig({ export default defineConfig({
server: { port: 5123, open: false }, server: { port: 5123, open: false },
@@ -14,7 +14,7 @@ export default defineConfig({
}, },
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, 'src') '@': fileURLToPath(new URL('./src', import.meta.url))
} }
}, },
plugins: [ plugins: [
+1493 -1492
View File
File diff suppressed because it is too large Load Diff