/* Billboard AR Experience — UI kit shared helpers
Reveal-on-scroll hook + wrapper, section shell, icon set. */
const { useEffect, useRef, useState } = React;
/* Expose design-system components as globals for the section files. */
Object.assign(window, window.BillboardARExperienceDesignSystem_6c41d9);
/* Reveal-on-scroll wrapper — CAPTURE / PRINT / NO-JS SAFE.
The element starts HIDDEN (opacity 0 + slide) only when we will actually
animate it (JS present, IntersectionObserver available, motion allowed) and
transitions to visible once it scrolls into view — so there is NO flash /
flicker. If we won't animate (reduced motion or no IO), it renders visible
from the first paint. Everything here is React, so "no JS" = no element. */
function Reveal({ children, as = "div", delay = 0, style, ...rest }) {
const ref = useRef(null);
const reduce = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const noIO = typeof window === "undefined" || !("IntersectionObserver" in window);
const animate = !reduce && !noIO;
const [inview, setInview] = useState(!animate); // visible immediately if we won't animate
useEffect(() => {
if (!animate) return;
const el = ref.current;
if (!el) return;
let done = false;
const reveal = () => { if (!done) { done = true; setInview(true); } };
const inView = () => {
const r = el.getBoundingClientRect();
const vh = window.innerHeight || document.documentElement.clientHeight;
return r.top < vh && r.bottom > 0;
};
// Already in the viewport at mount → reveal now, no IntersectionObserver
// round-trip (deterministic; also covers backgrounded-tab loads).
if (inView()) { reveal(); return; }
const io = new IntersectionObserver((entries) => {
if (entries.some((e) => e.isIntersecting)) { reveal(); io.disconnect(); }
}, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" });
io.observe(el);
// Safety: if loaded while the tab was hidden (IO throttled), re-check once
// the page becomes visible and the element happens to be in view.
const onVis = () => { if (!document.hidden && inView()) { reveal(); io.disconnect(); } };
document.addEventListener("visibilitychange", onVis);
return () => { io.disconnect(); document.removeEventListener("visibilitychange", onVis); };
}, []);
const Tag = as;
const { className, ...restNoClass } = rest;
const revealStyle = animate
? {
opacity: inview ? 1 : 0,
transform: inview ? "none" : "translateY(28px)",
transition: "opacity 760ms var(--ease-out) " + delay + "ms, transform 760ms var(--ease-out) " + delay + "ms",
willChange: inview ? "auto" : "opacity, transform",
}
: {};
return (
{children}
);
}
/* Section shell: handles tone (dark / light), vertical rhythm, centered column. */
function Section({ children, tone = "dark", id, style, inner, ...rest }) {
const bg = tone === "light"
? "var(--grad-light)"
: tone === "navy"
? "var(--surface-navy)"
: "var(--surface-base)";
const color = tone === "light" ? "var(--ink)" : "var(--white)";
return (
);
}
/* Lucide-style inline icons (1.75px stroke, currentColor). */
const ic = (paths) => (
);
const Icons = {
arrow: ic(<>>),
chevronDown: ic(),
graduation: ic(<>>),
network: ic(<>>),
camera: ic(<>>),
star: ic(),
mic: ic(<>>),
globe: ic(<>>),
badge: ic(<>>),
calendar: ic(<>>),
sparkles: ic(<>>),
briefcase: ic(<>>),
users: ic(<>>),
film: ic(<>>),
map: ic(<>>),
download: ic(<>>),
};
/* ---- Datos de contacto / redes ------------------------------ */
const BB = {
WHATSAPP: "5491171647891",
EMAIL: "experience@billboard.ar",
BRAND_EMAIL: "brandpartnerships@billboard.ar",
SITE: "https://www.billboard.ar",
};
/* ---- Firebase (compat) — persiste el lead para el bot de WhatsApp + dashboard.
Config del Web App — proyecto billboard-experience. ---------- */
firebase.initializeApp({
apiKey: "AIzaSyCKQ7zCP666SC3cgvvhzYkwhrmk3eRQl1U",
authDomain: "billboard-experience.firebaseapp.com",
projectId: "billboard-experience",
storageBucket: "billboard-experience.firebasestorage.app",
messagingSenderId: "330029000470",
appId: "1:330029000470:web:54c542b958219567591237",
measurementId: "G-8MEGQCSD8R", // Firebase Analytics (GA4) — propiedad "billboard-experience" en la cuenta Billboard AR
});
firebase.appCheck().activate("6Ld-hlMtAAAAAIxaZ976a7UdICjU_MIxqLhZVU9v", true);
const bbDb = firebase.firestore();
/* ---- Analytics (Firebase Analytics = GA4) --------------------
Consent Mode default + sin banner de cookies. bbTrack() es no-op si Analytics
no está disponible: measurementId placeholder, bloqueador, o init falla.
Todo lo trackeado se espeja en window.__bbEvents para debug/verificación. */
let bbAnalytics = null;
(function bbInitAnalytics() {
try {
const mid = (firebase.app().options.measurementId || "");
if (!mid || mid.indexOf("XXXX") !== -1) return; // placeholder → no-op
bbAnalytics = firebase.analytics();
bbAnalytics.setAnalyticsCollectionEnabled(true);
try {
bbAnalytics.setConsent({
ad_storage: "denied",
ad_user_data: "denied",
ad_personalization: "denied",
analytics_storage: "granted",
});
} catch (e) { /* setConsent no disponible en esta versión: defaults GA4 OK */ }
} catch (e) {
bbAnalytics = null;
}
})();
function bbTrack(eventName, params) {
try {
if (bbAnalytics) bbAnalytics.logEvent(eventName, params || {});
} catch (e) { /* no-op */ }
(window.__bbEvents = window.__bbEvents || []).push({ e: eventName, p: params || {}, t: Date.now() });
}
/* Scroll depth 25/50/75/100 (una vez cada uno). */
(function bbScrollDepth() {
const marks = [25, 50, 75, 100];
const fired = {};
const onScroll = function () {
const doc = document.documentElement;
const height = doc.scrollHeight - doc.clientHeight;
if (height <= 0) return;
const pct = Math.round(((doc.scrollTop || document.body.scrollTop) / height) * 100);
marks.forEach(function (m) {
if (pct >= m && !fired[m]) { fired[m] = true; bbTrack("scroll_depth", { percent: m }); }
});
};
window.addEventListener("scroll", onScroll, { passive: true });
})();
/* matchKey: últimos 10 dígitos del teléfono, para correlacionar con el wa_id de WhatsApp. */
function bbMatchKey(tel) {
const d = (tel || "").replace(/\D/g, "");
return d.length < 10 ? "" : d.slice(-10);
}
/* Guarda el lead en Firestore (fire-and-forget desde el form). */
function saveLead(fields) {
return bbDb.collection("leads").add({
nombre: fields.nombre || "",
email: fields.email || "",
telefono: fields.telefono || "",
estudias: fields.estudias || "",
universidad: fields.universidad || "",
mensaje: fields.mensaje || "",
matchKey: bbMatchKey(fields.telefono),
source: "landing",
status: "formulario_enviado",
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
wa: { waId: null, lastMessageId: null, firstInboundAt: null, lastInboundAt: null, windowExpiresAt: null },
flow: { intent: null, intentAt: null, duda: null, awaitingDuda: false },
stages: {
billboard: { status: "pending", by: null, at: null },
agency: { status: "pending", by: null, at: null },
},
emailFlags: { welcomeSent: false, infoSent: false, reactivationSent: false },
});
}
Object.assign(window, { Reveal, Section, Icons, BB, saveLead, bbTrack });