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; }
}
← → navigate · ESC close(() => {
'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 = `
`
: ``;
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);
}
})();
0 / 0 photos
Đang tải…
Không tìm thấy ảnh
Thử chọn năm khác
${item.src
? `
`
: ``}
`;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
? `
${df}
${item.caption ? `
${item.caption}
` : ''}