207 lines
15 KiB
HTML
207 lines
15 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>RecordGod — storefront builder</title>
|
|
<style>
|
|
:root{--pink:#ff5db1;--ink:#0b0b0d;--panel:#141418;--line:#26262c;--mut:#9a9aa6;--ok:#46d18a}
|
|
*{box-sizing:border-box}
|
|
html,body{margin:0;height:100%;background:var(--ink);color:#f0f0f2;font:14px/1.5 system-ui,sans-serif}
|
|
button{cursor:pointer;border:0;border-radius:8px;font:500 13px system-ui}
|
|
.btn{background:var(--pink);color:#1a0a14;padding:9px 14px}
|
|
.ghost{background:#1d1d22;color:#ccc;border:1px solid var(--line);padding:7px 10px}
|
|
input,select{padding:8px 9px;border:1px solid var(--line);border-radius:7px;background:#0e0e11;color:#eee;font:13px system-ui;width:100%}
|
|
#gate{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:var(--ink);z-index:20}
|
|
#gate .b{width:320px;text-align:center}#gate h1{color:var(--pink)}
|
|
#app{display:none;grid-template-rows:auto 1fr;height:100vh}
|
|
header{display:flex;align-items:center;gap:14px;padding:11px 18px;border-bottom:1px solid var(--line);background:#0e0e11}
|
|
header .t{font-weight:600}header .t b{color:var(--pink)}
|
|
header .sp{flex:1}#status{color:var(--mut);font-size:12px}
|
|
.main{display:grid;grid-template-columns:380px 1fr;overflow:hidden}
|
|
.editor{overflow:auto;border-right:1px solid var(--line);padding:16px}
|
|
.preview{overflow:auto;background:#1a1a1e;padding:24px}
|
|
.panel{border:1px solid var(--line);border-radius:10px;padding:13px;margin-bottom:14px;background:var(--panel)}
|
|
.panel h3{margin:0 0 10px;font-size:13px;font-weight:600;color:#fff}
|
|
label.l{display:block;color:var(--mut);font-size:12px;margin:9px 0 3px}
|
|
.row{display:flex;gap:8px;align-items:center}
|
|
.colors{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
|
.colorf{display:flex;gap:6px;align-items:center}.colorf input[type=color]{width:34px;height:30px;padding:0;border-radius:6px}
|
|
.chips{display:flex;flex-wrap:wrap;gap:6px}
|
|
.chip{padding:4px 9px;border-radius:16px;border:1px solid var(--line);background:#17171c;color:#bbb;font-size:12px;cursor:pointer}
|
|
.chip.on{background:#221a20;border-color:var(--pink);color:var(--pink)}
|
|
.ord{display:flex;align-items:center;gap:6px;padding:6px 8px;border:1px solid var(--line);border-radius:7px;margin:5px 0;background:#17171c}
|
|
.ord .nm{flex:1;font-size:13px}.ord button{background:none;color:var(--mut);padding:2px 5px}
|
|
.tabs{display:flex;gap:6px;margin-bottom:10px}
|
|
.tab{padding:6px 11px;border-radius:7px;background:#17171c;color:#bbb;font-size:12px}.tab.on{background:var(--pink);color:#1a0a14}
|
|
.swatch{width:22px;height:22px;border-radius:5px;cursor:pointer;border:1px solid #0006}
|
|
/* ---- preview, themed from config ---- */
|
|
.pv{--p:#ff5db1;--a:#46d18a;--bg:#0c0c0e;--pn:#141418;--tx:#f0f0f2;--r:12px;font-family:var(--font,system-ui)}
|
|
.pv-shop{background:var(--bg);color:var(--tx);border-radius:14px;overflow:hidden;border:1px solid #0006;max-width:1000px;margin:0 auto}
|
|
.pv-head{display:flex;align-items:center;gap:18px;padding:14px 20px;border-bottom:1px solid #ffffff14}
|
|
.pv-logo{height:30px;max-width:150px;object-fit:contain}.pv-logo.txt{font-weight:700;color:var(--p);font-size:20px}
|
|
.pv-nav{display:flex;gap:16px;flex:1}.pv-nav a{color:var(--tx);opacity:.85;font-size:14px;text-decoration:none}
|
|
.pv-grid{display:grid;gap:16px;padding:20px}
|
|
.pv-card{background:var(--pn);border-radius:var(--r);padding:10px;display:flex;flex-direction:column;gap:6px}
|
|
.pv-cover{width:100%;aspect-ratio:1;border-radius:calc(var(--r) - 4px)}
|
|
.pv-title{font-weight:600;font-size:14px;line-height:1.2}.pv-artist{color:#ffffff99;font-size:13px}
|
|
.pv-price{font-weight:600;color:var(--p);font-size:15px}.pv-price.big{font-size:28px}
|
|
.pv-pill{display:inline-block;padding:1px 8px;border-radius:14px;background:#ffffff1a;font-size:11px}
|
|
.pv-pill.ok{background:#46d18a22;color:var(--a)}
|
|
.pv-btn{background:var(--p);color:#160a10;padding:7px;border-radius:8px;font-weight:600;margin-top:2px}.pv-btn.big{padding:11px 22px;font-size:15px}
|
|
.pv-ghost{background:#ffffff14;color:var(--tx);padding:6px 9px;border-radius:8px}
|
|
.pv-page{display:grid;grid-template-columns:300px 1fr;gap:24px;padding:24px;border-top:1px solid #ffffff14}
|
|
.pv-bigcover{width:100%;aspect-ratio:1;border-radius:var(--r)}
|
|
.pv-h1{margin:.1em 0;font-size:30px}.pv-artist.big{font-size:18px;margin-bottom:8px}
|
|
.pv-meta{color:#ffffffaa;font-size:13px;margin:3px 0}
|
|
.pv-tracks{margin:12px 0;color:#ffffffcc;font-size:13px;line-height:1.9}
|
|
.pv-dg{margin:10px 0;padding:8px 12px;border-radius:8px;background:#46d18a18;color:var(--a);font-size:13px}
|
|
.lbl{color:var(--mut);font-size:11px;text-transform:uppercase;letter-spacing:.1em;margin:18px 0 8px;max-width:1000px;margin-inline:auto}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="gate"><div class="b"><h1>Record<b style="color:var(--pink)">God</b></h1><p style="color:var(--mut)">storefront builder</p>
|
|
<div class="row"><input id="tok" type="password" placeholder="admin token"><button class="btn" onclick="signin()">Enter</button></div>
|
|
<div id="gerr" style="color:var(--mut);font-size:12px;margin-top:8px"></div></div></div>
|
|
|
|
<div id="app">
|
|
<header>
|
|
<div class="t">Record<b>God</b> · storefront builder</div>
|
|
<div class="sp"></div>
|
|
<span id="status"></span>
|
|
<button class="ghost" onclick="resetDefaults()">Reset</button>
|
|
<button class="btn" onclick="save()">Save</button>
|
|
</header>
|
|
<div class="main">
|
|
<div class="editor" id="editor"></div>
|
|
<div class="preview"><div class="pv" id="pv"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const $=s=>document.querySelector(s);
|
|
let TOKEN=localStorage.getItem('rg_token')||'';
|
|
let cfg=null, palette=null, defaults=null, surface='card';
|
|
const hdr=()=>({'Authorization':'Bearer '+TOKEN,'Content-Type':'application/json'});
|
|
|
|
async function signin(){
|
|
TOKEN=$('#tok').value.trim();
|
|
const r=await fetch('/layout',{headers:hdr()});
|
|
if(!r.ok){$('#gerr').textContent='invalid token';return;}
|
|
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();
|
|
}
|
|
function resetDefaults(){ cfg=JSON.parse(JSON.stringify(defaults)); renderEditor(); renderPreview(); }
|
|
|
|
// ---------- editor ----------
|
|
function renderEditor(){
|
|
const th=cfg.theme;
|
|
const colorRow=(k,lbl)=>`<div class="colorf"><input type="color" value="${th[k]||'#000000'}" oninput="setTheme('${k}',this.value)"><span style="font-size:12px;color:var(--mut)">${lbl}</span></div>`;
|
|
$('#editor').innerHTML=`
|
|
<div class="panel"><h3>Brand import <span style="color:var(--mut);font-weight:400">— bam</span></h3>
|
|
<label class="l">Pull theme from a store's website</label>
|
|
<div class="row"><input id="impurl" placeholder="monsterrobot.party"><button class="ghost" onclick="importBrand()">Import</button></div>
|
|
<div id="impres" style="margin-top:8px"></div></div>
|
|
|
|
<div class="panel"><h3>Theme</h3>
|
|
<div class="colors">${colorRow('primary','primary')}${colorRow('accent','accent')}${colorRow('bg','background')}${colorRow('panel','panel')}${colorRow('text','text')}</div>
|
|
<label class="l">Logo URL</label><input value="${th.logo||''}" oninput="setTheme('logo',this.value)" placeholder="https://…/logo.png">
|
|
<div class="row" style="margin-top:8px"><div style="flex:1"><label class="l">Font</label><input value="${th.font||'system-ui'}" oninput="setTheme('font',this.value)"></div>
|
|
<div style="width:90px"><label class="l">Radius</label><input type="number" value="${th.radius??12}" oninput="setTheme('radius',+this.value)"></div>
|
|
<div style="width:90px"><label class="l">Card cols</label><input type="number" min="2" max="6" value="${th.cardCols??4}" oninput="setTheme('cardCols',+this.value)"></div></div></div>
|
|
|
|
<div class="panel"><h3>Header menu</h3><div id="menu"></div>
|
|
<button class="ghost" style="margin-top:6px" onclick="addMenu()">+ menu item</button></div>
|
|
|
|
<div class="panel"><h3>Page elements</h3>
|
|
<div class="tabs"><span class="tab ${surface==='card'?'on':''}" onclick="setSurface('card')">Item card</span><span class="tab ${surface==='product_page'?'on':''}" onclick="setSurface('product_page')">Product page</span></div>
|
|
<label class="l">Click to add / remove · arrows to reorder</label>
|
|
<div class="chips" id="chips"></div>
|
|
<div id="order" style="margin-top:10px"></div></div>`;
|
|
renderMenu(); renderChips(); renderOrder();
|
|
}
|
|
function renderMenu(){
|
|
$('#menu').innerHTML=cfg.menu.map((m,i)=>`<div class="ord">
|
|
<input style="flex:1" value="${m.label}" oninput="setMenu(${i},'label',this.value)">
|
|
<input style="flex:1" value="${m.href}" oninput="setMenu(${i},'href',this.value)">
|
|
<button onclick="moveMenu(${i},-1)">↑</button><button onclick="moveMenu(${i},1)">↓</button><button onclick="rmMenu(${i})">✕</button></div>`).join('');
|
|
}
|
|
function renderChips(){
|
|
$('#chips').innerHTML=palette[surface].map(el=>`<span class="chip ${cfg[surface].includes(el)?'on':''}" onclick="toggleEl('${el}')">${el.replace(/_/g,' ')}</span>`).join('');
|
|
}
|
|
function renderOrder(){
|
|
$('#order').innerHTML=cfg[surface].map((el,i)=>`<div class="ord"><span class="nm">${el.replace(/_/g,' ')}</span>
|
|
<button onclick="moveEl(${i},-1)">↑</button><button onclick="moveEl(${i},1)">↓</button><button onclick="toggleEl('${el}')">✕</button></div>`).join('');
|
|
}
|
|
function setSurface(s){ surface=s; renderEditor(); }
|
|
function setTheme(k,v){ cfg.theme[k]=v; renderPreview(); }
|
|
function setMenu(i,k,v){ cfg.menu[i][k]=v; renderPreview(); }
|
|
function addMenu(){ cfg.menu.push({label:'New',href:'/'}); renderMenu(); renderPreview(); }
|
|
function rmMenu(i){ cfg.menu.splice(i,1); renderMenu(); renderPreview(); }
|
|
function moveMenu(i,d){ const j=i+d; if(j<0||j>=cfg.menu.length)return; [cfg.menu[i],cfg.menu[j]]=[cfg.menu[j],cfg.menu[i]]; renderMenu(); renderPreview(); }
|
|
function toggleEl(el){ const a=cfg[surface]; const i=a.indexOf(el); if(i>=0)a.splice(i,1); else a.push(el); renderChips(); renderOrder(); renderPreview(); }
|
|
function moveEl(i,d){ const a=cfg[surface], j=i+d; if(j<0||j>=a.length)return; [a[i],a[j]]=[a[j],a[i]]; renderOrder(); renderPreview(); }
|
|
|
|
async function importBrand(){
|
|
const url=$('#impurl').value.trim(); if(!url)return;
|
|
$('#impres').innerHTML='<span style="color:var(--mut)">importing…</span>';
|
|
const r=await fetch('/layout/import-brand',{method:'POST',headers:hdr(),body:JSON.stringify({url})});
|
|
const d=await r.json();
|
|
if(!r.ok||!d.ok){ $('#impres').innerHTML='<span style="color:#e66">'+(d.detail||'failed')+'</span>'; return; }
|
|
const p=d.palette;
|
|
cfg.theme.primary=p.primary||cfg.theme.primary;
|
|
if(p.colors&&p.colors[1]) cfg.theme.accent=p.colors[1];
|
|
if(p.logo) cfg.theme.logo=p.logo;
|
|
if(p.font) cfg.theme.font=p.font;
|
|
$('#impres').innerHTML='pulled: '+(p.colors||[]).map(c=>`<span class="swatch" title="${c}" style="display:inline-block;background:${c}" onclick="setTheme('primary','${c}');renderEditor()"></span>`).join(' ')+(p.logo?' · logo ✓':'');
|
|
renderEditor(); renderPreview();
|
|
}
|
|
|
|
// ---------- 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>`,
|
|
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>`};
|
|
|
|
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];
|
|
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)">
|
|
${SAMPLE.map(rec=>`<div class="pv-card">${cfg.card.map(k=>cardEl[k]?cardEl[k](rec):'').join('')}</div>`).join('')}</div>
|
|
<div class="pv-page"><div>${cfg.product_page.filter(k=>k==='cover'||k==='gallery').map(k=>pageEl[k](r)).join('')}</div>
|
|
<div>${cfg.product_page.filter(k=>k!=='cover'&&k!=='gallery').map(k=>pageEl[k]?pageEl[k](r):'').join('')}</div></div>
|
|
</div>`;
|
|
}
|
|
|
|
async function save(){
|
|
$('#status').textContent='saving…';
|
|
const r=await fetch('/layout',{method:'POST',headers:hdr(),body:JSON.stringify({config:cfg})});
|
|
$('#status').textContent=r.ok?'saved ✓':'save failed'; setTimeout(()=>$('#status').textContent='',2000);
|
|
}
|
|
if(TOKEN){ $('#tok').value=TOKEN; signin(); }
|
|
</script>
|
|
</body>
|
|
</html>
|