Lint and format
This commit is contained in:
@@ -1,305 +1,327 @@
|
||||
<script setup lang="ts">
|
||||
import { debounce, isEmpty } from '~/utils/helpers';
|
||||
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";
|
||||
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, {
|
||||
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, {
|
||||
})
|
||||
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 [];
|
||||
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));
|
||||
});
|
||||
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[]>();
|
||||
const map = new Map<string, LaneData[]>()
|
||||
if (championsLanes.value) {
|
||||
for (const champion of championsLanes.value as ChampionData[]) {
|
||||
map.set(champion.alias.toLowerCase(), champion.lanes);
|
||||
map.set(champion.alias.toLowerCase(), champion.lanes)
|
||||
}
|
||||
}
|
||||
return map;
|
||||
});
|
||||
return map
|
||||
})
|
||||
|
||||
// Filter state
|
||||
const filteredChampions = ref<ChampionSummary[]>([]);
|
||||
const searchText = ref("");
|
||||
const searchBar = useTemplateRef("searchBar");
|
||||
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] || "";
|
||||
const laneMap = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'UTILITY']
|
||||
return laneMap[filter] || ''
|
||||
}
|
||||
|
||||
function filterChampionsByLane(laneFilter: number): void {
|
||||
if (laneFilter === -1) {
|
||||
filteredChampions.value = [...champions.value];
|
||||
return;
|
||||
filteredChampions.value = [...champions.value]
|
||||
return
|
||||
}
|
||||
|
||||
const laneName = filterToLane(laneFilter);
|
||||
const laneName = filterToLane(laneFilter)
|
||||
filteredChampions.value = champions.value.filter((champion: any) => {
|
||||
const championLanes = lanesMap.value.get(champion.alias.toLowerCase());
|
||||
if (!championLanes) return false;
|
||||
const championLanes = lanesMap.value.get(champion.alias.toLowerCase())
|
||||
if (!championLanes) return false
|
||||
|
||||
return championLanes.some(lane => lane.data === laneName);
|
||||
});
|
||||
return championLanes.some(lane => lane.data === laneName)
|
||||
})
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
const debouncedSearch = debounce((searchTerm: string) => {
|
||||
if (isEmpty(searchTerm)) {
|
||||
filteredChampions.value = [...champions.value];
|
||||
filteredChampions.value = [...champions.value]
|
||||
} else {
|
||||
filteredChampions.value = champions.value.filter((champion: any) =>
|
||||
champion.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
)
|
||||
}
|
||||
}, 300);
|
||||
}, 300)
|
||||
|
||||
// Watchers
|
||||
watch(searchBar, (newS, oldS) => {
|
||||
searchBar.value?.focus();
|
||||
});
|
||||
searchBar.value?.focus()
|
||||
})
|
||||
|
||||
watch(searchText, (newTerm) => {
|
||||
debouncedSearch(newTerm);
|
||||
});
|
||||
watch(searchText, newTerm => {
|
||||
debouncedSearch(newTerm)
|
||||
})
|
||||
|
||||
// Watch for changes in champions data and update filtered champions
|
||||
watch(champions, (newChampions) => {
|
||||
filteredChampions.value = [...newChampions];
|
||||
}, { immediate: true });
|
||||
watch(
|
||||
champions,
|
||||
newChampions => {
|
||||
filteredChampions.value = [...newChampions]
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Navigation
|
||||
async function navigateToChampion(championAlias: string): Promise<void> {
|
||||
try {
|
||||
await navigateTo(`/champion/${championAlias.toLowerCase()}`);
|
||||
await navigateTo(`/champion/${championAlias.toLowerCase()}`)
|
||||
} catch (error) {
|
||||
console.error('Navigation error:', error);
|
||||
console.error('Navigation error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize filtered champions
|
||||
onMounted(() => {
|
||||
filteredChampions.value = [...champions.value];
|
||||
});
|
||||
filteredChampions.value = [...champions.value]
|
||||
})
|
||||
|
||||
// Error handling
|
||||
const hasErrors = computed(() => championsError.value || lanesError.value);
|
||||
const isLoading = computed(() => loadingChampions.value || loadingLanes.value);
|
||||
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"></div>
|
||||
<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
|
||||
@keyup.enter="() => filteredChampions.length > 0 && navigateToChampion(filteredChampions[0].alias)"
|
||||
v-model="searchText"
|
||||
ref="searchBar"
|
||||
class="search-bar"
|
||||
type="text"
|
||||
placeholder="Search a champion"
|
||||
/>
|
||||
</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>
|
||||
<!-- 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,
|
||||
.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;
|
||||
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;
|
||||
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); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state p {
|
||||
margin: 0;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
margin: 0;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.error-state {
|
||||
color: var(--color-error);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
opacity: 0.7;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 400px;
|
||||
height: 60px;
|
||||
width: 400px;
|
||||
height: 60px;
|
||||
|
||||
background-color: var(--color-surface-darker);
|
||||
|
||||
font-size: 1.75rem;
|
||||
background-color: var(--color-surface-darker);
|
||||
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
padding-left: 10px;
|
||||
font-size: 1.75rem;
|
||||
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.search-bar:focus {
|
||||
border: 2px solid var(--color-on-surface);
|
||||
outline: none;
|
||||
border: 2px solid var(--color-on-surface);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#cs-lanefilter {
|
||||
margin: auto;
|
||||
margin-right: 20px;
|
||||
margin: auto;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.search-lanefilter-container {
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.champion-container {
|
||||
width: 90%;
|
||||
height: auto;
|
||||
width: 90%;
|
||||
height: auto;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 128px);
|
||||
grid-gap: 10px;
|
||||
justify-content: center;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 128px);
|
||||
grid-gap: 10px;
|
||||
justify-content: center;
|
||||
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.cs-champion-img-container {
|
||||
overflow: hidden; width: 120px; height: 120px;
|
||||
border: 1px solid var(--color-surface);
|
||||
overflow: hidden;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 1px solid var(--color-surface);
|
||||
}
|
||||
.cs-champion-img-container:hover {
|
||||
border: 1px solid var(--color-on-surface);
|
||||
border: 1px solid var(--color-on-surface);
|
||||
}
|
||||
.cs-champion-img {
|
||||
width: 116px;
|
||||
height: 116px;
|
||||
transform: translate(4px, 4px) scale(1.2, 1.2);
|
||||
width: 116px;
|
||||
height: 116px;
|
||||
transform: translate(4px, 4px) scale(1.2, 1.2);
|
||||
|
||||
user-select: none;
|
||||
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%;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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>
|
||||
|
||||
@@ -1,98 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
championId: number,
|
||||
winrate: number,
|
||||
pickrate: number,
|
||||
championId: number
|
||||
winrate: number
|
||||
pickrate: number
|
||||
gameCount: number
|
||||
}>()
|
||||
|
||||
const winrate = ref((props.winrate * 100).toFixed(2))
|
||||
watch(() => props.winrate, () => {winrate.value = (props.winrate * 100).toFixed(2)})
|
||||
watch(
|
||||
() => props.winrate,
|
||||
() => {
|
||||
winrate.value = (props.winrate * 100).toFixed(2)
|
||||
}
|
||||
)
|
||||
const pickrate = ref((props.pickrate * 100).toFixed(2))
|
||||
watch(() => props.pickrate, () => {pickrate.value = (props.pickrate * 100).toFixed(2)})
|
||||
watch(
|
||||
() => props.pickrate,
|
||||
() => {
|
||||
pickrate.value = (props.pickrate * 100).toFixed(2)
|
||||
}
|
||||
)
|
||||
|
||||
const { data: championData } : ChampionResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/champions/" + props.championId + ".json")
|
||||
const { data: championData }: ChampionResponse = await useFetch(
|
||||
CDRAGON_BASE +
|
||||
'plugins/rcp-be-lol-game-data/global/default/v1/champions/' +
|
||||
props.championId +
|
||||
'.json'
|
||||
)
|
||||
const championName = championData.value.name
|
||||
const championDescription = championData.value.title
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="display: flex; width: fit-content;">
|
||||
|
||||
<div class="champion-title-img-container">
|
||||
<NuxtImg width="160" height="160" class="champion-title-img" :src="CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-icons/' + championId + '.png'"/>
|
||||
</div>
|
||||
|
||||
<div id="ct-info-container">
|
||||
<h1>{{ championName }}</h1>
|
||||
<h3 id="ct-desc">{{ championDescription }}</h3>
|
||||
|
||||
<div id="ct-basic-stat-container">
|
||||
<h2 class="ct-basic-stat">{{ winrate }}% win.</h2>
|
||||
<h2 class="ct-basic-stat ct-basic-stat-margin">{{ pickrate }}% pick.</h2>
|
||||
<h2 class="ct-basic-stat">{{ gameCount }} games</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; width: fit-content">
|
||||
<div class="champion-title-img-container">
|
||||
<NuxtImg
|
||||
width="160"
|
||||
height="160"
|
||||
class="champion-title-img"
|
||||
:src="
|
||||
CDRAGON_BASE +
|
||||
'plugins/rcp-be-lol-game-data/global/default/v1/champion-icons/' +
|
||||
championId +
|
||||
'.png'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="ct-info-container">
|
||||
<h1>{{ championName }}</h1>
|
||||
<h3 id="ct-desc">{{ championDescription }}</h3>
|
||||
|
||||
<div id="ct-basic-stat-container">
|
||||
<h2 class="ct-basic-stat">{{ winrate }}% win.</h2>
|
||||
<h2 class="ct-basic-stat ct-basic-stat-margin">{{ pickrate }}% pick.</h2>
|
||||
<h2 class="ct-basic-stat">{{ gameCount }} games</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.champion-title-img-container {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
|
||||
border: 1px solid var(--color-on-surface);
|
||||
border: 1px solid var(--color-on-surface);
|
||||
}
|
||||
.champion-title-img {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
transform: translate(4px, 4px) scale(1.2, 1.2);
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
transform: translate(4px, 4px) scale(1.2, 1.2);
|
||||
|
||||
user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
#ct-info-container {
|
||||
margin-left: 15px;
|
||||
margin-top: 5px;
|
||||
margin-left: 15px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.ct-basic-stat-margin {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
#ct-desc {
|
||||
margin-top: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
#ct-basic-stat-container {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 650px) {
|
||||
.champion-title-img-container {
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
}
|
||||
.champion-title-img {
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
}
|
||||
#ct-desc {
|
||||
display: none;
|
||||
}
|
||||
.ct-basic-stat {
|
||||
text-align: center;
|
||||
}
|
||||
.ct-basic-stat-margin {
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
#ct-basic-stat-container {
|
||||
margin-top: 10px;
|
||||
}
|
||||
#ct-info-container {
|
||||
margin-left: 10px;
|
||||
margin-top: 0px;
|
||||
max-width: 220px;
|
||||
}
|
||||
.champion-title-img-container {
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
}
|
||||
.champion-title-img {
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
}
|
||||
#ct-desc {
|
||||
display: none;
|
||||
}
|
||||
.ct-basic-stat {
|
||||
text-align: center;
|
||||
}
|
||||
.ct-basic-stat-margin {
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
#ct-basic-stat-container {
|
||||
margin-top: 10px;
|
||||
}
|
||||
#ct-info-container {
|
||||
margin-left: 10px;
|
||||
margin-top: 0px;
|
||||
max-width: 220px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,67 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { LANE_IMAGES, LANE_IMAGES_HOVER, LANE_IMAGES_SELECTED, POSITIONS_STR } from '~/utils/cdragon';
|
||||
import {
|
||||
LANE_IMAGES,
|
||||
LANE_IMAGES_HOVER,
|
||||
LANE_IMAGES_SELECTED,
|
||||
POSITIONS_STR
|
||||
} from '~/utils/cdragon'
|
||||
|
||||
const emit = defineEmits<{
|
||||
filterChange: [value: number]
|
||||
filterChange: [value: number]
|
||||
}>()
|
||||
|
||||
const laneImgs = Array(5).fill(ref("")).map((_, index) => ref(LANE_IMAGES[index]))
|
||||
const laneImgs = Array(5)
|
||||
.fill(ref(''))
|
||||
.map((_, index) => ref(LANE_IMAGES[index]))
|
||||
const laneFilter = ref(-1)
|
||||
|
||||
function selectLaneFilter(index: number) {
|
||||
// Unselect previous filter
|
||||
if(laneFilter.value != -1) {
|
||||
laneImgs[laneFilter.value].value = LANE_IMAGES[laneFilter.value]
|
||||
// Unselect previous filter
|
||||
if (laneFilter.value != -1) {
|
||||
laneImgs[laneFilter.value].value = LANE_IMAGES[laneFilter.value]
|
||||
|
||||
// This is a deselection.
|
||||
if(laneFilter.value == index) {
|
||||
laneFilter.value = -1;
|
||||
emit('filterChange', laneFilter.value)
|
||||
return;
|
||||
}
|
||||
// This is a deselection.
|
||||
if (laneFilter.value == index) {
|
||||
laneFilter.value = -1
|
||||
emit('filterChange', laneFilter.value)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Select new one
|
||||
laneImgs[index].value = LANE_IMAGES_SELECTED[index]
|
||||
laneFilter.value = index
|
||||
emit('filterChange', laneFilter.value)
|
||||
// Select new one
|
||||
laneImgs[index].value = LANE_IMAGES_SELECTED[index]
|
||||
laneFilter.value = index
|
||||
emit('filterChange', laneFilter.value)
|
||||
}
|
||||
|
||||
function handleMouseOut(laneImg: Ref<string>, index: number) {
|
||||
if(laneImg.value == LANE_IMAGES_HOVER[index])
|
||||
laneImg.value = LANE_IMAGES[index]
|
||||
if (laneImg.value == LANE_IMAGES_HOVER[index]) laneImg.value = LANE_IMAGES[index]
|
||||
}
|
||||
function handleHover(laneImg: Ref<string>, index: number) {
|
||||
if(laneImg.value == LANE_IMAGES[index])
|
||||
laneImg.value = LANE_IMAGES_HOVER[index]
|
||||
if (laneImg.value == LANE_IMAGES[index]) laneImg.value = LANE_IMAGES_HOVER[index]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: fit-content;">
|
||||
<NuxtImg v-for="(laneImg, index) in laneImgs"
|
||||
format="webp"
|
||||
:alt="POSITIONS_STR[index]"
|
||||
class="lane-img" :src="laneImg.value"
|
||||
@mouseout="handleMouseOut(laneImg, index)"
|
||||
@mouseover="handleHover(laneImg, index)"
|
||||
@click="selectLaneFilter(index)"/>
|
||||
</div>
|
||||
<div style="width: fit-content">
|
||||
<NuxtImg
|
||||
v-for="(laneImg, index) in laneImgs"
|
||||
format="webp"
|
||||
:alt="POSITIONS_STR[index]"
|
||||
class="lane-img"
|
||||
:src="laneImg.value"
|
||||
@mouseout="handleMouseOut(laneImg, index)"
|
||||
@mouseover="handleHover(laneImg, index)"
|
||||
@click="selectLaneFilter(index)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.lane-img {
|
||||
width: 64px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
width: 64px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.lane-img:hover {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 450px) {
|
||||
.lane-img {
|
||||
width: 48px;
|
||||
}
|
||||
.lane-img {
|
||||
width: 48px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,44 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
imgWidth?: String,
|
||||
fontSize?: String
|
||||
imgWidth?: string
|
||||
fontSize?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: fit-content; max-width: 100%; overflow: hidden;">
|
||||
<NuxtLink style="display: flex; width: fit-content; text-decoration: none;" to="/">
|
||||
<NuxtImg id="logo-img" alt="BuildPath"
|
||||
format="webp"
|
||||
:width="imgWidth == null ? '120' : Number(imgWidth)"
|
||||
src="/buildpath-high-resolution-logo-transparent.png" />
|
||||
<h1 :style="'font-size: ' + (fontSize == null ? '5.0rem' : fontSize) + ';'" id="logo-text">BuildPath</h1>
|
||||
<div style="width: fit-content; max-width: 100%; overflow: hidden">
|
||||
<NuxtLink style="display: flex; width: fit-content; text-decoration: none" to="/">
|
||||
<NuxtImg
|
||||
id="logo-img"
|
||||
alt="BuildPath"
|
||||
format="webp"
|
||||
:width="imgWidth == null ? '120' : Number(imgWidth)"
|
||||
src="/buildpath-high-resolution-logo-transparent.png"
|
||||
/>
|
||||
<h1 id="logo-text" :style="'font-size: ' + (fontSize == null ? '5.0rem' : fontSize) + ';'">
|
||||
BuildPath
|
||||
</h1>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#logo-text {
|
||||
font-weight: 200;
|
||||
align-content: center;
|
||||
font-weight: 200;
|
||||
align-content: center;
|
||||
|
||||
margin-left: 20px;
|
||||
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
margin-left: 20px;
|
||||
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
#logo-img {
|
||||
user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 550px) {
|
||||
#logo-text {
|
||||
margin-left: 10px;
|
||||
}
|
||||
#logo-img {
|
||||
max-width: 80px;
|
||||
max-height: 103px;
|
||||
height: fit-content;
|
||||
}
|
||||
#logo-text {
|
||||
margin-left: 10px;
|
||||
}
|
||||
#logo-img {
|
||||
max-width: 80px;
|
||||
max-height: 103px;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string,
|
||||
id: number,
|
||||
winrate: number,
|
||||
pickrate: number,
|
||||
title: string
|
||||
id: number
|
||||
winrate: number
|
||||
pickrate: number
|
||||
gameCount: number
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -23,13 +22,23 @@ defineProps<{
|
||||
</div>
|
||||
<div class="text-[200px]">
|
||||
<!-- Champion image -->
|
||||
<div class="my-auto ml-10"
|
||||
style="overflow: hidden; width: 220px; height: 220px; border: 1px solid #B7B8E1;">
|
||||
<NuxtImg width="216px" height="216px"
|
||||
class="object-cover"
|
||||
style="transform: translate(4px, 4px) scale(1.2, 1.2); width: 216px; height: 216px;"
|
||||
:src="CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-icons/' + id + '.png'" />
|
||||
</div>
|
||||
<div
|
||||
class="my-auto ml-10"
|
||||
style="overflow: hidden; width: 220px; height: 220px; border: 1px solid #b7b8e1"
|
||||
>
|
||||
<NuxtImg
|
||||
width="216px"
|
||||
height="216px"
|
||||
class="object-cover"
|
||||
style="transform: translate(4px, 4px) scale(1.2, 1.2); width: 216px; height: 216px"
|
||||
:src="
|
||||
CDRAGON_BASE +
|
||||
'plugins/rcp-be-lol-game-data/global/default/v1/champion-icons/' +
|
||||
id +
|
||||
'.png'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
@@ -38,27 +47,21 @@ defineProps<{
|
||||
<!-- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"><path fill="#888888" d="m4.67 28l6.39-12l7.3 6.49a2 2 0 0 0 1.7.47a2 2 0 0 0 1.42-1.07L27 10.9l-1.82-.9l-5.49 11l-7.3-6.49a2 2 0 0 0-1.68-.51a2 2 0 0 0-1.42 1L4 25V2H2v26a2 2 0 0 0 2 2h26v-2Z" /></svg> -->
|
||||
<div class="pl-2">
|
||||
<div class="text-[#B7B8E1]">{{ (winrate * 100).toFixed(2) }}%</div>
|
||||
<div class="text-lg text-[#B7B8E1]">
|
||||
Winrate
|
||||
</div>
|
||||
<div class="text-lg text-[#B7B8E1]">Winrate</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row pr-10">
|
||||
<!-- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"><path fill="#888888" d="m16 6.52l2.76 5.58l.46 1l1 .15l6.16.89l-4.38 4.3l-.75.73l.18 1l1.05 6.13l-5.51-2.89L16 23l-.93.49l-5.51 2.85l1-6.13l.18-1l-.74-.77l-4.42-4.35l6.16-.89l1-.15l.46-1L16 6.52M16 2l-4.55 9.22l-10.17 1.47l7.36 7.18L6.9 30l9.1-4.78L25.1 30l-1.74-10.13l7.36-7.17l-10.17-1.48Z" /></svg> -->
|
||||
<div class="pl-2">
|
||||
<div class="text-[#B7B8E1]">{{ (pickrate * 100).toFixed(2) }}%</div>
|
||||
<div class="text-lg text-[#B7B8E1]">
|
||||
Pickrate
|
||||
</div>
|
||||
<div class="text-lg text-[#B7B8E1]">Pickrate</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row pr-10">
|
||||
<!-- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"><path fill="#888888" d="M22.45 6a5.47 5.47 0 0 1 3.91 1.64a5.7 5.7 0 0 1 0 8L16 26.13L5.64 15.64a5.7 5.7 0 0 1 0-8a5.48 5.48 0 0 1 7.82 0l2.54 2.6l2.53-2.58A5.44 5.44 0 0 1 22.45 6m0-2a7.47 7.47 0 0 0-5.34 2.24L16 7.36l-1.11-1.12a7.49 7.49 0 0 0-10.68 0a7.72 7.72 0 0 0 0 10.82L16 29l11.79-11.94a7.72 7.72 0 0 0 0-10.82A7.49 7.49 0 0 0 22.45 4Z" /></svg> -->
|
||||
<div class="pl-2">
|
||||
<div class="text-[#B7B8E1]">{{ gameCount }}</div>
|
||||
<div class="text-lg text-[#B7B8E1]">
|
||||
Games
|
||||
</div>
|
||||
<div class="text-lg text-[#B7B8E1]">Games</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,4 +89,3 @@ defineProps<{
|
||||
<div class="absolute bottom-0 w-full h-8 bg-[#B7B8E1]" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,41 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
title: string,
|
||||
bootsFirst?: number,
|
||||
sizePerc?: number
|
||||
title: string
|
||||
bootsFirst?: number
|
||||
sizePerc?: number
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="(sizePerc != undefined && sizePerc != null) ? 'max-height: ' + (sizePerc * 600) + 'px;' : ''" class="item-box">
|
||||
<div style="display:flex; flex-direction: column; justify-content: center; align-items: center;">
|
||||
<h2 class="item-box-title">{{ title }}</h2>
|
||||
<h5 v-if="bootsFirst != undefined && bootsFirst != null"
|
||||
style="margin: auto;">({{ (bootsFirst * 100).toFixed(2) }}%)</h5>
|
||||
<div
|
||||
:style="
|
||||
sizePerc != undefined && sizePerc != null ? 'max-height: ' + sizePerc * 600 + 'px;' : ''
|
||||
"
|
||||
class="item-box"
|
||||
>
|
||||
<div
|
||||
style="display: flex; flex-direction: column; justify-content: center; align-items: center"
|
||||
>
|
||||
<h2 class="item-box-title">{{ title }}</h2>
|
||||
<h5 v-if="bootsFirst != undefined && bootsFirst != null" style="margin: auto">
|
||||
({{ (bootsFirst * 100).toFixed(2) }}%)
|
||||
</h5>
|
||||
</div>
|
||||
<slot/>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item-box {
|
||||
border: 1px solid var(--color-on-surface);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-on-surface);
|
||||
border-radius: 8px;
|
||||
|
||||
margin: 10px;
|
||||
margin: 10px;
|
||||
|
||||
width: fit-content;
|
||||
height: 600px;
|
||||
width: fit-content;
|
||||
height: 600px;
|
||||
}
|
||||
.item-box-title {
|
||||
font-variant: small-caps;
|
||||
text-align: center;
|
||||
margin: 10px;
|
||||
font-variant: small-caps;
|
||||
text-align: center;
|
||||
margin: 10px;
|
||||
}
|
||||
@media only screen and (max-width: 1000px) {
|
||||
.item-box {
|
||||
width: 95%;
|
||||
height: fit-content;
|
||||
}
|
||||
.item-box {
|
||||
width: 95%;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,112 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
import svgdomarrows from 'svg-dom-arrows'
|
||||
|
||||
defineProps<{
|
||||
tree: ItemTree,
|
||||
parentCount?: number
|
||||
tree: ItemTree
|
||||
parentCount?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
mount: [end: Element],
|
||||
refresh: []
|
||||
mount: [end: Element]
|
||||
refresh: []
|
||||
}>()
|
||||
|
||||
const {data : items} : ItemResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/items.json")
|
||||
const { data: items }: ItemResponse = await useFetch(
|
||||
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/items.json'
|
||||
)
|
||||
const itemMap = reactive(new Map())
|
||||
for(let item of items.value) {
|
||||
itemMap.set(item.id, item)
|
||||
for (const item of items.value) {
|
||||
itemMap.set(item.id, item)
|
||||
}
|
||||
|
||||
import svgdomarrows from 'svg-dom-arrows';
|
||||
|
||||
const start : Ref<Element | null> = useTemplateRef("start")
|
||||
const arrows : Array<svgdomarrows.LinePath> = []
|
||||
const start: Ref<Element | null> = useTemplateRef('start')
|
||||
const arrows: Array<svgdomarrows.LinePath> = []
|
||||
|
||||
onMounted(() => {
|
||||
refreshArrows()
|
||||
emit('mount', start.value!!)
|
||||
refreshArrows()
|
||||
emit('mount', start.value!)
|
||||
})
|
||||
|
||||
onBeforeUpdate(() => {
|
||||
for(let arrow of arrows) {
|
||||
arrow.release()
|
||||
}
|
||||
arrows.splice(0, arrows.length)
|
||||
for (const arrow of arrows) {
|
||||
arrow.release()
|
||||
}
|
||||
arrows.splice(0, arrows.length)
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
refreshArrows()
|
||||
emit('mount', start.value!!)
|
||||
refreshArrows()
|
||||
emit('mount', start.value!)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
for(let arrow of arrows) {
|
||||
arrow.release()
|
||||
}
|
||||
for (const arrow of arrows) {
|
||||
arrow.release()
|
||||
}
|
||||
})
|
||||
|
||||
function drawArrow(start : Element, end : Element) {
|
||||
// console.log("drawArrow(", start, ", ", end, ")")
|
||||
if(start == null || end == null) return;
|
||||
function drawArrow(start: Element, end: Element) {
|
||||
// console.log("drawArrow(", start, ", ", end, ")")
|
||||
if (start == null || end == null) return
|
||||
|
||||
const arrow = new svgdomarrows.LinePath({
|
||||
start: {
|
||||
element: start,
|
||||
position: {
|
||||
top: 0.5,
|
||||
left: 1
|
||||
}
|
||||
},
|
||||
end: {
|
||||
element: end,
|
||||
position: {
|
||||
top: 0.5,
|
||||
left: 0
|
||||
}
|
||||
},
|
||||
style: 'stroke:var(--color-on-surface);stroke-width:3;fill:transparent;',
|
||||
appendTo: document.body
|
||||
})
|
||||
arrows.push(arrow)
|
||||
const arrow = new svgdomarrows.LinePath({
|
||||
start: {
|
||||
element: start,
|
||||
position: {
|
||||
top: 0.5,
|
||||
left: 1
|
||||
}
|
||||
},
|
||||
end: {
|
||||
element: end,
|
||||
position: {
|
||||
top: 0.5,
|
||||
left: 0
|
||||
}
|
||||
},
|
||||
style: 'stroke:var(--color-on-surface);stroke-width:3;fill:transparent;',
|
||||
appendTo: document.body
|
||||
})
|
||||
arrows.push(arrow)
|
||||
}
|
||||
|
||||
function refreshArrows() {
|
||||
for(let arrow of arrows) {
|
||||
arrow.redraw()
|
||||
}
|
||||
for (const arrow of arrows) {
|
||||
arrow.redraw()
|
||||
}
|
||||
}
|
||||
|
||||
// Redraw arrows on window resize
|
||||
addEventListener('resize', (_) => {
|
||||
refreshArrows()
|
||||
addEventListener('resize', _ => {
|
||||
refreshArrows()
|
||||
})
|
||||
addEventListener("scroll", (_) => {
|
||||
refreshArrows()
|
||||
addEventListener('scroll', _ => {
|
||||
refreshArrows()
|
||||
})
|
||||
|
||||
function handleSubtreeMount(end : Element) {
|
||||
drawArrow(start.value!!, end)
|
||||
refreshArrows()
|
||||
emit('refresh')
|
||||
function handleSubtreeMount(end: Element) {
|
||||
drawArrow(start.value!, end)
|
||||
refreshArrows()
|
||||
emit('refresh')
|
||||
}
|
||||
function handleRefresh() {
|
||||
refreshArrows()
|
||||
emit('refresh')
|
||||
refreshArrows()
|
||||
emit('refresh')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="display: flex; align-items: center;">
|
||||
|
||||
<div v-if="tree.data != undefined && tree.data != null" style="width: fit-content; height: fit-content;">
|
||||
<img ref="start" class="item-img" width="64" height="64"
|
||||
:alt="tree.data.toString()"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(tree.data).iconPath)" />
|
||||
<h3 style="width: fit-content; margin:auto; margin-bottom: 10px;">{{ ((tree.count / parentCount!!) * 100).toFixed(0) }}%</h3>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 30px;">
|
||||
<div style="width: fit-content; height: fit-content;" v-for="child in tree.children">
|
||||
<ItemTree @refresh="handleRefresh" @mount="(end) => handleSubtreeMount(end)" :tree="child" :parent-count="tree.count" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<div
|
||||
v-if="tree.data != undefined && tree.data != null"
|
||||
style="width: fit-content; height: fit-content"
|
||||
>
|
||||
<img
|
||||
ref="start"
|
||||
class="item-img"
|
||||
width="64"
|
||||
height="64"
|
||||
:alt="tree.data.toString()"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(tree.data).iconPath)"
|
||||
>
|
||||
<h3 style="width: fit-content; margin: auto; margin-bottom: 10px">
|
||||
{{ ((tree.count / parentCount!!) * 100).toFixed(0) }}%
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 30px">
|
||||
<div v-for="child in tree.children" style="width: fit-content; height: fit-content">
|
||||
<ItemTree
|
||||
:tree="child"
|
||||
:parent-count="tree.count"
|
||||
@refresh="handleRefresh"
|
||||
@mount="end => handleSubtreeMount(end)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,64 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { isEmpty, deepClone } from '~/utils/helpers';
|
||||
import { isEmpty, deepClone } from '~/utils/helpers'
|
||||
|
||||
const props = defineProps<{
|
||||
builds: Builds;
|
||||
loading?: boolean;
|
||||
error?: boolean;
|
||||
}>();
|
||||
builds: Builds
|
||||
loading?: boolean
|
||||
error?: boolean
|
||||
}>()
|
||||
|
||||
// Constants
|
||||
const ITEMS_API_URL = CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/items.json";
|
||||
const ITEMS_API_URL = CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/items.json'
|
||||
|
||||
// State
|
||||
const { data: items, pending: loadingItems, error: itemsError } = await useFetch(ITEMS_API_URL);
|
||||
const itemMap = ref<Map<number, any>>(new Map());
|
||||
const { data: items, pending: loadingItems, error: itemsError } = await useFetch(ITEMS_API_URL)
|
||||
const itemMap = ref<Map<number, any>>(new Map())
|
||||
|
||||
// Initialize item map
|
||||
watch(items, (newItems) => {
|
||||
try {
|
||||
const itemsData = newItems || [];
|
||||
if (Array.isArray(itemsData)) {
|
||||
const map = new Map<number, any>();
|
||||
for (const item of itemsData) {
|
||||
if (item?.id) {
|
||||
map.set(item.id, item);
|
||||
watch(
|
||||
items,
|
||||
newItems => {
|
||||
try {
|
||||
const itemsData = newItems || []
|
||||
if (Array.isArray(itemsData)) {
|
||||
const map = new Map<number, any>()
|
||||
for (const item of itemsData) {
|
||||
if (item?.id) {
|
||||
map.set(item.id, item)
|
||||
}
|
||||
}
|
||||
itemMap.value = map
|
||||
}
|
||||
itemMap.value = map;
|
||||
} catch (error) {
|
||||
console.error('Error initializing item map:', error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing item map:', error);
|
||||
}
|
||||
}, { immediate: true });
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Builds management
|
||||
const builds = ref<Builds>(deepClone(props.builds));
|
||||
const builds = ref<Builds>(deepClone(props.builds))
|
||||
|
||||
watch(() => props.builds, (newBuilds) => {
|
||||
builds.value = deepClone(newBuilds);
|
||||
trimBuilds(builds.value);
|
||||
trimLateGameItems(builds.value);
|
||||
}, { deep: true });
|
||||
watch(
|
||||
() => props.builds,
|
||||
newBuilds => {
|
||||
builds.value = deepClone(newBuilds)
|
||||
trimBuilds(builds.value)
|
||||
trimLateGameItems(builds.value)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Initialize with trimmed builds
|
||||
onMounted(() => {
|
||||
trimBuilds(builds.value);
|
||||
trimLateGameItems(builds.value);
|
||||
});
|
||||
trimBuilds(builds.value)
|
||||
trimLateGameItems(builds.value)
|
||||
})
|
||||
|
||||
/**
|
||||
* Trim builds tree to show only primary build paths
|
||||
*/
|
||||
function trimBuilds(builds: Builds): void {
|
||||
if (!builds?.tree?.children) return;
|
||||
if (!builds?.tree?.children) return
|
||||
|
||||
// Keep only the first child (primary build path)
|
||||
builds.tree.children.splice(1, builds.tree.children.length - 1);
|
||||
builds.tree.children.splice(1, builds.tree.children.length - 1)
|
||||
|
||||
// For the primary path, keep only the first child of the first child
|
||||
if (builds.tree.children[0]?.children) {
|
||||
builds.tree.children[0].children.splice(1, builds.tree.children[0].children.length - 1);
|
||||
builds.tree.children[0].children.splice(1, builds.tree.children[0].children.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,161 +74,206 @@ function trimBuilds(builds: Builds): void {
|
||||
* Remove items from lateGame that are already in the build tree
|
||||
*/
|
||||
function trimLateGameItems(builds: Builds): void {
|
||||
if (!builds?.tree || isEmpty(builds.lateGame)) return;
|
||||
if (!builds?.tree || isEmpty(builds.lateGame)) return
|
||||
|
||||
function trimLateGameItemsFromTree(tree: ItemTree): void {
|
||||
const foundIndex = builds.lateGame.findIndex((x) => x.data === tree.data);
|
||||
const foundIndex = builds.lateGame.findIndex(x => x.data === tree.data)
|
||||
if (foundIndex !== -1) {
|
||||
builds.lateGame.splice(foundIndex, 1);
|
||||
builds.lateGame.splice(foundIndex, 1)
|
||||
}
|
||||
|
||||
for (const child of tree.children || []) {
|
||||
trimLateGameItemsFromTree(child);
|
||||
trimLateGameItemsFromTree(child)
|
||||
}
|
||||
}
|
||||
|
||||
trimLateGameItemsFromTree(builds.tree);
|
||||
trimLateGameItemsFromTree(builds.tree)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item data safely
|
||||
*/
|
||||
function getItemData(itemId: number): any {
|
||||
return itemMap.value.get(itemId) || { iconPath: '' };
|
||||
return itemMap.value.get(itemId) || { iconPath: '' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate percentage for item display
|
||||
*/
|
||||
function getItemPercentage(item: { count: number }, total: number): string {
|
||||
if (total <= 0) return '0%';
|
||||
return ((item.count / total) * 100).toFixed(0) + '%';
|
||||
if (total <= 0) return '0%'
|
||||
return ((item.count / total) * 100).toFixed(0) + '%'
|
||||
}
|
||||
|
||||
// Error and loading states
|
||||
const hasError = computed(() => itemsError.value || props.error);
|
||||
const isLoading = computed(() => loadingItems.value || props.loading);
|
||||
const hasError = computed(() => itemsError.value || props.error)
|
||||
const isLoading = computed(() => loadingItems.value || props.loading)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="iv-container">
|
||||
|
||||
<div>
|
||||
<!-- Start items -->
|
||||
<ItemBox title="start" v-if="builds.suppItems == undefined || builds.suppItems == null">
|
||||
<div class="iv-items-container">
|
||||
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.start" >
|
||||
<NuxtImg v-if="item.data != null && item.data != undefined"
|
||||
class="item-img" width="64" height="64" :alt="item.data.toString()"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)" />
|
||||
<h3 style="width: fit-content; margin:auto; margin-bottom: 10px;">{{ (item.count/builds.tree.count * 100).toFixed(0) }}%</h3>
|
||||
</div>
|
||||
</div>
|
||||
</ItemBox>
|
||||
<!-- Supp items -->
|
||||
<ItemBox v-if="builds.suppItems != undefined && builds.suppItems != null" title="supp">
|
||||
<div class="iv-items-container">
|
||||
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.suppItems" >
|
||||
<NuxtImg v-if="item.data != null && item.data != undefined"
|
||||
class="item-img" width="64" height="64" :alt="item.data.toString()"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)" />
|
||||
<h3 style="width: fit-content; margin:auto; margin-bottom: 10px;">{{ (item.count/builds.tree.count * 100).toFixed(0) }}%</h3>
|
||||
</div>
|
||||
</div>
|
||||
</ItemBox>
|
||||
<div id="iv-container">
|
||||
<div>
|
||||
<!-- Start items -->
|
||||
<ItemBox v-if="builds.suppItems == undefined || builds.suppItems == null" title="start">
|
||||
<div class="iv-items-container">
|
||||
<div v-for="item in builds.start" style="margin-left: 5px; margin-right: 5px">
|
||||
<NuxtImg
|
||||
v-if="item.data != null && item.data != undefined"
|
||||
class="item-img"
|
||||
width="64"
|
||||
height="64"
|
||||
:alt="item.data.toString()"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)"
|
||||
/>
|
||||
<h3 style="width: fit-content; margin: auto; margin-bottom: 10px">
|
||||
{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</ItemBox>
|
||||
<!-- Supp items -->
|
||||
<ItemBox v-if="builds.suppItems != undefined && builds.suppItems != null" title="supp">
|
||||
<div class="iv-items-container">
|
||||
<div v-for="item in builds.suppItems" style="margin-left: 5px; margin-right: 5px">
|
||||
<NuxtImg
|
||||
v-if="item.data != null && item.data != undefined"
|
||||
class="item-img"
|
||||
width="64"
|
||||
height="64"
|
||||
:alt="item.data.toString()"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)"
|
||||
/>
|
||||
<h3 style="width: fit-content; margin: auto; margin-bottom: 10px">
|
||||
{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</ItemBox>
|
||||
</div>
|
||||
|
||||
<!-- Boots first : when champion rush boots -->
|
||||
<ItemBox v-if="builds.bootsFirst > 0.5" title="boots rush" :boots-first="builds.bootsFirst">
|
||||
<div class="iv-items-container">
|
||||
<div v-for="item in builds.boots" style="margin-left: 5px; margin-right: 5px">
|
||||
<NuxtImg
|
||||
v-if="item.data != null && item.data != undefined"
|
||||
class="item-img"
|
||||
width="64"
|
||||
height="64"
|
||||
:alt="item.data.toString()"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)"
|
||||
/>
|
||||
<h3 style="width: fit-content; margin: auto; margin-bottom: 10px">
|
||||
{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</ItemBox>
|
||||
|
||||
<!-- Core items -->
|
||||
<ItemBox title="core">
|
||||
<ItemTree style="margin: auto; width: fit-content" :tree="builds.tree" />
|
||||
</ItemBox>
|
||||
|
||||
<!-- Boots -->
|
||||
<ItemBox v-if="builds.bootsFirst <= 0.5" title="boots">
|
||||
<div class="iv-items-container">
|
||||
<div v-for="item in builds.boots.slice(0, 4)" style="margin-left: 5px; margin-right: 5px">
|
||||
<NuxtImg
|
||||
v-if="item.data != null && item.data != undefined"
|
||||
class="item-img"
|
||||
width="64"
|
||||
height="64"
|
||||
:alt="item.data.toString()"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)"
|
||||
/>
|
||||
<h3 style="width: fit-content; margin: auto; margin-bottom: 10px">
|
||||
{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</ItemBox>
|
||||
|
||||
<!-- Late game items -->
|
||||
<ItemBox title="late game">
|
||||
<div id="iv-late-game-container">
|
||||
<div class="iv-items-container">
|
||||
<div
|
||||
v-for="item in builds.lateGame.slice(0, 4)"
|
||||
style="margin-left: 5px; margin-right: 5px"
|
||||
>
|
||||
<NuxtImg
|
||||
v-if="item.data != null && item.data != undefined"
|
||||
class="item-img"
|
||||
width="64"
|
||||
height="64"
|
||||
:alt="item.data.toString()"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)"
|
||||
/>
|
||||
<h3 style="width: fit-content; margin: auto; margin-bottom: 10px">
|
||||
{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Boots first : when champion rush boots -->
|
||||
<ItemBox v-if="builds.bootsFirst > 0.5" title="boots rush" :bootsFirst="builds.bootsFirst">
|
||||
<div class="iv-items-container">
|
||||
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.boots" >
|
||||
<NuxtImg v-if="item.data != null && item.data != undefined"
|
||||
class="item-img" width="64" height="64" :alt="item.data.toString()"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)" />
|
||||
<h3 style="width: fit-content; margin:auto; margin-bottom: 10px;">{{ (item.count/builds.tree.count * 100).toFixed(0) }}%</h3>
|
||||
</div>
|
||||
</div>
|
||||
</ItemBox>
|
||||
|
||||
<!-- Core items -->
|
||||
<ItemBox title="core">
|
||||
<ItemTree style="margin:auto; width: fit-content;" :tree="builds.tree" />
|
||||
</ItemBox>
|
||||
|
||||
<!-- Boots -->
|
||||
<ItemBox v-if="builds.bootsFirst <= 0.5" title="boots">
|
||||
<div class="iv-items-container">
|
||||
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.boots.slice(0, 4)" >
|
||||
<NuxtImg v-if="item.data != null && item.data != undefined"
|
||||
class="item-img" width="64" height="64" :alt="item.data.toString()"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)" />
|
||||
<h3 style="width: fit-content; margin:auto; margin-bottom: 10px;">{{ (item.count/builds.tree.count * 100).toFixed(0) }}%</h3>
|
||||
</div>
|
||||
</div>
|
||||
</ItemBox>
|
||||
|
||||
<!-- Late game items -->
|
||||
<ItemBox title="late game">
|
||||
<div id="iv-late-game-container">
|
||||
|
||||
<div class="iv-items-container">
|
||||
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.lateGame.slice(0, 4)" >
|
||||
<NuxtImg v-if="item.data != null && item.data != undefined"
|
||||
class="item-img" width="64" height="64" :alt="item.data.toString()"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)" />
|
||||
<h3 style="width: fit-content; margin:auto; margin-bottom: 10px;">{{ (item.count/builds.tree.count * 100).toFixed(0) }}%</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="iv-items-container" v-if="builds.lateGame.length > 4">
|
||||
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.lateGame.slice(4, 8)" >
|
||||
<NuxtImg v-if="item.data != null && item.data != undefined"
|
||||
class="item-img" width="64" height="64" :alt="item.data.toString()"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)" />
|
||||
<h3 style="width: fit-content; margin:auto; margin-bottom: 10px;">{{ (item.count/builds.tree.count * 100).toFixed(0) }}%</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ItemBox>
|
||||
</div>
|
||||
<div v-if="builds.lateGame.length > 4" class="iv-items-container">
|
||||
<div
|
||||
v-for="item in builds.lateGame.slice(4, 8)"
|
||||
style="margin-left: 5px; margin-right: 5px"
|
||||
>
|
||||
<NuxtImg
|
||||
v-if="item.data != null && item.data != undefined"
|
||||
class="item-img"
|
||||
width="64"
|
||||
height="64"
|
||||
:alt="item.data.toString()"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)"
|
||||
/>
|
||||
<h3 style="width: fit-content; margin: auto; margin-bottom: 10px">
|
||||
{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ItemBox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#iv-container {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
}
|
||||
.iv-items-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
margin:auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
margin: auto;
|
||||
}
|
||||
.item-img {
|
||||
border: 1px solid var(--color-on-surface);
|
||||
margin: 10px;
|
||||
border: 1px solid var(--color-on-surface);
|
||||
margin: 10px;
|
||||
}
|
||||
#iv-late-game-container {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1000px) {
|
||||
#iv-container {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
.iv-items-container {
|
||||
flex-direction: row;
|
||||
}
|
||||
.item-img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
#iv-late-game-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
#iv-container {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
.iv-items-container {
|
||||
flex-direction: row;
|
||||
}
|
||||
.item-img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
#iv-late-game-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
championName?: string
|
||||
championLanes?: Array<LaneData>
|
||||
tierlistList?: boolean
|
||||
championName?: string
|
||||
championLanes?: Array<LaneData>
|
||||
tierlistList?: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
stateChange: [state: string, lane: number]
|
||||
stateChange: [state: string, lane: number]
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LazyNavSideBar :champion-name="championName"
|
||||
:champion-lanes="championLanes"
|
||||
:tierlist-list="tierlistList"
|
||||
@state-change="(s, l) => emit('stateChange', s, l)"/>
|
||||
<LazyNavBottomBar :champion-name="championName"
|
||||
:champion-lanes="championLanes"
|
||||
:tierlist-list="tierlistList"
|
||||
@state-change="(s, l) => emit('stateChange', s, l)"/>
|
||||
</template>
|
||||
<LazyNavSideBar
|
||||
:champion-name="championName"
|
||||
:champion-lanes="championLanes"
|
||||
:tierlist-list="tierlistList"
|
||||
@state-change="(s, l) => emit('stateChange', s, l)"
|
||||
/>
|
||||
<LazyNavBottomBar
|
||||
:champion-name="championName"
|
||||
:champion-lanes="championLanes"
|
||||
:tierlist-list="tierlistList"
|
||||
@state-change="(s, l) => emit('stateChange', s, l)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,111 +1,145 @@
|
||||
<script setup lang="ts">
|
||||
import { LANE_IMAGES, lanePositionToIndex, POSITIONS_STR } from '~/utils/cdragon';
|
||||
import { LANE_IMAGES, lanePositionToIndex, POSITIONS_STR } from '~/utils/cdragon'
|
||||
|
||||
defineProps<{
|
||||
championName?: string
|
||||
championLanes?: Array<LaneData>
|
||||
tierlistList?: boolean
|
||||
championName?: string
|
||||
championLanes?: Array<LaneData>
|
||||
tierlistList?: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
stateChange: [state: string, lane: number]
|
||||
stateChange: [state: string, lane: number]
|
||||
}>()
|
||||
|
||||
const state = ref("runes")
|
||||
const state = ref('runes')
|
||||
const laneState = ref(0)
|
||||
|
||||
function handleStateChange(newState : string, newLane: number) {
|
||||
state.value = newState;
|
||||
laneState.value = newLane;
|
||||
emit('stateChange', newState, newLane)
|
||||
function handleStateChange(newState: string, newLane: number) {
|
||||
state.value = newState
|
||||
laneState.value = newLane
|
||||
emit('stateChange', newState, newLane)
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const selected = ref("");
|
||||
if(route.path.startsWith("/tierlist/")) {
|
||||
const lane = route.params.lane as string
|
||||
selected.value = lane
|
||||
const selected = ref('')
|
||||
if (route.path.startsWith('/tierlist/')) {
|
||||
const lane = route.params.lane as string
|
||||
selected.value = lane
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="navbar-container">
|
||||
|
||||
<NuxtLink
|
||||
style="display: flex; width: fit-content; text-decoration: none; align-items: center; margin-left: 10px;"
|
||||
to="/"
|
||||
prefetch
|
||||
<div class="navbar-container">
|
||||
<NuxtLink
|
||||
style="
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
text-decoration: none;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
"
|
||||
to="/"
|
||||
prefetch
|
||||
>
|
||||
<NuxtImg
|
||||
id="navbar-logo-img"
|
||||
format="webp"
|
||||
src="/buildpath-high-resolution-logo-transparent.png"
|
||||
/>
|
||||
</NuxtLink>
|
||||
|
||||
<div
|
||||
v-for="(lane, i) in championLanes"
|
||||
style="display: flex; align-items: center; margin-left: 20px"
|
||||
>
|
||||
<NuxtImg
|
||||
format="webp"
|
||||
width="40"
|
||||
height="40"
|
||||
:src="LANE_IMAGES[lanePositionToIndex(lane.data)]"
|
||||
/>
|
||||
<div>
|
||||
<h2
|
||||
:class="
|
||||
'navbar-link ' + (state == 'runes' && laneState == i ? 'navbar-link-selected' : '')
|
||||
"
|
||||
@click="handleStateChange('runes', i)"
|
||||
>
|
||||
<NuxtImg format="webp" id="navbar-logo-img"
|
||||
src="/buildpath-high-resolution-logo-transparent.png" />
|
||||
</NuxtLink>
|
||||
|
||||
<div v-for="(lane, i) in championLanes" style="display: flex; align-items: center; margin-left: 20px;">
|
||||
<NuxtImg format="webp" width="40" height="40"
|
||||
:src="LANE_IMAGES[lanePositionToIndex(lane.data)]" />
|
||||
<div>
|
||||
<h2 :class="'navbar-link ' + (state == 'runes' && laneState == i ? 'navbar-link-selected' : '')"
|
||||
@click="handleStateChange('runes', i)" >
|
||||
Runes
|
||||
</h2>
|
||||
<h2 :class="'navbar-link ' + (state == 'items' && laneState == i ? 'navbar-link-selected' : '')"
|
||||
@click="handleStateChange('items', i)" >
|
||||
Items
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tierlistList == true" style="padding-left: 20px;">
|
||||
<h2 style="padding-left: 0px; font-size: 1.4rem; margin-top: 15px;">Tierlist</h2>
|
||||
<div style="display: flex;">
|
||||
<NuxtLink style="margin-top: 5px; margin-bottom: 5px;" v-for="(pos, i) in POSITIONS" :to="'/tierlist/' + pos">
|
||||
<div :class="selected == pos ? 'navbar-link-selected' : ''"
|
||||
class="navbar-link" style="display: flex; align-items: center;">
|
||||
<NuxtImg format="webp"
|
||||
width="30" height="30"
|
||||
:src="LANE_IMAGES[i]" :alt="POSITIONS_STR[i]" />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
Runes
|
||||
</h2>
|
||||
<h2
|
||||
:class="
|
||||
'navbar-link ' + (state == 'items' && laneState == i ? 'navbar-link-selected' : '')
|
||||
"
|
||||
@click="handleStateChange('items', i)"
|
||||
>
|
||||
Items
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tierlistList == true" style="padding-left: 20px">
|
||||
<h2 style="padding-left: 0px; font-size: 1.4rem; margin-top: 15px">Tierlist</h2>
|
||||
<div style="display: flex">
|
||||
<NuxtLink
|
||||
v-for="(pos, i) in POSITIONS"
|
||||
style="margin-top: 5px; margin-bottom: 5px"
|
||||
:to="'/tierlist/' + pos"
|
||||
>
|
||||
<div
|
||||
:class="selected == pos ? 'navbar-link-selected' : ''"
|
||||
class="navbar-link"
|
||||
style="display: flex; align-items: center"
|
||||
>
|
||||
<NuxtImg
|
||||
format="webp"
|
||||
width="30"
|
||||
height="30"
|
||||
:src="LANE_IMAGES[i]"
|
||||
:alt="POSITIONS_STR[i]"
|
||||
/>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.navbar-container {
|
||||
display: flex;
|
||||
display: flex;
|
||||
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
|
||||
background-color: #2B2826;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background-color: #2b2826;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
|
||||
margin: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
.navbar-link {
|
||||
user-select: none;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.navbar-link:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--color-surface-darker);
|
||||
cursor: pointer;
|
||||
background-color: var(--color-surface-darker);
|
||||
}
|
||||
.navbar-link-selected {
|
||||
background-color: var(--color-surface);
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
#navbar-logo-img {
|
||||
height: 70px;
|
||||
width: fit-content;
|
||||
max-width: 55px;
|
||||
height: 70px;
|
||||
width: fit-content;
|
||||
max-width: 55px;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1200px) {
|
||||
.navbar-container {
|
||||
display: none;
|
||||
}
|
||||
.navbar-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,135 +1,181 @@
|
||||
<script setup lang="ts">
|
||||
import { LANE_IMAGES, lanePositionToIndex, POSITIONS_STR } from '~/utils/cdragon';
|
||||
import { LANE_IMAGES, lanePositionToIndex, POSITIONS_STR } from '~/utils/cdragon'
|
||||
|
||||
defineProps<{
|
||||
championName?: string
|
||||
championLanes?: Array<LaneData>
|
||||
tierlistList?: boolean
|
||||
championName?: string
|
||||
championLanes?: Array<LaneData>
|
||||
tierlistList?: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
stateChange: [state: string, lane: number]
|
||||
stateChange: [state: string, lane: number]
|
||||
}>()
|
||||
|
||||
const state = ref("runes")
|
||||
const state = ref('runes')
|
||||
const laneState = ref(0)
|
||||
|
||||
function handleStateChange(newState : string, newLane: number) {
|
||||
state.value = newState;
|
||||
laneState.value = newLane;
|
||||
emit('stateChange', newState, newLane)
|
||||
function handleStateChange(newState: string, newLane: number) {
|
||||
state.value = newState
|
||||
laneState.value = newLane
|
||||
emit('stateChange', newState, newLane)
|
||||
}
|
||||
|
||||
const {data: stats}: {data: Ref<{patch: number, count: number}>} = await useFetch("/api/stats")
|
||||
const { data: stats }: { data: Ref<{ patch: number; count: number }> } =
|
||||
await useFetch('/api/stats')
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const selected = ref("");
|
||||
if(route.path.startsWith("/tierlist/")) {
|
||||
const lane = route.params.lane as string
|
||||
selected.value = lane
|
||||
const selected = ref('')
|
||||
if (route.path.startsWith('/tierlist/')) {
|
||||
const lane = route.params.lane as string
|
||||
selected.value = lane
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- To make content have a 300px margin -->
|
||||
<div class="sidebar-margin"></div>
|
||||
<!-- To make content have a 300px margin -->
|
||||
<div class="sidebar-margin"/>
|
||||
|
||||
<div class="sidebar-container">
|
||||
<Logo font-size="2.6rem" img-width="60" style="padding-left: 15px; padding-right: 15px; margin-top: 30px;"/>
|
||||
<div class="sidebar-container">
|
||||
<Logo
|
||||
font-size="2.6rem"
|
||||
img-width="60"
|
||||
style="padding-left: 15px; padding-right: 15px; margin-top: 30px"
|
||||
/>
|
||||
|
||||
<div v-for="(lane, i) in championLanes">
|
||||
|
||||
<div style="display: flex; align-items: center; margin-top: 30px; padding-right: 10px; overflow: hidden;">
|
||||
<h1 style="font-size: 2.4rem; padding-left: 20px;">{{ championName }}</h1>
|
||||
<NuxtImg format="webp" style="margin-left: 10px;"
|
||||
width="40" height="40"
|
||||
:src="LANE_IMAGES[lanePositionToIndex(lane.data)]" />
|
||||
<h2 v-if="championName != null && championName != undefined && championName.length < 8"
|
||||
style="margin-left: 5px; font-size: 1.8rem; font-weight: 200;">
|
||||
{{ POSITIONS_STR[lanePositionToIndex(lane.data.toLowerCase())] }}
|
||||
<div v-for="(lane, i) in championLanes">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 30px;
|
||||
padding-right: 10px;
|
||||
overflow: hidden;
|
||||
"
|
||||
>
|
||||
<h1 style="font-size: 2.4rem; padding-left: 20px">{{ championName }}</h1>
|
||||
<NuxtImg
|
||||
format="webp"
|
||||
style="margin-left: 10px"
|
||||
width="40"
|
||||
height="40"
|
||||
:src="LANE_IMAGES[lanePositionToIndex(lane.data)]"
|
||||
/>
|
||||
<h2
|
||||
v-if="championName != null && championName != undefined && championName.length < 8"
|
||||
style="margin-left: 5px; font-size: 1.8rem; font-weight: 200"
|
||||
>
|
||||
{{ POSITIONS_STR[lanePositionToIndex(lane.data.toLowerCase())] }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 :class="'sidebar-link ' + (state == 'runes' && laneState == i ? 'sidebar-link-selected' : '')"
|
||||
@click="handleStateChange('runes', i)"
|
||||
style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px;">Runes</h2>
|
||||
<h2 :class="'sidebar-link ' + (state == 'items' && laneState == i ? 'sidebar-link-selected' : '')"
|
||||
@click="handleStateChange('items', i)"
|
||||
style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px;">Items</h2>
|
||||
<h2
|
||||
:class="
|
||||
'sidebar-link ' + (state == 'runes' && laneState == i ? 'sidebar-link-selected' : '')
|
||||
"
|
||||
style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px"
|
||||
@click="handleStateChange('runes', i)"
|
||||
>
|
||||
Runes
|
||||
</h2>
|
||||
<h2
|
||||
:class="
|
||||
'sidebar-link ' + (state == 'items' && laneState == i ? 'sidebar-link-selected' : '')
|
||||
"
|
||||
style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px"
|
||||
@click="handleStateChange('items', i)"
|
||||
>
|
||||
Items
|
||||
</h2>
|
||||
|
||||
<h2 :class="'sidebar-link ' + (state == 'alternatives' && laneState == i ? 'sidebar-link-selected' : '')"
|
||||
@click="handleStateChange('alternatives', i)"
|
||||
style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px;">Alternatives</h2>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="tierlistList == true" style="margin-top: 30px;">
|
||||
<h2 style="padding-left: 20px; font-size: 2.4rem; margin-bottom: 10px;">Tierlist</h2>
|
||||
<NuxtLink style="margin-top: 5px; margin-bottom: 5px;" v-for="(pos, i) in POSITIONS" :to="'/tierlist/' + pos">
|
||||
<div :class="selected == pos ? 'sidebar-link-selected' : ''"
|
||||
class="sidebar-link" style="padding-left: 35px; display: flex; align-items: center;">
|
||||
<NuxtImg format="webp"
|
||||
width="40" height="40"
|
||||
:src="LANE_IMAGES[i]" :alt="POSITIONS_STR[i]" />
|
||||
<h3 style="font-size: 2.1rem; font-weight: 200; margin-left: 10px;">{{ POSITIONS_STR[i] }}</h3>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div style="position: absolute; bottom: 0; margin-bottom: 10px; padding-left: 10px;">
|
||||
<h3 style="font-size: 23px; font-weight: 200;">
|
||||
Patch {{ stats.patch }}
|
||||
</h3>
|
||||
<h3 style="font-size: 23px; font-weight: 200;">
|
||||
{{ stats.count }} games
|
||||
</h3>
|
||||
<NuxtLink to="/about"><h3>About</h3></NuxtLink>
|
||||
<h2 style="font-size: 12px; font-weight: 200; margin-top: 5px;">
|
||||
BuildPath isn't endorsed by Riot Games and doesn't reflect the views or opinions of
|
||||
Riot Games or anyone officially involved in producing or managing Riot Games properties.
|
||||
Riot Games, and all associated properties are trademarks or registered trademarks of Riot Games, Inc.
|
||||
</h2>
|
||||
</div>
|
||||
<h2
|
||||
:class="
|
||||
'sidebar-link ' +
|
||||
(state == 'alternatives' && laneState == i ? 'sidebar-link-selected' : '')
|
||||
"
|
||||
style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px"
|
||||
@click="handleStateChange('alternatives', i)"
|
||||
>
|
||||
Alternatives
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="tierlistList == true" style="margin-top: 30px">
|
||||
<h2 style="padding-left: 20px; font-size: 2.4rem; margin-bottom: 10px">Tierlist</h2>
|
||||
<NuxtLink
|
||||
v-for="(pos, i) in POSITIONS"
|
||||
style="margin-top: 5px; margin-bottom: 5px"
|
||||
:to="'/tierlist/' + pos"
|
||||
>
|
||||
<div
|
||||
:class="selected == pos ? 'sidebar-link-selected' : ''"
|
||||
class="sidebar-link"
|
||||
style="padding-left: 35px; display: flex; align-items: center"
|
||||
>
|
||||
<NuxtImg
|
||||
format="webp"
|
||||
width="40"
|
||||
height="40"
|
||||
:src="LANE_IMAGES[i]"
|
||||
:alt="POSITIONS_STR[i]"
|
||||
/>
|
||||
<h3 style="font-size: 2.1rem; font-weight: 200; margin-left: 10px">
|
||||
{{ POSITIONS_STR[i] }}
|
||||
</h3>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div style="position: absolute; bottom: 0; margin-bottom: 10px; padding-left: 10px">
|
||||
<h3 style="font-size: 23px; font-weight: 200">Patch {{ stats.patch }}</h3>
|
||||
<h3 style="font-size: 23px; font-weight: 200">{{ stats.count }} games</h3>
|
||||
<NuxtLink to="/about"><h3>About</h3></NuxtLink>
|
||||
<h2 style="font-size: 12px; font-weight: 200; margin-top: 5px">
|
||||
BuildPath isn't endorsed by Riot Games and doesn't reflect the views or opinions of Riot
|
||||
Games or anyone officially involved in producing or managing Riot Games properties. Riot
|
||||
Games, and all associated properties are trademarks or registered trademarks of Riot Games,
|
||||
Inc.
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.sidebar-container {
|
||||
background-color: #2B2826;
|
||||
width: 300px;
|
||||
background-color: #2b2826;
|
||||
width: 300px;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
.sidebar-margin {
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sidebar-link {
|
||||
user-select: none;
|
||||
margin: 5px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
margin: 5px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.sidebar-link:hover {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
|
||||
background-color: var(--color-surface-darker);
|
||||
background-color: var(--color-surface-darker);
|
||||
}
|
||||
.sidebar-link-selected {
|
||||
background-color: var(--color-surface);
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.sidebar-container {
|
||||
display: none;
|
||||
}
|
||||
.sidebar-margin {
|
||||
display: none;
|
||||
}
|
||||
.sidebar-container {
|
||||
display: none;
|
||||
}
|
||||
.sidebar-margin {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,116 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
primaryStyleId: number
|
||||
secondaryStyleId: number
|
||||
selectionIds: Array<number>
|
||||
primaryStyleId: number
|
||||
secondaryStyleId: number
|
||||
selectionIds: Array<number>
|
||||
}>()
|
||||
|
||||
const primaryStyle : Ref<PerkStyle> = ref({id:0, name:"", iconPath:"", slots:[]})
|
||||
const secondaryStyle : Ref<PerkStyle> = ref({id:0, name:"", iconPath:"", slots:[]})
|
||||
const primaryStyle: Ref<PerkStyle> = ref({ id: 0, name: '', iconPath: '', slots: [] })
|
||||
const secondaryStyle: Ref<PerkStyle> = ref({ id: 0, name: '', iconPath: '', slots: [] })
|
||||
|
||||
let { data: perks_data } : PerksResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/perks.json")
|
||||
const { data: perks_data }: PerksResponse = await useFetch(
|
||||
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perks.json'
|
||||
)
|
||||
const perks = reactive(new Map())
|
||||
for(let perk of perks_data.value) {
|
||||
perks.set(perk.id, perk)
|
||||
for (const perk of perks_data.value) {
|
||||
perks.set(perk.id, perk)
|
||||
}
|
||||
|
||||
let { data: stylesData } : PerkStylesResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json")
|
||||
watch(() => props.primaryStyleId, async (newP, oldP) => {refreshStyles()})
|
||||
watch(() => props.secondaryStyleId, async (newP, oldP) => {refreshStyles()})
|
||||
const { data: stylesData }: PerkStylesResponse = await useFetch(
|
||||
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json'
|
||||
)
|
||||
watch(
|
||||
() => props.primaryStyleId,
|
||||
async (newP, oldP) => {
|
||||
refreshStyles()
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => props.secondaryStyleId,
|
||||
async (newP, oldP) => {
|
||||
refreshStyles()
|
||||
}
|
||||
)
|
||||
|
||||
function refreshStyles() {
|
||||
for(let style of stylesData.value.styles) {
|
||||
if(style.id == (props.primaryStyleId)) {
|
||||
primaryStyle.value = style
|
||||
}
|
||||
if(style.id == (props.secondaryStyleId)) {
|
||||
secondaryStyle.value = style
|
||||
}
|
||||
for (const style of stylesData.value.styles) {
|
||||
if (style.id == props.primaryStyleId) {
|
||||
primaryStyle.value = style
|
||||
}
|
||||
if (style.id == props.secondaryStyleId) {
|
||||
secondaryStyle.value = style
|
||||
}
|
||||
}
|
||||
}
|
||||
refreshStyles()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div style="display: flex">
|
||||
<div class="rune-holder">
|
||||
<div class="rune-slot"><NuxtImg class="rune-style-img" style="margin: auto;" :src="CDRAGON_BASE + mapPath(primaryStyle.iconPath)" /></div>
|
||||
<div class="rune-slot" v-for="slot in primaryStyle.slots.slice(0, 1)">
|
||||
<NuxtImg width="48" v-for="perk in slot.perks" :class="'rune-img rune-keystone ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')" :src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"/>
|
||||
</div>
|
||||
<div class="rune-slot" v-for="slot in primaryStyle.slots.slice(1, 4)">
|
||||
<NuxtImg width="48" v-for="perk in slot.perks" :class="'rune-img ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')" :src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"/>
|
||||
</div>
|
||||
<div class="rune-slot">
|
||||
<NuxtImg
|
||||
class="rune-style-img"
|
||||
style="margin: auto"
|
||||
:src="CDRAGON_BASE + mapPath(primaryStyle.iconPath)"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="slot in primaryStyle.slots.slice(0, 1)" class="rune-slot">
|
||||
<NuxtImg
|
||||
v-for="perk in slot.perks"
|
||||
width="48"
|
||||
:class="
|
||||
'rune-img rune-keystone ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')
|
||||
"
|
||||
:src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="slot in primaryStyle.slots.slice(1, 4)" class="rune-slot">
|
||||
<NuxtImg
|
||||
v-for="perk in slot.perks"
|
||||
width="48"
|
||||
:class="'rune-img ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')"
|
||||
:src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rune-spacer-bar"></div>
|
||||
<div class="rune-spacer-bar"/>
|
||||
<div class="rune-holder" style="align-content: end">
|
||||
<div class="rune-slot"><img style="margin: auto;" :src="CDRAGON_BASE + mapPath(secondaryStyle.iconPath)" /></div>
|
||||
<div class="rune-slot" v-for="slot in secondaryStyle.slots.slice(1, 4)">
|
||||
<NuxtImg width="48" v-for="perk in slot.perks" :class="'rune-img ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')" :src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"/>
|
||||
</div>
|
||||
<!-- <div class="rune-slot mini" v-for="slot in primaryStyle.slots.slice(4, 7)">
|
||||
<div class="rune-slot">
|
||||
<img style="margin: auto" :src="CDRAGON_BASE + mapPath(secondaryStyle.iconPath)" >
|
||||
</div>
|
||||
<div v-for="slot in secondaryStyle.slots.slice(1, 4)" class="rune-slot">
|
||||
<NuxtImg
|
||||
v-for="perk in slot.perks"
|
||||
width="48"
|
||||
:class="'rune-img ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')"
|
||||
:src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="rune-slot mini" v-for="slot in primaryStyle.slots.slice(4, 7)">
|
||||
<img width="32" v-for="perk in slot.perks" :class="'rune-img ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')" :src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"/>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.rune-holder {
|
||||
/* align-content: end; */
|
||||
justify-content: center;
|
||||
/* align-content: end; */
|
||||
justify-content: center;
|
||||
}
|
||||
.rune-slot {
|
||||
width: calc(48*3px + 60px);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
width: calc(48 * 3px + 60px);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.mini {
|
||||
margin: auto;
|
||||
width: calc(32*3px + 60px);
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin: auto;
|
||||
width: calc(32 * 3px + 60px);
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.rune-img {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
filter: grayscale(1);
|
||||
border: 1px var(--color-on-surface) solid;
|
||||
border-radius:50%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
filter: grayscale(1);
|
||||
border: 1px var(--color-on-surface) solid;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.rune-keystone {
|
||||
border: none;
|
||||
border: none;
|
||||
}
|
||||
.rune-activated {
|
||||
filter: none;
|
||||
filter: none;
|
||||
}
|
||||
.rune-spacer-bar {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
border: 1px var(--color-on-surface) solid;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
border: 1px var(--color-on-surface) solid;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 650px) {
|
||||
.rune-slot {
|
||||
width: calc(24*3px + 30px);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.rune-img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.rune-style-img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.rune-spacer-bar {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.rune-slot {
|
||||
width: calc(24 * 3px + 30px);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.rune-img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.rune-style-img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.rune-spacer-bar {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,128 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
runes: Array<{count: number,
|
||||
primaryStyle: number,
|
||||
secondaryStyle: number,
|
||||
selections: Array<number>,
|
||||
pickrate: number}>
|
||||
runes: Array<{
|
||||
count: number
|
||||
primaryStyle: number
|
||||
secondaryStyle: number
|
||||
selections: Array<number>
|
||||
pickrate: number
|
||||
}>
|
||||
}>()
|
||||
|
||||
const currentlySelectedPage = ref(0)
|
||||
const primaryStyles : Ref<Array<PerkStyle>> = ref(Array(props.runes.length))
|
||||
const secondaryStyles : Ref<Array<PerkStyle>> = ref(Array(props.runes.length))
|
||||
const keystoneIds : Ref<Array<number>> = ref(Array(props.runes.length))
|
||||
const primaryStyles: Ref<Array<PerkStyle>> = ref(Array(props.runes.length))
|
||||
const secondaryStyles: Ref<Array<PerkStyle>> = ref(Array(props.runes.length))
|
||||
const keystoneIds: Ref<Array<number>> = ref(Array(props.runes.length))
|
||||
|
||||
let { data: perks_data } : PerksResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/perks.json")
|
||||
const { data: perks_data }: PerksResponse = await useFetch(
|
||||
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perks.json'
|
||||
)
|
||||
const perks = reactive(new Map())
|
||||
for(let perk of perks_data.value) {
|
||||
perks.set(perk.id, perk)
|
||||
for (const perk of perks_data.value) {
|
||||
perks.set(perk.id, perk)
|
||||
}
|
||||
|
||||
let { data: stylesData } : PerkStylesResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json")
|
||||
watch(() => props.runes, (newRunes, oldRunes) => {
|
||||
const { data: stylesData }: PerkStylesResponse = await useFetch(
|
||||
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json'
|
||||
)
|
||||
watch(
|
||||
() => props.runes,
|
||||
(newRunes, oldRunes) => {
|
||||
currentlySelectedPage.value = 0
|
||||
primaryStyles.value = Array(props.runes.length)
|
||||
secondaryStyles.value = Array(props.runes.length)
|
||||
keystoneIds.value = Array(props.runes.length)
|
||||
|
||||
refreshStylesKeystones()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
function refreshStylesKeystones() {
|
||||
for(let style of stylesData.value.styles) {
|
||||
for(let rune of props.runes) {
|
||||
if(style.id == rune.primaryStyle) {
|
||||
primaryStyles.value[props.runes.indexOf(rune)] = style
|
||||
for(let perk of style.slots[0].perks) {
|
||||
if(rune.selections.includes(perk)) {
|
||||
keystoneIds.value[props.runes.indexOf(rune)] = perk
|
||||
}
|
||||
}
|
||||
}
|
||||
if(style.id == rune.secondaryStyle) {
|
||||
secondaryStyles.value[props.runes.indexOf(rune)] = style
|
||||
}
|
||||
for (const style of stylesData.value.styles) {
|
||||
for (const rune of props.runes) {
|
||||
if (style.id == rune.primaryStyle) {
|
||||
primaryStyles.value[props.runes.indexOf(rune)] = style
|
||||
for (const perk of style.slots[0].perks) {
|
||||
if (rune.selections.includes(perk)) {
|
||||
keystoneIds.value[props.runes.indexOf(rune)] = perk
|
||||
}
|
||||
}
|
||||
}
|
||||
if (style.id == rune.secondaryStyle) {
|
||||
secondaryStyles.value[props.runes.indexOf(rune)] = style
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshStylesKeystones()
|
||||
|
||||
function runeSelect(index: number) {
|
||||
currentlySelectedPage.value = index
|
||||
currentlySelectedPage.value = index
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: fit-content;">
|
||||
<RunePage v-if="runes[currentlySelectedPage] != undefined && runes[currentlySelectedPage] != null"
|
||||
style="margin:auto; width: fit-content;"
|
||||
:primaryStyleId="runes[currentlySelectedPage].primaryStyle"
|
||||
:secondaryStyleId="runes[currentlySelectedPage].secondaryStyle"
|
||||
:selectionIds="runes[currentlySelectedPage].selections" />
|
||||
<div style="display: flex; margin-top: 20px; justify-content: center;">
|
||||
<div v-for="(_, i) in runes" @click="runeSelect(i)">
|
||||
<div :class="'rune-selector-entry ' + (i == currentlySelectedPage ? 'rune-selector-entry-selected' : '')">
|
||||
<div class="rs-styles-container">
|
||||
<NuxtImg class="rs-style-img" v-if="primaryStyles[i] != null && primaryStyles[i] != undefined"
|
||||
style="margin: auto;" :src="CDRAGON_BASE + mapPath(primaryStyles[i].iconPath)" />
|
||||
<NuxtImg class="rs-style-img" v-if="keystoneIds[i] != null && keystoneIds[i] != undefined"
|
||||
width="34" :src="CDRAGON_BASE + ( mapPath(perks.get(keystoneIds[i]).iconPath))"/>
|
||||
<NuxtImg class="rs-style-img" v-if="secondaryStyles[i] != null && secondaryStyles[i] != undefined"
|
||||
style="margin: auto;" :src="CDRAGON_BASE + mapPath(secondaryStyles[i].iconPath)" />
|
||||
</div>
|
||||
<div style="width: fit-content">
|
||||
<RunePage
|
||||
v-if="runes[currentlySelectedPage] != undefined && runes[currentlySelectedPage] != null"
|
||||
style="margin: auto; width: fit-content"
|
||||
:primary-style-id="runes[currentlySelectedPage].primaryStyle"
|
||||
:secondary-style-id="runes[currentlySelectedPage].secondaryStyle"
|
||||
:selection-ids="runes[currentlySelectedPage].selections"
|
||||
/>
|
||||
<div style="display: flex; margin-top: 20px; justify-content: center">
|
||||
<div v-for="(_, i) in runes" @click="runeSelect(i)">
|
||||
<div
|
||||
:class="
|
||||
'rune-selector-entry ' +
|
||||
(i == currentlySelectedPage ? 'rune-selector-entry-selected' : '')
|
||||
"
|
||||
>
|
||||
<div class="rs-styles-container">
|
||||
<NuxtImg
|
||||
v-if="primaryStyles[i] != null && primaryStyles[i] != undefined"
|
||||
class="rs-style-img"
|
||||
style="margin: auto"
|
||||
:src="CDRAGON_BASE + mapPath(primaryStyles[i].iconPath)"
|
||||
/>
|
||||
<NuxtImg
|
||||
v-if="keystoneIds[i] != null && keystoneIds[i] != undefined"
|
||||
class="rs-style-img"
|
||||
width="34"
|
||||
:src="CDRAGON_BASE + mapPath(perks.get(keystoneIds[i]).iconPath)"
|
||||
/>
|
||||
<NuxtImg
|
||||
v-if="secondaryStyles[i] != null && secondaryStyles[i] != undefined"
|
||||
class="rs-style-img"
|
||||
style="margin: auto"
|
||||
:src="CDRAGON_BASE + mapPath(secondaryStyles[i].iconPath)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="rs-pickrate">{{ (runes[i].pickrate * 100).toFixed(2) }}% pick.</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.rune-selector-entry {
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
|
||||
border-radius: 8%;
|
||||
border: 1px solid var(--color-on-surface);
|
||||
border-radius: 8%;
|
||||
border: 1px solid var(--color-on-surface);
|
||||
}
|
||||
.rune-selector-entry:hover {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rune-selector-entry-selected {
|
||||
background-color: var(--color-surface-darker);
|
||||
background-color: var(--color-surface-darker);
|
||||
}
|
||||
.rs-styles-container {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.rs-pickrate {
|
||||
text-align: center;
|
||||
margin-top: -40px;
|
||||
padding-bottom: 40px;
|
||||
text-align: center;
|
||||
margin-top: -40px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
@media only screen and (max-width: 650px) {
|
||||
.rune-selector-entry {
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
.rune-selector-entry {
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.rs-styles-container {
|
||||
margin-top: 17px;
|
||||
}
|
||||
.rs-pickrate {
|
||||
margin-top: 5px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.rs-style-img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.rs-styles-container {
|
||||
margin-top: 17px;
|
||||
}
|
||||
.rs-pickrate {
|
||||
margin-top: 5px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.rs-style-img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,38 +1,47 @@
|
||||
<script lang="ts" setup>
|
||||
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, plugins, scales } from 'chart.js'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
plugins,
|
||||
scales
|
||||
} from 'chart.js'
|
||||
import { Bar } from 'vue-chartjs'
|
||||
|
||||
// Register
|
||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
||||
|
||||
const props = defineProps<{
|
||||
data: Array<{title:string, data: Array<{lane: LaneData, champion: Champion}>}>
|
||||
data: Array<{ title: string; data: Array<{ lane: LaneData; champion: Champion }> }>
|
||||
}>()
|
||||
|
||||
const labels: Array<string> = []
|
||||
const pickrates: Array<number> = []
|
||||
const pickrates: Array<number> = []
|
||||
const images: Array<string> = []
|
||||
const backgroundColors: Array<string> = []
|
||||
const CHAMPION_CUT_THRESHOLD = 32
|
||||
const TIER_COLORS = ["#ff7f7e", "#ffbf7f", "#ffdf80", "#feff7f", "#beff7f", "#7eff80"]
|
||||
const TIER_COLORS = ['#ff7f7e', '#ffbf7f', '#ffdf80', '#feff7f', '#beff7f', '#7eff80']
|
||||
|
||||
let count = 0
|
||||
let colorIndex = 0
|
||||
for(let tier of props.data) {
|
||||
for(let {champion: champion, lane: lane} of tier.data) {
|
||||
if(count > CHAMPION_CUT_THRESHOLD) break;
|
||||
for (const tier of props.data) {
|
||||
for (const { champion: champion, lane: lane } of tier.data) {
|
||||
if (count > CHAMPION_CUT_THRESHOLD) break
|
||||
|
||||
labels.push(champion.name)
|
||||
pickrates.push(lane.pickrate * 100)
|
||||
images.push(CDRAGON_BASE + mapPath(champion.squarePortraitPath))
|
||||
backgroundColors.push(TIER_COLORS[colorIndex])
|
||||
labels.push(champion.name)
|
||||
pickrates.push(lane.pickrate * 100)
|
||||
images.push(CDRAGON_BASE + mapPath(champion.squarePortraitPath))
|
||||
backgroundColors.push(TIER_COLORS[colorIndex])
|
||||
|
||||
count++
|
||||
}
|
||||
colorIndex++
|
||||
count++
|
||||
}
|
||||
colorIndex++
|
||||
}
|
||||
|
||||
|
||||
const chartData = ref({
|
||||
labels: labels,
|
||||
datasets: [
|
||||
@@ -40,43 +49,45 @@ const chartData = ref({
|
||||
label: 'Pickrate',
|
||||
backgroundColor: backgroundColors,
|
||||
barPercentage: 1.0,
|
||||
data: pickrates,
|
||||
},
|
||||
],
|
||||
data: pickrates
|
||||
}
|
||||
]
|
||||
})
|
||||
const chartOptions = ref({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
callback: (() => "")
|
||||
}
|
||||
ticks: {
|
||||
callback: () => ''
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
display: false
|
||||
}
|
||||
}
|
||||
})
|
||||
const chartPlugins = [{
|
||||
id: "image-draw",
|
||||
afterDraw: ((chart: any) => {
|
||||
const ctx : CanvasRenderingContext2D = chart.ctx
|
||||
var xAxis = chart.scales.x;
|
||||
xAxis.ticks.forEach((value: any, index: number) => {
|
||||
var x = xAxis.getPixelForTick(index)
|
||||
var image = new Image()
|
||||
image.src = images[index]
|
||||
ctx.drawImage(image, x - 14, xAxis.bottom - 28, 28, 28)
|
||||
})
|
||||
})
|
||||
}]
|
||||
const chartPlugins = [
|
||||
{
|
||||
id: 'image-draw',
|
||||
afterDraw: (chart: any) => {
|
||||
const ctx: CanvasRenderingContext2D = chart.ctx
|
||||
const xAxis = chart.scales.x
|
||||
xAxis.ticks.forEach((value: any, index: number) => {
|
||||
const x = xAxis.getPixelForTick(index)
|
||||
const image = new Image()
|
||||
image.src = images[index]
|
||||
ctx.drawImage(image, x - 14, xAxis.bottom - 28, 28, 28)
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Bar :data="chartData" :options="chartOptions" :plugins="chartPlugins" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,73 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string
|
||||
tier: Array<{champion: Champion, lane: LaneData}>
|
||||
title: string
|
||||
tier: Array<{ champion: Champion; lane: LaneData }>
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div style="display: flex">
|
||||
<h2 class="tierlist-tier-title">{{ title }}</h2>
|
||||
<div class="tierlist-tier-container">
|
||||
<NuxtLink v-for="{champion: champion} in tier" :to="'/champion/' + champion.alias.toLowerCase()">
|
||||
<NuxtLink
|
||||
v-for="{ champion: champion } in tier"
|
||||
:to="'/champion/' + champion.alias.toLowerCase()"
|
||||
>
|
||||
<div class="champion-img-container">
|
||||
<NuxtImg class="champion-img" :src="CDRAGON_BASE + mapPath(champion.squarePortraitPath)" :alt="champion.name"/>
|
||||
<NuxtImg
|
||||
class="champion-img"
|
||||
:src="CDRAGON_BASE + mapPath(champion.squarePortraitPath)"
|
||||
:alt="champion.name"
|
||||
/>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.tierlist-tier-container {
|
||||
width: 90%;
|
||||
min-height: 122px;
|
||||
width: 90%;
|
||||
min-height: 122px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 128px);
|
||||
grid-gap: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 128px);
|
||||
grid-gap: 10px;
|
||||
|
||||
align-items: center;
|
||||
align-items: center;
|
||||
|
||||
margin: auto;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin: auto;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.tierlist-tier-title {
|
||||
font-size: 3.3rem;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
margin-top: 40px;
|
||||
|
||||
font-weight: 300;
|
||||
font-size: 3.3rem;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
margin-top: 40px;
|
||||
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.champion-img-container {
|
||||
overflow: hidden; width: 120px; height: 120px;
|
||||
border: 1px solid var(--color-surface);
|
||||
overflow: hidden;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 1px solid var(--color-surface);
|
||||
}
|
||||
.champion-img-container:hover {
|
||||
border: 1px solid var(--color-on-surface);
|
||||
border: 1px solid var(--color-on-surface);
|
||||
}
|
||||
.champion-img {
|
||||
width: 116px;
|
||||
height: 116px;
|
||||
transform: translate(4px, 4px) scale(1.2, 1.2);
|
||||
width: 116px;
|
||||
height: 116px;
|
||||
transform: translate(4px, 4px) scale(1.2, 1.2);
|
||||
|
||||
user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
@media only screen and (max-width: 450px) {
|
||||
.champion-img-container {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.champion-img {
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
}
|
||||
.tierlist-tier-container {
|
||||
grid-template-columns: repeat(auto-fit, 80px);
|
||||
min-height: 82px;
|
||||
}
|
||||
.champion-img-container {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.champion-img {
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
}
|
||||
.tierlist-tier-container {
|
||||
grid-template-columns: repeat(auto-fit, 80px);
|
||||
min-height: 82px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user