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:
parent
7abc11c146
commit
9c74f88b4a
|
@ -29,3 +29,5 @@ export const getNoneAnswer = (contractId: string, creator: User) => {
|
|||
text: 'None',
|
||||
}
|
||||
}
|
||||
|
||||
export const MAX_ANSWER_LENGTH = 240
|
||||
|
|
|
@ -38,6 +38,8 @@ export type FullContract<
|
|||
T
|
||||
|
||||
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 = {
|
||||
mechanism: 'dpm-2'
|
||||
|
@ -61,18 +63,20 @@ export type Binary = {
|
|||
outcomeType: 'BINARY'
|
||||
initialProbability: number
|
||||
resolutionProbability?: number // Used for BINARY markets resolved to MKT
|
||||
resolution?: 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
||||
}
|
||||
|
||||
export type Multi = {
|
||||
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 = {
|
||||
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'
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from '../../common/contract'
|
||||
import { User } from '../../common/user'
|
||||
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 { sendNewAnswerEmail } from './emails'
|
||||
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))
|
||||
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' }
|
||||
|
||||
// Run as transaction to prevent race conditions.
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Answer } from '../../common/answer'
|
|||
import { Bet } from '../../common/bet'
|
||||
import { getProbability } from '../../common/calculate'
|
||||
import { Comment } from '../../common/comment'
|
||||
import { Contract } from '../../common/contract'
|
||||
import { Contract, FreeResponseContract } from '../../common/contract'
|
||||
import { CREATOR_FEE } from '../../common/fees'
|
||||
import { PrivateUser, User } from '../../common/user'
|
||||
import { formatMoney, formatPercent } from '../../common/util/format'
|
||||
|
@ -98,6 +98,10 @@ const toDisplayResolution = (
|
|||
if (resolution === 'MKT' && resolutions) return 'MULTI'
|
||||
if (resolution === 'CANCEL') return 'N/A'
|
||||
|
||||
const answer = (contract as FreeResponseContract).answers?.find(
|
||||
(a) => a.id === resolution
|
||||
)
|
||||
if (answer) return answer.text
|
||||
return `#${resolution}`
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import clsx from 'clsx'
|
||||
import _ from 'lodash'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Answer } from '../../../common/answer'
|
||||
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
||||
|
@ -8,19 +7,14 @@ import { Col } from '../layout/col'
|
|||
import { Row } from '../layout/row'
|
||||
import { Avatar } from '../avatar'
|
||||
import { SiteLink } from '../site-link'
|
||||
import { BuyButton } from '../yes-no-selector'
|
||||
import { formatPercent } from '../../../common/util/format'
|
||||
import { getDpmOutcomeProbability } from '../../../common/calculate-dpm'
|
||||
import { tradingAllowed } from '../../lib/firebase/contracts'
|
||||
import { AnswerBetPanel } from './answer-bet-panel'
|
||||
import { Linkify } from '../linkify'
|
||||
import { User } from '../../../common/user'
|
||||
import { ContractActivity } from '../feed/contract-activity'
|
||||
|
||||
export function AnswerItem(props: {
|
||||
answer: Answer
|
||||
contract: FullContract<DPM, FreeResponse>
|
||||
user: User | null | undefined
|
||||
showChoice: 'radio' | 'checkbox' | undefined
|
||||
chosenProb: number | undefined
|
||||
totalChosenProb?: number
|
||||
|
@ -30,7 +24,6 @@ export function AnswerItem(props: {
|
|||
const {
|
||||
answer,
|
||||
contract,
|
||||
user,
|
||||
showChoice,
|
||||
chosenProb,
|
||||
totalChosenProb,
|
||||
|
@ -47,10 +40,6 @@ export function AnswerItem(props: {
|
|||
const wasResolvedTo =
|
||||
resolution === answer.id || (resolutions && resolutions[answer.id])
|
||||
|
||||
const [isBetting, setIsBetting] = useState(false)
|
||||
|
||||
const canBet = !isBetting && !showChoice && tradingAllowed(contract)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
|
@ -63,10 +52,8 @@ export function AnswerItem(props: {
|
|||
? 'bg-gray-50'
|
||||
: showChoice === 'radio'
|
||||
? 'bg-green-50'
|
||||
: 'bg-blue-50',
|
||||
canBet && 'cursor-pointer hover:bg-gray-100'
|
||||
: 'bg-blue-50'
|
||||
)}
|
||||
onClick={() => canBet && setIsBetting(true)}
|
||||
>
|
||||
<Col className="flex-1 gap-3">
|
||||
<div className="whitespace-pre-line">
|
||||
|
@ -83,124 +70,91 @@ export function AnswerItem(props: {
|
|||
{/* TODO: Show total pool? */}
|
||||
<div className="text-base">#{number}</div>
|
||||
</Row>
|
||||
|
||||
{isBetting && (
|
||||
<ContractActivity
|
||||
className="hidden md:flex"
|
||||
contract={contract}
|
||||
bets={[]}
|
||||
comments={[]}
|
||||
user={user}
|
||||
filterToOutcome={answer.id}
|
||||
mode="all"
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{isBetting ? (
|
||||
<AnswerBetPanel
|
||||
answer={answer}
|
||||
contract={contract}
|
||||
closePanel={() => setIsBetting(false)}
|
||||
className="sm:w-72"
|
||||
/>
|
||||
) : (
|
||||
<Row className="items-center justify-end gap-4 self-end sm:self-start">
|
||||
{!wasResolvedTo &&
|
||||
(showChoice === 'checkbox' ? (
|
||||
<input
|
||||
className="input input-bordered w-24 justify-self-end text-2xl"
|
||||
type="number"
|
||||
placeholder={`${roundedProb}`}
|
||||
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>
|
||||
<Row className="items-center justify-end gap-4 self-end sm:self-start">
|
||||
{!wasResolvedTo &&
|
||||
(showChoice === 'checkbox' ? (
|
||||
<input
|
||||
className="input input-bordered w-24 justify-self-end text-2xl"
|
||||
type="number"
|
||||
placeholder={`${roundedProb}`}
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{tradingAllowed(contract) && (
|
||||
<BuyButton
|
||||
className="btn-md flex-initial justify-end self-end !px-8"
|
||||
onClick={() => {
|
||||
setIsBetting(true)
|
||||
}}
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
{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>
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
</label>
|
||||
{showChoice === 'checkbox' && (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ export function AnswerResolvePanel(props: {
|
|||
: 'btn-disabled'
|
||||
|
||||
return (
|
||||
<Col className="gap-4 rounded bg-gray-50 p-4">
|
||||
<Col className="gap-4 rounded">
|
||||
<div>Resolve your market</div>
|
||||
<Col className="gap-4 sm:flex-row sm:items-center">
|
||||
<ChooseCancelSelector
|
||||
|
|
|
@ -38,7 +38,8 @@ export const AnswersGraph = memo(function AnswersGraph(props: {
|
|||
|
||||
const { width } = useWindowSize()
|
||||
|
||||
const labelLength = !width || width > 800 ? 50 : 20
|
||||
const isLargeWidth = !width || width > 800
|
||||
const labelLength = isLargeWidth ? 50 : 20
|
||||
|
||||
const endTime =
|
||||
resolutionTime || isClosed
|
||||
|
@ -68,16 +69,15 @@ export const AnswersGraph = memo(function AnswersGraph(props: {
|
|||
answers?.find((answer) => answer.id === outcome)?.text ?? 'None'
|
||||
const answerText =
|
||||
answer.slice(0, labelLength) + (answer.length > labelLength ? '...' : '')
|
||||
const id = `#${outcome}: ${answerText}`
|
||||
|
||||
return { id, data: points }
|
||||
return { id: answerText, data: points }
|
||||
})
|
||||
|
||||
data.reverse()
|
||||
|
||||
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 startDate = dayjs(contract.createdTime).isBefore(hoursAgo)
|
||||
? new Date(contract.createdTime)
|
||||
|
@ -87,8 +87,8 @@ export const AnswersGraph = memo(function AnswersGraph(props: {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="w-full overflow-hidden"
|
||||
style={{ height: height ?? (!width || width >= 800 ? 350 : 225) }}
|
||||
className="w-full"
|
||||
style={{ height: height ?? (isLargeWidth ? 350 : 250) }}
|
||||
>
|
||||
<ResponsiveLine
|
||||
data={data}
|
||||
|
@ -116,6 +116,32 @@ export const AnswersGraph = memo(function AnswersGraph(props: {
|
|||
enableArea
|
||||
areaOpacity={1}
|
||||
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>
|
||||
)
|
||||
|
|
|
@ -3,7 +3,6 @@ import { useLayoutEffect, useState } from 'react'
|
|||
|
||||
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
||||
import { Col } from '../layout/col'
|
||||
import { formatPercent } from '../../../common/util/format'
|
||||
import { useUser } from '../../hooks/use-user'
|
||||
import { getDpmOutcomeProbability } from '../../../common/calculate-dpm'
|
||||
import { useAnswers } from '../../hooks/use-answers'
|
||||
|
@ -11,6 +10,7 @@ import { tradingAllowed } from '../../lib/firebase/contracts'
|
|||
import { AnswerItem } from './answer-item'
|
||||
import { CreateAnswerPanel } from './create-answer-panel'
|
||||
import { AnswerResolvePanel } from './answer-resolve-panel'
|
||||
import { Spacer } from '../layout/spacer'
|
||||
|
||||
export function AnswersPanel(props: {
|
||||
contract: FullContract<DPM, FreeResponse>
|
||||
|
@ -31,7 +31,7 @@ export function AnswersPanel(props: {
|
|||
resolutions ? -1 * resolutions[answer.id] : 0
|
||||
),
|
||||
..._.sortBy(
|
||||
otherAnswers,
|
||||
resolution ? [] : otherAnswers,
|
||||
(answer) => -1 * getDpmOutcomeProbability(contract.totalShares, answer.id)
|
||||
),
|
||||
]
|
||||
|
@ -82,40 +82,41 @@ export function AnswersPanel(props: {
|
|||
|
||||
return (
|
||||
<Col className="gap-3">
|
||||
{sortedAnswers.map((answer) => (
|
||||
<AnswerItem
|
||||
key={answer.id}
|
||||
answer={answer}
|
||||
contract={contract}
|
||||
user={user}
|
||||
showChoice={showChoice}
|
||||
chosenProb={chosenAnswers[answer.id]}
|
||||
totalChosenProb={chosenTotal}
|
||||
onChoose={onChoose}
|
||||
onDeselect={onDeselect}
|
||||
/>
|
||||
))}
|
||||
{(resolveOption === 'CHOOSE' ||
|
||||
resolveOption === 'CHOOSE_MULTIPLE' ||
|
||||
resolution === 'MKT') &&
|
||||
sortedAnswers.map((answer) => (
|
||||
<AnswerItem
|
||||
key={answer.id}
|
||||
answer={answer}
|
||||
contract={contract}
|
||||
showChoice={showChoice}
|
||||
chosenProb={chosenAnswers[answer.id]}
|
||||
totalChosenProb={chosenTotal}
|
||||
onChoose={onChoose}
|
||||
onDeselect={onDeselect}
|
||||
/>
|
||||
))}
|
||||
|
||||
{sortedAnswers.length === 0 ? (
|
||||
<div className="p-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>
|
||||
{sortedAnswers.length === 0 && (
|
||||
<div className="pb-4 text-gray-500">No answers yet...</div>
|
||||
)}
|
||||
|
||||
{tradingAllowed(contract) && !resolveOption && (
|
||||
<CreateAnswerPanel contract={contract} />
|
||||
)}
|
||||
{tradingAllowed(contract) &&
|
||||
(!resolveOption || resolveOption === 'CANCEL') && (
|
||||
<CreateAnswerPanel contract={contract} />
|
||||
)}
|
||||
|
||||
{user?.id === creatorId && !resolution && (
|
||||
<AnswerResolvePanel
|
||||
contract={contract}
|
||||
resolveOption={resolveOption}
|
||||
setResolveOption={setResolveOption}
|
||||
chosenAnswers={chosenAnswers}
|
||||
/>
|
||||
<>
|
||||
<Spacer h={2} />
|
||||
<AnswerResolvePanel
|
||||
contract={contract}
|
||||
resolveOption={resolveOption}
|
||||
setResolveOption={setResolveOption}
|
||||
chosenAnswers={chosenAnswers}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from '../../../common/calculate-dpm'
|
||||
import { firebaseLogin } from '../../lib/firebase/users'
|
||||
import { Bet } from '../../../common/bet'
|
||||
import { MAX_ANSWER_LENGTH } from '../../../common/answer'
|
||||
|
||||
export function CreateAnswerPanel(props: {
|
||||
contract: FullContract<DPM, FreeResponse>
|
||||
|
@ -75,7 +76,7 @@ export function CreateAnswerPanel(props: {
|
|||
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
||||
|
||||
return (
|
||||
<Col className="gap-4 rounded bg-gray-50 p-4">
|
||||
<Col className="gap-4 rounded">
|
||||
<Col className="flex-1 gap-2">
|
||||
<div className="mb-1">Add your answer</div>
|
||||
<Textarea
|
||||
|
@ -84,7 +85,7 @@ export function CreateAnswerPanel(props: {
|
|||
className="textarea textarea-bordered w-full resize-none"
|
||||
placeholder="Type your answer..."
|
||||
rows={1}
|
||||
maxLength={10000}
|
||||
maxLength={MAX_ANSWER_LENGTH}
|
||||
/>
|
||||
<div />
|
||||
<Col
|
||||
|
|
|
@ -19,7 +19,7 @@ import { Bet } from '../../common/bet'
|
|||
import { placeBet, sellShares } from '../lib/firebase/api-call'
|
||||
import { BuyAmountInput, SellAmountInput } from './amount-input'
|
||||
import { InfoTooltip } from './info-tooltip'
|
||||
import { OutcomeLabel } from './outcome-label'
|
||||
import { BinaryOutcomeLabel, OutcomeLabel } from './outcome-label'
|
||||
import {
|
||||
calculatePayoutAfterCorrectBet,
|
||||
calculateShares,
|
||||
|
@ -58,7 +58,7 @@ export function BetPanel(props: {
|
|||
<Row className="items-center justify-between gap-2">
|
||||
<div>
|
||||
You have {formatWithCommas(Math.floor(shares))}{' '}
|
||||
<OutcomeLabel outcome={sharesOutcome} /> shares
|
||||
<BinaryOutcomeLabel outcome={sharesOutcome} /> shares
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
@ -147,7 +147,7 @@ export function BetPanelSwitcher(props: {
|
|||
<Row className="items-center justify-between gap-2">
|
||||
<div>
|
||||
You have {formatWithCommas(Math.floor(shares))}{' '}
|
||||
<OutcomeLabel outcome={sharesOutcome} /> shares
|
||||
<BinaryOutcomeLabel outcome={sharesOutcome} /> shares
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
@ -349,11 +349,12 @@ function BuyPanel(props: {
|
|||
{contract.mechanism === 'dpm-2' ? (
|
||||
<>
|
||||
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>
|
||||
|
@ -547,7 +548,12 @@ function SellSharesModal(props: {
|
|||
|
||||
<div className="mb-6">
|
||||
You have {formatWithCommas(Math.floor(shares))}{' '}
|
||||
<OutcomeLabel outcome={sharesOutcome} /> shares
|
||||
<OutcomeLabel
|
||||
outcome={sharesOutcome}
|
||||
contract={contract}
|
||||
truncate="long"
|
||||
/>{' '}
|
||||
shares
|
||||
</div>
|
||||
|
||||
<SellPanel
|
||||
|
|
|
@ -250,11 +250,16 @@ function MyContractBets(props: {
|
|||
</Row>
|
||||
|
||||
<Row className="flex-1 items-center gap-2 text-sm text-gray-500">
|
||||
{isBinary && (
|
||||
{(isBinary || resolution) && (
|
||||
<>
|
||||
{resolution ? (
|
||||
<div>
|
||||
Resolved <OutcomeLabel outcome={resolution} />
|
||||
Resolved{' '}
|
||||
<OutcomeLabel
|
||||
outcome={resolution}
|
||||
contract={contract}
|
||||
truncate="short"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-primary text-lg">{probPercent}</div>
|
||||
|
@ -510,7 +515,15 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
|||
</td>
|
||||
{isCPMM && <td>{shares >= 0 ? 'BUY' : 'SELL'}</td>}
|
||||
<td>
|
||||
<OutcomeLabel outcome={outcome} />
|
||||
{outcome === '0' ? (
|
||||
'ANTE'
|
||||
) : (
|
||||
<OutcomeLabel
|
||||
outcome={outcome}
|
||||
contract={contract}
|
||||
truncate="short"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>{formatMoney(Math.abs(amount))}</td>
|
||||
{!isCPMM && <td>{saleDisplay}</td>}
|
||||
|
@ -561,7 +574,8 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
|||
>
|
||||
<div className="mb-4 text-2xl">
|
||||
Sell {formatWithCommas(shares)} shares of{' '}
|
||||
<OutcomeLabel outcome={outcome} /> for {formatMoney(saleAmount)}?
|
||||
<OutcomeLabel outcome={outcome} contract={contract} truncate="long" />{' '}
|
||||
for {formatMoney(saleAmount)}?
|
||||
</div>
|
||||
{!!loanAmount && (
|
||||
<div className="mt-2">
|
||||
|
|
|
@ -3,7 +3,7 @@ import Link from 'next/link'
|
|||
import { ClockIcon, DatabaseIcon, PencilIcon } from '@heroicons/react/outline'
|
||||
import { TrendingUpIcon } from '@heroicons/react/solid'
|
||||
import { Row } from '../layout/row'
|
||||
import { formatMoney, formatPercent } from '../../../common/util/format'
|
||||
import { formatMoney } from '../../../common/util/format'
|
||||
import { UserLink } from '../user-page'
|
||||
import {
|
||||
Contract,
|
||||
|
@ -19,9 +19,20 @@ import { fromNow } from '../../lib/util/time'
|
|||
import { Avatar } from '../avatar'
|
||||
import { Spacer } from '../layout/spacer'
|
||||
import { useState } from 'react'
|
||||
import { getProbability } from '../../../common/calculate'
|
||||
import { ContractInfoDialog } from './contract-info-dialog'
|
||||
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: {
|
||||
contract: Contract
|
||||
|
@ -30,7 +41,7 @@ export function ContractCard(props: {
|
|||
className?: string
|
||||
}) {
|
||||
const { contract, showHotVolume, showCloseTime, className } = props
|
||||
const { question } = contract
|
||||
const { question, outcomeType, resolution } = contract
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -51,54 +62,48 @@ export function ContractCard(props: {
|
|||
/>
|
||||
<Spacer h={3} />
|
||||
|
||||
<Row className="justify-between gap-4">
|
||||
<Row
|
||||
className={clsx(
|
||||
'justify-between gap-4',
|
||||
outcomeType === 'FREE_RESPONSE' && 'flex-col items-start'
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className="break-words font-medium text-indigo-700"
|
||||
style={{ /* For iOS safari */ wordBreak: 'break-word' }}
|
||||
>
|
||||
{question}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ResolutionOrChance(props: {
|
||||
contract: Contract
|
||||
export function BinaryResolutionOrChance(props: {
|
||||
contract: FullContract<DPM | CPMM, Binary>
|
||||
large?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
const { contract, large, className } = props
|
||||
const { resolution, outcomeType } = contract
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
const { resolution } = contract
|
||||
|
||||
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 resolutionText =
|
||||
{
|
||||
YES: 'YES',
|
||||
NO: 'NO',
|
||||
MKT: isBinary
|
||||
? formatPercent(
|
||||
contract.resolutionProbability ?? getProbability(contract)
|
||||
)
|
||||
: 'MULTI',
|
||||
CANCEL: 'N/A',
|
||||
'': '',
|
||||
}[resolution || ''] ?? `#${resolution}`
|
||||
|
||||
return (
|
||||
<Col className={clsx(large ? 'text-4xl' : 'text-3xl', className)}>
|
||||
{resolution ? (
|
||||
|
@ -108,22 +113,41 @@ export function ResolutionOrChance(props: {
|
|||
>
|
||||
Resolved
|
||||
</div>
|
||||
<div className={resolutionColor}>{resolutionText}</div>
|
||||
<BinaryContractOutcomeLabel
|
||||
contract={contract}
|
||||
resolution={resolution}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
isBinary && (
|
||||
<>
|
||||
<div className={probColor}>{getBinaryProbPercent(contract)}</div>
|
||||
<div className={clsx(probColor, large ? 'text-xl' : 'text-base')}>
|
||||
chance
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
<>
|
||||
<div className={probColor}>{getBinaryProbPercent(contract)}</div>
|
||||
<div className={clsx(probColor, large ? 'text-xl' : 'text-base')}>
|
||||
chance
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</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: {
|
||||
contract: Contract
|
||||
showHotVolume?: boolean
|
||||
|
|
|
@ -6,7 +6,11 @@ import { useUser } from '../../hooks/use-user'
|
|||
import { Row } from '../layout/row'
|
||||
import { Linkify } from '../linkify'
|
||||
import clsx from 'clsx'
|
||||
import { ContractDetails, ResolutionOrChance } from './contract-card'
|
||||
import {
|
||||
FreeResponseResolution,
|
||||
ContractDetails,
|
||||
BinaryResolutionOrChance,
|
||||
} from './contract-card'
|
||||
import { Bet } from '../../../common/bet'
|
||||
import { Comment } from '../../../common/comment'
|
||||
import BetRow from '../bet-row'
|
||||
|
@ -18,11 +22,10 @@ export const ContractOverview = (props: {
|
|||
contract: Contract
|
||||
bets: Bet[]
|
||||
comments: Comment[]
|
||||
children?: any
|
||||
className?: string
|
||||
}) => {
|
||||
const { contract, bets, comments, children, className } = props
|
||||
const { question, resolution, creatorId, outcomeType } = contract
|
||||
const { contract, bets, className } = props
|
||||
const { question, creatorId, outcomeType, resolution } = contract
|
||||
|
||||
const user = useUser()
|
||||
const isCreator = user?.id === creatorId
|
||||
|
@ -36,8 +39,8 @@ export const ContractOverview = (props: {
|
|||
<Linkify text={question} />
|
||||
</div>
|
||||
|
||||
{(isBinary || resolution) && (
|
||||
<ResolutionOrChance
|
||||
{isBinary && (
|
||||
<BinaryResolutionOrChance
|
||||
className="hidden items-end xl:flex"
|
||||
contract={contract}
|
||||
large
|
||||
|
@ -45,15 +48,24 @@ export const ContractOverview = (props: {
|
|||
)}
|
||||
</Row>
|
||||
|
||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||
{(isBinary || resolution) && (
|
||||
<ResolutionOrChance contract={contract} />
|
||||
)}
|
||||
{isBinary ? (
|
||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||
<BinaryResolutionOrChance contract={contract} />
|
||||
|
||||
{isBinary && tradingAllowed(contract) && (
|
||||
<BetRow contract={contract} labelClassName="hidden" />
|
||||
)}
|
||||
</Row>
|
||||
{tradingAllowed(contract) && (
|
||||
<BetRow contract={contract} labelClassName="hidden" />
|
||||
)}
|
||||
</Row>
|
||||
) : (
|
||||
outcomeType === 'FREE_RESPONSE' &&
|
||||
resolution && (
|
||||
<FreeResponseResolution
|
||||
contract={contract}
|
||||
resolution={resolution}
|
||||
truncate="none"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
<ContractDetails
|
||||
contract={contract}
|
||||
|
@ -73,19 +85,13 @@ export const ContractOverview = (props: {
|
|||
/>
|
||||
)}
|
||||
|
||||
<Spacer h={6} />
|
||||
{contract.description && <Spacer h={6} />}
|
||||
|
||||
<ContractDescription
|
||||
className="px-2"
|
||||
contract={contract}
|
||||
isCreator={isCreator}
|
||||
/>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
{children}
|
||||
|
||||
<Spacer h={4} />
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -97,9 +97,10 @@ function groupBets(
|
|||
hideOutcome: boolean
|
||||
abbreviated: boolean
|
||||
smallAvatar: boolean
|
||||
reversed: boolean
|
||||
}
|
||||
) {
|
||||
const { hideOutcome, abbreviated, smallAvatar } = options
|
||||
const { hideOutcome, abbreviated, smallAvatar, reversed } = options
|
||||
|
||||
const commentsMap = mapCommentsByBetId(comments)
|
||||
const items: ActivityItem[] = []
|
||||
|
@ -171,7 +172,9 @@ function groupBets(
|
|||
if (group.length > 0) {
|
||||
pushGroup()
|
||||
}
|
||||
return abbreviated ? items.slice(-3) : items
|
||||
const abbrItems = abbreviated ? items.slice(-3) : items
|
||||
if (reversed) abbrItems.reverse()
|
||||
return abbrItems
|
||||
}
|
||||
|
||||
function getAnswerGroups(
|
||||
|
@ -182,9 +185,10 @@ function getAnswerGroups(
|
|||
options: {
|
||||
sortByProb: boolean
|
||||
abbreviated: boolean
|
||||
reversed: boolean
|
||||
}
|
||||
) {
|
||||
const { sortByProb, abbreviated } = options
|
||||
const { sortByProb, abbreviated, reversed } = options
|
||||
|
||||
let outcomes = _.uniq(bets.map((bet) => bet.outcome)).filter(
|
||||
(outcome) => getOutcomeProbability(contract, outcome) > 0.0001
|
||||
|
@ -208,9 +212,8 @@ function getAnswerGroups(
|
|||
outcomes = outcomes.slice(-2)
|
||||
}
|
||||
if (sortByProb) {
|
||||
outcomes = _.sortBy(
|
||||
outcomes,
|
||||
(outcome) => -1 * getOutcomeProbability(contract, outcome)
|
||||
outcomes = _.sortBy(outcomes, (outcome) =>
|
||||
getOutcomeProbability(contract, outcome)
|
||||
)
|
||||
} else {
|
||||
// Sort by recent bet.
|
||||
|
@ -233,6 +236,7 @@ function getAnswerGroups(
|
|||
hideOutcome: true,
|
||||
abbreviated,
|
||||
smallAvatar: true,
|
||||
reversed,
|
||||
})
|
||||
|
||||
if (abbreviated) items = items.slice(-2)
|
||||
|
@ -264,6 +268,8 @@ export function getAllContractActivityItems(
|
|||
const { abbreviated } = options
|
||||
const { outcomeType } = contract
|
||||
|
||||
const reversed = !abbreviated
|
||||
|
||||
bets =
|
||||
outcomeType === 'BINARY'
|
||||
? bets.filter((bet) => !bet.isAnte && !bet.isRedemption)
|
||||
|
@ -301,12 +307,14 @@ export function getAllContractActivityItems(
|
|||
{
|
||||
sortByProb: true,
|
||||
abbreviated,
|
||||
reversed,
|
||||
}
|
||||
)
|
||||
: groupBets(bets, comments, contract, user?.id, {
|
||||
hideOutcome: !!filterToOutcome,
|
||||
abbreviated,
|
||||
smallAvatar: !!filterToOutcome,
|
||||
reversed: false,
|
||||
}))
|
||||
)
|
||||
|
||||
|
@ -317,14 +325,7 @@ export function getAllContractActivityItems(
|
|||
items.push({ type: 'resolve', id: `${contract.resolutionTime}`, contract })
|
||||
}
|
||||
|
||||
if (!abbreviated) {
|
||||
items.reverse()
|
||||
for (const item of items) {
|
||||
if (item.type === 'answergroup') {
|
||||
item.items.reverse()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reversed) items.reverse()
|
||||
|
||||
return items
|
||||
}
|
||||
|
@ -362,12 +363,14 @@ export function getRecentContractActivityItems(
|
|||
{
|
||||
sortByProb: false,
|
||||
abbreviated: true,
|
||||
reversed: false,
|
||||
}
|
||||
)
|
||||
: groupBets(bets, comments, contract, user?.id, {
|
||||
hideOutcome: false,
|
||||
abbreviated: true,
|
||||
smallAvatar: false,
|
||||
reversed: false,
|
||||
})
|
||||
|
||||
return [questionItem, ...items]
|
||||
|
|
|
@ -26,7 +26,7 @@ import { Row } from '../layout/row'
|
|||
import { createComment, MAX_COMMENT_LENGTH } from '../../lib/firebase/comments'
|
||||
import { formatMoney, formatPercent } from '../../../common/util/format'
|
||||
import { Comment } from '../../../common/comment'
|
||||
import { ResolutionOrChance } from '../contract/contract-card'
|
||||
import { BinaryResolutionOrChance } from '../contract/contract-card'
|
||||
import { SiteLink } from '../site-link'
|
||||
import { Col } from '../layout/col'
|
||||
import { UserLink } from '../user-page'
|
||||
|
@ -144,7 +144,12 @@ export function FeedComment(props: {
|
|||
{!hideOutcome && (
|
||||
<>
|
||||
{' '}
|
||||
of <OutcomeLabel outcome={outcome} />
|
||||
of{' '}
|
||||
<OutcomeLabel
|
||||
outcome={outcome}
|
||||
contract={contract}
|
||||
truncate="short"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<RelativeTimestamp time={createdTime} />
|
||||
|
@ -227,7 +232,12 @@ export function FeedBet(props: {
|
|||
{!hideOutcome && (
|
||||
<>
|
||||
{' '}
|
||||
of <OutcomeLabel outcome={outcome} />
|
||||
of{' '}
|
||||
<OutcomeLabel
|
||||
outcome={outcome}
|
||||
contract={contract}
|
||||
truncate="short"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<RelativeTimestamp time={createdTime} />
|
||||
|
@ -344,8 +354,11 @@ export function FeedQuestion(props: {
|
|||
{question}
|
||||
</SiteLink>
|
||||
</Col>
|
||||
{(isBinary || resolution) && (
|
||||
<ResolutionOrChance className="items-center" contract={contract} />
|
||||
{isBinary && (
|
||||
<BinaryResolutionOrChance
|
||||
className="items-center"
|
||||
contract={contract}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
{showDescription && (
|
||||
|
@ -449,7 +462,12 @@ function FeedResolve(props: { contract: Contract }) {
|
|||
name={creatorName}
|
||||
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} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -482,8 +500,12 @@ function FeedClose(props: { contract: Contract }) {
|
|||
)
|
||||
}
|
||||
|
||||
function BetGroupSpan(props: { bets: Bet[]; outcome?: string }) {
|
||||
const { bets, outcome } = props
|
||||
function BetGroupSpan(props: {
|
||||
contract: Contract
|
||||
bets: Bet[]
|
||||
outcome?: string
|
||||
}) {
|
||||
const { contract, bets, outcome } = props
|
||||
|
||||
const numberTraders = _.uniqBy(bets, (b) => b.userId).length
|
||||
|
||||
|
@ -501,7 +523,12 @@ function BetGroupSpan(props: { bets: Bet[]; outcome?: string }) {
|
|||
{outcome && (
|
||||
<>
|
||||
{' '}
|
||||
of <OutcomeLabel outcome={outcome} />
|
||||
of{' '}
|
||||
<OutcomeLabel
|
||||
outcome={outcome}
|
||||
contract={contract}
|
||||
truncate="short"
|
||||
/>
|
||||
</>
|
||||
)}{' '}
|
||||
</span>
|
||||
|
@ -513,7 +540,7 @@ function FeedBetGroup(props: {
|
|||
bets: Bet[]
|
||||
hideOutcome: boolean
|
||||
}) {
|
||||
const { bets, hideOutcome } = props
|
||||
const { contract, bets, hideOutcome } = props
|
||||
|
||||
const betGroups = _.groupBy(bets, (bet) => bet.outcome)
|
||||
const outcomes = Object.keys(betGroups)
|
||||
|
@ -535,6 +562,7 @@ function FeedBetGroup(props: {
|
|||
{outcomes.map((outcome, index) => (
|
||||
<Fragment key={outcome}>
|
||||
<BetGroupSpan
|
||||
contract={contract}
|
||||
outcome={hideOutcome ? undefined : outcome}
|
||||
bets={betGroups[outcome]}
|
||||
/>
|
||||
|
@ -583,7 +611,7 @@ function FeedAnswerGroup(props: {
|
|||
<UserLink username={username} name={name} /> answered
|
||||
</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">
|
||||
<Linkify text={text} />
|
||||
</span>
|
||||
|
@ -599,13 +627,13 @@ function FeedAnswerGroup(props: {
|
|||
</span>
|
||||
<BuyButton
|
||||
className={clsx(
|
||||
'btn-sm hidden flex-initial !px-6 sm:flex',
|
||||
'btn-sm flex-initial !px-6 sm:flex',
|
||||
tradingAllowed(contract) ? '' : '!hidden'
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
</Col>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
@ -628,15 +656,6 @@ function FeedAnswerGroup(props: {
|
|||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
contract: Contract
|
||||
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
|
||||
|
||||
if (outcome === 'YES') return <YesLabel />
|
||||
if (outcome === 'NO') return <NoLabel />
|
||||
if (outcome === 'MKT') return <ProbLabel />
|
||||
if (outcome === 'CANCEL') return <CancelLabel />
|
||||
return <AnswerNumberLabel number={outcome} />
|
||||
return <CancelLabel />
|
||||
}
|
||||
|
||||
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() {
|
||||
|
@ -26,6 +87,32 @@ export function ProbLabel() {
|
|||
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 }) {
|
||||
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>
|
||||
}
|
||||
|
|
|
@ -195,7 +195,7 @@ function Button(props: {
|
|||
color === 'red' && 'bg-red-400 text-white hover:bg-red-500',
|
||||
color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-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
|
||||
)}
|
||||
onClick={onClick}
|
||||
|
|
|
@ -98,7 +98,7 @@ export function ContractPageContent(props: FirstArgument<typeof ContractPage>) {
|
|||
return <Custom404 />
|
||||
}
|
||||
|
||||
const { creatorId, isResolved, question, outcomeType } = contract
|
||||
const { creatorId, isResolved, question, outcomeType, resolution } = contract
|
||||
|
||||
const isCreator = user?.id === creatorId
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
@ -143,8 +143,10 @@ export function ContractPageContent(props: FirstArgument<typeof ContractPage>) {
|
|||
contract={contract}
|
||||
bets={bets ?? []}
|
||||
comments={comments ?? []}
|
||||
>
|
||||
{contract.outcomeType === 'FREE_RESPONSE' && (
|
||||
/>
|
||||
|
||||
{outcomeType === 'FREE_RESPONSE' &&
|
||||
(!isResolved || resolution === 'MKT') && (
|
||||
<>
|
||||
<Spacer h={4} />
|
||||
<AnswersPanel
|
||||
|
@ -153,7 +155,6 @@ export function ContractPageContent(props: FirstArgument<typeof ContractPage>) {
|
|||
<Spacer h={4} />
|
||||
</>
|
||||
)}
|
||||
</ContractOverview>
|
||||
|
||||
{contract.isResolved && (
|
||||
<>
|
||||
|
|
|
@ -8,8 +8,9 @@ import {
|
|||
import { DOMAIN } from '../../../../common/envs/constants'
|
||||
import { AnswersGraph } from '../../../components/answers/answers-graph'
|
||||
import {
|
||||
ResolutionOrChance,
|
||||
BinaryResolutionOrChance,
|
||||
ContractDetails,
|
||||
FreeResponseResolution,
|
||||
} from '../../../components/contract/contract-card'
|
||||
import { ContractProbGraph } from '../../../components/contract/contract-prob-graph'
|
||||
import { Col } from '../../../components/layout/col'
|
||||
|
@ -118,8 +119,14 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
|||
hideShareButtons
|
||||
/>
|
||||
|
||||
{(isBinary || resolution) && (
|
||||
<ResolutionOrChance contract={contract} />
|
||||
{isBinary && <BinaryResolutionOrChance contract={contract} />}
|
||||
|
||||
{outcomeType === 'FREE_RESPONSE' && resolution && (
|
||||
<FreeResponseResolution
|
||||
contract={contract}
|
||||
resolution={resolution}
|
||||
truncate="long"
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user