Files

189 lines
5.7 KiB
JavaScript

// 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 };
}