Skip to main contentScroll Top

Visual diary

Quick snapshots captured on a phone.
Random thoughts left unfiltered.

Updating slowly …

/* ─── TOKENS ────────────────────────────────────────────── */ :root { --bg: #111111; --surf: #181818; --card: #222222; --line: rgba(232, 232, 232, 0.08); --cream: #e8e8e8; --muted: #666666; --accent: #bbbbbb; --white: #ffffff; --black: #000000;--f-display: 'Cormorant Garamond', Georgia, serif; --f-body: 'Be Vietnam Pro', system-ui, sans-serif; --f-mono: 'Space Mono', monospace;--col-gap: 6px; --ease: cubic-bezier(0.25, 0.1, 0.25, 1); }/* ─── RESET ─────────────────────────────────────────────── */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } body { background: var(--bg); color: var(--cream); font-family: var(--f-body); font-weight: 300; overflow-x: hidden; cursor: crosshair; } button { border: none; background: none; cursor: crosshair; font: inherit; color: inherit; }/* ─── GRAIN ─────────────────────────────────────────────── */ body::after { content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 9999; opacity: 0.03; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='250' height='250'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='250' height='250' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E"); }/* ─── HIDE THEGEM PAGE ELEMENTS ─────────────────────────── */ .page-title-block, #page-title-block, .breadcrumbs-container, .thegem-breadcrumbs { display: none !important; } body.vl-page { background: #111111 !important; } .page-content-wrap, .site-content, .content-area, .elementor-section-wrap, .elementor-element:first-child { padding-top: 0 !important; margin-top: 0 !important; }/* ─── FILTER BAR ────────────────────────────────────────── */ .vl-filter-bar { position: sticky; top: 210px; z-index: 100; background: rgba(17, 17, 17, 0.96); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border-bottom: 1px solid rgba(232, 232, 232, 0.08); padding: 10px 32px; margin-bottom: 0; } .vl-filter-inner { display: flex; align-items: center; justify-content: space-between; gap: 16px; } .vl-filter-right { display: flex; align-items: center; gap: 20px; flex-shrink: 0; }/* ─── YEAR PILLS ─────────────────────────────────────────── */ .year-strip { display: flex; align-items: center; gap: 4px; overflow-x: auto; scrollbar-width: none; } .year-strip::-webkit-scrollbar { display: none; }.year-pill { font-family: var(--f-mono); font-size: 9px; letter-spacing: 0.15em; text-transform: uppercase; padding: 4px 12px; border: 1px solid var(--line); color: var(--muted); transition: all 0.15s; white-space: nowrap; flex-shrink: 0; } .year-pill:hover { border-color: var(--accent); color: var(--accent); } .year-pill.active { border-color: var(--cream); color: var(--cream); background: rgba(232, 232, 232, 0.06); }/* ─── COLUMN PICKER ──────────────────────────────────────── */ .col-picker { display: flex; gap: 3px; } .col-btn { width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; border: 1px solid var(--line); color: var(--muted); font-family: var(--f-mono); font-size: 9px; transition: all 0.15s; } .col-btn:hover { border-color: var(--accent); color: var(--accent); } .col-btn.active { border-color: var(--cream); color: var(--cream); }/* ─── PHOTO COUNT ────────────────────────────────────────── */ .vl-nav-count { font-family: var(--f-mono); font-size: 10px; letter-spacing: 0.15em; color: var(--muted); white-space: nowrap; } .vl-nav-count span { color: var(--cream); }/* ─── YEAR BANDS ─────────────────────────────────────────── */ .year-band { padding: 14px 32px 10px; display: flex; align-items: center; gap: 20px; position: sticky; top: 170px; z-index: 50; background: rgba(17, 17, 17, 0.96); backdrop-filter: blur(12px); border-bottom: 1px solid var(--line); } .year-band-label { font-family: var(--f-mono); font-size: 11px; letter-spacing: 0.3em; text-transform: uppercase; color: var(--accent); } .year-band-line { flex: 1; height: 1px; background: var(--line); } .year-band-count { font-family: var(--f-mono); font-size: 10px; letter-spacing: 0.15em; color: var(--muted); }/* ─── MASONRY ────────────────────────────────────────────── */ .masonry-outer { padding: 0 32px 80px; }.masonry { columns: 4; column-gap: var(--col-gap); } .masonry.cols-2 { columns: 2; } .masonry.cols-3 { columns: 3; } .masonry.cols-4 { columns: 4; } .masonry.cols-5 { columns: 5; } .masonry.cols-6 { columns: 6; }/* ─── MASONRY ITEM ───────────────────────────────────────── */ .m-item { break-inside: avoid; margin-bottom: var(--col-gap); position: relative; overflow: hidden; cursor: crosshair; background: var(--card); display: block; } .m-item:hover .m-overlay { opacity: 1; } .m-item:hover .m-img { transform: scale(1.025); }.m-img-wrap { position: relative; width: 100%; overflow: hidden; }.m-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: transform 0.55s var(--ease); }.m-placeholder { position: absolute; inset: 0; }.t0 { background: linear-gradient(160deg, #1a1a1a, #2e2e2e); } .t1 { background: linear-gradient(140deg, #141414, #252525); } .t2 { background: linear-gradient(170deg, #1c1c1c, #303030); } .t3 { background: linear-gradient(130deg, #181818, #282828); } .t4 { background: linear-gradient(150deg, #202020, #343434); } .t5 { background: linear-gradient(165deg, #121212, #222222); } .t6 { background: linear-gradient(145deg, #1e1e1e, #323232); } .t7 { background: linear-gradient(155deg, #161616, #2a2a2a); }.m-overlay { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.75) 0%, transparent 55%); opacity: 0; transition: opacity 0.3s; pointer-events: none; display: flex; flex-direction: column; justify-content: flex-end; padding: 14px; gap: 4px; }.m-date { font-family: var(--f-mono); font-size: 9px; letter-spacing: 0.2em; color: rgba(232, 232, 232, 0.6); }.m-caption { font-family: var(--f-body); font-size: 12px; line-height: 1.5; color: rgba(232, 232, 232, 0.8); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }/* ─── LOADING / EMPTY ────────────────────────────────────── */ .load-sentinel { height: 1px; margin-bottom: 80px; }.loading-indicator { text-align: center; padding: 40px; font-family: var(--f-mono); font-size: 10px; letter-spacing: 0.3em; text-transform: uppercase; color: var(--muted); display: none; } .loading-indicator.visible { display: block; }.empty-state { text-align: center; padding: 120px 40px; display: none; } .empty-state.visible { display: block; } .empty-state h3 { font-family: var(--f-display); font-size: 32px; font-weight: 300; font-style: italic; color: var(--muted); margin-bottom: 12px; } .empty-state p { font-size: 13px; color: var(--muted); font-family: var(--f-mono); letter-spacing: 0.1em; }/* ─── LIGHTBOX ───────────────────────────────────────────── */ .lb { position: fixed; inset: 0; z-index: 99999; background: rgba(0,0,0,0); display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: opacity 0.3s var(--ease), background 0.3s var(--ease); } .lb.open { opacity: 1; pointer-events: all; background: rgba(0,0,0,0.97); }.lb-stage { position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; padding: 72px 100px; }.lb-img-wrap { position: relative; max-width: 100%; max-height: 100%; display: flex; align-items: center; justify-content: center; z-index: 2; }.lb-img { max-width: 100%; max-height: calc(100vh - 144px); object-fit: contain; opacity: 0; transition: opacity 0.25s var(--ease); display: block; } .lb-img.loaded { opacity: 1; }.lb-placeholder { width: min(80vw, 900px); max-height: calc(100vh - 144px); display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.25s var(--ease); } .lb-placeholder.loaded { opacity: 1; } .lb-placeholder-inner { width: 100%; height: 100%; min-height: 400px; }.lb-prev, .lb-next { position: absolute; top: 50%; transform: translateY(-50%); width: 56px; height: 56px; display: flex; align-items: center; justify-content: center; border: 1px solid rgba(232, 232, 232, 0.12); color: rgba(232, 232, 232, 0.4); transition: all 0.2s; z-index: 10; background: rgba(0, 0, 0, 0.3); } .lb-prev { left: 24px; } .lb-next { right: 24px; } .lb-prev:hover, .lb-next:hover { border-color: rgba(232, 232, 232, 0.4); color: var(--cream); background: rgba(0, 0, 0, 0.6); } .lb-prev svg, .lb-next svg { width: 20px; height: 20px; }.lb-close { position: absolute; top: 20px; right: 24px; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; border: 1px solid rgba(232, 232, 232, 0.12); color: rgba(232, 232, 232, 0.4); transition: all 0.2s; z-index: 10; } .lb-close:hover { border-color: rgba(232, 232, 232, 0.4); color: var(--cream); }.lb-bar { position: absolute; bottom: 0; left: 0; right: 0; padding: 20px 32px; display: flex; justify-content: space-between; align-items: center; background: linear-gradient(to top, rgba(0,0,0,0.7) 0%, transparent 100%); pointer-events: none; z-index: 5; } .lb-info { display: flex; flex-direction: column; gap: 4px; pointer-events: auto; } .lb-date { font-family: var(--f-mono); font-size: 10px; letter-spacing: 0.2em; color: rgba(232, 232, 232, 0.45); } .lb-caption { font-family: var(--f-body); font-style: italic; font-size: 14px; line-height: 1.7; color: rgba(232, 232, 232, 0.6); max-width: 600px; } .lb-counter { font-family: var(--f-mono); font-size: 10px; letter-spacing: 0.15em; color: rgba(232, 232, 232, 0.3); pointer-events: auto; }.lb-thumbs { position: absolute; bottom: 0; left: 0; right: 0; height: 72px; display: flex; align-items: center; justify-content: center; gap: 4px; padding: 0 100px; overflow: hidden; pointer-events: none; z-index: 6; } .lb-thumb { width: 52px; height: 52px; flex-shrink: 0; overflow: hidden; opacity: 0.3; transition: opacity 0.2s, transform 0.2s; cursor: crosshair; pointer-events: auto; } .lb-thumb.active { opacity: 1; transform: scale(1.08); } .lb-thumb-inner { width: 100%; height: 100%; }.lb-keys { position: absolute; bottom: 88px; right: 24px; font-family: var(--f-mono); font-size: 9px; letter-spacing: 0.15em; color: rgba(232, 232, 232, 0.15); pointer-events: none; z-index: 5; }/* ─── RESPONSIVE ─────────────────────────────────────────── */ @media (max-width: 1200px) { .masonry.cols-5, .masonry.cols-6 { columns: 4; } } @media (max-width: 900px) { .col-picker { display: none; } .masonry, .masonry.cols-4, .masonry.cols-5, .masonry.cols-6 { columns: 3; } .masonry.cols-2 { columns: 2; } .masonry.cols-3 { columns: 3; } .lb-thumbs { display: none; } .lb-stage { padding: 64px 64px 80px; } .lb-prev { left: 12px; } .lb-next { right: 12px; } } @media (max-width: 600px) { .year-strip { display: none; } .masonry, .masonry.cols-3, .masonry.cols-4 { columns: 2; } .masonry-outer { padding: 0 16px 60px; } :root { --col-gap: 4px; } .vl-filter-bar { padding: 10px 16px; } .lb-stage { padding: 56px 48px 80px; } .lb-prev { left: 8px; width: 44px; height: 44px; } .lb-next { right: 8px; width: 44px; height: 44px; } .lb-keys { display: none; } }
0 / 0 photos
Đang tải…

Không tìm thấy ảnh

Thử chọn năm khác

(() => { 'use strict';/* ─── CONFIG ─────────────────────────────────────────────── */ const API_BASE = '/wp-json/wp/v2/visual-diary'; const PAGE_SIZE = 40; const THUMB_COUNT = 9; const TONES = ['t0', 't1', 't2', 't3', 't4', 't5', 't6', 't7'];/* ─── STATE ──────────────────────────────────────────────── */ let ALL_ITEMS = []; let LOADED_COUNT = 0; let TOTAL_COUNT = 0; let IS_LOADING = false; let CURRENT_YEAR = 'all'; let CURRENT_PAGE = 1; let LB_INDEX = 0; let LB_OPEN = false;/* ─── API ────────────────────────────────────────────────── */ async function fetchImages(year = 'all', page = 1) { const params = new URLSearchParams({ per_page: PAGE_SIZE, page, orderby: 'date', order: 'desc', _fields: 'id,date,featured_media_url,featured_media_thumb,width,height,content' });if (year !== 'all') { params.append('after', `${year}-01-01T00:00:00`); params.append('before', `${year}-12-31T23:59:59`); }const res = await fetch(`${API_BASE}?${params}`); if (!res.ok) throw new Error(`API error: ${res.status}`);const total = parseInt(res.headers.get('X-WP-Total') || '0', 10); const data = await res.json();return { total, items: data.map(p => ({ id: p.id, date: p.date.slice(0, 10), year: new Date(p.date).getFullYear(), src: p.featured_media_url || null, thumb: p.featured_media_thumb || null, w: p.width || 0, h: p.height || 0, caption: p.content?.rendered ? p.content.rendered.replace(/]+>/g, '').trim() : '', tone: TONES[p.id % TONES.length] })) }; }async function fetchYears() { try { const res = await fetch(`/wp-json/dannybach/v1/visual-diary-years?_=${Date.now()}`); if (!res.ok) return []; return await res.json(); } catch (e) { console.error('fetchYears:', e); return []; } }/* ─── INIT ───────────────────────────────────────────────── */ async function init() { showLoading(true); try { const [{ total, items }, years] = await Promise.all([fetchImages('all', 1), fetchYears()]);TOTAL_COUNT = total; ALL_ITEMS = items; LOADED_COUNT = items.length; CURRENT_PAGE = 1;buildYearPills(years); renderGrid(items, true); updateCounts(); } catch (e) { console.error('Visual Library init failed:', e); document.getElementById('emptyState')?.classList.add('visible'); } showLoading(false); setupInfiniteScroll(); }/* ─── YEAR PILLS ─────────────────────────────────────────── */ function buildYearPills(years) { const strip = document.getElementById('yearStrip'); if (!strip) return;strip.innerHTML = ''; years.forEach(y => { const btn = document.createElement('button'); btn.className = 'year-pill'; btn.dataset.year = y; btn.textContent = y; btn.addEventListener('click', () => applyFilter(y)); strip.appendChild(btn); }); }/* ─── FILTER ─────────────────────────────────────────────── */ async function applyFilter(year) { if (IS_LOADING) return; CURRENT_YEAR = year;document.querySelectorAll('.year-pill').forEach(p => { p.classList.toggle('active', String(p.dataset.year) === String(year)); });document.getElementById('gridContainer').innerHTML = ''; const es = document.getElementById('emptyState'); es?.classList.remove('visible'); window.scrollTo({ top: 0 }); showLoading(true);try { const result = await fetchImages(year, 1); TOTAL_COUNT = result.total; ALL_ITEMS = result.items; LOADED_COUNT = result.items.length; CURRENT_PAGE = 1;if (!result.items.length) { es?.classList.add('visible'); } else { renderGrid(result.items, true); } updateCounts(); } catch (e) { console.error('Filter error:', e); }showLoading(false); }/* ─── RENDER GRID ────────────────────────────────────────── */ function renderGrid(items, reset) { const container = document.getElementById('gridContainer'); const activeCols = document.querySelector('.col-btn.active')?.dataset.cols || '4';let curYear = null; let curMasonry = null;if (!reset) { const bands = container.querySelectorAll('.year-band-label'); const last = bands[bands.length - 1]; if (last) curYear = parseInt(last.textContent, 10); const masonries = container.querySelectorAll('.masonry'); curMasonry = masonries[masonries.length - 1] || null; }items.forEach((item, idx) => { if (item.year !== curYear) { curYear = item.year;const band = document.createElement('div'); band.className = 'year-band'; band.innerHTML = ` ${curYear}
`; container.appendChild(band);const outer = document.createElement('div'); outer.className = 'masonry-outer'; const masonry = document.createElement('div'); masonry.className = `masonry cols-${activeCols}`; outer.appendChild(masonry); container.appendChild(outer); curMasonry = masonry; }const globalIndex = reset ? idx : ALL_ITEMS.length - items.length + idx; curMasonry.appendChild(createItem(item, globalIndex)); }); }/* ─── CREATE ITEM ────────────────────────────────────────── */ function createItem(item, globalIndex) { const el = document.createElement('div'); el.className = 'm-item'; el.dataset.index = globalIndex;const df = formatDate(item.date); const ratio = (item.w && item.h) ? ((item.h / item.w) * 100).toFixed(1) : '100';el.innerHTML = `
${item.src ? `${df}` : `
`}
${df} ${item.caption ? `

${item.caption}

` : ''}
`;el.addEventListener('click', () => openLightbox(globalIndex)); return el; }/* ─── INFINITE SCROLL ────────────────────────────────────── */ function setupInfiniteScroll() { const sentinel = document.getElementById('sentinel'); const obs = new IntersectionObserver(async entries => { if (!entries[0].isIntersecting || IS_LOADING || LOADED_COUNT >= TOTAL_COUNT) return;IS_LOADING = true; showLoading(true); try { CURRENT_PAGE++; const result = await fetchImages(CURRENT_YEAR, CURRENT_PAGE); ALL_ITEMS = ALL_ITEMS.concat(result.items); LOADED_COUNT += result.items.length; renderGrid(result.items, false); updateCounts(); } catch (e) { console.error('Infinite scroll:', e); CURRENT_PAGE--; } showLoading(false); IS_LOADING = false; }, { rootMargin: '500px' });obs.observe(sentinel); }/* ─── LIGHTBOX ───────────────────────────────────────────── */ function setupLightbox() { const lbClose = document.getElementById('lbClose'); const lbPrev = document.getElementById('lbPrev'); const lbNext = document.getElementById('lbNext'); const lb = document.getElementById('lb');if (!lbClose || !lbPrev || !lbNext || !lb) return;lbClose.addEventListener('click', e => { e.stopPropagation(); closeLightbox(); }); lbPrev.addEventListener('click', e => { e.stopPropagation(); navigate(-1); }); lbNext.addEventListener('click', e => { e.stopPropagation(); navigate(1); });/* Close when clicking anywhere outside of the image itself */ lb.addEventListener('click', e => { if (!e.target.closest('#lbImgWrap')) { closeLightbox(); } });document.addEventListener('keydown', e => { if (!LB_OPEN) return; if (e.key === 'Escape') { e.preventDefault(); closeLightbox(); } if (e.key === 'ArrowLeft') { e.preventDefault(); navigate(-1); } if (e.key === 'ArrowRight') { e.preventDefault(); navigate(1); } }, true);let tx = 0; lb.addEventListener('touchstart', e => { tx = e.changedTouches[0].screenX; }, { passive: true });lb.addEventListener('touchend', e => { const dx = e.changedTouches[0].screenX - tx; if (Math.abs(dx) > 50) navigate(dx img.classList.add('loaded'); img.src = item.src; wrap.appendChild(img); } else { const ph = document.createElement('div'); ph.className = `lb-placeholder ${item.tone}`; ph.innerHTML = `
`; wrap.appendChild(ph); setTimeout(() => ph.classList.add('loaded'), 50); }document.getElementById('lbDate').textContent = formatDate(item.date); document.getElementById('lbCaption').textContent = item.caption || ''; document.getElementById('lbCounter').textContent = `${LB_INDEX + 1} / ${ALL_ITEMS.length}`; }function renderLbThumbs() { const strip = document.getElementById('lbThumbs'); strip.innerHTML = '';const half = Math.floor(THUMB_COUNT / 2); const start = Math.max(0, LB_INDEX - half); const end = Math.min(ALL_ITEMS.length, start + THUMB_COUNT);for (let i = start; i < end; i++) { const item = ALL_ITEMS[i]; const th = document.createElement('div'); th.className = `lb-thumb${i === LB_INDEX ? ' active' : ''}`; th.innerHTML = item.thumb ? `` : `
`; th.addEventListener('click', e => { e.stopPropagation(); LB_INDEX = i; renderLbImage(); renderLbThumbs(); }); strip.appendChild(th); } }/* ─── COLUMN PICKER ──────────────────────────────────────── */ function setupColPicker() { const picker = document.getElementById('colPicker'); if (!picker) return;picker.addEventListener('click', e => { const btn = e.target.closest('.col-btn'); if (!btn) return;picker.querySelectorAll('.col-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active');const cols = btn.dataset.cols; document.querySelectorAll('.masonry').forEach(m => { m.className = `masonry cols-${cols}`; }); }); }/* ─── HELPERS ────────────────────────────────────────────── */ function updateCounts() { document.getElementById('visibleCount').textContent = LOADED_COUNT; document.getElementById('totalCount').textContent = TOTAL_COUNT; }function showLoading(show) { document.getElementById('loadingIndicator')?.classList.toggle('visible', show); }function formatDate(d) { if (!d) return ''; const [year, month, day] = d.split('-'); const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return `${day} ${months[parseInt(month, 10) - 1]} ${year}`; }/* ─── START ──────────────────────────────────────────────── */ function start() { document.body.classList.add('vl-page'); setupLightbox(); setupColPicker(); init(); }if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start); } else { setTimeout(start, 300); } })();