function getCurrentPageName() {
const normalizedPath = window.location.pathname.replace(/\/+$/, "");
const raw = (normalizedPath.split("/").pop() || "").toLowerCase();
const normalized = raw.replace(/\.html$/, "");
return normalized || "index";
}
const isLocalDevHost =
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1";
function routeHref(page) {
if (page === "index") {
return isLocalDevHost ? "index.html" : "/";
}
return isLocalDevHost ? `${page}.html` : `/${page}`;
}
function renderSharedLayout() {
const currentPage = getCurrentPageName();
const headerMount = document.querySelector("[data-site-header]");
const footerMount = document.querySelector("[data-site-footer]");
if (headerMount) {
const navItems = [
{ page: "prices", href: routeHref("prices"), label: "Цены" },
{ page: "portfolio", href: routeHref("portfolio"), label: "Портфолио" },
{ page: "reviews", href: routeHref("reviews"), label: "Отзывы" },
];
const navHtml = navItems
.map((item) => {
const isCurrent = currentPage === item.page;
return ``;
})
.join("");
headerMount.outerHTML = `
`;
}
if (footerMount) {
const isPrivacyPage = currentPage === "privacy";
const isRequisitesPage = currentPage === "requisites";
footerMount.outerHTML = `
Позвонить
`;
}
}
renderSharedLayout();
const currentYear = document.getElementById("year");
if (currentYear) {
currentYear.textContent = new Date().getFullYear();
}
const reduceMotionMql = window.matchMedia("(prefers-reduced-motion: reduce)");
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
function smoothScrollTo(targetY) {
const startY = window.scrollY || document.documentElement.scrollTop;
const change = targetY - startY;
if (reduceMotionMql.matches) {
window.scrollTo(0, targetY);
return;
}
if (Math.abs(change) < 2) {
return;
}
const duration = Math.min(980, Math.max(480, Math.abs(change) * 0.62));
const start = performance.now();
const step = (now) => {
const t = Math.min(1, (now - start) / duration);
window.scrollTo(0, startY + change * easeInOutCubic(t));
if (t < 1) {
requestAnimationFrame(step);
}
};
requestAnimationFrame(step);
}
document.addEventListener(
"click",
(e) => {
if (document.documentElement.classList.contains("modal-open")) {
return;
}
const a = e.target.closest("a");
if (!a) {
return;
}
const href = a.getAttribute("href");
if (!href || href === "#" || href.charAt(0) !== "#") {
return;
}
if (a.hasAttribute("download")) {
return;
}
const el = document.querySelector(href);
if (!el) {
return;
}
e.preventDefault();
const header = document.querySelector(".header");
const headerH = header ? header.getBoundingClientRect().height : 0;
const gap = 16;
const y = el.getBoundingClientRect().top + window.scrollY - headerH - gap;
smoothScrollTo(Math.max(0, y));
history.pushState(null, "", href);
},
true
);
const isHomePage = () => {
return getCurrentPageName() === "index";
};
document.querySelectorAll("a.logo").forEach((link) => {
const href = (link.getAttribute("href") || "").trim();
const goesHome =
href === "index.html" ||
href === "./index.html" ||
href === "/" ||
href === "/index" ||
href.endsWith("/index.html");
if (!goesHome) {
return;
}
link.addEventListener("click", (event) => {
if (!isHomePage()) {
return;
}
event.preventDefault();
if (window.scrollY > 0) {
smoothScrollTo(0);
}
if (window.location.hash) {
history.replaceState(null, "", window.location.pathname + window.location.search);
}
});
});
const form = document.getElementById("contactForm");
if (form) {
form.setAttribute("novalidate", "novalidate");
const nameInput = form.querySelector('input[name="name"]');
const phoneInput = form.querySelector('input[name="phone"]');
const honeypotInput = form.querySelector('input[name="contact_fax"]');
const startedAtInput = form.querySelector('input[name="started_at"]');
const submitButton = form.querySelector('button[type="submit"]');
if (startedAtInput) {
startedAtInput.value = String(Date.now());
}
if (phoneInput) {
const formatPhone = (rawValue) => {
const digits = rawValue.replace(/\D/g, "");
const localDigits = digits.startsWith("7") ? digits.slice(1) : digits;
const trimmed = localDigits.slice(0, 10);
if (!trimmed.length) {
return "+7";
}
let result = "+7";
if (trimmed.length > 0) {
result += ` (${trimmed.slice(0, 3)}`;
}
if (trimmed.length >= 3) {
result += ")";
}
if (trimmed.length > 3) {
result += ` ${trimmed.slice(3, 6)}`;
}
if (trimmed.length > 6) {
result += `-${trimmed.slice(6, 8)}`;
}
if (trimmed.length > 8) {
result += `-${trimmed.slice(8, 10)}`;
}
return result;
};
phoneInput.addEventListener("focus", () => {
if (!phoneInput.value.trim()) {
phoneInput.value = "+7";
} else {
phoneInput.value = formatPhone(phoneInput.value);
}
});
phoneInput.addEventListener("input", () => {
phoneInput.value = formatPhone(phoneInput.value);
phoneInput.setSelectionRange(phoneInput.value.length, phoneInput.value.length);
});
phoneInput.addEventListener("keydown", (event) => {
const selectionStart = phoneInput.selectionStart;
const selectionEnd = phoneInput.selectionEnd;
const hasSelection =
selectionStart !== null &&
selectionEnd !== null &&
selectionStart !== selectionEnd;
const fullSelection =
selectionStart === 0 && selectionEnd === phoneInput.value.length;
if (
fullSelection &&
(event.key === "Backspace" || event.key === "Delete")
) {
event.preventDefault();
phoneInput.value = "";
return;
}
if (
(event.key === "Backspace" || event.key === "Delete") &&
selectionStart !== null &&
selectionStart <= 2 &&
!hasSelection
) {
event.preventDefault();
}
});
phoneInput.addEventListener("blur", () => {
if (phoneInput.value === "+7" || phoneInput.value === "+7 (") {
phoneInput.value = "";
}
});
}
const consentInput = form.querySelector('input[name="consent"]');
const consentLabel = form.querySelector(".contact-form__consent");
const nameValidationRegex = /^[A-Za-zА-Яа-яЁё\s-]{2,80}$/;
const showFormToast = (message, variant = "warning") => {
let toast = document.getElementById("formToast");
if (!toast) {
toast = document.createElement("div");
toast.id = "formToast";
toast.className = "form-toast";
document.body.appendChild(toast);
}
toast.textContent = message;
toast.classList.remove("form-toast--warning", "form-toast--success");
toast.classList.add(`form-toast--${variant}`);
toast.setAttribute("role", variant === "success" ? "status" : "alert");
toast.setAttribute("aria-live", "polite");
toast.classList.add("is-visible");
clearTimeout(toast._hideTimer);
const hideMs = variant === "success" ? 6000 : 5000;
toast._hideTimer = window.setTimeout(() => {
toast.classList.remove("is-visible");
}, hideMs);
toast.onclick = () => {
clearTimeout(toast._hideTimer);
toast.classList.remove("is-visible");
};
};
form.addEventListener("submit", (event) => {
event.preventDefault();
if (nameInput) {
const normalizedName = nameInput.value.trim().replace(/\s+/g, " ");
nameInput.value = normalizedName;
if (!normalizedName) {
showFormToast("Пожалуйста, укажите ваше имя", "warning");
nameInput.focus();
return;
}
if (!nameValidationRegex.test(normalizedName)) {
showFormToast(
"Имя должно содержать только буквы, пробелы и дефис (2-80 символов)",
"warning"
);
nameInput.focus();
return;
}
}
if (phoneInput) {
const phoneDigits = phoneInput.value.replace(/\D/g, "");
const isPhoneValid = phoneDigits.length === 11 && phoneDigits.startsWith("7");
if (!isPhoneValid) {
showFormToast("Пожалуйста, укажите корректный номер телефона", "warning");
phoneInput.focus();
return;
}
}
if (consentInput && !consentInput.checked) {
consentLabel?.classList.add("contact-form__consent--error");
consentLabel?.scrollIntoView({ behavior: "smooth", block: "center" });
showFormToast(
"Чтобы отправить заявку, отметьте согласие на обработку персональных данных",
"warning"
);
consentInput.focus();
return;
}
consentLabel?.classList.remove("contact-form__consent--error");
if (startedAtInput && !startedAtInput.value) {
startedAtInput.value = String(Date.now());
}
const formData = new FormData(form);
// Явно чистим honeypot: защищает от автозаполнения браузером.
if (honeypotInput) {
honeypotInput.value = "";
}
formData.delete("website");
formData.set("contact_fax", "");
if (!formData.get("consent")) {
formData.set("consent", "1");
}
if (submitButton) {
submitButton.disabled = true;
submitButton.setAttribute("aria-busy", "true");
}
fetch("submit.php", {
method: "POST",
body: formData,
headers: {
Accept: "application/json",
},
})
.then(async (response) => {
let payload = null;
try {
payload = await response.json();
} catch (error) {
payload = null;
}
if (!response.ok || !payload?.ok) {
const errorMessage =
payload?.message ||
"Не удалось отправить заявку. Позвоните нам, пожалуйста, по телефону +7 (921) 741-26-46";
throw new Error(errorMessage);
}
showFormToast("Спасибо! Заявка принята — скоро мы с вами свяжемся", "success");
form.reset();
if (startedAtInput) {
startedAtInput.value = String(Date.now());
}
})
.catch((error) => {
showFormToast(error.message, "warning");
})
.finally(() => {
if (submitButton) {
submitButton.disabled = false;
submitButton.removeAttribute("aria-busy");
}
});
});
if (consentInput) {
consentInput.addEventListener("change", () => {
if (consentInput.checked) {
consentLabel?.classList.remove("contact-form__consent--error");
}
});
}
}
const revealItems = document.querySelectorAll(".reveal");
const revealObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
entry.target.classList.add("is-visible");
observer.unobserve(entry.target);
});
},
{
threshold: 0.15,
}
);
revealItems.forEach((item) => revealObserver.observe(item));
const priceAccordion = document.getElementById("priceAccordion");
const fallbackPriceData = [
{
title: "Электромонтажные работы",
items: [
{ name: "Установка розетки", price: "от 650 ₽/шт" },
{ name: "Установка выключателя", price: "от 550 ₽/шт" },
{ name: "Монтаж подрозетника", price: "от 450 ₽/шт" },
{ name: "Монтаж точки освещения", price: "от 1 200 ₽/шт" },
{ name: "Монтаж потолочного светильника", price: "от 900 ₽/шт" },
],
},
{
title: "Плиточные работы",
items: [
{ name: "Укладка плитки 20x30", price: "от 350 ₽/шт" },
{ name: "Укладка керамогранита 60x60", price: "от 900 ₽/шт" },
{ name: "Запил плитки под 45 градусов", price: "от 500 ₽/пог.м" },
{ name: "Затирка эпоксидная", price: "от 300 ₽/м²" },
],
},
{
title: "Сантехнические работы",
items: [
{ name: "Установка смесителя", price: "от 1 500 ₽/шт" },
{ name: "Монтаж унитаза", price: "от 2 500 ₽/шт" },
{ name: "Установка раковины с тумбой", price: "от 3 000 ₽/шт" },
{ name: "Подключение полотенцесушителя", price: "от 2 800 ₽/шт" },
],
},
];
function escapeHtml(value) {
const text = String(value ?? "");
return text
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function normalizePriceData(data) {
if (!Array.isArray(data)) {
return JSON.parse(JSON.stringify(fallbackPriceData));
}
return data.map((category) => ({
title: typeof category?.title === "string" ? category.title : "",
items: Array.isArray(category?.items)
? category.items.map((item) => ({
name: typeof item?.name === "string" ? item.name : "",
price: typeof item?.price === "string" ? item.price : "",
amount: typeof item?.amount === "string" ? item.amount : "",
unit: typeof item?.unit === "string" ? item.unit : "",
total: typeof item?.total === "string" ? item.total : "",
}))
: [],
}));
}
async function loadPriceData() {
const externalData =
typeof window.PRICES_DATA !== "undefined" ? window.PRICES_DATA : null;
if (externalData) {
return normalizePriceData(externalData);
}
return JSON.parse(JSON.stringify(fallbackPriceData));
}
function buildPriceList(items) {
return items
.map(
(item) => {
const rawPrice = String(item.price || "").trim();
const desktopPrice = rawPrice
.replace(/\s*\/\s*[^\s]+$/u, "")
.trim();
const mobilePrice = rawPrice
.replace(/^от\s+/iu, "")
.replace(/\s*\/\s*[^\s]+$/u, "")
.trim();
const derivedUnitMatch = rawPrice.match(/\/\s*([^\s]+)$/);
const derivedUnit = derivedUnitMatch ? derivedUnitMatch[1] : "";
const amountValue = item.amount || "1";
const unitValue = item.unit || derivedUnit || "";
const normalizedName = String(item.name || "").trim().replace(/\s+/g, " ");
const fullName = escapeHtml(normalizedName);
return `
${fullName}
${escapeHtml(desktopPrice || rawPrice)}
${escapeHtml(mobilePrice || rawPrice)}
${escapeHtml(amountValue)}
${escapeHtml(unitValue || "—")}
`;
}
)
.join("");
}
function renderPriceAccordion(priceData) {
if (!priceAccordion) {
return;
}
priceAccordion.innerHTML = priceData
.map(
(category) => `
-
Виды работ
Цена
Кол-во
Ед.изм
${buildPriceList(Array.isArray(category.items) ? category.items : [])}
`
)
.join("");
}
if (priceAccordion) {
loadPriceData().then(renderPriceAccordion);
priceAccordion.addEventListener("click", (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
if (target.classList.contains("price-category__toggle")) {
const category = target.closest(".price-category");
const listWrap = category ? category.querySelector(".price-list-wrap") : null;
if (!listWrap) {
return;
}
const expanded = target.getAttribute("aria-expanded") === "true";
target.setAttribute("aria-expanded", String(!expanded));
listWrap.hidden = expanded;
}
});
}
const LOGO_KEY = "remont-spb-logo";
const DEFAULT_LOGO = "logo-interior-1.png";
const logoOptions = [
{ id: "logo-option-1.png", label: "1" },
{ id: "logo-option-2.png", label: "2" },
{ id: "logo-option-3.png", label: "3" },
{ id: "logo-option-4.png", label: "4" },
{ id: "logo-option-6.png", label: "5" },
{ id: "logo-right-2.png", label: "6" },
{ id: "logo-right-4.png", label: "7" },
{ id: "logo-right-5.png", label: "8" },
{ id: "logo-simple-1.png", label: "9" },
{ id: "logo-simple-3.png", label: "10" },
{ id: "logo-interior-1.png", label: "11" },
{ id: "logo-interior-6.png", label: "12" },
{ id: "logo-interior-8.png", label: "13" },
{ id: "logo-interior-9.png", label: "14" },
{ id: "logo-flat-2.png", label: "15" },
{ id: "logo-flat-3.png", label: "16" },
{ id: "logo-flat-5.png", label: "17" },
];
const BG_KEY = "remont-spb-bg";
const DEFAULT_BG = "bg-4.png";
const bgOptions = [
{ id: "none", label: "Без фона" },
{ id: "bg-1.png", label: "Фон 1" },
{ id: "bg-2.png", label: "Фон 2" },
{ id: "bg-3.png", label: "Фон 3" },
{ id: "bg-4.png", label: "Фон 4" },
{ id: "bg-5.png", label: "Фон 5" },
];
function applyLogo(logoId) {
localStorage.setItem(LOGO_KEY, logoId);
const logoPath = `image/${logoId}`;
const logoImgs = document.querySelectorAll(".logo img");
logoImgs.forEach((img) => {
img.src = logoPath;
});
}
function applyBg(bgId) {
localStorage.setItem(BG_KEY, bgId);
const heroBg = document.getElementById("heroBg");
const heroSection = document.getElementById("hero");
if (heroBg && heroSection) {
if (bgId === "none") {
heroBg.style.display = "none";
heroBg.src = "";
} else {
heroBg.style.display = "block";
heroBg.src = `image/${bgId}`;
}
}
}
function applyBrandingDefaults() {
// Применяем зафиксированные значения брендинга без UI-переключателя.
applyLogo(DEFAULT_LOGO);
applyBg(DEFAULT_BG);
}
applyBrandingDefaults();
/* Portfolio Gallery Logic */
const workCards = document.querySelectorAll('.work-card[data-images]');
const portfolioModal = document.getElementById('portfolioModal');
const portfolioTrack = document.getElementById('portfolioTrack');
if (portfolioModal && portfolioTrack) {
const closeButtons = portfolioModal.querySelectorAll('[data-close]');
const openModal = (images) => {
portfolioTrack.innerHTML = images
.map(
(src) =>
``
)
.join('');
portfolioModal.classList.add('is-open');
document.documentElement.classList.add('modal-open');
document.body.classList.add('modal-open');
};
const closeModal = () => {
portfolioModal.classList.remove('is-open');
document.documentElement.classList.remove('modal-open');
document.body.classList.remove('modal-open');
};
workCards.forEach(card => {
card.addEventListener('click', () => {
try {
const images = JSON.parse(card.dataset.images);
if (images && images.length) {
openModal(images);
}
} catch (e) {
console.error('Ошибка при чтении картинок галереи', e);
}
});
});
closeButtons.forEach(btn => btn.addEventListener('click', closeModal));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && portfolioModal.classList.contains('is-open')) {
closeModal();
}
});
// Логика перетаскивания (Drag-to-scroll)
let isDown = false;
let startX;
let scrollLeft;
portfolioTrack.addEventListener('mousedown', (e) => {
isDown = true;
portfolioTrack.classList.add('is-dragging');
startX = e.pageX - portfolioTrack.offsetLeft;
scrollLeft = portfolioTrack.scrollLeft;
});
portfolioTrack.addEventListener('mouseleave', () => {
isDown = false;
portfolioTrack.classList.remove('is-dragging');
});
portfolioTrack.addEventListener('mouseup', () => {
isDown = false;
portfolioTrack.classList.remove('is-dragging');
});
portfolioTrack.addEventListener('mousemove', (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - portfolioTrack.offsetLeft;
const walk = (x - startX) * 2; // Скорость прокрутки при перетаскивании
portfolioTrack.scrollLeft = scrollLeft - walk;
});
// Прокрутка колесиком мыши (Wheel scroll)
portfolioTrack.addEventListener('wheel', (e) => {
if (e.deltaY !== 0) {
e.preventDefault();
portfolioTrack.scrollLeft += e.deltaY;
}
});
// Блокируем прокрутку страницы, пока открыта модалка
portfolioModal.addEventListener(
'wheel',
(e) => {
if (!portfolioModal.classList.contains('is-open')) {
return;
}
e.preventDefault();
},
{ passive: false }
);
}