← back to Fashion Style Guides

public/app.js

209 lines

const LS_PREFIX = 'fsg.';
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);

const state = {
  brands: [],
  pantone: null,
  common: null,
  sortBy: localStorage.getItem(LS_PREFIX + 'sortBy') || 'name',
  density: parseInt(localStorage.getItem(LS_PREFIX + 'density') || '320', 10),
};

document.documentElement.style.setProperty('--card-w', state.density + 'px');

$('#sortBy').value = state.sortBy;
$('#density').value = state.density;
$('#sortBy').addEventListener('change', (e) => {
  state.sortBy = e.target.value;
  localStorage.setItem(LS_PREFIX + 'sortBy', state.sortBy);
  renderBrands();
});
$('#density').addEventListener('input', (e) => {
  state.density = parseInt(e.target.value, 10);
  localStorage.setItem(LS_PREFIX + 'density', state.density);
  document.documentElement.style.setProperty('--card-w', state.density + 'px');
});

$('#menuBtn').addEventListener('click', () => $('#drawer').hidden = false);
$$('.drawer-close, .drawer a').forEach(el => el.addEventListener('click', () => $('#drawer').hidden = true));

async function fetchJson(path) {
  const r = await fetch(path);
  if (!r.ok) throw new Error(path + ' returned ' + r.status);
  return r.json();
}

function huePos(hex) {
  const h = hex.replace('#', '');
  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
  if (max === min) return -1;
  const d = max - min;
  let hue;
  if (max === r) hue = ((g - b) / d) % 6;
  else if (max === g) hue = (b - r) / d + 2;
  else hue = (r - g) / d + 4;
  return (hue * 60 + 360) % 360;
}

function renderBrands() {
  let arr = [...state.brands];
  switch (state.sortBy) {
    case 'name': arr.sort((a, b) => a.name.localeCompare(b.name)); break;
    case 'founded-asc': arr.sort((a, b) => (a.founded || 9999) - (b.founded || 9999)); break;
    case 'founded-desc': arr.sort((a, b) => (b.founded || 0) - (a.founded || 0)); break;
    case 'primary-hue':
      arr.sort((a, b) => {
        const ha = huePos((a.palette[0] || {}).hex || '#000000');
        const hb = huePos((b.palette[0] || {}).hex || '#000000');
        return ha - hb;
      });
      break;
    case 'house': arr.sort((a, b) => (a.house_of || '').localeCompare(b.house_of || '') || a.name.localeCompare(b.name)); break;
  }

  const grid = $('#grid');
  grid.innerHTML = '';
  arr.forEach(b => {
    const card = document.createElement('div');
    card.className = 'card';
    const top = b.palette.slice(0, 4).map(c => `<span class="swatch" style="background:${c.hex}" title="${c.name} ${c.hex}"></span>`).join('');
    const chips = b.palette.map(c => `<span class="chip" style="background:${c.hex}" title="${c.name} ${c.hex}"></span>`).join('');
    card.innerHTML = `
      <div class="card-hero">${top}</div>
      <div class="card-body">
        <h3>${b.name}</h3>
        <div class="meta">${b.founded || ''} · ${b.house_of || ''} · ${b.headquarters || ''}</div>
        <p class="motif">${b.signature_motif || ''}</p>
        <div class="palette-row">${chips}</div>
        <div class="type">
          <b>Wordmark:</b> ${b.typography.wordmark}<br>
          <b>Display:</b> ${b.typography.display}<br>
          <b>Body:</b> ${b.typography.body}
        </div>
      </div>`;
    grid.appendChild(card);
  });
  $('#count').textContent = arr.length;
}

function renderCommon() {
  const wrap = $('#commonColors');
  wrap.innerHTML = '';
  state.common.common_color_families.forEach(fam => {
    const div = document.createElement('div');
    div.className = 'family';
    div.innerHTML = `
      <h4>${fam.family} <span class="count-pill">${fam.count} swatches</span></h4>
      <div class="family-chips">
        ${fam.colors.map(c => `<span class="chip" style="background:${c.hex}" title="${c.name} (${c.brand}) ${c.hex}"></span>`).join('')}
      </div>`;
    wrap.appendChild(div);
  });
}

