// ───────────────────────────────────────────────────────────── // POSTER CAROUSEL — Netflix-style, full-bleed, drag tátil 1:1 // Auto-scroll contínuo + momentum + arrasto direto pelo dedo // ───────────────────────────────────────────────────────────── const { useState: _useState, useEffect: _useEffect, useRef: _useRef, useMemo: _useMemo, useCallback: _useCallback } = React; // AUTO-SCROLL constant (pixels per millisecond) — bem lento, ambiente const AUTO_SPEED = 0.018; const FRICTION = 0.93; const VELOCITY_THRESHOLD = 0.02; const DRAG_THRESHOLD = 8; // px — abaixo disso é clique, acima é arrasto // ───────── PÔSTER UNITÁRIO ───────── function PosterCard({ module, t, isCenter, onOpen, onBuy, onCode, unlocked, width, height }) { const isSoon = module.status === "soon"; const isCombo = !!module.isCombo; const comboActive = isCombo && unlocked; // combo com tudo liberado const locked = !isSoon && !unlocked; // seção bloqueada // tonalidade sutil por módulo const TONES = { stone: { wash: "rgba(180,170,150,0.06)", glow: "rgba(220,200,160,0.10)" }, olive: { wash: "rgba(140,160,120,0.06)", glow: "rgba(160,180,130,0.10)" }, ink: { wash: "rgba(120,130,160,0.05)", glow: "rgba(150,160,200,0.08)" }, ash: { wash: "rgba(180,180,180,0.04)", glow: "rgba(200,200,200,0.06)" }, sage: { wash: "rgba(150,170,140,0.06)", glow: "rgba(175,195,150,0.13)" }, premium: { wash: "rgba(220,190,130,0.12)", glow: "rgba(240,210,150,0.18)" }, }; const tone = TONES[module.accent] || TONES.stone; const accessPill = (() => { if (isSoon) return { label: "Em breve" }; if (module.free) return { label: "Grátis" }; if (isCombo) return { label: comboActive ? "Ativo" : "Combo" }; return { label: unlocked ? "Liberado" : "Bloqueado" }; })(); // Decide rótulo do botão principal const buttonLabel = isCombo ? (comboActive ? "Acesso ativo" : "Adquirir") : module.free ? "Acessar grátis" : unlocked ? "Acessar" : "Comprar"; const countLabelStyle = { fontFamily: TYPE.sans, fontSize: 10, color: t.ink3, letterSpacing: "0.14em", textTransform: "uppercase", fontWeight: 700, marginBottom: 6, whiteSpace: "nowrap", }; const buttonBase = { fontFamily: TYPE.sans, fontSize: 12, letterSpacing: "0.12em", textTransform: "uppercase", fontWeight: 800, display: "flex", alignItems: "center", justifyContent: "center", gap: 8, width: "100%", padding: "16px 18px", borderRadius: 12, whiteSpace: "nowrap", boxSizing: "border-box", }; return (
{/* textura de papel sutil */}
{/* faixa de luz sutil */}
{/* moldura interior */}
{/* ÁREA DE TOQUE — abre o módulo / lista. NÃO cobre a área dos botões. */} {!isSoon && ( )} {/* CONTEÚDO */}
{/* TOP META */}
{isCombo ? "Combo" : module.sub}
{/* caixa de largura FIXA p/ o selo — mantém a quebra do texto à esquerda idêntica em qualquer estado (Bloqueado/Liberado/Grátis/etc) */}
{locked && ( )} {accessPill.label}
{/* CENTRO: número grande (borrado quando bloqueado) */}
{module.chapter}
{/* BOTTOM TÍTULO */}
{nl2br(module.name)}
{nl2br(module.desc)}
{/* PREÇO + AÇÃO */}
{isSoon ? (
{module.count}
Em breve
) : (unlocked || comboActive) ? (
{module.count}
{module.free ? "Grátis" : "Acesso liberado"}
) : (
{module.count}
{module.term === "ano" ? (
12x R$ {(module.price / 12).toFixed(2).replace(".", ",")}
{module.priceOld && R$ {module.priceOld}} ou R$ {module.price} à vista · 1 ano de acesso
) : (
{module.priceOld && ( R$ {module.priceOld} )} R$ {module.price}
)}
)} {comboActive ? (
Acesso ativo
) : ( )}
); } // ───────── CARROSSEL ───────── function PosterCarousel({ modules, t, onOpenModule, onBuyModule, onCodeModule, isUnlocked, frameWidth, frameHeight }) { const posterW = Math.round(frameWidth * 0.78); const posterH = Math.max(Math.round(frameHeight * 0.70) + 78, 460); const gap = 14; const itemW = posterW + gap; const sidePad = (frameWidth - posterW) / 2; const REPS = 3; const repeated = _useMemo(() => { const arr = []; for (let i = 0; i < REPS; i++) modules.forEach((m, idx) => arr.push({ ...m, _key: `${i}-${idx}` })); return arr; }, [modules]); const trackRef = _useRef(null); const wrapRef = _useRef(null); const offsetRef = _useRef(0); const velocityRef = _useRef(0); const rafRef = _useRef(0); const lastTimeRef = _useRef(0); const draggingRef = _useRef(false); const startXRef = _useRef(0); const startYRef = _useRef(0); const startOffsetRef = _useRef(0); const lastPxRef = _useRef(0); const lastTRef = _useRef(0); const movedRef = _useRef(false); // já passou do threshold? const [centerIdx, setCenterIdx] = _useState(0); const baseLen = modules.length * itemW; _useEffect(() => { offsetRef.current = -baseLen; if (trackRef.current) trackRef.current.style.transform = `translate3d(${offsetRef.current}px,0,0)`; }, [baseLen]); // loop principal _useEffect(() => { let canceled = false; const loop = (now) => { if (canceled) return; const dt = Math.min(50, now - (lastTimeRef.current || now)); lastTimeRef.current = now; if (!draggingRef.current) { offsetRef.current += velocityRef.current * dt; velocityRef.current *= FRICTION; if (Math.abs(velocityRef.current) < VELOCITY_THRESHOLD) { velocityRef.current = -AUTO_SPEED; } } while (offsetRef.current <= -2 * baseLen) offsetRef.current += baseLen; while (offsetRef.current > -baseLen) offsetRef.current -= baseLen; if (trackRef.current) { trackRef.current.style.transform = `translate3d(${offsetRef.current}px,0,0)`; } const localOffset = offsetRef.current % baseLen; const adjusted = (-localOffset - sidePad) / itemW; const idx = ((Math.round(adjusted) % modules.length) + modules.length) % modules.length; setCenterIdx((cur) => (cur === idx ? cur : idx)); // parallax sutil do glow interno (baseado na posição real na tela) const wrapEl = wrapRef.current, trEl = trackRef.current; if (wrapEl && trEl) { const half = wrapEl.clientWidth / 2; const base = wrapEl.getBoundingClientRect().left; const kids = trEl.children; for (let i = 0; i < kids.length; i++) { const r = kids[i].getBoundingClientRect(); const d = (r.left - base + r.width / 2) - half; kids[i].style.setProperty("--gx", (d * 0.05).toFixed(1) + "px"); } } rafRef.current = requestAnimationFrame(loop); }; rafRef.current = requestAnimationFrame(loop); return () => { canceled = true; cancelAnimationFrame(rafRef.current); }; }, [baseLen, itemW, sidePad, modules.length]); // ─── POINTER HANDLERS (sem capture — deixa clicks passarem) ─── _useEffect(() => { const wrap = wrapRef.current; if (!wrap) return; const onDown = (e) => { if (e.pointerType === "mouse" && e.button !== 0) return; draggingRef.current = true; movedRef.current = false; startXRef.current = e.clientX; startYRef.current = e.clientY; startOffsetRef.current = offsetRef.current; lastPxRef.current = e.clientX; lastTRef.current = performance.now(); velocityRef.current = 0; }; const onMove = (e) => { if (!draggingRef.current) return; const dx = e.clientX - startXRef.current; const dy = e.clientY - startYRef.current; // Se moveu mais vertical que horizontal nos primeiros pixels, é scroll da página if (!movedRef.current && Math.abs(dy) > Math.abs(dx) && Math.abs(dy) > DRAG_THRESHOLD) { draggingRef.current = false; return; } if (Math.abs(dx) > DRAG_THRESHOLD) { movedRef.current = true; e.preventDefault?.(); } if (movedRef.current) { offsetRef.current = startOffsetRef.current + dx; const now = performance.now(); const ddt = now - lastTRef.current; if (ddt > 0) { const dxLocal = e.clientX - lastPxRef.current; velocityRef.current = dxLocal / ddt; } lastPxRef.current = e.clientX; lastTRef.current = now; } }; const onUp = () => { draggingRef.current = false; }; // Listeners de move/up no documento garantem que continuamos // recebendo eventos mesmo se o dedo sair do wrap. // Click bubbles normalmente, sem ser sequestrado por pointer capture. wrap.addEventListener("pointerdown", onDown); window.addEventListener("pointermove", onMove, { passive: false }); window.addEventListener("pointerup", onUp); window.addEventListener("pointercancel", onUp); return () => { wrap.removeEventListener("pointerdown", onDown); window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp); window.removeEventListener("pointercancel", onUp); }; }, []); // Bloqueia o click no poster se foi arrasto (capture phase) const onClickCapture = (e) => { if (movedRef.current) { e.stopPropagation(); e.preventDefault(); movedRef.current = false; } }; return (
{repeated.map((m, i) => { const realIdx = i % modules.length; return ( onOpenModule?.(m)} onBuy={() => onBuyModule?.(m)} onCode={() => onCodeModule?.(m)} /> ); })}
{/* PAGINAÇÃO — pontos */}
{modules.map((m, i) => ( ))}
); } Object.assign(window, { PosterCarousel });