/* Coco Beauty Salon — app root + interactions */ (function () { function App() { const [menu, setMenu] = React.useState(false); React.useEffect(() => { document.body.classList.toggle("no-scroll", menu); }, [menu]); // (re)build icons after every render so re-rendered subtrees keep their glyphs React.useEffect(() => { if (window.lucide) window.lucide.createIcons(); }); return ( React.createElement(React.Fragment, null, React.createElement(Nav, { onMenu: () => setMenu(true) }), React.createElement(Drawer, { open: menu, onClose: () => setMenu(false) }), React.createElement("main", null, React.createElement(Hero), React.createElement(TrustBar), React.createElement(Services), React.createElement(Signature), React.createElement(Cinematic), React.createElement(Gallery), React.createElement(Prices), React.createElement(Reviews), React.createElement(Booking), React.createElement(FAQ), React.createElement(Contact) ), React.createElement(FooterSection) ) ); } const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; const touch = window.matchMedia && window.matchMedia("(hover: none)").matches; /* ---------- entrance overlay (kept up until content is actually ready) ---------- */ const intro = document.getElementById("intro"); const START = Date.now(); const MIN = reduce ? 200 : 2050; let mounted = false, done = false; function revealAll() { document.querySelectorAll(".reveal").forEach((el) => el.classList.add("in")); } function doHide() { if (done) return; done = true; if (intro) intro.classList.add("hide"); document.documentElement.classList.remove("intro-active"); document.body.classList.remove("intro-active", "no-scroll"); setTimeout(() => { if (intro) intro.style.display = "none"; }, 1200); } // fade out only once React has mounted AND the minimum cinematic time has passed function attemptHide() { if (done) return; if (!mounted) return; const el = Date.now() - START; if (el >= MIN) doHide(); else setTimeout(attemptHide, MIN - el + 20); } document.documentElement.classList.add("intro-active"); document.body.classList.add("intro-active"); if (intro) intro.addEventListener("click", doHide); // tap to skip window.addEventListener("load", attemptHide); setTimeout(attemptHide, MIN + 40); setTimeout(doHide, 7000); // hard fallback — never stuck /* ---------- mount (with retry: babel scripts may still be arriving) ---------- */ let tries = 0, rendered = false; function render() { if (rendered) return; rendered = true; try { ReactDOM.createRoot(document.getElementById("root")).render(React.createElement(App)); mounted = true; attemptHide(); requestAnimationFrame(() => requestAnimationFrame(() => { try { initInteractions(); } catch (e) { console.error(e); revealAll(); } })); } catch (e) { console.error(e); rendered = false; if (tries++ < 60) { setTimeout(mount, 120); } else { // last resort — never leave a blank page: show a minimal branded view const root = document.getElementById("root"); if (root && !root.children.length) { root.innerHTML = '
' + '
Coco Beauty Salon
' + '
' + '
Houston, Texas
' + '
10209 Veterans Memorial Drive · Please refresh to load the full site.
' + '
'; } revealAll(); doHide(); } } } function mount() { const ready = window.React && window.ReactDOM && typeof window.Nav === "function"; if (!ready) { if (tries++ < 60) { setTimeout(mount, 100); return; } } // Let the intro paint its opening animation (wordmark fade) before we do the // heavy DOM mount — the overlay is opaque, so mounting behind it is invisible. // This keeps the desktop entrance animation smooth (no main-thread contention). if (reduce) { render(); } else { setTimeout(render, 720); } } /* ---------- interactions ---------- */ function initInteractions() { /* scroll reveal */ const els = document.querySelectorAll(".reveal"); if (reduce || !("IntersectionObserver" in window)) { els.forEach((el) => el.classList.add("in")); } else { const io = new IntersectionObserver((entries) => { entries.forEach((en) => { if (en.isIntersecting) { en.target.classList.add("in"); io.unobserve(en.target); } }); }, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" }); els.forEach((el) => io.observe(el)); } /* count-up on the price preview numbers */ const nums = document.querySelectorAll(".pv[data-count]"); const animateCount = (elm) => { const target = parseFloat(elm.getAttribute("data-count")) || 0; const dur = 1200, t0 = performance.now(); const step = (t) => { const p = Math.min(1, (t - t0) / dur); const eased = 1 - Math.pow(1 - p, 3); elm.textContent = "$" + Math.round(eased * target); if (p < 1) requestAnimationFrame(step); else elm.textContent = "$" + target; }; requestAnimationFrame(step); }; if (nums.length) { if (reduce || !("IntersectionObserver" in window)) { nums.forEach((n) => { n.textContent = "$" + (parseFloat(n.getAttribute("data-count")) || 0); }); } else { const co = new IntersectionObserver((ents) => { ents.forEach((en) => { if (en.isIntersecting) { animateCount(en.target); co.unobserve(en.target); } }); }, { threshold: 0.5 }); nums.forEach((n) => co.observe(n)); } } /* lazy-load the map iframe when contact nears the viewport (keeps initial load fast) */ const mapFrame = document.querySelector(".map-wrap iframe[data-src]"); if (mapFrame) { const load = () => { if (!mapFrame.src) mapFrame.src = mapFrame.getAttribute("data-src"); }; if ("IntersectionObserver" in window) { const mo = new IntersectionObserver((ents) => { ents.forEach((en) => { if (en.isIntersecting) { load(); mo.disconnect(); } }); }, { rootMargin: "400px" }); mo.observe(mapFrame); } else { load(); } } /* parallax on cinematic panels */ if (!reduce) { const panels = Array.from(document.querySelectorAll("[data-parallax]")); let ticking = false; const update = () => { panels.forEach((p) => { const bg = p.querySelector(".cine-bg"); if (!bg) return; const r = p.getBoundingClientRect(); if (r.bottom < 0 || r.top > window.innerHeight) return; const prog = (r.top + r.height / 2 - window.innerHeight / 2) / window.innerHeight; bg.style.transform = "translateY(" + (prog * -60).toFixed(1) + "px)"; }); ticking = false; }; window.addEventListener("scroll", () => { if (!ticking) { ticking = true; requestAnimationFrame(update); } }, { passive: true }); update(); } /* custom cursor (desktop / fine pointer only) */ if (!touch && window.matchMedia("(pointer: fine)").matches) { const ring = document.getElementById("cursor"); const dot = document.getElementById("cursor-dot"); if (ring && dot) { document.body.classList.add("cursor-on"); let rx = -100, ry = -100, x = -100, y = -100, shown = false, scale = 1, ts = 1; window.addEventListener("mousemove", (e) => { x = e.clientX; y = e.clientY; dot.style.transform = "translate(" + x + "px," + y + "px) translate(-50%,-50%)"; if (!shown) { shown = true; ring.classList.add("ready"); dot.classList.add("ready"); } }, { passive: true }); const loop = () => { rx += (x - rx) * 0.2; ry += (y - ry) * 0.2; scale += (ts - scale) * 0.25; ring.style.transform = "translate(" + rx.toFixed(2) + "px," + ry.toFixed(2) + "px) translate(-50%,-50%) scale(" + scale.toFixed(3) + ")"; requestAnimationFrame(loop); }; loop(); const grow = () => ring.classList.add("grow"); const shrink = () => ring.classList.remove("grow"); window.addEventListener("mousedown", () => { ts = 0.8; }, { passive: true }); window.addEventListener("mouseup", () => { ts = 1; }, { passive: true }); document.addEventListener("mouseover", (e) => { if (e.target.closest("a,button,.svc-card,.gal-item,.faq-q,input,select,textarea")) grow(); else shrink(); }); window.addEventListener("mouseout", (e) => { if (!e.relatedTarget) { ring.classList.remove("ready"); dot.classList.remove("ready"); shown = false; } }); } } } /* ---------- go ---------- */ mount(); })();