PageSourceSearch

https://www.sloothy.ai/assets/app.js

js sloothy.ai collected 2026-10-05 14:58:38 UTC 9,339 bytes, 193 lines download raw bytes

1/* ============================================================
2   Sloothy — geteilte Laufzeit für alle statischen Seiten
3   Nav, Mobile-Menü, Scroll-Fortschritt ("roter Faden"),
4   Reveal-Animationen (GSAP, sichtbarkeits-sicher), Count-up,
5   Hero-Parallax, sanfte In-Page-Anker.
6   Kein Router — echte URLs, eine HTML-Datei pro Seite.
7   ============================================================ */
8(function () {
9  "use strict";
10  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
11  var nf = new Intl.NumberFormat("de-DE");
12  window.SLOOTHY_NF = nf;
13  var ctx = null;
14
15  /* ---------- Mobile-Menü ---------- */
16  var toggle = document.getElementById("navToggle");
17  var menu = document.getElementById("mobileMenu");
18  function closeMenu() {
19    document.body.classList.remove("menu-open");
20    if (toggle) toggle.setAttribute("aria-expanded", "false");
21  }
22  function openMenu() {
23    document.body.classList.add("menu-open");
24    if (toggle) toggle.setAttribute("aria-expanded", "true");
25  }
26  if (toggle) {
27    toggle.addEventListener("click", function () {
28      document.body.classList.contains("menu-open") ? closeMenu() : openMenu();
29    });
30  }
31  if (menu) menu.addEventListener("click", function (e) { if (e.target.tagName === "A") closeMenu(); });
32  window.addEventListener("keydown", function (e) { if (e.key === "Escape") closeMenu(); });
33
34  /* ---------- Eingeloggter Nutzer im Header ---------- */
35  function createProfileIdentity(user) {
36    const displayName = [user.firstName, user.lastName].filter(Boolean).join(" ") || (user.email || "").split("@")[0] || "Meine Projekte";
37    const initials = [user.firstName, user.lastName].filter(Boolean).map(function (name) { return name.charAt(0); }).join("").toUpperCase() || displayName.charAt(0).toUpperCase();
38    const profileImage = user.profileImage;
39    const imageUrl = profileImage && typeof profileImage === "object" ? (profileImage.filename ? `/api/media/file/${profileImage.filename}` : profileImage.url) : null;
40    const fragment = document.createDocumentFragment();
41    const avatar = imageUrl ? document.createElement("img") : document.createElement("span");
42    avatar.setAttribute("aria-hidden", "true");
43    avatar.style.cssText = "width:34px;height:34px;border-radius:50%;flex:none;";
44    if (imageUrl) {
45      avatar.src = imageUrl;
46      avatar.alt = "";
47      avatar.style.objectFit = "cover";
48    } else {
49      avatar.textContent = initials;
50      avatar.style.cssText += "display:inline-flex;align-items:center;justify-content:center;background:var(--green-100);color:var(--green-700);font-size:.78rem;font-weight:800;";
51    }
52    const name = document.createElement("span");
53    name.textContent = displayName;
54    fragment.append(avatar, name);
55    return { displayName: displayName, fragment: fragment };
56  }
57
58  async function initHeaderAccount() {
59    const links = document.querySelectorAll("[data-header-account]");
60    if (!links.length) return;
61    try {
62      const response = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" });
63      if (!response.ok) return;
64      const data = await response.json();
65      if (!data.user) return;
66      links.forEach(function (link) {
67        const identity = createProfileIdentity(data.user);
68        link.replaceChildren(identity.fragment);
69        link.href = "/projects/meine-projekte";
70        link.setAttribute("aria-label", `Zu meinen Projekten: ${identity.displayName}`);
71        if (link.dataset.headerAccount === "desktop") {
72          link.style.cssText = "min-height:44px;height:44px;padding:0 .9rem 0 .35rem;background:var(--card);color:var(--ink);border-color:var(--border-strong);box-shadow:var(--shadow-sm);";
73        }
74      });
75    } catch (_) {
76      // Statischer CTA bleibt bei Netzwerkfehlern nutzbar.
77    }
78  }
79
80  /* ---------- Nav-Zustand + Scroll-Fortschritt ---------- */
81  var nav = document.getElementById("nav");
82  var pbar = document.getElementById("progressBar");
83  function onScroll() {
84    if (nav) nav.classList.toggle("scrolled", window.scrollY > 12);
85    document.body.classList.toggle("tb-hidden", window.scrollY > 12);
86    if (pbar) {
87      var max = document.documentElement.scrollHeight - window.innerHeight;
88      pbar.style.width = (max > 0 ? Math.min(100, (window.scrollY / max) * 100) : 0) + "%";
89    }
90  }
91  window.addEventListener("scroll", onScroll, { passive: true });
92
93  /* ---------- Sanfte In-Page-Anker ---------- */
94  document.addEventListener("click", function (e) {
95    var a = e.target.closest("[data-scrollto]");
96    if (!a) return;
97    var el = document.getElementById(a.getAttribute("data-scrollto"));
98    if (el) { e.preventDefault(); el.scrollIntoView({ behavior: reduce ? "auto" : "smooth", block: "start" }); }
99  });
100
101  /* ---------- Count-up ---------- */
102  function countUp(el) {
103    if (el.dataset.done) return;
104    el.dataset.done = "1";
105    var target = parseFloat(el.getAttribute("data-count"));
106    var dec = parseInt(el.getAttribute("data-decimals") || "0", 10);
107    var fmt = new Intl.NumberFormat("de-DE", { minimumFractionDigits: dec, maximumFractionDigits: dec });
108    if (reduce) { el.textContent = fmt.format(target); return; }
109    var start = performance.now(), dur = 1400;
110    function tick(now) {
111      var t = Math.min((now - start) / dur, 1), e = 1 - Math.pow(1 - t, 3);
112      el.textContent = fmt.format(target * e);
113      if (t < 1) requestAnimationFrame(tick);
114    }
115    requestAnimationFrame(tick);
116  }
117  window.SLOOTHY_countUp = countUp;
118
119  /* ---------- Animationen (sichtbarkeits-sicher) ----------
120     Inhalt geht immer vor: ohne GSAP, bei reduced-motion oder
121     verstecktem Tab (gedrosseltes rAF) wird alles sofort gezeigt. */
122  function initAnimations() {
123    var hasGSAP = window.gsap && window.ScrollTrigger;
124    if (ctx) { ctx.revert(); ctx = null; }
125    if (!hasGSAP || reduce || document.hidden) {
126      document.querySelectorAll("[data-count]").forEach(countUp);
127      return;
128    }
129    var gsap = window.gsap, ScrollTrigger = window.ScrollTrigger;
130    gsap.registerPlugin(ScrollTrigger);
131    ctx = gsap.context(function () {
132      var heroEls = document.querySelectorAll("[data-hero]");
133      if (heroEls.length) {
134        var tl = gsap.timeline();
135        tl.from(heroEls, { opacity: 0, y: 26, duration: 0.9, ease: "power3.out", stagger: 0.1, delay: 0.05 });
136        setTimeout(function () { if (tl.progress() < 1) tl.progress(1); }, 1600);
137      }
138      var heroImg = document.getElementById("heroImg");
139      if (heroImg) {
140        gsap.to(heroImg, { scale: 1.14, ease: "none", scrollTrigger: { trigger: heroImg.closest(".hero"), start: "top top", end: "bottom top", scrub: true } });
141      }
142      document.querySelectorAll("[data-reveal]").forEach(function (el) {
143        gsap.from(el, { opacity: 0, y: 30, duration: 0.7, ease: "power2.out", scrollTrigger: { trigger: el, start: "top 88%" } });
144      });
145      document.querySelectorAll("[data-shot]").forEach(function (el) {
146        var img = el.querySelector("img") || el;
147        gsap.fromTo(img, { scale: 1.12, opacity: 0.45 }, { scale: 1, opacity: 1, duration: 1.1, ease: "power2.out", scrollTrigger: { trigger: el, start: "top 85%" } });
148      });
149      document.querySelectorAll(".band .bg, .cta-band .bg").forEach(function (el) {
150        gsap.fromTo(el, { yPercent: -6 }, { yPercent: 6, ease: "none", scrollTrigger: { trigger: el.parentElement, start: "top bottom", end: "bottom top", scrub: true } });
151      });
152      document.querySelectorAll("[data-count]").forEach(function (el) {
153        ScrollTrigger.create({ trigger: el, start: "top 92%", once: true, onEnter: function () { countUp(el); } });
154      });
155      ScrollTrigger.refresh();
156    });
157  }
158  document.addEventListener("visibilitychange", function () { if (!document.hidden && !ctx) initAnimations(); });
159
160  /* ---------- Preiskarte (Hover/Tap) ---------- */
161  function initStadtkarten() {
162    document.querySelectorAll("[data-stadtkarte]").forEach(function (map) {
163      var info = map.querySelector("[data-sk-info]");
164      var idle = info ? info.getAttribute("data-idle") : "";
165      map.querySelectorAll(".skg").forEach(function (g) {
166        function act() {
167          map.querySelectorAll(".skg.active").forEach(function (x) { if (x !== g) x.classList.remove("active"); });
168          g.classList.add("active");
169          if (info) info.innerHTML = "<b>" + g.dataset.name + "</b> · ø Wohnung: " + g.dataset.price + "/m² · Einordnung: " + g.dataset.band;
170        }
171        g.addEventListener("mouseenter", act);
172        g.addEventListener("focus", act);
173        g.addEventListener("click", act);
174        g.addEventListener("mouseleave", function () { g.classList.remove("active"); if (info) info.innerHTML = idle; });
175        g.addEventListener("blur", function () { g.classList.remove("active"); });
176      });
177    });
178  }
179
180  /* ---------- Boot ---------- */
181  function boot() {
182    onScroll();
183    initAnimations();
184    initStadtkarten();
185    initHeaderAccount();
186    if (window.SLOOTHY_initCalculators) window.SLOOTHY_initCalculators(nf);
187    if (document.fonts && document.fonts.ready) {
188      document.fonts.ready.then(function () { if (window.ScrollTrigger) window.ScrollTrigger.refresh(); });
189    }
190  }
191  if (document.readyState === "complete") boot();
192  else window.addEventListener("load", boot);
193})();

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.