1(() => { 2 'use strict'; 3 4 const body = document.body; 5 const menuButton = document.querySelector('[data-menu-button]'); 6 const nav = document.querySelector('[data-nav]'); 7 const form = document.getElementById('requestForm'); 8 const status = document.querySelector('[data-form-status]'); 9 const cityChip = document.querySelector('[data-city-chip]'); 10 const cityInput = document.querySelector('[data-city-input]'); 11 12 if (menuButton && nav) { 13 menuButton.addEventListener('click', () => { 14 body.classList.toggle('menu-open'); 15 }); 16 17 nav.addEventListener('click', (event) => { 18 if (event.target.closest('a')) { 19 body.classList.remove('menu-open'); 20 } 21 }); 22 } 23 24 const revealItems = document.querySelectorAll('.reveal'); 25 if ('IntersectionObserver' in window) { 26 const observer = new IntersectionObserver((entries) => { 27 entries.forEach((entry) => { 28 if (entry.isIntersecting) { 29 entry.target.classList.add('is-visible'); 30 observer.unobserve(entry.target); 31 } 32 }); 33 }, { threshold: 0.12 }); 34 35 revealItems.forEach((item) => observer.observe(item)); 36 } else { 37 revealItems.forEach((item) => item.classList.add('is-visible')); 38 } 39 40 function setStatus(message, type = '') { 41 if (!status) return; 42 status.textContent = message; 43 status.className = 'form-status'; 44 if (type) status.classList.add(type); 45 } 46 47 function validateForm(formElement) { 48 const required = formElement.querySelectorAll('[required]'); 49 for (const field of required) { 50 if ((field.type === 'checkbox' && !field.checked) || (field.type !== 'checkbox' && !String(field.value).trim())) { 51 field.focus(); 52 return 'ÐаполниÑе обÑзаÑелÑнÑе полÑ.'; 53 } 54 } 55 56 const email = formElement.querySelector('input[name="email"]'); 57 if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim())) { 58 email.focus(); 59 return 'УкажиÑе коÑÑекÑнÑй email.'; 60 } 61 62 const phone = formElement.querySelector('input[name="phone"]'); 63 if (phone && phone.value.replace(/\D/g, '').length < 10) { 64 phone.focus(); 65 return 'УкажиÑе коÑÑекÑнÑй ÑелеÑон.'; 66 } 67 68 const fileInput = formElement.querySelector('input[type="file"]'); 69 if (fileInput && fileInput.files.length > 8) { 70 fileInput.focus(); 71 return 'Ðожно пÑиложиÑÑ Ð½Ðµ более 8 Ñайлов.'; 72 } 73 74 if (fileInput) { 75 for (const file of fileInput.files) { 76 if (file.size > 10 * 1024 * 1024) { 77 fileInput.focus(); 78 return `Файл «${file.name}» болÑÑе 10 ÐÐ.`; 79 } 80 } 81 } 82 83 return ''; 84 } 85 86 if (form) { 87 form.addEventListener('submit', async (event) => { 88 event.preventDefault(); 89 90 const validationError = validateForm(form); 91 if (validationError) { 92 setStatus(validationError, 'error'); 93 return; 94 } 95 96 const submitButton = form.querySelector('button[type="submit"]'); 97 const originalText = submitButton ? submitButton.textContent : ''; 98 99 try { 100 if (submitButton) { 101 submitButton.disabled = true; 102 submitButton.textContent = 'ÐÑпÑавлÑем...'; 103 } 104 105 setStatus('ÐÑпÑавлÑем заÑвкÑ...'); 106 107 const response = await fetch(form.action, { 108 method: 'POST', 109 body: new FormData(form), 110 headers: { 111 'X-Requested-With': 'XMLHttpRequest' 112 } 113 }); 114 115 const data = await response.json().catch(() => null); 116 117 if (!response.ok || !data || data.success !== true) { 118 throw new Error(data && data.message ? data.message : 'Ðе ÑдалоÑÑ Ð¾ÑпÑавиÑÑ Ð·Ð°ÑвкÑ. ÐопÑобÑйÑе позже.'); 119 } 120 121 form.reset(); 122 setStatus(data.message || 'ÐаÑвка оÑпÑавлена. ÐÑ ÑвÑжемÑÑ Ñ Ð²Ð°Ð¼Ð¸ поÑле анализа ÑиÑÑаÑии.', 'success'); 123 } catch (error) { 124 setStatus(error.message || 'ÐÑибка оÑпÑавки. ÐопÑобÑйÑе позже.', 'error'); 125 } finally { 126 if (submitButton) { 127 submitButton.disabled = false;
128 submitButton.textContent = originalText; 129 } 130 } 131 }); 132 } 133 134 function setCity(city) { 135 const safeCity = String(city || '').trim(); 136 if (!safeCity) return; 137 138 if (cityInput) cityInput.value = safeCity; 139 if (cityChip && safeCity !== 'РоÑÑиÑ') { 140 cityChip.textContent = `РабоÑаем диÑÑанÑионно: ${safeCity} и вÑÑ Ð Ð¤`; 141 } 142 } 143 144 function detectCity() { 145 const savedCity = localStorage.getItem('praveo_city'); 146 if (savedCity) { 147 setCity(savedCity); 148 return; 149 } 150 151 if (!('geolocation' in navigator)) return; 152 153 navigator.geolocation.getCurrentPosition( 154 async (position) => { 155 try { 156 const { latitude, longitude } = position.coords; 157 const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${encodeURIComponent(latitude)}&lon=${encodeURIComponent(longitude)}&accept-language=ru`); 158 const data = await response.json(); 159 const city = data.address?.city || data.address?.town || data.address?.village || data.address?.state || ''; 160 if (city) { 161 localStorage.setItem('praveo_city', city); 162 setCity(city); 163 } 164 } catch (_) { 165 /* ÐеомодÑÐ»Ñ Ð½Ðµ должен ломаÑÑ ÑÐ°Ð¹Ñ */ 166 } 167 }, 168 () => {},
169 { enableHighAccuracy: false, timeout: 2500, maximumAge: 86400000 } 170 ); 171 } 172 173 detectCity(); 174})(); 175 176 177(function () { 178 document.addEventListener('praveo:form-success', function () { 179 if (window.PRAVEO_METRIKA_ID && typeof window.ym === 'function') { 180 window.ym(window.PRAVEO_METRIKA_ID, 'reachGoal', 'PRAVEO_FORM_SUCCESS'); 181 } 182 }); 183})(); 184 185 186(function () { 187 const status = document.querySelector('[data-form-status]'); 188 if (!status) return; 189 190 let fired = false; 191 const observer = new MutationObserver(function () { 192 if (!fired && status.classList.contains('success')) { 193 fired = true; 194 document.dispatchEvent(new CustomEvent('praveo:form-success')); 195 } 196 }); 197 198 observer.observe(status, { attributes: true, childList: true, subtree: true }); 199})(); 200 201 202// Stage 38: scroll-to-top button 203(function () { 204 const button = document.querySelector('[data-scroll-top]'); 205 if (!button) return; 206 207 const toggleButton = function () { 208 if (window.scrollY > 520) { 209 button.classList.add('is-visible'); 210 } else { 211 button.classList.remove('is-visible'); 212 } 213 }; 214 215 button.addEventListener('click', function () { 216 window.scrollTo({ 217 top: 0, 218 behavior: 'smooth' 219 }); 220 }); 221 222 window.addEventListener('scroll', toggleButton, { passive: true }); 223 toggleButton(); 224})(); 225 226 227// Stage 49: anchor links smooth scroll 228(function () { 229 document.querySelectorAll('a[href^="#"]').forEach(function(anchor) { 230 anchor.addEventListener('click', function(e) { 231 const id = this.getAttribute('href'); 232 if (!id || id === '#') return; 233 234 let target = null; 235 236 try { 237 target = document.querySelector(id); 238 } catch (error) { 239 return; 240 } 241 242 if (!target) return; 243 244 e.preventDefault(); 245 246 const offset = 110; 247 const top = target.getBoundingClientRect().top + window.pageYOffset - offset; 248 249 window.scrollTo({ 250 top: top, 251 behavior: 'smooth' 252 }); 253 254 history.replaceState(null, '', id); 255 }); 256 }); 257})(); 258 259 260// Stage 62: global scroll-to-top 261(function () { 262 const buttons = document.querySelectorAll('[data-scroll-top]'); 263 if (!buttons.length) return; 264 265 const toggle = function () { 266 buttons.forEach(function (button) { 267 if (window.scrollY > 520) { 268 button.classList.add('is-visible'); 269 } else { 270 button.classList.remove('is-visible'); 271 } 272 }); 273 }; 274 275 buttons.forEach(function (button) { 276 button.addEventListener('click', function () { 277 window.scrollTo({ top: 0, behavior: 'smooth' }); 278 }); 279 }); 280 281 window.addEventListener('scroll', toggle, { passive: true }); 282 toggle(); 283})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.