/* global React, Icon, useT */
const { useState: useStateG, useEffect: useEffectG, useCallback: useCallbackG } = React;
/* ============== PHOTOS DATA ==============
Each photo: src, cat (spaces|rooms|details), aspect (h or v),
span (gridSpan: 1 or 2 cols), titleKey, descKey
*/
const PHOTOS = [
// SPACES (8)
{ src:'img/fachada.jpeg', cat:'spaces', span:2, h:'tall', tk:'p_facade_t', dk:'p_facade_d' },
{ src:'img/cocina-comedor.jpeg', cat:'spaces', span:2, h:'tall', tk:'p_kitchen_t', dk:'p_kitchen_d' },
{ src:'img/cocina-escalera.jpeg', cat:'spaces', span:1, h:'tall', tk:'p_stairs_t', dk:'p_stairs_d' },
{ src:'img/cocina-cafetera.jpeg', cat:'spaces', span:1, h:'med', tk:'p_coffee_t', dk:'p_coffee_d' },
{ src:'img/salon-01.jpeg', cat:'spaces', span:1, h:'med', tk:'p_lounge1_t', dk:'p_lounge1_d' },
{ src:'img/salon-02.jpeg', cat:'spaces', span:1, h:'med', tk:'p_lounge2_t', dk:'p_lounge2_d' },
{ src:'img/patio.jpeg', cat:'spaces', span:2, h:'wide', tk:'p_patio_t', dk:'p_patio_d' },
{ src:'img/bajo-cubierta.jpeg', cat:'spaces', span:2, h:'wide', tk:'p_attic_t', dk:'p_attic_d' },
{ src:'img/comedor-sofa.jpeg', cat:'spaces', span:2, h:'tall', tk:'p_dinesofa_t', dk:'p_dinesofa_d' },
{ src:'img/comedor-vigas.jpeg', cat:'spaces', span:1, h:'tall', tk:'p_dinebeams_t', dk:'p_dinebeams_d' },
{ src:'img/salon-tv.jpeg', cat:'spaces', span:1, h:'med', tk:'p_loungetv_t', dk:'p_loungetv_d' },
{ src:'img/cocina-office.jpeg', cat:'spaces', span:1, h:'med', tk:'p_kitoffice_t', dk:'p_kitoffice_d' },
// ROOMS (5) — portrait photos, mark as tall so they get 2 rows
{ src:'img/hab-01.jpeg', cat:'rooms', span:1, h:'tall', tk:'p_h01_t', dk:'p_h01_d' },
{ src:'img/hab-02.jpeg', cat:'rooms', span:1, h:'tall', tk:'p_h02_t', dk:'p_h02_d' },
{ src:'img/hab-03.jpeg', cat:'rooms', span:1, h:'tall', tk:'p_h03_t', dk:'p_h03_d' },
{ src:'img/hab-04.jpeg', cat:'rooms', span:1, h:'tall', tk:'p_h04_t', dk:'p_h04_d' },
{ src:'img/hab-05.jpeg', cat:'rooms', span:1, h:'tall', tk:'p_h05_t', dk:'p_h05_d' },
// DETAILS (4)
{ src:'img/bajo-cubierta.jpeg', cat:'details', span:1, h:'med', tk:'p_hgarret_t', dk:'p_hgarret_d' },
{ src:'img/rincon.jpeg', cat:'details', span:1, h:'med', tk:'p_corner_t', dk:'p_corner_d' },
{ src:'img/rincon-lectura.jpeg', cat:'details', span:1, h:'med', tk:'p_readchair_t', dk:'p_readchair_d' },
{ src:'img/bano.jpeg', cat:'details', span:1, h:'tall', tk:'p_bath1_t', dk:'p_bath1_d' },
{ src:'img/bano-amarillo.jpeg', cat:'details', span:1, h:'tall', tk:'p_bath2_t', dk:'p_bath2_d' },
];
/* ============== HEADER ============== */
function GalleryHeader() {
const { t } = useT();
return (
{t('gal_back')}
{t('gal_eyebrow')}
{t('gal_h1_a')}
{t('gal_h1_b')}
{t('gal_lede')}
);
}
const GH = {
wrap:{ background:'#F5C801', color:'#1A1A1A', padding:'72px 28px 80px' },
inner:{ maxWidth:1280, margin:'0 auto' },
back:{ display:'inline-flex', alignItems:'center', gap:8, fontFamily:'Montserrat, sans-serif', fontWeight:700,
fontSize:12, letterSpacing:'.15em', textTransform:'uppercase', color:'#1A1A1A',
textDecoration:'none', marginBottom:32 },
eyebrow:{ fontFamily:'Montserrat, sans-serif', fontWeight:700, fontSize:12, letterSpacing:'.25em', textTransform:'uppercase' },
h1:{ fontFamily:'Montserrat, sans-serif', fontWeight:900, fontSize:'clamp(40px,5vw,76px)', lineHeight:.95,
letterSpacing:'-0.02em', margin:'18px 0 22px', textTransform:'uppercase' },
lede:{ fontFamily:'Inter, sans-serif', fontSize:'clamp(15px,1.2vw,18px)', maxWidth:680, lineHeight:1.55, margin:0 },
};
/* ============== FILTER BAR ============== */
function FilterBar({ filter, setFilter, count }) {
const { t } = useT();
const filters = [
['all', t('gal_filter_all')],
['spaces', t('gal_filter_spaces')],
['rooms', t('gal_filter_rooms')],
['details', t('gal_filter_details')],
];
const countLbl = count === 1 ? t('gal_count_one') : t('gal_count_n').replace('{n}', count);
return (
{filters.map(([k, lbl]) => (
))}
{countLbl}
);
}
const FB = {
wrap:{ position:'sticky', top:60, zIndex:40, background:'rgba(255,255,255,.92)', backdropFilter:'blur(10px)',
borderBottom:'1px solid #ECECEC' },
inner:{ maxWidth:1280, margin:'0 auto', padding:'14px 28px', display:'flex',
justifyContent:'space-between', alignItems:'center', gap:20, flexWrap:'wrap' },
chips:{ display:'flex', gap:8, flexWrap:'wrap' },
chip:{ background:'transparent', border:'1px solid #E5E5E5', color:'#2B2B2B',
padding:'8px 16px', borderRadius:999, cursor:'pointer',
fontFamily:'Montserrat, sans-serif', fontWeight:700, fontSize:12,
letterSpacing:'.1em', textTransform:'uppercase', transition:'all .15s' },
chipActive:{ background:'#1A1A1A', color:'#F5C801', borderColor:'#1A1A1A' },
count:{ fontFamily:'ui-monospace, monospace', fontSize:12, color:'#6B6B6B', letterSpacing:'.05em' },
};
/* ============== GRID ============== */
function GalleryGrid({ photos, onOpen }) {
const { t } = useT();
return (
{photos.map((p, i) => {
const span = p.span === 2 ? 'span 2' : 'span 1';
const rowSpan = p.h === 'tall' ? 'span 2' : 'span 1';
return (
);
})}
);
}
const GR = {
wrap:{ padding:'40px 28px 80px', maxWidth:1280, margin:'0 auto' },
grid:{ display:'grid', gridTemplateColumns:'repeat(4, 1fr)', gridAutoRows:'210px',
gridAutoFlow:'dense', gap:12 },
cell:{ position:'relative', overflow:'hidden', borderRadius:14, padding:0, border:'none',
background:'#F4F4F4', cursor:'pointer', display:'block' },
img:{ position:'absolute', inset:0, width:'100%', height:'100%', objectFit:'cover',
transition:'transform .5s ease' },
lbl:{ position:'absolute', bottom:12, left:12, background:'#1A1A1A', color:'#F5C801',
padding:'5px 10px', fontFamily:'Montserrat, sans-serif', fontWeight:700, fontSize:10,
letterSpacing:'.1em', textTransform:'uppercase', borderRadius:4, zIndex:2 },
overlay:{ position:'absolute', inset:0,
background:'linear-gradient(180deg, transparent 40%, rgba(0,0,0,.7) 100%)',
display:'flex', alignItems:'flex-end', padding:'14px 14px 44px',
opacity:0, transition:'opacity .2s' },
cellTitle:{ color:'#fff', fontFamily:'Inter, sans-serif', fontWeight:500, fontSize:13,
lineHeight:1.4 },
};
/* ============== LIGHTBOX ============== */
function Lightbox({ photos, index, onClose, onPrev, onNext }) {
const { t } = useT();
const isOpen = index !== -1;
useEffectG(() => {
if (!isOpen) return;
const onKey = (e) => {
if (e.key === 'Escape') onClose();
else if (e.key === 'ArrowLeft') onPrev();
else if (e.key === 'ArrowRight') onNext();
};
document.addEventListener('keydown', onKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.removeEventListener('keydown', onKey);
document.body.style.overflow = prevOverflow;
};
}, [isOpen, onClose, onPrev, onNext]);
// Touch swipe
const touch = React.useRef({ x:0, y:0 });
const onTouchStart = (e) => { touch.current = { x:e.touches[0].clientX, y:e.touches[0].clientY }; };
const onTouchEnd = (e) => {
const dx = e.changedTouches[0].clientX - touch.current.x;
const dy = e.changedTouches[0].clientY - touch.current.y;
if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy)) {
if (dx > 0) onPrev(); else onNext();
}
};
if (index === -1) return null;
const p = photos[index];
return (
e.stopPropagation()}>
{String(index+1).padStart(2,'0')} / {String(photos.length).padStart(2,'0')}
);
}
const LB = {
backdrop:{ position:'fixed', inset:0, background:'rgba(15,15,15,.96)', zIndex:1000,
display:'flex', alignItems:'center', justifyContent:'center', padding:'40px 60px' },
frame:{ position:'relative', maxWidth:'min(1200px, 100%)', maxHeight:'100%',
display:'flex', flexDirection:'column', gap:14, alignItems:'center' },
img:{ maxWidth:'100%', maxHeight:'calc(100vh - 180px)', objectFit:'contain', borderRadius:8,
boxShadow:'0 30px 80px rgba(0,0,0,.5)' },
cap:{ display:'flex', justifyContent:'space-between', alignItems:'flex-end', gap:24,
width:'100%', color:'#fff', maxWidth:900 },
capL:{ flex:1 },
capT:{ fontFamily:'Montserrat, sans-serif', fontWeight:800, fontSize:18, color:'#F5C801',
letterSpacing:'.02em', textTransform:'uppercase', marginBottom:6 },
capD:{ fontFamily:'Inter, sans-serif', fontSize:14, color:'#D9D9D9', lineHeight:1.5 },
capR:{ fontFamily:'ui-monospace, monospace', fontSize:13, color:'rgba(255,255,255,.5)', letterSpacing:'.05em',
flex:'0 0 auto' },
btn:{ position:'fixed', background:'rgba(255,255,255,.08)', border:'1px solid rgba(255,255,255,.18)',
color:'#fff', cursor:'pointer', backdropFilter:'blur(8px)',
display:'flex', alignItems:'center', justifyContent:'center', borderRadius:999 },
close:{ top:24, right:24, width:48, height:48, fontFamily:'Montserrat, sans-serif', fontWeight:700, fontSize:28, lineHeight:1 },
prev:{ left:20, top:'50%', transform:'translateY(-50%)', width:52, height:52 },
next:{ right:20, top:'50%', transform:'translateY(-50%)', width:52, height:52 },
};
/* ============== APP ============== */
function GalleryApp() {
const [filter, setFilter] = useStateG('all');
const [open, setOpen] = useStateG(-1);
const photos = filter === 'all' ? PHOTOS : PHOTOS.filter(p => p.cat === filter);
const onOpen = useCallbackG((i) => setOpen(i), []);
const onClose = useCallbackG(() => setOpen(-1), []);
const onPrev = useCallbackG(() => setOpen(i => (i - 1 + photos.length) % photos.length), [photos.length]);
const onNext = useCallbackG(() => setOpen(i => (i + 1) % photos.length), [photos.length]);
// Reset open if filter changes
useEffectG(() => { setOpen(-1); }, [filter]);
return (
);
}
window.GalleryApp = GalleryApp;