← back to Norma

agents/instagram-agent/public/notes-viewer.html

152 lines

<!doctype html><html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Instagram — What to Post Next (per account)</title>
<style>
:root{
  --bg:#0c0f1a;--s:#131829;--e:#1c2237;--t:#f0f0f5;--m:#a1a1b5;--mut:#6b7280;
  --p:#10b981;--ph:#059669;--r:#f43f5e;--amber:#f59e0b;--bd:#2a3048;--card:340px;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--t);font:15px/1.45 -apple-system,system-ui,Segoe UI,sans-serif}
a{color:var(--p);text-decoration:none}
header{position:sticky;top:0;z-index:20;background:var(--s);border-bottom:1px solid var(--bd);padding:12px 16px;display:flex;gap:12px;align-items:center;flex-wrap:wrap}
h1{font-size:16px;margin:0;font-weight:650;display:flex;gap:8px;align-items:center}
.sub{color:var(--m);font-size:12.5px}
.grow{flex:1}
button,input,textarea,select{font-family:inherit}
input{background:var(--e);color:var(--t);border:1px solid var(--bd);border-radius:8px;padding:7px 10px;font-size:13px;min-width:200px}
input:focus{outline:none;border-color:var(--p)}
.btn{background:var(--e);color:var(--t);border:1px solid var(--bd);border-radius:8px;padding:7px 12px;font-size:12.5px;cursor:pointer}
.btn:hover{border-color:var(--p)}
.seg{display:inline-flex;border:1px solid var(--bd);border-radius:8px;overflow:hidden}
.seg button{background:var(--e);color:var(--m);border:0;padding:7px 11px;font-size:12.5px;cursor:pointer}
.seg button.on{background:var(--p);color:#04120c;font-weight:650}
main{padding:14px 16px 80px}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--card),1fr));gap:14px}
.card{background:var(--s);border:1px solid var(--bd);border-radius:12px;padding:12px;display:flex;flex-direction:column;gap:9px}
.card.hide{display:none}
.chead{display:flex;align-items:center;gap:8px}
.dot{width:12px;height:12px;border-radius:50%;flex:none}
.h{font-weight:650;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pn{color:var(--mut);font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.card textarea{width:100%;min-height:104px;resize:vertical;background:var(--e);color:var(--t);border:1px solid var(--bd);border-radius:9px;padding:9px 10px;font-size:13px;line-height:1.5}
.card textarea:focus{outline:none;border-color:var(--p)}
.card textarea.dirty{border-color:var(--amber)}
.foot{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--mut);min-height:16px}
.when{display:flex;align-items:center;gap:5px}
.status{margin-left:auto}
.status.saving{color:var(--amber)}
.status.saved{color:var(--p)}
.status.err{color:var(--r)}
.card .lv{color:var(--m);font-size:11.5px}
.filled .h::after{content:"📝";font-size:11px;margin-left:6px}
.toast{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:var(--e);border:1px solid var(--bd);border-radius:10px;padding:10px 16px;font-size:13px;z-index:50;display:none}
.toast.show{display:block}.toast.err{border-color:var(--r);color:#ffd7de}
</style></head>
<body>
<header>
  <h1>📝 What to Post Next <span class="sub">— per account, from DW Shopify</span></h1>
  <input id="q" placeholder="Filter accounts…">
  <div class="seg" id="filt">
    <button data-f="all" class="on">All</button>
    <button data-f="with">With notes</button>
    <button data-f="without">Empty</button>
  </div>
  <span class="grow"></span>
  <span class="sub" id="summary">loading…</span>
  <a class="btn" href="/calendar">📅 Calendar</a>
  <a class="btn" href="/live">📸 List</a>
</header>

<main><div class="grid" id="grid"></div></main>
<div class="toast" id="toast"></div>

<script>
const $=(s)=>document.querySelector(s);
let ACCOUNTS=[];
function color(i){ return `hsl(${Math.round((i*137.508)%360)} 66% 60%)`; }
function toast(m,err){ const t=$('#toast'); t.textContent=m; t.className='toast show'+(err?' err':''); clearTimeout(t._h); t._h=setTimeout(()=>t.className='toast',2400); }
function fmtWhen(iso){
  if(!iso) return 'never edited';
  return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});
}

