frontend: refactor of the new build viewer

This commit is contained in:
2026-02-28 13:18:02 +01:00
parent 3e9a8295b2
commit 20ccb20738
6 changed files with 581 additions and 372 deletions

View File

@@ -1,5 +1,9 @@
<script setup lang="ts">
import { isEmpty, deepClone } from '~/utils/helpers'
import BuildVariantSelector from '~/components/build/BuildVariantSelector.vue'
import SummonerSpells from '~/components/build/SummonerSpells.vue'
import CompactRuneSelector from '~/components/build/CompactRuneSelector.vue'
import ItemRow from '~/components/build/ItemRow.vue'
const props = defineProps<{
runes: Array<{
@@ -136,6 +140,35 @@ const firstCoreItems = computed(() => {
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))
@@ -179,64 +212,25 @@ watch(
)
refreshStylesKeystones()
function selectBuild(index: number) {
currentlySelectedBuild.value = index
}
</script>
<template>
<div class="build-viewer">
<!-- Global Build Variant Selector -->
<div class="build-variant-selector">
<div
v-for="(_, i) in runes"
:key="i"
:class="['build-variant-card', { selected: i === currentlySelectedBuild }]"
@click="selectBuild(i)"
>
<div class="variant-content">
<!-- Keystone -->
<NuxtImg
v-if="keystoneIds[i] && perks.get(keystoneIds[i])"
class="variant-keystone"
:src="CDRAGON_BASE + mapPath(perks.get(keystoneIds[i]).iconPath)"
/>
<!-- First core item -->
<NuxtImg
v-if="itemMap.get(firstCoreItems[i])"
class="variant-item"
:src="CDRAGON_BASE + mapPath(itemMap.get(firstCoreItems[i])!.iconPath)"
/>
<div v-else class="variant-item-placeholder" />
</div>
<span class="variant-pickrate">{{ (runes[i].pickrate * 100).toFixed(1) }}%</span>
</div>
</div>
<!-- Global Build Variant Selector - Single variant with highest pickrate -->
<BuildVariantSelector
:keystone-id="keystoneIds[highestPickrateBuildIndex]"
:item-id="firstCoreItems[highestPickrateBuildIndex]"
:keystore="perks"
:item-map="itemMap"
:pickrate="1"
/>
<!-- Main Build Content -->
<div class="build-content">
<!-- Left Column: Summoner Spells + Runes -->
<div class="build-left-column">
<!-- Summoner Spells -->
<div class="summoner-spells-section">
<h3 class="section-title">Summoner Spells</h3>
<div class="summoner-spells-row">
<div
v-for="(spell, i) in mockSummonerSpells.slice(0, 2)"
:key="i"
class="summoner-spell-item"
>
<NuxtImg
v-if="summonerSpellMap.get(spell.id)"
class="summoner-spell-img"
:src="CDRAGON_BASE + mapPath(summonerSpellMap.get(spell.id)!.iconPath)"
/>
<div v-else class="summoner-spell-placeholder" />
<span class="spell-pickrate">{{ (spell.pickrate * 100).toFixed(0) }}%</span>
</div>
</div>
</div>
<SummonerSpells :spells="mockSummonerSpells" :summoner-spell-map="summonerSpellMap" />
<!-- Rune Page -->
<div class="rune-section">
@@ -249,6 +243,17 @@ function selectBuild(index: number) {
:selection-ids="runes[currentlySelectedBuild].selections"
/>
</div>
<!-- Compact Rune Selector -->
<CompactRuneSelector
:runes="runes"
:primary-styles="primaryStyles"
:secondary-styles="secondaryStyles"
:keystone-ids="keystoneIds"
:perks="perks"
:selected-index="currentlySelectedBuild"
@select="selectRune"
/>
</div>
</div>
@@ -259,60 +264,31 @@ function selectBuild(index: number) {
<!-- Start/Support + Boots Container -->
<div class="item-row-group">
<!-- Start Items -->
<div v-if="!builds.suppItems" class="item-row">
<span class="item-row-label">Start</span>
<div class="item-row-content">
<div v-for="item in builds.start" :key="item.data" class="item-cell">
<NuxtImg
v-if="itemMap.get(item.data)"
class="item-img"
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data)!.iconPath)"
/>
<div v-else class="item-placeholder" />
<span class="item-pickrate"
>{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%</span
>
</div>
</div>
</div>
<ItemRow
v-if="!builds.suppItems"
label="Start"
:items="builds.start"
:item-map="itemMap"
:total-count="builds.tree.count"
/>
<!-- Support Items -->
<div v-if="builds.suppItems" class="item-row">
<span class="item-row-label">Support</span>
<div class="item-row-content">
<div v-for="item in builds.suppItems" :key="item.data" class="item-cell">
<NuxtImg
v-if="itemMap.get(item.data)"
class="item-img"
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data)!.iconPath)"
/>
<div v-else class="item-placeholder" />
<span class="item-pickrate"
>{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%</span
>
</div>
</div>
</div>
<ItemRow
v-if="builds.suppItems"
label="Support"
:items="builds.suppItems"
:item-map="itemMap"
:total-count="builds.tree.count"
/>
<!-- Boots (regular or rush) -->
<div class="item-row">
<span class="item-row-label">{{
builds.bootsFirst > 0.5 ? 'Boots Rush' : 'Boots'
}}</span>
<div class="item-row-content">
<div v-for="item in builds.boots.slice(0, 2)" :key="item.data" class="item-cell">
<NuxtImg
v-if="itemMap.get(item.data)"
class="item-img"
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data)!.iconPath)"
/>
<div v-else class="item-placeholder" />
<span class="item-pickrate"
>{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%</span
>
</div>
</div>
</div>
<ItemRow
:label="builds.bootsFirst > 0.5 ? 'Boots Rush' : 'Boots'"
:items="builds.boots.slice(0, 2)"
:item-map="itemMap"
:total-count="builds.tree.count"
:max-items="2"
/>
</div>
<!-- Core Items Tree -->
@@ -322,22 +298,13 @@ function selectBuild(index: number) {
</div>
<!-- Late Game -->
<div class="item-row">
<span class="item-row-label">Late Game</span>
<div class="item-row-content">
<div v-for="item in builds.lateGame.slice(0, 6)" :key="item.data" class="item-cell">
<NuxtImg
v-if="itemMap.get(item.data)"
class="item-img"
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data)!.iconPath)"
/>
<div v-else class="item-placeholder" />
<span class="item-pickrate"
>{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%</span
>
</div>
</div>
</div>
<ItemRow
label="Late Game"
:items="builds.lateGame.slice(0, 6)"
:item-map="itemMap"
:total-count="builds.tree.count"
:max-items="6"
/>
</div>
</div>
</div>
@@ -353,77 +320,6 @@ function selectBuild(index: number) {
align-items: center;
}
/* Build Variant Selector - Global */
.build-variant-selector {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 20px;
padding: 0 12px;
overflow-x: auto;
scrollbar-width: none;
}
.build-variant-selector::-webkit-scrollbar {
display: none;
}
.build-variant-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 14px;
background: var(--color-surface-darker);
border: 2px solid transparent;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
min-width: 90px;
}
.build-variant-card:hover {
border-color: var(--color-on-surface);
}
.build-variant-card.selected {
border-color: #4a9eff;
background: linear-gradient(135deg, rgba(74, 158, 255, 0.15), rgba(74, 158, 255, 0.05));
}
.variant-content {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 4px;
}
.variant-keystone {
width: 28px;
height: 28px;
border-radius: 50%;
}
.variant-item {
width: 28px;
height: 28px;
border-radius: 4px;
border: 1px solid rgba(183, 184, 225, 0.3);
}
.variant-item-placeholder {
width: 28px;
height: 28px;
background: var(--color-surface);
border-radius: 4px;
border: 1px solid rgba(183, 184, 225, 0.3);
}
.variant-pickrate {
font-size: 0.75rem;
color: var(--color-on-surface);
opacity: 0.8;
}
/* Main Build Content */
.build-content {
display: flex;
@@ -449,30 +345,6 @@ function selectBuild(index: number) {
letter-spacing: 0.5px;
}
/* Rune page selector dots */
.rune-page-selector {
display: flex;
gap: 6px;
}
.rune-page-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-surface);
cursor: pointer;
transition: all 0.2s ease;
}
.rune-page-dot:hover {
background: var(--color-on-surface);
opacity: 0.6;
}
.rune-page-dot.active {
background: #4a9eff;
}
/* Left Column: Runes + Summoner Spells */
.build-left-column {
display: flex;
@@ -486,46 +358,9 @@ function selectBuild(index: number) {
}
.rune-page-wrapper {
transform: scale(0.85);
transform-origin: top left;
}
/* Summoner Spells - Compact */
.summoner-spells-section {
display: flex;
flex-direction: column;
}
.summoner-spells-row {
display: flex;
gap: 8px;
}
.summoner-spell-item {
display: flex;
align-items: center;
gap: 6px;
}
.summoner-spell-img {
width: 36px;
height: 36px;
border-radius: 4px;
border: 1px solid var(--color-on-surface);
}
.summoner-spell-placeholder {
width: 36px;
height: 36px;
background: var(--color-surface);
border-radius: 4px;
border: 1px solid var(--color-on-surface);
}
.spell-pickrate {
font-size: 0.7rem;
color: var(--color-on-surface);
opacity: 0.7;
margin-top: -45px;
transform: scale(0.7);
transform-origin: center;
}
/* Right Column: Items */
@@ -557,48 +392,6 @@ function selectBuild(index: number) {
letter-spacing: 0.5px;
}
.item-row-content {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.item-cell {
display: flex;
flex-direction: column;
align-items: center;
}
.item-img {
width: 48px;
height: 48px;
border-radius: 4px;
border: 1px solid var(--color-on-surface);
}
.item-placeholder {
width: 48px;
height: 48px;
background: var(--color-surface);
border-radius: 4px;
border: 1px solid var(--color-on-surface);
}
.item-pickrate {
font-size: 0.65rem;
color: var(--color-on-surface);
opacity: 0.6;
margin-top: 1px;
}
.item-separator {
width: 1px;
height: 48px;
background: var(--color-on-surface);
opacity: 0.3;
margin: 0 8px;
}
/* Responsive: Mobile */
@media only screen and (max-width: 900px) {
.build-content {
@@ -617,26 +410,6 @@ function selectBuild(index: number) {
align-items: center;
}
.build-variant-card {
min-width: 70px;
padding: 6px 10px;
}
.variant-keystone,
.variant-item {
width: 24px;
height: 24px;
}
.variant-item-placeholder {
width: 24px;
height: 24px;
}
.variant-pickrate {
font-size: 0.65rem;
}
.section-title {
font-size: 0.9rem;
}
@@ -650,43 +423,5 @@ function selectBuild(index: number) {
transform: scale(1);
transform-origin: center center;
}
.item-img {
width: 40px;
height: 40px;
}
.item-placeholder {
width: 40px;
height: 40px;
}
.summoner-spell-img,
.summoner-spell-placeholder {
width: 36px;
height: 36px;
}
}
@media only screen and (max-width: 400px) {
.build-variant-selector {
gap: 6px;
}
.build-variant-card {
min-width: 60px;
padding: 5px 8px;
}
.variant-keystone,
.variant-item {
width: 20px;
height: 20px;
}
.variant-item-placeholder {
width: 20px;
height: 20px;
}
}
</style>