← back to Wallco Ai

public/admin/workspace.html

203 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Admin Workspace — wallco.ai</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  :root{--bg:#0f1117;--ink:#e8eaf2;--mut:#8b93a7;--line:#262b3a;--card:#161a26;--accent:#34cbf4;}
  *{box-sizing:border-box}
  html,body{margin:0;height:100%;background:
    radial-gradient(1200px 600px at 20% -10%,#1a2030,transparent),var(--bg);
    color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;overflow:hidden}
  header.ws{position:fixed;top:0;left:0;right:0;height:52px;z-index:50;display:flex;align-items:center;
    gap:12px;padding:0 16px;background:rgba(15,17,23,.92);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
  h1{font-size:15px;margin:0;font-weight:650;letter-spacing:.2px}
  .sub{color:var(--mut);font-size:12px}
  button,select{font:inherit;color:var(--ink);background:#1c2030;border:1px solid var(--line);
    border-radius:8px;padding:7px 12px;cursor:pointer}
  button.primary{background:var(--accent);color:#06202a;border-color:var(--accent);font-weight:650}
  button:hover{border-color:var(--accent)}
  .spacer{flex:1}
  main{position:fixed;inset:52px 0 0 0;overflow:hidden}
  .empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;
    color:var(--mut);gap:14px;text-align:center;padding:24px}
  .empty h2{color:var(--ink);font-weight:600;margin:0}
  /* Windows */
  .win{position:absolute;background:var(--card);border:1px solid var(--line);border-radius:12px;
    box-shadow:0 22px 70px rgba(0,0,0,.5);display:flex;flex-direction:column;overflow:hidden;min-width:280px;min-height:180px}
  .win.focus{border-color:var(--accent);box-shadow:0 22px 80px rgba(52,203,244,.18),0 22px 70px rgba(0,0,0,.5)}
  .win-bar{display:flex;align-items:center;gap:8px;padding:7px 9px;cursor:grab;user-select:none;
    background:linear-gradient(#1c2030,#141826);border-bottom:1px solid var(--line)}
  .win-bar:active{cursor:grabbing}
  .win-grip{opacity:.6;letter-spacing:-2px}
  .win-title{font-weight:600;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
  .win-btn{border:0;background:rgba(255,255,255,.10);color:var(--ink);width:24px;height:22px;border-radius:6px;
    cursor:pointer;font-size:12px;line-height:1;padding:0}
  .win-btn:hover{background:rgba(255,255,255,.22)}
  .win-body{flex:1;position:relative;background:#fff}
  .win-body iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:#fff}
  .win-resize{position:absolute;right:2px;bottom:2px;width:16px;height:16px;cursor:nwse-resize;
    background:linear-gradient(135deg,transparent 50%,var(--mut) 50%);border-radius:0 0 8px 0;opacity:.7}
  .win.min .win-body,.win.min .win-resize{display:none}
  .win.min{height:auto!important;min-height:0}
  .chip{font-size:11px;color:var(--mut);border:1px solid var(--line);border-radius:999px;padding:3px 9px}
</style>
</head>
<body>
<header class="ws">
  <h1>🪟 Admin Workspace</h1>
  <span class="sub" id="count">0 panels</span>
  <span class="spacer"></span>
  <select id="addSel" title="Pick a grid/page to open as a panel">
    <option value="">+ Add panel…</option>
  </select>
  <button id="tileBtn" title="Auto-arrange all panels in a tidy grid">▦ Tile</button>
  <button id="cascadeBtn" title="Cascade panels diagonally">▧ Cascade</button>
  <button id="clearBtn" title="Close all panels">Clear</button>
</header>
<main id="stage">
  <div class="empty" id="empty">
    <h2>Empty workspace</h2>
    <div class="sub">Use <strong>+ Add panel</strong> to open any admin grid as a movable, resizable window.<br>
      Drag the title bar to move · drag the corner to resize · layout auto-saves.</div>
  </div>
</main>

<script>
// ---- Catalog of openable admin grids/pages ----
const PAGES = [
  ['Design Curator',        '/admin/design-curator'],
  ['Drunk Curator',         '/admin/drunk-curator'],
  ['Dogs Curator',          '/admin/dogs-curator'],
  ['Botanical Curator',     '/admin/botanical-wonders-curator'],
  ['Ghost Review',          '/admin/ghost-review'],
  ['Edges Review',          '/admin/edges-review'],
  ['Bleed Review',          '/admin/bleed-review'],
  ['Defect Registry',       '/admin/defect-registry'],
  ['Rooms Gallery',         '/admin/rooms'],
  ['Theme Gallery',         '/admin/theme-gallery'],
  ['Elements',              '/admin/elements'],
  ['Inspirations',          '/admin/inspirations'],
  ['Etsy Bucket',           '/admin/etsy-bucket'],
  ['Status Browser',        '/admin/status-browser'],
  ['Designs grid (public)', '/designs'],
];
const STORE = 'ws-admin-layout-v1';
const $ = s => document.querySelector(s);
const stage = $('#stage');
let z = 10, wins = [];

PAGES.forEach(([label,url]) => {
  const o = document.createElement('option'); o.value = url; o.textContent = label; $('#addSel').appendChild(o);
});

function save(){
  const data = wins.map(w => ({
    url:w.dataset.url, title:w.dataset.title,
    left:parseInt(w.style.left), top:parseInt(w.style.top),
    w:parseInt(w.style.width), h:parseInt(w.style.height),
    min:w.classList.contains('min'), z:+w.style.zIndex||10
  }));
  try{ localStorage.setItem(STORE, JSON.stringify(data)); }catch(e){}
  $('#count').textContent = wins.length + (wins.length===1?' panel':' panels');
  $('#empty').style.display = wins.length ? 'none' : '';
}

function focusWin(w){ wins.forEach(x=>x.classList.remove('focus')); w.classList.add('focus'); w.style.zIndex = ++z; }

function makeWin(opt){
  const w = document.createElement('div'); w.className='win';
  w.dataset.url = opt.url; w.dataset.title = opt.title || labelFor(opt.url);
  w.style.left=(opt.left??(40+wins.length*28))+'px';
  w.style.top =(opt.top ??(40+wins.length*28))+'px';
  w.style.width =(opt.w??560)+'px';
  w.style.height=(opt.h??460)+'px';
  w.style.zIndex = opt.z || (++z);
  w.innerHTML =
    '<div class="win-bar">'+
      '<span class="win-grip">⠿</span>'+
      '<span class="win-title"></span>'+
      '<a class="win-btn" title="Open full page" target="_blank" rel="noopener" href="'+esc(opt.url)+'">↗</a>'+
      '<button class="win-btn b-reload" title="Reload">⟳</button>'+
      '<button class="win-btn b-min" title="Collapse">—</button>'+
      '<button class="win-btn b-close" title="Close">✕</button>'+
    '</div>'+
    '<div class="win-body"><iframe src="'+esc(opt.url)+'" loading="lazy" referrerpolicy="same-origin"></iframe></div>'+
    '<div class="win-resize" title="Drag to resize"></div>';
  w.querySelector('.win-title').textContent = w.dataset.title;
  if (opt.min) w.classList.add('min');
  stage.appendChild(w); wins.push(w);

  const bar = w.querySelector('.win-bar');
  // drag
  let dx,dy,ox,oy,drag=false;
  bar.addEventListener('pointerdown', e=>{
    if(e.target.closest('.win-btn')) return;
    drag=true; focusWin(w); dx=e.clientX; dy=e.clientY; ox=parseInt(w.style.left); oy=parseInt(w.style.top);
    bar.setPointerCapture(e.pointerId); e.preventDefault();
  });
  bar.addEventListener('pointermove', e=>{
    if(!drag) return;
    let nl=ox+(e.clientX-dx), nt=oy+(e.clientY-dy);
    nl=Math.max(2-w.offsetWidth+80, Math.min(nl, innerWidth-60));
    nt=Math.max(0, Math.min(nt, innerHeight-90));
    w.style.left=nl+'px'; w.style.top=nt+'px';
  });
  bar.addEventListener('pointerup', ()=>{ if(drag){drag=false; save();} });
  // resize
  const rz=w.querySelector('.win-resize'); let rw,rh,rx,ry,res=false;
  rz.addEventListener('pointerdown', e=>{
    res=true; focusWin(w); rx=e.clientX; ry=e.clientY; rw=w.offsetWidth; rh=w.offsetHeight;
    rz.setPointerCapture(e.pointerId); e.preventDefault();
  });
  rz.addEventListener('pointermove', e=>{
    if(!res) return;
    w.style.width =Math.max(280, rw+(e.clientX-rx))+'px';
    w.style.height=Math.max(180, rh+(e.clientY-ry))+'px';
  });
  rz.addEventListener('pointerup', ()=>{ if(res){res=false; save();} });
  // focus on any click
  w.addEventListener('pointerdown', ()=>focusWin(w), true);
  // buttons
  w.querySelector('.b-reload').addEventListener('click', ()=>{ const f=w.querySelector('iframe'); f.src=f.src; });
  w.querySelector('.b-min').addEventListener('click', ()=>{ w.classList.toggle('min'); save(); });
  w.querySelector('.b-close').addEventListener('click', ()=>{ w.remove(); wins=wins.filter(x=>x!==w); save(); });

  focusWin(w); save();
  return w;
}

function labelFor(url){ const p = PAGES.find(x=>x[1]===url); return p?p[0]:url; }
function esc(s){ return String(s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }

// tile / cascade
function tile(){
  const n=wins.length; if(!n) return;
  const cols=Math.ceil(Math.sqrt(n)), rows=Math.ceil(n/cols);
  const W=innerWidth, H=innerHeight-52, gw=W/cols, gh=H/rows;
  wins.forEach((w,i)=>{ const c=i%cols, r=Math.floor(i/cols);
    w.classList.remove('min');
    w.style.left=Math.round(c*gw+4)+'px'; w.style.top=Math.round(r*gh+4)+'px';
    w.style.width=Math.round(gw-8)+'px'; w.style.height=Math.round(gh-8)+'px'; });
  save();
}
function cascade(){ wins.forEach((w,i)=>{ w.classList.remove('min');
  w.style.left=(40+i*30)+'px'; w.style.top=(40+i*30)+'px'; w.style.width='560px'; w.style.height='460px';
  w.style.zIndex=++z; }); save(); }

$('#addSel').addEventListener('change', e=>{ const url=e.target.value; if(url){ makeWin({url}); e.target.value=''; } });
$('#tileBtn').addEventListener('click', tile);
$('#cascadeBtn').addEventListener('click', cascade);
$('#clearBtn').addEventListener('click', ()=>{ if(wins.length && confirm('Close all panels?')){ wins.forEach(w=>w.remove()); wins=[]; save(); } });

// restore
(function init(){
  let data=[]; try{ data=JSON.parse(localStorage.getItem(STORE)||'[]'); }catch(e){}
  data.sort((a,b)=>(a.z||0)-(b.z||0)).forEach(makeWin);
  save();
})();
</script>
</body>
</html>