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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ - - - - - 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 @@ + + + + + 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() { + + 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 @@ - - - - - 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/')) { />