PageSourceSearch

https://term-loan.vercel.app/modal.js

js term-loan.vercel.app collected 2026-10-03 07:51:25 UTC 15,880 bytes, 400 lines download raw bytes

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.