Files
buildpath/frontend/components/item/Tree.vue
Valentin Haudiquet fe128c0848 Frontend updates: caching basic data (json) from CDragon
Implement caching in the patch_detector, consume the cache from API routes in frontend
2026-02-28 00:23:04 +01:00

197 lines
3.8 KiB
Vue

<script setup lang="ts">
import { LinePath as svgdomarrowsLinePath } from 'svg-dom-arrows'
defineProps<{
tree: ItemTree
parentCount?: number
}>()
const emit = defineEmits<{
mount: [end: Element]
refresh: []
}>()
const { data: items } = useFetch<Array<{ id: number; iconPath: string }>>('/api/cdragon/items', {
lazy: true, // Don't block rendering
server: false // Client-side only
})
// Create item map reactively
const itemMap = reactive(new Map<number, { id: number; iconPath: string }>())
watch(
items,
newItems => {
if (newItems) {
itemMap.clear()
for (const item of newItems) {
itemMap.set(item.id, item)
}
}
},
{ immediate: true }
)
function getItemIconPath(itemId: number): string {
const item = itemMap.get(itemId)
return item ? CDRAGON_BASE + mapPath(item.iconPath) : ''
}
const start: Ref<Element | null> = useTemplateRef('start')
const arrows: Array<svgdomarrowsLinePath> = []
onMounted(() => {
// Only refresh arrows and emit if start element is available
if (start.value) {
refreshArrows()
emit('mount', start.value)
}
})
onBeforeUpdate(() => {
for (const arrow of arrows) {
arrow.release()
}
arrows.splice(0, arrows.length)
})
onUpdated(() => {
if (start.value) {
refreshArrows()
emit('mount', start.value)
}
})
onUnmounted(() => {
for (const arrow of arrows) {
arrow.release()
}
})
function drawArrow(start: Element, end: Element) {
if (start == null || end == null) return
const arrow = new svgdomarrowsLinePath({
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:2;fill:transparent;',
appendTo: document.body
})
arrows.push(arrow)
// Redraw immediately after creation to ensure correct position
requestAnimationFrame(() => {
arrow.redraw()
})
}
function refreshArrows() {
for (const arrow of arrows) {
arrow.redraw()
}
}
// Redraw arrows on window resize
addEventListener('resize', _ => {
refreshArrows()
})
addEventListener('scroll', _ => {
refreshArrows()
})
function handleSubtreeMount(end: Element) {
if (start.value) {
drawArrow(start.value, end)
refreshArrows()
}
emit('refresh')
}
function handleRefresh() {
refreshArrows()
emit('refresh')
}
</script>
<template>
<div class="item-tree-container">
<div v-if="tree.data != undefined && tree.data != null" class="item-tree-node">
<img
ref="start"
class="item-tree-img"
:alt="tree.data.toString()"
:src="getItemIconPath(tree.data)"
/>
<span class="item-tree-pickrate">{{ ((tree.count / parentCount!!) * 100).toFixed(0) }}%</span>
</div>
<div class="item-tree-children">
<div v-for="child in tree.children" :key="child.data" class="item-tree-child">
<ItemTree
:tree="child"
:parent-count="tree.count"
@refresh="handleRefresh"
@mount="end => handleSubtreeMount(end)"
/>
</div>
</div>
</div>
</template>
<style>
.item-tree-container {
display: flex;
align-items: center;
justify-content: flex-start;
}
.item-tree-node {
display: flex;
flex-direction: column;
align-items: center;
width: fit-content;
}
.item-tree-img {
width: 48px;
height: 48px;
border-radius: 4px;
border: 1px solid var(--color-on-surface);
}
.item-tree-pickrate {
font-size: 0.65rem;
color: var(--color-on-surface);
opacity: 0.6;
margin-top: 2px;
}
.item-tree-children {
margin-left: 32px;
}
.item-tree-child {
width: fit-content;
}
@media only screen and (max-width: 900px) {
.item-tree-img {
width: 40px;
height: 40px;
}
.item-tree-children {
margin-left: 20px;
}
}
</style>