diff --git a/dev/package.json b/dev/package.json
index d8b9c41..9fab5f1 100644
--- a/dev/package.json
+++ b/dev/package.json
@@ -9,7 +9,8 @@
"import-matches": "node scripts/setup-db.js import-matches",
"import-patches": "node scripts/setup-db.js import-patches",
"generate-stats": "node scripts/setup-db.js generate-stats",
- "status": "node scripts/setup-db.js status"
+ "status": "node scripts/setup-db.js status",
+ "fetch-cdragon": "node scripts/fetch-cdragon.js"
},
"dependencies": {
"mongodb": "^6.10.0"
diff --git a/dev/scripts/fetch-cdragon.js b/dev/scripts/fetch-cdragon.js
new file mode 100644
index 0000000..703a180
--- /dev/null
+++ b/dev/scripts/fetch-cdragon.js
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+
+import { spawn } from 'child_process';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// Cache directory - use dev/cdragon by default
+const cacheDir = process.env.CDRAGON_CACHE_DIR || path.join(__dirname, '..', 'data', 'cdragon');
+
+// Dev MongoDB credentials (matching docker-compose.yml defaults)
+const mongoUser = process.env.MONGO_USER || 'root';
+const mongoPass = process.env.MONGO_PASS || 'password';
+const mongoHost = process.env.MONGO_HOST || 'localhost:27017';
+
+// Run patch_detector with the cache directory and dev MongoDB credentials
+const patchDetector = spawn('npx', ['tsx', '../patch_detector/index.ts'], {
+ cwd: path.join(__dirname, '..'),
+ env: {
+ ...process.env,
+ NODE_ENV: 'development',
+ CDRAGON_CACHE_DIR: cacheDir,
+ MONGO_USER: mongoUser,
+ MONGO_PASS: mongoPass,
+ MONGO_HOST: mongoHost
+ },
+ stdio: 'inherit'
+});
+
+patchDetector.on('close', (code) => {
+ process.exit(code || 0);
+});
diff --git a/dev/scripts/setup-db.js b/dev/scripts/setup-db.js
index b46eaf1..50747df 100644
--- a/dev/scripts/setup-db.js
+++ b/dev/scripts/setup-db.js
@@ -23,7 +23,7 @@ async function setupDatabase() {
const patchFile = path.join(dataDir, "patches.json");
if(!fs.existsSync(dataDir) || !fs.existsSync(patchFile)) {
fs.mkdirSync(dataDir, { recursive: true });
- console.log('📥 No data files found. Downloading latest snapshot...');
+ console.log('🚫 No data files found. Downloading latest snapshot...');
await downloadAndExtractSnapshot();
}
@@ -90,7 +90,11 @@ async function setupDatabase() {
console.log('✅ Skipping matches import - sufficient data already present');
}
- // 7. Run match collector to generate stats
+ // 7. Fetch CDragon data for the current patch
+ console.log('🎮 Fetching CDragon data...');
+ await fetchCDragonData();
+
+ // 8. Run match collector to generate stats
console.log('📊 Generating champion stats...');
await generateChampionStats();
@@ -322,6 +326,24 @@ async function generateChampionStats() {
}
}
+async function fetchCDragonData() {
+ try {
+ console.log('🔄 Running CDragon fetcher...');
+
+ // Run the fetch-cdragon script
+ const fetchCDragonPath = path.join(__dirname, 'fetch-cdragon.js');
+ execSync(`node ${fetchCDragonPath}`, {
+ stdio: 'inherit',
+ cwd: path.join(__dirname, '..')
+ });
+
+ console.log('✅ CDragon data fetched');
+ } catch (error) {
+ console.error('❌ Failed to fetch CDragon data:', error);
+ throw error;
+ }
+}
+
async function getMatchCount(patchVersion) {
const client = new MongoClient(getMongoUri());
await client.connect();
diff --git a/frontend/assets/css/main.css b/frontend/assets/css/main.css
index 6f37358..8049550 100644
--- a/frontend/assets/css/main.css
+++ b/frontend/assets/css/main.css
@@ -4,6 +4,7 @@
--color-surface: #312e2c;
--color-on-surface: #b7b8e1;
--color-surface-darker: #1f1d1c;
+ --color-gold: #ffd700;
}
/* Font setting */
diff --git a/frontend/components/ChampionSelector.vue b/frontend/components/ChampionSelector.vue
index b0345a6..0ff464d 100644
--- a/frontend/components/ChampionSelector.vue
+++ b/frontend/components/ChampionSelector.vue
@@ -2,8 +2,6 @@
import { debounce, isEmpty } from '~/utils/helpers'
// Constants
-const CDRAGON_CHAMPIONS_URL =
- CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json'
const CHAMPIONS_API_URL = '/api/champions'
// State
@@ -11,7 +9,7 @@ const {
data: championsData,
pending: loadingChampions,
error: championsError
-} = useFetch(CDRAGON_CHAMPIONS_URL, {
+} = useFetch('/api/cdragon/champion-summary', {
key: 'champions-data',
lazy: false,
server: false // Disable server-side fetching to avoid hydration issues
diff --git a/frontend/components/ChampionTitle.vue b/frontend/components/ChampionTitle.vue
index 1f3fa53..3be4cfb 100644
--- a/frontend/components/ChampionTitle.vue
+++ b/frontend/components/ChampionTitle.vue
@@ -41,8 +41,8 @@ const championDescription = computed(() => championData.value?.title || '')
- {{ championName }}
- {{ championDescription }}
+ {{ championName }}
+ {{ championDescription }}
-
{{ winrate }}% win.
- {{ pickrate }}% pick.
- {{ gameCount }} games
+ {{ winrate }}% win.
+
+ {{ pickrate }}% pick.
+
+ {{ gameCount }} games
@@ -68,15 +70,15 @@ const championDescription = computed(() => championData.value?.title || '')
diff --git a/frontend/components/build/BuildVariantSelector.vue b/frontend/components/build/BuildVariantSelector.vue
new file mode 100644
index 0000000..264b65f
--- /dev/null
+++ b/frontend/components/build/BuildVariantSelector.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
{{ (pickrate * 100).toFixed(1) }}%
+
+
+
+
+
diff --git a/frontend/components/build/CompactRuneSelector.vue b/frontend/components/build/CompactRuneSelector.vue
new file mode 100644
index 0000000..be4616a
--- /dev/null
+++ b/frontend/components/build/CompactRuneSelector.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
{{ (rune.pickrate * 100).toFixed(1) }}%
+
+
+
+
+
diff --git a/frontend/components/build/ItemRow.vue b/frontend/components/build/ItemRow.vue
new file mode 100644
index 0000000..ba452cf
--- /dev/null
+++ b/frontend/components/build/ItemRow.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
diff --git a/frontend/components/build/Viewer.vue b/frontend/components/build/Viewer.vue
new file mode 100644
index 0000000..ba4c818
--- /dev/null
+++ b/frontend/components/build/Viewer.vue
@@ -0,0 +1,260 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Items
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Core
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/item/Box.vue b/frontend/components/item/Box.vue
deleted file mode 100644
index acddfcb..0000000
--- a/frontend/components/item/Box.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
{{ title }}
-
- ({{ (bootsFirst * 100).toFixed(2) }}%)
-
-
-
-
-
-
-
diff --git a/frontend/components/item/ItemIcon.vue b/frontend/components/item/ItemIcon.vue
new file mode 100644
index 0000000..80af69d
--- /dev/null
+++ b/frontend/components/item/ItemIcon.vue
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
{{ (pickrate * 100).toFixed(0) }}%
+
+
+
+
+
+
diff --git a/frontend/components/item/ItemTooltip.vue b/frontend/components/item/ItemTooltip.vue
new file mode 100644
index 0000000..2183d51
--- /dev/null
+++ b/frontend/components/item/ItemTooltip.vue
@@ -0,0 +1,349 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/item/Tree.vue b/frontend/components/item/Tree.vue
index e312a8e..8a471b2 100644
--- a/frontend/components/item/Tree.vue
+++ b/frontend/components/item/Tree.vue
@@ -9,18 +9,19 @@ defineProps<{
const emit = defineEmits<{
mount: [end: Element]
refresh: []
+ parentReady: []
}>()
-const { data: items } = useFetch>(
- CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/items.json',
- {
- lazy: true, // Don't block rendering
- server: false // Client-side only
- }
-)
+const { data: items, pending: itemsLoading } = useFetch>('/api/cdragon/items', {
+ lazy: true, // Don't block rendering
+ server: false // Client-side only
+})
+
+// Track image loading state
+const imagesLoaded = ref(false)
// Create item map reactively
-const itemMap = reactive(new Map())
+const itemMap = reactive(new Map())
watch(
items,
newItems => {
@@ -34,19 +35,85 @@ watch(
{ immediate: true }
)
-function getItemIconPath(itemId: number): string {
- const item = itemMap.get(itemId)
- return item ? CDRAGON_BASE + mapPath(item.iconPath) : ''
+const startTreeItem = useTemplateRef('start')
+const arrows: Array = []
+const pendingChildMounts: Array = []
+
+// Get the actual icon element for arrow drawing
+const startElement = computed(() => startTreeItem.value?.iconElement ?? null)
+
+// Function to wait for an image to load
+function waitForImageLoad(imgElement: HTMLImageElement): Promise {
+ return new Promise(resolve => {
+ if (imgElement.complete) {
+ requestAnimationFrame(() => resolve())
+ return
+ }
+
+ imgElement.addEventListener(
+ 'load',
+ () => {
+ requestAnimationFrame(() => resolve())
+ },
+ { once: true }
+ )
+ imgElement.addEventListener(
+ 'error',
+ () => {
+ requestAnimationFrame(() => resolve())
+ },
+ { once: true }
+ )
+ })
}
-const start: Ref = useTemplateRef('start')
-const arrows: Array = []
+onMounted(async () => {
+ // Wait for next tick to ensure DOM is ready
+ await nextTick()
-onMounted(() => {
- // Only refresh arrows and emit if start element is available
- if (start.value) {
- refreshArrows()
- emit('mount', start.value)
+ // Wait for items to be loaded
+ await new Promise(resolve => {
+ if (!itemsLoading.value) {
+ resolve()
+ } else {
+ const unwatch = watch(itemsLoading, loading => {
+ if (!loading) {
+ unwatch()
+ resolve()
+ }
+ })
+ }
+ })
+
+ if (startElement.value) {
+ // Wait for the ItemIcon to load its image
+ const imgElement = startElement.value.querySelector('img')
+ if (imgElement) {
+ await waitForImageLoad(imgElement as HTMLImageElement)
+ }
+
+ // Now that image is loaded and DOM is ready, draw arrows
+ imagesLoaded.value = true
+
+ // Notify children that parent is ready
+ emit('parentReady')
+
+ // Draw any pending arrows from children that mounted before we were ready
+ if (pendingChildMounts.length > 0) {
+ await nextTick()
+ for (const childEnd of pendingChildMounts) {
+ drawArrow(startElement.value!, childEnd)
+ }
+ pendingChildMounts.length = 0
+ }
+
+ // Use multiple requestAnimationFrame to ensure rendering is complete
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ refreshArrows()
+ emit('mount', startElement.value!)
+ })
+ })
}
})
@@ -57,10 +124,17 @@ onBeforeUpdate(() => {
arrows.splice(0, arrows.length)
})
-onUpdated(() => {
- if (start.value) {
- refreshArrows()
- emit('mount', start.value)
+onUpdated(async () => {
+ await nextTick()
+
+ if (startElement.value && imagesLoaded.value) {
+ // Redraw arrows after DOM update
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ refreshArrows()
+ emit('mount', startElement.value!)
+ })
+ })
}
})
@@ -71,7 +145,6 @@ onUnmounted(() => {
})
function drawArrow(start: Element, end: Element) {
- // console.log("drawArrow(", start, ", ", end, ")")
if (start == null || end == null) return
const arrow = new svgdomarrowsLinePath({
@@ -89,16 +162,21 @@ function drawArrow(start: Element, end: Element) {
left: 0
}
},
- style: 'stroke:var(--color-on-surface);stroke-width:3;fill:transparent;',
+ style: 'stroke:var(--color-on-surface);stroke-width:2;fill:transparent;pointer-events:none;',
appendTo: document.body
})
arrows.push(arrow)
}
function refreshArrows() {
- for (const arrow of arrows) {
- arrow.redraw()
- }
+ // Double requestAnimationFrame to ensure layout is complete
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ for (const arrow of arrows) {
+ arrow.redraw()
+ }
+ })
+ })
}
// Redraw arrows on window resize
@@ -110,12 +188,36 @@ addEventListener('scroll', _ => {
})
function handleSubtreeMount(end: Element) {
- if (start.value) {
- drawArrow(start.value, end)
- refreshArrows()
+ if (startElement.value) {
+ if (imagesLoaded.value) {
+ // Parent is ready, draw arrow immediately
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ drawArrow(startElement.value!, end)
+ refreshArrows()
+ })
+ })
+ } else {
+ // Parent not ready yet, store for later
+ pendingChildMounts.push(end)
+ }
}
emit('refresh')
}
+
+function handleParentReady() {
+ // Parent became ready, redraw all our arrows
+ if (startElement.value && imagesLoaded.value) {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ refreshArrows()
+ })
+ })
+ }
+ // Propagate to children
+ emit('parentReady')
+}
+
function handleRefresh() {
refreshArrows()
emit('refresh')
@@ -123,37 +225,64 @@ function handleRefresh() {
-
-
-
![]()
+
+
-
- {{ ((tree.count / parentCount!!) * 100).toFixed(0) }}%
-
-
-
+
+
handleSubtreeMount(end)"
+ @parent-ready="handleParentReady"
/>
+
+
diff --git a/frontend/components/item/Viewer.vue b/frontend/components/item/Viewer.vue
deleted file mode 100644
index f66f7db..0000000
--- a/frontend/components/item/Viewer.vue
+++ /dev/null
@@ -1,349 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- {{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%
-
-
-
-
-
-
-
-
-
-
-
- {{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%
-
-
-
-
-
-
-
-
-
- {{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%
-
-
-
-
-
-
-
-
-
diff --git a/frontend/components/nav/BottomBar.vue b/frontend/components/nav/BottomBar.vue
index 7295c83..6cd1232 100644
--- a/frontend/components/nav/BottomBar.vue
+++ b/frontend/components/nav/BottomBar.vue
@@ -10,7 +10,7 @@ const emit = defineEmits<{
stateChange: [state: string, lane: number]
}>()
-const state = ref('runes')
+const state = ref('build')
const laneState = ref(0)
function handleStateChange(newState: string, newLane: number) {
@@ -49,16 +49,10 @@ if (route.path.startsWith('/tierlist/')) {
/>
-