From 634c0af85b22edfb737b2710c3fb549b07cff39b Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Wed, 8 Dec 2021 08:30:29 -0800 Subject: [PATCH] Finish porting simulator into React (#1) * Preview bid results; toggle bid type * Code cleanup: move hooks to where they're used * Extract header to separate component * Fix & reactify according to James's review * Remove unnecessary useMemo * Hack Chartjs type * Add some notes on DX Todos * Move non-page elements to lib/ --- web/README.md | 5 + web/components/header.tsx | 52 +++++++ web/components/hero.tsx | 51 +------ web/{pages => lib}/simulator/entries.ts | 35 +++-- web/{pages => lib}/simulator/sample-bids.ts | 0 web/pages/simulator/index.tsx | 154 +++++++++++--------- 6 files changed, 166 insertions(+), 131 deletions(-) create mode 100644 web/components/header.tsx rename web/{pages => lib}/simulator/entries.ts (60%) rename web/{pages => lib}/simulator/sample-bids.ts (100%) diff --git a/web/README.md b/web/README.md index 9902ae97..ff9d1ec0 100644 --- a/web/README.md +++ b/web/README.md @@ -11,3 +11,8 @@ Before committing, run `npm run format` to format your code. Recommended: Use a [Prettier editor integration](https://prettier.io/docs/en/editors.html) to automatically format on save + +## Developer Experience TODOs + +- Automatically run prettier as code commit hook? +- Prevent git pushing if there are Typescript errors? diff --git a/web/components/header.tsx b/web/components/header.tsx new file mode 100644 index 00000000..f2231d9d --- /dev/null +++ b/web/components/header.tsx @@ -0,0 +1,52 @@ +import { Popover } from '@headlessui/react' +import Link from 'next/link' + +const navigation = [ + { + name: 'About', + href: 'https://mantic.notion.site/About-Mantic-Markets-09bdde9044614e62a27477b4b1bf77ea', + }, + { name: 'Simulator', href: '/simulator' }, +] + +export function Header() { + return ( + +
+ +
+
+ ) +} diff --git a/web/components/hero.tsx b/web/components/hero.tsx index 07df912f..e218d696 100644 --- a/web/components/hero.tsx +++ b/web/components/hero.tsx @@ -1,57 +1,10 @@ -import { Popover } from '@headlessui/react' import { ConvertKitEmailForm } from './convert-kit-email-form' - -const navigation = [ - { - name: 'About', - href: 'https://mantic.notion.site/About-Mantic-Markets-09bdde9044614e62a27477b4b1bf77ea', - }, - { name: 'Simulator', href: 'https://simulator.mantic.markets' }, -] +import { Header } from './header' export const Hero = () => { return (
- {/*
*/} - - -
- -
-
- +
diff --git a/web/pages/simulator/entries.ts b/web/lib/simulator/entries.ts similarity index 60% rename from web/pages/simulator/entries.ts rename to web/lib/simulator/entries.ts index 6f9103bf..6e3e587f 100644 --- a/web/pages/simulator/entries.ts +++ b/web/lib/simulator/entries.ts @@ -13,8 +13,8 @@ export type Entry = { prob: number } -export function makeEntries(bids: Bid[]): Entry[] { - const entries: Entry[] = [] +function makeWeights(bids: Bid[]) { + const weights = [] let yesPot = 0 let noPot = 0 // First pass: calculate all the weights @@ -28,33 +28,36 @@ export function makeEntries(bids: Bid[]): Entry[] { noPot += noBid const prob = yesPot / (yesPot + noPot) - entries.push({ + weights.push({ yesBid, noBid, yesWeight, noWeight, prob, - // To be filled in below - yesPayout: 0, - noPayout: 0, - yesReturn: 0, - noReturn: 0, }) } + return weights +} +export function makeEntries(bids: Bid[]): Entry[] { const YES_SEED = bids[0].yesBid const NO_SEED = bids[0].noBid - const yesWeightsSum = entries.reduce((sum, entry) => sum + entry.yesWeight, 0) - const noWeightsSum = entries.reduce((sum, entry) => sum + entry.noWeight, 0) + const weights = makeWeights(bids) + const yesPot = weights.reduce((sum, { yesBid }) => sum + yesBid, 0) + const noPot = weights.reduce((sum, { noBid }) => sum + noBid, 0) + const yesWeightsSum = weights.reduce((sum, entry) => sum + entry.yesWeight, 0) + const noWeightsSum = weights.reduce((sum, entry) => sum + entry.noWeight, 0) // Second pass: calculate all the payouts - for (const entry of entries) { - const { yesBid, noBid, yesWeight, noWeight } = entry + const entries: Entry[] = [] + for (const weight of weights) { + const { yesBid, noBid, yesWeight, noWeight } = weight // Payout: You get your initial bid back, as well as your share of the // (noPot - seed) according to your yesWeight - entry.yesPayout = yesBid + (yesWeight / yesWeightsSum) * (noPot - NO_SEED) - entry.noPayout = noBid + (noWeight / noWeightsSum) * (yesPot - YES_SEED) - entry.yesReturn = (entry.yesPayout - yesBid) / yesBid - entry.noReturn = (entry.noPayout - noBid) / noBid + const yesPayout = yesBid + (yesWeight / yesWeightsSum) * (noPot - NO_SEED) + const noPayout = noBid + (noWeight / noWeightsSum) * (yesPot - YES_SEED) + const yesReturn = (yesPayout - yesBid) / yesBid + const noReturn = (noPayout - noBid) / noBid + entries.push({ ...weight, yesPayout, noPayout, yesReturn, noReturn }) } return entries } diff --git a/web/pages/simulator/sample-bids.ts b/web/lib/simulator/sample-bids.ts similarity index 100% rename from web/pages/simulator/sample-bids.ts rename to web/lib/simulator/sample-bids.ts diff --git a/web/pages/simulator/index.tsx b/web/pages/simulator/index.tsx index eda62127..e2c4c253 100644 --- a/web/pages/simulator/index.tsx +++ b/web/pages/simulator/index.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useEffect, useMemo, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { CategoryScale, Chart, @@ -11,8 +11,8 @@ import { } from 'chart.js' import { ChartData } from 'chart.js' import { Line } from 'react-chartjs-2' -import { bids as sampleBids } from './sample-bids' -import { Entry, makeEntries } from './entries' +import { bids as sampleBids } from '../../lib/simulator/sample-bids' +import { Entry, makeEntries } from '../../lib/simulator/entries' // Auto import doesn't work for some reason... // So we manually register ChartJS components instead: @@ -26,97 +26,136 @@ Chart.register( Legend ) -function toTable(entries: Entry[]) { - return entries.map((entry, i) => { - return ( - - {i + 1} - {toRowStart(entry)} - {toRowEnd(entry)} - - ) - }) +function TableBody(props: { entries: Entry[] }) { + return ( + + {props.entries.map((entry, i) => ( + + {i + 1} + + + + ))} + + ) } -function toRowStart(entry: Entry) { +function TableRowStart(props: { entry: Entry }) { + const { entry } = props if (entry.yesBid && entry.noBid) { return ( - + <>
SEED
{entry.yesBid} / {entry.noBid} -
+ ) } else if (entry.yesBid) { return ( - + <>
YES
{entry.yesBid} -
+ ) - } else if (entry.noBid) { + } else { return ( - + <>
NO
{entry.noBid} -
+ ) } } -function toRowEnd(entry: Entry) { - if (!entry.yesBid && !entry.noBid) { +function TableRowEnd(props: { entry: Entry | null }) { + const { entry } = props + if (!entry) { return ( - + <> N/A N/A N/A N/A - + ) } else if (entry.yesBid && entry.noBid) { return ( - + <> N/A {entry.prob.toFixed(2)} N/A N/A - + ) } else if (entry.yesBid) { return ( - + <> {entry.yesWeight.toFixed(2)} {entry.prob.toFixed(2)} {entry.yesPayout.toFixed(2)} {(entry.yesReturn * 100).toFixed(2)}% - + ) } else { return ( - + <> {entry.noWeight.toFixed(2)} {entry.prob.toFixed(2)} {entry.noPayout.toFixed(2)} {(entry.noReturn * 100).toFixed(2)}% - + ) } } -function newBidTable( - steps: number, - newBid: number, - setNewBid: (newBid: number) => void, - submitBid: () => void -) { +function NewBidTable(props: { + steps: number + bids: any[] + setSteps: (steps: number) => void + setBids: (bids: any[]) => void +}) { + const { steps, bids, setSteps, setBids } = props + // Prepare for new bids + const [newBid, setNewBid] = useState(0) + const [newBidType, setNewBidType] = useState('YES') + + function makeBid(type: string, bid: number) { + return { + yesBid: type == 'YES' ? bid : 0, + noBid: type == 'YES' ? 0 : bid, + } + } + + function submitBid() { + if (newBid <= 0) return + const bid = makeBid(newBidType, newBid) + bids.splice(steps, 0, bid) + setBids(bids) + setSteps(steps + 1) + setNewBid(0) + } + + function toggleBidType() { + setNewBidType(newBidType === 'YES' ? 'NO' : 'YES') + } + + const nextEntry = useMemo(() => { + if (newBid) { + const nextBid = makeBid(newBidType, newBid) + const fakeBids = [...bids.slice(0, steps), nextBid] + const entries = makeEntries(fakeBids) + return entries[entries.length - 1] + } + return null + }, [newBid, newBidType, steps]) + return ( @@ -137,16 +176,20 @@ function newBidTable( - {/* */} + - {toTable(entries)} + +
YES

NO
@@ -166,7 +209,7 @@ function newBidTable( onFocus={(e) => e.target.select()} />
Return
@@ -277,7 +299,7 @@ export default function Simulator() { Probability of
YES
- +