const { useState, useEffect } = React;
/* ── useIsMobile ─────────────────────────────────── */
const useIsMobile = (breakpoint = 768) => {
const [mobile, setMobile] = useState(() => window.innerWidth < breakpoint);
useEffect(() => {
const handler = () => setMobile(window.innerWidth < breakpoint);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, [breakpoint]);
return mobile;
};
/* ── Nav ─────────────────────────────────────────── */
const Nav = ({ page, setPage, onContact }) => {
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
const isMobile = useIsMobile();
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 24);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
useEffect(() => {
document.body.style.overflow = open ? 'hidden' : '';
return () => { document.body.style.overflow = ''; };
}, [open]);
const links = [
{ id: 'empieza', label: 'Empieza aquí' },
{ id: 'articulos', label: 'Artículos' },
{ id: 'laboratorio', label: 'Laboratorio' },
{ id: 'sobre', label: 'Sobre mí' },
];
const go = (id) => { setPage(id); setOpen(false); window.scrollTo(0, 0); };
const bar = (rot, ty, opacity = 1) => ({
display: 'block', width: 22, height: 2, background: 'var(--primary)',
borderRadius: 2, transformOrigin: 'center',
transition: 'all 0.3s cubic-bezier(0.4,0,0.2,1)',
transform: `rotate(${rot}deg) translateY(${ty}px)`,
opacity,
});
return (
<>
{isMobile && (
<>
setOpen(false)}
style={{
position: 'fixed', inset: 0, zIndex: 190,
background: 'rgba(26,43,38,0.3)',
backdropFilter: 'blur(2px)',
opacity: open ? 1 : 0,
pointerEvents: open ? 'all' : 'none',
transition: 'opacity 0.3s ease',
}}
/>
{links.map((l, i) => (
))}
>
)}
>
);
};
/* ── Footer ───────────────────────────────────────── */
const Footer = ({ setPage }) => {
const go = (id) => { setPage(id); window.scrollTo(0, 0); };
return (
);
};
/* ── Shared UI ────────────────────────────────────── */
const Btn = ({ onClick, variant = 'solid', children, small }) => {
const [hov, setHov] = useState(false);
if (variant === 'ghost') {
return (
);
}
if (variant === 'outline') {
return (
);
}
return (
);
};
const SectionLabel = ({ children }) => (
{children}
);
const Card = ({ children, onClick, hover = true, style = {} }) => {
const [hov, setHov] = useState(false);
return (
hover && setHov(true)}
onMouseLeave={() => hover && setHov(false)}
style={{
background: 'var(--white)',
borderRadius: 'var(--radius-lg)',
border: '1px solid var(--border)',
boxShadow: hov ? 'var(--shadow-md)' : 'var(--shadow)',
transform: hov && onClick ? 'translateY(-3px)' : 'translateY(0)',
transition: 'all 0.25s ease',
cursor: onClick ? 'pointer' : 'default',
...style,
}}
>
{children}
);
};
/* ── Contact Modal ────────────────────────────────── */
const ContactModal = ({ onClose }) => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [msg, setMsg] = useState('');
const [sent, setSent] = useState(false);
const [sending, setSending] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setSending(true);
try {
const res = await fetch('/api/v1/contacts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name.trim(), email: email.trim(), message: msg.trim() }),
});
const json = await res.json();
if (!res.ok) {
const detail = json?.error?.message || 'Error al enviar. Inténtalo de nuevo.';
setError(detail);
setSending(false);
return;
}
setSent(true);
} catch (_) {
setError('No se pudo conectar con el servidor. Comprueba tu conexión.');
} finally {
setSending(false);
}
};
const inputStyle = {
width: '100%', padding: '12px 16px', borderRadius: 10, fontSize: 15,
border: '1.5px solid var(--border)', background: 'var(--white)',
fontFamily: 'DM Sans', color: 'var(--text)', outline: 'none',
transition: 'border-color 0.2s',
};
return (
<>
Déjame tu consulta
Es privada. Te respondo personalmente.
{sent ? (
✉️
¡Consulta enviada!
He recibido tu mensaje. Te respondo en menos de 48 horas de forma personal y privada.
) : (
)}
>
);
};
Object.assign(window, { Nav, Footer, Btn, SectionLabel, Card, ContactModal, useIsMobile });