Improve Free response UI (#78)

* Add legend to free response graph

* Hide answers panel unless resolving. Correctly order answers

* No gray background for add answer & resolve panel. Tweak spacing

* Max answer length 240 chars

* Show answer text in resolution for market page, card instead of number.

* Remove remaining answer #'s. Refactor outcome/resolution labels.

* Move answer panel back up

* Tweak spacing

* Update placement of bet button on mobile for FR answer feed item

* Fix reversed feed for binary markets

* Show multi resolve options

* Clean up unused parts of answer item

* Lighten resolve buttons

* Show answer text in market resolve email
This commit is contained in:
James Grugett 2022-04-18 18:02:40 -05:00 committed by GitHub
parent 7abc11c146
commit 9c74f88b4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 448 additions and 289 deletions

View File

@ -29,3 +29,5 @@ export const getNoneAnswer = (contractId: string, creator: User) => {
text: 'None', text: 'None',
} }
} }
export const MAX_ANSWER_LENGTH = 240

View File

@ -38,6 +38,8 @@ export type FullContract<
T T
export type Contract = FullContract<DPM | CPMM, Binary | Multi | FreeResponse> export type Contract = FullContract<DPM | CPMM, Binary | Multi | FreeResponse>
export type BinaryContract = FullContract<DPM | CPMM, Binary>
export type FreeResponseContract = FullContract<DPM | CPMM, FreeResponse>
export type DPM = { export type DPM = {
mechanism: 'dpm-2' mechanism: 'dpm-2'
@ -61,18 +63,20 @@ export type Binary = {
outcomeType: 'BINARY' outcomeType: 'BINARY'
initialProbability: number initialProbability: number
resolutionProbability?: number // Used for BINARY markets resolved to MKT resolutionProbability?: number // Used for BINARY markets resolved to MKT
resolution?: 'YES' | 'NO' | 'MKT' | 'CANCEL'
} }
export type Multi = { export type Multi = {
outcomeType: 'MULTI' outcomeType: 'MULTI'
multiOutcomes: string[] // Used for outcomeType 'MULTI'. multiOutcomes: string[] // Used for outcomeType 'MULTI'.
resolutions?: { [outcome: string]: number } // Used for PROB resolutions?: { [outcome: string]: number } // Used for MKT resolution.
} }
export type FreeResponse = { export type FreeResponse = {
outcomeType: 'FREE_RESPONSE' outcomeType: 'FREE_RESPONSE'
answers: Answer[] // Used for outcomeType 'FREE_RESPONSE'. answers: Answer[] // Used for outcomeType 'FREE_RESPONSE'.
resolutions?: { [outcome: string]: number } // Used for PROB resolution?: string | 'MKT' | 'CANCEL'
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
} }
export type outcomeType = 'BINARY' | 'MULTI' | 'FREE_RESPONSE' export type outcomeType = 'BINARY' | 'MULTI' | 'FREE_RESPONSE'

View File

@ -9,7 +9,7 @@ import {
} from '../../common/contract' } from '../../common/contract'
import { User } from '../../common/user' import { User } from '../../common/user'
import { getLoanAmount, getNewMultiBetInfo } from '../../common/new-bet' import { getLoanAmount, getNewMultiBetInfo } from '../../common/new-bet'
import { Answer } from '../../common/answer' import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer'
import { getContract, getValues } from './utils' import { getContract, getValues } from './utils'
import { sendNewAnswerEmail } from './emails' import { sendNewAnswerEmail } from './emails'
import { Bet } from '../../common/bet' import { Bet } from '../../common/bet'
@ -31,7 +31,7 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
if (amount <= 0 || isNaN(amount) || !isFinite(amount)) if (amount <= 0 || isNaN(amount) || !isFinite(amount))
return { status: 'error', message: 'Invalid amount' } return { status: 'error', message: 'Invalid amount' }
if (!text || typeof text !== 'string' || text.length > 10000) if (!text || typeof text !== 'string' || text.length > MAX_ANSWER_LENGTH)
return { status: 'error', message: 'Invalid text' } return { status: 'error', message: 'Invalid text' }
// Run as transaction to prevent race conditions. // Run as transaction to prevent race conditions.

View File

@ -5,7 +5,7 @@ import { Answer } from '../../common/answer'
import { Bet } from '../../common/bet' import { Bet } from '../../common/bet'
import { getProbability } from '../../common/calculate' import { getProbability } from '../../common/calculate'
import { Comment } from '../../common/comment' import { Comment } from '../../common/comment'
import { Contract } from '../../common/contract' import { Contract, FreeResponseContract } from '../../common/contract'
import { CREATOR_FEE } from '../../common/fees' import { CREATOR_FEE } from '../../common/fees'
import { PrivateUser, User } from '../../common/user' import { PrivateUser, User } from '../../common/user'
import { formatMoney, formatPercent } from '../../common/util/format' import { formatMoney, formatPercent } from '../../common/util/format'
@ -98,6 +98,10 @@ const toDisplayResolution = (
if (resolution === 'MKT' && resolutions) return 'MULTI' if (resolution === 'MKT' && resolutions) return 'MULTI'
if (resolution === 'CANCEL') return 'N/A' if (resolution === 'CANCEL') return 'N/A'
const answer = (contract as FreeResponseContract).answers?.find(
(a) => a.id === resolution
)
if (answer) return answer.text
return `#${resolution}` return `#${resolution}`
} }

View File

