← back to Crazy News Channel Shadowman

cartoons/shadow-man.js

77 lines

// P24 — Shadow Man gallery (TK-12241). Data: shadow-man-data.js (window.P24_SHADOW_MAN).
// Theme + era filters, sort, density slider — all persisted to localStorage. Styling lives in
// ../assets/style.css (body.shadow-man-page); this file sets no inline styles except the
// --card-w custom property the shared gallery grid already reads.
(function () {
  const ITEMS = (window.P24_SHADOW_MAN && window.P24_SHADOW_MAN.items) || [];
  const $ = (id) => document.getElementById(id);
  const LS = { theme: 'p24shadowman.theme', era: 'p24shadowman.era', sort: 'p24shadowman.sort', density: 'p24shadowman.density' };
  const get = (k, d) => { try { return localStorage.getItem(k) ?? d; } catch { return d; } };
  const put = (k, v) => { try { localStorage.setItem(k, v); } catch {} };
  const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
  const fmt = (iso) => { const d = new Date(iso); return isNaN(d) ? '' : d.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); };

  const state = { theme: get(LS.theme, 'all'), era: get(LS.era, 'all'), sort: get(LS.sort, 'newest'), density: get(LS.density, '320') };

  function fill(sel, values, cur, label) {
    sel.innerHTML = `<option value="all">${esc(label)}</option>` + values.map(v => `<option value="${esc(v)}">${esc(v)}</option>`).join('');
    sel.value = values.includes(cur) ? cur : 'all';
    return sel.value;
  }

  function sorted(list) {
    const t = (x) => new Date(x.created_at).getTime() || 0;
    const a = [...list];
    switch (state.sort) {
      case 'oldest': return a.sort((x, y) => t(x) - t(y) || x.id.localeCompare(y.id));
      case 'title': return a.sort((x, y) => x.title.localeCompare(y.title));
      case 'theme': return a.sort((x, y) => x.theme.localeCompare(y.theme) || x.title.localeCompare(y.title));
      case 'era': return a.sort((x, y) => x.era.localeCompare(y.era) || x.title.localeCompare(y.title));
      default: return a.sort((x, y) => t(y) - t(x) || y.id.localeCompare(x.id));
    }
  }

  function render() {
    const list = sorted(ITEMS.filter(i => (state.theme === 'all' || i.theme === state.theme) && (state.era === 'all' || i.era === state.era)));
    $('smCount').textContent = `${list.length} of ${ITEMS.length}`;
    $('smEmpty').hidden = list.length > 0;
    $('smEmptyMsg').textContent = ITEMS.length ? 'No Shadow Man cartoons match this filter.' : 'No Shadow Man cartoons have been approved for publication yet.';
    $('smGrid').innerHTML = list.map(it => `
      <button type="button" class="card sm-card" role="listitem" data-id="${esc(it.id)}">
        <span class="thumb"><img src="${esc(it.src)}" alt="${esc(it.title)} — ${esc(it.caption)}" loading="lazy"></span>
        <span class="body">
          <span class="title">${esc(it.title)}</span>
          <span class="blurb sm-card-caption">${esc(it.caption)}</span>
          <span class="sm-tags"><span class="cat-badge">${esc(it.theme)}</span><span class="sm-era">${esc(it.era)}</span></span>
          <span class="meta" title="${esc(it.created_at)}">🕓 ${esc(fmt(it.created_at))}</span>
        </span>
      </button>`).join('');
  }

  function open(id) {
    const it = ITEMS.find(x => x.id === id);
    if (!it) return;
    $('smDialogTitle').textContent = it.title;
    $('smDialogMeta').textContent = `${it.theme} · ${it.era} · signed ${it.signature}`;
    $('smDialogImg').src = it.src;
    $('smDialogImg').alt = `${it.title} — ${it.caption}`;
    $('smDialogCaption').textContent = it.caption;
    $('smDialog').showModal();
  }

  state.theme = fill($('smTheme'), [...new Set(ITEMS.map(i => i.theme))].sort(), state.theme, 'All themes');
  state.era = fill($('smEra'), [...new Set(ITEMS.map(i => i.era))].sort(), state.era, 'All eras');
  $('smSort').value = state.sort;
  $('smDensity').value = state.density;
  document.documentElement.style.setProperty('--card-w', state.density + 'px');

  $('smTheme').addEventListener('change', e => { state.theme = e.target.value; put(LS.theme, state.theme); render(); });
  $('smEra').addEventListener('change', e => { state.era = e.target.value; put(LS.era, state.era); render(); });
  $('smSort').addEventListener('change', e => { state.sort = e.target.value; put(LS.sort, state.sort); render(); });
  $('smDensity').addEventListener('input', e => { state.density = e.target.value; put(LS.density, state.density); document.documentElement.style.setProperty('--card-w', state.density + 'px'); });
  $('smGrid').addEventListener('click', e => { const c = e.target.closest('.sm-card'); if (c) open(c.dataset.id); });
  $('smClose').addEventListener('click', () => $('smDialog').close());
  $('smDialog').addEventListener('click', e => { if (e.target === $('smDialog')) $('smDialog').close(); });
  render();
})();