// extras.jsx — Funcionalidades extra: progress bar, search, cart, post detail, recipe detail, now playing, 404 const { useState, useEffect, useRef, useCallback } = React; // ============================================ // READING PROGRESS BAR // ============================================ function ReadingProgress() { const [progress, setProgress] = useState(0); useEffect(() => { const onScroll = () => { const h = document.documentElement; const total = h.scrollHeight - h.clientHeight; setProgress(total > 0 ? (h.scrollTop / total) * 100 : 0); }; window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); return () => window.removeEventListener('scroll', onScroll); }, []); return
; } // ============================================ // COMMAND PALETTE (Cmd+K) // ============================================ function CommandPalette() { const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const inputRef = useRef(null); useEffect(() => { const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setOpen(o => !o); } else if (e.key === 'Escape') setOpen(false); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); useEffect(() => { if (open && inputRef.current) setTimeout(() => inputRef.current.focus(), 50); if (!open) setQuery(''); }, [open]); const data = window.SITE_DATA; const items = [ ...data.blog.map(p => ({ kind: 'Blog', title: p.title, sub: p.cat, href: '#blog' })), ...data.recipes.map(r => ({ kind: 'Receta', title: r.title, sub: r.cat, href: '#cocina' })), ...data.merch.map(m => ({ kind: 'Tienda', title: m.name, sub: m.price, href: '#merch' })), { kind: 'Ir a', title: 'Inicio', sub: 'Volver al hero', href: '#inicio' }, { kind: 'Ir a', title: 'Sobre mí', sub: 'Conoce a Cristhy', href: '#sobre-mi' }, { kind: 'Ir a', title: 'Newsletter', sub: 'Carta del domingo', href: '#newsletter' }, ]; const filtered = query ? items.filter(i => (i.title + ' ' + i.sub + ' ' + i.kind).toLowerCase().includes(query.toLowerCase())) : items.slice(0, 8); if (!open) return null; return (
setOpen(false)}>
e.stopPropagation()}>
setQuery(e.target.value)} /> esc
{filtered.length === 0 &&
Sin resultados para "{query}"
} {filtered.map((i, idx) => ( setOpen(false)}> {i.kind}
{i.title}
{i.sub}
))}
navegar abrir esc cerrar
); } // ============================================ // CART (localStorage) // ============================================ function useCart() { const [items, setItems] = useState(() => { try { return JSON.parse(localStorage.getItem('cristhy-cart') || '[]'); } catch { return []; } }); useEffect(() => { localStorage.setItem('cristhy-cart', JSON.stringify(items)); }, [items]); const add = (product) => setItems(prev => { const idx = prev.findIndex(i => i.id === product.id); if (idx >= 0) { const copy = [...prev]; copy[idx] = { ...copy[idx], qty: copy[idx].qty + 1 }; return copy; } return [...prev, { ...product, qty: 1 }]; }); const remove = (id) => setItems(prev => prev.filter(i => i.id !== id)); const clear = () => setItems([]); const total = items.reduce((s, i) => s + parseFloat(i.price) * i.qty, 0); return { items, add, remove, clear, total }; } function CartDrawer({ cart, open, onClose }) { return (
); } function CartButton({ count, onClick }) { return ( ); } // ============================================ // POST DETAIL MODAL // ============================================ function PostDetail({ post, onClose }) { const [progress, setProgress] = useState(0); const bodyRef = useRef(null); useEffect(() => { const el = bodyRef.current; if (!el) return; const onScroll = () => { const total = el.scrollHeight - el.clientHeight; setProgress(total > 0 ? (el.scrollTop / total) * 100 : 0); }; el.addEventListener('scroll', onScroll); return () => el.removeEventListener('scroll', onScroll); }, [post]); useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); document.body.style.overflow = post ? 'hidden' : ''; return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = ''; }; }, [post, onClose]); if (!post) return null; const body = `Hace seis meses convertimos una furgoneta vieja en hogar. Pintamos las paredes interiores con cal, pusimos una cocina pequeña, una cama que mira al horizonte y un panel solar que ya nos ha dado más de un disgusto. La libertad existe, sí. Pero también el panel solar que se rompe un martes a las 7 de la mañana cuando estás en mitad de la nada. La gente pregunta mucho por la parte instagramable: la luz dorada, los desayunos al sol, los lugares con nombre raro. Yo voy a contar lo otro: el día que se nos coló agua por la claraboya, el día que mi pareja me miró con ojos de "vámonos a casa", el día que descubrimos que en una furgo no caben todas las cosas que crees que necesitas. Vivir despacio en cuatro ruedas no es huir. Es elegir mejor qué te llevas.`; return (
e.stopPropagation()}>
{post.title}
{post.cat} · {post.date} · {post.readTime} de lectura

