From 4809a21e3880a49bc5b4a3f0e3d8192f139e4bb2 Mon Sep 17 00:00:00 2001 From: danielyxie Date: Tue, 23 Apr 2019 01:23:08 -0700 Subject: [PATCH] Finished React components for new Stock Market UI --- css/stockmarket.scss | 20 +- package-lock.json | 2 +- src/NetscriptFunctions.js | 3 +- src/PersonObjects/IPlayer.ts | 3 + src/StockMarket/IOrderBook.ts | 5 + src/StockMarket/IStockMarket.ts | 10 + src/StockMarket/Stock.ts | 16 +- src/StockMarket/StockMarket.js | 156 +------- .../data/TickerHeaderFormatData.ts | 11 + src/StockMarket/ui/InfoAndPurchases.tsx | 222 ++++++++++++ src/StockMarket/ui/Root.tsx | 75 ++++ src/StockMarket/ui/StockTicker.tsx | 343 ++++++++++++++++++ src/StockMarket/ui/StockTickerHeaderText.tsx | 41 +++ src/StockMarket/ui/StockTickerOrder.tsx | 42 +++ src/StockMarket/ui/StockTickerOrderList.tsx | 33 ++ .../ui/StockTickerPositionText.tsx | 117 ++++++ src/StockMarket/ui/StockTickerTxButton.tsx | 18 + src/StockMarket/ui/StockTickers.tsx | 129 +++++++ src/StockMarket/ui/StockTickersConfig.tsx | 61 ++++ src/index.html | 48 +-- src/ui/React/StdButtonPurchased.tsx | 25 +- 21 files changed, 1173 insertions(+), 207 deletions(-) create mode 100644 src/StockMarket/IOrderBook.ts create mode 100644 src/StockMarket/IStockMarket.ts create mode 100644 src/StockMarket/data/TickerHeaderFormatData.ts create mode 100644 src/StockMarket/ui/InfoAndPurchases.tsx create mode 100644 src/StockMarket/ui/Root.tsx create mode 100644 src/StockMarket/ui/StockTicker.tsx create mode 100644 src/StockMarket/ui/StockTickerHeaderText.tsx create mode 100644 src/StockMarket/ui/StockTickerOrder.tsx create mode 100644 src/StockMarket/ui/StockTickerOrderList.tsx create mode 100644 src/StockMarket/ui/StockTickerPositionText.tsx create mode 100644 src/StockMarket/ui/StockTickerTxButton.tsx create mode 100644 src/StockMarket/ui/StockTickers.tsx create mode 100644 src/StockMarket/ui/StockTickersConfig.tsx diff --git a/css/stockmarket.scss b/css/stockmarket.scss index b1c723c5e..677111f65 100644 --- a/css/stockmarket.scss +++ b/css/stockmarket.scss @@ -17,9 +17,13 @@ } } -#stock-market-list li { - button { - font-size: $defaultFontSize; +#stock-market-list { + list-style: none; + + li { + button { + font-size: $defaultFontSize; + } } } @@ -50,11 +54,21 @@ .stock-market-position-text { color: #fff; display: inline-block; + + p { + color: #fff; + display: block; + } } .stock-market-order-list { overflow-y: auto; max-height: 100px; + + li { + color: #fff; + padding: 4px; + } } .stock-market-order-cancel-btn { diff --git a/package-lock.json b/package-lock.json index 3df2e1b47..9b83b98cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "bitburner", - "version": "0.45.0", + "version": "0.46.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index 14fc95e05..9c8c7c71c 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -77,7 +77,6 @@ import { StockMarket, StockSymbols, SymbolToStockMap, - initStockMarket, initSymbolToStockMap, buyStock, sellStock, @@ -1708,7 +1707,7 @@ function NetscriptFunctions(workerScript) { throw makeRuntimeRejectMsg(workerScript, "ERROR: Invalid stock symbol passed into sellShort()"); } const res = sellShort(stock, shares, workerScript); - + return res ? stock.price : 0; }, placeOrder(symbol, shares, price, type, pos) { diff --git a/src/PersonObjects/IPlayer.ts b/src/PersonObjects/IPlayer.ts index 01f755533..ca7898745 100644 --- a/src/PersonObjects/IPlayer.ts +++ b/src/PersonObjects/IPlayer.ts @@ -32,7 +32,10 @@ export interface IPlayer { factions: string[]; firstTimeTraveled: boolean; hacknetNodes: (HacknetNode | string)[]; // HacknetNode object or IP of Hacknet Server + has4SData: boolean; + has4SDataTixApi: boolean; hashManager: HashManager; + hasTixApiAccess: boolean; hasWseAccount: boolean; homeComputer: string; hp: number; diff --git a/src/StockMarket/IOrderBook.ts b/src/StockMarket/IOrderBook.ts new file mode 100644 index 000000000..bd761d732 --- /dev/null +++ b/src/StockMarket/IOrderBook.ts @@ -0,0 +1,5 @@ +import { Order } from "./Order"; + +export interface IOrderBook { + [key: string]: Order[]; +} diff --git a/src/StockMarket/IStockMarket.ts b/src/StockMarket/IStockMarket.ts new file mode 100644 index 000000000..e743e9173 --- /dev/null +++ b/src/StockMarket/IStockMarket.ts @@ -0,0 +1,10 @@ +import { IOrderBook } from "./IOrderBook"; +import { Stock } from "./Stock"; + +export type IStockMarket = { + [key: string]: Stock; +} & { + lastUpdate: number; + storedCycles: number; + Orders: IOrderBook; +} diff --git a/src/StockMarket/Stock.ts b/src/StockMarket/Stock.ts index 401d250b1..d85888885 100644 --- a/src/StockMarket/Stock.ts +++ b/src/StockMarket/Stock.ts @@ -73,6 +73,11 @@ export class Stock { */ readonly cap: number; + /** + * Stocks previous share price + */ + lastPrice: number; + /** * Maximum number of shares that player can own (both long and short combined) */ @@ -114,11 +119,6 @@ export class Stock { */ playerShortShares: number; - /** - * The HTML element that displays the stock's info in the UI - */ - posTxtEl: HTMLElement | null; - /** * Stock's share price */ @@ -162,6 +162,7 @@ export class Stock { this.name = p.name; this.symbol = p.symbol; this.price = toNumber(p.initPrice); + this.lastPrice = this.price; this.playerShares = 0; this.playerAvgPx = 0; this.playerShortShares = 0; @@ -182,8 +183,11 @@ export class Stock { // Max Shares (Outstanding shares) is a percentage of total shares const outstandingSharePercentage: number = 0.15; this.maxShares = Math.round((this.totalShares * outstandingSharePercentage) / 1e5) * 1e5; + } - this.posTxtEl = null; + changePrice(newPrice: number): void { + this.lastPrice = this.price; + this.price = newPrice; } /** diff --git a/src/StockMarket/StockMarket.js b/src/StockMarket/StockMarket.js index d705fd6fb..18bba3138 100644 --- a/src/StockMarket/StockMarket.js +++ b/src/StockMarket/StockMarket.js @@ -8,6 +8,7 @@ import { getStockMarket4SDataCost, getStockMarket4STixApiCost } from "./StockMarketCosts"; +import { InitStockMetadata } from "./data/InitStockMetadata"; import { StockSymbols } from "./data/StockSymbols"; import { CONSTANTS } from "../Constants"; @@ -155,16 +156,6 @@ function executeOrder(order) { export let StockMarket = {}; // Maps full stock name -> Stock object export let SymbolToStockMap = {}; // Maps symbol -> Stock object -let formatHelpData = { - longestName: 0, - longestSymbol: 0, -}; - -for (const key in StockSymbols) { - formatHelpData.longestName = key.length > formatHelpData.longestName ? key.length : formatHelpData.longestName; - formatHelpData.longestSymbol = StockSymbols[key].length > formatHelpData.longestSymbol ? StockSymbols[key].length : formatHelpData.longestSymbol; -} - export function loadStockMarket(saveString) { if (saveString === "") { StockMarket = {}; @@ -174,145 +165,16 @@ export function loadStockMarket(saveString) { } export function initStockMarket() { - for (var stk in StockMarket) { + for (const stk in StockMarket) { if (StockMarket.hasOwnProperty(stk)) { delete StockMarket[stk]; } } - const randInt = getRandomInt; - - var ecorp = LocationName.AevumECorp; - var ecorpStk = new Stock(ecorp, StockSymbols[ecorp], randInt(40, 50) / 100, true, 19, randInt(17e3, 28e3), 2.4e12); - StockMarket[ecorp] = ecorpStk; - - var megacorp = LocationName.Sector12MegaCorp; - var megacorpStk = new Stock(megacorp, StockSymbols[megacorp], randInt(40,50)/100, true, 19, randInt(24e3, 34e3), 2.4e12); - StockMarket[megacorp] = megacorpStk; - - var blade = LocationName.Sector12BladeIndustries; - var bladeStk = new Stock(blade, StockSymbols[blade], randInt(70, 80)/100, true, 13, randInt(12e3, 25e3), 1.6e12); - StockMarket[blade] = bladeStk; - - var clarke = LocationName.AevumClarkeIncorporated; - var clarkeStk = new Stock(clarke, StockSymbols[clarke], randInt(65, 75)/100, true, 12, randInt(10e3, 25e3), 1.5e12); - StockMarket[clarke] = clarkeStk; - - var omnitek = LocationName.VolhavenOmniTekIncorporated; - var omnitekStk = new Stock(omnitek, StockSymbols[omnitek], randInt(60, 70)/100, true, 12, randInt(32e3, 43e3), 1.8e12); - StockMarket[omnitek] = omnitekStk; - - var foursigma = LocationName.Sector12FourSigma; - var foursigmaStk = new Stock(foursigma, StockSymbols[foursigma], randInt(100, 110)/100, true, 17, randInt(50e3, 80e3), 2e12); - StockMarket[foursigma] = foursigmaStk; - - var kuaigong = LocationName.ChongqingKuaiGongInternational; - var kuaigongStk = new Stock(kuaigong, StockSymbols[kuaigong], randInt(75, 85)/100, true, 10, randInt(16e3, 28e3), 1.9e12); - StockMarket[kuaigong] = kuaigongStk; - - var fulcrum = LocationName.AevumFulcrumTechnologies; - var fulcrumStk = new Stock(fulcrum, StockSymbols[fulcrum], randInt(120, 130)/100, true, 16, randInt(29e3, 36e3), 2e12); - StockMarket[fulcrum] = fulcrumStk; - - var storm = LocationName.IshimaStormTechnologies; - var stormStk = new Stock(storm, StockSymbols[storm], randInt(80, 90)/100, true, 7, randInt(20e3, 25e3), 1.2e12); - StockMarket[storm] = stormStk; - - var defcomm = LocationName.NewTokyoDefComm; - var defcommStk = new Stock(defcomm, StockSymbols[defcomm], randInt(60, 70)/100, true, 10, randInt(6e3, 19e3), 900e9); - StockMarket[defcomm] = defcommStk; - - var helios = LocationName.VolhavenHeliosLabs; - var heliosStk = new Stock(helios, StockSymbols[helios], randInt(55, 65)/100, true, 9, randInt(10e3, 18e3), 825e9); - StockMarket[helios] = heliosStk; - - var vitalife = LocationName.NewTokyoVitaLife; - var vitalifeStk = new Stock(vitalife, StockSymbols[vitalife], randInt(70, 80)/100, true, 7, randInt(8e3, 14e3), 1e12); - StockMarket[vitalife] = vitalifeStk; - - var icarus = LocationName.Sector12IcarusMicrosystems; - var icarusStk = new Stock(icarus, StockSymbols[icarus], randInt(60, 70)/100, true, 7.5, randInt(12e3, 24e3), 800e9); - StockMarket[icarus] = icarusStk; - - var universalenergy = LocationName.Sector12UniversalEnergy; - var universalenergyStk = new Stock(universalenergy, StockSymbols[universalenergy], randInt(50, 60)/100, true, 10, randInt(16e3, 29e3), 900e9); - StockMarket[universalenergy] = universalenergyStk; - - var aerocorp = LocationName.AevumAeroCorp; - var aerocorpStk = new Stock(aerocorp, StockSymbols[aerocorp], randInt(55, 65)/100, true, 6, randInt(8e3, 17e3), 640e9); - StockMarket[aerocorp] = aerocorpStk; - - var omnia = LocationName.VolhavenOmniaCybersystems; - var omniaStk = new Stock(omnia, StockSymbols[omnia], randInt(65, 75)/100, true, 4.5, randInt(6e3, 15e3), 600e9); - StockMarket[omnia] = omniaStk; - - var solaris = LocationName.ChongqingSolarisSpaceSystems; - var solarisStk = new Stock(solaris, StockSymbols[solaris], randInt(70, 80)/100, true, 8.5, randInt(14e3, 28e3), 705e9); - StockMarket[solaris] = solarisStk; - - var globalpharm = LocationName.NewTokyoGlobalPharmaceuticals; - var globalpharmStk = new Stock(globalpharm, StockSymbols[globalpharm], randInt(55, 65)/100, true, 10.5, randInt(12e3, 30e3), 695e9); - StockMarket[globalpharm] = globalpharmStk; - - var nova = LocationName.IshimaNovaMedical; - var novaStk = new Stock(nova, StockSymbols[nova], randInt(70, 80)/100, true, 5, randInt(15e3, 27e3), 600e9); - StockMarket[nova] = novaStk; - - var watchdog = LocationName.AevumWatchdogSecurity; - var watchdogStk = new Stock(watchdog, StockSymbols[watchdog], randInt(240, 260)/100, true, 1.5, randInt(4e3, 8.5e3), 450e9); - StockMarket[watchdog] = watchdogStk; - - var lexocorp = LocationName.VolhavenLexoCorp; - var lexocorpStk = new Stock(lexocorp, StockSymbols[lexocorp], randInt(115, 135)/100, true, 6, randInt(4.5e3, 8e3), 300e9); - StockMarket[lexocorp] = lexocorpStk; - - var rho = LocationName.AevumRhoConstruction; - var rhoStk = new Stock(rho, StockSymbols[rho], randInt(50, 70)/100, true, 1, randInt(2e3, 7e3), 180e9); - StockMarket[rho] = rhoStk; - - var alpha = LocationName.Sector12AlphaEnterprises; - var alphaStk = new Stock(alpha, StockSymbols[alpha], randInt(175, 205)/100, true, 10, randInt(4e3, 8.5e3), 240e9); - StockMarket[alpha] = alphaStk; - - var syscore = LocationName.VolhavenSysCoreSecurities; - var syscoreStk = new Stock(syscore, StockSymbols[syscore], randInt(150, 170)/100, true, 3, randInt(3e3, 8e3), 200e9); - StockMarket[syscore] = syscoreStk; - - var computek = LocationName.VolhavenCompuTek; - var computekStk = new Stock(computek, StockSymbols[computek], randInt(80, 100)/100, true, 4, randInt(1e3, 6e3), 185e9); - StockMarket[computek] = computekStk; - - var netlink = LocationName.AevumNetLinkTechnologies; - var netlinkStk = new Stock(netlink, StockSymbols[netlink], randInt(400, 430)/100, true, 1, randInt(1e3, 5e3), 58e9); - StockMarket[netlink] = netlinkStk; - - var omega = LocationName.IshimaOmegaSoftware; - var omegaStk = new Stock(omega, StockSymbols[omega], randInt(90, 110)/100, true, 0.5, randInt(1e3, 8e3), 60e9); - StockMarket[omega] = omegaStk; - - var fns = LocationName.Sector12FoodNStuff; - var fnsStk = new Stock(fns, StockSymbols[fns], randInt(70, 80)/100, false, 1, randInt(500, 4.5e3), 45e9); - StockMarket[fns] = fnsStk; - - var sigmacosm = "Sigma Cosmetics"; - var sigmacosmStk = new Stock(sigmacosm, StockSymbols[sigmacosm], randInt(260, 300)/100, true, 0, randInt(1.5e3, 3.5e3), 30e9); - StockMarket[sigmacosm] = sigmacosmStk; - - var joesguns = "Joes Guns"; - var joesgunsStk = new Stock(joesguns, StockSymbols[joesguns], randInt(360, 400)/100, true, 1, randInt(250, 1.5e3), 42e9); - StockMarket[joesguns] = joesgunsStk; - - var catalyst = "Catalyst Ventures"; - var catalystStk = new Stock(catalyst, StockSymbols[catalyst], randInt(120, 175)/100, true, 13.5, randInt(250, 1.5e3), 100e9); - StockMarket[catalyst] = catalystStk; - - var microdyne = "Microdyne Technologies"; - var microdyneStk = new Stock(microdyne, StockSymbols[microdyne], randInt(70, 80)/100, true, 8, randInt(15e3, 30e3), 360e9); - StockMarket[microdyne] = microdyneStk; - - var titanlabs = "Titan Laboratories"; - var titanlabsStk = new Stock(titanlabs, StockSymbols[titanlabs], randInt(50, 70)/100, true, 11, randInt(12e3, 24e3), 420e9); - StockMarket[titanlabs] = titanlabsStk; + for (const metadata of InitStockMetadata) { + const name = metadata.name; + StockMarket[name] = new Stock(metadata); + } var orders = {}; for (var name in StockMarket) { @@ -329,14 +191,14 @@ export function initStockMarket() { } export function initSymbolToStockMap() { - for (var name in StockSymbols) { + for (const name in StockSymbols) { if (StockSymbols.hasOwnProperty(name)) { - var stock = StockMarket[name]; + const stock = StockMarket[name]; if (stock == null) { console.error(`Could not find Stock for ${name}`); continue; } - var symbol = StockSymbols[name]; + const symbol = StockSymbols[name]; SymbolToStockMap[symbol] = stock; } } diff --git a/src/StockMarket/data/TickerHeaderFormatData.ts b/src/StockMarket/data/TickerHeaderFormatData.ts new file mode 100644 index 000000000..e88bf370a --- /dev/null +++ b/src/StockMarket/data/TickerHeaderFormatData.ts @@ -0,0 +1,11 @@ +import { StockSymbols } from "./StockSymbols"; + +export const TickerHeaderFormatData = { + longestName: 0, + longestSymbol: 0, +} + +for (const key in StockSymbols) { + TickerHeaderFormatData.longestName = Math.max(key.length, TickerHeaderFormatData.longestName); + TickerHeaderFormatData.longestSymbol = Math.max(StockSymbols[key].length, TickerHeaderFormatData.longestSymbol); +} diff --git a/src/StockMarket/ui/InfoAndPurchases.tsx b/src/StockMarket/ui/InfoAndPurchases.tsx new file mode 100644 index 000000000..9158eb638 --- /dev/null +++ b/src/StockMarket/ui/InfoAndPurchases.tsx @@ -0,0 +1,222 @@ +/** + * React component for the Stock Market UI. This component displays + * general information about the stock market, buttons for the various purchases, + * and a link to the documentation (Investopedia) + */ +import * as React from "react"; + +import { + getStockMarket4SDataCost, + getStockMarket4STixApiCost +} from "../StockMarketCosts"; + +import { CONSTANTS } from "../../Constants"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { StdButton } from "../../ui/React/StdButton"; +import { StdButtonPurchased } from "../../ui/React/StdButtonPurchased"; + +import { dialogBoxCreate } from "../../../utils/DialogBox"; + +type IProps = { + initStockMarket: () => void; + p: IPlayer; + rerender: () => void; +} + +const blockStyleMarkup = { + display: "block", +} + +export class InfoAndPurchases extends React.Component { + constructor(props: IProps) { + super(props); + + this.handleClick4SMarketDataHelpTip = this.handleClick4SMarketDataHelpTip.bind(this); + this.purchaseWseAccount = this.purchaseWseAccount.bind(this); + this.purchaseTixApiAccess = this.purchaseTixApiAccess.bind(this); + this.purchase4SMarketData = this.purchase4SMarketData.bind(this); + this.purchase4SMarketDataTixApiAccess = this.purchase4SMarketDataTixApiAccess.bind(this); + } + + handleClick4SMarketDataHelpTip() { + dialogBoxCreate( + "Access to the 4S Market Data feed will display two additional pieces " + + "of information about each stock: Price Forecast & Volatility

