/* 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 (
);
}
// ─────────────────────────────────────────────────────────────
// 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}
{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?
}>See pricing
setBookingOpen(false)} />
{lightboxIndex != null && visiblePhotos[lightboxIndex] && (
setLightboxIndex(null)}
onPrevious={previousPhoto}
onNext={nextPhoto}
/>
)}
);
}
window.ProjectsApp = ProjectsApp;