175 lines
		
	
	
	
		
			4.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			175 lines
		
	
	
	
		
			4.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| (() => {
 | |
|   'use strict';
 | |
| 
 | |
|   const navbar = document.querySelector('.navbar');
 | |
|   const featureCards = document.querySelectorAll('.feature-card');
 | |
|   const newsletterCards = document.querySelectorAll('.newsletter-card');
 | |
|   const progressBar = document.querySelector('.reading-progress');
 | |
|   const emailInput = document.getElementById('email-input');
 | |
|   const notifyBtn = document.getElementById('notify-button');
 | |
|   const emailRE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
 | |
| 
 | |
|   document.addEventListener(
 | |
|     'click',
 | |
|     (e) => {
 | |
|       const a = e.target.closest('a[href^="#"]');
 | |
|       if (!a) return;
 | |
| 
 | |
|       const href = a.getAttribute('href');
 | |
|       if (!href || href === '#') return;
 | |
| 
 | |
|       const target = document.querySelector(href);
 | |
|       if (!target) return;
 | |
| 
 | |
|       e.preventDefault();
 | |
|       target.scrollIntoView({ behavior: 'smooth', block: 'start' });
 | |
|     },
 | |
|     { passive: false }
 | |
|   );
 | |
| 
 | |
|   if ('IntersectionObserver' in window) {
 | |
|     const observer = new IntersectionObserver(
 | |
|       (entries, obs) => {
 | |
|         for (const entry of entries) {
 | |
|           if (entry.isIntersecting) {
 | |
|             entry.target.classList.add('is-visible');
 | |
|             obs.unobserve(entry.target);
 | |
|           }
 | |
|         }
 | |
|       },
 | |
|       { threshold: 0.1, rootMargin: '0px 0px -50px 0px' }
 | |
|     );
 | |
| 
 | |
|     featureCards.forEach((card) => {
 | |
|       card.classList.add('will-animate');
 | |
|       observer.observe(card);
 | |
|     });
 | |
|   } else {
 | |
|     featureCards.forEach((card) => card.classList.add('is-visible'));
 | |
|   }
 | |
| 
 | |
|   window.addEventListener('load', () => {
 | |
|     document
 | |
|       .querySelectorAll('.newsletter-header, .newsletter-content')
 | |
|       .forEach((el, i) => {
 | |
|         el.style.transitionDelay = `${i * 0.2}s`;
 | |
|         el.classList.add('is-visible');
 | |
|       });
 | |
| 
 | |
|     newsletterCards.forEach((card, i) => {
 | |
|       card.style.transitionDelay = `${i * 0.1}s`;
 | |
|       card.classList.add('is-visible');
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   let lastY = 0;
 | |
|   let ticking = false;
 | |
| 
 | |
|   function onScroll() {
 | |
|     lastY = window.scrollY || window.pageYOffset;
 | |
|     if (!ticking) {
 | |
|       requestAnimationFrame(updateOnScroll);
 | |
|       ticking = true;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function updateOnScroll() {
 | |
|     if (navbar) {
 | |
|       navbar.classList.toggle('navbar--scrolled', lastY > 50);
 | |
|       navbar.classList.toggle('navbar--deeper', lastY > 100);
 | |
|     }
 | |
| 
 | |
|     if (progressBar) {
 | |
|       const max = document.body.scrollHeight - window.innerHeight;
 | |
|       const progress = max > 0 ? Math.min(Math.max(lastY / max, 0), 1) : 0;
 | |
|       progressBar.style.width = `${progress * 100}%`;
 | |
|     }
 | |
| 
 | |
|     ticking = false;
 | |
|   }
 | |
| 
 | |
|   window.addEventListener('scroll', onScroll, { passive: true });
 | |
|   updateOnScroll(); // initial state
 | |
| 
 | |
|   if (notifyBtn && emailInput) {
 | |
|     let inFlight = false;
 | |
|     const controller = new AbortController();
 | |
| 
 | |
|     notifyBtn.addEventListener('click', async () => {
 | |
|       const email = emailInput.value.trim();
 | |
|       if (!emailRE.test(email)) {
 | |
|         alert('Please enter a valid email address.');
 | |
|         emailInput.focus();
 | |
|         return;
 | |
|       }
 | |
|       if (inFlight) return;
 | |
| 
 | |
|       inFlight = true;
 | |
|       notifyBtn.disabled = true;
 | |
| 
 | |
|       try {
 | |
|         const res = await fetch('/subscribe', {
 | |
|           method: 'POST',
 | |
|           headers: { 'Content-Type': 'application/json' },
 | |
|           body: JSON.stringify({ email }),
 | |
|           signal: controller.signal,
 | |
|         });
 | |
| 
 | |
|         let message = 'Thank you for subscribing!';
 | |
|         if (res.ok) {
 | |
|           const data = await res.json().catch(() => ({}));
 | |
|           message = data.message || message;
 | |
|         } else {
 | |
|           message = "Thanks! We'll notify you when we launch.";
 | |
|         }
 | |
| 
 | |
|         alert(message);
 | |
|         emailInput.value = '';
 | |
|       } catch (err) {
 | |
|         console.error('Subscribe error:', err);
 | |
|         alert("Thanks! We'll notify you when we launch.");
 | |
|         emailInput.value = '';
 | |
|       } finally {
 | |
|         notifyBtn.disabled = false;
 | |
|         inFlight = false;
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     window.addEventListener('beforeunload', () => controller.abort(), {
 | |
|       passive: true,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   window.shareNewsletter = async function shareNewsletter() {
 | |
|     try {
 | |
|       if (navigator.share) {
 | |
|         await navigator.share({
 | |
|           title: document.title,
 | |
|           text: 'Check out this newsletter from RideAware',
 | |
|           url: location.href,
 | |
|         });
 | |
|         return;
 | |
|       }
 | |
|     } catch (err) {
 | |
|       console.warn('navigator.share error/cancel:', err);
 | |
|     }
 | |
| 
 | |
|     if (navigator.clipboard && window.isSecureContext) {
 | |
|       try {
 | |
|         await navigator.clipboard.writeText(location.href);
 | |
|         alert('Newsletter URL copied to clipboard!');
 | |
|         return;
 | |
|       } catch {
 | |
|         /* fall through */
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     const tmp = document.createElement('input');
 | |
|     tmp.value = location.href;
 | |
|     document.body.appendChild(tmp);
 | |
|     tmp.select();
 | |
|     document.execCommand('copy');
 | |
|     document.body.removeChild(tmp);
 | |
|     alert('Newsletter URL copied to clipboard!');
 | |
|   };
 | |
| })();
 | 
