Compare commits

...

2 Commits

Author SHA1 Message Date
c362d6b12a frontend: refactor build viewer a bit more 2026-02-28 13:38:14 +01:00
7833780bcb frontend: refactor of the new build viewer
extracting the logic into composables
2026-02-28 13:29:33 +01:00
9 changed files with 301 additions and 680 deletions

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { isEmpty, deepClone } from '~/utils/helpers' import { getHighestPickrateBuildIndex, getFirstCoreItems } from '~/utils/buildHelpers'
import { MOCK_SUMMONER_SPELLS, BOOTS_RUSH_THRESHOLD } from '~/utils/mockData'
import BuildVariantSelector from '~/components/build/BuildVariantSelector.vue' import BuildVariantSelector from '~/components/build/BuildVariantSelector.vue'
import SummonerSpells from '~/components/build/SummonerSpells.vue' import SummonerSpells from '~/components/build/SummonerSpells.vue'
import CompactRuneSelector from '~/components/build/CompactRuneSelector.vue' import CompactRuneSelector from '~/components/build/CompactRuneSelector.vue'
@@ -14,204 +15,49 @@ const props = defineProps<{
pickrate: number pickrate: number
}> }>
builds: Builds builds: Builds
summonerSpells?: Array<{ id: number; count: number; pickrate: number }> // API data when available summonerSpells?: Array<{ id: number; count: number; pickrate: number }>
}>() }>()
// State // State
const currentlySelectedBuild = ref(0) const currentlySelectedBuild = ref(0)
// Fetch items from cached API // Use composables for data fetching
const { data: items } = useFetch<Array<Item>>('/api/cdragon/items', { const { itemMap } = useItemMap()
lazy: true, const { summonerSpellMap } = useSummonerSpellMap()
server: false
})
const itemMap = ref<Map<number, Item>>(new Map())
watch( // Use composable for rune styles
items, const { perks, primaryStyles, secondaryStyles, keystoneIds } = useRuneStyles(toRef(props, 'runes'))
newItems => {
if (Array.isArray(newItems)) { // Use composable for builds management
const map = new Map<number, Item>() const { builds } = useBuilds(toRef(props, 'builds'))
for (const item of newItems) {
if (item?.id) { // Summoner spells data - use提供的 or fall back to mock
map.set(item.id, item) const displaySummonerSpells = computed(() =>
} (props.summonerSpells && props.summonerSpells.length > 0)
} ? props.summonerSpells
itemMap.value = map : MOCK_SUMMONER_SPELLS
}
},
{ immediate: true }
) )
// Fetch summoner spells from cached API // Computed properties using utility functions
const { data: summonerSpellsData } = useFetch<Array<SummonerSpell>>( const firstCoreItems = computed(() => getFirstCoreItems(props.runes, builds.value))
'/api/cdragon/summoner-spells',
{
lazy: true,
server: false
}
)
const summonerSpellMap = ref<Map<number, SummonerSpell>>(new Map())
watch( const highestPickrateBuildIndex = computed(() => getHighestPickrateBuildIndex(props.runes))
summonerSpellsData,
newData => { const bootsLabel = computed(() =>
if (Array.isArray(newData)) { builds.value.bootsFirst > BOOTS_RUSH_THRESHOLD ? 'Boots Rush' : 'Boots'
const map = new Map<number, SummonerSpell>()
for (const spell of newData) {
if (spell?.id) {
map.set(spell.id, spell)
}
}
summonerSpellMap.value = map
}
},
{ immediate: true }
) )
// Mock summoner spells data if not provided by API // Reset selected build when runes change
const mockSummonerSpells = computed(() => {
if (props.summonerSpells && props.summonerSpells.length > 0) {
return props.summonerSpells
}
// Default mock data based on common summoner spells
return [
{ id: 4, count: 1000, pickrate: 0.45 }, // Flash
{ id: 7, count: 800, pickrate: 0.35 }, // Heal
{ id: 14, count: 600, pickrate: 0.15 }, // Ignite
{ id: 3, count: 200, pickrate: 0.05 } // Exhaust
]
})
// Builds management
const builds = ref<Builds>(deepClone(props.builds))
watch(
() => props.builds,
newBuilds => {
builds.value = deepClone(newBuilds)
trimBuilds(builds.value)
trimLateGameItems(builds.value)
},
{ deep: true }
)
onMounted(() => {
trimBuilds(builds.value)
trimLateGameItems(builds.value)
})
function trimBuilds(builds: Builds): void {
if (!builds?.tree?.children) return
builds.tree.children.splice(1, builds.tree.children.length - 1)
if (builds.tree.children[0]?.children) {
builds.tree.children[0].children.splice(1, builds.tree.children[0].children.length - 1)
}
}
function trimLateGameItems(builds: Builds): void {
if (!builds?.tree || isEmpty(builds.lateGame)) return
function trimLateGameItemsFromTree(tree: ItemTree): void {
const foundIndex = builds.lateGame.findIndex(x => x.data === tree.data)
if (foundIndex !== -1) {
builds.lateGame.splice(foundIndex, 1)
}
for (const child of tree.children || []) {
trimLateGameItemsFromTree(child)
}
}
trimLateGameItemsFromTree(builds.tree)
}
// Get first core item for build variant display
const firstCoreItems = computed(() => {
const result: number[] = []
for (let i = 0; i < props.runes.length; i++) {
const tree = builds.value?.tree
if (tree?.children?.[0]?.data) {
result.push(tree.children[0].data)
} else if (tree?.data) {
result.push(tree.data)
} else {
result.push(0)
}
}
return result
})
// Get the highest pickrate rune build
const highestPickrateBuildIndex = computed(() => {
if (props.runes.length === 0) return 0
let maxIndex = 0
let maxPickrate = props.runes[0].pickrate
for (let i = 1; i < props.runes.length; i++) {
if (props.runes[i].pickrate > maxPickrate) {
maxPickrate = props.runes[i].pickrate
maxIndex = i
}
}
return maxIndex
})
// State for compact rune selector
const selectedRuneIndex = ref(0)
watch(
() => currentlySelectedBuild,
() => {
selectedRuneIndex.value = 0
}
)
function selectRune(index: number) {
selectedRuneIndex.value = index
currentlySelectedBuild.value = index
}
// Rune styles
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 { data: perks_data }: PerksResponse = await useFetch('/api/cdragon/perks')
const perks = reactive(new Map())
for (const perk of perks_data.value) {
perks.set(perk.id, perk)
}
const { data: stylesData }: PerkStylesResponse = await useFetch('/api/cdragon/perkstyles')
function refreshStylesKeystones() {
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
}
}
}
}
watch( watch(
() => props.runes, () => props.runes,
() => { () => {
currentlySelectedBuild.value = 0 currentlySelectedBuild.value = 0
primaryStyles.value = Array(props.runes.length)
secondaryStyles.value = Array(props.runes.length)
keystoneIds.value = Array(props.runes.length)
refreshStylesKeystones()
} }
) )
refreshStylesKeystones() function selectRune(index: number): void {
currentlySelectedBuild.value = index
}
</script> </script>
<template> <template>
@@ -230,7 +76,7 @@ refreshStylesKeystones()
<!-- Left Column: Summoner Spells + Runes --> <!-- Left Column: Summoner Spells + Runes -->
<div class="build-left-column"> <div class="build-left-column">
<!-- Summoner Spells --> <!-- Summoner Spells -->
<SummonerSpells :spells="mockSummonerSpells" :summoner-spell-map="summonerSpellMap" /> <SummonerSpells :spells="displaySummonerSpells" :summoner-spell-map="summonerSpellMap" />
<!-- Rune Page --> <!-- Rune Page -->
<div class="rune-section"> <div class="rune-section">

