diff --git a/web/src/pages/bot-profile.ts b/web/src/pages/bot-profile.ts index 21d18cd..a05e11d 100644 --- a/web/src/pages/bot-profile.ts +++ b/web/src/pages/bot-profile.ts @@ -4,7 +4,7 @@ import { fetchBotProfile, type BotProfile } from '../api-types'; import { updateOGTags, getBotProfileOGTags, resetOGTags } from '../og-tags'; -import { initLazySections } from '../lib/lazy-section'; +import { initLazySections, lazySection } from '../lib/lazy-section'; export async function renderBotProfilePage(params: Record): Promise { const app = document.getElementById('app'); @@ -124,15 +124,19 @@ function renderProfile(container: HTMLElement, profile: BotProfile): void { -
- -
- ${renderRecentMatches(profile.recent_matches)} -
-
+ ${lazySection( + 'history', + `
+ +
+ ${renderRecentMatches(profile.recent_matches)} +
+
`, + { placeholder: '
' } + )} `; diff --git a/web/src/pages/leaderboard.ts b/web/src/pages/leaderboard.ts index c743cc8..1c5588a 100644 --- a/web/src/pages/leaderboard.ts +++ b/web/src/pages/leaderboard.ts @@ -4,7 +4,7 @@ import { fetchLeaderboard, type LeaderboardEntry } from '../api-types'; import { VirtualList } from '../lib/virtual-list'; -import { initLazySections } from '../lib/lazy-section'; +import { initLazySections, lazySection } from '../lib/lazy-section'; const ROW_HEIGHT = 48; @@ -63,8 +63,22 @@ function renderLeaderboard( // Desktop: virtual list or static table depending on size renderDesktopList(document.getElementById('lb-desktop')!, entries, useVirtualList); - // Mobile: always expandable cards (lazy-rendered for large lists) - renderMobileCards(document.getElementById('lb-mobile')!, entries); + // Mobile: lazy-rendered expandable cards for large lists + if (useVirtualList) { + // Wrap mobile cards in a lazy section so they don't render until scrolled into view + const mobileEl = document.getElementById('lb-mobile')!; + mobileEl.innerHTML = lazySection( + 'lb-mobile-cards', + entries.slice(0, 20).map(entry => renderMobileCard(entry)).join(''), + { placeholder: '
' } + ); + initMobileCardToggles(mobileEl); + if (entries.length > 20) { + addMobileShowMore(mobileEl, entries, 20); + } + } else { + renderMobileCards(document.getElementById('lb-mobile')!, entries); + } // Activate lazy sections initLazySections(container); diff --git a/web/src/pages/replay.ts b/web/src/pages/replay.ts index b1c4cf5..23251ce 100644 --- a/web/src/pages/replay.ts +++ b/web/src/pages/replay.ts @@ -181,6 +181,14 @@ function initReplayViewerWithClass(ReplayViewerClass: any, initialUrl?: string): + + @@ -753,7 +761,7 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { } } - // Handle canvas clicks for spatial annotation position + // Handle canvas clicks for follow player selection and annotation position canvas.addEventListener('click', (e: MouseEvent) => { if (!viewer.getReplay()) return; const replay = viewer.getReplay(); @@ -764,6 +772,20 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { const y = (e.clientY - rect.top) * (canvas.height / rect.height); const cellSize = viewer.getCellSize(); const mapRows = replay.map.rows; + const mapHeight = mapRows * cellSize; + const overlayY = mapHeight + 4; + const overlayPadding = 8; + const lineHeight = 20; + + // Check if click is on score overlay (follow player selection) + if (y >= overlayY + overlayPadding && x < replay.map.cols * cellSize) { + const relY = y - overlayY - overlayPadding; + const playerIdx = Math.floor(relY / lineHeight); + if (playerIdx >= 0 && playerIdx < replay.players.length) { + viewer.setFollowPlayer(viewer.getFollowPlayer() === playerIdx ? null : playerIdx); + return; + } + } // Only accept clicks within the map area (not score overlay) const col = Math.floor(x / cellSize); @@ -1113,6 +1135,11 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { } }); + const followZoomSelect = document.getElementById('follow-zoom-select') as HTMLSelectElement; + followZoomSelect.addEventListener('change', () => { + viewer.setFollowZoom(parseInt(followZoomSelect.value, 10)); + }); + // Accessibility toggle handlers const colorBlindToggle = document.getElementById('color-blind-toggle') as HTMLInputElement; const shapesToggle = document.getElementById('shapes-toggle') as HTMLInputElement;