Redesign the contract card (#235)

* Redesign the card

* Limit to 1 category on a card

* Make card tags a lighter gray

* Righbar always starts from the bottom
This commit is contained in:
Austin Chen 2022-05-16 19:15:22 -04:00 committed by GitHub
parent 05c94374c9
commit 6c6cbdc1a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 114 additions and 47 deletions

View File

@ -6,6 +6,7 @@ import {
Contract,
contractPath,
getBinaryProbPercent,
getBinaryProb,
} from 'web/lib/firebase/contracts'
import { Col } from '../layout/col'
import {
@ -20,11 +21,42 @@ import {
AnswerLabel,
BinaryContractOutcomeLabel,
FreeResponseOutcomeLabel,
OUTCOME_TO_COLOR,
} from '../outcome-label'
import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
import { AbbrContractDetails } from './contract-details'
import { TagsList } from '../tags-list'
import { CATEGORY_LIST } from 'common/categories'
// Return a number from 0 to 1 for this contract
// Resolved contracts are set to 1, for coloring purposes (even if NO)
function getProb(contract: Contract) {
const { outcomeType, resolution } = contract
return resolution
? 1
: outcomeType === 'BINARY'
? getBinaryProb(contract)
: outcomeType === 'FREE_RESPONSE'
? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '')
: 1 // Should not happen
}
function getColor(contract: Contract) {
const { resolution } = contract
if (resolution) {
return (
// @ts-ignore; TODO: Have better typing for contract.resolution?
OUTCOME_TO_COLOR[resolution] ||
// If resolved to a FR answer, use 'primary'
'primary'
)
}
const marketClosed = (contract.closeTime || Infinity) < Date.now()
return marketClosed
? 'gray-400'
: getProb(contract) >= 0.5
? 'primary'
: 'red-400'
}
export function ContractCard(props: {
contract: Contract
@ -33,18 +65,18 @@ export function ContractCard(props: {
className?: string
}) {
const { contract, showHotVolume, showCloseTime, className } = props
const { question, outcomeType, resolution } = contract
const { question, outcomeType } = contract
const { tags } = contract
const categories = tags.filter((tag) =>
CATEGORY_LIST.includes(tag.toLowerCase())
)
const prob = getProb(contract)
const color = getColor(contract)
const marketClosed = (contract.closeTime || Infinity) < Date.now()
const showTopBar = prob >= 0.5 || marketClosed
return (
<div>
<Col
className={clsx(
'relative gap-3 rounded-lg bg-white p-6 shadow-md hover:bg-gray-100',
'relative gap-3 rounded-lg bg-white p-6 pr-7 shadow-md hover:bg-gray-100',
className
)}
>
@ -66,9 +98,6 @@ export function ContractCard(props: {
>
{question}
</p>
{outcomeType !== 'FREE_RESPONSE' && categories.length > 0 && (
<TagsList tags={categories} noLabel />
)}
</Col>
{outcomeType === 'BINARY' && (
<BinaryResolutionOrChance
@ -86,9 +115,22 @@ export function ContractCard(props: {
/>
)}
{outcomeType === 'FREE_RESPONSE' && categories.length > 0 && (
<TagsList tags={categories} noLabel />
)}
<div
className={clsx(
'absolute right-0 top-0 w-2 rounded-tr-md',
'bg-gray-200'
)}
style={{ height: `${100 * (1 - prob)}%` }}
></div>
<div
className={clsx(
'absolute right-0 bottom-0 w-2 rounded-br-md',
`bg-${color}`,
// If we're showing the full bar, also round the top
prob === 1 ? 'rounded-tr-md' : ''
)}
style={{ height: `${100 * prob}%` }}
></div>
</Col>
</div>
)
@ -101,9 +143,7 @@ export function BinaryResolutionOrChance(props: {
}) {
const { contract, large, className } = props
const { resolution } = contract
const marketClosed = (contract.closeTime || Infinity) < Date.now()
const probColor = marketClosed ? 'text-gray-400' : 'text-primary'
const textColor = `text-${getColor(contract)}`
return (
<Col className={clsx(large ? 'text-4xl' : 'text-3xl', className)}>
@ -121,8 +161,8 @@ export function BinaryResolutionOrChance(props: {
</>
) : (
<>
<div className={probColor}>{getBinaryProbPercent(contract)}</div>
<div className={clsx(probColor, large ? 'text-xl' : 'text-base')}>
<div className={textColor}>{getBinaryProbPercent(contract)}</div>
<div className={clsx(textColor, large ? 'text-xl' : 'text-base')}>
chance
</div>
</>
@ -140,6 +180,7 @@ export function FreeResponseResolutionOrChance(props: {
const { resolution } = contract
const topAnswer = getTopAnswer(contract)
const textColor = `text-${getColor(contract)}`
return (
<Col className={clsx(resolution ? 'text-3xl' : 'text-xl', className)}>
@ -161,7 +202,7 @@ export function FreeResponseResolutionOrChance(props: {
answer={topAnswer}
truncate={truncate}
/>
<Col className="text-primary text-3xl">
<Col className={clsx('text-3xl', textColor)}>
<div>
{formatPercent(getOutcomeProbability(contract, topAnswer.id))}
</div>

View File

@ -1,6 +1,11 @@
import clsx from 'clsx'
import { ClockIcon, DatabaseIcon, PencilIcon } from '@heroicons/react/outline'
import { TrendingUpIcon } from '@heroicons/react/solid'
import {
ClockIcon,
DatabaseIcon,
PencilIcon,
CurrencyDollarIcon,
TrendingUpIcon,
} from '@heroicons/react/outline'
import { Row } from '../layout/row'
import { formatMoney } from 'common/util/format'
import { UserLink } from '../user-page'
@ -18,6 +23,8 @@ import { useState } from 'react'
import { ContractInfoDialog } from './contract-info-dialog'
import { Bet } from 'common/bet'
import NewContractBadge from '../new-contract-badge'
import { CATEGORY_LIST } from 'common/categories'
import { TagsList } from '../tags-list'
export function AbbrContractDetails(props: {
contract: Contract
@ -25,9 +32,19 @@ export function AbbrContractDetails(props: {
showCloseTime?: boolean
}) {
const { contract, showHotVolume, showCloseTime } = props
const { volume, volume24Hours, creatorName, creatorUsername, closeTime } =
contract
const {
volume,
volume24Hours,
creatorName,
creatorUsername,
closeTime,
tags,
} = contract
const { volumeLabel } = contractMetrics(contract)
// Show at most one category that this contract is tagged by
const categories = CATEGORY_LIST.filter((category) =>
tags.map((t) => t.toLowerCase()).includes(category)
).slice(0, 1)
return (
<Col className={clsx('gap-2 text-sm text-gray-500')}>
@ -45,21 +62,28 @@ export function AbbrContractDetails(props: {
/>
</Row>
{showHotVolume ? (
<Row className="gap-1">
<TrendingUpIcon className="h-5 w-5" /> {formatMoney(volume24Hours)}
</Row>
) : showCloseTime ? (
<Row className="gap-1">
<ClockIcon className="h-5 w-5" />
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
{fromNow(closeTime || 0)}
</Row>
) : volume > 0 ? (
<Row>{volumeLabel}</Row>
) : (
<NewContractBadge />
)}
<Row className="gap-3 text-gray-400">
{categories.length > 0 && (
<TagsList className="text-gray-400" tags={categories} noLabel />
)}
{showHotVolume ? (
<Row className="gap-0.5">
<TrendingUpIcon className="h-5 w-5" />{' '}
{formatMoney(volume24Hours)}
</Row>
) : showCloseTime ? (
<Row className="gap-0.5">
<ClockIcon className="h-5 w-5" />
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
{fromNow(closeTime || 0)}
</Row>
) : volume > 0 ? (
<Row>{volumeLabel}</Row>
) : (
<NewContractBadge />
)}
</Row>
</Row>
</Col>
)

View File

@ -83,6 +83,13 @@ export function FreeResponseOutcomeLabel(props: {
)
}
export const OUTCOME_TO_COLOR = {
YES: 'primary',
NO: 'red-400',
CANCEL: 'yellow-400',
MKT: 'blue-400',
}
export function YesLabel() {
return <span className="text-primary">YES</span>
}

View File

@ -10,13 +10,8 @@ function Hashtag(props: { tag: string; noLink?: boolean }) {
const category = CATEGORIES[tag.replace('#', '').toLowerCase()]
const body = (
<div
className={clsx(
'rounded-full border-2 bg-gray-100 px-3 py-1 shadow-md',
!noLink && 'cursor-pointer'
)}
>
<span className="text-sm text-gray-600">{category ?? tag}</span>
<div className={clsx('', !noLink && 'cursor-pointer')}>
<span className="text-sm">#{category ?? tag} </span>
</div>
)
@ -38,7 +33,7 @@ export function TagsList(props: {
const { tags, className, noLink, noLabel, label } = props
return (
<Row className={clsx('flex-wrap items-center gap-2', className)}>
{!noLabel && <div className="mr-1 text-gray-500">{label || 'Tags'}</div>}
{!noLabel && <div className="mr-1">{label || 'Tags'}</div>}
{tags.map((tag) => (
<Hashtag
key={tag}