View File

@@ -1,346 +0,0 @@
<script setup lang="ts">
import { isEmpty, deepClone } from '~/utils/helpers'
const props = defineProps<{
builds: Builds
loading?: boolean
error?: boolean
}>()
// State - use cached API endpoint instead of direct CDragon fetch
const {
data: items,
pending: loadingItems,
error: itemsError
} = useFetch('/api/cdragon/items', {
lazy: true, // Don't block rendering
server: false // Client-side only
})
const itemMap = ref<Map<number, unknown>>(new Map())
// Initialize item map
watch(
items,
newItems => {
try {
const itemsData = newItems || []
if (Array.isArray(itemsData)) {
const map = new Map<number, unknown>()
for (const item of itemsData) {
if (item?.id) {
map.set(item.id, item)
}
}
itemMap.value = map
}
} catch (error) {
console.error('Error initializing item map:', error)
}
},
{ immediate: true }
)
// Builds management
const builds = ref<Builds>(deepClone(props.builds))
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)
})
/**
* Trim builds tree to show only primary build paths
*/
function trimBuilds(builds: Builds): void {
if (!builds?.tree?.children) return
// Keep only the first child (primary build path)
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)
}
}
/**
* Remove items from lateGame that are already in the build tree
*/
function trimLateGameItems(builds: Builds): void {
if (!builds?.tree || isEmpty(builds.lateGame)) return
function trimLateGameItemsFromTree(tree: ItemTree): void {
const foundIndex = builds.lateGame.findIndex(x => x.data === tree.data)
if (foundIndex !== -1) {
builds.lateGame.splice(foundIndex, 1)
}
for (const child of tree.children || []) {
trimLateGameItemsFromTree(child)
}
}
trimLateGameItemsFromTree(builds.tree)
}
// Error and loading states
const _hasError = computed(() => itemsError.value || props.error)
const _isLoading = computed(() => loadingItems.value || props.loading)
</script>
<template>
<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"
:key="item.data"
style="margin-left: 5px; margin-right: 5px"
>
<NuxtImg
v-if="item.data != null && item.data != undefined && itemMap.get(item.data)"
class="item-img"
width="64"
height="64"
:alt="item.data.toString()"
:src="CDRAGON_BASE + mapPath((itemMap.get(item.data) as any).iconPath)"
/>
<div
v-else
style="
width: 64px;
height: 64px;
margin: 10px;
background: var(--color-surface);
border: 1px solid var(--color-on-surface);
"
></div>
<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"
:key="item.data"
style="margin-left: 5px; margin-right: 5px"
>
<NuxtImg
v-if="item.data != null && item.data != undefined && itemMap.get(item.data)"
class="item-img"
width="64"
height="64"
:alt="item.data.toString()"
:src="CDRAGON_BASE + mapPath((itemMap.get(item.data) as any).iconPath)"
/>
<div
v-else
style="
width: 64px;
height: 64px;
margin: 10px;
background: var(--color-surface);
border: 1px solid var(--color-on-surface);
"
></div>
<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"
:key="item.data"
style="margin-left: 5px; margin-right: 5px"
>
<NuxtImg
v-if="item.data != null && item.data != undefined && itemMap.get(item.data)"
class="item-img"
width="64"
height="64"
:alt="item.data.toString()"
:src="CDRAGON_BASE + mapPath((itemMap.get(item.data) as any).iconPath)"
/>
<div
v-else
style="
width: 64px;
height: 64px;
margin: 10px;
background: var(--color-surface);
border: 1px solid var(--color-on-surface);
"
></div>
<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)"
:key="item.data"
style="margin-left: 5px; margin-right: 5px"
>
<NuxtImg
v-if="item.data != null && item.data != undefined && itemMap.get(item.data)"
class="item-img"
width="64"
height="64"
:alt="item.data.toString()"
:src="CDRAGON_BASE + mapPath((itemMap.get(item.data) as any).iconPath)"
/>
<div
v-else
style="
width: 64px;
height: 64px;
margin: 10px;
background: var(--color-surface);
border: 1px solid var(--color-on-surface);
"
></div>
<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)"
:key="item.data"
style="margin-left: 5px; margin-right: 5px"
>
<NuxtImg
v-if="item.data != null && item.data != undefined && itemMap.get(item.data)"
class="item-img"
width="64"
height="64"
:alt="item.data.toString()"
:src="CDRAGON_BASE + mapPath((itemMap.get(item.data) as any).iconPath)"
/>
<div
v-else
style="
width: 64px;
height: 64px;
margin: 10px;
background: var(--color-surface);
border: 1px solid var(--color-on-surface);
"
></div>
<h3 style="width: fit-content; margin: auto; margin-bottom: 10px">
{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%
</h3>
</div>
</div>
<div v-if="builds.lateGame.length > 4" class="iv-items-container">
<div
v-for="item in builds.lateGame.slice(4, 8)"
:key="item.data"
style="margin-left: 5px; margin-right: 5px"
>
<NuxtImg
v-if="item.data != null && item.data != undefined && itemMap.get(item.data)"
class="item-img"
width="64"
height="64"
:alt="item.data.toString()"
:src="CDRAGON_BASE + mapPath((itemMap.get(item.data) as any).iconPath)"
/>
<div
v-else
style="
width: 64px;
height: 64px;
margin: 10px;
background: var(--color-surface);
border: 1px solid var(--color-on-surface);
"
></div>
<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;
}
.iv-items-container {
display: flex;
flex-direction: column;
width: fit-content;
height: fit-content;
margin: auto;
}
.item-img {
border: 1px solid var(--color-on-surface);
margin: 10px;
}
#iv-late-game-container {
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;
}
}
</style>

