/* 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();
})();