From bc042446221a806952ce3186a2cb90a9a675049b Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Fri, 8 Sep 2023 16:20:14 -0700 Subject: [PATCH] added preview view (#2030) * added preview view --- web/components/preview/preview-bet-button.tsx | 27 + web/components/preview/preview-bet-panel.tsx | 432 ++++++++++++++ .../preview/preview-contract-overview.tsx | 355 +++++++++++ .../preview/preview-contract-price.tsx | 164 ++++++ .../preview/preview-yes-no-selector.tsx | 198 +++++++ .../preview/[username]/[contractSlug].tsx | 549 ++++++++++++++++++ 6 files changed, 1725 insertions(+) create mode 100644 web/components/preview/preview-bet-button.tsx create mode 100644 web/components/preview/preview-bet-panel.tsx create mode 100644 web/components/preview/preview-contract-overview.tsx create mode 100644 web/components/preview/preview-contract-price.tsx create mode 100644 web/components/preview/preview-yes-no-selector.tsx create mode 100644 web/pages/preview/[username]/[contractSlug].tsx diff --git a/web/components/preview/preview-bet-button.tsx b/web/components/preview/preview-bet-button.tsx new file mode 100644 index 0000000000..a19cc7a1b7 --- /dev/null +++ b/web/components/preview/preview-bet-button.tsx @@ -0,0 +1,27 @@ +import { + BinaryContract, + CPMMBinaryContract, + PseudoNumericContract, + StonkContract, +} from 'common/contract' +import { UserBetsSummary } from 'web/components/bet/bet-summary' +import { User } from 'web/lib/firebase/users' +import { Col } from '../layout/col' +import { PreviewBuyPanel } from './preview-bet-panel' + +export function PreviewSignedInBinaryMobileBetting(props: { + contract: BinaryContract | PseudoNumericContract | StonkContract + user: User | null | undefined +}) { + const { contract, user } = props + + return ( + + + + ) +} diff --git a/web/components/preview/preview-bet-panel.tsx b/web/components/preview/preview-bet-panel.tsx new file mode 100644 index 0000000000..b76cae8cee --- /dev/null +++ b/web/components/preview/preview-bet-panel.tsx @@ -0,0 +1,432 @@ +import { CheckIcon } from '@heroicons/react/solid' +import clsx from 'clsx' +import { sumBy } from 'lodash' +import { useEffect, useState } from 'react' +import toast from 'react-hot-toast' + +import { LimitBet } from 'common/bet' +import { + CPMMBinaryContract, + CPMMMultiContract, + PseudoNumericContract, + StonkContract, +} from 'common/contract' +import { computeCpmmBet } from 'common/new-bet' +import { + formatLargeNumber, + formatMoney, + formatOutcomeLabel, + formatPercent, +} from 'common/util/format' +import { APIError, placeBet } from 'web/lib/firebase/api' +import { User, firebaseLogin } from 'web/lib/firebase/users' +import { Col } from '../layout/col' +import { Row } from '../layout/row' +import { BuyAmountInput } from '../widgets/amount-input' + +import { Answer } from 'common/answer' +import { getCpmmProbability } from 'common/calculate-cpmm' +import { calculateCpmmMultiArbitrageBet } from 'common/calculate-cpmm-arbitrage' +import { getFormattedMappedValue, getMappedValue } from 'common/pseudo-numeric' +import { STONK_NO, STONK_YES, getStonkDisplayShares } from 'common/stonk' +import { SINGULAR_BET } from 'common/user' +import { removeUndefinedProps } from 'common/util/object' +import { InfoTooltip } from 'web/components/widgets/info-tooltip' +import { useFocus } from 'web/hooks/use-focus' +import { track, withTracking } from 'web/lib/service/analytics' +import { isAndroid, isIOS } from 'web/lib/util/device' +import { useUnfilledBetsAndBalanceByUserId } from '../../hooks/use-bets' +import { Button } from '../buttons/button' +import { WarningConfirmationButton } from '../buttons/warning-confirmation-button' +import { PreviewYesNoSelector } from './preview-yes-no-selector' + +export type binaryOutcomes = 'YES' | 'NO' | undefined + +export function PreviewBuyPanel(props: { + contract: + | CPMMBinaryContract + | PseudoNumericContract + | StonkContract + | CPMMMultiContract + multiProps?: { answers: Answer[]; answerToBuy: Answer } + user: User | null | undefined + inModal: boolean + onBuySuccess?: () => void + singularView?: 'YES' | 'NO' | 'LIMIT' + initialOutcome?: binaryOutcomes | 'LIMIT' + location?: string +}) { + const { + contract, + multiProps, + user, + onBuySuccess, + singularView, + initialOutcome, + location = 'bet panel', + inModal, + } = props + + const isCpmmMulti = contract.mechanism === 'cpmm-multi-1' + if (isCpmmMulti && !multiProps) { + throw new Error('multiProps must be defined for cpmm-multi-1') + } + const shouldAnswersSumToOne = + 'shouldAnswersSumToOne' in contract ? contract.shouldAnswersSumToOne : false + + const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' + const isStonk = contract.outcomeType === 'STONK' + const [option, setOption] = useState(initialOutcome) + const { unfilledBets: allUnfilledBets, balanceByUserId } = + useUnfilledBetsAndBalanceByUserId(contract.id) + + const unfilledBetsMatchingAnswer = allUnfilledBets.filter( + (b) => b.answerId === multiProps?.answerToBuy?.id + ) + const unfilledBets = + isCpmmMulti && !shouldAnswersSumToOne + ? // Always filter to answer for non-sum-to-one cpmm multi + unfilledBetsMatchingAnswer + : allUnfilledBets + + const outcome = option === 'LIMIT' ? undefined : option + const seeLimit = option === 'LIMIT' + + const [betAmount, setBetAmount] = useState(10) + const [error, setError] = useState() + const [isSubmitting, setIsSubmitting] = useState(false) + + const [inputRef, focusAmountInput] = useFocus() + + useEffect(() => { + if (initialOutcome) { + setOption(initialOutcome) + } + }, [initialOutcome]) + + function onOptionChoice(choice: 'YES' | 'NO' | 'LIMIT') { + if (option === choice && !initialOutcome) { + setOption(undefined) + } else { + setOption(choice) + } + if (!isIOS() && !isAndroid()) { + focusAmountInput() + } + } + + function onBetChange(newAmount: number | undefined) { + setBetAmount(newAmount) + if (!outcome) { + setOption('YES') + } + } + + async function submitBet() { + if (!user || !betAmount) return + + setError(undefined) + setIsSubmitting(true) + placeBet( + removeUndefinedProps({ + outcome, + amount: betAmount, + contractId: contract.id, + answerId: multiProps?.answerToBuy.id, + }) + ) + .then((r) => { + console.log('placed bet. Result:', r) + setIsSubmitting(false) + setBetAmount(undefined) + if (onBuySuccess) onBuySuccess() + else { + toast('Trade submitted!', { + icon: , + }) + } + }) + .catch((e) => { + if (e instanceof APIError) { + setError(e.toString()) + } else { + console.error(e) + setError('Error placing bet') + } + setIsSubmitting(false) + }) + + track('bet', { + location, + outcomeType: contract.outcomeType, + slug: contract.slug, + contractId: contract.id, + amount: betAmount, + outcome, + isLimitOrder: false, + answerId: multiProps?.answerToBuy.id, + }) + } + + const betDisabled = + isSubmitting || !betAmount || !!error || outcome === undefined + + let currentPayout: number + let probBefore: number + let probAfter: number + if (isCpmmMulti && multiProps && contract.shouldAnswersSumToOne) { + const { answers, answerToBuy } = multiProps + const { newBetResult } = calculateCpmmMultiArbitrageBet( + answers, + answerToBuy, + outcome ?? 'YES', + betAmount ?? 0, + undefined, + unfilledBets, + balanceByUserId + ) + const { pool, p } = newBetResult.cpmmState + currentPayout = sumBy(newBetResult.takers, 'shares') + probBefore = answerToBuy.prob + probAfter = getCpmmProbability(pool, p) + } else { + const cpmmState = isCpmmMulti + ? { + pool: { + YES: multiProps!.answerToBuy.poolYes, + NO: multiProps!.answerToBuy.poolNo, + }, + p: 0.5, + } + : { pool: contract.pool, p: contract.p } + + const result = computeCpmmBet( + cpmmState, + outcome ?? 'YES', + betAmount ?? 0, + undefined, + unfilledBets, + balanceByUserId + ) + currentPayout = result.shares + probBefore = result.probBefore + probAfter = result.probAfter + } + + const probStayedSame = formatPercent(probAfter) === formatPercent(probBefore) + const probChange = Math.abs(probAfter - probBefore) + const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0 + const currentReturnPercent = formatPercent(currentReturn) + + const rawDifference = Math.abs( + getMappedValue(contract, probAfter) - getMappedValue(contract, probBefore) + ) + const displayedDifference = isPseudoNumeric + ? formatLargeNumber(rawDifference) + : formatPercent(rawDifference) + + const bankrollFraction = (betAmount ?? 0) / (user?.balance ?? 1e9) + + // warnings + const highBankrollSpend = + (betAmount ?? 0) >= 100 && bankrollFraction >= 0.5 && bankrollFraction <= 1 + const highProbMove = + (betAmount ?? 0) > 10 && probChange > 0.299 && bankrollFraction <= 1 + + const warning = highBankrollSpend + ? `You might not want to spend ${formatPercent( + bankrollFraction + )} of your balance on a single trade. \n\nCurrent balance: ${formatMoney( + user?.balance ?? 0 + )}` + : highProbMove + ? `Are you sure you want to move the probability by ${displayedDifference}?` + : undefined + + const displayError = !!outcome + const selected = seeLimit ? 'LIMIT' : outcome + + return ( + + + { + onOptionChoice(choice) + }} + yesLabel={isPseudoNumeric ? 'HIGHER' : isStonk ? STONK_YES : 'YES'} + noLabel={isPseudoNumeric ? 'LOWER' : isStonk ? STONK_NO : 'NO'} + /> + {/* {!!user && !isStonk && ( + + )} */} + + +
Amount
+ + + + + + + {isPseudoNumeric || isStonk ? ( + 'Shares' + ) : ( + <>Payout if {outcome ?? 'YES'} + )} + +
+ + {isStonk + ? getStonkDisplayShares(contract, currentPayout, 2) + : isPseudoNumeric + ? Math.floor(currentPayout) + : formatMoney(currentPayout)} + + + {isStonk || isPseudoNumeric ? '' : ' +' + currentReturnPercent} + +
+ + + + + {isPseudoNumeric + ? 'Estimated value' + : isStonk + ? 'New stock price' + : 'New probability'} + + {!isPseudoNumeric && !isStonk && ( + + )} + + {probStayedSame ? ( +
+ {getFormattedMappedValue(contract, probBefore)} +
+ ) : ( +
+ + {getFormattedMappedValue(contract, probAfter)} + + + {isPseudoNumeric ? ( + <> + ) : ( + <> + {' '} + {outcome != 'NO' && '+'} + {getFormattedMappedValue( + contract, + probAfter - probBefore + )} + + )} + +
+ )} + +
+ + {user ? ( + + ) : ( + + )} + + {/* +