feat: first back recording and display (#12)
All checks were successful
pipeline / lint-and-format (push) Successful in 4m35s
pipeline / build-and-push-images (push) Successful in 1m39s

Record first backs, group them by item sets and show the most popular ones, with gold and %, in the frontend.
This commit is contained in:
2026-04-28 20:10:20 +02:00
parent 7712abe3f0
commit db2ca353c5
6 changed files with 452 additions and 6 deletions

View File

@@ -0,0 +1,107 @@
<script setup lang="ts">
defineProps<{
firstBacks: FirstBackGroup[]
itemMap: Map<number, Item>
}>()
</script>
<template>
<div v-if="firstBacks && firstBacks.length > 0" class="item-row">
<span class="item-row-label">First Back</span>
<div class="first-back-content">
<div v-for="(group, index) in firstBacks" :key="index" class="first-back-option">
<span class="gold-cost">{{ group.itemSet.totalGold }}g</span>
<div class="option-items">
<template v-for="item in group.itemSet.items" :key="item.itemId">
<div class="item-with-count">
<ItemIcon
v-if="itemMap.get(item.itemId)"
:item="itemMap.get(item.itemId)!"
:size="36"
class="item-cell"
/>
<span v-if="item.count > 1" class="item-count">x{{ item.count }}</span>
</div>
</template>
</div>
<span class="pickrate">{{ (group.pickrate * 100).toFixed(0) }}%</span>
</div>
</div>
</div>
</template>
<style scoped>
.item-row {
display: flex;
flex-direction: column;
gap: 4px;
max-width: 400px;
}
.item-row-label {
font-size: 0.8rem;
font-weight: 500;
color: var(--color-on-surface);
opacity: 0.6;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.first-back-content {
display: flex;
flex-wrap: wrap;
gap: 16px;
overflow-x: hidden;
}
.first-back-option {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
.option-items {
display: flex;
gap: 2px;
}
.item-with-count {
position: relative;
display: flex;
align-items: center;
}
.item-cell {
flex-shrink: 0;
}
.item-count {
position: absolute;
bottom: -1px;
right: -1px;
background: rgba(0, 0, 0, 0.85);
color: #fff;
font-size: 0.55rem;
font-weight: 600;
padding: 0 2px;
border-radius: 2px;
min-width: 12px;
text-align: center;
}
.pickrate {
font-size: 0.65rem;
color: var(--color-on-surface);
opacity: 0.6;
white-space: nowrap;
}
.gold-cost {
font-size: 0.6rem;
color: #ffd700;
opacity: 0.8;
white-space: nowrap;
}
</style>

View File

@@ -3,6 +3,7 @@ import { getCoreItems, getLateGameItems } from '~/utils/buildHelpers'
import BuildVariantSelector from '~/components/build/BuildVariantSelector.vue'
import CompactRuneSelector from '~/components/build/CompactRuneSelector.vue'
import ItemRow from '~/components/build/ItemRow.vue'
import FirstBack from '~/components/build/FirstBack.vue'
const props = defineProps<{
builds: Builds
@@ -122,6 +123,13 @@ function selectBuild(index: number): void {
/>
</div>
<!-- First Back Section -->
<FirstBack
v-if="currentBuild.firstBacks && currentBuild.firstBacks.length > 0"
:first-backs="currentBuild.firstBacks"
:item-map="itemMap"
/>
<!-- Core Items Tree (children of start item) -->
<div v-if="currentBuild.items?.children?.length" class="item-row">
<span class="item-row-label">Core</span>

View File

@@ -35,7 +35,8 @@ export default withNuxt([
MatchupData: 'readonly',
Item: 'readonly',
SummonerSpell: 'readonly',
Perk: 'readonly'
Perk: 'readonly',
FirstBackGroup: 'readonly'
}
},
rules: {

View File

@@ -27,6 +27,7 @@ declare global {
suppItems: Array<{ count: number; data: number }>
startItems: Array<{ count: number; data: number }>
pickrate: number
firstBacks?: FirstBackGroup[]
}
/**
@@ -56,6 +57,32 @@ declare global {
championAlias: string
}
/**
* Represents an item in a first back item set
*/
interface FirstBackItemSetEntry {
itemId: number
count: number
}
/**
* Represents an item set (combination of items)
*/
interface ItemSet {
items: FirstBackItemSetEntry[]
totalGold: number
}
/**
* Represents a grouped first back by item set
*/
interface FirstBackGroup {
itemSet: ItemSet
count: number
pickrate: number
avgTimestamp: number
}
/**
* Represents lane-specific champion data
*/