Initial commit
This commit is contained in:
@@ -0,0 +1,481 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user