async function load(){
  let d;
  try{ d=await (await fetch('/api/live/account-notes')).json(); if(d.error) throw new Error(d.error); }
  catch(e){ toast('Load failed: '+e.message,true); return; }
  ACCOUNTS=d.accounts.map((a,i)=>({...a,color:color(i)}));
  render();
  updateSummary();
}
function updateSummary(){
  const withN=ACCOUNTS.filter(a=>a.note && a.note.trim()).length;
  $('#summary').textContent=`${withN}/${ACCOUNTS.length} accounts have a post plan`;
}

function render(){
  const grid=$('#grid'); grid.innerHTML='';
  for(const a of ACCOUNTS){
    const card=document.createElement('div');
    card.className='card'+((a.note&&a.note.trim())?' filled':'');
    card.dataset.handle=a.handle.toLowerCase();
    card.dataset.has=(a.note&&a.note.trim())?'1':'0';
    card.innerHTML=`
      <div class="chead">
        <span class="dot" style="background:${a.color}"></span>
        <span class="h">@${a.handle}</span>
        <a class="lv" href="/live?account=${a.ig_user_id}" title="Open this account's posts" style="margin-left:auto">posts ↗</a>
      </div>
      ${a.page_name?`<div class="pn">${a.page_name}</div>`:''}
      <textarea placeholder="What should @${a.handle} post next from the DW Shopify store? e.g. product handles, a collection, a theme, timing…">${(a.note||'').replace(/</g,'&lt;')}</textarea>
      <div class="foot">
        <span class="when">🕓 <span class="wt">${fmtWhen(a.updated_at)}</span></span>
        <span class="status"></span>
      </div>`;
    const ta=card.querySelector('textarea');
    const status=card.querySelector('.status');
    const wt=card.querySelector('.wt');
    let saved=a.note||'';
    let timer=null;
    const save=async()=>{
      const val=ta.value;
      if(val===saved){ ta.classList.remove('dirty'); status.className='status'; status.textContent=''; return; }
      status.className='status saving'; status.textContent='saving…';
      try{
        const r=await fetch('/api/live/account-notes',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({account:a.ig_user_id,note:val})});
        const j=await r.json(); if(!r.ok||j.error) throw new Error(j.error||('HTTP '+r.status));
        saved=val; a.note=val; a.updated_at=j.updated_at;
        ta.classList.remove('dirty');
        status.className='status saved'; status.textContent='✓ saved';
        wt.textContent=fmtWhen(j.updated_at);
        card.classList.toggle('filled', !!(val&&val.trim()));
        card.dataset.has=(val&&val.trim())?'1':'0';
        updateSummary(); applyFilter();
        setTimeout(()=>{ if(status.textContent==='✓ saved'){ status.className='status'; status.textContent=''; } },1500);
      }catch(e){ status.className='status err'; status.textContent='save failed'; toast('Save failed: '+e.message,true); }
    };
    ta.addEventListener('input',()=>{ ta.classList.toggle('dirty', ta.value!==saved); clearTimeout(timer); timer=setTimeout(save,900); });
    ta.addEventListener('blur',()=>{ clearTimeout(timer); save(); });
    grid.appendChild(card);
  }
  applyFilter();
}

let curFilter='all';
function applyFilter(){
  const q=($('#q').value||'').toLowerCase().trim();
  document.querySelectorAll('.card').forEach(c=>{
    const matchQ=!q||c.dataset.handle.includes(q);
    const has=c.dataset.has==='1';
    const matchF=curFilter==='all'||(curFilter==='with'&&has)||(curFilter==='without'&&!has);
    c.classList.toggle('hide',!(matchQ&&matchF));
  });
}
$('#q').addEventListener('input',applyFilter);
$('#filt').addEventListener('click',(e)=>{ const b=e.target.closest('button'); if(!b)return;
  document.querySelectorAll('#filt button').forEach(x=>x.classList.remove('on')); b.classList.add('on'); curFilter=b.dataset.f; applyFilter(); });

load();
</script>
</body></html>