chore: baseline of the live help4bis.com docroot as of 2026-10-23 (cockpit #168)
Excludes analytics/data, vendor, support, .env, ACME tokens. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
'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
|
||||
══════════════════════════════════════ */
|
||||
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; });
|
||||
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user