← back to Wallco Ai

public/admin/etsy-bucket.html

114 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Etsy bucket — wallco.ai admin</title>
<style>
  :root{--cols:5;--bg:#0e0f10;--panel:#17191b;--line:#272b2e;--ink:#e9ecef;--mut:#9aa3ab;--etsy:#ffb86b;--ok:#7bc96f;--bad:#ff4d4d;}
  *{box-sizing:border-box;}
  body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;}
  header{position:sticky;top:0;z-index:30;background:rgba(14,15,16,.96);backdrop-filter:blur(8px);border-bottom:1px solid var(--line);padding:10px 16px;display:flex;gap:12px;align-items:center;flex-wrap:wrap;}
  h1{font-size:16px;margin:0;font-weight:650;} h1 .ico{color:var(--etsy);}
  .pill{background:var(--panel);border:1px solid var(--line);border-radius:999px;padding:3px 10px;font-size:12px;color:var(--mut);} .pill b{color:var(--ink);}
  select,input[type=range]{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:5px 8px;font-size:13px;accent-color:var(--etsy);}
  label.ctl{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--mut);}
  .grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:10px;padding:14px 16px 80px;}
  .card{background:var(--panel);border:1px solid var(--line);border-radius:9px;overflow:hidden;display:flex;flex-direction:column;position:relative;}
  .card .thumb{position:relative;aspect-ratio:1/1;background:#000;}
  .card .thumb img{width:100%;height:100%;object-fit:cover;display:block;}
  .badges{position:absolute;top:6px;left:6px;display:flex;gap:4px;z-index:3;}
  .b{padding:2px 7px;font:700 10.5px sans-serif;border-radius:3px;background:rgba(0,0,0,.7);color:#fff;}
  .b.q{background:#5a3f17;color:var(--etsy);} .b.exp{background:#3b2e08;color:#f0b400;} .b.list{background:#143b1d;color:var(--ok);}
  .meta{padding:7px 9px;font-size:11.5px;display:flex;justify-content:space-between;}
  .meta .id{color:var(--mut);font-variant-numeric:tabular-nums;}
  .meta .dpi{color:var(--etsy);font-weight:700;}
  .when{padding:0 9px 8px;font-size:10.5px;color:var(--mut);font-variant-numeric:tabular-nums;}
  .rm{position:absolute;bottom:6px;right:6px;background:rgba(0,0,0,.75);color:#ff9a9a;border:1px solid #5a2222;border-radius:5px;font:700 10px sans-serif;padding:3px 7px;cursor:pointer;z-index:4;}
  .rm:hover{background:var(--bad);color:#fff;}
  #empty,#err{padding:60px 16px;text-align:center;color:var(--mut);} #err{color:#ff9a9a;}
  .hero{padding:14px 16px;background:#15171a;border-bottom:1px solid var(--line);display:flex;gap:14px;font-size:13px;color:var(--mut);}
  .hero b{color:var(--ink);}
</style>
</head>
<body>
<header>
  <h1><span class="ico">🛒</span> Etsy bucket</h1>
  <span class="pill" id="stat">loading…</span>
  <label class="ctl">State <select id="state"><option value="">all</option><option value="queued">queued</option><option value="exported">exported</option><option value="listed">listed</option><option value="sold">sold</option></select></label>
  <label class="ctl">Sort <select id="sort"><option value="newest">Newest</option><option value="oldest">Oldest</option><option value="id_desc">ID ↓</option><option value="id">ID ↑</option><option value="dpi">DPI ↓</option></select></label>
  <label class="ctl">Density <input type="range" id="density" min="3" max="8" step="1" value="5"></label>
  <a href="javascript:void(0)" id="explink" style="margin-left:auto;color:var(--etsy);font-size:13px;text-decoration:none;border:1px solid #5a3f17;padding:5px 10px;border-radius:7px">📦 Run export script</a>
</header>
<div class="hero">
  <span><b>How it works:</b> click 🛒 Etsy in any curator to send a design here. Bucket items are excluded from new-design pools. Run <code style="background:#0a0b0c;padding:2px 6px;border-radius:4px">node scripts/export-etsy-digital.js --limit 0</code> to bundle assets + listing copy.</span>
</div>
<div id="err" style="display:none"></div>
<div id="empty" style="display:none">Bucket is empty — pick designs in the curator and hit 🛒 Etsy.</div>
<div class="grid" id="grid"></div>
<script>
const ADMIN=new URLSearchParams(location.search).get('admin');
// q(path) — build a fetchable URL. Anchors '/'-prefixed paths to location.origin
// so URLs are credentials-free; the Fetch spec rejects Request construction
// when the resolved URL contains userinfo (happens when the page was loaded
// via http://admin:pw@host/...). Appends ?admin=<token> when present.
const q = s => {
  const path = s.startsWith('/') ? location.origin + s : s;
  return ADMIN ? path + (path.includes('?') ? '&' : '?') + 'admin=' + encodeURIComponent(ADMIN) : path;
};
const $=id=>document.getElementById(id);
$('density').value=+(localStorage.getItem('etsy_density')||5); document.documentElement.style.setProperty('--cols',$('density').value);
$('density').addEventListener('input',e=>{localStorage.setItem('etsy_density',e.target.value);document.documentElement.style.setProperty('--cols',e.target.value);});
$('state').addEventListener('change',load);
// Sort persists (Steve standing rule); client-side over the loaded bucket — no refetch.
$('sort').value=localStorage.getItem('etsy_sort')||'newest';
$('sort').addEventListener('change',e=>{localStorage.setItem('etsy_sort',e.target.value);render();});
function fmtDate(s){ if(!s) return '—'; const d=new Date(s); return isNaN(d)?String(s):d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }
function sortItems(rows){
  const r=rows.slice(), m=$('sort').value;
  if(m==='newest')      r.sort((a,b)=>String(b.added_at||'').localeCompare(String(a.added_at||'')));
  else if(m==='oldest') r.sort((a,b)=>String(a.added_at||'').localeCompare(String(b.added_at||'')));
  else if(m==='id_desc')r.sort((a,b)=>b.design_id-a.design_id);
  else if(m==='id')     r.sort((a,b)=>a.design_id-b.design_id);
  else if(m==='dpi')    r.sort((a,b)=>(b.target_dpi||0)-(a.target_dpi||0)||(b.design_id-a.design_id));
  return r;
}
$('explink').addEventListener('click',()=>{
  alert('From your terminal:\n\n  cd ~/Projects/wallco-ai\n  node scripts/export-etsy-digital.js --limit 0\n\nThis writes per-design assets + a listings.csv manifest to data/etsy-exports/<today>/. After upload, designs auto-progress from queued → exported.');
});
let ALL=[];
async function rm(id){
  if(!confirm(`Remove #${id} from Etsy bucket? It'll be eligible for the new-design pool again.`)) return;
  const r=await fetch(q('/api/etsy-bucket/'+id),{method:'DELETE'});
  if(r.ok){ ALL=ALL.filter(x=>x.design_id!==id); render(); }
}
function render(){
  $('empty').style.display=ALL.length?'none':'block';
  $('stat').innerHTML=`<b>${ALL.length}</b> in bucket`;
  $('grid').innerHTML=sortItems(ALL).map(x=>`
    <div class="card">
      <div class="thumb">
        <div class="badges"><span class="b ${({queued:'q',exported:'exp',listed:'list'})[x.state]||''}">${x.state}</span></div>
        <a href="${q('/design/'+x.design_id)}" target="_blank" rel="noopener noreferrer"><img loading="lazy" src="/designs/img/by-id/${x.design_id}" alt="#${x.design_id}"></a>
        <button class="rm" onclick="rm(${x.design_id})">✕ remove</button>
      </div>
      <div class="meta"><span class="id">#${x.design_id}</span><span class="dpi">${x.target_dpi}dpi</span></div>
      <div class="when" title="${x.added_at} added">🕓 ${fmtDate(x.added_at)}</div>
    </div>`).join('');
}
async function load(){
  try{
    const s=$('state').value;
    const r=await fetch(q('/api/etsy-bucket/list'+(s?'?state='+s:'')));
    if(!r.ok) throw new Error('list '+r.status+(r.status===404?' — append ?admin=TOKEN':''));
    const j=await r.json();
    ALL=j.items||[];
    render();
  }catch(e){ $('err').style.display='block'; $('err').textContent='Failed: '+e.message; }
}
load();
</script>
  <script src="/admin/admin-modal-kit.js" defer></script>
</body>
</html>