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.