PageSourceSearch

https://praveo.ru/assets/js/script.js?v=82-situations-page

js praveo.ru collected 2026-10-10 02:33:04 UTC 9,376 bytes, 283 lines download raw bytes

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.