PageSourceSearch

https://www.getgupt.xyz/script.js

js getgupt.xyz collected 2026-10-02 02:11:09 UTC 9,663 bytes, 281 lines download raw bytes

1/* ==========================================================================
2   Gupt landing — premium interactions
3   - Theme toggle (with smooth transition + system follow-up)
4   - Smooth scroll for in-page anchors
5   - FAQ exclusive accordion
6   - Reveal-on-scroll with stagger via data-delay
7   - Scroll-spy nav underline
8   - Magnetic buttons (cursor pull)
9   - Bento cards radial spotlight on hover
10   - Counter animations on stats
11   - Back-to-top button visibility
12   - Mobile burger toggle
13   ========================================================================== */
14(function () {
15  "use strict";
16
17  const root = document.documentElement;
18  const prefersReduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
19  const isPointerFine = matchMedia("(pointer: fine)").matches;
20  const STORAGE_KEY = "gupt-site-theme";
21
22  // ---------- 1. Theme toggle ----------
23  function applyTheme(theme, persist) {
24    root.classList.add("theme-transition");
25    root.setAttribute("data-theme", theme);
26    if (persist) {
27      try {
28        localStorage.setItem(STORAGE_KEY, theme);
29      } catch (e) {}
30    }
31    window.setTimeout(() => root.classList.remove("theme-transition"), 320);
32  }
33
34  document.querySelectorAll("[data-theme-toggle]").forEach((btn) => {
35    btn.addEventListener("click", () => {
36      const current = root.getAttribute("data-theme") || "light";
37      applyTheme(current === "dark" ? "light" : "dark", true);
38    });
39  });
40
41  // Follow OS theme if user hasn't explicitly chosen
42  try {
43    if (!localStorage.getItem(STORAGE_KEY) && window.matchMedia) {
44      const mq = window.matchMedia("(prefers-color-scheme: dark)");
45      const onChange = (e) => {
46        if (!localStorage.getItem(STORAGE_KEY)) {
47          applyTheme(e.matches ? "dark" : "light", false);
48        }
49      };
50      if (mq.addEventListener) mq.addEventListener("change", onChange);
51      else if (mq.addListener) mq.addListener(onChange);
52    }
53  } catch (e) {}
54
55  // ---------- 2. Smooth scroll ----------
56  document.querySelectorAll('a[href^="#"]').forEach((link) => {
57    link.addEventListener("click", (event) => {
58      const href = link.getAttribute("href");
59      if (!href || href === "#") return;
60      const target = document.querySelector(href);
61      if (!target) return;
62      event.preventDefault();
63      const top = target.getBoundingClientRect().top + window.scrollY - 80;
64      window.scrollTo({ top, behavior: prefersReduced ? "auto" : "smooth" });
65      history.replaceState(null, "", href);
66    });
67  });
68
69  // ---------- 3. FAQ accordion (exclusive) ----------
70  const faqItems = document.querySelectorAll(".faq details");
71  faqItems.forEach((item) => {
72    item.addEventListener("toggle", () => {
73      if (!item.open) return;
74      faqItems.forEach((other) => {
75        if (other !== item) other.open = false;
76      });
77    });
78  });
79
80  // ---------- 4. Footer year ----------
81  document.querySelectorAll("[data-year]").forEach((el) => {
82    el.textContent = String(new Date().getFullYear());
83  });
84
85  // ---------- 5. Nav: shadow + active section spy ----------
86  const nav = document.querySelector(".nav");
87  const navLinks = Array.from(document.querySelectorAll("[data-nav-link]"));
88  const sectionTargets = navLinks
89    .map((link) => {
90      const id = link.getAttribute("href");
91      return id ? document.querySelector(id) : null;
92    })
93    .filter(Boolean);
94
95  const onScroll = () => {
96    if (nav) nav.classList.toggle("is-scrolled", window.scrollY > 6);
97
98    // back-to-top visibility
99    const top = document.querySelector("[data-to-top]");
100    if (top) top.classList.toggle("is-visible", window.scrollY > 480);
101  };
102  onScroll();
103  window.addEventListener("scroll", onScroll, { passive: true });
104
105  // Scroll spy via IntersectionObserver — pick the first section "near the top"
106  if (sectionTargets.length && "IntersectionObserver" in window) {
107    const spy = new IntersectionObserver(
108      (entries) => {
109        entries.forEach((entry) => {
110          if (!entry.isIntersecting) return;
111          const id = "#" + entry.target.id;
112          navLinks.forEach((l) =>
113            l.classList.toggle("is-active", l.getAttribute("href") === id)
114          );
115        });
116      },
117      { rootMargin: "-30% 0px -55% 0px", threshold: 0 }
118    );
119    sectionTargets.forEach((s) => spy.observe(s));
120  }
121
122  // ---------- 6. Reveal-on-scroll with stagger ----------
123  if ("IntersectionObserver" in window && !prefersReduced) {
124    const reveals = document.querySelectorAll(".reveal");
125    reveals.forEach((el) => {
126      const delay = parseInt(el.getAttribute("data-delay") || "0", 10);
127      el.style.transitionDelay = delay + "ms";
128    });
129
130    const io = new IntersectionObserver(
131      (entries) => {
132        entries.forEach((entry) => {
133          if (entry.isIntersecting) {
134            entry.target.classList.add("is-in-view");
135            io.unobserve(entry.target);
136          }
137        });
138      },
139      { threshold: 0.14, rootMargin: "0px 0px -40px 0px" }
140    );
141    reveals.forEach((el) => io.observe(el));
142  } else {
143    document.querySelectorAll(".reveal").forEach((el) => el.classList.add("is-in-view"));
144  }
145
146  // ---------- 7. Magnetic buttons ----------
147  if (isPointerFine && !prefersReduced) {
148    document.querySelectorAll("[data-magnetic]").forEach((el) => {
149      const STRENGTH = 0.22;
150      const MAX = 8;
151
152      const onMove = (e) => {
153        const rect = el.getBoundingClientRect();
154        const x = e.clientX - rect.left - rect.width / 2;
155        const y = e.clientY - rect.top - rect.height / 2;
156        const dx = Math.max(-MAX, Math.min(MAX, x * STRENGTH));
157        const dy = Math.max(-MAX, Math.min(MAX, y * STRENGTH));
158        el.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
159      };
160
161      const onLeave = () => {
162        el.style.transform = "";
163      };
164
165      el.addEventListener("mousemove", onMove);
166      el.addEventListener("mouseleave", onLeave);
167    });
168  }
169
170  // ---------- 8. Bento cards: cursor spotlight ----------
171  if (isPointerFine && !prefersReduced) {
172    document.querySelectorAll(".bento-card").forEach((card) => {
173      card.addEventListener("mousemove", (e) => {
174        const rect = card.getBoundingClientRect();
175        const mx = ((e.clientX - rect.left) / rect.width) * 100;
176        const my = ((e.clientY - rect.top) / rect.height) * 100;
177        card.style.setProperty("--mx", mx + "%");
178        card.style.setProperty("--my", my + "%");
179      });
180    });
181  }
182
183  // ---------- 9. Counter animations ----------
184  if (!prefersReduced && "IntersectionObserver" in window) {
185    const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
186
187    const startCounter = (el) => {
188      const target = parseFloat(el.getAttribute("data-target") || "0");
189      const suffix = el.getAttribute("data-suffix") || "";
190      const duration = 1400;
191      const start = performance.now();
192
193      const tick = (now) => {
194        const t = Math.min(1, (now - start) / duration);
195        const value = Math.round(target * easeOutCubic(t));
196        el.textContent = String(value) + suffix;
197        if (t < 1) requestAnimationFrame(tick);
198      };
199      requestAnimationFrame(tick);
200    };
201
202    const counterIo = new IntersectionObserver(
203      (entries) => {
204        entries.forEach((entry) => {
205          if (entry.isIntersecting) {
206            startCounter(entry.target);
207            counterIo.unobserve(entry.target);
208          }
209        });
210      },
211      { threshold: 0.4 }
212    );
213
214    document.querySelectorAll("[data-counter]").forEach((el) => counterIo.observe(el));
215  }
216
217  // ---------- 10. Back to top ----------
218  document.querySelectorAll("[data-to-top]").forEach((btn) => {
219    btn.addEventListener("click", () => {
220      window.scrollTo({ top: 0, behavior: prefersReduced ? "auto" : "smooth" });
221    });
222  });
223
224  // ---------- 11. Mobile burger (mounts a slide-down drawer) ----------
225  const burger = document.querySelector("[data-burger]");
226  if (burger) {
227    const drawer = document.createElement("nav");
228    drawer.className = "nav-drawer";
229    drawer.setAttribute("aria-label", "Mobile menu");
230    drawer.innerHTML = `
231      <a href="#features">Features</a>
232      <a href="#how">How it works</a>
233      <a href="#privacy">Privacy</a>
234      <a href="#pricing">Pricing</a>
235      <a href="#faq">FAQ</a>
236    `;
237    document.body.appendChild(drawer);
238
239    const closeDrawer = () => {
240      drawer.classList.remove("is-open");
241      burger.setAttribute("aria-expanded", "false");
242    };
243
244    burger.addEventListener("click", () => {
245      const isOpen = drawer.classList.toggle("is-open");
246      burger.setAttribute("aria-expanded", isOpen ? "true" : "false");
247    });
248
249    drawer.querySelectorAll("a").forEach((a) => {
250      a.addEventListener("click", closeDrawer);
251    });
252
253    document.addEventListener("keydown", (e) => {
254      if (e.key === "Escape") closeDrawer();
255    });
256  }
257
258  // ---------- 12. Mesh blob parallax (very subtle, follows cursor) ----------
259  if (isPointerFine && !prefersReduced) {
260    const blobs = document.querySelectorAll(".mesh-blob");
261    if (blobs.length) {
262      let raf = null;
263      window.addEventListener(
264        "mousemove",
265        (e) => {
266          if (raf) return;
267          raf = requestAnimationFrame(() => {
268            const x = (e.clientX / window.innerWidth - 0.5) * 30;
269            const y = (e.clientY / window.innerHeight - 0.5) * 30;
270            blobs.forEach((b, i) => {
271              const factor = i === 0 ? 1 : i === 1 ? -1 : 0.6;
272              b.style.translate = `${x * factor}px ${y * factor}px`;
273            });
274            raf = null;
275          });
276        },
277        { passive: true }
278      );
279    }
280  }
281})();

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.