PageSourceSearch

https://truemetriks.com/assets/js/main.js?v=20260531c

js truemetriks.com collected 2026-09-24 09:06:36 UTC 7,424 bytes, 167 lines download raw bytes

1// main.js - theme toggle, mobile nav, smooth-scroll for anchors
2(function () {
3  'use strict';
4
5  // ---------- Theme toggle ----------
6  var themeToggle = document.querySelector('[data-theme-toggle]');
7  if (themeToggle) {
8    themeToggle.addEventListener('click', function () {
9      var current = document.documentElement.getAttribute('data-theme');
10      var next = current === 'dark' ? 'light' : 'dark';
11      document.documentElement.setAttribute('data-theme', next);
12      try { localStorage.setItem('tm-theme', next); } catch (e) {}
13      themeToggle.setAttribute('aria-pressed', next === 'dark' ? 'true' : 'false');
14    });
15    // Initial aria-pressed sync
16    var current = document.documentElement.getAttribute('data-theme');
17    themeToggle.setAttribute('aria-pressed', current === 'dark' ? 'true' : 'false');
18  }
19
20  // ---------- Mobile nav (hamburger overlay) ----------
21  var hamburger = document.querySelector('[data-nav-toggle]');
22  var nav = document.querySelector('[data-mobile-nav]');
23  if (hamburger && nav) {
24    function setNav(open) {
25      nav.classList.toggle('is-open', open);
26      hamburger.setAttribute('aria-expanded', open ? 'true' : 'false');
27      // .nav-open (on <html>) pins the header to the viewport top AND locks page
28      // scroll (both in CSS) so the hamburger always stays reachable to close the
29      // menu - position:sticky was scrolling out of reach on mobile and trapping
30      // the user behind the open overlay.
31      document.documentElement.classList.toggle('nav-open', open);
32    }
33    hamburger.addEventListener('click', function () {
34      setNav(!nav.classList.contains('is-open'));
35    });
36    // Close on link click
37    nav.querySelectorAll('a').forEach(function (a) {
38      a.addEventListener('click', function () { setNav(false); });
39    });
40    // Close on Escape
41    document.addEventListener('keydown', function (e) {
42      if (e.key === 'Escape' && nav.classList.contains('is-open')) setNav(false);
43    });
44  }
45
46  // ---------- Contact form (POSTs to rybbit API, MailerSend backend) ----------
47  // Same handler covers the marketing /contact form AND the embedded docs-page
48  // help form (one per page). data-contact-source carries the docs section/slug
49  // so the email's subject/body tell us which page the user submitted from.
50  // The endpoint is server/src/api/contact-message.ts (registered as
51  // POST /contact-message under the rybbit /api prefix).
52  var CONTACT_API_URL = 'https://app.truemetriks.com/api/contact-message';
53  var contactForms = document.querySelectorAll('[data-contact-form]');
54  contactForms.forEach(function (form) {
55    form.addEventListener('submit', function (e) {
56      e.preventDefault();
57      var get = function (n) {
58        var el = form.querySelector('[name="' + n + '"]');
59        return el ? el.value.trim() : '';
60      };
61      var submitBtn = form.querySelector('.form-submit');
62      var originalLabel = submitBtn ? submitBtn.textContent : 'Send message';
63      var statusEl = form.querySelector('[data-contact-status]');
64      var setStatus = function (kind, msg) {
65        if (!statusEl) return;
66        statusEl.textContent = msg || '';
67        statusEl.setAttribute('data-status', kind || '');
68      };
69
70      // Client-side guard: require a real message. Mirrors the server's
71      // 30-char minimum so users get instant feedback instead of a round-trip.
72      var MIN_MESSAGE_LEN = 30;
73      var messageVal = get('message');
74      if (messageVal.length < MIN_MESSAGE_LEN) {
75        setStatus('error', 'Please write at least ' + MIN_MESSAGE_LEN + ' characters so we can help you better.');
76        var msgEl = form.querySelector('[name="message"]');
77        if (msgEl) msgEl.focus();
78        return;
79      }
80
81      // Cloudflare Turnstile token (injected by the widget as a hidden input).
82      // If the widget is on the page but not solved yet, block and ask.
83      var turnstileToken = get('cf-turnstile-response');
84      if (form.querySelector('.cf-turnstile') && !turnstileToken) {
85        setStatus('error', 'Please complete the verification below, then send.');
86        return;
87      }
88
89      // Marketing form labels its optional field "company"; docs form uses
90      // "website". Normalise client-side so the API only knows "website".
91      var payload = {
92        name: get('name'),
93        email: get('email'),
94        website: get('website') || get('company') || '',
95        message: messageVal,
96        source: form.getAttribute('data-contact-source') || '',
97        companyName_hp: get('companyName_hp') || '',
98        turnstileToken: turnstileToken
99      };
100
101      if (submitBtn) {
102        submitBtn.disabled = true;
103        submitBtn.textContent = 'Sending...';
104      }
105      setStatus('', '');
106
107      fetch(CONTACT_API_URL, {
108        method: 'POST',
109        headers: { 'Content-Type': 'application/json' },
110        body: JSON.stringify(payload)
111      })
112        .then(function (res) {
113          return res.json().then(function (data) { return { ok: res.ok, data: data }; });
114        })
115        .then(function (r) {
116          if (r.ok && r.data && r.data.ok) {
117            // Swap the whole form for a success card. Replacement is per-form
118            // so multiple forms on the page (unlikely, but possible) are
119            // independent.
120            var success = document.createElement('div');
121            success.className = 'contact-form-success';
122            success.setAttribute('role', 'status');
123            success.innerHTML =
124              '<div class="contact-form-success__icon" aria-hidden="true">'
125                + '<svg width="44" height="44" viewBox="0 0 24 24" fill="none" '
126                + 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'
127                + '<polyline points="20 6 9 17 4 12"/></svg>'
128              + '</div>'
129              + '<h3 class="contact-form-success__title">Message sent</h3>'
130              + '<p class="contact-form-success__lede">Thanks - we received your message. '
131                + 'We reply within 24 hours on workdays, often much sooner.</p>';
132            form.parentNode.replaceChild(success, form);
133          } else {
134            var msg = (r.data && r.data.error)
135              || 'Could not send right now. Please email us at [email protected] instead.';
136            setStatus('error', msg);
137            // Turnstile tokens are single-use - reset so a retry gets a fresh one.
138            if (window.turnstile) { try { window.turnstile.reset(); } catch (e) {} }
139            if (submitBtn) {
140              submitBtn.disabled = false;
141              submitBtn.textContent = originalLabel;
142            }
143          }
144        })
145        .catch(function () {
146          setStatus('error', 'Network error. Please email us at [email protected] instead.');
147          if (window.turnstile) { try { window.turnstile.reset(); } catch (e) {} }
148          if (submitBtn) {
149            submitBtn.disabled = false;
150            submitBtn.textContent = originalLabel;
151          }
152        });
153    });
154  });
155
156  // ---------- Smooth scroll for in-page anchors ----------
157  document.querySelectorAll('a[href^="#"]').forEach(function (a) {
158    a.addEventListener('click', function (e) {
159      var target = document.querySelector(a.getAttribute('href'));
160      if (target) {
161        e.preventDefault();
162        target.scrollIntoView({ behavior: 'smooth', block: 'start' });
163      }
164    });
165  });
166
167})();

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.