PageSourceSearch

https://venbest.ua/wp-content/themes/venbest/new-homepage/js/home-v2.js?ver=1.2.6

js venbest.ua collected 2026-09-24 12:30:25 UTC 14,115 bytes, 325 lines download raw bytes

1/**
2 * Venbest Homepage v2 — інтерактив нових секцій.
3 * RouteMap-анімація (ГЕОЗОНА), карта України з офісами, hero fade-in.
4 * Портовано з React-лендінгу (RouteMap.tsx, UkraineMap.tsx, Index.tsx Hero).
5 * Логіка ETA-попапа лишається в main.js (initAddressCheck / fetchResponseTime).
6 */
7(function () {
8  'use strict';
9
10  var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
11
12  /* ============ ROUTEMAP (анімація маршруту екіпажу) ============ */
13  var ROUTE_DURATION = 2500;
14  var ROUTE_START_DELAY = 400;
15  var ARRIVAL_SECONDS = 6 * 60;
16
17  function easeInOutCubic(t) {
18    return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
19  }
20
21  function formatTime(seconds) {
22    var m = Math.floor(seconds / 60);
23    var s = Math.floor(seconds % 60);
24    return m + ':' + String(s).padStart(2, '0');
25  }
26
27  function initRouteMap(root) {
28    var track = root.querySelector('.vb-routemap__track');
29    var progress = root.querySelector('.vb-routemap__progress');
30    var marker = root.querySelector('.vb-routemap__marker');
31    var area = root.querySelector('.vb-routemap__area');
32    var timeEl = root.querySelector('[data-vb-route-time]');
33    var statusEl = root.querySelector('[data-vb-route-status]');
34    if (!track || !progress) return null;
35
36    var len = track.getTotalLength();
37    var rafId = null;
38    var timeoutId = null;
39    var started = false;
40
41    progress.style.strokeDasharray = String(len);
42    progress.style.strokeDashoffset = String(len);
43
44    function setStatus(status) {
45      root.setAttribute('data-status', status);
46      if (statusEl) {
47        statusEl.textContent =
48          status === 'arrived' ? (root.getAttribute('data-label-arrived') || 'Екіпаж прибув') :
49          status === 'driving' ? (root.getAttribute('data-label-driving') || 'Екіпаж в дорозі') : (root.getAttribute('data-label-idle') || 'Очікує тривоги');
50      }
51    }
52
53    function setProgress(p) {
54      progress.style.strokeDashoffset = String(len * (1 - p));
55      if (marker) {
56        var pt = track.getPointAtLength(len * p);
57        var ahead = track.getPointAtLength(Math.min(len * p + 2, len));
58        var angle = (Math.atan2(ahead.y - pt.y, ahead.x - pt.x) * 180) / Math.PI + 90;
59        marker.setAttribute('transform', 'translate(' + pt.x + ' ' + pt.y + ') rotate(' + angle + ')');
60        marker.style.opacity = p > 0 && p < 1 ? '1' : '0';
61      }
62      if (timeEl) timeEl.textContent = formatTime(p * ARRIVAL_SECONDS);
63    }
64
65    function play() {
66      if (rafId) cancelAnimationFrame(rafId);
67      if (timeoutId) clearTimeout(timeoutId);
68      if (area) area.style.opacity = '0.12';
69      if (reducedMotion) {
70        setProgress(1);
71        setStatus('arrived');
72        return;
73      }
74      setProgress(0);
75      setStatus('driving');
76      timeoutId = setTimeout(function () {
77        var t0 = performance.now();
78        function tick(now) {
79          var p = easeInOutCubic(Math.min((now - t0) / ROUTE_DURATION, 1));
80          setProgress(p);
81          if (p < 1) {
82            rafId = requestAnimationFrame(tick);
83          } else {
84            setStatus('arrived');
85          }
86        }
87        rafId = requestAnimationFrame(tick);
88      }, ROUTE_START_DELAY);
89    }
90
91    setProgress(0);
92    setStatus('idle');
93
94    // Перший запуск — секція у viewport (один раз)
95    if ('IntersectionObserver' in window) {
96      var observer = new IntersectionObserver(function (entries) {
97        if (entries.some(function (e) { return e.isIntersecting; }) && !started) {
98          started = true;
99          play();
100          observer.disconnect();
101        }
102      }, { threshold: 0.35 });
103      observer.observe(root);
104    } else if (!started) {
105      started = true;
106      play();
107    }
108
109    return {
110      replay: function (address) {
111        started = true;
112        if (address) {
113          var dest = root.querySelector('[data-vb-route-dest]');
114          if (dest) dest.textContent = address;
115        }
116        play();
117      },
118    };
119  }
120
121  var routeMaps = [];
122  document.querySelectorAll('[data-vb-routemap]').forEach(function (el) {
123    var api = initRouteMap(el);
124    if (api) routeMaps.push(api);
125  });
126
127  // Реплей при сабміті форми перевірки адреси (форма з блока address-check)
128  document.querySelectorAll('.address-check__form').forEach(function (form) {
129    form.addEventListener('submit', function () {
130      var input = form.querySelector('.address-check__input');
131      var address = input && input.value ? input.value.trim() : '';
132      routeMaps.forEach(function (rm) { rm.replay(address); });
133    });
134  });
135
136  /* ============ КАРТА УКРАЇНИ + ПАНЕЛЬ ОФІСІВ ============ */
137  function initCoverageMap(section) {
138    var dataEl = section.querySelector('[data-vb-map-data]');
139    var panel = section.querySelector('[data-vb-map-panel]');
140    var svg = section.querySelector('.vb-map__svg');
141    if (!dataEl || !panel || !svg) return;
142
143    var data;
144    try {
145      data = JSON.parse(dataEl.textContent);
146    } catch (e) {
147      return;
148    }
149    var names = data.names || {};
150    var offices = data.offices || {};
151    var activeCity = section.getAttribute('data-default-city') || 'kiyv';
152    var officeIdx = 0;
153
154    function esc(s) {
155      var d = document.createElement('div');
156      d.textContent = s == null ? '' : String(s);
157      return d.innerHTML;
158    }
159
160    function renderPanel() {
161      var cityOffices = offices[activeCity] || [];
162      var office = cityOffices[Math.min(officeIdx, Math.max(0, cityOffices.length - 1))];
163      var h = '';
164      h += '<div class="vb-map-panel__head"><span class="vb-map-panel__dot" aria-hidden="true"></span>' +
165           '<h3 class="vb-map-panel__city">' + esc(names[activeCity] || '') + '</h3></div>';
166      if (office) {
167        h += '<div class="vb-map-panel__body">';
168        h += '<div class="vb-map-panel__title">' + esc(office.title) + '</div>';
169        if (office.address) {
170          h += '<div class="vb-map-panel__row vb-map-panel__row--address">' + PIN_ICON + '<span>' + esc(office.address) + '</span></div>';
171        }
172        (office.phones || []).forEach(function (p) {
173          h += '<a class="vb-map-panel__phone" href="tel:' + esc(String(p).replace(/[^\d+]/g, '')) + '">' + PHONE_ICON + esc(p) + '</a>';
174        });
175        (office.schedule || []).forEach(function (s) {
176          h += '<div class="vb-map-panel__row vb-map-panel__row--schedule">' + CLOCK_ICON + '<span>' + esc(s) + '</span></div>';
177        });
178        h += '</div>';
179      } else {
180        h += '<p class="vb-map-panel__empty">Офіс у цьому місті тимчасово не приймає відвідувачів. Телефонуйте: <a href="tel:0800306300">0800 306 300</a></p>';
181      }
182      if (cityOffices.length > 1) {
183        h += '<div class="vb-map-panel__nav">';
184        h += '<div class="vb-map-panel__dots">';
185        cityOffices.forEach(function (_, i) {
186          h += '<button type="button" class="vb-map-panel__dot-btn' + (i === officeIdx ? ' is-active' : '') + '" data-office-idx="' + i + '" aria-label="Офіс ' + (i + 1) + '"></button>';
187        });
188        h += '</div>';
189        h += '<div class="vb-map-panel__arrows">';
190        h += '<button type="button" class="vb-map-panel__arrow" data-office-nav="-1" aria-label="Попередній офіс">' + ARROW_LEFT_ICON + '</button>';
191        h += '<button type="button" class="vb-map-panel__arrow" data-office-nav="1" aria-label="Наступний офіс">' + ARROW_RIGHT_ICON + '</button>';
192        h += '</div></div>';
193      }
194      panel.innerHTML = h;
195    }
196
197    function renderActive() {
198      svg.querySelectorAll('.vb-map__region').forEach(function (p) {
199        p.classList.toggle('is-active', p.getAttribute('data-city') === activeCity);
200        p.setAttribute('aria-pressed', p.getAttribute('data-city') === activeCity ? 'true' : 'false');
201      });
202      svg.querySelectorAll('.vb-map__label').forEach(function (g) {
203        g.classList.toggle('is-active', g.getAttribute('data-city') === activeCity);
204      });
205    }
206
207    function selectCity(id) {
208      if (!id || id === activeCity) return;
209      activeCity = id;
210      officeIdx = 0;
211      renderActive();
212      renderPanel();
213    }
214
215    svg.addEventListener('click', function (e) {
216      var region = e.target.closest('.vb-map__region');
217      if (region) selectCity(region.getAttribute('data-city'));
218    });
219    svg.addEventListener('keydown', function (e) {
220      if (e.key !== 'Enter' && e.key !== ' ') return;
221      var region = e.target.closest('.vb-map__region');
222      if (region) {
223        e.preventDefault();
224        selectCity(region.getAttribute('data-city'));
225      }
226    });
227    panel.addEventListener('click', function (e) {
228      var dot = e.target.closest('[data-office-idx]');
229      if (dot) {
230        officeIdx = parseInt(dot.getAttribute('data-office-idx'), 10) || 0;
231        renderPanel();
232        return;
233      }
234      var nav = e.target.closest('[data-office-nav]');
235      if (nav) {
236        var cityOffices = offices[activeCity] || [];
237        if (!cityOffices.length) return;
238        var dir = parseInt(nav.getAttribute('data-office-nav'), 10) || 1;
239        officeIdx = (officeIdx + dir + cityOffices.length) % cityOffices.length;
240        renderPanel();
241      }
242    });
243
244    renderActive();
245    renderPanel();
246  }
247
248  var PIN_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewB
248ox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"></path><circle cx="12" cy="10" r="3"></circle></svg>';
249  var PHONE_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"></path></svg>';
250  var CLOCK_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>';
251  var ARROW_LEFT_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg>';
252  var ARROW_RIGHT_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg>';
253
254  document.querySelectorAll('[data-vb-coverage-map]').forEach(initCoverageMap);
255
256  /* ============ FAQ V2 ACCORDION ============ */
257  document.querySelectorAll('[data-vb-faq]').forEach(function (list) {
258    list.addEventListener('click', function (e) {
259      var summary = e.target.closest('.vb-faq__summary');
260      if (!summary) return;
261      var item = summary.closest('.vb-faq__item');
262      var wasOpen = item.classList.contains('is-open');
263      list.querySelectorAll('.vb-faq__item.is-open').forEach(function (open) {
264        open.classList.remove('is-open');
265        var btn = open.querySelector('.vb-faq__summary');
266        if (btn) btn.setAttribute('aria-expanded', 'false');
267      });
268      if (!wasOpen) {
269        item.classList.add('is-open');
270        summary.setAttribute('aria-expanded', 'true');
271      }
272    });
273  });
274
275  /* ============ HERO FADE-IN ============ */
276  var heroInner = document.querySelector('[data-vb-hero-fade]');
277  if (heroInner && !reducedMotion) {
278    heroInner.style.opacity = '0';
279    heroInner.style.transform = 'translateY(24px)';
280    requestAnimationFrame(function () {
281      heroInner.style.transition = 'opacity .8s ease, transform .8s ease';
282      heroInner.style.opacity = '1';
283      heroInner.style.transform = 'translateY(0)';
284    });
285  }
286
287  /* ============ GEO: підказки міст при фокусі ============ */
288  var geoInput = document.getElementById('address-check-input');
289  var geoHints = document.querySelector('[data-vb-geo-hints]');
290
291  if (geoInput && geoHints) {
292    var geoHintsShow = function () {
293      if (!geoInput.value.trim()) { geoHints.hidden = false; }
294    };
295    var geoHintsHide = function () { geoHints.hidden = true; };
296
297    geoInput.addEventListener('focus', geoHintsShow);
298
299    geoInput.addEventListener('input', function () {
300      // Зʼявився текст — далі підказки віддає нативний Google Places.
301      if (geoInput.value.trim()) { geoHintsHide(); } else { geoHintsShow(); }
302    });
303
304    // mousedown + preventDefault, а не click: інакше blur спрацює раніше,
305    // список сховається, і клік не дійде до кнопки.
306    geoHints.addEventListener('mousedown', function (e) {
307      var btn = e.target.closest('[data-vb-city]');
308      if (!btn) { return; }
309      e.preventDefault();
310      geoInput.value = btn.getAttribute('data-vb-city') + ', ';
311      geoHintsHide();
312      geoInput.focus();
313      geoInput.dispatchEvent(new Event('input', { bubbles: true }));
314    });
315
316    geoInput.addEventListener('keydown', function (e) {
317      if (e.key === 'Escape') { geoHintsHide(); }
318    });
319
320    document.addEventListener('click', function (e) {
321      if (e.target !== geoInput && !geoHints.contains(e.target)) { geoHintsHide(); }
322    });
323  }
324
325})();

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.