HASH 89e7a2b974cc
DATE 2025-07-20
SUBJECT web: use dynamic page
FILES 3 CHANGED
HASH 89e7a2b974cc
DATE 2025-07-20
SUBJECT web: use dynamic page
FILES 3 CHANGED
┌─ web/src/components/DynamicRankings.astro ─────────────────────────────────┐
│ diff --git a/web/src/components/DynamicRankings.astro b/web/src/components/Dynamic │
│ Rankings.astro │
│ new file mode 100644 │
│ index 0000000..707ba63 │
│ --- /dev/null │
│ +++ b/web/src/components/DynamicRankings.astro │
│ @@ -0,0 +1,547 @@ │
│ +--- │
│ +// This component will be client-side rendered for interactivity │
│ +--- │
│ + │
│ +<div class="rankings-container"> │
│ + <div id="metadata-container"></div> │
│ + │
│ + <div class="controls"> │
│ + <h3 class="controls-title">Simulation Options</h3> │
│ + <div class="controls-grid"> │
│ + <div class="control-group"> │
│ + <label for="targetCount">Target Count:</label> │
│ + <select id="targetCount"> │
│ + <option value="single">Single Target</option> │
│ + <option value="three">3 Targets</option> │
│ + <option value="cleave">Cleave (2)</option> │
│ + <option value="ten">10 Targets</option> │
│ + </select> │
│ + </div> │
│ + │
│ + <div class="control-group"> │
│ + <label for="encounterType">Encounter:</label> │
│ + <select id="encounterType"> │
│ + <option value="raid">Raid</option> │
│ + </select> │
│ + </div> │
│ + │
│ + <div class="control-group"> │
│ + <label for="duration">Duration:</label> │
│ + <select id="duration"> │
│ + <option value="long">Long</option> │
│ + </select> │
│ + </div> │
│ + │
│ + <div class="control-group"> │
│ + <label for="phase">Phase:</label> │
│ + <select id="phase"> │
│ + <option value="p1">Phase 1</option> │
│ + </select> │
│ + </div> │
│ + </div> │
│ + </div> │
│ + │
│ + <div id="loading" class="loading hidden">Loading simulation data...</div> │
│ + <div id="error" class="error hidden"></div> │
│ + │
│ + <div id="chart-container"></div> │
│ +</div> │
│ + │
│ +<style> │
│ + .rankings-container { │
│ + max-width: 1200px; │
│ + margin: 0 auto; │
│ + padding: 20px; │
│ + } │
│ + │
│ + .controls { │
│ + margin-bottom: 30px; │
│ + padding: 20px; │
│ + background-color: #3a3a3a; │
│ + border-radius: 8px; │
│ + border: 1px solid #4a4a4a; │
│ + } │
│ + │
│ + .controls-title { │
│ + color: #ea6962; │
│ + margin: 0 0 15px 0; │
│ + font-size: 1.2em; │
│ + font-weight: 600; │
│ + } │
│ + │
│ + .controls-grid { │
│ + display: flex; │
│ + gap: 20px; │
│ + flex-wrap: wrap; │
│ + } │
│ + │
│ + .control-group { │
│ + display: flex; │
│ + flex-direction: column; │
│ + gap: 5px; │
│ + } │
│ + │
│ + .control-group label { │
│ + font-size: 0.9em; │
│ + color: #a9a9a9; │
│ + font-weight: 500; │
│ + text-transform: uppercase; │
│ + letter-spacing: 0.5px; │
│ + } │
│ + │
│ + .control-group select { │
│ + padding: 8px 12px; │
│ + background-color: #2a2a2a; │
│ + border: 1px solid #555; │
│ + border-radius: 4px; │
│ + color: #ffffff; │
│ + font-size: 0.9em; │
│ + min-width: 120px; │
│ + } │
│ + │
│ + .control-group select:focus { │
│ + outline: none; │
│ + border-color: #ea6962; │
│ + } │
│ + │
│ + .loading, .error { │
│ + text-align: center; │
│ + padding: 20px; │
│ + margin: 20px 0; │
│ + position: fixed; │
│ + top: 50%; │
│ + left: 50%; │
│ + transform: translate(-50%, -50%); │
│ + background-color: #2a2a2a; │
│ + border-radius: 8px; │
│ + z-index: 1000; │
│ + min-width: 200px; │
│ + } │
│ + │
│ + .loading { │
│ + color: #a9a9a9; │
│ + border: 1px solid #4a4a4a; │
│ + } │
│ + │
│ + .error { │
│ + color: #ea6962; │
│ + background-color: #3a2a2a; │
│ + border: 1px solid #ea6962; │
│ + } │
│ + │
│ + /* Prevent layout shifts by maintaining minimum heights */ │
│ + #metadata-container { │
│ + min-height: 120px; │
│ + transition: opacity 0.2s ease; │
│ + } │
│ + │
│ + #chart-container { │
│ + min-height: 400px; │
│ + transition: opacity 0.2s ease; │
│ + } │
│ + │
│ + .hidden { │
│ + display: none; │
│ + } │
│ + │
│ + @media (max-width: 768px) { │
│ + .controls-grid { │
│ + flex-direction: column; │
│ + gap: 15px; │
│ + } │
│ + } │
│ +</style> │
│ + │
│ +<script> │
│ + // WoW class colors for consistency │
│ + const classColors = { │
│ + death_knight: '#C41E3A', │
│ + druid: '#FF7C0A', │
│ + hunter: '#AAD372', │
│ + mage: '#3FC7EB', │
│ + monk: '#00FF98', │
│ + paladin: '#F48CBA', │
│ + priest: '#FFFFFF', │
│ + rogue: '#FFF468', │
│ + shaman: '#0070DD', │
│ + warlock: '#8788EE', │
│ + warrior: '#C69B6D' │
│ + }; │
│ + │
│ + interface SimulationData { │
│ + metadata: { │
│ + name: string; │
│ + timestamp: string; │
│ + iterations: number; │
│ + specCount: number; │
│ + encounterDuration: number; │
│ + encounterVariation: number; │
│ + targetCount: number; │
│ + raidBuffs: Record<string, boolean | number>; │
│ + }; │
│ + results: Record<string, Record<string, { │
│ + dps: number; │
│ + max: number; │
│ + min: number; │
│ + stdev: number; │
│ + loadout: any; │
│ + }>>; │
│ + } │
│ + │
│ + class DynamicRankings { │
│ + private currentData: SimulationData | null = null; │
│ + │
│ + constructor() { │
│ + this.bindEvents(); │
│ + this.loadInitialData(); │
│ + } │
│ + │
│ + private bindEvents() { │
│ + const selects = document.querySelectorAll('#targetCount, #encounterType, #d │
│ uration, #phase'); │
│ + selects.forEach(select => { │
│ + select.addEventListener('change', () => this.loadData()); │
│ + }); │
│ + } │
│ + │
│ + private async loadInitialData() { │
│ + await this.loadData(); │
│ + } │
│ + │
│ + private getFileName(): string { │
│ + const targetCount = (document.getElementById('targetCount') as HTMLSelectEl │
│ ement).value; │
│ + const encounterType = (document.getElementById('encounterType') as HTMLSele │
│ ctElement).value; │
│ + const duration = (document.getElementById('duration') as HTMLSelectElement) │
│ .value; │
│ + const phase = (document.getElementById('phase') as HTMLSelectElement).value │
│ ; │
│ + │
│ + return `dps_${phase}_${encounterType}_${targetCount}_${duration}.json`; │
│ + } │
│ + │
│ + private async loadData() { │
│ + const loadingEl = document.getElementById('loading')!; │
│ + const errorEl = document.getElementById('error')!; │
│ + const metadataContainer = document.getElementById('metadata-container')!; │
│ + const chartContainer = document.getElementById('chart-container')!; │
│ + │
│ + // Save current scroll position │
│ + const scrollY = window.scrollY; │
│ + │
│ + // Fade out content and show loading │
│ + metadataContainer.style.opacity = '0.5'; │
│ + chartContainer.style.opacity = '0.5'; │
│ + loadingEl.classList.remove('hidden'); │
│ + errorEl.classList.add('hidden'); │
│ + │
│ + try { │
│ + const fileName = this.getFileName(); │
│ + const response = await fetch(`/data/${fileName}`); │
│ + │
│ + if (!response.ok) { │
│ + throw new Error(`Failed to load ${fileName}: ${response.statusText}`); │
│ + } │
│ + │
│ + this.currentData = await response.json(); │
│ + │
│ + // Hide loading │
│ + loadingEl.classList.add('hidden'); │
│ + │
│ + // Render components │
│ + this.renderMetadata(); │
│ + this.renderChart(); │
│ + │
│ + // Fade content back in │
│ + metadataContainer.style.opacity = '1'; │
│ + chartContainer.style.opacity = '1'; │
│ + │
│ + // Restore scroll position after content is rendered │
│ + requestAnimationFrame(() => { │
│ + window.scrollTo(0, scrollY); │
│ + }); │
│ + │
│ + } catch (error) { │
│ + console.error('Error loading data:', error); │
│ + loadingEl.classList.add('hidden'); │
│ + errorEl.textContent = `Error loading simulation data: ${error.message}`; │
│ + errorEl.classList.remove('hidden'); │
│ + │
│ + // Clear content on error and restore opacity │
│ + metadataContainer.innerHTML = ''; │
│ + chartContainer.innerHTML = ''; │
│ + metadataContainer.style.opacity = '1'; │
│ + chartContainer.style.opacity = '1'; │
│ + } │
│ + } │
│ + │
│ + private renderMetadata() { │
│ + if (!this.currentData) return; │
│ + │
│ + const container = document.getElementById('metadata-container')!; │
│ + const metadata = this.currentData.metadata; │
│ + │
│ + const simulationDate = new Date(metadata.timestamp).toLocaleString('en-US', │
│ { │
│ + year: 'numeric', │
│ + month: 'long', │
│ + day: 'numeric', │
│ + hour: '2-digit', │
│ + minute: '2-digit', │
│ + timeZoneName: 'short' │
│ + }); │
│ + │
│ + const formatDuration = (seconds: number) => { │
│ + const minutes = Math.floor(seconds / 60); │
│ + const remainingSeconds = seconds % 60; │
│ + return minutes > 0 ? `${minutes}m ${remainingSeconds}s` : `${remainingSec │
│ onds}s`; │
│ + }; │
│ + │
│ + const formatRaidBuffs = (buffs: Record<string, boolean | number>) => { │
│ + const activeBuffs = Object.entries(buffs) │
│ + .filter(([_, value]) => value !== false && value !== 0) │
│ + .map(([key, value]) => { │
│ + const readable = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => │
│ str.toUpperCase()); │
│ + return typeof value === 'number' && value > 1 ? `${readable} (${value │
│ })` : readable; │
│ + }); │
│ + return activeBuffs.join(', '); │
│ + }; │
│ + │
│ + container.innerHTML = ` │
│ + <div class="simulation-metadata"> │
│ + <h3 class="metadata-title">Simulation Details</h3> │
│ + <div class="metadata-grid"> │
│ + <div class="metadata-item"> │
│ + <span class="metadata-label">Iterations</span> │
│ + <span class="metadata-value">${metadata.iterations.toLocaleString() │
│ }</span> │
│ + </div> │
│ + <div class="metadata-item"> │
│ + <span class="metadata-label">Specs Tested</span> │
│ + <span class="metadata-value">${metadata.specCount}</span> │
│ + </div> │
│ + <div class="metadata-item"> │
│ + <span class="metadata-label">Encounter Duration</span> │
│ + <span class="metadata-value">${formatDuration(metadata.encounterDur │
│ ation)}</span> │
│ + </div> │
│ + <div class="metadata-item"> │
│ + <span class="metadata-label">Duration Variation</span> │
│ + <span class="metadata-value">?${metadata.encounterVariation}s</span │
│ > │
│ + </div> │
│ + <div class="metadata-item"> │
│ + <span class="metadata-label">Target Count</span> │
│ + <span class="metadata-value">${metadata.targetCount}</span> │
│ + </div> │
│ + <div class="metadata-item"> │
│ + <span class="metadata-label">Date Simulated</span> │
│ + <span class="metadata-value">${simulationDate}</span> │
│ + </div> │
│ + <div class="metadata-item metadata-item-wide"> │
│ + <span class="metadata-label">Active Raid Buffs</span> │
│ + <span class="metadata-value metadata-value-wrap">${formatRaidBuffs( │
│ metadata.raidBuffs)}</span> │
│ + </div> │
│ + </div> │
│ + </div> │
│ + `; │
│ + } │
│ + │
│ + private renderChart() { │
│ + if (!this.currentData) return; │
│ + │
│ + const container = document.getElementById('chart-container')!; │
│ + │
│ + // Transform data for chart │
│ + const chartItems: Array<{label: string, sublabel: string, value: number, ca │
│ tegory: string}> = []; │
│ + │
│ + for (const [className, classSpecs] of Object.entries(this.currentData.resul │
│ ts)) { │
│ + for (const [specName, specData] of Object.entries(classSpecs)) { │
│ + chartItems.push({ │
│ + label: specName, │
│ + sublabel: className, │
│ + value: specData.dps, │
│ + category: className │
│ + }); │
│ + } │
│ + } │
│ + │
│ + // Sort by DPS descending │
│ + chartItems.sort((a, b) => b.value - a.value); │
│ + │
│ + // Create chart HTML │
│ + const maxDps = Math.max(...chartItems.map(item => item.value)); │
│ + │
│ + container.innerHTML = ` │
│ + <div class="chart-container"> │
│ + <h2 class="chart-title">DPS Rankings</h2> │
│ + <div class="chart-bars"> │
│ + ${chartItems.map((item, index) => ` │
│ + <div class="chart-item"> │
│ + <div class="chart-labels"> │
│ + <span class="chart-rank">#${index + 1}</span> │
│ + <span class="chart-label">${item.label}</span> │
│ + <span class="chart-sublabel">${item.sublabel}</span> │
│ + </div> │
│ + <div class="chart-bar-container"> │
│ + <div class="chart-bar" style="width: ${(item.value / maxDps) * │
│ 100}%; background-color: ${classColors[item.category] || '#666'}"> │
│ + </div> │
│ + <span class="chart-value">${Math.round(item.value).toLocaleStri │
│ ng()}</span> │
│ + </div> │
│ + </div> │
│ + `).join('')} │
│ + </div> │
│ + </div> │
│ + `; │
│ + } │
│ + } │
│ + │
│ + // Initialize when DOM is loaded │
│ + document.addEventListener('DOMContentLoaded', () => { │
│ + new DynamicRankings(); │
│ + }); │
│ +</script> │
│ + │
│ +<!-- Include metadata and chart styles --> │
│ +<style is:global> │
│ + .simulation-metadata { │
│ + background-color: #3a3a3a; │
│ + border-radius: 8px; │
│ + padding: 20px; │
│ + margin-bottom: 30px; │
│ + border: 1px solid #4a4a4a; │
│ + } │
│ + │
│ + .metadata-title { │
│ + color: #ea6962; │
│ + margin: 0 0 15px 0; │
│ + font-size: 1.2em; │
│ + font-weight: 600; │
│ + } │
│ + │
│ + .metadata-grid { │
│ + display: grid; │
│ + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); │
│ + gap: 15px; │
│ + } │
│ + │
│ + .metadata-item { │
│ + display: flex; │
│ + flex-direction: column; │
│ + gap: 4px; │
│ + } │
│ + │
│ + .metadata-item-wide { │
│ + grid-column: 1 / -1; │
│ + } │
│ + │
│ + .metadata-label { │
│ + font-size: 0.85em; │
│ + color: #a9a9a9; │
│ + font-weight: 500; │
│ + text-transform: uppercase; │
│ + letter-spacing: 0.5px; │
│ + } │
│ + │
│ + .metadata-value { │
│ + font-size: 1em; │
│ + color: #ffffff; │
│ + font-weight: 600; │
│ + } │
│ + │
│ + .metadata-value-wrap { │
│ + white-space: normal; │
│ + word-wrap: break-word; │
│ + line-height: 1.4; │
│ + } │
│ + │
│ + .chart-container { │
│ + background-color: #3a3a3a; │
│ + border-radius: 8px; │
│ + padding: 20px; │
│ + border: 1px solid #4a4a4a; │
│ + } │
│ + │
│ + .chart-title { │
│ + color: #ea6962; │
│ + margin: 0 0 20px 0; │
│ + font-size: 1.4em; │
│ + font-weight: 600; │
│ + } │
│ + │
│ + .chart-bars { │
│ + display: flex; │
│ + flex-direction: column; │
│ + gap: 8px; │
│ + } │
│ + │
│ + .chart-item { │
│ + display: flex; │
│ + align-items: center; │
│ + gap: 15px; │
│ + padding: 8px 0; │
│ + } │
│ + │
│ + .chart-labels { │
│ + display: flex; │
│ + align-items: center; │
│ + gap: 10px; │
│ + min-width: 200px; │
│ + flex-shrink: 0; │
│ + } │
│ + │
│ + .chart-rank { │
│ + font-size: 0.9em; │
│ + color: #a9a9a9; │
│ + font-weight: 600; │
│ + min-width: 30px; │
│ + } │
│ + │
│ + .chart-label { │
│ + font-weight: 600; │
│ + color: #ffffff; │
│ + text-transform: capitalize; │
│ + } │
│ + │
│ + .chart-sublabel { │
│ + font-size: 0.85em; │
│ + color: #a9a9a9; │
│ + text-transform: capitalize; │
│ + } │
│ + │
│ + .chart-bar-container { │
│ + display: flex; │
│ + align-items: center; │
│ + flex: 1; │
│ + gap: 10px; │
│ + } │
│ + │
│ + .chart-bar { │
│ + height: 20px; │
│ + border-radius: 3px; │
│ + min-width: 2px; │
│ + transition: width 0.3s ease; │
│ + } │
│ + │
│ + .chart-value { │
│ + font-weight: 600; │
│ + color: #ffffff; │
│ + font-size: 0.9em; │
│ + min-width: 60px; │
│ + text-align: right; │
│ + } │
│ + │
│ + @media (max-width: 768px) { │
│ + .metadata-grid { │
│ + grid-template-columns: 1fr; │
│ + } │
│ + │
│ + .chart-labels { │
│ + min-width: 150px; │
│ + flex-direction: column; │
│ + align-items: flex-start; │
│ + gap: 2px; │
│ + } │
│ + │
│ + .chart-item { │
│ + flex-direction: column; │
│ + align-items: stretch; │
│ + } │
│ + │
│ + .chart-bar-container { │
│ + width: 100%; │
│ + } │
│ + } │
│ +</style> │
│ \ No newline at end of file │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...ponents/DynamicRankings.astro ───┐
│ diff --git a/web/src/components/DynamicRanki │
│ ngs.astro b/web/src/components/DynamicRankin │
│ gs.astro │
│ new file mode 100644 │
│ index 0000000..707ba63 │
│ --- /dev/null │
│ +++ b/web/src/components/DynamicRankings.ast │
│ ro │
│ @@ -0,0 +1,547 @@ │
│ +--- │
│ +// This component will be client-side rende │
│ red for interactivity │
│ +--- │
│ + │
│ +<div class="rankings-container"> │
│ + <div id="metadata-container"></div> │
│ + │
│ + <div class="controls"> │
│ + <h3 class="controls-title">Simulation O │
│ ptions</h3> │
│ + <div class="controls-grid"> │
│ + <div class="control-group"> │
│ + <label for="targetCount">Target Cou │
│ nt:</label> │
│ + <select id="targetCount"> │
│ + <option value="single">Single Tar │
│ get</option> │
│ + <option value="three">3 Targets</ │
│ option> │
│ + <option value="cleave">Cleave (2) │
│ </option> │
│ + <option value="ten">10 Targets</o │
│ ption> │
│ + </select> │
│ + </div> │
│ + │
│ + <div class="control-group"> │
│ + <label for="encounterType">Encounte │
│ r:</label> │
│ + <select id="encounterType"> │
│ + <option value="raid">Raid</option │
│ > │
│ + </select> │
│ + </div> │
│ + │
│ + <div class="control-group"> │
│ + <label for="duration">Duration:</la │
│ bel> │
│ + <select id="duration"> │
│ + <option value="long">Long</option │
│ > │
│ + </select> │
│ + </div> │
│ + │
│ + <div class="control-group"> │
│ + <label for="phase">Phase:</label> │
│ + <select id="phase"> │
│ + <option value="p1">Phase 1</optio │
│ n> │
│ + </select> │
│ + </div> │
│ + </div> │
│ + </div> │
│ + │
│ + <div id="loading" class="loading hidden"> │
│ Loading simulation data...</div> │
│ + <div id="error" class="error hidden"></di │
│ v> │
│ + │
│ + <div id="chart-container"></div> │
│ +</div> │
│ + │
│ +<style> │
│ + .rankings-container { │
│ + max-width: 1200px; │
│ + margin: 0 auto; │
│ + padding: 20px; │
│ + } │
│ + │
│ + .controls { │
│ + margin-bottom: 30px; │
│ + padding: 20px; │
│ + background-color: #3a3a3a; │
│ + border-radius: 8px; │
│ + border: 1px solid #4a4a4a; │
│ + } │
│ + │
│ + .controls-title { │
│ + color: #ea6962; │
│ + margin: 0 0 15px 0; │
│ + font-size: 1.2em; │
│ + font-weight: 600; │
│ + } │
│ + │
│ + .controls-grid { │
│ + display: flex; │
│ + gap: 20px; │
│ + flex-wrap: wrap; │
│ + } │
│ + │
│ + .control-group { │
│ + display: flex; │
│ + flex-direction: column; │
│ + gap: 5px; │
│ + } │
│ + │
│ + .control-group label { │
│ + font-size: 0.9em; │
│ + color: #a9a9a9; │
│ + font-weight: 500; │
│ + text-transform: uppercase; │
│ + letter-spacing: 0.5px; │
│ + } │
│ + │
│ + .control-group select { │
│ + padding: 8px 12px; │
│ + background-color: #2a2a2a; │
│ + border: 1px solid #555; │
│ + border-radius: 4px; │
│ + color: #ffffff; │
│ + font-size: 0.9em; │
│ + min-width: 120px; │
│ + } │
│ + │
│ + .control-group select:focus { │
│ + outline: none; │
│ + border-color: #ea6962; │
│ + } │
│ + │
│ + .loading, .error { │
│ + text-align: center; │
│ + padding: 20px; │
│ + margin: 20px 0; │
│ + position: fixed; │
│ + top: 50%; │
│ + left: 50%; │
│ + transform: translate(-50%, -50%); │
│ + background-color: #2a2a2a; │
│ + border-radius: 8px; │
│ + z-index: 1000; │
│ + min-width: 200px; │
│ + } │
│ + │
│ + .loading { │
│ + color: #a9a9a9; │
│ + border: 1px solid #4a4a4a; │
│ + } │
│ + │
│ + .error { │
│ + color: #ea6962; │
│ + background-color: #3a2a2a; │
│ + border: 1px solid #ea6962; │
│ + } │
│ + │
│ + /* Prevent layout shifts by maintaining m │
│ inimum heights */ │
│ + #metadata-container { │
│ + min-height: 120px; │
│ + transition: opacity 0.2s ease; │
│ + } │
│ + │
│ + #chart-container { │
│ + min-height: 400px; │
│ + transition: opacity 0.2s ease; │
│ + } │
│ + │
│ + .hidden { │
│ + display: none; │
│ + } │
│ + │
│ + @media (max-width: 768px) { │
│ + .controls-grid { │
│ + flex-direction: column; │
│ + gap: 15px; │
│ + } │
│ + } │
│ +</style> │
│ + │
│ +<script> │
│ + // WoW class colors for consistency │
│ + const classColors = { │
│ + death_knight: '#C41E3A', │
│ + druid: '#FF7C0A', │
│ + hunter: '#AAD372', │
│ + mage: '#3FC7EB', │
│ + monk: '#00FF98', │
│ + paladin: '#F48CBA', │
│ + priest: '#FFFFFF', │
│ + rogue: '#FFF468', │
│ + shaman: '#0070DD', │
│ + warlock: '#8788EE', │
│ + warrior: '#C69B6D' │
│ + }; │
│ + │
│ + interface SimulationData { │
│ + metadata: { │
│ + name: string; │
│ + timestamp: string; │
│ + iterations: number; │
│ + specCount: number; │
│ + encounterDuration: number; │
│ + encounterVariation: number; │
│ + targetCount: number; │
│ + raidBuffs: Record<string, boolean | n │
│ umber>; │
│ + }; │
│ + results: Record<string, Record<string, │
│ { │
│ + dps: number; │
│ + max: number; │
│ + min: number; │
│ + stdev: number; │
│ + loadout: any; │
│ + }>>; │
│ + } │
│ + │
│ + class DynamicRankings { │
│ + private currentData: SimulationData | n │
│ ull = null; │
│ + │
│ + constructor() { │
│ + this.bindEvents(); │
│ + this.loadInitialData(); │
│ + } │
│ + │
│ + private bindEvents() { │
│ + const selects = document.querySelecto │
│ rAll('#targetCount, #encounterType, #duratio │
│ n, #phase'); │
│ + selects.forEach(select => { │
│ + select.addEventListener('change', ( │
│ ) => this.loadData()); │
│ + }); │
│ + } │
│ + │
│ + private async loadInitialData() { │
│ + await this.loadData(); │
│ + } │
│ + │
│ + private getFileName(): string { │
│ + const targetCount = (document.getElem │
│ entById('targetCount') as HTMLSelectElement) │
│ .value; │
│ + const encounterType = (document.getEl │
│ ementById('encounterType') as HTMLSelectElem │
│ ent).value; │
│ + const duration = (document.getElement │
│ ById('duration') as HTMLSelectElement).value │
│ ; │
│ + const phase = (document.getElementByI │
│ d('phase') as HTMLSelectElement).value; │
│ + │
│ + return `dps_${phase}_${encounterType} │
│ _${targetCount}_${duration}.json`; │
│ + } │
│ + │
│ + private async loadData() { │
│ + const loadingEl = document.getElement │
│ ById('loading')!; │
│ + const errorEl = document.getElementBy │
│ Id('error')!; │
│ + const metadataContainer = document.ge │
│ tElementById('metadata-container')!; │
│ + const chartContainer = document.getEl │
│ ementById('chart-container')!; │
│ + │
│ + // Save current scroll position │
│ + const scrollY = window.scrollY; │
│ + │
│ + // Fade out content and show loading │
│ + metadataContainer.style.opacity = '0. │
│ 5'; │
│ + chartContainer.style.opacity = '0.5'; │
│ + loadingEl.classList.remove('hidden'); │
│ + errorEl.classList.add('hidden'); │
│ + │
│ + try { │
│ + const fileName = this.getFileName() │
│ ; │
│ + const response = await fetch(`/data │
│ /${fileName}`); │
│ + │
│ + if (!response.ok) { │
│ + throw new Error(`Failed to load $ │
│ {fileName}: ${response.statusText}`); │
│ + } │
│ + │
│ + this.currentData = await response.j │
│ son(); │
│ + │
│ + // Hide loading │
│ + loadingEl.classList.add('hidden'); │
│ + │
│ + // Render components │
│ + this.renderMetadata(); │
│ + this.renderChart(); │
│ + │
│ + // Fade content back in │
│ + metadataContainer.style.opacity = ' │
│ 1'; │
│ + chartContainer.style.opacity = '1'; │
│ + │
│ + // Restore scroll position after co │
│ ntent is rendered │
│ + requestAnimationFrame(() => { │
│ + window.scrollTo(0, scrollY); │
│ + }); │
│ + │
│ + } catch (error) { │
│ + console.error('Error loading data:' │
│ , error); │
│ + loadingEl.classList.add('hidden'); │
│ + errorEl.textContent = `Error loadin │
│ g simulation data: ${error.message}`; │
│ + errorEl.classList.remove('hidden'); │
│ + │
│ + // Clear content on error and resto │
│ re opacity │
│ + metadataContainer.innerHTML = ''; │
│ + chartContainer.innerHTML = ''; │
│ + metadataContainer.style.opacity = ' │
│ 1'; │
│ + chartContainer.style.opacity = '1'; │
│ + } │
│ + } │
│ + │
│ + private renderMetadata() { │
│ + if (!this.currentData) return; │
│ + │
│ + const container = document.getElement │
│ ById('metadata-container')!; │
│ + const metadata = this.currentData.met │
│ adata; │
│ + │
│ + const simulationDate = new Date(metad │
│ ata.timestamp).toLocaleString('en-US', { │
│ + year: 'numeric', │
│ + month: 'long', │
│ + day: 'numeric', │
│ + hour: '2-digit', │
│ + minute: '2-digit', │
│ + timeZoneName: 'short' │
│ + }); │
│ + │
│ + const formatDuration = (seconds: numb │
│ er) => { │
│ + const minutes = Math.floor(seconds │
│ / 60); │
│ + const remainingSeconds = seconds % │
│ 60; │
│ + return minutes > 0 ? `${minutes}m $ │
│ {remainingSeconds}s` : `${remainingSeconds}s │
│ `; │
│ + }; │
│ + │
│ + const formatRaidBuffs = (buffs: Recor │
│ d<string, boolean | number>) => { │
│ + const activeBuffs = Object.entries( │
│ buffs) │
│ + .filter(([_, value]) => value !== │
│ false && value !== 0) │
│ + .map(([key, value]) => { │
│ + const readable = key.replace(/( │
│ [A-Z])/g, ' $1').replace(/^./, str => str.to │
│ UpperCase()); │
│ + return typeof value === 'number │
│ ' && value > 1 ? `${readable} (${value})` : │
│ readable; │
│ + }); │
│ + return activeBuffs.join(', '); │
│ + }; │
│ + │
│ + container.innerHTML = ` │
│ + <div class="simulation-metadata"> │
│ + <h3 class="metadata-title">Simula │
│ tion Details</h3> │
│ + <div class="metadata-grid"> │
│ + <div class="metadata-item"> │
│ + <span class="metadata-label"> │
│ Iterations</span> │
│ + <span class="metadata-value"> │
│ ${metadata.iterations.toLocaleString()}</spa │
│ n> │
│ + </div> │
│ + <div class="metadata-item"> │
│ + <span class="metadata-label"> │
│ Specs Tested</span> │
│ + <span class="metadata-value"> │
│ ${metadata.specCount}</span> │
│ + </div> │
│ + <div class="metadata-item"> │
│ + <span class="metadata-label"> │
│ Encounter Duration</span> │
│ + <span class="metadata-value"> │
│ ${formatDuration(metadata.encounterDuration) │
│ }</span> │
│ + </div> │
│ + <div class="metadata-item"> │
│ + <span class="metadata-label"> │
│ Duration Variation</span> │
│ + <span class="metadata-value"> │
│ ?${metadata.encounterVariation}s</span> │
│ + </div> │
│ + <div class="metadata-item"> │
│ + <span class="metadata-label"> │
│ Target Count</span> │
│ + <span class="metadata-value"> │
│ ${metadata.targetCount}</span> │
│ + </div> │
│ + <div class="metadata-item"> │
│ + <span class="metadata-label"> │
│ Date Simulated</span> │
│ + <span class="metadata-value"> │
│ ${simulationDate}</span> │
│ + </div> │
│ + <div class="metadata-item metad │
│ ata-item-wide"> │
│ + <span class="metadata-label"> │
│ Active Raid Buffs</span> │
│ + <span class="metadata-value m │
│ etadata-value-wrap">${formatRaidBuffs(metada │
│ ta.raidBuffs)}</span> │
│ + </div> │
│ + </div> │
│ + </div> │
│ + `; │
│ + } │
│ + │
│ + private renderChart() { │
│ + if (!this.currentData) return; │
│ + │
│ + const container = document.getElement │
│ ById('chart-container')!; │
│ + │
│ + // Transform data for chart │
│ + const chartItems: Array<{label: strin │
│ g, sublabel: string, value: number, category │
│ : string}> = []; │
│ + │
│ + for (const [className, classSpecs] of │
│ Object.entries(this.currentData.results)) { │
│ + for (const [specName, specData] of │
│ Object.entries(classSpecs)) { │
│ + chartItems.push({ │
│ + label: specName, │
│ + sublabel: className, │
│ + value: specData.dps, │
│ + category: className │
│ + }); │
│ + } │
│ + } │
│ + │
│ + // Sort by DPS descending │
│ + chartItems.sort((a, b) => b.value - a │
│ .value); │
│ + │
│ + // Create chart HTML │
│ + const maxDps = Math.max(...chartItems │
│ .map(item => item.value)); │
│ + │
│ + container.innerHTML = ` │
│ + <div class="chart-container"> │
│ + <h2 class="chart-title">DPS Ranki │
│ ngs</h2> │
│ + <div class="chart-bars"> │
│ + ${chartItems.map((item, index) │
│ => ` │
│ + <div class="chart-item"> │
│ + <div class="chart-labels"> │
│ + <span class="chart-rank"> │
│ #${index + 1}</span> │
│ + <span class="chart-label" │
│ >${item.label}</span> │
│ + <span class="chart-sublab │
│ el">${item.sublabel}</span> │
│ + </div> │
│ + <div class="chart-bar-conta │
│ iner"> │
│ + <div class="chart-bar" st │
│ yle="width: ${(item.value / maxDps) * 100}%; │
│ background-color: ${classColors[item.catego │
│ ry] || '#666'}"> │
│ + </div> │
│ + <span class="chart-value" │
│ >${Math.round(item.value).toLocaleString()}< │
│ /span> │
│ + </div> │
│ + </div> │
│ + `).join('')} │
│ + </div> │
│ + </div> │
│ + `; │
│ + } │
│ + } │
│ + │
│ + // Initialize when DOM is loaded │
│ + document.addEventListener('DOMContentLoad │
│ ed', () => { │
│ + new DynamicRankings(); │
│ + }); │
│ +</script> │
│ + │
│ +<!-- Include metadata and chart styles --> │
│ +<style is:global> │
│ + .simulation-metadata { │
│ + background-color: #3a3a3a; │
│ + border-radius: 8px; │
│ + padding: 20px; │
│ + margin-bottom: 30px; │
│ + border: 1px solid #4a4a4a; │
│ + } │
│ + │
│ + .metadata-title { │
│ + color: #ea6962; │
│ + margin: 0 0 15px 0; │
│ + font-size: 1.2em; │
│ + font-weight: 600; │
│ + } │
│ + │
│ + .metadata-grid { │
│ + display: grid; │
│ + grid-template-columns: repeat(auto-fit, │
│ minmax(200px, 1fr)); │
│ + gap: 15px; │
│ + } │
│ + │
│ + .metadata-item { │
│ + display: flex; │
│ + flex-direction: column; │
│ + gap: 4px; │
│ + } │
│ + │
│ + .metadata-item-wide { │
│ + grid-column: 1 / -1; │
│ + } │
│ + │
│ + .metadata-label { │
│ + font-size: 0.85em; │
│ + color: #a9a9a9; │
│ + font-weight: 500; │
│ + text-transform: uppercase; │
│ + letter-spacing: 0.5px; │
│ + } │
│ + │
│ + .metadata-value { │
│ + font-size: 1em; │
│ + color: #ffffff; │
│ + font-weight: 600; │
│ + } │
│ + │
│ + .metadata-value-wrap { │
│ + white-space: normal; │
│ + word-wrap: break-word; │
│ + line-height: 1.4; │
│ + } │
│ + │
│ + .chart-container { │
│ + background-color: #3a3a3a; │
│ + border-radius: 8px; │
│ + padding: 20px; │
│ + border: 1px solid #4a4a4a; │
│ + } │
│ + │
│ + .chart-title { │
│ + color: #ea6962; │
│ + margin: 0 0 20px 0; │
│ + font-size: 1.4em; │
│ + font-weight: 600; │
│ + } │
│ + │
│ + .chart-bars { │
│ + display: flex; │
│ + flex-direction: column; │
│ + gap: 8px; │
│ + } │
│ + │
│ + .chart-item { │
│ + display: flex; │
│ + align-items: center; │
│ + gap: 15px; │
│ + padding: 8px 0; │
│ + } │
│ + │
│ + .chart-labels { │
│ + display: flex; │
│ + align-items: center; │
│ + gap: 10px; │
│ + min-width: 200px; │
│ + flex-shrink: 0; │
│ + } │
│ + │
│ + .chart-rank { │
│ + font-size: 0.9em; │
│ + color: #a9a9a9; │
│ + font-weight: 600; │
│ + min-width: 30px; │
│ + } │
│ + │
│ + .chart-label { │
│ + font-weight: 600; │
│ + color: #ffffff; │
│ + text-transform: capitalize; │
│ + } │
│ + │
│ + .chart-sublabel { │
│ + font-size: 0.85em; │
│ + color: #a9a9a9; │
│ + text-transform: capitalize; │
│ + } │
│ + │
│ + .chart-bar-container { │
│ + display: flex; │
│ + align-items: center; │
│ + flex: 1; │
│ + gap: 10px; │
│ + } │
│ + │
│ + .chart-bar { │
│ + height: 20px; │
│ + border-radius: 3px; │
│ + min-width: 2px; │
│ + transition: width 0.3s ease; │
│ + } │
│ + │
│ + .chart-value { │
│ + font-weight: 600; │
│ + color: #ffffff; │
│ + font-size: 0.9em; │
│ + min-width: 60px; │
│ + text-align: right; │
│ + } │
│ + │
│ + @media (max-width: 768px) { │
│ + .metadata-grid { │
│ + grid-template-columns: 1fr; │
│ + } │
│ + │
│ + .chart-labels { │
│ + min-width: 150px; │
│ + flex-direction: column; │
│ + align-items: flex-start; │
│ + gap: 2px; │
│ + } │
│ + │
│ + .chart-item { │
│ + flex-direction: column; │
│ + align-items: stretch; │
│ + } │
│ + │
│ + .chart-bar-container { │
│ + width: 100%; │
│ + } │
│ + } │
│ +</style> │
│ \ No newline at end of file │
└──────────────────────────────────────────────┘
┌─ web/src/components/SimulationMetadata.astro ──────────────────────────────┐
│ diff --git a/web/src/components/SimulationMetadata.astro b/web/src/components/Simu │
│ lationMetadata.astro │
│ index 9d44954..f2a51c7 100644 │
│ --- a/web/src/components/SimulationMetadata.astro │
│ +++ b/web/src/components/SimulationMetadata.astro │
│ @@ -7,6 +7,8 @@ export interface Props { │
│ specCount?: number; │
│ encounterDuration?: number; │
│ encounterVariation?: number; │
│ + targetCount?: number; │
│ + raidBuffs?: Record<string, boolean | number>; │
│ }; │
│ } │
│ │
│ @@ -28,6 +30,18 @@ const formatDuration = (seconds: number) => { │
│ const remainingSeconds = seconds % 60; │
│ return minutes > 0 ? `${minutes}m ${remainingSeconds}s` : `${remainingSeconds}s │
│ `; │
│ }; │
│ + │
│ +// Format raid buffs for display │
│ +const formatRaidBuffs = (buffs: Record<string, boolean | number>) => { │
│ + const activeBuffs = Object.entries(buffs) │
│ + .filter(([_, value]) => value !== false && value !== 0) │
│ + .map(([key, value]) => { │
│ + // Convert camelCase to readable format │
│ + const readable = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.to │
│ UpperCase()); │
│ + return typeof value === 'number' && value > 1 ? `${readable} (${value})` : │
│ readable; │
│ + }); │
│ + return activeBuffs.join(', '); │
│ +}; │
│ --- │
│ │
│ <div class="simulation-metadata"> │
│ @@ -59,10 +73,24 @@ const formatDuration = (seconds: number) => { │
│ </div> │
│ )} │
│ │
│ + {metadata.targetCount && ( │
│ + <div class="metadata-item"> │
│ + <span class="metadata-label">Target Count</span> │
│ + <span class="metadata-value">{metadata.targetCount}</span> │
│ + </div> │
│ + )} │
│ + │
│ <div class="metadata-item"> │
│ - <span class="metadata-label">Simulated</span> │
│ + <span class="metadata-label">Date Simulated</span> │
│ <span class="metadata-value">{simulationDate}</span> │
│ </div> │
│ + │
│ + {metadata.raidBuffs && ( │
│ + <div class="metadata-item metadata-item-wide"> │
│ + <span class="metadata-label">Active Raid Buffs</span> │
│ + <span class="metadata-value metadata-value-wrap">{formatRaidBuffs(metadat │
│ a.raidBuffs)}</span> │
│ + </div> │
│ + )} │
│ </div> │
│ </div> │
│ │
│ @@ -108,9 +136,19 @@ const formatDuration = (seconds: number) => { │
│ font-weight: 600; │
│ } │
│ │
│ + .metadata-item-wide { │
│ + grid-column: 1 / -1; │
│ + } │
│ + │
│ + .metadata-value-wrap { │
│ + white-space: normal; │
│ + word-wrap: break-word; │
│ + line-height: 1.4; │
│ + } │
│ + │
│ @media (max-width: 768px) { │
│ .metadata-grid { │
│ grid-template-columns: 1fr; │
│ } │
│ } │
│ -</style> │
│ \ No newline at end of file │
│ +</style> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...ents/SimulationMetadata.astro ───┐
│ diff --git a/web/src/components/SimulationMe │
│ tadata.astro b/web/src/components/Simulation │
│ Metadata.astro │
│ index 9d44954..f2a51c7 100644 │
│ --- a/web/src/components/SimulationMetadata. │
│ astro │
│ +++ b/web/src/components/SimulationMetadata. │
│ astro │
│ @@ -7,6 +7,8 @@ export interface Props { │
│ specCount?: number; │
│ encounterDuration?: number; │
│ encounterVariation?: number; │
│ + targetCount?: number; │
│ + raidBuffs?: Record<string, boolean | nu │
│ mber>; │
│ }; │
│ } │
│ │
│ @@ -28,6 +30,18 @@ const formatDuration = (s │
│ econds: number) => { │
│ const remainingSeconds = seconds % 60; │
│ return minutes > 0 ? `${minutes}m ${remai │
│ ningSeconds}s` : `${remainingSeconds}s`; │
│ }; │
│ + │
│ +// Format raid buffs for display │
│ +const formatRaidBuffs = (buffs: Record<stri │
│ ng, boolean | number>) => { │
│ + const activeBuffs = Object.entries(buffs) │
│ + .filter(([_, value]) => value !== false │
│ && value !== 0) │
│ + .map(([key, value]) => { │
│ + // Convert camelCase to readable form │
│ at │
│ + const readable = key.replace(/([A-Z]) │
│ /g, ' $1').replace(/^./, str => str.toUpperC │
│ ase()); │
│ + return typeof value === 'number' && v │
│ alue > 1 ? `${readable} (${value})` : readab │
│ le; │
│ + }); │
│ + return activeBuffs.join(', '); │
│ +}; │
│ --- │
│ │
│ <div class="simulation-metadata"> │
│ @@ -59,10 +73,24 @@ const formatDuration = ( │
│ seconds: number) => { │
│ </div> │
│ )} │
│ │
│ + {metadata.targetCount && ( │
│ + <div class="metadata-item"> │
│ + <span class="metadata-label">Target │
│ Count</span> │
│ + <span class="metadata-value">{metad │
│ ata.targetCount}</span> │
│ + </div> │
│ + )} │
│ + │
│ <div class="metadata-item"> │
│ - <span class="metadata-label">Simulate │
│ d</span> │
│ + <span class="metadata-label">Date Sim │
│ ulated</span> │
│ <span class="metadata-value">{simulat │
│ ionDate}</span> │
│ </div> │
│ + │
│ + {metadata.raidBuffs && ( │
│ + <div class="metadata-item metadata-it │
│ em-wide"> │
│ + <span class="metadata-label">Active │
│ Raid Buffs</span> │
│ + <span class="metadata-value metadat │
│ a-value-wrap">{formatRaidBuffs(metadata.raid │
│ Buffs)}</span> │
│ + </div> │
│ + )} │
│ </div> │
│ </div> │
│ │
│ @@ -108,9 +136,19 @@ const formatDuration = │
│ (seconds: number) => { │
│ font-weight: 600; │
│ } │
│ │
│ + .metadata-item-wide { │
│ + grid-column: 1 / -1; │
│ + } │
│ + │
│ + .metadata-value-wrap { │
│ + white-space: normal; │
│ + word-wrap: break-word; │
│ + line-height: 1.4; │
│ + } │
│ + │
│ @media (max-width: 768px) { │
│ .metadata-grid { │
│ grid-template-columns: 1fr; │
│ } │
│ } │
│ -</style> │
│ \ No newline at end of file │
│ +</style> │
└──────────────────────────────────────────────┘
┌─ web/src/pages/index.astro ────────────────────────────────────────────────┐
│ diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro │
│ index 6aba47c..3a5123a 100644 │
│ --- a/web/src/pages/index.astro │
│ +++ b/web/src/pages/index.astro │
│ @@ -1,9 +1,49 @@ │
│ --- │
│ -import RankingsLayout from '../layouts/RankingsLayout.astro'; │
│ +import DynamicRankings from '../components/DynamicRankings.astro'; │
│ --- │
│ │
│ -<RankingsLayout │
│ - title="Single Target DPS Rankings" │
│ - dataFile="dps_p1_raid_single_long.json" │
│ - description="Simulation results for single target raid encounters in MoP" │
│ -/> │
│ +<html lang="en"> │
│ + <head> │
│ + <meta charset="utf-8" /> │
│ + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> │
│ + <meta name="viewport" content="width=device-width" /> │
│ + <title>WoW MoP DPS Rankings</title> │
│ + <style> │
│ + body { │
│ + font-family: Arial, sans-serif; │
│ + margin: 0; │
│ + padding: 0; │
│ + background-color: #282828; │
│ + color: #ffffff; │
│ + } │
│ + │
│ + .page-header { │
│ + text-align: center; │
│ + padding: 30px 20px; │
│ + background-color: #3a3a3a; │
│ + border-bottom: 1px solid #4a4a4a; │
│ + } │
│ + │
│ + .page-title { │
│ + color: #ea6962; │
│ + margin: 0 0 10px 0; │
│ + font-size: 2.5em; │
│ + font-weight: 700; │
│ + } │
│ + │
│ + .page-description { │
│ + color: #aaaaaa; │
│ + font-size: 1.1em; │
│ + margin: 0; │
│ + } │
│ + </style> │
│ + </head> │
│ + <body> │
│ + <div class="page-header"> │
│ + <h1 class="page-title">WoW MoP DPS Rankings</h1> │
│ + <p class="page-description">Interactive simulation results for all encounte │
│ rs and target counts</p> │
│ + </div> │
│ + │
│ + <DynamicRankings client:load /> │
│ + </body> │
│ +</html> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/src/pages/index.astro ──────────┐
│ diff --git a/web/src/pages/index.astro b/web │
│ /src/pages/index.astro │
│ index 6aba47c..3a5123a 100644 │
│ --- a/web/src/pages/index.astro │
│ +++ b/web/src/pages/index.astro │
│ @@ -1,9 +1,49 @@ │
│ --- │
│ -import RankingsLayout from '../layouts/Rank │
│ ingsLayout.astro'; │
│ +import DynamicRankings from '../components/ │
│ DynamicRankings.astro'; │
│ --- │
│ │
│ -<RankingsLayout │
│ - title="Single Target DPS Rankings" │
│ - dataFile="dps_p1_raid_single_long.json" │
│ - description="Simulation results for singl │
│ e target raid encounters in MoP" │
│ -/> │
│ +<html lang="en"> │
│ + <head> │
│ + <meta charset="utf-8" /> │
│ + <link rel="icon" type="image/svg+xml" h │
│ ref="/favicon.svg" /> │
│ + <meta name="viewport" content="width=de │
│ vice-width" /> │
│ + <title>WoW MoP DPS Rankings</title> │
│ + <style> │
│ + body { │
│ + font-family: Arial, sans-serif; │
│ + margin: 0; │
│ + padding: 0; │
│ + background-color: #282828; │
│ + color: #ffffff; │
│ + } │
│ + │
│ + .page-header { │
│ + text-align: center; │
│ + padding: 30px 20px; │
│ + background-color: #3a3a3a; │
│ + border-bottom: 1px solid #4a4a4a; │
│ + } │
│ + │
│ + .page-title { │
│ + color: #ea6962; │
│ + margin: 0 0 10px 0; │
│ + font-size: 2.5em; │
│ + font-weight: 700; │
│ + } │
│ + │
│ + .page-description { │
│ + color: #aaaaaa; │
│ + font-size: 1.1em; │
│ + margin: 0; │
│ + } │
│ + </style> │
│ + </head> │
│ + <body> │
│ + <div class="page-header"> │
│ + <h1 class="page-title">WoW MoP DPS Ra │
│ nkings</h1> │
│ + <p class="page-description">Interacti │
│ ve simulation results for all encounters and │
│ target counts</p> │
│ + </div> │
│ + │
│ + <DynamicRankings client:load /> │
│ + </body> │
│ +</html> │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET