Allow for non-binary contracts in contract page and related components

This commit is contained in:
James Grugett 2022-02-12 14:46:04 -06:00
parent 7223fa38bc
commit 5411401a2e
8 changed files with 128 additions and 111 deletions

View File

@ -33,19 +33,12 @@ export const createContract = functions
const creator = await getUser(userId) const creator = await getUser(userId)
if (!creator) return { status: 'error', message: 'User not found' } if (!creator) return { status: 'error', message: 'User not found' }
const { const { question, description, initialProb, ante, closeTime, tags } = data
question,
outcomeType,
description,
initialProb,
ante,
closeTime,
tags,
} = data
if (!question) if (!question)
return { status: 'error', message: 'Missing question field' } return { status: 'error', message: 'Missing question field' }
let outcomeType = data.outcomeType ?? 'BINARY'
if (outcomeType !== 'BINARY' && outcomeType !== 'MULTI') if (outcomeType !== 'BINARY' && outcomeType !== 'MULTI')
return { status: 'error', message: 'Invalid outcomeType' } return { status: 'error', message: 'Invalid outcomeType' }

View File

@ -18,7 +18,7 @@ import {
Contract, Contract,
getContractFromId, getContractFromId,
contractPath, contractPath,
contractMetrics, getBinaryProbPercent,
} from '../lib/firebase/contracts' } from '../lib/firebase/contracts'
import { Row } from './layout/row' import { Row } from './layout/row'
import { UserLink } from './user-page' import { UserLink } from './user-page'
@ -159,7 +159,7 @@ function MyContractBets(props: { contract: Contract; bets: Bet[] }) {
const { resolution } = contract const { resolution } = contract
const [collapsed, setCollapsed] = useState(true) const [collapsed, setCollapsed] = useState(true)
const { probPercent } = contractMetrics(contract) const probPercent = getBinaryProbPercent(contract)
return ( return (
<div <div
tabIndex={0} tabIndex={0}

View File

@ -7,6 +7,7 @@ import {
Contract, Contract,
contractMetrics, contractMetrics,
contractPath, contractPath,
getBinaryProbPercent,
} from '../lib/firebase/contracts' } from '../lib/firebase/contracts'
import { Col } from './layout/col' import { Col } from './layout/col'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@ -25,7 +26,7 @@ export function ContractCard(props: {
}) { }) {
const { contract, showHotVolume, showCloseTime, className } = props const { contract, showHotVolume, showCloseTime, className } = props
const { question, resolution } = contract const { question, resolution } = contract
const { probPercent } = contractMetrics(contract) const probPercent = getBinaryProbPercent(contract)
return ( return (
<div> <div>

View File

@ -6,7 +6,6 @@ import {
CheckIcon, CheckIcon,
DotsVerticalIcon, DotsVerticalIcon,
LockClosedIcon, LockClosedIcon,
StarIcon,
UserIcon, UserIcon,
UsersIcon, UsersIcon,
XIcon, XIcon,
@ -21,6 +20,7 @@ import {
contractPath, contractPath,
updateContract, updateContract,
tradingAllowed, tradingAllowed,
getBinaryProbPercent,
} from '../lib/firebase/contracts' } from '../lib/firebase/contracts'
import { useUser } from '../hooks/use-user' import { useUser } from '../hooks/use-user'
import { Linkify } from './linkify' import { Linkify } from './linkify'
@ -33,8 +33,8 @@ import { SiteLink } from './site-link'
import { Col } from './layout/col' import { Col } from './layout/col'
import { UserLink } from './user-page' import { UserLink } from './user-page'
import { DateTimeTooltip } from './datetime-tooltip' import { DateTimeTooltip } from './datetime-tooltip'
import { useBets } from '../hooks/use-bets' import { useBetsWithoutAntes } from '../hooks/use-bets'
import { Bet, withoutAnteBets } from '../lib/firebase/bets' import { Bet } from '../lib/firebase/bets'
import { Comment, mapCommentsByBetId } from '../lib/firebase/comments' import { Comment, mapCommentsByBetId } from '../lib/firebase/comments'
import { JoinSpans } from './join-spans' import { JoinSpans } from './join-spans'
import Textarea from 'react-expanding-textarea' import Textarea from 'react-expanding-textarea'
@ -302,9 +302,8 @@ function TruncatedComment(props: {
function FeedQuestion(props: { contract: Contract }) { function FeedQuestion(props: { contract: Contract }) {
const { contract } = props const { contract } = props
const { creatorName, creatorUsername, createdTime, question, resolution } = const { creatorName, creatorUsername, question, resolution } = contract
contract const { truePool } = contractMetrics(contract)
const { probPercent, truePool } = contractMetrics(contract)
// Currently hidden on mobile; ideally we'd fit this in somewhere. // Currently hidden on mobile; ideally we'd fit this in somewhere.
const closeMessage = const closeMessage =
@ -343,7 +342,7 @@ function FeedQuestion(props: { contract: Contract }) {
<ResolutionOrChance <ResolutionOrChance
className="items-center" className="items-center"
resolution={resolution} resolution={resolution}
probPercent={probPercent} probPercent={getBinaryProbPercent(contract)}
/> />
</Col> </Col>
<TruncatedComment <TruncatedComment
@ -642,12 +641,13 @@ export function ContractFeed(props: {
betRowClassName?: string betRowClassName?: string
}) { }) {
const { contract, feedType, betRowClassName } = props const { contract, feedType, betRowClassName } = props
const { id } = contract const { id, outcomeType } = contract
const isBinary = outcomeType === 'BINARY'
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(false)
const user = useUser() const user = useUser()
let bets = useBets(id) ?? props.bets const bets = useBetsWithoutAntes(contract, props.bets) ?? []
bets = withoutAnteBets(contract, bets)
const comments = useComments(id) ?? props.comments const comments = useComments(id) ?? props.comments
@ -715,7 +715,7 @@ export function ContractFeed(props: {
</li> </li>
))} ))}
</ul> </ul>
{tradingAllowed(contract) && ( {isBinary && tradingAllowed(contract) && (
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} /> <BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
)} )}
</div> </div>

View File

@ -1,9 +1,9 @@
import { import {
contractMetrics,
Contract, Contract,
deleteContract, deleteContract,
contractPath, contractPath,
tradingAllowed, tradingAllowed,
getBinaryProbPercent,
} from '../lib/firebase/contracts' } from '../lib/firebase/contracts'
import { Col } from './layout/col' import { Col } from './layout/col'
import { Spacer } from './layout/spacer' import { Spacer } from './layout/spacer'
@ -31,62 +31,59 @@ export const ContractOverview = (props: {
className?: string className?: string
}) => { }) => {
const { contract, bets, comments, folds, className } = props const { contract, bets, comments, folds, className } = props
const { resolution, creatorId, creatorName } = contract const { question, resolution, creatorId, outcomeType } = contract
const { probPercent, truePool } = contractMetrics(contract)
const user = useUser() const user = useUser()
const isCreator = user?.id === creatorId const isCreator = user?.id === creatorId
const isBinary = outcomeType === 'BINARY'
const tweetQuestion = isCreator const tweetText = getTweetText(contract, isCreator)
? contract.question
: `${creatorName}: ${contract.question}`
const tweetDescription = resolution
? `Resolved ${resolution}!`
: `Currently ${probPercent} chance, place your bets here:`
const url = `https://manifold.markets${contractPath(contract)}`
const tweetText = `${tweetQuestion}\n\n${tweetDescription}\n\n${url}`
return ( return (
<Col className={clsx('mb-6', className)}> <Col className={clsx('mb-6', className)}>
<Row className="justify-between gap-4 px-2"> <Row className="justify-between gap-4 px-2">
<Col className="gap-4"> <Col className="gap-4">
<div className="text-2xl text-indigo-700 md:text-3xl"> <div className="text-2xl text-indigo-700 md:text-3xl">
<Linkify text={contract.question} /> <Linkify text={question} />
</div> </div>
<Row className="items-center justify-between gap-4"> {isBinary && (
<ResolutionOrChance <Row className="items-center justify-between gap-4">
className="md:hidden" <ResolutionOrChance
resolution={resolution}
probPercent={probPercent}
large
/>
{tradingAllowed(contract) && (
<BetRow
contract={contract}
className="md:hidden" className="md:hidden"
labelClassName="hidden" resolution={resolution}
probPercent={getBinaryProbPercent(contract)}
large
/> />
)}
</Row> {tradingAllowed(contract) && (
<BetRow
contract={contract}
className="md:hidden"
labelClassName="hidden"
/>
)}
</Row>
)}
<ContractDetails contract={contract} /> <ContractDetails contract={contract} />
</Col> </Col>
<Col className="hidden items-end justify-between md:flex"> {isBinary && (
<ResolutionOrChance <Col className="hidden items-end justify-between md:flex">
className="items-end" <ResolutionOrChance
resolution={resolution} className="items-end"
probPercent={probPercent} resolution={resolution}
large probPercent={getBinaryProbPercent(contract)}
/> large
</Col> />
</Col>
)}
</Row> </Row>
<Spacer h={4} /> <Spacer h={4} />
<ContractProbGraph contract={contract} bets={bets} /> {isBinary && <ContractProbGraph contract={contract} bets={bets} />}
<Row className="mt-6 ml-4 hidden items-center justify-between gap-4 sm:flex"> <Row className="mt-6 ml-4 hidden items-center justify-between gap-4 sm:flex">
{folds.length === 0 ? ( {folds.length === 0 ? (
@ -110,12 +107,9 @@ export const ContractOverview = (props: {
<RevealableTagsInput className="mx-4 mt-4" contract={contract} /> <RevealableTagsInput className="mx-4 mt-4" contract={contract} />
)} )}
<Spacer h={12} />
{/* Show a delete button for contracts without any trading */} {/* Show a delete button for contracts without any trading */}
{isCreator && truePool === 0 && ( {isCreator && (isBinary ? bets.length <= 2 : bets.length <= 1) && (
<> <>
<Spacer h={8} />
<button <button
className="btn btn-xs btn-error btn-outline mt-1 max-w-fit self-end" className="btn btn-xs btn-error btn-outline mt-1 max-w-fit self-end"
onClick={async (e) => { onClick={async (e) => {
@ -129,6 +123,8 @@ export const ContractOverview = (props: {
</> </>
)} )}
<Spacer h={12} />
<ContractFeed <ContractFeed
contract={contract} contract={contract}
bets={bets} bets={bets}
@ -139,3 +135,22 @@ export const ContractOverview = (props: {
</Col> </Col>
) )
} }
const getTweetText = (contract: Contract, isCreator: boolean) => {
const { question, creatorName, resolution, outcomeType } = contract
const isBinary = outcomeType === 'BINARY'
const tweetQuestion = isCreator
? question
: `${question} Asked by ${creatorName}.`
const tweetDescription = resolution
? `Resolved ${resolution}!`
: isBinary
? `Currently ${getBinaryProbPercent(
contract
)} chance, place your bets here:`
: `Submit your own answer:`
const url = `https://manifold.markets${contractPath(contract)}`
return `${tweetQuestion}\n\n${tweetDescription}\n\n${url}`
}

View File

@ -27,21 +27,9 @@ export function contractPath(contract: Contract) {
} }
export function contractMetrics(contract: Contract) { export function contractMetrics(contract: Contract) {
const { const { pool, createdTime, resolutionTime, isResolved } = contract
pool,
phantomShares,
totalShares,
createdTime,
resolutionTime,
isResolved,
resolutionProbability,
} = contract
const truePool = pool.YES + pool.NO const truePool = _.sum(Object.values(pool))
const prob = resolutionProbability ?? getProbability(totalShares)
const probPercent = Math.round(prob * 100) + '%'
const startProb = getProbability(phantomShares)
const createdDate = dayjs(createdTime).format('MMM D') const createdDate = dayjs(createdTime).format('MMM D')
@ -49,7 +37,16 @@ export function contractMetrics(contract: Contract) {
? dayjs(resolutionTime).format('MMM D') ? dayjs(resolutionTime).format('MMM D')
: undefined : undefined
return { truePool, probPercent, startProb, createdDate, resolvedDate } return { truePool, createdDate, resolvedDate }
}
export function getBinaryProbPercent(contract: Contract) {
const { totalShares, resolutionProbability } = contract
const prob = resolutionProbability ?? getProbability(totalShares)
const probPercent = Math.round(prob * 100) + '%'
return probPercent
} }
export function tradingAllowed(contract: Contract) { export function tradingAllowed(contract: Contract) {

View File

@ -12,10 +12,10 @@ import { Title } from '../../components/title'
import { Spacer } from '../../components/layout/spacer' import { Spacer } from '../../components/layout/spacer'
import { User } from '../../lib/firebase/users' import { User } from '../../lib/firebase/users'
import { import {
contractMetrics,
Contract, Contract,
getContractFromSlug, getContractFromSlug,
tradingAllowed, tradingAllowed,
getBinaryProbPercent,
} from '../../lib/firebase/contracts' } from '../../lib/firebase/contracts'
import { SEO } from '../../components/SEO' import { SEO } from '../../components/SEO'
import { Page } from '../../components/page' import { Page } from '../../components/page'
@ -82,33 +82,27 @@ export default function ContractPage(props: {
return <Custom404 /> return <Custom404 />
} }
const { creatorId, isResolved, resolution, question } = contract const { creatorId, isResolved, question, outcomeType } = contract
const isCreator = user?.id === creatorId const isCreator = user?.id === creatorId
const isBinary = outcomeType === 'BINARY'
const allowTrade = tradingAllowed(contract) const allowTrade = tradingAllowed(contract)
const allowResolve = !isResolved && isCreator && !!user const allowResolve = !isResolved && isCreator && !!user
const hasSidePanel = isBinary && (allowTrade || allowResolve)
const { probPercent } = contractMetrics(contract) // TODO(James): Create SEO props for non-binary contracts.
const ogCardProps = isBinary ? getOpenGraphProps(contract) : undefined
const description = resolution
? `Resolved ${resolution}. ${contract.description}`
: `${probPercent} chance. ${contract.description}`
const ogCardProps = {
question,
probability: probPercent,
metadata: contractTextDetails(contract),
creatorName: contract.creatorName,
creatorUsername: contract.creatorUsername,
}
return ( return (
<Page wide={allowTrade || allowResolve}> <Page wide={hasSidePanel}>
<SEO {ogCardProps && (
title={question} <SEO
description={description} title={question}
url={`/${props.username}/${props.slug}`} description={ogCardProps.description}
ogCardProps={ogCardProps} url={`/${props.username}/${props.slug}`}
/> ogCardProps={ogCardProps}
/>
)}
<Col className="w-full justify-between md:flex-row"> <Col className="w-full justify-between md:flex-row">
<div className="flex-[3] rounded border-0 border-gray-100 bg-white px-2 py-6 md:px-6 md:py-8"> <div className="flex-[3] rounded border-0 border-gray-100 bg-white px-2 py-6 md:px-6 md:py-8">
@ -118,10 +112,10 @@ export default function ContractPage(props: {
comments={comments ?? []} comments={comments ?? []}
folds={folds} folds={folds}
/> />
<BetsSection contract={contract} user={user ?? null} /> <BetsSection contract={contract} user={user ?? null} bets={bets} />
</div> </div>
{(allowTrade || allowResolve) && ( {hasSidePanel && (
<> <>
<div className="md:ml-6" /> <div className="md:ml-6" />
@ -140,11 +134,13 @@ export default function ContractPage(props: {
) )
} }
function BetsSection(props: { contract: Contract; user: User | null }) { function BetsSection(props: {
contract: Contract
user: User | null
bets: Bet[]
}) {
const { contract, user } = props const { contract, user } = props
const bets = useBets(contract.id) const bets = useBets(contract.id) ?? props.bets
if (!bets || bets.length === 0) return <></>
// Decending creation time. // Decending creation time.
bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime) bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime)
@ -168,3 +164,21 @@ function BetsSection(props: { contract: Contract; user: User | null }) {
</div> </div>
) )
} }
const getOpenGraphProps = (contract: Contract<'BINARY'>) => {
const { resolution, question, creatorName, creatorUsername } = contract
const probPercent = getBinaryProbPercent(contract)
const description = resolution
? `Resolved ${resolution}. ${contract.description}`
: `${probPercent} chance. ${contract.description}`
return {
question,
probability: probPercent,
metadata: contractTextDetails(contract),
creatorName: creatorName,
creatorUsername: creatorUsername,
description,
}
}

View File

@ -3,6 +3,7 @@ import dayjs from 'dayjs'
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react' import { useState } from 'react'
import Textarea from 'react-expanding-textarea' import Textarea from 'react-expanding-textarea'
import { getProbability } from '../../common/calculate'
import { parseWordsAsTags } from '../../common/util/parse' import { parseWordsAsTags } from '../../common/util/parse'
import { AmountInput } from '../components/amount-input' import { AmountInput } from '../components/amount-input'
import { InfoTooltip } from '../components/info-tooltip' import { InfoTooltip } from '../components/info-tooltip'
@ -15,11 +16,7 @@ import { Page } from '../components/page'
import { Title } from '../components/title' import { Title } from '../components/title'
import { useUser } from '../hooks/use-user' import { useUser } from '../hooks/use-user'
import { createContract } from '../lib/firebase/api-call' import { createContract } from '../lib/firebase/api-call'
import { import { Contract, contractPath } from '../lib/firebase/contracts'
contractMetrics,
Contract,
contractPath,
} from '../lib/firebase/contracts'
type Prediction = { type Prediction = {
question: string question: string
@ -29,7 +26,7 @@ type Prediction = {
} }
function toPrediction(contract: Contract): Prediction { function toPrediction(contract: Contract): Prediction {
const { startProb } = contractMetrics(contract) const startProb = getProbability(contract.phantomShares)
return { return {
question: contract.question, question: contract.question,
description: contract.description, description: contract.description,