feat(builder): P1 page editor — live preview now renders a REAL release from the catalog (real covers/tracklist) so block-arranging is WYSIWYG; + View-live link

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-06-23 01:00:28 +10:00
parent fcc3d7b2ff
commit 386343cdd3

View File

@ -70,6 +70,7 @@
<div class="t">Record<b>God</b> · storefront builder</div>
<div class="sp"></div>
<span id="status"></span>
<a class="ghost" href="/records" target="_blank" style="text-decoration:none;color:#2a2a30">View live ↗</a>
<button class="ghost" onclick="resetDefaults()">Reset</button>
<button class="btn" onclick="save()">Save</button>
</header>
@ -81,6 +82,7 @@
<script>
const $=s=>document.querySelector(s);
const esc=s=>(s==null?'':String(s)).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
let TOKEN=localStorage.getItem('rg_token')||'';
let cfg=null, palette=null, defaults=null, surface='card';
const hdr=()=>({'Authorization':'Bearer '+TOKEN,'Content-Type':'application/json'});
@ -92,7 +94,7 @@ async function signin(){
localStorage.setItem('rg_token',TOKEN);
const d=await r.json(); cfg=d.config; palette=d.palette; defaults=d.defaults;
$('#gate').style.display='none'; $('#app').style.display='grid';
renderEditor(); renderPreview();
await loadSamples(); renderEditor(); renderPreview();
}
function resetDefaults(){ cfg=JSON.parse(JSON.stringify(defaults)); renderEditor(); renderPreview(); }
@ -161,32 +163,41 @@ async function importBrand(){
}
// ---------- preview ----------
const SAMPLE=[
{t:'Hounds of Love',a:'Kate Bush',p:'42.00',c:'NM',g:'Art Pop',h:320},
{t:'Midnight Marauders',a:'A Tribe Called Quest',p:'55.00',c:'VG+',g:'Hip Hop',h:32},
{t:'Selected Ambient Works 85-92',a:'Aphex Twin',p:'38.50',c:'VG',g:'Electronic',h:175},
{t:'Rumours',a:'Fleetwood Mac',p:'29.95',c:'VG+',g:'Rock',h:265},
];
const cover=(r,cls='pv-cover')=>`<div class="${cls}" style="background:linear-gradient(135deg,hsl(${r.h} 50% 38%),hsl(${(r.h+40)%360} 45% 28%))"></div>`;
const cardEl={cover:r=>cover(r),title:r=>`<div class="pv-title">${r.t}</div>`,artist:r=>`<div class="pv-artist">${r.a}</div>`,
price:r=>`<div class="pv-price">$${r.p}</div>`,condition:r=>`<span class="pv-pill">${r.c}</span>`,genre:r=>`<span class="pv-pill">${r.g}</span>`,
format:()=>`<span class="pv-pill">LP</span>`,year:()=>`<span class="pv-pill">1985</span>`,stock_badge:()=>`<span class="pv-pill ok">in stock</span>`,
let SAMPLE=[], PAGE=null; // real releases pulled from the live catalog
async function loadSamples(){
try{
const b=await fetch('/shop/browse?sort=new').then(r=>r.json());
SAMPLE=(b.items||[]).slice(0,8).map(r=>({id:r.release_id,t:r.title,a:r.artist,
p:r.price!=null?Number(r.price).toFixed(2):'',g:(r.genre||'').split(', ')[0],y:r.year,copies:r.copies}));
if(SAMPLE[0]){ const d=await fetch('/shop/release/'+SAMPLE[0].id).then(r=>r.json());
PAGE=Object.assign({}, SAMPLE[0], d.release||{}, {tracks:d.tracks||[], copies:d.copies||[]}); }
}catch(e){}
if(!SAMPLE.length) SAMPLE=[{t:'Sample Record',a:'Various Artists',p:'29.95',g:'Electronic',y:1990}];
}
const cover=(r,cls='pv-cover')=> r&&r.id
? `<img class="${cls}" src="/img/r/${r.id}" style="object-fit:cover" onerror="this.style.background='linear-gradient(135deg,#444,#222)';this.removeAttribute('src')">`
: `<div class="${cls}" style="background:linear-gradient(135deg,#444,#222)"></div>`;
const cardEl={cover:r=>cover(r),title:r=>`<div class="pv-title">${esc(r.t)}</div>`,artist:r=>`<div class="pv-artist">${esc(r.a)}</div>`,
price:r=>`<div class="pv-price">$${r.p}</div>`,condition:()=>`<span class="pv-pill">VG+</span>`,genre:r=>`<span class="pv-pill">${esc(r.g)}</span>`,
format:()=>`<span class="pv-pill">LP</span>`,year:r=>`<span class="pv-pill">${r.y||''}</span>`,stock_badge:()=>`<span class="pv-pill ok">in stock</span>`,
wishlist:()=>`<button class="pv-ghost"></button>`,cart:()=>`<button class="pv-btn">Add to cart</button>`};
const pageEl={cover:r=>cover(r,'pv-bigcover'),gallery:r=>cover(r,'pv-bigcover'),title:r=>`<h1 class="pv-h1">${r.t}</h1>`,artist:r=>`<div class="pv-artist big">${r.a}</div>`,
label:()=>`<div class="pv-meta">Label · 4AD</div>`,price:r=>`<div class="pv-price big">$${r.p}</div>`,condition:r=>`<div class="pv-meta">Condition · ${r.c}</div>`,
genre:r=>`<span class="pv-pill">${r.g}</span> `,format:()=>`<div class="pv-meta">Format · Vinyl LP</div>`,year:()=>`<div class="pv-meta">Year · 1985</div>`,
country:()=>`<div class="pv-meta">Country · UK</div>`,tracklist:()=>`<div class="pv-tracks">1. Running Up That Hill<br>2. Hounds of Love<br>3. The Big Sky<br>4. Mother Stands for Comfort</div>`,
cart:()=>`<button class="pv-btn big">Add to cart</button>`,wishlist:()=>`<button class="pv-ghost">♡ Wishlist</button>`,
description:()=>`<div class="pv-meta">A landmark 1985 art-pop record, first pressing, sleeve VG+.</div>`,
related:()=>`<div class="pv-meta" style="margin-top:14px">You may also like · 4 more →</div>`,dealgod_value:()=>`<div class="pv-dg">DealGod value · $48 · target $44 · buy-opportunity nearby</div>`};
const pageEl={cover:r=>cover(r,'pv-bigcover'),gallery:r=>cover(r,'pv-bigcover'),title:r=>`<h1 class="pv-h1">${esc(r.t||r.title)}</h1>`,artist:r=>`<div class="pv-artist big">${esc(r.a||r.artist)}</div>`,
label:r=>r.label?`<div class="pv-meta">Label · ${esc(r.label)}</div>`:'',price:r=>`<div class="pv-price big">$${r.p}</div>`,condition:()=>`<div class="pv-meta">Condition · VG+</div>`,
genre:r=>(r.genre||r.g||'').split(', ').filter(Boolean).map(g=>`<span class="pv-pill">${esc(g)}</span> `).join(''),
format:r=>r.format?`<div class="pv-meta">Format · ${esc(r.format)}</div>`:'',year:r=>r.year||r.y?`<div class="pv-meta">Year · ${r.year||r.y}</div>`:'',
country:r=>r.country?`<div class="pv-meta">Country · ${esc(r.country)}</div>`:'',
tracklist:r=>`<div class="pv-tracks">${(r.tracks||[]).slice(0,10).map(t=>`${esc(t.position||'')} ${esc(t.title||'')}`).join('<br>')||'<span style="opacity:.5">no tracklist</span>'}</div>`,
cart:()=>`<button class="pv-btn big">Buy online</button>`,wishlist:()=>`<button class="pv-ghost">♡ Wishlist</button>`,
description:r=>r.notes?`<div class="pv-meta">${esc(String(r.notes).slice(0,220))}</div>`:'',
related:()=>`<div class="pv-meta" style="margin-top:14px">You may also like →</div>`,dealgod_value:()=>`<div class="pv-dg">DealGod value · cross-store market</div>`};
function renderPreview(){
const t=cfg.theme, pv=$('#pv');
pv.style.setProperty('--p',t.primary);pv.style.setProperty('--a',t.accent);pv.style.setProperty('--bg',t.bg);
pv.style.setProperty('--pn',t.panel);pv.style.setProperty('--tx',t.text);pv.style.setProperty('--r',(t.radius??12)+'px');pv.style.setProperty('--font',t.font||'system-ui');
const logo=t.logo?`<img class="pv-logo" src="${t.logo}" onerror="this.replaceWith(Object.assign(document.createElement('span'),{className:'pv-logo txt',textContent:'RecordGod'}))">`:`<span class="pv-logo txt">RecordGod</span>`;
const nav=cfg.menu.map(m=>`<a href="#">${m.label}</a>`).join('');
const r=SAMPLE[0];
const nav=cfg.menu.map(m=>`<a href="#">${esc(m.label)}</a>`).join('');
const r=PAGE||SAMPLE[0]||{};
pv.innerHTML=`<div class="lbl">Storefront preview · live</div><div class="pv-shop">
<div class="pv-head">${logo}<nav class="pv-nav">${nav}</nav><button class="pv-btn">Cart</button></div>
<div class="pv-grid" style="grid-template-columns:repeat(${t.cardCols??4},1fr)">