View File

@@ -1,152 +0,0 @@
<script setup lang="ts">
const props = defineProps<{
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))
// Use cached API endpoints instead of direct CDragon fetch
const { data: perks_data }: PerksResponse = await useFetch('/api/cdragon/perks')
const perks = reactive(new Map())
for (const perk of perks_data.value) {
perks.set(perk.id, perk)
}
const { data: stylesData }: PerkStylesResponse = await useFetch('/api/cdragon/perkstyles')
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 (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
}
</script>
<template>
<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" :key="i" @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>
</template>
<style>
.rune-selector-entry {
width: 200px;
height: 120px;
margin-left: 10px;
margin-right: 10px;
border-radius: 8%;
border: 1px solid var(--color-on-surface);
}
.rune-selector-entry:hover {
cursor: pointer;
}
.rune-selector-entry-selected {
background-color: var(--color-surface-darker);
}
.rs-styles-container {
display: flex;
margin-top: 20px;
}
.rs-pickrate {
text-align: center;
margin-top: -40px;
padding-bottom: 40px;
}
@media only screen and (max-width: 650px) {
.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;
}
}
</style>

View File

@@ -0,0 +1,32 @@
/**
* Composable for managing build data with automatic trimming
* Handles deep cloning and tree manipulation
*/
import { deepClone } from '~/utils/helpers'
import { trimBuilds, trimLateGameItems } from '~/utils/buildHelpers'
export const useBuilds = (buildsProp: Ref<Builds>) => {
const builds = ref<Builds>(deepClone(buildsProp.value))
function trimBuildData(): void {
trimBuilds(builds.value)
trimLateGameItems(builds.value)
}
// Watch for changes and rebuild
watch(
() => buildsProp.value,
(newBuilds) => {
builds.value = deepClone(newBuilds)
trimBuildData()
},
{ deep: true }
)
// Initial trim on mount
onMounted(() => {
trimBuildData()
})
return { builds }
}

