← back to Rentv
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
M public/admin/pr-intelligence/content-studio.html
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
·
auto-save: 2026-08-04T19:05:58 (3 files) — data/markets.json 7cbb2d95 →