// Joaquin Jeri — Editorial Grid layout // Off-white bg · serif accents · masonry grid · fullscreen menu with submenus const { useState, useEffect, useRef } = React; // Viewport hook — `phone` < 600px, `tablet` < 960px function useViewport() { const get = () => { if (typeof window === 'undefined') return { phone: false, tablet: false }; const w = window.innerWidth; return { phone: w < 600, tablet: w < 960 }; }; const [vp, setVp] = useState(get); useEffect(() => { const onResize = () => setVp(get()); window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); return vp; } function V1Site({ tweaks }) { const { PROJECTS, RETRATOS, BIO, CLIENTS, BOCA_SVG_URL, HOME_ORDER } = window.JJ_DATA; const [page, setPage] = useState('home'); // home | project | portraits | info | contact const [activeProject, setActiveProject] = useState(null); const [menuOpen, setMenuOpen] = useState(false); const [filter, setFilter] = useState('all'); const fontStack = tweaks.fontFamily === 'serif' ? `"Cormorant Garamond", "Times New Roman", serif` : tweaks.fontFamily === 'mono' ? `"JetBrains Mono", ui-monospace, monospace` : `"Neue Haas Grotesk Display Pro", "Helvetica Neue", Helvetica, Arial, sans-serif`; const gap = tweaks.gridSpacing === 'dense' ? 2 : tweaks.gridSpacing === 'airy' ? 24 : 10; // When viewing "all", use the curated HOME_ORDER (with portrait tiles mixed in). // For category filters, fall back to filtering PROJECTS by cat. const filtered = filter === 'all' ? (HOME_ORDER || []).map(entry => { if (typeof entry === 'object' && entry.retratos) { const n = entry.retratos; return { id: `__retratos_${n}`, title: 'Portraits', client: 'RETRATOS', cat: 'portraits', cover: `images/retratos/retratos-${n}.jpg`, __nav: 'portraits' }; } return PROJECTS.find(p => p.id === entry); }).filter(Boolean) : PROJECTS.filter(p => p.cat === filter); const goProject = (p) => { if (p.__nav) { navigate(p.__nav); return; } setActiveProject(p); setPage('project'); window.scrollTo({ top: 0 }); }; const navigate = (p, f) => { setPage(p); setMenuOpen(false); if (f !== undefined) setFilter(f); window.scrollTo({ top: 0 }); }; return (
setMenuOpen(true)} onLogo={() => navigate('home', 'all')} bocaUrl={BOCA_SVG_URL} /> {menuOpen && ( setMenuOpen(false)} navigate={navigate} goProject={(p) => { setMenuOpen(false); goProject(p); }} fontStack={fontStack} projects={PROJECTS} /> )} {page === 'home' && ( )} {page === 'project' && activeProject && ( navigate('home', activeProject.cat)} /> )} {page === 'portraits' && } {page === 'info' && } {page === 'contact' && }
); } function V1Header({ onMenu, onLogo, bocaUrl }) { const { phone } = useViewport(); // Header height (used by spacer below): padding × 2 + content (~22px) + 1px border const headerHeight = phone ? 51 : 59; return (
{/* Spacer to push page content below the fixed header (so it doesn't slide under) */}