// 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.

Leer el último post Conoce más
📍 N 42°12' · W 8°45' Galicia, España EST. 2019
Retrato de Cristhy
Self-portrait, primavera
scroll
); } // ============================================ // 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 (
{block}{block}
); } // ============================================ // 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 (
— 01 / Blog

Lo último que he escrito

Ver archivo completo
{data.blogCategories.map(c => ( ))}
{posts.map((p, i) => (
onOpen && onOpen(p)} className={`blog-card ${p.featured && layout !== 'list' ? 'featured' : ''}`}>
{p.title}
{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 (
— 02 / Fotografía

Lo que la luz me deja

Ver galería completa
{data.photoCategories.map(c => ( ))}
{photos.map(p => (
setLightbox(p)}> {p.title}
{p.title}
))}
setLightbox(null)}> {lightbox && ( {lightbox.title} 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 (
— 03 / Cocina

Recetas que repito siempre

Ver recetario
{data.recipeCategories.map(c => ( ))}
{recipes.map(r => (
onOpen && onOpen(r)} className="recipe-card"> {r.title}
{r.cat} {r.time}

{r.title}

{r.ingredients}
))}
); } // ============================================ // MERCH // ============================================ function ProductArt({ kind }) { const colors = { 1: 'var(--pink-deep)', 2: 'var(--purple-deep)' }; if (kind === 'tote') return ( slow ); if (kind === 'mug') return ( ); if (kind === 'shirt') return ( huerta crew ); if (kind === 'stickers') return ( hi ); if (kind === 'hoodie') return ( ); if (kind === 'presets') return ( ); if (kind === 'ebook') return ( cocina lenta ); 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 (
— 04 / Tienda

Cositas bonitas

Ver tienda
{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}

))}
Instagram YouTube Facebook
Cristhy en casa
{data.about.stats.map((s, i) => (
{s.num}
{s.lbl}
))}
); } // ============================================ // 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 ? (
setEmail(e.target.value)} required />
) : (
¡Gracias! Te escribiré pronto ✦
)}
); } // ============================================ // FOOTER // ============================================ function Footer() { return ( ); } // Export Object.assign(window, { CustomCursor, Navbar, Hero, Marquee, BlogSection, PhotoSection, CookingSection, MerchSection, AboutSection, Newsletter, Footer });