Lint and format
This commit is contained in:
11
frontend/.prettierrc
Normal file
11
frontend/.prettierrc
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf",
|
||||
"vueIndentScriptAndStyle": false
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
# BuildPath - Nuxt
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@ useSeoMeta({
|
||||
title: 'BuildPath',
|
||||
ogTitle: 'BuildPath',
|
||||
description: 'BuildPath: a tool for League of Legends champions runes and build paths.',
|
||||
ogDescription: 'BuildPath: a tool for League of Legends champions runes and build paths.',
|
||||
ogDescription: 'BuildPath: a tool for League of Legends champions runes and build paths.'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NuxtRouteAnnouncer />
|
||||
<NuxtLayout>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,57 +1,72 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
|
||||
|
||||
:root {
|
||||
--color-surface: #312E2C;
|
||||
--color-on-surface: #B7B8E1;
|
||||
--color-surface-darker: #1f1d1c;
|
||||
--color-surface: #312e2c;
|
||||
--color-on-surface: #b7b8e1;
|
||||
--color-surface-darker: #1f1d1c;
|
||||
}
|
||||
|
||||
/* Font setting */
|
||||
h1,h2,h3,h4,h5,h6,p,a,input[type=text] {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
a,
|
||||
input[type='text'] {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
color: var(--color-on-surface);
|
||||
color: var(--color-on-surface);
|
||||
}
|
||||
|
||||
/* Default margins to none */
|
||||
h1,h2,h3,h4,h5,h6,p,a {
|
||||
margin: 0px;
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
a {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-surface);
|
||||
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: var(--color-surface);
|
||||
|
||||
/* default value: font-size: 16px; */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
/* default value: font-size: 16px; */
|
||||
}
|
||||
@media only screen and (max-width: 650px) {
|
||||
body {
|
||||
font-size: 12px;
|
||||
}
|
||||
html {
|
||||
font-size: 12px;
|
||||
}
|
||||
body {
|
||||
font-size: 12px;
|
||||
}
|
||||
html {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Different title settings */
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 300;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -9,16 +9,26 @@ const props = defineProps({
|
||||
<Title>{{ props.error.statusCode }} - BuildPath</Title>
|
||||
</Head>
|
||||
|
||||
<Logo style="margin: auto; margin-top: 64px; margin-bottom: 64px;" />
|
||||
<div style="margin:auto; width: fit-content; margin-top: 64px;">
|
||||
<Logo style="margin: auto; margin-top: 64px; margin-bottom: 64px" />
|
||||
<div style="margin: auto; width: fit-content; margin-top: 64px">
|
||||
<h1>{{ props.error.statusCode }} Error</h1>
|
||||
<h2>Something went wrong, sorry :(</h2>
|
||||
<div style="margin-top: 64px;">
|
||||
<h3 v-if="props.error.statusMessage != null
|
||||
&& props.error.statusMessage != undefined
|
||||
&& props.error.statusMessage != ''">Error message: {{ props.error.statusMessage }}</h3>
|
||||
<h3 v-if="props.error.data != null && props.error.data != undefined">Error data: {{ props.error.data }}</h3>
|
||||
<h3 v-if="props.error.cause != null && props.error.cause != undefined">Error cause: {{ props.error.cause }}</h3>
|
||||
<div style="margin-top: 64px">
|
||||
<h3
|
||||
v-if="
|
||||
props.error.statusMessage != null &&
|
||||
props.error.statusMessage != undefined &&
|
||||
props.error.statusMessage != ''
|
||||
"
|
||||
>
|
||||
Error message: {{ props.error.statusMessage }}
|
||||
</h3>
|
||||
<h3 v-if="props.error.data != null && props.error.data != undefined">
|
||||
Error data: {{ props.error.data }}
|
||||
</h3>
|
||||
<h3 v-if="props.error.cause != null && props.error.cause != undefined">
|
||||
Error cause: {{ props.error.cause }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
6
frontend/eslint.config.mjs
Normal file
6
frontend/eslint.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
||||
// @ts-check
|
||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||
|
||||
export default withNuxt(
|
||||
// Your custom configs here
|
||||
)
|
||||
@@ -5,43 +5,39 @@ export default defineNuxtConfig({
|
||||
css: ['~/assets/css/main.css'],
|
||||
|
||||
routeRules: {
|
||||
'/' : {prerender: false, swr: true},
|
||||
'/champion/**' : {swr: true}
|
||||
'/': { prerender: false, swr: true },
|
||||
'/champion/**': { swr: true }
|
||||
},
|
||||
|
||||
site: {
|
||||
url: 'https://buildpath.win',
|
||||
name: 'BuildPath',
|
||||
description: 'BuildPath: a tool for League of Legends champions runes and build paths.',
|
||||
defaultLocale: 'en', // not needed if you have @nuxtjs/i18n installed
|
||||
defaultLocale: 'en' // not needed if you have @nuxtjs/i18n installed
|
||||
},
|
||||
sitemap: {
|
||||
sources: [
|
||||
'/api/routemap'
|
||||
]
|
||||
sources: ['/api/routemap']
|
||||
},
|
||||
|
||||
app: {
|
||||
head: {
|
||||
htmlAttrs: {
|
||||
lang: 'en',
|
||||
lang: 'en'
|
||||
},
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/png', sizes: '96x96', href: '/favicon-96x96.png' },
|
||||
{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
|
||||
{ rel: 'shortcut icon', href: '/favicon.ico' },
|
||||
{ rel: 'apple-touch-icon', sizes: "180x180", href: '/apple-touch-icon.png' },
|
||||
{ rel: 'manifest', href: '/site.webmanifest' },
|
||||
],
|
||||
meta: [
|
||||
{name: "apple-mobile-web-app-title", content:"BuildPath"},
|
||||
{ rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' },
|
||||
{ rel: 'manifest', href: '/site.webmanifest' }
|
||||
],
|
||||
meta: [{ name: 'apple-mobile-web-app-title', content: 'BuildPath' }],
|
||||
charset: 'utf-8',
|
||||
viewport: 'width=device-width, initial-scale=1'
|
||||
}
|
||||
},
|
||||
|
||||
modules: ['@nuxt/image', '@nuxt/fonts', '@nuxtjs/seo', 'nuxt-umami'],
|
||||
modules: ['@nuxt/image', '@nuxt/fonts', '@nuxtjs/seo', 'nuxt-umami', '@nuxt/eslint'],
|
||||
|
||||
umami: {
|
||||
id: '98ef53ef-5fe1-4e29-a35e-56dc1283c212',
|
||||
@@ -49,12 +45,12 @@ export default defineNuxtConfig({
|
||||
autoTrack: true,
|
||||
domains: ['buildpath.win'],
|
||||
ignoreLocalhost: true,
|
||||
enabled: true,
|
||||
enabled: true
|
||||
},
|
||||
|
||||
fonts: {
|
||||
defaults: {
|
||||
weights: [200, 400],
|
||||
},
|
||||
},
|
||||
})
|
||||
weights: [200, 400]
|
||||
}
|
||||
}
|
||||
})
|
||||
3008
frontend/package-lock.json
generated
3008
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,14 @@
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
"postinstall": "nuxt prepare",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/eslint": "^1.12.1",
|
||||
"@nuxt/fonts": "^0.11.3",
|
||||
"@nuxt/image": "^1.10.0",
|
||||
"@nuxtjs/seo": "^3.0.3",
|
||||
@@ -24,7 +29,15 @@
|
||||
"vue-router": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
||||
"@typescript-eslint/parser": "^8.53.1",
|
||||
"@vue/compiler-sfc": "^3.5.13",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-nuxt": "^4.0.0",
|
||||
"eslint-plugin-vue": "^10.7.0",
|
||||
"prettier": "^3.8.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<Head>
|
||||
<Title>About</Title>
|
||||
</Head>
|
||||
<Head>
|
||||
<Title>About</Title>
|
||||
</Head>
|
||||
|
||||
<div class="about-main-content">
|
||||
<div class="about-main-content">
|
||||
<NavBar :tierlist-list="true" />
|
||||
|
||||
<div style="width: fit-content; margin: auto;">
|
||||
<Logo />
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<h1>About</h1>
|
||||
<h3 style="margin-top: 20px;">BuildPath: a tool for League of Legends champions runes and build paths.</h3>
|
||||
<h3 style="margin-top: 10px;">Copyright (C) Valentin Haudiquet (@vhaudiquet)</h3>
|
||||
<h3 style="margin-top: 20px;">Acknowledgments:</h3>
|
||||
<h3>- Sarah Emery, for the feedback on the designs and code</h3>
|
||||
<h3>- Martin Andrieux, for the nice algorithms :)</h3>
|
||||
<h3>- Paul Chaurand, for the feedback on the league data organization</h3>
|
||||
<h3>- Nathan Mérillon, for the tierlists ideas</h3>
|
||||
<h3>- Jean-Baptiste Döderlein, for the feedback on the mobile design</h3>
|
||||
<h3 style="margin-top: 20px;">Libraries used:</h3>
|
||||
<h3>Vue.JS, Nuxt.JS, Chart.JS, svg-dom-arrows</h3>
|
||||
<h2 style="font-size: 1rem; font-weight: 200; margin-top: 25px; max-width: 800px;">
|
||||
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>
|
||||
<div style="width: fit-content; margin: auto">
|
||||
<Logo />
|
||||
|
||||
<div style="margin-top: 20px">
|
||||
<h1>About</h1>
|
||||
<h3 style="margin-top: 20px">
|
||||
BuildPath: a tool for League of Legends champions runes and build paths.
|
||||
</h3>
|
||||
<h3 style="margin-top: 10px">Copyright (C) Valentin Haudiquet (@vhaudiquet)</h3>
|
||||
<h3 style="margin-top: 20px">Acknowledgments:</h3>
|
||||
<h3>- Sarah Emery, for the feedback on the designs and code</h3>
|
||||
<h3>- Martin Andrieux, for the nice algorithms :)</h3>
|
||||
<h3>- Paul Chaurand, for the feedback on the league data organization</h3>
|
||||
<h3>- Nathan Mérillon, for the tierlists ideas</h3>
|
||||
<h3>- Jean-Baptiste Döderlein, for the feedback on the mobile design</h3>
|
||||
<h3 style="margin-top: 20px">Libraries used:</h3>
|
||||
<h3>Vue.JS, Nuxt.JS, Chart.JS, svg-dom-arrows</h3>
|
||||
<h2 style="font-size: 1rem; font-weight: 200; margin-top: 25px; max-width: 800px">
|
||||
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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.about-main-content {
|
||||
display: flex;
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,99 +2,112 @@
|
||||
const route = useRoute()
|
||||
const championAlias = route.params.alias as string
|
||||
|
||||
const { data : championData } : {data : Ref<ChampionData>} = await useFetch("/api/champion/" + championAlias.toLowerCase())
|
||||
const { data: championData }: { data: Ref<ChampionData> } = await useFetch(
|
||||
'/api/champion/' + championAlias.toLowerCase()
|
||||
)
|
||||
const championId = championData.value.id
|
||||
|
||||
// Prefetch home page for faster navigation
|
||||
useHead({
|
||||
link: [
|
||||
{ rel: 'prefetch', href: '/' }
|
||||
]
|
||||
link: [{ rel: 'prefetch', href: '/' }]
|
||||
})
|
||||
|
||||
defineOgImageComponent('Champion', {
|
||||
title: championData.value.name,
|
||||
id: championId,
|
||||
winrate: championData.value.winrate,
|
||||
pickrate: championData.value.pickrate,
|
||||
gameCount: championData.value.gameCount,
|
||||
title: championData.value.name,
|
||||
id: championId,
|
||||
winrate: championData.value.winrate,
|
||||
pickrate: championData.value.pickrate,
|
||||
gameCount: championData.value.gameCount
|
||||
})
|
||||
useSeoMeta({
|
||||
title: championData.value.name,
|
||||
description: 'Build path and runes for ' + championData.value.name + ' on BuildPath'
|
||||
title: championData.value.name,
|
||||
description: 'Build path and runes for ' + championData.value.name + ' on BuildPath'
|
||||
})
|
||||
|
||||
const laneState = ref(0)
|
||||
const state = ref("runes")
|
||||
const state = ref('runes')
|
||||
const lane = ref(championData.value.lanes[laneState.value])
|
||||
function updateState(newState : string, newLane : number) {
|
||||
state.value = newState
|
||||
laneState.value = newLane
|
||||
lane.value = championData.value.lanes[laneState.value]
|
||||
function updateState(newState: string, newLane: number) {
|
||||
state.value = newState
|
||||
laneState.value = newLane
|
||||
lane.value = championData.value.lanes[laneState.value]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head>
|
||||
<Title>{{ championData.name }}</Title>
|
||||
</Head>
|
||||
<Head>
|
||||
<Title>{{ championData.name }}</Title>
|
||||
</Head>
|
||||
|
||||
<div id="alias-content-wrapper">
|
||||
|
||||
<NavBar :champion-name="championData.name"
|
||||
:champion-lanes="championData.lanes"
|
||||
@state-change="updateState"/>
|
||||
<div id="alias-content-wrapper">
|
||||
<NavBar
|
||||
:champion-name="championData.name"
|
||||
:champion-lanes="championData.lanes"
|
||||
@state-change="updateState"
|
||||
/>
|
||||
|
||||
<div id="champion-content">
|
||||
<ChampionTitle id="champion-title" v-if="championData.gameCount > 0"
|
||||
:champion-id="championId" :winrate="lane.winrate"
|
||||
:pickrate="lane.pickrate" :game-count="lane.count" />
|
||||
<RuneSelector v-if="state == 'runes' && championData.gameCount > 0"
|
||||
style="margin: auto; margin-top: 40px;"
|
||||
:runes="lane.runes!!" />
|
||||
<ItemViewer v-if="state == 'items' && championData.gameCount > 0"
|
||||
style="margin:auto; margin-top: 40px;"
|
||||
:builds="lane.builds!!" />
|
||||
<ItemTree v-if="state == 'alternatives' && championData.gameCount > 0"
|
||||
style="margin: auto; margin-top: 40px; width: fit-content;"
|
||||
:tree="lane.builds!!.tree" />
|
||||
<h2 v-if="championData.gameCount == 0"
|
||||
style="margin: auto; margin-top: 20px; width: fit-content;">
|
||||
Sorry, there is no data for this champion :(
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<ChampionTitle
|
||||
v-if="championData.gameCount > 0"
|
||||
id="champion-title"
|
||||
:champion-id="championId"
|
||||
:winrate="lane.winrate"
|
||||
:pickrate="lane.pickrate"
|
||||
:game-count="lane.count"
|
||||
/>
|
||||
<RuneSelector
|
||||
v-if="state == 'runes' && championData.gameCount > 0"
|
||||
style="margin: auto; margin-top: 40px"
|
||||
:runes="lane.runes!!"
|
||||
/>
|
||||
<ItemViewer
|
||||
v-if="state == 'items' && championData.gameCount > 0"
|
||||
style="margin: auto; margin-top: 40px"
|
||||
:builds="lane.builds!!"
|
||||
/>
|
||||
<ItemTree
|
||||
v-if="state == 'alternatives' && championData.gameCount > 0"
|
||||
style="margin: auto; margin-top: 40px; width: fit-content"
|
||||
:tree="lane.builds!!.tree"
|
||||
/>
|
||||
<h2
|
||||
v-if="championData.gameCount == 0"
|
||||
style="margin: auto; margin-top: 20px; width: fit-content"
|
||||
>
|
||||
Sorry, there is no data for this champion :(
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#alias-content-wrapper {
|
||||
display: flex;
|
||||
/* min-height: 100vh; */
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
/* min-height: 100vh; */
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
#champion-content {
|
||||
margin-top: 64px;
|
||||
margin-left: 39px;
|
||||
width: 100%;
|
||||
margin-top: 64px;
|
||||
margin-left: 39px;
|
||||
width: 100%;
|
||||
}
|
||||
@media only screen and (max-width: 650px) {
|
||||
#champion-content {
|
||||
margin: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
#champion-title {
|
||||
margin:auto;
|
||||
}
|
||||
#champion-content {
|
||||
margin: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
#champion-title {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1200px) {
|
||||
#alias-content-wrapper {
|
||||
flex-direction: column;
|
||||
padding-bottom: 120px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
#alias-content-wrapper {
|
||||
flex-direction: column;
|
||||
padding-bottom: 120px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,63 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { POSITIONS, LANE_IMAGES, POSITIONS_STR } from '~/utils/cdragon';
|
||||
|
||||
import { POSITIONS, LANE_IMAGES, POSITIONS_STR } from '~/utils/cdragon'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head>
|
||||
<Title>Home</Title>
|
||||
</Head>
|
||||
<Head>
|
||||
<Title>Home</Title>
|
||||
</Head>
|
||||
|
||||
<div class="index-main-content">
|
||||
<div class="index-main-content">
|
||||
<NavBar :tierlist-list="true" />
|
||||
|
||||
<ChampionSelector class="index-champion-selector"/>
|
||||
|
||||
</div>
|
||||
<ChampionSelector class="index-champion-selector" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.index-main-content {
|
||||
display: flex;
|
||||
margin-top: 50px;
|
||||
display: flex;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.index-champion-selector {
|
||||
width: 80%;
|
||||
width: 80%;
|
||||
}
|
||||
.index-tierlist-info-container {
|
||||
margin: auto;
|
||||
margin-top: 0px;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
margin-top: 0px;
|
||||
width: fit-content;
|
||||
}
|
||||
#index-tierlists {
|
||||
margin: auto;
|
||||
margin-top: 10px;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
margin-top: 10px;
|
||||
width: fit-content;
|
||||
}
|
||||
@media only screen and (max-width: 1240px) {
|
||||
.index-main-content {
|
||||
flex-direction: column;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.index-champion-selector {
|
||||
width: 100%;
|
||||
}
|
||||
.index-tierlist-info-container {
|
||||
display: flex;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.index-main-content {
|
||||
flex-direction: column;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.index-champion-selector {
|
||||
width: 100%;
|
||||
}
|
||||
.index-tierlist-info-container {
|
||||
display: flex;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
#index-statp {
|
||||
display: none;
|
||||
}
|
||||
.index-tierlist-info-container {
|
||||
margin-bottom: 10px;
|
||||
margin-left: 10px;
|
||||
#index-statp {
|
||||
display: none;
|
||||
}
|
||||
.index-tierlist-info-container {
|
||||
margin-bottom: 10px;
|
||||
margin-left: 10px;
|
||||
|
||||
display: none;
|
||||
}
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,105 +1,129 @@
|
||||
<script setup lang="ts">
|
||||
import { LANE_IMAGES, lanePositionToIndex, POSITIONS_STR } from '~/utils/cdragon';
|
||||
import { LANE_IMAGES, lanePositionToIndex, POSITIONS_STR } from '~/utils/cdragon'
|
||||
|
||||
const route = useRoute()
|
||||
const lane = route.params.lane as string
|
||||
|
||||
const {data: championsData} : ChampionsResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json")
|
||||
const { data: championsData }: ChampionsResponse = await useFetch(
|
||||
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json'
|
||||
)
|
||||
|
||||
const {data: championsLanes} : {data: Ref<Array<ChampionData>>} = await useFetch("/api/champions")
|
||||
const infoMap : Map<string, ChampionData> = new Map()
|
||||
for(let champion of championsLanes.value) {
|
||||
infoMap.set(champion.alias, champion)
|
||||
const { data: championsLanes }: { data: Ref<Array<ChampionData>> } =
|
||||
await useFetch('/api/champions')
|
||||
const infoMap: Map<string, ChampionData> = new Map()
|
||||
for (const champion of championsLanes.value) {
|
||||
infoMap.set(champion.alias, champion)
|
||||
}
|
||||
|
||||
const champions = championsData.value.slice(1).filter((champion) => {
|
||||
const championData : ChampionData | undefined = infoMap.get(champion.alias.toLowerCase())
|
||||
if(championData == undefined) return false;
|
||||
const champions = championsData.value.slice(1).filter(champion => {
|
||||
const championData: ChampionData | undefined = infoMap.get(champion.alias.toLowerCase())
|
||||
if (championData == undefined) return false
|
||||
|
||||
const lanes = championData.lanes
|
||||
return lanes.reduce((acc : boolean, current : {data:string, count:number}) =>
|
||||
acc || (current.data.toLowerCase() == lane.toLowerCase()), false)
|
||||
const lanes = championData.lanes
|
||||
return lanes.reduce(
|
||||
(acc: boolean, current: { data: string; count: number }) =>
|
||||
acc || current.data.toLowerCase() == lane.toLowerCase(),
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
const allChampions = champions.map((x) => {
|
||||
const championData : ChampionData = infoMap.get(x.alias.toLowerCase())!!
|
||||
const allChampions = champions
|
||||
.map(x => {
|
||||
const championData: ChampionData = infoMap.get(x.alias.toLowerCase())!
|
||||
|
||||
let currentLane = championData.lanes[0]
|
||||
for(let championLane of championData.lanes) {
|
||||
if(championLane.data.toLowerCase() == lane.toLowerCase()) {
|
||||
currentLane = championLane
|
||||
break
|
||||
}
|
||||
for (const championLane of championData.lanes) {
|
||||
if (championLane.data.toLowerCase() == lane.toLowerCase()) {
|
||||
currentLane = championLane
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {lane: currentLane, champion: x}
|
||||
}).sort((a, b) => b.lane.pickrate - a.lane.pickrate)
|
||||
return { lane: currentLane, champion: x }
|
||||
})
|
||||
.sort((a, b) => b.lane.pickrate - a.lane.pickrate)
|
||||
|
||||
const p_min = Math.min(...allChampions.map((x) => x.lane.pickrate))
|
||||
const p_max = Math.max(...allChampions.map((x) => x.lane.pickrate))
|
||||
const p_min = Math.min(...allChampions.map(x => x.lane.pickrate))
|
||||
const p_max = Math.max(...allChampions.map(x => x.lane.pickrate))
|
||||
|
||||
allChampions.map((x) => (x as {lane: LaneData, champion: Champion, scaledPickrate: number}).scaledPickrate = (x.lane.pickrate - p_min)/(p_max - p_min))
|
||||
allChampions.map(
|
||||
x =>
|
||||
((x as { lane: LaneData; champion: Champion; scaledPickrate: number }).scaledPickrate =
|
||||
(x.lane.pickrate - p_min) / (p_max - p_min))
|
||||
)
|
||||
allChampions.sort((a, b) => b.lane.pickrate - a.lane.pickrate)
|
||||
function tierFromScaledPickrate(min: number, max: number) {
|
||||
return (allChampions as Array<{lane: LaneData, champion: Champion, scaledPickrate: number}>)
|
||||
.filter(({scaledPickrate: scaledPickrate}) => {
|
||||
return scaledPickrate > min && scaledPickrate <= max
|
||||
})
|
||||
return (
|
||||
allChampions as Array<{ lane: LaneData; champion: Champion; scaledPickrate: number }>
|
||||
).filter(({ scaledPickrate: scaledPickrate }) => {
|
||||
return scaledPickrate > min && scaledPickrate <= max
|
||||
})
|
||||
}
|
||||
|
||||
const tiers: Array<{title:string, data: Array<{lane: LaneData, champion: Champion}>}> = []
|
||||
tiers.push({title: "S", data: tierFromScaledPickrate(0.9, 1)})
|
||||
tiers.push({title: "A", data: tierFromScaledPickrate(0.7, 0.9)})
|
||||
tiers.push({title: "B", data: tierFromScaledPickrate(0.5, 0.7)})
|
||||
tiers.push({title: "C", data: tierFromScaledPickrate(0.3, 0.5)})
|
||||
tiers.push({title: "D", data: tierFromScaledPickrate(0.1, 0.3)})
|
||||
tiers.push({title: "F", data: tierFromScaledPickrate(0, 0.1)})
|
||||
const tiers: Array<{ title: string; data: Array<{ lane: LaneData; champion: Champion }> }> = []
|
||||
tiers.push({ title: 'S', data: tierFromScaledPickrate(0.9, 1) })
|
||||
tiers.push({ title: 'A', data: tierFromScaledPickrate(0.7, 0.9) })
|
||||
tiers.push({ title: 'B', data: tierFromScaledPickrate(0.5, 0.7) })
|
||||
tiers.push({ title: 'C', data: tierFromScaledPickrate(0.3, 0.5) })
|
||||
tiers.push({ title: 'D', data: tierFromScaledPickrate(0.1, 0.3) })
|
||||
tiers.push({ title: 'F', data: tierFromScaledPickrate(0, 0.1) })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head>
|
||||
<Title>Tierlist for {{ POSITIONS_STR[lanePositionToIndex(lane)] }}</Title>
|
||||
</Head>
|
||||
<Head>
|
||||
<Title>Tierlist for {{ POSITIONS_STR[lanePositionToIndex(lane)] }}</Title>
|
||||
</Head>
|
||||
|
||||
<div style="display: flex; min-height: 100vh; align-items: stretch; width: 100%;">
|
||||
<NavBar :tierlist-list="true" />
|
||||
<div style="display: flex; min-height: 100vh; align-items: stretch; width: 100%">
|
||||
<NavBar :tierlist-list="true" />
|
||||
|
||||
<div id="tierlist-container" style="margin-left: 10px; width: 100%; overflow-y: scroll;">
|
||||
<div id="tierlist-container" style="margin-left: 10px; width: 100%; overflow-y: scroll">
|
||||
<div
|
||||
style="
|
||||
margin-left: 0px;
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
margin-bottom: 30px;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<h1 style="margin-left: 10px; font-size: 2.8rem; font-weight: 300">Tierlist for</h1>
|
||||
<NuxtImg
|
||||
format="webp"
|
||||
style="margin-left: 10px"
|
||||
width="50"
|
||||
height="50"
|
||||
:src="LANE_IMAGES[lanePositionToIndex(lane)]"
|
||||
/>
|
||||
<h1 style="margin-left: 10px; font-size: 2.8rem; font-weight: 300">
|
||||
{{ POSITIONS_STR[lanePositionToIndex(lane)] }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 0px; margin-top: 20px; display: flex; margin-bottom: 30px; align-items: center">
|
||||
<h1 style="margin-left: 10px; font-size: 2.8rem; font-weight: 300;">Tierlist for</h1>
|
||||
<NuxtImg format="webp" style="margin-left: 10px;"
|
||||
width="50" height="50"
|
||||
:src="LANE_IMAGES[lanePositionToIndex(lane)]" />
|
||||
<h1 style="margin-left: 10px; font-size: 2.8rem; font-weight: 300;">{{ POSITIONS_STR[lanePositionToIndex(lane)] }}</h1>
|
||||
</div>
|
||||
<TierlistTier v-for="tier in tiers" :title="tier.title" :tier="tier.data" />
|
||||
|
||||
<TierlistTier v-for="tier in tiers" :title="tier.title" :tier="tier.data" />
|
||||
|
||||
<TierlistChart id="chart" :data="tiers" />
|
||||
|
||||
</div>
|
||||
<TierlistChart id="chart" :data="tiers" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#chart {
|
||||
margin-left: 100px;
|
||||
margin-right: 100px;
|
||||
margin-bottom: 100px;
|
||||
margin-top: 40px
|
||||
margin-left: 100px;
|
||||
margin-right: 100px;
|
||||
margin-bottom: 100px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 450px) {
|
||||
#chart {
|
||||
margin-left: 2px;
|
||||
margin-right: 12px;
|
||||
margin-bottom: 40px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
#tierlist-container {
|
||||
padding-bottom: 120px;
|
||||
}
|
||||
#chart {
|
||||
margin-left: 2px;
|
||||
margin-right: 12px;
|
||||
margin-bottom: 40px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
#tierlist-container {
|
||||
padding-bottom: 120px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -18,4 +18,4 @@
|
||||
"theme_color": "#312E2C",
|
||||
"background_color": "#312E2C",
|
||||
"display": "standalone"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { MongoClient } from 'mongodb';
|
||||
import {connectToDatabase, fetchLatestPatch} from '../../utils/mongo'
|
||||
import type { MongoClient } from 'mongodb'
|
||||
import { connectToDatabase, fetchLatestPatch } from '../../utils/mongo'
|
||||
|
||||
async function championInfos(client: MongoClient, patch: string, championAlias: string) {
|
||||
const database = client.db("champions");
|
||||
const collection = database.collection(patch);
|
||||
const query = { alias:championAlias };
|
||||
const championInfo = (await collection.findOne(query)) as unknown as ChampionData;
|
||||
return championInfo
|
||||
const database = client.db('champions')
|
||||
const collection = database.collection(patch)
|
||||
const query = { alias: championAlias }
|
||||
const championInfo = (await collection.findOne(query)) as unknown as ChampionData
|
||||
return championInfo
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const championAlias = (getRouterParam(event, "alias") as string).toLowerCase()
|
||||
const client = await connectToDatabase();
|
||||
const latestPatch = await fetchLatestPatch(client);
|
||||
const data = await championInfos(client, latestPatch, championAlias);
|
||||
await client.close()
|
||||
return data
|
||||
export default defineEventHandler(async event => {
|
||||
const championAlias = (getRouterParam(event, 'alias') as string).toLowerCase()
|
||||
const client = await connectToDatabase()
|
||||
const latestPatch = await fetchLatestPatch(client)
|
||||
const data = await championInfos(client, latestPatch, championAlias)
|
||||
await client.close()
|
||||
return data
|
||||
})
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
import { MongoClient } from 'mongodb';
|
||||
import {connectToDatabase, fetchLatestPatch} from '../utils/mongo'
|
||||
import type { MongoClient } from 'mongodb'
|
||||
import { connectToDatabase, fetchLatestPatch } from '../utils/mongo'
|
||||
|
||||
async function champions(client: MongoClient, patch: string) {
|
||||
const database = client.db("champions");
|
||||
const collection = database.collection(patch);
|
||||
const data : Array<ChampionData> = (await collection.find().toArray()) as unknown as Array<ChampionData>
|
||||
data.map((x) => {
|
||||
if(x.lanes != undefined && x.lanes != null) {
|
||||
for(let lane of x.lanes) {
|
||||
delete lane.builds
|
||||
delete lane.runes
|
||||
}
|
||||
}
|
||||
})
|
||||
return data
|
||||
const database = client.db('champions')
|
||||
const collection = database.collection(patch)
|
||||
const data: Array<ChampionData> = (await collection
|
||||
.find()
|
||||
.toArray()) as unknown as Array<ChampionData>
|
||||
data.map(x => {
|
||||
if (x.lanes != undefined && x.lanes != null) {
|
||||
for (const lane of x.lanes) {
|
||||
delete lane.builds
|
||||
delete lane.runes
|
||||
}
|
||||
}
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (_) => {
|
||||
const client = await connectToDatabase();
|
||||
const latestPatch = await fetchLatestPatch(client);
|
||||
export default defineEventHandler(async _ => {
|
||||
const client = await connectToDatabase()
|
||||
const latestPatch = await fetchLatestPatch(client)
|
||||
|
||||
const data = await champions(client, latestPatch);
|
||||
|
||||
await client.close()
|
||||
const data = await champions(client, latestPatch)
|
||||
|
||||
return data
|
||||
await client.close()
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { CDRAGON_BASE } from "~/utils/cdragon";
|
||||
import { CDRAGON_BASE } from '~/utils/cdragon'
|
||||
|
||||
async function championRoutes() {
|
||||
const championsData : Array<Champion> = await
|
||||
(await fetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json")).json()
|
||||
|
||||
let routes : Array<string> = []
|
||||
for(let champion of championsData) {
|
||||
routes.push("/champion/" + champion.alias.toLowerCase())
|
||||
}
|
||||
return routes
|
||||
const championsData: Array<Champion> = await (
|
||||
await fetch(
|
||||
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json'
|
||||
)
|
||||
).json()
|
||||
|
||||
const routes: Array<string> = []
|
||||
for (const champion of championsData) {
|
||||
routes.push('/champion/' + champion.alias.toLowerCase())
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (_) => {
|
||||
const data = await championRoutes();
|
||||
return data
|
||||
export default defineEventHandler(async _ => {
|
||||
const data = await championRoutes()
|
||||
return data
|
||||
})
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { MongoClient } from 'mongodb';
|
||||
import {connectToDatabase, fetchLatestPatch} from '../utils/mongo'
|
||||
import type { MongoClient } from 'mongodb'
|
||||
import { connectToDatabase, fetchLatestPatch } from '../utils/mongo'
|
||||
|
||||
async function fetchGameCount(client : MongoClient, patch: string) {
|
||||
const database = client.db("matches");
|
||||
const matches = database.collection(patch);
|
||||
const count = await matches.countDocuments()
|
||||
return count
|
||||
async function fetchGameCount(client: MongoClient, patch: string) {
|
||||
const database = client.db('matches')
|
||||
const matches = database.collection(patch)
|
||||
const count = await matches.countDocuments()
|
||||
return count
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (_) => {
|
||||
const client = await connectToDatabase();
|
||||
const latestPatch = await fetchLatestPatch(client);
|
||||
const gameCount = await fetchGameCount(client, latestPatch)
|
||||
|
||||
await client.close()
|
||||
export default defineEventHandler(async _ => {
|
||||
const client = await connectToDatabase()
|
||||
const latestPatch = await fetchLatestPatch(client)
|
||||
const gameCount = await fetchGameCount(client, latestPatch)
|
||||
|
||||
return {patch: latestPatch, count: gameCount}
|
||||
await client.close()
|
||||
|
||||
return { patch: latestPatch, count: gameCount }
|
||||
})
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { MongoClient } from 'mongodb'
|
||||
|
||||
async function connectToDatabase() {
|
||||
// Create a MongoClient with a MongoClientOptions object to set the Stable API version
|
||||
let uri = `mongodb://${process.env.MONGO_USER}:${process.env.MONGO_PASS}@${process.env.MONGO_HOST}`
|
||||
if(process.env.MONGO_URI != undefined && process.env.MONGO_URI != null && process.env.MONGO_URI != "") {
|
||||
uri = process.env.MONGO_URI
|
||||
}
|
||||
const client = new MongoClient(uri)
|
||||
await client.connect()
|
||||
return client
|
||||
// Create a MongoClient with a MongoClientOptions object to set the Stable API version
|
||||
let uri = `mongodb://${process.env.MONGO_USER}:${process.env.MONGO_PASS}@${process.env.MONGO_HOST}`
|
||||
if (
|
||||
process.env.MONGO_URI != undefined &&
|
||||
process.env.MONGO_URI != null &&
|
||||
process.env.MONGO_URI != ''
|
||||
) {
|
||||
uri = process.env.MONGO_URI
|
||||
}
|
||||
const client = new MongoClient(uri)
|
||||
await client.connect()
|
||||
return client
|
||||
}
|
||||
|
||||
async function fetchLatestPatch(client : MongoClient) {
|
||||
const database = client.db("patches");
|
||||
const patches = database.collection("patches");
|
||||
const latestPatch = await patches.find().limit(1).sort({date:-1}).next()
|
||||
return latestPatch!!.patch as string
|
||||
async function fetchLatestPatch(client: MongoClient) {
|
||||
const database = client.db('patches')
|
||||
const patches = database.collection('patches')
|
||||
const latestPatch = await patches.find().limit(1).sort({ date: -1 }).next()
|
||||
return latestPatch!.patch as string
|
||||
}
|
||||
|
||||
export {connectToDatabase, fetchLatestPatch}
|
||||
export { connectToDatabase, fetchLatestPatch }
|
||||
|
||||
@@ -1,74 +1,73 @@
|
||||
declare global {
|
||||
/**
|
||||
* Represents an item in the build tree
|
||||
*/
|
||||
interface ItemTree {
|
||||
count: number;
|
||||
data: number;
|
||||
children: ItemTree[];
|
||||
}
|
||||
/**
|
||||
* Represents an item in the build tree
|
||||
*/
|
||||
interface ItemTree {
|
||||
count: number
|
||||
data: number
|
||||
children: ItemTree[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents champion build information
|
||||
*/
|
||||
interface Builds {
|
||||
start: Array<{count: number, data: number}>;
|
||||
tree: ItemTree;
|
||||
bootsFirst: number;
|
||||
boots: Array<{count: number, data: number}>;
|
||||
lateGame: Array<{count: number, data: number}>;
|
||||
suppItems?: Array<{count: number, data: number}>;
|
||||
}
|
||||
/**
|
||||
* Represents champion build information
|
||||
*/
|
||||
interface Builds {
|
||||
start: Array<{ count: number; data: number }>
|
||||
tree: ItemTree
|
||||
bootsFirst: number
|
||||
boots: Array<{ count: number; data: number }>
|
||||
lateGame: Array<{ count: number; data: number }>
|
||||
suppItems?: Array<{ count: number; data: number }>
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a rune configuration
|
||||
*/
|
||||
interface Rune {
|
||||
count: number;
|
||||
primaryStyle: number;
|
||||
secondaryStyle: number;
|
||||
selections: number[];
|
||||
pickrate: number;
|
||||
}
|
||||
/**
|
||||
* Represents a rune configuration
|
||||
*/
|
||||
interface Rune {
|
||||
count: number
|
||||
primaryStyle: number
|
||||
secondaryStyle: number
|
||||
selections: number[]
|
||||
pickrate: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents lane-specific champion data
|
||||
*/
|
||||
interface LaneData {
|
||||
data: string;
|
||||
count: number;
|
||||
winningMatches: number;
|
||||
losingMatches: number;
|
||||
winrate: number;
|
||||
pickrate: number;
|
||||
runes?: Rune[];
|
||||
builds?: Builds;
|
||||
}
|
||||
/**
|
||||
* Represents lane-specific champion data
|
||||
*/
|
||||
interface LaneData {
|
||||
data: string
|
||||
count: number
|
||||
winningMatches: number
|
||||
losingMatches: number
|
||||
winrate: number
|
||||
pickrate: number
|
||||
runes?: Rune[]
|
||||
builds?: Builds
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents complete champion data
|
||||
*/
|
||||
interface ChampionData {
|
||||
id: number;
|
||||
name: string;
|
||||
alias: string;
|
||||
gameCount: number;
|
||||
winrate: number;
|
||||
pickrate: number;
|
||||
lanes: LaneData[];
|
||||
}
|
||||
/**
|
||||
* Represents complete champion data
|
||||
*/
|
||||
interface ChampionData {
|
||||
id: number
|
||||
name: string
|
||||
alias: string
|
||||
gameCount: number
|
||||
winrate: number
|
||||
pickrate: number
|
||||
lanes: LaneData[]
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Champion summary from CDragon
|
||||
*/
|
||||
interface ChampionSummary {
|
||||
id: number;
|
||||
name: string;
|
||||
alias: string;
|
||||
squarePortraitPath: string;
|
||||
// Add other relevant fields as needed
|
||||
}
|
||||
/**
|
||||
* Champion summary from CDragon
|
||||
*/
|
||||
interface ChampionSummary {
|
||||
id: number
|
||||
name: string
|
||||
alias: string
|
||||
squarePortraitPath: string
|
||||
// Add other relevant fields as needed
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
export {}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
declare global {
|
||||
type ChampionsResponse = {
|
||||
data: Ref<Array<Champion>>
|
||||
}
|
||||
type ChampionResponse = {
|
||||
data: Ref<ChampionFull>
|
||||
}
|
||||
type Champion = {
|
||||
name: string
|
||||
alias: string
|
||||
squarePortraitPath: string
|
||||
}
|
||||
type ChampionFull = {
|
||||
name: string
|
||||
alias: string
|
||||
squarePortraitPath: string
|
||||
title: string
|
||||
}
|
||||
type ItemResponse = {
|
||||
data: Ref<Array<Item>>
|
||||
}
|
||||
type Item = {
|
||||
id: number
|
||||
}
|
||||
type PerksResponse = {
|
||||
data: Ref<Array<Perk>>
|
||||
}
|
||||
type Perk = {
|
||||
id: number
|
||||
name: string
|
||||
iconPath: string
|
||||
}
|
||||
type PerkStylesResponse = {
|
||||
data: Ref<{styles: Array<PerkStyle>}>
|
||||
}
|
||||
type PerkStyle = {
|
||||
id: number
|
||||
name: string
|
||||
iconPath: string
|
||||
slots: Array<{perks:Array<number>}>
|
||||
}
|
||||
type ChampionsResponse = {
|
||||
data: Ref<Array<Champion>>
|
||||
}
|
||||
type ChampionResponse = {
|
||||
data: Ref<ChampionFull>
|
||||
}
|
||||
type Champion = {
|
||||
name: string
|
||||
alias: string
|
||||
squarePortraitPath: string
|
||||
}
|
||||
type ChampionFull = {
|
||||
name: string
|
||||
alias: string
|
||||
squarePortraitPath: string
|
||||
title: string
|
||||
}
|
||||
type ItemResponse = {
|
||||
data: Ref<Array<Item>>
|
||||
}
|
||||
type Item = {
|
||||
id: number
|
||||
}
|
||||
type PerksResponse = {
|
||||
data: Ref<Array<Perk>>
|
||||
}
|
||||
type Perk = {
|
||||
id: number
|
||||
name: string
|
||||
iconPath: string
|
||||
}
|
||||
type PerkStylesResponse = {
|
||||
data: Ref<{ styles: Array<PerkStyle> }>
|
||||
}
|
||||
type PerkStyle = {
|
||||
id: number
|
||||
name: string
|
||||
iconPath: string
|
||||
slots: Array<{ perks: Array<number> }>
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
export {}
|
||||
|
||||
@@ -1,35 +1,55 @@
|
||||
const CDRAGON_BASE = "https://raw.communitydragon.org/latest/"
|
||||
const CDRAGON_BASE = 'https://raw.communitydragon.org/latest/'
|
||||
|
||||
/* Lanes */
|
||||
const POSITIONS = ["top", "jungle", "middle", "bottom", "utility"]
|
||||
const POSITIONS_STR = ["top", "jungle", "mid", "bot", "support"]
|
||||
const LANE_IMAGES = Array(5).fill("").map((_, index) => "/img/lanes/icon-position-" + POSITIONS[index] + ".png")
|
||||
const LANE_IMAGES_HOVER = Array(5).fill("").map((_, index) => "/img/lanes/icon-position-" + POSITIONS[index] + "-hover.png")
|
||||
const LANE_IMAGES_SELECTED = Array(5).fill("").map((_, index) => "/img/lanes/icon-position-" + POSITIONS[index] + "-blue.png")
|
||||
function laneIndexToPosition(index : number) {
|
||||
switch(index) {
|
||||
case 0: return "top"
|
||||
case 1: return "jungle"
|
||||
case 2: return "middle"
|
||||
case 3: return "bottom"
|
||||
case 4: return "utility"
|
||||
}
|
||||
return null
|
||||
const POSITIONS = ['top', 'jungle', 'middle', 'bottom', 'utility']
|
||||
const POSITIONS_STR = ['top', 'jungle', 'mid', 'bot', 'support']
|
||||
const LANE_IMAGES = Array(5)
|
||||
.fill('')
|
||||
.map((_, index) => '/img/lanes/icon-position-' + POSITIONS[index] + '.png')
|
||||
const LANE_IMAGES_HOVER = Array(5)
|
||||
.fill('')
|
||||
.map((_, index) => '/img/lanes/icon-position-' + POSITIONS[index] + '-hover.png')
|
||||
const LANE_IMAGES_SELECTED = Array(5)
|
||||
.fill('')
|
||||
.map((_, index) => '/img/lanes/icon-position-' + POSITIONS[index] + '-blue.png')
|
||||
function laneIndexToPosition(index: number) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return 'top'
|
||||
case 1:
|
||||
return 'jungle'
|
||||
case 2:
|
||||
return 'middle'
|
||||
case 3:
|
||||
return 'bottom'
|
||||
case 4:
|
||||
return 'utility'
|
||||
}
|
||||
return null
|
||||
}
|
||||
function lanePositionToIndex(position : string) {
|
||||
const p = position.toLowerCase()
|
||||
for(let i = 0; i < POSITIONS.length; i++) {
|
||||
if(p == POSITIONS[i]) return i;
|
||||
}
|
||||
return -1;
|
||||
function lanePositionToIndex(position: string) {
|
||||
const p = position.toLowerCase()
|
||||
for (let i = 0; i < POSITIONS.length; i++) {
|
||||
if (p == POSITIONS[i]) return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function mapPath(assetPath : string) {
|
||||
if(assetPath === undefined || assetPath === null) return ""
|
||||
return assetPath.toLowerCase().replace("/lol-game-data/assets/", "plugins/rcp-be-lol-game-data/global/default/")
|
||||
function mapPath(assetPath: string) {
|
||||
if (assetPath === undefined || assetPath === null) return ''
|
||||
return assetPath
|
||||
.toLowerCase()
|
||||
.replace('/lol-game-data/assets/', 'plugins/rcp-be-lol-game-data/global/default/')
|
||||
}
|
||||
|
||||
export {
|
||||
mapPath, CDRAGON_BASE, laneIndexToPosition, lanePositionToIndex,
|
||||
POSITIONS, LANE_IMAGES, LANE_IMAGES_HOVER, LANE_IMAGES_SELECTED, POSITIONS_STR
|
||||
}
|
||||
export {
|
||||
mapPath,
|
||||
CDRAGON_BASE,
|
||||
laneIndexToPosition,
|
||||
lanePositionToIndex,
|
||||
POSITIONS,
|
||||
LANE_IMAGES,
|
||||
LANE_IMAGES_HOVER,
|
||||
LANE_IMAGES_SELECTED,
|
||||
POSITIONS_STR
|
||||
}
|
||||
|
||||
@@ -8,15 +8,18 @@
|
||||
* @param wait Time in milliseconds to wait before calling the function
|
||||
* @returns Debounced function
|
||||
*/
|
||||
export function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void {
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||
export function debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
return function(...args: Parameters<T>): void {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
return function (...args: Parameters<T>): void {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(() => func(...args), wait)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,12 +29,12 @@ export function debounce<T extends (...args: any[]) => any>(func: T, wait: numbe
|
||||
* @returns Parsed JSON or default value
|
||||
*/
|
||||
export function safeJsonParse<T>(data: string, defaultValue: T): T {
|
||||
try {
|
||||
return JSON.parse(data) as T;
|
||||
} catch (error) {
|
||||
console.error('JSON parse error:', error);
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(data) as T
|
||||
} catch (error) {
|
||||
console.error('JSON parse error:', error)
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,7 +44,7 @@ export function safeJsonParse<T>(data: string, defaultValue: T): T {
|
||||
* @returns Formatted percentage string
|
||||
*/
|
||||
export function formatPercentage(value: number, decimals: number = 0): string {
|
||||
return (value * 100).toFixed(decimals) + '%';
|
||||
return (value * 100).toFixed(decimals) + '%'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,8 +53,8 @@ export function formatPercentage(value: number, decimals: number = 0): string {
|
||||
* @returns Capitalized string
|
||||
*/
|
||||
export function capitalize(str: string): string {
|
||||
if (!str) return '';
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||
if (!str) return ''
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,14 +63,14 @@ export function capitalize(str: string): string {
|
||||
* @returns Readable lane name
|
||||
*/
|
||||
export function getLaneName(position: string): string {
|
||||
const laneMap: Record<string, string> = {
|
||||
'top': 'Top',
|
||||
'jungle': 'Jungle',
|
||||
'middle': 'Middle',
|
||||
'bottom': 'Bottom',
|
||||
'utility': 'Support'
|
||||
};
|
||||
return laneMap[position.toLowerCase()] || position;
|
||||
const laneMap: Record<string, string> = {
|
||||
top: 'Top',
|
||||
jungle: 'Jungle',
|
||||
middle: 'Middle',
|
||||
bottom: 'Bottom',
|
||||
utility: 'Support'
|
||||
}
|
||||
return laneMap[position.toLowerCase()] || position
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,7 +79,7 @@ export function getLaneName(position: string): string {
|
||||
* @returns Full image URL
|
||||
*/
|
||||
export function getChampionImageUrl(championAlias: string): string {
|
||||
return `/img/champions/${championAlias.toLowerCase()}.png`;
|
||||
return `/img/champions/${championAlias.toLowerCase()}.png`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +88,7 @@ export function getChampionImageUrl(championAlias: string): string {
|
||||
* @returns Full item image URL
|
||||
*/
|
||||
export function getItemImageUrl(itemId: number): string {
|
||||
return `${CDRAGON_BASE}plugins/rcp-be-lol-game-data/global/default/v1/items/${itemId}.png`;
|
||||
return `${CDRAGON_BASE}plugins/rcp-be-lol-game-data/global/default/v1/items/${itemId}.png`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +97,7 @@ export function getItemImageUrl(itemId: number): string {
|
||||
* @returns Full rune image URL
|
||||
*/
|
||||
export function getRuneImageUrl(runeId: number): string {
|
||||
return `${CDRAGON_BASE}plugins/rcp-be-lol-game-data/global/default/v1/perks/${runeId}.png`;
|
||||
return `${CDRAGON_BASE}plugins/rcp-be-lol-game-data/global/default/v1/perks/${runeId}.png`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,12 +106,12 @@ export function getRuneImageUrl(runeId: number): string {
|
||||
* @returns Formatted string
|
||||
*/
|
||||
export function formatLargeNumber(num: number): string {
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
return num.toString();
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M'
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K'
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +120,7 @@ export function formatLargeNumber(num: number): string {
|
||||
* @returns Cloned object
|
||||
*/
|
||||
export function deepClone<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,11 +129,11 @@ export function deepClone<T>(obj: T): T {
|
||||
* @returns True if value is empty
|
||||
*/
|
||||
export function isEmpty(value: any): boolean {
|
||||
if (value === null || value === undefined) return true;
|
||||
if (typeof value === 'string' && value.trim() === '') return true;
|
||||
if (Array.isArray(value) && value.length === 0) return true;
|
||||
if (typeof value === 'object' && Object.keys(value).length === 0) return true;
|
||||
return false;
|
||||
if (value === null || value === undefined) return true
|
||||
if (typeof value === 'string' && value.trim() === '') return true
|
||||
if (Array.isArray(value) && value.length === 0) return true
|
||||
if (typeof value === 'object' && Object.keys(value).length === 0) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,9 +142,9 @@ export function isEmpty(value: any): boolean {
|
||||
* @returns CSS color class
|
||||
*/
|
||||
export function getWinrateColor(winrate: number): string {
|
||||
if (winrate > 0.55) return 'text-green-500';
|
||||
if (winrate < 0.45) return 'text-red-500';
|
||||
return 'text-yellow-500';
|
||||
if (winrate > 0.55) return 'text-green-500'
|
||||
if (winrate < 0.45) return 'text-red-500'
|
||||
return 'text-yellow-500'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,15 +153,15 @@ export function getWinrateColor(winrate: number): string {
|
||||
* @returns Formatted duration string
|
||||
*/
|
||||
export function formatDuration(ms: number): string {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const seconds = Math.floor(ms / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${seconds % 60}s`;
|
||||
} else {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${seconds % 60}s`
|
||||
} else {
|
||||
return `${seconds}s`
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user