← back to Rentv Tour
component-set.mjs
86 lines
import fs from 'fs';
const W = 1920, H = 1080;
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
// id, label, tier(front|admin), one-line VO (Steve cloned voice)
export const COMPONENTS = [
// ── Front end ──
['home', 'RENTV Front Page', 'front', "The RENTV front page — live commercial real estate news for the Western U.S."],
['markets', 'Markets', 'front', "Markets — C.R.E. fundamentals and a live snapshot across every tracked metro."],
['deals', 'Deals', 'front', "Deals — real commercial transactions, filterable by property type, deal type, and market."],
['pulse', 'Market Pulse', 'front', "Market Pulse — the analytics layer over every tracked deal, from volume to top transactions."],
['review', 'The REview', 'front', "The Review — RENTV's commercial real estate video platform, native in the shell."],
['hub', 'Intelligence Hub', 'front', "The Intelligence Hub — every RENTV tool and data surface, one click away."],
['search', 'Search', 'front', "Global search — one box across news, deals, videos, and the whole platform."],
['directory', 'CRE Directory', 'front', "The C.R.E. Directory — firms and players across the Western U.S. commercial market."],
['la-commercial', 'LA Commercial', 'front', "L.A. Commercial — property-level intelligence and transactions for the Los Angeles market."],
['brief', 'The RENTV Brief', 'front', "The RENTV Brief — the daily digest of what moved in Western U.S. commercial real estate."],
// ── Admin backend ──
['backend', 'Backend Hub', 'admin', "The RENTV backend hub — every internal tool in one place, admin only."],
['desk', 'Broker & Owner Desk', 'admin', "The Broker and Owner Desk — a live registry of three hundred eighty-two thousand brokers and thirty-four thousand firms, plus sublease inventory."],
['audience', 'Audience & CRM', 'admin', "Audience and C.R.M. — every subscriber and sublease broker, auto-segmented into one prospect list."],
['admin', 'Admin Dashboards', 'admin', "Ten admin dashboards, each a different back-office view over the real, live data."],
['versions', 'Site Versions', 'admin', "Ten front-page design versions — pick the look, all running on live data."],
['consulting', 'Consulting', 'admin', "The consulting deliverable — RENTV's growth strategy, concepts, and command center, folded in."],
['press', 'Video Library', 'admin', "The video library — every RENTV film, walkthrough, and recap in one admin gallery."],
['social', 'Social Media', 'admin', "Social Media — LinkedIn, X, and Instagram channels, the content calendar, and draft posts, all in one place."],
];
const byId = Object.fromEntries(COMPONENTS.map(c => [c[0], c]));
function emit(id, voDur) {
const [, label, tier] = byId[id];
const accent = tier === 'admin' ? '#c8a15a' : '#0a6cff';
const tag = tier === 'admin' ? 'BACKEND' : 'FRONT END';
const TITLE = 1.3;
const TOTAL = +Math.max(voDur + 0.4, TITLE + 3.0).toFixed(2);
const shotStart = TITLE, shotDur = +(TOTAL - TITLE).toFixed(3);
const html = `<!doctype html>
<html lang="en"><head>
<meta charset="UTF-8" /><meta name="viewport" content="width=1920, height=1080" />
<title>RENTV — ${esc(label)}</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
body{margin:0;background:#070b10;font-family:system-ui,-apple-system,"Helvetica Neue",Arial,sans-serif}
#root{position:relative;width:${W}px;height:${H}px;overflow:hidden}
.clip{position:absolute;inset:0}
#bg{background:radial-gradient(120% 120% at 50% 0%,#0d1725 0%,#070b10 60%)}
.shot{overflow:hidden}
.simg{width:100%;height:100%;object-fit:cover;object-position:top center;display:block;will-change:transform,opacity}
.card{display:grid;place-items:center}
.ci{text-align:center;padding:0 120px}
.ci .tag{font-size:20px;letter-spacing:.4em;font-weight:600;color:${accent};margin-bottom:18px}
.ci h1{margin:0;color:#f3f7fc;font-size:88px;font-weight:500;letter-spacing:-.01em}
.label{left:56px;bottom:60px;right:auto;top:auto;width:auto;height:auto}
.li{display:flex;align-items:center;gap:14px}
.li .num{color:#fff;font-size:18px;font-weight:600;letter-spacing:.08em;padding:8px 13px;border-radius:8px;background:${accent}}
.li .txt{color:#fff;font-size:30px;font-weight:500;background:rgba(9,15,24,.9);padding:8px 18px;border-radius:10px}
</style></head>
<body>
<div id="root" data-composition-id="main" data-start="0" data-width="${W}" data-height="${H}" data-duration="${TOTAL}" data-fps="30">
<div id="bg" class="clip" data-start="0" data-duration="${TOTAL}" data-track-index="0"></div>
<audio id="vo" src="assets/vo.m4a" data-start="0" data-volume="1"></audio>
<section id="title" class="clip card" data-start="0" data-duration="${TITLE}" data-track-index="1"><div class="ci"><div class="tag" id="tt">${tag}</div><h1 id="th">${esc(label)}</h1></div></section>
<div id="shot" class="clip shot" data-start="${shotStart}" data-duration="${shotDur}" data-track-index="2"><img id="simg" class="simg" src="assets/${id}.png" alt="${esc(label)}"></div>
<div id="lbl" class="clip label" data-start="${shotStart}" data-duration="${shotDur}" data-track-index="3"><div id="li" class="li"><span class="num">${tag}</span><span class="txt">${esc(label)}</span></div></div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.from('#th',{y:32,autoAlpha:0,duration:0.55,ease:'power3.out'},0.12);
tl.from('#tt',{y:-14,autoAlpha:0,duration:0.45},0.04);
tl.from('#simg',{opacity:0,duration:0.35,ease:'power1.out'},${shotStart.toFixed(3)});
tl.fromTo('#simg',{scale:1.07},{scale:1.0,duration:${shotDur},ease:'none'},${shotStart.toFixed(3)});
tl.from('#li',{y:24,autoAlpha:0,duration:0.4,ease:'power2.out'},${(shotStart + 0.1).toFixed(3)});
window.__timelines['main'] = tl;
</script>
</body></html>`;
fs.writeFileSync('videos/components/index.html', html);
return TOTAL;
}
// ── CLI ──
const [cmd, a, b] = process.argv.slice(2);
if (cmd === 'ids') console.log(COMPONENTS.map(c => c[0]).join(' '));
else if (cmd === 'vo') process.stdout.write(byId[a][3]);
else if (cmd === 'emit') console.log('TOTAL=' + emit(a, parseFloat(b)));