// 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 = `
${p.class}${p.type === 'series' ? 'SERIES' : ''}
${(p.role || p.summary || '').split('—')[0].slice(0, 46)}
${p.mpn || p.id}
${p.when_to_use || p.summary || ''}
PKG ${p.package || (p.series && p.series.package) || '—'} 3D ✓${p.stock ? `STK ${(p.stock / 1000).toFixed(0)}k` : ''}
`; 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 = '
No parts match — try a broader description, or contribute one.
'; 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); }); });