// Investments tab: nine societies in three risk tiers, all visible at once. import { portfolio } from '../engine/investments.js'; import { t } from '../services/i18n.js'; import { h, icon, makeBuyable, setBuyable } from './dom.js'; import { bump, floater, pop, purchased } from './fx.js'; import { moneyFormatter, setText, signedPercent } from './text.js'; const AMOUNTS = [0.01, 0.1, 0.5, 1]; const TIERS = ['low', 'mid', 'high']; export function createInvestView(root, actions) { let ctx; let fmt; let money; let amount = 0.1; let summary; const rows = new Map(); function societyRow(society) { const change = h('span', { class: 'badge' }); const price = h('span'); const held = h('span'); const sell = h('button', { class: 'btn', type: 'button', text: t('invest.sell'), onclick: (event) => { event.stopPropagation(); // the block itself buys if (actions.sell(society.id, amount)) { pop(root); floater(sell, '↘'); bump(held); } }, }); const root = h( 'li', { class: 'card card--stack' }, h( 'div', { class: 'card__row' }, icon(`inv.${society.id}`), h( 'div', { class: 'card__body' }, h('h3', { class: 'card__title' }, h('span', { text: t(`theme.inv.${society.id}.name`) }), change), h('p', { class: 'card__meta' }, price), h('p', { class: 'card__meta' }, held), ), ), h('div', { class: 'card__actions' }, h('span', { class: 'card__hint', text: t('invest.clickToBuy') }), sell), ); makeBuyable(root, () => { if (!actions.buy(society.id, amount)) return; purchased(root, '↗'); bump(held); }); return { root, change, price, held, sell }; } function tierPanel(tier) { const cfg = ctx.investments.tiers[tier]; const list = h('ul', { class: 'cards' }); for (const society of ctx.investments.societies.filter((s) => s.tier === tier)) { const row = societyRow(society); rows.set(society.id, row); list.append(row.root); } return h( 'section', { class: `panel panel--${tier}` }, h('h2', { class: 'panel__title', text: t(`invest.tier.${tier}`) }), h('p', { class: 'hint', text: t('invest.tierInfo', { swing: fmt.number(cfg.swing * 100), dividend: fmt.number(cfg.dividend * 100), }), }), list, ); } function rebuild(newCtx, formatter) { ctx = newCtx; fmt = formatter; money = moneyFormatter(fmt); rows.clear(); summary = { value: h('strong'), basis: h('strong'), profit: h('strong'), next: h('p', { class: 'hint' }), }; const amountGroup = h( 'fieldset', { class: 'segmented' }, h('legend', { class: 'sr-only', text: t('invest.amount') }), ...AMOUNTS.map((a) => h( 'label', {}, h('input', { type: 'radio', name: 'invest-amount', value: String(a), checked: a === amount, onchange: () => { amount = a; }, }), h('span', { text: `${a * 100}%` }), ), ), ); root.replaceChildren( h( 'section', { class: 'panel panel--wide' }, h('h2', { class: 'panel__title', text: t('tab.invest') }), h('p', { class: 'hint', text: t('invest.intro', { seconds: ctx.investments.intervalSeconds, fee: fmt.number(ctx.investments.fee * 100), }), }), h( 'dl', { class: 'stats' }, h('div', {}, h('dt', { text: t('invest.portfolio') }), h('dd', {}, summary.value)), h('div', {}, h('dt', { text: t('invest.invested') }), h('dd', {}, summary.basis)), h('div', {}, h('dt', { text: t('invest.profit') }), h('dd', {}, summary.profit)), ), h('p', { class: 'field__label', text: t('invest.amountLabel') }), amountGroup, summary.next, ), ...TIERS.map(tierPanel), ); } function update(state) { const p = portfolio(state, ctx); setText(summary.value, money(p.value)); setText(summary.basis, money(p.basis)); setText(summary.profit, money(p.profit)); summary.profit.classList.toggle('pos', p.profit > 0); summary.profit.classList.toggle('neg', p.profit < 0); const remaining = Math.ceil(ctx.investments.intervalSeconds - state.investTimer); setText(summary.next, t('invest.nextUpdate', { seconds: remaining })); for (const society of ctx.investments.societies) { const row = rows.get(society.id); const price = state.invest.prices[society.id]; const shares = state.invest.holdings[society.id]; const change = state.invest.change[society.id]; const basis = state.invest.basis[society.id]; const arrow = change > 0 ? '▲' : change < 0 ? '▼' : '■'; setText(row.change, `${arrow} ${signedPercent(change, fmt)}`); row.change.classList.toggle('pos', change > 0); row.change.classList.toggle('neg', change < 0); setText( row.price, t('invest.priceLine', { price: fmt.price(price), gap: signedPercent(price / society.basePrice - 1, fmt), }), ); const value = shares * price; setText( row.held, shares > 0 ? t('invest.heldLine', { value: money(value), profit: signedPercent(basis > 0 ? value / basis - 1 : 0, fmt) }) : t('invest.heldNone'), ); setBuyable(row.root, state.funds >= 0.01); row.sell.disabled = shares <= 0; } } return { rebuild, update }; }