228 lines
6.7 KiB
JavaScript
228 lines
6.7 KiB
JavaScript
(() => {
|
|
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 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();
|
|
}
|
|
});
|
|
})();
|