PageSourceSearch

https://rachanaranade.com/js/app-helpers.js

js rachanaranade.com collected 2026-09-24 13:14:03 UTC 10,600 bytes, 227 lines download raw bytes

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.