231 lines
9.6 KiB
JavaScript
231 lines
9.6 KiB
JavaScript
'use strict';
|
|
|
|
/* ══════════════════════════════════════
|
|
NAVIGATION — DROPDOWN (click-based)
|
|
══════════════════════════════════════ */
|
|
function initDropdowns() {
|
|
document.querySelectorAll('.nav__dropdown').forEach(item => {
|
|
const trigger = item.querySelector('.nav__trigger');
|
|
const menu = item.querySelector('.nav__dropdown-menu');
|
|
if (!trigger || !menu) return;
|
|
|
|
trigger.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const isOpen = menu.classList.toggle('open');
|
|
trigger.setAttribute('aria-expanded', isOpen);
|
|
// Close siblings
|
|
document.querySelectorAll('.nav__dropdown-menu.open').forEach(m => {
|
|
if (m !== menu) {
|
|
m.classList.remove('open');
|
|
m.closest('.nav__dropdown').querySelector('.nav__trigger')?.setAttribute('aria-expanded','false');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// Close on outside click
|
|
document.addEventListener('click', () => {
|
|
document.querySelectorAll('.nav__dropdown-menu.open').forEach(m => {
|
|
m.classList.remove('open');
|
|
m.closest('.nav__dropdown').querySelector('.nav__trigger')?.setAttribute('aria-expanded','false');
|
|
});
|
|
});
|
|
|
|
// Close on Escape
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
document.querySelectorAll('.nav__dropdown-menu.open').forEach(m => {
|
|
m.classList.remove('open');
|
|
m.closest('.nav__dropdown').querySelector('.nav__trigger')?.setAttribute('aria-expanded','false');
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/* ══════════════════════════════════════
|
|
NAVIGATION — MOBILE BURGER
|
|
══════════════════════════════════════ */
|
|
function initMobileNav() {
|
|
const burger = document.getElementById('nav-burger');
|
|
const navLinks = document.getElementById('nav-links');
|
|
if (!burger || !navLinks) return;
|
|
|
|
burger.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const isOpen = navLinks.classList.toggle('nav--open');
|
|
burger.setAttribute('aria-expanded', isOpen);
|
|
document.body.style.overflow = isOpen ? 'hidden' : '';
|
|
burger.innerHTML = isOpen ? iconX() : iconMenu();
|
|
});
|
|
|
|
// Close on link click
|
|
navLinks.querySelectorAll('a').forEach(link => {
|
|
link.addEventListener('click', () => {
|
|
navLinks.classList.remove('nav--open');
|
|
burger.setAttribute('aria-expanded','false');
|
|
document.body.style.overflow = '';
|
|
burger.innerHTML = iconMenu();
|
|
});
|
|
});
|
|
|
|
// Close on outside click
|
|
document.addEventListener('click', (e) => {
|
|
if (!burger.contains(e.target) && !navLinks.contains(e.target)) {
|
|
navLinks.classList.remove('nav--open');
|
|
burger.setAttribute('aria-expanded','false');
|
|
document.body.style.overflow = '';
|
|
burger.innerHTML = iconMenu();
|
|
}
|
|
});
|
|
}
|
|
|
|
function iconMenu() {
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>`;
|
|
}
|
|
function iconX() {
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
|
|
}
|
|
|
|
/* ══════════════════════════════════════
|
|
ACTIVE NAV LINK
|
|
══════════════════════════════════════ */
|
|
function initActiveNav() {
|
|
const path = window.location.pathname;
|
|
const file = path.split('/').pop() || 'index.html';
|
|
document.querySelectorAll('.nav__links a').forEach(a => {
|
|
const href = a.getAttribute('href');
|
|
if (href && (href === file || (file === '' && href === 'index.html'))) {
|
|
a.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
/* ══════════════════════════════════════
|
|
STICKY NAV SHADOW
|
|
══════════════════════════════════════ */
|
|
function initStickyNav() {
|
|
const nav = document.querySelector('.nav');
|
|
if (!nav) return;
|
|
const update = () => nav.classList.toggle('scrolled', window.scrollY > 30);
|
|
window.addEventListener('scroll', update, { passive: true });
|
|
update();
|
|
}
|
|
|
|
/* ══════════════════════════════════════
|
|
SCROLL REVEAL
|
|
══════════════════════════════════════ */
|
|
function initReveal() {
|
|
const io = new IntersectionObserver((entries) => {
|
|
entries.forEach(e => {
|
|
if (e.isIntersecting) {
|
|
e.target.classList.add('visible');
|
|
io.unobserve(e.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.06, rootMargin: '0px 0px -24px 0px' });
|
|
|
|
// Individual reveal elements
|
|
document.querySelectorAll('.reveal').forEach(el => io.observe(el));
|
|
|
|
// Stagger groups (observe the container, trigger class on it)
|
|
document.querySelectorAll('.stagger').forEach(el => io.observe(el));
|
|
}
|
|
|
|
/* ══════════════════════════════════════
|
|
SMOOTH ANCHOR SCROLL
|
|
══════════════════════════════════════ */
|
|
function initAnchorScroll() {
|
|
document.querySelectorAll('a[href^="#"]').forEach(a => {
|
|
a.addEventListener('click', (e) => {
|
|
const id = a.getAttribute('href');
|
|
const target = document.querySelector(id);
|
|
if (target) {
|
|
e.preventDefault();
|
|
const top = target.getBoundingClientRect().top + window.scrollY - 76;
|
|
window.scrollTo({ top, behavior: 'smooth' });
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ══════════════════════════════════════
|
|
FORM HANDLER
|
|
══════════════════════════════════════ */
|
|
const formShownAt = Date.now();
|
|
|
|
function initForms() {
|
|
document.querySelectorAll('.intake-form').forEach(form => {
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const btn = form.querySelector('[type="submit"]');
|
|
const originalText = btn.textContent;
|
|
btn.textContent = 'Sending…';
|
|
btn.disabled = true;
|
|
|
|
// Collect all named fields
|
|
const data = {};
|
|
new FormData(form).forEach((val, key) => { data[key] = val; });
|
|
// Spam check: how long the visitor took to fill the form (measured on their own clock)
|
|
data.fill_ms = Date.now() - formShownAt;
|
|
|
|
try {
|
|
const res = await fetch('/contact.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
const json = await res.json();
|
|
|
|
if (json.ok) {
|
|
form.innerHTML = `
|
|
<div style="text-align:center;padding:2.5rem 1rem;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 24 24" fill="none" stroke="#16A34A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin:0 auto 1.25rem;display:block"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
|
<h3 style="font-family:'DM Sans',sans-serif;color:#1A1A2E;margin-bottom:.5rem;font-size:22px;">Sent.</h3>
|
|
<p style="color:#6B7280;font-size:15px;max-width:400px;margin:0 auto;">We'll be back within one business day with a clear recommendation and price.</p>
|
|
</div>`;
|
|
} else {
|
|
throw new Error(json.error || 'Send failed');
|
|
}
|
|
} catch (err) {
|
|
// Show inline error — don't wipe the form
|
|
let errEl = form.querySelector('.form-error');
|
|
if (!errEl) {
|
|
errEl = document.createElement('p');
|
|
errEl.className = 'form-error';
|
|
errEl.style.cssText = 'color:#E8352A;font-size:14px;margin-top:0.75rem;';
|
|
form.querySelector('.intake-form__actions').appendChild(errEl);
|
|
}
|
|
errEl.textContent = err.message || 'Something went wrong. Please email project@help4bis.com directly.';
|
|
btn.textContent = originalText;
|
|
btn.disabled = false;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ══════════════════════════════════════
|
|
BACK TO TOP
|
|
══════════════════════════════════════ */
|
|
function initBackToTop() {
|
|
const btn = document.getElementById('back-to-top');
|
|
if (!btn) return;
|
|
window.addEventListener('scroll', () => {
|
|
btn.classList.toggle('visible', window.scrollY > 500);
|
|
}, { passive: true });
|
|
btn.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
|
|
}
|
|
|
|
/* ══════════════════════════════════════
|
|
INIT
|
|
══════════════════════════════════════ */
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initDropdowns();
|
|
initMobileNav();
|
|
initActiveNav();
|
|
initStickyNav();
|
|
initReveal();
|
|
initAnchorScroll();
|
|
initForms();
|
|
initBackToTop();
|
|
});
|