diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index 8fffd844..2390ffc6 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -6,7 +6,6 @@ import { Contract, contractPath, getBinaryProbPercent, - listenForContract, } from 'web/lib/firebase/contracts' import { Col } from '../layout/col' import { @@ -26,8 +25,7 @@ import { import { getOutcomeProbability, getTopAnswer } from 'common/calculate' import { AvatarDetails, MiscDetails } from './contract-details' import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm' -import { useEffect, useState } from 'react' -import { QuickBet, QuickOutcomeView, ProbBar, getColor } from './quick-bet' +import { QuickBet, ProbBar, getColor } from './quick-bet' import { useContractWithPreload } from 'web/hooks/use-contract' export function ContractCard(props: { @@ -54,7 +52,7 @@ export function ContractCard(props: { className )} > - +
) : ( - + {outcomeType === 'BINARY' && ( + + )} + + {outcomeType === 'NUMERIC' && ( + + )} + + {outcomeType === 'FREE_RESPONSE' && ( + } + truncate="long" + /> + )} + )} -
) @@ -106,9 +124,8 @@ export function BinaryResolutionOrChance(props: { contract: FullContract large?: boolean className?: string - hideText?: boolean }) { - const { contract, large, className, hideText } = props + const { contract, large, className } = props const { resolution } = contract const textColor = `text-${getColor(contract)}` @@ -129,11 +146,9 @@ export function BinaryResolutionOrChance(props: { ) : ( <>
{getBinaryProbPercent(contract)}
- {!hideText && ( -
- chance -
- )} +
+ chance +
)} @@ -162,9 +177,8 @@ export function FreeResponseResolutionOrChance(props: { contract: FreeResponseContract truncate: 'short' | 'long' | 'none' className?: string - hideText?: boolean }) { - const { contract, truncate, className, hideText } = props + const { contract, truncate, className } = props const { resolution } = contract const topAnswer = getTopAnswer(contract) @@ -189,7 +203,7 @@ export function FreeResponseResolutionOrChance(props: {
{formatPercent(getOutcomeProbability(contract, topAnswer.id))}
- {!hideText &&
chance
} +
chance
) @@ -201,9 +215,8 @@ export function FreeResponseResolutionOrChance(props: { export function NumericResolutionOrExpectation(props: { contract: NumericContract className?: string - hideText?: boolean }) { - const { contract, className, hideText } = props + const { contract, className } = props const { resolution } = contract const textColor = `text-${getColor(contract)}` @@ -222,9 +235,7 @@ export function NumericResolutionOrExpectation(props: {
{formatLargeNumber(getExpectedValue(contract))}
- {!hideText && ( -
expected
- )} +
expected
)} diff --git a/web/components/contract/quick-bet.tsx b/web/components/contract/quick-bet.tsx index 35fcad50..58f31e67 100644 --- a/web/components/contract/quick-bet.tsx +++ b/web/components/contract/quick-bet.tsx @@ -1,5 +1,9 @@ import clsx from 'clsx' -import { getOutcomeProbability, getTopAnswer } from 'common/calculate' +import { + getOutcomeProbability, + getOutcomeProbabilityAfterBet, + getTopAnswer, +} from 'common/calculate' import { getExpectedValue } from 'common/calculate-dpm' import { Contract, @@ -8,55 +12,81 @@ import { DPM, Binary, NumericContract, - FreeResponse, + FreeResponseContract, } from 'common/contract' -import { formatMoney } from 'common/util/format' +import { + formatLargeNumber, + formatMoney, + formatPercent, +} from 'common/util/format' +import { useState } from 'react' import toast from 'react-hot-toast' import { useUser } from 'web/hooks/use-user' import { useUserContractBets } from 'web/hooks/use-user-bets' import { placeBet } from 'web/lib/firebase/api-call' -import { getBinaryProb } from 'web/lib/firebase/contracts' +import { getBinaryProb, getBinaryProbPercent } from 'web/lib/firebase/contracts' import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon' import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon' import { Col } from '../layout/col' import { OUTCOME_TO_COLOR } from '../outcome-label' import { useSaveShares } from '../use-save-shares' -import { - BinaryResolutionOrChance, - NumericResolutionOrExpectation, - FreeResponseResolutionOrChance, -} from './contract-card' + +const BET_SIZE = 10 export function QuickBet(props: { contract: Contract }) { const { contract } = props const user = useUser() const userBets = useUserContractBets(user?.id, contract.id) - const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares( + const { yesFloorShares, noFloorShares } = useSaveShares( contract as FullContract, userBets ) - // TODO: For some reason, Floor Shares are inverted for non-BINARY markets + // TODO: This relies on a hack in useSaveShares, where noFloorShares includes + // all non-YES shares. Ideally, useSaveShares should group by all outcomes const hasUpShares = contract.outcomeType === 'BINARY' ? yesFloorShares : noFloorShares const hasDownShares = contract.outcomeType === 'BINARY' ? noFloorShares : yesFloorShares - const color = getColor(contract) + const [upHover, setUpHover] = useState(false) + const [downHover, setDownHover] = useState(false) + + let previewProb = undefined + try { + previewProb = upHover + ? getOutcomeProbabilityAfterBet( + contract, + quickOutcome(contract, 'UP') || '', + BET_SIZE + ) + : downHover + ? 1 - + getOutcomeProbabilityAfterBet( + contract, + quickOutcome(contract, 'DOWN') || '', + BET_SIZE + ) + : undefined + } catch (e) { + // Catch any errors from hovering on an invalid option + } + + const color = getColor(contract, previewProb) async function placeQuickBet(direction: 'UP' | 'DOWN') { const betPromise = async () => { const outcome = quickOutcome(contract, direction) return await placeBet({ - amount: 10, + amount: BET_SIZE, outcome, contractId: contract.id, }) } const shortQ = contract.question.slice(0, 20) toast.promise(betPromise(), { - loading: `${formatMoney(10)} on "${shortQ}"...`, - success: `${formatMoney(10)} on "${shortQ}"...`, + loading: `${formatMoney(BET_SIZE)} on "${shortQ}"...`, + success: `${formatMoney(BET_SIZE)} on "${shortQ}"...`, error: (err) => `${err.message}`, }) } @@ -68,7 +98,7 @@ export function QuickBet(props: { contract: Contract }) { if (contract.outcomeType === 'FREE_RESPONSE') { // TODO: Implement shorting of free response answers if (direction === 'DOWN') { - throw new Error("Can't short free response answers") + throw new Error("Can't bet against free response answers") } return getTopAnswer(contract)?.id } @@ -81,18 +111,19 @@ export function QuickBet(props: { contract: Contract }) { return ( {/* Up bet triangle */}
setUpHover(true)} + onMouseLeave={() => setUpHover(false)} onClick={() => placeQuickBet('UP')} - >
+ />
{formatMoney(10)}
@@ -101,31 +132,43 @@ export function QuickBet(props: { contract: Contract }) { ) : ( - + )}
- + {/* Down bet triangle */}
setDownHover(true)} + onMouseLeave={() => setDownHover(false)} onClick={() => placeQuickBet('DOWN')} >
{hasDownShares > 0 ? ( ) : ( - + )}
{formatMoney(10)} @@ -135,10 +178,10 @@ export function QuickBet(props: { contract: Contract }) { ) } -export function ProbBar(props: { contract: Contract }) { - const { contract } = props - const color = getColor(contract) - const prob = getProb(contract) +export function ProbBar(props: { contract: Contract; previewProb?: number }) { + const { contract, previewProb } = props + const color = getColor(contract, previewProb) + const prob = previewProb ?? getProb(contract) return ( <>
+ />
+ /> ) } -export function QuickOutcomeView(props: { contract: Contract }) { - const { contract } = props +function QuickOutcomeView(props: { + contract: Contract + previewProb?: number + caption?: 'chance' | 'expected' +}) { + const { contract, previewProb, caption } = props const { outcomeType } = contract + // If there's a preview prob, display that instead of the current prob + const override = + previewProb === undefined ? undefined : formatPercent(previewProb) + const textColor = `text-${getColor(contract, previewProb)}` + + let display: string | undefined + switch (outcomeType) { + case 'BINARY': + display = getBinaryProbPercent(contract) + break + case 'NUMERIC': + display = formatLargeNumber(getExpectedValue(contract as NumericContract)) + break + case 'FREE_RESPONSE': + const topAnswer = getTopAnswer(contract as FreeResponseContract) + display = + topAnswer && + formatPercent(getOutcomeProbability(contract, topAnswer.id)) + break + } + return ( - <> - {outcomeType === 'BINARY' && ( - - )} - - {outcomeType === 'NUMERIC' && ( - - )} - - {outcomeType === 'FREE_RESPONSE' && ( - } - truncate="long" - hideText - /> - )} - + + {override ?? display} + {caption &&
{caption}
} + + ) } @@ -215,15 +262,14 @@ function getNumericScale(contract: NumericContract) { return (ev - min) / (max - min) } -export function getColor(contract: Contract) { +export function getColor(contract: Contract, previewProb?: number) { // TODO: Not sure why eg green-400 doesn't work here; try upgrading Tailwind // TODO: Try injecting a gradient here // return 'primary' const { resolution } = contract if (resolution) { return ( - // @ts-ignore; TODO: Have better typing for contract.resolution? - OUTCOME_TO_COLOR[resolution] || + OUTCOME_TO_COLOR[resolution as 'YES' | 'NO' | 'CANCEL' | 'MKT'] ?? // If resolved to a FR answer, use 'primary' 'primary' ) @@ -233,9 +279,6 @@ export function getColor(contract: Contract) { } const marketClosed = (contract.closeTime || Infinity) < Date.now() - return marketClosed - ? 'gray-400' - : getProb(contract) >= 0.5 - ? 'primary' - : 'red-400' + const prob = previewProb ?? getProb(contract) + return marketClosed ? 'gray-400' : prob >= 0.5 ? 'primary' : 'red-400' }