// src/components.jsx — shared layout pieces (Header, Footer, Reveal, icons, modal) const { useState, useEffect, useRef } = React; /* ─── Icons ─────────────────────────────────────────────── */ const Icon = ({ name, size = 16 }) => { const paths = { arrow: , 'arrow-up-right': , close: , menu: , mail: , instagram: , linkedin: , facebook: , download: , play: , chevron: , plus: , map: , phone: , quote: , search: , check: , trophy: , }; return ( {paths[name]} ); }; /* ─── Idioma (ES/EN) ────────────────────────────────────── */ // t('texto español', 'english text') — si falta el inglés, cae al español. const LangContext = React.createContext({ lang: 'es', setLang: () => {}, t: (es) => es }); const useLang = () => React.useContext(LangContext); const LangProvider = ({ children }) => { const initial = (() => { try { const p = new URLSearchParams(window.location.search).get('lang'); if (p === 'en' || p === 'es') return p; const s = localStorage.getItem('lpde_lang'); if (s === 'en' || s === 'es') return s; } catch (e) {} return 'es'; })(); const [lang, setLangState] = useState(initial); const setLang = (l) => { setLangState(l); try { localStorage.setItem('lpde_lang', l); } catch (e) {} }; useEffect(() => { document.documentElement.lang = lang; }, [lang]); const t = React.useCallback((es, en) => (lang === 'en' && en != null ? en : es), [lang]); return {children}; }; const LangToggle = () => { const { lang, setLang } = useLang(); return ( ); }; /* ─── useReveal hook + Reveal wrapper ───────────────────── */ const useReveal = () => { useEffect(() => { const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -50px 0px' }); document.querySelectorAll('.reveal:not(.in)').forEach(el => io.observe(el)); return () => io.disconnect(); }); }; /* ─── Animated count-up ─────────────────────────────────── */ const CountUp = ({ to, duration = 1400, suffix = '' }) => { const [val, setVal] = useState(0); const ref = useRef(null); const started = useRef(false); useEffect(() => { if (!ref.current) return; const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting && !started.current) { started.current = true; const start = performance.now(); const tick = (now) => { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); setVal(Math.round(eased * to)); if (t < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); } }); }, { threshold: 0.3 }); io.observe(ref.current); return () => io.disconnect(); }, [to, duration]); return {val}{suffix}; }; /* ─── Header / Nav ──────────────────────────────────────── */ const NAV_ITEMS = [ { id: 'presentacion', label: 'Presentación', en: 'About' }, { id: 'equipo', label: 'Equipo', en: 'Team' }, { id: 'noticias', label: 'Noticias y recursos', en: 'News & Resources' }, { id: 'formacion', label: 'Formación', en: 'Training' }, { id: 'calendario', label: 'Calendario', en: 'Calendar' }, { id: 'snde', label: 'Selección Nacional', en: 'National Team' }, { id: 'admision', label: 'Admisión', en: 'Membership' }, { id: 'tnde', label: 'TNDE', en: 'TNDE' }, ]; const Header = ({ page, setPage }) => { const { t } = useLang(); const [menuOpen, setMenuOpen] = useState(false); // Menú móvil: bloquear scroll del fondo + cerrar con Esc useEffect(() => { if (!menuOpen) return; const onKey = (e) => { if (e.key === 'Escape') setMenuOpen(false); }; document.addEventListener('keydown', onKey); document.body.style.overflow = 'hidden'; return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = ''; }; }, [menuOpen]); const go = (id) => { setMenuOpen(false); setPage(id); }; return (
go('presentacion')} aria-label="Inicio LPDE"> LPDE — Liga Peruana de Debate Escolar
{/* Portal a body: el backdrop-filter del .nav crea un containing block que atraparía (y recortaría) este overlay position:fixed */} {menuOpen && ReactDOM.createPortal(
LPDE
, document.body )}
); }; /* ─── Footer ────────────────────────────────────────────── */ const Footer = ({ setPage }) => { const { t } = useLang(); return ( ); }; /* ─── Marquee ───────────────────────────────────────────── */ const Marquee = ({ items }) => { const doubled = [...items, ...items]; return (
{doubled.map((t, i) => ( {t} ))}
); }; /* ─── Modal ─────────────────────────────────────────────── */ const Modal = ({ open, onClose, children }) => { useEffect(() => { if (!open) return; const onKey = (e) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', onKey); document.body.style.overflow = 'hidden'; return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = ''; }; }, [open, onClose]); if (!open) return null; return (
e.stopPropagation()}> {children}
); }; /* ─── Page wrapper (handles reveal init + key) ──────────── */ const Page = ({ children, screen }) => { useReveal(); useEffect(() => { window.scrollTo({ top: 0, behavior: 'instant' }); }, []); return (
{children}
); }; Object.assign(window, { Icon, useReveal, CountUp, Header, Footer, Marquee, Modal, Page, NAV_ITEMS, LangProvider, useLang, LangToggle });