@ -1,6 +1,5 @@
import clsx from 'clsx' import clsx from 'clsx'
import _ from 'lodash' import _ from 'lodash'
import { useState } from 'react'
import { Answer } from '../../../common/answer' import { Answer } from '../../../common/answer'
import { DPM, FreeResponse, FullContract } from '../../../common/contract' import { DPM, FreeResponse, FullContract } from '../../../common/contract'
@ -8,19 +7,14 @@ import { Col } from '../layout/col'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { Avatar } from '../avatar' import { Avatar } from '../avatar'
import { SiteLink } from '../site-link' import { SiteLink } from '../site-link'
import { BuyButton } from '../yes-no-selector'
import { formatPercent } from '../../../common/util/format' import { formatPercent } from '../../../common/util/format'
import { getDpmOutcomeProbability } from '../../../common/calculate-dpm' import { getDpmOutcomeProbability } from '../../../common/calculate-dpm'
import { tradingAllowed } from '../../lib/firebase/contracts' import { tradingAllowed } from '../../lib/firebase/contracts'
import { AnswerBetPanel } from './answer-bet-panel'
import { Linkify } from '../linkify' import { Linkify } from '../linkify'
import { User } from '../../../common/user'
import { ContractActivity } from '../feed/contract-activity'
export function AnswerItem(props: { export function AnswerItem(props: {
answer: Answer answer: Answer
contract: FullContract<DPM, FreeResponse> contract: FullContract<DPM, FreeResponse>
user: User | null | undefined
showChoice: 'radio' | 'checkbox' | undefined showChoice: 'radio' | 'checkbox' | undefined
chosenProb: number | undefined chosenProb: number | undefined
totalChosenProb?: number totalChosenProb?: number
@ -30,7 +24,6 @@ export function AnswerItem(props: {
const { const {
answer, answer,
contract, contract,
user,
showChoice, showChoice,
chosenProb, chosenProb,
totalChosenProb, totalChosenProb,
@ -47,10 +40,6 @@ export function AnswerItem(props: {
const wasResolvedTo = const wasResolvedTo =
resolution === answer.id || (resolutions && resolutions[answer.id]) resolution === answer.id || (resolutions && resolutions[answer.id])
const [isBetting, setIsBetting] = useState(false)
const canBet = !isBetting && !showChoice && tradingAllowed(contract)
return ( return (
<div <div
className={clsx( className={clsx(
@ -63,10 +52,8 @@ export function AnswerItem(props: {
? 'bg-gray-50' ? 'bg-gray-50'
: showChoice === 'radio' : showChoice === 'radio'
? 'bg-green-50' ? 'bg-green-50'
: 'bg-blue-50', : 'bg-blue-50'
canBet && 'cursor-pointer hover:bg-gray-100'
)} )}
onClick={() => canBet && setIsBetting(true)}
> >
<Col className="flex-1 gap-3"> <Col className="flex-1 gap-3">
<div className="whitespace-pre-line"> <div className="whitespace-pre-line">
@ -83,124 +70,91 @@ export function AnswerItem(props: {
{/* TODO: Show total pool? */} {/* TODO: Show total pool? */}
<div className="text-base">#{number}</div> <div className="text-base">#{number}</div>
</Row> </Row>
{isBetting && (
<ContractActivity
className="hidden md:flex"
contract={contract}
bets={[]}
comments={[]}
user={user}
filterToOutcome={answer.id}
mode="all"
/>
)}
</Col> </Col>
{isBetting ? ( <Row className="items-center justify-end gap-4 self-end sm:self-start">
<AnswerBetPanel {!wasResolvedTo &&
answer={answer} (showChoice === 'checkbox' ? (
contract={contract} <input
closePanel={() => setIsBetting(false)} className="input input-bordered w-24 justify-self-end text-2xl"
className="sm:w-72" type="number"
/> placeholder={`${roundedProb}`}
) : ( maxLength={9}
<Row className="items-center justify-end gap-4 self-end sm:self-start"> value={chosenProb ? Math.round(chosenProb) : ''}
{!wasResolvedTo && onChange={(e) => {
(showChoice === 'checkbox' ? ( const { value } = e.target
<input const numberValue = value
className="input input-bordered w-24 justify-self-end text-2xl" ? parseInt(value.replace(/[^\d]/, ''))
type="number" : 0
placeholder={`${roundedProb}`} if (!isNaN(numberValue)) onChoose(answer.id, numberValue)
maxLength={9} }}
value={chosenProb ? Math.round(chosenProb) : ''} />
onChange={(e) => {
const { value } = e.target
const numberValue = value
? parseInt(value.replace(/[^\d]/, ''))
: 0
if (!isNaN(numberValue)) onChoose(answer.id, numberValue)
}}
/>
) : (
<div
className={clsx(
'text-2xl',
tradingAllowed(contract) ? 'text-green-500' : 'text-gray-500'
)}
>
{probPercent}
</div>
))}
{showChoice ? (
<div className="form-control py-1">
<label className="label cursor-pointer gap-3">
<span className="">Choose this answer</span>
{showChoice === 'radio' && (
<input
className={clsx('radio', chosenProb && '!bg-green-500')}
type="radio"
name="opt"
checked={isChosen}
onChange={() => onChoose(answer.id, 1)}
value={answer.id}
/>
)}
{showChoice === 'checkbox' && (
<input
className={clsx('checkbox', chosenProb && '!bg-blue-500')}
type="checkbox"
name="opt"
checked={isChosen}
onChange={() => {
if (isChosen) onDeselect(answer.id)
else {
onChoose(answer.id, 100 * prob)
}
}}
value={answer.id}
/>
)}
</label>
{showChoice === 'checkbox' && (
<div className="ml-1">
{chosenProb && totalChosenProb
? Math.round((100 * chosenProb) / totalChosenProb)
: 0}
% share
</div>
)}
</div>
) : ( ) : (
<> <div
{tradingAllowed(contract) && ( className={clsx(
<BuyButton 'text-2xl',
className="btn-md flex-initial justify-end self-end !px-8" tradingAllowed(contract) ? 'text-green-500' : 'text-gray-500'
onClick={() => { )}
setIsBetting(true) >
}} {probPercent}
</div>
))}
{showChoice ? (
<div className="form-control py-1">
<label className="label cursor-pointer gap-3">
<span className="">Choose this answer</span>
{showChoice === 'radio' && (
<input
className={clsx('radio', chosenProb && '!bg-green-500')}
type="radio"
name="opt"
checked={isChosen}
onChange={() => onChoose(answer.id, 1)}
value={answer.id}
/> />
)} )}
{wasResolvedTo && ( {showChoice === 'checkbox' && (
<Col className="items-end"> <input
<div className={clsx('checkbox', chosenProb && '!bg-blue-500')}
className={clsx( type="checkbox"
'text-xl', name="opt"
resolution === 'MKT' ? 'text-blue-700' : 'text-green-700' checked={isChosen}
)} onChange={() => {
> if (isChosen) onDeselect(answer.id)
Chosen{' '} else {
{resolutions onChoose(answer.id, 100 * prob)
? `${Math.round(resolutions[answer.id])}%` }
: ''} }}
</div> value={answer.id}
<div className="text-2xl text-gray-500">{probPercent}</div> />
</Col>
)} )}
</> </label>
)} {showChoice === 'checkbox' && (
</Row> <div className="ml-1">
)} {chosenProb && totalChosenProb
? Math.round((100 * chosenProb) / totalChosenProb)
: 0}
% share
</div>
)}
</div>
) : (
wasResolvedTo && (
<Col className="items-end">
<div
className={clsx(
'text-xl',
resolution === 'MKT' ? 'text-blue-700' : 'text-green-700'
)}
>
Chosen{' '}
{resolutions ? `${Math.round(resolutions[answer.id])}%` : ''}
</div>
<div className="text-2xl text-gray-500">{probPercent}</div>
</Col>
)
)}
</Row>
</div> </div>
) )
} }

View File

@ -71,7 +71,7 @@ export function AnswerResolvePanel(props: {
: 'btn-disabled' : 'btn-disabled'
return ( return (
<Col className="gap-4 rounded bg-gray-50 p-4"> <Col className="gap-4 rounded">
<div>Resolve your market</div> <div>Resolve your market</div>
<Col className="gap-4 sm:flex-row sm:items-center"> <Col className="gap-4 sm:flex-row sm:items-center">
<ChooseCancelSelector <ChooseCancelSelector

View File

@ -38,7 +38,8 @@ export const AnswersGraph = memo(function AnswersGraph(props: {
const { width } = useWindowSize() const { width } = useWindowSize()
const labelLength = !width || width > 800 ? 50 : 20 const isLargeWidth = !width || width > 800
const labelLength = isLargeWidth ? 50 : 20
const endTime = const endTime =
resolutionTime || isClosed resolutionTime || isClosed
@ -68,16 +69,15 @@ export const AnswersGraph = memo(function AnswersGraph(props: {
answers?.find((answer) => answer.id === outcome)?.text ?? 'None' answers?.find((answer) => answer.id === outcome)?.text ?? 'None'
const answerText = const answerText =
answer.slice(0, labelLength) + (answer.length > labelLength ? '...' : '') answer.slice(0, labelLength) + (answer.length > labelLength ? '...' : '')
const id = `#${outcome}: ${answerText}`
return { id, data: points } return { id: answerText, data: points }
}) })
data.reverse() data.reverse()
const yTickValues = [0, 25, 50, 75, 100] const yTickValues = [0, 25, 50, 75, 100]
const numXTickValues = !width || width < 800 ? 2 : 5 const numXTickValues = isLargeWidth ? 5 : 2
const hoursAgo = latestTime.subtract(5, 'hours') const hoursAgo = latestTime.subtract(5, 'hours')
const startDate = dayjs(contract.createdTime).isBefore(hoursAgo) const startDate = dayjs(contract.createdTime).isBefore(hoursAgo)
? new Date(contract.createdTime) ? new Date(contract.createdTime)
@ -87,8 +87,8 @@ export const AnswersGraph = memo(function AnswersGraph(props: {
return ( return (
<div <div
className="w-full overflow-hidden" className="w-full"
style={{ height: height ?? (!width || width >= 800 ? 350 : 225) }} style={{ height: height ?? (isLargeWidth ? 350 : 250) }}
> >
<ResponsiveLine <ResponsiveLine
data={data} data={data}
@ -116,6 +116,32 @@ export const AnswersGraph = memo(function AnswersGraph(props: {
enableArea enableArea
areaOpacity={1} areaOpacity={1}
margin={{ top: 20, right: 28, bottom: 22, left: 40 }} margin={{ top: 20, right: 28, bottom: 22, left: 40 }}
legends={[
{
anchor: 'top-left',
direction: 'column',
justify: false,
translateX: isLargeWidth ? 5 : 2,
translateY: 0,
itemsSpacing: 0,
itemTextColor: 'black',
itemDirection: 'left-to-right',
itemWidth: isLargeWidth ? 288 : 138,
itemHeight: 20,
itemBackground: 'white',
itemOpacity: 0.9,
symbolSize: 12,
effects: [
{
on: 'hover',
style: {
itemBackground: 'rgba(255, 255, 255, 1)',
itemOpacity: 1,
},
},
],
},
]}
/> />
</div> </div>
) )

View File

@ -3,7 +3,6 @@ import { useLayoutEffect, useState } from 'react'
import { DPM, FreeResponse, FullContract } from '../../../common/contract' import { DPM, FreeResponse, FullContract } from '../../../common/contract'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { formatPercent } from '../../../common/util/format'
import { useUser } from '../../hooks/use-user' import { useUser } from '../../hooks/use-user'
import { getDpmOutcomeProbability } from '../../../common/calculate-dpm' import { getDpmOutcomeProbability } from '../../../common/calculate-dpm'
import { useAnswers } from '../../hooks/use-answers' import { useAnswers } from '../../hooks/use-answers'
@ -11,6 +10,7 @@ import { tradingAllowed } from '../../lib/firebase/contracts'
import { AnswerItem } from './answer-item' import { AnswerItem } from './answer-item'
import { CreateAnswerPanel } from './create-answer-panel' import { CreateAnswerPanel } from './create-answer-panel'
import { AnswerResolvePanel } from './answer-resolve-panel' import { AnswerResolvePanel } from './answer-resolve-panel'
import { Spacer } from '../layout/spacer'
export function AnswersPanel(props: { export function AnswersPanel(props: {
contract: FullContract<DPM, FreeResponse> contract: FullContract<DPM, FreeResponse>
@ -31,7 +31,7 @@ export function AnswersPanel(props: {
resolutions ? -1 * resolutions[answer.id] : 0 resolutions ? -1 * resolutions[answer.id] : 0
), ),
..._.sortBy( ..._.sortBy(
otherAnswers, resolution ? [] : otherAnswers,
(answer) => -1 * getDpmOutcomeProbability(contract.totalShares, answer.id) (answer) => -1 * getDpmOutcomeProbability(contract.totalShares, answer.id)
), ),
] ]
@ -82,40 +82,41 @@ export function AnswersPanel(props: {
return ( return (
<Col className="gap-3"> <Col className="gap-3">
{sortedAnswers.map((answer) => ( {(resolveOption === 'CHOOSE' ||
<AnswerItem resolveOption === 'CHOOSE_MULTIPLE' ||
key={answer.id} resolution === 'MKT') &&
answer={answer} sortedAnswers.map((answer) => (
contract={contract} <AnswerItem
user={user} key={answer.id}
showChoice={showChoice} answer={answer}
chosenProb={chosenAnswers[answer.id]} contract={contract}
totalChosenProb={chosenTotal} showChoice={showChoice}
onChoose={onChoose} chosenProb={chosenAnswers[answer.id]}
onDeselect={onDeselect} totalChosenProb={chosenTotal}
/> onChoose={onChoose}
))} onDeselect={onDeselect}
/>
))}
{sortedAnswers.length === 0 ? ( {sortedAnswers.length === 0 && (
<div className="p-4 text-gray-500">No answers yet...</div> <div className="pb-4 text-gray-500">No answers yet...</div>
) : (
<div className="self-end p-4 text-gray-500">
None of the above:{' '}
{formatPercent(getDpmOutcomeProbability(contract.totalShares, '0'))}
</div>
)} )}
{tradingAllowed(contract) && !resolveOption && ( {tradingAllowed(contract) &&
<CreateAnswerPanel contract={contract} /> (!resolveOption || resolveOption === 'CANCEL') && (
)} <CreateAnswerPanel contract={contract} />
)}
{user?.id === creatorId && !resolution && ( {user?.id === creatorId && !resolution && (
<AnswerResolvePanel <>
contract={contract} <Spacer h={2} />
resolveOption={resolveOption} <AnswerResolvePanel
setResolveOption={setResolveOption} contract={contract}
chosenAnswers={chosenAnswers} resolveOption={resolveOption}
/> setResolveOption={setResolveOption}
chosenAnswers={chosenAnswers}
/>
</>
)} )}
</Col> </Col>
) )

View File

@ -22,6 +22,7 @@ import {
} from '../../../common/calculate-dpm' } from '../../../common/calculate-dpm'
import { firebaseLogin } from '../../lib/firebase/users' import { firebaseLogin } from '../../lib/firebase/users'
import { Bet } from '../../../common/bet' import { Bet } from '../../../common/bet'
import { MAX_ANSWER_LENGTH } from '../../../common/answer'
export function CreateAnswerPanel(props: { export function CreateAnswerPanel(props: {
contract: FullContract<DPM, FreeResponse> contract: FullContract<DPM, FreeResponse>
@ -75,7 +76,7 @@ export function CreateAnswerPanel(props: {
const currentReturnPercent = (currentReturn * 100).toFixed() + '%' const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
return ( return (
<Col className="gap-4 rounded bg-gray-50 p-4"> <Col className="gap-4 rounded">
<Col className="flex-1 gap-2"> <Col className="flex-1 gap-2">
<div className="mb-1">Add your answer</div> <div className="mb-1">Add your answer</div>
<Textarea <Textarea
@ -84,7 +85,7 @@ export function CreateAnswerPanel(props: {
className="textarea textarea-bordered w-full resize-none" className="textarea textarea-bordered w-full resize-none"
placeholder="Type your answer..." placeholder="Type your answer..."
rows={1} rows={1}
maxLength={10000} maxLength={MAX_ANSWER_LENGTH}
/> />
<div /> <div />
<Col <Col

View File

@ -19,7 +19,7 @@ import { Bet } from '../../common/bet'
import { placeBet, sellShares } from '../lib/firebase/api-call' import { placeBet, sellShares } from '../lib/firebase/api-call'
import { BuyAmountInput, SellAmountInput } from './amount-input' import { BuyAmountInput, SellAmountInput } from './amount-input'
import { InfoTooltip } from './info-tooltip' import { InfoTooltip } from './info-tooltip'
import { OutcomeLabel } from './outcome-label' import { BinaryOutcomeLabel, OutcomeLabel } from './outcome-label'
import { import {
calculatePayoutAfterCorrectBet, calculatePayoutAfterCorrectBet,
calculateShares, calculateShares,
@ -58,7 +58,7 @@ export function BetPanel(props: {
<Row className="items-center justify-between gap-2"> <Row className="items-center justify-between gap-2">
<div> <div>
You have {formatWithCommas(Math.floor(shares))}{' '} You have {formatWithCommas(Math.floor(shares))}{' '}
<OutcomeLabel outcome={sharesOutcome} /> shares <BinaryOutcomeLabel outcome={sharesOutcome} /> shares
</div> </div>
<button <button
@ -147,7 +147,7 @@ export function BetPanelSwitcher(props: {
<Row className="items-center justify-between gap-2"> <Row className="items-center justify-between gap-2">
<div> <div>
You have {formatWithCommas(Math.floor(shares))}{' '} You have {formatWithCommas(Math.floor(shares))}{' '}
<OutcomeLabel outcome={sharesOutcome} /> shares <BinaryOutcomeLabel outcome={sharesOutcome} /> shares
</div> </div>
<button <button
@ -349,11 +349,12 @@ function BuyPanel(props: {
{contract.mechanism === 'dpm-2' ? ( {contract.mechanism === 'dpm-2' ? (
<> <>
Estimated Estimated
<br /> payout if <OutcomeLabel outcome={betChoice ?? 'YES'} /> <br /> payout if{' '}
<BinaryOutcomeLabel outcome={betChoice ?? 'YES'} />
</> </>
) : ( ) : (
<> <>
Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} /> Payout if <BinaryOutcomeLabel outcome={betChoice ?? 'YES'} />
</> </>
)} )}
</div> </div>
@ -547,7 +548,12 @@ function SellSharesModal(props: {
<div className="mb-6"> <div className="mb-6">
You have {formatWithCommas(Math.floor(shares))}{' '} You have {formatWithCommas(Math.floor(shares))}{' '}
<OutcomeLabel outcome={sharesOutcome} /> shares <OutcomeLabel
outcome={sharesOutcome}
contract={contract}
truncate="long"
/>{' '}
shares
</div> </div>
<SellPanel <SellPanel

View File

@ -250,11 +250,16 @@ function MyContractBets(props: {
</Row> </Row>
<Row className="flex-1 items-center gap-2 text-sm text-gray-500"> <Row className="flex-1 items-center gap-2 text-sm text-gray-500">
{isBinary && ( {(isBinary || resolution) && (
<> <>
{resolution ? ( {resolution ? (
<div> <div>
Resolved <OutcomeLabel outcome={resolution} /> Resolved{' '}
<OutcomeLabel
outcome={resolution}
contract={contract}
truncate="short"
/>
</div> </div>
) : ( ) : (
<div className="text-primary text-lg">{probPercent}</div> <div className="text-primary text-lg">{probPercent}</div>
@ -510,7 +515,15 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
</td> </td>
{isCPMM && <td>{shares >= 0 ? 'BUY' : 'SELL'}</td>} {isCPMM && <td>{shares >= 0 ? 'BUY' : 'SELL'}</td>}
<td> <td>
<OutcomeLabel outcome={outcome} /> {outcome === '0' ? (
'ANTE'
) : (
<OutcomeLabel
outcome={outcome}
contract={contract}
truncate="short"
/>
)}
</td> </td>
<td>{formatMoney(Math.abs(amount))}</td> <td>{formatMoney(Math.abs(amount))}</td>
{!isCPMM && <td>{saleDisplay}</td>} {!isCPMM && <td>{saleDisplay}</td>}
@ -561,7 +574,8 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
> >
<div className="mb-4 text-2xl"> <div className="mb-4 text-2xl">
Sell {formatWithCommas(shares)} shares of{' '} Sell {formatWithCommas(shares)} shares of{' '}
<OutcomeLabel outcome={outcome} /> for {formatMoney(saleAmount)}? <OutcomeLabel outcome={outcome} contract={contract} truncate="long" />{' '}
for {formatMoney(saleAmount)}?
</div> </div>
{!!loanAmount && ( {!!loanAmount && (
<div className="mt-2"> <div className="mt-2">

View File

@ -3,7 +3,7 @@ import Link from 'next/link'
import { ClockIcon, DatabaseIcon, PencilIcon } from '@heroicons/react/outline' import { ClockIcon, DatabaseIcon, PencilIcon } from '@heroicons/react/outline'
import { TrendingUpIcon } from '@heroicons/react/solid' import { TrendingUpIcon } from '@heroicons/react/solid'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { formatMoney, formatPercent } from '../../../common/util/format' import { formatMoney } from '../../../common/util/format'
import { UserLink } from '../user-page' import { UserLink } from '../user-page'
import { import {
Contract, Contract,
@ -19,9 +19,20 @@ import { fromNow } from '../../lib/util/time'
import { Avatar } from '../avatar' import { Avatar } from '../avatar'
import { Spacer } from '../layout/spacer' import { Spacer } from '../layout/spacer'
import { useState } from 'react' import { useState } from 'react'
import { getProbability } from '../../../common/calculate'
import { ContractInfoDialog } from './contract-info-dialog' import { ContractInfoDialog } from './contract-info-dialog'
import { Bet } from '../../../common/bet' import { Bet } from '../../../common/bet'
import {
Binary,
CPMM,
DPM,
FreeResponse,
FreeResponseContract,
FullContract,
} from '../../../common/contract'
import {
BinaryContractOutcomeLabel,
FreeResponseOutcomeLabel,
} from '../outcome-label'
export function ContractCard(props: { export function ContractCard(props: {
contract: Contract contract: Contract
@ -30,7 +41,7 @@ export function ContractCard(props: {
className?: string className?: string
}) { }) {
const { contract, showHotVolume, showCloseTime, className } = props const { contract, showHotVolume, showCloseTime, className } = props
const { question } = contract const { question, outcomeType, resolution } = contract
return ( return (
<div> <div>
@ -51,54 +62,48 @@ export function ContractCard(props: {
/> />
<Spacer h={3} /> <Spacer h={3} />
<Row className="justify-between gap-4"> <Row
className={clsx(
'justify-between gap-4',
outcomeType === 'FREE_RESPONSE' && 'flex-col items-start'
)}
>
<p <p
className="break-words font-medium text-indigo-700" className="break-words font-medium text-indigo-700"
style={{ /* For iOS safari */ wordBreak: 'break-word' }} style={{ /* For iOS safari */ wordBreak: 'break-word' }}
> >
{question} {question}
</p> </p>
<ResolutionOrChance className="items-center" contract={contract} /> {outcomeType === 'BINARY' && (
<BinaryResolutionOrChance
className="items-center"
contract={contract}
/>
)}
{outcomeType === 'FREE_RESPONSE' && resolution && (
<FreeResponseResolution
contract={contract as FullContract<DPM, FreeResponse>}
resolution={resolution}
truncate="long"
/>
)}
</Row> </Row>
</div> </div>
</div> </div>
) )
} }
export function ResolutionOrChance(props: { export function BinaryResolutionOrChance(props: {
contract: Contract contract: FullContract<DPM | CPMM, Binary>
large?: boolean large?: boolean
className?: string className?: string
}) { }) {
const { contract, large, className } = props const { contract, large, className } = props
const { resolution, outcomeType } = contract const { resolution } = contract
const isBinary = outcomeType === 'BINARY'
const marketClosed = (contract.closeTime || Infinity) < Date.now() const marketClosed = (contract.closeTime || Infinity) < Date.now()
const resolutionColor =
{
YES: 'text-primary',
NO: 'text-red-400',
MKT: 'text-blue-400',
CANCEL: 'text-yellow-400',
'': '', // Empty if unresolved
}[resolution || ''] ?? 'text-primary'
const probColor = marketClosed ? 'text-gray-400' : 'text-primary' const probColor = marketClosed ? 'text-gray-400' : 'text-primary'
const resolutionText =
{
YES: 'YES',
NO: 'NO',
MKT: isBinary
? formatPercent(
contract.resolutionProbability ?? getProbability(contract)
)
: 'MULTI',
CANCEL: 'N/A',
'': '',
}[resolution || ''] ?? `#${resolution}`
return ( return (
<Col className={clsx(large ? 'text-4xl' : 'text-3xl', className)}> <Col className={clsx(large ? 'text-4xl' : 'text-3xl', className)}>
{resolution ? ( {resolution ? (
@ -108,22 +113,41 @@ export function ResolutionOrChance(props: {
> >
Resolved Resolved
</div> </div>
<div className={resolutionColor}>{resolutionText}</div> <BinaryContractOutcomeLabel
contract={contract}
resolution={resolution}
/>
</> </>
) : ( ) : (
isBinary && ( <>
<> <div className={probColor}>{getBinaryProbPercent(contract)}</div>
<div className={probColor}>{getBinaryProbPercent(contract)}</div> <div className={clsx(probColor, large ? 'text-xl' : 'text-base')}>
<div className={clsx(probColor, large ? 'text-xl' : 'text-base')}> chance
chance </div>
</div> </>
</>
)
)} )}
</Col> </Col>
) )
} }
export function FreeResponseResolution(props: {
contract: FreeResponseContract
resolution: string
truncate: 'short' | 'long' | 'none'
}) {
const { contract, resolution, truncate } = props
return (
<Col className="text-xl">
<div className={clsx('text-base text-gray-500')}>Resolved</div>
<FreeResponseOutcomeLabel
contract={contract}
resolution={resolution}
truncate={truncate}
/>
</Col>
)
}
function AbbrContractDetails(props: { function AbbrContractDetails(props: {
contract: Contract contract: Contract
showHotVolume?: boolean showHotVolume?: boolean

View File

@ -6,7 +6,11 @@ import { useUser } from '../../hooks/use-user'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { Linkify } from '../linkify' import { Linkify } from '../linkify'
import clsx from 'clsx' import clsx from 'clsx'
import { ContractDetails, ResolutionOrChance } from './contract-card' import {
FreeResponseResolution,
ContractDetails,
BinaryResolutionOrChance,
} from './contract-card'
import { Bet } from '../../../common/bet' import { Bet } from '../../../common/bet'
import { Comment } from '../../../common/comment' import { Comment } from '../../../common/comment'
import BetRow from '../bet-row' import BetRow from '../bet-row'
@ -18,11 +22,10 @@ export const ContractOverview = (props: {
contract: Contract contract: Contract
bets: Bet[] bets: Bet[]
comments: Comment[] comments: Comment[]
children?: any
className?: string className?: string
}) => { }) => {
const { contract, bets, comments, children, className } = props const { contract, bets, className } = props
const { question, resolution, creatorId, outcomeType } = contract const { question, creatorId, outcomeType, resolution } = contract
const user = useUser() const user = useUser()
const isCreator = user?.id === creatorId const isCreator = user?.id === creatorId
@ -36,8 +39,8 @@ export const ContractOverview = (props: {
<Linkify text={question} /> <Linkify text={question} />
</div> </div>
{(isBinary || resolution) && ( {isBinary && (
<ResolutionOrChance <BinaryResolutionOrChance
className="hidden items-end xl:flex" className="hidden items-end xl:flex"
contract={contract} contract={contract}
large large
@ -45,15 +48,24 @@ export const ContractOverview = (props: {
)} )}
</Row> </Row>
<Row className="items-center justify-between gap-4 xl:hidden"> {isBinary ? (
{(isBinary || resolution) && ( <Row className="items-center justify-between gap-4 xl:hidden">
<ResolutionOrChance contract={contract} /> <BinaryResolutionOrChance contract={contract} />
)}
{isBinary && tradingAllowed(contract) && ( {tradingAllowed(contract) && (
<BetRow contract={contract} labelClassName="hidden" /> <BetRow contract={contract} labelClassName="hidden" />
)} )}
</Row> </Row>
) : (
outcomeType === 'FREE_RESPONSE' &&
resolution && (
<FreeResponseResolution
contract={contract}
resolution={resolution}
truncate="none"
/>
)
)}
<ContractDetails <ContractDetails
contract={contract} contract={contract}
@ -73,19 +85,13 @@ export const ContractOverview = (props: {
/> />
)} )}
<Spacer h={6} /> {contract.description && <Spacer h={6} />}
<ContractDescription <ContractDescription
className="px-2" className="px-2"
contract={contract} contract={contract}
isCreator={isCreator} isCreator={isCreator}
/> />
<Spacer h={4} />
{children}
<Spacer h={4} />
</Col> </Col>
) )
} }

View File

@ -97,9 +97,10 @@ function groupBets(
hideOutcome: boolean hideOutcome: boolean
abbreviated: boolean abbreviated: boolean
smallAvatar: boolean smallAvatar: boolean
reversed: boolean
} }
) { ) {
const { hideOutcome, abbreviated, smallAvatar } = options const { hideOutcome, abbreviated, smallAvatar, reversed } = options
const commentsMap = mapCommentsByBetId(comments) const commentsMap = mapCommentsByBetId(comments)
const items: ActivityItem[] = [] const items: ActivityItem[] = []
@ -171,7 +172,9 @@ function groupBets(
if (group.length > 0) { if (group.length > 0) {
pushGroup() pushGroup()
} }
return abbreviated ? items.slice(-3) : items const abbrItems = abbreviated ? items.slice(-3) : items
if (reversed) abbrItems.reverse()
return abbrItems
} }
function getAnswerGroups( function getAnswerGroups(
@ -182,9 +185,10 @@ function getAnswerGroups(
options: { options: {
sortByProb: boolean sortByProb: boolean
abbreviated: boolean abbreviated: boolean
reversed: boolean
} }
) { ) {
const { sortByProb, abbreviated } = options const { sortByProb, abbreviated, reversed } = options
let outcomes = _.uniq(bets.map((bet) => bet.outcome)).filter( let outcomes = _.uniq(bets.map((bet) => bet.outcome)).filter(
(outcome) => getOutcomeProbability(contract, outcome) > 0.0001 (outcome) => getOutcomeProbability(contract, outcome) > 0.0001
@ -208,9 +212,8 @@ function getAnswerGroups(
outcomes = outcomes.slice(-2) outcomes = outcomes.slice(-2)
} }
if (sortByProb) { if (sortByProb) {
outcomes = _.sortBy( outcomes = _.sortBy(outcomes, (outcome) =>
outcomes, getOutcomeProbability(contract, outcome)
(outcome) => -1 * getOutcomeProbability(contract, outcome)
) )
} else { } else {
// Sort by recent bet. // Sort by recent bet.
@ -233,6 +236,7 @@ function getAnswerGroups(
hideOutcome: true, hideOutcome: true,
abbreviated, abbreviated,
smallAvatar: true, smallAvatar: true,
reversed,
}) })
if (abbreviated) items = items.slice(-2) if (abbreviated) items = items.slice(-2)
@ -264,6 +268,8 @@ export function getAllContractActivityItems(
const { abbreviated } = options const { abbreviated } = options
const { outcomeType } = contract const { outcomeType } = contract
const reversed = !abbreviated
bets = bets =
outcomeType === 'BINARY' outcomeType === 'BINARY'
? bets.filter((bet) => !bet.isAnte && !bet.isRedemption) ? bets.filter((bet) => !bet.isAnte && !bet.isRedemption)
@ -301,12 +307,14 @@ export function getAllContractActivityItems(
{ {
sortByProb: true, sortByProb: true,
abbreviated, abbreviated,
reversed,
} }
) )
: groupBets(bets, comments, contract, user?.id, { : groupBets(bets, comments, contract, user?.id, {
hideOutcome: !!filterToOutcome, hideOutcome: !!filterToOutcome,
abbreviated, abbreviated,
smallAvatar: !!filterToOutcome, smallAvatar: !!filterToOutcome,
reversed: false,
})) }))
) )
@ -317,14 +325,7 @@ export function getAllContractActivityItems(
items.push({ type: 'resolve', id: `${contract.resolutionTime}`, contract }) items.push({ type: 'resolve', id: `${contract.resolutionTime}`, contract })
} }
if (!abbreviated) { if (reversed) items.reverse()
items.reverse()
for (const item of items) {
if (item.type === 'answergroup') {
item.items.reverse()
}
}
}
return items return items
} }
@ -362,12 +363,14 @@ export function getRecentContractActivityItems(
{ {
sortByProb: false, sortByProb: false,
abbreviated: true, abbreviated: true,
reversed: false,
} }
) )
: groupBets(bets, comments, contract, user?.id, { : groupBets(bets, comments, contract, user?.id, {
hideOutcome: false, hideOutcome: false,
abbreviated: true, abbreviated: true,
smallAvatar: false, smallAvatar: false,
reversed: false,
}) })
return [questionItem, ...items] return [questionItem, ...items]

View File

@ -26,7 +26,7 @@ import { Row } from '../layout/row'
import { createComment, MAX_COMMENT_LENGTH } from '../../lib/firebase/comments' import { createComment, MAX_COMMENT_LENGTH } from '../../lib/firebase/comments'
import { formatMoney, formatPercent } from '../../../common/util/format' import { formatMoney, formatPercent } from '../../../common/util/format'
import { Comment } from '../../../common/comment' import { Comment } from '../../../common/comment'
import { ResolutionOrChance } from '../contract/contract-card' import { BinaryResolutionOrChance } from '../contract/contract-card'
import { SiteLink } from '../site-link' 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'
@ -144,7 +144,12 @@ export function FeedComment(props: {
{!hideOutcome && ( {!hideOutcome && (
<> <>
{' '} {' '}
of <OutcomeLabel outcome={outcome} /> of{' '}
<OutcomeLabel
outcome={outcome}
contract={contract}
truncate="short"
/>
</> </>
)} )}
<RelativeTimestamp time={createdTime} /> <RelativeTimestamp time={createdTime} />
@ -227,7 +232,12 @@ export function FeedBet(props: {
{!hideOutcome && ( {!hideOutcome && (
<> <>
{' '} {' '}
of <OutcomeLabel outcome={outcome} /> of{' '}
<OutcomeLabel
outcome={outcome}
contract={contract}
truncate="short"
/>
</> </>
)} )}
<RelativeTimestamp time={createdTime} /> <RelativeTimestamp time={createdTime} />
@ -344,8 +354,11 @@ export function FeedQuestion(props: {
{question} {question}
</SiteLink> </SiteLink>
</Col> </Col>
{(isBinary || resolution) && ( {isBinary && (
<ResolutionOrChance className="items-center" contract={contract} /> <BinaryResolutionOrChance
className="items-center"
contract={contract}
/>
)} )}
</Col> </Col>
{showDescription && ( {showDescription && (
@ -449,7 +462,12 @@ function FeedResolve(props: { contract: Contract }) {
name={creatorName} name={creatorName}
username={creatorUsername} username={creatorUsername}
/>{' '} />{' '}
resolved this market to <OutcomeLabel outcome={resolution} />{' '} resolved this market to{' '}
<OutcomeLabel
outcome={resolution}
contract={contract}
truncate="long"
/>{' '}
<RelativeTimestamp time={contract.resolutionTime || 0} /> <RelativeTimestamp time={contract.resolutionTime || 0} />
</div> </div>
</div> </div>
@ -482,8 +500,12 @@ function FeedClose(props: { contract: Contract }) {
) )
} }
function BetGroupSpan(props: { bets: Bet[]; outcome?: string }) { function BetGroupSpan(props: {
const { bets, outcome } = props contract: Contract
bets: Bet[]
outcome?: string
}) {
const { contract, bets, outcome } = props
const numberTraders = _.uniqBy(bets, (b) => b.userId).length const numberTraders = _.uniqBy(bets, (b) => b.userId).length
@ -501,7 +523,12 @@ function BetGroupSpan(props: { bets: Bet[]; outcome?: string }) {
{outcome && ( {outcome && (
<> <>
{' '} {' '}
of <OutcomeLabel outcome={outcome} /> of{' '}
<OutcomeLabel
outcome={outcome}
contract={contract}
truncate="short"
/>
</> </>
)}{' '} )}{' '}
</span> </span>
@ -513,7 +540,7 @@ function FeedBetGroup(props: {
bets: Bet[] bets: Bet[]
hideOutcome: boolean hideOutcome: boolean
}) { }) {
const { bets, hideOutcome } = props const { contract, bets, hideOutcome } = props
const betGroups = _.groupBy(bets, (bet) => bet.outcome) const betGroups = _.groupBy(bets, (bet) => bet.outcome)
const outcomes = Object.keys(betGroups) const outcomes = Object.keys(betGroups)
@ -535,6 +562,7 @@ function FeedBetGroup(props: {
{outcomes.map((outcome, index) => ( {outcomes.map((outcome, index) => (
<Fragment key={outcome}> <Fragment key={outcome}>
<BetGroupSpan <BetGroupSpan
contract={contract}
outcome={hideOutcome ? undefined : outcome} outcome={hideOutcome ? undefined : outcome}
bets={betGroups[outcome]} bets={betGroups[outcome]}
/> />
@ -583,7 +611,7 @@ function FeedAnswerGroup(props: {
<UserLink username={username} name={name} /> answered <UserLink username={username} name={name} /> answered
</div> </div>
<Row className="align-items justify-between gap-4"> <Col className="align-items justify-between gap-4 sm:flex-row">
<span className="whitespace-pre-line text-lg"> <span className="whitespace-pre-line text-lg">
<Linkify text={text} /> <Linkify text={text} />
</span> </span>
@ -599,13 +627,13 @@ function FeedAnswerGroup(props: {
</span> </span>
<BuyButton <BuyButton
className={clsx( className={clsx(
'btn-sm hidden flex-initial !px-6 sm:flex', 'btn-sm flex-initial !px-6 sm:flex',
tradingAllowed(contract) ? '' : '!hidden' tradingAllowed(contract) ? '' : '!hidden'
)} )}
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
/> />
</Row> </Row>
</Row> </Col>
</Col> </Col>
</Row> </Row>
@ -628,15 +656,6 @@ function FeedAnswerGroup(props: {
</div> </div>
</div> </div>
))} ))}
<div
className={clsx('ml-10 mt-4', tradingAllowed(contract) ? '' : 'hidden')}
>
<BuyButton
className="btn-sm !px-6 sm:hidden"
onClick={() => setOpen(true)}
/>
</div>
</Col> </Col>
) )
} }

View File

@ -1,13 +1,74 @@
import { Answer } from '../../common/answer'
import { getProbability } from '../../common/calculate'
import {
Binary,
Contract,
CPMM,
DPM,
FreeResponse,
FreeResponseContract,
FullContract,
} from '../../common/contract'
import { formatPercent } from '../../common/util/format'
export function OutcomeLabel(props: { export function OutcomeLabel(props: {
contract: Contract
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' | string outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' | string
truncate: 'short' | 'long' | 'none'
}) {
const { outcome, contract, truncate } = props
if (contract.outcomeType === 'BINARY')
return <BinaryOutcomeLabel outcome={outcome as any} />
return (
<FreeResponseOutcomeLabel
contract={contract as FullContract<DPM, FreeResponse>}
resolution={outcome}
truncate={truncate}
/>
)
}
export function BinaryOutcomeLabel(props: {
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT'
}) { }) {
const { outcome } = props const { outcome } = props
if (outcome === 'YES') return <YesLabel /> if (outcome === 'YES') return <YesLabel />
if (outcome === 'NO') return <NoLabel /> if (outcome === 'NO') return <NoLabel />
if (outcome === 'MKT') return <ProbLabel /> if (outcome === 'MKT') return <ProbLabel />
if (outcome === 'CANCEL') return <CancelLabel /> return <CancelLabel />
return <AnswerNumberLabel number={outcome} /> }
export function BinaryContractOutcomeLabel(props: {
contract: FullContract<DPM | CPMM, Binary>
resolution: 'YES' | 'NO' | 'CANCEL' | 'MKT'
}) {
const { contract, resolution } = props
if (resolution === 'MKT') {
const prob = contract.resolutionProbability ?? getProbability(contract)
return <ProbPercentLabel prob={prob} />
}
return <BinaryOutcomeLabel outcome={resolution} />
}
export function FreeResponseOutcomeLabel(props: {
contract: FreeResponseContract
resolution: string | 'CANCEL' | 'MKT'
truncate: 'short' | 'long' | 'none'
}) {
const { contract, resolution, truncate } = props
if (resolution === 'CANCEL') return <CancelLabel />
if (resolution === 'MKT') return <MultiLabel />
const { answers } = contract
const chosen = answers?.find((answer) => answer.id === resolution)
if (!chosen) return <AnswerNumberLabel number={resolution} />
return <AnswerLabel answer={chosen} truncate={truncate} />
} }
export function YesLabel() { export function YesLabel() {
@ -26,6 +87,32 @@ export function ProbLabel() {
return <span className="text-blue-400">PROB</span> return <span className="text-blue-400">PROB</span>
} }
export function MultiLabel() {
return <span className="text-blue-400">MULTI</span>
}
export function ProbPercentLabel(props: { prob: number }) {
const { prob } = props
return <span className="text-blue-400">{formatPercent(prob)}</span>
}
export function AnswerNumberLabel(props: { number: string }) { export function AnswerNumberLabel(props: { number: string }) {
return <span className="text-primary">#{props.number}</span> return <span className="text-primary">#{props.number}</span>
} }
export function AnswerLabel(props: {
answer: Answer
truncate: 'short' | 'long' | 'none'
}) {
const { answer, truncate } = props
const { text } = answer
let truncated = text
if (truncate === 'short' && text.length > 20) {
truncated = text.slice(0, 10) + '...' + text.slice(-10)
} else if (truncate === 'long' && text.length > 75) {
truncated = text.slice(0, 75) + '...'
}
return <span className="text-primary">{truncated}</span>
}

View File

@ -195,7 +195,7 @@ function Button(props: {
color === 'red' && 'bg-red-400 text-white hover:bg-red-500', color === 'red' && 'bg-red-400 text-white hover:bg-red-500',
color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500', color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500',
color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500', color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500',
color === 'gray' && 'bg-gray-300 text-gray-700 hover:bg-gray-400', color === 'gray' && 'bg-gray-200 text-gray-700 hover:bg-gray-300',
className className
)} )}
onClick={onClick} onClick={onClick}

View File

@ -98,7 +98,7 @@ export function ContractPageContent(props: FirstArgument<typeof ContractPage>) {
return <Custom404 /> return <Custom404 />
} }
const { creatorId, isResolved, question, outcomeType } = contract const { creatorId, isResolved, question, outcomeType, resolution } = contract
const isCreator = user?.id === creatorId const isCreator = user?.id === creatorId
const isBinary = outcomeType === 'BINARY' const isBinary = outcomeType === 'BINARY'
@ -143,8 +143,10 @@ export function ContractPageContent(props: FirstArgument<typeof ContractPage>) {
contract={contract} contract={contract}
bets={bets ?? []} bets={bets ?? []}
comments={comments ?? []} comments={comments ?? []}
> />
{contract.outcomeType === 'FREE_RESPONSE' && (
{outcomeType === 'FREE_RESPONSE' &&
(!isResolved || resolution === 'MKT') && (
<> <>
<Spacer h={4} /> <Spacer h={4} />
<AnswersPanel <AnswersPanel
@ -153,7 +155,6 @@ export function ContractPageContent(props: FirstArgument<typeof ContractPage>) {
<Spacer h={4} /> <Spacer h={4} />
</> </>
)} )}
</ContractOverview>
{contract.isResolved && ( {contract.isResolved && (
<> <>

View File

@ -8,8 +8,9 @@ import {
import { DOMAIN } from '../../../../common/envs/constants' import { DOMAIN } from '../../../../common/envs/constants'
import { AnswersGraph } from '../../../components/answers/answers-graph' import { AnswersGraph } from '../../../components/answers/answers-graph'
import { import {
ResolutionOrChance, BinaryResolutionOrChance,
ContractDetails, ContractDetails,
FreeResponseResolution,
} from '../../../components/contract/contract-card' } from '../../../components/contract/contract-card'
import { ContractProbGraph } from '../../../components/contract/contract-prob-graph' import { ContractProbGraph } from '../../../components/contract/contract-prob-graph'
import { Col } from '../../../components/layout/col' import { Col } from '../../../components/layout/col'
@ -118,8 +119,14 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
hideShareButtons hideShareButtons
/> />
{(isBinary || resolution) && ( {isBinary && <BinaryResolutionOrChance contract={contract} />}
<ResolutionOrChance contract={contract} />
{outcomeType === 'FREE_RESPONSE' && resolution && (
<FreeResponseResolution
contract={contract}
resolution={resolution}
truncate="long"
/>
)} )}
</Row> </Row>