Compare commits

..

11 Commits

Author SHA1 Message Date
Spythere a34eef098b Merge pull request #158 from Spythere/development
v1.33.0
2026-03-22 23:17:43 +01:00
Spythere 97a829a21c Merge pull request #156 from Spythere/development
v1.32.0 hotfixes
2026-02-27 00:32:11 +01:00
Spythere 83444f64d0 Merge pull request #155 from Spythere/development
v1.32.0
2026-02-26 21:15:04 +01:00
Spythere c2f7eef146 Merge pull request #153 from Spythere/development
v1.31.1
2026-01-16 22:27:16 +01:00
Spythere 3c78af4dc0 Merge pull request #151 from Spythere/development
v1.31.0
2026-01-10 21:22:48 +01:00
Spythere fc7a9be9dd Merge pull request #150 from Spythere/development
Fix for station statistics dropdown overflow
2025-12-13 02:21:19 +01:00
Spythere 3c3a114a38 Merge pull request #149 from Spythere/development
Information about migration to the new domain
2025-12-13 00:20:13 +01:00
Spythere fe6972c1f8 Merge pull request #148 from Spythere/development
Extended isChristmas check from 20th to 6th December
2025-12-05 21:28:04 +01:00
Spythere 08b9b72dcd Merge pull request #147 from Spythere/development
Hotfix for VPS deploy
2025-12-04 00:27:38 +01:00
Spythere c90be042e7 Merge pull request #146 from Spythere/development
Updated GitHub workflow for deploying files to dedicated VPS
2025-12-04 00:21:41 +01:00
Spythere 430a05ab38 Merge pull request #145 from Spythere/development
v1.30.7
2025-11-28 01:14:13 +01:00
61 changed files with 2003 additions and 2636 deletions
+2 -1
View File
@@ -15,12 +15,13 @@ pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.vscode/settings.json
node_modules
*.log
-7
View File
@@ -1,7 +0,0 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}
-3
View File
@@ -1,3 +0,0 @@
{
"recommendations": ["Vue.volar", "esbenp.prettier-vscode"]
}
Vendored
-1
View File
@@ -1 +0,0 @@
/// <reference types="vite/client" />
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "stacjownik",
"version": "1.35.0",
"version": "1.33.0",
"private": true,
"type": "module",
"scripts": {
@@ -26,12 +26,12 @@
"vue-router": "^4.4.0"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
"@types/node": "^24.12.0",
"@types/node": "^24.3.1",
"@types/showdown": "^2.0.6",
"@vite-pwa/assets-generator": "^1.0.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
"axios": "^1.9.0",
"prettier": "^3.3.3",
"typescript": "^5.5.4",
"vite": "^7.1.4",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

+6 -9
View File
@@ -30,6 +30,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import axios from 'axios';
import { version } from '../package.json';
import { Status } from './typings/common';
@@ -113,15 +114,11 @@ export default defineComponent({
}
try {
const response = await fetch(
'https://api.github.com/repos/Spythere/stacjownik/releases/latest'
);
const releaseData = await (
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
).data;
if (!response.ok) {
throw new Error('Failed to fetch release data from repository!');
}
const releaseData = await response.json();
if (!releaseData) return;
this.store.appUpdate = {
version,
@@ -133,7 +130,7 @@ export default defineComponent({
(storageVersion != '' && storageVersion != version && this.isOnProductionHost) ||
import.meta.env.VITE_UPDATE_TEST === 'test';
} catch (error) {
console.error(error);
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
}
StorageManager.setStringValue(STORAGE_VERSION_KEY, version);
+3 -19
View File
@@ -1,9 +1,7 @@
<template>
<Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
<div class="content" tabindex="0" ref="content">
<h1 class="content-title">
<i class="fa-solid fa-wand-sparkles"></i> {{ $t('update.title') }}
</h1>
<h1 class="content-title"><i class="fa-solid fa-wand-sparkles"></i> {{ $t('update.title') }}</h1>
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
<div class="no-features" v-else>{{ $t('update.no-data') }}</div>
@@ -89,27 +87,13 @@ export default defineComponent({
::v-deep(h2) {
padding: 0.5em 0;
&::after {
content: '';
display: block;
height: 2px;
width: 100%;
background-color: #aaa;
margin-top: 0.25em;
}
}
::v-deep(h3) {
padding-bottom: 0.25em;
border-bottom: 1px solid #aaa;
}
::v-deep(ul) {
list-style: disc;
padding: 0.5em 1.5em;
line-height: 1.5em;
padding: 0 1.5em;
padding-bottom: 0.5em;
}
.content {
@@ -1,325 +0,0 @@
<template>
<div class="driver-propositions">
<h3>{{ t('trains.number-propositions-header') }}</h3>
<div class="categories-select">
<button
v-for="(category, i) in availableCategories"
class="btn btn--option btn--action"
@click="selectCategory(i)"
:class="{ checked: i == chosenCategoryIndex }"
>
{{ category }}
</button>
</div>
<div v-if="numberPropositions.length > 0" class="propositions-numbers">
<div v-if="chosenCategory">
<b>{{ chosenCategory }} </b> -
{{ t(`categories.${chosenCategory.slice(0, 2)}`) }}
({{ t(`categories.${chosenCategory.slice(2)}`) }})
</div>
<div v-if="chosenCategoryRules">
<span v-if="chosenCategoryRules[0]"
>{{ t('trains.number-propositions-third-number') }}
<b class="text--primary">{{ chosenCategoryRules[0] }}</b> &bull;
</span>
<span
>{{
t('trains.number-propositions-last-nums', {
count: chosenCategoryRules[1].length
})
}}
<b class="text--primary">{{ chosenCategoryRules[1] }}</b> -
<b class="text--primary">{{ chosenCategoryRules[2] }}</b></span
>
</div>
<div style="margin-top: 0.5em">
<b>{{ t('trains.number-propositions-title') }}&nbsp;</b>
<i>{{ numberPropositions.join(', ') }}</i>
</div>
</div>
<div class="no-propositions" v-else>{{ t('trains.number-propositions-empty') }}</div>
<div class="cargo-warnings" v-if="getCargoWarnings.size > 0">
<hr />
<h3>{{ t('cargo-warnings.title') }}</h3>
<div class="warnings-container">
<div
v-for="warning in getCargoWarnings"
class="train-badge"
:class="`${warning.split('-')[0]}`"
>
{{ t('cargo-warnings.' + warning) }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, PropType, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { Train } from '../../typings/common';
import rulesJSON from '../../data/trainNumberRules.json';
import { useApiStore } from '../../store/apiStore';
const { t } = useI18n();
const apiStore = useApiStore();
const props = defineProps({
chosenTrain: {
type: Object as PropType<Train>,
required: true
}
});
const emits = defineEmits(['selectCategory']);
const chosenCategoryIndex = ref(0);
const numberPropositions = ref<string[]>([]);
const chosenCategoryRules = ref<any[]>([]);
watch(
computed(() => props.chosenTrain.trainNo),
() => {
chosenCategoryIndex.value = 0;
generateNumberPropositions();
}
);
onMounted(() => {
generateNumberPropositions();
});
function generateNumberPropositions() {
const categoryCode = chosenCategory.value?.slice(0, 2);
const trainNoStr = props.chosenTrain.trainNo.toString();
// Get category rules
const rules = categoryCode
? ((rulesJSON.categoriesRules as any)[categoryCode] as any[])
: undefined;
if (!categoryCode || !rules) {
numberPropositions.value.length = 0;
chosenCategoryRules.value.length = 0;
return;
}
const [thirdNumber, minRange, maxRange] = rules;
const propositionsArr: string[] = [];
for (let i = 0; i < 5; i++) {
let generatedNumStr = '';
generatedNumStr += trainNoStr[0] ?? Math.floor(Math.random() * 10);
generatedNumStr += trainNoStr[1] ?? Math.floor(Math.random() * 10);
// Third number
generatedNumStr += thirdNumber ?? '';
// Remaining numbers
const rangeNums = minRange?.length ?? 3;
const randRange = Math.floor(
Math.random() * (Number(maxRange) - Number(minRange)) + Number(minRange)
).toString();
const leadingZeros = new Array(Math.abs(randRange.toString().length - rangeNums))
.fill('0')
.join('');
generatedNumStr += `${leadingZeros}${randRange}`;
const isNumberTaken =
apiStore.activeData?.trains?.some((t) => t.trainNo.toString() == generatedNumStr) ?? false;
if (!isNumberTaken) {
propositionsArr.push(generatedNumStr);
} else {
i--;
}
if (Number(randRange) > Number(maxRange)) break;
}
numberPropositions.value = propositionsArr;
chosenCategoryRules.value = rules;
}
const chosenCategory = computed(() => {
return availableCategories.value[chosenCategoryIndex.value];
});
const getCargoWarnings = computed(() => {
const stockList = props.chosenTrain.stockList;
let warnings: Set<string> = new Set();
stockList.forEach((stockVehicle) => {
const [vehicleName, vehicleCargo] = stockVehicle.split(':');
if (vehicleName.startsWith('WB117')) warnings.add(vehicleCargo ? 'twr-un1965' : 'tn-un1965');
else if (vehicleName.startsWith('445Rb'))
warnings.add(vehicleCargo ? 'tn-un1202' : 'tn-un1202-empty');
else if (vehicleName.startsWith('EDK80')) warnings.add('pn-edk80');
if (vehicleCargo) {
if (vehicleCargo.startsWith('wt_20')) warnings.add('pn-innofreight');
else if (/^(tank|vehicles_01|truck)/.test(vehicleCargo)) warnings.add('pn-military');
}
});
return warnings;
});
const availableCategories = computed(() => {
const stockList = props.chosenTrain.stockList;
const headVehicle = stockList[0]?.split('-')[0] ?? '';
let availableCategories: string[] = [];
let categoryTraction = 'E';
let vehicleTypesSet = new Set<string>();
let wagonsNamesSet = new Set<string>();
let cargoNamesSet = new Set<string>();
for (const stockName of stockList) {
const [vehicleName, ...cargoList] = stockName.split(':');
const vehicleData = apiStore.vehiclesData?.vehicles.find((v) => v.name == vehicleName);
if (!vehicleData) continue;
vehicleTypesSet.add(vehicleData.type);
if (vehicleData.type.startsWith('wagon-')) wagonsNamesSet.add(vehicleData.name.split('_')[0]);
if (cargoList !== undefined) cargoList.forEach((c) => cargoNamesSet.add(c.split('_')[0]));
}
let vehicleTypesArr = [...vehicleTypesSet];
let wagonsNamesArr = [...wagonsNamesSet];
// Traction
if (vehicleTypesArr[0] == 'loco-electric') categoryTraction = 'E';
else if (vehicleTypesArr[0] == 'loco-diesel') categoryTraction = 'S';
else if (vehicleTypesArr[0] == 'unit-electric') categoryTraction = 'J';
else categoryTraction = 'M';
// EMU / DMU - M*, R*, P*
if (vehicleTypesArr.length == 1 && (categoryTraction == 'J' || categoryTraction == 'M')) {
availableCategories.push('MO', 'MP', 'MM', 'RO', 'RP', 'RA', 'RM', 'PW');
}
// Only locos (up to 3) - LT, LP, LS
else if (stockList.length <= 3 && vehicleTypesArr.every((v) => v.startsWith('loco-'))) {
if (/^(EU|ET|201E|4E|SU|ST|M62|CTLR4C)/.test(headVehicle)) availableCategories.push('LT');
if (/^(EU|EP|SU|SP)/.test(headVehicle)) availableCategories.push('LP');
if (/^(SM)/.test(headVehicle)) availableCategories.push('LS');
}
// Only locos (more than 3) - TH
else if (stockList.length > 3 && vehicleTypesArr.every((v) => v.startsWith('loco-'))) {
availableCategories.push('TH');
}
// Loco(s) + passenger only wagons - M*, R*, E*, P*
else if (vehicleTypesArr.every((v) => v.startsWith('loco-') || v == 'wagon-passenger')) {
availableCategories.push('EI', 'EC', 'EN', 'MO', 'MP', 'MM', 'RO', 'RP', 'RA', 'RM', 'PW');
}
// Loco(s) + cargo only / mixed wagons - T*, Z*
else {
if (wagonsNamesArr.every((v) => /^(627Z|412Z)/.test(v)))
availableCategories.push('TC', 'TD', 'TS');
else if (stockList.slice(1).every((v) => /PKPE/.test(v))) {
availableCategories.push('ZU', 'ZN');
} else if (wagonsNamesArr.length < 3 || cargoNamesSet.size < 3) {
availableCategories.push('TM', 'TG', 'TS', 'TK');
} else {
availableCategories.push('TN', 'TR', 'TS', 'TK');
}
}
return availableCategories.map((c) => `${c}${categoryTraction}`);
});
function selectCategory(i: number) {
chosenCategoryIndex.value = i;
generateNumberPropositions();
}
</script>
<style lang="scss" scoped>
@use '../../styles/responsive';
@use '../../styles/badge';
.driver-propositions {
margin-bottom: 1em;
padding: 0.5em;
background-color: #111;
}
.categories-select {
display: inline-flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
position: relative;
&::after {
content: '';
position: absolute;
bottom: calc(-0.5em);
left: 0;
width: 100%;
height: 2px;
background-color: #aaa;
}
}
.propositions-numbers {
margin-top: 1em;
}
.no-propositions {
margin-top: 1em;
color: #ccc;
}
.cargo-warnings {
margin-top: 0.5em;
h3 {
margin: 0.5em 0;
}
}
.warnings-container {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
}
@include responsive.smallScreen {
.driver-propositions {
text-align: center;
}
.categories-select {
justify-content: center;
}
.warnings-container {
justify-content: center;
}
}
</style>
+244 -7
View File
@@ -4,18 +4,63 @@
<!-- Train action buttons -->
<div class="train-stock-actions">
<button class="btn btn--action" @click="copyStockToClipboard()">
<button class="btn btn--action" style="margin: 1em 0" @click="copyStockToClipboard()">
<i class="fa-regular fa-copy"></i> {{ i18n.t('trains.stock-copy') }}
</button>
<button class="btn btn--action" @click="toggleNumberPropositions()">
<button class="btn btn--action" style="margin: 1em 0" @click="toggleNumberPropositions()">
<i class="fa-regular fa-lightbulb"></i> {{ i18n.t('trains.number-propositions') }}
</button>
</div>
<!-- Proposed numbers container -->
<transition name="view-anim">
<DriverPropositions :chosenTrain="chosenTrain" v-if="arePropositionsVisible" />
<transition name="view-anim" class="propositions-container">
<div v-if="arePropositionsVisible">
<h3 style="margin-bottom: 0.5em">{{ i18n.t('trains.number-propositions-header') }}</h3>
<div class="categories-select">
<button
v-for="(category, i) in availableCategories"
class="btn btn--option btn--action"
@click="selectCategory(i)"
:class="{ checked: i == chosenCategoryIndex }"
>
{{ category }}
</button>
</div>
<div v-if="numberPropositions.length > 0" class="propositions-numbers">
<div v-if="chosenCategory">
<b>{{ chosenCategory }} </b> -
{{ i18n.t(`categories.${chosenCategory.slice(0, 2)}`) }}
({{ i18n.t(`categories.${chosenCategory.slice(2)}`) }})
</div>
<div v-if="chosenCategoryRules">
<span v-if="chosenCategoryRules[0]"
>{{ i18n.t('trains.number-propositions-third-number') }}
<b class="text--primary">{{ chosenCategoryRules[0] }}</b> &bull;
</span>
<span
>{{
i18n.t('trains.number-propositions-last-nums', {
count: chosenCategoryRules[1].length
})
}}
<b class="text--primary">{{ chosenCategoryRules[1] }}</b> -
<b class="text--primary">{{ chosenCategoryRules[2] }}</b></span
>
</div>
<div style="margin-top: 0.5em">
<b>{{ i18n.t('trains.number-propositions-title') }}&nbsp;</b>
<i>{{ numberPropositions.join(', ') }}</i>
</div>
</div>
<div class="no-propositions" v-else>{{ i18n.t('trains.number-propositions-empty') }}</div>
</div>
</transition>
<StockList :trainStockList="chosenTrain.stockList" :key="chosenTrain.id" :showPreviews="true" />
@@ -27,15 +72,25 @@
import { PropType, ref } from 'vue';
import { Train } from '../../typings/common';
import { useI18n } from 'vue-i18n';
import { useApiStore } from '../../store/apiStore';
import StockList from '../Global/StockList.vue';
import TrainSchedule from '../TrainsView/TrainSchedule.vue';
import TrainInfo from '../TrainsView/TrainInfo.vue';
import DriverPropositions from './DriverPropositions.vue';
import rulesJSON from '../../data/trainNumberRules.json';
import { computed } from 'vue';
import { watch } from 'vue';
const apiStore = useApiStore();
const i18n = useI18n();
const arePropositionsVisible = ref(false);
const chosenCategoryIndex = ref(0);
const numberPropositions = ref<string[]>([]);
const chosenCategoryRules = ref<any[]>([]);
const props = defineProps({
chosenTrain: {
@@ -64,7 +119,153 @@ function copyStockToClipboard() {
function toggleNumberPropositions() {
arePropositionsVisible.value = !arePropositionsVisible.value;
if (arePropositionsVisible.value) generateNumberPropositions();
}
function selectCategory(i: number) {
chosenCategoryIndex.value = i;
generateNumberPropositions();
}
function generateNumberPropositions() {
const categoryCode = chosenCategory.value?.slice(0, 2);
const trainNoStr = props.chosenTrain.trainNo.toString();
// Get category rules
const rules = categoryCode
? ((rulesJSON.categoriesRules as any)[categoryCode] as any[])
: undefined;
if (!categoryCode || !rules) {
numberPropositions.value.length = 0;
chosenCategoryRules.value.length = 0;
return;
}
const [thirdNumber, minRange, maxRange] = rules;
const propositionsArr: string[] = [];
for (let i = 0; i < 5; i++) {
let generatedNumStr = '';
generatedNumStr += trainNoStr.at(0) ?? Math.floor(Math.random() * 10);
generatedNumStr += trainNoStr.at(1) ?? Math.floor(Math.random() * 10);
// Third number
generatedNumStr += thirdNumber ?? '';
// Remaining numbers
const rangeNums = minRange?.length ?? 3;
const randRange = Math.floor(
Math.random() * (Number(maxRange) - Number(minRange)) + Number(minRange)
).toString();
const leadingZeros = new Array(Math.abs(randRange.toString().length - rangeNums))
.fill('0')
.join('');
generatedNumStr += `${leadingZeros}${randRange}`;
const isNumberTaken =
apiStore.activeData?.trains?.some((t) => t.trainNo.toString() == generatedNumStr) ?? false;
if (!isNumberTaken) {
propositionsArr.push(generatedNumStr);
} else {
i--;
}
if (Number(randRange) > Number(maxRange)) break;
}
numberPropositions.value = propositionsArr;
chosenCategoryRules.value = rules;
}
const chosenCategory = computed(() => {
return availableCategories.value.at(chosenCategoryIndex.value);
});
const availableCategories = computed(() => {
const stockList = props.chosenTrain.stockList;
const headVehicle = stockList.at(0)?.split('-')[0] ?? '';
let availableCategories: string[] = [];
let categoryTraction = 'E';
let vehicleTypesSet = new Set<string>();
let wagonsNamesSet = new Set<string>();
let cargoNamesSet = new Set<string>();
for (const stockName of stockList) {
const [vehicleName, ...cargoList] = stockName.split(':');
const vehicleData = apiStore.vehiclesData?.vehicles.find((v) => v.name == vehicleName);
if (!vehicleData) continue;
vehicleTypesSet.add(vehicleData.type);
if (vehicleData.type.startsWith('wagon-')) wagonsNamesSet.add(vehicleData.name.split('_')[0]);
if (cargoList !== undefined) cargoList.forEach((c) => cargoNamesSet.add(c.split('_')[0]));
}
let vehicleTypesArr = [...vehicleTypesSet];
let wagonsNamesArr = [...wagonsNamesSet];
// Traction
if (vehicleTypesArr[0] == 'loco-electric') categoryTraction = 'E';
else if (vehicleTypesArr[0] == 'loco-diesel') categoryTraction = 'S';
else if (vehicleTypesArr[0] == 'unit-electric') categoryTraction = 'J';
else categoryTraction = 'M';
// EMU / DMU - M*, R*, P*
if (vehicleTypesArr.length == 1 && (categoryTraction == 'J' || categoryTraction == 'M')) {
availableCategories.push('MO', 'MP', 'MM', 'RO', 'RP', 'RA', 'RM', 'PW');
}
// Only locos (up to 3) - LT, LP, LS
else if (stockList.length <= 3 && vehicleTypesArr.every((v) => v.startsWith('loco-'))) {
if (/^(EU|ET|201E|4E|SU|ST|M62|CTLR4C)/.test(headVehicle)) availableCategories.push('LT');
if (/^(EU|EP|SU|SP)/.test(headVehicle)) availableCategories.push('LP');
if (/^(SM)/.test(headVehicle)) availableCategories.push('LS');
}
// Only locos (more than 3) - TH
else if (stockList.length > 3 && vehicleTypesArr.every((v) => v.startsWith('loco-'))) {
availableCategories.push('TH');
}
// Loco(s) + passenger only wagons - M*, R*, E*, P*
else if (vehicleTypesArr.every((v) => v.startsWith('loco-') || v == 'wagon-passenger')) {
availableCategories.push('EI', 'EC', 'EN', 'MO', 'MP', 'MM', 'RO', 'RP', 'RA', 'RM', 'PW');
}
// Loco(s) + cargo only / mixed wagons - T*, Z*
else {
if (wagonsNamesArr.every((v) => /^(627Z|412Z)/.test(v)))
availableCategories.push('TC', 'TD', 'TS');
else if (stockList.slice(1).every((v) => /PKPE/.test(v))) {
availableCategories.push('ZU', 'ZN');
} else if (wagonsNamesArr.length < 3 || cargoNamesSet.size < 3) {
availableCategories.push('TM', 'TG', 'TS', 'TK');
} else {
availableCategories.push('TN', 'TR', 'TS', 'TK');
}
}
return availableCategories.map((c) => `${c}${categoryTraction}`);
});
watch(
computed(() => `${props.chosenTrain.trainNo}`),
() => {
chosenCategoryIndex.value = 0;
generateNumberPropositions();
}
);
</script>
<style lang="scss" scoped>
@@ -78,13 +279,49 @@ function toggleNumberPropositions() {
.train-stock-actions {
display: flex;
gap: 0.5em;
}
.propositions-container {
margin-bottom: 1em;
padding: 0.5em;
background-color: #111;
}
.categories-select {
display: inline-flex;
flex-wrap: wrap;
gap: 0.5em;
margin: 1em 0;
position: relative;
&::after {
content: '';
position: absolute;
bottom: calc(-0.5em);
left: 0;
width: 100%;
height: 2px;
background-color: #aaa;
}
}
.propositions-numbers {
margin-top: 1em;
}
.no-propositions {
margin-top: 1em;
color: #ccc;
}
@include responsive.smallScreen {
.train-stock-actions {
.propositions-container {
text-align: center;
}
.categories-select {
justify-content: center;
}
}
+1 -3
View File
@@ -8,7 +8,6 @@
:images="images"
:image-fallbacks="imagesFallbacks"
:show-previews="showPreviews"
:thumbnail-size="thumbnailSize"
/>
</li>
</ul>
@@ -26,8 +25,7 @@ export default defineComponent({
props: {
trainStockList: { type: Array as PropType<string[]>, required: true },
tractionOnly: { type: Boolean, required: false },
showPreviews: { type: Boolean },
thumbnailSize: { type: Number }
showPreviews: { type: Boolean }
},
data() {
+3 -4
View File
@@ -9,7 +9,7 @@
<img
v-for="(thumbnailImage, imageIndex) in images"
:src="`https://stacjownik.spythere.eu/static/thumbnails/${thumbnailImage}.png`"
:height="thumbnailSize || 70"
height="70"
loading="lazy"
:data-crosshair-cursor="showPreviews"
:data-tooltip-type="showPreviews ? 'VehiclePreviewTooltip' : ''"
@@ -28,8 +28,7 @@ const props = defineProps({
vehicleString: { type: String, required: true },
images: { type: Object as PropType<string[]>, required: true },
imageFallbacks: { type: Object as PropType<string[]>, required: true },
showPreviews: { type: Boolean },
thumbnailSize: { type: Number }
showPreviews: { type: Boolean }
});
const thumbRef = ref(null) as Ref<HTMLElement | null>;
@@ -68,7 +67,7 @@ function onImageLoad() {
max-width: 90%;
text-align: center;
color: #aaa;
font-size: 0.8em;
font-size: 0.85em;
margin: 0 auto;
padding: 0.25em 0;
}
@@ -18,20 +18,7 @@
</b>
<span
v-if="isCreator(entry.dispatcherName)"
data-tooltip-type="CreatorTooltip"
:data-tooltip-content="$t('donations.creator-message')"
>
<router-link
class="text--creator"
:to="`/journal/dispatchers?search-dispatcher=${entry.dispatcherName}`"
>
{{ entry.dispatcherName }}
</router-link>
</span>
<span
v-else-if="apiStore.donatorsData.includes(entry.dispatcherName)"
v-if="apiStore.donatorsData.includes(entry.dispatcherName)"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.dispatcher-message')"
>
@@ -135,7 +122,6 @@ import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore';
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
import FlagIcon from '../../Global/FlagIcon.vue';
import { isCreator } from '../../../utils/userUtils';
export default defineComponent({
props: {
@@ -148,7 +134,7 @@ export default defineComponent({
emits: ['toggleShowExtraInfo'],
data() {
return { regions, apiStore: useApiStore(), isCreator };
return { regions, apiStore: useApiStore() };
},
methods: {
+4 -11
View File
@@ -269,9 +269,9 @@ export default defineComponent({
this.searchTimeout = window.setTimeout(async () => {
try {
const suggestions: string[] = await this.apiStore.client.get(
`api/get${type}Suggestions?name=${value}`
);
const suggestions: string[] = await (
await this.apiStore.client!.get(`api/get${type}Suggestions?name=${value}`)
).data;
this[`${type}Suggestions`] = suggestions;
} catch (error) {
@@ -336,17 +336,10 @@ export default defineComponent({
display: grid;
grid-template-rows: 1fr auto;
overflow: hidden;
max-height: calc(100% - 4.5em);
top: 3.5em;
padding: 1em 0;
max-height: 530px;
}
.options_content {
overflow: auto;
padding: 0 1em;
}
.options_actions {
padding: 0 1em;
}
</style>
@@ -64,18 +64,10 @@ function navigateToProfile() {
<style lang="scss" scoped>
@use '../../styles/dropdown';
@use '../../styles/dropdown-filters';
@use '../../styles/responsive';
.dropdown_wrapper {
left: auto;
right: 0;
max-width: 700px;
top: 3.5em;
}
@include responsive.smallScreen {
.dropdown_wrapper {
top: 6.25em;
}
}
</style>
@@ -201,20 +201,22 @@ const driverRouteLocation = computed<RouteLocationRaw | null>(() => {
async function fetchTimetableDetails() {
try {
const responseData = await apiStore.client.get<API.TimetableHistory.Response>(
const responseData = await apiStore.client!.get<API.TimetableHistory.Response>(
'api/getTimetables',
{
timetableId: props.timetableEntry.id,
returnType: 'detailed'
params: {
timetableId: props.timetableEntry.id,
returnType: 'detailed'
}
}
);
if (!responseData || responseData.length != 1) {
if (!responseData || responseData.data.length != 1) {
timetableDetails.value = null;
return;
}
timetableDetails.value = responseData[0];
timetableDetails.value = responseData.data[0];
} catch (error) {
// this.dataStatus = Status.Data.Error;
console.error(error);
@@ -59,17 +59,7 @@
</strong>
<router-link
v-if="isCreator(timetable.driverName)"
class="text--creator"
data-tooltip-type="CreatorTooltip"
:data-tooltip-content="$t('donations.creator-message')"
:to="`/journal/timetables?search-driver=${timetable.driverName}`"
>
<strong>{{ timetable.driverName }}</strong>
</router-link>
<router-link
v-else-if="apiStore.donatorsData.includes(timetable.driverName)"
v-if="apiStore.donatorsData.includes(timetable.driverName)"
class="text--donator"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.driver-message')"
@@ -125,7 +115,6 @@ import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore';
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
import FlagIcon from '../../Global/FlagIcon.vue';
import { isCreator } from '../../../utils/userUtils';
export default defineComponent({
components: { FlagIcon },
@@ -133,8 +122,7 @@ export default defineComponent({
data() {
return {
apiStore: useApiStore(),
isCreator
apiStore: useApiStore()
};
},
+1 -2
View File
@@ -15,8 +15,7 @@ export namespace Journal {
| 'search-issuedFrom'
| 'search-terminatingAt'
| 'search-via'
| 'select-categoryCode'
| 'search-headUnit';
| 'select-categoryCode';
export type TimetableSearchType = {
[key in TimetableSearchKey]: string;
@@ -5,10 +5,7 @@
<ProfilePlayerAvatar :playerTD2Info="playerTD2Info" />
<div>
<h2
class="player-name-header"
:class="{ 'text--donator': isPlayerDonator, 'text--creator': isPlayerCreator }"
>
<h2 class="player-name-header" :class="{ 'text--donator': isPlayerDonator }">
<a :href="`https://td2.info.pl/profile/?u=${route.query.playerId}`" target="_blank">
<img
v-if="isPlayerDonator"
@@ -235,7 +232,6 @@ import { useApiStore } from '../../store/apiStore';
import StationStatusBadge from '../Global/StationStatusBadge.vue';
import ProfilePlayerAvatar from './ProfilePlayerAvatar.vue';
import { getRegionNameById } from '../../utils/regionUtils';
import { isCreator } from '../../utils/userUtils';
const { t } = useI18n();
@@ -261,8 +257,6 @@ const isPlayerDonator = computed(() =>
props.playerName ? apiStore.donatorsData.includes(props.playerName) : false
);
const isPlayerCreator = computed(() => (props.playerName ? isCreator(props.playerName) : false));
const activeDispatches = computed(() => {
if (!props.playerName) return [];
if (!apiStore.activeData || !apiStore.activeData.activeSceneries) return [];
@@ -127,8 +127,9 @@ export default defineComponent({
this.station?.name || this.onlineScenery?.name
}&countFrom=${countFrom}&countLimit=${countLimit}`;
const historyAPIData: API.DispatcherHistory.Response =
await this.apiStore.client.get(requestString);
const historyAPIData: API.DispatcherHistory.Response = await (
await this.apiStore.client!.get(requestString)
).data;
this.dataStatus = Status.Data.Loaded;
return historyAPIData;
+7 -1
View File
@@ -24,6 +24,12 @@ import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
const prevPath = ref('/');
onMounted(() => {
prevPath.value = (route.meta['prevPath'] as string) ?? '/';
});
defineProps({
station: {
type: Object as PropType<Station>
@@ -40,7 +46,7 @@ defineProps({
});
function onReturnButtonClick() {
router.push('/');
router.push(prevPath.value);
}
</script>
@@ -9,16 +9,9 @@
</span>
<router-link class="dispatcher-name" :to="`/profile?playerId=${onlineScenery.dispatcherId}`">
<span
class="text--creator"
v-if="isCreator(onlineScenery.dispatcherName)"
:title="$t('donations.creator-message')"
>
{{ onlineScenery.dispatcherName }}
</span>
<span
class="text--donator"
v-else-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)"
v-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)"
:title="$t('donations.dispatcher-message')"
>
{{ onlineScenery.dispatcherName }}
@@ -58,7 +51,6 @@ import StationStatusBadge from '../../Global/StationStatusBadge.vue';
import { ActiveScenery } from '../../../typings/common';
import { useApiStore } from '../../../store/apiStore';
import FlagIcon from '../../Global/FlagIcon.vue';
import { isCreator } from '../../../utils/userUtils';
export default defineComponent({
mixins: [styleMixin, dateMixin, routerMixin],
@@ -66,8 +58,7 @@ export default defineComponent({
data() {
return {
apiStore: useApiStore(),
isCreator
apiStore: useApiStore()
};
},
@@ -1,6 +1,6 @@
<template>
<section class="info-routes" v-if="station.generalInfo">
<div class="routes one-way" v-if="singleRoutesAvailable.length > 0">
<div class="routes one-way" v-if="oneWayRoutes.length > 0">
<button
class="routes-btn"
@click="toggleRoutesVisibility('single')"
@@ -12,7 +12,7 @@
</button>
<ul class="routes-list">
<li v-for="route in singleRoutesFiltered" :key="route.routeName">
<li v-for="route in oneWayRoutes" :key="route.routeName">
<span :class="{ 'no-catenary': !route.isElectric, internal: route.isInternal }">
{{ route.routeName }}</span
>
@@ -24,17 +24,10 @@
</span>
<span v-if="route.isRouteSBL" class="sbl">SBL</span>
</li>
<li v-if="singleRoutesFiltered.length == 0">
<span class="routes-hidden">
<i class="fa-solid fa-eye-slash"></i>
{{ $t('scenery.routes-hidden') }}
</span>
</li>
</ul>
</div>
<div class="routes two-way" v-if="doubleRoutesAvailable.length > 0">
<div class="routes two-way" v-if="twoWayRoutes.length > 0">
<button
class="routes-btn"
@click="toggleRoutesVisibility('double')"
@@ -46,7 +39,7 @@
</button>
<ul class="routes-list">
<li v-for="route in doubleRoutesFiltered" :key="route.routeName">
<li v-for="route in twoWayRoutes" :key="route.routeName">
<span :class="{ 'no-catenary': !route.isElectric, internal: route.isInternal }">
{{ route.routeName }}
</span>
@@ -61,13 +54,6 @@
</span>
<span v-if="route.isRouteSBL" class="sbl">SBL</span>
</li>
<li v-if="doubleRoutesFiltered.length == 0">
<span class="routes-hidden">
<i class="fa-solid fa-eye-slash"></i>
{{ $t('scenery.routes-hidden') }}
</span>
</li>
</ul>
</div>
</section>
@@ -116,32 +102,20 @@ export default defineComponent({
},
computed: {
singleRoutesAvailable() {
oneWayRoutes() {
return (
this.station.generalInfo?.routes.single
.filter((r) => !r.hidden)
.filter((r) => !r.isInternal || r.isInternal == this.showInternalSingleRoutes)
.sort((r1, r2) => r1.routeName.localeCompare(r2.routeName)) ?? []
);
},
doubleRoutesAvailable() {
twoWayRoutes() {
return (
this.station.generalInfo?.routes.double
.filter((r) => !r.hidden)
.filter((r) => !r.isInternal || r.isInternal == this.showInternalDoubleRoutes)
.sort((r1, r2) => r1.routeName.localeCompare(r2.routeName)) ?? []
);
},
singleRoutesFiltered() {
return this.singleRoutesAvailable.filter(
(r) => this.showInternalSingleRoutes || !r.isInternal
);
},
doubleRoutesFiltered() {
return this.doubleRoutesAvailable.filter(
(r) => this.showInternalDoubleRoutes || !r.isInternal
);
}
}
});
@@ -180,6 +154,11 @@ ul.routes-list {
li {
margin: 0.5em 0.25em;
cursor: pointer;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
& > span {
padding: 0.2em;
@@ -203,16 +182,11 @@ ul.routes-list {
background-color: #303030;
color: #cfcfcf;
}
&.sbl {
color: var(--clr-primary);
background-color: #404040;
}
&.routes-hidden {
background-color: #4b4b4b;
}
&:last-child {
border-radius: 0 0.5em 0.5em 0;
}
@@ -29,8 +29,7 @@
<i
v-if="
train.timetableData != undefined &&
train.lastSeen <= Date.now() - 60000 &&
!train.online
(train.lastSeen <= Date.now() - 60000 || !train.online)
"
class="fa-solid fa-user-slash"
style="color: lightcoral"
@@ -210,7 +210,7 @@
</div>
<div class="item-stock-list" v-if="showStockThumbnails">
<StockList :trainStockList="row.train.stockList" :thumbnailSize="45" />
<StockList :trainStockList="row.train.stockList" />
</div>
</router-link>
</transition-group>
@@ -348,7 +348,7 @@ const tabliceZbiorczeHref = computed(() => {
});
const pragotronHref = computed(() => {
let url = `https://pragotron-td2.spythere.eu/board?name=${props.station!.name}&region=${mainStore.region.id}`;
let url = `https://pragotron-td2.web.app/board?name=${props.station!.name}&region=${mainStore.region.id}`;
if (props.chosenCheckpoint) url += `&checkpoint=${props.chosenCheckpoint}`;
return url;
@@ -149,12 +149,11 @@ export default defineComponent({
requestFilters['returnType'] = 'short';
try {
const response: API.TimetableHistory.ResponseShort = await this.apiStore.client.get(
'api/getTimetables',
requestFilters
);
console.log(response);
const response: API.TimetableHistory.ResponseShort = await (
await this.apiStore.client!.get('api/getTimetables', {
params: requestFilters
})
).data;
this.historyList = response;
@@ -1,213 +0,0 @@
<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-if="bestScoreList.length > 0">
<li v-for="(value, i) in bestScoreList">
<div>
{{ t('ordinal', { count: i + 1 }) }} {{ t('scenery.top-list.place') }} -
<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 v-else class="no-data">
<span v-if="currentListScope == 'name'">{{ t('scenery.top-list.no-data-general') }}</span>
<span v-else>{{ t('scenery.top-list.no-data-current-hash') }}</span>
</div>
</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;
}
.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;
}
}
.no-data {
padding: 1em 0.5em;
font-size: 1.1em;
background-color: #333;
color: #ccc;
}
</style>
+5 -13
View File
@@ -18,31 +18,23 @@ export function getTrainStopStatus(
return StopStatus.TERMINATED;
}
if (
!stopInfo.terminatesHere &&
stopInfo.confirmed &&
currentStationName.startsWith(sceneryName)
) {
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == sceneryName) {
return StopStatus.DEPARTED;
}
if (
!stopInfo.terminatesHere &&
stopInfo.confirmed &&
!currentStationName.startsWith(sceneryName)
) {
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != sceneryName) {
return StopStatus.DEPARTED_AWAY;
}
if (currentStationName.startsWith(sceneryName) && !stopInfo.stopped) {
if (currentStationName == sceneryName && !stopInfo.stopped) {
return StopStatus.ONLINE;
}
if (currentStationName.startsWith(sceneryName) && stopInfo.stopped) {
if (currentStationName == sceneryName && stopInfo.stopped) {
return StopStatus.STOPPED;
}
if (!currentStationName.startsWith(sceneryName)) {
if (currentStationName != sceneryName) {
return StopStatus.ARRIVING;
}
@@ -278,10 +278,6 @@ export default defineComponent({
color: #ccc;
}
.dropdown_wrapper {
top: 2.5em;
}
@include responsive.smallScreen {
.stats-title {
text-align: center;
@@ -290,9 +286,5 @@ export default defineComponent({
.filter-button > span {
display: none;
}
.no-data {
text-align: center;
}
}
</style>
+5 -25
View File
@@ -1,5 +1,5 @@
<template>
<section class="station_table" @scroll="onScroll" ref="tableRef">
<section class="station_table">
<Loading
v-if="apiStore.dataStatuses.connection == Status.Loading && filteredStationList.length == 0"
/>
@@ -131,16 +131,7 @@
<td class="station-dispatcher-name">
<span v-if="station.onlineInfo?.dispatcherName">
<b
v-if="isCreator(station.onlineInfo.dispatcherName)"
data-tooltip-type="CreatorTooltip"
:data-tooltip-content="$t('donations.creator-message')"
>
<img src="/images/icon-creator.png" alt="creator icon" />
<span class="text--creator">&nbsp;{{ station.onlineInfo.dispatcherName }}</span>
</b>
<b
v-else-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.dispatcher-message')"
>
@@ -362,7 +353,6 @@ import { ActiveSorter, HeadIdsType, headIconsIds, headIds } from './typings';
import { filterStations, sortStations } from './utils';
import { getLanguageNameById } from '../../utils/languageUtils';
import FlagIcon from '../Global/FlagIcon.vue';
import { isCreator } from '../../utils/userUtils';
export default defineComponent({
emits: ['toggleDonationCard'],
@@ -373,9 +363,7 @@ export default defineComponent({
data: () => ({
headIconsIds,
headIds,
scrollTop: 0,
getChangedFilters,
isCreator
getChangedFilters
}),
setup() {
@@ -403,10 +391,6 @@ export default defineComponent({
};
},
activated() {
(this.$refs['tableRef'] as HTMLElement).scrollTop = this.scrollTop;
},
methods: {
getSceneryRoute(station: Station) {
this.$router.push({
@@ -447,10 +431,6 @@ export default defineComponent({
}));
return JSON.stringify(usersTrains);
},
onScroll(e: Event) {
this.scrollTop = (e.target as HTMLElement).scrollTop;
}
}
});
@@ -633,8 +613,8 @@ tbody tr {
.station-dispatcher-name {
img {
max-height: 1.3em;
vertical-align: text-top;
max-width: 1.35em;
vertical-align: text-bottom;
}
}
-36
View File
@@ -1,36 +0,0 @@
<template>
<div class="tooltip-content">
<img src="/images/icon-creator.png" alt="creator icon" />
<b class="text--creator">&nbsp;{{ tooltipStore.content }}</b>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({
data() {
return {
tooltipStore: useTooltipStore()
};
}
});
</script>
<style lang="scss" scoped>
.tooltip-content {
padding: 0.5em;
border-radius: 0.25em;
width: 100%;
background-color: #333;
box-shadow: 0 0 10px 2px #aaa;
}
img {
vertical-align: text-bottom;
height: 1.25em;
}
</style>
+8 -3
View File
@@ -1,7 +1,7 @@
<template>
<div class="tooltip-content">
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
<b class="text--donator">&nbsp;{{ tooltipStore.content }}</b>
<span>{{ tooltipStore.content }}</span>
</div>
</template>
@@ -20,6 +20,11 @@ export default defineComponent({
<style lang="scss" scoped>
.tooltip-content {
display: flex;
align-items: center;
gap: 0.5em;
flex-wrap: wrap;
padding: 0.5em;
border-radius: 0.25em;
@@ -30,7 +35,7 @@ export default defineComponent({
}
img {
vertical-align: text-bottom;
height: 1.25em;
vertical-align: middle;
height: 1em;
}
</style>
+2 -4
View File
@@ -8,13 +8,12 @@
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import DonatorTooltip from './DonatorTooltip.vue';
import CreatorTooltip from './CreatorTooltip.vue';
import VehiclePreviewTooltip from './VehiclePreviewTooltip.vue';
import BaseTooltip from './BaseTooltip.vue';
import SpawnsTooltip from './SpawnsTooltip.vue';
import UsersTooltip from './UsersTooltip.vue';
import HtmlTooltip from './HtmlTooltip.vue';
import TrainInfoTooltip from './TrainInfoTooltip.vue';
import TrainInfoTooltip from "./TrainInfoTooltip.vue";
const BOX_PADDING_PX = 20;
@@ -26,8 +25,7 @@ export default defineComponent({
SpawnsTooltip,
UsersTooltip,
HtmlTooltip,
TrainInfoTooltip,
CreatorTooltip
TrainInfoTooltip
},
data() {
+8 -15
View File
@@ -56,21 +56,12 @@
</b>
<b
v-if="isCreator(train.driverName)"
data-tooltip-type="CreatorTooltip"
:data-tooltip-content="$t('donations.creator-message')"
>
<img src="/images/icon-creator.png" alt="creator icon" />
<span class="text--creator">&nbsp;{{ train.driverName }}</span>
</b>
<b
v-else-if="apiStore.donatorsData.includes(train.driverName)"
v-if="apiStore.donatorsData.includes(train.driverName)"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.driver-message')"
>
<span class="text--donator">{{ train.driverName }}&nbsp;</span>
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
<span class="text--donator">&nbsp;{{ train.driverName }}</span>
</b>
<span v-else>{{ train.driverName }}</span>
@@ -213,7 +204,6 @@ import trainCategoryMixin from '../../mixins/trainCategoryMixin';
import ProgressBar from '../Global/ProgressBar.vue';
import StockList from '../Global/StockList.vue';
import FlagIcon from '../Global/FlagIcon.vue';
import { isCreator } from '../../utils/userUtils';
export default defineComponent({
mixins: [trainInfoMixin, styleMixin, trainCategoryMixin],
@@ -232,8 +222,7 @@ export default defineComponent({
data() {
return {
store: useMainStore(),
apiStore: useApiStore(),
isCreator
apiStore: useApiStore()
};
},
@@ -289,6 +278,8 @@ export default defineComponent({
display: flex;
flex-direction: column;
font-size: 1em;
gap: 0.25em;
background-color: #1a1a1a;
gap: 0.5em;
}
@@ -367,6 +358,8 @@ export default defineComponent({
.status-badges {
display: flex;
flex-wrap: wrap;
margin-left: 0.25em;
gap: 0.25em;
img {
@@ -379,7 +372,7 @@ export default defineComponent({
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
padding: 0.25em 0;
padding: 0.5em 0;
}
.progress-distance {
@@ -210,10 +210,6 @@ export default defineComponent({
@use '../../styles/dropdown';
@use '../../styles/dropdown-filters';
.dropdown_wrapper {
top: 2.5em;
}
.search_content > div {
margin: 0.5em auto;
}
+1 -2
View File
@@ -250,10 +250,9 @@ h3 {
.dropdown_wrapper {
max-width: 600px;
top: 2.5em;
}
@include responsive.smallScreen {
@include responsive.smallScreen{
.no-data {
text-align: center;
}
-23
View File
@@ -1,23 +0,0 @@
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();
}
}
+2 -36
View File
@@ -9,43 +9,9 @@ const i18n = createI18n({
warnHtmlMessage: false,
fallbackLocale: 'pl',
pluralizationRules: {
en: {
ordinal: (ctx: { named: (arg0: string) => number }) => {
const number = ctx.named('count');
const suffixes: Record<number, string> = {
1: 'st',
2: 'nd',
3: 'rd'
};
const suffix = suffixes[number % 10] || 'th';
return `${number}${suffix}`;
}
}
},
messages: {
en: {
...enLang,
ordinal: (ctx: { named: (arg0: string) => number }) => {
const number = ctx.named('count');
const suffixes: Record<number, string> = {
1: 'st',
2: 'nd',
3: 'rd'
};
const suffix = suffixes[number % 10] || 'th';
return `${number}${suffix}`;
}
},
pl: {
...plLang,
ordinal: '{count}.'
}
en: enLang,
pl: plLang
},
enableLegacy: false
});
+6 -36
View File
@@ -42,8 +42,7 @@
"action-paypal": "DONATE WITH PAYPAL",
"action-buycoffee": "BUY ME A COFFEE!",
"dispatcher-message": "Dispatcher supporting the Stacjownik project!",
"driver-message": "Driver supporting the Stacjownik project!",
"creator-message": "Creator of the Stacjownik project"
"driver-message": "Driver supporting the Stacjownik project!"
},
"warnings": {
"TWR": "Train with high risk cargo",
@@ -200,7 +199,6 @@
"search-date-from": "Date (UTC+2 / CEST)",
"search-date-to": "Date (UTC+2 / CEST)",
"select-categoryCode": "Train category",
"search-headUnit": "Traction unit (e.g. EP09, ET22-401)",
"sort-mass": "mass",
"sort-speed": "speed",
"sort-length": "length",
@@ -439,25 +437,15 @@
"driver-not-found-others": "Player {driver} is online as:",
"driver-not-found-return": "RETURN TO THE MAIN SITE",
"stock-copy": "COPY THE STOCK",
"number-propositions": "NUMBER & WARNINGS SUGGESTIONS",
"number-propositions": "PROPOSE NUMBER",
"stock-clipboard-success": "Successfully copied the railway stock in a text form to your clipboard!",
"stock-clipboard-failure": "Oops! Something happened and the railway stock couldn't be copied to your clipboard! :/",
"number-propositions-header": "Generate number examples for a train category:",
"number-propositions-header": "Generate number examples for selected category:",
"number-propositions-third-number": "Third digit:",
"number-propositions-last-nums": "{count} last digits from the range of:",
"number-propositions-title": "Propositions:",
"number-propositions-empty": "No propositions available for the chosen category! :/"
},
"cargo-warnings": {
"title": "Additional cargo warnings:",
"pn-innofreight": "PN: Innofreight C45: exceeded gauge",
"twr-un1965": "TWR: UN1965 (LPG)",
"tn-un1965": "TN: unclean tanks after UN1965",
"tn-un1202": "TN: UN1202 (diesel fuel)",
"tn-un1202-empty": "TN: unclean tanks after UN1202",
"pn-military": "PN: military transport",
"pn-edk80": "PN: EDK80 railway crane"
},
"train-stats": {
"stats-button": "STATISTICS",
"title": "ONLINE TRAINS STATS",
@@ -570,7 +558,7 @@
"no-users": "NO ACTIVE PLAYERS",
"no-spawns": "NO OPEN SPAWNS",
"no-scenery": "Oops! This scenery doesn't exist!",
"return-btn": "BACK TO SCENERIES",
"return-btn": "BACK TO THE MAIN SITE",
"history-btn": "View the dispatcher history",
"info-btn": "Return to the scenery view",
"authors-title": "Scenery author | Scenery authors",
@@ -580,12 +568,10 @@
"additional-tools-title": "Additional tools",
"one-way-routes": "Single track routes",
"two-way-routes": "Double track routes",
"routes-hidden": "Hidden internal routes",
"no-data": "No available data about this scenery",
"option-active-timetables": "Active timetables",
"option-timetables-history": "Timetables history PL1",
"option-dispatchers-history": "Dispatchers history PL1",
"option-top-list": "Scenery records",
"btn-show-timetable-thumbnails": "Show rolling stock thumbnails",
"btn-hide-timetable-thumbnails": "Hide rolling stock thumbnails",
"timetable-includesScenery": "ALL TIMETABLES",
@@ -598,30 +584,14 @@
"dispatcher-rate": "Rate:",
"dispatcher-status-changes": "Status changes:",
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
"history-list-empty": "No saved scenery history!",
"history-list-empty": "No recorded scenery history!",
"forum-topic": "Scenery's forum topic",
"gnr-link": "Train orders generator",
"pragotron-link": "Timetable pallet board",
"tablice-link": "Timetable summary board <br> (by Thundo)",
"bottom-info": "Show full history in the Journal tab",
"btn-show-internal-routes": "Show 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": "place",
"dispatcher-rating": "Rating: {n}",
"duty-count": "No duties | 1 duty | Duties: {n}",
"duration": "Duration:",
"no-data-general": "No best scores for this scenery on the PL1 server!",
"no-data-current-hash": "No best scores for the current scenery hash on the PL1 server!"
}
"btn-hide-internal-routes": "Hide internal routes"
},
"availability": {
"title": "Availability",
+5 -35
View File
@@ -42,8 +42,7 @@
"action-paypal": "PRZELEJ PAYPALEM",
"action-buycoffee": "POSTAW KAWĘ!",
"dispatcher-message": "Dyżurny wspierający projekt Stacjownika!",
"driver-message": "Maszynista wspierający projekt Stacjownika!",
"creator-message": "Twórca projektu Stacjownik"
"driver-message": "Maszynista wspierający projekt Stacjownika!"
},
"warnings": {
"TWR": "Pociąg z towarami niebezpiecznymi wysokiego ryzyka",
@@ -197,7 +196,6 @@
"search-date-from": "Data (UTC+2 / CEST)",
"search-date-to": "Data (UTC+2 / CEST)",
"select-categoryCode": "Kategoria pociągu",
"search-headUnit": "Pojazd trakcyjny (np. EP09, ET22-137)",
"sort-routeDistance": "kilometraż",
"sort-allStopsCount": "stacje",
"sort-beginDate": "data",
@@ -426,25 +424,15 @@
"driver-not-found-others": "Gracz {driver} jest online jako:",
"driver-not-found-return": "WRÓĆ NA STRONĘ GŁÓWNĄ",
"stock-copy": "SKOPIUJ SKŁAD",
"number-propositions": "PROPOZYCJE NUMERÓW I UWAG",
"number-propositions": "ZAPROPONUJ NUMER",
"stock-clipboard-success": "Pomyślnie skopiowano skład w postaci tekstowej do schowka!",
"stock-clipboard-failure": "Ups! Nie udało się skopiować składu do schowka! :/",
"number-propositions-header": "Wygeneruj propozycje numerów dla pociągu kategorii:",
"number-propositions-header": "Wygeneruj propozycje numerów dla kategorii pociągu:",
"number-propositions-third-number": "Trzecia cyfra:",
"number-propositions-last-nums": "{count} ostatnie cyfry z przedziału:",
"number-propositions-title": "Propozycje:",
"number-propositions-empty": "Brak propozycji dla wybranej kategorii! :/"
},
"cargo-warnings": {
"title": "Dodatkowe uwagi przewozowe:",
"pn-innofreight": "PN: Innofreight C45: przekroczona skrajnia",
"twr-un1965": "TWR: UN1965 (LPG)",
"tn-un1965": "TN: brudne cysterny po UN1965",
"tn-un1202": "TN: UN1202 (olej napędowy)",
"tn-un1202-empty": "TN: brudne cysterny po UN1202",
"pn-military": "PN: transport wojskowy",
"pn-edk80": "PN: żuraw kolejowy EDK80"
},
"train-stats": {
"stats-button": "STATYSTYKI",
"title": "STATYSTYKI AKTYWNYCH POCIĄGÓW",
@@ -556,7 +544,7 @@
"no-users": "BRAK AKTYWNYCH GRACZY",
"no-spawns": "BRAK OTWARTYCH SPAWNÓW",
"no-scenery": "Ups! Ta sceneria nie istnieje!",
"return-btn": "POWRÓT DO SCENERII",
"return-btn": "POWRÓT DO STRONY GŁÓWNEJ",
"history-btn": "Przejdź do widoku historii dyżurnych ruchu",
"info-btn": "Wróć do widoku scenerii",
"authors-title": "Autor scenerii | Autorzy scenerii",
@@ -566,12 +554,10 @@
"additional-tools-title": "Dodatkowe narzędzia",
"one-way-routes": "Szlaki jednotorowe",
"two-way-routes": "Szlaki dwutorowe",
"routes-hidden": "Ukryto szlaki wewnętrzne",
"no-data": "Brak informacji o tej scenerii",
"option-active-timetables": "Aktywne rozkłady jazdy",
"option-timetables-history": "Historia rozkładó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-hide-timetable-thumbnails": "Ukrywaj podglądy składów",
"timetable-includesScenery": "WSZYSTKIE RJ",
@@ -591,23 +577,7 @@
"tablice-link": "Tablica informacyjna zbiorcza <br> (autorstwa Thundo)",
"bottom-info": "Pokaż pełną historię w zakładce Dziennika",
"btn-show-internal-routes": "Pokazuj 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": "miejsce",
"dispatcher-rating": "Ocena: {n}",
"duty-count": "Brak dyżurów | 1 dyżur | Dyżury: {n}",
"duration": "Czas:",
"no-data-general": "Brak zapisanych rekordów scenerii na serwerze PL1!",
"no-data-current-hash": "Brak zapisanych rekordów scenerii z obecnym hashem na serwerze PL1!"
}
"btn-hide-internal-routes": "Ukrywaj szlaki wewnętrzne"
},
"availability": {
"title": "Dostępność",
+26 -26
View File
@@ -94,14 +94,14 @@ export const initFilters = {
minTwoWayCatenary: 0,
minTwoWayInt: 0,
minTwoWayCatenaryInt: 0,
maxOneWay: 10,
maxOneWayCatenary: 10,
maxOneWayInt: 20,
maxOneWayCatenaryInt: 20,
maxTwoWay: 10,
maxTwoWayCatenary: 10,
maxTwoWayInt: 20,
maxTwoWayCatenaryInt: 20,
maxOneWay: 5,
maxOneWayCatenary: 5,
maxOneWayInt: 5,
maxOneWayCatenaryInt: 5,
maxTwoWay: 5,
maxTwoWayCatenary: 5,
maxTwoWayInt: 5,
maxTwoWayCatenaryInt: 5,
authors: '',
projects: '',
lines: ''
@@ -122,62 +122,62 @@ export const sliderGroups: SliderGroup[] = [
export const sliderGroupsOptions: Record<SliderGroup, SliderOptions[]> = {
vMax: [
{ id: 'minVmax', minRange: 0, maxRange: 200, step: 20 },
{ id: 'maxVmax', minRange: 0, maxRange: 200, step: 20 }
{ id: 'minVmax', minRange: 0, maxRange: 200, step: 10 },
{ id: 'maxVmax', minRange: 0, maxRange: 200, step: 10 }
],
level: [
{ id: 'minLevel', minRange: 0, maxRange: 20, step: 1 },
{ id: 'maxLevel', minRange: 0, maxRange: 20, step: 1 }
],
routeOneWay: [
{ id: 'minOneWay', minRange: 0, maxRange: 10, step: 1 },
{ id: 'maxOneWay', minRange: 0, maxRange: 10, step: 1 }
{ id: 'minOneWay', minRange: 0, maxRange: 5, step: 1 },
{ id: 'maxOneWay', minRange: 0, maxRange: 5, step: 1 }
],
routeOneWayCatenary: [
{ id: 'minOneWayCatenary', minRange: 0, maxRange: 10, step: 1 },
{ id: 'maxOneWayCatenary', minRange: 0, maxRange: 10, step: 1 }
{ id: 'minOneWayCatenary', minRange: 0, maxRange: 5, step: 1 },
{ id: 'maxOneWayCatenary', minRange: 0, maxRange: 5, step: 1 }
],
routeOneWayInternal: [
{ id: 'minOneWayInt', minRange: 0, maxRange: 20, step: 1 },
{ id: 'maxOneWayInt', minRange: 0, maxRange: 20, step: 1 }
{ id: 'minOneWayInt', minRange: 0, maxRange: 5, step: 1 },
{ id: 'maxOneWayInt', minRange: 0, maxRange: 5, step: 1 }
],
routeOneWayInternalCatenary: [
{
id: 'minOneWayCatenaryInt',
minRange: 0,
maxRange: 20,
maxRange: 5,
step: 1
},
{
id: 'maxOneWayCatenaryInt',
minRange: 0,
maxRange: 20,
maxRange: 5,
step: 1
}
],
routeTwoWay: [
{ id: 'minTwoWay', minRange: 0, maxRange: 10, step: 1 },
{ id: 'maxTwoWay', minRange: 0, maxRange: 10, step: 1 }
{ id: 'minTwoWay', minRange: 0, maxRange: 5, step: 1 },
{ id: 'maxTwoWay', minRange: 0, maxRange: 5, step: 1 }
],
routeTwoWayCatenary: [
{ id: 'minTwoWayCatenary', minRange: 0, maxRange: 10, step: 1 },
{ id: 'maxTwoWayCatenary', minRange: 0, maxRange: 10, step: 1 }
{ id: 'minTwoWayCatenary', minRange: 0, maxRange: 5, step: 1 },
{ id: 'maxTwoWayCatenary', minRange: 0, maxRange: 5, step: 1 }
],
routeTwoWayInternal: [
{ id: 'minTwoWayInt', minRange: 0, maxRange: 20, step: 1 },
{ id: 'maxTwoWayInt', minRange: 0, maxRange: 20, step: 1 }
{ id: 'minTwoWayInt', minRange: 0, maxRange: 5, step: 1 },
{ id: 'maxTwoWayInt', minRange: 0, maxRange: 5, step: 1 }
],
routeTwoWayInternalCatenary: [
{
id: 'minTwoWayCatenaryInt',
minRange: 0,
maxRange: 20,
maxRange: 5,
step: 1
},
{
id: 'maxTwoWayCatenaryInt',
minRange: 0,
maxRange: 20,
maxRange: 5,
step: 1
}
]
+32 -24
View File
@@ -2,20 +2,7 @@ import { defineStore } from 'pinia';
import { API } from '../typings/api';
import { Status } from '../typings/common';
import { StationJSONData } from './typings';
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;
}
import axios, { AxiosInstance } from 'axios';
export const useApiStore = defineStore('apiStore', {
state: () => ({
@@ -38,13 +25,30 @@ export const useApiStore = defineStore('apiStore', {
nextUpdateTime: 0,
nextDataCheckTime: 0,
client: new HttpClient(baseURL),
client: undefined as AxiosInstance | undefined,
activeDataScheduler: undefined as number | undefined
}),
actions: {
async setupAPIData() {
let baseURL = 'https://stacjownik.spythere.eu';
switch (import.meta.env.VITE_API_MODE) {
case 'development':
baseURL = 'http://localhost:3001';
break;
case 'mocking':
baseURL = 'http://localhost:3123';
break;
default:
break;
}
this.client = axios.create({
baseURL
});
this.connectToAPI();
},
@@ -78,9 +82,9 @@ export const useApiStore = defineStore('apiStore', {
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
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;
this.activeData = response.data;
this.dataStatuses.connection = Status.Data.Loaded;
} catch (error) {
this.dataStatuses.connection = Status.Data.Error;
@@ -90,9 +94,9 @@ export const useApiStore = defineStore('apiStore', {
async fetchDonatorsData() {
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;
this.donatorsData = response.data;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania informacji o donatorach:', error);
}
@@ -100,7 +104,9 @@ export const useApiStore = defineStore('apiStore', {
async fetchStationsGeneralInfo() {
try {
const sceneryData = await this.client.get<StationJSONData[]>(`api/getSceneries`);
const sceneryData: StationJSONData[] = (
await this.client!.get<StationJSONData[]>(`api/getSceneries`)
).data;
this.dataStatuses.sceneries = Status.Data.Loaded;
this.sceneryData = sceneryData;
@@ -112,10 +118,10 @@ export const useApiStore = defineStore('apiStore', {
async fetchVehiclesInfo() {
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;
this.dataStatuses.vehicles = response ? Status.Data.Loaded : Status.Data.Warning;
this.vehiclesData = response.data;
this.dataStatuses.vehicles = response.data ? Status.Data.Loaded : Status.Data.Warning;
} catch (error) {
this.dataStatuses.vehicles = Status.Data.Error;
console.error('Ups! Wystąpił błąd podczas pobierania informacji o pojazdach:', error);
@@ -124,7 +130,9 @@ export const useApiStore = defineStore('apiStore', {
async fetchDailyStats() {
try {
const res = await this.client.get<API.DailyStats.Response>('api/getDailyStats');
const res: API.DailyStats.Response = await (
await this.client!.get('api/getDailyStats')
).data;
this.dailyStatsData = res;
+1 -2
View File
@@ -9,8 +9,7 @@ export const tooltipKeys = [
'SpawnsTooltip',
'UsersTooltip',
'HtmlTooltip',
'TrainInfoTooltip',
'CreatorTooltip'
'TrainInfoTooltip'
] as const;
export type TooltipType = (typeof tooltipKeys)[number];
+1
View File
@@ -85,6 +85,7 @@
padding: 0.1em 0.3em;
border-radius: 0.2em;
font-weight: bold;
user-select: none;
&.twr {
background-color: var(--clr-twr);
+2 -2
View File
@@ -78,14 +78,14 @@ h1.option-title {
display: flex;
gap: 0.5em;
width: 100%;
margin-top: 1em;
margin-top: 0.5em;
button {
width: 100%;
}
}
@include responsive.smallScreen {
@include responsive.smallScreen{
h1 {
text-align: center;
+2 -1
View File
@@ -26,7 +26,7 @@
.dropdown_wrapper {
position: absolute;
left: 0;
top: 0;
top: calc(100% + 0.5em);
background-color: var(--clr-bg3);
box-shadow: 0 0 5px 1px var(--clr-primary);
@@ -34,6 +34,7 @@
width: 100%;
max-width: 550px;
max-height: 750px;
overflow: auto;
padding: 1em;
-13
View File
@@ -217,19 +217,6 @@ ul {
text-shadow: #f050ff 0 0 10px;
}
&--creator {
color: var(--clr-primary);
color: transparent;
background: var(--clr-primary);
background: linear-gradient(90deg, gold 30%, #ffffff 70%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: gold 0 0 10px;
}
&--discord {
color: var(--clr-donator);
color: transparent;
+3 -2
View File
@@ -24,8 +24,8 @@
width: 100%;
margin: 0 auto;
padding: 1em 0;
position: relative;
}
.journal_refreshed-date {
@@ -57,6 +57,7 @@
justify-content: space-between;
align-items: center;
gap: 0.5em;
position: relative;
}
.btn--load-data {
@@ -67,7 +68,7 @@
font-size: 1.2em;
}
@include responsive.smallScreen {
@include responsive.smallScreen{
.journal_top-bar {
justify-content: center;
flex-wrap: wrap;
+1
View File
@@ -15,6 +15,7 @@
gap: 0.25em;
min-width: 200px;
margin-right: 0.25em;
}
&-input {
+1 -3
View File
@@ -253,10 +253,8 @@ export namespace API {
pn?: number;
tn?: number;
headUnitName?: string;
headUnitType?: string;
returnType?: 'all' | 'short' | 'detailed';
sortBy?: Journal.TimetableSorter['id'];
}
-3
View File
@@ -1,3 +0,0 @@
export function isCreator(name: string) {
return /(spythere|kowbojyt)/.test(name.toLowerCase());
}
+6 -8
View File
@@ -217,10 +217,9 @@ export default defineComponent({
this.scrollDataLoaded = false;
this.currentQueryParams['countFrom'] = this.historyList.length;
const responseData: API.DispatcherHistory.Response = await await this.apiStore.client.get(
`api/getDispatchers`,
this.currentQueryParams
);
const responseData: API.DispatcherHistory.Response = await (
await this.apiStore.client!.get(`api/getDispatchers`, { params: this.currentQueryParams })
).data;
if (!responseData) return;
@@ -277,10 +276,9 @@ export default defineComponent({
this.currentQueryParams = queryParams;
try {
const responseData: API.DispatcherHistory.Response = await this.apiStore.client.get(
`api/getDispatchers`,
this.currentQueryParams
);
const responseData: API.DispatcherHistory.Response = await (
await this.apiStore.client!.get(`api/getDispatchers`, { params: this.currentQueryParams })
).data;
if (!responseData) {
this.dataStatus = Status.Data.Error;
+14 -25
View File
@@ -173,9 +173,8 @@ export default defineComponent({
'search-issuedFrom': '',
'search-via': '',
'search-terminatingAt': '',
'search-headUnit': '',
'search-date-from': '',
'select-categoryCode': ''
'select-categoryCode': '',
'search-date-from': ''
} as Journal.TimetableSearchType);
const countFromIndex = ref(0);
@@ -278,10 +277,11 @@ export default defineComponent({
this.currentQueryParams['countFrom'] = this.timetableHistory.length;
const responseData: API.TimetableHistory.Response = await this.apiStore.client.get(
'api/getTimetables',
this.currentQueryParams
);
const responseData: API.TimetableHistory.Response = await (
await this.apiStore.client!.get('api/getTimetables', {
params: this.currentQueryParams
})
).data;
if (!responseData) return;
@@ -297,8 +297,6 @@ export default defineComponent({
async fetchHistoryData() {
this.extraInfoIndexes.length = 0;
const queryParams: API.TimetableHistory.QueryParams = {};
const driverName = this.searchersValues['search-driver'].trim() || undefined;
const trainNo = this.searchersValues['search-train'].trim() || undefined;
const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
@@ -308,7 +306,6 @@ export default defineComponent({
const via = this.searchersValues['search-via'].trim() || undefined;
const terminatingAt = this.searchersValues['search-terminatingAt'].trim() || undefined;
const categoryCode = this.searchersValues['select-categoryCode'].trim() || undefined;
const headUnit = this.searchersValues['search-headUnit'].trim() || undefined;
let dateFromISO: string | undefined = undefined;
let dateToISO: string | undefined = undefined;
@@ -324,6 +321,8 @@ export default defineComponent({
dateToISO = dateTo.toISOString();
}
const queryParams: API.TimetableHistory.QueryParams = {};
this.filterList
.filter((f) => f.isActive)
.forEach((f) => {
@@ -395,27 +394,17 @@ export default defineComponent({
queryParams['sortBy'] =
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))
this.dataStatus = Status.Data.Loading;
this.currentQueryParams = queryParams;
try {
const responseData: API.TimetableHistory.ResponseShort = await this.apiStore.client.get(
'api/getTimetables',
this.currentQueryParams
);
const responseData: API.TimetableHistory.ResponseShort = await (
await this.apiStore.client!.get('api/getTimetables', {
params: this.currentQueryParams
})
).data;
if (!responseData) {
this.dataStatus = Status.Data.Error;
+24 -20
View File
@@ -40,6 +40,7 @@ import Loading from '../components/Global/Loading.vue';
import ProfileSummary from '../components/PlayerProfileView/ProfileSummary.vue';
import ProfileRecentStats from '../components/PlayerProfileView/ProfileRecentStats.vue';
import ProfileHistoryList from '../components/PlayerProfileView/ProfileHistoryList.vue';
import axios from 'axios';
const { t } = useI18n();
const router = useRouter();
@@ -70,28 +71,29 @@ onDeactivated(() => {
});
async function fetchPlayerInfo(playerId: number) {
return apiStore.client.get<API.PlayerInfo.Data>('api/getPlayerInfo', {
playerId
return apiStore.client!.get<API.PlayerInfo.Data>('api/getPlayerInfo', {
params: {
playerId
}
});
}
async function fetchPlayerJournal(playerId: number) {
return apiStore.client.get<API.PlayerJournal.Data>('api/getPlayerJournal', {
playerId,
dateScope: '30d'
return apiStore.client!.get<API.PlayerJournal.Data>('api/getPlayerJournal', {
params: {
playerId,
dateScope: '30d'
}
});
}
async function fetchPlayerTd2Info(playerName: string): Promise<Td2API.UsersInfoByName.Response> {
const response = await fetch(
`https://api.td2.info.pl?method=getUsersInfoByName&name=${playerName}`
);
if (!response.ok) {
throw new Error('fetchPlayerTd2Info: could not fetch data');
}
return response.json();
async function fetchPlayerTd2Info(playerName: string) {
return axios.get<Td2API.UsersInfoByName.Response>('https://api.td2.info.pl', {
params: {
method: 'getUsersInfoByName',
name: playerName
}
});
}
async function fetchPlayerData() {
@@ -114,21 +116,23 @@ async function fetchPlayerData() {
const playerInfoResp = await fetchPlayerInfo(playerId.value);
playerName.value =
playerInfoResp.driverStats.driverName || playerInfoResp.dispatcherStats.dispatcherName || '';
playerInfoResp.data.driverStats.driverName ||
playerInfoResp.data.dispatcherStats.dispatcherName ||
'';
if (!playerName.value) {
router.push('/');
return;
}
playerInfo.value = playerName.value ? playerInfoResp : undefined;
playerInfo.value = playerName.value ? playerInfoResp.data : undefined;
playerInfoStatus.value = Status.Data.Loaded;
if (playerName.value) {
const playerTD2InfoResp = await fetchPlayerTd2Info(playerName.value);
if (playerTD2InfoResp.success && playerTD2InfoResp.message.length == 1) {
playerTD2Info.value = playerTD2InfoResp.message[0];
if (playerTD2InfoResp.data.success && playerTD2InfoResp.data.message.length == 1) {
playerTD2Info.value = playerTD2InfoResp.data.message[0];
}
}
} catch (error) {
@@ -140,7 +144,7 @@ async function fetchPlayerData() {
try {
const playerJournalResp = await fetchPlayerJournal(playerId.value);
playerJournal.value = playerJournalResp;
playerJournal.value = playerJournalResp.data;
playerJournalStatus.value = Status.Data.Loaded;
} catch (error) {
playerJournal.value = undefined;
+1 -6
View File
@@ -58,7 +58,6 @@ import SceneryDispatchersHistory from '../components/SceneryView/SceneryDispatch
import { useApiStore } from '../store/apiStore';
import { Status } from '../typings/common';
import SceneryTopList from '../components/SceneryView/SceneryTopList.vue';
const route = useRoute();
const router = useRouter();
@@ -90,10 +89,6 @@ const viewModes = [
{
id: 'scenery.option-dispatchers-history',
component: SceneryDispatchersHistory
},
{
id: 'scenery.option-top-list',
component: SceneryTopList
}
];
@@ -189,7 +184,7 @@ function setViewMode(componentName: string) {
background-color: #181818;
border-radius: 0.5em;
padding: 1em;
padding: 0.5em;
}
.scenery-left {
+5 -1
View File
@@ -116,10 +116,13 @@ export default defineComponent({
<style lang="scss" scoped>
@use '../styles/responsive';
.trains-view {
position: relative;
}
.trains_wrapper {
margin: 1rem auto;
max-width: var(--max-container-width);
position: relative;
}
.trains_topbar {
@@ -127,6 +130,7 @@ export default defineComponent({
align-items: center;
gap: 0.5em;
position: relative;
margin-bottom: 0.5em;
}
-15
View File
@@ -1,15 +0,0 @@
{
"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"
}
}
+17 -4
View File
@@ -1,11 +1,24 @@
{
"files": [],
"compilerOptions": {
"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": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}
+6 -9
View File
@@ -1,12 +1,9 @@
// 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": {
"module": "preserve",
"moduleResolution": "bundler",
"types": ["node", "vite/client", "vite-plugin-pwa/client"],
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
}
"composite": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+4 -7
View File
@@ -1,7 +1,7 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { VitePWA } from 'vite-plugin-pwa';
import { fileURLToPath } from 'node:url';
import path from 'path';
export default defineConfig({
server: { port: 5123, open: false },
@@ -14,7 +14,7 @@ export default defineConfig({
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
'@': path.resolve(__dirname, 'src')
}
},
plugins: [
@@ -29,13 +29,10 @@ export default defineConfig({
{
urlPattern:
/^https:\/\/stacjownik.spythere.eu\/api\/(getVehiclesData|getDonators|getSceneries)/i,
handler: 'StaleWhileRevalidate',
handler: 'CacheFirst',
options: {
cacheName: 'stacjownik-api-cache',
cacheableResponse: { statuses: [0, 200] },
expiration: {
maxAgeSeconds: 3600
}
cacheableResponse: { statuses: [0, 200] }
}
}
]
+1500 -1501
View File
File diff suppressed because it is too large Load Diff