// components.jsx — Componentes principales del sitio
const { useState, useEffect, useRef, useMemo } = React;
// ============================================
// CURSOR PERSONALIZADO
// ============================================
function CustomCursor() {
const dotRef = useRef(null);
const ringRef = useRef(null);
useEffect(() => {
if (window.matchMedia('(hover: none)').matches) return;
let dotX = 0, dotY = 0, ringX = 0, ringY = 0;
let mx = window.innerWidth / 2, my = window.innerHeight / 2;
let rafId;
const onMove = (e) => { mx = e.clientX; my = e.clientY; };
window.addEventListener('mousemove', onMove);
const tick = () => {
dotX += (mx - dotX) * 0.6;
dotY += (my - dotY) * 0.6;
ringX += (mx - ringX) * 0.18;
ringY += (my - ringY) * 0.18;
if (dotRef.current) dotRef.current.style.transform = `translate(${dotX}px, ${dotY}px) translate(-50%, -50%)`;
if (ringRef.current) ringRef.current.style.transform = `translate(${ringX}px, ${ringY}px) translate(-50%, -50%)`;
rafId = requestAnimationFrame(tick);
};
rafId = requestAnimationFrame(tick);
// Hover detection
const hoverables = 'a, button, .chip, .photo-item, .recipe-card, .blog-card, .product, input, .theme-toggle';
const onOver = (e) => {
if (e.target.closest(hoverables)) document.body.classList.add('cursor-hover');
};
const onOut = (e) => {
if (e.target.closest(hoverables)) document.body.classList.remove('cursor-hover');
};
document.addEventListener('mouseover', onOver);
document.addEventListener('mouseout', onOut);
return () => {
window.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseover', onOver);
document.removeEventListener('mouseout', onOut);
cancelAnimationFrame(rafId);
};
}, []);
return (
);
}
// ============================================
// NAVBAR
// ============================================
function Navbar({ theme, toggleTheme, cartCount, onCart }) {
const [scrolled, setScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const [active, setActive] = useState('inicio');
useEffect(() => {
const onScroll = () => {
setScrolled(window.scrollY > 20);
const sections = ['inicio', 'blog', 'fotografia', 'cocina', 'merch', 'sobre-mi'];
for (const id of sections) {
const el = document.getElementById(id);
if (el) {
const r = el.getBoundingClientRect();
if (r.top <= 120 && r.bottom > 120) {
setActive(id);
break;
}
}
}
};
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
return () => window.removeEventListener('scroll', onScroll);
}, []);
const links = [
{ id: 'blog', label: 'Blog' },
{ id: 'fotografia', label: 'Fotografía' },
{ id: 'cocina', label: 'Cocina' },
{ id: 'merch', label: 'Merch' },
{ id: 'sobre-mi', label: 'Sobre mí' },
];
return (
);
}
// ============================================
// HERO
// ============================================
function Hero() {
return (
&
Diario personal · 2026
Vivir
despacio,
contar
bonito.
Escribo, cocino, fotografío y vivo despacio entre una furgoneta y una casa con huerto. Aquí dejo lo que aprendo por el camino.
📍 N 42°12' · W 8°45'
Galicia, España
EST. 2019
Self-portrait, primavera
);
}
// ============================================
// MARQUEE
// ============================================
function Marquee() {
const items = ['Vida lenta', '✦', 'Huerto & gallinas', '✦', 'Fotografía', '✦', 'Recetas caseras', '✦', 'Autocaravana', '✦', 'Vida en pareja', '✦'];
const block = (
{items.map((it, i) => it === '✦' ? ✦ : {it})}
);
return (
);
}
// ============================================
// BLOG
// ============================================
function BlogSection({ layout, onOpen }) {
const [filter, setFilter] = useState('Todo');
const data = window.SITE_DATA;
const posts = filter === 'Todo' ? data.blog : data.blog.filter(p => p.cat === filter);
return (
{data.blogCategories.map(c => (
))}
{posts.map((p, i) => (
onOpen && onOpen(p)} className={`blog-card ${p.featured && layout !== 'list' ? 'featured' : ''}`}>
{p.cat}
·
{p.date}
·
{p.readTime}
{p.title}
{p.excerpt}
))}
);
}
// ============================================
// FOTOGRAFÍA
// ============================================
function PhotoSection() {
const [filter, setFilter] = useState('Todo');
const [lightbox, setLightbox] = useState(null);
const data = window.SITE_DATA;
const photos = filter === 'Todo' ? data.photos : data.photos.filter(p => p.cat === filter);
const next = () => {
if (lightbox === null) return;
const idx = photos.findIndex(p => p.id === lightbox.id);
setLightbox(photos[(idx + 1) % photos.length]);
};
const prev = () => {
if (lightbox === null) return;
const idx = photos.findIndex(p => p.id === lightbox.id);
setLightbox(photos[(idx - 1 + photos.length) % photos.length]);
};
useEffect(() => {
const onKey = (e) => {
if (!lightbox) return;
if (e.key === 'Escape') setLightbox(null);
else if (e.key === 'ArrowRight') next();
else if (e.key === 'ArrowLeft') prev();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
});
return (
{data.photoCategories.map(c => (
))}
{photos.map(p => (
setLightbox(p)}>
{p.title}
))}
setLightbox(null)}>
{lightbox && (
e.stopPropagation()} />
{lightbox.title} · {lightbox.cat}
)}
);
}
// ============================================
// COCINA
// ============================================
function CookingSection({ onOpen }) {
const [filter, setFilter] = useState('Todo');
const data = window.SITE_DATA;
const recipes = filter === 'Todo' ? data.recipes : data.recipes.filter(r => r.cat === filter);
return (
{data.recipeCategories.map(c => (
))}
{recipes.map(r => (
onOpen && onOpen(r)} className="recipe-card">
{r.title}
{r.ingredients}
))}
);
}
// ============================================
// MERCH
// ============================================
function ProductArt({ kind }) {
const colors = { 1: 'var(--pink-deep)', 2: 'var(--purple-deep)' };
if (kind === 'tote') return (
);
if (kind === 'mug') return (
);
if (kind === 'shirt') return (
);
if (kind === 'stickers') return (
);
if (kind === 'hoodie') return (
);
if (kind === 'presets') return (
);
if (kind === 'ebook') return (
);
if (kind === 'print') return (
);
return null;
}
function MerchSection({ onAdd }) {
const data = window.SITE_DATA;
const [added, setAdded] = useState(null);
const handleAdd = (p, e) => {
e && e.stopPropagation();
if (onAdd) onAdd(p);
setAdded(p.id);
setTimeout(() => setAdded(null), 1200);
};
return (
{data.merch.map(p => (
{p.name}
{p.price}
{p.tag}
))}
);
}
// ============================================
// ABOUT
// ============================================
function AboutSection() {
const data = window.SITE_DATA;
return (
— 05 / Sobre mí
Un poquito de mí
{data.about.bio.map((p, i) => (
{i === 0 ? {p.split('.')[0]}. : null}{i === 0 ? p.substring(p.indexOf('.') + 1) : p}
))}
{data.about.stats.map((s, i) => (
))}
);
}
// ============================================
// NEWSLETTER
// ============================================
function Newsletter() {
const [email, setEmail] = useState('');
const [sent, setSent] = useState(false);
const submit = (e) => { e.preventDefault(); if (email) setSent(true); };
return (
Newsletter
Carta del domingo
Una vez al mes te cuento qué cocino, qué leo y dónde estoy. Sin spam, sin promesas raras. Solo el café del domingo.
{!sent ? (
) : (
¡Gracias! Te escribiré pronto ✦
)}
);
}
// ============================================
// FOOTER
// ============================================
function Footer() {
return (
);
}
// Export
Object.assign(window, {
CustomCursor, Navbar, Hero, Marquee,
BlogSection, PhotoSection, CookingSection, MerchSection,
AboutSection, Newsletter, Footer
});