/* global React */ // ============================================================ // Craque Cidadão — shared utilities (Icon, motion, decorations) // ============================================================ const CC_LOGO = 'assets/logo-craque-vertical-onwhite.png'; // WhatsApp config (used by floating button + CTAs) const WA_NUMBER = '5521966377153'; const WA_MSG = encodeURIComponent('Olá! Vim pelo site da ONG Craque Cidadão e gostaria de saber mais.'); const WA_LINK = `https://wa.me/${WA_NUMBER}?text=${WA_MSG}`; const IG_LINK = 'https://www.instagram.com/ongcraquecidadao/'; // Lucide icon helper — injects a detached that lucide swaps, // so React never owns the replaced node (no reconciliation errors). function Icon({ name, size = 20, color = 'currentColor', strokeWidth = 2, style = {} }) { const ref = React.useRef(null); React.useEffect(() => { const host = ref.current; if (!host || !window.lucide) return; host.innerHTML = ''; const tmp = document.createElement('i'); tmp.setAttribute('data-lucide', name); tmp.setAttribute('width', size); tmp.setAttribute('height', size); tmp.setAttribute('stroke-width', strokeWidth); host.appendChild(tmp); window.lucide.createIcons(); }, [name, size, strokeWidth]); return ; } // Global motion preference (set by Tweaks; defaults to subtle) window.CC_MOTION = window.CC_MOTION || 'subtle'; // Reveal — fade/slide a block in when it scrolls into view. function Reveal({ children, delay = 0, y = 24, as = 'div', style = {}, ...rest }) { const ref = React.useRef(null); const [shown, setShown] = React.useState(window.CC_MOTION === 'off'); React.useEffect(() => { if (window.CC_MOTION === 'off') { setShown(true); return; } const el = ref.current; if (!el) return; // Already in view on mount? show immediately. const r = el.getBoundingClientRect(); if (r.top < (window.innerHeight || 800) && r.bottom > 0) setShown(true); const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { setShown(true); io.disconnect(); } }); }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }); io.observe(el); // Failsafe: if IO never delivers (some embedded previews), reveal anyway. const fs = setTimeout(() => setShown(true), 700); return () => { io.disconnect(); clearTimeout(fs); }; }, []); const Tag = as; return ( {children} ); } // CountUp — animated numeral. Counts when scrolled into view. function CountUp({ end, duration = 1400, prefix = '+', className, style }) { const ref = React.useRef(null); const [val, setVal] = React.useState(window.CC_MOTION === 'off' ? end : 0); React.useEffect(() => { if (window.CC_MOTION === 'off') { setVal(end); return; } const el = ref.current; if (!el) return; let raf, started = false; const run = (ts0) => { const step = (ts) => { const p = Math.min(1, (ts - ts0) / duration); const eased = 1 - Math.pow(1 - p, 3); setVal(Math.round(eased * end)); if (p < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); }; const start = () => { if (!started) { started = true; requestAnimationFrame(run); io.disconnect(); } }; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) start(); }); }, { threshold: 0.4 }); io.observe(el); // If already on screen at mount, kick it off. const r = el.getBoundingClientRect(); if (r.top < (window.innerHeight || 800) && r.bottom > 0) start(); // Failsafe: if IO never delivers, snap to the final value. const fs = setTimeout(() => { if (!started) { started = true; setVal(end); } }, 800); return () => { io.disconnect(); clearTimeout(fs); if (raf) cancelAnimationFrame(raf); }; }, [end]); const padded = String(val).padStart(String(end).length >= 2 && end < 100 ? 2 : 0, '0'); return ( {prefix} {padded} ); } // CourtLines — subtle futsal-court decoration for dark/blue areas. function CourtLines({ opacity = 0.12, color = '#fff' }) { return ( ); } // Speed lines — energetic diagonal motion streaks (decorative). function SpeedLines({ color = 'var(--gold-400)', style = {} }) { return ( ); } // Section wrapper with consistent vertical rhythm (density-aware via --cc-sy). function Section({ children, bg = 'transparent', style = {}, narrow = false, id }) { return (
{children}
); } // Official WhatsApp glyph. function WAIcon({ size = 20, color = 'currentColor' }) { return ( ); } // Polished WhatsApp button — official glyph, gradient, hover lift. Shared everywhere. function WAButton({ children = 'Fale pelo WhatsApp', size = 'lg', full = false, style = {} }) { const [h, setH] = React.useState(false); const S = { sm: { padding: '9px 16px', fs: 13.5, ic: 16, gap: 8 }, md: { padding: '12px 20px', fs: 15, ic: 18, gap: 9 }, lg: { padding: '15px 28px', fs: 16.5, ic: 21, gap: 11 }, }[size]; return ( setH(true)} onMouseLeave={() => setH(false)} style={{ display: full ? 'flex' : 'inline-flex', width: full ? '100%' : undefined, alignItems: 'center', justifyContent: 'center', gap: S.gap, padding: S.padding, borderRadius: 'var(--radius-pill)', textDecoration: 'none', fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: S.fs, letterSpacing: '0.01em', color: '#fff', background: 'linear-gradient(135deg, #2BD96E 0%, #1EBE5B 100%)', boxShadow: h ? '0 16px 34px rgba(30,190,91,0.45)' : '0 10px 24px rgba(30,190,91,0.32)', transform: h ? 'translateY(-2px)' : 'none', transition: 'transform var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out)', whiteSpace: 'nowrap', }}> {children} ); } Object.assign(window, { CC_LOGO, WA_NUMBER, WA_LINK, IG_LINK, Icon, WAIcon, WAButton, Reveal, CountUp, CourtLines, SpeedLines, Section });