1// app-helpers.js â Application helper functions 2// Moved from inline <script> blocks in index.html for CSP compliance. 3// This file MUST be loaded synchronously (no async/defer) because: 4// ⢠onRecaptchaApiLoad must be defined before reCAPTCHA's onload fires 5// ⢠downloadFile/geoHelper must be available before Blazor components call them 6 7// ââ reCAPTCHA ready signal âââââââââââââââââââââââââââââââââââââââââ 8// Referenced by: <script src="...recaptcha/api.js?...&onload=onRecaptchaApiLoad"> 9function onRecaptchaApiLoad() { 10 window._recaptchaReady = true; 11} 12 13// ââ File download helper âââââââââââââââââââââââââââââââââââââââââââ 14window.downloadFile = function (filename, contentType, base64Data) { 15 var link = document.createElement('a'); 16 link.href = 'data:' + contentType + ';base64,' + base64Data; 17 link.download = filename; 18 document.body.appendChild(link); 19 link.click(); 20 document.body.removeChild(link); 21}; 22 23// ââ Invoice print helper âââââââââââââââââââââââââââââââââââââââââââ 24// Opens invoice HTML in a new tab (bypasses auth header requirement) 25window.openInvoiceInNewTab = function (html) { 26 var win = window.open('', '_blank'); 27 if (win) { 28 win.document.open(); 29 win.document.write(html); 30 win.document.close(); 31 } else { 32 alert('Popup blocked. Please allow popups for this site to print invoices.'); 33 } 34}; 35 36// ââ Geo detection ââââââââââââââââââââââââââââââââââââââââââââââââââ 37// Called by CurrencyService.InitializeAsync() to get the user's real country. 38// Runs in the BROWSER so it gets the actual public IP, not the server's IP. 39// Uses 4 independent sources with individual timeouts. 40window.geoHelper = { 41 _cached: null, 42 43 _fetchWithTimeout: async function(url, timeoutMs) { 44 var ctrl = new AbortController(); 45 var timer = setTimeout(function() { ctrl.abort(); }, timeoutMs); 46 try { 47 var r = await fetch(url, { cache: 'no-store', signal: ctrl.signal }); 48 return r; 49 } finally { 50 clearTimeout(timer); 51 } 52 }, 53 54 getCountryCode: async function () { 55 if (window.geoHelper._cached) return window.geoHelper._cached; 56 57 // Source 1: Cloudflare CDN trace 58 try { 59 var r = await window.geoHelper._fetchWithTimeout( 60 'https://www.cloudflare.com/cdn-cgi/trace', 2000); 61 var text = await r.text(); 62 var loc = text.split('\n').find(function(l) { return l.startsWith('loc='); }); 63 if (loc) { 64 var code = loc.split('=')[1].trim().toUpperCase(); 65 if (code && code.length === 2 && code !== 'XX' && code !== 'T1') { 66 window.geoHelper._cached = code; 67 console.log('[GeoHelper] Source 1 (Cloudflare trace):', code); 68 return code; 69 } 70 } 71 } catch(e) { console.warn('[GeoHelper] Source 1 failed:', e.message); } 72 73 // Source 2: ip-api.com 74 try { 75 var r2 = await window.geoHelper._fetchWithTimeout( 76 'http://ip-api.com/json/?fields=countryCode,status', 2500); 77 var j2 = await r2.json(); 78 if (j2 && j2.status === 'success' && j2.countryCode && j2.countryCode.length === 2) { 79 var code2 = j2.countryCode.toUpperCase(); 80 window.geoHelper._cached = code2; 81 console.log('[GeoHelper] Source 2 (ip-api.com):', code2); 82 return code2; 83 } 84 } catch(e) { console.warn('[GeoHelper] Source 2 failed:', e.message); } 85 86 // Source 3: ipapi.co 87 try { 88 var r3 = await window.geoHelper._fetchWithTimeout( 89 'https://ipapi.co/country/', 2500); 90 var code3 = (await r3.text()).trim().toUpperCase(); 91 if (code3 && code3.length === 2 && code3.indexOf('<') === -1) { 92 window.geoHelper._cached = code3; 93 console.log('[GeoHelper] Source 3 (ipapi.co):', code3); 94 return code3; 95 } 96 } catch(e) { console.warn('[GeoHelper] Source 3 failed:', e.message); } 97 98 // Source 4: freeipapi.com 99 try { 100 var r4 = await window.geoHelper._fetchWithTimeout( 101 'https://freeipapi.com/api/json', 2500); 102 var j4 = await r4.json(); 103 if (j4 && j4.countryCode && j4.countryCode.length === 2) { 104 var code4 = j4.countryCode.toUpperCase(); 105 window.geoHelper._cached = code4; 106 console.log('[GeoHelper] Source 4 (freeipapi.com):', code4); 107 return code4; 108 } 109 } catch(e) { console.warn('[GeoHelper] Source 4 failed:', e.message); } 110 111 console.warn('[GeoHelper] All sources failed â server will detect from IP headers'); 112 return null; 113 } 114}; 115 116// ââ Home page animations âââââââââââââââââââââââââââââââââââââââââââ 117// Idempotent, safe to call multiple times as async data loads 118window.homeAnimations = function () { 119 120 // Typing animation â init once only 121 if (!window._hpTypingStarted) { 122 var typingEl = document.getElementById('hp-typing-text'); 123 if (typingEl) { 124 window._hpTypingStarted = true; 125 var words = ['Simple', 'Practical', 'Fun']; 126 var wordIdx = 0, charIdx = 0, isDeleting = false;
127 function type() { 128 var word = words[wordIdx]; 129 typingEl.textContent = isDeleting 130 ? word.substring(0, charIdx - 1) 131 : word.substring(0, charIdx + 1); 132 isDeleting ? charIdx-- : charIdx++; 133 var speed = isDeleting ? 80 : 150; 134 if (!isDeleting && charIdx === word.length) { speed = 1800; isDeleting = true; } 135 else if (isDeleting && charIdx === 0) { isDeleting = false; wordIdx = (wordIdx + 1) % words.length; speed = 600; } 136 setTimeout(type, speed); 137 } 138 setTimeout(type, 1200); 139 } 140 } 141 142 // Credential counters + 3D tilt 143 (function initCredAnimations() { 144 var firstCard = document.querySelector('.hp-cred .card'); 145 if (!firstCard) return; 146 147 ['hp-num-20', 'hp-num-17', 'hp-num-mil'].forEach(function(id) { 148 var el = document.getElementById(id); 149 if (el) delete el.dataset.counted; 150 }); 151 152 var n20 = document.getElementById('hp-num-20'); 153 if (n20) n20.textContent = '0+'; 154 var n17 = document.getElementById('hp-num-17'); 155 if (n17) n17.textContent = '0+'; 156 var nm = document.getElementById('hp-num-mil'); 157 if (nm) nm.textContent = '1M+'; 158 159 if (window._hpCredObs) { try { window._hpCredObs.disconnect(); } catch (e) {} } 160 161 var credObs = new IntersectionObserver(function(entries) { 162 entries.forEach(function(e) { 163 if (!e.isIntersecting) return; 164 credObs.unobserve(e.target); 165 function countUp(el, target, suffix, duration) { 166 if (!el || el.dataset.counted) return; 167 el.dataset.counted = '1'; 168 var current = 0, steps = 40, iv = duration / steps; 169 var t = setInterval(function() { 170 current = Math.min(current + target / steps, target); 171 el.textContent = Math.floor(current) + suffix; 172 if (current >= target) clearInterval(t); 173 }, iv); 174 } 175 setTimeout(function() { countUp(document.getElementById('hp-num-20'), 20, '+', 1500); }, 150); 176 setTimeout(function() { countUp(document.getElementById('hp-num-17'), 17, '+', 1500); }, 350); 177 setTimeout(function() { 178 var m = document.getElementById('hp-num-mil'); 179 if (m && !m.dataset.counted) { 180 m.dataset.counted = '1'; 181 var s = ['1M+','3M+','5M+','10M+','Millions'], i = 0; 182 var t = setInterval(function() { m.textContent = s[i++]; if (i >= s.length) clearInterval(t); }, 280); 183 } 184 }, 550); 185 }); 186 }, { threshold: 0.25 }); 187 window._hpCredObs = credObs; 188 credObs.observe(firstCard); 189 190 if (!window._hpCredTiltInit) { 191 window._hpCredTiltInit = true; 192 document.addEventListener('mousemove', function(e) { 193 var card = e.target.closest('.hp-cred .card'); 194 if (!card) return; 195 var r = card.getBoundingClientRect(); 196 var dx = (e.clientX - r.left) / r.width - 0.5; 197 var dy = (e.clientY - r.top) / r.height - 0.5; 198 card.style.transform = 'translateY(-10px) rotateX(' + (-dy*8) + 'deg) rotateY(' + (dx*8) + 'deg)'; 199 }); 200 document.addEventListener('mouseleave', function(e) { 201 var card = e.target && e.target.closest && e.target.closest('.hp-cred .card'); 202 if (card) card.style.transform = ''; 203 }, true); 204 } 205 })(); 206 207 // FAQ slide-in observer 208 if (window._hpFaqObs) window._hpFaqObs.disconnect(); 209 window._hpFaqObs = new IntersectionObserver(function(entries) { 210 entries.forEach(function(e) { if (e.isIntersecting) e.target.classList.add('hp-visible'); }); 211 }, { threshold: 0.05, rootMargin: '0px 0px -20px 0px' }); 212 document.querySelectorAll('.hp-faq-item').forEach(function(el) { window._hpFaqObs.observe(el); }); 213 214 // Flip cards mobile tap-to-flip 215 if (!window._hpFlipInit) { 216 window._hpFlipInit = true; 217 document.querySelectorAll('.flip-card-wrapper').forEach(function(wrapper) { 218 wrapper.addEventListener('click', function() { 219 if (window.innerWidth <= 1024) 220 wrapper.querySelector('.flip-card').classList.toggle('flipped'); 221 }); 222 }); 223 document.querySelectorAll('.start-path-btn').forEach(function(btn
223) { 224 btn.addEventListener('click', function(e) { e.stopPropagation(); }); 225 }); 226 } 227};
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.