/** * Главная страница — интерактив, порт React-поведения на ванильный JS. * Данные и SVG-иконки приходят из window.DOTA2_HOME (inc/enqueue.php). */ (function () { 'use strict'; var DATA = window.DOTA2_HOME || {}; var EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; function icon(name, cls) { var svg = (DATA.icons && DATA.icons[name]) || ''; if (!svg) return ''; return cls ? svg.replace('/g, '>').replace(/"/g, '"'); } /* ---------------- reveal on scroll (порт motion whileInView) ---------------- */ (function initReveal() { var els = document.querySelectorAll('[data-reveal]'); if (!els.length) return; if (reduceMotion || typeof IntersectionObserver === 'undefined') return; els.forEach(function (el) { var type = el.getAttribute('data-reveal'); var y = parseFloat(el.getAttribute('data-reveal-y') || '0'); var x = parseFloat(el.getAttribute('data-reveal-x') || '0'); var dur = parseFloat(el.getAttribute('data-reveal-dur') || '400'); var delay = parseFloat(el.getAttribute('data-reveal-delay') || '0'); var amount = parseFloat(el.getAttribute('data-reveal-amount') || '0.15'); el.__reveal = { type: type, y: y, x: x, dur: dur, delay: delay, amount: amount }; if (type === 'bar') { el.style.transform = 'scaleY(0)'; } else { el.style.opacity = '0'; if (y || x) el.style.transform = 'translate(' + x + 'px,' + y + 'px)'; } }); var reveal = function (entry, io) { var el = entry.target; var r = el.__reveal; // как framer viewport.amount: показываем только когда видна заметная часть if (!entry.isIntersecting || entry.intersectionRatio < r.amount) return; io.unobserve(el); el.style.transition = 'opacity ' + r.dur + 'ms ' + EASE + ' ' + r.delay + 'ms, transform ' + r.dur + 'ms ' + EASE + ' ' + r.delay + 'ms'; requestAnimationFrame(function () { el.style.opacity = ''; el.style.transform = r.type === 'bar' ? 'scaleY(1)' : 'none'; if (el.querySelector) { el.querySelectorAll('[data-podium-bar]').forEach(function (bar) { bar.style.transition = 'transform 700ms ' + EASE + ' 150ms'; bar.style.transform = 'scaleX(1)'; }); } window.setTimeout(function () { el.style.transition = ''; el.style.transform = ''; }, r.dur + r.delay + 60); }); }; var observers = {}; els.forEach(function (el) { el.querySelectorAll && el.querySelectorAll('[data-podium-bar]').forEach(function (bar) { bar.style.transform = 'scaleX(0)'; bar.style.transformOrigin = 'left'; }); var amount = el.__reveal.amount; if (!observers[amount]) { observers[amount] = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { reveal(entry, observers[amount]); }); }, { threshold: amount }); } observers[amount].observe(el); }); })(); /* ---------------- animated metrics (порт animated-metric.tsx) ---------------- */ (function initMetrics() { var els = document.querySelectorAll('[data-metric]'); if (!els.length || typeof IntersectionObserver === 'undefined') return; var io = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (!entry.isIntersecting) return; var el = entry.target; io.unobserve(el); var value = parseFloat(el.getAttribute('data-metric-value') || '0'); var prefix = el.getAttribute('data-metric-prefix') || ''; var suffix = el.getAttribute('data-metric-suffix') || ''; if (reduceMotion) { el.textContent = prefix + value.toFixed(1) + suffix; return; } var start = performance.now(); var duration = 700; function tick(now) { var p = Math.min(1, (now - start) / duration); var eased = 1 - Math.pow(1 - p, 3); el.textContent = prefix + (value * eased).toFixed(1) + suffix; if (p < 1) requestAnimationFrame(tick); } el.textContent = prefix + '0.0' + suffix; requestAnimationFrame(tick); }); }, { threshold: 0.4 }); els.forEach(function (el) { io.observe(el); }); })(); /* ---------------- animated background (порт animated-dota-background.tsx) ---------------- */ (function initBackground() { var root = document.querySelector('[data-dota-bg]'); if (!root) return; if (!reduceMotion && root.querySelectorAll) { root.querySelectorAll('[data-bg-pulse]').forEach(function (el) { var parts = (el.getAttribute('data-bg-pulse') || '').split(','); var dur = parseFloat(parts[0] || '18') * 1000; var lo = parseFloat(parts[1] || '0.5'); var hi = parseFloat(parts[2] || '0.95'); if (el.animate) { el.animate([{ opacity: lo }, { opacity: hi }, { opacity: lo }], { duration: dur, iterations: Infinity, easing: 'ease-in-out' }); } }); } var coarse = window.matchMedia('(pointer: coarse)').matches; var mobile = window.matchMedia('(max-width: 767px)').matches; if (reduceMotion || coarse || mobile) return; var layers = root.querySelectorAll('[data-bg-parallax]'); var raf = 0; window.addEventListener('mousemove', function (e) { cancelAnimationFrame(raf); raf = requestAnimationFrame(function () { var px = (e.clientX / window.innerWidth - 0.5) * 2; var py = (e.clientY / window.innerHeight - 0.5) * 2; layers.forEach(function (layer) { var m = parseFloat(layer.getAttribute('data-bg-parallax') || '0'); layer.style.transition = 'transform 900ms cubic-bezier(0.16, 1, 0.3, 1)'; layer.style.transform = 'translate(' + px * m + 'px,' + py * m + 'px)'; }); }); }, { passive: true }); })(); /* ---------------- scroll down cue ---------------- */ (function initScrollCue() { var btn = document.querySelector('[data-scroll-cue]'); if (!btn) return; // Как в React (framer-motion): стрелка «плавает» независимо от prefers-reduced-motion. var bob = btn.querySelector('[data-cue-bob]'); if (bob && bob.animate) { bob.animate( [ { transform: 'translateY(0)', opacity: 0.8 }, { transform: 'translateY(10px)', opacity: 1 }, { transform: 'translateY(0)', opacity: 0.8 } ], { duration: 2800, iterations: Infinity, easing: 'ease-in-out' } ); } btn.addEventListener('click', function () { var next = document.getElementById('tournaments') || document.querySelector('[data-next-section]'); if (next) { var top = next.getBoundingClientRect().top + window.scrollY; window.scrollTo({ top: Math.max(0, top - 80), behavior: 'smooth' }); return; } window.scrollBy({ top: window.innerHeight * 0.85, behavior: 'smooth' }); }); })(); /* ---------------- featured news carousel (порт featured-news-carousel.tsx) ---------------- */ (function initFeaturedCarousel() { var root = document.querySelector('[data-featured-carousel]'); if (!root) return; var track = root.querySelector('[data-carousel-track]'); var slides = root.querySelectorAll('[data-carousel-slide]'); var dots = root.querySelectorAll('[data-carousel-dot]'); var counter = root.querySelector('[data-carousel-counter]'); var delay = parseInt(root.getAttribute('data-autoplay') || '5000', 10); var count = slides.length; var current = 0; var timer = 0; var hovered = false; var visible = true; var DOT_ACTIVE = ['scale-110', 'border-accent-hot', 'bg-accent-hot', 'shadow-[0_0_10px_2px_color-mix(in_srgb,var(--accent-hot)_55%,transparent)]']; var DOT_IDLE = ['border-white/25', 'bg-white/10', 'hover:border-[color-mix(in_srgb,var(--accent-hot)_60%,transparent)]', 'hover:bg-white/20']; function pad(n) { return String(n).padStart(2, '0'); } function render() { // Движение трека — через Web Animations API, как embla в React (JS-движок, // не зависит от глобального прибития CSS-transition при prefers-reduced-motion). var target = 'translateX(-' + current * 100 + '%)'; if (track.animate && track.style.transform !== target) { var from = getComputedStyle(track).transform; track.style.transform = target; track.animate( [ { transform: 'none' === from ? 'translateX(0px)' : from }, { transform: target } ], { duration: 700, easing: EASE } ); } else { track.style.transform = target; } slides.forEach(function (slide, i) { if (i === current) slide.removeAttribute('aria-hidden'); else slide.setAttribute('aria-hidden', 'true'); var img = slide.querySelector('[data-carousel-img]'); if (img) { img.classList.toggle('scale-[1.05]', i === current); img.classList.toggle('scale-[1.09]', i !== current); } }); dots.forEach(function (dot, i) { var active = i === current; dot.setAttribute('aria-selected', active ? 'true' : 'false'); DOT_ACTIVE.forEach(function (c) { dot.classList.toggle(c, active); }); DOT_IDLE.forEach(function (c) { dot.classList.toggle(c, !active); }); var ring = dot.querySelector('[data-carousel-ring]'); if (active && !ring) { dot.insertAdjacentHTML('beforeend', ''); } else if (!active && ring) { ring.remove(); } }); if (counter) counter.textContent = pad(current + 1) + ' / ' + pad(count); } function goTo(i) { current = (i + count) % count; render(); } function stop() { window.clearInterval(timer); timer = 0; } function play() { // Автоплей — как embla-carousel-autoplay в React: работает всегда. stop(); timer = window.setInterval(function () { if (!hovered && visible) goTo(current + 1); }, delay); } root.querySelector('[data-carousel-prev]').addEventListener('click', function () { goTo(current - 1); play(); }); root.querySelector('[data-carousel-next]').addEventListener('click', function () { goTo(current + 1); play(); }); dots.forEach(function (dot, i) { dot.addEventListener('click', function () { goTo(i); play(); }); }); root.addEventListener('mouseenter', function () { hovered = true; }); root.addEventListener('mouseleave', function () { hovered = false; }); root.addEventListener('focusin', function () { hovered = true; }); root.addEventListener('focusout', function () { hovered = false; }); if (typeof IntersectionObserver !== 'undefined') { new IntersectionObserver(function (entries) { visible = !!(entries[0] && entries[0].isIntersecting); }, { threshold: 0.15 }).observe(root); } play(); })(); /* ---------------- match tabs (порт match-tabs.tsx) ---------------- */ (function initMatchTabs() { var section = document.querySelector('[data-matches-section]'); if (!section) return; var tabs = section.querySelectorAll('[data-match-tab]'); tabs.forEach(function (tab) { tab.addEventListener('click', function () { var value = tab.getAttribute('data-match-tab'); tabs.forEach(function (t) { var active = t === tab; t.setAttribute('aria-selected', active ? 'true' : 'false'); t.classList.toggle('text-accent-hot', active); t.classList.toggle('text-txt-3', !active); t.classList.toggle('hover:text-txt-2', !active); var ind = t.querySelector('[data-tab-indicator]'); if (ind) ind.hidden = !active; }); section.querySelectorAll('[data-match-list]').forEach(function (list) { list.hidden = list.getAttribute('data-match-list') !== value; }); }); }); })(); /* ---------------- top heroes role filter ---------------- */ (function initTopHeroes() { var section = document.querySelector('[data-top-heroes]'); if (!section) return; var buttons = section.querySelectorAll('[data-top-role]'); var ACTIVE = ['bg-[color-mix(in_srgb,var(--accent-hot)_18%,transparent)]', 'text-txt-1', 'shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--accent-hot)_45%,transparent)]']; var IDLE = ['text-txt-3', 'hover:bg-white/[0.04]', 'hover:text-txt-1']; buttons.forEach(function (btn) { btn.addEventListener('click', function () { var role = btn.getAttribute('data-top-role'); buttons.forEach(function (b) { var active = b === btn; b.setAttribute('aria-pressed', active ? 'true' : 'false'); ACTIVE.forEach(function (c) { b.classList.toggle(c, active); }); IDLE.forEach(function (c) { b.classList.toggle(c, !active); }); }); section.querySelectorAll('[data-top-panel]').forEach(function (panel) { var show = panel.getAttribute('data-top-panel') === role; panel.hidden = !show; if (show && !reduceMotion) { panel.style.opacity = '0'; panel.style.transform = 'translateY(8px)'; panel.style.transition = 'opacity 300ms ' + EASE + ', transform 300ms ' + EASE; requestAnimationFrame(function () { panel.style.opacity = '1'; panel.style.transform = 'none'; }); window.setTimeout(function () { panel.style.cssText = ''; }, 360); } }); section.querySelectorAll('[data-top-ranking]').forEach(function (list) { list.hidden = list.getAttribute('data-top-ranking') !== role; }); }); }); })(); /* ---------------- scroll-карусели (замена embla dragFree) ---------------- */ function bindScrollCarousel(root) { var viewport = root.querySelector('[data-carousel-viewport]'); var prev = root.querySelector('[data-carousel-prev]'); var next = root.querySelector('[data-carousel-next]'); if (!viewport || !prev || !next) return null; function sync() { var max = viewport.scrollWidth - viewport.clientWidth - 1; prev.disabled = viewport.scrollLeft <= 0; next.disabled = viewport.scrollLeft >= max; } prev.addEventListener('click', function () { viewport.scrollBy({ left: -Math.max(220, viewport.clientWidth * 0.6), behavior: reduceMotion ? 'auto' : 'smooth' }); }); next.addEventListener('click', function () { viewport.scrollBy({ left: Math.max(220, viewport.clientWidth * 0.6), behavior: reduceMotion ? 'auto' : 'smooth' }); }); viewport.addEventListener('scroll', sync, { passive: true }); window.addEventListener('resize', sync); sync(); return { sync: sync, viewport: viewport }; } /* ---------------- trending builds ---------------- */ (function initBuilds() { var section = document.querySelector('[data-trending-builds]'); if (!section) return; var carousel = bindScrollCarousel(section.querySelector('[data-build-carousel]')); var roleButtons = section.querySelectorAll('[data-build-role]'); var cards = section.querySelectorAll('[data-build-card]'); var wraps = section.querySelectorAll('[data-build-card-wrap]'); var panels = section.querySelectorAll('[data-build-panel]'); var empty = section.querySelector('[data-build-empty]'); var CARD_ON = ['border-[color-mix(in_srgb,var(--accent-hot)_60%,transparent)]', 'shadow-[0_0_22px_rgba(228,73,48,0.16)]']; var CARD_OFF = ['border-white/8', 'hover:border-white/20']; function selectBuild(id) { cards.forEach(function (card) { var on = card.getAttribute('data-build-card') === id; card.setAttribute('aria-pressed', on ? 'true' : 'false'); CARD_ON.forEach(function (c) { card.classList.toggle(c, on); }); CARD_OFF.forEach(function (c) { card.classList.toggle(c, !on); }); }); var shown = null; panels.forEach(function (panel) { var on = panel.getAttribute('data-build-panel') === id; panel.hidden = !on; if (on) shown = panel; }); if (empty) empty.hidden = !!shown; if (shown && !reduceMotion) { shown.style.opacity = '0'; shown.style.transform = 'translateX(10px)'; shown.style.transition = 'opacity 300ms ' + EASE + ', transform 300ms ' + EASE; requestAnimationFrame(function () { shown.style.opacity = '1'; shown.style.transform = 'none'; }); window.setTimeout(function () { shown.style.cssText = ''; }, 360); } } cards.forEach(function (card) { card.addEventListener('click', function () { selectBuild(card.getAttribute('data-build-card')); }); }); roleButtons.forEach(function (btn) { btn.addEventListener('click', function () { var role = btn.getAttribute('data-build-role'); roleButtons.forEach(function (b) { var active = b === btn; b.setAttribute('aria-pressed', active ? 'true' : 'false'); b.classList.toggle('text-accent-hot', active); b.classList.toggle('text-txt-2', !active); b.classList.toggle('hover:text-txt-1', !active); var pill = b.querySelector('[data-role-pill]'); if (pill) pill.hidden = !active; var svg = b.querySelector('svg'); if (svg) { svg.classList.toggle('text-accent-hot', active); svg.classList.toggle('text-txt-3', !active); } }); var first = null; wraps.forEach(function (wrap) { var show = role === 'all' || wrap.getAttribute('data-role') === role; wrap.hidden = !show; if (show && !first) { var card = wrap.querySelector('[data-build-card]'); if (card) first = card.getAttribute('data-build-card'); } }); if (first) { selectBuild(first); } else { panels.forEach(function (panel) { panel.hidden = true; }); if (empty) empty.hidden = false; } if (carousel) carousel.sync(); }); }); })(); /* ---------------- counter picks ---------------- */ (function initCounters() { var section = document.querySelector('[data-counter-picks]'); if (!section) return; bindScrollCarousel(section.querySelector('[data-counter-carousel]')); var tabs = section.querySelectorAll('[data-counter-metric]'); var track = section.querySelector('[data-counter-track]'); tabs.forEach(function (tab) { tab.addEventListener('click', function () { var metric = tab.getAttribute('data-counter-metric'); tabs.forEach(function (t) { var active = t === tab; t.setAttribute('aria-pressed', active ? 'true' : 'false'); t.classList.toggle('text-txt-1', active); t.classList.toggle('text-txt-3', !active); t.classList.toggle('hover:text-txt-2', !active); var pill = t.querySelector('[data-metric-pill]'); if (pill) pill.hidden = !active; }); var items = Array.prototype.slice.call(track.querySelectorAll('[data-counter-item]')); items.sort(function (a, b) { var key = metric === 'winRate' ? 'data-win-rate' : 'data-pick-rate'; return parseFloat(b.getAttribute(key)) - parseFloat(a.getAttribute(key)); }); items.forEach(function (item) { var value = metric === 'winRate' ? item.getAttribute('data-win-rate') : item.getAttribute('data-pick-rate'); item.querySelector('[data-counter-value]').textContent = value + '%'; item.querySelector('[data-counter-label]').textContent = metric === 'winRate' ? 'Win Rate' : 'Pick Rate'; track.appendChild(item); }); }); }); section.querySelectorAll('[data-counter-filter]').forEach(function (cell) { var toggle = cell.querySelector('[data-filter-toggle]'); var list = cell.querySelector('[data-filter-list]'); var value = cell.querySelector('[data-filter-value]'); toggle.addEventListener('click', function () { var open = list.hidden; list.hidden = !open; toggle.setAttribute('aria-expanded', open ? 'true' : 'false'); }); cell.querySelectorAll('[data-filter-option]').forEach(function (opt) { opt.addEventListener('click', function () { var next = opt.getAttribute('data-filter-option'); value.textContent = next; cell.querySelectorAll('[data-filter-option]').forEach(function (o) { var on = o === opt; o.classList.toggle('text-accent-hot', on); o.classList.toggle('text-txt-2', !on); }); list.hidden = true; toggle.setAttribute('aria-expanded', 'false'); }); }); }); var method = section.querySelector('[data-counter-method]'); if (method) { var mToggle = method.querySelector('[data-method-toggle]'); var mPanel = method.querySelector('[data-method-panel]'); var mIcon = mToggle.querySelector('svg:last-of-type'); mToggle.addEventListener('click', function () { var open = mPanel.hidden; mPanel.hidden = !open; mToggle.setAttribute('aria-expanded', open ? 'true' : 'false'); if (mIcon) mIcon.classList.toggle('rotate-45', open); }); } })(); /* ---------------- leaderboard (порт leaderboard-table.tsx) ---------------- */ (function initLeaderboard() { var section = document.querySelector('[data-leaderboard]'); if (!section || !DATA.players) return; var state = { region: 'all', role: 'all', sort: 'mmr', direction: 'desc' }; var tbody = section.querySelector('[data-lb-tbody]'); var mobile = section.querySelector('[data-lb-mobile]'); var emptyEl = section.querySelector('[data-lb-empty]'); var countEl = section.querySelector('[data-lb-count]'); var resetBtn = section.querySelector('[data-lb-reset]'); var COLS = 'grid grid-cols-[52px_minmax(150px,1.4fr)_minmax(130px,1fr)_84px_120px_104px_92px_140px_86px] items-center gap-3 px-4'; var fmt = function (n) { return Number(n).toLocaleString('en-US'); }; function rowTone(rank) { if (rank === 1) return 'border-l-2 border-l-accent-hot bg-[linear-gradient(90deg,rgba(228,73,48,0.06),transparent_45%)]'; if (rank === 2) return 'border-l-2 border-l-white/20'; if (rank === 3) return 'border-l-2 border-l-[rgba(176,110,63,0.5)]'; return 'border-l-2 border-l-transparent'; } function rankBadge(rank) { if (rank > 3) return '' + rank + ''; var tone = rank === 1 ? 'border-[rgba(212,165,86,0.55)] text-[#e3bd75] shadow-[0_0_14px_rgba(212,165,86,0.16)] bg-[linear-gradient(150deg,rgba(212,165,86,0.18),transparent)]' : rank === 2 ? 'border-white/22 text-[#d6d2cb] bg-[linear-gradient(150deg,rgba(255,255,255,0.10),transparent)]' : 'border-[rgba(176,110,63,0.45)] text-[#c58a5c] bg-[linear-gradient(150deg,rgba(176,110,63,0.16),transparent)]'; return '' + rank + ''; } function avatar(p) { // Реальное фото игрока — приоритетно, иначе инициалы. if (p.photo) { return ''; } return ''; } function teamCell(p) { if (!p.team) return 'Free agent'; var logo = p.team.logo ? '' : '' + esc(p.team.name.slice(0, 2)) + ''; return '' + logo + '' + esc(p.team.name) + ''; } function regionCell(region) { var tone = (DATA.regionTone && DATA.regionTone[region]) || 'from-white/20 to-white/5'; return '' + esc(region) + ''; } function roleCell(role) { var meta = DATA.roleMeta[role]; return '' + icon(meta.icon, 'h-3.5 w-3.5 text-txt-3') + esc(meta.label) + ''; } function formCell(form) { var label = form.map(function (r) { return r === 'W' ? 'Win' : 'Loss'; }).join(', '); var cells = form.map(function (r) { var cls = r === 'W' ? 'border-[rgba(66,201,139,0.28)] bg-[rgba(66,201,139,0.12)] text-[#42c98b]' : 'border-[rgba(228,73,48,0.28)] bg-[rgba(228,73,48,0.10)] text-[#e4795f]'; return ''; }).join(''); return '' + cells + ''; } function trendCell(trend) { if (trend === undefined || trend === null) return ''; var pos = trend > 0; var neg = trend < 0; var cls = pos ? 'text-[#42c98b]' : neg ? 'text-[#e44930]' : 'text-txt-3'; var arrow = pos ? '▲' : neg ? '▼' : '—'; var value = pos ? '+' + fmt(trend) : fmt(trend); return ' ' + value + ''; } function filtered() { var list = DATA.players.filter(function (p) { return (state.region === 'all' || p.region === state.region) && (state.role === 'all' || p.role === state.role); }); var value = function (p) { return state.sort === 'mmr' ? (p.mmr || 0) : state.sort === 'winRate' ? (p.winRate || 0) : (p.trend || 0); }; list.sort(function (a, b) { return state.direction === 'desc' ? value(b) - value(a) : value(a) - value(b); }); return list.slice(0, 10); } function render() { var players = filtered(); tbody.innerHTML = players.map(function (p) { return '' + '' + rankBadge(p.rank) + '' + '' + avatar(p) + '' + esc(p.nickname) + '' + '' + teamCell(p) + '' + '' + regionCell(p.region) + '' + '' + roleCell(p.role) + '' + '' + fmt(p.mmr) + '' + '' + p.winRate.toFixed(1) + '%' + '' + formCell(p.recentForm) + '' + '' + trendCell(p.trend) + '' + ''; }).join(''); mobile.innerHTML = players.slice(0, 7).map(function (p) { return '
  • ' + '
    ' + rankBadge(p.rank) + '' + avatar(p) + '' + esc(p.nickname) + '' + '' + esc((p.team ? p.team.name : 'Free agent') + ' · ' + DATA.roleMeta[p.role].label + ' · ' + p.region) + '' + '' + fmt(p.mmr) + '
    ' + '
    ' + '' + p.winRate.toFixed(1) + '% WR' + trendCell(p.trend) + '' + formCell(p.recentForm) + '
  • '; }).join(''); emptyEl.hidden = players.length > 0; countEl.textContent = 'Showing top ' + players.length + ' players'; resetBtn.hidden = players.length > 0; section.querySelectorAll('[data-lb-sort]').forEach(function (btn) { var active = btn.getAttribute('data-lb-sort') === state.sort; btn.classList.toggle('text-txt-1', active); var chev = btn.querySelector('svg'); if (chev) { chev.classList.toggle('opacity-90', active); chev.classList.toggle('opacity-30', !active); chev.classList.toggle('rotate-180', active && state.direction === 'asc'); } }); } function bindSegment(group, key) { var buttons = section.querySelectorAll('[data-lb-' + group + ']'); buttons.forEach(function (btn) { btn.addEventListener('click', function () { state[key] = btn.getAttribute('data-lb-' + group); buttons.forEach(function (b) { var active = b === btn; b.setAttribute('aria-pressed', active ? 'true' : 'false'); b.classList.toggle('text-txt-1', active); b.classList.toggle('text-txt-3', !active); b.classList.toggle('hover:text-txt-2', !active); var pill = b.querySelector('[data-lb-pill]'); if (pill) pill.hidden = !active; }); render(); }); }); } bindSegment('region', 'region'); bindSegment('role', 'role'); section.querySelectorAll('[data-lb-sort]').forEach(function (btn) { btn.addEventListener('click', function () { var key = btn.getAttribute('data-lb-sort'); if (key === state.sort) { state.direction = state.direction === 'desc' ? 'asc' : 'desc'; } else { state.sort = key; state.direction = 'desc'; } render(); }); }); resetBtn.addEventListener('click', function () { state.region = 'all'; state.role = 'all'; section.querySelectorAll('[data-lb-region="all"], [data-lb-role="all"]').forEach(function (b) { b.click(); }); }); })(); /* ---------------- tournament: автопрокрутка сетки к текущей стадии ---------------- */ (function initBracketScroll() { var wrap = document.querySelector('[data-bracket-scroll]'); if (!wrap || wrap.scrollWidth <= wrap.clientWidth) return; var x = parseInt(wrap.getAttribute('data-scroll-x') || '0', 10); // Целевая колонка — ближе к правому краю видимой области (прошлые слева). var left = Math.max(0, Math.min(x - wrap.clientWidth * 0.45, wrap.scrollWidth - wrap.clientWidth)); wrap.scrollLeft = left; })(); /* ---------------- tournament: mobile lower bracket toggle ---------------- */ (function initLowerBracket() { var toggle = document.querySelector('[data-lower-toggle]'); var panel = document.querySelector('[data-lower-panel]'); if (!toggle || !panel) return; toggle.addEventListener('click', function () { var open = panel.hidden; panel.hidden = !open; toggle.setAttribute('aria-expanded', open ? 'true' : 'false'); var chev = toggle.querySelector('svg'); if (chev) chev.classList.toggle('rotate-180', open); }); })(); /* ---------------- draft lab (порт draft-service.ts + draft-lab-section.tsx) ---------------- */ (function initDraft() { var section = document.querySelector('[data-draft-lab]'); if (!section || !DATA.draftPool) return; var pool = DATA.draftPool; var byId = {}; pool.forEach(function (h) { byId[h.id] = h; }); var ALL_ROLES = ['carry', 'mid', 'offlane', 'support', 'hard-support']; var NEED_META = [ { id: 'frontline', label: 'Frontline', icon: 'shield-half' }, { id: 'initiation', label: 'Initiation', icon: 'flame' }, { id: 'teamfight', label: 'Teamfight', icon: 'swords' }, { id: 'disables', label: 'Disables', icon: 'zap' }, { id: 'push', label: 'Push', icon: 'layers' } ]; var TONE = { good: { bar: '#42c98b', text: 'text-[#42c98b]', label: 'Good', icon: 'check' }, medium: { bar: '#e0872f', text: 'text-[#e0872f]', label: 'Medium', icon: 'circle-alert' }, low: { bar: '#e44930', text: 'text-accent-hot', label: 'Low', icon: 'x' } }; var MARS_REASONS = [ { id: 'r1', title: 'Strong teamfight presence', description: 'Arena of Blood and Spear of Mars provide game-changing teamfight control.', icon: 'swords' }, { id: 'r2', title: 'Frontline your team needs', description: 'Adds a durable initiator to pair with your carries and supports.', icon: 'shield-half' }, { id: 'r3', title: 'Excels against their draft', description: 'Punishes mobile enemy cores and reduces their freedom in fights.', icon: 'target' }, { id: 'r4', title: 'Flexible item build paths', description: 'Can adapt initiation or defensive items depending on game state.', icon: 'wand-2' } ]; var SLOT_CLIP = 'polygon(0 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%)'; var TONES = { radiant: { border: 'border-[rgba(66,201,139,0.28)]', hover: 'hover:border-[rgba(66,201,139,0.55)]', glow: 'group-hover:bg-[radial-gradient(circle_at_50%_100%,rgba(66,201,139,0.16),transparent_70%)]', text: 'text-[#42c98b]' }, dire: { border: 'border-[rgba(228,73,48,0.28)]', hover: 'hover:border-[rgba(228,73,48,0.55)]', glow: 'group-hover:bg-[radial-gradient(circle_at_50%_100%,rgba(228,73,48,0.16),transparent_70%)]', text: 'text-accent-hot' } }; var initial = { radiant: { picks: ['puck', 'juggernaut', 'lion', null, null], bans: [null, null, null, null, null, null] }, dire: { picks: ['doom_bringer', 'storm_spirit', 'shadow_shaman', null, null], bans: [null, null, null, null, null, null, null] } }; var state = JSON.parse(JSON.stringify(initial)); var preview = null; var picker = null; // { side, slot, mode } var analyzeTimer = 0; var analyzeAnim = null; function heroesOf(team) { return team.picks.filter(Boolean).map(function (id) { return byId[id]; }); } function blockedIds() { return [] .concat(state.radiant.picks.filter(Boolean)) .concat(state.dire.picks.filter(Boolean)) .concat(state.radiant.bans.filter(Boolean)) .concat(state.dire.bans.filter(Boolean)); } function missingRoles(team) { var taken = heroesOf(team).map(function (h) { return h.role; }); var remaining = ALL_ROLES.slice(); taken.forEach(function (role) { var i = remaining.indexOf(role); if (i >= 0) remaining.splice(i, 1); }); return remaining; } function enemyProfile(team) { var heroes = heroesOf(team); var base = { frontline: 0, initiation: 0, teamfight: 0, disables: 0, push: 0 }; if (!heroes.length) return base; heroes.forEach(function (h) { var t = h.traits; base.frontline += t.frontline / heroes.length; base.initiation += t.initiation / heroes.length; base.teamfight += t.teamfight / heroes.length; base.disables += t.disables / heroes.length; base.push += t.push / heroes.length; }); return base; } function statusOf(score) { return score >= 70 ? 'good' : score >= 45 ? 'medium' : 'low'; } function computeNeeds(team, previewHero) { var heroes = heroesOf(team).concat(previewHero ? [previewHero] : []); return NEED_META.map(function (meta) { var values = heroes.map(function (h) { return h.traits[meta.id] || 0; }).sort(function (a, b) { return b - a; }); var best = values[0] || 0; var weights = [0.16, 0.11, 0.07, 0.05]; var support = values.slice(1).reduce(function (acc, v, i) { return acc + v * (weights[i] !== undefined ? weights[i] : 0.03); }, 0); var score = Math.max(0, Math.min(99, Math.round(best * 0.55 + support))); return { id: meta.id, label: meta.label, score: score, status: statusOf(score), icon: meta.icon }; }); } function genericReasons(hero) { var t = hero.traits; return [ { id: 'r1', title: t.teamfight >= 70 ? 'Strong teamfight presence' : 'Reliable lane presence', description: hero.name + ' adds dependable value to grouped fights around objectives.', icon: 'swords' }, { id: 'r2', title: t.frontline >= 70 ? 'Frontline your team needs' : 'Covers a missing damage angle', description: 'Fills the gap your current lineup leaves open as a ' + hero.roleLabel.toLowerCase() + '.', icon: 'shield-half' }, { id: 'r3', title: 'Works against their draft', description: hero.name + ' matches up well into the heroes the enemy has already committed to.', icon: 'target' }, { id: 'r4', title: 'Flexible item build paths', description: 'Can shift between utility and damage items as the game develops.', icon: 'wand-2' } ]; } function tagsFor(hero) { var t = hero.traits; var tags = []; if (t.frontline >= 70) tags.push('Frontline'); if (t.initiation >= 70) tags.push('Initiation'); if (t.teamfight >= 70) tags.push('Teamfight'); if (t.disables >= 75) tags.push('Disables'); if (t.push >= 75) tags.push('Push'); var enemy = heroesOf(state.dire)[0]; if (enemy) tags.push('Strong vs ' + enemy.name); return tags.slice(0, 4); } function fitScore(hero, needs) { var gap = needs.reduce(function (acc, n) { return acc + ((100 - n.score) / 100) * hero.traits[n.id]; }, 0); return gap / needs.length; } function candidateScore(hero, needs, missing, enemy) { var need = fitScore(hero, needs); var roleBonus = missing.length === 0 ? 0 : missing.indexOf(hero.role) >= 0 ? 26 : -22; var counter = (enemy.teamfight / 100) * hero.traits.disables * 0.16 + (enemy.initiation / 100) * hero.traits.frontline * 0.14 + (enemy.push / 100) * hero.traits.teamfight * 0.08; return need + roleBonus + counter; } function isDemoDraft() { return state.radiant.picks.filter(Boolean).join(',') === 'puck,juggernaut,lion' && state.dire.picks.filter(Boolean).join(',') === 'doom_bringer,storm_spirit,shadow_shaman' && state.radiant.bans.concat(state.dire.bans).every(function (b) { return !b; }); } function demoRecommendation() { return { hero: byId.mars, score: 87, tags: ['Frontline', 'Initiation', 'Teamfight', 'Strong vs Doom'], reasons: MARS_REASONS, alternatives: [byId.centaur, byId.axe, byId.tidehunter], roleTarget: 'Offlane', complete: false, needs: [ { id: 'frontline', label: 'Frontline', score: 82, status: 'good', icon: 'shield-half' }, { id: 'initiation', label: 'Initiation', score: 64, status: 'medium', icon: 'flame' }, { id: 'teamfight', label: 'Teamfight', score: 78, status: 'good', icon: 'swords' }, { id: 'disables', label: 'Disables', score: 48, status: 'medium', icon: 'zap' }, { id: 'push', label: 'Push', score: 28, status: 'low', icon: 'layers' } ] }; } function getRecommendation() { if (isDemoDraft() && !preview) return demoRecommendation(); var taken = blockedIds(); var takenSet = {}; taken.forEach(function (id) { takenSet[id] = true; }); var baseNeeds = computeNeeds(state.radiant, null); var missing = missingRoles(state.radiant); var enemy = enemyProfile(state.dire); var complete = heroesOf(state.radiant).length >= 5; var candidates = pool .filter(function (h) { return !takenSet[h.id]; }) .map(function (h) { return { hero: h, fit: candidateScore(h, baseNeeds, missing, enemy) }; }) .sort(function (a, b) { return b.fit - a.fit; }); var top = candidates[0]; var hero = preview && !takenSet[preview] ? byId[preview] : (top ? top.hero : pool[0]); var needs = computeNeeds(state.radiant, hero); var rawScore = candidateScore(hero, baseNeeds, missing, enemy); var score = Math.max(45, Math.min(96, Math.round(38 + rawScore * 0.62))); var alternatives = candidates .filter(function (c) { return c.hero.id !== hero.id; }) .slice(0, 3) .map(function (c) { return c.hero; }); var rec = { hero: hero, score: score, tags: tagsFor(hero), reasons: hero.id === 'mars' ? MARS_REASONS : genericReasons(hero), alternatives: alternatives, needs: needs, complete: complete }; if (missing[0]) { var labels = { carry: 'Carry', mid: 'Mid', offlane: 'Offlane', support: 'Support', 'hard-support': 'Hard Support' }; rec.roleTarget = labels[missing[0]]; } return rec; } /* ---------- рендер ---------- */ function slotHtml(heroId, side, index) { var tone = TONES[side]; var label = side === 'radiant' ? 'Radiant' : 'Dire'; if (!heroId) { return ''; } var hero = byId[heroId]; return '
    ' + '' + esc(hero.name) + ' portrait' + '' + '
    ' + '' + '' + '
    ' + '
    ' + '

    ' + esc(hero.name) + '

    ' + '

    ' + esc(hero.roleLabel) + '

    ' + '
    ' + '' + label + ' pick ' + (index + 1) + '' + '
    '; } function banHtml(heroId, side, index) { var label = side === 'radiant' ? 'Radiant' : 'Dire'; if (!heroId) { return ''; } var hero = byId[heroId]; return ''; } function renderSides() { ['radiant', 'dire'].forEach(function (side) { var col = section.querySelector('[data-draft-side="' + side + '"]'); state[side].picks.forEach(function (heroId, i) { col.querySelector('[data-draft-slot="' + i + '"]').innerHTML = slotHtml(heroId, side, i); }); state[side].bans.forEach(function (heroId, i) { col.querySelector('[data-draft-ban="' + i + '"]').innerHTML = banHtml(heroId, side, i); }); }); } function needsHtml(needs) { return needs.map(function (need) { var t = TONE[need.status]; return '
  • ' + '' + icon(need.icon, 'h-3.5 w-3.5 shrink-0 text-txt-3') + '' + esc(need.label) + '' + '' + '' + need.score + '%' + '' + icon(t.icon, 'h-3 w-3') + t.label + '' + '
  • '; }).join(''); } function renderRecommendation(analyzing) { var rec = getRecommendation(); var box = section.querySelector('[data-recommended-pick]'); var headline = rec.complete ? 'Radiant lineup is full' : analyzing ? 'Analyzing draft...' : rec.roleTarget ? 'Recommended ' + rec.roleTarget : 'Recommended pick'; var img = box.querySelector('[data-rec-image]'); img.src = rec.hero.image; img.alt = rec.hero.name + ' artwork'; box.querySelector('[data-rec-headline]').textContent = headline; box.querySelector('[data-rec-name]').textContent = rec.hero.name; box.querySelector('[data-rec-score]').textContent = rec.score; box.querySelector('[data-rec-sub]').textContent = rec.complete ? 'Remove a pick to get a suggestion' : 'Draft score'; box.querySelector('[data-rec-tags]').innerHTML = rec.tags.map(function (t) { return '+ ' + esc(t) + ''; }).join(''); box.querySelector('[data-rec-alts]').innerHTML = rec.alternatives.map(function (alt) { var pressed = preview === alt.id; return ''; }).join(''); box.querySelectorAll('[data-alt-id]').forEach(function (btn) { btn.addEventListener('click', function () { var id = btn.getAttribute('data-alt-id'); preview = preview === id ? null : id; update(); }); }); section.querySelector('[data-rec-reasons]').innerHTML = rec.reasons.map(function (r) { return '
  • ' + '' + icon(r.icon, 'h-3.5 w-3.5 text-[#42c98b]') + '' + '' + esc(r.title) + '' + '' + esc(r.description) + '' + '
  • '; }).join(''); section.querySelectorAll('[data-rec-needs]').forEach(function (list) { list.innerHTML = needsHtml(rec.needs); }); } function update() { renderSides(); renderRecommendation(true); var shimmer = section.querySelector('[data-rec-analyzing]'); var bar = section.querySelector('[data-rec-analyzing-bar]'); if (shimmer && !reduceMotion) { shimmer.hidden = false; if (analyzeAnim) analyzeAnim.cancel(); if (bar && bar.animate) { analyzeAnim = bar.animate( [{ transform: 'translateX(-100%)' }, { transform: 'translateX(300%)' }], { duration: 1100, iterations: Infinity, easing: 'linear' } ); } } window.clearTimeout(analyzeTimer); analyzeTimer = window.setTimeout(function () { if (shimmer) shimmer.hidden = true; if (analyzeAnim) { analyzeAnim.cancel(); analyzeAnim = null; } renderRecommendation(false); }, 380); } /* ---------- hero picker ---------- */ var pickerRoot = section.querySelector('[data-hero-picker]'); var pickerTitle = pickerRoot.querySelector('[data-picker-title]'); var pickerSearch = pickerRoot.querySelector('[data-picker-search]'); var pickerRoleButtons = pickerRoot.querySelectorAll('[data-picker-role]'); var pickerHeroes = pickerRoot.querySelectorAll('[data-picker-hero]'); var pickerEmpty = pickerRoot.querySelector('[data-picker-empty]'); var pickerRole = 'all'; function openPicker(side, slot, mode) { picker = { side: side, slot: slot, mode: mode }; pickerTitle.textContent = mode === 'ban' ? (side === 'radiant' ? 'Ban a hero (Your team)' : 'Ban a hero (Enemy team)') : (side === 'radiant' ? 'Add hero — Your team' : 'Add hero — Enemy team'); pickerSearch.value = ''; pickerRole = 'all'; syncPickerRoles(); filterPicker(); pickerRoot.hidden = false; document.body.style.overflow = 'hidden'; pickerSearch.focus(); } function closePicker() { picker = null; pickerRoot.hidden = true; document.body.style.overflow = ''; } function syncPickerRoles() { pickerRoleButtons.forEach(function (b) { var active = b.getAttribute('data-picker-role') === pickerRole; b.setAttribute('aria-pressed', active ? 'true' : 'false'); b.classList.toggle('bg-[color-mix(in_srgb,var(--accent-hot)_18%,transparent)]', active); b.classList.toggle('text-txt-1', active); b.classList.toggle('shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--accent-hot)_45%,transparent)]', active); b.classList.toggle('text-txt-3', !active); b.classList.toggle('hover:bg-white/[0.04]', !active); b.classList.toggle('hover:text-txt-1', !active); }); } function filterPicker() { var query = pickerSearch.value.trim().toLowerCase(); var taken = {}; blockedIds().forEach(function (id) { taken[id] = true; }); var any = false; pickerHeroes.forEach(function (li) { var id = li.getAttribute('data-hero-id'); var role = li.getAttribute('data-hero-role'); var name = li.getAttribute('data-hero-name'); var show = (pickerRole === 'all' || role === pickerRole) && name.indexOf(query) !== -1; li.hidden = !show; if (show) any = true; var btn = li.querySelector('button'); var disabled = !!taken[id]; btn.disabled = disabled; btn.classList.toggle('cursor-not-allowed', disabled); btn.classList.toggle('opacity-35', disabled); btn.classList.toggle('hover:border-[rgba(228,73,48,0.45)]', !disabled); li.querySelector('[data-hero-status]').textContent = disabled ? 'In draft' : byId[id].roleLabel; }); pickerEmpty.hidden = any; } pickerSearch.addEventListener('input', filterPicker); pickerRoleButtons.forEach(function (b) { b.addEventListener('click', function () { pickerRole = b.getAttribute('data-picker-role'); syncPickerRoles(); filterPicker(); }); }); pickerRoot.querySelector('[data-picker-overlay]').addEventListener('click', closePicker); pickerRoot.querySelector('[data-picker-close]').addEventListener('click', closePicker); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !pickerRoot.hidden) closePicker(); }); pickerHeroes.forEach(function (li) { li.querySelector('button').addEventListener('click', function () { if (!picker) return; var id = li.getAttribute('data-hero-id'); if (picker.mode === 'pick') { state[picker.side].picks[picker.slot] = id; } else { state[picker.side].bans[picker.slot] = id; } preview = null; closePicker(); update(); }); }); /* ---------- делегирование кликов по слотам/банам ---------- */ ['radiant', 'dire'].forEach(function (side) { var col = section.querySelector('[data-draft-side="' + side + '"]'); col.addEventListener('click', function (e) { var slotEl = e.target.closest && e.target.closest('[data-draft-slot]'); if (slotEl) { var slot = parseInt(slotEl.getAttribute('data-draft-slot'), 10); if (e.target.closest('[data-slot-remove]')) { state[side].picks[slot] = null; preview = null; update(); return; } if (e.target.closest('[data-slot-replace]') || e.target.closest('[data-slot-pick]') || (state[side].picks[slot] === null && e.target.closest('button'))) { openPicker(side, slot, 'pick'); return; } return; } var banEl = e.target.closest && e.target.closest('[data-draft-ban]'); if (banEl && e.target.closest('button')) { var index = parseInt(banEl.getAttribute('data-draft-ban'), 10); if (state[side].bans[index]) { state[side].bans[index] = null; preview = null; update(); } else { openPicker(side, index, 'ban'); } } }); }); var altsBox = section.querySelector('[data-rec-alts]'); altsBox.querySelectorAll('[data-alt-id]').forEach(function (btn) { btn.addEventListener('click', function () { var id = btn.getAttribute('data-alt-id'); preview = preview === id ? null : id; update(); }); }); section.querySelector('[data-draft-reset]').addEventListener('click', function () { state = JSON.parse(JSON.stringify(initial)); preview = null; update(); }); var advToggle = section.querySelector('[data-advanced-toggle]'); var advPanel = section.querySelector('[data-advanced-panel]'); if (advToggle && advPanel) { advToggle.addEventListener('click', function () { var open = advPanel.hidden; advPanel.hidden = !open; advToggle.setAttribute('aria-expanded', open ? 'true' : 'false'); var chev = advToggle.querySelector('svg'); if (chev) chev.classList.toggle('rotate-180', open); }); } })(); /* ---------------- FAQ accordion (порт faq-section.tsx) ---------------- */ (function initFaq() { var section = document.querySelector('[data-faq]'); if (!section) return; var items = section.querySelectorAll('[data-faq-item]'); function setOpen(item, open) { var btn = item.querySelector('[data-faq-toggle]'); var panel = item.querySelector('[data-faq-panel]'); var num = item.querySelector('[data-faq-num]'); var q = item.querySelector('[data-faq-q]'); var plus = item.querySelector('[data-faq-plus]'); btn.setAttribute('aria-expanded', open ? 'true' : 'false'); num.classList.toggle('text-accent-hot', open); num.classList.toggle('text-accent-hot/70', !open); q.classList.toggle('text-txt-1', open); q.classList.toggle('text-txt-1/85', !open); q.classList.toggle('group-hover:text-txt-1', !open); plus.classList.toggle('rotate-45', open); plus.classList.toggle('border-accent-hot/60', open); plus.classList.toggle('text-accent-hot', open); plus.classList.toggle('border-white/10', !open); plus.classList.toggle('text-txt-2', !open); plus.classList.toggle('group-hover:border-accent-hot/40', !open); plus.classList.toggle('group-hover:text-accent-hot', !open); if (reduceMotion) { panel.hidden = !open; return; } if (open) { panel.hidden = false; var h = panel.scrollHeight; panel.style.height = '0px'; panel.style.opacity = '0'; panel.style.transition = 'height 260ms ' + EASE + ', opacity 260ms ' + EASE; requestAnimationFrame(function () { panel.style.height = h + 'px'; panel.style.opacity = '1'; }); window.setTimeout(function () { panel.style.cssText = ''; }, 300); } else if (!panel.hidden) { var h0 = panel.scrollHeight; panel.style.height = h0 + 'px'; panel.style.transition = 'height 260ms ' + EASE + ', opacity 260ms ' + EASE; requestAnimationFrame(function () { panel.style.height = '0px'; panel.style.opacity = '0'; }); window.setTimeout(function () { panel.hidden = true; panel.style.cssText = ''; }, 270); } } items.forEach(function (item) { item.querySelector('[data-faq-toggle]').addEventListener('click', function () { var isOpen = item.querySelector('[data-faq-toggle]').getAttribute('aria-expanded') === 'true'; items.forEach(function (other) { if (other !== item && other.querySelector('[data-faq-toggle]').getAttribute('aria-expanded') === 'true') { setOpen(other, false); } }); setOpen(item, !isOpen); }); }); })(); /* ---------------- back to top (порт back-to-top.tsx) ---------------- */ (function initBackToTop() { var btn = document.querySelector('[data-back-to-top]'); if (!btn || btn.dataset.bttInit) return; // main.js уже инициализировал. btn.dataset.bttInit = '1'; var shown = false; var ticking = false; function apply(show) { if (show === shown) return; shown = show; btn.tabIndex = show ? 0 : -1; btn.setAttribute('aria-hidden', show ? 'false' : 'true'); btn.classList.toggle('pointer-events-auto', show); btn.classList.toggle('scale-100', show); btn.classList.toggle('opacity-75', show); btn.classList.toggle('pointer-events-none', !show); btn.classList.toggle('scale-[0.94]', !show); btn.classList.toggle('opacity-0', !show); } function onScroll() { if (ticking) return; ticking = true; requestAnimationFrame(function () { apply(window.scrollY > Math.max(900, window.innerHeight * 1.5)); ticking = false; }); } onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); btn.addEventListener('click', function () { window.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' }); }); })(); })();