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 `${item.label}`; }) .join(""); headerMount.outerHTML = `
+7 (921) 741-26-46 Ежедневно 10:00-18:00 Санкт-Петербург
VK
`; } 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 normalizePriceItems(items) { if (!Array.isArray(items)) { return []; } return 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 : "", })); } function normalizePriceData(data) { if (!Array.isArray(data)) { return JSON.parse(JSON.stringify(fallbackPriceData)); } return data.map((category) => { const children = Array.isArray(category?.children) ? category.children.map((child) => ({ title: typeof child?.title === "string" ? child.title : "", items: normalizePriceItems(child?.items), })) : []; return { title: typeof category?.title === "string" ? category.title : "", items: normalizePriceItems(category?.items), children, }; }); } 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 buildPriceTable(items) { return ` `; } function slugifyCategory(title) { const map = { а: "a", б: "b", в: "v", г: "g", д: "d", е: "e", ё: "e", ж: "zh", з: "z", и: "i", й: "y", к: "k", л: "l", м: "m", н: "n", о: "o", п: "p", р: "r", с: "s", т: "t", у: "u", ф: "f", х: "h", ц: "ts", ч: "ch", ш: "sh", щ: "sch", ъ: "", ы: "y", ь: "", э: "e", ю: "yu", я: "ya", }; return String(title || "") .toLowerCase() .split("") .map((ch) => map[ch] ?? ch) .join("") .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } function openPriceCategoryFromHash() { if (!priceAccordion) { return; } const hash = decodeURIComponent(location.hash.replace(/^#/, "")).trim(); if (!hash) { return; } const category = priceAccordion.querySelector( `.price-category#${CSS.escape(hash)}` ); if (!(category instanceof HTMLElement)) { return; } const toggle = category.querySelector(".price-category__toggle"); const listWrap = toggle?.nextElementSibling; if ( toggle instanceof HTMLElement && listWrap instanceof HTMLElement && listWrap.classList.contains("price-list-wrap") ) { toggle.setAttribute("aria-expanded", "true"); listWrap.hidden = false; } requestAnimationFrame(() => { const header = document.querySelector(".header"); const offset = header instanceof HTMLElement ? header.getBoundingClientRect().height + 12 : 12; const top = category.getBoundingClientRect().top + window.scrollY - offset; window.scrollTo({ top: Math.max(0, top), behavior: "smooth" }); }); } function renderPriceAccordion(priceData) { if (!priceAccordion) { return; } priceAccordion.innerHTML = priceData .map((category) => { const hasChildren = Array.isArray(category.children) && category.children.length > 0; const categoryId = slugifyCategory(category.title || "kategoriya"); if (hasChildren) { const childrenHtml = category.children .map( (child) => `
    ` ) .join(""); return `
    `; } return `
    `; }) .join(""); openPriceCategoryFromHash(); } if (priceAccordion) { loadPriceData().then(renderPriceAccordion); window.addEventListener("hashchange", openPriceCategoryFromHash); priceAccordion.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof HTMLElement)) { return; } const toggle = target.closest( ".price-category__toggle, .price-subcategory__toggle" ); if (!toggle || !(toggle instanceof HTMLElement)) { return; } const listWrap = toggle.nextElementSibling; if (!(listWrap instanceof HTMLElement) || !listWrap.classList.contains("price-list-wrap")) { return; } const expanded = toggle.getAttribute("aria-expanded") === "true"; toggle.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 } ); }