308 lines
7.5 KiB
Vue
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>
|