/* ============================================================ * ИИ-наставник Landing Page — scripts.js * Pure vanilla JS, no dependencies * ============================================================ */ (function () { 'use strict'; /* ---------------------------------------------------------- * 1. Analytics Abstraction * ---------------------------------------------------------- */ var YM_COUNTER_ID = 106976327; function trackEvent(name, params) { if (YM_COUNTER_ID && typeof ym === 'function') { ym(YM_COUNTER_ID, 'reachGoal', name, params || {}); } // Debug logging in development if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { console.log('[track]', name, params || ''); } } /* ---------------------------------------------------------- * 2. Generic Tab Controller * Reused for hero audience tabs and use case tabs. * Supports arrow-key navigation and ARIA. * ---------------------------------------------------------- */ function initTabs(tablistSelector, panelSelector) { var tablist = document.querySelector(tablistSelector); if (!tablist) return; var tabs = Array.prototype.slice.call(tablist.querySelectorAll('[role="tab"]')); var panels = []; tabs.forEach(function (tab) { var panelId = tab.getAttribute('aria-controls'); var panel = document.getElementById(panelId); if (panel) panels.push(panel); }); function activateTab(tab) { tabs.forEach(function (t) { t.setAttribute('aria-selected', 'false'); t.classList.remove('active'); t.setAttribute('tabindex', '-1'); }); panels.forEach(function (p) { p.hidden = true; p.classList.remove('active'); }); tab.setAttribute('aria-selected', 'true'); tab.classList.add('active'); tab.removeAttribute('tabindex'); var panelId = tab.getAttribute('aria-controls'); var panel = document.getElementById(panelId); if (panel) { panel.hidden = false; panel.classList.add('active'); } trackEvent('tab_switch', { tablist: tablistSelector, tab: tab.id }); } tabs.forEach(function (tab) { tab.addEventListener('click', function () { activateTab(tab); }); }); // Arrow key navigation tablist.addEventListener('keydown', function (e) { var index = tabs.indexOf(document.activeElement); if (index === -1) return; var next; if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { next = tabs[(index + 1) % tabs.length]; } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { next = tabs[(index - 1 + tabs.length) % tabs.length]; } else if (e.key === 'Home') { next = tabs[0]; } else if (e.key === 'End') { next = tabs[tabs.length - 1]; } if (next) { e.preventDefault(); next.focus(); activateTab(next); } }); } /* ---------------------------------------------------------- * 3. FAQ Accordion * One item open at a time. Animated max-height. * ---------------------------------------------------------- */ function initAccordion() { var faqList = document.querySelector('.faq-list'); if (!faqList) return; var buttons = Array.prototype.slice.call(faqList.querySelectorAll('.faq-question')); buttons.forEach(function (btn) { var answerId = btn.getAttribute('aria-controls'); var answer = document.getElementById(answerId); if (!answer) return; // Set initial state answer.style.maxHeight = '0'; answer.style.overflow = 'hidden'; answer.style.transition = 'max-height 0.3s ease'; btn.addEventListener('click', function () { var isOpen = btn.getAttribute('aria-expanded') === 'true'; // Close all buttons.forEach(function (b) { b.setAttribute('aria-expanded', 'false'); var a = document.getElementById(b.getAttribute('aria-controls')); if (a) a.style.maxHeight = '0'; }); // Open clicked (if was closed) if (!isOpen) { btn.setAttribute('aria-expanded', 'true'); answer.style.maxHeight = answer.scrollHeight + 'px'; trackEvent('faq_open', { question: answerId }); } }); }); } /* ---------------------------------------------------------- * 4. Contact Form * Validates, logs to console, shows success message. * ---------------------------------------------------------- */ function initContactForm() { var form = document.getElementById('contact-form'); if (!form) return; var successEl = form.querySelector('.form-success'); var submitBtn = form.querySelector('[type="submit"]'); form.addEventListener('submit', function (e) { e.preventDefault(); // Basic validation var name = form.querySelector('#form-name'); var email = form.querySelector('#form-email'); var valid = true; // Clear previous errors Array.prototype.slice.call(form.querySelectorAll('.form-error')).forEach(function (el) { el.remove(); }); Array.prototype.slice.call(form.querySelectorAll('.form-input')).forEach(function (el) { el.classList.remove('input-error'); }); if (!name.value.trim()) { showFieldError(name, 'Введите имя'); valid = false; } if (!email.value.trim()) { showFieldError(email, 'Введите email'); valid = false; } else if (!isValidEmail(email.value.trim())) { showFieldError(email, 'Проверьте email'); valid = false; } if (!valid) return; // Collect data var data = { name: name.value.trim(), email: email.value.trim(), organization: form.querySelector('#form-org').value.trim(), message: form.querySelector('#form-message').value.trim() }; console.log('[form] Contact form submitted:', data); trackEvent('form_submit', data); // Show success if (submitBtn) submitBtn.hidden = true; if (successEl) successEl.hidden = false; form.reset(); }); } function showFieldError(input, message) { input.classList.add('input-error'); var error = document.createElement('p'); error.className = 'form-error'; error.textContent = message; input.parentNode.appendChild(error); } function isValidEmail(str) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str); } /* ---------------------------------------------------------- * 5. Demo Carousel * Arrow/dot navigation, touch swipe, keyboard support. * ---------------------------------------------------------- */ function initCarousel() { var carousel = document.querySelector('.demo-carousel'); if (!carousel) return; var track = carousel.querySelector('.carousel-track'); var slides = Array.prototype.slice.call(carousel.querySelectorAll('.carousel-slide')); var prevBtn = carousel.querySelector('.carousel-btn-prev'); var nextBtn = carousel.querySelector('.carousel-btn-next'); var dots = Array.prototype.slice.call(carousel.querySelectorAll('.carousel-dot')); var viewport = carousel.querySelector('.carousel-viewport'); var currentIndex = 0; var totalSlides = slides.length; function goToSlide(index) { if (index < 0 || index >= totalSlides) return; currentIndex = index; track.style.transform = 'translateX(-' + (currentIndex * 100) + '%)'; // Update dots dots.forEach(function (dot, i) { dot.classList.toggle('active', i === currentIndex); dot.setAttribute('aria-selected', i === currentIndex ? 'true' : 'false'); }); // Update buttons if (prevBtn) prevBtn.disabled = currentIndex === 0; if (nextBtn) nextBtn.disabled = currentIndex === totalSlides - 1; trackEvent('carousel_slide', { slide: currentIndex }); } if (prevBtn) { prevBtn.addEventListener('click', function () { goToSlide(currentIndex - 1); }); } if (nextBtn) { nextBtn.addEventListener('click', function () { goToSlide(currentIndex + 1); }); } dots.forEach(function (dot, i) { dot.addEventListener('click', function () { goToSlide(i); }); }); // Keyboard navigation carousel.addEventListener('keydown', function (e) { if (e.key === 'ArrowLeft') { goToSlide(currentIndex - 1); } else if (e.key === 'ArrowRight') { goToSlide(currentIndex + 1); } }); // Touch swipe support var touchStartX = 0; viewport.addEventListener('touchstart', function (e) { touchStartX = e.changedTouches[0].screenX; }, { passive: true }); viewport.addEventListener('touchend', function (e) { var touchEndX = e.changedTouches[0].screenX; var diff = touchStartX - touchEndX; if (Math.abs(diff) > 50) { if (diff > 0) { goToSlide(currentIndex + 1); } else { goToSlide(currentIndex - 1); } } }, { passive: true }); // Sync dots with scroll-snap on mobile var scrollTimeout; viewport.addEventListener('scroll', function () { clearTimeout(scrollTimeout); scrollTimeout = setTimeout(function () { var scrollLeft = viewport.scrollLeft; var slideWidth = viewport.offsetWidth; if (slideWidth > 0) { var newIndex = Math.round(scrollLeft / slideWidth); if (newIndex !== currentIndex && newIndex >= 0 && newIndex < totalSlides) { currentIndex = newIndex; dots.forEach(function (dot, i) { dot.classList.toggle('active', i === currentIndex); dot.setAttribute('aria-selected', i === currentIndex ? 'true' : 'false'); }); if (prevBtn) prevBtn.disabled = currentIndex === 0; if (nextBtn) nextBtn.disabled = currentIndex === totalSlides - 1; } } }, 100); }, { passive: true }); } /* ---------------------------------------------------------- * 6. Scroll Tracking * IntersectionObserver on sections + scroll depth milestones. * ---------------------------------------------------------- */ function initScrollTracking() { if (!('IntersectionObserver' in window)) return; var sections = document.querySelectorAll('section[id], header[id]'); var observed = {}; var observer = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting && !observed[entry.target.id]) { observed[entry.target.id] = true; trackEvent('section_view', { section: entry.target.id }); } }); }, { threshold: 0.3 }); sections.forEach(function (section) { observer.observe(section); }); // Scroll depth milestones var milestones = [25, 50, 75, 100]; var reached = {}; window.addEventListener('scroll', function () { var scrollTop = window.pageYOffset || document.documentElement.scrollTop; var docHeight = document.documentElement.scrollHeight - window.innerHeight; if (docHeight <= 0) return; var pct = Math.round((scrollTop / docHeight) * 100); milestones.forEach(function (m) { if (pct >= m && !reached[m]) { reached[m] = true; trackEvent('scroll_depth', { depth: m }); } }); }, { passive: true }); } /* ---------------------------------------------------------- * 7. CTA Click Tracking * Tracks clicks on buttons and contact links via data-track. * ---------------------------------------------------------- */ function initCTATracking() { document.addEventListener('click', function (e) { var el = e.target.closest('[data-track]'); if (!el) return; var eventName = el.getAttribute('data-track'); var params = {}; // Collect all data-* attributes as params Array.prototype.slice.call(el.attributes).forEach(function (attr) { if (attr.name.indexOf('data-') === 0 && attr.name !== 'data-track') { params[attr.name.replace('data-', '')] = attr.value; } }); trackEvent(eventName, params); }); } /* ---------------------------------------------------------- * 8. Smooth Scroll for Anchor Links * ---------------------------------------------------------- */ function initSmoothScroll() { document.addEventListener('click', function (e) { var link = e.target.closest('a[href^="#"]'); if (!link) return; var href = link.getAttribute('href'); if (href === '#') return; var target = document.querySelector(href); if (!target) return; e.preventDefault(); target.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Update URL without scrolling if (history.pushState) { history.pushState(null, '', href); } }); } /* ---------------------------------------------------------- * 9. Image Lightbox * Click to zoom any content image. * ---------------------------------------------------------- */ function initLightbox() { var lightbox = document.getElementById('lightbox'); var lightboxImg = document.getElementById('lightbox-img'); if (!lightbox || !lightboxImg) return; var selectors = [ '.hero-visual img', '.flow-diagram img', '.step-screenshot', '.use-case-screenshot', '.student-visual img', '.start-screenshot-item img' ]; var images = document.querySelectorAll(selectors.join(',')); Array.prototype.slice.call(images).forEach(function (img) { img.addEventListener('click', function () { lightboxImg.src = img.src; lightboxImg.alt = img.alt; lightbox.hidden = false; document.body.style.overflow = 'hidden'; trackEvent('lightbox_open', { image: img.src }); }); }); function closeLightbox() { lightbox.hidden = true; lightboxImg.src = ''; document.body.style.overflow = ''; } lightbox.addEventListener('click', closeLightbox); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !lightbox.hidden) { closeLightbox(); } }); } /* ---------------------------------------------------------- * 10. Init All * ---------------------------------------------------------- */ function init() { initTabs('.audience-tabs', '.audience-panel'); initTabs('.use-case-tabs', '.use-case-panel'); initAccordion(); initCarousel(); initLightbox(); initScrollTracking(); initCTATracking(); initSmoothScroll(); } // Run on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();