function renderCoty() {
  const grid = $('#cotyGrid');
  grid.innerHTML = '';
  state.pantone.color_of_the_year.forEach(c => {
    const card = document.createElement('div');
    card.className = 'coty';
    let swatch;
    if (!c.hex) swatch = `<div class="coty-swatch" style="background:repeating-linear-gradient(45deg,#eee,#eee 8px,#ddd 8px,#ddd 16px)"></div>`;
    else if (c.hex.includes(',')) {
      const [h1, h2] = c.hex.split(',');
      swatch = `<div class="coty-swatch dual"><span style="background:${h1}"></span><span style="background:${h2}"></span></div>`;
    } else {
      swatch = `<div class="coty-swatch" style="background:${c.hex}"></div>`;
    }
    card.innerHTML = `${swatch}
      <div class="coty-body">
        <div class="year">${c.year}</div>
        <h4>${c.name}</h4>
        <div class="pmscode">${c.pantone ? 'Pantone ' + c.pantone : ''}</div>
        ${c.note ? `<div class="note">${c.note}</div>` : ''}
      </div>`;
    grid.appendChild(card);
  });
}

function renderFashionPalette() {
  const p = state.pantone.fashion_palette_recent;
  const wrap = $('#fashionPalette');
  wrap.innerHTML = `
    <div class="season">${p.season} · published ${p.published}</div>
    <div class="colors">
      ${p.colors.map(c => `
        <div class="pc">
          <div class="pc-swatch" style="background:${c.hex}"></div>
          <div class="pc-body">
            <div class="name">${c.name}</div>
            <div class="pms">Pantone ${c.pantone} · ${c.hex.toUpperCase()}</div>
          </div>
        </div>`).join('')}
    </div>
    <p class="lede" style="margin-top:18px">${p.note}</p>`;
}

function renderHomePalette() {
  const p = state.pantone.home_interiors_palette_recent;
  const wrap = $('#homePalette');
  wrap.innerHTML = `<div class="season">PANTONEVIEW Home + Interiors ${p.year} · published ${p.published}</div>` +
    p.themes.map(t => `
      <div class="theme">
        <h3>${t.theme}</h3>
        <p>${t.description}</p>
        <div class="colors">
          ${t.colors.map(c => `
            <div class="pc">
              <div class="pc-swatch" style="background:${c.hex}"></div>
              <div class="pc-body">
                <div class="name">${c.name}</div>
                <div class="pms">Pantone ${c.pantone} · ${c.hex.toUpperCase()}</div>
              </div>
            </div>`).join('')}
        </div>
      </div>`).join('') +
    `<p class="lede">${p.note}</p>`;
}

async function loadAgentStatus() {
  try {
    const s = await fetchJson('/api/agent/status');
    if (!s.last_run) {
      $('#agentStatus').textContent = 'no runs yet — fire `npm run refresh` to seed';
    } else {
      $('#agentStatus').textContent =
        `Last run mtime: ${s.last_run.mtime}\nLines in log: ${s.last_run.line_count}\nLast log line:\n  ${s.last_run.last_line}`;
    }
  } catch (e) { $('#agentStatus').textContent = 'agent-status fetch failed: ' + e.message; }
}

async function init() {
  try {
    const [bdata, pdata, cdata] = await Promise.all([
      fetchJson('/api/brands'),
      fetchJson('/api/pantone'),
      fetchJson('/api/common-colors'),
    ]);
    state.brands = bdata.brands;
    state.pantone = pdata;
    state.common = cdata;
    $('#lastUpdated').textContent = 'Data last updated ' + bdata.last_updated;
    renderBrands();
    renderCommon();
    renderCoty();
    renderFashionPalette();
    renderHomePalette();
    loadAgentStatus();
  } catch (e) {
    console.error(e);
    $('#grid').innerHTML = '<p style="color:#a30000;padding:20px">Load failed: ' + e.message + '</p>';
  }
}

init();