/* global React */ // Mereke landing — shared hooks & small helpers const { useState, useEffect, useRef, useCallback } = React; /* ---- Reliable scroll-position reveal engine ---------------------- IntersectionObserver is unreliable inside some preview iframes, so we use a single rAF-throttled scroll/resize checker reading getBoundingClientRect (which always works). Elements register a callback fired once when their top crosses ~88% of the viewport. */ const __revealSet = new Set(); let __revealQueued = false; function __checkReveals() { __revealQueued = false; const vh = window.innerHeight || document.documentElement.clientHeight; __revealSet.forEach((entry) => { const r = entry.el.getBoundingClientRect(); if (r.top < vh * 0.9 && r.bottom > 0) { entry.cb(); __revealSet.delete(entry); } }); } function __queueReveal() { if (__revealQueued) return; __revealQueued = true; setTimeout(__checkReveals, 40); } function __registerReveal(el, cb) { const entry = { el, cb }; __revealSet.add(entry); __queueReveal(); return () => __revealSet.delete(entry); } if (typeof window !== "undefined" && !window.__revealBound) { window.__revealBound = true; window.addEventListener("scroll", __queueReveal, { passive: true }); window.addEventListener("resize", __queueReveal); window.addEventListener("load", __queueReveal); // safety sweep in case scroll events are coalesced setInterval(__checkReveals, 350); } /* Scroll-reveal: adds .in when element enters viewport, then locks to the final state so content is guaranteed visible even if the iframe's transition timeline is throttled (background tabs / preview frames). */ function Reveal({ children, className = "", delay = 0, tag = "div", style = {}, ...rest }) { const ref = useRef(null); const [seen, setSeen] = useState(false); const [locked, setLocked] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; return __registerReveal(el, () => setSeen(true)); }, []); useEffect(() => { if (!seen) return; const t = setTimeout(() => setLocked(true), 1150); return () => clearTimeout(t); }, [seen]); const Tag = tag; const dcls = delay ? ` d${delay}` : ""; const lockStyle = locked ? { opacity: 1, transform: "none", transition: "none" } : null; return React.createElement(Tag, { ref, className: `reveal${dcls} ${seen ? "in" : ""} ${className}`, style: lockStyle ? { ...style, ...lockStyle } : style, ...rest }, children); } /* Count-up trigger that fires when scrolled into view */ function useInView() { const ref = useRef(null); const [inView, setInView] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; return __registerReveal(el, () => setInView(true)); }, []); return [ref, inView]; } function CountUp({ to, duration = 1800, prefix = "", suffix = "", sep = " " }) { const [ref, inView] = useInView(); const [val, setVal] = useState(0); useEffect(() => { if (!inView) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (reduce) { setVal(to); return; } const ease = (t) => 1 - Math.pow(1 - t, 3); const start = Date.now(); const id = setInterval(() => { const p = Math.min((Date.now() - start) / duration, 1); setVal(Math.round(ease(p) * to)); if (p >= 1) clearInterval(id); }, 33); return () => clearInterval(id); }, [inView, to, duration]); const fmt = (n) => n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, sep); return React.createElement("span", { ref }, prefix + fmt(val) + suffix); } /* Parallax: translateY a ref based on scroll position (timer-throttled) */ function useParallax(strength = 0.18) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (reduce) return; let queued = false; const update = () => { queued = false; const r = el.getBoundingClientRect(); const vh = window.innerHeight; const center = r.top + r.height / 2; const off = (center - vh / 2) / vh; el.style.transform = `translate3d(0, ${(-off * strength * 100).toFixed(2)}px, 0)`; }; const onScroll = () => { if (!queued) { queued = true; setTimeout(update, 16); } }; update(); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); }; }, [strength]); return ref; } const Spark = ({ className = "", style = {} }) => React.createElement("span", { className: `mk-spark ${className}`, "aria-hidden": "true", style }); const Eyebrow = ({ children, onRed = false }) => React.createElement("span", { className: `lp-eyebrow ${onRed ? "on-red" : ""}` }, React.createElement(Spark), children); const A = "assets"; // asset base /* ---- Event bus for modals ---------------------------------------- */ function openLead(detail) { window.dispatchEvent(new CustomEvent("mereke:lead", { detail: detail || null })); } function openProduct(p) { window.dispatchEvent(new CustomEvent("mereke:product", { detail: p })); } function useBus(name, handler) { useEffect(() => { const fn = (e) => handler(e.detail); window.addEventListener(name, fn); return () => window.removeEventListener(name, fn); }, [name, handler]); } /* ---- Premium CTA button ------------------------------------------ */ function CTA({ children, variant = "amber", size = "", onClick, href, lead = false, style = {} }) { const cls = `lp-btn lp-btn-${variant}${size ? " lp-btn-" + size : ""}`; const handle = (e) => { if (lead) { e.preventDefault(); openLead(); } if (onClick) onClick(e); }; if (href && !lead) return React.createElement("a", { href, className: cls, style, onClick: handle }, children); return React.createElement(href ? "a" : "button", { href: href || undefined, className: cls, style, onClick: handle, type: href ? undefined : "button" }, children); } /* ---- Ethnic texture layer (recolorable, optional parallax) -------- */ function Ethno({ color = "var(--ink-900)", variant = "", parallax = 0, style = {} }) { const ref = useRef(null); useEffect(() => { if (!parallax) return; const el = ref.current; if (!el) return; if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; let queued = false; const update = () => { queued = false; const r = el.parentElement.getBoundingClientRect(); const vh = window.innerHeight; const off = (r.top + r.height / 2 - vh / 2) / vh; el.style.transform = `translate3d(0, ${(-off * parallax * 100).toFixed(1)}px, 0)`; }; const onS = () => { if (!queued) { queued = true; setTimeout(update, 16); } }; update(); window.addEventListener("scroll", onS, { passive: true }); window.addEventListener("resize", onS); return () => { window.removeEventListener("scroll", onS); window.removeEventListener("resize", onS); }; }, [parallax]); return React.createElement("div", { ref, "aria-hidden": "true", className: `lp-ethno ${variant}`, style: { color, ...style } }); } /* ---- Ethnic divider ---------------------------------------------- */ const DividerMark = ({ style = {} }) => React.createElement("div", { className: "lp-divider-mark", style }, React.createElement("span", { className: "ln" }), React.createElement("span", { className: "curl flip" }), React.createElement(Spark, { style: { width: 15, height: 15 } }), React.createElement("span", { className: "curl" }), React.createElement("span", { className: "ln" })); /* ---- Countdown to a date ----------------------------------------- */ function daysUntil(dateStr) { const t = new Date(dateStr).getTime(); const now = Date.now(); const d = Math.max(0, Math.ceil((t - now) / 86400000)); return d; } // Источник визита для CRM. Снимаем один раз при заходе: переходы по якорям // (#katalog, #zayavka) стирают query-строку, и к моменту отправки формы UTM уже не достать. const UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"]; function captureSource() { try { if (sessionStorage.getItem("mereke:src")) return; const q = new URLSearchParams(location.search); const src = { landing: location.href, ref: document.referrer || "" }; UTM_KEYS.forEach((k) => { const v = q.get(k); if (v) src[k] = v; }); sessionStorage.setItem("mereke:src", JSON.stringify(src)); } catch (_) {} } function getSource() { try { return JSON.parse(sessionStorage.getItem("mereke:src") || "null") || {}; } catch (_) { return {}; } } captureSource(); Object.assign(window, { Reveal, useInView, CountUp, useParallax, Spark, Eyebrow, A, openLead, openProduct, useBus, CTA, Ethno, DividerMark, daysUntil, getSource });