← back to Patterndesignlab
public/admin.html
111 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Admin · Pattern Design Lab</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<header class="top"><div class="wrap">
<a class="brand" href="/">Pattern Design<span class="dot">.</span>Lab <small>admin</small></a>
<nav class="navlinks"><a href="/">Storefront</a></nav>
</div></header>
<div class="wrap">
<div class="tabbar">
<button id="tab-designs" class="on" onclick="showTab('designs')">Designs</button>
<button id="tab-apps" onclick="showTab('apps')">Designer applications</button>
</div>
<div id="view-designs">
<div class="adminbar">
<h1 style="margin:0;font-size:22px">Design catalog</h1>
<span class="count" id="count" style="color:#6b6b6b"></span>
<div style="margin-left:auto"><label for="sort" style="font-size:12px;color:#6b6b6b">Sort</label>
<select id="sort" onchange="render()" style="height:36px;border:1px solid var(--line);border-radius:8px;padding:0 10px">
<option value="newest">Newest</option><option value="oldest">Oldest</option>
<option value="title">Title A→Z</option><option value="designer">Designer</option>
</select></div>
<div style="margin-left:14px"><label for="density" style="font-size:12px;color:#6b6b6b">Density</label>
<input id="density" type="range" min="160" max="360" step="20" value="240"
oninput="document.documentElement.style.setProperty('--admin-min',this.value+'px');localStorage.setItem('pdl.admdensity',this.value)"></div>
</div>
<div class="admgrid" id="grid"></div>
</div>
<div id="view-apps" hidden>
<div class="adminbar">
<h1 style="margin:0;font-size:22px">Designer applications</h1>
<span class="count" id="appcount" style="color:#6b6b6b"></span>
</div>
<div class="appgrid" id="appgrid"></div>
<div class="empty" id="appempty" hidden>No applications yet.</div>
</div>
</div>
<script>
let rows=[], apps=[];
function esc(s){ return (s==null?'':String(s)).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
// Steve standing rule: every admin card shows created DATE + TIME (local tz), ISO in title attr.
function fmtDate(iso){ try{ return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }catch{ return iso; } }
function showTab(t){
document.getElementById('view-designs').hidden = t!=='designs';
document.getElementById('view-apps').hidden = t!=='apps';
document.getElementById('tab-designs').classList.toggle('on', t==='designs');
document.getElementById('tab-apps').classList.toggle('on', t==='apps');
if(t==='apps' && !apps.length) loadApps();
}
async function loadApps(){
const j=await (await fetch('/api/admin/applications')).json();
apps=j.applications||[];
document.getElementById('appcount').textContent=j.count+(j.count===1?' application':' applications');
document.getElementById('appempty').hidden = j.count>0;
document.getElementById('appgrid').innerHTML = apps.map(a=>{
const linkify=v=>/^https?:\/\//i.test(v)?`<a href="${esc(v)}" target="_blank" rel="noopener noreferrer">${esc(v)}</a>`:esc(v);
const samples=(a.samples||'').split(/\n+/).filter(Boolean);
return `<div class="appcard">
<h3>${esc(a.name)}</h3>
<div class="when" title="${esc(a.created_at)}">🕓 ${fmtDate(a.created_at)}</div>
<div class="arow"><span class="k">Email</span><a href="mailto:${esc(a.email)}">${esc(a.email)}</a></div>
${a.portfolio?`<div class="arow"><span class="k">Portfolio</span>${linkify(a.portfolio)}</div>`:''}
${samples.length?`<div class="arow"><span class="k">Samples</span>${samples.map(linkify).join('<br>')}</div>`:''}
${a.about?`<div class="arow"><span class="k">About</span>${esc(a.about)}</div>`:''}
</div>`;
}).join('');
}
async function load(){ const j=await (await fetch('/api/admin/designs')).json(); rows=j.designs; document.getElementById('count').textContent=j.count+' designs'; restoreDensity(); render(); }
function restoreDensity(){ const v=localStorage.getItem('pdl.admdensity'); if(v){ document.documentElement.style.setProperty('--admin-min',v+'px'); const el=document.getElementById('density'); if(el) el.value=v; } }
function render(){
const s=document.getElementById('sort').value;
const r=[...rows];
if(s==='newest') r.sort((a,b)=>new Date(b.created_at)-new Date(a.created_at));
if(s==='oldest') r.sort((a,b)=>new Date(a.created_at)-new Date(b.created_at));
if(s==='title') r.sort((a,b)=>a.title.localeCompare(b.title));
if(s==='designer') r.sort((a,b)=>(a.designer_name||'').localeCompare(b.designer_name||''));
document.getElementById('grid').innerHTML = r.map(d=>`
<div class="acard">
<div class="t" style="background-image:url('${d.img}')"></div>
<div class="b">
<h3>${esc(d.title)}</h3>
<div class="when" title="${esc(d.created_at)}">🕓 ${fmtDate(d.created_at)}</div>
<div class="row">
<span class="tag">${esc(d.designer_name)}</span>
<span class="tag">${esc(d.style||'')}</span>
<span class="tag"><span class="swatch" style="display:inline-block;vertical-align:-1px;background:${d.dominant_hex}"></span> ${esc(d.colorway||'')}</span>
<span class="tag">${d.seamless?'seamless':'mural'}</span>
<span class="tag">${esc(d.pricing_status||'')}</span>
<span class="tag">${d.price_min!=null?'$'+d.price_min+(d.price_max&&d.price_max!=d.price_min?'–$'+d.price_max:''):'no price'}</span>
</div>
<div style="margin-top:8px"><a href="/design/${esc(d.id)}" style="color:var(--accent-ink);font-size:13px">Open →</a></div>
</div>
</div>`).join('');
}
load();
</script>
</body>
</html>