From 9c74f88b4a51f199508891fdeac2698981664397 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 18 Apr 2022 18:02:40 -0500 Subject: [PATCH] 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 --- common/answer.ts | 2 + common/contract.ts | 8 +- functions/src/create-answer.ts | 4 +- functions/src/emails.ts | 6 +- web/components/answers/answer-item.tsx | 206 +++++++----------- .../answers/answer-resolve-panel.tsx | 2 +- web/components/answers/answers-graph.tsx | 38 +++- web/components/answers/answers-panel.tsx | 63 +++--- .../answers/create-answer-panel.tsx | 5 +- web/components/bet-panel.tsx | 18 +- web/components/bets-list.tsx | 22 +- web/components/contract/contract-card.tsx | 106 +++++---- web/components/contract/contract-overview.tsx | 48 ++-- web/components/feed/activity-items.ts | 31 +-- web/components/feed/feed-items.tsx | 63 ++++-- web/components/outcome-label.tsx | 91 +++++++- web/components/yes-no-selector.tsx | 2 +- web/pages/[username]/[contractSlug].tsx | 9 +- web/pages/embed/[username]/[contractSlug].tsx | 13 +- 19 files changed, 448 insertions(+), 289 deletions(-) diff --git a/common/answer.ts b/common/answer.ts index 36dd0415..9dcc3828 100644 --- a/common/answer.ts +++ b/common/answer.ts @@ -29,3 +29,5 @@ export const getNoneAnswer = (contractId: string, creator: User) => { text: 'None', } } + +export const MAX_ANSWER_LENGTH = 240 diff --git a/common/contract.ts b/common/contract.ts index 77568f49..6e362de0 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -38,6 +38,8 @@ export type FullContract< T export type Contract = FullContract +export type BinaryContract = FullContract +export type FreeResponseContract = FullContract 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' diff --git a/functions/src/create-answer.ts b/functions/src/create-answer.ts index 4bbfb089..1da8f350 100644 --- a/functions/src/create-answer.ts +++ b/functions/src/create-answer.ts @@ -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. diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 14c198cc..143e938d 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -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}` } diff --git a/web/components/answers/answer-item.tsx b/web/components/answers/answer-item.tsx index 07e4ffc7..fdeafea0 100644 --- a/web/components/answers/answer-item.tsx +++ b/web/components/answers/answer-item.tsx @@ -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 - 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 (
canBet && setIsBetting(true)} >
@@ -83,124 +70,91 @@ export function AnswerItem(props: { {/* TODO: Show total pool? */}
#{number}
- - {isBetting && ( - - )} - {isBetting ? ( - setIsBetting(false)} - className="sm:w-72" - /> - ) : ( - - {!wasResolvedTo && - (showChoice === 'checkbox' ? ( - { - const { value } = e.target - const numberValue = value - ? parseInt(value.replace(/[^\d]/, '')) - : 0 - if (!isNaN(numberValue)) onChoose(answer.id, numberValue) - }} - /> - ) : ( -
- {probPercent} -
- ))} - {showChoice ? ( -
- - {showChoice === 'checkbox' && ( -
- {chosenProb && totalChosenProb - ? Math.round((100 * chosenProb) / totalChosenProb) - : 0} - % share -
- )} -
+ + {!wasResolvedTo && + (showChoice === 'checkbox' ? ( + { + const { value } = e.target + const numberValue = value + ? parseInt(value.replace(/[^\d]/, '')) + : 0 + if (!isNaN(numberValue)) onChoose(answer.id, numberValue) + }} + /> ) : ( - <> - {tradingAllowed(contract) && ( - { - setIsBetting(true) - }} +
+ {probPercent} +
+ ))} + {showChoice ? ( +
+ + {showChoice === 'checkbox' && ( +
+ {chosenProb && totalChosenProb + ? Math.round((100 * chosenProb) / totalChosenProb) + : 0} + % share +
+ )} +
+ ) : ( + wasResolvedTo && ( + +
+ Chosen{' '} + {resolutions ? `${Math.round(resolutions[answer.id])}%` : ''} +
+
{probPercent}
+ + ) + )} +
) } diff --git a/web/components/answers/answer-resolve-panel.tsx b/web/components/answers/answer-resolve-panel.tsx index c2fbc341..41aa90b2 100644 --- a/web/components/answers/answer-resolve-panel.tsx +++ b/web/components/answers/answer-resolve-panel.tsx @@ -71,7 +71,7 @@ export function AnswerResolvePanel(props: { : 'btn-disabled' return ( - +
Resolve your market
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 (
= 800 ? 350 : 225) }} + className="w-full" + style={{ height: height ?? (isLargeWidth ? 350 : 250) }} >
) diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index 61dfec61..f7277618 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -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 @@ -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 ( - {sortedAnswers.map((answer) => ( - - ))} + {(resolveOption === 'CHOOSE' || + resolveOption === 'CHOOSE_MULTIPLE' || + resolution === 'MKT') && + sortedAnswers.map((answer) => ( + + ))} - {sortedAnswers.length === 0 ? ( -
No answers yet...
- ) : ( -
- None of the above:{' '} - {formatPercent(getDpmOutcomeProbability(contract.totalShares, '0'))} -
+ {sortedAnswers.length === 0 && ( +
No answers yet...
)} - {tradingAllowed(contract) && !resolveOption && ( - - )} + {tradingAllowed(contract) && + (!resolveOption || resolveOption === 'CANCEL') && ( + + )} {user?.id === creatorId && !resolution && ( - + <> + + + )} ) diff --git a/web/components/answers/create-answer-panel.tsx b/web/components/answers/create-answer-panel.tsx index 3aa3c0cd..6a3dd8c6 100644 --- a/web/components/answers/create-answer-panel.tsx +++ b/web/components/answers/create-answer-panel.tsx @@ -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 @@ -75,7 +76,7 @@ export function CreateAnswerPanel(props: { const currentReturnPercent = (currentReturn * 100).toFixed() + '%' return ( - +
Add your answer