1/** 2 * Live Data Upsell System 3 * Handles live data promotion banners and pricing modals 4 */ 5 6class LiveDataUpsell { 7 constructor() { 8 this.checkUserSubscription().then(() => { 9 if (!this.isUserSubscribed) { 10 this.init(); 11 this.setupEventListeners(); 12 this.startDelayedPopup(); 13 } 14 }); 15 } 16 17 init() { 18 // Banner removed per user request 19 this.createPricingModal(); 20 } 21 22 createUpsellBanner() { 23 // Check if banner already exists 24 if (document.getElementById('live-data-banner')) return; 25 26 const banner = document.createElement('div'); 27 banner.id = 'live-data-banner'; 28 banner.className = 'live-data-banner'; 29 banner.innerHTML = ` 30 <div class="live-data-banner-content"> 31 <div class="live-data-banner-icon">â¡</div> 32 <div class="live-data-banner-text"> 33 <strong>Get Hourly Forecasts</strong> 34 <span>Hourly predictions, live streaming & instant alerts</span> 35 </div> 36 <div class="live-data-banner-actions"> 37 <button class="live-data-banner-btn primary" onclick="liveDataUpsell.showPricingModal()"> 38 Subscribe Now 39 </button> 40 <button class="live-data-banner-btn secondary" onclick="liveDataUpsell.dismissBanner()"> 41 × 42 </button> 43 </div> 44 </div> 45 `; 46 47 // Insert banner at top of page 48 document.body.insertBefore(banner, document.body.firstChild); 49 50 // Add CSS styles 51 this.addBannerStyles(); 52 53 // Animate in 54 setTimeout(() => { 55 banner.classList.add('show'); 56 }, 500); 57 } 58 59 addBannerStyles() { 60 if (document.getElementById('live-data-banner-styles')) return; 61 62 const styles = document.createElement('style'); 63 styles.id = 'live-data-banner-styles'; 64 styles.textContent = ` 65 .live-data-banner { 66 position: fixed; 67 top: -80px; 68 left: 0; 69 right: 0; 70 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 71 color: white; 72 z-index: 1000; 73 transition: transform 0.3s ease-in-out; 74 box-shadow: 0 2px 10px rgba(0,0,0,0.1); 75 } 76 77 .live-data-banner.show { 78 transform: translateY(80px); 79 } 80 81 .live-data-banner-content { 82 display: flex; 83 align-items: center; 84 justify-content: space-between; 85 max-width: 1200px; 86 margin: 0 auto; 87 padding: 12px 20px; 88 gap: 16px; 89 } 90 91 .live-data-banner-icon { 92 font-size: 24px; 93 animation: pulse 2s infinite; 94 } 95 96 @keyframes pulse { 97 0% { transform: scale(1); } 98 50% { transform: scale(1.1); } 99 100% { transform: scale(1); } 100 } 101 102 .live-data-banner-text { 103 flex: 1; 104 display: flex; 105 flex-direction: column; 106 gap: 2px; 107 } 108 109 .live-data-banner-text strong { 110 font-size: 16px; 111 font-weight: 600; 112 } 113 114 .live-data-banner-text span { 115 font-size: 13px; 116 opacity: 0.9; 117 } 118 119 .live-data-banner-actions { 120 display: flex; 121 gap: 8px; 122 align-items: center; 123 } 124 125 .live-data-banner-btn { 126 padding: 8px 16px; 127 border: none; 128 border-radius: 6px; 129 font-weight: 500; 130 cursor: pointer; 131 transition: all 0.2s; 132 font-size: 14px; 133 } 134 135 .live-data-banner-btn.primary { 136 background: rgba(255,255,255,0.2); 137 color: white; 138 backdrop-filter: blur(10px); 139 } 140 141 .live-data-banner-btn.primary:hover { 142 background: rgba(255,255,255,0.3); 143 transform: translateY(-1px); 144 } 145 146 .live-data-banner-btn.secondary { 147 background: transparent; 148 color: white; 149 font-size: 18px; 150 padding: 4px 8px; 151 opacity: 0.7; 152 } 153 154 .live-data-banner-btn.secondary:hover { 155 opacity: 1; 156 background: rgba(255,255,255,0.1); 157 } 158 159 /* Adjust page content for banner */ 160 body.banner-active { 161 padding-top: 80px; 162 } 163 164 @media (max-width: 768px) { 165 .live-data-banner-content { 166 flex-direction: column; 167 text-align: center; 168 gap: 12px; 169 padding: 16px 20px; 170 } 171 172 .live-data-banner-text { 173 order: 1; 174 } 175 176 .live-data-banner-icon { 177 order: 2; 178 font-size: 20px; 179 } 180 181 .live-data-banner-actions { 182 order: 3; 183 justify-content: center; 184 } 185 186 body.banner-active { 187 padding-top: 120px; 188 } 189 } 190 `; 191 192 document.head.appendChild(styles); 193 document.body.classList.add('banner-active'); 194 } 195 196 createPricingModal() { 197 // Check if modal already exists 198 if (document.getElementById('pricing-modal')) return; 199 200 const modal = document.createElement('div'); 201 modal.id = 'pricing-modal'; 202 modal.className = 'pricing-modal'; 203 modal.style.display = 'none'; 204 modal.innerHTML = ` 205 <div class="pricing-modal-content"> 206 <div class="pricing-modal-header"> 207 <h3>Choose Your Plan</h3> 208 <button class="pricing-modal-close" onclick="liveDataUpsell.hidePricingModal()">×</button> 209 </div> 210 211 <div class="pricing-plans"> 212 <div class="pricing-plan recommended"> 213 <div class="plan-badge">Most Popular</div> 214 <div class="plan-header"> 215 <h4>Pro Plan</h4> 216 <div class="plan-price">
217 <span class="price">$19</span> 218 <span class="period">/month</span> 219 </div> 220 </div> 221 <div class="plan-features"> 222 <div class="feature">Hourly predictions for 70+ coins</div> 223 <div class="feature">Live streaming price data</div> 224 <div class="feature">Advanced interactive charts</div> 225 <div class="feature">Instant price alerts</div> 226 <div class="feature">Technical indicators</div> 227 <div class="feature">Market sentiment analysis</div> 228 </div> 229 <button class="plan-button primary" onclick="liveDataUpsell.selectPlan('pro')"> 230 Subscribe 231 </button> 232 </div> 233 234 <div class="pricing-plan"> 235 <div class="plan-header"> 236 <h4>Premium Plan</h4> 237 <div class="plan-price"> 238 <span class="price">$49</span> 239 <span class="period">/month</span> 240 </div> 241 </div> 242 <div class="plan-features"> 243 <div class="feature">Everything in Pro</div> 244 <div class="feature">Portfolio tracking</div> 245 <div class="feature">Advanced AI insights</div> 246 <div class="feature">Custom alert strategies</div> 247 <div class="feature">API access</div> 248 <div class="feature">Priority support</div> 249 </div> 250 <button class="plan-button secondary" onclick="liveDataUpsell.selectPlan('premium')"> 251 Subscribe 252 </button> 253 </div> 254 255 <div class="pricing-plan"> 256 <div class="plan-header"> 257 <h4>Basic Plan</h4> 258 <div class="plan-price"> 259 <span class="price">$9</span> 260 <span class="period">/month</span> 261 </div> 262 </div> 263 <div class="plan-features"> 264 <div class="feature">Hourly data for 10 coins</div> 265 <div class="feature">Basic charts</div> 266 <div class="feature">Email alerts</div> 267 <div class="feature">Standard predictions</div> 268 </div> 269 <button class="plan-button secondary" onclick="liveDataUpsell.selectPlan('basic')"> 270 Subscribe 271 </button> 272 </div> 273 </div> 274 275 <div class="pricing-footer"> 276 <div class="trust-indicators"> 277 <div class="trust-item"> 278 <span class="trust-icon">ð</span> 279 <span>Secure payments</span> 280 </div> 281 <div class="trust-item"> 282 <span class="trust-icon">â©ï¸</span> 283 <span>Cancel anytime</span> 284 </div> 285 <div class="trust-item"> 286 <span class="trust-icon">ð</span> 287 <span>24/7 support</span> 288 </div> 289 </div> 290 </div> 291 </div> 292 `; 293 294 document.body.appendChild(modal); 295 this.addModalStyles(); 296 } 297 298 addModalStyles() { 299 if (document.getElementById('pricing-modal-styles')) return; 300 301 const styles = document.createElement('style'); 302 styles.id = 'pricing-modal-styles'; 303 styles.textContent = ` 304 .pricing-modal { 305 position: fixed; 306 top: 0; 307 left: 0; 308 right: 0; 309 bottom: 0; 310 background: rgba(0,0,0,0.8); 311 z-index: 2000; 312 display: flex; 313 align-items: center; 314 justify-content: center; 315 padding: 20px; 316 backdrop-filter: blur(5px); 317 } 318 319 .pricing-modal-content { 320 background: white; 321 border-radius: 16px; 322 max-width: 1000px; 323 width: 100%; 324 max-height: 90vh; 325 overflow-y: auto; 326 position: relative; 327 animation: modalSlideIn 0.3s ease-out; 328 } 329 330 @keyframes modalSlideIn { 331 from { 332 opacity: 0; 333 transform: translateY(-50px) scale(0.95); 334 } 335 to { 336 opacity: 1; 337 transform: translateY(0) scale(1); 338 } 339 } 340 341 .pricing-modal-header { 342 display: flex; 343 justify-content: space-between; 344 align-items: center; 345 padding: 24px 24px 16px; 346 border-bottom: 1px solid #eee; 347 } 348 349 .pricing-modal-header h3 { 350 margin: 0; 351 font-size: 24px; 352 color: #333; 353 } 354 355 .pricing-modal-close { 356 background: none; 357 border: none; 358 font-size: 24px; 359 cursor: pointer; 360 color: #666; 361 padding: 4px; 362 border-radius: 4px; 363 transition: all 0.2s; 364 } 365
366 .pricing-modal-close:hover { 367 background: #f5f5f5; 368 color: #333; 369 } 370 371 .pricing-plans { 372 display: grid; 373 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); 374 gap: 20px; 375 padding: 24px; 376 } 377 378 .pricing-plan { 379 border: 2px solid #eee; 380 border-radius: 12px; 381 padding: 24px; 382 position: relative; 383 background: white; 384 transition: all 0.3s ease; 385 } 386 387 .pricing-plan:hover { 388 border-color: #667eea; 389 transform: translateY(-4px); 390 box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15); 391 } 392 393 .pricing-plan.recommended { 394 border-color: #667eea; 395 background: linear-gradient(135deg, #f8f9ff 0%, #fff 100%); 396 } 397 398 .plan-badge { 399 position: absolute; 400 top: -12px; 401 left: 50%; 402 transform: translateX(-50%); 403 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 404 color: white; 405 padding: 6px 16px; 406 border-radius: 20px; 407 font-size: 12px; 408 font-weight: 600; 409 } 410 411 .plan-header { 412 text-align: center; 413 margin-bottom: 24px; 414 } 415 416 .plan-header h4 { 417 margin: 0 0 12px 0; 418 font-size: 20px; 419 color: #333; 420 } 421 422 .plan-price { 423 display: flex; 424 align-items: baseline; 425 justify-content: center; 426 gap: 4px; 427 margin-bottom: 8px; 428 } 429 430 .plan-price .price { 431 font-size: 36px; 432 font-weight: 700; 433 color: #333; 434 } 435 436 .plan-price .period { 437 font-size: 16px; 438 color: #666; 439 } 440 441 .plan-trial { 442 font-size: 14px; 443 color: #4caf50; 444 font-weight: 500; 445 } 446 447 .plan-features { 448 margin-bottom: 24px; 449 } 450 451 .feature { 452 display: flex; 453 align-items: center; 454 gap: 8px; 455 padding: 8px 0; 456 font-size: 14px; 457 color: #555; 458 } 459 460 .plan-button { 461 width: 100%; 462 padding: 12px 24px; 463 border: none; 464 border-radius: 8px; 465 font-weight: 600; 466 font-size: 16px; 467 cursor: pointer; 468 transition: all 0.2s; 469 } 470 471 .plan-button.primary { 472 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 473 color: white; 474 } 475 476 .plan-button.primary:hover { 477 transform: translateY(-2px); 478 box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); 479 } 480 481 .plan-button.secondary { 482 background: white; 483 color: #667eea; 484 border: 2px solid #667eea; 485 } 486 487 .plan-button.secondary:hover { 488 background: #667eea; 489 color: white; 490 transform: translateY(-2px); 491 } 492 493 .pricing-footer { 494 padding: 24px; 495 border-top: 1px solid #eee; 496 background: #f8f9fa; 497 border-radius: 0 0 16px 16px; 498 } 499 500 .trust-indicators { 501 display: flex; 502 justify-content: center; 503 gap: 32px; 504 flex-wrap: wrap; 505 } 506 507 .trust-item { 508 display: flex; 509 align-items: center; 510 gap: 8px; 511 font-size: 14px; 512 color: #666; 513 } 514 515 .trust-icon { 516 font-size: 16px; 517 } 518 519 @media (max-width: 768px) { 520 .pricing-plans { 521 grid-template-columns: 1fr; 522 padding: 16px; 523 } 524 525 .pricing-plan { 526 padding: 20px; 527 } 528 529 .trust-indicators { 530 gap: 16px; 531 flex-direction: column; 532 align-items: center; 533 } 534 } 535 `; 536 537 document.head.appendChild(styles); 538 } 539 540 showPricingModal() { 541 const modal = document.getElementById('pricing-modal'); 542 if (modal) { 543 modal.style.display = 'flex'; 544 document.body.style.overflow = 'hidden'; 545 546 // Track event 547 this.trackEvent('pricing_modal_opened'); 548 } 549 } 550 551 hidePricingModal() { 552 const modal = document.getElementById('pricing-modal'); 553 if (modal) { 554 modal.style.display = 'none'; 555 document.body.style.overflow = 'auto'; 556 } 557 } 558 559 selectPlan(planType) { 560 // Check if user is logged in 561 const currentUser = window.getCurrentUser ? window.getCurrentUser() : null; 562 563 if (currentUser) { 564 // User is logged in, redirect to checkout 565 window.location.href = `/pricing?plan=${planType}`; 566 } else { 567 // Show signup modal first 568 this.hidePricingModal(); 569 if (window.showSignupModal) { 570 window.showSignupModal(); 571 } else { 572 window.location.href = `/signup?redirect=/pricing&plan=${planType}`; 573 } 574 } 575 576 // Track event 577 this.trackEvent('plan_selected', { plan: planType }); 578 } 579 580 dismissBanner() { 581 const banner = document.getElementById('live-data-banner'); 582 if (banner) { 583 banner.classList.remove('show'); 584 document.body.classList.remove('banner-active'); 585 setTimeout(() => { 586 banner.remove(); 587 }, 300); 588 } 589 590 // Store dismissal in localStorage 591 localStorage.setItem('live_data_banner_dismissed', Date.now().toString()); 592 593 // Track event 594 this.trackEvent('banner_dismissed'); 595 } 596 597 startDelayedPopup() {
598 // Don't show popup if banner was recently dismissed 599 const dismissed = localStorage.getItem('live_data_banner_dismissed'); 600 if (dismissed && (Date.now() - parseInt(dismissed)) < 24 * 60 * 60 * 1000) { 601 return; 602 } 603 604 // Show popup after 30 seconds on coin pages 605 if (window.location.pathname.includes('/coin/')) { 606 setTimeout(() => { 607 // Only show if user hasn't already interacted with upsells 608 if (!localStorage.getItem('upsell_interaction')) { 609 this.showPricingModal(); 610 localStorage.setItem('upsell_interaction', Date.now().toString()); 611 612 // Track event 613 this.trackEvent('delayed_popup_shown'); 614 } 615 }, 30000); // 30 seconds 616 } 617 } 618 619 setupEventListeners() { 620 // Close modal on outside click 621 document.addEventListener('click', (e) => { 622 const modal = document.getElementById('pricing-modal'); 623 if (modal && e.target === modal) { 624 this.hidePricingModal(); 625 } 626 }); 627 628 // ESC key to close modal 629 document.addEventListener('keydown', (e) => { 630 if (e.key === 'Escape') { 631 this.hidePricingModal(); 632 } 633 }); 634 } 635 636 async checkUserSubscription() { 637 try { 638 // Get current user from auth system 639 const currentUser = window.getCurrentUser ? window.getCurrentUser() : null; 640 641 if (!currentUser) { 642 this.isUserSubscribed = false; 643 return; 644 } 645 646 // Check if user has active subscription 647 // Similar logic to netwrck payments.py - any active plan means subscribed 648 this.isUserSubscribed = currentUser.is_subscribed === true; 649 650 console.log('User subscription status:', { 651 user_id: currentUser.id, 652 is_subscribed: currentUser.is_subscribed, 653 subscription_status: currentUser.subscription_status, 654 should_show_upsell: !this.isUserSubscribed 655 }); 656 657 } catch (error) { 658 console.error('Error checking subscription status:', error); 659 // Default to showing upsell if we can't determine status 660 this.isUserSubscribed = false; 661 } 662 } 663 664 trackEvent(eventName, properties = {}) { 665 // Integrate with analytics if available 666 if (typeof gtag !== 'undefined') { 667 gtag('event', eventName, { 668 ...properties, 669 event_category: 'upsell', 670 page_location: window.location.href 671 }); 672 } 673 674 // Console log for debugging 675 console.log('Upsell Event:', eventName, properties); 676 } 677} 678 679// Initialize the upsell system 680let liveDataUpsell; 681 682document.addEventListener('DOMContentLoaded', function() { 683 // Only initialize on coin pages or main coins page 684 if (window.location.pathname.includes('/coin') || window.location.pathname === '/coins') { 685 liveDataUpsell = new LiveDataUpsell(); 686 window.liveDataUpsell = liveDataUpsell; 687 } 688});
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.