" + + "Price Forecast indicates the probability the stock has of increasing or " + + "decreasing. A '+' forecast means the stock has a higher chance of increasing " + + "than decreasing, and a '-' means the opposite. The number of '+/-' symbols " + + "is used to illustrate the magnitude of these probabilities. For example, " + + "'+++' means that the stock has a significantly higher chance of increasing " + + "than decreasing, while '+' means that the stock only has a slightly higher chance " + + "of increasing than decreasing.

" + + "Volatility represents the maximum percentage by which a stock's price " + + "can change every tick (a tick occurs every few seconds while the game " + + "is running).

" + + "A stock's price forecast can change over time. This is also affected by volatility. " + + "The more volatile a stock is, the more its price forecast will change." + ); + } + + purchaseWseAccount() { + if (this.props.p.hasWseAccount) { return; } + if (!this.props.p.canAfford(CONSTANTS.WSEAccountCost)) { return; } + this.props.p.hasWseAccount = true; + this.props.initStockMarket(); + this.props.p.loseMoney(CONSTANTS.WSEAccountCost); + this.props.rerender(); + } + + purchaseTixApiAccess() { + if (this.props.p.hasTixApiAccess) { return; } + if (!this.props.p.canAfford(CONSTANTS.TIXAPICost)) { return; } + this.props.p.hasTixApiAccess = true; + this.props.p.loseMoney(CONSTANTS.TIXAPICost); + this.props.rerender(); + } + + purchase4SMarketData() { + if (this.props.p.has4SData) { return; } + if (!this.props.p.canAfford(getStockMarket4SDataCost())) { return; } + this.props.p.has4SData = true; + this.props.p.loseMoney(getStockMarket4SDataCost()); + this.props.rerender(); + } + + purchase4SMarketDataTixApiAccess() { + if (this.props.p.has4SDataTixApi) { return; } + if (!this.props.p.canAfford(getStockMarket4STixApiCost())) { return; } + this.props.p.has4SDataTixApi = true; + this.props.p.loseMoney(getStockMarket4STixApiCost()); + this.props.rerender(); + } + + renderPurchaseWseAccountButton(): React.ReactElement { + if (this.props.p.hasWseAccount) { + return ( + + ) + } else { + const cost = CONSTANTS.WSEAccountCost; + return ( + + ) + } + } + + renderPurchaseTixApiAccessButton(): React.ReactElement { + if (this.props.p.hasTixApiAccess) { + return ( + + ) + } else { + const cost = CONSTANTS.TIXAPICost; + return ( + + ) + } + } + + renderPurchase4SMarketDataButton(): React.ReactElement { + if (this.props.p.has4SData) { + return ( + + ) + } else { + const cost = getStockMarket4SDataCost(); + return ( + + ) + } + } + + renderPurchase4SMarketDataTixApiAccessButton(): React.ReactElement { + if (!this.props.p.hasTixApiAccess) { + return ( + + ) + } else if (this.props.p.has4SDataTixApi) { + return ( + + ) + } else { + const cost = getStockMarket4STixApiCost(); + return ( + + ) + } + } + + render() { + const documentationLink = "https://bitburner.readthedocs.io/en/latest/basicgameplay/stockmarket.html"; + return ( +
+

