/* global React, ReactDOM, Eyebrow, Reveal, Icon, Button, Grain, TBMark, BookingModal, WhatsAppFloating, Footer */ const { useState, useEffect, useMemo } = React; // ───────────────────────────────────────────────────────────── // Lightweight header for the Projects page (links back to home) // ───────────────────────────────────────────────────────────── function ProjectsHeader({ scrolled, openBooking }) { const [menuOpen, setMenuOpen] = useState(false); const onLight = scrolled; const fg = onLight ? 'var(--asphalt)' : 'var(--limestone)'; useEffect(() => { if (!menuOpen) return undefined; const previousOverflow = document.body.style.overflow; const onKeyDown = (event) => { if (event.key === 'Escape') setMenuOpen(false); }; document.body.style.overflow = 'hidden'; window.addEventListener('keydown', onKeyDown); return () => { document.body.style.overflow = previousOverflow; window.removeEventListener('keydown', onKeyDown); }; }, [menuOpen]); return (
tainá borges.
{menuOpen && ReactDOM.createPortal((
tainá borges.
), document.body)}
); } function navLinkStyle(fg) { return { background: 'transparent', border: 'none', cursor: 'pointer', padding: '4px 0', fontFamily: 'var(--font-sans)', fontWeight: 400, fontSize: 14, textDecoration: 'none', color: fg, opacity: 0.7, }; } // ───────────────────────────────────────────────────────────── // Page hero // ───────────────────────────────────────────────────────────── function ProjectsHero({ total, totalFrames }) { return (
the work · archive

every
project.

Explore complete New York City photography sessions. Each gallery follows the original story, location, and sequence from the day it was photographed.

); } function HeroStat({ n, label }) { return (
{n}
{label}
); } // ───────────────────────────────────────────────────────────── // Sticky category filter / project index // ───────────────────────────────────────────────────────────── function ProjectIndex({ projects, filter, setFilter, scrollToProject }) { const cats = ['all', 'portraits', 'lifestyle', 'behind-the-scenes']; return (
{cats.map(c => { const active = filter === c; return ( ); })}
{projects.map(p => ( ))}
); } // ───────────────────────────────────────────────────────────── // A single project block — header + masonry photo grid // ───────────────────────────────────────────────────────────── function ProjectBlock({ project, index, onOpenPhoto }) { return (
{/* Project header */}
{String(index + 1).padStart(2, '0')} {project.category}

{project.title} · {project.year}

{project.blurb}

{/* Responsive gallery grid. A normal grid keeps native image boxes measurable so browser lazy-loading works consistently. */}
{project.photos.map((ph, i) => ( ))}
); } function ProjMeta({ icon, label }) { return ( {label} ); } function ProjectTile({ photo, eager, priority, onOpenPhoto, project }) { return ( ); } // ───────────────────────────────────────────────────────────── // Lightbox // ───────────────────────────────────────────────────────────── function Lightbox({ photo, onClose, onPrevious, onNext, index, total }) { useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose(); if (e.key === 'ArrowLeft') onPrevious(); if (e.key === 'ArrowRight') onNext(); }; window.addEventListener('keydown', onKey); document.body.style.overflow = 'hidden'; return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = ''; }; }, [onClose, onPrevious, onNext]); useEffect(() => { const preload = new Image(); preload.decoding = 'async'; preload.src = photo.src; }, [photo.src]); return (
e.stopPropagation()} style={{ position: 'relative', maxWidth: '90vw', maxHeight: '86vh', display: 'flex', flexDirection: 'column', gap: 16, animation: 'tbModalIn 380ms var(--ease-cinematic)', }}> {photo.title}
{photo.title} {String(index + 1).padStart(3, '0')} / {String(total).padStart(3, '0')} · {photo.meta} · {photo.year}
); } // ───────────────────────────────────────────────────────────── // Page root // ───────────────────────────────────────────────────────────── function ProjectsApp() { const all = window.TB_COLLECTIONS || []; const [scrolled, setScrolled] = useState(false); const [filter, setFilter] = useState('all'); const [lightboxIndex, setLightboxIndex] = useState(null); const [bookingOpen, setBookingOpen] = useState(false); useEffect(() => { let raf = 0; let last = window.scrollY > 80; const update = () => { const next = window.scrollY > 80; if (next !== last) { last = next; setScrolled(next); } raf = 0; }; const onScroll = () => { if (!raf) raf = requestAnimationFrame(update); }; window.addEventListener('scroll', onScroll, { passive: true }); return () => { window.removeEventListener('scroll', onScroll); cancelAnimationFrame(raf); }; }, []); // Open directly to a hash project on load useEffect(() => { if (!window.location.hash) return undefined; const id = window.location.hash.slice(1); const jump = () => { const el = document.getElementById(id); if (el) window.scrollTo({ top: Math.max(0, el.offsetTop - 56), behavior: 'auto' }); }; // Recheck after fonts and nearby lazy images settle so deep links always // land on their project rather than an obsolete pre-layout offset. const timers = [80, 600, 1600].map((delay) => setTimeout(jump, delay)); return () => timers.forEach(clearTimeout); }, []); const projects = useMemo( () => filter === 'all' ? all : all.filter(p => p.category === filter), [filter, all] ); const visiblePhotos = useMemo(() => projects.flatMap((project) => project.photos.map((photo) => ({ src: photo.src, title: photo.title, meta: project.location, year: project.year, })) ), [projects]); const totalFrames = all.reduce((s, p) => s + (p.frames || 0), 0); const openPhoto = (photo) => { const index = visiblePhotos.findIndex((item) => item.src === photo.src); if (index >= 0) setLightboxIndex(index); }; const previousPhoto = () => setLightboxIndex((index) => (index - 1 + visiblePhotos.length) % visiblePhotos.length); const nextPhoto = () => setLightboxIndex((index) => (index + 1) % visiblePhotos.length); const scrollToProject = (id) => { const el = document.getElementById(id); if (el) window.scrollTo({ top: el.offsetTop - 56, behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' }); }; return (
setBookingOpen(true)} />
{projects.map((p, i) => ( ))} {/* CTA back to booking */}

want one of these
made for you?

); } window.ProjectsApp = ProjectsApp;