View File

@@ -0,0 +1,30 @@
/**
* Composable for fetching and managing item data from CDragon API
* Returns a reactive Map of item ID to item data
*/
export const useItemMap = () => {
const { data: items } = useFetch<Array<Item>>('/api/cdragon/items', {
lazy: true,
server: false
})
const itemMap = ref<Map<number, Item>>(new Map())
watch(
items,
(newItems) => {
if (Array.isArray(newItems)) {
const map = new Map<number, Item>()
for (const item of newItems) {
if (item?.id) {
map.set(item.id, item)
}
}
itemMap.value = map
}
},
{ immediate: true }
)
return { itemMap }
}

View File

@@ -0,0 +1,93 @@
/**
* Composable for fetching and managing rune styles and keystones
* Transforms rune data into format needed for display components
*/
export const useRuneStyles = (runes: Ref<Array<{
count: number
primaryStyle: number
secondaryStyle: number
selections: Array<number>
pickrate: number
}>>) => {
const primaryStyles = ref<Array<PerkStyle>>(Array(runes.value.length))
const secondaryStyles = ref<Array<PerkStyle>>(Array(runes.value.length))
const keystoneIds = ref<Array<number>>(Array(runes.value.length))
const { data: perksData } = useFetch('/api/cdragon/perks')
const { data: stylesData } = useFetch('/api/cdragon/perkstyles')
const perks = reactive(new Map<number, Perk>())
watch(
perksData,
(newPerks) => {
if (Array.isArray(newPerks)) {
perks.clear()
for (const perk of newPerks) {
if (perk?.id) {
perks.set(perk.id, perk)
}
}
}
},
{ immediate: true }
)
function refreshStylesKeystones(): void {
if (!stylesData.value?.styles) return
primaryStyles.value = Array(runes.value.length)
secondaryStyles.value = Array(runes.value.length)
keystoneIds.value = Array(runes.value.length)
for (const style of stylesData.value.styles) {
for (const rune of runes.value) {
const runeIndex = runes.value.indexOf(rune)
if (style.id === rune.primaryStyle) {
primaryStyles.value[runeIndex] = style
// Find keystone from first slot
if (style.slots?.[0]?.perks) {
for (const perk of style.slots[0].perks) {
if (rune.selections.includes(perk)) {
keystoneIds.value[runeIndex] = perk
break
}
}
}
}
if (style.id === rune.secondaryStyle) {
secondaryStyles.value[runeIndex] = style
}
}
}
}
// Refresh when styles data loads or runes change
watch(
[stylesData, runes],
() => {
refreshStylesKeystones()
},
{ immediate: true }
)
// Reset when runes array changes
watch(
() => runes.value.length,
() => {
primaryStyles.value = Array(runes.value.length)
secondaryStyles.value = Array(runes.value.length)
keystoneIds.value = Array(runes.value.length)
refreshStylesKeystones()
}
)
return {
perks,
primaryStyles,
secondaryStyles,
keystoneIds
}
}

