126 lines
4.2 KiB
JavaScript
126 lines
4.2 KiB
JavaScript
const body = document.body;
|
|
const menuToggle = document.querySelector('.menu-toggle');
|
|
const navLinks = document.querySelectorAll('.site-nav a');
|
|
const progress = document.querySelector('.scroll-progress span');
|
|
|
|
if (menuToggle) {
|
|
menuToggle.addEventListener('click', () => {
|
|
const isOpen = body.classList.toggle('menu-open');
|
|
menuToggle.setAttribute('aria-expanded', String(isOpen));
|
|
});
|
|
}
|
|
|
|
navLinks.forEach((link) => {
|
|
link.addEventListener('click', () => {
|
|
body.classList.remove('menu-open');
|
|
menuToggle?.setAttribute('aria-expanded', 'false');
|
|
});
|
|
});
|
|
|
|
const updateProgress = () => {
|
|
if (!progress) return;
|
|
const max = document.documentElement.scrollHeight - window.innerHeight;
|
|
const value = max > 0 ? window.scrollY / max : 0;
|
|
progress.style.transform = `scaleX(${value})`;
|
|
};
|
|
|
|
window.addEventListener('scroll', updateProgress, { passive: true });
|
|
updateProgress();
|
|
|
|
const revealObserver = new IntersectionObserver((entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('is-visible');
|
|
revealObserver.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.12, rootMargin: '0px 0px -5% 0px' });
|
|
|
|
document.querySelectorAll('.reveal').forEach((element) => revealObserver.observe(element));
|
|
|
|
const currentPage = body.dataset.page;
|
|
document.querySelectorAll(`[data-nav="${currentPage}"]`).forEach((link) => {
|
|
link.setAttribute('aria-current', 'page');
|
|
});
|
|
|
|
const dot = document.querySelector('.cursor-dot');
|
|
const ring = document.querySelector('.cursor-ring');
|
|
|
|
if (dot && ring && window.matchMedia('(pointer: fine)').matches) {
|
|
let mouseX = 0;
|
|
let mouseY = 0;
|
|
let ringX = 0;
|
|
let ringY = 0;
|
|
|
|
window.addEventListener('mousemove', (event) => {
|
|
mouseX = event.clientX;
|
|
mouseY = event.clientY;
|
|
dot.classList.add('is-active');
|
|
ring.classList.add('is-active');
|
|
dot.style.left = `${mouseX}px`;
|
|
dot.style.top = `${mouseY}px`;
|
|
});
|
|
|
|
const animateRing = () => {
|
|
ringX += (mouseX - ringX) * 0.16;
|
|
ringY += (mouseY - ringY) * 0.16;
|
|
ring.style.left = `${ringX}px`;
|
|
ring.style.top = `${ringY}px`;
|
|
requestAnimationFrame(animateRing);
|
|
};
|
|
|
|
animateRing();
|
|
|
|
document.querySelectorAll('a, button, input, textarea, select').forEach((target) => {
|
|
target.addEventListener('mouseenter', () => ring.classList.add('is-hovering'));
|
|
target.addEventListener('mouseleave', () => ring.classList.remove('is-hovering'));
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('[data-count]').forEach((element) => {
|
|
const target = Number(element.dataset.count);
|
|
const suffix = element.dataset.suffix || '';
|
|
const counterObserver = new IntersectionObserver((entries) => {
|
|
entries.forEach((entry) => {
|
|
if (!entry.isIntersecting) return;
|
|
const start = performance.now();
|
|
const duration = 1300;
|
|
const tick = (time) => {
|
|
const progressValue = Math.min((time - start) / duration, 1);
|
|
const eased = 1 - Math.pow(1 - progressValue, 4);
|
|
element.textContent = `${Math.round(target * eased)}${suffix}`;
|
|
if (progressValue < 1) requestAnimationFrame(tick);
|
|
};
|
|
requestAnimationFrame(tick);
|
|
counterObserver.disconnect();
|
|
});
|
|
}, { threshold: 0.5 });
|
|
counterObserver.observe(element);
|
|
});
|
|
|
|
const contactForm = document.querySelector('[data-contact-form]');
|
|
if (contactForm) {
|
|
contactForm.addEventListener('submit', (event) => {
|
|
event.preventDefault();
|
|
const status = contactForm.querySelector('.form-status');
|
|
const name = new FormData(contactForm).get('name') || 'there';
|
|
status.textContent = `Thank you, ${name}. Your collaboration brief is ready for review.`;
|
|
contactForm.reset();
|
|
});
|
|
}
|
|
|
|
const applicationForm = document.querySelector('[data-application-form]');
|
|
if (applicationForm) {
|
|
applicationForm.addEventListener('submit', (event) => {
|
|
event.preventDefault();
|
|
const status = applicationForm.querySelector('.form-status');
|
|
const name = new FormData(applicationForm).get('name') || 'builder';
|
|
status.textContent = `Application received, ${name}. Connect this form to a submission service before launch.`;
|
|
applicationForm.reset();
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('[data-year]').forEach((node) => {
|
|
node.textContent = new Date().getFullYear();
|
|
});
|