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.