2022-09-21 05:07:40 +00:00
|
|
|
import { sortBy, partition, sum } from 'lodash'
|
2022-06-13 16:04:56 +00:00
|
|
|
import { useEffect, useState } from 'react'
|
2022-02-20 22:25:58 +00:00
|
|
|
|
2022-07-28 02:40:33 +00:00
|
|
|
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
2022-02-20 22:25:58 +00:00
|
|
|
import { Col } from '../layout/col'
|
2022-05-09 13:04:36 +00:00
|
|
|
import { useUser } from 'web/hooks/use-user'
|
|
|
|
import { getDpmOutcomeProbability } from 'common/calculate-dpm'
|
|
|
|
import { useAnswers } from 'web/hooks/use-answers'
|
|
|
|
import { tradingAllowed } from 'web/lib/firebase/contracts'
|
2022-02-20 22:25:58 +00:00
|
|
|
import { AnswerItem } from './answer-item'
|
|
|
|
import { CreateAnswerPanel } from './create-answer-panel'
|
|
|
|
import { AnswerResolvePanel } from './answer-resolve-panel'
|
2022-04-18 23:02:40 +00:00
|
|
|
import { Spacer } from '../layout/spacer'
|
2022-05-09 13:04:36 +00:00
|
|
|
import { getOutcomeProbability } from 'common/calculate'
|
|
|
|
import { Answer } from 'common/answer'
|
2022-05-17 15:55:26 +00:00
|
|
|
import clsx from 'clsx'
|
|
|
|
import { formatPercent } from 'common/util/format'
|
|
|
|
import { Modal } from 'web/components/layout/modal'
|
|
|
|
import { AnswerBetPanel } from 'web/components/answers/answer-bet-panel'
|
|
|
|
import { Row } from 'web/components/layout/row'
|
|
|
|
import { Avatar } from 'web/components/avatar'
|
|
|
|
import { Linkify } from 'web/components/linkify'
|
2022-09-12 20:30:15 +00:00
|
|
|
import { Button } from 'web/components/button'
|
2022-09-15 03:28:40 +00:00
|
|
|
import { useAdmin } from 'web/hooks/use-admin'
|
|
|
|
import { needsAdminToResolve } from 'web/pages/[username]/[contractSlug]'
|
2022-10-12 06:59:11 +00:00
|
|
|
import { CHOICE_ANSWER_COLORS } from '../charts/contract/choice'
|
2022-10-04 22:36:03 +00:00
|
|
|
import { useChartAnswers } from '../charts/contract/choice'
|
2022-10-12 18:05:58 +00:00
|
|
|
import { ChatIcon } from '@heroicons/react/outline'
|
2022-02-20 22:25:58 +00:00
|
|
|
|
2022-10-14 05:07:54 +00:00
|
|
|
export function getAnswerColor(answer: Answer, answersArray: string[]) {
|
|
|
|
const colorIndex = answersArray.indexOf(answer.text)
|
|
|
|
return colorIndex != undefined && colorIndex < CHOICE_ANSWER_COLORS.length
|
|
|
|
? CHOICE_ANSWER_COLORS[colorIndex]
|
|
|
|
: '#B1B1C7'
|
|
|
|
}
|
|
|
|
|
2022-07-28 02:40:33 +00:00
|
|
|
export function AnswersPanel(props: {
|
|
|
|
contract: FreeResponseContract | MultipleChoiceContract
|
2022-10-12 18:05:58 +00:00
|
|
|
onAnswerCommentClick: (answer: Answer) => void
|
2022-07-28 02:40:33 +00:00
|
|
|
}) {
|
2022-09-15 03:28:40 +00:00
|
|
|
const isAdmin = useAdmin()
|
2022-10-12 18:05:58 +00:00
|
|
|
const { contract, onAnswerCommentClick } = props
|
2022-07-28 02:40:33 +00:00
|
|
|
const { creatorId, resolution, resolutions, totalBets, outcomeType } =
|
|
|
|
contract
|
2022-09-12 20:30:15 +00:00
|
|
|
const [showAllAnswers, setShowAllAnswers] = useState(false)
|
2022-02-20 22:25:58 +00:00
|
|
|
|
2022-09-13 13:48:41 +00:00
|
|
|
const answers = (useAnswers(contract.id) ?? contract.answers).filter(
|
|
|
|
(a) => a.number != 0 || contract.outcomeType === 'MULTIPLE_CHOICE'
|
|
|
|
)
|
2022-10-04 22:36:03 +00:00
|
|
|
|
2022-09-26 03:29:13 +00:00
|
|
|
const hasZeroBetAnswers = answers.some((answer) => totalBets[answer.id] < 1)
|
|
|
|
|
|
|
|
const [winningAnswers, losingAnswers] = partition(
|
|
|
|
answers.filter((a) => (showAllAnswers ? true : totalBets[a.id] > 0)),
|
|
|
|
(answer) =>
|
|
|
|
answer.id === resolution || (resolutions && resolutions[answer.id])
|
2022-02-20 22:25:58 +00:00
|
|
|
)
|
2022-09-26 03:29:13 +00:00
|
|
|
const sortedAnswers = [
|
|
|
|
...sortBy(winningAnswers, (answer) =>
|
|
|
|
resolutions ? -1 * resolutions[answer.id] : 0
|
|
|
|
),
|
|
|
|
...sortBy(
|
|
|
|
resolution ? [] : losingAnswers,
|
|
|
|
(answer) => -1 * getDpmOutcomeProbability(contract.totalShares, answer.id)
|
|
|
|
),
|
|
|
|
]
|
|
|
|
|
|
|
|
const answerItems = sortBy(
|
|
|
|
losingAnswers.length > 0 ? losingAnswers : sortedAnswers,
|
|
|
|
(answer) => -getOutcomeProbability(contract, answer.id)
|
2022-09-21 05:07:40 +00:00
|
|
|
)
|
|
|
|
|
2022-02-20 22:25:58 +00:00
|
|
|
const user = useUser()
|
|
|
|
|
|
|
|
const [resolveOption, setResolveOption] = useState<
|
|
|
|
'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
|
|
|
>()
|
|
|
|
const [chosenAnswers, setChosenAnswers] = useState<{
|
|
|
|
[answerId: string]: number
|
|
|
|
}>({})
|
|
|
|
|
2022-05-22 08:36:05 +00:00
|
|
|
const chosenTotal = sum(Object.values(chosenAnswers))
|
2022-02-20 22:25:58 +00:00
|
|
|
|
|
|
|
const onChoose = (answerId: string, prob: number) => {
|
|
|
|
if (resolveOption === 'CHOOSE') {
|
|
|
|
setChosenAnswers({ [answerId]: prob })
|
|
|
|
} else {
|
|
|
|
setChosenAnswers((chosenAnswers) => {
|
|
|
|
return {
|
|
|
|
...chosenAnswers,
|
|
|
|
[answerId]: prob,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const onDeselect = (answerId: string) => {
|
|
|
|
setChosenAnswers((chosenAnswers) => {
|
|
|
|
const newChosenAnswers = { ...chosenAnswers }
|
|
|
|
delete newChosenAnswers[answerId]
|
|
|
|
return newChosenAnswers
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-06-13 16:04:56 +00:00
|
|
|
useEffect(() => {
|
2022-02-20 22:25:58 +00:00
|
|
|
setChosenAnswers({})
|
|
|
|
}, [resolveOption])
|
|
|
|
|
|
|
|
const showChoice = resolution
|
|
|
|
? undefined
|
|
|
|
: resolveOption === 'CHOOSE'
|
|
|
|
? 'radio'
|
|
|
|
: resolveOption === 'CHOOSE_MULTIPLE'
|
|
|
|
? 'checkbox'
|
|
|
|
: undefined
|
|
|
|
|
2022-10-14 05:07:54 +00:00
|
|
|
const answersArray = useChartAnswers(contract).map(
|
|
|
|
(answer, _index) => answer.text
|
2022-10-04 22:36:03 +00:00
|
|
|
)
|
|
|
|
|
2022-02-20 22:25:58 +00:00
|
|
|
return (
|
|
|
|
<Col className="gap-3">
|
2022-05-04 22:03:06 +00:00
|
|
|
{(resolveOption || resolution) &&
|
2022-09-26 03:29:13 +00:00
|
|
|
sortedAnswers.map((answer) => (
|
2022-04-18 23:02:40 +00:00
|
|
|
<AnswerItem
|
2022-09-26 03:29:13 +00:00
|
|
|
key={answer.id}
|
|
|
|
answer={answer}
|
2022-04-18 23:02:40 +00:00
|
|
|
contract={contract}
|
|
|
|
showChoice={showChoice}
|
2022-09-26 03:29:13 +00:00
|
|
|
chosenProb={chosenAnswers[answer.id]}
|
2022-04-18 23:02:40 +00:00
|
|
|
totalChosenProb={chosenTotal}
|
|
|
|
onChoose={onChoose}
|
|
|
|
onDeselect={onDeselect}
|
|
|
|
/>
|
|
|
|
))}
|
2022-02-20 22:25:58 +00:00
|
|
|
|
2022-05-04 22:03:06 +00:00
|
|
|
{!resolveOption && (
|
2022-09-21 05:07:40 +00:00
|
|
|
<Col
|
|
|
|
className={clsx(
|
|
|
|
'gap-2 pr-2 md:pr-0',
|
|
|
|
tradingAllowed(contract) ? '' : '-mb-6'
|
|
|
|
)}
|
|
|
|
>
|
2022-09-26 03:29:13 +00:00
|
|
|
{answerItems.map((item) => (
|
2022-10-04 22:36:03 +00:00
|
|
|
<OpenAnswer
|
|
|
|
key={item.id}
|
|
|
|
answer={item}
|
|
|
|
contract={contract}
|
2022-10-12 18:05:58 +00:00
|
|
|
onAnswerCommentClick={onAnswerCommentClick}
|
2022-10-14 05:07:54 +00:00
|
|
|
color={getAnswerColor(item, answersArray)}
|
2022-10-04 22:36:03 +00:00
|
|
|
/>
|
2022-09-21 05:07:40 +00:00
|
|
|
))}
|
2022-09-26 03:29:13 +00:00
|
|
|
{hasZeroBetAnswers && !showAllAnswers && (
|
2022-09-21 05:07:40 +00:00
|
|
|
<Button
|
|
|
|
className="self-end"
|
|
|
|
color="gray-white"
|
|
|
|
onClick={() => setShowAllAnswers(true)}
|
|
|
|
size="md"
|
|
|
|
>
|
|
|
|
Show More
|
|
|
|
</Button>
|
|
|
|
)}
|
|
|
|
</Col>
|
2022-04-26 15:53:12 +00:00
|
|
|
)}
|
|
|
|
|
2022-09-26 03:29:13 +00:00
|
|
|
{answers.length <= 1 && (
|
2022-05-04 22:03:06 +00:00
|
|
|
<div className="pb-4 text-gray-500">No answers yet...</div>
|
|
|
|
)}
|
|
|
|
|
2022-10-06 22:20:46 +00:00
|
|
|
{outcomeType === 'FREE_RESPONSE' && tradingAllowed(contract) && (
|
|
|
|
<CreateAnswerPanel contract={contract} />
|
|
|
|
)}
|
2022-02-20 22:25:58 +00:00
|
|
|
|
2022-09-15 03:28:40 +00:00
|
|
|
{(user?.id === creatorId || (isAdmin && needsAdminToResolve(contract))) &&
|
|
|
|
!resolution && (
|
|
|
|
<>
|
|
|
|
<Spacer h={2} />
|
|
|
|
<AnswerResolvePanel
|
|
|
|
isAdmin={isAdmin}
|
|
|
|
isCreator={user?.id === creatorId}
|
|
|
|
contract={contract}
|
|
|
|
resolveOption={resolveOption}
|
|
|
|
setResolveOption={setResolveOption}
|
|
|
|
chosenAnswers={chosenAnswers}
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
)}
|
2022-02-20 22:25:58 +00:00
|
|
|
</Col>
|
|
|
|
)
|
|
|
|
}
|
2022-04-26 15:53:12 +00:00
|
|
|
|
2022-05-17 15:55:26 +00:00
|
|
|
function OpenAnswer(props: {
|
2022-07-28 02:40:33 +00:00
|
|
|
contract: FreeResponseContract | MultipleChoiceContract
|
2022-05-17 15:55:26 +00:00
|
|
|
answer: Answer
|
2022-10-14 05:07:54 +00:00
|
|
|
color: string
|
2022-10-12 18:05:58 +00:00
|
|
|
onAnswerCommentClick: (answer: Answer) => void
|
2022-05-17 15:55:26 +00:00
|
|
|
}) {
|
2022-10-14 05:07:54 +00:00
|
|
|
const { answer, contract, onAnswerCommentClick, color } = props
|
2022-10-04 22:36:03 +00:00
|
|
|
const { username, avatarUrl, text } = answer
|
2022-05-17 15:55:26 +00:00
|
|
|
const prob = getDpmOutcomeProbability(contract.totalShares, answer.id)
|
|
|
|
const probPercent = formatPercent(prob)
|
|
|
|
const [open, setOpen] = useState(false)
|
2022-10-12 06:59:11 +00:00
|
|
|
const colorWidth = 100 * Math.max(prob, 0.01)
|
2022-05-17 15:55:26 +00:00
|
|
|
|
|
|
|
return (
|
2022-10-04 22:36:03 +00:00
|
|
|
<Col className="my-1 px-2">
|
2022-09-09 21:08:42 +00:00
|
|
|
<Modal open={open} setOpen={setOpen} position="center">
|
2022-05-17 15:55:26 +00:00
|
|
|
<AnswerBetPanel
|
|
|
|
answer={answer}
|
|
|
|
contract={contract}
|
|
|
|
closePanel={() => setOpen(false)}
|
|
|
|
className="sm:max-w-84 !rounded-md bg-white !px-8 !py-6"
|
|
|
|
isModal={true}
|
|
|
|
/>
|
|
|
|
</Modal>
|
|
|
|
|
2022-10-04 22:36:03 +00:00
|
|
|
<Col
|
|
|
|
className={clsx(
|
2022-10-12 06:59:11 +00:00
|
|
|
'relative w-full rounded-lg transition-all',
|
2022-10-04 22:36:03 +00:00
|
|
|
tradingAllowed(contract) ? 'text-greyscale-7' : 'text-greyscale-5'
|
|
|
|
)}
|
2022-10-12 06:59:11 +00:00
|
|
|
style={{
|
2022-10-14 05:07:54 +00:00
|
|
|
background: `linear-gradient(to right, ${color}90 ${colorWidth}%, #FBFBFF ${colorWidth}%)`,
|
2022-10-12 06:59:11 +00:00
|
|
|
}}
|
2022-10-04 22:36:03 +00:00
|
|
|
>
|
|
|
|
<Row className="z-20 -mb-1 justify-between gap-2 py-2 px-3">
|
|
|
|
<Row>
|
|
|
|
<Avatar
|
|
|
|
className="mt-0.5 mr-2 inline h-5 w-5 border border-transparent transition-transform hover:border-none"
|
|
|
|
username={username}
|
|
|
|
avatarUrl={avatarUrl}
|
|
|
|
/>
|
2022-10-13 14:04:01 +00:00
|
|
|
<Linkify className="text-md whitespace-pre-line" text={text} />
|
2022-10-04 22:36:03 +00:00
|
|
|
</Row>
|
|
|
|
<Row className="gap-2">
|
|
|
|
<div className="my-auto text-xl">{probPercent}</div>
|
|
|
|
{tradingAllowed(contract) && (
|
|
|
|
<Button
|
|
|
|
size="2xs"
|
|
|
|
color="gray-outline"
|
2022-09-21 05:07:40 +00:00
|
|
|
onClick={() => setOpen(true)}
|
2022-10-04 22:36:03 +00:00
|
|
|
className="my-auto"
|
|
|
|
>
|
|
|
|
BUY
|
|
|
|
</Button>
|
|
|
|
)}
|
2022-10-12 18:05:58 +00:00
|
|
|
{
|
|
|
|
<button
|
|
|
|
className="p-1"
|
|
|
|
onClick={() => onAnswerCommentClick(answer)}
|
|
|
|
>
|
|
|
|
<ChatIcon className="text-greyscale-4 hover:text-greyscale-6 h-5 w-5 transition-colors" />
|
|
|
|
</button>
|
|
|
|
}
|
2022-10-04 22:36:03 +00:00
|
|
|
</Row>
|
|
|
|
</Row>
|
|
|
|
</Col>
|
2022-05-17 15:55:26 +00:00
|
|
|
</Col>
|
|
|
|
)
|
|
|
|
}
|