1class IpaModal { 2 constructor() { 3 this.modal = document.getElementById('ipaModal'); 4 this.closeBtn = document.querySelector('.ipa-close'); 5 this.termLoanIpaButton = document.getElementById('termLoanIpaButton'); 6 this.form = document.getElementById('ipaForm'); 7 this.submitBtn = this.form.querySelector('button[type="submit"]'); 8 this.originalButtonText = this.submitBtn ? this.submitBtn.innerHTML : 'Submit'; 9 this.isInIframe = window !== window.parent; 10 11 this.modal.style.display = 'none'; 12 13 this.initializeEvents(); 14 } 15 16 initializeEvents() { 17 // Open modal event 18 this.termLoanIpaButton.addEventListener('click', () => { 19 this.openModal(); 20 }); 21 22 // Close modal events 23 this.closeBtn.addEventListener('click', () => this.closeModal()); 24 window.addEventListener('click', (e) => { 25 if (e.target === this.modal) this.closeModal(); 26 }); 27 28 // Form submission 29 this.form.addEventListener('submit', (e) => this.handleSubmit(e)); 30 31 // Handle mobile viewport adjustments 32 if (this.isInIframe) { 33 window.addEventListener('resize', () => this.handleResize()); 34 } 35 } 36 37 handleResize() { 38 if (this.modal.style.display === 'block') { 39 this.adjustModalPosition(); 40 } 41 } 42 43 adjustModalPosition() { 44 const modalContent = this.modal.querySelector('.ipa-modal-content'); 45 46 if (window.innerWidth <= 768) { 47 this.modal.style.top = '0'; 48 this.modal.style.height = '100%'; 49 this.modal.style.overflow = 'auto'; 50 51 if (modalContent) { 52 modalContent.style.marginTop = '20px'; 53 modalContent.style.marginBottom = '20px'; 54 modalContent.style.maxHeight = 'none'; 55 } 56 } else { 57 this.modal.style.top = ''; 58 this.modal.style.height = ''; 59 this.modal.style.overflow = ''; 60 61 if (modalContent) { 62 modalContent.style.marginTop = ''; 63 modalContent.style.marginBottom = ''; 64 modalContent.style.maxHeight = ''; 65 } 66 } 67 } 68 69 openModal() { 70 if (this.isInIframe) { 71 window.parent.postMessage({ 72 type: 'showModal' 73 }, '*'); 74 75 this.modal.style.display = 'block'; 76 this.adjustModalPosition(); 77 } else { 78 this.modal.style.display = 'block'; 79 } 80 } 81 82 closeModal() { 83 if (this.isInIframe) { 84 window.parent.postMessage({ 85 type: 'closeModal' 86 }, '*'); 87 } 88 89 this.modal.style.display = 'none'; 90 const modalContent = this.modal.querySelector('.ipa-modal-content'); 91 if (modalContent) { 92 modalContent.style.marginTop = ''; 93 modalContent.style.marginBottom = ''; 94 modalContent.style.maxHeight = ''; 95 } 96 97 this.form.reset(); 98 this.clearErrors(); 99 } 100 101 validateForm() { 102 let isValid = true; 103 const name = document.getElementById('name'); 104 const email = document.getElementById('emailAddress'); 105 const mobile = document.getElementById('mobileNumber'); 106 107 if (!name.value.trim()) { 108 this.showError(name, 'nameError', 'Name is required'); 109 isValid = false; 110 } 111 112 if (!email.value.trim()) { 113 this.showError(email, 'emailError', 'Email is required'); 114 isValid = false; 115 } else if (!/\S+@\S+\.\S+/.test(email.value)) { 116 this.showError(email, 'emailError', 'Please enter a valid email address'); 117 isValid = false; 118 } 119 120 if (!mobile.value.trim()) { 121 this.showError(mobile, 'mobileError', 'Mobile number is required'); 122 isValid = false; 123 } else if (!/^[89]\d{7}$/.test(mobile.value)) { 124 this.showError(mobile, 'mobileError', 'Please enter a valid Singapore mobile number'); 125 isValid = false;
126 } 127 128 return isValid; 129 } 130 131 showError(input, errorId, message) { 132 const errorElement = document.getElementById(errorId); 133 input.classList.add('error'); 134 errorElement.textContent = message; 135 errorElement.style.display = 'block'; 136 } 137 138 clearErrors() { 139 document.querySelectorAll('.ipa-error').forEach(error => { 140 error.style.display = 'none'; 141 }); 142 document.querySelectorAll('.ipa-input-group input').forEach(input => { 143 input.classList.remove('error'); 144 }); 145 } 146 147 getLoanDetails() { 148 return { 149 averageAge: document.getElementById('averageAge').value, 150 propertyValue: document.getElementById('propertyValue').value, 151 cpfUsed: document.getElementById('cpfUsed').value, 152 yearsSincePurchase: document.getElementById('yearsSincePurchase').value, 153 outstandingHomeLoan: document.getElementById('outstandingHomeLoan').value, 154 outstandingTermLoan: document.getElementById('outstandingTermLoan').value, 155 additionalTermLoan: document.getElementById('additionalTermLoan').value, 156 homeLoanTenure: document.getElementById('homeLoanTenure').value, 157 termLoanTenure: document.getElementById('termLoanTenure').value, 158 interestRate: document.getElementById('interestRate').value, 159 eligibleLoanAmount: document.getElementById('eligibleLoanValue').textContent, 160 homeLoanMonthlyInstalment: document.getElementById('homeLoanMonthlyValue').textContent, 161 termLoanMonthlyInstalment: document.getElementById('termLoanMonthlyValue').textContent 162 }; 163 } 164 165 showNotification(type = 'success', title = 'Success!', message = 'Thank you! Your report is being processed and will arrive in your email shortly.') { 166 if (this.isInIframe) { 167 window.parent.postMessage({ 168 type: 'showNotification', 169 notification: { 170 type: type, 171 title: title, 172 message: message, 173 icon: type === 'success' ? 174 `<svg viewBox="0 0 24 24" width="24" height="24"> 175 <path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm4.59-12.42L10 14.17l-2.59-2.58L6 13l4 4 8-8z"/> 176 </svg>` : 177 `<svg viewBox="0 0 24 24" width="24" height="24"> 178 <path fill="#EF4444" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/> 179 </svg>` 180 } 181 }, '*'); 182 } 183 } 184 185 showLoading() { 186 if (this.submitBtn) { 187 this.submitBtn.disabled = true; 188 this.submitBtn.innerHTML = 'Almost there! Preparing your report...'; 189 } 190 } 191 192 hideLoading() { 193 if (this.submitBtn) { 194 this.submitBtn.disabled = false; 195 this.submitBtn.innerHTML = this.originalButtonText; 196 } 197 } 198 199async handleSubmit(e) { 200 e.preventDefault(); 201 if (this.validateForm()) { 202 this.showLoading(); 203 204 try { 205 const formData = new FormData(this.form);
206 const loanDetails = this.getLoanDetails(); 207 const userName = formData.get('name'); 208 const userEmail = formData.get('emailAddress'); 209 210 // Create temporary container 211 const captureContainer = document.createElement('div'); 212 captureContainer.className = 'pdf-capture-container'; 213 214 captureContainer.innerHTML = ` 215 <div class="pdf-header"> 216 <div class="company-info"> 217 <h1>The Loan Connection</h1> 218 <h2>Getting The Right Mortgage</h2> 219 </div> 220 <div class="user-info"> 221 <p><strong>Prepared for:</strong> ${userName || 'Valued Customer'}</p> 222 <p><strong>Email:</strong> ${userEmail || 'N/A'}</p> 223 <p><strong>Generated on:</strong> ${new Date().toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })}</p> 224 </div> 225 </div> 226 `; 227 228 // Clone results section 229 const resultsSection = document.querySelector('.results-section'); 230 const resultsClone = resultsSection.cloneNode(true); 231 232 // Remove hidden elements but keep sliders 233 resultsClone.querySelectorAll('.hidden').forEach(el => { 234 if (!el.classList.contains('slider')) { 235 el.remove(); 236 } 237 }); 238 239 // Make all elements visible 240 resultsClone.querySelectorAll('*').forEach(el => { 241 if (window.getComputedStyle(el).display === 'none' && !el.classList.contains('slider')) { 242 el.style.display = 'block'; 243 } 244 }); 245 246 // Update additional term loan value in the clone 247 const additionalTermLoanClone = resultsClone.querySelector('#additionalTermLoan'); 248 const originalAdditionalTermLoan = document.getElementById('additionalTermLoan'); 249 if (additionalTermLoanClone && originalAdditionalTermLoan) { 250 const formattedValue = originalAdditionalTermLoan.value; 251 additionalTermLoanClone.value = formattedValue; 252 additionalTermLoanClone.placeholder = formattedValue; 253 additionalTermLoanClone.style.display = 'block'; 254 additionalTermLoanClone.style.visibility = 'visible'; 255 additionalTermLoanClone.style.opacity = '1'; 256 } 257 258 captureContainer.appendChild(resultsClone); 259 260 captureContainer.innerHTML += ` 261 <div class="pdf-footer"> 262 <hr> 263 <p>This report was generated on ${new Date().toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })}</p> 264 <p>For any questions, please contact our mortgage specialists at <a href="mailto:[email protected]">[email protected]</a></p> 265 </div> 266 `; 267 268 document.body.appendChild(captureContainer); 269 270 const width = Math.max(resultsClone.scrollWidth, 800); 271 const height = Math.max(resultsClone.scrollHeight + 200, 1800); 272 273 captureContainer.style.width = `${width}px`; 274 275 const canvas = await html2canvas(captureContainer, { 276 scale: 2, 277 useCORS: true, 278 logging: false, 279 backgroundColor: '#ffffff', 280 width: width, 281 height: height, 282 windowWidth: width, 283 windowHeight: height 284 }); 285 286 document.body.removeChild(captureContainer); 287 288 const { jsPDF } = window.jspdf; 289 const pdf = new jsPDF({ 290 orientation: width > height ? 'landscape' : 'portrait', 291 unit: 'px', 292 format: [width, height] 293 }); 294 295 const imgData = canvas.toDataURL('image/jpeg', 1.0); 296 pdf.addImage(imgData, 'JPEG', 0, 0, width, height, '', 'FAST'); 297 298 const pdfBase64 = pdf.output('datauristring').split(',')[1]; 299 300 // Prepare submission data 301 const submissionData = { 302 timestamp: new Date().toISOString(), 303 name: formData.get('name'), 304 email: formData.get('emailAddress'), 305 mobile: formData.get('mobileNumber'), 306 pdfData: pdfBase64,
307 ...loanDetails 308 }; 309 310 // ââ Supabase config ââ 311 const SUPABASE_URL = 'https://uedyfcthpzjbfvcqxgkt.supabase.co'; 312 const SUPABASE_SERVICE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InVlZHlmY3RocHpqYmZ2Y3F4Z2t0Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3ODYzNzI2MywiZXhwIjoyMDk0MjEzMjYzfQ.j7IjJV1stke3TkWSobxd0BWzyeNC-CDE_taNXsunvcw'; 313 314 // Build GS form payload 315 const scriptURL = 'https://script.google.com/macros/s/AKfycbz406ljW85NuJHps0EjGEz0xuvz3BLJVV1XNh40S5fAJeLfWaEFkFLj4MFuFn6iVi-Idg/exec'; 316 const gsForm = new FormData(); 317 Object.keys(submissionData).forEach(key => { 318 gsForm.append(key, submissionData[key]); 319 }); 320 321 const privyrPayload = { 322 name: formData.get('name') || '', 323 phone: formData.get('mobileNumber') || '', 324 email: formData.get('emailAddress') || '', 325 source: 'Equity Loan Calculator' 326 }; 327
328 const [gsResult, privyrResult, supabaseResult] = await Promise.allSettled([ 329 330 // 1. Google Sheets 331 fetch(scriptURL, { 332 method: 'POST', 333 mode: 'no-cors', 334 body: gsForm 335 }), 336 337 // 2. Privyr CRM 338 fetch('https://www.privyr.com/api/v1/incoming-leads/0vZfjMQw/jqnkVwwV', { 339 method: 'POST', 340 headers: { 'Content-Type': 'application/json' }, 341 body: JSON.stringify(privyrPayload) 342 }), 343 344 // 3. Supabase 345 fetch(`${SUPABASE_URL}/rest/v1/leads`, { 346 method: 'POST', 347 headers: { 348 'Content-Type': 'application/json', 349 'apikey': SUPABASE_SERVICE_KEY, 350 'Authorization': `Bearer ${SUPABASE_SERVICE_KEY}`, 351 }, 352 body: JSON.stringify({ 353 name: formData.get('name') || '', 354 email: (formData.get('emailAddress') || '').toLowerCase(), 355 phone: formData.get('mobileNumber') || '', 356 source: 'equity-loan-calculator', 357 }) 358 }) 359 360 ]); 361 362 // Log results 363 console.log('[TLC] GS:', gsResult.status); 364 console.log('[TLC] Privyr:', privyrResult.status); 365 366 if (supabaseResult.status === 'fulfilled') { 367 const res = supabaseResult.value; 368 const text = await res.text(); 369 console.log('[TLC] Supabase HTTP:', res.status); 370 console.log('[TLC] Supabase body:', text); 371 } else { 372 console.error('[TLC] Supabase error:', supabaseRe
372sult.reason); 373 } 374 375 this.hideLoading(); 376 this.closeModal(); 377 378 this.showNotification( 379 'success', 380 'Success!', 381 'Thank you! Your report is being processed and will arrive in your email shortly.' 382 ); 383 384 } catch (error) { 385 console.error('Error submitting form:', error); 386 this.hideLoading(); 387 388 this.showNotification( 389 'error', 390 'Error', 391 'Sorry, there was an error submitting your form. Please try again.' 392 ); 393 } 394 } 395} 396} 397 398document.addEventListener('DOMContentLoaded', () => { 399 new IpaModal(); 400});
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.