← back to Rentv
feat(services): per-firm pitch note rides into the /social letter
4c3bc49165f9f5fb07193c2f80eacf3452b4eb66 · 2026-08-06 14:16:22 -0700 · Steve
Admin card gets a dashed 'Note for the pitch' field. On Solicit (single or bulk) the
note is stored on the outreach item and — in social.html's letterFor — opens the
generated sponsorship letter (capitalized, auto-punctuated) so each pitch is
personalized (e.g. 'Met at ICSC Las Vegas — keen on industrial.'). Note is editable:
re-soliciting a firm updates its stored note; the note also shows in the outreach
rail sub-line. Verified E2E via Playwright: /services note → localStorage → /social
letterFor output contains the note.
Files touched
M public/services.htmlM public/social.html
Diff
commit 4c3bc49165f9f5fb07193c2f80eacf3452b4eb66
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Aug 6 14:16:22 2026 -0700
feat(services): per-firm pitch note rides into the /social letter
Admin card gets a dashed 'Note for the pitch' field. On Solicit (single or bulk) the
note is stored on the outreach item and — in social.html's letterFor — opens the
generated sponsorship letter (capitalized, auto-punctuated) so each pitch is
personalized (e.g. 'Met at ICSC Las Vegas — keen on industrial.'). Note is editable:
re-soliciting a firm updates its stored note; the note also shows in the outreach
rail sub-line. Verified E2E via Playwright: /services note → localStorage → /social
letterFor output contains the note.
---
public/services.html | 22 +++++++++++++++-------
public/social.html | 4 ++++
2 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/public/services.html b/public/services.html
index 486ed673..3e105304 100644
--- a/public/services.html
+++ b/public/services.html
@@ -94,6 +94,9 @@
.act.sol{border-color:#1857b8;color:#1857b8;background:#f2f7ff;font-weight:800}
.act.sol:hover{background:#1857b8;color:#fff}
.act.sol.added{border-color:var(--up);color:var(--up);background:rgba(15,138,77,.08);pointer-events:none}
+ .notein{width:100%;font:500 12.5px 'Inter';padding:7px 9px;border:1px dashed #b9c6da;border-radius:8px;background:#fafcff;color:var(--ink);outline:0}
+ .notein:focus{border-color:#1857b8;border-style:solid}
+ .notein::placeholder{color:#9aa6b4}
/* Admin-only floating outreach basket → hands off to /social (RENTV's sponsorship pitch tool) */
.obasket{position:fixed;right:18px;bottom:18px;z-index:80;display:none;align-items:center;gap:9px;background:#0b0e12;color:#fff;border-radius:30px;padding:11px 17px;box-shadow:0 8px 30px rgba(6,9,12,.3);font:800 13px 'Inter';cursor:pointer;border:1px solid #1c232b}
.obasket.show{display:inline-flex}
@@ -224,25 +227,29 @@ function gload(){ try{ return JSON.parse(localStorage.getItem(GKEY))||[]; }catch
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); $('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();
+function scatOf(){ return SOCIAL_CAT[cat]||'Vendors / Service Providers'; }
+function outreachSub(r,note){ return [r.city?cap(r.city):'',r.loc_count>1?(r.loc_count+' locations'):'',r.featured?'Featured':'',note?('“'+note+'”'):''].filter(Boolean).join(' · '); }
+function noteFor(r){ const scat=scatOf(); const it=gload().find(x=>x&&x.kind==='contact'&&x.cat===scat&&(x.name||'').toLowerCase()===String(r.name).toLowerCase()); return it&&it.note?it.note:''; }
+function cardNote(id){ const el=document.querySelector('.notein[data-noteid="'+String(id).replace(/"/g,'')+'"]'); return el?el.value.trim():''; }
+function addToOutreach(r,note){ // adds OR updates the note if already present; returns true if newly added
+ const scat=scatOf(); note=(note||'').trim();
+ const l=gload(); const ex=l.find(x=>x&&x.kind==='contact'&&x.cat===scat&&(x.name||'').toLowerCase()===String(r.name).toLowerCase());
+ if(ex){ if(note&&ex.note!==note){ ex.note=note; ex.sub=outreachSub(r,note); gsave(l); } return false; }
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(' · '),
+ name:cap(r.name), sub:outreachSub(r,note), note:note,
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;
- addToOutreach(r);
+ addToOutreach(r,cardNote(id));
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++; });
+ rows.forEach(r=>{ if(addToOutreach(r,cardNote(r.id))) 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;
@@ -326,6 +333,7 @@ function card(r){
${spec}
${loc?`<div class="loc">${loc}</div>`:''}
${lic}
+ ${ADMIN?`<input class="notein" data-noteid="${esc(r.id)}" maxlength="140" placeholder="Note for the pitch (optional)…" value="${esc(noteFor(r))}">`:''}
<div class="acts">${acts.join('')||'<span class="lic">Contact via public registry ↗</span>'}</div>
</article>`;
}
diff --git a/public/social.html b/public/social.html
index a65c69f8..99937497 100644
--- a/public/social.html
+++ b/public/social.html
@@ -587,7 +587,11 @@
var ev=nextEvent(x.market);
var isCat=(x.kind==='category'||x.kind==='segment');
var greet=isCat?'there':(looksFirm(x.name)?(cap(x.name)+' team'):cap(firstName(x.name)));
+ // Personalization note carried over from the /services "Solicit" handoff — opens the letter.
+ var note=(x.note?String(x.note).trim().replace(/\s+/g,' '):'');
+ var noteLine=note?(note.charAt(0).toUpperCase()+note.slice(1)+(/[.!?]$/.test(note)?'':'.')+'\n\n'):'';
var text='Hi '+greet+',\n\n'
+ +noteLine
+'I’m '+RENTV_FACTS.contact.name+' with RENTV — now in our 25th year covering the deals that move Western U.S. commercial real estate, from Texas and Arizona to California and the Pacific NW. We’re home to CRE Talk, the State of the Market conferences, and The REview.\n\n'
+'Given '+roleAngle(role,v)+', I wanted to reach out directly.\n\n'
+'I’d love to '+offer.line(ev)+'.\n\n'
← 980b5351 chore: remove dead looksPlace helper + vN patch bump (sessio
·
back to Rentv
·
fix(deals): summarize() collapses whitespace runs — the sent ef48e64f →