1/** 2 * Newsletter Signup Ajax Handler 3 * Handles form submission for newsletter signup forms across all pages 4 */ 5(function () { 6 'use strict'; 7 8 // Initialize when DOM is ready 9 document.addEventListener('DOMContentLoaded', function () { 10 initNewsletterForm(); 11 }); 12 13 function initNewsletterForm() { 14 const form = document.querySelector('.newsletter-form'); 15 if (!form) return; // No newsletter form on this page 16 17 const emailInput = form.querySelector('input[name="email"]'); // More flexible selector 18 const submitBtn = form.querySelector('.newsletter-btn'); 19 const btnText = submitBtn.querySelector('.newsletter-btn-text'); 20 const btnLoading = submitBtn.querySelector('.newsletter-btn-loading'); 21 const errorContainer = document.getElementById('newsletter-error'); 22 const successContainer = document.getElementById('newsletter-success'); 23 24 // Handle form submission 25 form.addEventListener('submit', function (e) { 26 e.preventDefault(); 27 handleFormSubmission(); 28 }); 29 30 // Clear errors when user starts typing 31 emailInput.addEventListener('input', function () { 32 clearMessages(); 33 emailInput.classList.remove('is-invalid'); 34 }); 35 36 function handleFormSubmission() { 37 // Get form data 38 const formData = new FormData(form); 39 const ajaxUrl = form.dataset.ajaxUrl; 40 41 if (!ajaxUrl) { 42 console.error('Newsletter form missing data-ajax-url attribute'); 43 showError('Configuration error. Please try again later.'); 44 return; 45 } 46 47 // Validate email client-side first 48 const email = emailInput.value.trim(); 49 if (!email) { 50 showError('Please enter your email address.'); 51 emailInput.focus(); 52 return; 53 } 54 55 if (!isValidEmail(email)) { 56 showError('Please enter a valid email address.'); 57 emailInput.focus(); 58 return; 59 } 60 61 // Show loading state 62 setLoadingState(true); 63 clearMessages(); 64 65 // Submit form via Ajax 66 fetch(ajaxUrl, { 67 method: 'POST', 68 body: formData, 69 headers: { 70 'X-Requested-With': 'XMLHttpRequest', 71 'X-CSRFToken': getCSRFToken() 72 }, 73 credentials: 'same-origin' 74 }) 75 .then(response => { 76 if (!response.ok) { 77 throw new Error(`HTTP ${response.status}: ${response.statusText}`); 78 } 79 return response.json(); 80 }) 81 .then(data => { 82 handleResponse(data); 83 }) 84 .catch(error => { 85 console.error('Newsletter signup error:', error); 86 showError('Something went wrong. Please try again later.'); 87 }) 88 .finally(() => { 89 setLoadingState(false); 90 }); 91 } 92 93 function handleResponse(data) { 94 if (data.success) { 95 // Success - clear form and show success message 96 form.reset(); 97 showSuccess(data.message || 'Thank you for subscribing!'); 98 99 // Optional: Track conversion for analytics 100 if (typeof gtag !== 'undefined') { 101 gtag('event', 'newsletter_signup', { 102 'event_category': 'engagement', 103 'event_label': 'footer_newsletter' 104 }); 105 } 106 } else { 107 // Handle errors 108 if (data.form_errors && data.form_errors.email) { 109 showError(data.form_errors.email); 110 emailInput.classList.add('is-invalid'); 111 } else { 112 showError(data.error || 'Something went wrong. Please try again.'); 113 } 114 115 // Focus back to input for better UX 116 emailInput.focus(); 117 } 118 } 119 120 function setLoadingState(loading) { 121 if (loading) { 122 submitBtn.disabled = true; 123 btnText.classList.add('d-none'); 124 btnLoading.classList.remove('d-none'); 125 emailInput.disabled = true; 126 } else { 127 submitBtn.disabled = false; 128 btnText.classList.remove('d-none'); 129 btnLoading.classList.add('d-none'); 130 emailInput.disabled = false; 131 } 132 } 133 134 function showError(message) { 135 clearMessages(); 136 errorContainer.textContent = message; 137 errorContainer.classList.remove('d-none'); 138 139 // Scroll error into view if needed 140 if (!isElementInViewport(errorContainer)) { 141 errorContainer.scrollIntoView({ 142 behavior: 'smooth', 143 block: 'center' 144 }); 145 } 146 } 147 148 function showSuccess(message) { 149 clearMessages(); 150 successContainer.textContent = message; 151 successContainer.classList.remove('d-none'); 152 153 // Auto-hide success message after 5 seconds 154 setTimeout(() => { 155 successContainer.classList.add('d-none'); 156 }, 5000); 157 } 158 159 function clearMessages() { 160 errorContainer.classList.add('d-none'); 161 successContainer.classList.add('d-none'); 162 errorContainer.textContent = ''; 163 successContainer.textContent = ''; 164 } 165 166 function isValidEmail(email) { 167 // Basic email validation - Django form will do the heavy lifting 168 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 169 return emailRegex.test(email); 170 } 171 172 function getCSRFToken() { 173 // Get CSRF token from form 174 const csrfInput = form.querySelector('[name="csrfmiddlewaretoken"]'); 175 if (csrfInput) { 176 return csrfInput.value; 177 } 178 179 // Fallback: get from cookie 180 const cookies = document.cookie.split(';'); 181 for (let cookie of cookies) { 182 const [name, value] = cookie.trim().split('='); 183 if (name === 'csrftoken') { 184 return value; 185 } 186 } 187 188 console.warn('CSRF token not found'); 189 return ''; 190 } 191 192 function isElementInViewport(element) { 193 const rect = element.getBoundingClientRect(); 194 return ( 195 rect.top >= 0 && 196 rect.left >= 0 && 197 rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && 198 rect.right <= (window.innerWidth || document.documentElement.clientWidth) 199 ); 200 } 201 } 202})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.