/* ========================================================================== Yingjie Li — Online Gallery Dynamic data from /api/data with static fallback baked into the file. ========================================================================== */ (() => { 'use strict'; // ============================================================ // 0. Theme toggle (light / dark) with persistence // ============================================================ const THEME_KEY = 'yl-theme'; const root = document.documentElement; const stored = localStorage.getItem(THEME_KEY); if (stored === 'dark' || stored === 'light') root.dataset.theme = stored; const themeBtn = document.getElementById('theme-toggle'); if (themeBtn) { themeBtn.addEventListener('click', () => { const next = (root.dataset.theme === 'dark') ? 'light' : 'dark'; root.dataset.theme = next; localStorage.setItem(THEME_KEY, next); }); } // ============================================================ // 1. Static fallback data (used while /api/data loads) // ============================================================ const FALLBACK = { hero: { image: 'rowing_tea_party.jpg', title: 'Rowing Tea Party', year: 2023, num: '020' }, bio: { quote: 'Painting is the way I keep the small things from disappearing.', paragraphs: [] // bio body stays static in HTML for SEO }, exhibitions: [], contact: { email: 'yingjie.ly@gmail.com', etsy: 'https://www.etsy.com/shop/CuriousJCArt', gallery: 'https://www.visualexpansiongallery.com/yingjie-li' }, works: [ { num:'001', file:'Art1_2013.jpg', title:'Moon Dancer', year:2013, w:1763, h:2267 }, { num:'002', file:'Art2_2014.jpg', title:'Clock', year:2014, w:1275, h:1568 }, { num:'003', file:'Art3_2014.jpg', title:'Pig in the Forest', year:2014, w:2465, h:1622 }, { num:'004', file:'Art4_2015.jpg', title:'Once Upon a Time', year:2015, w:2183, h:1832 }, { num:'005', file:'Art5_2016.jpg', title:'Girl on Pig', year:2016, w:1773, h:2255 }, { num:'006', file:'Art6_2016.jpg', title:'Relief', year:2016, w:1773, h:2255 }, { num:'007', file:'Art7_2016.jpg', title:'Siren', year:2016, w:1773, h:2255 }, { num:'008', file:'Art8_2016.jpg', title:'Train Is Coming to Town', year:2016, w:2235, h:1789 }, { num:'009', file:'Art9_2017.jpg', title:'Magic Forest', year:2017, w:1826, h:2190 }, { num:'010', file:'Art10_2018.jpg', title:'Hide and Seek', year:2018, w:2705, h:3305 }, { num:'011', file:'Art11_2018.jpg', title:'Tea Party', year:2018, w:1985, h:1655 }, { num:'012', file:'Art12_2018.jpg', title:'T Is for Terrific Things', year:2018, w:1939, h:2061 }, { num:'013', file:'a_friendly_recital.jpg', title:'A Friendly Recital', year:2023, w:1500, h:1500, gallery:true }, { num:'014', file:'bubble_buddies.jpg', title:'Bubble Buddies', year:2023, w:1500, h:1500, gallery:true }, { num:'015', file:'bunny_in_red.jpg', title:'Bunny in Red', year:2023, w:1500, h:2091, gallery:true }, { num:'016', file:'candy_wagon.jpg', title:'Candy Wagon', year:2023, w:1500, h:1218, gallery:true }, { num:'017', file:'forest_magic.jpg', title:'Forest Magic', year:2023, w:1500, h:1192, gallery:true }, { num:'018', file:'music_in_the_forest.jpg', title:'Music in the Forest', year:2023, w:1500, h:1889, gallery:true }, { num:'019', file:'pig_ride.jpg', title:'Pig Ride', year:2023, w:1493, h:2031, gallery:true }, { num:'020', file:'rowing_tea_party.jpg', title:'Rowing Tea Party', year:2023, w:1438, h:1841, gallery:true }, { num:'021', file:'rowing_with_a_friend.jpg',title:'Rowing With a Friend', year:2023, w:1500, h:1920, gallery:true }, { num:'022', file:'sweet_dreams.jpg', title:'Sweet Dreams', year:2023, w:1500, h:2093, gallery:true }, { num:'023', file:'here_have_a_sip.jpg', title:'Here, Have a Sip', year:2024, w:1968, h:1545, gallery:true } ] }; let data = FALLBACK; let works = data.works; // ============================================================ // 2. Helpers // ============================================================ function imgUrl(file) { if (!file) return ''; if (file.startsWith('http') || file.startsWith('/')) return file; return '/images/' + file; // bare name = built-in artwork } function romanize(num) { const map = [['M',1000],['CM',900],['D',500],['CD',400],['C',100],['XC',90],['L',50],['XL',40],['X',10],['IX',9],['V',5],['IV',4],['I',1]]; let r = ''; for (const [l, v] of map) while (num >= v) { r += l; num -= v; } return r; } function escapeHtml(s) { return String(s ?? '').replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'})[c]); } function escapeAttr(s) { return String(s ?? '').replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'})[c]); } // ============================================================ // 3. IntersectionObserver for reveals // ============================================================ const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('is-in'); io.unobserve(e.target); } }); }, { rootMargin: '0px 0px -8% 0px', threshold: 0.05 }); document.querySelectorAll('.reveal').forEach(el => io.observe(el)); // ============================================================ // 4. Lightbox // ============================================================ const lb = document.getElementById('lightbox'); const lbImg = lb.querySelector('.lb-image'); const lbNum = lb.querySelector('.lb-num'); const lbTitle = lb.querySelector('.lb-title'); const lbYear = lb.querySelector('.lb-year'); const lbClose = lb.querySelector('.lb-close'); const lbPrev = lb.querySelector('.lb-prev'); const lbNext = lb.querySelector('.lb-next'); let workEls = []; let currentIndex = 0; let visibleWorks = []; function getVisible() { return workEls .map((el, i) => ({ el, i })) .filter(({ el }) => !el.classList.contains('is-hidden')) .map(({ i }) => i); } function openLightbox(idx) { visibleWorks = getVisible(); if (visibleWorks.length === 0) return; if (!visibleWorks.includes(idx)) idx = visibleWorks[0]; currentIndex = idx; updateLightbox(); lb.classList.add('is-open'); lb.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; } function closeLightbox() { lb.classList.remove('is-open'); lb.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; } function updateLightbox() { const w = works[currentIndex]; if (!w) return; lbImg.src = imgUrl(w.file); lbImg.alt = `${w.title}, ${w.year}, by Yingjie Li`; lbNum.textContent = `N° ${w.num}`; lbTitle.innerHTML = `${escapeHtml(w.title)}`; lbYear.textContent = `${romanize(w.year)} · ${w.year}`; const vi = visibleWorks.indexOf(currentIndex); [-1, 1].forEach(d => { const next = visibleWorks[(vi + d + visibleWorks.length) % visibleWorks.length]; if (next !== undefined && works[next]) { const img = new Image(); img.src = imgUrl(works[next].file); } }); } function navigate(dir) { const vi = visibleWorks.indexOf(currentIndex); const ni = (vi + dir + visibleWorks.length) % visibleWorks.length; currentIndex = visibleWorks[ni]; updateLightbox(); } lbClose.addEventListener('click', closeLightbox); lbPrev.addEventListener('click', () => navigate(-1)); lbNext.addEventListener('click', () => navigate(1)); lb.addEventListener('click', (e) => { if (e.target === lb) closeLightbox(); }); document.addEventListener('keydown', (e) => { if (!lb.classList.contains('is-open')) return; if (e.key === 'Escape') closeLightbox(); if (e.key === 'ArrowLeft') navigate(-1); if (e.key === 'ArrowRight') navigate(1); }); let touchStartX = null; lb.addEventListener('touchstart', (e) => { touchStartX = e.changedTouches[0].clientX; }, { passive: true }); lb.addEventListener('touchend', (e) => { if (touchStartX === null) return; const dx = e.changedTouches[0].clientX - touchStartX; if (Math.abs(dx) > 50) navigate(dx < 0 ? 1 : -1); touchStartX = null; }, { passive: true }); // ============================================================ // 5. Render gallery // ============================================================ const galleryEl = document.getElementById('gallery'); function renderGallery() { galleryEl.innerHTML = ''; works.forEach((w, i) => { const fig = document.createElement('figure'); fig.className = 'work'; fig.dataset.year = w.year; fig.dataset.index = i; fig.style.transitionDelay = `${(i % 3) * 80}ms`; const badge = w.gallery ? `On view` : ''; fig.innerHTML = `