PageSourceSearch

https://soapboxsuperapp.com/js/main.js

js soapboxsuperapp.com collected 2026-10-02 00:33:12 UTC 8,642 bytes, 195 lines download raw bytes

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.