Files
floorithardwoodfloors.com/assets/js/main.js
T
Concept Agent 81feccdc1a Migrate to Stack A: PHP-fpm + nginx + supervisord, drop flat HTML + Python API
- Remove old flat HTML pages (index, about, blog, contact, reviews, services/*, locations/*)
- Remove Python/Flask API container (api/)
- Remove old root nginx.conf and components/
- Add infra/: full nginx.conf (http block at /etc/nginx/nginx.conf), php-fpm-pool.conf (TCP listen), supervisord.conf, entrypoint.sh (auto-generates ALTCHA_HMAC_KEY)
- Add src/: PHP router, page/service/location/blog templates, contact handler, altcha handler, promo endpoint, SQLite data files
- Rewrite Dockerfile: single container, tini PID 1, healthcheck, all env vars declared
- Update docker-compose.yml: port 8096, env_file, healthcheck
- Update .dockerignore: exclude .env.*, include robots.txt/sitemap.xml/404.html/500.html
- Update assets: tokens.css, promo-popup.css/js, altcha.min.js, refactored form.js/main.js

Verified: all 17 routes 200, protection audit PASS, Resend confirmed working

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:56:56 +02:00

199 lines
6.6 KiB
JavaScript

/* ============================================================
main.js: Scroll animations, counters, FAQ, BA slider
============================================================ */
(function () {
'use strict';
/* --- Scroll animation (IntersectionObserver) ------------ */
function initScrollAnimations() {
const els = document.querySelectorAll('[data-animate]');
if (!els.length) return;
const obs = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('in-view');
obs.unobserve(entry.target);
}
});
}, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
els.forEach(el => obs.observe(el));
}
/* --- Animated counters ---------------------------------- */
function animateCounter(el) {
const target = parseFloat(el.dataset.count);
const suffix = el.dataset.suffix || '';
const prefix = el.dataset.prefix || '';
const decimals = (target % 1 !== 0) ? 1 : 0;
const duration = 1600;
const start = performance.now();
function tick(now) {
const elapsed = now - start;
const progress = Math.min(elapsed / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3);
const value = target * ease;
el.textContent = prefix + value.toFixed(decimals) + suffix;
if (progress < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
function initCounters() {
const counters = document.querySelectorAll('[data-count]');
if (!counters.length) return;
const obs = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
animateCounter(entry.target);
obs.unobserve(entry.target);
}
});
}, { threshold: 0.3 });
counters.forEach(el => obs.observe(el));
}
/* --- FAQ accordion -------------------------------------- */
function initFAQ() {
document.querySelectorAll('.faq-item').forEach(item => {
const q = item.querySelector('.faq-question');
if (!q) return;
q.addEventListener('click', () => {
const isOpen = item.classList.contains('open');
// close all
document.querySelectorAll('.faq-item.open').forEach(i => {
i.classList.remove('open');
});
if (!isOpen) item.classList.add('open');
});
});
}
/* --- Before / After slider ------------------------------ */
function initBASlider(slider) {
const handle = slider.querySelector('.ba-handle');
const beforeWrap = slider.querySelector('.ba-before-wrap');
if (!handle || !beforeWrap) return;
let dragging = false;
function setPosition(clientX) {
const rect = slider.getBoundingClientRect();
const rawPct = (clientX - rect.left) / rect.width;
const pct = Math.min(Math.max(rawPct, 0.02), 0.98);
beforeWrap.style.width = (pct * 100) + '%';
handle.style.left = (pct * 100) + '%';
}
handle.addEventListener('mousedown', () => { dragging = true; });
handle.addEventListener('touchstart', () => { dragging = true; }, { passive: true });
window.addEventListener('mousemove', e => { if (dragging) setPosition(e.clientX); });
window.addEventListener('touchmove', e => { if (dragging) setPosition(e.touches[0].clientX); }, { passive: true });
window.addEventListener('mouseup', () => { dragging = false; });
window.addEventListener('touchend', () => { dragging = false; });
slider.addEventListener('click', e => setPosition(e.clientX));
}
function initBASliders() {
document.querySelectorAll('.ba-slider').forEach(initBASlider);
}
/* --- Smooth scroll for anchor links --------------------- */
function initSmoothScroll() {
document.querySelectorAll('a[href^="#"]').forEach(a => {
a.addEventListener('click', e => {
const id = a.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (!target) return;
e.preventDefault();
const headerH = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--header-h')) || 72;
const top = target.getBoundingClientRect().top + window.scrollY - headerH - 16;
window.scrollTo({ top, behavior: 'smooth' });
});
});
}
/* --- Video hero fallback -------------------------------- */
function initHeroVideo() {
const video = document.querySelector('.hero-video-wrap video');
if (!video) return;
video.play().catch(() => {
// autoplay blocked: poster image is visible; nothing to do
});
}
/* --- Testimonial auto-scroll (on mobile) ---------------- */
function initTestimonialScroll() {
const track = document.querySelector('.testimonial-track');
if (!track) return;
let isPaused = false;
track.addEventListener('mouseenter', () => { isPaused = true; });
track.addEventListener('mouseleave', () => { isPaused = false; });
// keyboard scroll within track
track.setAttribute('tabindex', '0');
}
/* --- Header scroll + mobile nav ------------------------- */
function initNav() {
var header = document.getElementById('site-header');
var mobileNav = document.getElementById('mobileNav');
var menuBtn = document.querySelector('.header-menu-btn');
var closeBtn = document.getElementById('mobileNavClose');
var overlay = document.getElementById('mobileNavOverlay');
if (!header) return;
window.addEventListener('scroll', function () {
header.classList.toggle('scrolled', window.scrollY > 40);
}, { passive: true });
function openNav() {
mobileNav.classList.add('open');
mobileNav.setAttribute('aria-hidden', 'false');
if (menuBtn) menuBtn.setAttribute('aria-expanded', 'true');
document.body.style.overflow = 'hidden';
}
function closeNav() {
mobileNav.classList.remove('open');
mobileNav.setAttribute('aria-hidden', 'true');
if (menuBtn) menuBtn.setAttribute('aria-expanded', 'false');
document.body.style.overflow = '';
}
if (menuBtn) menuBtn.addEventListener('click', openNav);
if (closeBtn) closeBtn.addEventListener('click', closeNav);
if (overlay) overlay.addEventListener('click', closeNav);
}
/* --- Boot: initialize all modules ------------------------ */
function boot() {
initNav();
initScrollAnimations();
initCounters();
initFAQ();
initBASliders();
initSmoothScroll();
initHeroVideo();
initTestimonialScroll();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();