(() => { const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const selectors = [ ".section__head", ".info-card", ".feature-card", ".object-card", ".service-card", ".gallery__item", ".review-card", ".stats article", ".booking__form", ".contacts-card", ".hours-card", ]; const elements = Array.from(document.querySelectorAll(selectors.join(","))); if (!elements.length) return; const siblingCounters = new WeakMap(); elements.forEach((element) => { element.classList.add("scroll-reveal"); const parent = element.parentElement; let offsetIndex = 0; if (parent) { offsetIndex = siblingCounters.get(parent) ?? 0; siblingCounters.set(parent, offsetIndex + 1); } element.style.setProperty("--reveal-delay", `${(offsetIndex % 4) * 80}ms`); }); if (reduceMotion || !("IntersectionObserver" in window)) { elements.forEach((element) => element.classList.add("is-visible")); return; } const observer = new IntersectionObserver( (entries, currentObserver) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; entry.target.classList.add("is-visible"); currentObserver.unobserve(entry.target); }); }, { threshold: 0.18, rootMargin: "0px 0px -12% 0px" } ); elements.forEach((element) => observer.observe(element)); })(); (() => { const counters = Array.from(document.querySelectorAll(".js-counter")); if (!counters.length) return; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const formatCounter = (counter, value) => { const suffix = counter.dataset.suffix || ""; counter.textContent = `${value}${suffix}`; }; const setFinalValue = (counter) => { const target = Number(counter.dataset.target || 0); formatCounter(counter, Number.isFinite(target) ? target : 0); }; const animateCounter = (counter) => { const target = Number(counter.dataset.target || 0); const duration = Number(counter.dataset.duration || 1300); if (!Number.isFinite(target) || target <= 0) { setFinalValue(counter); return; } let startTime = null; const tick = (timestamp) => { if (startTime === null) startTime = timestamp; const progress = Math.min((timestamp - startTime) / duration, 1); const currentValue = Math.floor(progress * target); formatCounter(counter, currentValue); if (progress < 1) { window.requestAnimationFrame(tick); return; } setFinalValue(counter); }; window.requestAnimationFrame(tick); }; if (reduceMotion || !("IntersectionObserver" in window)) { counters.forEach(setFinalValue); return; } const observer = new IntersectionObserver( (entries, currentObserver) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; animateCounter(entry.target); currentObserver.unobserve(entry.target); }); }, { threshold: 0.45 } ); counters.forEach((counter) => { formatCounter(counter, 0); observer.observe(counter); }); })(); (() => { const slider = document.querySelector("[data-reviews-slider]"); if (!slider) return; const viewport = slider.querySelector("[data-reviews-viewport]"); const track = slider.querySelector("[data-reviews-track]"); const prevButton = slider.querySelector("[data-reviews-prev]"); const nextButton = slider.querySelector("[data-reviews-next]"); const dotsContainer = document.querySelector("[data-reviews-dots]"); const cards = Array.from(track?.querySelectorAll(".review-card") ?? []); if (!viewport || !track || !prevButton || !nextButton || !dotsContainer || cards.length < 2) return; let currentIndex = 0; let touchStartX = 0; const readVisibleSlides = () => { const visibleValue = Number.parseInt(getComputedStyle(slider).getPropertyValue("--reviews-visible"), 10); return Number.isFinite(visibleValue) && visibleValue > 0 ? visibleValue : 1; }; const readTrackGap = () => { const styles = getComputedStyle(track); const gapValue = styles.gap || styles.columnGap || "0"; const parsedGap = Number.parseFloat(gapValue); return Number.isFinite(parsedGap) ? parsedGap : 0; }; const maxIndex = () => Math.max(0, cards.length - readVisibleSlides()); const renderDots = () => { const total = maxIndex() + 1; dotsContainer.innerHTML = ""; for (let index = 0; index < total; index += 1) { const dot = document.createElement("button"); dot.type = "button"; dot.className = "reviews__dot"; dot.setAttribute("aria-label", `Показать отзыв ${index + 1}`); dot.addEventListener("click", () => { currentIndex = index; update(); }); dotsContainer.appendChild(dot); } }; const update = () => { const max = maxIndex(); currentIndex = Math.min(Math.max(currentIndex, 0), max); const cardWidth = cards[0].getBoundingClientRect().width; const offset = (cardWidth + readTrackGap()) * currentIndex; track.style.transform = `translateX(${-offset}px)`; prevButton.disabled = currentIndex === 0; nextButton.disabled = currentIndex === max; const dots = Array.from(dotsContainer.querySelectorAll(".reviews__dot")); dots.forEach((dot, index) => { dot.classList.toggle("is-active", index === currentIndex); dot.setAttribute("aria-current", index === currentIndex ? "true" : "false"); }); }; prevButton.addEventListener("click", () => { currentIndex -= 1; update(); }); nextButton.addEventListener("click", () => { currentIndex += 1; update(); }); viewport.addEventListener("touchstart", (event) => { touchStartX = event.changedTouches[0].clientX; }, { passive: true }); viewport.addEventListener("touchend", (event) => { const deltaX = event.changedTouches[0].clientX - touchStartX; const threshold = 48; if (deltaX > threshold) { currentIndex -= 1; update(); return; } if (deltaX < -threshold) { currentIndex += 1; update(); } }, { passive: true }); const handleResize = () => { renderDots(); update(); }; let resizeRaf = null; window.addEventListener("resize", () => { if (resizeRaf) return; resizeRaf = window.requestAnimationFrame(() => { resizeRaf = null; handleResize(); }); }); renderDots(); update(); })(); (() => { const bookingForm = document.getElementById("booking-form"); const successModal = document.getElementById("booking-success-modal"); if (!bookingForm || !successModal) return; const closeTriggers = Array.from(successModal.querySelectorAll("[data-modal-close]")); const openModal = () => { successModal.classList.add("is-open"); successModal.setAttribute("aria-hidden", "false"); }; const closeModal = () => { successModal.classList.remove("is-open"); successModal.setAttribute("aria-hidden", "true"); }; const triggerAnalytics = () => { const eventName = "заявка"; const metrikaIdRaw = bookingForm.dataset.metrikaId || document.body.dataset.metrikaId || window.YANDEX_METRIKA_ID; const metrikaId = Number(metrikaIdRaw); if (typeof window.ym === "function" && Number.isFinite(metrikaId) && metrikaId > 0) { window.ym(metrikaId, "reachGoal", eventName); } const legacyCounterKey = Object.keys(window).find((key) => { return key.startsWith("yaCounter") && typeof window[key]?.reachGoal === "function"; }); if (legacyCounterKey) { window[legacyCounterKey].reachGoal(eventName); } if (typeof window.gtag === "function") { window.gtag("event", eventName, { event_category: "lead", event_label: "booking_form" }); } if (Array.isArray(window.dataLayer)) { window.dataLayer.push({ event: eventName, event_category: "lead", event_label: "booking_form" }); } }; const triggerAutoReply = async (email) => { if (!email) return; const autoReplyEndpoint = bookingForm.dataset.autoreplyEndpoint || window.BOOKING_AUTOREPLY_ENDPOINT; if (!autoReplyEndpoint) return; try { await fetch(autoReplyEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, source: "booking-form" }) }); } catch (error) { // Auto-reply is optional and should not block form success. console.error("Auto-reply request failed", error); } }; bookingForm.addEventListener("submit", async (event) => { event.preventDefault(); if (!bookingForm.checkValidity()) { bookingForm.reportValidity(); return; } const formData = new FormData(bookingForm); const email = String(formData.get("email") || "").trim(); triggerAnalytics(); await triggerAutoReply(email); bookingForm.reset(); openModal(); }); closeTriggers.forEach((trigger) => { trigger.addEventListener("click", closeModal); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape" && successModal.classList.contains("is-open")) { closeModal(); } }); })();