/* 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()}> {t(p.tk)}
{t(p.tk)}
{t(p.dk)}
{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;