{post.title}

{post.excerpt}

{body.split('\n\n').map((para, i) =>

{para}

)}
Compartir: Twitter Facebook Copiar enlace
); } // ============================================ // RECIPE DETAIL MODAL // ============================================ function RecipeDetail({ recipe, onClose }) { useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); document.body.style.overflow = recipe ? 'hidden' : ''; return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = ''; }; }, [recipe, onClose]); if (!recipe) return null; const ingredients = [ '300g de harina', '200g de azúcar', '4 huevos camperos', '120ml de aceite de oliva virgen extra', 'Ralladura de 2 limones', '1 yogur natural', 'Levadura química', 'Una pizca de sal', ]; const steps = [ 'Precalienta el horno a 180°C con calor arriba y abajo.', 'Mezcla los huevos con el azúcar hasta que blanqueen.', 'Añade el aceite y el yogur. Bate suave.', 'Incorpora la harina con la levadura tamizadas. La ralladura al final.', 'Vuelca en molde engrasado. Hornea 45 minutos.', 'Deja enfriar en el molde 10 minutos antes de desmoldar.', ]; return (
e.stopPropagation()}>
{recipe.title}
{recipe.cat}

{recipe.title}

{recipe.time}Tiempo
6Raciones
FácilDificultad

Ingredientes

    {ingredients.map((i, idx) =>
  • {i}
  • )}

Pasos

    {steps.map((s, idx) =>
  1. {String(idx + 1).padStart(2, '0')}{s}
  2. )}
); } // ============================================ // NOW PLAYING WIDGET // ============================================ function NowWidget() { const items = [ { icon: '🍳', label: 'Cocinando', text: 'Bizcocho de limón' }, { icon: '📖', label: 'Leyendo', text: 'El cuento de la criada' }, { icon: '🎧', label: 'Escuchando', text: 'Lana del Rey · Honeymoon' }, { icon: '🌿', label: 'Plantando', text: 'Tomates cherry' }, ]; const [idx, setIdx] = useState(0); useEffect(() => { const t = setInterval(() => setIdx(i => (i + 1) % items.length), 4000); return () => clearInterval(t); }, []); return (
{items[idx].label} ahora {items[idx].icon} {items[idx].text}
); } // ============================================ // EASTER EGG — gallinitas que cruzan // ============================================ function ChickenEasterEgg() { const [chicken, setChicken] = useState(null); useEffect(() => { let last = 0; const onScroll = () => { if (Math.random() > 0.985 && Date.now() - last > 8000 && !chicken) { last = Date.now(); setChicken({ id: Date.now(), top: 60 + Math.random() * 30 }); setTimeout(() => setChicken(null), 6000); } }; window.addEventListener('scroll', onScroll); return () => window.removeEventListener('scroll', onScroll); }, [chicken]); if (!chicken) return null; return (
🐔 cloc cloc
); } // Export Object.assign(window, { ReadingProgress, CommandPalette, useCart, CartDrawer, CartButton, PostDetail, RecipeDetail, NowWidget, ChickenEasterEgg });