1/* SoapBox Marketing Website â Main JS 2 Scroll reveal, mobile nav, smooth interactions */ 3 4/* ââ Scroll-reveal via IntersectionObserver ââ */ 5(function initReveal() { 6 const els = document.querySelectorAll('.reveal'); 7 if (!els.length) return; 8 const observer = new IntersectionObserver((entries) => { 9 entries.forEach(e => { 10 if (e.isIntersecting) { 11 e.target.classList.add('visible'); 12 observer.unobserve(e.target); 13 } 14 }); 15 }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' }); 16 els.forEach(el => observer.observe(el)); 17})(); 18 19/* ââ Mobile nav hamburger ââ */ 20(function initNav() { 21 const burger = document.querySelector('.nav-hamburger'); 22 const drawer = document.querySelector('.nav-drawer'); 23 if (!burger || !drawer) return; 24 25 burger.addEventListener('click', () => { 26 const isOpen = drawer.classList.toggle('open'); 27 burger.classList.toggle('open', isOpen); 28 document.body.style.overflow = isOpen ? 'hidden' : ''; 29 }); 30 31 // Close when a drawer link is tapped 32 drawer.querySelectorAll('a').forEach(a => { 33 a.addEventListener('click', () => { 34 drawer.classList.remove('open'); 35 burger.classList.remove('open'); 36 document.body.style.overflow = ''; 37 }); 38 }); 39 40 // Close on outside click 41 document.addEventListener('click', (e) => { 42 if (!burger.contains(e.target) && !drawer.contains(e.target)) { 43 drawer.classList.remove('open'); 44 burger.classList.remove('open'); 45 document.body.style.overflow = ''; 46 } 47 }); 48})(); 49 50/* ââ Nav background on scroll ââ */ 51(function initNavScroll() { 52 const nav = document.querySelector('.nav'); 53 if (!nav) return; 54 const update = () => { 55 nav.style.background = window.scrollY > 20 56 ? 'rgba(18,40,54,0.97)' 57 : 'rgba(18,40,54,0.92)'; 58 }; 59 window.addEventListener('scroll', update, { passive: true }); 60 update(); 61})(); 62 63/* ââ Smooth anchor scrolling for in-page links ââ */ 64document.querySelectorAll('a[href^="#"]').forEach(a => { 65 a.addEventListener('click', function(e) { 66 const target = document.querySelector(this.getAttribute('href')); 67 if (target) { 68 e.preventDefault(); 69 const navH = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--nav-h')) || 68; 70 const top = target.getBoundingClientRect().top + window.scrollY - navH - 12; 71 window.scrollTo({ top, behavior: 'smooth' }); 72 } 73 }); 74}); 75 76/* ââ Monthly / Annual pricing toggle âââââââââââââââââââââââââââââââ 77 Swaps the displayed price + cycle label across every .pricing-card 78 on the page based on which toggle button is active. Cards opt in by 79 defining data-monthly / data-annual on the `.price` element and on 80 the inner `.price-cycle` span (so "$50 /mo" can become "$42 /mo, 81 billed annually"). No data-* on a card means it's a Free / Custom 82 card and gets left alone. 83 84 Lives in main.js because it's a few lines and main.js already loads 85 on every page that has a pricing block. If a page lacks the toggle, 86 the listener bind is a no-op (querySelectorAll returns []). */ 87document.querySelectorAll('.billing-toggle').forEach((toggle) => { 88 const buttons = toggle.querySelectorAll('.billing-toggle-btn'); 89 buttons.forEach((btn) => { 90 btn.addEventListener('click', () => { 91 const cycle = btn.dataset.cycle; // 'monthly' | 'annual' 92 93 // Visual + a11y state on the toggle itself 94 buttons.forEach((b) => { 95 const active = b === btn; 96 b.classList.toggle('is-active', active); 97 b.setAttribute('aria-pressed', active ? 'true' : 'false'); 98 }); 99 100 // Body-level class lets CSS show / hide the "billed annually" note 101 // under each price without per-card JS. Hidden via visibility (not 102 // display:none) so the reserved row keeps card heights aligned. 103 document.body.classList.toggle('is-billing-annual', cycle === 'annual'); 104 105 // Swap each card's displayed price + cycle suffix 106 document.querySelectorAll('.pricing-card .price').forEach((priceEl) => { 107 const next = priceEl.dataset[cycle]; // dataset.monthly or .annual 108 if (!next) return; // Free / Custom card â skip 109 // The cycle suffix lives in a child <span class="price-cycle">. 110 // We only update it if it carries data-monthly/data-annual; for 111 // the new layout the suffix stays "/mo" forever and the 112 // "billed annually" line is its own row below the price. 113 const cycleSpan = priceEl.querySelector('.price-cycle'); 114 if (cycleSpan) { 115 priceEl.firstChild.textContent = next; // text node before the span 116 const nextCycleText = cycleSpan.dataset[cycle]; 117 if (nextCycleText) cycleSpan.textContent = nextCycleText; 118 } else { 119 priceEl.textContent = next; 120 } 121 }); 122 }); 123 }); 124}); 125 126/* ââ Adaptive "Download" button âââââââââââââââââââââââââââââââââââââââ 127 Replaces the nav's 1â2 store links with ONE platform-aware button on 128 every page. On a phone it goes straight to the matching store; on 129 desktop it opens a popover with both store badges, a QR code (scan to 130 install on your phone), and the web-app link. Progressive enhancement: 131 with JS off, the original store links remain. */ 132(function initAppDownload() { 133 var APP_STORE = "https://apps.apple.com/us/app/soapbox-prayer/id6761029784"; 134 var PLAY = "https://play.google.com/store/apps/details?id=com.soapboxsuper.app"; 135 var WEB = "https://app.soapboxsuperapp.com"; 136 137 var ua = navigator.userAgent || ""; 138 var isIOS = /iPad|iPhone|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); 139 var isAndroid = /Android/.test(ua); 140 141 // Swap the store link(s) in each nav container for one adaptive button. 142 var buttons = []; 143 document.querySelectorAll('.nav-actions, .nav-drawer').forEach(function (c) { 144 var links = c.querySelectorAll('.btn-download-nav'); 145 if (!links.length) return; 146 var btn = document.createElement('a'); 147 btn.className = 'btn-download-nav app-dl-btn'; 148 btn.setAttribute('role', 'button'); 149 // Sensible href for no-JS / open-in-new-tab; the click handler overrides on desktop. 150 btn.href = isIOS ? APP_STORE : isAndroid ? PLAY : WEB; 151 btn.textContent = '⬠Download'; 152 links[0].parentNode.insertBefore(btn, links[0]); 153 links.forEach(function (l) { l.remove(); }); 154 buttons.push(btn); 155 }); 156 if (!buttons.length) return; 157 158 // Desktop popover (built once, appended to body). 159 var pop = document.createElement('div'); 160 pop.className = 'app-pop'; 161 pop.setAttribute('hidden', ''); 162 pop.innerHTML = 163 '<button class="app-pop-x" aria-label="Close">Ã</button>' + 164 '<p class="app-pop-title">Get the SoapBox app</p>' + 165 '<div class="app-pop-stores">' + 166 '<a href="' + APP_STORE + '" target="_blank" rel="noopener"><img src="/assets/appstore-badge.svg" alt="Download on the App Store"></a>' + 167 '<a href="' + PLAY + '" target="_blank" rel="noopener"><img src="/assets/google-play-badge.png" alt="Get it on Google Play"></a>' + 168 '</div>' + 169 '<div class="app-pop-qr"><img src="/assets/qr-app.png" width="116" height="116" alt="Scan to install SoapBox on your phone"><span>Scan to install on your phone</span></div>' + 170 '<a class="app-pop-web" href="' + WEB + '" target="_blank" rel="noopener">Or open the web app â</a>';
171 document.body.appendChild(pop); 172 173 function close() { pop.setAttribute('hidden', ''); } 174 function openNear(btn) { 175 pop.removeAttribute('hidden'); 176 var r = btn.getBoundingClientRect(); 177 var left = Math.max(12, Math.min(r.right - pop.offsetWidth, window.innerWidth - pop.offsetWidth - 12)); 178 pop.style.top = (r.bottom + window.scrollY + 10) + 'px'; 179 pop.style.left = (left + window.scrollX) + 'px'; 180 } 181 182 buttons.forEach(function (btn) { 183 btn.addEventListener('click', function (e) { 184 if (isIOS || isAndroid) return; // phones: let the href take them to the store 185 e.preventDefault(); // desktop: show the chooser 186 if (pop.hasAttribute('hidden')) openNear(btn); else close(); 187 }); 188 }); 189 pop.querySelector('.app-pop-x').addEventListener('click', close); 190 document.addEventListener('click', function (e) { 191 if (!pop.hasAttribute('hidden') && !pop.contains(e.target) && !e.target.classList.contains('app-dl-btn')) close(); 192 }); 193 document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); }); 194 window.addEventListener('resize', close); 195})();
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.