PageSourceSearch

https://marvitel.com.br/script.js

js marvitel.com.br collected 2026-10-08 12:28:43 UTC 20,004 bytes, 589 lines download raw bytes

1// ── Mobile menu ──
2const menuBtn = document.getElementById('menu-btn');
3const mobileMenu = document.getElementById('mobile-menu');
4menuBtn.addEventListener('click', () => mobileMenu.classList.toggle('hidden'));
5
6// ── HERO SLIDER ──
7const slides = Array.from(document.querySelectorAll('.hero-slide'));
8const dots = Array.from(document.querySelectorAll('#slider-dots .dot'));
9let current = 0;
10let timer;
11
12function goTo(n) {
13  // Remove active do slide atual
14  slides[current].classList.remove('active');
15  dots[current].classList.remove('active');
16  // Novo índice com loop
17  current = (n + slides.length) % slides.length;
18  // Activa novo slide e dot
19  slides[current].classList.add('active');
20  dots[current].classList.add('active');
21}
22
23function startTimer() {
24  clearInterval(timer);
25  timer = setInterval(() => {
26    if (!document.hidden) goTo(current + 1);
27  }, 6000);
28}
29
30// Dots navegam pelo slider
31dots.forEach((dot, i) => {
32  dot.addEventListener('click', () => {
33    goTo(i);
34    startTimer(); // reinicia o timer ao clicar
35  });
36});
37
38// Inicia autoplay
39startTimer();
40
41// ── SEGMENT TABS ──
42const segTabs = document.querySelectorAll('.seg-tab');
43const tabPanels = document.querySelectorAll('.tab-panel');
44
45// ── Segment Dropdown Toggle & Hover ──
46document.querySelectorAll('.relative.group').forEach(container => {
47  const btn = container.querySelector('.seg-dropdown-btn');
48  const menu = container.querySelector('.seg-dropdown-menu');
49  if (!btn || !menu) return;
50
51  // Click toggle (mainly for mobile)
52  btn.addEventListener('click', (e) => {
53    e.stopPropagation();
54    const isExpanded = btn.getAttribute('aria-expanded') === 'true';
55
56    // Close other menus
57    document.querySelectorAll('.seg-dropdown-menu.show').forEach(m => {
58      if (m !== menu) m.classList.remove('show');
59    });
60    document.querySelectorAll('.seg-dropdown-btn').forEach(b => {
61      if (b !== btn) b.setAttribute('aria-expanded', 'false');
62    });
63
64    btn.setAttribute('aria-expanded', !isExpanded);
65    menu.classList.toggle('show');
66  });
67
68  // Hover support (matches megamenu behavior on desktop)
69  container.addEventListener('mouseenter', () => {
70    if (window.innerWidth >= 1024) {
71      btn.setAttribute('aria-expanded', 'true');
72      // No need to add .show here as CSS group-hover handles visibility
73    }
74  });
75
76  container.addEventListener('mouseleave', () => {
77    if (window.innerWidth >= 1024) {
78      btn.setAttribute('aria-expanded', 'false');
79    }
80  });
81});
82
83// ── Segment Dropdown Item Click ──
84document.querySelectorAll('.seg-dropdown-item').forEach(item => {
85  item.addEventListener('click', () => {
86    const seg = item.dataset.seg;
87    const labelText = item.textContent.trim();
88    const iconHtml = item.querySelector('svg').outerHTML;
89
90    // Update all labels and icons in triggers
91    document.querySelectorAll('.seg-current-label').forEach(el => {
92      el.textContent = labelText;
93    });
94    document.querySelectorAll('.seg-current-icon').forEach(el => {
95      el.innerHTML = iconHtml;
96    });
97
98    // Close all menus
99    document.querySelectorAll('.seg-dropdown-menu').forEach(m => m.classList.remove('show'));
100    document.querySelectorAll('.seg-dropdown-btn').forEach(b => b.setAttribute('aria-expanded', 'false'));
101
102    // Trigger existing tab logic by finding the correct .seg-tab
103    // We target the wide-screen tabs for the click trigger
104    const correspondingTab = document.querySelector(`.seg-tab[data-seg="${seg}"]`);
105    if (correspondingTab) {
106      correspondingTab.click(); // This will trigger panel switch AND scroll
107    }
108  });
109});
110
111// ── Close on click outside ──
112document.addEventListener('click', () => {
113  document.querySelectorAll('.seg-dropdown-menu').forEach(m => m.classList.remove('show'));
114  document.querySelectorAll('.seg-dropdown-btn').forEach(b => b.setAttribute('aria-expanded', 'false'));
115});
116
117segTabs.forEach(tab => {
118  tab.addEventListener('click', () => {
119    // Skip scroll/active logic if this is a dropdown toggle button
120    if (tab.classList.contains('seg-dropdown-btn')) return;
121
122    // Remove active from ALL seg-tabs (handles dual desktop/mobile tab set
122s)
123    document.querySelectorAll('.seg-tab.active').forEach(activeTab => {
124      activeTab.classList.remove('active');
125      activeTab.setAttribute('aria-selected', 'false');
126    });
127    const activePanel = document.querySelector('.tab-panel.active');
128
129    if (activePanel) activePanel.classList.remove('active');
130
131    tab.classList.add('active');
132    tab.setAttribute('aria-selected', 'true');
133
134    // Auto-scroll to content
135    const targetSection = document.getElementById('section-tabs');
136    if (targetSection) {
137      targetSection.scrollIntoView({ behavior: 'smooth' });
138    }
139
140    const seg = tab.dataset.seg;
141    const panel = document.getElementById('panel-' + seg);
142    if (panel) {
143      panel.classList.add('active');
144
145      // Quando o segmento (Empresa/Condomínio) fica visível, agendamos o recálculo via requestAnimationFrame
146      // para garantir que o CSS display:block já foi processado pelo browser
147      requestAnimationFrame(() => {
148        const activePlanTab = panel.querySelector('.plan-tab.active');
149        if (activePlanTab && window.carouselRefreshFns) {
150          const plan = activePlanTab.dataset.plan;
151          if (window.carouselRefreshFns[plan]) window.carouselRefreshFns[plan]();
152        }
153
154        // Garante que o carrossel de serviços (onde existe) seja "acordado" da tela oculta
155        panel.querySelectorAll('.svc-track').forEach(t => {
156          if (typeof t._refresh === 'function') t._refresh();
157        });
158      });
159    }
160  });
161});
162
163// ── SERVICE CARD CAROUSEL (Empresa) — loop infinito ──
164(function () {
165  const track = document.getElementById('svc-track-empresa');
166  const prevBtn = document.getElementById('svc-prev-empresa');
167  const nextBtn = document.getElementById('svc-next-empresa');
168  if (!track || !prevBtn || !nextBtn) return;
169
170  // 1. Clonar todos os cards originais e appendar (cria "anel" duplo)
171  const originals = Array.from(track.querySelectorAll('.svc-card'));
172  const N = originals.length;
173  originals.forEach(c => track.appendChild(c.cloneNode(true)));
174  // track agora tem 2*N cards
175
176  let current = 0; // índice lógico, pode ir além de N
177  let isResetting = false;
178
179  function perView() { return window.innerWidth >= 1024 ? 3 : 1; }
180
181  function cardWidth() {
182    const c = track.querySelectorAll('.svc-card')[0];
183    return c ? c.offsetWidth + 18 : 240;
184  }
185
186  // Move o track para o índice dado, com ou sem transição
187  function moveTo(index, animated) {
188    track.style.transition = animated
189      ? 'transform 0.42s cubic-bezier(0.4, 0, 0.2, 1)'
190      : 'none';
191    track.style.transform = `translateX(-${index * cardWidth()}px)`;
192  }
193
194  // Avança/recua e gerencia o reset silencioso ao cruzar os limites
195  function goTo(index) {
196    current = index;
197    moveTo(current, true);
198
199    // Atualiza estado visual dos botões (sempre habilitados no loop infinito)
200    prevBtn.disabled = false;
201    nextBtn.disabled = false;
202    prevBtn.style.opacity = '1';
203    nextBtn.style.opacity = '1';
204  }
205
206  // Após a transição terminar, verifica se precisa de reset silencioso
207  track.addEventListener('transitionend', () => {
208    if (isResetting) return;
209    if (current >= N) {
210      isResetting = true;
211      current -= N;
212      moveTo(current, false);
213      requestAnimationFrame(() => isResetting = false);
214    } else if (current < 0) {
215      isResetting = true;
216      current += N;
217      moveTo(current, false);
218      requestAnimationFrame(() => isResetting = false);
219    }
220  });
221
222  // ── Autoplay ──
223  const INTERVAL = 3500;
224  let autoTimer;
225
226  function startAuto() {
227    clearInterval(autoTimer);
228    autoTimer = setInterval(() => {
229      if (track && track.offsetWidth > 0 && !document.hidden) {
230        goTo(current + 1);
231      }
232    }, INTERVAL);
233  }
234
235  prevBtn.addEventListener('click', () => { goTo(current - 1); startAuto(); });
236  nextBtn.addEventListener('click', () => { goTo(current + 1); startAuto(); });
237
238  // Recalculate on resize
239  window.addEventListener('resize', () => moveTo(current, false));
240
241  // Init
242  goTo(0);
243  startAuto();
244})();
245
246// ── SERVICE CARD CAROUSEL — fábrica reutilizável ──
247function makeServiceCarousel(trackId, prevId, nextId) {
248  const track = document.getElementById(trackId);
249  const prevBtn = document.getElementById(prevId);
250  const nextBtn = document.getElementById(nextId);
251  if (!track) return;
252
253  const originals = Array.from(track.querySelectorAll('.svc-card'));
254  const N = originals.length;
255  originals.forEach(c => track.appendChild(c.cloneNode(true)));
256
257  let current = 0;
258  let isResetting = false;
259
260  function cardWidth() {
261    const c = track.querySelectorAll('.svc-card')[0];
262    return c ? c.offsetWidth + 18 : 240;
263  }
264
265  function moveTo(index, animated) {
266    track.style.transition = animated
267      ? 'transform 0.42s cubic-bezier(0.4, 0, 0.2, 1)'
268      : 'none';
269    track.style.transform = `translateX(-${index * cardWidth()}px)`;
270  }
271
272  function goTo(index) {
273    current = index;
274    moveTo(current, true);
275    if (prevBtn) {
276      prevBtn.disabled = false;
277      prevBtn.style.opacity = '1';
278    }
279    if (nextBtn) {
280      nextBtn.disabled = false;
281      nextBtn.style.opacity = '1';
282    }
283  }
284
285  track.addEventListener('transitionend', () => {
286    if (isResetting) return;
287    if (current >= N) {
288      isResetting = true;
289      current -= N;
290      moveTo(current, false);
291      requestAnimationFrame(() => isResetting = false);
292    } else if (current < 0) {
293      isResetting = true;
294      current += N;
295      moveTo(current, false);
296      requestAnimationFrame(() => isResetting = false);
297    }
298  });
299
300  const INTERVAL = 3500;
301  let autoTimer;
302  function startAuto() {
303    clearInterval(autoTimer);
304    autoTimer = setInterval(() => {
305      if (track && track.offsetWidth > 0 && !document.hidden) {
306        goTo(current + 1);
307      }
308    }, INTERVAL);
309  }
310
311  if (prevBtn) prevBtn.addEventListener('click', () => { goTo(current - 1); startAuto(); });
312  if (nextBtn) nextBtn.addEventListener('click', () => { goTo(current + 1); startAuto(); });
313
314  window.addEventListener('resize', () => moveTo(current, false));
315
316  track._refresh = () => {
317    moveTo(current, false);
318  };
319
320  goTo(0);
321  startAuto();
322}
323
324makeServiceCarousel('svc-track-condominio', 'svc-prev-condominio', 'svc-next-condominio');
325makeServiceCarousel('svc-track-evento', 'svc-prev-evento', 'svc-next-evento');
326
327// ── GALLERY CAROUSEL (Evento) ──
328(function initGalleryCarousel() {
329  const track = document.getElementById('gallery-track-evento');
330  const prevBtn = document.getElementById('gallery-prev-evento');
331  const nextBtn = document.getElementById('gallery-next-evento');
332  const dotsContainer = document.getElementById('gallery-dots-evento');
333  if (!track) return;
334
335  const slides = Array.from(track.querySelectorAll('.gallery-slide'));
336  const total = slides.length;
337  let current = 0;
338
339  function getItemsPerView() {
340    return window.innerWidth >= 1024 ? 3 : window.innerWidth >= 768 ? 2 : 1;
341  }
342
343  function updateCarousel() {
344    const perView = getItemsPerView();
345    const maxIndex = Math.max(0, total - perView);
346    if (current > maxIndex) current = maxIndex;
347    if (current < 0) current = 0;
348
349    const gap = 16; // gap-4 is 1rem = 16px
350    const slideW = slides[0].offsetWidth;
351    track.style.transform = `translateX(-${current * (slideW + gap)}px)`;
352
353    if (dotsContainer) {
354      Array.from(dotsContainer.children).forEach((dot, i) => {
355        if (i === current) {
356          dot.className = 'h-2 w-6 bg-azul-tradicao rounded-full transition-all duration-300';
357        } else {
358          dot.className = 'h-2 w-2 bg-gray-300 rounded-full transition-all duration-300';
359        }
360      });
361    }
362  }
363
364  function renderDots() {
365    if (!dotsContainer) return;
366    const perView = getItemsPerView();
367    const maxIndex = Math.max(0, total - perView);
368    dotsContainer.innerHTML = '';
369    for (let i = 0; i <= maxIndex; i++) {
370        const dot = document.createElement('button');
371        dot.className = i === current ? 'h-2 w-6 bg-azul-tradicao rounded-full transition-all duration-300' : 'h-2 w-2 bg-gray-300 rounded-full transition-all duration-300';
372        dot.setAttribute('aria-label', 'Slide ' + (i + 1));
373        dot.addEventListener('click', () => { current = i; updateCarousel(); startAuto(); });
374        dotsContainer.appendChild(dot);
375    }
376  }
377
378  let autoTimer;
379  function startAuto() {
380    clearInterval(autoTimer);
381    autoTimer = setInterval(() => {
382      if (track && track.offsetWidth > 0 && !document.hidden) {
383        const maxIndex = Math.max(0, total - getItemsPerView());
384        current = current >= maxIndex ? 0 : current + 1;
385        updateCarousel();
386      }
387    }, 4000);
388  }
389
390  if (prevBtn) prevBtn.addEventListener('click', () => { 
391      const maxIndex = Math.max(0, total - getItemsPerView());
392      current = current <= 0 ? maxIndex : current - 1; 
393      updateCarousel(); startAuto(); 
394  });
395  if (nextBtn) nextBtn.addEventListener('click', () => { 
396      const maxIndex = Math.max(0, total - getItemsPerView());
397      current = current >= maxIndex ? 0 : current + 1; 
398      updateCarousel(); startAuto(); 
399  });
400
401  window.addEventListener('resize', () => {
402    current = 0; 
403    renderDots();
404    updateCarousel();
405  });
406
407  renderDots();
408  updateCarousel();
409  startAuto();
410})();
411// ── PLAN TABS ──
412const carouselRefreshFns = {};
413window.carouselRefreshFns = carouselRefreshFns;
414
415document.querySelectorAll('.plan-tab').forEach(tab => {
416  tab.addEventListener('click', () => {
417    const parentSegment = tab.closest('.tab-panel');
418    if (!parentSegment) return;
419
420    parentSegment.querySelectorAll('.plan-tab').forEach(t => { t.classList.remove('active'); t.setAttribute('aria-selected', 'false'); });
421    parentSegment.querySelectorAll('.plan-panel').forEach(p => p.classList.remove('active'));
422
423    tab.classList.add('active');
424    tab.setAttribute('aria-selected', 'true');
425
426    const plan = tab.dataset.plan;
427    const targetPanel = document.getElementById('plan-panel-' + plan);
428    if (targetPanel) targetPanel.classList.add('active');
429
430    if (window.carouselRefreshFns[plan]) window.carouselRefreshFns[plan]();
431  });
432});
433
434// ── PLAN CARDS CAROUSEL (mobile/tablet) ──
435(function initPlanCarousels() {
436  const configs = [
437    { trackId: 'plan-track-internet', dotsId: 'plan-dots-internet', planKey: 'internet' },
438    { trackId: 'plan-track-adicionais', dotsId: 'plan-dots-adicionais', planKey: 'adicionais' },
439    { trackId: 'plan-track-internet-condominio', dotsId: 'plan-dots-internet-condominio', planKey: 'internet-condominio' },
440    { trackId: 'plan-track-banda-larga-condominio', dotsId: 'plan-dots-banda-larga-condominio', planKey: 'banda-larga-condominio' },
441    { trackId: 'plan-track-adicionais-condominio', dotsId: 'plan-dots-adicionais-condominio', planKey: 'adicionais-condominio' },
442    { trackId: 'plan-track-tv-condominio', dotsId: 'plan-dots-tv-condominio', planKey: 'tv-condominio' },
443    { trackId: 'plan-track-internet-evento', dotsId: 'plan-dots-internet-evento', planKey: 'internet-evento' },
444    { trackId: 'plan-track-adicionais-evento', dotsId: 'plan-dots-adicionais-evento', planKey: 'adicionais-evento' },
445    { trackId: 'plan-track-tv-evento', dotsId: 'plan-dots-tv-evento', planKey: 'tv-evento' },
446  ];
447
448  configs.forEach(({ trackId, dotsId, planKey }) => {
449    const track = document.getElementById(trackId);
450    if (!track) return;
451
452    const outer = track.closest('.plan-carousel-outer');
453    const dotsContainer = dotsId ? document.getElementById(dotsId) : null;
454    const cards = Array.from(track.querySelectorAll('.plan-card'));
455    const total = cards.length;
456    let current = 0;
457    let startX = 0;
458    let isDragging = false;
459
460    function getSlideWidth() {
461      const s = getComputedStyle(outer);
462      let w = outer.clientWidth
463        - parseFloat(s.paddingLeft || 0)
464        - parseFloat(s.paddingRight || 0);
465      if (w <= 0) {
466        let el = outer.parentElement;
467        while (el && el.clientWidth === 0) el = el.parentElement;
468        if (el) w = el.clientWidth - parseFloat(s.paddingLeft || 0) - parseFloat(s.paddingRight || 0);
469      }
470      return Math.max(w, 1);
471    }
472
473    function setCardWidths() {
474      const w = getSlideWidth();
475      cards.forEach(card => {
476        card.style.width = w + 'px';
477        card.style.minWidth = w + 'px';
478        card.style.flexShrink = '0';
479      });
480    }
481
482    function updateDots() {
483      if (!dotsContainer) return;
484      Array.from(dotsContainer.children).forEach((dot, i) => {
485        dot.className = i === current ? 'plan-dot plan-dot-active' : 'plan-dot';
486      });
487    }
488
489    function renderDots() {
490      if (!dotsContainer) return;
491      dotsContainer.innerHTML = '';
492      for (let i = 0; i < total; i++) {
493        const dot = document.createElement('button');
494        dot.className = i === 0 ? 'plan-dot plan-dot-active' : 'plan-dot';
495        dot.setAttribute('aria-label', 'Slide ' + (i + 1));
496        dot.addEventListener('click', () => { goTo(i); });
497        dotsContainer.appendChild(dot);
498      }
499    }
500
501    function goTo(idx) {
502      if (idx < 0) current = total - 1;
503      else if (idx >= total) current = 0;
504      else current = idx;
505      
506      const w = getSlideWidth();
507      track.style.transform = 'translateX(-' + (current * w) + 'px)';
508      updateDots();
509    }
510
511    function refresh() {
512      setCardWidths();
513      goTo(current);
514    }
515
516    carouselRefreshFns[planKey] = refresh;
517
518    document.querySelectorAll('.plan-carousel-btn[data-carousel="' + trackId + '"]').forEach(btn => {
519      btn.addEventListener('click', () => {
520        if (btn.classList.contains('prev')) goTo(current - 1);
521        else goTo(current + 1);
522      });
523    });
524
525    track.addEventListener('touchstart', e => { startX = e.touches[0].clientX; isDragging = true; }, { passive: true });
526    track.addEventListener('touchend', e => {
527      if (!isDragging) return;
528      const diff = startX - e.changedTouches[0].clientX;
529      if (Math.abs(diff) > 40) goTo(current + (diff > 0 ? 1 : -1));
530      isDragging = false;
531    });
532
533    window.addEventListener('resize', () => {
534      setCardWidths();
535      goTo(current);
536    });
537
538    setCardWidths();
539    renderDots();
540    goTo(0);
541  });
542})();
543
544// ── BACK TO TOP ──
545const backToTop = document.getElementById('back-to-top');
546window.addEventListener('scroll', () => {
547  backToTop.classList.toggle('visible', window.scrollY > 300);
548});
549backToTop.addEventListener('click', () => {
550  window.scrollTo({ top: 0, behavior: 'smooth' });
551});
552
553// ── COOKIE NOTICE ──
554(function initCookieNotice() {
555  if (localStorage.getItem('cookieAccepted')) return;
556
557  const cookieBar = document.createElement('div');
558  cookieBar.className = 'cookie-notice-bar';
559  cookieBar.innerHTML = `
560    <div class="cookie-notice-content">
561      <p>Utilizamos cookies para melhorar sua experiência. Ao continuar, você aceita nossa <a href="/public/politica-privacidade.html">Política de Privacidade</a>.</p>
562      <button id="accept-cookies-btn" class="cookie-notice-btn">Entendi e concordo</button>
563    </div>
564  `;
565
566  document.body.appendChild(cookieBar);
567
568  document.getElementById('accept-cookies-btn').addEventListener('click', () => {
569    localStorage.setItem('cookieAccepted', 'true');
570    cookieBar.classList.add('hide');
571    setTimeout(() => cookieBar.remove(), 300);
572  });
573})();
574
575// ── HASH NAVIGATION (Condomínios) ──
576function checkHashAndNavigate() {
577  if (window.location.hash === '#condominios') {
578    const tabCondominio = document.querySelector('.seg-tab[data-seg="condominio"]');
579    if (tabCondominio) {
580      setTimeout(() => {
581        // Ignora a animação e força o scroll da tab
582        tabCondominio.click();
583      }, 150);
584    }
585  }
586}
587
588window.addEventListener('load', checkHashAndNavigate);
589window.addEventListener('hashchange', checkHashAndNavigate);

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.