parts-store/assets/app.js
gator 625999e541 catalog: the 3D chip on a card was also a literal
Same problem as the detail page: every card claimed 3D whether or not model_3d
was set. Derive it.
2026-08-16 14:46:20 +00:00

64 lines
3.6 KiB
JavaScript

// app.js — catalog gallery: load parts, functional search over use-cases, class filter, PBR thumbnails.
import { renderThumbnail } from '/assets/viewer.js';
const $ = (s) => document.querySelector(s);
let PARTS = [], activeClass = null;
function haystack(p) {
return [p.role, p.summary, p.when_to_use, p.usage, p.mpn, p.class, p.subclass, p.package,
(p.use_cases || []).join(' '), Object.values(p.tags || {}).flat().join(' ')].join(' ').toLowerCase();
}
function score(p, terms) {
const h = haystack(p); let s = 0;
for (const t of terms) {
if (!t) continue;
const uc = (p.use_cases || []).join(' ').toLowerCase();
if (uc.includes(t)) s += 3; // functional use-case match weighs most
if ((p.class || '').toLowerCase().includes(t)) s += 2;
if ((p.mpn || '').toLowerCase().includes(t)) s += 2;
if (h.includes(t)) s += 1;
}
return s;
}
function chip(label, val) {
const c = document.createElement('span'); c.className = 'chip' + (activeClass === val ? ' on' : '');
c.textContent = label; c.onclick = () => { activeClass = activeClass === val ? null : val; render(); };
return c;
}
function renderChips() {
const counts = {}; for (const p of PARTS) counts[p.class] = (counts[p.class] || 0) + 1;
const box = $('#chips'); box.innerHTML = '';
box.appendChild(chip('All', null));
Object.entries(counts).sort((a, b) => b[1] - a[1]).forEach(([c, n]) => box.appendChild(chip(`${c} (${n})`, c)));
}
function card(p) {
const el = document.createElement('a'); el.className = 'card reveal'; el.href = `/part.html?id=${p.id}`;
el.innerHTML = `<div class="thumb"><span class="cls">${p.class}</span>${p.type === 'series' ? '<span class="ser">SERIES</span>' : ''}<canvas></canvas></div>
<div class="body"><div class="nm">${(p.role || p.summary || '').split('—')[0].slice(0, 46)}</div>
<div class="mpn">${p.mpn || p.id}</div>
<div class="sm">${p.when_to_use || p.summary || ''}</div>
<div class="meta"><span><b>PKG</b> ${p.package || (p.series && p.series.package) || '—'}</span>
${p.model_3d ? '<span><b>3D</b> ✓</span>' : ''}${p.stock ? `<span><b>STK</b> ${(p.stock / 1000).toFixed(0)}k</span>` : ''}</div></div>`;
return el;
}
async function render() {
const terms = ($('#q').value || '').toLowerCase().split(/\s+/).filter((t) => t.length > 1);
let list = PARTS.filter((p) => !activeClass || p.class === activeClass);
if (terms.length) list = list.map((p) => ({ p, s: score(p, terms) })).filter((x) => x.s > 0).sort((a, b) => b.s - a.s).map((x) => x.p);
$('#count').textContent = `${list.length} part${list.length === 1 ? '' : 's'}${activeClass ? ' · ' + activeClass : ''}${terms.length ? ' · ranked by relevance' : ''}`;
const grid = $('#grid'); grid.innerHTML = '';
if (!list.length) { grid.innerHTML = '<div class="empty">No parts match — try a broader description, or <a href="/submit.html">contribute one</a>.</div>'; return; }
const cards = list.map((p) => { const el = card(p); grid.appendChild(el); return { el, p }; });
window.dispatchEvent(new Event('catalog:rendered'));
// thumbnails one at a time through the shared renderer
for (const { el, p } of cards) {
const cv = el.querySelector('canvas');
const data = await renderThumbnail(p.model_3d, 340);
if (data) { const img = new Image(); img.src = data; img.style.cssText = 'width:100%;height:100%;object-fit:contain'; cv.replaceWith(img); }
}
}
window.ppcbReady(async () => {
PARTS = await window.getCatalog(); // authed — only reachable once signed in
renderChips(); render();
let t; $('#q').addEventListener('input', () => { clearTimeout(t); t = setTimeout(render, 160); });
});