← 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();