1"use strict"; 2const getConfig = () => { 3 const firebaseConfig = window.__FIREBASE_CONFIG__ || { 4 apiKey: "AIzaSyCWNfG7vP8swLWC_dZMQFJ0CHqlALEE0Hs", 5 authDomain: "geotap-441a7.firebaseapp.com", 6 projectId: "geotap-441a7", 7 storageBucket: "geotap-441a7.firebasestorage.app", 8 messagingSenderId: "541755752861", 9 appId: "1:541755752861:web:4028ca25b676f262a92f74", 10 measurementId: "G-C89EQ4L3FP" 11 }; 12 const stripeConfig = window.__STRIPE_CONFIG__ || { publicKey: '' }; 13 const isDevelopment = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; 14 const apiConfig = { 15 baseUrl: isDevelopment ? 'http://localhost:3000' : 'https://www.loc-atr.me' 16 }; 17 console.log('ð§ Configuration loaded:', { 18 stripeConfigured: !!stripeConfig.publicKey, 19 environment: isDevelopment ? 'development' : 'production', 20 apiBaseUrl: apiConfig.baseUrl 21 }); 22 return { firebaseConfig, stripeConfig, apiConfig }; 23}; 24function initializeWebsite() { 25 console.log('Initializing Locatr website...'); 26 setupMobileNavigation(); 27 setupSmoothScrolling(); 28 setupNavbarScrollEffects(); 29 setupIntersectionObservers(); 30 setupExtensionPreview(); 31 setupSearchDemo(); 32 setupStatsAnimation(); 33 setupPricingCards(); 34 setupFeatureCards(); 35 setupButtonEffects(); 36 setupDownloadButtons(); 37 setupContactForm(); 38 console.log('Website initialization complete'); 39} 40function setupMobileNavigation() { 41 const navToggle = document.querySelector('.nav-toggle'); 42 const navLinks = document.querySelector('.nav-links'); 43 if (navToggle && navLinks) { 44 navToggle.addEventListener('click', () => { 45 navLinks.classList.toggle('active'); 46 navToggle.classList.toggle('active'); 47 }); 48 } 49} 50function setupSmoothScrolling() { 51 const navLinksElements = document.querySelectorAll('a[href^="#"]'); 52 navLinksElements.forEach((link) => { 53 link.addEventListener('click', (e) => { 54 e.preventDefault(); 55 const targetId = link.getAttribute('href'); 56 const targetSection = document.querySelector(targetId); 57 if (targetSection) { 58 const offsetTop = targetSection.offsetTop - 70; 59 window.scrollTo({ 60 top: offsetTop, 61 behavior: 'smooth' 62 }); 63 const navLinks = document.querySelector('.nav-links'); 64 const navToggle = document.querySelector('.nav-toggle'); 65 if (navLinks && navLinks.classList.contains('active')) { 66 navLinks.classList.remove('active'); 67 navToggle?.classList.remove('active'); 68 } 69 } 70 }); 71 }); 72} 73function setupNavbarScrollEffects() { 74 const navbar = document.querySelector('.navbar'); 75 let lastScrollTop = 0; 76 if (!navbar) 77 return; 78 window.addEventListener('scroll', () => { 79 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 80 if (scrollTop > 50) { 81 navbar.classList.add('scrolled'); 82 } 83 else { 84 navbar.classList.remove('scrolled'); 85 } 86 if (scrollTop > lastScrollTop && scrollTop > 100) { 87 navbar.style.transform = 'translateY(-100%)'; 88 } 89 else { 90 navbar.style.transform = 'translateY(0)'; 91 } 92 lastScrollTop = scrollTop; 93 }); 94} 95function setupIntersectionObservers() { 96 const observerOptions = { 97 threshold: 0.1, 98 rootMargin: '0px 0px -50px 0px' 99 }; 100 const observer = new IntersectionObserver((entries) => { 101 entries.forEach((entry) => { 102 if (entry.isIntersecting) { 103 entry.target.classList.add('animate-in'); 104 } 105 }); 106 }, observerOptions); 107 const animateElements = document.querySelectorAll('.feature-card, .step, .pricing-card'); 108 animateElements.forEach((el) => { 109 observer.observe(el); 110 }); 111} 112function setupExtensionPreview() { 113 const extensionPreview = document.querySelector('.extension-preview'); 114 if (extensionPreview) { 115 extensionPreview.addEventListener('mouseenter', function () { 116 this.style.transform = 'perspective(1000px) rotateY(-5deg) rotateX(2deg) scale(1.02)'; 117 }); 118 extensionPreview.addEventListener('mouseleave', function () { 119 this.style.transform = 'perspective(1000px) rotateY(-15deg) rotateX(5deg) scale(1)'; 120 }); 121 } 122} 123function setupSearchDemo() { 124 const searchInput = document.querySelector('.search-demo input'); 125 if (searchInput) { 126 const originalValue = searchInput.value; 127 searchInput.value = ''; 128 let i = 0; 129 const typeWriter = () => { 130 if (i < originalValue.length) { 131 searchInput.value += originalValue.charAt(i); 132 i++; 133 setTimeout(typeWriter, 100); 134 } 135 }; 136 setTimeout(typeWriter, 2000); 137 } 138} 139function setupStatsAnimation() { 140 const statsNumbers = document.querySelectorAll('.stat-number'); 141 const animateStats = () => {
142 statsNumbers.forEach((stat) => { 143 const target = parseInt(stat.textContent?.replace(/[^\d]/g, '') || '0'); 144 const increment = target / 100; 145 let current = 0; 146 const config = { 147 increment, 148 target, 149 hasK: stat.textContent?.includes('K') || false, 150 hasStar: stat.textContent?.includes('â ') || false 151 }; 152 const updateCounter = () => { 153 if (current < config.target) { 154 current += config.increment; 155 if (config.hasK) { 156 stat.textContent = Math.ceil(current / 1000) + 'K+'; 157 } 158 else if (config.hasStar) { 159 stat.textContent = (current / 10).toFixed(1) + 'â '; 160 } 161 else { 162 stat.textContent = Math.ceil(current / 1000) + 'K+'; 163 } 164 requestAnimationFrame(updateCounter); 165 } 166 }; 167 updateCounter(); 168 }); 169 }; 170 const heroSection = document.querySelector('.hero'); 171 if (heroSection) { 172 const heroObserver = new IntersectionObserver((entries) => { 173 entries.forEach((entry) => { 174 if (entry.isIntersecting) { 175 setTimeout(animateStats, 1000); 176 heroObserver.unobserve(entry.target); 177 } 178 }); 179 }, { threshold: 0.5 }); 180 heroObserver.observe(heroSection); 181 } 182} 183function setupPricingCards() { 184 const pricingCards = document.querySelectorAll('.pricing-card'); 185 pricingCards.forEach((card) => { 186 card.addEventListener('mouseenter', function () { 187 this.style.transform = 'translateY(-10px) scale(1.02)'; 188 }); 189 card.addEventListener('mouseleave', function () { 190 if (this.classList.contains('featured')) { 191 this.style.transform = 'scale(1.05)'; 192 } 193 else { 194 this.style.transform = 'translateY(0) scale(1)'; 195 } 196 }); 197 }); 198} 199function setupFeatureCards() { 200 const featureCards = document.querySelectorAll('.feature-card'); 201 const featureObserver = new IntersectionObserver((entries) => { 202 entries.forEach((entry, index) => { 203 if (entry.isIntersecting) { 204 setTimeout(() => { 205 const target = entry.target; 206 target.style.opacity = '1'; 207 target.style.transform = 'translateY(0)'; 208 }, index * 100); 209 featureObserver.unobserve(entry.target); 210 } 211 }); 212 }, { threshold: 0.1 }); 213 featureCards.forEach((card) => { 214 card.style.opacity = '0'; 215 card.style.transform = 'translateY(30px)'; 216 card.style.transition = 'opacity 0.6s ease, transform 0.6s ease'; 217 featureObserver.observe(card); 218 }); 219} 220function setupButtonEffects() { 221 const buttons = document.querySelectorAll('.btn'); 222 buttons.forEach((button) => { 223 button.addEventListener('click', function (e) { 224 const ripple = document.createElement('span'); 225 const rect = this.getBoundingClientRect(); 226 const size = Math.max(rect.width, rect.height); 227 const x = e.clientX - rect.left - size / 2; 228 const y = e.clientY - rect.top - size / 2; 229 ripple.style.width = ripple.style.height = size + 'px'; 230 ripple.style.left = x + 'px'; 231 ripple.style.top = y + 'px'; 232 ripple.classList.add('ripple'); 233 this.appendChild(ripple); 234 setTimeout(() => { 235 ripple.remove(); 236 }, 600); 237 }); 238 }); 239} 240function setupDownloadButtons() { 241 const downloadButtons = document.querySelectorAll('[data-action="download"]'); 242 downloadButtons.forEach((button) => { 243 button.addEventListener('click', (e) => { 244 e.preventDefault(); 245 if (typeof gtag !== 'undefined') { 246 gtag('event', 'download_extension', { 247 event_category: 'engagement', 248 event_label: 'chrome_extension' 249 }); 250 } 251 window.open('https://chrome.google.com/webstore/detail/locatr/extension-id', '_blank'); 252 }); 253 }); 254} 255function setupContactForm() { 256 const contactForm = document.querySelector('#contact-form'); 257 if (contactForm) { 258 contactForm.addEventListener('submit', async (e) => { 259 e.preventDefault(); 260 const formData = new FormData(contactForm); 261 const data = { 262 name: formData.get('name'), 263 email: formData.get('email'), 264 message: formData.get('message') 265 }; 266 try { 267 console.log('Contact form submission:', data);
268 showNotification('Thank you for your message! We\'ll get back to you soon.', 'success'); 269 contactForm.reset(); 270 } 271 catch (error) { 272 console.error('Error submitting contact form:', error); 273 showNotification('There was an error sending your message. Please try again.', 'error'); 274 } 275 }); 276 } 277} 278function showNotification(message, type = 'info') { 279 const notification = document.createElement('div'); 280 notification.className = `notification notification-${type}`; 281 notification.textContent = message; 282 Object.assign(notification.style, { 283 position: 'fixed', 284 top: '20px', 285 right: '20px', 286 padding: '15px 20px', 287 borderRadius: '5px', 288 color: 'white', 289 backgroundColor: type === 'success' ? '#4CAF50' : type === 'error' ? '#f44336' : '#2196F3', 290 zIndex: '10000', 291 boxShadow: '0 4px 6px rgba(0,0,0,0.1)', 292 opacity: '0', 293 transform: 'translateX(100%)', 294 transition: 'all 0.3s ease' 295 }); 296 document.body.appendChild(notification); 297 setTimeout(() => { 298 notification.style.opacity = '1'; 299 notification.style.transform = 'translateX(0)'; 300 }, 100); 301 setTimeout(() => { 302 notification.style.opacity = '0'; 303 notification.style.transform = 'translateX(100%)'; 304 setTimeout(() => { 305 notification.remove(); 306 }, 300); 307 }, 5000); 308} 309function debounce(func, wait) { 310 let timeout; 311 return function executedFunction(...args) { 312 const later = () => { 313 clearTimeout(timeout); 314 func(...args); 315 }; 316 clearTimeout(timeout); 317 timeout = setTimeout(later, wait); 318 }; 319} 320const optimizedScrollHandler = debounce(() => { 321}, 16); 322function addRippleStyles() { 323 if (document.querySelector('#ripple-styles')) 324 return; 325 const style = document.createElement('style'); 326 style.id = 'ripple-styles'; 327 style.textContent = ` 328 .btn { position: relative; overflow: hidden; } 329 .ripple { 330 position: absolute; 331 background: rgba(255,255,255,0.3); 332 border-radius: 50%; 333 transform: scale(0); 334 animation: ripple 0.6s linear; 335 pointer-events: none; 336 } 337 @keyframes ripple { 338 to { transform: scale(4); opacity: 0; } 339 } 340 `; 341 document.head.appendChild(style); 342} 343document.addEventListener('DOMContentLoaded', () => { 344 console.log('[Website] DOM loaded, initializing...'); 345 initializeWebsite(); 346 addRippleStyles(); 347 setupPurchaseSystem(); 348}); 349document.addEventListener('visibilitychange', () => { 350 if (document.hidden) { 351 console.log('Tab hidden - pausing animations'); 352 } 353 else { 354 console.log('Tab visible - resuming animations'); 355 } 356}); 357let currentUser = null; 358let stripe = null; 359let auth = null; 360function initializePurchaseSystem() { 361 console.log('[Purchase] Initializing purchase system...'); 362 const { firebaseConfig, stripeConfig } = getConfig(); 363 if (typeof firebase !== 'undefined') { 364 try { 365 const app = firebase.initializeApp(firebaseConfig); 366 auth = firebase.auth(); 367 auth.onAuthStateChanged((user) => { 368 currentUser = user; 369 console.log('[Purchase] Auth state changed:', user ? user.email : 'Not logged in'); 370 updatePurchaseButtons(); 371 }); 372 console.log('[Purchase] Firebase initialized successfully'); 373 } 374 catch (error) { 375 console.error('[Purchase] Firebase initialization error:', error); 376 } 377 } 378 else { 379 console.warn('[Purchase] Firebase not available'); 380 } 381 if (typeof Stripe !== 'undefined') { 382 if (stripeConfig.publicKey) { 383 try { 384 stripe = Stripe(stripeConfig.publicKey); 385 console.log('[Purchase] Stripe initialized successfully'); 386 console.log('[Purchase] Stripe key prefix:', stripeConfig.publicKey.substring(0, 8) + '...'); 387 console.log('[Purchase] Is production key:', stripeConfig.publicKey.startsWith('pk_live_')); 388 } 389 catch (error) { 390 console.error('[Purchase] Stripe initialization error:', error); 391 } 392 } 393 else { 394 console.warn('[Purchase] Stripe public key not configured'); 395 console.log('[Purchase] stripeConfig:', stripeConfig); 396 } 397 } 398 else { 399 console.warn('[Purchase] Stripe not available'); 400 } 401} 402function updatePurchaseButtons() { 403 const purchaseButtons = document.querySelectorAll('.purchase-btn'); 404 console.log('Updating purchase buttons, found:', purchaseButtons.length); 405 purchaseButtons.forEach((button) => { 406 if (currentUser) { 407 button.disabled = false;
408 const originalText = button.textContent?.replace(' (Sign in required)', '') || ''; 409 button.textContent = originalText; 410 } 411 else { 412 button.disabled = false; 413 const originalText = button.textContent?.replace(' (Sign in required)', '') || ''; 414 button.textContent = originalText + ' (Sign in required)'; 415 } 416 }); 417} 418async function handlePurchase(tier, price, credits) { 419 console.log(`Purchase initiated: ${tier} pack ($${price} for ${credits} searches)`); 420 if (!currentUser) { 421 const confirmed = confirm('Please sign in first to make a purchase. Click OK to open the account page.'); 422 if (confirmed) { 423 window.open('https://www.loc-atr.me/account.html', '_blank'); 424 } 425 return; 426 } 427 const { apiConfig, stripeConfig } = getConfig(); 428 const isDevelopment = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; 429 if (isDevelopment && !stripeConfig.publicKey) { 430 const confirmed = confirm(`Development Mode Detected!\n\n` + 431 `The purchase would be for: ${tier} pack ($${price} for ${credits} searches)\n\n` + 432 `To test payments:\n` + 433 `1. Start API server: cd server && npm run dev:ts\n` + 434 `2. Or click OK to simulate payment completion`); 435 if (confirmed) { 436 console.log('[Purchase] Development mode: Simulating successful payment'); 437 alert(`â Mock Payment Successful!\n\nYou would have purchased:\n${tier} pack - $${price} for ${credits} searches\n\nIn production, this would redirect to Stripe checkout.`); 438 } 439 return; 440 } 441 if (isDevelopment) { 442 try { 443 const healthCheck = await fetch(`${apiConfig.baseUrl}/health`, { method: 'HEAD' }); 444 if (!healthCheck.ok) { 445 throw new Error('API server not responding'); 446 } 447 } 448 catch (error) { 449 console.warn('[Purchase] API server not available:', error); 450 const confirmed = confirm(`â ï¸ Development API Server Not Running\n\n` + 451 `Cannot connect to ${apiConfig.baseUrl}\n\n` + 452 `Options:\n` + 453 `1. Start API server: cd server && npm run dev:ts\n` + 454 `2. Click OK to simulate successful payment\n` + 455 `3. Click Cancel to abort`); 456 if (confirmed) { 457 console.log('[Purchase] Development mode: Simulating payment due to API unavailability'); 458 alert(`â Mock Payment Successful!\n\nYou would have purchased:\n${tier} pack - $${price} for ${credits} searches\n\nNote: This was simulated because the API server isn't running.\nIn production, this would redirect to Stripe checkout.`); 459 } 460 return; 461 } 462 } 463 try { 464 const token = await currentUser.getIdToken(); 465 console.log('[Purchase] Got user token, making API call...'); 466 const isProduction = window.location.hostname === 'www.loc-atr.me' || 467 window.location.hostname === 'loc-atr.me'; 468 const baseUrl = isProduction ? 'https://www.loc-atr.me' : window.location.origin; 469 function validateUrl(url) { 470 try { 471 const parsed = new URL(url); 472 return parsed.protocol === 'http:' || parsed.protocol === 'https:'; 473 } 474 catch { 475 return false; 476 } 477 } 478 const successUrl = `${baseUrl}/account.html?payment=success`; 479 const cancelUrl = `${baseUrl}/#pricing`; 480 const fallbackUrls = { 481 success: 'https://www.loc-atr.me/account.html?payment=success', 482 cancel: 'https://www.loc-atr.me/#pricing' 483 }; 484 const finalSuccessUrl = validateUrl(successUrl) ? successUrl : fallbackUrls.success; 485 const finalCancelUrl = validateUrl(cancelUrl) ? cancelUrl : fallbackUrls.cancel; 486 console.log('Payment Debug:', { 487 windowOrigin: window.location.origin, 488 hostname: window.location.hostname, 489 isProduction: isProduction, 490 baseUrl: baseUrl, 491 generatedUrls: { successUrl, cancelUrl }, 492 finalUrls: { finalSuccessUrl, finalCancelUrl }, 493 userAgent: navigator.userAgent, 494 timestamp: new Date().toISOString() 495 }); 496 const response = await fetch(`${apiConfig.baseUrl}/api/payment/create-checkout-session`, { 497 method: 'POST', 498 headers: { 499 'Authorization': `Bearer ${token}`, 500 'Content-Type': 'application/json' 501 }, 502 body: JSON.stringify({ 503 tier: tier, 504 successUrl: finalSuccessUrl, 505 cancelUrl: finalCancelUrl 506 }) 507 }); 508 const data = await response.json(); 509 console.log('Checkout session response:', data); 510 if (data.success && data.sessionId) { 511 console.log('Redirecting to Stripe checkout...'); 512 if (!stripe) { 513 console.error('[Purchase] Stripe object is null - initialization failed'); 514 alert('Payment system error: Stripe not initialized. Please refresh the page and try again.'); 515 return; 516 } 517 const { error }
517 = await stripe.redirectToCheckout({ 518 sessionId: data.sessionId 519 }); 520 if (error) { 521 console.error('Stripe checkout error:', error); 522 alert('Payment failed: ' + error.message); 523 } 524 } 525 else { 526 console.error('Failed to create checkout session:', data); 527 alert('Unable to process payment: ' + (data.error || 'Unknown error')); 528 } 529 } 530 catch (error) { 531 console.error('Purchase error:', error); 532 alert('An error occurred: ' + error.message); 533 } 534} 535function initializePurchaseButtons() { 536 console.log('Initializing purchase buttons...'); 537 const purchaseButtons = document.querySelectorAll('.purchase-btn'); 538 console.log('Found purchase buttons:', purchaseButtons.length); 539 purchaseButtons.forEach((button, index) => { 540 console.log(`Setting up button ${index}:`, button.dataset); 541 const newButton = button.cloneNode(true); 542 button.parentNode?.replaceChild(newButton, button); 543 newButton.addEventListener('click', (e) => { 544 console.log('Purchase button clicked!', e.target.dataset); 545 e.preventDefault(); 546 e.stopPropagation(); 547 const tier = newButton.dataset.tier || ''; 548 const price = newButton.dataset.price || ''; 549 const credits = newButton.dataset.credits || ''; 550 console.log('Calling handlePurchase with:', { tier, price, credits }); 551 handlePurchase(tier, price, credits); 552 }); 553 }); 554 updatePurchaseButtons(); 555} 556function setupPurchaseSystem() { 557 initializePurchaseSystem(); 558 if (document.readyState === 'loading') { 559 document.addEventListener('DOMContentLoaded', initializePurchaseButtons); 560 } 561 else { 562 initializePurchaseButtons(); 563 } 564 setTimeout(initializePurchaseButtons, 1000); 565} 566window.handlePurchase = handlePurchase;
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.