← 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 & 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> <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 {'&':'&','<':'<','>':'>','"':'"'}[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>