document.addEventListener('DOMContentLoaded', () => {
const body = document.body;
// loading
const loader = document.querySelector('.page-loader');
body.classList.add('is-loading');
// menu
const menuBtn = document.querySelector('.menu-btn');
const mobileMenu = document.querySelector('.mobile-menu');
const tabletMenu = document.querySelector('.tablet-menu');
menuBtn?.addEventListener('click', () => {
body.classList.toggle('menu-open');
const isOpen = body.classList.contains('menu-open');
mobileMenu?.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
tabletMenu?.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
});
document.querySelectorAll('.mobile-menu a').forEach((link) => {
link.addEventListener('click', () => {
body.classList.remove('menu-open');
mobileMenu?.setAttribute('aria-hidden', 'true');
});
});
document.querySelectorAll('.tablet-menu a').forEach((link) => {
link.addEventListener('click', () => {
body.classList.remove('menu-open');
tabletMenu?.setAttribute('aria-hidden', 'true');
});
});
const productDropdown = document.querySelector('.product-dropdown');
productDropdown?.addEventListener('mouseenter', () => {
body.classList.remove('menu-open');
mobileMenu?.setAttribute('aria-hidden', 'true');
});
// hero/header/product tabs
const header = document.querySelector('.site-header');
const productTabs = document.querySelector('.product-tabs');
const hero = document.querySelector('.hero');
const channelSection = document.querySelector('#channel');
let lastScrollY = window.scrollY;
function updateHeaderState() {
if (!hero) return;
const currentScrollY = window.scrollY;
const isScrollingUp = currentScrollY < lastScrollY;
const passedHero = currentScrollY >= hero.offsetTop;
header?.classList.toggle('is-scrolled', passedHero);
productTabs?.classList.toggle('is-compact', passedHero);
if (channelSection && productTabs) {
const rect = channelSection.getBoundingClientRect();
const passedChannelHalf =
rect.top + rect.height / 2 <= window.innerHeight / 2;
productTabs.classList.toggle(
'is-hidden',
passedChannelHalf && !isScrollingUp
);
}
lastScrollY = currentScrollY;
}
window.addEventListener('scroll', updateHeaderState);
window.addEventListener('resize', updateHeaderState);
updateHeaderState();
// accordion
document.querySelectorAll('.accordion').forEach((item) => {
const btn = item.querySelector('button');
const icon = item.querySelector('.accordion-icon');
btn?.addEventListener('click', () => {
const isOpen = item.classList.toggle('is-open');
if (!icon) return;
icon.src = isOpen
? icon.dataset.open
: icon.dataset.close;
});
});
// modal
const openModal = (id) => {
const modal = document.getElementById(id);
if (!modal) return;
modal.classList.add('is-open');
modal.setAttribute('aria-hidden', 'false');
body.style.overflow = 'hidden';
};
const closeModal = (modal) => {
if (!modal) return;
modal.classList.remove('is-open');
modal.setAttribute('aria-hidden', 'true');
body.style.overflow = '';
};
document.querySelectorAll('[data-modal-open]').forEach((btn) => {
btn.addEventListener('click', () => {
openModal(btn.dataset.modalOpen);
});
});
document.querySelectorAll('[data-modal-close]').forEach((btn) => {
btn.addEventListener('click', () => {
closeModal(btn.closest('.modal'));
});
});
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return;
document.querySelectorAll('.modal.is-open').forEach(closeModal);
});
// review slider
document.querySelectorAll('[data-review-slider]').forEach((slider) => {
const track = slider.querySelector('.review-track');
const viewport = slider.querySelector('.review-viewport');
const cards = [...slider.querySelectorAll('.review-card')];
const prev = slider.querySelector('.prev');
const next = slider.querySelector('.next');
const controls = slider.querySelector('.review-controls');
const currentText = slider.querySelector('.review-count__current');
const totalText = slider.querySelector('.review-count__total');
if (!track || !viewport || !cards.length || !controls) return;
let currentIndex = 0;
let resizeTimer;
const isTouchDevice = () =>
'ontouchstart' in window ||
navigator.maxTouchPoints > 0;
const getVisibleCount = () =>
window.innerWidth <= 600 ? 1 : 2;
const getMaxIndex = () =>
Math.max(0, cards.length - 1);
const getMoveX = () => {
const gap = parseFloat(getComputedStyle(track).gap) || 0;
return cards[0].offsetWidth + gap;
};
const syncCardHeight = () => {
cards.forEach((card) => {
card.style.height = 'auto';
});
let maxHeight = 0;
cards.forEach((card) => {
maxHeight = Math.max(maxHeight, card.offsetHeight);
});
cards.forEach((card) => {
card.style.height = `${maxHeight}px`;
});
};
const updateControls = () => {
controls.style.display = cards.length <= getVisibleCount()
? 'none'
: '';
if (totalText) {
totalText.textContent = cards.length;
}
};
const update = (nextIndex) => {
const maxIndex = getMaxIndex();
currentIndex = Math.max(0, Math.min(nextIndex, maxIndex));
cards.forEach((card) => {
card.hidden = false;
});
track.style.transition = 'transform .45s ease';
track.style.transform = `translateX(${-currentIndex * getMoveX()}px)`;
if (currentText) {
currentText.textContent = currentIndex + 1;
}
if (prev) prev.disabled = currentIndex === 0;
if (next) next.disabled = currentIndex === maxIndex;
};
prev?.addEventListener('click', () => update(currentIndex - 1));
next?.addEventListener('click', () => update(currentIndex + 1));
// swipe
let startX = 0;
let currentX = 0;
let isDragging = false;
viewport.addEventListener('touchstart', (e) => {
if (!isTouchDevice()) return;
startX = e.touches[0].clientX;
currentX = startX;
isDragging = true;
track.style.transition = 'none';
});
viewport.addEventListener('touchmove', (e) => {
if (!isDragging || !isTouchDevice()) return;
currentX = e.touches[0].clientX;
const diffX = currentX - startX;
const baseX = -currentIndex * getMoveX();
track.style.transform = `translateX(${baseX + diffX}px)`;
});
viewport.addEventListener('touchend', () => {
if (!isDragging || !isTouchDevice()) return;
const diffX = currentX - startX;
const threshold = 50;
isDragging = false;
if (diffX < -threshold) {
update(currentIndex + 1);
} else if (diffX > threshold) {
update(currentIndex - 1);
} else {
update(currentIndex);
}
});
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
currentIndex = Math.min(currentIndex, getMaxIndex());
updateControls();
syncCardHeight();
update(currentIndex);
}, 150);
});
updateControls();
syncCardHeight();
update(currentIndex);
});
// text split
document.querySelectorAll('.js-text-reveal').forEach((el) => {
const lines = el.querySelectorAll('.reveal-line');
let index = 0;
if (lines.length) {
lines.forEach((line, lineIndex) => {
const text = line.textContent.trim();
line.innerHTML = [...text].map((char) => {
return `${char}`;
}).join('');
});
} else {
const inner = el.querySelector('.text-reveal__inner') || el;
const text = inner.textContent.trim();
inner.innerHTML = [...text].map((char) => {
return `${char}`;
}).join('');
}
el.classList.add('is-ready');
});
// observers
const revealObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
entry.target.classList.add('is-active');
revealObserver.unobserve(entry.target);
});
}, {
threshold: 0.4
});
const upgradeDecoObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
entry.target.classList.add('is-active');
upgradeDecoObserver.unobserve(entry.target);
});
}, {
threshold: 0.35
});
const reviewSection = document.querySelector('.reviews');
const reviewHex = document.querySelector('.js-review-hex');
const reviewHexObserver = new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting) return;
reviewHex?.classList.add('is-active');
reviewHexObserver.unobserve(entry.target);
}, {
threshold: 0.35
});
const footer = document.querySelector('.site-footer');
const footerObserver = new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting) return;
footer?.classList.add('is-active');
footerObserver.unobserve(entry.target);
}, {
threshold: 0.2
});
const sectionBg = document.querySelector('.section-bg');
const sectionBgObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
sectionBg?.classList.add('is-active');
sectionBgObserver.unobserve(entry.target);
});
}, {
threshold: 0.2
});
function initRevealAnimations() {
document.querySelectorAll('.js-text-reveal').forEach((el) => {
revealObserver.observe(el);
});
document.querySelectorAll('.js-underline-reveal').forEach((el) => {
revealObserver.observe(el);
});
document.querySelectorAll('.js-upgrade-deco').forEach((block) => {
upgradeDecoObserver.observe(block);
});
if (reviewSection && reviewHex) {
reviewHexObserver.observe(reviewSection);
}
if (footer) {
footerObserver.observe(footer);
}
if (sectionBg) {
sectionBgObserver.observe(sectionBg);
}
}
// start animations after loading
window.addEventListener('load', () => {
setTimeout(() => {
loader?.classList.add('is-hide');
body.classList.remove('is-loading');
initRevealAnimations();
}, 600);
});
});
// menu active
const navLinks = document.querySelectorAll(
'.desktop-nav a, .tablet-menu__nav a, .mobile-menu__nav a'
);
const sections = [
'#feature',
'#timing',
'#quality',
'#review',
'#faq',
'#channel'
]
.map((selector) => document.querySelector(selector))
.filter(Boolean);
function updateActiveNav() {
const offset = window.innerHeight * 0.35;
let currentId = sections[0]?.id;
sections.forEach((section) => {
const rect = section.getBoundingClientRect();
if (rect.top <= offset) {
currentId = section.id;
}
});
navLinks.forEach((link) => {
link.classList.toggle(
'active',
link.getAttribute('href') === `#${currentId}`
);
});
}
window.addEventListener('scroll', updateActiveNav);
window.addEventListener('resize', updateActiveNav);
window.addEventListener('load', updateActiveNav);
document.querySelectorAll(
'.desktop-nav a, .tablet-menu__nav a, .mobile-menu__nav a'
).forEach((link) => {
link.addEventListener('click', () => {
const id = link.getAttribute('href')?.replace('#', '');
navLinks.forEach((item) => {
item.classList.toggle(
'active',
item.getAttribute('href') === `#${id}`
);
});
setTimeout(updateActiveNav, 500);
});
});
updateActiveNav();