175 lines
		
	
	
		
			No EOL
		
	
	
		
			5.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			175 lines
		
	
	
		
			No EOL
		
	
	
		
			5.3 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!');
 | |
|     };
 | |
|   })();  
 | 
