← back to Rentv 2026
feat(services): 'Solicit all visible' bulk action (admin)
0250fee38c8eb0ed95c9015d4c95460bc8d6c77d · 2026-08-06 13:21:18 -0700 · Steve
Adds a one-click 'Solicit all visible' button (admin-only, in the control bar) that
queues every firm currently loaded on screen into the outreach basket at once —
deduped, respecting the current category/metro/search/city filter and any Load-More
pages already expanded. Refactored the single-Solicit path into a shared
addToOutreach() helper; BYID is cleared on each reset so 'visible' means exactly the
current view; button shows an 'Added N (M visible)' confirmation.
Files touched
Diff
commit 0250fee38c8eb0ed95c9015d4c95460bc8d6c77d
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Aug 6 13:21:18 2026 -0700
feat(services): 'Solicit all visible' bulk action (admin)
Adds a one-click 'Solicit all visible' button (admin-only, in the control bar) that
queues every firm currently loaded on screen into the outreach basket at once —
deduped, respecting the current category/metro/search/city filter and any Load-More
pages already expanded. Refactored the single-Solicit path into a shared
addToOutreach() helper; BYID is cleared on each reset so 'visible' means exactly the
current view; button shows an 'Added N (M visible)' confirmation.
---
public/services.html | 36 +++++++++++++++++++++++++-----------
1 file changed, 25 insertions(+), 11 deletions(-)
diff --git a/public/services.html b/public/services.html
index 87c9e5ed..0c086b0d 100644
--- a/public/services.html
+++ b/public/services.html
@@ -99,6 +99,8 @@
.obasket.show{display:inline-flex}
.obasket b{background:var(--red);border-radius:20px;padding:1px 9px;font-variant-numeric:tabular-nums}
.obasket:hover{background:#141a21}
+ .solall{border:1.5px solid #1857b8;color:#1857b8;background:#f2f7ff;border-radius:8px;padding:7px 12px;font:800 12px 'Inter';cursor:pointer;white-space:nowrap}
+ .solall:hover{background:#1857b8;color:#fff}
.more{display:block;width:100%;margin:14px 0 60px;padding:13px;border:1.5px solid var(--line);border-radius:10px;background:#fff;font:800 13px 'Inter';color:var(--ink);cursor:pointer}
.more:hover{border-color:var(--red);color:var(--red)}
@@ -166,6 +168,7 @@
<div class="ctlbar">
<div class="meta" id="meta"></div>
<div class="ctls">
+ <button class="solall" id="solall" style="display:none" title="Add every firm currently loaded on screen to the outreach list">✉ Solicit all visible</button>
<label class="ctl">Sort
<select id="sort" aria-label="Sort providers">
<option value="locations">Most locations</option>
@@ -219,26 +222,37 @@ const GKEY='rentv.'+'growthLI.v1'; // shared localStorage bucket social.html r
const SOCIAL_CAT={construction:'Construction / GCs',lenders:'Lenders & Capital Sources',escrow:'Escrow',developers:'Developers',legal:'Legal',title:'Title',property:'Vendors / Service Providers'};
function gload(){ try{ return JSON.parse(localStorage.getItem(GKEY))||[]; }catch(e){ return []; } }
function gsave(l){ localStorage.setItem(GKEY,JSON.stringify(l)); }
-function outreachRefresh(){ const n=gload().filter(x=>x&&x.kind==='contact').length; $('ocount').textContent=n; $('obasket').classList.toggle('show',ADMIN); }
+function outreachRefresh(){ const n=gload().filter(x=>x&&x.kind==='contact').length; $('ocount').textContent=n; $('obasket').classList.toggle('show',ADMIN); $('solall').style.display=ADMIN?'inline-flex':'none'; }
function inOutreach(name,scat){ return gload().some(x=>x&&x.kind==='contact'&&x.cat===scat&&(x.name||'').toLowerCase()===String(name).toLowerCase()); }
+function addToOutreach(r){ // returns true if newly added (deduped by name+mapped role)
+ const scat=SOCIAL_CAT[cat]||'Vendors / Service Providers';
+ if(inOutreach(r.name,scat)) return false;
+ const l=gload();
+ l.push({ id:'sv'+Date.now().toString(36)+Math.floor(Math.random()*1e6).toString(36), kind:'contact', cat:scat,
+ name:cap(r.name), sub:[r.city?cap(r.city):'',r.loc_count>1?(r.loc_count+' locations'):'',r.featured?'Featured':''].filter(Boolean).join(' · '),
+ terms:r.name+' '+(r.city||'')+' commercial real estate', market:r.market||market,
+ phone:r.phone||'', website:r.website||'', license_no:r.license_no||'', status:0, added:Date.now() });
+ gsave(l); return true;
+}
function solicit(id,btn){
const r=BYID[id]; if(!r)return;
- const scat=SOCIAL_CAT[cat]||'Vendors / Service Providers';
- if(!inOutreach(r.name,scat)){
- const l=gload();
- l.push({ id:'sv'+Date.now().toString(36)+Math.floor(Math.random()*1e4).toString(36), kind:'contact', cat:scat,
- name:cap(r.name), sub:[r.city?cap(r.city):'',r.loc_count>1?(r.loc_count+' locations'):'',r.featured?'Featured':''].filter(Boolean).join(' · '),
- terms:r.name+' '+(r.city||'')+' commercial real estate', market:r.market||market,
- phone:r.phone||'', website:r.website||'', license_no:r.license_no||'', status:0, added:Date.now() });
- gsave(l);
- }
+ addToOutreach(r);
if(btn){ btn.classList.add('added'); btn.textContent='✓ In outreach'; }
outreachRefresh();
}
+function solicitAll(){
+ const rows=Object.values(BYID); let added=0;
+ rows.forEach(r=>{ if(addToOutreach(r)) added++; });
+ document.querySelectorAll('.act.sol').forEach(b=>{ b.classList.add('added'); b.textContent='✓ In outreach'; });
+ outreachRefresh();
+ const btn=$('solall'), prev=btn.dataset.label||btn.textContent; btn.dataset.label=prev;
+ btn.textContent='✓ Added '+added+' ('+rows.length+' visible)'; setTimeout(()=>{ btn.textContent=btn.dataset.label; },2800);
+}
async function checkAdmin(){
try{ const w=await (await fetch('/api/whoami')).json(); ADMIN=(w&&w.role==='admin'); }catch(e){ ADMIN=false; }
outreachRefresh();
}
+$('solall').onclick=solicitAll;
// URL state (deep-linkable): /services?cat=lenders&city=GLENDALE&q=capital
const p=new URLSearchParams(location.search);
@@ -318,7 +332,7 @@ function card(r){
async function load(reset){
const my=++seq;
- if(reset){$('rows').innerHTML='';$('meta').innerHTML='<span class="spin"></span>Loading…';$('more').style.display='none';}
+ if(reset){$('rows').innerHTML='';$('meta').innerHTML='<span class="spin"></span>Loading…';$('more').style.display='none';for(const k in BYID)delete BYID[k];}
const u=new URLSearchParams(); u.set('cat',cat); u.set('market',market); u.set('sort',sort); u.set('limit',PAGE); u.set('offset',offset);
if(q)u.set('q',q); if(city)u.set('city',city);
let d; try{ d=await (await fetch('/api/services?'+u.toString())).json(); }catch(e){ if(my===seq)$('meta').textContent='Services directory unavailable — try again.'; return; }
← 17716ea2 harden(location): Cody gate — normalize whitespace before th
·
back to Rentv 2026
·
auto-data-snapshot: 2026-08-06T13:27:53 (7 data files) — dat a3ae8ad5 →