View File

@@ -0,0 +1,33 @@
/**
* Composable for fetching and managing summoner spell data from CDragon API
* Returns a reactive Map of spell ID to spell data
*/
export const useSummonerSpellMap = () => {
const { data: summonerSpellsData } = useFetch<Array<SummonerSpell>>(
'/api/cdragon/summoner-spells',
{
lazy: true,
server: false
}
)
const summonerSpellMap = ref<Map<number, SummonerSpell>>(new Map())
watch(
summonerSpellsData,
(newData) => {
if (Array.isArray(newData)) {
const map = new Map<number, SummonerSpell>()
for (const spell of newData) {
if (spell?.id) {
map.set(spell.id, spell)
}
}
summonerSpellMap.value = map
}
},
{ immediate: true }
)
return { summonerSpellMap }
}

View File

@@ -0,0 +1,69 @@
import { isEmpty } from './helpers'
/**
* Trims the build tree to only show the first path
* Removes alternate build paths to keep the UI clean
*/
export function trimBuilds(builds: Builds): void {
if (!builds?.tree?.children) return
// Keep only the first child (primary build path)
builds.tree.children.splice(1, builds.tree.children.length - 1)
// Also trim grandchildren to first path only
if (builds.tree.children[0]?.children) {
builds.tree.children[0].children.splice(1, builds.tree.children[0].children.length - 1)
}
}
/**
* Removes late game items that appear in the core build tree
* Prevents duplicate items from being shown
*/
export function trimLateGameItems(builds: Builds): void {
if (!builds?.tree || isEmpty(builds.lateGame)) return
const coreItemIds = new Set<number>()
// Collect all item IDs from the tree
function collectItemIds(tree: ItemTree): void {
if (tree.data !== undefined) {
coreItemIds.add(tree.data)
}
for (const child of tree.children || []) {
collectItemIds(child)
}
}
collectItemIds(builds.tree)
// Remove late game items that appear in core
builds.lateGame = builds.lateGame.filter((item) => !coreItemIds.has(item.data))
}
/**
* Gets the index of the build with the highest pickrate
*/
export function getHighestPickrateBuildIndex(
runes: Array<{ pickrate: number }>
): number {
if (runes.length === 0) return 0
return runes.reduce((maxIdx, rune, idx, arr) =>
rune.pickrate > arr[maxIdx].pickrate ? idx : maxIdx,
0
)
}
/**
* Gets the first core item for each build variant
*/
export function getFirstCoreItems(
runes: unknown[],
builds: Builds
): number[] {
return runes.map(() => {
const tree = builds?.tree
return tree?.children?.[0]?.data ?? tree?.data ?? 0
})
}

View File

@@ -0,0 +1,16 @@
/**
* Mock data for development and fallback scenarios
* Used when API data is not available
*/
export const MOCK_SUMMONER_SPELLS = [
{ id: 4, count: 1000, pickrate: 0.45 }, // Flash
{ id: 7, count: 800, pickrate: 0.35 }, // Heal
{ id: 14, count: 600, pickrate: 0.15 }, // Ignite
{ id: 3, count: 200, pickrate: 0.05 } // Exhaust
]
/**
* Constants used throughout the application
*/
export const BOOTS_RUSH_THRESHOLD = 0.5