Compare commits

..

17 Commits

Author SHA1 Message Date
Spythere 83444f64d0 Merge pull request #155 from Spythere/development
v1.32.0
2026-02-26 21:15:04 +01:00
Spythere a5f9f8901b chore(profile): redirecting to main site when player is not found 2026-02-26 14:26:31 +01:00
Spythere 0276e0754b fix(profile): improper image loading when switching between users 2026-02-25 21:38:43 +01:00
Spythere 0d495ede2d fix(profile): filtering online trains and dispatches 2026-02-25 21:16:44 +01:00
Spythere 48c0a32017 fix(profile): player avatar loading 2026-02-25 21:16:19 +01:00
Spythere 26f2ced266 chore(app): improved API refresh times at about 31-32s 2026-02-24 22:21:02 +01:00
Spythere 4f17b1a704 refactor(profile): moved fetching data to view root to ensure proper loading on activating 2026-02-24 22:13:52 +01:00
Spythere 50068a239c bump(version): v1.32.0 2026-02-23 22:10:02 +01:00
Spythere 662748f705 Merge pull request #154 from Spythere/feature/user-profile
Feature: Player Profile
2026-02-23 22:06:54 +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
7 changed files with 151 additions and 128 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.31.1", "version": "1.32.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -14,7 +14,7 @@
</div> </div>
<div class="history-list-box"> <div class="history-list-box">
<Loading v-if="journalDataStatus == Status.Data.Loading" /> <Loading v-if="journalStatus == Status.Data.Loading" />
<div v-else-if="combinedJournal.length == 0" class="no-recent-history"> <div v-else-if="combinedJournal.length == 0" class="no-recent-history">
{{ t('profile.list.no-recent-history') }} {{ t('profile.list.no-recent-history') }}
@@ -107,12 +107,21 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onActivated, onDeactivated, onMounted, reactive, ref } from 'vue'; import {
computed,
onActivated,
onDeactivated,
onMounted,
onUnmounted,
PropType,
reactive,
ref
} from 'vue';
import { dateToLocaleString, humanizeDuration } from '../../composables/time'; import { dateToLocaleString, humanizeDuration } from '../../composables/time';
import { API } from '../../typings/api'; import { API } from '../../typings/api';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import { useRoute } from 'vue-router'; import { onBeforeRouteUpdate, useRoute } from 'vue-router';
import { Status } from '../../typings/common'; import { Status } from '../../typings/common';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
@@ -127,18 +136,19 @@ interface JournalEntry {
const props = defineProps({ const props = defineProps({
playerName: { playerName: {
type: String type: String
},
playerJournal: {
type: Object as PropType<API.PlayerJournal.Data>,
},
journalStatus: {
type: Number as PropType<Status.Data>,
required: true
} }
}); });
const { t } = useI18n(); const { t } = useI18n();
const apiStore = useApiStore();
const route = useRoute();
const playerId = ref(-1);
const playerJournal = ref<API.PlayerJournal.Data | null>(null);
const journalDataStatus = ref(Status.Data.Initialized);
const intervalId = ref(-1);
const activeFilterTypes = reactive<Record<JournalEntryType, boolean>>({ const activeFilterTypes = reactive<Record<JournalEntryType, boolean>>({
Timetable: true, Timetable: true,
@@ -146,23 +156,13 @@ const activeFilterTypes = reactive<Record<JournalEntryType, boolean>>({
IssuedTimetable: true IssuedTimetable: true
}); });
onMounted(() => {
fetchPlayerJournal();
intervalId.value = setInterval(fetchPlayerJournal, 30000);
});
onDeactivated(() => {
clearInterval(intervalId.value);
intervalId.value = -1;
});
const combinedJournal = computed<JournalEntry[]>(() => { const combinedJournal = computed<JournalEntry[]>(() => {
if (!playerJournal.value || !props.playerName) return []; if (!props.playerJournal || !props.playerName) return [];
const list = [ const list = [
...playerJournal.value.timetables, ...props.playerJournal.timetables,
...playerJournal.value.duties, ...props.playerJournal.duties,
...playerJournal.value.issuedTimetables ...props.playerJournal.issuedTimetables
] ]
.reduce<JournalEntry[]>((acc, v) => { .reduce<JournalEntry[]>((acc, v) => {
// Timetable or dispatcher type // Timetable or dispatcher type
@@ -209,35 +209,6 @@ function toggleFilter(filterType: JournalEntryType) {
activeFilterTypes[filterType] = toggledState; activeFilterTypes[filterType] = toggledState;
} }
async function fetchPlayerJournal() {
const queryPlayerId = Number(route.query.playerId) || -1;
if (!apiStore.client || !queryPlayerId) return;
if (queryPlayerId != playerId.value) {
journalDataStatus.value = Status.Data.Loading;
}
playerId.value = queryPlayerId;
try {
const response = await apiStore.client.get<API.PlayerJournal.Data>('api/getPlayerJournal', {
params: {
playerId: queryPlayerId,
dateScope: '30d'
}
});
playerJournal.value = response.data;
journalDataStatus.value = Status.Data.Loaded;
} catch (error) {
console.error(error);
journalDataStatus.value = Status.Data.Error;
}
return null;
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,38 +1,46 @@
<template> <template>
<div class="player-avatar"> <div class="player-avatar">
<img <img
v-if="avatarId" v-if="props.playerTD2Info && props.playerTD2Info.avatar"
:src="`https://td2.info.pl/index.php?action=dlattach;attach=${props.playerTD2Info.avatar};type=avatar`"
class="player-avatar-image" class="player-avatar-image"
ref="avatarImageRef" ref="avatarImageRef"
:src="`https://td2.info.pl/index.php?action=dlattach;attach=${avatarId};type=avatar`"
alt="player image" alt="player image"
@load="onAvatarLoadSuccess" @load="onAvatarLoadSuccess"
@error="onAvatarLoadError" @error="onAvatarLoadError"
/> />
<img <img
v-if="avatarLoadingStatus == Status.Data.Error || avatarId == 0" v-if="
avatarLoadingStatus == Status.Data.Error ||
(props.playerTD2Info && !props.playerTD2Info.avatar)
"
class="img-placeholder" class="img-placeholder"
height="100" height="100"
src="/images/default-avatar.jpg" src="/images/default-avatar.jpg"
/> />
<Loading v-else-if="avatarLoadingStatus == Status.Data.Loading || avatarId === undefined" /> <Loading v-else-if="avatarLoadingStatus == Status.Data.Loading" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { computed, onMounted, PropType, ref, useTemplateRef } from 'vue';
import { Status } from '../../typings/common'; import { Status } from '../../typings/common';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import { Td2API } from '../../typings/api';
defineProps({ const props = defineProps({
avatarId: { playerTD2Info: {
type: Number type: Object as PropType<Td2API.UsersInfoByName.UserInfo>
} }
}); });
const avatarImageRef = ref<HTMLImageElement | null>(null); onMounted(() => {
console.log(avatarImageRef.value);
});
const avatarImageRef = useTemplateRef('avatarImageRef');
const avatarLoadingStatus = ref<Status.Data>(Status.Data.Loading); const avatarLoadingStatus = ref<Status.Data>(Status.Data.Loading);
function onAvatarLoadSuccess() { function onAvatarLoadSuccess() {
@@ -2,7 +2,7 @@
<section class="profile-summary"> <section class="profile-summary">
<div class="player-info"> <div class="player-info">
<div class="info-main"> <div class="info-main">
<ProfilePlayerAvatar :avatarId="playerTD2Info?.avatar" /> <ProfilePlayerAvatar :playerTD2Info="playerTD2Info" />
<div> <div>
<h2 class="player-name-header" :class="{ 'text--donator': isPlayerDonator }"> <h2 class="player-name-header" :class="{ 'text--donator': isPlayerDonator }">
@@ -69,16 +69,16 @@
</a> </a>
</div> </div>
<!-- Current activity --> <!-- Current activities -->
<div <div
class="player-activity" class="player-activities-box"
v-if="activeDispatches.length > 0 || activeTrains.length > 0" v-if="activeDispatches.length > 0 || activeTrains.length > 0"
> >
<div class="info-activity" v-if="activeDispatches.length > 0"> <div class="info-activity" v-if="activeDispatches.length > 0">
<router-link <router-link
v-for="d in activeDispatches" v-for="d in activeDispatches"
class="dispatcher-badge" class="dispatcher-badge"
:to="`/scenery?station=${d.stationName}`" :to="`/scenery?station=${d.stationName}&region=${d.region}`"
> >
<img src="/images/icon-user.svg" width="25" alt="user icon" /> <img src="/images/icon-user.svg" width="25" alt="user icon" />
<b>{{ d.stationName }} ({{ getRegionNameById(d.region) }})</b> <b>{{ d.stationName }} ({{ getRegionNameById(d.region) }})</b>
@@ -88,17 +88,17 @@
<div class="info-activity" v-if="activeTrains.length > 0"> <div class="info-activity" v-if="activeTrains.length > 0">
<router-link <router-link
v-for="d in activeTrains" v-for="t in activeTrains"
:to="`/driver?trainId=${d.id}`" :to="`/driver?trainId=${t.id}`"
class="driver-badge" class="driver-badge"
> >
<img src="/images/icon-train.svg" width="25" alt="train icon" /> <img src="/images/icon-train.svg" width="25" alt="train icon" />
<span v-if="d.timetable" class="text--primary">{{ d.timetable.category }}</span> <span v-if="t.timetable" class="text--primary">{{ t.timetable.category }}</span>
<span>{{ d.trainNo }}</span> <span>{{ t.trainNo }}</span>
&bull; &bull;
<span>{{ d.currentStationName }} ({{ getRegionNameById(d.region) }})</span> <span>{{ t.currentStationName }} ({{ getRegionNameById(t.region) }})</span>
&bull; &bull;
<span class="text--grayed">{{ d.stockString.split(';')[0] }}</span> <span class="text--grayed">{{ t.stockString.split(';')[0] }}</span>
</router-link> </router-link>
</div> </div>
</div> </div>
@@ -221,7 +221,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, PropType, ref } from 'vue'; import { computed, onActivated, onMounted, PropType, ref, watch } from 'vue';
import { API, Td2API } from '../../typings/api'; import { API, Td2API } from '../../typings/api';
import { calculateExpStyles } from '../../composables/badge'; import { calculateExpStyles } from '../../composables/badge';
import { getCountPercentage } from '../../utils/calcUtils'; import { getCountPercentage } from '../../utils/calcUtils';
@@ -230,7 +230,6 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import StationStatusBadge from '../Global/StationStatusBadge.vue'; import StationStatusBadge from '../Global/StationStatusBadge.vue';
import axios from 'axios';
import ProfilePlayerAvatar from './ProfilePlayerAvatar.vue'; import ProfilePlayerAvatar from './ProfilePlayerAvatar.vue';
import { getRegionNameById } from '../../utils/regionUtils'; import { getRegionNameById } from '../../utils/regionUtils';
@@ -245,21 +244,19 @@ const props = defineProps({
required: true required: true
}, },
playerTD2Info: {
type: Object as PropType<Td2API.UsersInfoByName.UserInfo>
},
playerName: { playerName: {
type: String type: String
} }
}); });
const playerTD2Info = ref<Td2API.UsersInfoByName.UserInfo | null>(null);
const isPlayerDonator = computed(() => const isPlayerDonator = computed(() =>
props.playerName ? apiStore.donatorsData.includes(props.playerName) : false props.playerName ? apiStore.donatorsData.includes(props.playerName) : false
); );
onMounted(() => {
fetchTD2Data();
});
const activeDispatches = computed(() => { const activeDispatches = computed(() => {
if (!props.playerName) return []; if (!props.playerName) return [];
if (!apiStore.activeData || !apiStore.activeData.activeSceneries) return []; if (!apiStore.activeData || !apiStore.activeData.activeSceneries) return [];
@@ -278,27 +275,6 @@ const activeTrains = computed(() => {
(t) => t.driverName == props.playerName && (t.lastSeen >= Date.now() - 60000 || t.online) (t) => t.driverName == props.playerName && (t.lastSeen >= Date.now() - 60000 || t.online)
); );
}); });
async function fetchTD2Data() {
if (!props.playerName) return;
try {
const response = await axios.get<Td2API.UsersInfoByName.Response>('https://api.td2.info.pl', {
params: {
method: 'getUsersInfoByName',
name: props.playerName
}
});
if (response.data.success && response.data.message.length == 1) {
playerTD2Info.value = response.data.message[0];
}
} catch (error) {
console.error(error);
}
return;
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -372,8 +348,6 @@ async function fetchTD2Data() {
gap: 0.25em; gap: 0.25em;
font-weight: bold; font-weight: bold;
padding: 0.25em 0.5em;
border-radius: 0.5em; border-radius: 0.5em;
} }
} }
-1
View File
@@ -74,7 +74,6 @@ const routes: Array<RouteRecordRaw> = [
const router = createRouter({ const router = createRouter({
scrollBehavior(to, from, savedPosition) { scrollBehavior(to, from, savedPosition) {
console.log(to.name);
if ( if (
(to.name == 'SceneryView' || to.name == 'DriverView' || to.name == 'PlayerProfileView') && (to.name == 'SceneryView' || to.name == 'DriverView' || to.name == 'PlayerProfileView') &&
from.name !== to.name && from.name !== to.name &&
+1 -1
View File
@@ -68,7 +68,7 @@ export const useApiStore = defineStore('apiStore', {
// Active data fefresh // Active data fefresh
if (t >= this.nextUpdateTime) { if (t >= this.nextUpdateTime) {
this.fetchActiveData(); this.fetchActiveData();
this.nextUpdateTime = t + 20000; this.nextUpdateTime = t + 31000;
} }
window.requestAnimationFrame(this.updateTick); window.requestAnimationFrame(this.updateTick);
+92 -21
View File
@@ -1,15 +1,23 @@
<template> <template>
<div class="profile-view"> <div class="profile-view">
<div class="profile-wrapper" v-if="playerInfo && playerDataStatus == Status.Data.Loaded"> <div class="profile-wrapper" v-if="playerInfo && playerInfoStatus == Status.Data.Loaded">
<ProfileSummary :playerInfo="playerInfo" :playerName="playerName" /> <ProfileSummary
:playerInfo="playerInfo"
:playerTD2Info="playerTD2Info"
:playerName="playerName"
/>
<div class="profile-side"> <div class="profile-side">
<ProfileRecentStats :playerInfo="playerInfo" /> <ProfileRecentStats :playerInfo="playerInfo" />
<ProfileHistoryList :playerName="playerName" /> <ProfileHistoryList
:playerName="playerName"
:playerJournal="playerJournal"
:journalStatus="playerJournalStatus"
/>
</div> </div>
</div> </div>
<Loading v-else-if="playerDataStatus == Status.Data.Loading" /> <Loading v-else-if="playerInfoStatus == Status.Data.Loading" />
<div class="no-data-found" v-else> <div class="no-data-found" v-else>
<div> <div>
@@ -22,9 +30,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onActivated, onDeactivated, ref } from 'vue'; import { onActivated, onDeactivated, ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useApiStore } from '../store/apiStore'; import { useApiStore } from '../store/apiStore';
import { API } from '../typings/api'; import { API, Td2API } from '../typings/api';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { Status } from '../typings/common'; import { Status } from '../typings/common';
@@ -32,8 +40,10 @@ 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 apiStore = useApiStore(); const apiStore = useApiStore();
const route = useRoute(); const route = useRoute();
@@ -41,15 +51,18 @@ const route = useRoute();
const playerId = ref(-1); const playerId = ref(-1);
const playerName = ref(''); const playerName = ref('');
const playerInfo = ref<API.PlayerInfo.Data | null>(null); const playerInfo = ref<API.PlayerInfo.Data | undefined>(undefined);
const playerDataStatus = ref(Status.Data.Initialized); const playerTD2Info = ref<Td2API.UsersInfoByName.UserInfo | undefined>(undefined);
const playerJournal = ref<API.PlayerJournal.Data | undefined>(undefined);
const playerInfoStatus = ref(Status.Data.Initialized);
const playerJournalStatus = ref(Status.Data.Initialized);
const intervalId = ref(-1); const intervalId = ref(-1);
onActivated(() => { onActivated(() => {
fetchPlayerData(); fetchPlayerData();
intervalId.value = setInterval(fetchPlayerData, 32000);
intervalId.value = setInterval(fetchPlayerData, 30000);
}); });
onDeactivated(() => { onDeactivated(() => {
@@ -57,32 +70,85 @@ onDeactivated(() => {
intervalId.value = -1; intervalId.value = -1;
}); });
async function fetchPlayerInfo(playerId: number) {
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', {
params: {
playerId,
dateScope: '30d'
}
});
}
async function fetchPlayerTd2Info(playerName: string) {
return axios.get<Td2API.UsersInfoByName.Response>('https://api.td2.info.pl', {
params: {
method: 'getUsersInfoByName',
name: playerName
}
});
}
async function fetchPlayerData() { async function fetchPlayerData() {
const queryPlayerId = Number(route.query.playerId) || -1; const queryPlayerId = Number(route.query.playerId) || -1;
if (!apiStore.client || !queryPlayerId) return; if (!apiStore.client || !queryPlayerId) return;
if (queryPlayerId != playerId.value) { if (queryPlayerId != playerId.value) {
playerDataStatus.value = Status.Data.Loading; playerInfoStatus.value = Status.Data.Loading;
playerJournalStatus.value = Status.Data.Loading;
playerInfo.value = undefined;
playerTD2Info.value = undefined;
playerJournal.value = undefined;
} }
playerId.value = queryPlayerId; playerId.value = queryPlayerId;
try { try {
const response = await apiStore.client.get<API.PlayerInfo.Data>('api/getPlayerInfo', { const playerInfoResp = await fetchPlayerInfo(playerId.value);
params: {
playerId: queryPlayerId
}
});
playerName.value = playerName.value =
response.data.driverStats.driverName || response.data.dispatcherStats.dispatcherName || ''; playerInfoResp.data.driverStats.driverName ||
playerInfoResp.data.dispatcherStats.dispatcherName ||
'';
playerInfo.value = response.data || null; if (!playerName.value) {
playerDataStatus.value = Status.Data.Loaded; router.push('/');
return;
}
playerInfo.value = playerName.value ? playerInfoResp.data : undefined;
playerInfoStatus.value = Status.Data.Loaded;
if (playerName.value) {
const playerTD2InfoResp = await fetchPlayerTd2Info(playerName.value);
if (playerTD2InfoResp.data.success && playerTD2InfoResp.data.message.length == 1) {
playerTD2Info.value = playerTD2InfoResp.data.message[0];
}
}
} catch (error) { } catch (error) {
console.error(error); playerInfo.value = undefined;
playerDataStatus.value = Status.Data.Error; playerTD2Info.value = undefined;
playerInfoStatus.value = Status.Data.Error;
}
try {
const playerJournalResp = await fetchPlayerJournal(playerId.value);
playerJournal.value = playerJournalResp.data;
playerJournalStatus.value = Status.Data.Loaded;
} catch (error) {
playerJournal.value = undefined;
playerJournalStatus.value = Status.Data.Error;
} }
} }
</script> </script>
@@ -100,7 +166,12 @@ async function fetchPlayerData() {
} }
.no-data-found { .no-data-found {
display: flex;
align-items: center;
justify-content: center;
text-align: center; text-align: center;
font-size: 1.35em;
max-width: var(--max-container-width); max-width: var(--max-container-width);
width: 100%; width: 100%;