/* ============================================================ ZS RECYCLING — Main JS (nav, hamburger, scroll effects) ============================================================ */ (function () { 'use strict'; // ---- Nav scroll effect ---- const nav = document.querySelector('.site-nav'); if (nav) { window.addEventListener('scroll', function () { if (window.scrollY > 20) { nav.classList.add('scrolled'); } else { nav.classList.remove('scrolled'); } }, { passive: true }); } // ---- Active nav link ---- const currentPath = window.location.pathname.replace(/\/$/, ''); document.querySelectorAll('.nav-links a, .mobile-nav a').forEach(function (link) { const href = link.getAttribute('href'); if (!href) return; const linkPath = href.replace(/\/$/, ''); if ( currentPath === linkPath || (linkPath !== '' && currentPath.endsWith(linkPath)) ) { link.classList.add('active'); } }); // ---- Hamburger / mobile nav ---- const hamburger = document.querySelector('.hamburger'); const mobileNav = document.querySelector('.mobile-nav'); if (hamburger && mobileNav) { hamburger.addEventListener('click', function () { const isOpen = mobileNav.classList.contains('open'); hamburger.classList.toggle('open', !isOpen); mobileNav.classList.toggle('open', !isOpen); hamburger.setAttribute('aria-expanded', String(!isOpen)); document.body.style.overflow = isOpen ? '' : 'hidden'; }); // Close on mobile nav link click mobileNav.querySelectorAll('a').forEach(function (link) { link.addEventListener('click', function () { hamburger.classList.remove('open'); mobileNav.classList.remove('open'); hamburger.setAttribute('aria-expanded', 'false'); document.body.style.overflow = ''; }); }); // Close on outside click document.addEventListener('click', function (e) { if ( mobileNav.classList.contains('open') && !mobileNav.contains(e.target) && !hamburger.contains(e.target) ) { hamburger.classList.remove('open'); mobileNav.classList.remove('open'); hamburger.setAttribute('aria-expanded', 'false'); document.body.style.overflow = ''; } }); } // ---- Animate elements on scroll ---- const fadeEls = document.querySelectorAll( '.card, .stat-card, .service-card, .item-card, .cert-grid-item, .about-block' ); if ('IntersectionObserver' in window && fadeEls.length > 0) { const fadeObserver = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { entry.target.style.opacity = '1'; entry.target.style.transform = 'translateY(0)'; fadeObserver.unobserve(entry.target); } }); }, { threshold: 0.12 }); fadeEls.forEach(function (el, i) { el.style.opacity = '0'; el.style.transform = 'translateY(24px)'; el.style.transition = 'opacity 0.55s ease ' + (i % 4 * 0.08) + 's, transform 0.55s ease ' + (i % 4 * 0.08) + 's'; fadeObserver.observe(el); }); } // ---- Stat counter animation ---- function animateCounter(el, target, prefix, suffix) { const duration = 1800; const start = performance.now(); const isFloat = target % 1 !== 0; function step(now) { const elapsed = now - start; const progress = Math.min(elapsed / duration, 1); const eased = 1 - Math.pow(1 - progress, 3); const value = eased * target; el.textContent = prefix + (isFloat ? value.toFixed(1) : Math.round(value).toLocaleString()) + suffix; if (progress < 1) requestAnimationFrame(step); } requestAnimationFrame(step); } const statEls = document.querySelectorAll('[data-count]'); if ('IntersectionObserver' in window && statEls.length > 0) { const countObserver = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { const el = entry.target; const target = parseFloat(el.dataset.count); const prefix = el.dataset.prefix || ''; const suffix = el.dataset.suffix || ''; animateCounter(el, target, prefix, suffix); countObserver.unobserve(el); } }); }, { threshold: 0.5 }); statEls.forEach(function (el) { countObserver.observe(el); }); } // ---- Contact form: validation + loading + banners ---- (function () { var contactForm = document.querySelector('.contact-form'); if (!contactForm) return; var submitBtn = contactForm.querySelector('.form-submit'); var originalBtnHTML = submitBtn ? submitBtn.innerHTML : ''; // ── Show notification banners from URL params ──────────── var params = new URLSearchParams(window.location.search); if (params.has('success') || params.has('error')) { var type = params.has('success') ? 'success' : 'error'; var banner = document.getElementById('form-' + type); if (banner) { banner.style.display = 'block'; requestAnimationFrame(function () { banner.classList.add('show'); }); // Scroll to banner setTimeout(function () { banner.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 100); } // Clean URL without page reload var url = new URL(window.location); url.searchParams.delete('success'); url.searchParams.delete('error'); window.history.replaceState({}, '', url); } // ── Client-side validation on submit ───────────────────── contactForm.addEventListener('submit', function (e) { var valid = true; var firstInvalid = null; // Reset previous error states contactForm.querySelectorAll('[required]').forEach(function (field) { field.style.borderColor = ''; var errorEl = field.parentNode.querySelector('.field-error'); if (errorEl) errorEl.remove(); }); // Validate each required field contactForm.querySelectorAll('[required]').forEach(function (field) { if (!field.value.trim()) { field.style.borderColor = '#e74c3c'; valid = false; if (!firstInvalid) firstInvalid = field; // Show inline error var error = document.createElement('span'); error.className = 'field-error'; error.style.cssText = 'color:#e74c3c;font-size:0.75rem;margin-top:0.25rem;display:block;'; var label = field.parentNode.querySelector('label'); var fieldName = label ? label.textContent.replace('*', '').trim() : 'This field'; error.textContent = fieldName + ' is required.'; field.parentNode.appendChild(error); } }); if (!valid) { e.preventDefault(); if (firstInvalid) firstInvalid.focus(); return; } // ── Show loading state ───────────────────────────────── if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = ' Sending\u2026'; } }); })(); })();