[object Object]

← back to Rentv 2026

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

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 2026  ·  fix(deals): summarize() collapses whitespace runs — the sent ef48e64f →