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'
}