[object Object]

← back to Rentv 2026

Content Studio UX: feed category filter + copy-to-clipboard on blog/social (Cody-gated, HTTPS guard)

8d7917c18a433af07c26e12f5538e03604e88c87 · 2026-08-04 18:42:29 -0700 · steve

Files touched

Diff

commit 8d7917c18a433af07c26e12f5538e03604e88c87
Author: steve <steve@designerwallcoverings.com>
Date:   Tue Aug 4 18:42:29 2026 -0700

    Content Studio UX: feed category filter + copy-to-clipboard on blog/social (Cody-gated, HTTPS guard)
---
 public/admin/pr-intelligence/content-studio.html | 26 ++++++++++++++++++++----
 1 file changed, 22 insertions(+), 4 deletions(-)

diff --git a/public/admin/pr-intelligence/content-studio.html b/public/admin/pr-intelligence/content-studio.html
index e7d57a12..6edab7f0 100644
--- a/public/admin/pr-intelligence/content-studio.html
+++ b/public/admin/pr-intelligence/content-studio.html
@@ -26,6 +26,8 @@
   .cc{font-size:11px;color:var(--muted,#5b636b);margin-top:4px}
   .hl{margin:0;padding:0;list-style:none} .hl li{position:relative;padding:6px 0 6px 20px;font-size:13px;line-height:1.45;border-bottom:1px solid var(--line,#eee)}
   .hl li:last-child{border-bottom:0} .hl li::before{content:"▸";position:absolute;left:2px;color:#0a66c2}
+  .cs-copy{font-size:11px;font-weight:600;color:#0a66c2;text-decoration:none;margin-left:8px;cursor:pointer}
+  .cs-copy:hover{text-decoration:underline}
   .spin{display:inline-block;width:13px;height:13px;border:2px solid #c9d3dd;border-top-color:#0a6cff;border-radius:50%;animation:sp .7s linear infinite;vertical-align:-2px}
   @keyframes sp{to{transform:rotate(360deg)}}
 </style>
@@ -53,6 +55,7 @@
     <h3>News feed <span class="cs-meta" id="ncount"></span></h3>
     <div class="sub" id="nsub">Newest first. Deepens as the archive back-fills.</div>
     <input id="q" placeholder="Filter headlines…" style="width:100%;box-sizing:border-box;font:inherit;font-size:13px;padding:7px 10px;border:1px solid var(--line,#d7dde3);border-radius:8px;margin-bottom:6px">
+    <select id="catf" style="width:100%;box-sizing:border-box;font:inherit;font-size:12.5px;padding:6px 9px;border:1px solid var(--line,#d7dde3);border-radius:8px;margin:0 0 6px"><option value="">All categories</option></select>
     <label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted,#5b636b);margin:0 0 8px;cursor:pointer"><input type="checkbox" id="showdups"> Show all (incl. cross-source duplicates)</label>
     <div class="cs-list" id="list"></div>
   </div>
@@ -83,7 +86,8 @@
   const listEl = document.getElementById('list'), qEl = document.getElementById('q');
   function paintList() {
     const q = qEl.value.trim().toLowerCase();
-    const rows = articles.filter(a => !q || (a.title || '').toLowerCase().includes(q));
+    const cf = document.getElementById('catf').value;
+    const rows = articles.filter(a => (!q || (a.title || '').toLowerCase().includes(q)) && (!cf || (a.cat || 'RENTV') === cf));
     listEl.innerHTML = rows.length ? rows.map(a => {
       const img = a.image ? (a.image.startsWith('http') ? a.image : location.origin + a.image) : '';
       return `<div class="cs-art${selected && String(selected.id) === String(a.id) ? ' on' : ''}" data-id="${PR.esc(a.id)}" title="${PR.esc(a.first_seen || a.date || '')}">
@@ -93,6 +97,9 @@
     }).join('') : '<div class="empty">No matches</div>';
     listEl.querySelectorAll('[data-id]').forEach(el => el.addEventListener('click', () => selectArticle(el.dataset.id)));
   }
+  const catf = document.getElementById('catf');
+  ['Sales', 'Leases', 'Financing', 'Development', 'Multifamily', 'Office', 'Retail', 'Industrial', 'People', 'Markets'].forEach(x => { const o = document.createElement('option'); o.value = x; o.textContent = x; catf.appendChild(o); });
+  catf.addEventListener('change', paintList);
   qEl.addEventListener('input', paintList); paintList();
   document.getElementById('showdups').addEventListener('change', async (e) => {
     await loadFeed(!e.target.checked); // checked = show raw (dedup off)
@@ -132,7 +139,7 @@
       </div>
       <div class="cs-field"><label>Standfirst (dek)</label><input id="b_dek"></div>
       <div class="cs-field"><label>Image URL</label><input id="b_image"></div>
-      <div class="cs-field"><label>Body</label><textarea id="b_body" class="cs-body"></textarea></div>
+      <div class="cs-field"><label>Body <a class="cs-copy" data-copy="b_body">📋 copy</a></label><textarea id="b_body" class="cs-body"></textarea></div>
       <div class="cs-row">
         <button class="btn" id="publish">🚀 Publish to blog</button>
         <span class="cs-meta" id="pubmeta"></span>
@@ -140,9 +147,9 @@
     </div>
     <div class="cs-card" id="socialCard" style="display:none">
       <h3>Social</h3><div class="sub">Saves to social drafts (feeds the LinkedIn Posting composer). Nothing auto-posts.</div>
-      <div class="cs-field"><label>LinkedIn</label><textarea id="s_li" class="cs-social"></textarea>
+      <div class="cs-field"><label>LinkedIn <a class="cs-copy" data-copy="s_li">📋 copy</a></label><textarea id="s_li" class="cs-social"></textarea>
         <div class="cs-row" style="margin-top:6px"><button class="btn sm" id="save_li">💾 Save LinkedIn draft</button><span class="cs-meta" id="li_meta"></span></div></div>
-      <div class="cs-field"><label>X / Twitter</label><textarea id="s_x" class="cs-social"></textarea>
+      <div class="cs-field"><label>X / Twitter <a class="cs-copy" data-copy="s_x">📋 copy</a></label><textarea id="s_x" class="cs-social"></textarea>
         <div class="cs-row" style="margin-top:6px"><button class="btn sm" id="save_x">💾 Save X draft</button><span class="cs-meta" id="x_meta"></span></div></div>
     </div>`;
 
@@ -224,6 +231,17 @@
   }
   $('save_li').addEventListener('click', () => saveDraft('LinkedIn', 's_li', 'li_meta'));
   $('save_x').addEventListener('click', () => saveDraft('X', 's_x', 'x_meta'));
+
+  // Copy-to-clipboard on blog body + social drafts (the hand-off step).
+  document.addEventListener('click', (e) => {
+    const a = e.target.closest('.cs-copy'); if (!a) return;
+    e.preventDefault();
+    const el = document.getElementById(a.getAttribute('data-copy')); if (!el) return;
+    if (!navigator.clipboard) { PR.toast('copy needs HTTPS', true); return; } // undefined on insecure origins
+    navigator.clipboard.writeText(el.value || '').then(() => {
+      const o = a.textContent; a.textContent = '✓ copied'; setTimeout(() => { a.textContent = o; }, 1400);
+    }).catch(() => PR.toast('copy failed', true));
+  });
 })();
 </script>
 </body>

← 657151c6 auto-save: 2026-08-04T18:35:48 (3 files) — data/markets.json  ·  back to Rentv 2026  ·  auto-save: 2026-08-04T19:05:58 (3 files) — data/markets.json 7cbb2d95 →