← back to Rentv 2026

public/admin/publish.html

67 lines

<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>RENTV Admin — Publish</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=Playfair+Display:wght@700&display=swap" rel="stylesheet">
<style>
:root{--a:#c8102e;--bg:#0e1116;--pan:#161b22;--ln:#242b34;--sub:#8b949e;--ink:#e6edf3}
*{box-sizing:border-box;margin:0;padding:0}body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--ink);line-height:1.5}
a{color:inherit;text-decoration:none}.wrap{max-width:1180px;margin:0 auto;padding:0 22px}
.abar{background:#0a0d11;border-bottom:1px solid var(--ln)}.abar .wrap{display:flex;justify-content:space-between;align-items:center;height:52px}
.abrand{font-family:'Playfair Display',serif;font-size:20px}.abrand span{color:var(--a)}.abar a{font-size:12px;color:var(--sub)}.abar a:hover{color:var(--ink)}
.lay{display:grid;grid-template-columns:1fr 380px;gap:26px;padding:24px 0 60px}@media(max-width:900px){.lay{grid-template-columns:1fr}}
.card{background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:20px}
h1{font-size:20px;margin-bottom:4px}.mut{color:var(--sub);font-size:13px}
label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--sub);margin:14px 0 5px}
input,textarea,select{width:100%;background:var(--bg);border:1px solid var(--ln);border-radius:6px;color:var(--ink);font:inherit;font-size:14px;padding:10px 12px}
textarea{min-height:260px;resize:vertical;line-height:1.6}
.row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.btn{background:var(--a);color:#fff;border:0;border-radius:6px;font-weight:800;font-size:13px;text-transform:uppercase;letter-spacing:.05em;padding:12px 20px;cursor:pointer;margin-top:16px}
.btn.g{background:transparent;border:1px solid var(--ln);color:var(--ink);margin-left:8px}
.ok{display:none;margin-top:12px;color:#3fb950;font-size:13px}
.pl{list-style:none}.pl li{border-bottom:1px solid var(--ln);padding:12px 0}.pl h4{font-size:15px}.pl .m{font-size:11px;color:var(--sub);margin-top:3px}
.badge{font-size:10px;text-transform:uppercase;letter-spacing:.06em;padding:2px 7px;border-radius:10px;font-weight:800}
.badge.pub{background:rgba(63,185,80,.15);color:#3fb950}.badge.dr{background:rgba(139,148,158,.15);color:var(--sub)}
.pl .act{margin-top:6px;display:flex;gap:12px}.pl .act a{color:var(--a);font-size:12px;font-weight:700;cursor:pointer}.pl .act a.del{color:var(--sub)}
.hd{padding:20px 0 0}.hd h1{font-size:22px}
</style></head><body>
<div class="abar"><div class="wrap"><div class="abrand">REN<span>TV</span> Admin · Publish</div><a href="/admin">← admin</a> &nbsp; <a href="/blog">view blog →</a></div></div>
<div class="wrap"><div class="hd"><h1>Publish a RENTV Original</h1><p class="mut">Write and publish original articles. They appear on <a href="/blog" style="color:var(--a)">/blog</a> and open at /post/&lt;id&gt;.</p></div>
<div class="lay">
  <form class="card" id="f">
    <input type="hidden" id="pid">
    <label>Headline</label><input id="title" required placeholder="MG Properties Buys 188-Unit Portland Res Asset">
    <div class="row"><div><label>Section</label><input id="cat" placeholder="Multifamily"></div><div><label>Author</label><input id="author" placeholder="RENTV Staff"></div></div>
    <label>Dek (one-line summary)</label><input id="dek" placeholder="A short standfirst under the headline.">
    <label>Lead image URL (optional)</label><input id="image" placeholder="https://…/photo.jpg">
    <label>Body — separate paragraphs with a blank line</label><textarea id="body" placeholder="First paragraph…

Second paragraph…"></textarea>
    <label>Status</label><select id="status"><option value="draft">Draft</option><option value="published">Published (live on /blog)</option></select>
    <div><button class="btn" type="submit" id="save">Save post</button><button class="btn g" type="button" id="reset">New / clear</button></div>
    <div class="ok" id="ok">✓ Saved.</div>
  </form>
  <div class="card"><h1 style="font-size:16px">Existing posts <span class="mut" id="cnt"></span></h1><ul class="pl" id="list"><li class="mut">Loading…</li></ul></div>
</div></div>
<script>
var $=function(id){return document.getElementById(id);};
var esc=function(s){return (s||'').replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];});};
function load(){fetch('/api/posts').then(function(r){return r.json();}).then(function(d){
  $('cnt').textContent='· '+d.count;
  $('list').innerHTML=(d.items||[]).map(function(p){return '<li><span class="badge '+(p.status==='published'?'pub">Live':'dr">Draft')+'</span> <h4 style="display:inline;margin-left:6px">'+esc(p.title)+'</h4>'+
    '<div class="m">'+esc(p.cat)+' · '+esc(p.author)+' · '+esc((p.updated_at||'').slice(0,16).replace("T"," "))+' · '+esc(p.id)+'</div>'+
    '<div class="act"><a onclick="edit(\''+p.id+'\')">Edit</a><a onclick="pub(\''+p.id+'\',\''+(p.status==='published'?'draft':'published')+'\')">'+(p.status==='published'?'Unpublish':'Publish')+'</a><a class="del" onclick="del(\''+p.id+'\')">Delete</a> <a href="/post/'+p.id+'" target="_blank">Open ↗</a></div></li>';}).join('')||'<li class="mut">No posts yet — write your first above.</li>';
});}
window.edit=function(id){fetch('/api/posts/'+id).then(function(r){return r.json();}).then(function(p){
  $('pid').value=p.id;$('title').value=p.title||'';$('cat').value=p.cat||'';$('author').value=p.author||'';$('dek').value=p.dek||'';$('image').value=p.image||'';$('body').value=p.body||'';$('status').value=p.status||'draft';
  window.scrollTo({top:0,behavior:'smooth'});$('save').textContent='Update post';});};
window.pub=function(id,st){fetch('/api/posts/'+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({status:st})}).then(load);};
window.del=function(id){if(confirm('Delete this post?'))fetch('/api/posts/'+id,{method:'DELETE'}).then(load);};
$('reset').onclick=function(){$('f').reset();$('pid').value='';$('save').textContent='Save post';};
$('f').addEventListener('submit',function(e){e.preventDefault();
  var body={title:$('title').value,cat:$('cat').value,author:$('author').value,dek:$('dek').value,image:$('image').value,body:$('body').value,status:$('status').value};
  var id=$('pid').value,url=id?'/api/posts/'+id:'/api/posts',m=id?'PUT':'POST';
  fetch(url,{method:m,headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(function(r){return r.json();}).then(function(j){
    if(j.ok){$('ok').style.display='block';setTimeout(function(){$('ok').style.display='none';},2500);$('f').reset();$('pid').value='';$('save').textContent='Save post';load();}
    else alert(j.error||'Save failed');});});
load();
</script></body></html>