// part.js — part detail: interactive PBR viewer + full curated record (specs, pins, spice, downloads).
import { mountViewer } from '/assets/viewer.js';
const esc = (s) => (s == null ? '' : String(s).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c])));
const id = new URLSearchParams(location.search).get('id');
function dl(name, text, mime = 'text/plain') {
const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([text], { type: mime }));
a.download = name; a.click();
}
function spendCredit() {
let c = parseInt(localStorage.getItem('ppcb_credits') || '5', 10);
if (c <= 0) { alert('Out of download credits — contribute a curated part to earn 5 more.'); return false; }
localStorage.setItem('ppcb_credits', --c); document.getElementById('cred-n').textContent = c; return true;
}
(async () => {
const parts = await (await fetch('/assets/catalog.json')).json();
const p = parts.find((x) => x.id === id);
const root = document.getElementById('root');
document.getElementById('cred-n').textContent = localStorage.getItem('ppcb_credits') || '5';
if (!p) { root.innerHTML = '
'; return; }
document.title = `${p.mpn || p.id} — PromptPCB Parts`;
const params = (p.params || []).map((x) => `| ${esc(x.name)} | ${esc(x.value)} ${esc(x.unit || '')}${x.note ? `${esc(x.note)}` : ''} |
`).join('');
const pins = (p.pins || []).map((x) => `${esc(x.pin ?? '')}${esc(x.name || '')}${esc(x.type || '')}
`).join('');
const ucs = (p.use_cases || []).map((u) => `${esc(u)}`).join('');
const tagList = Object.entries(p.tags || {}).filter(([, v]) => v != null && v !== '' && !Array.isArray(v)).map(([k, v]) => `${esc(k)} ${esc(v)}`).join('');
const kr = ((p.tags || {}).key_ratings || []).map((r) => `${esc(r)}`).join('');
const ser = p.series ? `${Object.entries(p.series).filter(([k]) => k !== 'mesh').map(([k, v]) => `${esc(k)} ${esc(v)}`).join('')}
` : '';
root.innerHTML = `
PBR · drag to orbit${esc(p.package || (p.series && p.series.package) || '')}${(p.pins || []).length} pins
${esc(p.class)}${p.type === 'series' ? ' · series' : ''}
${esc((p.role || p.summary || '').split('—')[0])}
${esc(p.mpn || p.id)} · ${esc(p.manufacturer || '')} · ${esc(p.lcsc || '')}
${esc(p.summary)}
✓ 3D model
✓ SPICE
✓ Datasheet
✓ Pin map (${(p.pins || []).length})
✓ Verified
${p.when_to_use ? `
When to use
${esc(p.when_to_use)}
` : ''}
${ucs ? `
` : ''}
${params ? `
` : ''}
${ser}
${pins ? `
Pins & pads · aligned to footprint
${pins}
` : ''}
${(tagList || kr) ? `
Requirement tags
${kr}${tagList}
` : ''}
${p.spice ? `
SPICE model
${esc(p.spice)}` : ''}
`;
mountViewer(document.getElementById('cv'), p.model_3d);
document.getElementById('dl-all').onclick = async () => {
if (!spendCredit()) return;
const obj = await (await fetch(p.model_3d)).text();
dl(`${p.id}.obj`, obj);
dl(`${p.id}.cir`, `* ${p.mpn} SPICE model — PromptPCB Parts\n${p.spice || ''}\n`);
dl(`${p.id}.json`, JSON.stringify(p, null, 2), 'application/json');
};
document.getElementById('lib').onclick = () => {
const lib = JSON.parse(localStorage.getItem('ppcb_lib') || '[]');
if (!lib.includes(p.id)) { lib.push(p.id); localStorage.setItem('ppcb_lib', JSON.stringify(lib)); }
document.getElementById('lib').textContent = '✓ In library';
};
})();