/* global React, Icons, Badge, Btn, Pill, Meter, Score, Stat, Eyebrow */
const { useState, useMemo } = React;
/* =========================================================================
BUILD CONFIGURATOR — flagship interactive
========================================================================= */
const GEAR_CATALOG = {
camera: [
{ id: 'osmo-pocket-3', name: 'DJI Osmo Pocket 3', price: 519, draw: 4.2, weight: 179, score: 9.1, tag: 'IRL favorite' },
{ id: 'sony-zv-1f', name: 'Sony ZV-1F', price: 449, draw: 3.8, weight: 229, score: 8.2, tag: null },
{ id: 'gopro-13', name: 'GoPro Hero 13 Black', price: 399, draw: 5.5, weight: 154, score: 8.8, tag: 'Rugged' },
{ id: 'iphone-only', name: 'iPhone (use yours)', price: 0, draw: 3.0, weight: 200, score: 7.4, tag: 'Free' },
],
audio: [
{ id: 'rode-wgo3', name: 'Rode Wireless GO 3', price: 349, draw: 1.2, weight: 84, score: 9.0, tag: 'Lab pick' },
{ id: 'dji-mic-2', name: 'DJI Mic 2', price: 349, draw: 1.4, weight: 91, score: 8.7, tag: null },
{ id: 'shure-mv7', name: 'Shure MV7+', price: 279, draw: 2.0, weight: 550, score: 8.0, tag: 'Static' },
],
router: [
{ id: 'peplink-br1', name: 'Peplink MAX BR1 Pro 5G',price: 1499, draw: 12, weight: 470, score: 9.4, tag: 'Pro' },
{ id: 'gl-mt2500', name: 'GL.iNet Brume 2', price: 159, draw: 4, weight: 180, score: 7.6, tag: 'Budget' },
{ id: 'liveu-solo-pro',name: 'LiveU Solo Pro', price: 1499, draw: 14, weight: 380, score: 9.2, tag: null },
{ id: 'none-router', name: 'No router (tethered)', price: 0, draw: 0, weight: 0, score: 6.0, tag: 'Skip' },
],
power: [
{ id: 'omni-20', name: 'Omnicharge Omni 20+', price: 199, draw: 0, weight: 510, capacity: 70, score: 8.8, tag: null },
{ id: 'ec-river-2', name: 'EcoFlow River 2', price: 239, draw: 0, weight: 3500, capacity: 256, score: 8.6, tag: 'Stationary' },
{ id: 'anker-737', name: 'Anker 737 PowerCore', price: 149, draw: 0, weight: 632, capacity: 86, score: 9.1, tag: 'Best value' },
{ id: 'none-power', name: 'None (run off phone)', price: 0, draw: 0, weight: 0, capacity: 0, score: 5.0, tag: 'Risky' },
],
pack: [
{ id: 'gunrun-v3', name: 'Gunrun Mark V backpack',price: 749, draw: 0, weight: 1800, score: 9.6, tag: 'Flagship' },
{ id: 'peak-30', name: 'Peak Design Travel 30L',price: 299, draw: 0, weight: 1500, score: 7.9, tag: 'Generic' },
{ id: 'sling', name: 'Just a sling bag', price: 89, draw: 0, weight: 400, score: 6.5, tag: 'Starter' },
],
};
const PRESETS = {
starter: { name: 'Starter', budget: 500, audience: 'Beginner', env: 'Phone' },
mobile: { name: 'Mobile Creator', budget: 1500, audience: 'Beginner', env: 'Phone' },
travel: { name: 'Travel IRL', budget: 2500, audience: 'Upgrader', env: 'Travel' },
backpack: { name: 'Pro Backpack', budget: 4500, audience: 'Pro', env: 'Backpack' },
event: { name: 'Event Broadcast',budget: 6000, audience: 'Pro', env: 'Event' },
house: { name: 'StreamerHouse', budget: 9500, audience: 'Pro', env: 'Stationary' },
};
// The bonding/data market bifurcated into three lanes in 2025-26 — not a
// single starter→pro ladder. Each lane is a different bet, not a different
// budget. Builds are grouped by the lane they actually live in.
const LANES = [
{
id: 'vertical',
cls: 'is-vertical',
title: 'Phone · Vertical',
tag: 'Osmo + Speedify lane',
sub: 'Mobile-first, vertical-native, software bonding. TikTok Live / Kick.',
presets: ['starter', 'mobile'],
},
{
id: 'sleeper',
cls: 'is-sleeper',
title: 'Sleeper · BELABOX',
tag: 'BELABOX + Calyx lane',
sub: 'DIY / open-source bonding, no monthly hardware tax. Mixed rigs.',
presets: ['travel', 'house'],
},
{
id: 'prestige',
cls: 'is-prestige',
title: 'Prestige · LiveU',
tag: 'Solo PRO + UnlimitedIRL lane',
sub: 'Carrier-grade hardware, broadcast-grade SLA. Pays for itself at scale.',
presets: ['backpack', 'event'],
},
];
const PRESET_PICKS = {
starter: { camera: 'iphone-only', audio: 'shure-mv7', router: 'none-router', power: 'none-power', pack: 'sling' },
mobile: { camera: 'sony-zv-1f', audio: 'dji-mic-2', router: 'gl-mt2500', power: 'anker-737', pack: 'peak-30' },
travel: { camera: 'osmo-pocket-3', audio: 'rode-wgo3', router: 'gl-mt2500', power: 'omni-20', pack: 'peak-30' },
backpack: { camera: 'osmo-pocket-3', audio: 'rode-wgo3', router: 'peplink-br1', power: 'omni-20', pack: 'gunrun-v3' },
event: { camera: 'gopro-13', audio: 'rode-wgo3', router: 'liveu-solo-pro', power: 'omni-20', pack: 'gunrun-v3' },
house: { camera: 'osmo-pocket-3', audio: 'shure-mv7', router: 'peplink-br1', power: 'ec-river-2', pack: 'peak-30' },
};
const SLOTS = [
{ key: 'camera', label: 'Camera', icon: },
{ key: 'audio', label: 'Audio', icon: },
{ key: 'router', label: 'Bonded uplink', icon: },
{ key: 'power', label: 'Power', icon: },
{ key: 'pack', label: 'Pack / mount', icon: },
];
const PRESET_KEYS = Object.keys(PRESETS);
const BuildPage = () => {
const [presetId, setPresetId] = useState('backpack');
const [picks, setPicks] = useState(PRESET_PICKS.backpack);
const [carriers, setCarriers] = useState(3);
const [hours, setHours] = useState(4);
const [bitrate, setBitrate] = useState(6);
const applyPreset = (id) => {
setPresetId(id);
setPicks(PRESET_PICKS[id]);
};
const swap = (slot, id) => {
setPicks({ ...picks, [slot]: id });
};
// Roll up the selected items
const items = SLOTS.map((s) => {
const item = GEAR_CATALOG[s.key].find(g => g.id === picks[s.key]);
return { slot: s, item };
});
const total = items.reduce((sum, it) => sum + (it.item?.price || 0), 0);
const totalDraw = items.reduce((sum, it) => sum + (it.item?.draw || 0), 0);
const totalWeight = items.reduce((sum, it) => sum + (it.item?.weight || 0), 0);
const powerItem = items.find(i => i.slot.key === 'power').item;
const powerCap = powerItem.capacity || 0; // Wh
const runtime = totalDraw > 0 ? powerCap / totalDraw : 0;
const avgScore = items.filter(i => i.item).reduce((s, i) => s + i.item.score, 0) / items.filter(i => i.item).length;
// Data plan strategy
const dataMonthly = 60 + carriers * 65 + (bitrate > 8 ? 40 : 0);
const dataNeeded = bitrate * 0.45 * hours;
return (
{/* HEADER ========================================================== */}
Flagship tool · Build Configurator v2.1
Configure your build.
Pick a preset, swap any component. The BOM, runtime, weight, and data
strategy recalculate live. Every part is tested in our lab.
Live calc
Catalog · Q2 ’26
BUILD ID · BC-{presetId.toUpperCase()}-{Math.abs(hash(JSON.stringify(picks))).toString(36).slice(0,5).toUpperCase()}
{/* PRESETS — three lanes, not a ladder ============================ */}
01 · Pick your lane
Three lanes, not a ladder.
The 2026 IRL market bifurcated. Each lane is a different bet — not a different budget tier.
Pick the lane your stream actually lives in, then a starting build inside it.
Tip: swap any part after selecting
{LANES.map(lane => (
{lane.tag}
{lane.presets.length} builds
{lane.title}
{lane.sub}
{lane.presets.map(k => (
applyPreset(k)}
className="card"
style={{
padding: '12px 14px',
cursor: 'pointer',
background: presetId === k ? 'var(--bg-3)' : 'var(--bg-2)',
borderColor: presetId === k ? 'var(--accent)' : 'var(--line-2)',
textAlign: 'left',
position: 'relative',
}}>
{presetId === k && }
{k}
{PRESETS[k].name}
~${PRESETS[k].budget.toLocaleString()}
))}
))}
{/* MAIN BUILDER ==================================================== */}
{/* LEFT: slots ============================================== */}
02 · Bill of Materials
{/* Share build uses native Web Share API where available, falls back to clipboard. */}
{
const url = window.location.href;
if (navigator.share) navigator.share({ title: 'My livestreamers.org build', url });
else navigator.clipboard?.writeText(url);
}}> Share build
{/* Export PDF = browser print-to-PDF for now. Real PDF export with custom layout lands in Phase 2. */}
window.print()}> Export PDF
{SLOTS.map((slot) => {
const item = GEAR_CATALOG[slot.key].find(g => g.id === picks[slot.key]);
return (
swap(slot.key, id)} />
);
})}
{/* Data plan sub-section */}
03 · Data strategy
Carrier mix & uplink budget
Quarterly verified
= 3 ? 'SIM injector' : carriers === 2 ? 'Dual-SIM router' : 'Hotspot + phone'} sub="See bonding guide" tone="accent" />
{[
{ n: 'Verizon', plan: 'Business 5G Unl Plus', mbps: '15-50 ↑' },
{ n: 'T-Mobile', plan: 'Business 5G Unl Adv', mbps: '20-100 ↑' },
{ n: 'AT&T', plan: 'Business Unl Premium', mbps: '10-25 ↑' },
{ n: 'Visible', plan: 'Plus (deprio at 50G)', mbps: '5-15 ↑' },
].slice(0, carriers).map((c, i) => (
{c.n.toUpperCase()}
{c.plan}
{c.mbps}
))}
{carriers < 4 && Array.from({ length: 4 - carriers }).map((_, i) => (
empty slot
))}
{/* RIGHT: live spec sheet ================================== */}
{/* mini-header */}
{PRESETS[presetId].name}
For: {PRESETS[presetId].audience} · {PRESETS[presetId].env}
{/* total */}
Build total
{items.filter(i => i.item?.price > 0).length} parts
${total.toLocaleString()}
Affiliate disclosed · prices reflect typical street, May ’26
{/* sub-stats */}
} sub="lab-tested" />
i.item?.price > 0).length * 2.5).toFixed(0)} min`} sub="cold start" />
{/* runtime visual */}
Power budget
= 4 ? 'var(--accent)' : runtime >= 2 ? 'var(--warn)' : 'var(--bad)' }}>
{runtime >= 4 ? 'HEALTHY' : runtime >= 2 ? 'TIGHT' : 'INSUFFICIENT'}
= 4 ? '' : runtime >= 2 ? 'warn' : 'bad'} />
0h 4h target 8h+
{/* call-out */}
PRO TIP
{runtime < 2 ? 'Your draw outpaces your battery. Swap to the Omnicharge 20+ or add a second pack.' :
totalDraw > 15 ? 'High draw — keep a 100W USB-PD wall in reach during cuts.' :
'Solid balance. Pack an extra cable for the router; it’s the #1 dead-stream cause we log.'}
{/* CTA */}
{/* Add-to-cart routes through the affiliate registry in Phase 5.
For now: open an early-access mailto with the build summary. */}
}
href={`mailto:hello@livestreamers.org?subject=${encodeURIComponent('Build: ' + PRESETS[presetId].name + ' ($' + total + ')')}`}>
Email me this build
window.print()}> Print PDF
{
const url = window.location.href;
if (navigator.share) navigator.share({ title: 'My livestreamers.org build', url });
else navigator.clipboard?.writeText(url);
}}> Share
{/* wiring diagram peek */}
Wiring diagram
{/* Full interactive wiring diagram lands in Phase 3 — for now the SVG below is the preview. */}
PREVIEW
{/* boxes */}
{[['CAM', 30, 20], ['MIC', 30, 80], ['BAT', 290, 20], ['BAT', 290, 80]].map(([l, x, y], i) => (
{l}
))}
ROUTER
bonded
{/* lines */}
{/* TEST LOG ========================================================= */}
Field-tested · Most recent runs for this build
From the lab log.
{/* "All test logs" lands when MDX runs ship in Phase 4. */}
{[
{ loc: 'Downtown LA', date: 'May 02 ’26', drops: 0, bitrate: 6.2, dur: '4h 18m', notes: 'Heavy crowd, dropped to T-Mobile once.' },
{ loc: 'Shibuya · Tokyo', date: 'Apr 24 ’26', drops: 2, bitrate: 5.4, dur: '3h 02m', notes: 'Convention floor, AT&T roaming spotty.' },
{ loc: 'Suburban drive · TX', date: 'Apr 18 ’26', drops: 0, bitrate: 7.0, dur: '6h 41m', notes: 'Highway test, 4 carriers, all healthy.' },
].map((t, i) => (
{t.date.toUpperCase()}
{t.drops === 0 ? '0 drops' : `${t.drops} drops`}
{t.loc}
BITRATE {t.bitrate} Mbps
DUR {t.dur}
{t.notes}
))}
);
};
const SlotRow = ({ slot, selected, options, onChange }) => {
const [open, setOpen] = useState(false);
return (
{slot.icon}
{slot.label}
{selected?.name || 'Empty'}
{selected?.tag &&
{selected.tag} }
PRICE
${selected?.price || 0}
setOpen(!open)}>
{open ? 'Close' : 'Swap'}
{open ? : }
{open && (
Alternatives · {options.length}
{options.map(o => (
{ onChange(o.id); setOpen(false); }}
className="row between"
style={{
padding: '10px 12px',
background: o.id === selected?.id ? 'var(--bg-3)' : 'var(--bg-1)',
border: '1px solid ' + (o.id === selected?.id ? 'var(--accent)' : 'var(--line-2)'),
borderRadius: 6,
textAlign: 'left',
cursor: 'pointer',
width: '100%',
}}>
{o.id === selected?.id ?
:
}
{o.name}
{o.tag &&
{o.tag} }
${o.price}
))}
)}
);
};
const SliderControl = ({ label, value, setValue, min, max, step, unit }) => (
);
const Tile = ({ label, value, sub, tone }) => (
{label}
{value}
{sub &&
{sub}
}
);
const SpecTile = ({ label, value, sub }) => (
);
function hash(s) {
let h = 0;
for (let i = 0; i < s.length; i++) h = ((h << 5) - h) + s.charCodeAt(i) | 0;
return h;
}
window.BuildPage = BuildPage;