← back to Rentv
mockups/assets/chrome.js
170 lines
/* RENTV redesign — shared chrome + numbering engine.
Include once per layout: <script src="assets/chrome.js"></script>
Then call: RENTV.mount({ dark:false, layout:'L03' })
- builds the top bar (hamburger upper-left = ALL tabs+data, user icon upper-right = sign-in)
- loads the REAL live data snapshot from ./data/*.json and exposes RENTV.data
- scans [data-eln] elements, injects clickable number badges, wires the "My Picks" tray */
window.RENTV = (function () {
// ---- canonical navigation model (single source of truth for every drawer) ----
const NAV = {
Sections: [
['Sales', '/sales'], ['Leases', '/leases'], ['Financing', '/financing'],
['Development', '/development'], ['Multifamily', '/multifamily'],
['Industrial', '/industrial'], ['Retail', '/retail'],
],
Intelligence: [
['Markets', '/markets'], ['LA Commercial', '/la-commercial'], ['The REview', '/review'],
['CRE Talk', '/cre-talk'], ['Conferences', '/conferences'], ['Blog', '/blog'],
],
Company: [
['About', '/about'], ['Advertise', '/advertise'],
['Subscribe / Newsletters', '/subscribe'], ['Versions', '/versions'],
],
};
const usd = n => n == null ? '—' : '$' + Number(n).toLocaleString();
const shortUsd = n => { n = Number(n) || 0; return n >= 1e9 ? '$' + (n / 1e9).toFixed(2) + 'B' : n >= 1e6 ? '$' + (n / 1e6).toFixed(1) + 'M' : usd(n); };
const dstr = d => d ? new Date(d).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : '—';
async function loadData() {
// live-first: same-origin /api/<ep> (works on the real rentv-v1 domain, browser carries auth);
// fall back to the bundled snapshot so the gallery also renders anywhere static.
const get = (api, file) => fetch('/api/' + api).then(r => { if (!r.ok) throw 0; return r.json(); })
.catch(() => fetch('data/' + file).then(r => r.json()).catch(() => null));
const [news, videos, la, ticker] = await Promise.all([
get('news', 'news.json'), get('videos', 'videos.json'),
get('la-commercial', 'la-commercial.json'), get('ticker', 'ticker.json')]);
return { news: news || { items: [] }, videos: videos || { items: [] }, la: la || {}, ticker: ticker || {} };
}
// ticker.json is an object of {label,value,dir?,change_pct?,as_of?} metrics
function tickerMetrics(t) {
return Object.entries(t || {})
.filter(([, m]) => m && typeof m === 'object' && m.label && m.value != null)
.map(([, m]) => m);
}
function buildChrome(opts) {
const d = RENTV.data, t = d.ticker || {};
const metrics = tickerMetrics(t);
const topAsset = (d.la.marquee || [])[0];
const snapCells = metrics.map(m => {
const cls = m.dir === 'up' ? ' up' : m.dir === 'down' ? ' dn' : '';
const chg = m.change_pct != null ? ` <span style="font-size:11px">${m.change_pct > 0 ? '+' : ''}${m.change_pct}%</span>` : '';
return `<div class="m"><div class="k">${m.label}</div><div class="v${cls}">${m.value}${chg}</div></div>`;
}).join('') +
`<div class="m"><div class="k">Live Stories</div><div class="v">${(d.news.items || []).length}</div></div>` +
`<div class="m"><div class="k">Top LA Asset</div><div class="v">${topAsset ? shortUsd(topAsset.assessed_total) : '—'}</div></div>`;
// top bar
const bar = document.createElement('header');
bar.className = 'rv-top' + (opts.dark ? ' dark' : '');
bar.innerHTML = `
<button class="rv-burger" id="rvBurger" aria-label="Menu">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
<span class="lbl">Menu</span>
</button>
<a class="rv-brand" href="/">REN<b>TV</b></a>
<button class="rv-user" id="rvUser" title="Sign in" aria-label="Sign in">
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="3.5"/><path d="M4.5 20a7.5 7.5 0 0 1 15 0" stroke-linecap="round"/></svg>
</button>`;
document.body.prepend(bar);
// scrim + drawer
const scrim = document.createElement('div'); scrim.className = 'rv-scrim'; scrim.id = 'rvScrim';
const grp = (name) => `<div class="rv-grp"><div class="gt">${name}</div></div><nav>` +
NAV[name].map(([label, href]) => `<a href="${href}">${label}</a>`).join('') + `</nav>`;
const drawer = document.createElement('aside'); drawer.className = 'rv-drawer'; drawer.id = 'rvDrawer';
drawer.innerHTML = `
<div class="dh"><span class="w">REN<b>TV</b></span><button id="rvClose" aria-label="Close">✕</button></div>
<div class="rv-snap">${snapCells}</div>
${grp('Sections')}${grp('Intelligence')}${grp('Company')}
<div class="df">RENTV.com — Commercial Real Estate News · data live from rentv.com + LA County Assessor + CA DRE</div>`;
document.body.append(scrim, drawer);
const open = () => { scrim.classList.add('open'); drawer.classList.add('open'); };
const close = () => { scrim.classList.remove('open'); drawer.classList.remove('open'); };
document.getElementById('rvBurger').onclick = open;
document.getElementById('rvClose').onclick = close;
scrim.onclick = close;
document.getElementById('rvUser').onclick = () => alert('Sign in — the ONLY thing the upper-right icon does. All navigation + data lives in the upper-left hamburger.');
}
// ---- numbering + pick tray ----
function initNumbers(layout) {
const KEY = 'rentvPicks';
const picks = new Set(JSON.parse(localStorage.getItem(KEY) || '[]'));
let n = 0;
document.querySelectorAll('[data-eln]').forEach(el => {
n++;
const code = layout + '·' + String(n).padStart(2, '0');
el.dataset.elnCode = code;
const badge = document.createElement('span');
badge.className = 'eln' + (picks.has(code) ? ' picked' : '');
badge.textContent = code;
badge.title = 'Click to add/remove from your picks';
badge.onclick = (e) => { e.stopPropagation(); toggle(code, badge); };
el.appendChild(badge);
});
// FAB + tray
const fab = document.createElement('div'); fab.className = 'rv-fab';
fab.innerHTML = `<button class="t-num">① Numbers</button><button class="t-pick">★ My Picks (<span id="rvPickN">${picks.size}</span>)</button>`;
document.body.append(fab);
const tray = document.createElement('div'); tray.className = 'rv-tray'; tray.id = 'rvTray';
tray.innerHTML = `<div class="th"><b>My Picks — elements to keep in the final design</b><button class="x" id="rvTrayX">✕</button></div>
<div class="chips" id="rvChips"></div>
<div class="acts"><button class="copy" id="rvCopy">Copy picks</button><button class="clr" id="rvClr">Clear all</button></div>`;
document.body.append(tray);
const numBtn = fab.querySelector('.t-num'), pickBtn = fab.querySelector('.t-pick');
numBtn.onclick = () => { document.body.classList.toggle('eln-on'); numBtn.classList.toggle('on'); };
pickBtn.onclick = () => tray.classList.add('open');
document.getElementById('rvTrayX').onclick = () => tray.classList.remove('open');
document.body.classList.add('eln-on'); numBtn.classList.add('on'); // numbers ON by default
function save() { localStorage.setItem(KEY, JSON.stringify([...picks])); document.getElementById('rvPickN').textContent = picks.size; renderChips(); }
function toggle(code, badge) { if (picks.has(code)) picks.delete(code); else picks.add(code); if (badge) badge.classList.toggle('picked'); save(); }
function renderChips() {
const box = document.getElementById('rvChips');
if (!picks.size) { box.innerHTML = '<span class="empty">No picks yet. Turn on Numbers, then click the ▦ badge on any element you want in the final build.</span>'; return; }
box.innerHTML = [...picks].sort().map(c => `<span class="chip">${c}<button data-c="${c}">✕</button></span>`).join('');
box.querySelectorAll('button').forEach(b => b.onclick = () => {
picks.delete(b.dataset.c);
const badge = [...document.querySelectorAll('.eln')].find(e => e.dataset && e.textContent === b.dataset.c);
document.querySelectorAll('.eln').forEach(e => { if (e.textContent === b.dataset.c) e.classList.remove('picked'); });
save();
});
}
document.getElementById('rvCopy').onclick = () => {
const txt = [...picks].sort().join(', ');
navigator.clipboard?.writeText(txt); alert('Copied ' + picks.size + ' picks:\n\n' + (txt || '(none)'));
};
document.getElementById('rvClr').onclick = () => { picks.clear(); document.querySelectorAll('.eln.picked').forEach(e => e.classList.remove('picked')); save(); };
renderChips();
}
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
const SECT_COLOR = { Financing: '#2563eb', Sales: '#c8102e', Leases: '#0f8a4d', Development: '#b45309', Multifamily: '#7c3aed', Industrial: '#0891b2', Retail: '#db2777' };
const sectionColor = c => SECT_COLOR[c] || '#5b636b';
// image with graceful fallback to a section-tinted gradient
const imgTag = (src, cls, seed) => {
const hue = ((String(seed || src).length * 37) % 360);
const grad = `linear-gradient(135deg,hsl(${hue} 45% 32%),hsl(${(hue + 40) % 360} 50% 20%))`;
return `<span class="${cls || ''}" style="display:block;background:${grad};background-size:cover;background-position:center"${src ? ` data-bg="${esc(src)}"` : ''}></span>`;
};
function hydrateImages() { // set bg images after mount; hide broken ones gracefully (gradient remains)
document.querySelectorAll('[data-bg]').forEach(el => {
const im = new Image(); const url = el.getAttribute('data-bg');
im.onload = () => { el.style.backgroundImage = `url("${url}")`; };
im.src = url;
});
}
return {
NAV, usd, shortUsd, dstr, esc, sectionColor, imgTag, hydrateImages, data: null,
async mount(opts) {
opts = opts || {};
this.data = await loadData();
buildChrome(opts);
if (opts.render) opts.render(this.data);
this.hydrateImages();
initNumbers(opts.layout || 'L');
},
};
})();