← back to Consulting Designerwallcoverings Com

public/admin/index.html

129 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Designer Wallcoverings · Growth &amp; Social Command Center</title>
<style>
  :root{--ink:#14110e;--bg:#12100e;--panel:#1c1915;--gold:#c6a765;--line:#2c2823;--txt:#e9e2d6}
  *{box-sizing:border-box}
  body{margin:0;font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--txt)}
  header{padding:16px 24px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center}
  header h1{font-size:16px;font-weight:600;letter-spacing:.08em;margin:0}
  header .eyebrow{color:var(--gold);font-size:11px;letter-spacing:.28em;text-transform:uppercase}
  header a{color:var(--gold);text-decoration:none;font-size:13px}
  .wrap{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px;padding:20px 24px}
  .panel{background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:16px 18px}
  .panel h2{font-size:13px;letter-spacing:.14em;text-transform:uppercase;color:var(--gold);margin:0 0 10px;display:flex;justify-content:space-between;align-items:center}
  .panel h2 .tag{font-size:10px;color:#8a8272;letter-spacing:.06em}
  textarea{width:100%;min-height:150px;background:#0e0c0a;color:#cfe6cf;border:1px solid var(--line);border-radius:4px;font-family:'SF Mono',ui-monospace,monospace;font-size:12px;padding:10px;resize:vertical}
  button{background:var(--gold);color:#12100e;border:0;padding:8px 16px;font-weight:600;letter-spacing:.08em;font-size:12px;border-radius:4px;cursor:pointer;margin-top:8px}
  button:hover{filter:brightness(1.08)}
  .saved{color:#7ad17a;font-size:12px;margin-left:10px}
  .note{font-size:12px;color:#8a8272;margin:0 0 8px}
</style>
</head>
<body>
  <header>
    <div><div class="eyebrow">Integrated Social · Growth</div><h1>Designer Wallcoverings — Command Center</h1></div>
    <div><a href="/portal">← Deliverable</a> &nbsp; <a href="/">Sign-in</a></div>
  </header>

  <!-- Low Hanging Fruit — reusable ranked read-only view over /api/admin/suggestions.
       ROI = impact/effort; time from effort; quick wins (low effort + high impact) flagged. -->
  <section id="lhf">
    <style>
      #lhf{margin:20px 24px 4px;background:#1c1915;border:1px solid #2c2823;border-radius:6px;padding:18px 20px}
      #lhf .lhf-h{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap;margin-bottom:2px}
      #lhf .lhf-h h2{font-size:13px;letter-spacing:.14em;text-transform:uppercase;color:#c6a765;margin:0}
      #lhf .lhf-sub{color:#8a8272;font-size:12px}
      #lhf ol{list-style:none;margin:14px 0 0;padding:0;display:grid;gap:10px}
      #lhf li{display:grid;grid-template-columns:34px 1fr;gap:12px;background:#161310;border:1px solid #2c2823;border-radius:5px;padding:12px 14px}
      #lhf li.fruit{border-color:#c6a765}
      #lhf .rk{font-size:20px;font-weight:800;color:#c6a765;line-height:1.1}
      #lhf .ti{font-weight:600;color:#e9e2d6}
      #lhf .badges{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0}
      #lhf .b{font-size:10px;letter-spacing:.05em;text-transform:uppercase;padding:3px 8px;border-radius:3px;border:1px solid #2c2823;color:#b8ad97}
      #lhf .b.roi-high,#lhf .b.time-quick{color:#7ad17a;border-color:#3a5a3a}
      #lhf .b.roi-solid{color:#c6a765}
      #lhf .b.fruit{background:#c6a765;color:#12100e;border:0;font-weight:700}
      #lhf .b.when{color:#8a8272;border-color:#2c2823;font-variant-numeric:tabular-nums}
      #lhf .rat{font-size:12.5px;color:#b8ad97;margin-top:2px}
      #lhf .why{font-size:12.5px;color:#8a8272;margin-top:4px}
      #lhf .why b{color:#c6a765;font-weight:600}
      #lhf .empty{color:#8a8272;font-size:13px}
    </style>
    <div class="lhf-h">
      <h2>⚡ Low Hanging Fruit</h2>
      <span class="lhf-sub">ranked by ROI ÷ time — fastest, highest-return moves first</span>
    </div>
    <p style="color:#8a8272;font-size:12px;margin:8px 0 0">⚡ <b style="color:#c6a765">Quick wins</b> (low effort · high impact) leverage assets you already have — activate, don’t rebuild. That is why they are simple.</p>
    <ol id="lhf-list"><li class="empty">Loading growth moves…</li></ol>
    <script>
    (function(){
      var TIME=function(e){return e<=2?['Quick','time-quick']:e===3?['Moderate','time-mod']:['Involved','time-inv'];};
      var ROI=function(i,e){var r=i/Math.max(1,e);return r>=2?['High','roi-high']:r>=1.2?['Solid','roi-solid']:['Low','roi-low'];};
      function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];});}
      // Standing rule: every admin card shows the record's created DATE AND TIME
      // (admin's local tz), full ISO in title=. Reference: /admin/design-curator fmtDate().
      function fmtDate(iso){if(!iso)return null;var d=new Date(iso);if(isNaN(d))return null;return d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}
      fetch('/api/admin/suggestions').then(function(r){return r.ok?r.json():[];}).then(function(list){
        var el=document.getElementById('lhf-list');
        list=(list||[]).filter(function(s){return s&&s.title;});
        if(!list.length){el.innerHTML='<li class="empty">No growth moves yet — add ideas in the Growth Moves panel below.</li>';return;}
        list.forEach(function(s){s._i=Number(s.impact)||0;s._e=Number(s.effort)||3;s._roi=s._i/Math.max(1,s._e);s._fruit=s._e<=2&&s._i>=4;});
        list.sort(function(a,b){return b._roi-a._roi||a._e-b._e||b._i-a._i;});
        el.innerHTML=list.map(function(s,idx){
          var t=TIME(s._e),ro=ROI(s._i,s._e);
          var when=fmtDate(s.created_at||s.createdAt||s.added_at);
          return `<li class="${s._fruit?'fruit':''}"><div class="rk">${idx+1}</div><div>`+
            `<div class="ti">${esc(s.title)}</div>`+
            `<div class="badges">${s._fruit?'<span class="b fruit">⚡ Quick win</span>':''}`+
            `<span class="b ${ro[1]}">ROI ${s._roi.toFixed(1)}</span><span class="b ${t[1]}">⏱ ${t[0]}</span>`+
            `${s.category?`<span class="b">${esc(s.category)}</span>`:''}`+
            `<span class="b">impact ${s._i} · effort ${s._e}</span>`+
            `${when?`<span class="b when" title="${esc(s.created_at||s.createdAt||s.added_at)}">🕓 ${esc(when)}</span>`:''}</div>`+
            `${s.rationale?`<div class="rat">${esc(s.rationale)}</div>`:''}</div></li>`;
        }).join('');
      }).catch(function(){document.getElementById('lhf-list').innerHTML='<li class="empty">Could not load suggestions.</li>';});
    })();
    </script>
  </section>

  <div class="wrap" id="wrap"></div>

<script>
// Each panel is a live-editable view of one data bucket. Edit the JSON, Save,
// and the server rewrites data/<bucket>.json. Script runners (competitors-scan,
// best-times, ig-pull, places-read, gen-media) repopulate these from real data.
const PANELS = [
  ['socials','Connected Accounts','Instagram · Facebook · TikTok · YouTube · LinkedIn · GBP'],
  ['best-times','Best Times to Post','per-platform posting cadence'],
  ['content-calendar','Content Calendar','planned posts / campaigns — DRAFT ONLY'],
  ['competitors','Competitor Radar','who else is in the market + their social/ads'],
  ['ads','Ad Signals','which platforms competitors advertise on'],
  ['directories','Listings & Directories','GBP · Yelp · Apple · Bing'],
  ['suggestions','Growth Moves','impact / effort ranked ideas'],
  ['media','Generated Media','reels / graphics produced for this client'],
];
const wrap = document.getElementById('wrap');
async function j(u,opt){ const r=await fetch(u,opt); return r.ok?r.json():null; }
for (const [key,title,tag] of PANELS){
  const p = document.createElement('div'); p.className='panel';
  p.innerHTML = '<h2>'+title+'<span class="tag">'+tag+'</span></h2>'
    + '<textarea id="ta-'+key+'">loading…</textarea>'
    + '<div><button data-k="'+key+'">Save '+key+'</button><span class="saved" id="ok-'+key+'"></span></div>';
  wrap.appendChild(p);
  j('/api/admin/'+key).then(d=>{ document.getElementById('ta-'+key).value = JSON.stringify(d ?? [], null, 2); });
}
wrap.addEventListener('click', async (e)=>{
  const b = e.target.closest('button[data-k]'); if(!b) return;
  const k = b.dataset.k; const ta = document.getElementById('ta-'+k); let body;
  try { body = JSON.parse(ta.value); } catch { document.getElementById('ok-'+k).textContent='✗ invalid JSON'; return; }
  const r = await fetch('/api/admin/'+k,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
  document.getElementById('ok-'+k).textContent = r.ok ? '✓ saved '+new Date().toLocaleTimeString() : '✗ error';
  setTimeout(()=>{document.getElementById('ok-'+k).textContent='';},4000);
});
</script>
</body>
</html>