/* 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 (
{children}
); } /* Lucide-style inline icons (1.75px stroke, currentColor). */ const ic = (paths) => ( {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 });