// Shared UI: nav, footer, logo mark, utils const { useState, useEffect, useRef, useCallback } = React; // El tema lo define data-tema en ("claro" | "oscuro") y lo aplica // page-transition.js antes del primer pintado. Acá sólo se lee, para que // React no pise la decisión ni provoque un cambio de color al montar. function useTheme() { const raiz = document.documentElement; const leer = () => (raiz.getAttribute('data-tema') === 'oscuro' ? 'dark' : 'light'); const [theme, setTheme] = useState(leer); const aplicar = useCallback((next) => { const oscuro = next === 'dark'; raiz.setAttribute('data-tema', oscuro ? 'oscuro' : 'claro'); if (oscuro) raiz.setAttribute('data-theme', 'dark'); else raiz.removeAttribute('data-theme'); setTheme(oscuro ? 'dark' : 'light'); }, []); useEffect(() => { setTheme(leer()); }, []); return [theme, aplicar]; } // Intersection observer hook for reveal-on-scroll function useReveal() { const ref = useRef(null); const [visible, setVisible] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; // Immediate check: if already in viewport at mount, reveal const rect = el.getBoundingClientRect(); if (rect.top < window.innerHeight && rect.bottom > 0) { setVisible(true); return; } const obs = new IntersectionObserver((entries) => { entries.forEach((e) => {if (e.isIntersecting) {setVisible(true);obs.disconnect();}}); }, { threshold: 0.1, rootMargin: "0px 0px -10% 0px" }); obs.observe(el); return () => obs.disconnect(); }, []); return [ref, visible]; } // Animated number that counts up when visible function CountUp({ to, duration = 1600, suffix = "" }) { const [ref, visible] = useReveal(); const [val, setVal] = useState(0); useEffect(() => { if (!visible) return; const start = performance.now(); let raf; const tick = (t) => { const p = Math.min(1, (t - start) / duration); const eased = 1 - Math.pow(1 - p, 3); setVal(Math.floor(to * eased)); if (p < 1) raf = requestAnimationFrame(tick);else setVal(to); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [visible, to, duration]); return {val}{suffix}; } // Reveal wrapper function Reveal({ children, delay = 0, style }) { const [ref, visible] = useReveal(); return (
{children}
); } // Logo mark function LogoMark({ size = 40 }) { return FCPB; } // Navigation function Nav({ activePage = "home" }) { const [theme, setTheme] = useTheme(); const [scrolled, setScrolled] = useState(false); const [open, setOpen] = useState(false); useEffect(() => { const on = () => setScrolled(window.scrollY > 20); on(); window.addEventListener('scroll', on, { passive: true }); return () => window.removeEventListener('scroll', on); }, []); const links = [ { id: "home", href: "index.html", label: "Inicio", ico: "fa-house" }, { id: "categorias", href: "categorias.html", label: "Categorías", ico: "fa-layer-group" }, { id: "partidos", href: "index.html#partidos", label: "Partidos", ico: "fa-calendar-days" }, { id: "galeria", href: "index.html#galeria", label: "Galería", ico: "fa-images" }, { id: "contacto", href: "contacto.html", label: "Contacto", ico: "fa-envelope" }, // Acceso al panel: socios, delegados, tesorería y administración. { id: "socios", href: "panel/", label: "Socios", ico: "fa-lock", externo: true }]; return (