Files
buildpath/frontend/pages/champion/[alias].vue
Valentin Haudiquet 7a34c16087
All checks were successful
pipeline / lint-and-format (push) Successful in 4m50s
pipeline / build-and-push-images (push) Successful in 1m37s
Champion page improvements
2026-01-23 23:25:17 +01:00

308 lines
7.5 KiB
Vue

<script setup lang="ts">
import { ref, computed } from 'vue'
const route = useRoute()
const championAlias = route.params.alias as string
// State management
const isLoading = ref(true)
const error = ref<string | null>(null)
const laneState = ref(0)
const state = ref('runes')
// Use useAsyncData with client-side fetching for faster initial page load
const {
data: championData,
pending,
error: fetchError,
refresh
} = useAsyncData<ChampionData>(
'champion-' + championAlias.toLowerCase(),
() => $fetch('/api/champion/' + championAlias.toLowerCase()),
{
server: false, // Disable server-side fetching for faster initial load
lazy: false // Fetch immediately on client
}
)
// Handle errors from useAsyncData
watchEffect(() => {
if (fetchError.value) {
console.error('Error fetching champion data:', fetchError.value)
error.value =
fetchError.value instanceof Error
? fetchError.value.message
: typeof fetchError.value === 'string'
? fetchError.value
: 'Unknown error occurred'
}
})
// Computed properties
const championId = computed(() => championData.value?.id || 0)
const lane = computed(() => championData.value?.lanes?.[laneState.value] || null)
// Update loading state with proper error handling
isLoading.value = pending.value
// Watch for data changes to update loading state
watch(
[championData, fetchError, pending],
([newData, newError, newPending]) => {
if (newError) {
error.value =
newError instanceof Error
? newError.message
: typeof newError === 'string'
? newError
: 'Unknown error occurred'
isLoading.value = false
} else if (newData) {
isLoading.value = false
} else {
isLoading.value = newPending
}
},
{ immediate: true }
)
// Add timeout to prevent infinite loading
const loadingTimeout = setTimeout(() => {
if (isLoading.value && !championData.value && !error.value) {
console.warn('Champion data loading timed out')
error.value = 'Loading took too long. Please try again.'
isLoading.value = false
}
}, 5000) // 5 second timeout
// Clean up timeout when component unmounts
onUnmounted(() => {
clearTimeout(loadingTimeout)
})
// Set up SEO after data is loaded - only on server side
if (import.meta.server) {
watchEffect(() => {
if (championData.value) {
defineOgImageComponent('Champion', {
title: championData.value.name,
id: championId.value,
winrate: championData.value.winrate,
pickrate: championData.value.pickrate,
gameCount: championData.value.gameCount
})
useSeoMeta({
title: championData.value.name,
description: 'Build path and runes for ' + championData.value.name + ' on BuildPath'
})
}
})
}
// Prefetch home page for faster navigation
useHead({
link: [{ rel: 'prefetch', href: '/' }]
})
function updateState(newState: string, newLane: number) {
state.value = newState
laneState.value = newLane
}
// Function to retry fetching data
function fetchChampionData() {
error.value = null
isLoading.value = true
refresh().catch(err => {
console.error('Error refreshing champion data:', err)
error.value = err instanceof Error ? err.message : 'Failed to refresh champion data'
isLoading.value = false
})
}
</script>
<template>
<div class="champion-root">
<!-- Loading state -->
<div v-if="isLoading" class="loading-state">
<div class="loading-spinner" />
<p>Loading champion data...</p>
</div>
<!-- Error state -->
<div v-else-if="error" class="error-state">
<Logo style="margin: auto; margin-top: 64px; margin-bottom: 64px" />
<div style="margin: auto; width: fit-content; margin-top: 64px">
<h1>Error Loading Champion</h1>
<h2>Sorry, we couldn't load this champion's data</h2>
<div style="margin-top: 64px">
<h3 v-if="error">Error: {{ error }}</h3>
<button class="retry-button" @click="fetchChampionData">Retry</button>
</div>
</div>
</div>
<!-- Success state -->
<div v-else-if="championData" class="champion-success">
<Head>
<Title>{{ championData.name }}</Title>
</Head>
<div id="alias-content-wrapper">
<NavBar
:champion-name="championData.name"
:champion-lanes="championData.lanes"
@state-change="updateState"
/>
<div id="champion-content">
<ChampionTitle
v-if="championData.gameCount > 0 && lane"
id="champion-title"
:champion-id="championId"
:winrate="lane.winrate || 0"
:pickrate="lane.pickrate || 0"
:game-count="lane.count || 0"
/>
<ClientOnly>
<LazyRuneSelector
v-if="state == 'runes' && championData.gameCount > 0 && lane?.runes"
style="margin: auto; margin-top: 40px"
:runes="lane.runes"
/>
</ClientOnly>
<ClientOnly>
<LazyItemViewer
v-if="state == 'items' && championData.gameCount > 0 && lane?.builds"
style="margin: auto; margin-top: 40px"
:builds="lane.builds"
/>
</ClientOnly>
<ClientOnly>
<LazyItemTree
v-if="state == 'alternatives' && championData.gameCount > 0 && lane?.builds?.tree"
style="margin: auto; margin-top: 40px; width: fit-content"
:tree="lane.builds.tree"
/>
</ClientOnly>
<h2
v-if="championData.gameCount == 0"
style="margin: auto; margin-top: 20px; width: fit-content"
>
Sorry, there is no data for this champion :(
</h2>
</div>
</div>
</div>
</div>
</template>
<style>
#alias-content-wrapper {
display: flex;
/* min-height: 100vh; */
align-items: stretch;
width: 100%;
overflow: hidden;
}
#champion-content {
margin-top: 64px;
margin-left: 39px;
width: 100%;
}
/* Loading state styles */
.loading-state {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
width: 100%;
gap: 16px;
}
.loading-spinner {
width: 60px;
height: 60px;
border: 6px solid var(--color-surface);
border-top: 6px solid var(--color-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-state p {
margin: 0;
color: var(--color-on-surface);
font-size: 1.2rem;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%,
100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
/* Error state styles */
.error-state {
text-align: center;
padding: 64px 20px;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.retry-button {
background-color: #4caf50;
border: none;
color: white;
padding: 12px 24px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin-top: 20px;
cursor: pointer;
border-radius: 8px;
transition: background-color 0.3s;
}
.retry-button:hover {
background-color: #45a049;
}
@media only screen and (max-width: 650px) {
#champion-content {
margin: auto;
margin-top: 10px;
}
#champion-title {
margin: auto;
}
}
@media only screen and (max-width: 1200px) {
#alias-content-wrapper {
flex-direction: column;
padding-bottom: 120px;
margin-top: 20px;
}
}
</style>