← back to Paul Conrad Cartoons Rebrand
public/p24.js
98 lines
// Inkwell — P24 Art gallery (TK-12230).
// Loads /api/p24 and renders every imported photo + cartoon INLINE with a type filter,
// a sort select, and a density slider. All three persist to localStorage.
// Every card carries a created date+time chip (admin rule) with the ISO value in title=.
(function () {
const LS = { type: 'inkwell:p24:type', sort: 'inkwell:p24:sort', density: 'inkwell:p24:density' };
const get = (k, d) => { try { const v = localStorage.getItem(k); return v === null ? d : JSON.parse(v); } catch (e) { return d; } };
const set = (k, v) => { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) { /* ignore */ } };
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
const fmtDate = (iso) => {
const d = new Date(iso);
return isNaN(d) ? '' : d.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
};
let ITEMS = [];
let state = { type: get(LS.type, 'all'), sort: get(LS.sort, 'newest'), density: get(LS.density, 240) };
function sorted(list) {
const a = [...list];
const t = (x) => new Date(x.date).getTime() || 0;
switch (state.sort) {
case 'oldest': return a.sort((x, y) => t(x) - t(y));
case 'title-az': return a.sort((x, y) => x.title.localeCompare(y.title));
case 'type': return a.sort((x, y) => x.type.localeCompare(y.type) || t(y) - t(x));
case 'newest':
default: return a.sort((x, y) => t(y) - t(x));
}
}
function card(it) {
const panels = it.panels && it.panels.length
? `<div class="panels">${it.panels.map((p, i) => `<img src="${esc(p)}" alt="${esc(it.title)} — panel ${i + 1}" loading="lazy">`).join('')}</div>`
: '';
return `<article class="card p24-card ${esc(it.type)}" data-id="${esc(it.id)}" tabindex="0">
<div class="thumb"><img src="${esc(it.src)}" alt="${esc(it.title)}" loading="lazy"></div>
<div class="body">
<div class="title">${esc(it.title)}</div>
<span class="when" title="${esc(it.date)}">🕓 ${esc(fmtDate(it.date))}</span>
${it.caption ? `<div class="caption">${esc(it.caption)}</div>` : ''}
${panels}
<div class="badges"><span class="badge ${esc(it.type)}">${it.type === 'photo' ? 'Photo' : 'Cartoon'}</span>${it.category ? `<span class="topic-tag">${esc(it.category)}</span>` : ''}</div>
</div>
</article>`;
}
function render() {
document.querySelectorAll('#p24-filter button').forEach(b => b.classList.toggle('active', b.dataset.type === state.type));
const list = sorted(ITEMS.filter(i => state.type === 'all' || i.type === state.type));
document.getElementById('p24-count').textContent = `${list.length} of ${ITEMS.length}`;
const grid = document.getElementById('p24-grid');
grid.innerHTML = list.length ? list.map(card).join('') : '<div class="empty-note">Nothing matches this filter.</div>';
grid.querySelectorAll('.p24-card').forEach(c => {
c.addEventListener('click', () => open(c.dataset.id));
c.addEventListener('keydown', (e) => { if (e.key === 'Enter') open(c.dataset.id); });
});
}
function open(id) {
const it = ITEMS.find(x => x.id === id);
if (!it) return;
const panels = it.panels && it.panels.length
? `<div class="p24-panels">${it.panels.map((p, i) => `<img src="${esc(p)}" alt="${esc(it.title)} — panel ${i + 1}">`).join('')}</div>` : '';
document.getElementById('modal-body').innerHTML = `
<button class="close-x" id="modal-close">×</button>
<h2>${esc(it.title)}</h2>
<div class="modal-meta"><span class="when" title="${esc(it.date)}">🕓 ${esc(fmtDate(it.date))}</span> · ${it.type === 'photo' ? 'Photo' : 'Cartoon'}${it.category ? ' · ' + esc(it.category) : ''}</div>
<img class="p24-full" src="${esc(it.src)}" alt="${esc(it.title)}">
${panels}
${it.caption ? `<p>${esc(it.caption)}</p>` : ''}
<dl><dt>Source</dt><dd>P24 · ${esc(it.source_page)}</dd></dl>`;
document.getElementById('modal-backdrop').classList.add('open');
document.getElementById('modal-close').addEventListener('click', () => document.getElementById('modal-backdrop').classList.remove('open'));
}
function init() {
const sortEl = document.getElementById('p24-sort');
const densEl = document.getElementById('p24-density');
sortEl.value = state.sort;
densEl.value = state.density;
document.documentElement.style.setProperty('--p24-min', state.density + 'px');
sortEl.addEventListener('change', () => { state.sort = sortEl.value; set(LS.sort, state.sort); render(); });
densEl.addEventListener('input', () => {
state.density = Number(densEl.value);
document.documentElement.style.setProperty('--p24-min', state.density + 'px');
set(LS.density, state.density);
});
document.querySelectorAll('#p24-filter button').forEach(b => b.addEventListener('click', () => {
state.type = b.dataset.type; set(LS.type, state.type); render();
}));
}
fetch('/api/p24')
.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(doc => { ITEMS = doc.items || []; init(); render(); })
.catch(err => { document.getElementById('p24-grid').innerHTML = `<div class="empty-note">Could not load P24 art — ${esc(err.message)}</div>`; });
})();