chore(profile): moved player avatar and its logic to separate component

This commit is contained in:
2026-02-20 01:27:35 +01:00
parent b7db3edd9b
commit 86fbaa2510
3 changed files with 147 additions and 84 deletions
@@ -0,0 +1,72 @@
<template>
<div class="player-avatar">
<img
v-if="avatarId"
class="player-avatar-image"
ref="avatarImageRef"
:src="`https://td2.info.pl/index.php?action=dlattach;attach=${avatarId};type=avatar`"
alt="player image"
@load="onAvatarLoadSuccess"
@error="onAvatarLoadError"
/>
<img
v-if="avatarLoadingStatus == Status.Data.Error || avatarId == 0"
class="img-placeholder"
height="100"
src="/images/default-avatar.jpg"
/>
<Loading v-else-if="avatarLoadingStatus == Status.Data.Loading || avatarId === undefined" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Status } from '../../typings/common';
import Loading from '../Global/Loading.vue';
defineProps({
avatarId: {
type: Number
}
});
const avatarImageRef = ref<HTMLImageElement | null>(null);
const avatarLoadingStatus = ref<Status.Data>(Status.Data.Loading);
function onAvatarLoadSuccess() {
if (!avatarImageRef.value) return;
avatarLoadingStatus.value = Status.Data.Loaded;
avatarImageRef.value.style.opacity = '1';
}
function onAvatarLoadError() {
if (!avatarImageRef.value) return;
avatarLoadingStatus.value = Status.Data.Error;
avatarImageRef.value.src = '/images/default-avatar.jpg';
avatarImageRef.value.style.opacity = '1';
}
</script>
<style lang="scss" scoped>
.player-avatar {
display: flex;
justify-content: center;
align-items: center;
position: relative;
min-height: 110px;
.loading {
top: 50%;
margin: 0;
}
img.player-avatar-image {
opacity: 0;
}
}
</style>
@@ -2,14 +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">
<img <ProfilePlayerAvatar :avatarId="playerTD2Info?.avatar" />
v-if="playerTD2Info"
:src="`https://td2.info.pl/index.php?action=dlattach;attach=${playerTD2Info.avatar};type=avatar`"
alt="player image"
@error="(e) => ((e.target as any).src = '/images/default-avatar.jpg')"
/>
<img class="img-placeholder" height="100" src="/images/default-avatar.jpg" v-else />
<div> <div>
<h2 class="player-name-header" :class="{ 'text--donator': isPlayerDonator }"> <h2 class="player-name-header" :class="{ 'text--donator': isPlayerDonator }">
@@ -228,7 +221,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, PropType } from 'vue'; import { computed, onMounted, PropType, ref } 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';
@@ -237,6 +230,10 @@ 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 { Status } from '../../typings/common';
import Loading from '../Global/Loading.vue';
import ProfilePlayerAvatar from './ProfilePlayerAvatar.vue';
const { t } = useI18n(); const { t } = useI18n();
@@ -249,19 +246,21 @@ const props = defineProps({
required: true required: true
}, },
playerTD2Info: {
type: Object as PropType<Td2API.UsersInfoByName.UserInfo | null>
},
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 [];
@@ -280,6 +279,27 @@ 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>
@@ -293,23 +313,6 @@ const activeTrains = computed(() => {
overflow: auto; overflow: auto;
} }
.player-stats {
display: flex;
flex-direction: column;
gap: 1em;
hr {
margin: 0.5em 0;
}
}
.player-info,
.player-stats > div {
background-color: var(--clr-tile);
border-radius: 0.5em;
padding: 1em;
}
.player-name-header { .player-name-header {
margin: 0.5em 0; margin: 0.5em 0;
@@ -327,14 +330,6 @@ const activeTrains = computed(() => {
gap: 1em; gap: 1em;
} }
.player-journal-links {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
}
.badge-container { .badge-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -348,11 +343,12 @@ const activeTrains = computed(() => {
} }
} }
.stats-header { .player-journal-links {
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
gap: 0.25em; flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
} }
.info-activity { .info-activity {
@@ -362,22 +358,49 @@ const activeTrains = computed(() => {
flex-wrap: wrap; flex-wrap: wrap;
gap: 1em; gap: 1em;
margin-top: 1em; margin-top: 1em;
.dispatcher-badge {
display: flex;
align-items: center;
gap: 0.25em;
}
.driver-badge {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
font-weight: bold;
padding: 0.25em 0.5em;
border-radius: 0.5em;
}
} }
.info-activity > .dispatcher-badge { .player-stats {
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: 0.25em; gap: 1em;
hr {
margin: 0.5em 0;
}
} }
.info-activity > .driver-badge { .player-info,
display: flex; .player-stats > div {
align-items: center; background-color: var(--clr-tile);
gap: 0.25em;
font-weight: bold;
padding: 0.25em 0.5em;
border-radius: 0.5em; border-radius: 0.5em;
padding: 1em;
}
.stats-header {
display: flex;
align-items: center;
justify-content: center;
gap: 0.25em;
} }
@include responsive.midScreen { @include responsive.midScreen {
+1 -33
View File
@@ -1,11 +1,7 @@
<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 && playerDataStatus == Status.Data.Loaded">
<ProfileSummary <ProfileSummary :playerInfo="playerInfo" :playerName="playerName" />
:playerInfo="playerInfo"
:playerTD2Info="playerTD2Info"
:playerName="playerName"
/>
<div class="profile-side"> <div class="profile-side">
<ProfileRecentStats :playerInfo="playerInfo" /> <ProfileRecentStats :playerInfo="playerInfo" />
@@ -49,7 +45,6 @@ const route = useRoute();
const playerName = ref(''); const playerName = ref('');
const playerInfo = ref<API.PlayerInfo.Data | null>(null); const playerInfo = ref<API.PlayerInfo.Data | null>(null);
const playerTD2Info = ref<Td2API.UsersInfoByName.UserInfo | null>(null);
const playerDataStatus = ref(Status.Data.Initialized); const playerDataStatus = ref(Status.Data.Initialized);
watch( watch(
@@ -67,7 +62,6 @@ async function fetchAllData() {
const playerId = route.query.playerId?.toString(); const playerId = route.query.playerId?.toString();
playerInfo.value = null; playerInfo.value = null;
playerTD2Info.value = null;
playerDataStatus.value = Status.Data.Loading; playerDataStatus.value = Status.Data.Loading;
if (!playerId) { if (!playerId) {
@@ -92,12 +86,7 @@ async function fetchAllData() {
return; return;
} }
const playerTd2InfoResponse = await fetchPlayerTD2Info(playerName.value);
playerInfo.value = playerInfoResponse; playerInfo.value = playerInfoResponse;
playerTD2Info.value = playerTd2InfoResponse;
// playerJournal.value = playerJournalResponse;
playerDataStatus.value = Status.Data.Loaded; playerDataStatus.value = Status.Data.Loaded;
} }
@@ -118,27 +107,6 @@ async function fetchPlayerInfoData(playerId: string) {
return null; return null;
} }
async function fetchPlayerTD2Info(playerName: string) {
if (!apiStore.client || !playerName) return null;
try {
const response = await axios.get<Td2API.UsersInfoByName.Response>('https://api.td2.info.pl', {
params: {
method: 'getUsersInfoByName',
name: playerName
}
});
if (response.data.success && response.data.message.length == 1) {
return response.data.message[0];
}
} catch (error) {
console.error(error);
}
return null;
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>