Welcome to the World Stock Exchange (WSE)!



+

+ To begin trading, you must first purchase an account. +

+ {this.renderPurchaseWseAccountButton()} + + Investopedia + +

Trade Information eXchange (TIX) API

+

+ TIX, short for Trade Information eXchange, is the communications protocol + used by the WSE. Purchasing access to the TIX API lets you write code to create + your own algorithmic/automated trading strategies. +

+ {this.renderPurchaseTixApiAccessButton()} +

Four Sigma (4S) Market Data Feed

+

+ Four Sigma's (4S) Market Data Feed provides information about stocks that will help + your trading strategies. +

+ {this.renderPurchase4SMarketDataButton()} + + {this.renderPurchase4SMarketDataTixApiAccessButton()} +

+ Commission Fees: Every transaction you make has + a {numeralWrapper.formatMoney(CONSTANTS.StockMarketCommission)} commission fee. +

+

+ WARNING: When you reset after installing Augmentations, the Stock + Market is reset. You will retain your WSE Account, access to the + TIX API, and 4S Market Data access. However, all of your stock + positions are lost, so make sure to sell your stocks before + installing Augmentations! +

+
+ ) + } +} diff --git a/src/StockMarket/ui/Root.tsx b/src/StockMarket/ui/Root.tsx new file mode 100644 index 000000000..6d68b27bc --- /dev/null +++ b/src/StockMarket/ui/Root.tsx @@ -0,0 +1,75 @@ +/** + * Root React component for the Stock Market UI + */ +import * as React from "react"; + +import { InfoAndPurchases } from "./InfoAndPurchases"; +import { StockTickers } from "./StockTickers"; + +import { IStockMarket } from "../IStockMarket"; +import { Stock } from "../Stock"; +import { OrderTypes } from "../data/OrderTypes"; +import { PositionTypes } from "../data/PositionTypes"; + +import { IPlayer } from "../../PersonObjects/IPlayer"; + +type txFn = (stock: Stock, shares: number) => boolean; +export type placeOrderFn = (stock: Stock, shares: number, price: number, ordType: OrderTypes, posType: PositionTypes) => boolean; + +type IProps = { + buyStockLong: txFn; + buyStockShort: txFn; + cancelOrder: (params: object) => void; + initStockMarket: () => void; + p: IPlayer; + placeOrder: placeOrderFn; + sellStockLong: txFn; + sellStockShort: txFn; + stockMarket: IStockMarket; +} + +type IState = { + rerenderFlag: boolean; +} + +export class StockMarketRoot extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + rerenderFlag: false, + } + + this.rerender = this.rerender.bind(this); + } + + rerender(): void { + this.setState((prevState) => { + return { + rerenderFlag: !prevState.rerenderFlag, + } + }); + } + + render() { + return ( +
+ + +
+ ) + } +} diff --git a/src/StockMarket/ui/StockTicker.tsx b/src/StockMarket/ui/StockTicker.tsx new file mode 100644 index 000000000..f474cc3ee --- /dev/null +++ b/src/StockMarket/ui/StockTicker.tsx @@ -0,0 +1,343 @@ +/** + * React Component for a single stock ticker in the Stock Market UI + */ +import * as React from "react"; + +import { StockTickerHeaderText } from "./StockTickerHeaderText"; +import { StockTickerOrderList } from "./StockTickerOrderList"; +import { StockTickerPositionText } from "./StockTickerPositionText"; +import { StockTickerTxButton } from "./StockTickerTxButton"; + +import { Order } from "../Order"; +import { Stock } from "../Stock"; +import { OrderTypes } from "../data/OrderTypes"; +import { PositionTypes } from "../data/PositionTypes"; + +import { CONSTANTS } from "../../Constants"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { SourceFileFlags } from "../../SourceFile/SourceFileFlags"; + +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { + yesNoTxtInpBoxClose, + yesNoTxtInpBoxCreate, + yesNoTxtInpBoxGetInput, + yesNoTxtInpBoxGetNoButton, + yesNoTxtInpBoxGetYesButton, +} from "../../../utils/YesNoBox"; + +enum SelectorOrderType { + Market = "Market Order", + Limit = "Limit Order", + Stop = "Stop Order", +} + +export type txFn = (stock: Stock, shares: number) => boolean; +export type placeOrderFn = (stock: Stock, shares: number, price: number, ordType: OrderTypes, posType: PositionTypes) => boolean; + +type IProps = { + buyStockLong: txFn; + buyStockShort: txFn; + cancelOrder: (params: object) => void; + orders: Order[]; + p: IPlayer; + placeOrder: placeOrderFn; + sellStockLong: txFn; + sellStockShort: txFn; + stock: Stock; +} + +type IState = { + orderType: SelectorOrderType; + position: PositionTypes; + qty: string; +} + +export class StockTicker extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + orderType: SelectorOrderType.Market, + position: PositionTypes.Long, + qty: "", + } + + this.handleBuyButtonClick = this.handleBuyButtonClick.bind(this); + this.handleBuyMaxButtonClick = this.handleBuyMaxButtonClick.bind(this); + this.handleOrderTypeChange = this.handleOrderTypeChange.bind(this); + this.handlePositionTypeChange = this.handlePositionTypeChange.bind(this); + this.handleQuantityChange = this.handleQuantityChange.bind(this); + this.handleSellButtonClick = this.handleSellButtonClick.bind(this); + this.handleSellAllButtonClick = this.handleSellAllButtonClick.bind(this); + } + + createPlaceOrderPopupBox(yesTxt: string, popupTxt: string, yesBtnCb: (price: number) => void) { + const yesBtn = yesNoTxtInpBoxGetYesButton(); + const noBtn = yesNoTxtInpBoxGetNoButton(); + + yesBtn!.innerText = yesTxt; + yesBtn!.addEventListener("click", () => { + const price = parseFloat(yesNoTxtInpBoxGetInput()); + if (isNaN(price)) { + dialogBoxCreate(`Invalid input for price: ${yesNoTxtInpBoxGetInput()}`); + return false; + } + + yesBtnCb(price); + yesNoTxtInpBoxClose(); + }); + + noBtn!.innerText = "Cancel Order"; + noBtn!.addEventListener("click", () => { + yesNoTxtInpBoxClose(); + }); + + yesNoTxtInpBoxCreate(popupTxt); + } + + handleBuyButtonClick() { + const shares = parseInt(this.state.qty); + if (isNaN(shares)) { + dialogBoxCreate(`Invalid input for quantity (number of shares): ${this.state.qty}`); + return; + } + + switch (this.state.orderType) { + case SelectorOrderType.Market: { + if (this.state.position === PositionTypes.Short) { + this.props.buyStockShort(this.props.stock, shares); + } else { + this.props.buyStockLong(this.props.stock, shares); + } + break; + } + case SelectorOrderType.Limit: { + this.createPlaceOrderPopupBox( + "Place Buy Limit Order", + "Enter the price for your Limit Order", + (price: number) => { + this.props.placeOrder(this.props.stock, shares, price, OrderTypes.LimitBuy, this.state.position); + } + ); + break; + } + case SelectorOrderType.Stop: { + this.createPlaceOrderPopupBox( + "Place Buy Stop Order", + "Enter the price for your Stop Order", + (price: number) => { + this.props.placeOrder(this.props.stock, shares, price, OrderTypes.StopBuy, this.state.position); + } + ); + break; + } + default: + break; + } + } + + handleBuyMaxButtonClick() { + const playerMoney: number = this.props.p.money.toNumber(); + + const stock = this.props.stock; + let maxShares = Math.floor((playerMoney - CONSTANTS.StockMarketCommission) / this.props.stock.price); + maxShares = Math.min(maxShares, Math.round(stock.maxShares - stock.playerShares - stock.playerShortShares)); + + switch (this.state.orderType) { + case SelectorOrderType.Market: { + if (this.state.position === PositionTypes.Short) { + this.props.buyStockShort(stock, maxShares); + } else { + this.props.buyStockLong(stock, maxShares); + } + break; + } + case SelectorOrderType.Limit: { + this.createPlaceOrderPopupBox( + "Place Buy Limit Order", + "Enter the price for your Limit Order", + (price: number) => { + this.props.placeOrder(stock, maxShares, price, OrderTypes.LimitBuy, this.state.position); + } + ); + break; + } + case SelectorOrderType.Stop: { + this.createPlaceOrderPopupBox( + "Place Buy Stop Order", + "Enter the price for your Stop Order", + (price: number) => { + this.props.placeOrder(stock, maxShares, price, OrderTypes.StopBuy, this.state.position); + } + ) + break; + } + default: + break; + } + } + + handleOrderTypeChange(e: React.ChangeEvent) { + const val = e.target.value; + + // The select value returns a string. Afaik TypeScript doesnt make it easy + // to convert that string back to an enum type so we'll just do this for now + switch (val) { + case SelectorOrderType.Limit: + this.setState({ + orderType: SelectorOrderType.Limit, + }); + break; + case SelectorOrderType.Stop: + this.setState({ + orderType: SelectorOrderType.Stop, + }); + break; + case SelectorOrderType.Market: + default: + this.setState({ + orderType: SelectorOrderType.Market, + }); + } + } + + handlePositionTypeChange(e: React.ChangeEvent) { + const val = e.target.value; + + if (val === "Short") { + this.setState({ + position: PositionTypes.Short, + }); + } else { + this.setState({ + position: PositionTypes.Long, + }); + } + + } + + handleQuantityChange(e: React.ChangeEvent) { + this.setState({ + qty: e.target.value, + }); + } + + handleSellButtonClick() { + const shares = parseInt(this.state.qty); + if (isNaN(shares)) { + dialogBoxCreate(`Invalid input for quantity (number of shares): ${this.state.qty}`); + return; + } + + switch (this.state.orderType) { + case SelectorOrderType.Market: { + if (this.state.position === PositionTypes.Short) { + this.props.sellStockShort(this.props.stock, shares); + } else { + this.props.sellStockLong(this.props.stock, shares); + } + break; + } + case SelectorOrderType.Limit: { + this.createPlaceOrderPopupBox( + "Place Sell Limit Order", + "Enter the price for your Limit Order", + (price: number) => { + this.props.placeOrder(this.props.stock, shares, price, OrderTypes.LimitSell, this.state.position); + } + ); + break; + } + case SelectorOrderType.Stop: { + this.createPlaceOrderPopupBox( + "Place Sell Stop Order", + "Enter the price for your Stop Order", + (price: number) => { + this.props.placeOrder(this.props.stock, shares, price, OrderTypes.StopSell, this.state.position); + } + ) + break; + } + default: + break; + } + } + + handleSellAllButtonClick() { + const stock = this.props.stock; + + switch (this.state.orderType) { + case SelectorOrderType.Market: { + if (this.state.position === PositionTypes.Short) { + this.props.sellStockShort(stock, stock.playerShortShares); + } else { + this.props.sellStockLong(stock, stock.playerShares); + } + break; + } + default: { + dialogBoxCreate(`ERROR: 'Sell All' only works for Market Orders`); + break; + } + } + } + + // Whether the player has access to orders besides market orders (limit/stop) + hasOrderAccess(): boolean { + return (this.props.p.bitNodeN === 8 || (SourceFileFlags[8] >= 3)); + } + + // Whether the player has access to shorting stocks + hasShortAccess(): boolean { + return (this.props.p.bitNodeN === 8 || (SourceFileFlags[8] >= 2)); + } + + render() { + return ( +
  • + +
    + + + + + + + + + + +
    +
  • + ) + } +} diff --git a/src/StockMarket/ui/StockTickerHeaderText.tsx b/src/StockMarket/ui/StockTickerHeaderText.tsx new file mode 100644 index 000000000..9c0b3ef6f --- /dev/null +++ b/src/StockMarket/ui/StockTickerHeaderText.tsx @@ -0,0 +1,41 @@ +/** + * React Component for the text on a stock ticker's header. This text displays + * general information on the stock such as the name, symbol, price, and + * 4S Market Data + */ +import * as React from "react"; + +import { Stock } from "../Stock"; +import { TickerHeaderFormatData } from "../data/TickerHeaderFormatData"; + +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { numeralWrapper } from "../../ui/numeralFormat"; + +type IProps = { + p: IPlayer; + stock: Stock; +} + +export function StockTickerHeaderText(props: IProps): React.ReactElement { + const stock = props.stock; + const p = props.p; + + const stockPriceFormat = numeralWrapper.formatMoney(stock.price); + + let hdrText = `${stock.name}${" ".repeat(1 + TickerHeaderFormatData.longestName - stock.name.length + (TickerHeaderFormatData.longestSymbol - stock.symbol.length))}${stock.symbol} -${" ".repeat(10 - stockPriceFormat.length)}${stockPriceFormat}`; + if (props.p.has4SData) { + hdrText += ` - Volatility: ${numeralWrapper.format(stock.mv, '0,0.00')}% - Price Forecast: `; + hdrText += (stock.b ? "+" : "-").repeat(Math.floor(stock.otlkMag / 10) + 1); + } + + let styleMarkup = { + color: "#66ff33" + }; + if (stock.lastPrice === stock.price) { + styleMarkup.color = "white"; + } else if (stock.lastPrice > stock.price) { + styleMarkup.color = "red"; + } + + return
    {hdrText}
    ; +} diff --git a/src/StockMarket/ui/StockTickerOrder.tsx b/src/StockMarket/ui/StockTickerOrder.tsx new file mode 100644 index 000000000..ff87e7eb0 --- /dev/null +++ b/src/StockMarket/ui/StockTickerOrder.tsx @@ -0,0 +1,42 @@ +/** + * React component for displaying a single order in a stock's order book + */ +import * as React from "react"; + +import { Order } from "../Order"; +import { PositionTypes } from "../data/PositionTypes"; + +import { numeralWrapper } from "../../ui/numeralFormat"; + +type IProps = { + cancelOrder: (params: object) => void; + order: Order; +} + +export class StockTickerOrder extends React.Component { + constructor(props: IProps) { + super(props); + + this.handleCancelOrderClick = this.handleCancelOrderClick.bind(this); + } + + handleCancelOrderClick() { + this.props.cancelOrder({ order: this.props.order }); + } + + render() { + const order = this.props.order; + + const posTxt = order.pos === PositionTypes.Long ? "Long Position" : "Short Position"; + const txt = `${order.type} - ${posTxt} - ${order.shares} @ ${numeralWrapper.formatMoney(order.price)}` + + return ( +
  • + {txt} + +
  • + ) + } +} diff --git a/src/StockMarket/ui/StockTickerOrderList.tsx b/src/StockMarket/ui/StockTickerOrderList.tsx new file mode 100644 index 000000000..beb21f940 --- /dev/null +++ b/src/StockMarket/ui/StockTickerOrderList.tsx @@ -0,0 +1,33 @@ +/** + * React component for displaying a stock's order list in the Stock Market UI. + * This component resides in the stock ticker + */ +import * as React from "react"; + +import { StockTickerOrder } from "./StockTickerOrder"; + +import { Order } from "../Order"; +import { Stock } from "../Stock"; + +import { IPlayer } from "../../PersonObjects/IPlayer"; + +type IProps = { + cancelOrder: (params: object) => void; + orders: Order[]; + p: IPlayer; + stock: Stock; +} + +export class StockTickerOrderList extends React.Component { + render() { + const orders: React.ReactElement[] = []; + for (let i = 0; i < this.props.orders.length; ++i) { + const o = this.props.orders[i]; + orders.push(); + } + + return ( +
      {orders}
    + ) + } +} diff --git a/src/StockMarket/ui/StockTickerPositionText.tsx b/src/StockMarket/ui/StockTickerPositionText.tsx new file mode 100644 index 000000000..36107cb00 --- /dev/null +++ b/src/StockMarket/ui/StockTickerPositionText.tsx @@ -0,0 +1,117 @@ +/** + * React Component for the text on a stock ticker that display's information + * about the player's position in that stock + */ +import * as React from "react"; + +import { Stock } from "../Stock"; + +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { SourceFileFlags } from "../../SourceFile/SourceFileFlags"; + +type IProps = { + p: IPlayer; + stock: Stock; +} + +const blockStyleMarkup = { + display: "block", +} + +export class StockTickerPositionText extends React.Component { + renderLongPosition(): React.ReactElement { + const stock = this.props.stock; + + // Caculate total returns + const totalCost = stock.playerShares * stock.playerAvgPx; + const gains = (stock.price - stock.playerAvgPx) * stock.playerShares; + let percentageGains = gains / totalCost; + if (isNaN(percentageGains)) { percentageGains = 0; } + + return ( +
    +

    + Short Position: + + Shares in the long position will increase in value if the price + of the corresponding stock increases + +

    +

    + Shares: {numeralWrapper.format(stock.playerShares, "0,0")} +

    +

    + Average Price: {numeralWrapper.formatMoney(stock.playerAvgPx)} + (Total Cost: {numeralWrapper.formatMoney(totalCost)}) +

    +

    + Profit: {numeralWrapper.formatMoney(gains)} + ({numeralWrapper.formatPercentage(percentageGains)}) +

    +
    + ) + } + + renderShortPosition(): React.ReactElement | null { + const stock = this.props.stock; + + // Caculate total returns + const totalCost = stock.playerShortShares * stock.playerAvgShortPx; + const gains = (stock.playerAvgShortPx - stock.price) * stock.playerShortShares; + let percentageGains = gains / totalCost; + if (isNaN(percentageGains)) { percentageGains = 0; } + + if (this.props.p.bitNodeN === 8 || (SourceFileFlags[8] >= 2)) { + return ( +
    +

    + Short Position: + + Shares in the short position will increase in value if the + price of the corresponding stock decreases + +

    +

    + Shares: {numeralWrapper.format(stock.playerShortShares, "0,0")} +

    +

    + Average Price: {numeralWrapper.formatMoney(stock.playerAvgShortPx)} + (Total Cost: {numeralWrapper.formatMoney(totalCost)}) +

    +

    + Profit: {numeralWrapper.formatMoney(gains)} + ({numeralWrapper.formatPercentage(percentageGains)}) +

    +
    + ) + } else { + return null; + } + } + + render() { + const stock = this.props.stock; + + return ( +
    +

    + Max Shares: ${numeralWrapper.formatMoney(stock.maxShares)} +

    +

    + Ask Price: {numeralWrapper.formatMoney(stock.getAskPrice())} + + See Investopedia for details on what this is + +

    +

    + + See Investopedia for details on what this is + +

    + {this.renderLongPosition()} + {this.renderShortPosition()} +
    + ) + } +} diff --git a/src/StockMarket/ui/StockTickerTxButton.tsx b/src/StockMarket/ui/StockTickerTxButton.tsx new file mode 100644 index 000000000..e15a730b1 --- /dev/null +++ b/src/StockMarket/ui/StockTickerTxButton.tsx @@ -0,0 +1,18 @@ +/** + * React Component for a button that initiates a transaction on the Stock Market UI + * (Buy, Sell, Buy Max, etc.) + */ +import * as React from "react"; + +type IProps = { + onClick: () => void; + text: string; +} + +export function StockTickerTxButton(props: IProps): React.ReactElement { + return ( + + ) +} diff --git a/src/StockMarket/ui/StockTickers.tsx b/src/StockMarket/ui/StockTickers.tsx new file mode 100644 index 000000000..886bbeaf5 --- /dev/null +++ b/src/StockMarket/ui/StockTickers.tsx @@ -0,0 +1,129 @@ +/** + * React Component for the Stock Market UI. This is the container for all + * of the stock tickers. It also contains the configuration for the + * stock ticker UI (watchlist filter, portfolio vs all mode, etc.) + */ +import * as React from "react"; + +import { StockTicker } from "./StockTicker"; +import { StockTickersConfig, TickerDisplayMode } from "./StockTickersConfig"; + +import { IStockMarket } from "../IStockMarket"; +import { Stock } from "../Stock"; +import { OrderTypes } from "../data/OrderTypes"; +import { PositionTypes } from "../data/PositionTypes"; + +import { IPlayer } from "../../PersonObjects/IPlayer"; + +export type txFn = (stock: Stock, shares: number) => boolean; +export type placeOrderFn = (stock: Stock, shares: number, price: number, ordType: OrderTypes, posType: PositionTypes) => boolean; + +type IProps = { + buyStockLong: txFn; + buyStockShort: txFn; + cancelOrder: (params: object) => void; + p: IPlayer; + placeOrder: placeOrderFn; + sellStockLong: txFn; + sellStockShort: txFn; + stockMarket: IStockMarket; +} + +type IState = { + rerenderFlag: boolean; + tickerDisplayMode: TickerDisplayMode; + watchlistFilter: string; + watchlistSymbols: string[]; +} + +export class StockTickers extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + rerenderFlag: false, + tickerDisplayMode: TickerDisplayMode.AllStocks, + watchlistFilter: "", + watchlistSymbols: [], + } + + this.changeDisplayMode = this.changeDisplayMode.bind(this); + this.changeWatchlistFilter = this.changeWatchlistFilter.bind(this); + } + + changeDisplayMode() { + if (this.state.tickerDisplayMode === TickerDisplayMode.AllStocks) { + this.setState({ + tickerDisplayMode: TickerDisplayMode.Portfolio, + }); + } else { + this.setState({ + tickerDisplayMode: TickerDisplayMode.AllStocks, + }); + } + } + + changeWatchlistFilter(e: React.ChangeEvent) { + const watchlist = e.target.value; + const sanitizedWatchlist = watchlist.replace(/\s/g, ''); + + this.setState({ + watchlistFilter: watchlist, + }); + + if (sanitizedWatchlist !== "") { + this.setState({ + watchlistSymbols: sanitizedWatchlist.split(","), + }); + } + } + + rerender() { + this.setState((prevState) => { + return { + rerenderFlag: !prevState.rerenderFlag, + } + }); + } + + render() { + const tickers: React.ReactElement[] = []; + for (const stockMarketProp in this.props.stockMarket) { + const val = this.props.stockMarket[stockMarketProp]; + if (val instanceof Stock) { + let orders = this.props.stockMarket.Orders[val.symbol]; + if (orders == null) { + orders = []; + } + + tickers.push( + + ) + } + } + + return ( +
    + + +
      + {tickers} +
    +
    + ) + } +} diff --git a/src/StockMarket/ui/StockTickersConfig.tsx b/src/StockMarket/ui/StockTickersConfig.tsx new file mode 100644 index 000000000..8af3891e6 --- /dev/null +++ b/src/StockMarket/ui/StockTickersConfig.tsx @@ -0,0 +1,61 @@ +/** + * React component for the tickers configuration section of the Stock Market UI. + * This config lets you change the way stock tickers are displayed (watchlist, + * all/portoflio mode, etc) + */ +import * as React from "react"; + +import { StdButton } from "../../ui/React/StdButton"; + +export enum TickerDisplayMode { + AllStocks, + Portfolio, +} + +type IProps = { + changeDisplayMode: () => void; + changeWatchlistFilter: (e: React.ChangeEvent) => void; + tickerDisplayMode: TickerDisplayMode; +} + +export class StockTickersConfig extends React.Component { + constructor(props: IProps) { + super(props); + } + + renderDisplayModeButton() { + let txt: string = ""; + let tooltip: string = ""; + if (this.props.tickerDisplayMode === TickerDisplayMode.Portfolio) { + txt = "Switch to 'All Stocks' Mode"; + tooltip = "Displays all stocks on the WSE"; + } else { + txt = "Switch to 'Portfolio' Mode"; + tooltip = "Displays only the stocks for which you have shares or orders"; + } + + return ( + + ) + } + + render() { + return ( +
    + {this.renderDisplayModeButton()} + + +
    + ) + } +} diff --git a/src/index.html b/src/index.html index 950d821c3..5bce3adcd 100644 --- a/src/index.html +++ b/src/index.html @@ -290,53 +290,7 @@ if (htmlWebpackPlugin.options.googleAnalytics.trackingId) { %>
    -

    - Welcome to the World Stock Exchange (WSE)!

    - - To begin trading, you must first purchase an account. WSE accounts will persist - after you 'reset' by installing Augmentations. -

    - Buy WSE Account - Investopedia - -

    Trade Information eXchange (TIX) API

    -

    - TIX, short for Trade Information eXchange, is the communications protocol supported by the WSE. - Purchasing access to the TIX API lets you write code to create your own algorithmic/automated - trading strategies. -

    - If you purchase access to the TIX API, you will retain that access even after - you 'reset' by installing Augmentations. -

    - Buy Trade Information eXchange (TIX) API Access - -

    Four Sigma (4S) Market Data Feed

    -

    - Four Sigma's (4S) Market Data Feed provides information about stocks - that will help your trading strategies. -

    - If you purchase access to 4S Market Data and/or the 4S TIX API, you will - retain that access even after you 'reset' by installing Augmentations. -

    - - - Buy 4S Market Data Feed - -
    ?
    - - - Buy 4S Market Data TIX API Access - - -

    - - Expand tickers - Collapse tickers -

    - - Update Watchlist -
      -
    +
    diff --git a/src/ui/React/StdButtonPurchased.tsx b/src/ui/React/StdButtonPurchased.tsx index 40e5ed7ce..db041af0a 100644 --- a/src/ui/React/StdButtonPurchased.tsx +++ b/src/ui/React/StdButtonPurchased.tsx @@ -7,13 +7,36 @@ interface IStdButtonPurchasedProps { onClick?: (e: React.MouseEvent) => any; style?: object; text: string; + tooltip?: string; +} + +type IInnerHTMLMarkup = { + __html: string; } export class StdButtonPurchased extends React.Component { render() { + const hasTooltip = this.props.tooltip != null && this.props.tooltip !== ""; + let className = "std-button-bought"; + if (hasTooltip) { + className += " tooltip"; + } + + // Tooltip will be set using inner HTML + let tooltipMarkup: IInnerHTMLMarkup | null; + if (hasTooltip) { + tooltipMarkup = { + __html: this.props.tooltip! + } + } + return ( - ) }