482 lines
17 KiB
JavaScript
482 lines
17 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function () {
|
|
// ===== THEME SWITCHER =====
|
|
var themeBtn = document.getElementById('theme-btn');
|
|
var themeIcon = document.getElementById('theme-icon');
|
|
var themeTooltip = document.getElementById('theme-tooltip');
|
|
var html = document.documentElement;
|
|
|
|
var themes = ['system', 'dark', 'light'];
|
|
var icons = { system: '💻', dark: '🌙', light: '☀️' };
|
|
var labels = {
|
|
system: 'Системная тема',
|
|
dark: 'Тёмная тема',
|
|
light: 'Светлая тема'
|
|
};
|
|
|
|
function getSystemTheme() {
|
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
}
|
|
|
|
function applyTheme(mode) {
|
|
var actual = mode === 'system' ? getSystemTheme() : mode;
|
|
html.setAttribute('data-theme', actual);
|
|
if (themeIcon) themeIcon.textContent = icons[mode];
|
|
if (themeTooltip) themeTooltip.textContent = labels[mode] + ' — нажмите для смены';
|
|
}
|
|
|
|
function getStoredTheme() {
|
|
return localStorage.getItem('theme') || 'system';
|
|
}
|
|
|
|
function cycleTheme() {
|
|
var current = getStoredTheme();
|
|
var idx = themes.indexOf(current);
|
|
var next = themes[(idx + 1) % themes.length];
|
|
localStorage.setItem('theme', next);
|
|
applyTheme(next);
|
|
}
|
|
|
|
// Init theme
|
|
applyTheme(getStoredTheme());
|
|
|
|
// Listen for system theme changes
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
|
|
if (getStoredTheme() === 'system') {
|
|
applyTheme('system');
|
|
}
|
|
});
|
|
|
|
if (themeBtn) {
|
|
themeBtn.addEventListener('click', cycleTheme);
|
|
}
|
|
|
|
// ===== YANDEX MAP THEME =====
|
|
var mapFrame = document.getElementById('yandex-map');
|
|
if (mapFrame) {
|
|
var mapLon = mapFrame.getAttribute('data-lon');
|
|
var mapLat = mapFrame.getAttribute('data-lat');
|
|
var mapAddress = mapFrame.getAttribute('data-address') || '';
|
|
|
|
function updateMapTheme() {
|
|
var theme = html.getAttribute('data-theme') || 'light';
|
|
var params = 'll=' + mapLon + ',' + mapLat +
|
|
'&z=16' +
|
|
'&pt=' + mapLon + ',' + mapLat + ',pm2rdl' +
|
|
'&text=' + encodeURIComponent(mapAddress) +
|
|
'&theme=' + theme;
|
|
mapFrame.src = 'https://yandex.ru/map-widget/v1/?' + params;
|
|
}
|
|
|
|
updateMapTheme();
|
|
|
|
var mapObserver = new MutationObserver(function (mutations) {
|
|
for (var i = 0; i < mutations.length; i++) {
|
|
if (mutations[i].attributeName === 'data-theme') {
|
|
updateMapTheme();
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
mapObserver.observe(html, { attributes: true });
|
|
}
|
|
|
|
// ===== BURGER MENU =====
|
|
var burger = document.getElementById('burger');
|
|
var nav = document.querySelector('.header__nav');
|
|
if (burger && nav) {
|
|
burger.addEventListener('click', function () {
|
|
nav.classList.toggle('active');
|
|
});
|
|
}
|
|
|
|
// ===== SMOOTH SCROLL =====
|
|
document.querySelectorAll('a[href^="#"]').forEach(function (link) {
|
|
link.addEventListener('click', function (e) {
|
|
var id = this.getAttribute('href');
|
|
if (id === '#') return;
|
|
var target = document.querySelector(id);
|
|
if (target) {
|
|
e.preventDefault();
|
|
var headerH = document.querySelector('.header');
|
|
var offset = headerH ? headerH.offsetHeight : 0;
|
|
var top = target.getBoundingClientRect().top + window.pageYOffset - offset;
|
|
window.scrollTo({ top: top, behavior: 'smooth' });
|
|
if (nav) nav.classList.remove('active');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ===== PHONE MASK =====
|
|
var phoneInput = document.getElementById('phone');
|
|
if (phoneInput) {
|
|
phoneInput.addEventListener('focus', function () {
|
|
if (this.value === '') {
|
|
this.value = '8 (___) ___-__-__';
|
|
}
|
|
});
|
|
phoneInput.addEventListener('input', function () {
|
|
var val = this.value.replace(/\D/g, '');
|
|
var formatted = '8 (';
|
|
if (val.length > 1) formatted += val.substring(1, 4);
|
|
if (val.length >= 4) formatted += ') ';
|
|
if (val.length > 4) formatted += val.substring(4, 7);
|
|
if (val.length >= 7) formatted += '-';
|
|
if (val.length > 7) formatted += val.substring(7, 9);
|
|
if (val.length >= 9) formatted += '-';
|
|
if (val.length > 9) formatted += val.substring(9, 11);
|
|
this.value = formatted.trim();
|
|
});
|
|
phoneInput.addEventListener('blur', function () {
|
|
if (this.value === '8 (___) ___-__-__' || this.value.replace(/\D/g, '').length < 11) {
|
|
this.value = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
// ===== CHARACTER COUNTER =====
|
|
var messageField = document.getElementById('message');
|
|
var charCount = document.querySelector('.char-count');
|
|
if (messageField && charCount) {
|
|
messageField.addEventListener('input', function () {
|
|
var remaining = 500 - this.value.length;
|
|
charCount.textContent = 'Осталось: ' + remaining;
|
|
});
|
|
}
|
|
|
|
// ===== CAROUSEL =====
|
|
var carouselEl = document.querySelector('.carousel');
|
|
var track = document.querySelector('.carousel__track');
|
|
var slides = track ? Array.from(track.querySelectorAll('.case')) : [];
|
|
var prevBtn = document.querySelector('.carousel__prev');
|
|
var nextBtn = document.querySelector('.carousel__next');
|
|
var counterEl = document.getElementById('carousel-counter');
|
|
|
|
if (carouselEl && track && slides.length > 0 && prevBtn && nextBtn) {
|
|
var realCount = slides.length;
|
|
var isTransitioning = false;
|
|
|
|
// Clone first and last slide for infinite loop
|
|
if (realCount > 1) {
|
|
var firstClone = slides[0].cloneNode(true);
|
|
var lastClone = slides[realCount - 1].cloneNode(true);
|
|
firstClone.classList.add('clone');
|
|
lastClone.classList.add('clone');
|
|
track.insertBefore(lastClone, track.firstChild);
|
|
track.appendChild(firstClone);
|
|
}
|
|
|
|
// Re-query all slides including clones
|
|
slides = Array.from(track.querySelectorAll('.case'));
|
|
var totalSlides = slides.length;
|
|
|
|
// Start at real slide #2 (index 2 because clone of last is at index 0)
|
|
var currentIndex = realCount > 1 ? 2 : 1;
|
|
|
|
var autoTimer = null;
|
|
|
|
function getSlideWidth() {
|
|
return Math.round(carouselEl.clientWidth * 0.72);
|
|
}
|
|
|
|
function setupSlides() {
|
|
var w = getSlideWidth();
|
|
for (var i = 0; i < slides.length; i++) {
|
|
slides[i].style.width = w + 'px';
|
|
}
|
|
}
|
|
|
|
function updateActiveClass() {
|
|
var realIdx = getRealIndex(currentIndex);
|
|
for (var i = 0; i < slides.length; i++) {
|
|
if (slides[i].classList.contains('clone')) {
|
|
slides[i].classList.remove('case--active');
|
|
} else {
|
|
var thisRealIdx = i - 1;
|
|
slides[i].classList.toggle('case--active', thisRealIdx === realIdx);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getRealIndex(idx) {
|
|
if (idx <= 0) return realCount - 1;
|
|
if (idx >= realCount + 1) return 0;
|
|
return idx - 1;
|
|
}
|
|
|
|
function goToSlide(index, animate) {
|
|
var slideW = getSlideWidth();
|
|
var carouselW = carouselEl.clientWidth;
|
|
var offset = (slideW * index) - (carouselW / 2) + (slideW / 2);
|
|
if (offset < 0) offset = 0;
|
|
|
|
if (animate === false) {
|
|
track.style.transition = 'none';
|
|
} else {
|
|
track.style.transition = 'transform 0.5s cubic-bezier(0.4, 0, 0.2, 1)';
|
|
}
|
|
|
|
track.style.transform = 'translateX(-' + offset + 'px)';
|
|
updateActiveClass();
|
|
|
|
if (counterEl) {
|
|
counterEl.textContent = (getRealIndex(currentIndex) + 1) + ' / ' + realCount;
|
|
}
|
|
}
|
|
|
|
function handleTransitionEnd() {
|
|
isTransitioning = false;
|
|
// If we landed on a clone, jump to the real slide without animation
|
|
if (currentIndex <= 0) {
|
|
currentIndex = realCount;
|
|
goToSlide(currentIndex, false);
|
|
} else if (currentIndex >= realCount + 1) {
|
|
currentIndex = 1;
|
|
goToSlide(currentIndex, false);
|
|
}
|
|
}
|
|
|
|
track.addEventListener('transitionend', handleTransitionEnd);
|
|
|
|
function startAuto() {
|
|
stopAuto();
|
|
if (realCount > 1) {
|
|
autoTimer = setInterval(function () {
|
|
currentIndex++;
|
|
goToSlide(currentIndex, true);
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
function stopAuto() {
|
|
if (autoTimer) {
|
|
clearInterval(autoTimer);
|
|
autoTimer = null;
|
|
}
|
|
}
|
|
|
|
prevBtn.addEventListener('click', function () {
|
|
if (isTransitioning) return;
|
|
isTransitioning = true;
|
|
stopAuto();
|
|
currentIndex--;
|
|
goToSlide(currentIndex, true);
|
|
startAuto();
|
|
});
|
|
|
|
nextBtn.addEventListener('click', function () {
|
|
if (isTransitioning) return;
|
|
isTransitioning = true;
|
|
stopAuto();
|
|
currentIndex++;
|
|
goToSlide(currentIndex, true);
|
|
startAuto();
|
|
});
|
|
|
|
var resizeTimer;
|
|
window.addEventListener('resize', function () {
|
|
clearTimeout(resizeTimer);
|
|
resizeTimer = setTimeout(function () {
|
|
setupSlides();
|
|
goToSlide(currentIndex, false);
|
|
}, 150);
|
|
});
|
|
|
|
setupSlides();
|
|
goToSlide(currentIndex, false);
|
|
startAuto();
|
|
}
|
|
|
|
// ===== COOKIE BANNER =====
|
|
var cookieBanner = document.getElementById('cookie-banner');
|
|
var cookieAccept = document.getElementById('cookie-accept');
|
|
if (cookieBanner && cookieAccept) {
|
|
if (!localStorage.getItem('cookies_accepted')) {
|
|
cookieBanner.classList.add('active');
|
|
}
|
|
cookieAccept.addEventListener('click', function () {
|
|
localStorage.setItem('cookies_accepted', '1');
|
|
cookieBanner.classList.remove('active');
|
|
});
|
|
}
|
|
|
|
// ===== STICKY PANEL =====
|
|
var stickyPanel = document.getElementById('sticky-panel');
|
|
var heroSection = document.getElementById('hero');
|
|
var footerEl = document.querySelector('.footer');
|
|
if (stickyPanel && heroSection) {
|
|
var footerVisible = false;
|
|
|
|
if (footerEl) {
|
|
var footerObserver = new IntersectionObserver(function (entries) {
|
|
footerVisible = entries[0].isIntersecting;
|
|
if (footerVisible) {
|
|
stickyPanel.classList.remove('active');
|
|
}
|
|
}, { threshold: 0.1 });
|
|
footerObserver.observe(footerEl);
|
|
}
|
|
|
|
var ticking = false;
|
|
|
|
function updateSticky() {
|
|
var heroBottom = heroSection.getBoundingClientRect().bottom;
|
|
if (!footerVisible) {
|
|
stickyPanel.classList.toggle('active', heroBottom < 0);
|
|
}
|
|
ticking = false;
|
|
}
|
|
|
|
window.addEventListener('scroll', function () {
|
|
if (!ticking) {
|
|
window.requestAnimationFrame(updateSticky);
|
|
ticking = true;
|
|
}
|
|
}, { passive: true });
|
|
|
|
updateSticky();
|
|
}
|
|
|
|
// ===== FAQ ACCORDION =====
|
|
document.querySelectorAll('.faq-question').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var item = this.closest('.faq-item');
|
|
if (!item) return;
|
|
var isActive = item.classList.contains('active');
|
|
document.querySelectorAll('.faq-item').forEach(function (el) {
|
|
el.classList.remove('active');
|
|
});
|
|
if (!isActive) {
|
|
item.classList.add('active');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ===== SCROLL REVEAL ANIMATIONS =====
|
|
var revealElements = document.querySelectorAll(
|
|
'.problem-card, .service-card, .industry-card, .step, .case, .faq-item, .testimonial-card, .info-card, .about-card'
|
|
);
|
|
if (revealElements.length > 0) {
|
|
var revealObserver = new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (entry) {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('visible');
|
|
revealObserver.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.1,
|
|
rootMargin: '0px 0px -40px 0px'
|
|
});
|
|
|
|
revealElements.forEach(function (el, i) {
|
|
el.style.transitionDelay = (i % 3) * 0.08 + 's';
|
|
revealObserver.observe(el);
|
|
});
|
|
}
|
|
|
|
// ===== FORM SUBMIT =====
|
|
var form = document.getElementById('contact-form');
|
|
var responseDiv = document.getElementById('form-response');
|
|
|
|
if (form && responseDiv) {
|
|
// Убираем ошибку при фокусе на поле
|
|
form.querySelectorAll('input, textarea').forEach(function (input) {
|
|
input.addEventListener('focus', function () {
|
|
var group = this.closest('.form-group');
|
|
if (group) {
|
|
group.classList.remove('form-group--error');
|
|
var err = group.querySelector('.error-msg');
|
|
if (err) {
|
|
err.classList.remove('visible');
|
|
err.textContent = '';
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
form.addEventListener('submit', async function (e) {
|
|
e.preventDefault();
|
|
|
|
// Сброс всех ошибок
|
|
document.querySelectorAll('.form-group--error').forEach(function (el) {
|
|
el.classList.remove('form-group--error');
|
|
});
|
|
document.querySelectorAll('.error-msg.visible').forEach(function (el) {
|
|
el.classList.remove('visible');
|
|
el.textContent = '';
|
|
});
|
|
|
|
// Клиентская валидация
|
|
var hasError = false;
|
|
|
|
var nameInput = document.getElementById('name');
|
|
if (nameInput && !nameInput.value.trim()) {
|
|
showFieldError(nameInput, 'Введите ваше имя');
|
|
hasError = true;
|
|
}
|
|
|
|
var phoneInput = document.getElementById('phone');
|
|
if (phoneInput) {
|
|
var digits = phoneInput.value.replace(/\D/g, '');
|
|
if (digits.length < 11) {
|
|
showFieldError(phoneInput, 'Введите корректный номер телефона');
|
|
hasError = true;
|
|
}
|
|
}
|
|
|
|
var agreeInput = document.getElementById('agree');
|
|
if (agreeInput && !agreeInput.checked) {
|
|
var agreeGroup = agreeInput.closest('.form-group');
|
|
if (agreeGroup) {
|
|
agreeGroup.classList.add('form-group--error');
|
|
var errSpan = agreeGroup.querySelector('.error-msg');
|
|
if (errSpan) {
|
|
errSpan.textContent = 'Необходимо согласиться с политикой обработки персональных данных';
|
|
errSpan.classList.add('visible');
|
|
}
|
|
}
|
|
hasError = true;
|
|
}
|
|
|
|
if (hasError) return;
|
|
|
|
var formData = new FormData(form);
|
|
formData.append('ajax', '1');
|
|
|
|
try {
|
|
var res = await fetch(window.location.href, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
var data = await res.json();
|
|
|
|
if (data.success) {
|
|
form.style.display = 'none';
|
|
responseDiv.innerHTML =
|
|
'<div class="success-msg">' +
|
|
'<div class="check-icon">' +
|
|
'<svg viewBox="0 0 24 24"><polyline points="4 12 10 18 20 6"></polyline></svg>' +
|
|
'</div>' +
|
|
'<p>' + data.message + '</p>' +
|
|
'</div>';
|
|
} else {
|
|
responseDiv.innerHTML = '<p style="color:var(--error); font-weight:600; text-align:center;">' + data.message + '</p>';
|
|
}
|
|
} catch (err) {
|
|
responseDiv.innerHTML = '<p style="color:var(--error); font-weight:600; text-align:center;">Произошла ошибка. Попробуйте позже.</p>';
|
|
}
|
|
});
|
|
}
|
|
|
|
function showFieldError(input, message) {
|
|
var group = input.closest('.form-group');
|
|
if (!group) return;
|
|
group.classList.add('form-group--error');
|
|
var errSpan = group.querySelector('.error-msg');
|
|
if (errSpan) {
|
|
errSpan.textContent = message;
|
|
errSpan.classList.add('visible');
|
|
}
|
|
}
|
|
});
|