frontend: refactor of the new build viewer
extracting the logic into composables
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { isEmpty, deepClone } from '~/utils/helpers'
|
||||
import { deepClone } from '~/utils/helpers'
|
||||
import { trimBuilds, trimLateGameItems, getHighestPickrateBuildIndex, getFirstCoreItems } from '~/utils/buildHelpers'
|
||||
import BuildVariantSelector from '~/components/build/BuildVariantSelector.vue'
|
||||
import SummonerSpells from '~/components/build/SummonerSpells.vue'
|
||||
import CompactRuneSelector from '~/components/build/CompactRuneSelector.vue'
|
||||
@@ -14,60 +15,18 @@ const props = defineProps<{
|
||||
pickrate: number
|
||||
}>
|
||||
builds: Builds
|
||||
summonerSpells?: Array<{ id: number; count: number; pickrate: number }> // API data when available
|
||||
summonerSpells?: Array<{ id: number; count: number; pickrate: number }>
|
||||
}>()
|
||||
|
||||
// State
|
||||
const currentlySelectedBuild = ref(0)
|
||||
|
||||
// Fetch items from cached API
|
||||
const { data: items } = useFetch<Array<Item>>('/api/cdragon/items', {
|
||||
lazy: true,
|
||||
server: false
|
||||
})
|
||||
const itemMap = ref<Map<number, Item>>(new Map())
|
||||
// Use composables for data fetching
|
||||
const { itemMap } = useItemMap()
|
||||
const { summonerSpellMap } = useSummonerSpellMap()
|
||||
|
||||
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 }
|
||||
)
|
||||
|
||||
// Fetch summoner spells from cached API
|
||||
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 }
|
||||
)
|
||||
// Use composable for rune styles
|
||||
const { perks, primaryStyles, secondaryStyles, keystoneIds } = useRuneStyles(toRef(props, 'runes'))
|
||||
|
||||
// Mock summoner spells data if not provided by API
|
||||
const mockSummonerSpells = computed(() => {
|
||||
@@ -88,7 +47,7 @@ const builds = ref<Builds>(deepClone(props.builds))
|
||||
|
||||
watch(
|
||||
() => props.builds,
|
||||
newBuilds => {
|
||||
(newBuilds) => {
|
||||
builds.value = deepClone(newBuilds)
|
||||
trimBuilds(builds.value)
|
||||
trimLateGameItems(builds.value)
|
||||
@@ -101,117 +60,22 @@ onMounted(() => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
// Computed properties using utility functions
|
||||
const firstCoreItems = computed(() => getFirstCoreItems(props.runes, builds.value))
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const highestPickrateBuildIndex = computed(() => getHighestPickrateBuildIndex(props.runes))
|
||||
|
||||
// Reset selected build when runes change
|
||||
watch(
|
||||
() => props.runes,
|
||||
() => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
||||
30
frontend/composables/useItemMap.ts
Normal file
30
frontend/composables/useItemMap.ts
Normal 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 }
|
||||
}
|
||||
93
frontend/composables/useRuneStyles.ts
Normal file
93
frontend/composables/useRuneStyles.ts
Normal 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
|
||||
}
|
||||
}
|
||||
33
frontend/composables/useSummonerSpellMap.ts
Normal file
33
frontend/composables/useSummonerSpellMap.ts
Normal 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 }
|
||||
}
|
||||
69
frontend/utils/buildHelpers.ts
Normal file
69
frontend/utils/buildHelpers.ts
Normal 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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user