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)
if (!creator) return { status: 'error', message: 'User not found' }
const {
question,
outcomeType,
description,
initialProb,
ante,
closeTime,
tags,
} = data
const { question, description, initialProb, ante, closeTime, tags } = data
if (!question)
return { status: 'error', message: 'Missing question field' }
let outcomeType = data.outcomeType ?? 'BINARY'
if (outcomeType !== 'BINARY' && outcomeType !== 'MULTI')
return { status: 'error', message: 'Invalid outcomeType' }

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import {
contractMetrics,
Contract,
deleteContract,
contractPath,
tradingAllowed,
getBinaryProbPercent,
} from '../lib/firebase/contracts'
import { Col } from './layout/col'
import { Spacer } from './layout/spacer'
@ -31,62 +31,59 @@ export const ContractOverview = (props: {
className?: string
}) => {
const { contract, bets, comments, folds, className } = props
const { resolution, creatorId, creatorName } = contract
const { probPercent, truePool } = contractMetrics(contract)
const { question, resolution, creatorId, outcomeType } = contract
const user = useUser()
const isCreator = user?.id === creatorId
const isBinary = outcomeType === 'BINARY'
const tweetQuestion = 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}`
const tweetText = getTweetText(contract, isCreator)
return (
<Col className={clsx('mb-6', className)}>
<Row className="justify-between gap-4 px-2">
<Col className="gap-4">
<div className="text-2xl text-indigo-700 md:text-3xl">
<Linkify text={contract.question} />
<Linkify text={question} />
</div>
<Row className="items-center justify-between gap-4">
<ResolutionOrChance
className="md:hidden"
resolution={resolution}
probPercent={probPercent}
large
/>
{tradingAllowed(contract) && (
<BetRow
contract={contract}
{isBinary && (
<Row className="items-center justify-between gap-4">
<ResolutionOrChance
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} />
</Col>
<Col className="hidden items-end justify-between md:flex">
<ResolutionOrChance
className="items-end"
resolution={resolution}
probPercent={probPercent}
large
/>
</Col>
{isBinary && (
<Col className="hidden items-end justify-between md:flex">
<ResolutionOrChance
className="items-end"
resolution={resolution}
probPercent={getBinaryProbPercent(contract)}
large
/>
</Col>
)}
</Row>
<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">
{folds.length === 0 ? (
@ -110,12 +107,9 @@ export const ContractOverview = (props: {
<RevealableTagsInput className="mx-4 mt-4" contract={contract} />
)}
<Spacer h={12} />
{/* Show a delete button for contracts without any trading */}
{isCreator && truePool === 0 && (
{isCreator && (isBinary ? bets.length <= 2 : bets.length <= 1) && (
<>
<Spacer h={8} />
<button
className="btn btn-xs btn-error btn-outline mt-1 max-w-fit self-end"
onClick={async (e) => {
@ -129,6 +123,8 @@ export const ContractOverview = (props: {
</>
)}
<Spacer h={12} />
<ContractFeed
contract={contract}
bets={bets}
@ -139,3 +135,22 @@ export const ContractOverview = (props: {
</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) {
const {
pool,
phantomShares,
totalShares,
createdTime,
resolutionTime,
isResolved,
resolutionProbability,
} = contract
const { pool, createdTime, resolutionTime, isResolved } = contract
const truePool = pool.YES + pool.NO
const prob = resolutionProbability ?? getProbability(totalShares)
const probPercent = Math.round(prob * 100) + '%'
const startProb = getProbability(phantomShares)
const truePool = _.sum(Object.values(pool))
const createdDate = dayjs(createdTime).format('MMM D')
@ -49,7 +37,16 @@ export function contractMetrics(contract: Contract) {
? dayjs(resolutionTime).format('MMM D')
: 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) {

View File

@ -12,10 +12,10 @@ import { Title } from '../../components/title'
import { Spacer } from '../../components/layout/spacer'
import { User } from '../../lib/firebase/users'
import {
contractMetrics,
Contract,
getContractFromSlug,
tradingAllowed,
getBinaryProbPercent,
} from '../../lib/firebase/contracts'
import { SEO } from '../../components/SEO'
import { Page } from '../../components/page'
@ -82,33 +82,27 @@ export default function ContractPage(props: {
return <Custom404 />
}
const { creatorId, isResolved, resolution, question } = contract
const { creatorId, isResolved, question, outcomeType } = contract
const isCreator = user?.id === creatorId
const isBinary = outcomeType === 'BINARY'
const allowTrade = tradingAllowed(contract)
const allowResolve = !isResolved && isCreator && !!user
const hasSidePanel = isBinary && (allowTrade || allowResolve)
const { probPercent } = contractMetrics(contract)
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,
}
// TODO(James): Create SEO props for non-binary contracts.
const ogCardProps = isBinary ? getOpenGraphProps(contract) : undefined
return (
<Page wide={allowTrade || allowResolve}>
<SEO
title={question}
description={description}
url={`/${props.username}/${props.slug}`}
ogCardProps={ogCardProps}
/>
<Page wide={hasSidePanel}>
{ogCardProps && (
<SEO
title={question}
description={ogCardProps.description}
url={`/${props.username}/${props.slug}`}
ogCardProps={ogCardProps}
/>
)}
<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">
@ -118,10 +112,10 @@ export default function ContractPage(props: {
comments={comments ?? []}
folds={folds}
/>
<BetsSection contract={contract} user={user ?? null} />
<BetsSection contract={contract} user={user ?? null} bets={bets} />
</div>
{(allowTrade || allowResolve) && (
{hasSidePanel && (
<>
<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 bets = useBets(contract.id)
if (!bets || bets.length === 0) return <></>
const bets = useBets(contract.id) ?? props.bets
// Decending creation time.
bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime)
@ -168,3 +164,21 @@ function BetsSection(props: { contract: Contract; user: User | null }) {
</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 { useState } from 'react'
import Textarea from 'react-expanding-textarea'
import { getProbability } from '../../common/calculate'
import { parseWordsAsTags } from '../../common/util/parse'
import { AmountInput } from '../components/amount-input'
import { InfoTooltip } from '../components/info-tooltip'
@ -15,11 +16,7 @@ import { Page } from '../components/page'
import { Title } from '../components/title'
import { useUser } from '../hooks/use-user'
import { createContract } from '../lib/firebase/api-call'
import {
contractMetrics,
Contract,
contractPath,
} from '../lib/firebase/contracts'
import { Contract, contractPath } from '../lib/firebase/contracts'
type Prediction = {
question: string
@ -29,7 +26,7 @@ type Prediction = {
}
function toPrediction(contract: Contract): Prediction {
const { startProb } = contractMetrics(contract)
const startProb = getProbability(contract.phantomShares)
return {
question: contract.question,
description: contract.description,