328 lines
7.1 KiB
Vue
328 lines
7.1 KiB
Vue
<script setup lang="ts">
|
|
import { debounce, isEmpty } from '~/utils/helpers'
|
|
|
|
// Constants
|
|
const CDRAGON_CHAMPIONS_URL =
|
|
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json'
|
|
const CHAMPIONS_API_URL = '/api/champions'
|
|
|
|
// State
|
|
const {
|
|
data: championsData,
|
|
pending: loadingChampions,
|
|
error: championsError
|
|
} = useFetch(CDRAGON_CHAMPIONS_URL, {
|
|
key: 'champions-data',
|
|
lazy: false,
|
|
server: false // Disable server-side fetching to avoid hydration issues
|
|
})
|
|
const {
|
|
data: championsLanes,
|
|
pending: loadingLanes,
|
|
error: lanesError
|
|
} = useFetch(CHAMPIONS_API_URL, {
|
|
key: 'champions-lanes',
|
|
lazy: false,
|
|
server: false // Disable server-side fetching to avoid hydration issues
|
|
})
|
|
|
|
// Data processing
|
|
const champions = computed(() => {
|
|
if (!championsData.value || !Array.isArray(championsData.value)) return []
|
|
|
|
return championsData.value
|
|
.slice(1)
|
|
.filter((champion: any) => !champion.name.includes('Doom Bot'))
|
|
.sort((a: any, b: any) => a.name.localeCompare(b.name))
|
|
})
|
|
|
|
const lanesMap = computed(() => {
|
|
const map = new Map<string, LaneData[]>()
|
|
if (championsLanes.value) {
|
|
for (const champion of championsLanes.value as ChampionData[]) {
|
|
map.set(champion.alias.toLowerCase(), champion.lanes)
|
|
}
|
|
}
|
|
return map
|
|
})
|
|
|
|
// Filter state
|
|
const filteredChampions = ref<ChampionSummary[]>([])
|
|
const searchText = ref('')
|
|
const searchBar = useTemplateRef('searchBar')
|
|
|
|
// Lane filtering
|
|
function filterToLane(filter: number): string {
|
|
const laneMap = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'UTILITY']
|
|
return laneMap[filter] || ''
|
|
}
|
|
|
|
function filterChampionsByLane(laneFilter: number): void {
|
|
if (laneFilter === -1) {
|
|
filteredChampions.value = [...champions.value]
|
|
return
|
|
}
|
|
|
|
const laneName = filterToLane(laneFilter)
|
|
filteredChampions.value = champions.value.filter((champion: any) => {
|
|
const championLanes = lanesMap.value.get(champion.alias.toLowerCase())
|
|
if (!championLanes) return false
|
|
|
|
return championLanes.some(lane => lane.data === laneName)
|
|
})
|
|
}
|
|
|
|
// Search functionality
|
|
const debouncedSearch = debounce((searchTerm: string) => {
|
|
if (isEmpty(searchTerm)) {
|
|
filteredChampions.value = [...champions.value]
|
|
} else {
|
|
filteredChampions.value = champions.value.filter((champion: any) =>
|
|
champion.name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
)
|
|
}
|
|
}, 300)
|
|
|
|
// Watchers
|
|
watch(searchBar, (newS, oldS) => {
|
|
searchBar.value?.focus()
|
|
})
|
|
|
|
watch(searchText, newTerm => {
|
|
debouncedSearch(newTerm)
|
|
})
|
|
|
|
// Watch for changes in champions data and update filtered champions
|
|
watch(
|
|
champions,
|
|
newChampions => {
|
|
filteredChampions.value = [...newChampions]
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
// Navigation
|
|
async function navigateToChampion(championAlias: string): Promise<void> {
|
|
try {
|
|
await navigateTo(`/champion/${championAlias.toLowerCase()}`)
|
|
} catch (error) {
|
|
console.error('Navigation error:', error)
|
|
}
|
|
}
|
|
|
|
// Initialize filtered champions
|
|
onMounted(() => {
|
|
filteredChampions.value = [...champions.value]
|
|
})
|
|
|
|
// Error handling
|
|
const hasErrors = computed(() => championsError.value || lanesError.value)
|
|
const isLoading = computed(() => loadingChampions.value || loadingLanes.value)
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<!-- Loading state -->
|
|
<div v-if="isLoading" class="loading-state">
|
|
<div class="loading-spinner"/>
|
|
<p>Loading champions...</p>
|
|
</div>
|
|
|
|
<!-- Error state -->
|
|
<div v-else-if="hasErrors" class="error-state">
|
|
<p>Failed to load champion data. Please refresh the page.</p>
|
|
</div>
|
|
|
|
<!-- Main content -->
|
|
<div v-else>
|
|
<div class="search-lanefilter-container">
|
|
<LaneFilter id="cs-lanefilter" @filter-change="filterChampionsByLane" />
|
|
<input
|
|
ref="searchBar"
|
|
v-model="searchText"
|
|
class="search-bar"
|
|
type="text"
|
|
placeholder="Search a champion"
|
|
@keyup.enter="
|
|
() => filteredChampions.length > 0 && navigateToChampion(filteredChampions[0].alias)
|
|
"
|
|
>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div v-if="filteredChampions.length === 0" class="empty-state">
|
|
<p>No champions found. Try a different search or filter.</p>
|
|
</div>
|
|
|
|
<div v-else class="champion-container">
|
|
<NuxtLink
|
|
v-for="champion in filteredChampions"
|
|
:key="champion.id"
|
|
:to="'/champion/' + champion.alias.toLowerCase()"
|
|
style="width: fit-content; height: fit-content"
|
|
>
|
|
<div class="cs-champion-img-container">
|
|
<NuxtImg
|
|
format="webp"
|
|
class="cs-champion-img"
|
|
:src="CDRAGON_BASE + mapPath(champion.squarePortraitPath)"
|
|
:alt="champion.name"
|
|
/>
|
|
</div>
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style>
|
|
/* Loading and error states */
|
|
.loading-state,
|
|
.error-state,
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
color: var(--color-on-surface);
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.loading-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 4px solid var(--color-surface);
|
|
border-top: 4px solid var(--color-primary);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.loading-state p {
|
|
margin: 0;
|
|
animation: pulse 1.5s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%,
|
|
100% {
|
|
opacity: 0.6;
|
|
}
|
|
50% {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.error-state {
|
|
color: var(--color-error);
|
|
}
|
|
|
|
.empty-state {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.search-bar {
|
|
width: 400px;
|
|
height: 60px;
|
|
|
|
background-color: var(--color-surface-darker);
|
|
|
|
font-size: 1.75rem;
|
|
|
|
border-radius: 12px;
|
|
border: none;
|
|
padding-left: 10px;
|
|
}
|
|
.search-bar:focus {
|
|
border: 2px solid var(--color-on-surface);
|
|
outline: none;
|
|
}
|
|
|
|
#cs-lanefilter {
|
|
margin: auto;
|
|
margin-right: 20px;
|
|
}
|
|
|
|
.search-lanefilter-container {
|
|
width: fit-content;
|
|
margin: auto;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.champion-container {
|
|
width: 90%;
|
|
height: auto;
|
|
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, 128px);
|
|
grid-gap: 10px;
|
|
justify-content: center;
|
|
|
|
margin: auto;
|
|
margin-top: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.cs-champion-img-container {
|
|
overflow: hidden;
|
|
width: 120px;
|
|
height: 120px;
|
|
border: 1px solid var(--color-surface);
|
|
}
|
|
.cs-champion-img-container:hover {
|
|
border: 1px solid var(--color-on-surface);
|
|
}
|
|
.cs-champion-img {
|
|
width: 116px;
|
|
height: 116px;
|
|
transform: translate(4px, 4px) scale(1.2, 1.2);
|
|
|
|
user-select: none;
|
|
}
|
|
|
|
@media only screen and (max-width: 920px) {
|
|
.search-lanefilter-container {
|
|
flex-direction: column;
|
|
}
|
|
#cs-lanefilter {
|
|
margin-right: auto;
|
|
}
|
|
.champion-container {
|
|
width: 100%;
|
|
}
|
|
}
|
|
@media only screen and (max-width: 450px) {
|
|
.search-bar {
|
|
width: 92%;
|
|
height: 40px;
|
|
}
|
|
.cs-champion-img-container {
|
|
width: 80px;
|
|
height: 80px;
|
|
}
|
|
.cs-champion-img {
|
|
width: 76px;
|
|
height: 76px;
|
|
}
|
|
.champion-container {
|
|
grid-template-columns: repeat(auto-fit, 80px);
|
|
}
|
|
.search-lanefilter-container {
|
|
margin-top: 10px;
|
|
}
|
|
}
|
|
</style>
|