PageSourceSearch

https://samedia.ru/Modules/Storage/Storage/Cache/08e2a05c13d21004a…?ver=e5ea0cd8-6bb1-4dfa-b1a5-61b32ab2a4bf

js samedia.ru collected 2026-10-07 01:51:26 UTC 141,482 bytes, 2,708 lines download raw bytes

1function JSApp() { 
2
3// Javascript 
4document.addEventListener('DOMContentLoaded', function() {
5        if (window.location.hash === '#box_portfolioMosaic') {
6            window.location.replace('/projects'); // replace не сохраняет историю
7        }
8    });
9
10
11// Javascript 
12// Добавляем SVG круг для каждой кнопки закрытия
13    (function() {
14        const addBorderRing = () => {
15            const closeBtns = document.querySelectorAll('.close-btn');
16            closeBtns.forEach(btn => {
17                if (!btn.querySelector('.border-ring')) {
18                    const ring = document.createElement('div');
19                    ring.className = 'border-ring';
20                    ring.innerHTML = `
21                        <svg viewBox="0 0 42 42" xmlns="http://www.w3.org/2000/svg">
22                            <circle cx="21" cy="21" r="19.5" />
23                        </svg>
24                    `;
25                    btn.appendChild(ring);
26                }
27            });
28        };
29
30        // Ждем появления кнопки в DOM
31        const observer = new MutationObserver(() => {
32            addBorderRing();
33        });
34
35        observer.observe(document.body, { childList: true, subtree: true });
36
37        // Вызываем сразу, если кнопка уже есть
38        if (document.readyState === 'loading') {
39            document.addEventListener('DOMContentLoaded', addBorderRing);
40        } else {
41            addBorderRing();
42        }
43    })();
44
45
46// Javascript 
47(function() {
48        const mountElement = document.getElementById('CallbackFormWidget');
49        if (mountElement && !mountElement.__vue_app__) {
50
51            const template = `
52                <div v-if="isMounted"
53                     class="fixed inset-0 bg-black/40 backdrop-blur-md flex items-start justify-center z-[9999] pt-20 transition-opacity duration-300"
54                     :class="isModalOpen ? 'opacity-100' : 'opacity-0'">
55
56                    <div class="relative w-full max-w-md mx-4"><div class="transition-all duration-300 ease-out relative"
57                             :class="isModalOpen
58                                 ? 'translate-y-0 opacity-100'
59                                 : '-translate-y-full opacity-0'"><div class="absolute z-20" style="right: -48px; top: -6px;">
60                                <button @click="closeModal"
61                                        class="close-btn w-10 h-10 flex items-center justify-center rounded-full bg-slate-800 text-white shadow-lg transition-all duration-300 hover:scale-110 focus:outline-none">
62                                    <svg class="w-5 h-5 transition-all duration-300" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
63                                        <path d="M18 6L6 18M6 6L18 18" stroke-linecap="round"/>
64                                    </svg>
65                                </button>
66                            </div><div class="bg-white rounded-3xl shadow-2xl">
67                                <div class="p-8 md:p-10">
68
69                                    <div class="flex justify-center mb-6">
70                                        <div class="relative">
71                                            <div class="absolute inset-0 rounded-full bg-amber-100 animate-ping opacity-75"></div>
72                                            <div class="relative w-20 h-20 rounded-full bg-gradient-to-br from-amber-500 to-amber-600 shadow-lg flex items-center justify-center animate-bounce-subtle">
73                                                <svg class="w-10 h-10 text-white animate-spin-slow" viewB
73ox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
74                                                    <circle cx="12" cy="12" r="10" stroke="currentColor"/>
75                                                    <path d="M12 6V12L15 15" stroke="currentColor" stroke-linecap="round"/>
76                                                </svg>
77                                            </div>
78                                        </div>
79                                    </div>
80
81                                    <h2 class="text-2xl md:text-3xl font-bold text-center text-slate-800 mb-3">
82                                        Оставить заявку
83                                    </h2>
84                                    <p class="text-sm text-center text-slate-500 mb-6 leading-relaxed">
85                                        Оставьте номер телефона, и мы свяжемся с вами
86                                    </p>
87
88                                    <div v-if="showSuccess" class="text-center py-4">
89                                        <div class="w-16 h-16 mx-auto mb-3 rounded-full bg-emerald-100 flex items-center justify-center">
90                                            <svg class="w-8 h-8 text-emerald-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
91                                                <path d="M20 6L9 17L4 12" stroke-linecap="round" stroke-linejoin="round"/>
92                                            </svg>
93                                        </div>
94                                        <h3 class="text-xl font-bold text-slate-800 mb-2">Спасибо!</h3>
95                                        <p class="text-sm text-slate-500 mb-6">Мы свяжемся с вами в ближайшее время</p>
96                                        <button @click="closeModal" class="px-6 py-2.5 bg-amber-600 hover:bg-amber-700 text-white rounded-full text-sm font-medium transition-all duration-300 hover:shadow-lg transform hover:-translate-y-0.5">
97                                            Закрыть
98                                        </button>
99                                    </div>
100
101                                    <form v-else @submit.prevent="submitForm" class="space-y-4">
102
103                                        <div>
104                                            <input
105                                                type="tel"
106                                                id="phone-input"
107                                                autocomplete="off"
108                                                data-lpignore="true"
109                                                data-1p-ignore="true"
110                                                placeholder="+7 (___) ___-__-__"
111                                                class="w-full px-4 py-3 bg-white border rounded-xl text-slate-800 placeholder:text-slate-400 focus:border-amber-500 focus:ring-2 focus:ring-amber-500/20 transition-all duration-200 outline-none"
112                                                :class="errors.phone ? 'border-red-500' : 'border-slate-200'"
113                                            >
114                                            <p v-if="errors.phone" class="text-xs text-red-500 mt-1 ml-2">{{ errors.phone }}</p>
115                                        </div>
116
117                                        <div>
118                                            <input
119                                                type="text"
120                                                v-model="form.name"
121                                                autocomplete="off"
122                                                placeholder="Ваше имя (необязательно)"
123                                                class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl text-slate-800 placeholder:text-slate-400 focus:border-amber-500 focus:ring-2 focus:ring-amber-500/20 transition-all duration-200 outline-none"
124                                            >
125                                        </div>
126
127                                        <div>
128                                            <textarea
129                                                v-model="form.message"
130                                                rows="2"
131                                                placeholder="Сообщение (необязательно)"
132                                                class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl text-slate-800 placeholder:text-slate-400 focus:border-amber-500 focus:ring-2 focus:ring-amber-500/20 transition-all duration-200 outline-none resize-none"
133                                            ></textarea>
134                                        </div><div>
135                                            <label class="flex items-start gap-3 cursor-pointer group">
136                                                <input type="checkbox" v-model="form.agreePersonalData" @change="validateForm" class="hidden">
137                                                <div class="flex-shrink-0 w-5 h-5 mt-0.5 border-2 rounded-md flex items-center justify-center transition-all duration-200"
138                                                    :class="form.agreePersonalData
139                                                        ? 'bg-amber-500 border-amber-500'
140                                                        : 'border-slate-300 group-hover:border-amber-400'"
141                                                >
142                                                    <svg v-if="form.agreePersonalData" class="w-3.5 h-3.5 text-white" viewBox="0 0 12 10" fill="none" stroke="currentColor" stroke-width="2">
143                                                        <path d="M1 5L4 8L11 1" stroke-linecap="round" stroke-linejoin="round"/>
144                                                    </svg>
145                                                </div>
146                                                <span class="text-xs text-slate-600 leading-relaxed">
147                                                    Я даю согласие на обработку персональных данных в соответствии с
148                                                    <a href="/docs/personal-data-processing-policy" target="_blank" class="text-amber-600 hover:text-amber-700 hover:underline">Политикой обработки персональных данных</a>
149                                                </span>
150                                            </label>
151                                            <p v-if="errors.agreePersonalData" class="text-xs text-red-500 ml-8">{{ errors.agreePersonalData }}</p>
152                                        </div>
153
154                                        <button
155                                            type="submit"
156                                            :disabled="!isFormValid || isSubmitting"
157                                            class="btn w-full justify-center transition-all duration-300 transform hover:-translate-y-0.5 active:translate-y-0 mt-2"
158                                            :class="(!isFormValid || isSubmitting) ? 'opacity-50 cursor-not-allowed hover:translate-y-0' : ''"
159                                        >
160                                            <div v-if="!isSubmitting">
161                                                <span>О</span><span>т</span><span>п</span><span>р</span><span>а</span><span>в</span><span>и</span><span>т</span><span>ь</span>
162                                            </div>
163                                            <div v-else class="flex items-center gap-2">
164                                                <svg class="w-5 h-5 animate-spin" viewBox="0 0 24 24" fill="none">
165                                                    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
166                                                    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
167                                                </svg>
168                                                <span>Отправка...</span>
169                                            </div>
170                                            <svg viewBox="0 0 16 16" width="16" height="16">
171                                                <path fill-rule="evenodd" d="M5.736 3.032h6.566a.73.73 0 0 1 .73.73v6.565a.73.73 0 1 1-1.46 0V5.523l-7.509 7.509L3.032 12l7.509-7.51H5.736a.73.73 0 1 1 0-1.458Z"></path>
172                                            </svg>
173                                        </button>
174                                    </form>
175                                </div>
176                            </div>
177                        </div>
178                    </div>
179                </div>
180            `;
181
182            const { createApp, ref, computed, onMounted, onBeforeUnmount, nextTick } = Vue;
183
184            const app = createApp({
185                template: template,
186                setup() {
187                    const isMounted = ref(false);
188                    const isModalOpen = ref(false);
189                    const isSubmitting = ref(false);
190                    const showSuccess = ref(false);
191
192                    const form = ref({
193                        name: '',
194                        phone: '',
195                        message: '',
196                        agreePersonalData: false
197                    });
198
199                    const errors = ref({
200                        phone: '',
201                        agreePersonalData: ''
202                    });
203
204                    let maskInstance = null;
205
206                    const validateForm = () => {
207                        const phoneDigits = (form.value.phone || '').replace(/\D/g, '');
208
209                        if (!form.value.phone || phoneDigits.length < 10) {
210                            errors.value.phone = 'Введите корректный номер телефона';
211                            return false;
212                        } else {
213                            errors.value.phone = '';
214                        }
215
216                        if (!form.value.agreePersonalData) {
217                            errors.value.agreePersonalData = 'Подтвердите согласие на обработку персональных данных';
218                            return false;
219                        } else {
220                            errors.value.agreePersonalData = '';
221                        }
222
223                        return true;
224                    };
225
226                    const isFormValid = computed(() => {
227                        const phoneDigits = (form.value.phone || '').replace(/\D/g, '');
228                        return phoneDigits.length >= 10 && form.value.agreePersonalData;
229                    });
230
231                    const initMaskAndFocus = () => {
232                        const phoneInput = document.querySelector('#CallbackFormWidget #phone-input');
233                        if (phoneInput && typeof IMask !== 'undefined' && !maskInstance) {
234                            try {
235                                maskInstance = IMask(phoneInput, {
236                                    mask: '+7 (000) 000-00-00',
237                                    lazy: true,
238                                    placeholderChar: '_'
239                                });
240
241                                maskInstance.on('accept', () => {
242                                    form.value.phone = maskInstance.unmaskedValue;
243                                });
244
245                                setTimeout(() => {
246                                    phoneInput.focus();
247                                }, 50);
248                            } catch(e) {
249                                console.warn('IMask init error:', e);
250                            }
251                        }
252                    };
253
254                    const openModal = () => {
255                        isMounted.value = true;
256                        nextTick(() => {
257                            setTimeout(() => {
258                                isModalOpen.value = true;
259                            }, 10);
260                            setTimeout(() => {
261                                initMaskAndFocus();
262                            }, 200);
263                        });
264                    };
265
266                    const closeModal = () => {
267                        isModalOpen.value = false;
268
269                        if (window.location.hash === '#callBack') {
270                            history.pushState('', document.title, window.location.pathname + window.location.search);
271                        }
272
273                        setTimeout(() => {
274                            isMounted.value = false;
275                            if (!showSuccess.value) {
276                                resetForm();
277                            }
278                        }, 300);
279                    };
280
281                    const resetForm = () => {
282                        form.value = {
283                            name: '',
284                            phone: '',
285                            message: '',
286                            agreePersonalData: false
287                        };
288                        errors.value = {
289                            phone: '',
290                            agreePersonalData: ''
291                        };
292                        showSuccess.value = false;
293                        isSubmitting.value = false;
294
295                        if (maskInstance) {
296                            maskInstance.destroy();
297                            maskInstance = null;
298                        }
299                    };
300
301                    const submitForm = async () => {
302                        if (!isFormValid.value || isSubmitting.value) return;
303
304                        if (!validateForm()) return;
305
306                        isSubmitting.value = true;
307
308                        const cleanPhone = (form.value.phone || '').replace(/\D/g, '');
309                        const submitData = {
310                            name: form.value.name.trim() || '',
311                            phone: cleanPhone,
312                            message: form.value.message.trim() || '',
313                            agreePersonalData: form.value.agreePersonalData
314                        };
315
316                        try {
317                            const response = await api.action('form:sendCallback', submitData);
318
319                            if (response.status === 'ok') {
320                                showSuccess.value = true;
321
322                                setTimeout(() => {
323                                    if (showSuccess.value) {
324                                        closeModal();
325                                        resetForm();
326                                    }
327                                }, 3000);
328                            } else {
329                                if (response.message) {
330                                    alert(response.message);
331                                }
332                                isSubmitting.value = false;
333                            }
334                        } catch (error) {
335                            console.error('Ошибка отправки:', error);
336                            alert('Произошла ошибка при отправке. Пожалуйста, попробуйте позже.');
337                            isSubmitting.value = false;
338                        }
339                    };
340
341                    const handleHashChange = () => {
342                        if (window.location.hash === '#callBack') {
343                            openModal();
344                        } else if (window.location.hash !== '#callBack' && isMounted.value) {
345                            closeModal();
346                        }
347                    };
348
349                    const handleEscape = (e) => {
350                        if (e.key === 'Escape' && isMounted.value) {
351                            closeModal();
352                        }
353                    };
354
355                    onMounted(() => {
356                        if (window.location.hash === '#callBack') {
357                            openModal();
358                        }
359
360                        window.addEventListener('hashchange', handleHashChange);
361                        window.addEventListener('keydown', handleEscape);
362                    });
363
364                    onBeforeUnmount(() => {
365                        window.removeEventListener('hashchange', handleHashChange);
366                        window.removeEventListener('keydown', handleEscape);
367
368                        if (maskInstance) {
369                            maskInstance.destroy();
370                            maskInstance = null;
371                        }
372                    });
373
374                    return {
375                        isMounted,
376                        isModalOpen,
377                        isSubmitting,
378                        showSuccess,
379                        form,
380                        errors,
381                        isFormValid,
382                        closeModal,
383                        submitForm,
384                        validateForm
385                    };
386                }
387            });
388
389            app.mount(mountElement);
390            mountElement.__vue_app__ = app;
391        }
392    })();
393
394
395// Javascript 
396(function() {
397        const { createApp, ref, onMounted } = Vue;
398
399        const mountElement = document.getElementById('CookieWidget');
400        if (mountElement && !mountElement.__vue_app__) {
401            const cookieApp = createApp({
402                setup() {
403                    const showBanner = ref(false);
404                    const cookiePolicyLink = ref('/docs/cookies-processing-policy');
405
406                    const checkCookieConsent = () => {
407                        const consent = localStorage.getItem('cookie_consent');
408                        if (consent !== 'accepted') {
409                            showBanner.value = true;
410                        }
411                    };
412
413                    const acceptCookies = () => {
414                        localStorage.setItem('cookie_consent', 'accepted');
415                        showBanner.value = false;
416
417                        if (window.showNotification) {
418                            window.showNotification('Спасибо! Настройки cookies сохранены', 'success');
419                        }
420                    };
421
422                    onMounted(() => {
423                        checkCookieConsent();
424                    });
425
426                    return {
427                        showBanner,
428                        cookiePolicyLink,
429                        acceptCookies
430                    };
431                }
432            });
433
434            cookieApp.mount(mountElement);
435            mountElement.__vue_app__ = cookieApp;
436        }
437    })();
438
439
440// Javascript 
441(function() {
442        'use strict';
443
444        // Ждем загрузки DOM
445        if (document.readyState === 'loading') {
446            document.addEventListener('DOMContentLoaded', initScrollToTop);
447        } else {
448            initScrollToTop();
449        }
450
451        function initScrollToTop() {
452            // Получаем кнопку
453            const scrollToTopBtn = document.getElementById('scrollToTopBtn');
454
455            if (!scrollToTopBtn) return;
456
457            // Флаг для предотвращения множественных обработчиков
458            let isScrolling = false;
459            let animationFrameId = null;
460
461            // Функция прокрутки наверх (ИСПРАВЛЕННАЯ)
462            const scrollToTop = () => {
463                // Если уже скроллим, выходим
464                if (isScrolling) return;
465
466                // Отменяем предыдущую анимацию, если есть
467                if (animationFrameId) {
468                    cancelAnimationFrame(animationFrameId);
469                    animationFrameId = null;
470                }
471
472                isScrolling = true;
473
474                const scrollStep = () => {
475                    const currentScroll = window.pageYOffset || document.documentElement.scrollTop;
476
477                    // Используем порог 1px чтобы избежать бесконечного цикла
478                    if (currentScroll > 1) {
479                        // Минимальный шаг прокрутки - 1px
480                        const step = Math.max(1, Math.floor(currentScroll / 10));
481                        const newScroll = Math.max(0, currentScroll - step);
482
483                        window.scrollTo(0, newScroll);
484
485                        // Продолжаем анимацию
486                        animationFrameId = requestAnimationFrame(scrollStep);
487                    } else {
488                        // Достигли верха - принудительно ставим 0 и сбрасываем флаги
489                        window.scrollTo(0, 0);
490                        isScrolling = false;
491                        animationFrameId = null;
492                    }
493                };
494
495                // Запускаем анимацию
496                animationFrameId = requestAnimationFrame(scrollStep);
497            };
498
499            // Функция показа/скрытия кнопки
500            const toggleScrollButton = () => {
501                const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
502                if (scrollPosition > 300) {
503                    scrollToTopBtn.classList.add('visible');
504                } else {
505                    scrollToTopBtn.classList.remove('visible');
506                }
507            };
508
509            // Добавляем обработчик клика
510            scrollToTopBtn.addEventListener('click', scrollToTop);
511
512            // Добавляем обработчик скролла с throttle для производительности
513            let ticking = false;
514            window.addEventListener('scroll', () => {
515                if (!ticking) {
516                    requestAnimationFrame(() => {
517                        toggleScrollButton();
518                        ticking = false;
519                    });
520                    ticking = true;
521                }
522            });
523
524            // Вызываем при загрузке, чтобы проверить начальное положение
525            toggleScrollButton();
526        }
527    })();
528
529
530// Javascript 
531// Добавляем Vue-приложение только для управления адаптивностью (опционально)
532    (function () {
533        const mountElement = document.getElementById('FooterWidget');
534        if (mountElement && !mountElement.__vue_app__ && typeof Vue !== 'undefined') {
535            const {createApp, ref, onMounted, onBeforeUnmount} = Vue;
536
537            const footerApp = createApp({
538                setup() {
539                    // Отслеживание ширины экрана для адаптивных классов
540                    const isMobile = ref(window.innerWidth < 768);
541
542                    const handleResize = () => {
543                        isMobile.value = window.innerWidth < 768;
544                    };
545
546                    onMounted(() => {
547                        window.addEventListener('resize', handleResize);
548                    });
549
550                    onBeforeUnmount(() => {
551                        window.removeEventListener('resize', handleResize);
552                    });
553
554                    return {
555                        isMobile
556                    };
557                }
558            });
559
560            footerApp.mount(mountElement);
561            mountElement.__vue_app__ = footerApp;
562        }
563    })();
564
565
566// Javascript 
567(function () {
568        const {createApp, ref, computed, watch, nextTick, onMounted} = Vue;
569
570        const mountElement = document.getElementById('WizardFormWidget');
571        if (mountElement && !mountElement.__vue_app__) {
572
573            // Компонент: Выбор задач
574            const CallBackQuizTaskType = {
575                props: ['errors', 'selectedTypes'],
576                emits: ['update:types'],
577                setup(props, {emit}) {
578                    const tasksList = [
579                        {value: 'web-design', label: 'Веб-дизайн', icon: 'bi-brush'},
580                        {value: 'app-design', label: 'Дизайн приложений', icon: 'bi-phone'},
581                        {value: 'website-dev', label: 'Разработка сайта', icon: 'bi-code
581-square'},
582                        {value: 'service-dev', label: 'Разработка сервиса', icon: 'bi-cloud'},
583                        {value: 'mobile-dev', label: 'Мобильное приложение', icon: 'bi-phone'},
584                        {value: 'support', label: 'Техподдержка', icon: 'bi-headset'},
585                        {value: 'seo', label: 'SEO-продвижение', icon: 'bi-graph-up'},
586                        {value: 'context', label: 'Контекстная реклама', icon: 'bi-megaphone'},
587                        {value: 'automation', label: 'Автоматизация', icon: 'bi-gear-wide-connected'},
588                        {value: 'ai', label: 'AI внедрение', icon: 'bi-cpu'},
589                        {value: 'consultation', label: 'Консультация', icon: 'bi-chat-dots'}
590                    ];
591
592                    const localSelected = ref(props.selectedTypes || []);
593
594                    const toggleTask = (value) => {
595                        const index = localSelected.value.indexOf(value);
596                        if (index > -1) {
597                            localSelected.value.splice(index, 1);
598                        } else {
599                            localSelected.value.push(value);
600                        }
601                        emit('update:types', localSelected.value);
602                    };
603
604                    return {tasksList, localSelected, toggleTask};
605                },
606                template: `
607                <div class="CallBackQuizTaskType">
608                    <div class="CallBackQuizTaskType_container">
609                        <div class="CallBackQuizTaskType_list">
610                            <label v-for="task in tasksList" :key="task.value"
611                                   class="CallBackQuizTaskTypeItem"
612                                   :class="{ active: localSelected.includes(task.value) }">
613                                <input
614                                    type="checkbox"
615                                    :value="task.value"
616                                    :checked="localSelected.includes(task.value)"
617                                    @change="toggleTask(task.value)"
618                                    class="CallBackQuizTaskTypeItem__input">
619                                <span class="CallBackQuizTaskTypeItem__icon">
620                                    <i :class="task.icon"></i>
621                                </span>
622                                <span class="CallBackQuizTaskTypeItem__text">{{ task.label }}</span>
623                            </label>
624                        </div>
625                    </div>
626                    <div v-if="errors?.tasks" class="CallBackQuiz__error">
627                        {{ errors.tasks }}
628                    </div>
629                </div>
630            `
631            };
632
633            // Компонент: Контактная форма
634            const CallBackQuizForm = {
635                props: ['errors', 'formData', 'selectedTypes'],
636                emits: ['update:form'],
637                setup(props, {emit}) {
638                    const localForm = ref({
639                        name: props.formData?.name || '',
640                        phone: props.formData?.phone || '',
641                        email: props.formData?.email || '',
642                        agreePersonalData: props.formData?.agreePersonalData || false
643                    });
644
645                    // Получаем названия выбранных задач
646                    const tasksList = [
647                        {value: 'web-design', label: 'Веб-дизайн'},
648                        {value: 'app-design', label: 'Дизайн приложений'},
649                        {value: 'website-dev', label: 'Разработка сайта'},
650                        {value: 'service-dev', label: 'Разработка сервиса'},
651                        {value: 'mobile-dev', label: 'Мобильное приложение'},
652                        {value: 'support', label: 'Техподдержка'},
653                        {value: 'seo', label: 'SEO-продвижение'},
654                        {value: 'context', label: 'Контекстная реклама'},
655                        {value: 'automation', label: 'Автоматизация'},
656                        {value: 'ai', label: 'AI внедрение'},
657                        {value: 'consultation', label: 'Консультация'}
658                    ];
659
660                    const selectedTasksLabels = computed(() => {
661                        if (!props.selectedTypes || props.selectedTypes.length === 0) return [];
662                        return tasksList
663                            .filter(task => props.selectedTypes.includes(task.value))
664                            .map(task => task.label);
665                    });
666
667                    // Отслеживаем изменения формы
668                    watch(localForm, (newVal) => {
669                        emit('update:form', newVal);
670                    }, {deep: true});
671
672                    let phoneMask = null;
673
674                    const initMask = () => {
675                        nextTick(() => {
676                            const phoneInput = document.querySelector('#quiz-phone-input');
677                            if (phoneInput && typeof IMask !== 'undefined' && !phoneMask) {
678                                phoneMask = IMask(phoneInput, {
679                                    mask: '+7 (000) 000-00-00',
680                                    lazy: true,
681                                    placeholderChar: '_'
682                                });
683                                phoneMask.on('accept', () => {
684                                    localForm.value.phone = phoneMask.unmaskedValue;
685                                });
686                            }
687                        });
688                    };
689
690                    onMounted(() => {
691                        initMask();
692                    });
693
694                    return {localForm, selectedTasksLabels, initMask};
695                },
696                template: `
697                <div class="CallBackQuizForm">
698                    <div class="CallBackQuizForm__header">
699                        <h3 class="CallBackQuizForm__title">Расскажите о себе</h3>
700                        <div class="CallBackQuizForm__selected-tasks" v-if="selectedTasksLabels.length > 0">
701                            <span class="selected-tasks-label">Вы выбрали:</span>
702                            <div class="selected-tasks-list">
703                                <span v-for="task in selectedTasksLabels" :key="task" class="selected-task-tag">
704                                    {{ task }}
705                                </span>
706                            </div>
707                        </div>
708                    </div>
709
710                    <div class="CallBackQuizForm__container">
711                        <div class="CallBackQuizForm__field">
712                            <label class="CallBackQuizForm__text-label">
713                                <input
714                                    v-model="localForm.name"
715                                    class="CallBackQuizForm__text-input"
716                                    placeholder="Ваше имя"
717                                    type="text"
718                                    maxlength="36">
719                            </label>
720                            <div v-if="errors?.name" class="CallBackQuizForm__error">{{ errors.name }}</div>
721                        </div>
722
723                        <div class="CallBackQuizForm__field">
724                            <label class="CallBackQuizForm__text-label">
725                                <input
726                                    id="quiz-phone-input"
727                                    class="CallBackQuizForm__text-input"
728                                    placeholder="+7 (___) ___-__-__"
729                                    type="tel">
730                            </label>
731                            <div v-if="errors?.phone" class="CallBackQuizForm__error">{{ errors.phone }}</div>
732                        </div>
733
734                        <div class="CallBackQuizForm__field">
735                            <label class="CallBackQuizForm__text-label">
736                                <input
737                                    v-model="localForm.email"
738                                    class="CallBackQuizForm__text-input"
739                                    placeholder="E-mail"
740                                    type="email">
741                            </label>
742                            <div v-if="errors?.email" class="CallBackQuizForm__error">{{ errors.email }}</div>
743                        </div><div class="CallBackQuizForm__field">
744                            <label class="CallBackQuizForm__checkbox-label">
745                                <input
746                                    v-model="localForm.agreePersonalData"
747                                    class="CallBackQuizForm__checkbox-input"
748                                    type="checkbox">
749                                <span class="CallBackQuizForm__checkbox-label-text-wr">
750                                    <span class="CallBackQuizForm__checkbox-label-text">Я даю согласие на обработку персональных данных в соответствии с </span>
751                                    <a href="/docs/personal-data-processing-policy" target="_blank" class="CallBackQuizForm__checkbox-label-text-accent">Политикой обработки персональных данных</a>
752                                </span>
753                            </label>
754                            <div v-if="errors?.agreePersonalData" class="CallBackQuizForm__error">{{ errors.agreePersonalData }}</div>
755                        </div>
756                    </div>
757                </div>
758            `
759            };
760
761            const WizardApp = createApp({
762                components: {CallBackQuizTaskType, CallBackQuizForm},
763                setup() {
764                    const currentStep = ref(1);
765                    const transitionEffect = ref('');
766                    const isSubmitting = ref(false);
767                    const selectedTypes = ref([]);
768                    const formData = ref({
769                        name: '',
770                        phone: '',
771                        email: '',
772                        agreePersonalData: false
773                    });
774                    const errors = ref({});
775
776                    const activeComponent = computed(() => {
777                        if (currentStep.value === 1) return 'CallBackQuizTaskType';
778                        if (currentStep.value === 2) return 'CallBackQuizForm';
779                        return null;
780                    });
781
782                    // Валидация шага 1
783                    const isStep1Valid = computed(() => {
784                        return selectedTypes.value.length > 0;
785                    });
786
787                    // Валидация шага 2 (реальная валидация полей)
788                    const isStep2Valid = computed(() => {
789                        // Проверка имени
790                        const isNameValid = formData.value.name.trim().length >= 2;
791
792                        // Проверка телефона
793                        const phoneDigits = formData.value.phone.replace(/\D/g, '');
794                        const isPhoneValid = formData.value.phone && phoneDigits.length >= 10;
795
796                        // Проверка email
797                        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
798                        const isEmailValid = formData.value.email && emailRegex.test(formData.value.email);
799
800                        // Проверка согласия на обработку персональных данных
801                        const isPersonalDataValid = formData.value.agreePersonalData === true;
802
803                        return isNameValid && isPhoneValid && isEmailValid && isPersonalDataValid;
804                    });
805
806                    const validateStep1 = () => {
807                        const newErrors = {};
808                        if (selectedTypes.value.length === 0) {
809                            newErrors.tasks = 'Выберите хотя бы одну задачу';
810                        }
811                        errors.value = newErrors;
812                        return Object.keys(newErrors).length === 0;
813                    };
814
815                    const validateStep2 = () => {
816                        const newErrors = {};
817
818                        if (!formData.value.name.trim()) {
819                            newErrors.name = 'Введите ваше имя';
820                        } else if (formData.value.name.trim().length < 2) {
821                            newErrors.name = 'Имя должно содержать минимум 2 символа';
822                        }
823
824                        const phoneDigits = formData.value.phone.replace(/\D/g, '');
825                        if (!formData.value.phone || phoneDigits.length < 10) {
826                            newErrors.phone = 'Введите корректный номер телефона';
827                        }
828
829                        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
830                        if (!formData.value.email) {
831                            newErrors.email = 'Введите email';
832                        } else if (!emailRegex.test(formData.value.email)) {
833                            newErrors.email = 'Введите корректный email адрес';
834                        }
835
836                        if (!formData.value.agreePersonalData) {
837                            newErrors.agreePersonalData = 'Подтвердите согласие на обработку персональных данных';
838                        }
839
840                        errors.value = newErrors;
841                        return Object.keys(newErrors).length === 0;
842                    };
843
844                    const nextStep = () => {
845                        if (validateStep1() && isStep1Valid.value) {
846                            transitionEffect.value = 'CallBackQuiz-in';
847                            currentStep.value = 2;
848                        }
849                    };
850
851                    const prevStep = () => {
852                        transitionEffect.value = 'CallBackQuiz-out';
853                        currentStep.value = 1;
854                        errors.value = {};
855                    };
856
857                    const updateTypes = (types) => {
858                        selectedTypes.value = types;
859                        if (errors.value.tasks) {
860                            delete errors.value.tasks;
861                        }
862                    };
863
864                    const updateForm = (data) => {
865                        formData.value = data;
866                        // Очищаем ошибки при изменении полей
867                        if (errors.value.name && formData.value.name.trim().length >= 2) {
868                            delete errors.value.name;
869                        }
870                        if (errors.value.phone) {
871                            const phoneDigits = formData.value.phone.replace(/\D/g, '');
872                            if (phoneDigits.length >= 10) {
873                                delete errors.value.phone;
874                            }
875                        }
876                        if (errors.value.email) {
877                            const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
878                            if (emailRegex.test(formData.value.email)) {
879                                delete errors.value.email;
880                            }
881                        }
882                        if (errors.value.agreePersonalData && formData.value.agreePersonalData) {
883                            delete errors.value.agreePersonalData;
884                        }
885                    };
886
887                    const scrollToTop = () => {
888                        nextTick(() => {
889                            window.scrollTo({
890                                top: 0,
891                                behavior: 'smooth'
892                            });
893                        });
894                    };
895
896                    const handleSubmit = async () => {
897                        if (!validateStep2() || !isStep2Valid.value) return;
898
899                        isSubmitting.value = true;
900
901                        const submitData = {
902                            types: selectedTypes.value,
903                            name: formData.value.name.trim(),
904                            phone: formData.value.phone.replace(/\D/g, ''),
905                            email: formData.value.email.trim(),
906                            agreePersonalData: formData.value.agreePersonalData
907                        };
908
909                        try {
910                            const response = await api.action('form:submitWizard', submitData);
911
912                            if (response.status === 'ok') {
913                                if (window.showNotification) {
914                                    window.showNotification(response.message || 'Заявка успешно отправлена!', 'success');
915                                }
916
917                                // Сброс формы
918                                selectedTypes.value = [];
919                                formData.value = {
920                                    name: '',
921                                    phone: '',
922                                    email: '',
923                                    agreePersonalData: false
924                                };
925                                currentStep.value = 1;
926
927                                // Скролл вверх после успешной отправки
928                                setTimeout(() => {
929                                    scrollToTop();
930                                }, 500);
931                            }
932                        } catch (error) {
933                            console.error('Ошибка отправки:', error);
934                            if (window.showNotification) {
935                                window.showNotification('Произошла ошибка. Пожалуйста, попробуйте позже.', 'error');
936                            }
937                        } finally {
938                            isSubmitting.value = false;
939                        }
940                    };
941
942                    watch(currentStep, (newIndex, oldIndex) => {
943                        if (newIndex < oldIndex) {
944                            transitionEffect.value = 'CallBackQuiz-out';
945                        } else if (newIndex > oldIndex) {
946                            transitionEffect.value = 'CallBackQuiz-in';
947                        }
948                    });
949
950                    return {
951                        currentStep,
952                        transitionEffect,
953                        isSubmitting,
954                        selectedTypes,
955                        formData,
956                        errors,
957                        activeComponent,
958                        isStep1Valid,
959                        isStep2Valid,
960                        nextStep,
961                        prevStep,
962                        updateTypes,
963                        updateForm,
964                        handleSubmit,
965                        scrollToTop
966                    };
967                }
968            });
969
970            WizardApp.mount(mountElement);
971            mountElement.__vue_app__ = WizardApp;
972        }
973    })();
974
975
976// Javascript 
977// Инициализация Swiper
978    function initSwiper() {
979        const swiperEl = document.querySelector('#WidgetsAboutHistoryWidget .historySwiper');
980        if (swiperEl && typeof Swiper !== 'undefined') {
981            new Swiper(swiperEl, {
982                slidesPerView: 2,
983                spaceBetween: 16,
984                slidesPerGroup: 2,
985                loop: true,
986                autoplay: {
987                    delay: 2000,
988                    disableOnInteraction: false,
989                    pauseOnMouseEnter: true,
990                },
991                pagination: {
992                    el: '.swiper-pagination',
993                    clickable: true,
994                },
995                breakpoints: {
996                    320: {
997                        slidesPerView: 1,
998                        slidesPerGroup: 1,
999                    },
1000                    640: {
1001                        slidesPerView: 1.5,
1002                        slidesPerGroup: 2,
1003                    },
1004                    768: {
1005                        slidesPerView: 2,
1006                        slidesPerGroup: 2,
1007                    },
1008                }
1009            });
1010        }
1011    }
1012
1013    // Анимация элементов при скролле
1014    function initScrollAnimations() {
1015        const animatedElements = document.querySelectorAll('#WidgetsAboutHistoryWidget .animate-on-scroll');
1016        const lines = document.querySelectorAll('#WidgetsAboutHistoryWidget .Icon-line-decor');
1017        const block = document.querySelector('#WidgetsAboutHistoryWidget .MainPageAnnotationBlock');
1018
1019        if ((animatedElements.length === 0 && lines.length === 0) || !block) return;
1020
1021        // Функция для анимации текстовых элементов
1022        const animateTextElements = () => {
1023            animatedElements.forEach((el, index) => {
1024                setTimeout(() => {
1025                    el.classList.add('animated');
1026                }, index * 200); // Каждый элемент появляется с задержкой 200ms
1027            });
1028        };
1029
1030        // Функция для анимации линии
1031        const animateLines = () => {
1032            lines.forEach(line => {
1033                line.classList.add('animation');
1034            });
1035        };
1036
1037        // Создаем наблюдатель за блоком
1038        const observer = new IntersectionObserver((entries) => {
1039            entries.forEach(entry => {
1040                if (entry.isIntersecting) {
1041                    // Анимируем текстовые элементы
1042                    animateTextElements();
1043                    // Анимируем линию с задержкой
1044                    setTimeout(() => {
1045                        animateLines();
1046                    }, 800);
1047                    observer.unobserve(entry.target);
1048                }
1049            });
1050        }, { threshold: 0.3 }); // Срабатывает когда 30% блока видно
1051
1052        observer.observe(block);
1053    }
1054
1055    // Запускаем
1056    if (document.readyState === 'loading') {
1057        document.addEventListener('DOMContentLoaded', function () {
1058            initSwiper();
1059            initScrollAnimations();
1060        });
1061    } else {
1062        initSwiper();
1063        initScrollAnimations();
1064    }
1065
1066
1067// Javascript 
1068(function() {
1069        const { createApp, ref, computed, onMounted, onBeforeUnmount, nextTick, watch } = Vue;
1070
1071        const mountElement = document.getElementById('ReviewsWidget');
1072        if (mountElement && !mountElement.__vue_app__) {
1073            const ReviewsWidgetApp = createApp({
1074                setup() {
1075                    const reviewsData = ref({ video: [], images: [] });
1076                    const loading = ref(true);
1077                    const activeTab = ref('video');
1078                    const isActiveModal = ref(false);
1079                    const currentCardData = ref(null);
1080                    let swiperInstance = null;
1081                    let resizeObserver = null;
1082                    const cardRef = ref(null);
1083                    const listRef = ref(null);
1084                    const sliderContainer = ref(null);
1085
1086                    const currentReviews = computed(() => reviewsData.value[activeTab.value] || []);
1087                    const currentCard = computed(() => currentCardData.value);
1088
1089                    const totalReviewsCount = computed(() => {
1090                        return reviewsData.value.video.length + reviewsData.value.images.length;
1091                    });
1092
1093                    const syncHeights = () => {
1094                        nextTick(() => {
1095                            const card = cardRef.value;
1096                            const slider = sliderContainer.value;
1097
1098                            if (card && slider) {
1099                                const cardHeight = card.offsetHeight;
1100                                slider.style.minHeight = cardHeight + 'px';
1101                            }
1102                        });
1103                    };
1104
1105                    const startAutoplay = () => {
1106                        if (swiperInstance && swiperInstance.autoplay && currentReviews.value.length > 3) {
1107                            swiperInstance.autoplay.stop();
1108                            swiperInstance.autoplay.start();
1109                        }
1110                    };
1111
1112                    const stopAutoplay = () => {
1113                        if (swiperInstance && swiperInstance.autoplay) {
1114                            swiperInstance.autoplay.stop();
1115                        }
1116                    };
1117
1118                    const loadReviews = async () => {
1119                        loading.value = true;
1120                        try {
1121                            const response = await api.action('reviews:getReviews', {});
1122
1123                            if (response.status === 'ok' && response.data) {
1124                                reviewsData.value = {
1125                                    video: response.data.video || [],
1126                                    images: response.data.images || []
1127                                };
1128                            }
1129                        } catch (error) {
1130                            console.error('Ошибка загрузки отзывов:', error);
1131                        } finally {
1132                            loading.value = false;
1133                            nextTick(() => initSwiper());
1134                        }
1135                    };
1136
1137                    const openModal = (card) => {
1138                        currentCardData.value = card;
1139                        isActiveModal.value = true;
1140                        stopAutoplay();
1141                    };
1142
1143                    const closeModal = () => {
1144                        isActiveModal.value = false;
1145                        currentCardData.value = null;
1146                        startAutoplay();
1147                    };
1148
1149                    const initSwiper = () => {
1150                        nextTick(() => {
1151                            if (swiperInstance) {
1152                                swiperInstance.destroy(true, true);
1153                                swiperInstance = null;
1154                            }
1155
1156                            const swiperEl = document.querySelector('.reviewsSwiper');
1157                            if (swiperEl && currentReviews.value.length > 0 && typeof Swiper !== 'undefined') {
1158                                swiperInstance = new Swiper(swiperEl, {
1159                                    slidesPerView: 2.5,
1160                                    spaceBetween: 16,
1161                                    centeredSlides: false,
1162                                    loop: currentReviews.value.length > 3,
1163                                    autoplay: {
1164                                        delay: 4000,
1165                                        disableOnInteraction: false,
1166                                        pauseOnMouseEnter: true,
1167                                        stopOnLastSlide: false,
1168                                    },
1169                                    breakpoints: {
1170                                        320: { slidesPerView: 1.2, spaceBetween: 8 },
1171                                        480: { slidesPerView: 1.5, spaceBetween: 8 },
1172                                        640: { slidesPerView: 2, spaceBetween: 8 },
1173                                        768: { slidesPerView: 2.2, spaceBetween: 8 },
1174                                        1024: { slidesPerView: 2.5, spaceBetween: 8 },
1175                                        1280: { slidesPerView: 2.5, spaceBetween: 8 },
1176                                        1920: { slidesPerView: 3.5, spaceBetween: 8 }
1177                                    },
1178                                    on: {
1179                                        init: function() {
1180                                            syncHeights();
1181                                        },
1182                                        resize: function() {
1183                                            syncHeights();
1184                                        },
1185                                        touchStart: function() {
1186                                            if (swiperInstance && swiperInstance.autoplay) {
1187                                                swiperInstance.autoplay.stop();
1188                                            }
1189                                        },
1190                                        touchEnd: function() {
1191                                            if (swiperInstance && swiperInstance.autoplay && currentReviews.value.length > 3) {
1192                                                swiperInstance.autoplay.start();
1193                                            }
1194                                        }
1195                                    }
1196                                });
1197                            }
1198
1199                            syncHeights();
1200                            startAutoplay();
1201                        });
1202                    };
1203
1204                    const handleResize = () => {
1205                        syncHeights();
1206                    };
1207
1208                    watch(activeTab, () => {
1209                        if (!loading.value) {
1210                            nextTick(() => {
1211                                initSwiper();
1212                            });
1213                        }
1214                    });
1215
1216                    watch(currentReviews, () => {
1217                        if (!loading.value && currentReviews.value.length > 0) {
1218                            nextTick(() => {
1219                                initSwiper();
1220                            });
1221                        }
1222                    });
1223
1224                    onMounted(() => {
1225                        loadReviews();
1226                        syncHeights();
1227
1228                        resizeObserver = new ResizeObserver(() => {
1229                            syncHeights();
1230                        });
1231
1232                        if (cardRef.value) {
1233                            resizeObserver.observe(cardRef.value);
1234                        }
1235
1236                        window.addEventListener('resize', handleResize);
1237                    });
1238
1239                    onBeforeUnmount(() => {
1240                        if (swiperInstance) {
1241                            swiperInstance.destroy(true, true);
1242                            swiperInstance = null;
1243                        }
1244                        if (resizeObserver) {
1245                            resizeObserver.disconnect();
1246                        }
1247                        window.removeEventListener('resize', handleResize);
1248                        document.body.style.overflow = '';
1249                    });
1250
1251                    return {
1252                        reviewsData,
1253                        loading,
1254                        activeTab,
1255                        isActiveModal,
1256                        currentReviews,
1257                        currentCard,
1258                        totalReviewsCount,
1259                        cardRef,
1260                        listRef,
1261                        sliderContainer,
1262                        openModal,
1263                        closeModal
1264                    };
1265                }
1266            });
1267            ReviewsWidgetApp.mount(mountElement);
1268            mountElement.__vue_app__ = ReviewsWidgetApp;
1269        }
1270    })();
1271
1272
1273// Javascript 
1274(function () {
1275        const { createApp, ref, computed, onMounted, onUnmounted, nextTick } = Vue;
1276
1277        const mountElement = document.getElementById('ServicesWidget');
1278        if (mountElement && !mountElement.__vue_app__) {
1279            const ServicesWidgetApp = createApp({
1280                setup() {
1281                    const servicesData = ref([{"id":1,"title":"\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0441\u0430\u0439\u0442\u043e\u0432","image":"\/Modules\/Ui\/Layouts\/PublicLayout\/Static\/img\/services\/\u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435_\u0441\u0430\u0439\u0442\u043e\u0432.png","backgroundColor":"#F4CBCB","items":[{"text":"\u041a\u043e\u0440\u043f\u043e\u0440\u0430\u0442\u0438\u0432\u043d\u044b\u0435 \u0441\u0430\u0439\u0442\u044b"},{"text":"\u041a\u0430\u0442\u0430\u043b\u043e\u0433\u0438"},{"text":"\u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442-\u043c\u0430\u0433\u0430\u0437\u0438\u043d\u044b"},{"text":"\u0421\u043b\u043e\u0436\u043d\u044b\u0435 \u0446\u0438\u0444\u0440\u043e\u0432\u044b\u0435 \u044d\u043a\u043e\u0441\u0438\u0441\u0442\u0435\u043c\u044b (\u0432\u0435\u0431-\u0441\u0435\u0440\u0432\u0438\u0441\u044b)"}]},{"id":2,"title":"\u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0440\u0435\u043a\u043b\u0430\u043c\u0430","image":"\/Modules\/Ui\/Layouts\/PublicLayout\/Static\/img\/services\/\u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442_\u0440\u0435\u043a\u043b\u0430\u043c\u0430.png","backgroundColor":"#F9D2A3","items":[{"text":"\u041a\u043e\u043c\u043f\u043b\u0435\u043a\u0441\u043d\u0430\u044f \u043b\u0438\u0434\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u044f"},{"text":"SEO \u2013 \u043f\u0440\u043e\u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435"},{"text":"\u0412\u0435\u0434\u0435\u043d\u0438\u0435 \u0420\u041a"},{"text":"\u0410\u0432\u0438\u0442\u043e"}]},{"id":3,"title":"\u041c\u043e\u0434\u0435\u0440\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0441\u0430\u0439\u0442\u043e\u0432","image":"\/Modules\/Ui\/Layouts\/PublicLayout\/Static\/img\/services\/\u043c\u043e\u0434\u0435\u0440\u043d\u0438\u0437\u0430\u0446\u0438\u044f.png","backgroundColor":"#85C0D5","items":[{"text":"\u0422\u0435\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u0443\u043a\u0440\u0430\u0448\u0435\u043d\u0438\u044f \u043a \u043f\u0440\u0430\u0437\u0434\u043d\u0438\u043a\u0430\u043c"},{"text":"\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u043d\u044b\u0435 \u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u0434\u043e\u0440\u0430\u0431\u043e\u0442\u043a\u0438"}]},{"id":4,"title":"\u0422\u0435\u0445. \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 \u0441\u0430\u0439\u0442\u043e\u0432","image":"\/Modules\/Ui\/Layouts\/PublicLayout\/Static\/img\/services\/\u0442\u0435\u0445_\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430.png","backgroundColor":"#E9C8BA","items":[{"text":"\u041e\u0431\u043b\u0430\u0447\u043d\u0430\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 \u0441\u0430\u0439\u0442\u043e\u0432"},{"text":"\u0422\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0438 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 \u0441\u0430\u0439\u0442\u043e\u0432"}]},{"id":5,"title":"\u041c\u043e\u0431\u0438\u043b\u044c\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f","image":"\/Modules\/Ui\/Layouts\/PublicLayout\/Static\/img\/services\/\u043c\u043e\u0431\u0438\u043b\u044c\u043d\u044b\u0435_\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.png","backgroundColor":"#8CAAD0","items":[{"text":"Native"},{"text":"PWA"}]},{"id":6,"title":"\u0411\u0440\u0435\u043d\u0434\u0438\u043d\u0433 \u0438 \u0434\u0438\u0437\u0430\u0439\u043d","image":"\/Modules\/Ui\/Layouts\/PublicLayout\/Static\/img\/services\/\u0431\u0440\u0435\u043d\u0434\u0438\u043d\u0433_\u0438_\u0434\u0438\u0437\u0430\u0439\u043d.png","backgroundColor":"#EBD598","items":[{"text":"\u0420\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043b\u043e\u0433\u043e\u0442\u0438\u043f\u0430 \u0438 \u0432\u0438\u0437\u0438\u0442\u043e\u043a"},{"text":"\u0420\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0444\u0438\u0440\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u0441\u0442\u0438\u043b\u044f"},{"text":"\u041d\u0435\u0439\u043c\u0438\u043d\u0433"},{"text":"\u0420\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0430 Marketing Kit"}]},{"id":7,"title":"\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430","image":"\/Modules\/Ui\/Layouts\/PublicLayout\/Static\/img\/services\/\u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f.png","backgroundColor":"#97A8EA","items":[{"text":"\u0412\u043d\u0435\u0434\u0440\u0435\u043d\u0438\u0435 CRM"},{"text":"\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441 1\u0421"}]},{"id":8,"title":"\u0410\u0443\u0434\u0438\u0442","image":"\/Modules\/Ui\/Layouts\/PublicLayout\/Static\/img\/services\/\u0430\u0443\u0434\u0438\u0442.png","backgroundColor":"#B0DED9","items":[{"text":"\u0410\u0443\u0434\u0438\u0442 \u0441\u0430\u0439\u0442\u0430"},{"text":"\u0410\u0443\u0434\u0438\u0442 \u0440\u0435\u043a\u043b\u0430\u043c\u043d\u044b\u0445 \u043a\u0430\u043c\u043f\u0430\u043d\u0438\u0439"},{"text":"\u0410\u0443\u0434\u0438\u0442 CRM-\u0441\u0438\u0441\u0442\u0435\u043c"},{"text":"\u041a\u043e\u043c\u043f\u043b\u0435\u043a\u0441\u043d\u044b\u0439 \u0430\u0443\u0434\u0438\u0442 \u043e\u043d\u043b\u0430\u0439\u043d-\u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b"}]},{"id":9,"title":"\u041a\u043e\u043d\u0441\u0430\u043b\u0442\u0438\u043d\u0433 \u043f\u0440\u043e\u0434\u0430\u0436","image":"\/Modules\/Ui\/Layouts\/PublicLayout\/Static\/img\/services\/\u043a\u043e\u043d\u0441\u0430\u043b\u0442\u0438\u043d\u0433.png","backgroundColor":"#C1DA92","items":[{"text":"\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u043e\u043d\u0441\u0430\u043b\u0442\u0438\u043d\u0433"},{"text":"\u0410\u0440\u0435\u043d\u0434\u0430 \u0420\u041e\u041f\u0430"}]}]);
1282                    const activeServiceIndex = ref(0);
1283                    const isTransitioning = ref(false);
1284                    const mainBlock = ref(null);
1285                    const previewImage = ref(null);
1286                    const topOffset = ref(0);
1287
1288                    // Трансформации для параллакс эффекта
1289                    const wrapperRotateX = ref(0);
1290                    const wrapperRotateY = ref(0);
1291                    const imageTranslateX = ref(0);
1292                    const imageTranslateY = ref(0);
1293
1294                    const currentService = computed(() => servicesData.value[activeServiceIndex.value]);
1295
1296                    // Вычисляемые стили для обертки
1297                    const wrapperTransform = computed(() => {
1298                        return `perspective(1000px) rotateX(${wrapperRotateX.value}deg) rotateY(${wrapperRotateY.value}deg)`;
1299                    });
1300
1301                    // Вычисляемые стили для изображения
1302                    const imageTransform = computed(() => {
1303                        return `perspective(1000px) rotateX(0deg) rotateY(0deg) translateX(${imageTranslateX.value}px) translateY(${imageTranslateY.value}px) translateZ(0px)`;
1304                    });
1305
1306                    const formatNumber = (num) => {
1307                        return String(num).padStart(2, '0');
1308                    };
1309
1310                    const setActiveService = async (index, service) => {
1311                        if (isTransitioning.value || activeServiceIndex.value === index) return;
1312
1313                        isTransitioning.value = true;
1314
1315                        // Сбрасываем трансформации
1316                        resetTransform();
1317
1318                        // Меняем активную услугу
1319                        activeServiceIndex.value = index;
1320
1321                        // Ждем окончания анимации
1322                        setTimeout(() => {
1323                            isTransitioning.value = false;
1324                        }, 600);
1325                    };
1326
1327                    // Обработчик движения мыши для параллакс эффекта
1328                    const handleMouseMove = (event) => {
1329                        if (isTransitioning.value) return;
1330
1331                        const rect = event.currentTarget.getBoundingClientRect();
1332                        const x = (event.clientX - rect.left) / rect.width - 0.5;
1333                        const y = (event.clientY - rect.top) / rect.height - 0.5;
1334
1335                        // Обновляем трансформацию для обертки
1336                        wrapperRotateX.value = y * -7;
1337                        wrapperRotateY.value = x * 7;
1338
1339                        // Обновляем трансформацию для изображения
1340                        imageTranslateX.value = x * 20;
1341                        imageTranslateY.value = y * 20;
1342                    };
1343
1344                    // Сброс трансформации
1345                    const resetTransform = () => {
1346                        wrapperRotateX.value = 0;
1347                        wrapperRotateY.value = 0;
1348                        imageTranslateX.value = 0;
1349                        imageTranslateY.value = 0;
1350                    };
1351
1352                    // Функция для расчета отступа
1353                    const calculateOffset = () => {
1354                        if (!mainBlock.value || !previewImage.value) return;
1355
1356                        const mainBlockRect = mainBlock.value.getBoundingClientRect();
1357                        const previewHeight = previewImage.value.offsetHeight;
1358                        const windowHeight = window.innerHeight;
1359
1360                        // Рассчитываем отступ для центрирования блока внутри MainPageServicesBlock
1361                        const availableHeight = mainBlockRect.height;
1362                        const offset = (availableHeight - previewHeight) / 2;
1363
1364                        // Устанавливаем отступ сверху
1365                        topOffset.value = Math.max(0, offset);
1366                        previewImage.value.style.top = topOffset.value + 'px';
1367                    };
1368
1369                    // Обновление позиции при скролле и ресайзе
1370                    let ticking = false;
1371                    const updatePosition = () => {
1372                        if (!ticking) {
1373                            requestAnimationFrame(() => {
1374                                calculateOffset();
1375                                ticking = false;
1376                            });
1377                            ticking = true;
1378                        }
1379                    };
1380
1381                    // Хуки для анимации
1382                    const onBeforeEnter = (el) => {
1383                        el.style.transformOrigin = 'center center';
1384                        el.style.transform = 'scale(0.3)';
1385                        el.style.opacity = '0';
1386                        el.style.willChange = 'transform, opacity';
1387                    };
1388
1389                    const onEnter = (el, done) => {
1390                        el.offsetHeight;
1391                        el.style.transition = 'transform 0.5s cubic-bezier(0.34, 1.2, 0.64, 1), opacity 0.4s ease-out';
1392                        el.style.transform = 'scale(1)';
1393                        el.style.opacity = '1';
1394                        setTimeout(done, 500);
1395                    };
1396
1397                    const onAfterEnter = (el) => {
1398                        el.style.transition = '';
1399                        el.style.willChange = '';
1400                        // После анимации пересчитываем позицию
1401                        setTimeout(() => {
1402                            calculateOffset();
1403                        }, 50);
1404                    };
1405
1406                    const onBeforeLeave = (el) => {
1407                        el.style.transformOrigin = 'center center';
1408                        el.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease-out';
1409                        el.style.transform = 'scale(0.3)';
1410                        el.style.opacity = '0';
1411                        el.style.willChange = 'transform, opacity';
1412                    };
1413
1414                    const onLeave = (el, done) => {
1415                        setTimeout(() => {
1416                            done();
1417                        }, 300);
1418                    };
1419
1420                    const onAfterLeave = (el) => {
1421                        el.style.transition = '';
1422                        el.style.transform = '';
1423                        el.style.opacity = '';
1424                        el.style.willChange = '';
1425                    };
1426
1427                    const preloadImages = () => {
1428                        servicesData.value.forEach(service => {
1429                            if (service.image) {
1430                                const img = new Image();
1431                                img.src = service.image;
1432                            }
1433                        });
1434                    };
1435
1436                    // Наблюдатель за изменением размера блока
1437                    let resizeObserver = null;
1438
1439                    onMounted(() => {
1440                        preloadImages();
1441                        nextTick(() => {
1442                            calculateOffset();
1443
1444                            // Слушаем события скролла и ресайза
1445                            window.addEventListener('scroll', updatePosition);
1446                            window.addEventListener('resize', updatePosition);
1447
1448                            // Наблюдатель за изменением размера MainPageServicesBlock
1449                            if (mainBlock.value && window.ResizeObserver) {
1450                                resizeObserver = new ResizeObserver(() => {
1451                                    calculateOffset();
1452                                });
1453                                resizeObserver.observe(mainBlock.value);
1454                            }
1455
1456                            // Наблюдатель за изменением DOM (появление новых элементов)
1457                            const mutationObserver = new MutationObserver(() => {
1458                                calculateOffset();
1459                            });
1460                            mutationObserver.observe(mainBlock.value, {
1461                                childList: true,
1462                                subtree: true,
1463                                attributes: true,
1464                                attributeFilter: ['style', 'class']
1465                            });
1466                        });
1467                    });
1468
1469                    onUnmounted(() => {
1470                        window.removeEventListener('scroll', updatePosition);
1471                        window.removeEventListener('resize', updatePosition);
1472                        if (resizeObserver) {
1473                            resizeObserver.disconnect();
1474                        }
1475                    });
1476
1477                    return {
1478                        servicesData,
1479                        activeServiceIndex,
1480                        currentService,
1481                        formatNumber,
1482                        setActiveService,
1483                        wrapperTransform,
1484                        imageTransform,
1485                        handleMouseMove,
1486                        resetTransform,
1487                        isTransitioning,
1488                        mainBlock,
1489                        previewImage,
1490                        onBeforeEnter,
1491                        onEnter,
1492                        onAfterEnter,
1493                        onBeforeLeave,
1494                        onLeave,
1495                        onAfterLeave
1496                    };
1497                }
1498            });
1499
1500            ServicesWidgetApp.mount(mountElement);
1501            mountElement.__vue_app__ = ServicesWidgetApp;
1502        }
1503    })();
1504
1505
1506// Javascript 
1507(function () {
1508        const {createApp, ref, computed, onMounted, onUnmounted, watch, nextTick} = Vue;
1509
1510        const mountElement = document.getElementById('ProjectsListWidget');
1511        if (mountElement && !mountElement.__vue_app__) {
1512            const ProjectsListWidgetApp = createApp({
1513                setup() {
1514                    const filtersData = ref([]);
1515                    const projectsData = ref([]);
1516                    const isLoading = ref(true);
1517                    const activeFilter = ref('all');
1518                    const isFilterActive = ref(false);
1519                    const isMobileFilterOpen = ref(false);
1520                    const projectsContainer = ref(null);
1521
1522                    const selectedFilters = ref({});
1523                    const projects = ref([]);
1524
1525                    const filterMapping = {
1526                        'type': 'typeId',
1527                        'service': 'serviceId',
1528                        'theme': 'themeId'
1529                    };
1530
1531                    const itemsPerPage = 6;
1532                    const currentPage = ref(1);
1533                    const isPaginationLoading = ref(false);
1534
1535                    // Функция для управления классом на body
1536                    const updateBodyClass = (isOpen) => {
1537                        if (isOpen) {
1538                            document.body.classList.add('filter--open');
1539                        } else {
1540                            document.body.classList.remove('filter--open');
1541                        }
1542                    };
1543
1544                    // Функция для скролла к блоку проектов
1545                    const scrollToProjects = () => {
1546                        nextTick(() => {
1547                            if (projectsContainer.value) {
1548                                const headerOffset = 100;
1549                                const elementPosition = projectsContainer.value.getBoundingClientRect().top;
1550                                const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
1551
1552                                window.scrollTo({
1553                                    top: offsetPosition,
1554                                    behavior: 'smooth'
1555                                });
1556                            }
1557                        });
1558                    };
1559
1560                    const getOptionCount = (filterId, optionId) => {
1561                        const projectField = filterMapping[filterId] || filterId + 'Id';
1562                        return projectsData.value.filter(project => {
1563                            const projectValues = project.filter[projectField] || [];
1564                            return projectValues.includes(optionId);
1565                        }).length;
1566                    };
1567
1568                    const getAvailableFilterOptions = (filterId) => {
1569                        const filter = filtersData.value.find(f => f.id === filterId);
1570                        if (!filter) return [];
1571
1572                        let availableOptions = filter.values.filter(option => {
1573                            return getOptionCount(filterId, option.id) > 0;
1574                        });
1575
1576                        const hasOtherFilters = Object.keys(selectedFilters.value).some(key =>
1577                            key !== filterId && selectedFilters.value[key] && selectedFilters.value[key].length > 0
1578                        );
1579
1580                        if (hasOtherFilters) {
1581                            let filteredByOthers = [...projectsData.value];
1582                            const projectField = filterMapping[filterId] || filterId + 'Id';
1583
1584                            for (const [fId, values] of Object.entries(selectedFilters.value)) {
1585                                if (fId !== filterId && values && values.length > 0) {
1586                                    const field = filterMapping[fId] || fId + 'Id';
1587                                    filteredByOthers = filteredByOthers.filter(project => {
1588                                        const projectValues = project.filter[field] || [];
1589                                        return values.some(v => projectValues.includes(v));
1590                                    });
1591                                }
1592                            }
1593
1594                            const availableIds = new Set();
1595                            filteredByOthers.forEach(project => {
1596                                const projectValues = project.filter[projectField] || [];
1597                                projectValues.forEach(val => availableIds.add(val));
1598                            });
1599
1600                            availableOptions = availableOptions.filter(item => availableIds.has(item.id));
1601                        }
1602
1603                        return availableOptions;
1604                    };
1605
1606                    const getProjectsMatchingCurrentFilters = () => {
1607                        let result = [...projectsData.value];
1608
1609                        for (const [filterId, selectedValues] of Object.entries(selectedFilters.value)) {
1610                            if (selectedValues && selectedValues.length > 0) {
1611                                const projectField = filterMapping[filterId] || filterId + 'Id';
1612
1613                                result = result.filter(project => {
1614                                    const projectValues = project.filter[projectField] || [];
1615                                    return selectedValues.some(val => projectValues.includes(val));
1616                                });
1617                            }
1618                        }
1619
1620                        return result;
1621                    };
1622
1623                    const filteredProjects = computed(() => {
1624                        if (activeFilter.value !== 'filter') return projects.value;
1625
1626                        const hasActiveFilters = Object.keys(selectedFilters.value).some(key =>
1627                            selectedFilters.value[key] && selectedFilters.value[key].length > 0
1628                        );
1629                        if (!hasActiveFilters) return projects.value;
1630
1631                        return getProjectsMatchingCurrentFilters();
1632                    });
1633
1634                    const totalPages = computed(() => {
1635                        return Math.ceil(filteredProjects.value.length / itemsPerPage);
1636                    });
1637
1638                    const paginatedProjects = computed(() => {
1639                        const start = (currentPage.value - 1) * itemsPerPage;
1640                        const end = start + itemsPerPage;
1641                        return filteredProjects.value.slice(start, end);
1642                    });
1643
1644                    const displayedPages = computed(() => {
1645                        const total = totalPages.value;
1646                        const current = currentPage.value;
1647                        const delta = 2;
1648                        const range = [];
1649                        const rangeWithDots = [];
1650                        let l;
1651
1652                        if (total === 0) return [1];
1653
1654                        for (let i = 1; i <= total; i++) {
1655                            if (i === 1 || i === total || (i >= current - delta && i <= current + delta)) {
1656                                range.push(i);
1657                            }
1658                        }
1659
1660                        range.forEach((i, pos) => {
1661                            if (l) {
1662                                if (i - l === 2) {
1663                                    rangeWithDots.push(l + 1);
1664                                } else if (i - l !== 1) {
1665                                    rangeWithDots.push('...');
1666                                }
1667                            }
1668                            rangeWithDots.push(i);
1669                            l = i;
1670                        });
1671
1672                        return rangeWithDots;
1673                    });
1674
1675                    const highlightBarStyle = ref({});
1676
1677                    const updateHighlightBar = () => {
1678                        if (isPaginationLoading.value) return;
1679
1680                        nextTick(() => {
1681                            const activeButton = document.querySelector('.PaginationComponentButton-wr.active .PaginationComponentButton');
1682                            const paginationContainer = document.querySelector('.PaginationComponent__pagination');
1683                            const highlightBar = document.querySelector('.PaginationComponent__highlight-bar');
1684
1685                            if (activeButton && paginationContainer && highlightBar) {
1686                                const buttonRect = activeButton.getBoundingClientRect();
1687                                const containerRect = paginationContainer.getBoundingClientRect();
1688                                highlightBar.style.width = buttonRect.width + 'px';
1689                                highlightBar.style.transform = 'translateX(' + (buttonRect.left - containerRect.left) + 'px)';
1690                            }
1691                        });
1692                    };
1693
1694                    const goToPage = async (page) => {
1695                        if (page < 1 || page > totalPages.value || page === currentPage.value || isPaginationLoading.value) return;
1696
1697                        isPaginationLoading.value = true;
1698                        await new Promise(resolve => setTimeout(resolve, 300));
1699                        currentPage.value = page;
1700                        isPaginationLoading.value = false;
1701
1702                        setTimeout(() => {
1703                            const portfolioBlock = document.querySelector('.MainPagePortfolio');
1704                            if (portfolioBlock) {
1705                                portfolioBlock.scrollIntoView({behavior: 'smooth', block: 'start'});
1706                            }
1707                        }, 500);
1708                    };
1709
1710                    const resetToFirstPage = () => {
1711                        currentPage.value = 1;
1712                    };
1713
1714                    watch(filteredProjects, () => {
1715                        resetToFirstPage();
1716                        nextTick(() => updateHighlightBar());
1717                    });
1718
1719                    watch(currentPage, () => {
1720                        nextTick(() => {
1721                            if (!isPaginationLoading.value) updateHighlightBar();
1722                        });
1723                    });
1724
1725                    watch(selectedFilters, () => {
1726                        if (activeFilter.value === 'filter') {
1727                            resetToFirstPage();
1728                        }
1729                    }, {deep: true});
1730
1731                    const setActiveFilter = (filter) => {
1732                        requestAnimationFrame(() => {
1733                            activeFilter.value = filter;
1734                            if (filter === 'all') {
1735                                isFilterActive.value = false;
1736                                isMobileFilterOpen.value = false;
1737                                updateBodyClass(false);
1738                            }
1739                        });
1740                    };
1741
1742                    const toggleFilter = () => {
1743                        requestAnimationFrame(() => {
1744                            if (activeFilter.value === 'all') {
1745                                activeFilter.value = 'filter';
1746                                isFilterActive.value = true;
1747                                updateBodyClass(true);
1748                            } else {
1749                                activeFilter.value = 'all';
1750                                isFilterActive.value = false;
1751                                updateBodyClass(false);
1752                                resetToFirstPage();
1753                            }
1754                        });
1755                    };
1756
1757                    const toggleMobileFilter = (event) => {
1758                        // Предотвращаем стандартное поведение и всплытие
1759                        if (event) {
1760                            event.preventDefault();
1761                            event.stopPropagation();
1762                        }
1763
1764                        // Закрываем десктопный фильтр, если открыт
1765                        if (isFilterActive.value) {
1766                            isFilterActive.value = false;
1767                            activeFilter.value = 'all';
1768                            updateBodyClass(false);
1769                        }
1770                        // Открываем/закрываем мобильную панель
1771                        isMobileFilterOpen.value = !isMobileFilterOpen.value;
1772                        if (isMobileFilterOpen.value) {
1773                            document.body.classList.add('mobile-filter--open');
1774                        } else {
1775                            document.body.classList.remove('mobile-filter--open');
1776                            // Убеждаемся, что filter--open тоже удален
1777                            updateBodyClass(false);
1778                        }
1779                    };
1780
1781                    const closeMobileFilter = (event) => {
1782                        if (event) {
1783                            event.preventDefault();
1784                            event.stopPropagation();
1785                        }
1786                        if (isMobileFilterOpen.value) {
1787                            isMobileFilterOpen.value = false;
1788                            document.body.classList.remove('mobile-filter--open');
1789                            // Убеждаемся, что filter--open тоже удален
1790                            updateBodyClass(false);
1791                            // Скролл к блоку проектов
1792                            setTimeout(() => {
1793                                scrollToProjects();
1794                            }, 350);
1795                        }
1796                    };
1797
1798                    const applyMobileFilters = (event) => {
1799                        if (event) {
1800                            event.preventDefault();
1801                            event.stopPropagation();
1802                        }
1803                        activeFilter.value = 'filter';
1804                        isFilterActive.value = true;
1805                        isMobileFilterOpen.value = false;
1806                        document.body.classList.remove('mobile-filter--open');
1807                        // НЕ добавляем filter--open, так как фильтр уже применен и панель закрыта
1808                        updateBodyClass(false);
1809                        resetToFirstPage();
1810                        // Скролл к блоку проектов с задержкой для применения фильтров
1811                        setTimeout(() => {
1812                            scrollToProjects();
1813                        }, 350);
1814                    };
1815
1816                    const resetFilters = (event) => {
1817                        if (event) {
1818                            event.preventDefault();
1819                            event.stopPropagation();
1820                        }
1821                        const newFilters = {};
1822                        filtersData.value.forEach(filter => {
1823                            newFilters[filter.id] = [];
1824                        });
1825                        selectedFilters.value = newFilters;
1826                        resetToFirstPage();
1827                        // Если мобильная панель открыта, применяем и закрываем
1828                        if (isMobileFilterOpen.value) {
1829                            applyMobileFilters();
1830                        } else {
1831                            // Если панель закрыта, просто скроллим к проектам
1832                            setTimeout(() => {
1833                                scrollToProjects();
1834                            }, 200);
1835                        }
1836                    };
1837
1838                    const applyFilters = () => {
1839                        isFilterActive.value = false;
1840                        updateBodyClass(false);
1841                    };
1842
1843                    const playVideo = (event, videoSrc) => {
1844                        const card = event.currentTarget;
1845                        const video = card.querySelector('.AppProjectsPreviewCard__preview-video');
1846                        if (video && videoSrc) video.play();
1847                    };
1848
1849                    const pauseVideo = (event) => {
1850                        const card = event.currentTarget;
1851                        const video = card.querySelector('.AppProjectsPreviewCard__preview-video');
1852                        if (video) {
1853                            video.pause();
1854                            video.currentTime = 0;
1855                        }
1856                    };
1857
1858                    const loadProjects = async () => {
1859                        try {
1860                            const res = await api.action('projects:getProjects', {});
1861
1862                            if (res.status === 'ok' && res.data) {
1863                                if (res.data.filter && Array.isArray(res.data.filter)) {
1864                                    filtersData.value = res.data.filter;
1865
1866                                    const initialFilters = {};
1867                                    filtersData.value.forEach(filter => {
1868                                        initialFilters[filter.id] = [];
1869                                    });
1870                                    selectedFilters.value = initialFilters;
1871                                }
1872
1873                                if (res.data.projects && Array.isArray(res.data.projects)) {
1874                                    projects.value = res.data.projects;
1875                                    projectsData.value = res.data.projects;
1876                                }
1877                            }
1878                        } catch (error) {
1879                            console.error('Ошибка загрузки проектов:', error);
1880                        } finally {
1881                            setTimeout(() => {
1882                                isLoading.value = false;
1883                            }, 100);
1884                        }
1885                    };
1886
1887                    const handleClickOutside = (event) => {
1888                        if (!event.target.closest('.MainPagePortfolio__button-container') && isFilterActive.value) {
1889                            isFilterActive.value = false;
1890                            activeFilter.value = 'all';
1891                            updateBodyClass(false);
1892                        }
1893                    };
1894
1895                    onMounted(async () => {
1896                        await loadProjects();
1897                        document.addEventListener('click', handleClickOutside);
1898                        setTimeout(() => updateHighlightBar(), 100);
1899                    });
1900
1901                    onUnmounted(() => {
1902                        document.removeEventListener('click', handleClickOutside);
1903                        updateBodyClass(false);
1904                        document.body.classList.remove('mobile-filter--open');
1905                    });
1906
1907                    return {
1908                        filtersData,
1909                        activeFilter,
1910                        isFilterActive,
1911                        isMobileFilterOpen,
1912                        selectedFilters,
1913                        projects,
1914                        projectsContainer,
1915                        filteredProjects,
1916                        paginatedProjects,
1917                        totalPages,
1918                        displayedPages,
1919                        highlightBarStyle,
1920                        currentPage,
1921                        isPaginationLoading,
1922                        setActiveFilter,
1923                        toggleFilter,
1924                        toggleMobileFilter,
1925                        closeMobileFilter,
1926                        resetFilters,
1927                        applyFilters,
1928                        applyMobileFilters,
1929                        playVideo,
1930                        pauseVideo,
1931                        goToPage,
1932                        isLoading,
1933                        getAvailableFilterOptions,
1934                        getOptionCount,
1935                        scrollToProjects
1936                    };
1937                }
1938            });
1939
1940            ProjectsListWidgetApp.mount(mountElement);
1941            mountElement.__vue_app__ = ProjectsListWidgetApp;
1942        }
1943    })();
1944
1945
1946// Javascript 
1947// Добавляем SVG круг для кнопки закрытия
1948    (function () {
1949        const addBorderRing = () => {
1950            const closeBtns = document.querySelectorAll('#HeroBlock .close-btn');
1951            closeBtns.forEach(btn => {
1952                if (!btn.querySelector('.border-ring')) {
1953                    const ring = document.createElement('div');
1954                    ring.className = 'border-ring';
1955                    ring.innerHTML = `
1956                        <svg viewBox="0 0 42 42" xmlns="http://www.w3.org/2000/svg">
1957                            <circle cx="21" cy="21" r="19.5" />
1958                        </svg>
1959                    `;
1960                    btn.appendChild(ring);
1961                }
1962            });
1963        };
1964
1965        // Наблюдатель за появлением кнопок
1966        const observer = new MutationObserver(() => {
1967            addBorderRing();
1968        });
1969
1970        observer.observe(document.body, {childList: true, subtree: true});
1971
1972        if (document.readyState === 'loading') {
1973            document.addEventListener('DOMContentLoaded', addBorderRing);
1974        } else {
1975            addBorderRing();
1976        }
1977    })();
1978
1979
1980// Javascript 
1981(function () {
1982        const {createApp, ref, computed, onMounted, onUnmounted, nextTick, watch} = Vue;
1983
1984        const mountElement = document.getElementById('HeroBlock');
1985        if (mountElement && !mountElement.__vue_app__) {
1986            const heroBlockApp = createApp({
1987                setup() {
1988                    const animatedItems = [
1989                        {
1990                            word: "кодим",
1991                            image: "/Modules/Ui/Layouts/PublicLayout/Static/img/words/кодим.png",
1992                            video: "https://storage.yandexcloud.net/int-samedia/Modules/Storage/Storage/Files/Wc2beebb0/D3054b574/F6a50f7f859f5aSd3618848.webm",
1993                            isVideo: true,
1994                            alt: "Разработка сайтов и приложений",
1995                            fullDescription: "Пишем чистый код, создаём мощные веб-сервисы и мобильные приложения, которые решают реальные задачи бизнеса.",
1996                            buttonText: "Заказать разработку",
1997                            buttonLink: "#callBack",
1998                            buttonClass: "button--development",
1999                            buttonIcon: '<i class="bi bi-code-square"></i>'
2000                        },
2001                        {
2002                            word: "дизайним",
2003                            image: "/Modules/Ui/Layouts/PublicLayout/Static/img/words/дизайним.png",
2004                            video: "https://storage.yandexcloud.net/int-samedia/Modules/Storage/Storage/Files/Wc2beebb0/D3054b574/F6a50f89bdfef7S09ec8c76.webm",
2005                            isVideo: true,
2006                            alt: "Продающий вкусный дизайн",
2007                            fullDescription: "Создаём продающий вкусный дизайн, который привлекает внимание, вызывает эмоции и конвертирует посетителей в клиентов.",
2008                            buttonText: "Заказать дизайн",
2009                            buttonLink: "#callBack",
2010                            buttonClass: "button--design",
2011                            buttonIcon: '<i class="bi bi-brush"></i>'
2012                        },
2013                        {
2014                            word: "консультируем",
2015                            image: "/Modules/Ui/Layouts/PublicLayout/Static/img/words/консультируем.png",
2016                            video: "https://storage.yandexcloud.net/int-samedia/Modules/Storage/Storage/Files/Wc2beebb0/D3054b574/F6a50b7d404a98Sb6e4a92d.webm",
2017                            isVideo: true,
2018                            alt: "Консалтинг продаж",
2019                            fullDescription: "Проводим глубокий аудит вашего бизнеса, разрабатываем стратегию продаж и помогаем масштабироваться.",
2020                            buttonText: "Получить консультацию",
2021                            buttonLink: "#callBack",
2022                            buttonClass: "button--consulting",
2023                            buttonIcon: '<i class="bi bi-person-badge"></i>'
2024                        },
2025                        {
2026                            word: "запускаем",
2027                            image: "/Modules/Ui/Layouts/PublicLayout/Static/img/words/запускаем.png",
2028                            video: "https://storage.yandexcloud.net/int-samedia/Modules/Storage/Storage/Files/Wc2beebb0/D3054b574/F6a5a02e8692e2S93672068.webm",
2029                            isVideo: true,
2030                            alt: "Запуск веб-сервисов",
2031                            fullDescription: "Выводим на рынок сложные digital-сервисы, SaaS-платформы и цифровые экосистемы полного цикла.",
2032                            buttonText: "Запустить проект",
2033                            buttonLink: "#callBack",
2034                            buttonClass: "button--launch",
2035                            buttonIcon: '<i class="bi bi-rocket-takeoff"></i>'
2036                        },
2037                        {
2038                            word: "продвигаем",
2039                            image: "/Modules/Ui/Layouts/PublicLayout/Static/img/words/продвигаем.png",
2040                            video: "https://storage.yandexcloud.net/int-samedia/Modules/Storage/Storage/Files/Wc2beebb0/D3054b574/F6a5a03e4761b8Sc635873a.webm",
2041                            isVideo: true,
2042                            alt: "Умное SEO продвижение",
2043                            fullDescription: "Выводим сайты в ТОП с помощью новейшего умного SEO, привлекаем целевую аудиторию и увеличиваем продажи.",
2044                            buttonText: "Заказать продвижение",
2045                            buttonLink: "#callBack",
2046                            buttonClass: "button--seo",
2047                            buttonIcon: '<i class="bi bi-graph-up"></i>'
2048                        },
2049                        {
2050                            word: "внедряем",
2051                            image: "/Modules/Ui/Layouts/PublicLayout/Static/img/words/внедряем.png",
2052                            video: "https://storage.yandexcloud.net/int-samedia/Modules/Storage/Storage/Files/Wc2beebb0/D3054b574/F6a5a04292a799Scc77d93b.webm",
2053                            isVideo: true,
2054                            alt: "AI внедрения",
2055                            fullDescription: "Интегрируем искусственный интеллект в ваш бизнес: чат-боты, нейросети, прогнозную аналитику и автоматизацию.",
2056                            buttonText: "Внедрить AI",
2057                            buttonLink: "#callBack",
2058                            buttonClass: "button--ai",
2059                            buttonIcon: '<i class="bi bi-cpu"></i>'
2060                        },
2061                        {
2062                            word: "автоматизируем",
2063                            image: "/Modules/Ui/Layouts/PublicLayout/Static/img/words/автоматизируем.png",
2064                            video: "https://storage.yandexcloud.net/int-samedia/Modules/Storage/Storage/Files/Wc2beebb0/D3054b574/F6a5a03bc0a4e0Sb7b96849.webm",
2065                            isVideo: true,
2066                            alt: "Автоматизация бизнеса",
2067                            fullDescription: "Оптимизируем рутинные процессы, внедряем CRM, ERP и RPA-решения, экономим до 70% вашего времени.",
2068                            buttonText: "Автоматизировать",
2069                            buttonLink: "#callBack",
2070                            buttonClass: "button--automation",
2071                            buttonIcon: '<i class="bi bi-gear-wide-connected"></i>'
2072                        },
2073                        {
2074                            word: "поддерживаем",
2075                            image: "/Modules/Ui/Layouts/PublicLayout/Static/img/words/поддерживаем.png",
2076                            video: "https://storage.yandexcloud.net/int-samedia/Modules/Storage/Storage/Files/Wc2beebb0/D3054b574/F6a5a04033ff68Scfd29cee.webm",
2077                            isVideo: true,
2078                            alt: "Техническая поддержка",
2079                            fullDescription: "Обеспечиваем круглосуточную техподдержку, мониторинг и развитие ваших проектов 24/7/365.",
2080                            buttonText: "Нужна поддержка",
2081                            buttonLink: "#callBack",
2082                            buttonClass: "button--support",
2083                            buttonIcon: '<i class="bi bi-headset"></i>'
2084                        }
2085                    ];
2086
2087                    const animatedIndex = ref(0);
2088                    const currentImage = ref(animatedItems[0].image);
2089                    const currentVideo = ref(animatedItems[0].video);
2090                    const currentMediaType = ref(animatedItems[0].isVideo ? 'video' : 'image');
2091                    const currentAlt = ref(animatedItems[0].alt);
2092                    const currentFullDescription = ref(animatedItems[0].fullDescription);
2093                    const currentButtonText = ref(animatedItems[0].buttonText);
2094                    const currentButtonLink = ref(animatedItems[0].buttonLink);
2095                    const currentButtonClass = ref(animatedItems[0].buttonClass);
2096                    const currentButtonIcon = ref(animatedItems[0].buttonIcon);
2097
2098                    const displayedDescription = ref('');
2099                    const isTyping = ref(false);
2100                    let typingInterval = null;
2101                    let currentCharIndex = 0;
2102                    let pendingDescription = '';
2103
2104                    const userMessage = ref('');
2105                    const userMessageTime = computed(() => {
2106                        const now = new Date();
2107                        return now.toLocaleTimeString('ru-RU', {hour: '2-digit', minute: '2-digit'});
2108                    });
2109                    const currentTime = computed(() => {
2110                        const now = new Date();
2111                        return now.toLocaleTimeString('ru-RU', {hour: '2-digit', minute: '2-digit'});
2112                    });
2113
2114                    const messagesContainer = ref(null);
2115
2116                    // Состояния для модального окна
2117                    const isModalOpen = ref(false);
2118                    const isModalVisible = ref(false);
2119                    let modalTimeout = null;
2120
2121                    const scrollToBottom = () => {
2122                        nextTick(() => {
2123                            if (messagesContainer.value) {
2124                                messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
2125                            }
2126                        });
2127                    };
2128
2129                    const isHovered = ref(false);
2130                    let step = 0;
2131                    let wordTimer = null;
2132                    const total = animatedItems.length;
2133                    const period = 2 * (total - 1);
2134
2135                    const TYPING_SPEED = 30;
2136                    const TYPING_DELAY = 200;
2137
2138                    const startTyping = (text) => {
2139                        if (typingInterval) {
2140                            clearInterval(typingInterval);
2141                            typingInterval = null;
2142                        }
2143
2144                        currentCharIndex = 0;
2145                        displayedDescription.value = '';
2146                        isTyping.value = true;
2147
2148                        if (!text) return;
2149
2150                        pendingDescription = text;
2151
2152                        setTimeout(() => {
2153                            typingInterval = setInterval(() => {
2154                                if (currentCharIndex < pendingDescription.length) {
2155                                    displayedDescription.value += pendingDescription[currentCharIndex];
2156                                    currentCharIndex++;
2157                                    scrollToBottom();
2158                                } else {
2159                                    clearInterval(typingInterval);
2160                                    typingInterval = null;
2161                                    isTyping.value = false;
2162                                }
2163                            }, TYPING_SPEED);
2164                        }, TYPING_DELAY);
2165                    };
2166
2167                    const finishTyping = () => {
2168                        if (typingInterval) {
2169                            clearInterval(typingInterval);
2170                            typingInterval = null;
2171                        }
2172                        if (pendingDescription) {
2173                            displayedDescription.value = pendingDescription;
2174                        }
2175                        isTyping.value = false;
2176                    };
2177
2178                    const changeSlide = (newIndex) => {
2179                        finishTyping();
2180
2181                        currentMediaType.value = animatedItems[newIndex].isVideo ? 'video' : 'image';
2182                        currentVideo.value = animatedItems[newIndex].video;
2183                        currentImage.value = animatedItems[newIndex].image;
2184                        currentAlt.value = animatedItems[newIndex].alt;
2185                        currentFullDescription.value = animatedItems[newIndex].fullDescription;
2186                        currentButtonText.value = animatedItems[newIndex].buttonText;
2187                        currentButtonLink.value = animatedItems[newIndex].buttonLink;
2188                        currentButtonClass.value = animatedItems[newIndex].buttonClass;
2189                        currentButtonIcon.value = animatedItems[newIndex].buttonIcon;
2190
2191                        startTyping(animatedItems[newIndex].fullDescription);
2192                    };
2193
2194                    const startAnimation = () => {
2195                        if (wordTimer) clearInterval(wordTimer);
2196
2197                        wordTimer = setInterval(() => {
2198                            if (isHovered.value) return;
2199
2200                            step = (step + 1) % period;
2201                            const newIndex = step < total ? step : period - step;
2202                            animatedIndex.value = newIndex;
2203                            changeSlide(newIndex);
2204                        }, 4000);
2205                    };
2206
2207                    const onRightBlockMouseEnter = () => {
2208                        isHovered.value = true;
2209                    };
2210
2211                    const onRightBlockMouseLeave = () => {
2212                        isHovered.value = false;
2213                    };
2214
2215                    const currentHour = ref(0);
2216                    const currentMinute = ref(0);
2217                    let clockInterval = null;
2218                    const formattedHour = computed(() => String(currentHour.value).padStart(2, '0'));
2219                    const formattedMinute = computed(() => String(currentMinute.value).padStart(2, '0'));
2220
2221                    const isWorkTime = computed(() => {
2222                        const now = new Date();
2223                        const day = now.getDay();
2224                        const totalMinutes = currentHour.value * 60 + currentMinute.value;
2225                        const isWeekend = day === 0 || day === 6;
2226                        return !isWeekend && totalMinutes >= 9 * 60 && totalMinutes < 19 * 60;
2227                    });
2228
2229                    const updateClock = () => {
2230                        const now = new Date();
2231                        currentHour.value = now.getHours();
2232                        currentMinute.value = now.getMinutes();
2233                    };
2234
2235                    // Открытие модального окна
2236                    const openModal = () => {
2237                        if (modalTimeout) clearTimeout(modalTimeout);
2238                        isModalOpen.value = true;
2239
2240                        setTimeout(() => {
2241                            isModalVisible.value = true;
2242                        }, 10);
2243                    };
2244
2245                    // Закрытие модального окна
2246                    const closeModal = () => {
2247                        isModalVisible.value = false;
2248
2249                        modalTimeout = setTimeout(() => {
2250                            isModalOpen.value = false;
2251                        }, 300);
2252                    };
2253
2254                    // Добавление border-ring к кнопке закрытия
2255                    const addBorderRing = () => {
2256                        const closeBtn = document.querySelector('#HeroBlock .close-btn');
2257                        if (closeBtn && !closeBtn.querySelector('.border-ring')) {
2258                            const ring = document.createElement('div');
2259                            ring.className = 'border-ring';
2260                            ring.innerHTML = `
2261                                <svg viewBox="0 0 42 42" xmlns="http://www.w3.org/2000/svg">
2262                                    <circle cx="21" cy="21" r="19.5" />
2263                                </svg>
2264                            `;
2265                            closeBtn.appendChild(ring);
2266                        }
2267                    };
2268
2269                    // Escape для закрытия модального окна
2270                    const handleEscape = (e) => {
2271                        if (e.key === 'Escape' && isModalOpen.value) {
2272                            closeModal();
2273                        }
2274                    };
2275
2276                    watch(userMessage, () => {
2277                        nextTick(() => {
2278                            const textarea = document.querySelector('.messenger-input');
2279                            if (textarea) {
2280                                textarea.style.height = 'auto';
2281                                textarea.style.height = Math.min(textarea.scrollHeight, 80) + 'px';
2282                            }
2283                        });
2284                    });
2285
2286                    watch(displayedDescription, () => {
2287                        scrollToBottom();
2288                    });
2289
2290                    watch(userMessage, () => {
2291                        scrollToBottom();
2292                    });
2293
2294                    // Следим за появлением кнопки закрытия в DOM
2295                    watch(isModalOpen, (newVal) => {
2296                        if (newVal) {
2297                            nextTick(() => {
2298                                addBorderRing();
2299                            });
2300                        }
2301                    });
2302
2303                    onMounted(() => {
2304                        startTyping(animatedItems[0].fullDescription);
2305                        startAnimation();
2306                        updateClock();
2307                        clockInterval = setInterval(updateClock, 1000);
2308                        scrollToBottom();
2309                        window.addEventListener('keydown', handleEscape);
2310                    });
2311
2312                    onUnmounted(() => {
2313                        if (wordTimer) clearInterval(wordTimer);
2314                        if (clockInterval) clearInterval(clockInterval);
2315                        if (typingInterval) clearInterval(typingInterval);
2316                        if (modalTimeout) clearTimeout(modalTimeout);
2317                        window.removeEventListener('keydown', handleEscape);
2318                        document.body.style.overflow = '';
2319                    });
2320
2321                    return {
2322                        animatedItems,
2323                        animatedIndex,
2324                        currentImage,
2325                        currentVideo,
2326                        currentMediaType,
2327                        currentAlt,
2328                        displayedDescription,
2329                        isTyping,
2330                        currentFullDescription,
2331                        currentButtonText,
2332                        currentButtonLink,
2333                        currentButtonClass,
2334                        currentButtonIcon,
2335                        formattedHour,
2336                        formattedMinute,
2337                        isWorkTime,
2338                        userMessage,
2339                        userMessageTime,
2340                        currentTime,
2341                        messagesContainer,
2342                        onRightBlockMouseEnter,
2343                        onRightBlockMouseLeave,
2344                        isModalOpen,
2345                        isModalVisible,
2346                        openModal,
2347                        closeModal
2348                    };
2349                }
2350            });
2351
2352            heroBlockApp.mount(mountElement);
2353            mountElement.__vue_app__ = heroBlockApp;
2354        }
2355    })();
2356
2357
2358// Javascript 
2359(function () {
2360        const {createApp, ref, computed, onMounted, onBeforeUnmount, nextTick} = Vue;
2361
2362        // Проверяем, не смонтировано ли уже приложение
2363        const mountElement = document.getElementById('HeaderWidget');
2364        if (mountElement && !mountElement.__vue_app__) {
2365            const headerApp = createApp({
2366                setup() {
2367                    // Данные из PHP
2368                    const phoneNumber = ref('+7 (863) 303-35-70');
2369                    const phoneTel = ref('78633033570');
2370                    const socials = ref([{"code":"whatsapp","link":"https:\/\/wa.me\/message\/OPMVSNKUENWEC1","svg":"<svg viewBox=\"0 0 15 14\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\">\n    <path d=\"M7.64177 0.333313C11.3238 0.333313 14.3084 3.31798 14.3084 6.99998C14.3084 10.682 11.3238 13.6666 7.64177 13.6666C6.46363 13.6685 5.30621 13.3568 4.28844 12.7633L0.977774 13.6666L1.87911 10.3546C1.28517 9.33656 0.973139 8.17865 0.975107 6.99998C0.975107 3.31798 3.95977 0.333313 7.64177 0.333313ZM5.36977 3.86665L5.23644 3.87198C5.15012 3.87724 5.06576 3.89991 4.98844 3.93865C4.91612 3.9796 4.8501 4.0308 4.79244 4.09065C4.71244 4.16598 4.66711 4.23131 4.61844 4.29465C4.37185 4.61525 4.23909 5.00886 4.24111 5.41331C4.24244 5.73998 4.32777 6.05798 4.46111 6.35531C4.73377 6.95665 5.18244 7.59331 5.77444 8.18331C5.91711 8.32531 6.05711 8.46798 6.20777 8.60065C6.94337 9.24829 7.81994 9.71534 8.76777 9.96465L9.14644 10.0226C9.26977 10.0293 9.39311 10.02 9.51711 10.014C9.71127 10.004 9.90085 9.95138 10.0724 9.85998C10.1597 9.81501 10.2449 9.76607 10.3278 9.71331C10.3278 9.71331 10.3564 9.69465 10.4111 9.65331C10.5011 9.58665 10.5564 9.53931 10.6311 9.46131C10.6864 9.40398 10.7344 9.33665 10.7711 9.25998C10.8231 9.15131 10.8751 8.94398 10.8964 8.77131C10.9124 8.63931 10.9078 8.56731 10.9058 8.52265C10.9031 8.45131 10.8438 8.37731 10.7791 8.34598L10.3911 8.17198C10.3911 8.17198 9.81111 7.91931 9.45644 7.75798C9.41933 7.74178 9.37956 7.73251 9.33911 7.73065C9.29349 7.72596 9.24741 7.73109 9.20394 7.7457C9.16048 7.7603 9.12064 7.78404 9.08711 7.81531C9.08377 7.81398 9.03911 7.85198 8.55711 8.43598C8.52944 8.47315 8.49133 8.50125 8.44764 8.51668C8.40395 8.53212 8.35665 8.53419 8.31177 8.52265C8.26834 8.511 8.22579 8.4963 8.18444 8.47865C8.10177 8.44398 8.07311 8.43065 8.01644 8.40665C7.63382 8.23968 7.27957 8.01408 6.96644 7.73798C6.88244 7.66465 6.80444 7.58465 6.72444 7.50731C6.46216 7.25613 6.23359 6.97198 6.04444 6.66198L6.00511 6.59865C5.97686 6.55609 5.95401 6.51018 5.93711 6.46198C5.91177 6.36398 5.97777 6.28531 5.97777 6.28531C5.97777 6.28531 6.13977 6.10
2370798 6.21511 6.01198C6.28844 5.91865 6.35044 5.82798 6.39044 5.76331C6.46911 5.63665 6.49377 5.50665 6.45244 5.40598C6.26577 4.94998 6.07244 4.49598 5.87377 4.04531C5.83444 3.95598 5.71777 3.89198 5.61177 3.87931C5.57577 3.87531 5.53977 3.87131 5.50377 3.86865C5.41424 3.8642 5.32453 3.86509 5.23511 3.87131L5.36977 3.86665Z\"><\/path>\n<\/svg>"},{"code":"max","link":"https:\/\/max.ru\/u\/f9LHodD0cOJbbUyoWUAHxcElBilGJwTc1LqVkjR8Y-euEY3BmJla3S14h40","svg":"<svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\">\n    <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M7.15041 13.96C5.77649 13.96 5.138 13.7586 4.02816 12.9528C3.32616 13.8593 1.10315 14.5677 1.00621 13.3557C1.00621 12.4459 0.805632 11.677 0.578317 10.8377C0.307544 9.80359 0 8.65201 0 6.98338C0 2.99815 3.25596 0 7.11364 0C10.9747 0 14 3.14588 14 7.02031C14.0129 10.8348 10.9484 13.9397 7.15041 13.96ZM7.20724 3.44469C5.32854 3.34732 3.86436 4.65335 3.5401 6.70136C3.27267 8.39685 3.74736 10.4616 4.15185 10.5691C4.34574 10.6161 4.8338 10.2199 5.138 9.91439C5.64101 10.2634 6.22676 10.473 6.83618 10.5221C8.78281 10.6161 10.4461 9.1277 10.5769 7.17475C10.6529 5.21767 9.15415 3.56004 7.20724 3.44805L7.20724 3.44469Z\" fill=\"white\"><\/path>\n<\/svg>"},{"code":"telegram","link":"http:\/\/telegram.me\/samedia_group","svg":"<svg viewBox=\"0 0 17 15\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\">\n    <path d=\"M1.29299 5.84567L13.7087 1.05859C14.285 0.850408 14.7882 1.19916 14.6015 2.07052L14.6026 2.06944L12.4886 12.0288C12.3319 12.7349 11.9124 12.9066 11.3254 12.574L8.10608 10.2014L6.55331 11.6973C6.38161 11.869 6.23675 12.0138 5.90409 12.0138L6.13266 8.73766L12.0991 3.3475C12.3588 3.11893 12.0411 2.99016 11.6988 3.21766L4.32556 7.85987L1.14705 6.86833C0.457049 6.64941 0.442025 6.17833 1.29299 5.84567Z\"><\/path>\n<\/svg>"}]);
2371                    const allMenuItems = ref([{"url":"\/","title":"\u0413\u043b\u0430\u0432\u043d\u0430\u044f","showInMainMenu":false,"showInModalMenu":true},{"url":"\/services","title":"\u0423\u0441\u043b\u0443\u0433\u0438","showInMainMenu":true,"showInModalMenu":true,"hasSubmenu":true},{"url":"\/projects","title":"\u041f\u0440\u043e\u0435\u043a\u0442\u044b","showInMainMenu":true,"showInModalMenu":true},{"url":"\/about","title":"\u041e \u043d\u0430\u0441","showInMainMenu":true,"showInModalMenu":true},{"url":"\/reviews","title":"\u041e\u0442\u0437\u044b\u0432\u044b","showInMainMenu":false,"showInModalMenu":true},{"url":"\/contacts","title":"\u041a\u043e\u043d\u0442\u0430\u043a\u0442\u044b","showInMainMenu":true,"showInModalMenu":true}]);
2372                    const cities = ref([{"id":1,"name":"\u041c\u043e\u0441\u043a\u0432\u0430","active":false,"image":"https:\/\/storage.yandexcloud.net\/int-samedia\/Modules\/Storage\/Storage\/Files\/Wc2beebb0\/D3054b574\/F6a4b8cb9cd687S21d4b36a.webp"},{"id":3,"name":"\u041a\u0440\u0430\u0441\u043d\u043e\u0434\u0430\u0440","active":false,"image":"https:\/\/storage.yandexcloud.net\/int-samedia\/Modules\/Storage\/Storage\/Files\/Wc2beebb0\/D3054b574\/F6a4b8cc1c1755Sdf6aed10.webp"},{"id":4,"name":"\u0420\u043e\u0441\u0442\u043e\u0432-\u043d\u0430-\u0414\u043e\u043d\u0443","active":true,"image":"https:\/\/storage.yandexcloud.net\/int-samedia\/Modules\/Storage\/Storage\/Files\/Wc2beebb0\/D3054b574\/F6a4b8ccc0ba7aSc40d899d.webp"}]);
2373
2374                    // Разделяем меню
2375                    // Для десктопного меню - только пункты с showInMainMenu = true (первые 4)
2376                    const desktopMenuItems = computed(() => allMenuItems.value.filter(item => item.showInMainMenu === true).slice(0, 4));
2377
2378                    // Для модального меню - только пункты с showInModalMenu = true
2379                    const modalMenuItems = computed(() => allMenuItems.value.filter(item => item.showInModalMenu === true));
2380
2381                    // Refs для DOM элементов
2382                    const navMenu = ref(null);
2383                    const scrollListElement = ref(null);
2384                    const scrollListElementInner = ref(null);
2385                    const scrollElement = ref(null);
2386
2387                    // Состояния UI
2388                    const isCityDropdownOpen = ref(false);
2389                    const isMobileMenuOpen = ref(false);
2390                    const isActiveInnerMenu = ref(false);
2391                    const activeIndex = ref(0);
2392                    const activeIndexPreview = ref(0);
2393                    const scrollHeight = ref(0);
2394
2395                    // --- Состояния для скрытия хедера при скролле ---
2396                    const isHeaderVisible = ref(true);
2397                    const lastScrollY = ref(0);
2398                    const isDesktop = ref(window.innerWidth > 768);
2399
2400                    // Активный город
2401                    const activeCity = ref(cities.value.find(city => city.active) || cities.value[0]);
2402                    const activeCityName = computed(() => activeCity.value.name);
2403
2404                    // Highlight bar для меню
2405                    const activeMenuItemIndex = ref(-1);
2406                    const hoveredMenuItemIndex = ref(-1);
2407                    const highlightBarStyle = ref({});
2408
2409                    let resizeObserver = null;
2410                    let updateHighlightTimeout = null;
2411                    let scrollTimeout = null;
2412
2413
2414                    // Методы для городов
2415                    const toggleCityDropdown = () => {
2416                        isCityDropdownOpen.value = !isCityDropdownOpen.value;
2417                    };
2418
2419                    const closeCityDropdown = () => {
2420                        isCityDropdownOpen.value = false;
2421                    };
2422
2423                    const selectCity = (city) => {
2424                        api.action('city:set', {'id': city.id});
2425                    };
2426
2427                    // Методы для меню и подсветки
2428                    const isActiveRoute = (url) => {
2429                        if (url === '/') {
2430                            return window.location.pathname === url;
2431                        }
2432                        return window.location.pathname.startsWith(url);
2433                    };
2434
2435                    const updateHighlightBar = (index) => {
2436                        if (!navMenu.value) return;
2437                        const items = navMenu.value.querySelectorAll('li');
2438                        if (!items.length || index < 0 || index >= items.length) return;
2439                        const targetItem = items[index];
2440                        const link = targetItem.querySelector('a');
2441                        if (!link) return;
2442                        const linkRect = link.getBoundingClientRect();
2443                        const navRect = navMenu.value.getBoundingClientRect();
2444                        highlightBarStyle.value = {
2445                            width: linkRect.width + 'px',
2446                            transform: 'translateX(' + (linkRect.left - navRect.left) + 'px)',
2447                            opacity: '1'
2448                        };
2449                    };
2450
2451                    const handleMenuItemHover = (index) => {
2452                        hoveredMenuItemIndex.value = index;
2453                        updateHighlightBar(index);
2454                    };
2455
2456                    const handleMenuItemLeave = () => {
2457                        hoveredMenuItemIndex.value = -1;
2458                        if (activeMenuItemIndex.value !== -1) {
2459                            updateHighlightBar(activeMenuItemIndex.value);
2460                        } else {
2461                            highlightBarStyle.value = {opacity: '0'};
2462                        }
2463                    };
2464
2465                    const handleMenuItemClick = (index) => {
2466                        activeMenuItemIndex.value = index;
2467                        updateHighlightBar(index);
2468                    };
2469
2470                    const setActiveMenuItemFromUrl = () => {
2471                        const currentPath = window.location.pathname;
2472                        const index = desktopMenuItems.value.findIndex(item => {
2473                            if (item.url === '/') {
2474                                return currentPath === item.url;
2475                            }
2476                            return currentPath.startsWith(item.url);
2477                        });
2478                        if (index !== -1) {
2479                            activeMenuItemIndex.value = index;
2480                            nextTick(() => updateHighlightBar(index));
2481                        } else {
2482                            activeMenuItemIndex.value = -1;
2483                        }
2484                    };
2485
2486                    // Методы для мобильного меню
2487                    const toggleMobileMenu = () => {
2488                        isMobileMenuOpen.value = !isMobileMenuOpen.value;
2489                        if (isMobileMenuOpen.value) {
2490                            setDefaultScrollHeight();
2491                        } else {
2492                            isActiveInnerMenu.value = false;
2493                        }
2494                    };
2495
2496                    const closeMobileMenu = () => {
2497                        isMobileMenuOpen.value = false;
2498                        isActiveInnerMenu.value = false;
2499                    };
2500
2501                    const setActiveIndex = (index) => {
2502                        setDefaultScrollHeight();
2503                        activeIndex.value = index;
2504                        isActiveInnerMenu.value = true;
2505                    };
2506
2507                    const setActiveIndexPreview = (index) => {
2508                        activeIndexPreview.value = index;
2509                    };
2510
2511                    const setDefaultScrollHeight = () => {
2512                        if (scrollListElementInner.value) {
2513                            scrollListElementInner.value.scrollIntoView({
2514                                behavior: "auto",
2515                                block: "start",
2516                            });
2517                        }
2518                        scrollHeight.value = 0;
2519                    };
2520
2521                    const closeInnerMenu = () => {
2522                        setDefaultScrollHeight();
2523                        isActiveInnerMenu.value = false;
2524                    };
2525
2526                    const updateScrollHeight = () => {
2527                        if (scrollListElement.value && scrollElement.value) {
2528                            const listHeight = scrollListElement.value.scrollHeight;
2529                            const scrollTop = scrollListElement.value.scrollTop;
2530                            const windowHeight = scrollListElement.value.clientHeight;
2531                            const scrollPercentage = scrollTop / (listHeight - windowHeight);
2532                            scrollHeight.value = scrollPercentage * scrollElement.value.offsetHeight;
2533                        }
2534                    };
2535
2536                    // --- Функция обработки скролла для скрытия хедера ---
2537                    const handleScroll = () => {
2538                        if (!isDesktop.value) return;
2539
2540                        // Тормозим частоту обновления для производительности
2541                        if (scrollTimeout) return;
2542                        scrollTimeout = setTimeout(() => {
2543                            const currentScrollY = window.scrollY;
2544                            const deltaY = currentScrollY - lastScrollY.value;
2545
2546                            // Скролл вниз и не в самом верху -> скрываем
2547                            if (deltaY > 0 && currentScrollY > 50) {
2548                                if (isHeaderVisible.value) {
2549                                    isHeaderVisible.value = false;
2550                                }
2551                            }
2552                            // Скролл вверх -> показываем
2553                            else if (deltaY < 0) {
2554                                if (!isHeaderVisible.value) {
2555                                    isHeaderVisible.value = true;
2556                                }
2557                            }
2558
2559                            lastScrollY.value = currentScrollY;
2560                            scrollTimeout = null;
2561                        }, 10);
2562                    };
2563
2564                    // --- Обновление состояния isDesktop при ресайзе ---
2565                    const handleResizeForHeader = () => {
2566                        const wasDesktop = isDesktop.value;
2567                        isDesktop.value = window.innerWidth > 768;
2568
2569                        // Если перешли с десктопа на мобильный - показываем хедер
2570                        if (wasDesktop && !isDesktop.value) {
2571                            isHeaderVisible.value = true;
2572                        }
2573                    };
2574
2575                    // Обработчик ресайза для меню
2576                    const handleResize = () => {
2577                        if (updateHighlightTimeout) clearTimeout(updateHighlightTimeout);
2578                        updateHighlightTimeout = setTimeout(() => {
2579                            if (hoveredMenuItemIndex.value !== -1) {
2580                                updateHighlightBar(hoveredMenuItemIndex.value);
2581                            } else if (activeMenuItemIndex.value !== -1) {
2582                                updateHighlightBar(activeMenuItemIndex.value);
2583                            }
2584                        }, 100);
2585
2586                        if (window.innerWidth > 768 && isMobileMenuOpen.value) {
2587                            closeMobileMenu();
2588                        }
2589                    };
2590
2591
2592                    // Escape для закрытия меню
2593                    const handleEscape = (e) => {
2594                        if (e.key === 'Escape' && isMobileMenuOpen.value) {
2595                            closeMobileMenu();
2596                        }
2597                    };
2598
2599                    // Lifecycle hooks
2600                    onMounted(() => {
2601                        nextTick(() => {
2602                            setActiveMenuItemFromUrl();
2603
2604                            window.addEventListener('resize', handleResize);
2605                            window.addEventListener('resize', handleResizeForHeader);
2606                            window.addEventListener('keydown', handleEscape);
2607                            window.addEventListener('scroll', handleScroll);
2608
2609                            if (navMenu.value) {
2610                                resizeObserver = new ResizeObserver(() => {
2611                                    if (hoveredMenuItemIndex.value !== -1) {
2612                                        updateHighlightBar(hoveredMenuItemIndex.value);
2613                                    } else if (activeMenuItemIndex.value !== -1) {
2614                                        updateHighlightBar(activeMenuItemIndex.value);
2615                                    }
2616                                });
2617                                resizeObserver.observe(navMenu.value);
2618                            }
2619
2620                            if (scrollListElement.value) {
2621                                scrollListElement.value.addEventListener('scroll', updateScrollHeight);
2622                            }
2623                        });
2624                    });
2625
2626                    onBeforeUnmount(() => {
2627                        window.removeEventListener('resize', handleResize);
2628                        window.removeEventListener('resize', handleResizeForHeader);
2629                        window.removeEventListener('keydown', handleEscape);
2630                        window.removeEventListener('scroll', handleScroll);
2631                        if (resizeObserver) {
2632                            resizeObserver.disconnect();
2633                        }
2634                        if (updateHighlightTimeout) {
2635                            clearTimeout(updateHighlightTimeout);
2636                        }
2637                        if (scrollTimeout) {
2638                            clearTimeout(scrollTimeout);
2639                        }
2640                        if (scrollListElement.value) {
2641                            scrollListElement.value.removeEventListener('scroll', updateScrollHeight);
2642                        }
2643                        document.body.style.overflow = '';
2644                    });
2645
2646                    return {
2647                        phoneNumber,
2648                        phoneTel,
2649                        socials,
2650                        desktopMenuItems,
2651                        modalMenuItems,
2652                        cities,
2653                        activeCityName,
2654                        navMenu,
2655                        scrollListElement,
2656                        scrollListElementInner,
2657                        scrollElement,
2658                        isCityDropdownOpen,
2659                        isMobileMenuOpen,
2660                        isActiveInnerMenu,
2661                        scrollHeight,
2662                        highlightBarStyle,
2663                        isHeaderVisible,
2664                        isDesktop,
2665                        toggleCityDropdown,
2666                        closeCityDropdown,
2667                        selectCity,
2668                        isActiveRoute,
2669                        handleMenuItemHover,
2670                        handleMenuItemLeave,
2671                        handleMenuItemClick,
2672                        toggleMobileMenu,
2673                        closeMobileMenu,
2674                        setActiveIndex,
2675                        setActiveIndexPreview,
2676                        closeInnerMenu
2677                    };
2678                }
2679            });
2680
2681            // Добавляем директиву v-click-outside
2682            headerApp.directive('click', {
2683                beforeMount(el, binding) {
2684                    if (binding.arg === 'outside') {
2685                        el.clickOutsideEvent = (event) => {
2686                            if (!(el === event.target || el.contains(event.target))) {
2687                                binding.value();
2688                            }
2689                        };
2690                        document.addEventListener('click', el.clickOutsideEvent);
2691                    }
2692                },
2693                unmounted(el) {
2694                    if (el.clickOutsideEvent) {
2695                        document.removeEventListener('click', el.clickOutsideEvent);
2696                    }
2697                }
2698            });
2699
2700            const headerAppInstance = headerApp.mount(mountElement);
2701            mountElement.__vue_app__ = headerAppInstance;
2702        }
2703    })();
2704
2705
2706 }
2707
2708JSApp();

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.