// Botón de llamada a la acción gamificado — un compañero que salta // a distintos lugares de la pantalla siguiendo tu lectura. const { useState: useJState, useEffect: useJEffect, useRef: useJRef } = React; const JOURNEY_FORM_URL = "https://form.typeform.com/to/vkeyUZXs"; // Perchas: posiciones por las que va saltando (fracciones del área libre) const PERCHES = [ { fx: 0.02, fy: 0.93 }, // abajo izquierda { fx: 0.60, fy: 0.90 }, // abajo centro { fx: 0.02, fy: 0.42 }, // borde izquierdo, a media altura { fx: 0.55, fy: 0.06 }, // arriba centro { fx: 0.03, fy: 0.07 }, // arriba izquierda { fx: 0.65, fy: 0.45 }, // centro derecha (junto al contenido) ]; const TAUNTS = [ "¡Sígueme!", "¡Por aquí!", "¡Vas muy bien!", "¿Me alcanzas?", "El quiz te espera…", "¡Ya casi!", "¡Una más y listo!", ]; const HOP_COLORS = ["#d96e48", "#3e8e9b", "#7a8f3e", "#9b6ed9", "#d9a23e", "#c75e7f"]; const SETTLE_PERCH = { fx: 0.02, fy: 0.93 }; // reposo: abajo a la izquierda const SETTLE_AFTER_MS = 3 * 60 * 1000; // 3 minutos function JourneyButton({ chatOpen }) { const KEY = "ego-journey-v1"; const [reached, setReached] = useJState(() => { try { return JSON.parse(localStorage.getItem(KEY)) || []; } catch (e) { return []; } }); const [total, setTotal] = useJState(0); const [current, setCurrent] = useJState(0); // sección visible ahora const [started, setStarted] = useJState(() => reached.length > 0); const [pos, setPos] = useJState(null); const [hopping, setHopping] = useJState(false); const [bubble, setBubble] = useJState(null); const [settled, setSettled] = useJState(false); // por visita: cada carga vuelve a jugar const settledRef = useJRef(settled); const chatOpenRef = useJRef(chatOpen); const wrapRef = useJRef(null); const prevCurrent = useJRef(-1); const hopCount = useJRef(0); const bubbleTimer = useJRef(null); useJEffect(() => { localStorage.setItem(KEY, JSON.stringify(reached)); }, [reached]); // Se asienta tras 3 minutos, o al llegar al fondo (lo que ocurra primero) useJEffect(() => { settledRef.current = settled; }, [settled]); useJEffect(() => { const t = setTimeout(() => setSettled(true), SETTLE_AFTER_MS); return () => clearTimeout(t); }, []); // Observa el scroll: progreso acumulado + sección actual useJEffect(() => { const secs = Array.from(document.querySelectorAll("main .sec")); setTotal(secs.length); let ticking = false; const check = () => { ticking = false; const line = window.innerHeight * 0.6; let cur = 0; const newly = []; secs.forEach((el, i) => { if (el.getBoundingClientRect().top < line) { newly.push(i); cur = i + 1; } }); setCurrent(cur); // ¿Llegamos al fondo de la página? const doc = document.documentElement; if (window.scrollY + window.innerHeight >= doc.scrollHeight - 80) setSettled(true); setReached((prev) => { const merged = Array.from(new Set([...prev, ...newly])); return merged.length === prev.length ? prev : merged; }); }; const onScroll = () => { if (!ticking) { ticking = true; requestAnimationFrame(check); } }; window.addEventListener("scroll", onScroll, { passive: true }); check(); return () => window.removeEventListener("scroll", onScroll); }, []); // Posición de la percha según la sección actual (esquiva chat y barra móvil) const computePos = (cur, isSettled) => { // Con el chat abierto en móvil, baja al fondo para no estorbar la conversación const mobileChat = chatOpenRef.current && window.innerWidth < 1024; const perch = (isSettled || mobileChat) ? SETTLE_PERCH : PERCHES[cur % PERCHES.length]; const w = window.innerWidth, h = window.innerHeight; const btn = wrapRef.current; const bw = btn ? btn.offsetWidth : 215; const bh = btn ? btn.offsetHeight : 64; let availW = w; if (w >= 1024) { const chat = document.querySelector(".ego-panel"); if (chat) availW = chat.getBoundingClientRect().left; } const minY = w < 1024 ? 82 : 14; const x = Math.max(14, Math.min(perch.fx * (availW - bw), availW - bw - 14)); const y = Math.max(minY, Math.min(perch.fy * (h - bh), h - bh - 12)); return { x, y }; }; // Salta de percha cada vez que cambias de sección (subiendo o bajando); // una vez asentado, se queda quieto abajo. useJEffect(() => { if (settled) { setPos(computePos(0, true)); setHopping(false); return; } setPos(computePos(current, false)); if (prevCurrent.current === -1) { prevCurrent.current = current; return; } if (current !== prevCurrent.current) { prevCurrent.current = current; hopCount.current += 1; setHopping(true); setBubble(TAUNTS[hopCount.current % TAUNTS.length]); clearTimeout(bubbleTimer.current); bubbleTimer.current = setTimeout(() => setBubble(null), 2600); const t = setTimeout(() => setHopping(false), 950); return () => clearTimeout(t); } }, [current, total, settled]); useJEffect(() => { const onResize = () => setPos(computePos(prevCurrent.current < 0 ? 0 : prevCurrent.current, settledRef.current)); window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); }, []); // Reposiciona al abrir/cerrar el chat en móvil useJEffect(() => { chatOpenRef.current = chatOpen; setPos(computePos(prevCurrent.current < 0 ? 0 : prevCurrent.current, settledRef.current)); if (chatOpen && window.innerWidth < 1024) setBubble(null); }, [chatOpen]); const done = total > 0 && reached.length >= total; const pct = total > 0 ? reached.length / total : 0; useJEffect(() => { if (done) { setBubble("¡Lo lograste!"); clearTimeout(bubbleTimer.current); bubbleTimer.current = setTimeout(() => setBubble(null), 3500); } }, [done]); const go = () => { if (!started) setStarted(true); if (done) { window.open(JOURNEY_FORM_URL, "_blank", "noopener"); return; } const secs = Array.from(document.querySelectorAll("main .sec")); const nextIdx = secs.findIndex((_, i) => !reached.includes(i)); const target = secs[nextIdx === -1 ? 0 : nextIdx]; if (target) { const y = target.getBoundingClientRect().top + window.scrollY - 84; window.scrollTo({ top: y, behavior: "smooth" }); } }; const R = 15.5; const C = 2 * Math.PI * R; const label = done ? "Empezar ahora" : !started ? "Empezar" : "Continuar"; const hopColor = HOP_COLORS[hopCount.current % HOP_COLORS.length]; const bubbleBelow = pos ? pos.y < window.innerHeight * 0.5 : true; return (
{bubble ? (
{bubble}
) : null}
); } Object.assign(window, { JourneyButton });