← back to Consulting Rentv Com
auto-save: 2026-07-26T16:14:26 (1 files) — build.mjs
f71d04ef8d7d984584474067f56dbf27a690154f · 2026-07-26 16:14:27 -0700 · Steve Abrams
Files touched
Diff
commit f71d04ef8d7d984584474067f56dbf27a690154f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 26 16:14:27 2026 -0700
auto-save: 2026-07-26T16:14:26 (1 files) — build.mjs
---
build.mjs | 38 +++++++++++++++++++++++++++++++++++++-
1 file changed, 37 insertions(+), 1 deletion(-)
diff --git a/build.mjs b/build.mjs
index c1c464b..ffbd2ca 100644
--- a/build.mjs
+++ b/build.mjs
@@ -12,6 +12,9 @@ mkdirSync(`${OUT}/versions`, { recursive: true });
const client = (() => { try { return JSON.parse(readFileSync(join(HERE, 'data', 'client.json'), 'utf8')); } catch { return {}; } })();
// Real CRE data sampled from CRCP (~/Projects/commercialrealestate) — deals, map points, Census demographics.
const CRCP = (() => { try { return JSON.parse(readFileSync(join(HERE, 'data', 'deals-sample.json'), 'utf8')); } catch { return { deals: [], extraPoints: [], demographics: [] }; } })();
+// Growth-move suggestions (impact/effort scored) — the same bucket the admin ranks.
+// Surfaced client-facing in the deliverable as the "Low Hanging Fruit" section.
+const SUGGESTIONS = (() => { try { return JSON.parse(readFileSync(join(HERE, 'data', 'suggestions.json'), 'utf8')); } catch { return []; } })();
/* ------------------------------------------------------------------ data --- */
const BIZ = {
@@ -302,7 +305,7 @@ const partners = [
];
const partnerRows = partners.map((p,i)=>`<div class="pcard rv"><div class="pnum">${String(i+1).padStart(2,'0')}</div><div><h4>${p[0]}</h4><p>${p[1]}</p></div></div>`).join('');
-const CATS = [['summary','Executive Summary'],['swot','SWOT Analysis'],['reputation','Reputation & Authority'],['empire','The Empire & Sublease.com'],['competitors','Competitors'],['wires','News + Conference Rivals'],['advertising','Where to Advertise'],['timing','Timing & Cadence'],['audit','Website Audit'],['teardown','Page-by-Page Teardown'],['social','Social Media Audit'],['suggested','Suggested New Websites'],['partners','Unlikely Partners'],['revenue','Unlikely Revenue'],['videos','Video + Brand Films'],['dealdesk','Live Deal Desk (demo)'],['roadmap','90-Day Roadmap'],['sources','Sources & Methodology']];
+const CATS = [['summary','Executive Summary'],['swot','SWOT Analysis'],['lowhanging','Low Hanging Fruit'],['reputation','Reputation & Authority'],['empire','The Empire & Sublease.com'],['competitors','Competitors'],['wires','News + Conference Rivals'],['advertising','Where to Advertise'],['timing','Timing & Cadence'],['audit','Website Audit'],['teardown','Page-by-Page Teardown'],['social','Social Media Audit'],['suggested','Suggested New Websites'],['partners','Unlikely Partners'],['revenue','Unlikely Revenue'],['videos','Video + Brand Films'],['dealdesk','Live Deal Desk (demo)'],['roadmap','90-Day Roadmap'],['sources','Sources & Methodology']];
/* ---- Live Deal Desk (real CRCP data) --------------------------------------- */
const DESK_DEALS = (CRCP.deals || []).filter(d => d.score != null).slice(0, 24);
@@ -363,6 +366,30 @@ function swotAgent(input){
}
const swotGrid = swotAgent(SWOT_INPUT);
+// ---- Low Hanging Fruit (client-facing) — ranks the suggestions bucket by
+// ROI = impact/effort and time = effort; flags true quick wins (low effort +
+// high impact). Server-rendered from suggestions.json at build time so the
+// deliverable is a static, self-contained brief (no runtime fetch/auth). ----
+const lhfEsc = (s) => String(s == null ? '' : s).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]));
+function lowHangingFruit(list) {
+ const items = (list || []).filter((s) => s && s.title).map((s) => {
+ const i = Number(s.impact) || 0, e = Number(s.effort) || 3;
+ return { title: s.title, category: s.category, rationale: s.rationale, i, e, roi: i / Math.max(1, e), fruit: e <= 2 && i >= 4 };
+ }).sort((a, b) => b.roi - a.roi || a.e - b.e || b.i - a.i);
+ const timeLabel = (e) => (e <= 2 ? 'Quick' : e === 3 ? 'Moderate' : 'Involved');
+ if (!items.length) return '<p class="lead2">No growth moves scored yet.</p>';
+ return '<ol class="lhf-list">' + items.map((s, idx) =>
+ `<li class="lhf-item${s.fruit ? ' fruit' : ''} rv"><div class="lhf-rk">${idx + 1}</div><div>` +
+ `<div class="lhf-ti">${lhfEsc(s.title)}</div>` +
+ `<div class="lhf-badges">${s.fruit ? '<span class="lhf-b fruit">⚡ Quick win</span>' : ''}` +
+ `<span class="lhf-b">ROI ${s.roi.toFixed(1)}</span><span class="lhf-b">⏱ ${timeLabel(s.e)}</span>` +
+ `${s.category ? `<span class="lhf-b">${lhfEsc(s.category)}</span>` : ''}` +
+ `<span class="lhf-b">impact ${s.i} · effort ${s.e}</span></div>` +
+ `${s.rationale ? `<p class="lhf-rat">${lhfEsc(s.rationale)}</p>` : ''}</div></li>`
+ ).join('') + '</ol>';
+}
+const lhfList = lowHangingFruit(SUGGESTIONS);
+
/* ---- Sources & methodology (data provenance) ------------------------------
* Honest map of where every part of the brief comes from and how it's made, so
* the client can weigh each claim by its provenance. Tiers are colour-coded and
@@ -470,6 +497,15 @@ td.mo{font-family:'Cormorant Garamond',serif;color:var(--gold);font-size:18px;wi
.swotq.pos{border-top:2px solid #9fd6b0}.swotq.neg{border-top:2px solid #e8a0a0}
.swotq.pos h3{color:#9fd6b0}.swotq.neg h3{color:#e8a0a0}
.swotq ul{margin:0;padding-left:18px}.swotq li{color:var(--dim);font-size:15px;line-height:1.55;margin:6px 0}
+.lhf-list{list-style:none;margin:34px 0 0;padding:0;display:grid;gap:14px}
+.lhf-item{display:grid;grid-template-columns:44px 1fr;gap:18px;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:20px 22px}
+.lhf-item.fruit{border-color:var(--gold)}
+.lhf-rk{font-family:'Cormorant Garamond',Georgia,serif;font-size:32px;color:var(--gold);line-height:1}
+.lhf-ti{font-size:19px;color:var(--ink);font-weight:500;line-height:1.3}
+.lhf-badges{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 0}
+.lhf-b{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);border:1px solid var(--line);padding:4px 10px;border-radius:3px}
+.lhf-b.fruit{background:var(--gold);color:var(--bg);border:0;font-weight:700}
+.lhf-rat{color:var(--dim);font-size:15px;line-height:1.55;margin-top:10px}
.srclegend{display:flex;flex-wrap:wrap;gap:10px;margin:26px 0 4px}
.srcb{display:inline-block;font-size:10px;letter-spacing:.14em;text-transform:uppercase;padding:5px 11px;border-radius:3px;border:1px solid var(--line);white-space:nowrap}
.srcb.v{color:#9fd6b0;border-color:rgba(159,214,176,.4)}
← f74d3f3 slideshow: point rentv Present button at /portal/slideshow (
·
back to Consulting Rentv Com
·
portal(DTD-B): client-facing Low Hanging Fruit section — ser 5868b69 →