RECORDGOD/site/records.html
type-two 28a46615a5 feat(shop): entity browse pages — artist/label/genre/style storefront views
records.html reads /artist|label|genre|style/{val} from the path, sets the
filter + an entity header (artist/label name via /shop/{kind}/{id}). /shop/browse
gains artist/label EXISTS filters + primary artist_id; release page links
artist→/artist/{id} and genre/style pills→/{kind}/{name}.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 01:18:15 +10:00

127 lines
9.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Records</title>
<style>
:root{--primary:#ff5db1;--accent:#46d18a;--bg:#0c0c0e;--panel:#141418;--text:#f0f0f2;--mut:#8a8a98;--line:#26262c;--radius:12px;--cols:4;--font:system-ui}
*{box-sizing:border-box}
html,body{margin:0;background:var(--bg);color:var(--text);font-family:var(--font),system-ui,sans-serif}
a{color:inherit;text-decoration:none}
header{display:flex;align-items:center;gap:16px;padding:14px 22px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--bg);z-index:10}
.logo{font-weight:800;font-size:20px}.logo b{color:var(--primary)}
.logo img{height:30px;vertical-align:middle}
nav.menu{display:flex;gap:16px;flex:1}nav.menu a{color:var(--mut);font-size:14px}nav.menu a:hover{color:var(--text)}
.sbox{display:flex;gap:6px}.sbox input{padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--text);width:220px;font:inherit}
button,.btn{cursor:pointer;border:0;border-radius:9px;font:600 13px var(--font),system-ui;padding:9px 13px;background:var(--primary);color:#10070c}
.ghost{background:var(--panel);color:var(--text);border:1px solid var(--line)}
.wrap{display:grid;grid-template-columns:230px 1fr;gap:22px;max-width:1340px;margin:0 auto;padding:20px}
.filters{align-self:start;position:sticky;top:74px}
.fgroup{margin-bottom:16px}.fgroup h4{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--mut);margin:0 0 8px}
.chip{display:inline-block;padding:4px 9px;margin:2px 3px 2px 0;border:1px solid var(--line);border-radius:20px;font-size:12px;cursor:pointer;color:var(--mut)}
.chip:hover{color:var(--text)}.chip.on{background:var(--primary);color:#10070c;border-color:var(--primary)}
.fnum{display:flex;gap:6px}.fnum input{width:100%;padding:7px;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--text);font:inherit}
.topbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}
select{padding:8px 10px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--text);font:inherit}
.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:16px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;transition:transform .12s,box-shadow .12s}
.card:hover{transform:translateY(-3px);box-shadow:0 8px 24px rgba(0,0,0,.35)}
.card .cov{width:100%;aspect-ratio:1;object-fit:cover;background:#222;display:block}
.card .cb{padding:10px}
.card .ti{font-weight:600;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.card .ar{color:var(--mut);font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.card .ge{color:var(--mut);font-size:11px;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.card .pr{color:var(--primary);font-weight:800;font-size:16px;margin-top:6px}
.card .cp{color:var(--mut);font-size:11px}
.pg{display:flex;gap:8px;align-items:center;justify-content:center;margin:22px 0}
.muted{color:var(--mut)}
</style>
</head>
<body>
<header>
<a class="logo" href="/records" id="logo">Record<b>God</b></a>
<nav class="menu" id="menu"></nav>
<div class="sbox"><input id="q" placeholder="search records…"><button onclick="applyQ()">Search</button></div>
</header>
<div class="wrap">
<aside class="filters">
<div class="fgroup"><h4>Sort</h4>
<select id="sort" onchange="reload()"><option value="new">Newest in</option><option value="price_asc">Price ↑</option><option value="price_desc">Price ↓</option><option value="year">Year</option><option value="artist">Artist AZ</option><option value="title">Title AZ</option></select></div>
<div class="fgroup"><h4>Price</h4><div class="fnum"><input id="pmin" type="number" placeholder="$ min" oninput="debReload()"><input id="pmax" type="number" placeholder="$ max" oninput="debReload()"></div></div>
<div class="fgroup"><h4>Year</h4><div class="fnum"><input id="ymin" type="number" placeholder="from" oninput="debReload()"><input id="ymax" type="number" placeholder="to" oninput="debReload()"></div></div>
<div class="fgroup"><h4>Genre</h4><div id="fGenres"></div></div>
<div class="fgroup"><h4>Style</h4><div id="fStyles"></div></div>
<div class="fgroup"><h4>Format</h4><div id="fFormats"></div></div>
</aside>
<main>
<h2 id="ehdr" style="display:none;margin:0 0 12px;font-weight:600;font-size:22px"></h2>
<div class="topbar"><div class="muted" id="count">loading…</div><button class="ghost" onclick="clearAll()">clear filters</button></div>
<div class="grid" id="grid"></div>
<div class="pg" id="pg"></div>
</main>
</div>
<script>
const $=s=>document.querySelector(s);
const esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
let CFG={}, ST={q:'',genre:'',style:'',fmt:'',artist:'',label:'',page:1};
const money=n=>n==null?'':'$'+Number(n).toFixed(2);
async function parseEntity(){ // /artist/9 · /label/5 · /genre/Electronic · /style/House → header + filter
const m=location.pathname.match(/\/(artist|label|genre|style)\/(.+)$/);
if(!m) return '';
const kind=m[1], val=decodeURIComponent(m[2]);
if(kind==='genre'){ ST.genre=val; return `Genre · <b>${esc(val)}</b>`; }
if(kind==='style'){ ST.style=val; return `Style · <b>${esc(val)}</b>`; }
if(kind==='artist'){ ST.artist=val; const d=await fetch('/shop/artist/'+val).then(r=>r.json()).catch(()=>({})); return `Artist · <b>${esc((d.artist||{}).name||val)}</b>`; }
if(kind==='label'){ ST.label=val; const d=await fetch('/shop/label/'+val).then(r=>r.json()).catch(()=>({})); return `Label · <b>${esc((d.label||{}).name||val)}</b>`; }
return '';
}
async function boot(){
CFG=await fetch('/shop/config').then(r=>r.json()).catch(()=>({}));
const t=CFG.theme||{}; const R=document.documentElement.style;
for(const [k,v] of Object.entries({'--primary':t.primary,'--accent':t.accent,'--bg':t.bg,'--panel':t.panel,'--text':t.text,'--font':t.font})) if(v) R.setProperty(k,v);
if(t.radius) R.setProperty('--radius',t.radius+'px');
if(t.cardCols) R.setProperty('--cols',t.cardCols);
if(t.logo) $('#logo').innerHTML=`<img src="${esc(t.logo)}">`;
$('#menu').innerHTML=(CFG.menu||[]).map(m=>`<a href="${esc(m.href||'#')}">${esc(m.label||'')}</a>`).join('');
const eh=await parseEntity(); if(eh){ const e=$('#ehdr'); e.innerHTML=eh+' <a href="/records" style="font-size:13px;color:var(--mut)">· all records</a>'; e.style.display='block'; }
loadFacets(); reload();
}
async function loadFacets(){
const f=await fetch('/shop/facets').then(r=>r.json()).catch(()=>({}));
const chip=(arr,key)=>(arr||[]).map(x=>`<span class="chip" data-k="${key}" data-v="${esc(x.name)}" onclick="toggleFacet('${key}','${esc(x.name).replace(/'/g,"&#39;")}')">${esc(x.name)} <span class="muted">${x.n}</span></span>`).join('');
$('#fGenres').innerHTML=chip(f.genres,'genre'); $('#fStyles').innerHTML=chip(f.styles,'style'); $('#fFormats').innerHTML=chip(f.formats,'fmt');
}
function toggleFacet(k,v){ ST[k]=ST[k]===v?'':v; ST.page=1; syncChips(); reload(); }
function syncChips(){ document.querySelectorAll('.chip').forEach(c=>c.classList.toggle('on', ST[c.dataset.k]===c.dataset.v)); }
function applyQ(){ ST.q=$('#q').value.trim(); ST.page=1; reload(); }
$('#q')&&$('#q').addEventListener('keydown',e=>{ if(e.key==='Enter') applyQ(); });
let dt; function debReload(){ clearTimeout(dt); dt=setTimeout(reload,400); }
function clearAll(){ ST={q:'',genre:'',style:'',fmt:'',artist:ST.artist,label:ST.label,page:1}; $('#q').value=''; ['pmin','pmax','ymin','ymax'].forEach(i=>$('#'+i).value=''); syncChips(); reload(); }
function qs(){
const p=new URLSearchParams(); if(ST.q)p.set('q',ST.q); if(ST.genre)p.set('genre',ST.genre); if(ST.style)p.set('style',ST.style); if(ST.fmt)p.set('fmt',ST.fmt);
if(ST.artist)p.set('artist',ST.artist); if(ST.label)p.set('label',ST.label);
const g=(id,k)=>{ const v=$('#'+id).value; if(v)p.set(k,v); }; g('pmin','price_min');g('pmax','price_max');g('ymin','year_min');g('ymax','year_max');
p.set('sort',$('#sort').value); p.set('page',ST.page); return p.toString();
}
async function reload(){
const d=await fetch('/shop/browse?'+qs()).then(r=>r.json());
const show=k=>(CFG.card||['cover','title','artist','price','condition']).includes(k);
$('#count').textContent=`${d.total.toLocaleString()} records`;
$('#grid').innerHTML=(d.items||[]).map(r=>`<a class="card" href="/release/${r.release_id}">
${show('cover')?`<img class="cov" src="/img/r/${r.release_id}" loading="lazy" onerror="this.style.visibility='hidden'">`:''}
<div class="cb">${show('title')?`<div class="ti">${esc(r.title||'')}</div>`:''}${show('artist')?`<div class="ar">${esc(r.artist||'')}</div>`:''}
${show('genre')?`<div class="ge">${esc(r.genre||'')}${r.year?' · '+r.year:''}</div>`:''}
${show('price')?`<div class="pr">${money(r.price)}${r.copies>1?` <span class="cp">· ${r.copies} copies</span>`:''}</div>`:''}</div></a>`).join('')
|| '<div class="muted" style="grid-column:1/-1;padding:30px;text-align:center">no records match</div>';
const pages=Math.max(1,Math.ceil(d.total/d.per));
$('#pg').innerHTML=`<button class="ghost" ${ST.page<=1?'disabled':''} onclick="goPage(-1)"> prev</button><span class="muted">page ${ST.page} / ${pages}</span><button class="ghost" ${ST.page>=pages?'disabled':''} onclick="goPage(1)">next </button>`;
}
function goPage(n){ ST.page+=n; window.scrollTo(0,0); reload(); }
boot();
</script>
</body>
</html>